fix: restore project goals and flow records
This commit is contained in:
@@ -180,6 +180,7 @@ export function normalizeRuntime(payload = {}) {
|
|||||||
.map((goal) => goal.goal || goal.objective)
|
.map((goal) => goal.goal || goal.objective)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(';')
|
.join(';')
|
||||||
|
const taskSummary = summarizeTask(thread.title || thread.preview)
|
||||||
return {
|
return {
|
||||||
id: thread.id,
|
id: thread.id,
|
||||||
name: thread.agentNickname || thread.agentRole || thread.title || thread.role || thread.id || '未命名线程',
|
name: thread.agentNickname || thread.agentRole || thread.title || thread.role || thread.id || '未命名线程',
|
||||||
@@ -188,7 +189,8 @@ export function normalizeRuntime(payload = {}) {
|
|||||||
projectHints: [thread.title, thread.preview, thread.agentPath].filter(Boolean),
|
projectHints: [thread.title, thread.preview, thread.agentPath].filter(Boolean),
|
||||||
status,
|
status,
|
||||||
statusZh: formatStatus(status),
|
statusZh: formatStatus(status),
|
||||||
goal: goalText || '没有目标记录',
|
goal: goalText || taskSummary || '未记录结构化目标',
|
||||||
|
goalSource: goalText ? '结构化目标' : taskSummary ? '线程任务摘要' : '无目标来源',
|
||||||
process: describeRuntimeProcess(thread),
|
process: describeRuntimeProcess(thread),
|
||||||
source: source.label,
|
source: source.label,
|
||||||
confidence: source.confidenceLabel,
|
confidence: source.confidenceLabel,
|
||||||
@@ -210,12 +212,15 @@ export function normalizeRuntime(payload = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isRuntimeAgentThread(thread) {
|
function isRuntimeAgentThread(thread) {
|
||||||
|
if (thread?.threadSource === 'user') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return Boolean(
|
return Boolean(
|
||||||
thread?.agentNickname ||
|
thread?.agentNickname ||
|
||||||
thread?.agentRole ||
|
thread?.agentRole ||
|
||||||
thread?.agentPath ||
|
thread?.agentPath ||
|
||||||
thread?.threadSource === 'subagent' ||
|
thread?.threadSource === 'subagent' ||
|
||||||
thread?.role,
|
(!thread?.threadSource && thread?.role),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,9 +241,18 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
|
|||||||
const targetPath = normalizePath(projectPath)
|
const targetPath = normalizePath(projectPath)
|
||||||
const agents = runtime.agents.filter((agent) => belongsToProject(agent, targetPath))
|
const agents = runtime.agents.filter((agent) => belongsToProject(agent, targetPath))
|
||||||
const threadIds = new Set(agents.map((agent) => agent.id))
|
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 threads = runtime.threads.filter((thread) => threadIds.has(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))
|
||||||
const edges = runtime.edges.filter((edge) => threadIds.has(edge.fromThreadId) && threadIds.has(edge.toThreadId))
|
const edges = runtime.edges.filter((edge) =>
|
||||||
|
(threadIds.has(edge.fromThreadId) || threadIds.has(edge.toThreadId)) &&
|
||||||
|
projectThreadIds.has(edge.fromThreadId) &&
|
||||||
|
projectThreadIds.has(edge.toThreadId),
|
||||||
|
)
|
||||||
|
const handoffs = edges.map((edge) => normalizeProjectHandoff(edge, { agentByID, threadByID, source: runtime.source }))
|
||||||
|
const phaseGroups = buildPhaseGroups(agents)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...runtime,
|
...runtime,
|
||||||
@@ -246,6 +260,8 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
|
|||||||
threads,
|
threads,
|
||||||
goals,
|
goals,
|
||||||
edges,
|
edges,
|
||||||
|
handoffs,
|
||||||
|
phaseGroups,
|
||||||
isEmpty: agents.length === 0,
|
isEmpty: agents.length === 0,
|
||||||
emptyTitle: '这个项目没有运行线程',
|
emptyTitle: '这个项目没有运行线程',
|
||||||
emptyText: projectPath
|
emptyText: projectPath
|
||||||
@@ -254,6 +270,91 @@ 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'
|
||||||
|
return {
|
||||||
|
from: runtimeNodeName(edge.fromThreadId, context),
|
||||||
|
to: runtimeNodeName(edge.toThreadId, context),
|
||||||
|
summary: formatStatus(status),
|
||||||
|
status,
|
||||||
|
time: edge.createdAt || '后端事件',
|
||||||
|
source: source.label,
|
||||||
|
confidence: source.confidenceLabel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runtimeNodeName(threadID, { agentByID, threadByID }) {
|
||||||
|
if (!threadID) {
|
||||||
|
return '未知线程'
|
||||||
|
}
|
||||||
|
const agent = agentByID.get(threadID)
|
||||||
|
if (agent) {
|
||||||
|
return agent.name
|
||||||
|
}
|
||||||
|
const thread = threadByID.get(threadID)
|
||||||
|
if (thread?.threadSource === 'user' || (!thread?.agentNickname && !thread?.agentRole && !thread?.agentPath && !thread?.role)) {
|
||||||
|
return '主线程'
|
||||||
|
}
|
||||||
|
return thread?.agentNickname || thread?.agentRole || thread?.role || '未知线程'
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPhaseGroups(agents) {
|
||||||
|
const groups = new Map()
|
||||||
|
for (const agent of agents) {
|
||||||
|
const phaseName = extractPhaseName([agent.goal, ...(agent.projectHints || [])]) || '未标注阶段'
|
||||||
|
if (!groups.has(phaseName)) {
|
||||||
|
groups.set(phaseName, [])
|
||||||
|
}
|
||||||
|
groups.get(phaseName).push(agent)
|
||||||
|
}
|
||||||
|
return [...groups.entries()]
|
||||||
|
.map(([name, phaseAgents]) => {
|
||||||
|
const roles = [...new Set(phaseAgents.map((agent) => agent.role).filter(Boolean))].sort((a, b) => a.localeCompare(b, 'zh-CN'))
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
status: phaseStatus(phaseAgents),
|
||||||
|
statusZh: formatStatus(phaseStatus(phaseAgents)),
|
||||||
|
roles,
|
||||||
|
agents: phaseAgents,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => phaseSortKey(a.name) - phaseSortKey(b.name) || a.name.localeCompare(b.name, 'zh-CN'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function phaseStatus(agents) {
|
||||||
|
if (agents.some((agent) => agent.status === 'running' || agent.status === 'active')) {
|
||||||
|
return 'running'
|
||||||
|
}
|
||||||
|
if (agents.length > 0 && agents.every((agent) => agent.status === 'complete' || agent.status === 'done')) {
|
||||||
|
return 'complete'
|
||||||
|
}
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPhaseName(values) {
|
||||||
|
for (const value of values) {
|
||||||
|
const text = String(value || '')
|
||||||
|
const chinese = text.match(/阶段\s*([0-9一二三四五六七八九十]+)/)
|
||||||
|
if (chinese) {
|
||||||
|
return `阶段 ${chinese[1]}`
|
||||||
|
}
|
||||||
|
const english = text.match(/\bphase\s*([0-9]+)\b/i)
|
||||||
|
if (english) {
|
||||||
|
return `阶段 ${english[1]}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function phaseSortKey(name) {
|
||||||
|
const match = String(name).match(/阶段\s*([0-9]+)/)
|
||||||
|
if (!match) {
|
||||||
|
return Number.MAX_SAFE_INTEGER
|
||||||
|
}
|
||||||
|
return Number(match[1])
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeWorkflow(payload = {}) {
|
export function normalizeWorkflow(payload = {}) {
|
||||||
const source = normalizeSource(payload.source)
|
const source = normalizeSource(payload.source)
|
||||||
const phases = Array.isArray(payload.phases)
|
const phases = Array.isArray(payload.phases)
|
||||||
@@ -390,6 +491,14 @@ function normalizePath(path) {
|
|||||||
return String(path || '').replace(/\/+$/, '')
|
return String(path || '').replace(/\/+$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function summarizeTask(value) {
|
||||||
|
const firstLine = String(value || '').split('\n').map((line) => line.trim()).find(Boolean) || ''
|
||||||
|
if (firstLine.length <= 180) {
|
||||||
|
return firstLine
|
||||||
|
}
|
||||||
|
return `${firstLine.slice(0, 177)}...`
|
||||||
|
}
|
||||||
|
|
||||||
function belongsToProject(agent, targetPath) {
|
function belongsToProject(agent, targetPath) {
|
||||||
if (!targetPath) {
|
if (!targetPath) {
|
||||||
return false
|
return false
|
||||||
@@ -404,6 +513,28 @@ function belongsToProject(agent, targetPath) {
|
|||||||
return [agent.process, ...(agent.projectHints || [])].some((value) => containsPathToken(value, targetPath))
|
return [agent.process, ...(agent.projectHints || [])].some((value) => containsPathToken(value, targetPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function threadBelongsToProject(thread, targetPath) {
|
||||||
|
if (!targetPath) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const cwd = normalizePath(thread?.cwd)
|
||||||
|
if (cwd === targetPath) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!cwd || !targetPath.startsWith(`${cwd}/`)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return [thread?.title, thread?.preview, thread?.agentPath].some((value) => containsPathToken(value, targetPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
function threadCanJoinProjectFlow(thread, targetPath) {
|
||||||
|
if (threadBelongsToProject(thread, targetPath)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const cwd = normalizePath(thread?.cwd)
|
||||||
|
return Boolean(cwd && targetPath.startsWith(`${cwd}/`))
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSpawnStatus(status) {
|
function normalizeSpawnStatus(status) {
|
||||||
if (status === 'open') {
|
if (status === 'open') {
|
||||||
return 'running'
|
return 'running'
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ test('does not turn ordinary conversation threads into agents', () => {
|
|||||||
title: '为什么点 yoilun 这个项目出来的都是对话的信息',
|
title: '为什么点 yoilun 这个项目出来的都是对话的信息',
|
||||||
preview: '普通用户对话,不是智能体',
|
preview: '普通用户对话,不是智能体',
|
||||||
threadSource: 'user',
|
threadSource: 'user',
|
||||||
|
role: 'user',
|
||||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -208,9 +209,94 @@ test('does not turn ordinary conversation threads into agents', () => {
|
|||||||
assert.deepEqual(projectRuntime.agents.map((agent) => agent.id), ['reviewer'])
|
assert.deepEqual(projectRuntime.agents.map((agent) => agent.id), ['reviewer'])
|
||||||
assert.equal(projectRuntime.agents[0].name, 'Lorentz')
|
assert.equal(projectRuntime.agents[0].name, 'Lorentz')
|
||||||
assert.equal(projectRuntime.agents[0].process, '子智能体线程')
|
assert.equal(projectRuntime.agents[0].process, '子智能体线程')
|
||||||
|
assert.equal(projectRuntime.agents[0].goal, '审查 /Users/yoilun 项目')
|
||||||
|
assert.equal(projectRuntime.agents[0].goalSource, '线程任务摘要')
|
||||||
assert.doesNotMatch(projectRuntime.agents[0].process, /审查 \/Users\/yoilun/)
|
assert.doesNotMatch(projectRuntime.agents[0].process, /审查 \/Users\/yoilun/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('builds project handoffs and phase groups from runtime edges and agent tasks', () => {
|
||||||
|
const runtime = normalizeRuntime({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'main',
|
||||||
|
cwd: '/repo/a',
|
||||||
|
title: '主控对话',
|
||||||
|
threadSource: 'user',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'coder',
|
||||||
|
cwd: '/repo/a',
|
||||||
|
title: '阶段 6:实现安全写回',
|
||||||
|
agentNickname: 'Averroes',
|
||||||
|
agentRole: '后端架构师',
|
||||||
|
threadSource: 'subagent',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reviewer',
|
||||||
|
cwd: '/repo/a',
|
||||||
|
title: '阶段 6:审查安全写回',
|
||||||
|
agentNickname: 'Rawls',
|
||||||
|
agentRole: '代码审查员',
|
||||||
|
threadSource: 'subagent',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pm',
|
||||||
|
cwd: '/repo/a',
|
||||||
|
title: '阶段 7:整理最终文档',
|
||||||
|
agentNickname: 'Zeno',
|
||||||
|
agentRole: '高级项目经理',
|
||||||
|
threadSource: 'subagent',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'other-main',
|
||||||
|
cwd: '/repo/b',
|
||||||
|
title: '其他项目主控对话',
|
||||||
|
threadSource: 'user',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{ fromThreadId: 'main', toThreadId: 'coder', reason: 'closed' },
|
||||||
|
{ fromThreadId: 'coder', toThreadId: 'reviewer', reason: 'open' },
|
||||||
|
{ fromThreadId: 'main', toThreadId: 'pm', reason: 'closed' },
|
||||||
|
{ fromThreadId: 'other-main', toThreadId: 'coder', reason: 'open' },
|
||||||
|
],
|
||||||
|
source: { kind: 'sqlite_readonly', confidence: 'high' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const projectRuntime = filterRuntimeByProject(runtime, '/repo/a')
|
||||||
|
|
||||||
|
assert.deepEqual(projectRuntime.handoffs.map((handoff) => handoff.summary), [
|
||||||
|
'已完成',
|
||||||
|
'运行中',
|
||||||
|
'已完成',
|
||||||
|
])
|
||||||
|
assert.deepEqual(projectRuntime.handoffs.map((handoff) => `${handoff.from} -> ${handoff.to}`), [
|
||||||
|
'主线程 -> Averroes',
|
||||||
|
'Averroes -> Rawls',
|
||||||
|
'主线程 -> Zeno',
|
||||||
|
])
|
||||||
|
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'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('project view does not include sample runtime fallbacks', async () => {
|
||||||
|
const thisFile = fileURLToPath(import.meta.url)
|
||||||
|
const viewPath = new URL('../views/ProjectView.vue', `file://${thisFile}`)
|
||||||
|
const source = await readFile(viewPath, 'utf8')
|
||||||
|
|
||||||
|
assert.doesNotMatch(source, /sampleProjects/)
|
||||||
|
assert.doesNotMatch(source, /sampleAgentMatrix/)
|
||||||
|
assert.match(source, /goalSource/)
|
||||||
|
})
|
||||||
|
|
||||||
test('keeps project threads when Codex cwd is an ancestor but metadata names the project path', () => {
|
test('keeps project threads when Codex cwd is an ancestor but metadata names the project path', () => {
|
||||||
const runtime = normalizeRuntime({
|
const runtime = normalizeRuntime({
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@@ -260,6 +260,23 @@ button {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-flow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-section {
|
||||||
|
min-width: 0;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-heading {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.matrix-row {
|
.matrix-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(180px, 1.2fr) minmax(220px, 1fr) minmax(170px, 1fr) minmax(150px, 0.8fr) minmax(130px, 0.7fr);
|
grid-template-columns: minmax(180px, 1.2fr) minmax(220px, 1fr) minmax(170px, 1fr) minmax(150px, 0.8fr) minmax(130px, 0.7fr);
|
||||||
@@ -374,6 +391,13 @@ button {
|
|||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-block span {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-grid {
|
.detail-grid {
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -448,6 +472,14 @@ button {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.phase-list.compact li {
|
||||||
|
grid-template-columns: 18px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase-list.compact .status-badge {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.phase-list li {
|
.phase-list li {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 18px minmax(0, 1fr) auto;
|
grid-template-columns: 18px minmax(0, 1fr) auto;
|
||||||
@@ -871,6 +903,7 @@ button {
|
|||||||
.panel-heading.horizontal,
|
.panel-heading.horizontal,
|
||||||
.draft-header,
|
.draft-header,
|
||||||
.setting-row,
|
.setting-row,
|
||||||
|
.project-flow,
|
||||||
.form-grid {
|
.form-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import HandoffTimeline from '../components/HandoffTimeline.vue'
|
||||||
import StatusBadge from '../components/StatusBadge.vue'
|
import StatusBadge from '../components/StatusBadge.vue'
|
||||||
import { apiClient } from '../api/client'
|
import { apiClient } from '../api/client'
|
||||||
import { filterRuntimeByProject, normalizeProjects, normalizeRuntime } from '../api/normalizers'
|
import { filterRuntimeByProject, normalizeProjects, normalizeRuntime } from '../api/normalizers'
|
||||||
import { agentMatrix as sampleAgentMatrix, projects as sampleProjects } from '../data'
|
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
@@ -80,16 +80,6 @@ async function loadReadonlyData() {
|
|||||||
</dl>
|
</dl>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="error" class="sample-fallback">
|
|
||||||
<p class="eyebrow">示例 / 等待连接</p>
|
|
||||||
<article v-for="project in sampleProjects" :key="project.id" class="project-item">
|
|
||||||
<div>
|
|
||||||
<strong>示例:{{ project.name }}</strong>
|
|
||||||
<span>{{ project.path }}</span>
|
|
||||||
</div>
|
|
||||||
<StatusBadge :label="project.statusZh" :status="project.status" source="示例数据" confidence="低" />
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section class="panel matrix-panel" aria-label="项目状态矩阵">
|
<section class="panel matrix-panel" aria-label="项目状态矩阵">
|
||||||
@@ -104,7 +94,7 @@ async function loadReadonlyData() {
|
|||||||
<div v-if="loading" class="load-state">加载中</div>
|
<div v-if="loading" class="load-state">加载中</div>
|
||||||
<div v-else-if="error" class="empty-state compact error-state">
|
<div v-else-if="error" class="empty-state compact error-state">
|
||||||
<strong>连接失败</strong>
|
<strong>连接失败</strong>
|
||||||
<p>无法读取 `/api/runtime/threads`,下面仅显示明确标注的示例状态。</p>
|
<p>无法读取 `/api/runtime/threads`,当前不显示示例运行状态。</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="projectRuntime.isEmpty" class="empty-state compact">
|
<div v-else-if="projectRuntime.isEmpty" class="empty-state compact">
|
||||||
<strong>{{ projectRuntime.emptyTitle }}</strong>
|
<strong>{{ projectRuntime.emptyTitle }}</strong>
|
||||||
@@ -128,33 +118,57 @@ async function loadReadonlyData() {
|
|||||||
<span role="cell">
|
<span role="cell">
|
||||||
<StatusBadge :label="agent.statusZh" :status="agent.status" :source="agent.source" :confidence="agent.confidence" />
|
<StatusBadge :label="agent.statusZh" :status="agent.status" :source="agent.source" :confidence="agent.confidence" />
|
||||||
</span>
|
</span>
|
||||||
<span role="cell">{{ agent.goal }}</span>
|
<span role="cell">
|
||||||
|
{{ agent.goal }}
|
||||||
|
<small>{{ agent.goalSource }}</small>
|
||||||
|
</span>
|
||||||
<span role="cell">{{ agent.process }}</span>
|
<span role="cell">{{ agent.process }}</span>
|
||||||
<span role="cell">{{ agent.lastActivity }}</span>
|
<span role="cell">{{ agent.lastActivity }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="sample-fallback matrix-table" role="table" aria-label="示例智能体状态矩阵">
|
<div v-if="!loading && !error && !projectRuntime.isEmpty" class="project-flow">
|
||||||
<div class="matrix-row head" role="row">
|
<div class="flow-section">
|
||||||
<span role="columnheader">示例智能体</span>
|
<div class="panel-heading horizontal compact-heading">
|
||||||
<span role="columnheader">状态</span>
|
<div>
|
||||||
<span role="columnheader">目标</span>
|
<p class="eyebrow">阶段进度</p>
|
||||||
<span role="columnheader">进程</span>
|
<h3>项目内智能体阶段</h3>
|
||||||
<span role="columnheader">最近活动</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-for="agent in sampleAgentMatrix" :key="agent.id" class="matrix-row" role="row">
|
<span class="read-only-chip">{{ projectRuntime.phaseGroups.length }} 个阶段</span>
|
||||||
<span role="cell">
|
</div>
|
||||||
<strong>示例:{{ agent.name }}</strong>
|
<ol v-if="projectRuntime.phaseGroups.length > 0" class="phase-list compact">
|
||||||
<small>{{ agent.role }}</small>
|
<li v-for="phase in projectRuntime.phaseGroups" :key="phase.name" :data-status="phase.status">
|
||||||
</span>
|
<div class="phase-dot" aria-hidden="true"></div>
|
||||||
<span role="cell">
|
<div>
|
||||||
<StatusBadge :label="agent.statusZh" :status="agent.status" source="示例数据" confidence="低" />
|
<strong>{{ phase.name }}</strong>
|
||||||
</span>
|
<p>{{ phase.roles.join('、') || '未记录角色分类' }}</p>
|
||||||
<span role="cell">{{ agent.goal }}</span>
|
<span>{{ phase.agents.map((agent) => agent.name).join('、') }}</span>
|
||||||
<span role="cell">{{ agent.process }}</span>
|
</div>
|
||||||
<span role="cell">{{ agent.lastActivity }}</span>
|
<StatusBadge :label="phase.statusZh" :status="phase.status" source="SQLite 表" confidence="高" />
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<div v-else class="empty-state compact">
|
||||||
|
<strong>没有阶段归属</strong>
|
||||||
|
<p>当前项目智能体线程没有可解析的阶段标记。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flow-section">
|
||||||
|
<div class="panel-heading horizontal compact-heading">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">交互方向</p>
|
||||||
|
<h3>项目内智能体流程记录</h3>
|
||||||
|
</div>
|
||||||
|
<span class="read-only-chip">{{ projectRuntime.handoffs.length }} 条交接</span>
|
||||||
|
</div>
|
||||||
|
<HandoffTimeline v-if="projectRuntime.handoffs.length > 0" :items="projectRuntime.handoffs" />
|
||||||
|
<div v-else class="empty-state compact">
|
||||||
|
<strong>没有交接记录</strong>
|
||||||
|
<p>当前项目没有匹配到主线程与智能体之间的交接边。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<aside class="panel detail-panel" aria-label="详情面板">
|
<aside class="panel detail-panel" aria-label="详情面板">
|
||||||
@@ -171,10 +185,11 @@ async function loadReadonlyData() {
|
|||||||
<div class="detail-block">
|
<div class="detail-block">
|
||||||
<h3>角色摘要</h3>
|
<h3>角色摘要</h3>
|
||||||
<p>{{ selectedAgent?.goal || '选择项目或等待运行线程后显示真实目标。' }}</p>
|
<p>{{ selectedAgent?.goal || '选择项目或等待运行线程后显示真实目标。' }}</p>
|
||||||
|
<span v-if="selectedAgent?.goalSource">{{ selectedAgent.goalSource }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-block">
|
<div class="detail-block">
|
||||||
<h3>证据说明</h3>
|
<h3>证据说明</h3>
|
||||||
<p>数据来自只读接口。连接失败时只显示明确标注的示例,不会把示例伪装成真实状态。</p>
|
<p>数据来自只读接口。连接失败时保持错误状态,不使用示例数据伪装真实状态。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<span>项目数</span><strong>{{ projectState.projects.length }}</strong>
|
<span>项目数</span><strong>{{ projectState.projects.length }}</strong>
|
||||||
|
|||||||
Reference in New Issue
Block a user