@kingsoftcloud/agentkit-harness-ui 0.0.0-stage → 0.0.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.
Files changed (56) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +116 -0
  3. package/README.md +292 -2
  4. package/markdown/build/build-markdown.cjs +66 -0
  5. package/markdown/dist/THIRD_PARTY_LICENSES.txt +2993 -0
  6. package/markdown/dist/markdown.css +1 -0
  7. package/markdown/dist/markdown.mjs +325 -0
  8. package/markdown/source/markdown-factory.mjs +613 -0
  9. package/markdown/source/markdown.css +288 -0
  10. package/markdown/source/table-factory.mjs +1588 -0
  11. package/markdown/source/table.css +487 -0
  12. package/package.json +96 -4
  13. package/src/activity-groups.mjs +74 -0
  14. package/src/browser-response-groups.mjs +57 -0
  15. package/src/command-output.mjs +57 -0
  16. package/src/components.mjs +9 -0
  17. package/src/context-markers.mjs +19 -0
  18. package/src/context-status.mjs +34 -0
  19. package/src/diff.mjs +240 -0
  20. package/src/explore-tool-call.mjs +139 -0
  21. package/src/file-preview.mjs +50 -0
  22. package/src/host/native-a2a.mjs +90 -0
  23. package/src/host/native-child-file-pane.mjs +49 -0
  24. package/src/host/native-child-pane.mjs +94 -0
  25. package/src/host/native-command-pane.mjs +64 -0
  26. package/src/host/native-run-panel.mjs +202 -0
  27. package/src/host/session-task-events.mjs +247 -0
  28. package/src/host/state/native-a2a.mjs +122 -0
  29. package/src/host/state/native-child-observation.mjs +35 -0
  30. package/src/host/state/native-children.mjs +93 -0
  31. package/src/host/state/native-outcome.mjs +35 -0
  32. package/src/host/state/native-recovery.mjs +187 -0
  33. package/src/host/state/native-runtime-route.mjs +26 -0
  34. package/src/host/state/native-session-stop.mjs +40 -0
  35. package/src/host/state/native-status-panel.mjs +23 -0
  36. package/src/host/state/native-task-read-model.mjs +89 -0
  37. package/src/host/state/task-links.mjs +40 -0
  38. package/src/host/task-timeline.mjs +185 -0
  39. package/src/index.mjs +52 -0
  40. package/src/input-preview.mjs +44 -0
  41. package/src/inspector.mjs +40 -0
  42. package/src/project.mjs +132 -0
  43. package/src/protocol.mjs +362 -0
  44. package/src/react-binding.mjs +486 -0
  45. package/src/reducer.mjs +355 -0
  46. package/src/renderers.mjs +644 -0
  47. package/src/status-panel.mjs +1156 -0
  48. package/src/styles.mjs +278 -0
  49. package/src/summary-playback.mjs +39 -0
  50. package/src/summary.mjs +160 -0
  51. package/src/terminal.mjs +157 -0
  52. package/src/tool-detail.mjs +208 -0
  53. package/src/tool-error.mjs +33 -0
  54. package/src/tool-identity.mjs +40 -0
  55. package/src/view.mjs +364 -0
  56. package/src/work-segments.mjs +41 -0
