Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ PR 创建时,Store 自动生成 `ReviewEntry[]`,包含所有其他 Agent 作
6. 用户点击"停止"→ `stopAgent()` **乐观更新**前端状态为 `stopped`,再 `await sendToEngine({ type: 'stop-agent' })` → Engine 调用 `agent.stop()` 等待进程退出 → 推送 `agent-status` 事件;后端失败时自动回滚状态
7. 用户发送指令 → `sendInstruction()` 执行完毕且检测到文件变更 → Engine **自动调用 `autoSubmitForReview()`**:git add/commit/push → 创建 PR;任何步骤失败时,Engine 将完整执行日志(stdout + stderr + exit code)全量回传给 Kimi CLI,由 Agent 自主判断并修复,然后重试(最多 3 轮)→ 状态变为 `reviewing` → **自动启动 CI 监控**:Engine 每 30s 轮询 GitHub Checks API,CI 失败时自动将失败日志回传给 Agent 修复并重新提交(最多 3 轮);CI 通过或超时后停止轮询

**全局组件挂载**:`App.vue` 全局挂载 `SwarmConfirmModal`(确认弹窗)和 `SwarmToast`(Toast 通知),配合 `useConfirm` / `useToast` composables 提供命令式调用能力。

**日志分流**:
- `input` / `output` 及关键状态变更(执行完毕/已停止/Token耗尽等)通过 `log` 事件进入前端聊天面板
- `system` / `error` 技术日志(PID、命令行参数、文件变更数、内部异常等)输出到 **stderr**,由终端直接显示(带 `[Agent]`/`[Kimi]`/`[Git]` 组件前缀和颜色),不污染 UI
Expand Down
7 changes: 7 additions & 0 deletions docs/COMPONENT_PATTERNS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,18 @@ const emit = defineEmits<{...}>()
- `LoginView.vue` — 独立页面组件:登录表单 + API Key 验证 + Kimi Code 控制台外链
- `AgentDashboard.vue` — 列表容器组件:统计卡片 + Agent 网格 + 空状态
- `CreateTaskModal.vue` — 表单弹窗组件:新建 Agent 的信息收集与提交
- `SwarmConfirmModal.vue` — 确认弹窗组件:类型化图标 + 双按钮确认/取消,配合 useConfirm composable
- `SwarmToast.vue` — Toast 通知组件:类型化图标 + 进度条 + 自动消失,配合 useToast composable
- `AgentDetail.vue` — 聊天式多轮对话组件:Header + Info + PR 审阅 + 文件变更 + 聊天消息区(input/output/system/error 气泡)+ 输入框。日期字段通过 new Date(string).toLocaleTimeString() 显示
- `TaskCard.vue` — 卡片组件:Agent 状态 + Token 进度 + 审阅徽章
- `AnalyticsPanel.vue` — 数据展示型组件:状态分布、Token 排行、任务列表
- `SettingsPanel.vue` — 纯信息展示型设置面板,使用 lucide 图标 + code 标签展示命令指引

## Composables

- `useConfirm.ts` — 命令式确认弹窗:全局 reactive 状态,支持 Promise 化调用
- `useToast.ts` — 命令式 Toast 通知:全局 reactive 数组,支持自动定时移除

## 视觉风格

