fix: merge project workflow into project view
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import ProjectView from './views/ProjectView.vue'
|
||||
import WorkflowView from './views/WorkflowView.vue'
|
||||
import AgentView from './views/AgentView.vue'
|
||||
import DraftsView from './views/DraftsView.vue'
|
||||
import SettingsView from './views/SettingsView.vue'
|
||||
@@ -9,7 +8,6 @@ import StatusBadge from './components/StatusBadge.vue'
|
||||
|
||||
const tabs = [
|
||||
{ id: 'projects', label: '项目视图', component: ProjectView },
|
||||
{ id: 'workflow', label: '工作流视图', component: WorkflowView },
|
||||
{ id: 'agents', label: '智能体视图', component: AgentView },
|
||||
{ id: 'drafts', label: '草稿', component: DraftsView },
|
||||
{ id: 'settings', label: '设置', component: SettingsView },
|
||||
@@ -30,7 +28,7 @@ const activeComponent = computed(() => tabs.find((tab) => tab.id === activeTab.v
|
||||
<div class="connection-card" aria-label="连接状态">
|
||||
<StatusBadge label="安全接口" status="complete" confidence="中" source="计划文件" />
|
||||
<strong>按需读取和确认写回</strong>
|
||||
<span>项目、运行线程和工作流保持只读;智能体草稿仅在校验、备份和确认后单文件写回。</span>
|
||||
<span>项目、运行线程和项目内工作流保持只读;智能体草稿仅在校验、备份和确认后单文件写回。</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -195,6 +195,11 @@ export function normalizeRuntime(payload = {}) {
|
||||
source: source.label,
|
||||
confidence: source.confidenceLabel,
|
||||
lastActivity: thread.updatedAt || thread.createdAt || '没有时间记录',
|
||||
displayName: actorDisplayName({
|
||||
name: thread.agentNickname,
|
||||
role: thread.agentRole || thread.role,
|
||||
}),
|
||||
phaseName: extractPhaseName([goalText, taskSummary, thread.title, thread.preview, thread.agentPath]) || '未标注阶段',
|
||||
}
|
||||
})
|
||||
const source = normalizeSource(payload.source)
|
||||
@@ -243,9 +248,12 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
|
||||
const threadIds = new Set(agents.map((agent) => agent.id))
|
||||
const agentByID = new Map(agents.map((agent) => [agent.id, agent]))
|
||||
const threadByID = new Map(runtime.threads.map((thread) => [thread.id, thread]))
|
||||
const projectThreadIds = new Set(runtime.threads.filter((thread) => threadCanJoinProjectFlow(thread, targetPath)).map((thread) => thread.id))
|
||||
const projectThreadIds = new Set([
|
||||
...threadIds,
|
||||
...runtime.threads.filter((thread) => threadBelongsToProject(thread, targetPath)).map((thread) => thread.id),
|
||||
])
|
||||
const threads = runtime.threads.filter((thread) => threadIds.has(thread.id))
|
||||
const goals = runtime.goals.filter((goal) => threadIds.has(goal.threadId))
|
||||
const goals = runtime.goals.filter((goal) => threadIds.has(goal.threadId) || projectThreadIds.has(goal.threadId))
|
||||
const edges = runtime.edges.filter((edge) =>
|
||||
(threadIds.has(edge.fromThreadId) || threadIds.has(edge.toThreadId)) &&
|
||||
projectThreadIds.has(edge.fromThreadId) &&
|
||||
@@ -253,6 +261,8 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
|
||||
)
|
||||
const handoffs = edges.map((edge) => normalizeProjectHandoff(edge, { agentByID, threadByID, source: runtime.source }))
|
||||
const phaseGroups = buildPhaseGroups(agents)
|
||||
const phaseHandoffs = buildPhaseHandoffs(handoffs, phaseGroups)
|
||||
const supervision = buildSupervision({ agents, handoffs, projectThreadIds, runtime, threadByID, goals })
|
||||
|
||||
return {
|
||||
...runtime,
|
||||
@@ -262,6 +272,8 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
|
||||
edges,
|
||||
handoffs,
|
||||
phaseGroups,
|
||||
phaseHandoffs,
|
||||
supervision,
|
||||
isEmpty: agents.length === 0,
|
||||
emptyTitle: '这个项目没有运行线程',
|
||||
emptyText: projectPath
|
||||
@@ -273,14 +285,22 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
|
||||
function normalizeProjectHandoff(edge, context) {
|
||||
const source = normalizeSource(edge.source, context.source)
|
||||
const status = normalizeSpawnStatus(edge.reason || edge.status) || 'unknown'
|
||||
const fromAgent = context.agentByID.get(edge.fromThreadId)
|
||||
const toAgent = context.agentByID.get(edge.toThreadId)
|
||||
const fromIsMain = isMainThread(context.threadByID.get(edge.fromThreadId))
|
||||
const toIsMain = isMainThread(context.threadByID.get(edge.toThreadId))
|
||||
const directionLabel = handoffDirectionLabel({ fromAgent, toAgent, fromIsMain, toIsMain })
|
||||
return {
|
||||
id: `${edge.fromThreadId || 'unknown'}-${edge.toThreadId || 'unknown'}-${edge.createdAt || edge.reason || edge.status || 'event'}`,
|
||||
from: runtimeNodeName(edge.fromThreadId, context),
|
||||
to: runtimeNodeName(edge.toThreadId, context),
|
||||
summary: formatStatus(status),
|
||||
directionLabel,
|
||||
status,
|
||||
time: edge.createdAt || '后端事件',
|
||||
source: source.label,
|
||||
confidence: source.confidenceLabel,
|
||||
phaseName: handoffPhaseName({ fromAgent, toAgent, toIsMain }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,19 +310,50 @@ function runtimeNodeName(threadID, { agentByID, threadByID }) {
|
||||
}
|
||||
const agent = agentByID.get(threadID)
|
||||
if (agent) {
|
||||
return agent.name
|
||||
return agent.displayName
|
||||
}
|
||||
const thread = threadByID.get(threadID)
|
||||
if (thread?.threadSource === 'user' || (!thread?.agentNickname && !thread?.agentRole && !thread?.agentPath && !thread?.role)) {
|
||||
return '主线程'
|
||||
if (isMainThread(thread)) {
|
||||
return '主线程 / 主智能体监管'
|
||||
}
|
||||
return thread?.agentNickname || thread?.agentRole || thread?.role || '未知线程'
|
||||
return actorDisplayName({
|
||||
name: thread?.agentNickname,
|
||||
role: thread?.agentRole || thread?.role,
|
||||
fallbackName: '未知线程',
|
||||
fallbackRole: '角色未知',
|
||||
})
|
||||
}
|
||||
|
||||
function handoffPhaseName({ fromAgent, toAgent, toIsMain }) {
|
||||
if (toIsMain) {
|
||||
return fromAgent?.phaseName || '未标注阶段'
|
||||
}
|
||||
if (toAgent?.phaseName) {
|
||||
return toAgent.phaseName
|
||||
}
|
||||
return fromAgent?.phaseName || '未标注阶段'
|
||||
}
|
||||
|
||||
function handoffDirectionLabel({ fromAgent, toAgent, fromIsMain, toIsMain }) {
|
||||
if (fromIsMain && toAgent) {
|
||||
return '主线程派发'
|
||||
}
|
||||
if (fromAgent && toIsMain) {
|
||||
return '回到主线程'
|
||||
}
|
||||
if (fromAgent && toAgent && fromAgent.phaseName !== toAgent.phaseName) {
|
||||
return '跨阶段交接'
|
||||
}
|
||||
if (fromAgent && toAgent) {
|
||||
return '子智能体交接'
|
||||
}
|
||||
return '线程交接'
|
||||
}
|
||||
|
||||
function buildPhaseGroups(agents) {
|
||||
const groups = new Map()
|
||||
for (const agent of agents) {
|
||||
const phaseName = extractPhaseName([agent.goal, ...(agent.projectHints || [])]) || '未标注阶段'
|
||||
const phaseName = agent.phaseName || extractPhaseName([agent.goal, ...(agent.projectHints || [])]) || '未标注阶段'
|
||||
if (!groups.has(phaseName)) {
|
||||
groups.set(phaseName, [])
|
||||
}
|
||||
@@ -322,6 +373,54 @@ function buildPhaseGroups(agents) {
|
||||
.sort((a, b) => phaseSortKey(a.name) - phaseSortKey(b.name) || a.name.localeCompare(b.name, 'zh-CN'))
|
||||
}
|
||||
|
||||
function buildPhaseHandoffs(handoffs, phaseGroups) {
|
||||
const groups = new Map(phaseGroups.map((phase) => [phase.name, { ...phase, handoffs: [] }]))
|
||||
for (const handoff of handoffs) {
|
||||
const phaseName = handoff.phaseName || '未标注阶段'
|
||||
if (!groups.has(phaseName)) {
|
||||
groups.set(phaseName, {
|
||||
name: phaseName,
|
||||
status: handoff.status,
|
||||
statusZh: formatStatus(handoff.status),
|
||||
roles: [],
|
||||
agents: [],
|
||||
handoffs: [],
|
||||
})
|
||||
}
|
||||
groups.get(phaseName).handoffs.push(handoff)
|
||||
}
|
||||
return [...groups.values()]
|
||||
.filter((phase) => phase.agents.length > 0 || phase.handoffs.length > 0)
|
||||
.map((phase) => ({
|
||||
...phase,
|
||||
status: phaseStatus([...phase.agents, ...phase.handoffs]),
|
||||
statusZh: formatStatus(phaseStatus([...phase.agents, ...phase.handoffs])),
|
||||
}))
|
||||
.sort((a, b) => phaseSortKey(a.name) - phaseSortKey(b.name) || a.name.localeCompare(b.name, 'zh-CN'))
|
||||
}
|
||||
|
||||
function buildSupervision({ agents, handoffs, projectThreadIds, runtime, threadByID, goals }) {
|
||||
const mainThread = [...projectThreadIds]
|
||||
.map((threadID) => threadByID.get(threadID))
|
||||
.find((thread) => isMainThread(thread))
|
||||
const orchestrator = agents.find((agent) => /编排|主智能体|监管/.test(`${agent.role} ${agent.name} ${agent.goal}`))
|
||||
const supervisorID = mainThread?.id || orchestrator?.id || ''
|
||||
const supervisorGoal = goals.find((goal) => goal.threadId === supervisorID) || goals.find((goal) => projectThreadIds.has(goal.threadId))
|
||||
const status = mainThread?.status || orchestrator?.status || supervisorGoal?.status || (handoffs.length > 0 ? 'recent' : 'unknown')
|
||||
const source = normalizeSource(mainThread?.source || orchestrator?.sourceDetail, runtime.source)
|
||||
return {
|
||||
actor: mainThread ? '主线程 / 主智能体监管' : orchestrator?.displayName || '主线程 / 主智能体监管',
|
||||
status,
|
||||
statusZh: formatStatus(status),
|
||||
goal: supervisorGoal?.goal || supervisorGoal?.objective || orchestrator?.goal || '没有目标记录',
|
||||
lastActivity: mainThread?.updatedAt || mainThread?.createdAt || supervisorGoal?.updatedAt || orchestrator?.lastActivity || '没有时间记录',
|
||||
handoffCount: handoffs.length,
|
||||
agentCount: agents.length,
|
||||
source: source.label,
|
||||
confidence: source.confidenceLabel,
|
||||
}
|
||||
}
|
||||
|
||||
function phaseStatus(agents) {
|
||||
if (agents.some((agent) => agent.status === 'running' || agent.status === 'active')) {
|
||||
return 'running'
|
||||
@@ -347,6 +446,21 @@ function extractPhaseName(values) {
|
||||
return ''
|
||||
}
|
||||
|
||||
function actorDisplayName({ name = '', role = '', fallbackName = '', fallbackRole = '未记录角色' } = {}) {
|
||||
const cleanName = String(name || '').trim()
|
||||
const cleanRole = String(role || '').trim()
|
||||
if (cleanName && cleanRole) {
|
||||
return `${cleanName} / ${cleanRole}`
|
||||
}
|
||||
if (cleanRole) {
|
||||
return cleanRole
|
||||
}
|
||||
if (cleanName) {
|
||||
return `${cleanName} / ${fallbackRole}`
|
||||
}
|
||||
return fallbackName ? `${fallbackName} / ${fallbackRole}` : `未知线程 / ${fallbackRole}`
|
||||
}
|
||||
|
||||
function phaseSortKey(name) {
|
||||
const match = String(name).match(/阶段\s*([0-9]+)/)
|
||||
if (!match) {
|
||||
@@ -528,11 +642,14 @@ function threadBelongsToProject(thread, targetPath) {
|
||||
}
|
||||
|
||||
function threadCanJoinProjectFlow(thread, targetPath) {
|
||||
if (threadBelongsToProject(thread, targetPath)) {
|
||||
return true
|
||||
return threadBelongsToProject(thread, targetPath)
|
||||
}
|
||||
const cwd = normalizePath(thread?.cwd)
|
||||
return Boolean(cwd && targetPath.startsWith(`${cwd}/`))
|
||||
|
||||
function isMainThread(thread) {
|
||||
if (!thread || thread.threadSource === 'subagent') {
|
||||
return false
|
||||
}
|
||||
return Boolean(thread.threadSource === 'user' || (!thread.agentNickname && !thread.agentRole && !thread.agentPath && !thread.role))
|
||||
}
|
||||
|
||||
function normalizeSpawnStatus(status) {
|
||||
|
||||
@@ -202,6 +202,124 @@ test('filters runtime to selected project and displays agent names from Codex me
|
||||
assert.equal(projectRuntime.agents[0].statusZh, '运行中')
|
||||
})
|
||||
|
||||
test('agent display names include role without inventing a nickname', () => {
|
||||
const runtime = normalizeRuntime({
|
||||
items: [
|
||||
{
|
||||
id: 'role-only',
|
||||
cwd: '/repo/a',
|
||||
title: '阶段 1:产品规划',
|
||||
agentRole: '产品经理',
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
{
|
||||
id: 'name-only',
|
||||
cwd: '/repo/a',
|
||||
title: '阶段 1:补充资料',
|
||||
agentNickname: 'Nash',
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
],
|
||||
source: { kind: 'sqlite_readonly', confidence: 'high' },
|
||||
})
|
||||
|
||||
const projectRuntime = filterRuntimeByProject(runtime, '/repo/a')
|
||||
|
||||
assert.deepEqual(projectRuntime.agents.map((agent) => agent.displayName), [
|
||||
'产品经理',
|
||||
'Nash / 未记录角色',
|
||||
])
|
||||
})
|
||||
|
||||
test('supervision only uses project-proven main threads', () => {
|
||||
const runtime = normalizeRuntime({
|
||||
items: [
|
||||
{
|
||||
id: 'wrong-main',
|
||||
cwd: '/Users/yoilun',
|
||||
title: '普通对话,没有项目路径',
|
||||
threadSource: 'user',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
{
|
||||
id: 'right-main',
|
||||
cwd: '/Users/yoilun',
|
||||
title: '监管 /Users/yoilun/Code/codex-agent-manager 项目',
|
||||
threadSource: 'user',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
{
|
||||
id: 'coder',
|
||||
cwd: '/Users/yoilun',
|
||||
title: '阶段 2:实现 /Users/yoilun/Code/codex-agent-manager',
|
||||
agentNickname: 'Ada',
|
||||
agentRole: '前端开发者',
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
],
|
||||
goals: [
|
||||
{ threadId: 'wrong-main', goal: '监管别的项目', status: 'active' },
|
||||
{ threadId: 'right-main', goal: '监管 codex-agent-manager 项目', status: 'active' },
|
||||
],
|
||||
edges: [
|
||||
{ fromThreadId: 'wrong-main', toThreadId: 'coder', reason: 'open' },
|
||||
{ fromThreadId: 'right-main', toThreadId: 'coder', reason: 'open' },
|
||||
],
|
||||
source: { kind: 'sqlite_readonly', confidence: 'high' },
|
||||
})
|
||||
|
||||
const projectRuntime = filterRuntimeByProject(runtime, '/Users/yoilun/Code/codex-agent-manager')
|
||||
|
||||
assert.deepEqual(projectRuntime.handoffs.map((handoff) => handoff.from), ['主线程 / 主智能体监管'])
|
||||
assert.equal(projectRuntime.supervision.goal, '监管 codex-agent-manager 项目')
|
||||
})
|
||||
|
||||
test('bare subagent threads are not treated as main supervision threads', () => {
|
||||
const runtime = normalizeRuntime({
|
||||
items: [
|
||||
{
|
||||
id: 'main',
|
||||
cwd: '/repo/a',
|
||||
title: '监管 /repo/a 项目',
|
||||
threadSource: 'user',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
{
|
||||
id: 'bare-sub',
|
||||
cwd: '/repo/a',
|
||||
title: '阶段 3:没有角色元数据的子智能体',
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
{
|
||||
id: 'reviewer',
|
||||
cwd: '/repo/a',
|
||||
title: '阶段 3:审查',
|
||||
agentNickname: 'Rawls',
|
||||
agentRole: '代码审查员',
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
],
|
||||
goals: [
|
||||
{ threadId: 'main', goal: '监管真实项目', status: 'active' },
|
||||
{ threadId: 'bare-sub', goal: '裸子智能体目标', status: 'active' },
|
||||
],
|
||||
edges: [
|
||||
{ fromThreadId: 'bare-sub', toThreadId: 'reviewer', reason: 'open' },
|
||||
],
|
||||
source: { kind: 'sqlite_readonly', confidence: 'high' },
|
||||
})
|
||||
|
||||
const projectRuntime = filterRuntimeByProject(runtime, '/repo/a')
|
||||
|
||||
assert.equal(projectRuntime.supervision.goal, '监管真实项目')
|
||||
assert.equal(projectRuntime.handoffs[0].directionLabel, '子智能体交接')
|
||||
})
|
||||
|
||||
test('does not turn ordinary conversation threads into agents', () => {
|
||||
const runtime = normalizeRuntime({
|
||||
items: [
|
||||
@@ -275,6 +393,15 @@ test('builds project handoffs and phase groups from runtime edges and agent task
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
{
|
||||
id: 'publisher',
|
||||
cwd: '/repo/a',
|
||||
title: '阶段 7:发布变更',
|
||||
agentNickname: 'Noether',
|
||||
agentRole: '发布经理',
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
{
|
||||
id: 'other-main',
|
||||
cwd: '/repo/b',
|
||||
@@ -287,8 +414,13 @@ test('builds project handoffs and phase groups from runtime edges and agent task
|
||||
{ fromThreadId: 'main', toThreadId: 'coder', reason: 'closed' },
|
||||
{ fromThreadId: 'coder', toThreadId: 'reviewer', reason: 'open' },
|
||||
{ fromThreadId: 'main', toThreadId: 'pm', reason: 'closed' },
|
||||
{ fromThreadId: 'reviewer', toThreadId: 'main', reason: 'closed' },
|
||||
{ fromThreadId: 'reviewer', toThreadId: 'publisher', reason: 'open' },
|
||||
{ fromThreadId: 'other-main', toThreadId: 'coder', reason: 'open' },
|
||||
],
|
||||
goals: [
|
||||
{ threadId: 'main', goal: '监管 /repo/a 项目内智能体流程', status: 'active', updatedAt: '2026-05-26T09:00:00Z' },
|
||||
],
|
||||
source: { kind: 'sqlite_readonly', confidence: 'high' },
|
||||
})
|
||||
|
||||
@@ -298,27 +430,69 @@ test('builds project handoffs and phase groups from runtime edges and agent task
|
||||
'已完成',
|
||||
'运行中',
|
||||
'已完成',
|
||||
'已完成',
|
||||
'运行中',
|
||||
])
|
||||
assert.deepEqual(projectRuntime.handoffs.map((handoff) => `${handoff.from} -> ${handoff.to}`), [
|
||||
'主线程 -> Averroes',
|
||||
'Averroes -> Rawls',
|
||||
'主线程 -> Zeno',
|
||||
'主线程 / 主智能体监管 -> Averroes / 后端架构师',
|
||||
'Averroes / 后端架构师 -> Rawls / 代码审查员',
|
||||
'主线程 / 主智能体监管 -> Zeno / 高级项目经理',
|
||||
'Rawls / 代码审查员 -> 主线程 / 主智能体监管',
|
||||
'Rawls / 代码审查员 -> Noether / 发布经理',
|
||||
])
|
||||
assert.deepEqual(projectRuntime.handoffs.map((handoff) => handoff.directionLabel), [
|
||||
'主线程派发',
|
||||
'子智能体交接',
|
||||
'主线程派发',
|
||||
'回到主线程',
|
||||
'跨阶段交接',
|
||||
])
|
||||
assert.ok(projectRuntime.handoffs.every((handoff) => handoff.time === '后端事件'))
|
||||
assert.deepEqual(projectRuntime.phaseGroups.map((phase) => phase.name), ['阶段 6', '阶段 7'])
|
||||
assert.equal(projectRuntime.phaseGroups[0].status, 'running')
|
||||
assert.deepEqual(projectRuntime.phaseGroups[0].roles, ['代码审查员', '后端架构师'])
|
||||
assert.deepEqual(projectRuntime.phaseGroups[0].agents.map((agent) => agent.name), ['Averroes', 'Rawls'])
|
||||
assert.deepEqual(projectRuntime.phaseGroups[0].agents.map((agent) => agent.displayName), ['Averroes / 后端架构师', 'Rawls / 代码审查员'])
|
||||
assert.deepEqual(projectRuntime.phaseHandoffs.map((phase) => phase.name), ['阶段 6', '阶段 7'])
|
||||
assert.deepEqual(projectRuntime.phaseHandoffs[0].handoffs.map((handoff) => `${handoff.from} -> ${handoff.to}`), [
|
||||
'主线程 / 主智能体监管 -> Averroes / 后端架构师',
|
||||
'Averroes / 后端架构师 -> Rawls / 代码审查员',
|
||||
'Rawls / 代码审查员 -> 主线程 / 主智能体监管',
|
||||
])
|
||||
assert.deepEqual(projectRuntime.phaseHandoffs[1].handoffs.map((handoff) => `${handoff.from} -> ${handoff.to}`), [
|
||||
'主线程 / 主智能体监管 -> Zeno / 高级项目经理',
|
||||
'Rawls / 代码审查员 -> Noether / 发布经理',
|
||||
])
|
||||
assert.equal(projectRuntime.supervision.actor, '主线程 / 主智能体监管')
|
||||
assert.equal(projectRuntime.supervision.goal, '监管 /repo/a 项目内智能体流程')
|
||||
assert.equal(projectRuntime.supervision.handoffCount, 5)
|
||||
assert.equal(projectRuntime.supervision.agentCount, 4)
|
||||
})
|
||||
|
||||
test('project view does not include sample runtime fallbacks', async () => {
|
||||
test('project and app views keep workflow inside the selected project view', async () => {
|
||||
const thisFile = fileURLToPath(import.meta.url)
|
||||
const viewPath = new URL('../views/ProjectView.vue', `file://${thisFile}`)
|
||||
const source = await readFile(viewPath, 'utf8')
|
||||
const appPath = new URL('../App.vue', `file://${thisFile}`)
|
||||
const viewSource = await readFile(viewPath, 'utf8')
|
||||
const appSource = await readFile(appPath, 'utf8')
|
||||
|
||||
assert.doesNotMatch(source, /sampleProjects/)
|
||||
assert.doesNotMatch(source, /sampleAgentMatrix/)
|
||||
assert.match(source, /goalSource/)
|
||||
assert.doesNotMatch(viewSource, /sampleProjects/)
|
||||
assert.doesNotMatch(viewSource, /sampleAgentMatrix/)
|
||||
assert.doesNotMatch(appSource, /WorkflowView/)
|
||||
assert.doesNotMatch(appSource, /工作流视图/)
|
||||
assert.match(viewSource, /selectedAgentId/)
|
||||
assert.match(viewSource, /selectAgent/)
|
||||
assert.match(viewSource, /phaseHandoffs/)
|
||||
assert.match(viewSource, /supervision/)
|
||||
assert.match(viewSource, /goalSource/)
|
||||
})
|
||||
|
||||
test('handoff timeline uses normalized id as stable key', async () => {
|
||||
const thisFile = fileURLToPath(import.meta.url)
|
||||
const componentPath = new URL('../components/HandoffTimeline.vue', `file://${thisFile}`)
|
||||
const source = await readFile(componentPath, 'utf8')
|
||||
|
||||
assert.match(source, /:key="item\.id \|\|/)
|
||||
})
|
||||
|
||||
test('keeps project threads when Codex cwd is an ancestor but metadata names the project path', () => {
|
||||
|
||||
@@ -6,12 +6,12 @@ defineProps({
|
||||
|
||||
<template>
|
||||
<ol class="handoff-timeline">
|
||||
<li v-for="item in items" :key="`${item.from}-${item.to}-${item.summary}`">
|
||||
<li v-for="item in items" :key="item.id || `${item.from}-${item.to}-${item.summary}`">
|
||||
<div class="timeline-marker" aria-hidden="true"></div>
|
||||
<div>
|
||||
<p class="timeline-title">{{ item.from }} → {{ item.to }}</p>
|
||||
<p>{{ item.summary }}</p>
|
||||
<span>{{ item.time }} · 来源 {{ item.source }} · 置信度 {{ item.confidence }}</span>
|
||||
<span>{{ item.directionLabel || '线程交接' }} · {{ item.time }} · 来源 {{ item.source }} · 置信度 {{ item.confidence }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
@@ -295,6 +295,19 @@ button {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.matrix-row.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.matrix-row.clickable:hover,
|
||||
.matrix-row.clickable.selected {
|
||||
background: color-mix(in srgb, var(--green-soft) 62%, var(--panel));
|
||||
}
|
||||
|
||||
.matrix-row.clickable.selected {
|
||||
box-shadow: inset 4px 0 0 var(--green);
|
||||
}
|
||||
|
||||
.matrix-row > span {
|
||||
min-width: 0;
|
||||
padding: 14px;
|
||||
@@ -438,6 +451,7 @@ button {
|
||||
}
|
||||
|
||||
.project-item[role="button"]:focus-visible,
|
||||
.matrix-row[tabindex]:focus-visible,
|
||||
.agent-list-item:focus-visible,
|
||||
.tab-button:focus-visible {
|
||||
outline: 3px solid color-mix(in srgb, var(--terracotta) 48%, transparent);
|
||||
@@ -550,6 +564,36 @@ button {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.phase-handoff-groups {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.phase-handoff-group {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.phase-handoff-group:first-child {
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.phase-handoff-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.muted-line {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.58;
|
||||
}
|
||||
|
||||
.graph-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import HandoffTimeline from '../components/HandoffTimeline.vue'
|
||||
import StatusBadge from '../components/StatusBadge.vue'
|
||||
import { apiClient } from '../api/client'
|
||||
@@ -10,13 +10,34 @@ const error = ref('')
|
||||
const projectState = ref(normalizeProjects())
|
||||
const runtimeState = ref(normalizeRuntime())
|
||||
const selectedProjectId = ref('')
|
||||
const selectedAgentId = ref('')
|
||||
|
||||
const selectedProject = computed(() => projectState.value.projects.find((project) => project.id === selectedProjectId.value))
|
||||
const projectRuntime = computed(() => filterRuntimeByProject(runtimeState.value, selectedProject.value?.path || ''))
|
||||
const selectedAgent = computed(() => projectRuntime.value.agents[0])
|
||||
const selectedAgent = computed(() =>
|
||||
projectRuntime.value.agents.find((agent) => agent.id === selectedAgentId.value) ?? projectRuntime.value.agents[0],
|
||||
)
|
||||
|
||||
onMounted(loadReadonlyData)
|
||||
|
||||
watch(
|
||||
() => projectRuntime.value.agents.map((agent) => agent.id).join('|'),
|
||||
() => {
|
||||
if (!projectRuntime.value.agents.some((agent) => agent.id === selectedAgentId.value)) {
|
||||
selectedAgentId.value = projectRuntime.value.agents[0]?.id ?? ''
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function selectProject(projectId) {
|
||||
selectedProjectId.value = projectId
|
||||
selectedAgentId.value = ''
|
||||
}
|
||||
|
||||
function selectAgent(agent) {
|
||||
selectedAgentId.value = agent.id
|
||||
}
|
||||
|
||||
async function loadReadonlyData() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
@@ -28,6 +49,7 @@ async function loadReadonlyData() {
|
||||
projectState.value = normalizeProjects(projectsPayload)
|
||||
runtimeState.value = normalizeRuntime(runtimePayload)
|
||||
selectedProjectId.value = projectState.value.projects[0]?.id ?? ''
|
||||
selectedAgentId.value = projectRuntime.value.agents[0]?.id ?? ''
|
||||
} catch (err) {
|
||||
error.value = err?.message || '连接后端接口失败'
|
||||
} finally {
|
||||
@@ -60,8 +82,8 @@ async function loadReadonlyData() {
|
||||
:class="{ selected: project.id === selectedProjectId }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="selectedProjectId = project.id"
|
||||
@keyup.enter="selectedProjectId = project.id"
|
||||
@click="selectProject(project.id)"
|
||||
@keyup.enter="selectProject(project.id)"
|
||||
>
|
||||
<div>
|
||||
<strong>{{ project.name }}</strong>
|
||||
@@ -110,10 +132,19 @@ async function loadReadonlyData() {
|
||||
<span role="columnheader">进程</span>
|
||||
<span role="columnheader">最近活动</span>
|
||||
</div>
|
||||
<div v-for="agent in projectRuntime.agents" :key="agent.id" class="matrix-row" role="row">
|
||||
<div
|
||||
v-for="agent in projectRuntime.agents"
|
||||
:key="agent.id"
|
||||
class="matrix-row clickable"
|
||||
:class="{ selected: agent.id === selectedAgentId }"
|
||||
role="row"
|
||||
tabindex="0"
|
||||
@click="selectAgent(agent)"
|
||||
@keyup.enter="selectAgent(agent)"
|
||||
>
|
||||
<span role="cell">
|
||||
<strong>{{ agent.name }}</strong>
|
||||
<small>{{ agent.role }}</small>
|
||||
<strong>{{ agent.displayName }}</strong>
|
||||
<small>{{ agent.phaseName }}</small>
|
||||
</span>
|
||||
<span role="cell">
|
||||
<StatusBadge :label="agent.statusZh" :status="agent.status" :source="agent.source" :confidence="agent.confidence" />
|
||||
@@ -142,7 +173,7 @@ async function loadReadonlyData() {
|
||||
<div>
|
||||
<strong>{{ phase.name }}</strong>
|
||||
<p>{{ phase.roles.join('、') || '未记录角色分类' }}</p>
|
||||
<span>{{ phase.agents.map((agent) => agent.name).join('、') }}</span>
|
||||
<span>{{ phase.agents.map((agent) => agent.displayName).join('、') }}</span>
|
||||
</div>
|
||||
<StatusBadge :label="phase.statusZh" :status="phase.status" source="SQLite 表" confidence="高" />
|
||||
</li>
|
||||
@@ -161,7 +192,16 @@ async function loadReadonlyData() {
|
||||
</div>
|
||||
<span class="read-only-chip">{{ projectRuntime.handoffs.length }} 条交接</span>
|
||||
</div>
|
||||
<HandoffTimeline v-if="projectRuntime.handoffs.length > 0" :items="projectRuntime.handoffs" />
|
||||
<div v-if="projectRuntime.phaseHandoffs.length > 0" class="phase-handoff-groups">
|
||||
<section v-for="phase in projectRuntime.phaseHandoffs" :key="phase.name" class="phase-handoff-group">
|
||||
<div class="phase-handoff-heading">
|
||||
<strong>{{ phase.name }}</strong>
|
||||
<StatusBadge :label="phase.statusZh" :status="phase.status" source="SQLite 表" confidence="高" />
|
||||
</div>
|
||||
<HandoffTimeline v-if="phase.handoffs.length > 0" :items="phase.handoffs" />
|
||||
<p v-else class="muted-line">这个阶段暂时没有交接边。</p>
|
||||
</section>
|
||||
</div>
|
||||
<div v-else class="empty-state compact">
|
||||
<strong>没有交接记录</strong>
|
||||
<p>当前项目没有匹配到主线程与智能体之间的交接边。</p>
|
||||
@@ -174,7 +214,7 @@ async function loadReadonlyData() {
|
||||
<aside class="panel detail-panel" aria-label="详情面板">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">详情</p>
|
||||
<h2>{{ selectedAgent?.name || selectedProject?.name || '只读详情' }}</h2>
|
||||
<h2>{{ selectedAgent?.displayName || selectedProject?.name || '只读详情' }}</h2>
|
||||
</div>
|
||||
<StatusBadge
|
||||
:label="selectedAgent?.statusZh || selectedProject?.statusZh || (error ? '连接失败' : '等待数据')"
|
||||
@@ -187,6 +227,11 @@ async function loadReadonlyData() {
|
||||
<p>{{ selectedAgent?.goal || '选择项目或等待运行线程后显示真实目标。' }}</p>
|
||||
<span v-if="selectedAgent?.goalSource">{{ selectedAgent.goalSource }}</span>
|
||||
</div>
|
||||
<div class="detail-block">
|
||||
<h3>主智能体监管</h3>
|
||||
<p>{{ projectRuntime.supervision.actor }}:{{ projectRuntime.supervision.goal }}</p>
|
||||
<span>{{ projectRuntime.supervision.statusZh }} · 最近活动 {{ projectRuntime.supervision.lastActivity }}</span>
|
||||
</div>
|
||||
<div class="detail-block">
|
||||
<h3>证据说明</h3>
|
||||
<p>数据来自只读接口。连接失败时保持错误状态,不使用示例数据伪装真实状态。</p>
|
||||
@@ -194,6 +239,8 @@ async function loadReadonlyData() {
|
||||
<div class="detail-grid">
|
||||
<span>项目数</span><strong>{{ projectState.projects.length }}</strong>
|
||||
<span>当前项目线程数</span><strong>{{ projectRuntime.threads.length }}</strong>
|
||||
<span>子智能体数</span><strong>{{ projectRuntime.supervision.agentCount }}</strong>
|
||||
<span>交接记录</span><strong>{{ projectRuntime.supervision.handoffCount }}</strong>
|
||||
<span>接口</span><strong>{{ error ? '连接失败' : '只读连接' }}</strong>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user