@ifrankwang/openspec-agents 0.146.1 → 0.147.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp-server/cli.mjs +49 -163
- package/README.md +122 -59
- package/package.json +1 -1
- package/src/adapters/mcp-common/index.ts +3 -5
- package/src/adapters/opencode/index.ts +2 -2
- package/src/adapters/plugin-common/index.ts +0 -2
- package/src/core/index.ts +0 -3
- package/src/core/provider.ts +1 -3
- package/.mcp-server/dashboard/index.html +0 -295
- package/assets/dashboard/index.html +0 -295
- package/src/adapters/opencode/dashboard.ts +0 -64
- package/src/core/dashboard.ts +0 -101
- package/src/dashboard/page.ts +0 -19
package/src/core/provider.ts
CHANGED
|
@@ -46,14 +46,12 @@ export interface RuntimeTool {
|
|
|
46
46
|
/** 工具注册表:工具名 → 定义。 */
|
|
47
47
|
export type ToolRegistry = Record<string, RuntimeTool>
|
|
48
48
|
|
|
49
|
-
/** 运行时配置注入:agent/skill
|
|
49
|
+
/** 运行时配置注入:agent/skill 注入指令。 */
|
|
50
50
|
export interface IRuntimeConfig {
|
|
51
51
|
/** 分发的 agent 定义根目录(各适配器按自身注入格式消费)。 */
|
|
52
52
|
agentsDir?: string
|
|
53
53
|
/** 分发的 skill 根目录列表(各适配器按自身发现机制注册)。 */
|
|
54
54
|
skillsDirs?: string[]
|
|
55
|
-
/** 可选编排进度看板。 */
|
|
56
|
-
dashboard?: { port?: number; hostname?: string }
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
/**
|
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>编排进度看板 · 5 列看板</title>
|
|
7
|
-
<style>
|
|
8
|
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
|
9
|
-
:root{
|
|
10
|
-
--bg:#f8fafc;--surface:#fff;--surface-2:#f1f5f9;
|
|
11
|
-
--ink:#0f172a;--muted:#475569;--quiet:#94a3b8;
|
|
12
|
-
--line:#e2e8f0;--line-2:#cbd5e1;
|
|
13
|
-
--blue:#2563eb;--blue-soft:#eff6ff;
|
|
14
|
-
--green:#16a34a;--green-soft:#f0fdf4;
|
|
15
|
-
--amber:#d97706;--amber-soft:#fffbeb;
|
|
16
|
-
--red:#dc2626;--red-soft:#fef2f2;
|
|
17
|
-
--violet:#7c3aed;--violet-soft:#f5f3ff;
|
|
18
|
-
--teal:#0d9488;--teal-soft:#f0fdfa;
|
|
19
|
-
--radius:12px;--radius-sm:8px;--pill:999px;
|
|
20
|
-
--shadow:0 1px 2px rgba(15,23,42,.04),0 6px 16px rgba(15,23,42,.04);
|
|
21
|
-
--sans:'Inter',system-ui,sans-serif;
|
|
22
|
-
--mono:'JetBrains Mono',ui-monospace,monospace;
|
|
23
|
-
}
|
|
24
|
-
*{box-sizing:border-box}html{font-size:15px}body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.55;-webkit-font-smoothing:antialiased}
|
|
25
|
-
button{font:inherit;cursor:pointer}button:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
|
|
26
|
-
.shell{max-width:1440px;margin:0 auto;padding:32px 28px 80px}
|
|
27
|
-
/* header */
|
|
28
|
-
.topbar{display:flex;justify-content:space-between;align-items:flex-start;gap:24px;padding-bottom:26px;border-bottom:1px solid var(--line)}
|
|
29
|
-
.brand{display:flex;flex-direction:column;gap:8px}
|
|
30
|
-
.badge{display:inline-flex;align-items:center;gap:8px;font:600 .68rem/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
|
31
|
-
.badge::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px var(--green-soft)}
|
|
32
|
-
h1{margin:0;font-size:1.6rem;font-weight:700;letter-spacing:-.03em}
|
|
33
|
-
.meta{display:flex;gap:28px;text-align:right;font-size:.8rem;color:var(--muted)}
|
|
34
|
-
.meta span{display:flex;flex-direction:column;gap:5px}.meta strong{color:var(--ink);font-weight:600}
|
|
35
|
-
/* 5 列看板 */
|
|
36
|
-
.board{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;align-items:start;padding-top:24px}
|
|
37
|
-
.col{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden;display:flex;flex-direction:column}
|
|
38
|
-
.col-head{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:14px 16px;border-bottom:1px solid var(--line)}
|
|
39
|
-
.col-head h3{margin:0;font-size:.92rem;font-weight:700}
|
|
40
|
-
.col-count{font:600 .78rem/1 var(--mono);padding:4px 10px;background:var(--surface-2);border-radius:var(--pill);color:var(--muted)}
|
|
41
|
-
.col-body{max-height:calc(100vh - 280px);overflow:auto;padding:12px;display:flex;flex-direction:column;gap:12px}
|
|
42
|
-
.board-empty{grid-column:1/-1;padding:48px 0;text-align:center;color:var(--quiet);font-size:.9rem}
|
|
43
|
-
/* 卡片 */
|
|
44
|
-
.card{position:relative;background:var(--surface);border:1px solid var(--line);border-left-width:3px;border-radius:var(--radius-sm);box-shadow:0 1px 2px rgba(15,23,42,.04);padding:12px 14px;display:flex;flex-direction:column;gap:8px}
|
|
45
|
-
.card.task{border-left-color:var(--blue)}
|
|
46
|
-
.card.issue{border-left-color:var(--violet)}
|
|
47
|
-
.card.suspended{border-style:dashed;opacity:.94}
|
|
48
|
-
.card-head{display:flex;align-items:center;gap:8px}
|
|
49
|
-
.type-tag{flex:0 0 auto;width:22px;height:22px;border-radius:6px;display:inline-flex;align-items:center;justify-content:center;font:700 .7rem/1 var(--mono);color:#fff}
|
|
50
|
-
.type-tag.task{background:var(--blue)}.type-tag.issue{background:var(--violet)}
|
|
51
|
-
.src-icon{flex:0 0 auto;font-size:.85rem;color:var(--quiet);line-height:1}
|
|
52
|
-
.card-title{margin:0;font-size:.9rem;font-weight:700;line-height:1.4;flex:1}
|
|
53
|
-
.card-desc{font-size:.78rem;color:var(--muted);line-height:1.5}
|
|
54
|
-
.card-desc.collapsed{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
55
|
-
.desc-toggle{margin-top:-4px;font-size:.72rem;color:var(--blue);background:none;border:none;padding:0;cursor:pointer;text-align:left}
|
|
56
|
-
.card-labels{display:flex;flex-wrap:wrap;gap:6px}
|
|
57
|
-
.chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:var(--pill);font:500 .68rem/1.4 var(--mono);background:var(--surface-2);color:var(--muted);border:1px solid var(--line)}
|
|
58
|
-
.card-children{display:flex;flex-direction:column;gap:6px;border-top:1px dashed var(--line);padding-top:8px}
|
|
59
|
-
.child-row{display:flex;align-items:center;gap:6px;font-size:.76rem;color:var(--muted)}
|
|
60
|
-
.child-type{flex:0 0 auto;width:16px;height:16px;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;font:700 .58rem/1 var(--mono);color:#fff}
|
|
61
|
-
.child-type.task{background:var(--blue)}.child-type.issue{background:var(--violet)}
|
|
62
|
-
.child-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
63
|
-
.child-phase{flex:0 0 auto;padding:1px 6px;border-radius:var(--pill);font:600 .62rem/1.3 var(--mono);background:var(--surface-2);color:var(--muted);border:1px solid var(--line)}
|
|
64
|
-
.card-foot{display:flex;justify-content:space-between;align-items:flex-end;gap:8px;margin-top:auto;border-top:1px solid var(--line);padding-top:8px}
|
|
65
|
-
.card-verdicts{display:flex;flex-wrap:wrap;gap:6px}
|
|
66
|
-
.verdict{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border-radius:var(--pill);font:600 .68rem/1.4 var(--mono);background:var(--surface-2);color:var(--muted)}
|
|
67
|
-
.verdict.passed{background:var(--green-soft);color:var(--green)}
|
|
68
|
-
.verdict.failed{background:var(--red-soft);color:var(--red)}
|
|
69
|
-
.verdict.pending{background:var(--surface-2);color:var(--quiet)}
|
|
70
|
-
.card-meta{display:flex;align-items:center;gap:6px;flex-shrink:0}
|
|
71
|
-
.step-tag{font:.68rem/1.4 var(--mono);color:var(--quiet)}
|
|
72
|
-
.suspend-tag{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:var(--pill);font:600 .7rem/1.4 var(--mono);background:var(--amber-soft);color:var(--amber);border:1px solid var(--amber);cursor:pointer}
|
|
73
|
-
.suspend-reason{margin-top:4px;font-size:.72rem;color:var(--amber);background:var(--amber-soft);border-radius:var(--radius-sm);padding:6px 8px}
|
|
74
|
-
/* severity 色条 */
|
|
75
|
-
.sev-bar{position:absolute;top:0;left:0;right:0;height:4px;border-radius:var(--radius-sm) var(--radius-sm) 0 0}
|
|
76
|
-
.sev-bar.critical{background:var(--red)}.sev-bar.high{background:var(--amber)}.sev-bar.medium{background:var(--violet)}.sev-bar.low{background:var(--teal)}.sev-bar.info{background:var(--line-2)}
|
|
77
|
-
/* WorkItem 详情弹窗 */
|
|
78
|
-
.modal-open{overflow:hidden}
|
|
79
|
-
.modal-overlay{position:fixed;inset:0;z-index:100;background:rgba(15,23,42,.45);display:flex;align-items:flex-start;justify-content:center;padding:48px 16px;overflow:auto}
|
|
80
|
-
.modal-overlay[hidden]{display:none}
|
|
81
|
-
.modal-dialog{background:var(--surface);border-radius:var(--radius);box-shadow:0 20px 50px rgba(15,23,42,.2);max-width:640px;width:100%;max-height:calc(100vh - 96px);display:flex;flex-direction:column}
|
|
82
|
-
.modal-head{display:flex;align-items:center;gap:12px;padding:14px 18px;border-bottom:1px solid var(--line)}
|
|
83
|
-
.modal-title{flex:1;font-size:.92rem;font-weight:700}
|
|
84
|
-
.modal-close{background:none;border:none;font-size:1.2rem;color:var(--muted);line-height:1;padding:4px 8px;border-radius:6px}
|
|
85
|
-
.modal-close:hover{background:var(--surface-2);color:var(--ink)}
|
|
86
|
-
.modal-back{background:none;border:1px solid var(--line);border-radius:var(--radius-sm);font-size:.78rem;color:var(--muted);padding:4px 10px}
|
|
87
|
-
.modal-back:hover{background:var(--surface-2);color:var(--ink)}
|
|
88
|
-
.modal-back[hidden]{display:none}
|
|
89
|
-
.modal-body{padding:18px;overflow:auto;display:flex;flex-direction:column;gap:14px}
|
|
90
|
-
.detail-title{margin:0;font-size:1.1rem;font-weight:700}
|
|
91
|
-
.detail-block{display:flex;flex-direction:column;gap:8px}
|
|
92
|
-
.detail-block h5{margin:0;font-size:.68rem;letter-spacing:.06em;text-transform:uppercase;color:var(--quiet)}
|
|
93
|
-
.meta-row{display:flex;gap:10px;font-size:.8rem;padding:4px 0;border-bottom:1px dashed var(--line)}
|
|
94
|
-
.meta-key{flex:0 0 96px;color:var(--quiet);font:500 .76rem/1.4 var(--mono)}
|
|
95
|
-
.meta-val{flex:1;color:var(--ink);word-break:break-word}
|
|
96
|
-
.detail-desc{margin:0;font-size:.82rem;color:var(--muted);white-space:pre-wrap}
|
|
97
|
-
.detail-verdicts{display:flex;flex-wrap:wrap;gap:6px}
|
|
98
|
-
.detail-verdict{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border-radius:var(--pill);font:600 .68rem/1.4 var(--mono);background:var(--surface-2);color:var(--muted)}
|
|
99
|
-
.detail-verdict.passed{background:var(--green-soft);color:var(--green)}
|
|
100
|
-
.detail-verdict.failed{background:var(--red-soft);color:var(--red)}
|
|
101
|
-
.detail-verdict.pending{background:var(--surface-2);color:var(--quiet)}
|
|
102
|
-
.detail-children{display:flex;flex-direction:column;gap:6px}
|
|
103
|
-
.detail-child{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid var(--line);border-radius:var(--radius-sm);font-size:.8rem;cursor:pointer}
|
|
104
|
-
.detail-child:hover{background:var(--surface-2);border-color:var(--line-2)}
|
|
105
|
-
@media(max-width:1024px){.board{grid-template-columns:repeat(2,1fr)}}
|
|
106
|
-
@media(max-width:640px){.board{grid-template-columns:1fr}.topbar{flex-direction:column;gap:14px}.meta{text-align:left}}
|
|
107
|
-
</style>
|
|
108
|
-
</head>
|
|
109
|
-
<body>
|
|
110
|
-
<div class="shell">
|
|
111
|
-
<header class="topbar">
|
|
112
|
-
<div class="brand">
|
|
113
|
-
<div class="badge" id="connBadge"><span>实时同步</span></div>
|
|
114
|
-
<h1>编排进度看板</h1>
|
|
115
|
-
</div>
|
|
116
|
-
<div class="meta">
|
|
117
|
-
<span>变更编号<strong id="changeId">-</strong></span>
|
|
118
|
-
<span>基准分支<strong id="baseBranch">-</strong></span>
|
|
119
|
-
<span>最后更新<strong id="updatedAt">-</strong></span>
|
|
120
|
-
</div>
|
|
121
|
-
</header>
|
|
122
|
-
<main id="dashboard"></main>
|
|
123
|
-
</div>
|
|
124
|
-
<div class="modal-overlay" id="wiModal" hidden>
|
|
125
|
-
<div class="modal-dialog" role="dialog" aria-modal="true" aria-label="工作项详情">
|
|
126
|
-
<header class="modal-head">
|
|
127
|
-
<button class="modal-back" id="wiBack" hidden>← 返回</button>
|
|
128
|
-
<span class="modal-title">工作项详情</span>
|
|
129
|
-
<button class="modal-close" id="wiClose" aria-label="关闭">×</button>
|
|
130
|
-
</header>
|
|
131
|
-
<div class="modal-body" id="wiBody"></div>
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
<script>
|
|
135
|
-
const COLUMNS=[['todo','待办'],['in_progress','处理中'],['review','审核中'],['done','完成'],['cancelled','已取消']];
|
|
136
|
-
const SOURCE_ICONS={openspec:'◈',ado:'◆',custom:'✚'};
|
|
137
|
-
const VERDICT_ICONS={passed:'✓',failed:'✕',pending:'○'};
|
|
138
|
-
const AGENT_SHORT={'openspec-architect':'architect','openspec-developer':'dev','openspec-reviewer-tool':'tool','openspec-reviewer-task':'task','openspec-reviewer-style':'style','openspec-reviewer-architecture':'arch','openspec-reviewer-performance':'perf','openspec-reviewer-security':'sec','openspec-reviewer-maintainability':'maint'};
|
|
139
|
-
let dashboardState={};
|
|
140
|
-
function esc(v){return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>','\'':''','"':'"'}[c]))}
|
|
141
|
-
function cls(v){return String(v??'').replace(/[^a-zA-Z0-9_-]/g,'_')}
|
|
142
|
-
function shortAgent(key){if(AGENT_SHORT[key])return AGENT_SHORT[key];const m=/^openspec-reviewer-(.+)$/.exec(key)||/^openspec-(.+)$/.exec(key);return m?m[1]:(key||'')}
|
|
143
|
-
function descBlock(text){if(!text)return '';if(text.length<90)return `<div class="card-desc">${esc(text)}</div>`;return `<div class="card-desc collapsed">${esc(text)}</div><button class="desc-toggle">展开</button>`}
|
|
144
|
-
function verdictDot(v){return `<span class="verdict ${cls(v.verdict)}" title="${esc(v.stepId)}">${esc(shortAgent(v.agentKey))}:${VERDICT_ICONS[v.verdict]||'○'}</span>`}
|
|
145
|
-
function renderCard(c){
|
|
146
|
-
const isIssue=c.type==='issue';
|
|
147
|
-
const typeTag=`<span class="type-tag ${isIssue?'issue':'task'}">${isIssue?'I':'T'}</span>`;
|
|
148
|
-
const srcIcon=`<span class="src-icon" title="${esc(c.source||'')}">${SOURCE_ICONS[c.source]||'●'}</span>`;
|
|
149
|
-
const sevBar=isIssue&&c.severity?`<span class="sev-bar ${cls(String(c.severity).toLowerCase())}" title="${esc(c.severity)}"></span>`:'';
|
|
150
|
-
const labels=(c.labels||[]).map(l=>`<span class="chip">${esc(l)}</span>`).join('');
|
|
151
|
-
const step=c.currentStep?`<span class="step-tag">${esc(c.currentStep)}</span>`:'';
|
|
152
|
-
const suspTag=c.suspended?'<span class="suspend-tag" role="button" tabindex="0">⏸ 暂停</span>':'';
|
|
153
|
-
const suspReason=c.suspended?`<div class="suspend-reason" hidden>${esc(c.suspendReason||'未提供暂停原因')}</div>`:'';
|
|
154
|
-
const verdicts=(c.agentVerdicts||[]).map(verdictDot).join('');
|
|
155
|
-
const children=(c.children||[]).map(ch=>`<div class="child-row"><span class="child-type ${cls(ch.type)}">${ch.type==='issue'?'I':'T'}</span><span class="child-title">${esc(ch.title)}</span>${ch.type==='task'?`<span class="child-phase">${esc(ch.phase)}</span>`:''}</div>`).join('');
|
|
156
|
-
return `<article class="card ${isIssue?'issue':'task'}${c.suspended?' suspended':''}" data-id="${esc(c.id)}">${sevBar}
|
|
157
|
-
<div class="card-head">${typeTag}${srcIcon}<h4 class="card-title">${esc(c.title)}</h4></div>
|
|
158
|
-
${descBlock(c.description)}
|
|
159
|
-
${labels?`<div class="card-labels">${labels}</div>`:''}
|
|
160
|
-
${children?`<div class="card-children">${children}</div>`:''}
|
|
161
|
-
<div class="card-foot">${verdicts?`<div class="card-verdicts">${verdicts}</div>`:'<span></span>'}<div class="card-meta">${step}${suspTag}</div></div>
|
|
162
|
-
${suspReason}
|
|
163
|
-
</article>`;
|
|
164
|
-
}
|
|
165
|
-
function renderBoard(){
|
|
166
|
-
const cards=dashboardState.workItemCards||[];
|
|
167
|
-
if(!cards.length)return `<section class="board"><div class="board-empty">暂无工作项数据(旧格式状态或未初始化)</div></section>`;
|
|
168
|
-
return `<section class="board">${COLUMNS.map(([key,label])=>{
|
|
169
|
-
const items=cards.filter(c=>c.phase===key);
|
|
170
|
-
return `<div class="col" data-col="${key}"><header class="col-head"><h3>${label}</h3><span class="col-count">${items.length}</span></header><div class="col-body">${items.map(renderCard).join('')}</div></div>`;
|
|
171
|
-
}).join('')}</section>`;
|
|
172
|
-
}
|
|
173
|
-
function render(){
|
|
174
|
-
document.getElementById('changeId').textContent=dashboardState.changeId||'-';
|
|
175
|
-
document.getElementById('baseBranch').textContent=dashboardState.baseBranch||'-';
|
|
176
|
-
document.getElementById('updatedAt').textContent=dashboardState.updatedAt?new Date(dashboardState.updatedAt).toLocaleString():'-';
|
|
177
|
-
document.getElementById('dashboard').innerHTML=renderBoard();
|
|
178
|
-
document.querySelectorAll('.desc-toggle').forEach(btn=>btn.addEventListener('click',e=>{
|
|
179
|
-
const d=e.target.previousElementSibling;
|
|
180
|
-
if(d&&d.classList.contains('card-desc')){d.classList.toggle('collapsed');e.target.textContent=d.classList.contains('collapsed')?'展开':'收起'}
|
|
181
|
-
}));
|
|
182
|
-
document.querySelectorAll('.suspend-tag').forEach(tag=>tag.addEventListener('click',e=>{
|
|
183
|
-
const card=e.target.closest('.card');
|
|
184
|
-
const reason=card&&card.querySelector('.suspend-reason');
|
|
185
|
-
if(reason)reason.hidden=!reason.hidden;
|
|
186
|
-
}));
|
|
187
|
-
if(modalState.id)refreshModal();
|
|
188
|
-
}
|
|
189
|
-
/* WorkItem 详情弹窗 */
|
|
190
|
-
let modalState={id:null,back:[]};
|
|
191
|
-
function findWorkItem(items,id){
|
|
192
|
-
for(const it of items||[]){
|
|
193
|
-
if(it.id===id)return it;
|
|
194
|
-
if(it.children&&it.children.length){
|
|
195
|
-
const found=findWorkItem(it.children,id);
|
|
196
|
-
if(found)return found;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
function renderTags(tags){
|
|
202
|
-
const verdicts=Object.entries(tags||{}).map(([key,verdict])=>{
|
|
203
|
-
const sep=key.indexOf(':');
|
|
204
|
-
const stepId=sep===-1?key:key.slice(0,sep);
|
|
205
|
-
const agentKey=sep===-1?key:key.slice(sep+1);
|
|
206
|
-
return `<span class="detail-verdict ${cls(verdict)}" title="${esc(key)}">${esc(shortAgent(agentKey))}:${VERDICT_ICONS[verdict]||'○'}</span>`;
|
|
207
|
-
}).join('');
|
|
208
|
-
return verdicts?`<div class="detail-block"><h5>Agent 裁决</h5><div class="detail-verdicts">${verdicts}</div></div>`:'';
|
|
209
|
-
}
|
|
210
|
-
function renderWriteback(wb){
|
|
211
|
-
if(!wb)return '';
|
|
212
|
-
const rows=Object.entries(wb).filter(([k])=>!k.startsWith('_')).map(([k,v])=>{
|
|
213
|
-
const val=typeof v==='object'?JSON.stringify(v):String(v);
|
|
214
|
-
return `<div class="meta-row"><span class="meta-key">${esc(k)}</span><span class="meta-val">${esc(val)}</span></div>`;
|
|
215
|
-
}).join('');
|
|
216
|
-
return rows?`<div class="detail-block"><h5>写回</h5>${rows}</div>`:'';
|
|
217
|
-
}
|
|
218
|
-
function renderMeta(metadata){
|
|
219
|
-
const rows=Object.entries(metadata||{}).filter(([k])=>!k.startsWith('_')).map(([k,v])=>{
|
|
220
|
-
const val=typeof v==='object'?JSON.stringify(v):String(v);
|
|
221
|
-
return `<div class="meta-row"><span class="meta-key">${esc(k)}</span><span class="meta-val">${esc(val)}</span></div>`;
|
|
222
|
-
}).join('');
|
|
223
|
-
return rows?`<div class="detail-block"><h5>元数据</h5>${rows}</div>`:'';
|
|
224
|
-
}
|
|
225
|
-
function renderChildren(children){
|
|
226
|
-
if(!children||!children.length)return '';
|
|
227
|
-
const rows=(children||[]).map(ch=>`<div class="detail-child" data-id="${esc(ch.id)}" role="button" tabindex="0">
|
|
228
|
-
<span class="child-type ${cls(ch.type)}">${ch.type==='issue'?'I':'T'}</span>
|
|
229
|
-
<span class="child-title">${esc(ch.title)}</span>
|
|
230
|
-
<span class="child-phase">${esc(ch.phase)}</span>
|
|
231
|
-
</div>`).join('');
|
|
232
|
-
return `<div class="detail-block"><h5>Children</h5><div class="detail-children">${rows}</div></div>`;
|
|
233
|
-
}
|
|
234
|
-
function renderDetail(item){
|
|
235
|
-
const rows=[['类型',item.type],['来源',item.source],['ID',item.id]];
|
|
236
|
-
if(item.externalId)rows.push(['外部ID',item.externalId]);
|
|
237
|
-
rows.push(['阶段',item.phase]);
|
|
238
|
-
if(item.severity)rows.push(['严重级别',item.severity]);
|
|
239
|
-
rows.push(['当前步骤',item.currentStep||'-']);
|
|
240
|
-
const kv=rows.map(([k,v])=>`<div class="meta-row"><span class="meta-key">${esc(k)}</span><span class="meta-val">${esc(v)}</span></div>`).join('');
|
|
241
|
-
return `<h3 class="detail-title">${esc(item.title)}</h3>
|
|
242
|
-
${item.suspended?`<div class="suspend-reason">⏸ 暂停:${esc(typeof (item.metadata||{})['suspend_reason']==='string'?(item.metadata||{})['suspend_reason']:'未提供暂停原因')}</div>`:''}
|
|
243
|
-
${item.description?`<div class="detail-block"><h5>描述</h5><p class="detail-desc">${esc(item.description)}</p></div>`:''}
|
|
244
|
-
<div class="detail-block"><h5>字段</h5>${kv}</div>
|
|
245
|
-
${item.labels&&item.labels.length?`<div class="detail-block"><h5>标签</h5><div class="card-labels">${item.labels.map(l=>`<span class="chip">${esc(l)}</span>`).join('')}</div></div>`:''}
|
|
246
|
-
${renderTags(item.tags)}
|
|
247
|
-
${renderWriteback(item.writeback)}
|
|
248
|
-
${renderMeta(item.metadata)}
|
|
249
|
-
${renderChildren(item.children)}`;
|
|
250
|
-
}
|
|
251
|
-
function refreshModal(){
|
|
252
|
-
const modal=document.getElementById('wiModal');
|
|
253
|
-
const item=findWorkItem(dashboardState.workItems||[],modalState.id);
|
|
254
|
-
if(!item){closeDetail();return;}
|
|
255
|
-
document.getElementById('wiBody').innerHTML=renderDetail(item);
|
|
256
|
-
document.getElementById('wiBack').hidden=!modalState.back.length;
|
|
257
|
-
modal.hidden=false;
|
|
258
|
-
document.body.classList.add('modal-open');
|
|
259
|
-
}
|
|
260
|
-
function openDetail(id,back){
|
|
261
|
-
modalState={id,back:back||[]};
|
|
262
|
-
refreshModal();
|
|
263
|
-
}
|
|
264
|
-
function closeDetail(){
|
|
265
|
-
modalState={id:null,back:[]};
|
|
266
|
-
const modal=document.getElementById('wiModal');
|
|
267
|
-
if(modal)modal.hidden=true;
|
|
268
|
-
document.body.classList.remove('modal-open');
|
|
269
|
-
}
|
|
270
|
-
document.addEventListener('click',e=>{
|
|
271
|
-
if(e.target.closest('.desc-toggle,.suspend-tag'))return;
|
|
272
|
-
const card=e.target.closest('.card');
|
|
273
|
-
if(card&&card.dataset.id)openDetail(card.dataset.id,[]);
|
|
274
|
-
});
|
|
275
|
-
document.getElementById('wiModal').addEventListener('click',e=>{
|
|
276
|
-
if(e.target.closest('#wiBack')){
|
|
277
|
-
const prev=modalState.back[modalState.back.length-1];
|
|
278
|
-
openDetail(prev,modalState.back.slice(0,-1));
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
if(e.target.classList.contains('modal-overlay')){closeDetail();return;}
|
|
282
|
-
if(e.target.closest('.modal-close')){closeDetail();return;}
|
|
283
|
-
const child=e.target.closest('.detail-child');
|
|
284
|
-
if(child&&child.dataset.id)openDetail(child.dataset.id,[...modalState.back,modalState.id]);
|
|
285
|
-
});
|
|
286
|
-
document.addEventListener('keydown',e=>{
|
|
287
|
-
if(e.key==='Escape'){const modal=document.getElementById('wiModal');if(modal&&!modal.hidden)closeDetail();}
|
|
288
|
-
});
|
|
289
|
-
async function loadState(){try{const res=await fetch('/api/state');if(!res.ok)throw new Error('fetch failed');const data=await res.json();if(data&&data.active===true){dashboardState=data;render()}}catch(e){console.error('[dashboard] load state failed',e)}}
|
|
290
|
-
render();
|
|
291
|
-
loadState();
|
|
292
|
-
setInterval(loadState,2000);
|
|
293
|
-
</script>
|
|
294
|
-
</body>
|
|
295
|
-
</html>
|
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>编排进度看板 · 5 列看板</title>
|
|
7
|
-
<style>
|
|
8
|
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
|
9
|
-
:root{
|
|
10
|
-
--bg:#f8fafc;--surface:#fff;--surface-2:#f1f5f9;
|
|
11
|
-
--ink:#0f172a;--muted:#475569;--quiet:#94a3b8;
|
|
12
|
-
--line:#e2e8f0;--line-2:#cbd5e1;
|
|
13
|
-
--blue:#2563eb;--blue-soft:#eff6ff;
|
|
14
|
-
--green:#16a34a;--green-soft:#f0fdf4;
|
|
15
|
-
--amber:#d97706;--amber-soft:#fffbeb;
|
|
16
|
-
--red:#dc2626;--red-soft:#fef2f2;
|
|
17
|
-
--violet:#7c3aed;--violet-soft:#f5f3ff;
|
|
18
|
-
--teal:#0d9488;--teal-soft:#f0fdfa;
|
|
19
|
-
--radius:12px;--radius-sm:8px;--pill:999px;
|
|
20
|
-
--shadow:0 1px 2px rgba(15,23,42,.04),0 6px 16px rgba(15,23,42,.04);
|
|
21
|
-
--sans:'Inter',system-ui,sans-serif;
|
|
22
|
-
--mono:'JetBrains Mono',ui-monospace,monospace;
|
|
23
|
-
}
|
|
24
|
-
*{box-sizing:border-box}html{font-size:15px}body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.55;-webkit-font-smoothing:antialiased}
|
|
25
|
-
button{font:inherit;cursor:pointer}button:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
|
|
26
|
-
.shell{max-width:1440px;margin:0 auto;padding:32px 28px 80px}
|
|
27
|
-
/* header */
|
|
28
|
-
.topbar{display:flex;justify-content:space-between;align-items:flex-start;gap:24px;padding-bottom:26px;border-bottom:1px solid var(--line)}
|
|
29
|
-
.brand{display:flex;flex-direction:column;gap:8px}
|
|
30
|
-
.badge{display:inline-flex;align-items:center;gap:8px;font:600 .68rem/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
|
31
|
-
.badge::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px var(--green-soft)}
|
|
32
|
-
h1{margin:0;font-size:1.6rem;font-weight:700;letter-spacing:-.03em}
|
|
33
|
-
.meta{display:flex;gap:28px;text-align:right;font-size:.8rem;color:var(--muted)}
|
|
34
|
-
.meta span{display:flex;flex-direction:column;gap:5px}.meta strong{color:var(--ink);font-weight:600}
|
|
35
|
-
/* 5 列看板 */
|
|
36
|
-
.board{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;align-items:start;padding-top:24px}
|
|
37
|
-
.col{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden;display:flex;flex-direction:column}
|
|
38
|
-
.col-head{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:14px 16px;border-bottom:1px solid var(--line)}
|
|
39
|
-
.col-head h3{margin:0;font-size:.92rem;font-weight:700}
|
|
40
|
-
.col-count{font:600 .78rem/1 var(--mono);padding:4px 10px;background:var(--surface-2);border-radius:var(--pill);color:var(--muted)}
|
|
41
|
-
.col-body{max-height:calc(100vh - 280px);overflow:auto;padding:12px;display:flex;flex-direction:column;gap:12px}
|
|
42
|
-
.board-empty{grid-column:1/-1;padding:48px 0;text-align:center;color:var(--quiet);font-size:.9rem}
|
|
43
|
-
/* 卡片 */
|
|
44
|
-
.card{position:relative;background:var(--surface);border:1px solid var(--line);border-left-width:3px;border-radius:var(--radius-sm);box-shadow:0 1px 2px rgba(15,23,42,.04);padding:12px 14px;display:flex;flex-direction:column;gap:8px}
|
|
45
|
-
.card.task{border-left-color:var(--blue)}
|
|
46
|
-
.card.issue{border-left-color:var(--violet)}
|
|
47
|
-
.card.suspended{border-style:dashed;opacity:.94}
|
|
48
|
-
.card-head{display:flex;align-items:center;gap:8px}
|
|
49
|
-
.type-tag{flex:0 0 auto;width:22px;height:22px;border-radius:6px;display:inline-flex;align-items:center;justify-content:center;font:700 .7rem/1 var(--mono);color:#fff}
|
|
50
|
-
.type-tag.task{background:var(--blue)}.type-tag.issue{background:var(--violet)}
|
|
51
|
-
.src-icon{flex:0 0 auto;font-size:.85rem;color:var(--quiet);line-height:1}
|
|
52
|
-
.card-title{margin:0;font-size:.9rem;font-weight:700;line-height:1.4;flex:1}
|
|
53
|
-
.card-desc{font-size:.78rem;color:var(--muted);line-height:1.5}
|
|
54
|
-
.card-desc.collapsed{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
55
|
-
.desc-toggle{margin-top:-4px;font-size:.72rem;color:var(--blue);background:none;border:none;padding:0;cursor:pointer;text-align:left}
|
|
56
|
-
.card-labels{display:flex;flex-wrap:wrap;gap:6px}
|
|
57
|
-
.chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:var(--pill);font:500 .68rem/1.4 var(--mono);background:var(--surface-2);color:var(--muted);border:1px solid var(--line)}
|
|
58
|
-
.card-children{display:flex;flex-direction:column;gap:6px;border-top:1px dashed var(--line);padding-top:8px}
|
|
59
|
-
.child-row{display:flex;align-items:center;gap:6px;font-size:.76rem;color:var(--muted)}
|
|
60
|
-
.child-type{flex:0 0 auto;width:16px;height:16px;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;font:700 .58rem/1 var(--mono);color:#fff}
|
|
61
|
-
.child-type.task{background:var(--blue)}.child-type.issue{background:var(--violet)}
|
|
62
|
-
.child-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
63
|
-
.child-phase{flex:0 0 auto;padding:1px 6px;border-radius:var(--pill);font:600 .62rem/1.3 var(--mono);background:var(--surface-2);color:var(--muted);border:1px solid var(--line)}
|
|
64
|
-
.card-foot{display:flex;justify-content:space-between;align-items:flex-end;gap:8px;margin-top:auto;border-top:1px solid var(--line);padding-top:8px}
|
|
65
|
-
.card-verdicts{display:flex;flex-wrap:wrap;gap:6px}
|
|
66
|
-
.verdict{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border-radius:var(--pill);font:600 .68rem/1.4 var(--mono);background:var(--surface-2);color:var(--muted)}
|
|
67
|
-
.verdict.passed{background:var(--green-soft);color:var(--green)}
|
|
68
|
-
.verdict.failed{background:var(--red-soft);color:var(--red)}
|
|
69
|
-
.verdict.pending{background:var(--surface-2);color:var(--quiet)}
|
|
70
|
-
.card-meta{display:flex;align-items:center;gap:6px;flex-shrink:0}
|
|
71
|
-
.step-tag{font:.68rem/1.4 var(--mono);color:var(--quiet)}
|
|
72
|
-
.suspend-tag{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:var(--pill);font:600 .7rem/1.4 var(--mono);background:var(--amber-soft);color:var(--amber);border:1px solid var(--amber);cursor:pointer}
|
|
73
|
-
.suspend-reason{margin-top:4px;font-size:.72rem;color:var(--amber);background:var(--amber-soft);border-radius:var(--radius-sm);padding:6px 8px}
|
|
74
|
-
/* severity 色条 */
|
|
75
|
-
.sev-bar{position:absolute;top:0;left:0;right:0;height:4px;border-radius:var(--radius-sm) var(--radius-sm) 0 0}
|
|
76
|
-
.sev-bar.critical{background:var(--red)}.sev-bar.high{background:var(--amber)}.sev-bar.medium{background:var(--violet)}.sev-bar.low{background:var(--teal)}.sev-bar.info{background:var(--line-2)}
|
|
77
|
-
/* WorkItem 详情弹窗 */
|
|
78
|
-
.modal-open{overflow:hidden}
|
|
79
|
-
.modal-overlay{position:fixed;inset:0;z-index:100;background:rgba(15,23,42,.45);display:flex;align-items:flex-start;justify-content:center;padding:48px 16px;overflow:auto}
|
|
80
|
-
.modal-overlay[hidden]{display:none}
|
|
81
|
-
.modal-dialog{background:var(--surface);border-radius:var(--radius);box-shadow:0 20px 50px rgba(15,23,42,.2);max-width:640px;width:100%;max-height:calc(100vh - 96px);display:flex;flex-direction:column}
|
|
82
|
-
.modal-head{display:flex;align-items:center;gap:12px;padding:14px 18px;border-bottom:1px solid var(--line)}
|
|
83
|
-
.modal-title{flex:1;font-size:.92rem;font-weight:700}
|
|
84
|
-
.modal-close{background:none;border:none;font-size:1.2rem;color:var(--muted);line-height:1;padding:4px 8px;border-radius:6px}
|
|
85
|
-
.modal-close:hover{background:var(--surface-2);color:var(--ink)}
|
|
86
|
-
.modal-back{background:none;border:1px solid var(--line);border-radius:var(--radius-sm);font-size:.78rem;color:var(--muted);padding:4px 10px}
|
|
87
|
-
.modal-back:hover{background:var(--surface-2);color:var(--ink)}
|
|
88
|
-
.modal-back[hidden]{display:none}
|
|
89
|
-
.modal-body{padding:18px;overflow:auto;display:flex;flex-direction:column;gap:14px}
|
|
90
|
-
.detail-title{margin:0;font-size:1.1rem;font-weight:700}
|
|
91
|
-
.detail-block{display:flex;flex-direction:column;gap:8px}
|
|
92
|
-
.detail-block h5{margin:0;font-size:.68rem;letter-spacing:.06em;text-transform:uppercase;color:var(--quiet)}
|
|
93
|
-
.meta-row{display:flex;gap:10px;font-size:.8rem;padding:4px 0;border-bottom:1px dashed var(--line)}
|
|
94
|
-
.meta-key{flex:0 0 96px;color:var(--quiet);font:500 .76rem/1.4 var(--mono)}
|
|
95
|
-
.meta-val{flex:1;color:var(--ink);word-break:break-word}
|
|
96
|
-
.detail-desc{margin:0;font-size:.82rem;color:var(--muted);white-space:pre-wrap}
|
|
97
|
-
.detail-verdicts{display:flex;flex-wrap:wrap;gap:6px}
|
|
98
|
-
.detail-verdict{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border-radius:var(--pill);font:600 .68rem/1.4 var(--mono);background:var(--surface-2);color:var(--muted)}
|
|
99
|
-
.detail-verdict.passed{background:var(--green-soft);color:var(--green)}
|
|
100
|
-
.detail-verdict.failed{background:var(--red-soft);color:var(--red)}
|
|
101
|
-
.detail-verdict.pending{background:var(--surface-2);color:var(--quiet)}
|
|
102
|
-
.detail-children{display:flex;flex-direction:column;gap:6px}
|
|
103
|
-
.detail-child{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid var(--line);border-radius:var(--radius-sm);font-size:.8rem;cursor:pointer}
|
|
104
|
-
.detail-child:hover{background:var(--surface-2);border-color:var(--line-2)}
|
|
105
|
-
@media(max-width:1024px){.board{grid-template-columns:repeat(2,1fr)}}
|
|
106
|
-
@media(max-width:640px){.board{grid-template-columns:1fr}.topbar{flex-direction:column;gap:14px}.meta{text-align:left}}
|
|
107
|
-
</style>
|
|
108
|
-
</head>
|
|
109
|
-
<body>
|
|
110
|
-
<div class="shell">
|
|
111
|
-
<header class="topbar">
|
|
112
|
-
<div class="brand">
|
|
113
|
-
<div class="badge" id="connBadge"><span>实时同步</span></div>
|
|
114
|
-
<h1>编排进度看板</h1>
|
|
115
|
-
</div>
|
|
116
|
-
<div class="meta">
|
|
117
|
-
<span>变更编号<strong id="changeId">-</strong></span>
|
|
118
|
-
<span>基准分支<strong id="baseBranch">-</strong></span>
|
|
119
|
-
<span>最后更新<strong id="updatedAt">-</strong></span>
|
|
120
|
-
</div>
|
|
121
|
-
</header>
|
|
122
|
-
<main id="dashboard"></main>
|
|
123
|
-
</div>
|
|
124
|
-
<div class="modal-overlay" id="wiModal" hidden>
|
|
125
|
-
<div class="modal-dialog" role="dialog" aria-modal="true" aria-label="工作项详情">
|
|
126
|
-
<header class="modal-head">
|
|
127
|
-
<button class="modal-back" id="wiBack" hidden>← 返回</button>
|
|
128
|
-
<span class="modal-title">工作项详情</span>
|
|
129
|
-
<button class="modal-close" id="wiClose" aria-label="关闭">×</button>
|
|
130
|
-
</header>
|
|
131
|
-
<div class="modal-body" id="wiBody"></div>
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
<script>
|
|
135
|
-
const COLUMNS=[['todo','待办'],['in_progress','处理中'],['review','审核中'],['done','完成'],['cancelled','已取消']];
|
|
136
|
-
const SOURCE_ICONS={openspec:'◈',ado:'◆',custom:'✚'};
|
|
137
|
-
const VERDICT_ICONS={passed:'✓',failed:'✕',pending:'○'};
|
|
138
|
-
const AGENT_SHORT={'openspec-architect':'architect','openspec-developer':'dev','openspec-reviewer-tool':'tool','openspec-reviewer-task':'task','openspec-reviewer-style':'style','openspec-reviewer-architecture':'arch','openspec-reviewer-performance':'perf','openspec-reviewer-security':'sec','openspec-reviewer-maintainability':'maint'};
|
|
139
|
-
let dashboardState={};
|
|
140
|
-
function esc(v){return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>','\'':''','"':'"'}[c]))}
|
|
141
|
-
function cls(v){return String(v??'').replace(/[^a-zA-Z0-9_-]/g,'_')}
|
|
142
|
-
function shortAgent(key){if(AGENT_SHORT[key])return AGENT_SHORT[key];const m=/^openspec-reviewer-(.+)$/.exec(key)||/^openspec-(.+)$/.exec(key);return m?m[1]:(key||'')}
|
|
143
|
-
function descBlock(text){if(!text)return '';if(text.length<90)return `<div class="card-desc">${esc(text)}</div>`;return `<div class="card-desc collapsed">${esc(text)}</div><button class="desc-toggle">展开</button>`}
|
|
144
|
-
function verdictDot(v){return `<span class="verdict ${cls(v.verdict)}" title="${esc(v.stepId)}">${esc(shortAgent(v.agentKey))}:${VERDICT_ICONS[v.verdict]||'○'}</span>`}
|
|
145
|
-
function renderCard(c){
|
|
146
|
-
const isIssue=c.type==='issue';
|
|
147
|
-
const typeTag=`<span class="type-tag ${isIssue?'issue':'task'}">${isIssue?'I':'T'}</span>`;
|
|
148
|
-
const srcIcon=`<span class="src-icon" title="${esc(c.source||'')}">${SOURCE_ICONS[c.source]||'●'}</span>`;
|
|
149
|
-
const sevBar=isIssue&&c.severity?`<span class="sev-bar ${cls(String(c.severity).toLowerCase())}" title="${esc(c.severity)}"></span>`:'';
|
|
150
|
-
const labels=(c.labels||[]).map(l=>`<span class="chip">${esc(l)}</span>`).join('');
|
|
151
|
-
const step=c.currentStep?`<span class="step-tag">${esc(c.currentStep)}</span>`:'';
|
|
152
|
-
const suspTag=c.suspended?'<span class="suspend-tag" role="button" tabindex="0">⏸ 暂停</span>':'';
|
|
153
|
-
const suspReason=c.suspended?`<div class="suspend-reason" hidden>${esc(c.suspendReason||'未提供暂停原因')}</div>`:'';
|
|
154
|
-
const verdicts=(c.agentVerdicts||[]).map(verdictDot).join('');
|
|
155
|
-
const children=(c.children||[]).map(ch=>`<div class="child-row"><span class="child-type ${cls(ch.type)}">${ch.type==='issue'?'I':'T'}</span><span class="child-title">${esc(ch.title)}</span>${ch.type==='task'?`<span class="child-phase">${esc(ch.phase)}</span>`:''}</div>`).join('');
|
|
156
|
-
return `<article class="card ${isIssue?'issue':'task'}${c.suspended?' suspended':''}" data-id="${esc(c.id)}">${sevBar}
|
|
157
|
-
<div class="card-head">${typeTag}${srcIcon}<h4 class="card-title">${esc(c.title)}</h4></div>
|
|
158
|
-
${descBlock(c.description)}
|
|
159
|
-
${labels?`<div class="card-labels">${labels}</div>`:''}
|
|
160
|
-
${children?`<div class="card-children">${children}</div>`:''}
|
|
161
|
-
<div class="card-foot">${verdicts?`<div class="card-verdicts">${verdicts}</div>`:'<span></span>'}<div class="card-meta">${step}${suspTag}</div></div>
|
|
162
|
-
${suspReason}
|
|
163
|
-
</article>`;
|
|
164
|
-
}
|
|
165
|
-
function renderBoard(){
|
|
166
|
-
const cards=dashboardState.workItemCards||[];
|
|
167
|
-
if(!cards.length)return `<section class="board"><div class="board-empty">暂无工作项数据(旧格式状态或未初始化)</div></section>`;
|
|
168
|
-
return `<section class="board">${COLUMNS.map(([key,label])=>{
|
|
169
|
-
const items=cards.filter(c=>c.phase===key);
|
|
170
|
-
return `<div class="col" data-col="${key}"><header class="col-head"><h3>${label}</h3><span class="col-count">${items.length}</span></header><div class="col-body">${items.map(renderCard).join('')}</div></div>`;
|
|
171
|
-
}).join('')}</section>`;
|
|
172
|
-
}
|
|
173
|
-
function render(){
|
|
174
|
-
document.getElementById('changeId').textContent=dashboardState.changeId||'-';
|
|
175
|
-
document.getElementById('baseBranch').textContent=dashboardState.baseBranch||'-';
|
|
176
|
-
document.getElementById('updatedAt').textContent=dashboardState.updatedAt?new Date(dashboardState.updatedAt).toLocaleString():'-';
|
|
177
|
-
document.getElementById('dashboard').innerHTML=renderBoard();
|
|
178
|
-
document.querySelectorAll('.desc-toggle').forEach(btn=>btn.addEventListener('click',e=>{
|
|
179
|
-
const d=e.target.previousElementSibling;
|
|
180
|
-
if(d&&d.classList.contains('card-desc')){d.classList.toggle('collapsed');e.target.textContent=d.classList.contains('collapsed')?'展开':'收起'}
|
|
181
|
-
}));
|
|
182
|
-
document.querySelectorAll('.suspend-tag').forEach(tag=>tag.addEventListener('click',e=>{
|
|
183
|
-
const card=e.target.closest('.card');
|
|
184
|
-
const reason=card&&card.querySelector('.suspend-reason');
|
|
185
|
-
if(reason)reason.hidden=!reason.hidden;
|
|
186
|
-
}));
|
|
187
|
-
if(modalState.id)refreshModal();
|
|
188
|
-
}
|
|
189
|
-
/* WorkItem 详情弹窗 */
|
|
190
|
-
let modalState={id:null,back:[]};
|
|
191
|
-
function findWorkItem(items,id){
|
|
192
|
-
for(const it of items||[]){
|
|
193
|
-
if(it.id===id)return it;
|
|
194
|
-
if(it.children&&it.children.length){
|
|
195
|
-
const found=findWorkItem(it.children,id);
|
|
196
|
-
if(found)return found;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
function renderTags(tags){
|
|
202
|
-
const verdicts=Object.entries(tags||{}).map(([key,verdict])=>{
|
|
203
|
-
const sep=key.indexOf(':');
|
|
204
|
-
const stepId=sep===-1?key:key.slice(0,sep);
|
|
205
|
-
const agentKey=sep===-1?key:key.slice(sep+1);
|
|
206
|
-
return `<span class="detail-verdict ${cls(verdict)}" title="${esc(key)}">${esc(shortAgent(agentKey))}:${VERDICT_ICONS[verdict]||'○'}</span>`;
|
|
207
|
-
}).join('');
|
|
208
|
-
return verdicts?`<div class="detail-block"><h5>Agent 裁决</h5><div class="detail-verdicts">${verdicts}</div></div>`:'';
|
|
209
|
-
}
|
|
210
|
-
function renderWriteback(wb){
|
|
211
|
-
if(!wb)return '';
|
|
212
|
-
const rows=Object.entries(wb).filter(([k])=>!k.startsWith('_')).map(([k,v])=>{
|
|
213
|
-
const val=typeof v==='object'?JSON.stringify(v):String(v);
|
|
214
|
-
return `<div class="meta-row"><span class="meta-key">${esc(k)}</span><span class="meta-val">${esc(val)}</span></div>`;
|
|
215
|
-
}).join('');
|
|
216
|
-
return rows?`<div class="detail-block"><h5>写回</h5>${rows}</div>`:'';
|
|
217
|
-
}
|
|
218
|
-
function renderMeta(metadata){
|
|
219
|
-
const rows=Object.entries(metadata||{}).filter(([k])=>!k.startsWith('_')).map(([k,v])=>{
|
|
220
|
-
const val=typeof v==='object'?JSON.stringify(v):String(v);
|
|
221
|
-
return `<div class="meta-row"><span class="meta-key">${esc(k)}</span><span class="meta-val">${esc(val)}</span></div>`;
|
|
222
|
-
}).join('');
|
|
223
|
-
return rows?`<div class="detail-block"><h5>元数据</h5>${rows}</div>`:'';
|
|
224
|
-
}
|
|
225
|
-
function renderChildren(children){
|
|
226
|
-
if(!children||!children.length)return '';
|
|
227
|
-
const rows=(children||[]).map(ch=>`<div class="detail-child" data-id="${esc(ch.id)}" role="button" tabindex="0">
|
|
228
|
-
<span class="child-type ${cls(ch.type)}">${ch.type==='issue'?'I':'T'}</span>
|
|
229
|
-
<span class="child-title">${esc(ch.title)}</span>
|
|
230
|
-
<span class="child-phase">${esc(ch.phase)}</span>
|
|
231
|
-
</div>`).join('');
|
|
232
|
-
return `<div class="detail-block"><h5>Children</h5><div class="detail-children">${rows}</div></div>`;
|
|
233
|
-
}
|
|
234
|
-
function renderDetail(item){
|
|
235
|
-
const rows=[['类型',item.type],['来源',item.source],['ID',item.id]];
|
|
236
|
-
if(item.externalId)rows.push(['外部ID',item.externalId]);
|
|
237
|
-
rows.push(['阶段',item.phase]);
|
|
238
|
-
if(item.severity)rows.push(['严重级别',item.severity]);
|
|
239
|
-
rows.push(['当前步骤',item.currentStep||'-']);
|
|
240
|
-
const kv=rows.map(([k,v])=>`<div class="meta-row"><span class="meta-key">${esc(k)}</span><span class="meta-val">${esc(v)}</span></div>`).join('');
|
|
241
|
-
return `<h3 class="detail-title">${esc(item.title)}</h3>
|
|
242
|
-
${item.suspended?`<div class="suspend-reason">⏸ 暂停:${esc(typeof (item.metadata||{})['suspend_reason']==='string'?(item.metadata||{})['suspend_reason']:'未提供暂停原因')}</div>`:''}
|
|
243
|
-
${item.description?`<div class="detail-block"><h5>描述</h5><p class="detail-desc">${esc(item.description)}</p></div>`:''}
|
|
244
|
-
<div class="detail-block"><h5>字段</h5>${kv}</div>
|
|
245
|
-
${item.labels&&item.labels.length?`<div class="detail-block"><h5>标签</h5><div class="card-labels">${item.labels.map(l=>`<span class="chip">${esc(l)}</span>`).join('')}</div></div>`:''}
|
|
246
|
-
${renderTags(item.tags)}
|
|
247
|
-
${renderWriteback(item.writeback)}
|
|
248
|
-
${renderMeta(item.metadata)}
|
|
249
|
-
${renderChildren(item.children)}`;
|
|
250
|
-
}
|
|
251
|
-
function refreshModal(){
|
|
252
|
-
const modal=document.getElementById('wiModal');
|
|
253
|
-
const item=findWorkItem(dashboardState.workItems||[],modalState.id);
|
|
254
|
-
if(!item){closeDetail();return;}
|
|
255
|
-
document.getElementById('wiBody').innerHTML=renderDetail(item);
|
|
256
|
-
document.getElementById('wiBack').hidden=!modalState.back.length;
|
|
257
|
-
modal.hidden=false;
|
|
258
|
-
document.body.classList.add('modal-open');
|
|
259
|
-
}
|
|
260
|
-
function openDetail(id,back){
|
|
261
|
-
modalState={id,back:back||[]};
|
|
262
|
-
refreshModal();
|
|
263
|
-
}
|
|
264
|
-
function closeDetail(){
|
|
265
|
-
modalState={id:null,back:[]};
|
|
266
|
-
const modal=document.getElementById('wiModal');
|
|
267
|
-
if(modal)modal.hidden=true;
|
|
268
|
-
document.body.classList.remove('modal-open');
|
|
269
|
-
}
|
|
270
|
-
document.addEventListener('click',e=>{
|
|
271
|
-
if(e.target.closest('.desc-toggle,.suspend-tag'))return;
|
|
272
|
-
const card=e.target.closest('.card');
|
|
273
|
-
if(card&&card.dataset.id)openDetail(card.dataset.id,[]);
|
|
274
|
-
});
|
|
275
|
-
document.getElementById('wiModal').addEventListener('click',e=>{
|
|
276
|
-
if(e.target.closest('#wiBack')){
|
|
277
|
-
const prev=modalState.back[modalState.back.length-1];
|
|
278
|
-
openDetail(prev,modalState.back.slice(0,-1));
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
if(e.target.classList.contains('modal-overlay')){closeDetail();return;}
|
|
282
|
-
if(e.target.closest('.modal-close')){closeDetail();return;}
|
|
283
|
-
const child=e.target.closest('.detail-child');
|
|
284
|
-
if(child&&child.dataset.id)openDetail(child.dataset.id,[...modalState.back,modalState.id]);
|
|
285
|
-
});
|
|
286
|
-
document.addEventListener('keydown',e=>{
|
|
287
|
-
if(e.key==='Escape'){const modal=document.getElementById('wiModal');if(modal&&!modal.hidden)closeDetail();}
|
|
288
|
-
});
|
|
289
|
-
async function loadState(){try{const res=await fetch('/api/state');if(!res.ok)throw new Error('fetch failed');const data=await res.json();if(data&&data.active===true){dashboardState=data;render()}}catch(e){console.error('[dashboard] load state failed',e)}}
|
|
290
|
-
render();
|
|
291
|
-
loadState();
|
|
292
|
-
setInterval(loadState,2000);
|
|
293
|
-
</script>
|
|
294
|
-
</body>
|
|
295
|
-
</html>
|