- **白色简约主题**:全局白底(`bg-white` / `bg-gray-50`),深灰文字(`text-gray-900` / `text-gray-700`),浅灰边框(`border-gray-200`)
Expand Down
4 changes: 4 additions & 0 deletions docs/FRONTEND.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ Vue 3 + TypeScript + Vite + Tailwind CSS + lucide-vue-next + Tauri v2 + @tauri-a
- `components/LoginView.vue` — API Key 登录页(验证 + keyring 存储)
- `components/AgentDashboard.vue` — Agent 卡片网格(最多 5 个,点击进入详情)
- `components/CreateTaskModal.vue` — 新建 Agent 弹窗:收集 name / repoUrl / instruction / tokenBudget
- `components/SwarmConfirmModal.vue` — 确认弹窗组件:支持 danger/warning/info 类型,配合 useConfirm 使用
- `components/SwarmToast.vue` — Toast 通知组件:支持 error/success/info/warning,自动消失
- `App.vue` — 主入口:布局框架 + 视图路由(dashboard/agent-detail/analytics)+ 全局事件处理(如文件 diff 查看)
- `components/AgentDetail.vue` — Agent 详情:指令输入 + 日志流 + PR 审阅 + 文件变更(点击通过 engine 获取 diff)
- `composables/useConfirm.ts` — 全局确认弹窗状态管理(命令式 API)
- `composables/useToast.ts` — 全局 Toast 通知状态管理(命令式 API)
- `components/SettingsPanel.vue` — 系统设置(GitHub Token + Kimi CLI 安装指引)
- `components/AnalyticsPanel.vue` — 监控分析:状态分布、Token 排行、活跃/审阅任务
- `components/TaskCard.vue` — Agent 卡片:状态 + Token 进度 + 审阅徽章
Expand Down
2 changes: 1 addition & 1 deletion kimi-code-swarm/agent-engine/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ export class Agent {
this.state.ciStatus = 'failure'
this.log('error', `CI 失败: ${failedRun.name}`)

const logs = await getCheckRunLogs(githubToken, this.state.repoUrl, failedRun.id)
const logs = await getCheckRunLogs(githubToken, this.state.repoUrl, failedRun.id, failedRun.details_url)
await this.fixBasedOnCiFailure(logs || `Check run "${failedRun.name}" failed. No logs available.`, githubToken)
return
}
Expand Down
26 changes: 23 additions & 3 deletions kimi-code-swarm/agent-engine/src/github-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export interface CheckRun {
status: string
conclusion: string | null
html_url: string
details_url?: string
started_at: string | null
}

