one-code-extension 0.1.4 → 0.3.0
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/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow run viewer (pure): key decoding, phase grouping, the two-level
|
|
3
|
+
* drill-down layout (Phases → agents of a phase → agent detail), and the
|
|
4
|
+
* below-editor status strip — Claude Code's workflow UI, aligned to a frame
|
|
5
|
+
* capture of CC 2.1.233. The thin ctx.ui.custom component in index.ts owns
|
|
6
|
+
* nothing but mutable state, event subscriptions, and repaint calls (same
|
|
7
|
+
* split as plan-mode's viewer).
|
|
8
|
+
*
|
|
9
|
+
* Every rendered line must stay within the width given: pi-tui crashes the
|
|
10
|
+
* whole app on an overwide line. All cells are cut/padded as PLAIN text
|
|
11
|
+
* before painting so ANSI escapes never enter the width accounting.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { cutPlainText as cut, formatDuration, padPlainText, splitCell, splitRow, visibleWidth, wrapPlainText } from "../lib/tui-render.ts";
|
|
15
|
+
import { formatTokenCount } from "../subagents/usage.ts";
|
|
16
|
+
import type { AgentRecord, RunStatus } from "./types.ts";
|
|
17
|
+
|
|
18
|
+
/** Theme access the renderer needs: fg tokens ("accent", "dim", "error", …) and bold. */
|
|
19
|
+
export interface ViewerPaint {
|
|
20
|
+
fg(color: string, text: string): string;
|
|
21
|
+
bold(text: string): string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Immutable snapshot of one run, built from RunHandle each render. */
|
|
25
|
+
export interface ViewerRunSnapshot {
|
|
26
|
+
runId: string;
|
|
27
|
+
name: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
status: RunStatus;
|
|
30
|
+
startedAt: number;
|
|
31
|
+
finishedAt?: number;
|
|
32
|
+
errorMessage?: string;
|
|
33
|
+
agents: AgentRecord[];
|
|
34
|
+
/** Phase titles declared in the script's meta, in declared order. */
|
|
35
|
+
declaredPhases?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Mutable viewer state owned by the wiring component. */
|
|
39
|
+
export interface ViewerState {
|
|
40
|
+
runIndex: number;
|
|
41
|
+
/** Drill level: the phase list, or the agents of the selected phase. */
|
|
42
|
+
level: "phases" | "agents";
|
|
43
|
+
phaseCursor: number;
|
|
44
|
+
/** Cursor within the selected phase's agents (agents level only). */
|
|
45
|
+
agentCursor: number;
|
|
46
|
+
detailScroll: number;
|
|
47
|
+
promptExpanded: boolean;
|
|
48
|
+
/** Set after `s` hit an existing, different saved file — next `s` overwrites. */
|
|
49
|
+
confirmOverwrite: boolean;
|
|
50
|
+
/** Transient footer message (save confirmation etc.). */
|
|
51
|
+
notice?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function initialViewerState(runIndex = 0): ViewerState {
|
|
55
|
+
return {
|
|
56
|
+
runIndex,
|
|
57
|
+
level: "phases",
|
|
58
|
+
phaseCursor: 0,
|
|
59
|
+
agentCursor: 0,
|
|
60
|
+
detailScroll: 0,
|
|
61
|
+
promptExpanded: false,
|
|
62
|
+
confirmOverwrite: false,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type ViewerKey =
|
|
67
|
+
| { kind: "up" }
|
|
68
|
+
| { kind: "down" }
|
|
69
|
+
| { kind: "pageUp" }
|
|
70
|
+
| { kind: "pageDown" }
|
|
71
|
+
| { kind: "nextRun" }
|
|
72
|
+
| { kind: "enter" }
|
|
73
|
+
| { kind: "back" }
|
|
74
|
+
| { kind: "stop" }
|
|
75
|
+
| { kind: "save" }
|
|
76
|
+
| { kind: "close" };
|
|
77
|
+
|
|
78
|
+
export function decodeViewerKey(data: string): ViewerKey | undefined {
|
|
79
|
+
switch (data) {
|
|
80
|
+
case "\x1b[A":
|
|
81
|
+
case "\x1bOA":
|
|
82
|
+
return { kind: "up" };
|
|
83
|
+
case "\x1b[B":
|
|
84
|
+
case "\x1bOB":
|
|
85
|
+
return { kind: "down" };
|
|
86
|
+
case "\x1b[5~":
|
|
87
|
+
return { kind: "pageUp" };
|
|
88
|
+
case "\x1b[6~":
|
|
89
|
+
return { kind: "pageDown" };
|
|
90
|
+
case "\t":
|
|
91
|
+
return { kind: "nextRun" };
|
|
92
|
+
case "\r":
|
|
93
|
+
case "\n":
|
|
94
|
+
return { kind: "enter" };
|
|
95
|
+
case "\x1b":
|
|
96
|
+
return { kind: "back" };
|
|
97
|
+
case "x":
|
|
98
|
+
case "X":
|
|
99
|
+
return { kind: "stop" };
|
|
100
|
+
case "s":
|
|
101
|
+
case "S":
|
|
102
|
+
return { kind: "save" };
|
|
103
|
+
case "q":
|
|
104
|
+
case "\x03": // ctrl+c — close from anywhere, like q
|
|
105
|
+
return { kind: "close" };
|
|
106
|
+
default:
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Keys the below-editor status strip responds to while it holds soft focus
|
|
113
|
+
* (Claude Code's down-arrow-to-the-workflow-entry flow). Anything it does not
|
|
114
|
+
* recognize means the user started typing — the wiring drops focus and lets
|
|
115
|
+
* the byte through to the editor.
|
|
116
|
+
*/
|
|
117
|
+
export type StatusKey = "up" | "down" | "open" | "leave" | "stop";
|
|
118
|
+
|
|
119
|
+
/** The strip understands a subset of the viewer's keys, renamed to its intents. */
|
|
120
|
+
const STATUS_KEY_FROM_VIEWER: Partial<Record<ViewerKey["kind"], StatusKey>> = {
|
|
121
|
+
up: "up",
|
|
122
|
+
down: "down",
|
|
123
|
+
enter: "open",
|
|
124
|
+
back: "leave",
|
|
125
|
+
stop: "stop",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export function decodeStatusKey(data: string): StatusKey | undefined {
|
|
129
|
+
const key = decodeViewerKey(data);
|
|
130
|
+
return key && STATUS_KEY_FROM_VIEWER[key.kind];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** What pressing `s` should do, given what's on disk. Wiring does the fs. */
|
|
134
|
+
export function planSave(input: {
|
|
135
|
+
name: string;
|
|
136
|
+
exists: boolean;
|
|
137
|
+
sameContent: boolean;
|
|
138
|
+
confirmed: boolean;
|
|
139
|
+
}): { action: "write" | "confirm" | "noop"; notice: string } {
|
|
140
|
+
const target = `.claude/workflows/${input.name}.js`;
|
|
141
|
+
if (!input.exists || input.sameContent) return { action: "write", notice: `saved to ${target}` };
|
|
142
|
+
if (input.confirmed) return { action: "write", notice: `overwrote ${target}` };
|
|
143
|
+
return { action: "confirm", notice: `${target} exists and differs — press s again to overwrite` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Phase grouping
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
export interface PhaseGroup {
|
|
151
|
+
title: string;
|
|
152
|
+
/** Whether any agent has run (declared-only phases render dim, countless). */
|
|
153
|
+
started: boolean;
|
|
154
|
+
agents: { record: AgentRecord; agentIndex: number }[];
|
|
155
|
+
done: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Declared phases (script meta) first in declared order, then any phase seen
|
|
160
|
+
* in the event stream that was not declared (child workflows' `▸ name` groups,
|
|
161
|
+
* ad-hoc phase() titles), then a default "agents" group for phaseless agents.
|
|
162
|
+
*/
|
|
163
|
+
export function buildPhases(run: ViewerRunSnapshot): PhaseGroup[] {
|
|
164
|
+
const groups: PhaseGroup[] = [];
|
|
165
|
+
const byTitle = new Map<string, PhaseGroup>();
|
|
166
|
+
const add = (title: string): PhaseGroup => {
|
|
167
|
+
let group = byTitle.get(title);
|
|
168
|
+
if (!group) {
|
|
169
|
+
group = { title, started: false, agents: [], done: 0 };
|
|
170
|
+
byTitle.set(title, group);
|
|
171
|
+
groups.push(group);
|
|
172
|
+
}
|
|
173
|
+
return group;
|
|
174
|
+
};
|
|
175
|
+
for (const title of run.declaredPhases ?? []) add(title);
|
|
176
|
+
for (const [agentIndex, record] of run.agents.entries()) {
|
|
177
|
+
const group = add(record.phase ?? "agents");
|
|
178
|
+
group.started = true;
|
|
179
|
+
group.agents.push({ record, agentIndex });
|
|
180
|
+
if (record.status !== "running") group.done++;
|
|
181
|
+
}
|
|
182
|
+
return groups;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Clamp state against the current snapshot (agents appear while live). */
|
|
186
|
+
export function clampViewerState(state: ViewerState, runs: ViewerRunSnapshot[]): void {
|
|
187
|
+
state.runIndex = runs.length ? Math.min(state.runIndex, runs.length - 1) : 0;
|
|
188
|
+
const run = runs[state.runIndex];
|
|
189
|
+
const phases = run ? buildPhases(run) : [];
|
|
190
|
+
state.phaseCursor = phases.length ? Math.min(Math.max(0, state.phaseCursor), phases.length - 1) : 0;
|
|
191
|
+
const agents = phases[state.phaseCursor]?.agents.length ?? 0;
|
|
192
|
+
state.agentCursor = agents ? Math.min(Math.max(0, state.agentCursor), agents - 1) : 0;
|
|
193
|
+
if (agents === 0 && state.level === "agents") state.level = "phases";
|
|
194
|
+
if (state.detailScroll < 0) state.detailScroll = 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Formatting helpers (all plain text; painting happens at the very end)
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
/** Cut then pad plain text to exactly `width` columns. */
|
|
202
|
+
const cell = padPlainText;
|
|
203
|
+
|
|
204
|
+
// The splitRow/splitCell layout rule (left half padded against a right-aligned
|
|
205
|
+
// annotation, fused when narrow) is shared from lib/tui-render.ts — the
|
|
206
|
+
// header, the pane rows, and the status strip all build on it.
|
|
207
|
+
|
|
208
|
+
const STATUS_MARK: Record<AgentRecord["status"], string> = {
|
|
209
|
+
running: "●",
|
|
210
|
+
done: "✔",
|
|
211
|
+
failed: "✗",
|
|
212
|
+
replayed: "↻",
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Boxed panes
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
interface PaneLine {
|
|
220
|
+
text: string;
|
|
221
|
+
style?: "accent" | "dim" | "error" | "bold";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A bordered pane with a title in the top border, Claude Code style:
|
|
226
|
+
* ┌ Title ────┐ / │ content │ rows / └────┘. `height` includes both borders;
|
|
227
|
+
* content is cut/padded to the inner width before painting.
|
|
228
|
+
*/
|
|
229
|
+
function boxPane(title: string, lines: PaneLine[], width: number, height: number, paint: ViewerPaint): string[] {
|
|
230
|
+
const innerWidth = Math.max(1, width - 4);
|
|
231
|
+
const contentRows = Math.max(0, height - 2);
|
|
232
|
+
const titleCut = cut(title, Math.max(0, width - 4));
|
|
233
|
+
const dashes = "─".repeat(Math.max(0, width - visibleWidth(titleCut) - 4));
|
|
234
|
+
const out: string[] = [paint.fg("dim", "┌ ") + paint.bold(titleCut) + paint.fg("dim", ` ${dashes}┐`)];
|
|
235
|
+
for (let i = 0; i < contentRows; i++) {
|
|
236
|
+
const line = lines[i];
|
|
237
|
+
const plain = cell(line?.text ?? "", innerWidth);
|
|
238
|
+
const painted = !line?.style ? plain : line.style === "bold" ? paint.bold(plain) : paint.fg(line.style, plain);
|
|
239
|
+
out.push(`${paint.fg("dim", "│")} ${painted} ${paint.fg("dim", "│")}`);
|
|
240
|
+
}
|
|
241
|
+
out.push(paint.fg("dim", `└${"─".repeat(Math.max(0, width - 2))}┘`));
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Slice rows so the cursor stays visible in a `visible`-row window. */
|
|
246
|
+
function scrollWindow<T>(rows: T[], cursor: number, visible: number): T[] {
|
|
247
|
+
let offset = 0;
|
|
248
|
+
if (cursor >= visible) offset = cursor - visible + 1;
|
|
249
|
+
if (rows.length > visible) offset = Math.min(offset, rows.length - visible);
|
|
250
|
+
return rows.slice(offset, offset + visible);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Pane content builders
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
/** Left pane, phases level: `✔ Survey 1/1` / `2 Analyze 0/5` / `3 Synthesize`. */
|
|
258
|
+
function phaseRows(phases: PhaseGroup[], cursor: number, innerWidth: number): PaneLine[] {
|
|
259
|
+
return phases.map((phase, index) => {
|
|
260
|
+
const selected = index === cursor;
|
|
261
|
+
const allDone = phase.started && phase.done === phase.agents.length;
|
|
262
|
+
const mark = selected ? "❯" : allDone ? "✔" : String(index + 1);
|
|
263
|
+
const count = phase.started ? `${phase.done}/${phase.agents.length}` : "";
|
|
264
|
+
const text = splitCell(`${mark} ${phase.title}`, count, innerWidth);
|
|
265
|
+
if (selected) return { text, style: "accent" as const };
|
|
266
|
+
return { text, style: phase.started ? undefined : ("dim" as const) };
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Agent rows: `mark label Model · 31.6k tok` with a right-aligned duration —
|
|
272
|
+
* the non-interactive preview beside the Phases pane (the selectable list at
|
|
273
|
+
* the agents level is agentLabelRows).
|
|
274
|
+
*/
|
|
275
|
+
function agentRows(agents: PhaseGroup["agents"], innerWidth: number, now: number): PaneLine[] {
|
|
276
|
+
if (!agents.length) return [{ text: "No agents yet — waiting for the first agent() call…", style: "dim" }];
|
|
277
|
+
const labelWidth = Math.min(24, Math.max(10, Math.floor(innerWidth * 0.4)));
|
|
278
|
+
return agents.map(({ record }) => {
|
|
279
|
+
const meta = [record.model, record.tokens ? `${formatTokenCount(record.tokens.output)} tok` : ""]
|
|
280
|
+
.filter(Boolean)
|
|
281
|
+
.join(" · ");
|
|
282
|
+
const left = ` ${STATUS_MARK[record.status]} ${cell(record.label, labelWidth)} ${meta}`;
|
|
283
|
+
const text = splitCell(left, formatDuration(record.startedAt, record.finishedAt, now), innerWidth);
|
|
284
|
+
return { text, style: record.status === "failed" ? ("error" as const) : undefined };
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const STATUS_TITLE: Record<AgentRecord["status"], string> = {
|
|
289
|
+
running: "● Running",
|
|
290
|
+
done: "✔ Completed",
|
|
291
|
+
failed: "✗ Failed",
|
|
292
|
+
replayed: "↻ Replayed (cached)",
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/** How many wrapped prompt lines show before `⏎ expand` kicks in. */
|
|
296
|
+
export const PROMPT_PREVIEW_LINES = 3;
|
|
297
|
+
/** How many trailing tool calls the Activity section shows. */
|
|
298
|
+
export const ACTIVITY_TAIL = 3;
|
|
299
|
+
|
|
300
|
+
/** Right pane, agents level: the full agent detail. */
|
|
301
|
+
export function buildDetail(
|
|
302
|
+
record: AgentRecord | undefined,
|
|
303
|
+
width: number,
|
|
304
|
+
now: number,
|
|
305
|
+
promptExpanded: boolean,
|
|
306
|
+
): PaneLine[] {
|
|
307
|
+
if (!record) return [{ text: "No agents yet — waiting for the first agent() call…", style: "dim" }];
|
|
308
|
+
const lines: PaneLine[] = [];
|
|
309
|
+
const title = STATUS_TITLE[record.status] + (record.model ? ` · ${record.model}` : "");
|
|
310
|
+
lines.push({ text: title, style: record.status === "failed" ? "error" : "accent" });
|
|
311
|
+
const toolCalls = record.toolCalls;
|
|
312
|
+
const stats: string[] = [];
|
|
313
|
+
if (record.tokens) stats.push(`${formatTokenCount(record.tokens.output)} tok`);
|
|
314
|
+
stats.push(`${toolCalls} tool call${toolCalls === 1 ? "" : "s"}`);
|
|
315
|
+
const duration = formatDuration(record.startedAt, record.finishedAt, now);
|
|
316
|
+
if (duration) stats.push(duration);
|
|
317
|
+
if (record.cost) stats.push(`$${record.cost.toFixed(4)}`);
|
|
318
|
+
lines.push({ text: stats.join(" · "), style: "dim" });
|
|
319
|
+
|
|
320
|
+
const section = (header: string, body: string[], bodyStyle?: PaneLine["style"]) => {
|
|
321
|
+
lines.push({ text: "" });
|
|
322
|
+
lines.push({ text: header, style: "bold" });
|
|
323
|
+
for (const text of body) lines.push({ text: ` ${text}`, style: bodyStyle });
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const promptLines = record.prompt ? wrapPlainText(record.prompt, width - 2) : ["(not recorded)"];
|
|
327
|
+
if (promptLines.length > PROMPT_PREVIEW_LINES && !promptExpanded) {
|
|
328
|
+
const hidden = promptLines.length - PROMPT_PREVIEW_LINES;
|
|
329
|
+
section("Prompt", [
|
|
330
|
+
...promptLines.slice(0, PROMPT_PREVIEW_LINES),
|
|
331
|
+
`… +${hidden} more line${hidden === 1 ? "" : "s"} (⏎ to expand)`,
|
|
332
|
+
]);
|
|
333
|
+
} else {
|
|
334
|
+
section("Prompt", promptLines);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const tail = record.activity.slice(-ACTIVITY_TAIL);
|
|
338
|
+
const activityHeader =
|
|
339
|
+
toolCalls > tail.length ? `Activity · last ${tail.length} of ${toolCalls} tool calls` : "Activity";
|
|
340
|
+
section(
|
|
341
|
+
activityHeader,
|
|
342
|
+
tail.length
|
|
343
|
+
? tail.map((tool) => cut(`${tool.name}(${tool.argsSummary ?? ""})`, width - 2))
|
|
344
|
+
: [record.status === "replayed" ? "(replayed — activity not recorded)" : "No tool calls."],
|
|
345
|
+
tail.length ? undefined : "dim",
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (record.status === "failed") {
|
|
349
|
+
section("Error", wrapPlainText(record.error ?? "unknown", width - 2), "error");
|
|
350
|
+
} else {
|
|
351
|
+
section("Outcome", record.outcome ? wrapPlainText(record.outcome, width - 2) : ["Still running…"], record.outcome ? undefined : "dim");
|
|
352
|
+
}
|
|
353
|
+
return lines;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Render
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
export interface ViewerInput {
|
|
361
|
+
runs: ViewerRunSnapshot[];
|
|
362
|
+
state: ViewerState;
|
|
363
|
+
width: number;
|
|
364
|
+
height: number;
|
|
365
|
+
now: number;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const MIN_TWO_PANE_WIDTH = 70;
|
|
369
|
+
|
|
370
|
+
export function renderViewer(input: ViewerInput, paint: ViewerPaint): string[] {
|
|
371
|
+
const { runs, state } = input;
|
|
372
|
+
const width = Math.max(20, input.width);
|
|
373
|
+
if (!runs.length) {
|
|
374
|
+
return [
|
|
375
|
+
"",
|
|
376
|
+
paint.fg("accent", cut("No workflow runs this session.", width)),
|
|
377
|
+
paint.fg("dim", cut('Start one with the workflow tool (or say "ultracode"). esc close', width)),
|
|
378
|
+
"",
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
clampViewerState(state, runs);
|
|
382
|
+
const run = runs[state.runIndex];
|
|
383
|
+
const phases = buildPhases(run);
|
|
384
|
+
const phase = phases[state.phaseCursor];
|
|
385
|
+
|
|
386
|
+
const out: string[] = [];
|
|
387
|
+
|
|
388
|
+
// Header: name + right-aligned stats; description (+ run cycle hint) below.
|
|
389
|
+
const doneCount = run.agents.filter((a) => a.status !== "running").length;
|
|
390
|
+
const stats = [
|
|
391
|
+
`${doneCount}/${run.agents.length} agents`,
|
|
392
|
+
formatDuration(run.startedAt, run.finishedAt, input.now),
|
|
393
|
+
run.status === "running" ? "" : run.status,
|
|
394
|
+
]
|
|
395
|
+
.filter(Boolean)
|
|
396
|
+
.join(" · ");
|
|
397
|
+
const headerLine = (left: string, leftColor: string, right: string): string => {
|
|
398
|
+
const row = splitRow(left, right, width);
|
|
399
|
+
if ("fused" in row) return paint.fg(leftColor, row.fused);
|
|
400
|
+
return paint.fg(leftColor, row.left) + " " + paint.fg("dim", row.right);
|
|
401
|
+
};
|
|
402
|
+
out.push(headerLine(run.name, "accent", stats));
|
|
403
|
+
const runHint = runs.length > 1 ? `run ${state.runIndex + 1}/${runs.length} (tab)` : run.runId;
|
|
404
|
+
out.push(headerLine(run.description ?? "", "dim", runHint));
|
|
405
|
+
if (run.errorMessage && run.status !== "completed") {
|
|
406
|
+
out.push(paint.fg("error", cut(`⚠ ${run.errorMessage}`, width)));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Body: two boxed panes, side by side (or stacked when narrow).
|
|
410
|
+
const footerRows = 1;
|
|
411
|
+
const bodyHeight = Math.max(6, input.height - out.length - footerRows);
|
|
412
|
+
const atAgents = state.level === "agents" && phase && phase.agents.length > 0;
|
|
413
|
+
|
|
414
|
+
const agentCountTitle = phase
|
|
415
|
+
? `${phase.title} · ${phase.agents.length} agent${phase.agents.length === 1 ? "" : "s"}`
|
|
416
|
+
: "agents";
|
|
417
|
+
const leftTitle = atAgents ? agentCountTitle : "Phases";
|
|
418
|
+
const rightTitle = atAgents ? (phase.agents[state.agentCursor]?.record.label ?? "agent") : agentCountTitle;
|
|
419
|
+
const leftWidthWanted = atAgents ? 32 : 26;
|
|
420
|
+
const leftRowCount = atAgents ? phase.agents.length : phases.length;
|
|
421
|
+
|
|
422
|
+
// Pane content is built once, at the real inner widths of whichever layout runs.
|
|
423
|
+
const buildPanes = (leftInner: number, rightInner: number, leftVisible: number, rightVisible: number) => {
|
|
424
|
+
if (!atAgents) {
|
|
425
|
+
return {
|
|
426
|
+
left: scrollWindow(phaseRows(phases, state.phaseCursor, leftInner), state.phaseCursor, leftVisible),
|
|
427
|
+
// No phase groups yet (no declared phases, first agent() pending) still
|
|
428
|
+
// gets agentRows' "No agents yet…" waiting line, not a blank pane.
|
|
429
|
+
right: agentRows(phase?.agents ?? [], rightInner, input.now).slice(0, rightVisible),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
left: scrollWindow(agentLabelRows(phase.agents, state.agentCursor), state.agentCursor, leftVisible),
|
|
434
|
+
right: detailWindow(
|
|
435
|
+
buildDetail(phase.agents[state.agentCursor]?.record, rightInner, input.now, state.promptExpanded),
|
|
436
|
+
state,
|
|
437
|
+
rightVisible,
|
|
438
|
+
rightInner,
|
|
439
|
+
),
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
if (width >= MIN_TWO_PANE_WIDTH) {
|
|
444
|
+
const leftWidth = Math.min(leftWidthWanted, Math.max(18, Math.floor(width * 0.25)));
|
|
445
|
+
const rightWidth = width - leftWidth - 1;
|
|
446
|
+
const visible = bodyHeight - 2;
|
|
447
|
+
const panes = buildPanes(leftWidth - 4, rightWidth - 4, visible, visible);
|
|
448
|
+
const left = boxPane(leftTitle, panes.left, leftWidth, bodyHeight, paint);
|
|
449
|
+
const right = boxPane(rightTitle, panes.right, rightWidth, bodyHeight, paint);
|
|
450
|
+
for (let i = 0; i < bodyHeight; i++) out.push(`${left[i]} ${right[i]}`);
|
|
451
|
+
} else {
|
|
452
|
+
// Narrow: stack the two panes.
|
|
453
|
+
const leftHeight = Math.max(4, Math.min(leftRowCount + 2, Math.floor(bodyHeight / 2)));
|
|
454
|
+
const rightHeight = bodyHeight - leftHeight;
|
|
455
|
+
const inner = width - 4;
|
|
456
|
+
const panes = buildPanes(inner, inner, leftHeight - 2, rightHeight - 2);
|
|
457
|
+
out.push(...boxPane(leftTitle, panes.left, width, leftHeight, paint));
|
|
458
|
+
out.push(...boxPane(rightTitle, panes.right, width, rightHeight, paint));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Footer: per-level key hints, replaced by a transient notice when set.
|
|
462
|
+
const stopHint = run.status === "running" ? "x stop workflow · " : "";
|
|
463
|
+
const hints = !atAgents
|
|
464
|
+
? `↑↓ select · ⏎ open · ${runs.length > 1 ? "tab run · " : ""}${stopHint}s save · esc close`
|
|
465
|
+
: `↑↓ agent · ⏎ prompt · pgup/pgdn scroll · ${stopHint}s save · esc back`;
|
|
466
|
+
out.push(paint.fg("dim", cut(state.notice ?? hints, width)));
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Left pane, agents level: mark + label rows with the ❯ cursor. */
|
|
471
|
+
function agentLabelRows(agents: PhaseGroup["agents"], cursor: number): PaneLine[] {
|
|
472
|
+
return agents.map(({ record }, index) => {
|
|
473
|
+
const selected = index === cursor;
|
|
474
|
+
const text = `${selected ? "❯" : " "} ${STATUS_MARK[record.status]} ${record.label}`;
|
|
475
|
+
if (selected) return { text, style: "accent" as const };
|
|
476
|
+
return { text, style: record.status === "failed" ? ("error" as const) : undefined };
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Apply detailScroll with clamping and a `lines X–Y of N` trailer when cut. */
|
|
481
|
+
function detailWindow(detail: PaneLine[], state: ViewerState, visible: number, innerWidth: number): PaneLine[] {
|
|
482
|
+
state.detailScroll = Math.max(0, Math.min(state.detailScroll, Math.max(0, detail.length - visible)));
|
|
483
|
+
const window = detail.slice(state.detailScroll, state.detailScroll + visible);
|
|
484
|
+
if (detail.length > visible) {
|
|
485
|
+
const last = Math.min(detail.length, state.detailScroll + visible);
|
|
486
|
+
if (window.length === visible) window.pop();
|
|
487
|
+
window.push({
|
|
488
|
+
text: cut(`lines ${state.detailScroll + 1}–${last} of ${detail.length}`, innerWidth),
|
|
489
|
+
style: "dim",
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return window;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
// Status strip (the persistent below-editor rows, Claude Code's bottom entry)
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
export interface StatusRowsInput {
|
|
500
|
+
/** The runs that can appear (newest first) — callers may pass just the visible slice. */
|
|
501
|
+
runs: ViewerRunSnapshot[];
|
|
502
|
+
/** Session-wide run count behind the "+N more" line; defaults to runs.length. */
|
|
503
|
+
totalRuns?: number;
|
|
504
|
+
/** Index into `runs` of the soft-focused row; undefined when unfocused. */
|
|
505
|
+
selected?: number;
|
|
506
|
+
width: number;
|
|
507
|
+
now: number;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Rows shown before collapsing the rest into a "+N more" line. */
|
|
511
|
+
export const MAX_STATUS_ROWS = 3;
|
|
512
|
+
|
|
513
|
+
const STATUS_ROW_MARK: Record<RunStatus, string> = {
|
|
514
|
+
running: "○",
|
|
515
|
+
completed: "●",
|
|
516
|
+
failed: "✗",
|
|
517
|
+
aborted: "◼",
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* One line per run: marker + name + description on the left, right-aligned
|
|
522
|
+
* `done/total agents done · elapsed · ↓ tokens` stats. The soft-focused row
|
|
523
|
+
* swaps its marker for ❯ and is painted accent by the caller's palette.
|
|
524
|
+
*/
|
|
525
|
+
export function renderStatusRows(input: StatusRowsInput, paint: ViewerPaint): string[] {
|
|
526
|
+
const width = Math.max(20, input.width);
|
|
527
|
+
if (!input.runs.length) return [];
|
|
528
|
+
const out: string[] = [];
|
|
529
|
+
const shown = input.runs.slice(0, MAX_STATUS_ROWS);
|
|
530
|
+
for (const [index, run] of shown.entries()) {
|
|
531
|
+
const selected = input.selected === index;
|
|
532
|
+
const done = run.agents.filter((a) => a.status !== "running").length;
|
|
533
|
+
const outTokens = run.agents.reduce((sum, a) => sum + (a.tokens?.output ?? 0), 0);
|
|
534
|
+
const stats = [
|
|
535
|
+
`${done}/${run.agents.length} agents done`,
|
|
536
|
+
formatDuration(run.startedAt, run.finishedAt, input.now),
|
|
537
|
+
outTokens ? `↓ ${formatTokenCount(outTokens)} tokens` : "",
|
|
538
|
+
// Unlike the viewer header, "completed" stays silent here — the strip's
|
|
539
|
+
// "n/n agents done" already says it (CC's strip does the same); only
|
|
540
|
+
// abnormal ends (failed/aborted) earn a word.
|
|
541
|
+
run.status === "running" || run.status === "completed" ? "" : run.status,
|
|
542
|
+
]
|
|
543
|
+
.filter(Boolean)
|
|
544
|
+
.join(" · ");
|
|
545
|
+
const mark = selected ? "❯" : STATUS_ROW_MARK[run.status];
|
|
546
|
+
const left = `${mark} ${run.name} ${run.description ?? ""}`.trimEnd();
|
|
547
|
+
const row = splitRow(left, stats, width);
|
|
548
|
+
if (selected) {
|
|
549
|
+
out.push(paint.fg("accent", "fused" in row ? row.fused : `${row.left} ${row.right}`));
|
|
550
|
+
} else if ("fused" in row) {
|
|
551
|
+
out.push(paint.fg(run.status === "failed" ? "error" : "dim", row.fused));
|
|
552
|
+
} else {
|
|
553
|
+
const leftPart = run.status === "failed" ? paint.fg("error", row.left) : row.left;
|
|
554
|
+
out.push(`${leftPart} ${paint.fg("dim", row.right)}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const total = input.totalRuns ?? input.runs.length;
|
|
558
|
+
if (total > shown.length) {
|
|
559
|
+
out.push(paint.fg("dim", cut(` +${total - shown.length} more — /workflows`, width)));
|
|
560
|
+
}
|
|
561
|
+
return out;
|
|
562
|
+
}
|