@@ -0,0 +1,1156 @@
1
+ /** A row is "ended" only when its status is an explicit terminal value; an
2
+ * absent or unknown value is kept current (never fabricated as ended). */
3
+ const ENDED_STATES = new Set([
4
+ "done", "ended", "finished", "completed", "succeeded", "success",
5
+ "cancelled", "canceled", "error", "failed", "stopped", "exited",
6
+ "aborted", "terminated", "timed_out", "output_limit",
7
+ ]);
8
+
9
+ export function isEndedStatus(value) {
10
+ if (value == null) return false;
11
+ return ENDED_STATES.has(String(value).trim().toLowerCase());
12
+ }
13
+
14
+ /** Split rows into current (running) and ended by the given status key. */
15
+ export function classifyRowsByStatus(rows, statusKey) {
16
+ const current = [];
17
+ const ended = [];
18
+ for (const row of Array.isArray(rows) ? rows : []) {
19
+ if (row && isEndedStatus(row[statusKey])) ended.push(row);
20
+ else current.push(row);
21
+ }
22
+ return { current, ended };
23
+ }
24
+
25
+ function isRunningRow(row, statusKey) {
26
+ if (typeof row.isRunning === "boolean") return row.isRunning;
27
+ return ["running", "active", "preparing_context", "requesting_model", "executing_tools", "compacting", "reviewing_delivery"].includes(row[statusKey]);
28
+ }
29
+
30
+ /** Normalise a plan item status to one of pending | in_progress | completed.
31
+ * Accepts both the source's camelCase ("inProgress") and the host contract's
32
+ * snake_case ("in_progress"); anything unknown falls back to pending, which
33
+ * is the source's behaviour for non-completed, non-in-progress items. */
34
+ export function planStatusKind(status) {
35
+ if (status === "completed" || status === "complete") return "completed";
36
+ if (status === "in_progress" || status === "inProgress" || status === "running")
37
+ return "in_progress";
38
+ return "pending";
39
+ }
40
+
41
+ function getCurrentPlanItem(plan) {
42
+ const items = (plan && plan.items) || [];
43
+ return (
44
+ items.find((i) => planStatusKind(i && i.status) === "in_progress") ||
45
+ items.find((i) => planStatusKind(i && i.status) === "pending") ||
46
+ null
47
+ );
48
+ }
49
+
50
+ function getCompletedPlanItem(plan) {
51
+ const items = (plan && plan.items) || [];
52
+ for (let i = items.length - 1; i >= 0; i--) {
53
+ if (planStatusKind(items[i] && items[i].status) === "completed") return items[i];
54
+ }
55
+ return null;
56
+ }
57
+
58
+ function formatCount(singular, plural, count) {
59
+ const tmpl = count === 1 ? singular : plural;
60
+ return String(tmpl).replace("{count}", String(count));
61
+ }
62
+
63
+ /** Duration formatter, verbatim units logic from the source. */
64
+ export function formatDurationUnits(totalSeconds, labels) {
65
+ const seconds = Math.max(0, Math.floor(totalSeconds));
66
+ const hours = Math.floor(seconds / 3600);
67
+ const minutes = Math.floor((seconds % 3600) / 60);
68
+ const restSeconds = seconds % 60;
69
+ const parts = [];
70
+ if (hours > 0) parts.push(`${hours}${labels.durationHours}`);
71
+ if (minutes > 0) parts.push(`${minutes}${labels.durationMinutes}`);
72
+ if (restSeconds > 0 || parts.length === 0) parts.push(`${restSeconds}${labels.durationSeconds}`);
73
+ return parts.join(" ");
74
+ }
75
+
76
+ const COMPACT_TODO_THRESHOLD = 6;
77
+ const TODO_FOCUS_WINDOW_SIZE = 3;
78
+
79
+ /**
80
+ * Plan focus window, verbatim from the source: lists of 6 or fewer render in
81
+ * full; longer lists centre a 3-item focus window on the running (else first
82
+ * unfinished, else last) item, with earlier/later rows folded into
83
+ * keyboard-accessible hover/focus previews. Snapshot order is preserved.
84
+ */
85
+ export function getStatusPanelTodoFocusWindow(items) {
86
+ const list = Array.isArray(items) ? items : [];
87
+ if (list.length <= COMPACT_TODO_THRESHOLD) {
88
+ return { compact: false, precedingItems: [], focusItems: list, followingItems: [] };
89
+ }
90
+ const runningIndex = list.findIndex((i) => planStatusKind(i && i.status) === "in_progress");
91
+ const firstUnfinishedIndex = list.findIndex((i) => planStatusKind(i && i.status) !== "completed");
92
+ const focusIndex =
93
+ runningIndex >= 0
94
+ ? runningIndex
95
+ : firstUnfinishedIndex >= 0
96
+ ? firstUnfinishedIndex
97
+ : Math.max(0, list.length - TODO_FOCUS_WINDOW_SIZE);
98
+ // Backfill from the front so the window keeps three rows near the list end.
99
+ const focusStartIndex = Math.max(0, Math.min(focusIndex, list.length - TODO_FOCUS_WINDOW_SIZE));
100
+ const focusEndIndex = Math.min(list.length, focusStartIndex + TODO_FOCUS_WINDOW_SIZE);
101
+ return {
102
+ compact: true,
103
+ precedingItems: list.slice(0, focusStartIndex),
104
+ focusItems: list.slice(focusStartIndex, focusEndIndex),
105
+ followingItems: list.slice(focusEndIndex),
106
+ };
107
+ }
108
+
109
+ /** Longest running elapsed across rows that carry a startedAt, in seconds
110
+ * floored to at least 1 (matches the source's collapsed trailing label). */
111
+ function longestElapsedSeconds(rows, now) {
112
+ let max = 0;
113
+ let has = false;
114
+ for (const r of rows) {
115
+ if (r && r.startedAt != null) {
116
+ has = true;
117
+ max = Math.max(max, Math.max(0, now - r.startedAt));
118
+ }
119
+ }
120
+ return has ? Math.max(1, Math.floor(max / 1000)) : null;
121
+ }
122
+
123
+ /**
124
+ * Mini-summary priority, reusing the source's branch order with the model
125
+ * trimmed to what the host injects (no goal / session plans / workflows):
126
+ * current plan item → Git changes → completed plan item → running
127
+ * terminals/agents count → finished-child directory → isRunning fallback.
128
+ * Returns a plain descriptor (icon is a name string) or null.
129
+ */
130
+ export function resolveStatusPanelMiniSummary(model, labels) {
131
+ const m = model || {};
132
+ const currentPlanItem = getCurrentPlanItem(m.plan);
133
+ if (currentPlanItem) {
134
+ return { key: "currentPlan", icon: "arrowRight", text: currentPlanItem.content || "" };
135
+ }
136
+ const git = m.git;
137
+ if (git && (git.added || 0) + (git.removed || 0) > 0) {
138
+ return { key: "git", icon: "fileDiff", text: labels.changes, added: git.added || 0, removed: git.removed || 0 };
139
+ }
140
+ const completedPlanItem = getCompletedPlanItem(m.plan);
141
+ if (completedPlanItem) {
142
+ return { key: "completedPlan", icon: "checkCircle", text: completedPlanItem.content || "" };
143
+ }
144
+ const { current: runningTerminals } = classifyRowsByStatus(m.terminals, "state");
145
+ const { current: runningAgents } = classifyRowsByStatus(m.agents, "phase");
146
+ const runningCount = runningTerminals.filter(row => isRunningRow(row, "state")).length + runningAgents.filter(row => isRunningRow(row, "phase")).length;
147
+ if (runningCount > 0) {
148
+ const hasTerminals = runningTerminals.length > 0;
149
+ const hasAgents = runningAgents.length > 0;
150
+ const icon = hasTerminals && hasAgents ? "activity" : hasTerminals ? "squareTerminal" : "bot";
151
+ const text = hasAgents
152
+ ? formatCount(labels.agentsMiniValue, labels.agentsMiniValuePlural, runningCount)
153
+ : formatCount(labels.terminalsMiniValue, labels.terminalsMiniValuePlural, runningCount);
154
+ return { key: "runningCount", icon, text, count: runningCount };
155
+ }
156
+ if (runningTerminals.length + runningAgents.length > 0) return { key: "unconfirmed", icon: "activity", text: labels.unconfirmed };
157
+ const { ended: endedTerminals } = classifyRowsByStatus(m.terminals, "state");
158
+ const { ended: endedAgents } = classifyRowsByStatus(m.agents, "phase");
159
+ const endedCount = endedTerminals.length + endedAgents.length;
160
+ if (endedCount > 0) {
161
+ return { key: "endedDirectory", icon: "checkCircle", text: labels.ended, count: endedCount };
162
+ }
163
+ if (m.isRunning) {
164
+ return { key: "running", icon: null, runningDot: true, text: labels.running };
165
+ }
166
+ return null;
167
+ }
168
+
169
+ // ── Default labels (zh-CN, the authoritative locale) ──────────────────────
170
+
171
+ export const DEFAULT_LABELS = {
172
+ title: "状态",
173
+ environment: "Git 工具",
174
+ changes: "更改",
175
+ branch: "分支",
176
+ commitPush: "提交 / 推送",
177
+ clean: "干净",
178
+ todo: "进程",
179
+ terminals: "终端",
180
+ agents: "智能体",
181
+ ended: "已结束",
182
+ endedAgents: "已结束的智能体",
183
+ endedTerminals: "已结束的终端",
184
+ running: "运行中",
185
+ unconfirmed: "状态待确认",
186
+ runningStop: "停止",
187
+ showMini: "收起为胶囊",
188
+ showPanel: "展开状态",
189
+ displayMode: "状态面板展开策略",
190
+ displayModeAuto: "自动展开",
191
+ stopRunning: "停止运行中的后台任务",
192
+ openSubagent: "打开子智能体会话",
193
+ openTerminal: "查看 {title} 的输出",
194
+ todoCompletedFold: "已完成 {count} 项",
195
+ todoEarlierFold: "前面 {count} 项",
196
+ todoLaterFold: "后面 {count} 项",
197
+ todoWaitingFold: "待处理 {count} 项",
198
+ terminalsOpenValue: "{count} 个后台运行",
199
+ terminalsOpenValuePlural: "{count} 个后台运行",
200
+ terminalsMiniValue: "{count} 后台",
201
+ terminalsMiniValuePlural: "{count} 后台",
202
+ agentsOpenValue: "{count} 运行",
203
+ agentsOpenValuePlural: "{count} 运行",
204
+ agentsMiniValue: "{count} 运行",
205
+ agentsMiniValuePlural: "{count} 运行",
206
+ durationHours: "小时",
207
+ durationMinutes: "分",
208
+ durationSeconds: "秒",
209
+ };
210
+
211
+ // ── Inline Lucide icon geometry (viewBox 0 0 24 24, stroke currentColor) ──
212
+
213
+ const ICONS = {
214
+ arrowRight: [["path", { d: "M5 12h14" }], ["path", { d: "m12 5 7 7-7 7" }]],
215
+ bot: [
216
+ ["path", { d: "M12 8V4H8" }],
217
+ ["rect", { width: 16, height: 12, x: 4, y: 8, rx: 2 }],
218
+ ["path", { d: "M2 14h2" }],
219
+ ["path", { d: "M20 14h2" }],
220
+ ["path", { d: "M15 13v2" }],
221
+ ["path", { d: "M9 13v2" }],
222
+ ],
223
+ checkCircle: [["circle", { cx: 12, cy: 12, r: 10 }], ["path", { d: "m9 12 2 2 4-4" }]],
224
+ chevronDown: [["path", { d: "m6 9 6 6 6-6" }]],
225
+ chevronLeft: [["path", { d: "m15 18-6-6 6-6" }]],
226
+ chevronRight: [["path", { d: "m9 18 6-6-6-6" }]],
227
+ circle: [["circle", { cx: 12, cy: 12, r: 10 }]],
228
+ ellipsis: [
229
+ ["circle", { cx: 5, cy: 12, r: 1 }],
230
+ ["circle", { cx: 12, cy: 12, r: 1 }],
231
+ ["circle", { cx: 19, cy: 12, r: 1 }],
232
+ ],
233
+ fileDiff: [
234
+ ["path", { d: "M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" }],
235
+ ["path", { d: "M12 13H8" }],
236
+ ["path", { d: "M8 17h4" }],
237
+ ["path", { d: "M16 11h-4" }],
238
+ ["path", { d: "M16 15h-4" }],
239
+ ],
240
+ gitBranch: [
241
+ ["line", { x1: 6, x2: 6, y1: 3, y2: 15 }],
242
+ ["circle", { cx: 18, cy: 6, r: 3 }],
243
+ ["circle", { cx: 6, cy: 18, r: 3 }],
244
+ ["path", { d: "M18 9a9 9 0 0 1-9 9" }],
245
+ ],
246
+ gitCommit: [
247
+ ["circle", { cx: 12, cy: 12, r: 3 }],
248
+ ["line", { x1: 3, x2: 9, y1: 12, y2: 12 }],
249
+ ["line", { x1: 15, x2: 21, y1: 12, y2: 12 }],
250
+ ],
251
+ listChecks: [
252
+ ["path", { d: "m3 17 2 2 4-4" }],
253
+ ["path", { d: "m3 7 2 2 4-4" }],
254
+ ["path", { d: "M13 6h8" }],
255
+ ["path", { d: "M13 12h8" }],
256
+ ["path", { d: "M13 18h8" }],
257
+ ],
258
+ maximize2: [
259
+ ["path", { d: "M8 3H5a2 2 0 0 0-2 2v3" }],
260
+ ["path", { d: "M21 8V5a2 2 0 0 0-2-2h-3" }],
261
+ ["path", { d: "M3 16v3a2 2 0 0 0 2 2h3" }],
262
+ ["path", { d: "M16 21h3a2 2 0 0 0 2-2v-3" }],
263
+ ],
264
+ minimize2: [
265
+ ["path", { d: "M8 3v3a2 2 0 0 1-2 2H3" }],
266
+ ["path", { d: "M21 8h-3a2 2 0 0 1-2-2V3" }],
267
+ ["path", { d: "M3 16h3a2 2 0 0 1 2 2v3" }],
268
+ ["path", { d: "M16 21v-3a2 2 0 0 1 2-2h3" }],
269
+ ],
270
+ square: [["rect", { width: 18, height: 18, x: 3, y: 3, rx: 2 }]],
271
+ squareTerminal: [
272
+ ["rect", { width: 18, height: 18, x: 3, y: 3, rx: 2 }],
273
+ ["path", { d: "m7 11 2-2-2-2" }],
274
+ ["path", { d: "M11 13h4" }],
275
+ ],
276
+ activity: [["path", { d: "M22 12h-4l-3 9L9 3l-3 9H2" }]],
277
+ };
278
+
279
+ // ── Scoped CSS ─────────────────────────────────────────────────────────────
280
+
281
+ export const statusPanelStyles = `
282
+ .harness-status-panel-root{
283
+ position:absolute; top:0; left:0; right:0; z-index:20;
284
+ display:flex; justify-content:flex-end;
285
+ padding:1rem 1rem 0 0;
286
+ pointer-events:none;
287
+ container-type:inline-size; container-name:harness-status-panel;
288
+ --sp-fg: var(--harness-fg, currentColor);
289
+ --sp-fg-subtle: color-mix(in srgb, var(--sp-fg) 60%, transparent);
290
+ --sp-fg-subtlest: color-mix(in srgb, var(--sp-fg) 40%, transparent);
291
+ --sp-bg: var(--harness-bg, var(--color-popover, #ffffff));
292
+ --sp-border: color-mix(in srgb, var(--sp-fg) 16%, transparent);
293
+ --sp-hover: color-mix(in srgb, var(--sp-fg) 8%, transparent);
294
+ --sp-focus: var(--sp-fg);
295
+ --sp-added: var(--harness-added, #1a7f37);
296
+ --sp-removed: var(--harness-removed, #cf222e);
297
+ --sp-success: var(--harness-added, #1a7f37);
298
+ --sp-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
299
+ color: var(--sp-fg);
300
+ }
301
+ @media (prefers-color-scheme: dark){
302
+ .harness-status-panel-root{
303
+ --sp-bg: var(--harness-bg, var(--color-popover, #1c1c1e));
304
+ --sp-added: var(--harness-added, #4ac26b);
305
+ --sp-removed: var(--harness-removed, #ff7b72);
306
+ --sp-success: var(--harness-added, #4ac26b);
307
+ }
308
+ }
309
+ .harness-status-panel-root[data-theme="light"]{
310
+ --sp-bg: var(--harness-bg, var(--color-popover, #ffffff));
311
+ --sp-added:#1a7f37; --sp-removed:#cf222e; --sp-success:#1a7f37;
312
+ }
313
+ .harness-status-panel-root[data-theme="dark"]{
314
+ --sp-bg: var(--harness-bg, var(--color-popover, #1c1c1e));
315
+ --sp-added: var(--harness-added, #4ac26b);
316
+ --sp-removed: var(--harness-removed, #ff7b72);
317
+ --sp-success: var(--harness-added, #4ac26b);
318
+ }
319
+ .harness-status-panel{
320
+ position:relative; pointer-events:auto;
321
+ border-radius:1rem; border:1px solid var(--sp-border);
322
+ background:var(--sp-bg); color:var(--sp-fg);
323
+ box-shadow:0 4px 12px rgba(0,0,0,.08);
324
+ font-family:inherit; font-size:inherit; line-height:1.4;
325
+ overflow:visible;
326
+ transition:border-radius .3s ease, padding .3s ease, background-color .3s ease, box-shadow .3s ease;
327
+ }
328
+ .harness-status-panel-root svg{ width:1em; height:1em; flex:none; stroke:currentColor; fill:none; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
329
+ .harness-status-panel[data-display-mode="auto"],
330
+ .harness-status-panel[data-display-mode="mini"]{
331
+ display:inline-flex; flex-direction:column;
332
+ max-height:2.125rem; width:max-content; max-width:min(20rem, calc(100cqw - 2rem));
333
+ }
334
+ .harness-status-panel[data-display-mode="panel"]{
335
+ display:flex; flex-direction:column;
336
+ max-height:min(64dvh, 32rem); width:20rem; max-width:calc(100cqw - 2rem);
337
+ }
338
+ @container harness-status-panel (min-width: 1280px){
339
+ .harness-status-panel[data-display-mode="auto"]{
340
+ display:flex; flex-direction:column;
341
+ max-height:min(64dvh, 32rem); width:20rem; max-width:calc(100cqw - 2rem);
342
+ }
343
+ .harness-status-panel[data-display-mode="auto"] .harness-status-panel__sections{ display:flex; }
344
+ .harness-status-panel[data-display-mode="auto"] .harness-status-panel__controls{ display:flex; }
345
+ .harness-status-panel[data-display-mode="auto"] .harness-status-panel__mini{ display:none; }
346
+ }
347
+ .harness-status-panel__controls{
348
+ display:none; position:absolute; right:0.75rem; top:0.75rem; z-index:10;
349
+ align-items:center; gap:0.25rem;
350
+ }
351
+ .harness-status-panel[data-display-mode="panel"] .harness-status-panel__controls{ display:flex; }
352
+ .harness-status-panel__ctrl{
353
+ display:inline-flex; align-items:center; justify-content:center;
354
+ width:1.5rem; height:1.5rem; padding:0; border:0; border-radius:0.375rem;
355
+ background:transparent; color:var(--sp-fg-subtle); cursor:pointer; font:inherit;
356
+ }
357
+ .harness-status-panel__ctrl:hover{ background:var(--sp-hover); color:var(--sp-fg); }
358
+ .harness-status-panel__ctrl:focus-visible{ outline:2px solid var(--sp-focus); outline-offset:1px; }
359
+ .harness-status-panel__sections{
360
+ display:none; flex-direction:column; gap:0.5rem;
361
+ min-height:0; flex:1 1 auto; overflow-x:hidden; overflow-y:auto; padding:0.5rem;
362
+ }
363
+ .harness-status-panel[data-display-mode="panel"] .harness-status-panel__sections{ display:flex; }
364
+ .harness-status-panel__mini{ display:block; }
365
+ .harness-status-panel[data-display-mode="panel"] .harness-status-panel__mini{ display:none; }
366
+ .harness-status-panel__section{ min-width:0; flex:none; }
367
+ .harness-status-panel__section--separated{ border-top:1px solid var(--sp-border); padding-top:0.5rem; }
368
+ .harness-status-panel__section-header{ display:flex; align-items:center; gap:0.375rem; min-height:2rem; padding:0 0.5rem 0 0.5rem; padding-right:2rem; }
369
+ .harness-status-panel__section-trigger{
370
+ display:flex; align-items:center; gap:0.25rem; min-width:0; flex:none;
371
+ background:transparent; border:0; padding:0; text-align:left; cursor:pointer;
372
+ color:var(--sp-fg-subtle); font:inherit;
373
+ }
374
+ .harness-status-panel__section-trigger:focus-visible{ outline:2px solid var(--sp-focus); outline-offset:1px; border-radius:0.25rem; }
375
+ .harness-status-panel__section-title{ font-size:0.875em; color:var(--sp-fg-subtle); }
376
+ .harness-status-panel__chevron{ width:0.875em; height:0.875em; color:var(--sp-fg-subtle); opacity:0; transition:opacity .15s; }
377
+ .harness-status-panel__section-header:hover .harness-status-panel__chevron,
378
+ .harness-status-panel__section-trigger:focus-visible .harness-status-panel__chevron{ opacity:1; }
379
+ .harness-status-panel__section-trailing{ display:inline-flex; align-items:center; gap:0.375rem; min-width:0; max-width:100%; font-size:0.75em; color:var(--sp-fg-subtlest); }
380
+ .harness-status-panel__section-content{ min-width:0; }
381
+ .harness-status-panel__section[data-status-section="plan"] .harness-status-panel__section-content{ max-height:20rem; overflow-y:auto; }
382
+ .harness-status-panel__section[data-status-section="terminals"] .harness-status-panel__section-content,
383
+ .harness-status-panel__section[data-status-section="agents"] .harness-status-panel__section-content{ max-height:12rem; overflow-y:auto; }
384
+ .harness-status-panel__row,.harness-status-panel__ended-row{
385
+ position:relative; display:flex; align-items:flex-start; gap:0.5rem;
386
+ padding:0.5rem; border-radius:0.5rem; min-height:2rem;
387
+ }
388
+ .harness-status-panel__row:hover,.harness-status-panel__ended-row:hover{ background:var(--sp-hover); }
389
+ .harness-status-panel__row-open{ position:absolute; inset:0; border:0; background:transparent; border-radius:0.5rem; cursor:pointer; z-index:0; }
390
+ .harness-status-panel__row-open:focus-visible{ outline:2px solid var(--sp-focus); outline-offset:-1px; }
391
+ .harness-status-panel__row-icon{ width:1em; height:1em; color:var(--sp-fg-subtle); position:relative; z-index:1; pointer-events:none; margin-top:0.125em; }
392
+ .harness-status-panel__row-body{ position:relative; z-index:1; pointer-events:none; flex:1; min-width:0; display:flex; flex-direction:column; gap:0.25rem; }
393
+ .harness-status-panel__row-title{ margin:0; overflow:hidden; text-overflow:ellipsis; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; line-height:1.25; }
394
+ .harness-status-panel__row-meta{ display:flex; flex-wrap:wrap; align-items:center; gap:0.5rem; font-size:0.85em; }
395
+ .harness-status-panel__elapsed{ color:var(--sp-fg-subtle); font-variant-numeric:tabular-nums; font-family:var(--sp-mono); }
396
+ .harness-status-panel__row-role{ color:var(--sp-fg-subtlest); font-size:0.85em; }
397
+ .harness-status-panel__stop{
398
+ position:relative; z-index:2; pointer-events:auto;
399
+ display:inline-flex; align-items:center; gap:0.25rem;
400
+ height:1.5rem; padding:0 0.375rem; margin-left:auto;
401
+ border:0; border-radius:0.25rem; background:transparent; color:var(--sp-fg); font:inherit; font-size:0.85em; cursor:pointer;
402
+ }
403
+ .harness-status-panel__stop:hover{ background:var(--sp-hover); }
404
+ .harness-status-panel__stop:focus-visible{ outline:2px solid var(--sp-focus); outline-offset:1px; }
405
+ .harness-status-panel__stop-icon{ width:0.75em; height:0.75em; fill:currentColor; stroke:none; }
406
+ .harness-status-panel__action{
407
+ display:flex; align-items:center; gap:0.5rem; width:100%; min-width:0;
408
+ height:2rem; padding:0 0.5rem; border:0; border-radius:0.5rem;
409
+ background:transparent; color:var(--sp-fg); text-align:left; font:inherit; cursor:pointer;
410
+ }
411
+ .harness-status-panel__action:hover{ background:var(--sp-hover); }
412
+ .harness-status-panel__action:focus-visible{ outline:2px solid var(--sp-focus); outline-offset:-1px; }
413
+ .harness-status-panel__action-static{ display:flex; align-items:center; gap:0.5rem; height:2rem; padding:0 0.5rem; opacity:.85; }
414
+ .harness-status-panel__action-label{ flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
415
+ .harness-status-panel__git-delta{ display:inline-flex; gap:0.25rem; font-family:var(--sp-mono); font-variant-numeric:tabular-nums; font-size:0.85em; }
416
+ .harness-status-panel__added{ color:var(--sp-added); }
417
+ .harness-status-panel__removed{ color:var(--sp-removed); }
418
+ .harness-status-panel__git-dirty{ color:var(--sp-fg-subtle); font-size:0.85em; }
419
+ .harness-status-panel__action-icon{ width:1em; height:1em; color:var(--sp-fg); flex:none; }
420
+ .harness-status-panel__plan-list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; }
421
+ .harness-status-panel__plan-item{ display:flex; align-items:flex-start; gap:0.5rem; padding:0.375rem 0.5rem; border-radius:0.5rem; min-height:2rem; }
422
+ .harness-status-panel__plan-item:hover{ background:var(--sp-hover); }
423
+ .harness-status-panel__plan-icon{ width:0.875em; height:0.875em; margin-top:0.125em; flex:none; }
424
+ .harness-status-panel__plan-icon--done{ color:var(--sp-success); }
425
+ .harness-status-panel__plan-icon--active{ color:var(--sp-fg); }
426
+ .harness-status-panel__plan-icon--pending{ color:var(--sp-fg-subtlest); }
427
+ .harness-status-panel__plan-content{ flex:1; min-width:0; line-height:1.25; overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; }
428
+ .harness-status-panel__plan-content--done{ color:var(--sp-fg-subtlest); text-decoration:line-through; }
429
+ .harness-status-panel__plan-count{ font-variant-numeric:tabular-nums; }
430
+ .harness-status-panel__plan-count--done{ color:var(--sp-success); }
431
+ .harness-status-panel__fold{ position:relative; }
432
+ .harness-status-panel__fold-trigger{
433
+ display:flex; align-items:center; gap:0.5rem; width:100%; min-height:2rem; padding:0 0.5rem;
434
+ border:0; border-radius:0.5rem; background:transparent; color:var(--sp-fg-subtle); text-align:left; font:inherit; cursor:pointer;
435
+ }
436
+ .harness-status-panel__fold-trigger:hover,.harness-status-panel__fold-trigger:focus-visible{ background:var(--sp-hover); }
437
+ .harness-status-panel__fold-trigger:focus-visible{ outline:2px solid var(--sp-focus); outline-offset:-1px; }
438
+ .harness-status-panel__fold-trigger .harness-status-panel__fold-chevron{ width:0.875em; height:0.875em; }
439
+ .harness-status-panel__fold-preview{
440
+ display:none; position:absolute; left:0; bottom:100%; z-index:20;
441
+ width:20rem; max-width:min(20rem, calc(100cqw - 2rem));
442
+ border:1px solid var(--sp-border); border-radius:0.75rem; background:var(--sp-bg);
443
+ box-shadow:0 4px 12px rgba(0,0,0,.12); padding:0.75rem;
444
+ }
445
+ .harness-status-panel__fold:hover .harness-status-panel__fold-preview,
446
+ .harness-status-panel__fold:focus-within .harness-status-panel__fold-preview{ display:block; }
447
+ .harness-status-panel__fold-preview-header{ display:flex; align-items:center; min-height:2rem; padding:0 0.5rem; color:var(--sp-fg-subtle); font-size:0.875em; }
448
+ .harness-status-panel__ended--separated{ border-top:1px solid var(--sp-border); margin-top:0.5rem; padding-top:0.5rem; }
449
+ .harness-status-panel__ended-header{ display:flex; align-items:center; gap:0.5rem; min-height:2rem; padding:0 0.5rem; color:var(--sp-fg); }
450
+ .harness-status-panel__ended-icon{ width:1em; height:1em; color:var(--sp-fg-subtle); }
451
+ .harness-status-panel__ended-count{ margin-left:auto; color:var(--sp-fg-subtle); }
452
+ .harness-status-panel__ended-row > span,.harness-status-panel__ended-row > svg{ pointer-events:none; }
453
+ .harness-status-panel__ended-chevron{ width:0.875em; height:0.875em; color:var(--sp-fg-subtle); }
454
+ .harness-status-panel__mini-trigger{
455
+ display:inline-flex; flex-direction:column; align-items:stretch; width:max-content; max-width:100%;
456
+ background:transparent; border:0; padding:0; text-align:left; cursor:pointer; color:var(--sp-fg); font:inherit;
457
+ }
458
+ .harness-status-panel__mini-trigger:hover{ background:var(--sp-hover); border-radius:1rem; }
459
+ .harness-status-panel__mini-trigger:focus-visible{ outline:2px solid var(--sp-focus); outline-offset:1px; border-radius:1rem; }
460
+ .harness-status-panel__mini-metric{ display:flex; align-items:center; gap:0.375rem; min-width:0; max-width:100%; height:2rem; padding:0 0.75rem 0 0.5rem; font-size:0.875em; }
461
+ .harness-status-panel__mini-icon{ position:relative; width:1em; height:1em; flex:none; }
462
+ .harness-status-panel__mini-icon svg{ position:absolute; inset:0; transition:opacity .15s; }
463
+ .harness-status-panel__mini-icon-hover{ opacity:0; }
464
+ .harness-status-panel__mini-trigger:hover .harness-status-panel__mini-icon-base,
465
+ .harness-status-panel__mini-trigger:focus-visible .harness-status-panel__mini-icon-base{ opacity:0; }
466
+ .harness-status-panel__mini-trigger:hover .harness-status-panel__mini-icon-hover,
467
+ .harness-status-panel__mini-trigger:focus-visible .harness-status-panel__mini-icon-hover{ opacity:1; }
468
+ .harness-status-panel__mini-text{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
469
+ .harness-status-panel__mini-count{ color:var(--sp-fg-subtlest); flex:none; }
470
+ .harness-status-panel__running-dot{ width:0.375em; height:0.375em; border-radius:9999px; background:var(--sp-success); flex:none; animation:harness-status-panel-pulse 1.5s ease-in-out infinite; }
471
+ @keyframes harness-status-panel-pulse{ 50%{ opacity:.4; } }
472
+ @media (prefers-reduced-motion: reduce){ .harness-status-panel__running-dot{ animation:none; } }
473
+ .harness-status-panel__menu{ position:relative; }
474
+ .harness-status-panel__menu-popover{
475
+ position:absolute; right:0; top:calc(100% + 4px); z-index:30;
476
+ min-width:11rem; border:1px solid var(--sp-border); border-radius:0.75rem; background:var(--sp-bg);
477
+ box-shadow:0 4px 12px rgba(0,0,0,.12); padding:0.25rem;
478
+ }
479
+ .harness-status-panel__menu-item{
480
+ display:flex; align-items:center; width:100%; min-height:2rem; padding:0 0.5rem;
481
+ border:0; border-radius:0.375rem; background:transparent; color:var(--sp-fg); text-align:left; font:inherit; cursor:pointer;
482
+ }
483
+ .harness-status-panel__menu-item:hover{ background:var(--sp-hover); }
484
+ .harness-status-panel__menu-item:focus-visible{ outline:2px solid var(--sp-focus); outline-offset:-1px; }
485
+ `;
486
+
487
+ // ── Factory ────────────────────────────────────────────────────────────────
488
+
489
+ export function createStatusPanelComponents(React, ReactDOM = {}) {
490
+ const h = React.createElement;
491
+ const { useState, useEffect, useCallback, useRef, memo } = React;
492
+
493
+ function Icon(name, extraProps) {
494
+ const paths = ICONS[name];
495
+ if (!paths) return null;
496
+ const props = {
497
+ viewBox: "0 0 24 24",
498
+ "aria-hidden": true,
499
+ ...(extraProps || {}),
500
+ };
501
+ return h("svg", props, paths.map(([t, p], i) => h(t, { key: i, ...p })));
502
+ }
503
+
504
+ function StopButton({ workId, onCancel, labels }) {
505
+ return h(
506
+ "button",
507
+ {
508
+ type: "button",
509
+ className: "harness-status-panel__stop",
510
+ "data-row-cancel": workId,
511
+ "aria-label": labels.stopRunning,
512
+ onClick: (event) => {
513
+ event.stopPropagation();
514
+ onCancel(workId);
515
+ },
516
+ },
517
+ Icon("square", { className: "harness-status-panel__stop-icon" }),
518
+ h("span", null, labels.runningStop),
519
+ );
520
+ }
521
+
522
+ function Row({ row, kind, now, openCallback, onCancel, labels }) {
523
+ const title = row.title || row.id || row.runId || "";
524
+ const elapsed =
525
+ row.startedAt != null
526
+ ? formatDurationUnits(Math.floor((now - row.startedAt) / 1000), labels)
527
+ : null;
528
+ const canCancel = row.canCancel === true && typeof onCancel === "function";
529
+ const openable = typeof openCallback === "function";
530
+ const iconName = kind === "terminal" ? "squareTerminal" : "bot";
531
+ const statusKey = kind === "terminal" ? "state" : "phase";
532
+ return h(
533
+ "li",
534
+ {
535
+ className: "harness-status-panel__row",
536
+ "data-row-kind": kind,
537
+ "data-row-state": row[statusKey],
538
+ "data-row-id": row.id || row.runId,
539
+ "data-can-cancel": canCancel ? "true" : "false",
540
+ },
541
+ openable
542
+ ? h("button", {
543
+ type: "button",
544
+ className: "harness-status-panel__row-open",
545
+ "data-row-open": kind,
546
+ "aria-label": (kind === "terminal" ? labels.openTerminal : labels.openSubagent + ":{title}").replace(
547
+ "{title}",
548
+ title,
549
+ ),
550
+ onClick: (event) => {
551
+ event.stopPropagation();
552
+ openCallback(row);
553
+ },
554
+ })
555
+ : null,
556
+ Icon(iconName, { className: "harness-status-panel__row-icon" }),
557
+ h(
558
+ "div",
559
+ { className: "harness-status-panel__row-body" },
560
+ h("p", { className: "harness-status-panel__row-title" }, title),
561
+ h(
562
+ "div",
563
+ { className: "harness-status-panel__row-meta" },
564
+ kind === "agent" && row.role
565
+ ? h("span", { className: "harness-status-panel__row-role" }, row.role)
566
+ : null,
567
+ elapsed ? h("span", { className: "harness-status-panel__elapsed" }, elapsed) : null,
568
+ canCancel ? h(StopButton, { workId: row.id || row.runId, onCancel, labels }) : null,
569
+ ),
570
+ ),
571
+ );
572
+ }
573
+
574
+ function EndedDirectory({ ended, kind, openCallback, openDirectory, labels }) {
575
+ if (ended.length === 0) return null;
576
+ const iconName = kind === "terminal" ? "squareTerminal" : "bot";
577
+ const statusKey = kind === "terminal" ? "state" : "phase";
578
+ if (typeof openDirectory === "function") return h("div", { className: "harness-status-panel__ended harness-status-panel__ended--separated" },
579
+ h("button", { type: "button", className: "harness-status-panel__action", onClick: () => openDirectory(), "data-ended-directory": kind },
580
+ Icon("checkCircle"), h("span", { className: "harness-status-panel__action-label" }, kind === "agent" ? labels.endedAgents : labels.endedTerminals),
581
+ h("span", null, String(ended.length)), Icon("chevronRight")));
582
+ return h(
583
+ "div",
584
+ {
585
+ className: "harness-status-panel__ended harness-status-panel__ended--separated",
586
+ "data-ended-kind": kind,
587
+ },
588
+ h(
589
+ "div",
590
+ { className: "harness-status-panel__ended-header" },
591
+ Icon("checkCircle", { className: "harness-status-panel__ended-icon" }),
592
+ h("span", null, labels.ended),
593
+ h("span", { className: "harness-status-panel__ended-count" }, String(ended.length)),
594
+ ),
595
+ h(
596
+ "ul",
597
+ { style: { listStyle: "none", margin: 0, padding: 0 } },
598
+ ended.map((row) =>
599
+ h(
600
+ "li",
601
+ {
602
+ key: row.id || row.runId,
603
+ className: "harness-status-panel__ended-row",
604
+ "data-row-kind": kind,
605
+ "data-row-state": row[statusKey],
606
+ "data-row-ended": "true",
607
+ },
608
+ typeof openCallback === "function"
609
+ ? h("button", {
610
+ type: "button",
611
+ className: "harness-status-panel__row-open",
612
+ "data-row-open": kind,
613
+ "aria-label": (kind === "terminal" ? labels.openTerminal : labels.openSubagent + ":{title}").replace(
614
+ "{title}",
615
+ row.title || row.id || row.runId || "",
616
+ ),
617
+ onClick: (event) => {
618
+ event.stopPropagation();
619
+ openCallback(row);
620
+ },
621
+ })
622
+ : null,
623
+ Icon(iconName, { className: "harness-status-panel__row-icon" }),
624
+ h("span", { className: "harness-status-panel__row-title" }, row.title || row.id || row.runId || ""),
625
+ Icon("chevronRight", { className: "harness-status-panel__ended-chevron" }),
626
+ ),
627
+ ),
628
+ ),
629
+ );
630
+ }
631
+
632
+ function Section({ section, title, separated, open, defaultOpen = true, onOpenChange, trailing, children, footer }) {
633
+ const [uncontrolled, setUncontrolled] = useState(defaultOpen);
634
+ const isControlled = open !== undefined && open !== null;
635
+ const isOpen = isControlled ? open : uncontrolled;
636
+ const toggle = () => {
637
+ const next = !isOpen;
638
+ if (!isControlled) setUncontrolled(next);
639
+ if (typeof onOpenChange === "function") onOpenChange(next);
640
+ };
641
+ return h(
642
+ "div",
643
+ {
644
+ className: `harness-status-panel__section${separated ? " harness-status-panel__section--separated" : ""}`,
645
+ "data-status-section": section,
646
+ "data-state": isOpen ? "open" : "closed",
647
+ },
648
+ h(
649
+ "div",
650
+ { className: "harness-status-panel__section-header" },
651
+ h(
652
+ "button",
653
+ {
654
+ type: "button",
655
+ className: "harness-status-panel__section-trigger",
656
+ "aria-expanded": isOpen,
657
+ "data-status-section-trigger": section,
658
+ onClick: toggle,
659
+ },
660
+ h("span", { className: "harness-status-panel__section-title" }, title),
661
+ Icon(isOpen ? "chevronDown" : "chevronRight", { className: "harness-status-panel__chevron" }),
662
+ ),
663
+ trailing ? h("div", { className: "harness-status-panel__section-trailing" }, trailing(isOpen)) : null,
664
+ ),
665
+ isOpen ? h("div", { className: "harness-status-panel__section-content" }, children, footer) : null,
666
+ );
667
+ }
668
+
669
+ function gitDeltaSpan(git) {
670
+ return h(
671
+ "span",
672
+ { className: "harness-status-panel__git-delta" },
673
+ h("span", { className: "harness-status-panel__added" }, `+${git.added || 0}`),
674
+ " ",
675
+ h("span", { className: "harness-status-panel__removed" }, `-${git.removed || 0}`),
676
+ );
677
+ }
678
+
679
+ function GitSection({ git, onOpenGitReview, onGitBranch, onGitCommitPush, labels, separated }) {
680
+ const hasChanges = (git.added || 0) + (git.removed || 0) > 0;
681
+ const reviewRow = onOpenGitReview
682
+ ? h(
683
+ "button",
684
+ {
685
+ type: "button",
686
+ className: "harness-status-panel__action",
687
+ "data-git-action": "review",
688
+ onClick: () => onOpenGitReview(),
689
+ },
690
+ Icon("fileDiff", { className: "harness-status-panel__action-icon" }),
691
+ h("span", { className: "harness-status-panel__action-label" }, labels.changes),
692
+ gitDeltaSpan(git),
693
+ )
694
+ : h(
695
+ "div",
696
+ { className: "harness-status-panel__action-static", "data-git-action": "review" },
697
+ Icon("fileDiff", { className: "harness-status-panel__action-icon" }),
698
+ h("span", { className: "harness-status-panel__action-label" }, labels.changes),
699
+ gitDeltaSpan(git),
700
+ );
701
+ const branchRow = (git.branchName || onGitBranch) &&
702
+ (onGitBranch
703
+ ? h(
704
+ "button",
705
+ {
706
+ type: "button",
707
+ className: "harness-status-panel__action",
708
+ "data-git-action": "branch",
709
+ onClick: () => onGitBranch(),
710
+ },
711
+ Icon("gitBranch", { className: "harness-status-panel__action-icon" }),
712
+ h("span", { className: "harness-status-panel__action-label" }, git.branchName || labels.branch),
713
+ git.dirtyFileCount ? h("span", { className: "harness-status-panel__git-dirty" }, `· ${git.dirtyFileCount}`) : null,
714
+ )
715
+ : h(
716
+ "div",
717
+ { className: "harness-status-panel__action-static", "data-git-action": "branch" },
718
+ Icon("gitBranch", { className: "harness-status-panel__action-icon" }),
719
+ h("span", { className: "harness-status-panel__action-label" }, git.branchName || labels.branch),
720
+ git.dirtyFileCount ? h("span", { className: "harness-status-panel__git-dirty" }, `· ${git.dirtyFileCount}`) : null,
721
+ ));
722
+ const commitRow = onGitCommitPush
723
+ ? h(
724
+ "button",
725
+ {
726
+ type: "button",
727
+ className: "harness-status-panel__action",
728
+ "data-git-action": "commit-push",
729
+ onClick: () => onGitCommitPush(),
730
+ },
731
+ Icon("gitCommit", { className: "harness-status-panel__action-icon" }),
732
+ h("span", { className: "harness-status-panel__action-label" }, labels.commitPush),
733
+ )
734
+ : null;
735
+ return h(
736
+ Section,
737
+ {
738
+ section: "environment",
739
+ title: labels.environment,
740
+ separated,
741
+ trailing: (isOpen) =>
742
+ isOpen
743
+ ? null
744
+ : h(
745
+ "span",
746
+ { className: "harness-status-panel__git-delta" },
747
+ h("span", { className: "harness-status-panel__added", style: !hasChanges ? { opacity: 0.5 } : null }, `+${git.added || 0}`),
748
+ " ",
749
+ h("span", { className: "harness-status-panel__removed", style: !hasChanges ? { opacity: 0.5 } : null }, `-${git.removed || 0}`),
750
+ ),
751
+ },
752
+ h("div", null, reviewRow, branchRow, commitRow),
753
+ );
754
+ }
755
+
756
+ function PlanRow({ item }) {
757
+ const kind = planStatusKind(item.status);
758
+ const iconName = kind === "completed" ? "checkCircle" : kind === "in_progress" ? "arrowRight" : "circle";
759
+ const iconClass = `harness-status-panel__plan-icon harness-status-panel__plan-icon--${kind === "completed" ? "done" : kind === "in_progress" ? "active" : "pending"}`;
760
+ return h(
761
+ "li",
762
+ {
763
+ className: "harness-status-panel__plan-item",
764
+ "data-plan-status": kind,
765
+ "data-plan-id": item.id,
766
+ },
767
+ Icon(iconName, { className: iconClass }),
768
+ h(
769
+ "span",
770
+ {
771
+ title: item.content || "",
772
+ className: `harness-status-panel__plan-content${kind === "completed" ? " harness-status-panel__plan-content--done" : ""}`,
773
+ },
774
+ item.content || "",
775
+ ),
776
+ );
777
+ }
778
+
779
+ function FoldPreview({ group, items, labels }) {
780
+ const allCompleted = items.every(i => planStatusKind(i?.status) === "completed");
781
+ const allPending = items.every(i => planStatusKind(i?.status) === "pending");
782
+ const template = group === "preceding" ? allCompleted ? labels.todoCompletedFold : labels.todoEarlierFold
783
+ : allPending ? labels.todoWaitingFold : labels.todoLaterFold;
784
+ const text = String(template).replace("{count}", String(items.length));
785
+ const ref = useRef(null), previewRef = useRef(null), closeTimer = useRef(null);
786
+ const [position, setPosition] = useState(null);
787
+ const close = () => { clearTimeout(closeTimer.current); closeTimer.current = setTimeout(() => setPosition(null), 120); };
788
+ const show = () => {
789
+ clearTimeout(closeTimer.current);
790
+ const node = ref.current;
791
+ if (!node) return;
792
+ const rect = node.getBoundingClientRect(), doc = node.ownerDocument.documentElement;
793
+ const width = Math.min(320, doc.clientWidth - 32), height = Math.min(320, doc.clientHeight - 32);
794
+ const above = rect.top >= height + 8;
795
+ setPosition({ left: Math.max(16, Math.min(rect.left, doc.clientWidth - width - 16)),
796
+ top: above ? Math.max(16, rect.top - height - 8) : Math.min(rect.bottom + 8, doc.clientHeight - height - 16), width, maxHeight: height });
797
+ };
798
+ useEffect(() => () => clearTimeout(closeTimer.current), []);
799
+ const preview = h("div", { ref: previewRef, role: "tooltip", className: "harness-status-panel__fold-preview",
800
+ "data-fold-preview": group,
801
+ style: position && ReactDOM.createPortal ? { ...position, position: "fixed", display: "block", overflowY: "auto", pointerEvents: "auto" } : undefined,
802
+ onMouseEnter: () => clearTimeout(closeTimer.current), onMouseLeave: close },
803
+ h("div", { className: "harness-status-panel__fold-preview-header" }, text),
804
+ h("ul", { className: "harness-status-panel__plan-list" }, items.map(i => h(PlanRow, { key: i.id, item: i }))));
805
+ const root = ref.current?.closest(".harness-status-panel-root");
806
+ return h("div", { ref, className: "harness-status-panel__fold", "data-fold-group": group,
807
+ onMouseEnter: show, onMouseLeave: close, onFocus: show,
808
+ onBlur: event => { if (!ref.current?.contains(event.relatedTarget) && !previewRef.current?.contains(event.relatedTarget)) close(); },
809
+ onKeyDown: event => { if (event.key === "Escape") { clearTimeout(closeTimer.current); setPosition(null); } } },
810
+ h("button", { type: "button", className: "harness-status-panel__fold-trigger", "aria-expanded": !!position,
811
+ "data-fold-trigger": group, onClick: () => position ? setPosition(null) : show() },
812
+ Icon("chevronLeft", { className: "harness-status-panel__fold-chevron" }), h("span", null, text)),
813
+ root && ReactDOM.createPortal ? position && ReactDOM.createPortal(preview, root) : preview);
814
+ }
815
+
816
+ function PlanItems({ items, labels }) {
817
+ const win = getStatusPanelTodoFocusWindow(items);
818
+ return h(
819
+ "ul",
820
+ { className: "harness-status-panel__plan-list" },
821
+ win.compact && win.precedingItems.length > 0
822
+ ? h("li", { key: "preceding" }, h(FoldPreview, { group: "preceding", items: win.precedingItems, labels }))
823
+ : null,
824
+ win.focusItems.map((item) => h(PlanRow, { key: item.id, item })),
825
+ win.compact && win.followingItems.length > 0
826
+ ? h("li", { key: "following" }, h(FoldPreview, { group: "following", items: win.followingItems, labels }))
827
+ : null,
828
+ );
829
+ }
830
+
831
+ function PlanSection({ plan, labels, separated }) {
832
+ const items = (plan && plan.items) || [];
833
+ const completedCount =
834
+ plan.completedCount != null
835
+ ? plan.completedCount
836
+ : items.filter((i) => planStatusKind(i && i.status) === "completed").length;
837
+ const totalCount = plan.totalCount != null ? plan.totalCount : items.length;
838
+ const isCompleted = totalCount > 0 && completedCount >= totalCount;
839
+ return h(
840
+ Section,
841
+ {
842
+ section: "plan",
843
+ title: labels.todo,
844
+ separated,
845
+ trailing: () =>
846
+ h(
847
+ "span",
848
+ {
849
+ className: `harness-status-panel__plan-count${isCompleted ? " harness-status-panel__plan-count--done" : ""}`,
850
+ "data-plan-complete": isCompleted ? "true" : "false",
851
+ },
852
+ `${completedCount}/${totalCount}`,
853
+ ),
854
+ },
855
+ h(PlanItems, { items, labels }),
856
+ );
857
+ }
858
+
859
+ function LiveSection({ section, title, kind, rows, ended, now, openCallback, openDirectory, onCancel, labels, separated, open, onOpenChange, openValue, openValuePlural, miniValue, miniValuePlural }) {
860
+ const ordered = [...rows].sort((a, b) => (a.startedAt || 0) - (b.startedAt || 0));
861
+ const active = rows.filter(row => isRunningRow(row, kind === "terminal" ? "state" : "phase"));
862
+ const elapsed = longestElapsedSeconds(active, now);
863
+ return h(
864
+ Section,
865
+ {
866
+ section,
867
+ title,
868
+ separated,
869
+ defaultOpen: false,
870
+ open,
871
+ onOpenChange,
872
+ trailing: (isOpen) =>
873
+ rows.length === 0 ? null : active.length === 0 ? h("span", null, labels.unconfirmed) : isOpen
874
+ ? h("span", null, formatCount(openValue, openValuePlural, active.length))
875
+ : [
876
+ elapsed ? h("span", { key: "el", className: "harness-status-panel__elapsed" }, formatDurationUnits(elapsed, labels)) : null,
877
+ elapsed ? h("span", { key: "sep" }, "·") : null,
878
+ h("span", { key: "cnt" }, formatCount(miniValue, miniValuePlural, active.length)),
879
+ ],
880
+ footer: h(EndedDirectory, { ended, kind, openCallback, openDirectory, labels }),
881
+ },
882
+ h(
883
+ "ul",
884
+ { style: { listStyle: "none", margin: 0, padding: 0 } },
885
+ ordered.map((row) =>
886
+ h(Row, {
887
+ key: row.id || row.runId,
888
+ row,
889
+ kind,
890
+ now,
891
+ openCallback,
892
+ onCancel,
893
+ labels,
894
+ }),
895
+ ),
896
+ ),
897
+ );
898
+ }
899
+
900
+ function StatusSummaryRow({ model, labels, onExpand }) {
901
+ const summary = resolveStatusPanelMiniSummary(model, labels);
902
+ if (!summary) return null;
903
+ return h(
904
+ "button",
905
+ {
906
+ type: "button",
907
+ className: "harness-status-panel__mini-trigger",
908
+ "aria-label": labels.showPanel,
909
+ "data-mini-summary": summary.key,
910
+ onClick: onExpand,
911
+ },
912
+ h(
913
+ "span",
914
+ { className: "harness-status-panel__mini-metric" },
915
+ summary.runningDot
916
+ ? h("span", { className: "harness-status-panel__running-dot", "aria-hidden": true })
917
+ : h(
918
+ "span",
919
+ { className: "harness-status-panel__mini-icon" },
920
+ Icon(summary.icon, { className: "harness-status-panel__mini-icon-base" }),
921
+ Icon("maximize2", { className: "harness-status-panel__mini-icon-hover" }),
922
+ ),
923
+ h("span", { className: "harness-status-panel__mini-text" }, summary.text),
924
+ summary.key === "git"
925
+ ? h(
926
+ "span",
927
+ { className: "harness-status-panel__git-delta" },
928
+ h("span", { className: "harness-status-panel__added" }, `+${summary.added}`),
929
+ " ",
930
+ h("span", { className: "harness-status-panel__removed" }, `-${summary.removed}`),
931
+ )
932
+ : null,
933
+ summary.key === "endedDirectory"
934
+ ? h("span", { className: "harness-status-panel__mini-count" }, String(summary.count))
935
+ : null,
936
+ ),
937
+ );
938
+ }
939
+
940
+ function MenuButton({ label, autoLabel, onAuto, variant }) {
941
+ const [open, setOpen] = useState(false);
942
+ const ref = useRef(null);
943
+ useEffect(() => {
944
+ if (!open) return;
945
+ const handler = (event) => {
946
+ if (ref.current && event.target && !ref.current.contains(event.target)) setOpen(false);
947
+ };
948
+ if (typeof document !== "undefined") {
949
+ document.addEventListener("mousedown", handler);
950
+ return () => document.removeEventListener("mousedown", handler);
951
+ }
952
+ }, [open]);
953
+ return h(
954
+ "div",
955
+ { className: "harness-status-panel__menu", ref },
956
+ h(
957
+ "button",
958
+ {
959
+ type: "button",
960
+ className: "harness-status-panel__ctrl",
961
+ "aria-label": label,
962
+ "aria-expanded": open,
963
+ "aria-haspopup": "menu",
964
+ onKeyDown: event => { if (event.key === "Escape") { setOpen(false); event.currentTarget.focus(); } },
965
+ "data-display-mode-menu": "true",
966
+ onClick: () => setOpen(!open),
967
+ },
968
+ Icon("ellipsis"),
969
+ ),
970
+ open
971
+ ? h(
972
+ "div",
973
+ { className: "harness-status-panel__menu-popover", role: "menu", onKeyDown: event => { if (event.key === "Escape") { setOpen(false); ref.current?.querySelector("button")?.focus(); } } },
974
+ h(
975
+ "button",
976
+ {
977
+ type: "button",
978
+ role: "menuitemradio",
979
+ "aria-checked": variant === "auto",
980
+ className: "harness-status-panel__menu-item",
981
+ "data-display-mode-value": "auto",
982
+ onClick: () => {
983
+ setOpen(false);
984
+ onAuto();
985
+ ref.current?.querySelector("button")?.focus();
986
+ },
987
+ },
988
+ autoLabel,
989
+ ),
990
+ )
991
+ : null,
992
+ );
993
+ }
994
+
995
+ function ConversationStatusPanel(props) {
996
+ const labels = { ...DEFAULT_LABELS, ...(props.labels || {}) };
997
+ const variant = props.variant || "auto";
998
+ const model = props.model || {};
999
+ const git = model.git || null;
1000
+ const plan = model.plan && model.plan.items && model.plan.items.length > 0 ? model.plan : null;
1001
+ const agents = Array.isArray(model.agents) ? model.agents : [];
1002
+ const terminals = Array.isArray(model.terminals) ? model.terminals : [];
1003
+ const { current: currentTerminals, ended: endedTerminals } = classifyRowsByStatus(terminals, "state");
1004
+ const { current: currentAgents, ended: endedAgents } = classifyRowsByStatus(agents, "phase");
1005
+
1006
+ const hasGit = !!git;
1007
+ const hasPlan = !!plan;
1008
+ const hasTerminals = currentTerminals.length > 0 || endedTerminals.length > 0;
1009
+ const hasAgents = currentAgents.length > 0 || endedAgents.length > 0;
1010
+ const hasContent = hasGit || hasPlan || hasTerminals || hasAgents || !!model.isRunning;
1011
+
1012
+ const controlledNow = props.now;
1013
+ const [nowState, setNowState] = useState(() => (controlledNow != null ? controlledNow : Date.now()));
1014
+ useEffect(() => {
1015
+ if (controlledNow != null) return;
1016
+ if (currentTerminals.length === 0 && currentAgents.length === 0) return;
1017
+ if (typeof window === "undefined") return;
1018
+ const id = window.setInterval(() => setNowState(Date.now()), 1000);
1019
+ return () => window.clearInterval(id);
1020
+ }, [controlledNow, currentTerminals.length, currentAgents.length]);
1021
+ const now = controlledNow != null ? controlledNow : nowState;
1022
+
1023
+ const handleExpand = useCallback(() => {
1024
+ if (typeof props.onVariantChange === "function") props.onVariantChange("panel");
1025
+ }, [props.onVariantChange]);
1026
+ const handleMini = useCallback(() => {
1027
+ if (typeof props.onVariantChange === "function") props.onVariantChange("mini");
1028
+ }, [props.onVariantChange]);
1029
+ const handleAuto = useCallback(() => {
1030
+ if (typeof props.onVariantChange === "function") props.onVariantChange("auto");
1031
+ }, [props.onVariantChange]);
1032
+
1033
+ const onCancel = typeof props.onCancel === "function" ? props.onCancel : null;
1034
+ const state = variant === "mini" ? "collapsed" : "expanded";
1035
+
1036
+ const gitSeparated = false;
1037
+ const planSeparated = hasGit;
1038
+ const terminalsSeparated = hasGit || hasPlan;
1039
+ const agentsSeparated = hasGit || hasPlan || hasTerminals;
1040
+
1041
+ if (!hasContent) return null;
1042
+ return h(
1043
+ "div",
1044
+ { "data-theme": props.theme, className: `harness-status-panel-root${props.className ? " " + props.className : ""}` },
1045
+ h(
1046
+ "aside",
1047
+ {
1048
+ className: "harness-status-panel",
1049
+ "data-state": state,
1050
+ "data-display-mode": variant,
1051
+ "data-running": String(!!model.isRunning),
1052
+ "data-running-terminal-count": String(currentTerminals.filter(row => isRunningRow(row, "state")).length),
1053
+ "data-running-agent-count": String(currentAgents.filter(row => isRunningRow(row, "phase")).length),
1054
+ "data-ended-terminal-count": String(endedTerminals.length),
1055
+ "data-ended-agent-count": String(endedAgents.length),
1056
+ "aria-label": labels.title,
1057
+ role: "complementary",
1058
+ },
1059
+ variant !== "mini"
1060
+ ? h(
1061
+ "div",
1062
+ { className: "harness-status-panel__controls" },
1063
+ h(MenuButton, { label: labels.displayMode, autoLabel: labels.displayModeAuto, onAuto: handleAuto, variant }),
1064
+ h(
1065
+ "button",
1066
+ {
1067
+ type: "button",
1068
+ className: "harness-status-panel__ctrl",
1069
+ "aria-label": labels.showMini,
1070
+ "data-collapse-to-mini": "true",
1071
+ onClick: handleMini,
1072
+ },
1073
+ Icon("minimize2"),
1074
+ ),
1075
+ )
1076
+ : null,
1077
+ variant !== "mini"
1078
+ ? h(
1079
+ "div",
1080
+ { className: "harness-status-panel__sections" },
1081
+ hasGit
1082
+ ? h(GitSection, {
1083
+ git,
1084
+ onOpenGitReview: props.onOpenGitReview,
1085
+ onGitBranch: props.onGitBranch,
1086
+ onGitCommitPush: props.onGitCommitPush,
1087
+ labels,
1088
+ separated: gitSeparated,
1089
+ })
1090
+ : null,
1091
+ hasPlan
1092
+ ? h(PlanSection, { plan, labels, separated: planSeparated })
1093
+ : null,
1094
+ hasTerminals
1095
+ ? h(LiveSection, {
1096
+ section: "terminals",
1097
+ title: labels.terminals,
1098
+ kind: "terminal",
1099
+ rows: currentTerminals,
1100
+ ended: endedTerminals,
1101
+ now,
1102
+ openCallback: props.onOpenTerminal,
1103
+ openDirectory: props.onOpenTerminalDirectory,
1104
+ onCancel,
1105
+ labels,
1106
+ separated: terminalsSeparated,
1107
+ open: props.terminalSectionOpen,
1108
+ onOpenChange: props.onTerminalSectionOpenChange,
1109
+ openValue: labels.terminalsOpenValue,
1110
+ openValuePlural: labels.terminalsOpenValuePlural,
1111
+ miniValue: labels.terminalsMiniValue,
1112
+ miniValuePlural: labels.terminalsMiniValuePlural,
1113
+ })
1114
+ : null,
1115
+ hasAgents
1116
+ ? h(LiveSection, {
1117
+ section: "agents",
1118
+ title: labels.agents,
1119
+ kind: "agent",
1120
+ rows: currentAgents,
1121
+ ended: endedAgents,
1122
+ now,
1123
+ openCallback: props.onOpenSubagent,
1124
+ openDirectory: props.onOpenSubagentDirectory,
1125
+ onCancel,
1126
+ labels,
1127
+ separated: agentsSeparated,
1128
+ open: props.agentSectionOpen,
1129
+ onOpenChange: props.onAgentSectionOpenChange,
1130
+ openValue: labels.agentsOpenValue,
1131
+ openValuePlural: labels.agentsOpenValuePlural,
1132
+ miniValue: labels.agentsMiniValue,
1133
+ miniValuePlural: labels.agentsMiniValuePlural,
1134
+ })
1135
+ : null,
1136
+ )
1137
+ : null,
1138
+ h(
1139
+ "div",
1140
+ { className: "harness-status-panel__mini", "aria-hidden": variant === "panel" },
1141
+ h(StatusSummaryRow, { model, labels, onExpand: handleExpand }),
1142
+ ),
1143
+ ),
1144
+ );
1145
+ }
1146
+
1147
+ function SubagentDirectory({ agents = [], onOpenSubagent, theme, labels: suppliedLabels }) {
1148
+ const labels = { ...DEFAULT_LABELS, ...suppliedLabels };
1149
+ return h("div", { className: "harness-status-panel-root", "data-theme": theme, style: { position: "relative", display: "block", padding: "1rem", pointerEvents: "auto" } },
1150
+ h("style", null, statusPanelStyles),
1151
+ h("h3", { style: { margin: "0 0 1rem", font: "inherit" } }, labels.endedAgents),
1152
+ h("ul", { style: { listStyle: "none", padding: 0, margin: 0 } },
1153
+ agents.filter(row => isEndedStatus(row.phase)).map(row => h(Row, { key: row.runId, row, kind: "agent", now: Date.now(), openCallback: onOpenSubagent, labels }))));
1154
+ }
1155
+ return { ConversationStatusPanel: memo ? memo(ConversationStatusPanel) : ConversationStatusPanel, SubagentDirectory };
1156
+ }