Expand Down Expand Up @@ -147,18 +148,38 @@ export async function getCheckRuns(

/**
* 获取失败 check run 的日志文本
* GitHub 返回的是 text/plain 的日志文件,跟随重定向获取
* 优先使用 GitHub Actions job logs API(check-runs 的 logs_url 经常为 null)
*/
export async function getCheckRunLogs(
token: string,
repoUrl: string,
checkRunId: number,
detailsUrl?: string,
): Promise<string | null> {
const repo = parseRepoUrl(repoUrl)
if (!repo) return null

const url = `${GITHUB_API}/repos/${repo.owner}/${repo.repo}/check-runs/${checkRunId}/logs`
// 方案 A: 从 details_url 提取 job_id,调用 Actions job logs API
if (detailsUrl) {
const match = detailsUrl.match(/\/job\/(\d+)$/)
if (match) {
const jobId = match[1]
const url = `${GITHUB_API}/repos/${repo.owner}/${repo.repo}/actions/jobs/${jobId}/logs`
try {
const res = await fetch(url, { headers: getHeaders(token) })
if (res.ok) {
const text = await res.text()
return text.length > 8000 ? text.slice(0, 8000) + '\n...[truncated]' : text
}
console.error(`[github-api] job logs ${res.status}`)
} catch (err) {
console.error(`[github-api] job logs 异常: ${String(err)}`)
}
}
}

// 方案 B: fallback 到 check-runs logs 端点
const url = `${GITHUB_API}/repos/${repo.owner}/${repo.repo}/check-runs/${checkRunId}/logs`
try {
const res = await fetch(url, {
headers: { ...getHeaders(token), Accept: 'application/vnd.github.v3+json' },
Expand All @@ -168,7 +189,6 @@ export async function getCheckRunLogs(
console.error(`[github-api] getCheckRunLogs ${res.status}`)
return null
}
// 日志可能很大,截断到 8000 字符以内
const text = await res.text()
return text.length > 8000 ? text.slice(0, 8000) + '\n...[truncated]' : text
} catch (err) {
Expand Down
6 changes: 6 additions & 0 deletions kimi-code-swarm/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import AgentDetail from './components/AgentDetail.vue'
import CreateTaskModal from './components/CreateTaskModal.vue'
import SettingsPanel from './components/SettingsPanel.vue'
import AnalyticsPanel from './components/AnalyticsPanel.vue'
import SwarmConfirmModal from './components/SwarmConfirmModal.vue'
import SwarmToast from './components/SwarmToast.vue'
import { useSwarmStore } from './store/useSwarmStore'

const store = useSwarmStore()
Expand Down Expand Up @@ -196,4 +198,8 @@ function handleLogout() {
@create="store.createAgent"
/>
</div>

<!-- Global Overlay Components -->
<SwarmConfirmModal />
<SwarmToast />
</template>
109 changes: 109 additions & 0 deletions kimi-code-swarm/src/components/SwarmConfirmModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<script setup lang="ts">
import { computed } from 'vue'
import { AlertTriangle, Info, AlertCircle, X } from 'lucide-vue-next'
import { useConfirm } from '../composables/useConfirm'

const { state, resolve } = useConfirm()

const config = computed(() => {
switch (state.type) {
case 'danger':
return {
icon: AlertTriangle,
iconBg: 'bg-red-50',
iconColor: 'text-red-600',
confirmBg: 'bg-red-600 hover:bg-red-700',
confirmText: 'text-white',
border: 'border-red-200',
}
case 'warning':
return {
icon: AlertCircle,
iconBg: 'bg-amber-50',
iconColor: 'text-amber-600',
confirmBg: 'bg-amber-600 hover:bg-amber-700',
confirmText: 'text-white',
border: 'border-amber-200',
}
default:
return {
icon: Info,
iconBg: 'bg-blue-50',
iconColor: 'text-blue-600',
confirmBg: 'bg-swarm-600 hover:bg-swarm-700',
confirmText: 'text-white',
border: 'border-gray-200',
}
}
})
</script>

<template>
<Transition
enter-active-class="transition-opacity duration-200"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition-opacity duration-150"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="state.isOpen" class="fixed inset-0 z-[90] flex items-center justify-center">
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="resolve(false)" />

<!-- Modal -->
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 scale-95 translate-y-2"
enter-to-class="opacity-100 scale-100 translate-y-0"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 scale-100 translate-y-0"
leave-to-class="opacity-0 scale-95 translate-y-2"
>
<div
v-if="state.isOpen"
class="relative bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden border"
:class="config.border"
>
<div class="p-6">
<div class="flex items-start gap-4">
<div
class="w-10 h-10 rounded-xl flex items-center justify-center shrink-0"
:class="config.iconBg"
>
<component :is="config.icon" class="w-5 h-5" :class="config.iconColor" />
</div>
<div class="flex-1 min-w-0">
<h3 class="text-base font-semibold text-gray-900">{{ state.title }}</h3>
<p v-if="state.message" class="text-sm text-gray-500 mt-1 leading-relaxed whitespace-pre-line">
{{ state.message }}
</p>
</div>
<button
class="text-gray-300 hover:text-gray-600 transition-colors shrink-0"
@click="resolve(false)"
>
<X class="w-4 h-4" />
</button>
</div>
</div>

<div class="px-6 pb-6 flex items-center justify-end gap-3">
<button
class="px-4 py-2 rounded-lg text-sm font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100 transition-colors"
@click="resolve(false)"
>
{{ state.cancelText }}
</button>
<button
:class="['px-4 py-2 rounded-lg text-sm font-medium transition-colors', config.confirmBg, config.confirmText]"
@click="resolve(true)"
>
{{ state.confirmText }}
</button>
</div>
</div>
</Transition>
</div>
</Transition>
</template>
88 changes: 88 additions & 0 deletions kimi-code-swarm/src/components/SwarmToast.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<script setup lang="ts">
import { X, AlertCircle, CheckCircle2, Info, AlertTriangle } from 'lucide-vue-next'
import { useToast, type ToastType } from '../composables/useToast'

const { toasts, remove } = useToast()

const config: Record<ToastType, { icon: typeof AlertCircle; color: string; bg: string; border: string; bar: string }> = {
error: {
icon: AlertCircle,
color: 'text-red-600',
bg: 'bg-red-50',
border: 'border-red-200',
bar: 'bg-red-500',
},
success: {
icon: CheckCircle2,
color: 'text-emerald-600',
bg: 'bg-emerald-50',
border: 'border-emerald-200',
bar: 'bg-emerald-500',
},
info: {
icon: Info,
color: 'text-blue-600',
bg: 'bg-blue-50',
border: 'border-blue-200',
bar: 'bg-blue-500',
},
warning: {
icon: AlertTriangle,
color: 'text-amber-600',
bg: 'bg-amber-50',
border: 'border-amber-200',
bar: 'bg-amber-500',
},
}

function getConfig(type: ToastType) {
return config[type]
}
</script>

<template>
<div class="fixed top-4 right-4 z-[100] flex flex-col gap-3 pointer-events-none">
<TransitionGroup
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="translate-x-8 opacity-0 scale-95"
enter-to-class="translate-x-0 opacity-100 scale-100"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="translate-x-0 opacity-100 scale-100"
leave-to-class="translate-x-8 opacity-0 scale-95"
>
<div
v-for="toast in toasts"
:key="toast.id"
class="pointer-events-auto w-80 bg-white rounded-xl border shadow-lg overflow-hidden flex flex-col"
:class="[getConfig(toast.type).border]"
>
<div class="flex items-start gap-3 p-4">
<div
class="w-8 h-8 rounded-lg flex items-center justify-center shrink-0"
:class="getConfig(toast.type).bg"
>
<component :is="getConfig(toast.type).icon" class="w-4 h-4" :class="getConfig(toast.type).color" />
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-semibold text-gray-900">{{ toast.title }}</p>
<p v-if="toast.message" class="text-xs text-gray-500 mt-0.5 leading-relaxed">{{ toast.message }}</p>
</div>
<button
class="text-gray-300 hover:text-gray-600 transition-colors shrink-0 mt-0.5"
@click="remove(toast.id)"
>
<X class="w-4 h-4" />
</button>
</div>
<!-- Progress bar -->
<div class="h-0.5 w-full bg-gray-100">
<div
class="h-full"
:class="getConfig(toast.type).bar"
:style="{ animation: `shrink ${toast.duration}ms linear forwards`, width: '100%' }"
/>
</div>
</div>
</TransitionGroup>
</div>
</template>
51 changes: 51 additions & 0 deletions kimi-code-swarm/src/composables/useConfirm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { reactive } from 'vue'

export type ConfirmType = 'danger' | 'warning' | 'info'

interface ConfirmState {
isOpen: boolean
type: ConfirmType
title: string
message: string
confirmText: string
cancelText: string
resolve: ((value: boolean) => void) | null
}

const state = reactive<ConfirmState>({
isOpen: false,
type: 'info',
title: '',
message: '',
confirmText: '确认',
cancelText: '取消',
resolve: null,
})

export function useConfirm() {
function confirm(options: {
type?: ConfirmType
title: string
message?: string
confirmText?: string
cancelText?: string
}): Promise<boolean> {
return new Promise((resolve) => {
state.type = options.type ?? 'info'
state.title = options.title
state.message = options.message ?? ''
state.confirmText = options.confirmText ?? '确认'
state.cancelText = options.cancelText ?? '取消'
state.resolve = resolve
state.isOpen = true
})
}

function resolve(value: boolean) {
state.isOpen = false
state.resolve?.(value)
state.resolve = null
}

return { state, confirm, resolve }
}
Loading
Loading