toolcraft 0.0.243 → 0.0.245

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 (30) hide show
  1. package/composition.json +2 -2
  2. package/dist/composition.json +2 -2
  3. package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
  4. package/node_modules/toolcraft-design/README.md +6 -0
  5. package/node_modules/toolcraft-design/dist/acp/index.d.ts +2 -0
  6. package/node_modules/toolcraft-design/dist/acp/index.js +1 -0
  7. package/node_modules/toolcraft-design/dist/acp/plan.d.ts +10 -0
  8. package/node_modules/toolcraft-design/dist/acp/plan.js +50 -0
  9. package/node_modules/toolcraft-design/dist/dashboard/components/output-pane.d.ts +6 -2
  10. package/node_modules/toolcraft-design/dist/dashboard/components/output-pane.js +118 -6
  11. package/node_modules/toolcraft-design/dist/dashboard/components/run-view.d.ts +27 -0
  12. package/node_modules/toolcraft-design/dist/dashboard/components/run-view.js +275 -0
  13. package/node_modules/toolcraft-design/dist/dashboard/components/stats-pane.d.ts +1 -1
  14. package/node_modules/toolcraft-design/dist/dashboard/components/stats-pane.js +2 -8
  15. package/node_modules/toolcraft-design/dist/dashboard/composer-layout.d.ts +12 -0
  16. package/node_modules/toolcraft-design/dist/dashboard/composer-layout.js +45 -0
  17. package/node_modules/toolcraft-design/dist/dashboard/composer.d.ts +24 -0
  18. package/node_modules/toolcraft-design/dist/dashboard/composer.js +107 -0
  19. package/node_modules/toolcraft-design/dist/dashboard/dashboard.d.ts +4 -0
  20. package/node_modules/toolcraft-design/dist/dashboard/dashboard.js +198 -37
  21. package/node_modules/toolcraft-design/dist/dashboard/elapsed.d.ts +1 -0
  22. package/node_modules/toolcraft-design/dist/dashboard/elapsed.js +8 -0
  23. package/node_modules/toolcraft-design/dist/dashboard/index.d.ts +2 -1
  24. package/node_modules/toolcraft-design/dist/dashboard/store.js +2 -1
  25. package/node_modules/toolcraft-design/dist/dashboard/terminal-width.js +1 -1
  26. package/node_modules/toolcraft-design/dist/dashboard/terminal.js +22 -47
  27. package/node_modules/toolcraft-design/dist/dashboard/types.d.ts +37 -0
  28. package/node_modules/toolcraft-design/dist/terminal/input.js +24 -13
  29. package/node_modules/toolcraft-schema/package.json +1 -1
  30. package/package.json +2 -2
package/composition.json CHANGED
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.243",
116
+ "version": "0.0.245",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.243",
126
+ "version": "0.0.245",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.243",
116
+ "version": "0.0.245",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.243",
126
+ "version": "0.0.245",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.243",
11
+ "version": "0.0.245",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -76,6 +76,12 @@ import { renderTable } from "toolcraft-design/render-table";
76
76
  - Root-level types: `Dashboard`, `DashboardOptions`.
77
77
  - The namespace also exports `renderDashboardSnapshot`, `defaultHints`, and the dashboard snapshot, state, output, command, statistics, and footer-hint types.
78
78
 
79
+ Use `appearance: "conversation"` for a focused transcript with ordered plans, task progress, and current agent activity. Supply `onSubmit` to add a message and plan composer; the callback receives `{ kind, text, afterPlanId? }` and may reject to keep the draft editable. Populate `stats.run` with the current plan, tasks, steps, and queue.
80
+
81
+ The composer queues with Enter or Tab, inserts a newline with Alt+Enter, switches between messages and plans with Ctrl+P, and selects the target plan with Alt+Up/Down. Arrow keys move within the draft, including wrapped lines and Unicode text. Esc opens browsing controls; `i` returns to messages, `p` adds a plan, `v` opens the scrollable task and plan list at the current work, and `d` reveals action details. In the list, `f` returns to current work and Home/End jump to either end. Shortcut labels wrap in narrow terminals. Optional `hints` and `keymap` customize harness controls while browsing. Ctrl+C remains available during submission.
82
+
83
+ Output entries accept `role: "agent" | "user" | "action" | "reasoning" | "plan"` and a separate `detail` field. Reuse an entry ID to update an action or agent checklist in place. `acp.formatAgentPlan` produces a compact checklist and full details; `acp.renderAgentPlan` supports terminal, Markdown, and JSON output. Raw details and reasoning remain collapsed until requested; retained text and details are bounded for long runs.
84
+
79
85
  ### Explorer
80
86
 
81
87
  - `explorer`: namespace containing the complete explorer API.
@@ -2,3 +2,5 @@ export { renderAgentMessage, renderToolStart, renderToolComplete, renderReasonin
2
2
  export type { AcpOutputState } from "./components.js";
3
3
  export { getAcpWriter, withAcpWriter } from "./writer.js";
4
4
  export type { AcpLineWriter } from "./writer.js";
5
+ export { formatAgentPlan, renderAgentPlan } from "./plan.js";
6
+ export type { AgentPlanEntry } from "./plan.js";
@@ -1,2 +1,3 @@
1
1
  export { renderAgentMessage, renderToolStart, renderToolComplete, renderReasoning, renderUsage, renderError, renderPermissionRejected } from "./components.js";
2
2
  export { getAcpWriter, withAcpWriter } from "./writer.js";
3
+ export { formatAgentPlan, renderAgentPlan } from "./plan.js";
@@ -0,0 +1,10 @@
1
+ export type AgentPlanEntry = {
2
+ content: string;
3
+ status: "pending" | "in_progress" | "completed";
4
+ };
5
+ /** A compact preview of an agent's checklist; the complete list stays in details. */
6
+ export declare function formatAgentPlan(entries: readonly AgentPlanEntry[]): {
7
+ text: string;
8
+ detail?: string;
9
+ };
10
+ export declare function renderAgentPlan(entries: readonly AgentPlanEntry[]): void;
@@ -0,0 +1,50 @@
1
+ import { color } from "../components/color.js";
2
+ import { plainTerminalText } from "../dashboard/ansi.js";
3
+ import { truncateToWidth } from "../dashboard/terminal-width.js";
4
+ import { resolveOutputFormat } from "../internal/output-format.js";
5
+ import { getAcpWriter } from "./writer.js";
6
+ /** A compact preview of an agent's checklist; the complete list stays in details. */
7
+ export function formatAgentPlan(entries) {
8
+ if (entries.length === 0)
9
+ return { text: "Agent checklist cleared" };
10
+ const completed = entries.filter((entry) => entry.status === "completed").length;
11
+ const heading = `Agent checklist · ${completed}/${entries.length}`;
12
+ const active = entries.findIndex((entry) => entry.status === "in_progress");
13
+ const pending = entries.findIndex((entry) => entry.status === "pending");
14
+ const focus = active >= 0 ? active : pending >= 0 ? pending : entries.length;
15
+ const start = Math.max(0, Math.min(focus - 1, entries.length - 5));
16
+ const end = Math.min(entries.length, start + 5);
17
+ const lines = [heading];
18
+ const full = [heading];
19
+ if (start > 0)
20
+ lines.push(` ↑ ${start} earlier step${start === 1 ? "" : "s"}`);
21
+ let shortened = start > 0 || end < entries.length;
22
+ for (const [index, entry] of entries.entries()) {
23
+ const marker = { completed: "✓", in_progress: "›", pending: "○" }[entry.status];
24
+ const content = plainTerminalText(entry.content);
25
+ full.push(` ${marker} ${content}`);
26
+ if (index < start || index >= end)
27
+ continue;
28
+ const preview = truncateToWidth(content, 100);
29
+ shortened ||= content !== preview;
30
+ lines.push(` ${marker} ${preview}`);
31
+ }
32
+ if (end < entries.length)
33
+ lines.push(` ↓ ${entries.length - end} more step${entries.length - end === 1 ? "" : "s"} · d Details`);
34
+ return { text: lines.join("\n"), ...(shortened ? { detail: full.join("\n") } : {}) };
35
+ }
36
+ export function renderAgentPlan(entries) {
37
+ const format = resolveOutputFormat();
38
+ if (format === "json") {
39
+ getAcpWriter()(JSON.stringify({ event: "plan", entries }));
40
+ return;
41
+ }
42
+ const plan = formatAgentPlan(entries);
43
+ const text = plan.detail ?? plan.text;
44
+ if (format === "markdown") {
45
+ const [heading, ...lines] = text.split("\n");
46
+ getAcpWriter()([`**${heading}**`, ...lines.map((line) => `- ${line.trimStart()}`)].join("\n"));
47
+ }
48
+ else
49
+ getAcpWriter()(color.dim(text));
50
+ }
@@ -8,5 +8,9 @@ export type VisualLine = {
8
8
  prefixStyle: CellStyle;
9
9
  segments?: StyledSegment[];
10
10
  };
11
- export declare function renderOutputPane(buffer: ScreenBuffer, rect: Rect, items: OutputItem[], scrollOffset?: number): number;
12
- export declare function computeVisualLines(items: OutputItem[], width: number): VisualLine[];
11
+ export declare function renderOutputPane(buffer: ScreenBuffer, rect: Rect, items: OutputItem[], scrollOffset?: number, options?: {
12
+ conversation?: boolean;
13
+ details?: boolean;
14
+ now?: number;
15
+ }): number;
16
+ export declare function computeVisualLines(items: OutputItem[], width: number, preformatted?: boolean): VisualLine[];
@@ -1,15 +1,56 @@
1
1
  import { selectViewportTail } from "../../viewport.js";
2
2
  import { getTheme } from "../../internal/theme-detect.js";
3
+ import { supportsColor } from "../../internal/color-support.js";
4
+ import { parse, render } from "../../terminal-markdown/index.js";
3
5
  import { hasAnsi, parseAnsi } from "../ansi.js";
6
+ import { formatElapsed } from "../elapsed.js";
4
7
  import { displayWidth, expandTabs, graphemes, graphemeWidth } from "../terminal-width.js";
5
8
  const TEXT_OFFSET = 3;
6
9
  const CONTINUATION_PREFIX = "│";
7
- export function renderOutputPane(buffer, rect, items, scrollOffset = 0) {
10
+ const agentLines = new WeakMap();
11
+ export function renderOutputPane(buffer, rect, items, scrollOffset = 0, options = {}) {
8
12
  buffer.clearRect(rect);
9
13
  if (rect.width <= 0 || rect.height <= 0) {
10
14
  return 0;
11
15
  }
12
- const { rows: visualLines, offset: actualOffset } = selectViewportTail(items, rect.height, scrollOffset, item => computeVisualLines([item], rect.width));
16
+ const { rows: visualLines, offset: actualOffset } = selectViewportTail(options.conversation && !options.details ? foldCompletedActions(items) : items, rect.height, scrollOffset, item => {
17
+ if (options.conversation && item.role === "reasoning" && !options.details)
18
+ return [];
19
+ let label = item.text;
20
+ const elapsed = options.now === undefined ? 0 : options.now - item.ts;
21
+ if (options.conversation && item.role === "action" && item.kind === "tool" && Number.isFinite(elapsed) && elapsed >= 1000) {
22
+ const duration = formatElapsed(elapsed);
23
+ label += ` · ${elapsed >= 3_600_000 ? duration : duration.slice(3)}`;
24
+ }
25
+ const text = options.details && item.detail ? item.role === "plan" ? item.detail : `${label}\n${item.detail}` : label;
26
+ const agentReply = options.conversation && item.role === "agent"
27
+ ? renderAgentLines(item, rect.width, rect.height + scrollOffset + 4) : undefined;
28
+ const lines = agentReply?.lines ?? computeVisualLines([text === item.text ? item : { ...item, text }], rect.width);
29
+ if (!options.conversation)
30
+ return lines;
31
+ const prose = item.role === "agent" || item.role === "user";
32
+ if (prose) {
33
+ while (lines.length > 0 && lines.at(-1).text.trim().length === 0)
34
+ lines.pop();
35
+ while (lines.length > 0 && lines[0].text.trim().length === 0)
36
+ lines.shift();
37
+ }
38
+ const style = item.kind === "error" ? getTheme().styles.error
39
+ : prose ? {} : getTheme().styles.muted;
40
+ const prefix = item.role === "user" ? "›" : item.role === "agent" ? "•"
41
+ : item.kind === "tool" ? "›" : item.kind === "success" ? "✓"
42
+ : item.kind === "error" ? "!" : "·";
43
+ for (let index = 0; index < lines.length; index++) {
44
+ const line = lines[index];
45
+ line.prefix = index === 0 && agentReply?.complete !== false ? prefix : "";
46
+ line.prefixStyle = style;
47
+ line.style = style;
48
+ }
49
+ if (prose && lines.length > 0) {
50
+ lines.push({ text: "", prefix: "", style: {}, prefixStyle: {} });
51
+ }
52
+ return lines;
53
+ });
13
54
  const textRect = {
14
55
  x: rect.x + TEXT_OFFSET,
15
56
  y: rect.y,
@@ -46,7 +87,73 @@ export function renderOutputPane(buffer, rect, items, scrollOffset = 0) {
46
87
  }
47
88
  return actualOffset;
48
89
  }
49
- export function computeVisualLines(items, width) {
90
+ function renderAgentLines(item, width, budget) {
91
+ const theme = getTheme();
92
+ const color = supportsColor();
93
+ const cached = agentLines.get(item);
94
+ if (cached?.text === item.text && cached.width === width && cached.theme === theme && cached.color === color
95
+ && (cached.complete || cached.budget >= budget)) {
96
+ return { lines: cached.lines.map((line) => ({ ...line })), complete: cached.complete };
97
+ }
98
+ let lines;
99
+ let complete = true;
100
+ try {
101
+ const ast = parse(item.text).ast;
102
+ const options = { width: Math.max(1, width - TEXT_OFFSET), showFrontmatter: true };
103
+ let text;
104
+ if (ast.type !== "root" || containsFootnotes(ast))
105
+ text = render(ast, options);
106
+ else {
107
+ const fragments = [];
108
+ let rows = 0;
109
+ let index = ast.children.length - 1;
110
+ for (; index >= 0 && rows < budget; index--) {
111
+ const fragment = render(ast.children[index], options);
112
+ fragments.push(fragment);
113
+ rows += fragment.split("\n").length - 1;
114
+ }
115
+ complete = index < 0;
116
+ text = fragments.reverse().join("");
117
+ }
118
+ lines = computeVisualLines([{ ...item, text }], width, true);
119
+ }
120
+ catch {
121
+ // A partially streamed document can contain invalid frontmatter or syntax.
122
+ lines = computeVisualLines([item], width);
123
+ }
124
+ agentLines.set(item, { text: item.text, width, theme, color, budget, complete, lines });
125
+ return { lines: lines.map((line) => ({ ...line })), complete };
126
+ }
127
+ function containsFootnotes(node) {
128
+ return node.type === "footnoteDefinition" || node.type === "footnoteReference"
129
+ || ("children" in node && node.children.some(containsFootnotes));
130
+ }
131
+ function foldCompletedActions(items) {
132
+ const result = [];
133
+ let completed = [];
134
+ const flush = () => {
135
+ if (completed.length >= 4) {
136
+ result.push({ role: "action", kind: "status", ts: completed[0].ts,
137
+ text: `${completed.length - 2} earlier actions · d Details` }, ...completed.slice(-2));
138
+ }
139
+ else
140
+ result.push(...completed);
141
+ completed = [];
142
+ };
143
+ for (const item of items) {
144
+ if (item.role === "reasoning")
145
+ continue;
146
+ if (item.role === "action" && item.kind === "success")
147
+ completed.push(item);
148
+ else {
149
+ flush();
150
+ result.push(item);
151
+ }
152
+ }
153
+ flush();
154
+ return result;
155
+ }
156
+ export function computeVisualLines(items, width, preformatted = false) {
50
157
  if (width <= 0) {
51
158
  return [];
52
159
  }
@@ -55,8 +162,8 @@ export function computeVisualLines(items, width) {
55
162
  const visualLines = [];
56
163
  for (const item of items) {
57
164
  const itemStyle = getItemStyle(item.kind);
58
- if (hasAnsi(item.text) || hasCursorControls(item.text)) {
59
- const styledLines = parseAnsi(item.text, hasAnsi(item.text) ? {} : itemStyle);
165
+ if (preformatted || hasAnsi(item.text) || hasCursorControls(item.text)) {
166
+ const styledLines = parseAnsi(item.text, preformatted || hasAnsi(item.text) ? {} : itemStyle);
60
167
  let firstRow = true;
61
168
  for (const styledLine of styledLines) {
62
169
  const rows = hardWrapSegments(styledLine.segments, textWidth);
@@ -177,10 +284,12 @@ function wrapParagraph(value, width) {
177
284
  const tokens = tokenize(value);
178
285
  const lines = [];
179
286
  let currentLine = "";
287
+ let currentWidth = 0;
180
288
  let pendingSpace = "";
181
289
  const flushLine = () => {
182
290
  lines.push(currentLine);
183
291
  currentLine = "";
292
+ currentWidth = 0;
184
293
  pendingSpace = "";
185
294
  };
186
295
  for (const token of tokens) {
@@ -194,13 +303,16 @@ function wrapParagraph(value, width) {
194
303
  for (let index = 0; index < chunks.length; index += 1) {
195
304
  const chunk = chunks[index] ?? "";
196
305
  const gap = index === 0 ? pendingSpace : "";
197
- if (currentLine.length > 0 && displayWidth(`${currentLine}${gap}${chunk}`) > width) {
306
+ const chunkWidth = displayWidth(chunk);
307
+ if (currentLine.length > 0 && currentWidth + gap.length + chunkWidth > width) {
198
308
  flushLine();
199
309
  }
200
310
  if (currentLine.length > 0 && gap.length > 0) {
201
311
  currentLine += gap;
312
+ currentWidth += gap.length;
202
313
  }
203
314
  currentLine += chunk;
315
+ currentWidth += chunkWidth;
204
316
  pendingSpace = "";
205
317
  if (index < chunks.length - 1) {
206
318
  flushLine();
@@ -0,0 +1,27 @@
1
+ import type { ScreenBuffer } from "../buffer.js";
2
+ import type { ComposerState } from "../composer.js";
3
+ import type { DashboardStats, OutputItem, Rect } from "../types.js";
4
+ import type { FooterHint } from "./footer.js";
5
+ export type RunViewOptions = {
6
+ title: string;
7
+ stats: DashboardStats;
8
+ output: OutputItem[];
9
+ scrollOffset: number;
10
+ composer?: ComposerState;
11
+ submitting?: boolean;
12
+ showQueue?: boolean;
13
+ showDetails?: boolean;
14
+ workOffset?: number;
15
+ feedback?: string;
16
+ hints?: FooterHint[];
17
+ now?: number;
18
+ };
19
+ export declare function renderRunView(buffer: ScreenBuffer, options: RunViewOptions): {
20
+ scrollOffset: number;
21
+ outputRect: Rect;
22
+ workOffset: number;
23
+ cursor?: {
24
+ x: number;
25
+ y: number;
26
+ };
27
+ };
@@ -0,0 +1,275 @@
1
+ import { getTheme } from "../../internal/theme-detect.js";
2
+ import { plainTerminalText } from "../ansi.js";
3
+ import { layoutComposer } from "../composer-layout.js";
4
+ import { displayWidth, truncateToWidth } from "../terminal-width.js";
5
+ import { renderOutputPane } from "./output-pane.js";
6
+ import { formatElapsed, formatNumber } from "./stats-pane.js";
7
+ export function renderRunView(buffer, options) {
8
+ const { stats } = options;
9
+ const composer = options.showQueue && !options.composer?.focused ? undefined : options.composer;
10
+ const run = stats.run;
11
+ const theme = getTheme().styles;
12
+ const width = Math.max(0, Math.min(164, buffer.width - 4));
13
+ const x = Math.max(0, Math.floor((buffer.width - width) / 2));
14
+ const sidebarWidth = !options.showQueue && width >= 106 ? Math.min(40, Math.floor(width * 0.29)) : 0;
15
+ const transcriptWidth = Math.max(0, width - (sidebarWidth > 0 ? sidebarWidth + 3 : 0));
16
+ const queue = run?.queue ?? [];
17
+ const plans = queue.filter((item) => item.kind === "plan");
18
+ const activePlanIndex = Math.max(0, plans.findIndex((item) => item.id === run?.activePlanId));
19
+ const activePlan = plans[activePlanIndex];
20
+ const nextPlan = plans[activePlanIndex + 1];
21
+ const progress = plans.length > 0 ? ` · Plan ${activePlanIndex + 1}/${plans.length}` : "";
22
+ put(buffer, { x, y: 0, width, height: 1 }, 0, `${options.title}${progress}`, { bold: true });
23
+ const context = [run?.agent, run?.model ?? (run?.agent ? "default model" : undefined), run?.cwd].filter(Boolean).join(" · ");
24
+ put(buffer, { x, y: 1, width, height: 1 }, 0, context, theme.muted);
25
+ const draft = composer ? layoutComposer(composer, Math.max(1, transcriptWidth - 3)) : undefined;
26
+ const inputLines = draft ? Math.min(3, draft.lines.length) : 0;
27
+ const composerHeight = composer ? inputLines + 3 : 0;
28
+ const hint = composer?.focused
29
+ ? composer.kind === "plan" ? "Enter Queue plan Ctrl+P Message Esc Browse"
30
+ : "Enter Queue Alt+Enter Newline Ctrl+P Plan Alt+↑↓ Target Esc Browse"
31
+ : options.showQueue ? "↑↓ Scroll PgUp/PgDn Page Home/End Jump f Current v Activity i Message q Quit"
32
+ : options.hints ? options.hints.map((hint) => `${hint.key} ${hint.label}`).join(" ")
33
+ : width < 100 ? "i Message p Plan v Tasks d Details ↑↓ Scroll f Follow q Quit"
34
+ : "i Message p Add plan v Tasks & plans d Details ↑↓ Scroll f Follow q Quit";
35
+ const footerLines = [""];
36
+ for (const part of plainTerminalText(hint).split(" ")) {
37
+ const previous = footerLines.at(-1);
38
+ if (previous && displayWidth(`${previous} ${part}`) > width)
39
+ footerLines.push(part);
40
+ else
41
+ footerLines[footerLines.length - 1] = previous ? `${previous} ${part}` : part;
42
+ }
43
+ const footerY = Math.max(0, buffer.height - footerLines.length);
44
+ const composerY = Math.max(3, footerY - composerHeight);
45
+ const contentBottom = composer ? composerY - 1 : footerY - 1;
46
+ let outputY = 3;
47
+ if (sidebarWidth === 0) {
48
+ if (activePlan?.kind === "plan") {
49
+ put(buffer, { x, y: outputY++, width, height: 1 }, 0, `${marker(activePlan.status)} ${activePlanIndex + 1}. ${basename(activePlan.path)}`, { bold: true });
50
+ }
51
+ if (nextPlan?.kind === "plan" && !options.showQueue) {
52
+ put(buffer, { x, y: outputY++, width, height: 1 }, 0, `○ ${activePlanIndex + 2}. ${basename(nextPlan.path)} · next`, theme.muted);
53
+ }
54
+ }
55
+ const tasks = run?.tasks ?? [];
56
+ const completed = tasks.filter((task) => task.status === "completed").length;
57
+ const taskCount = tasks.length > 0 ? `${completed}/${tasks.length} tasks` : `${stats.iterationsLabel ?? "Iterations"} ${stats.iterations}`;
58
+ const step = run?.activeStep ? truncateToWidth(plainTerminalText(run.activeStep), Math.max(8, Math.floor(transcriptWidth / 4))) : undefined;
59
+ const progressLabel = [step, taskCount].filter(Boolean).join(" · ");
60
+ let phaseLabel = run?.phase ?? stats.currentAction ?? stats.status;
61
+ if (phaseLabel === "Follow-up") {
62
+ const messages = queue.filter((item) => item.kind === "message" && item.afterPlanId === run?.activePlanId);
63
+ const active = messages.findIndex((item) => item.status === "running");
64
+ if (active >= 0)
65
+ phaseLabel += ` ${active + 1}/${messages.length}`;
66
+ }
67
+ const phase = truncateToWidth(plainTerminalText(phaseLabel), Math.max(8, transcriptWidth - progressLabel.length - 7));
68
+ const statusMarker = { running: "●", error: "!", paused: "Ⅱ", idle: "○", done: "✓" }[stats.status];
69
+ put(buffer, { x, y: outputY++, width: transcriptWidth, height: 1 }, 0, `${statusMarker} ${phase} · ${progressLabel}${run?.activity ? ` · ${run.activity}` : ""}`, stats.status === "error" ? theme.error : { bold: true });
70
+ outputY++;
71
+ const outputRect = { x, y: outputY, width: transcriptWidth, height: Math.max(0, contentBottom - outputY) };
72
+ let scrollOffset = options.scrollOffset;
73
+ let workOffset = options.workOffset ?? 0;
74
+ if (options.showQueue) {
75
+ workOffset = renderWorkList(buffer, outputRect, stats, options.workOffset);
76
+ }
77
+ else {
78
+ scrollOffset = renderOutputPane(buffer, outputRect, options.output, options.scrollOffset, {
79
+ conversation: true, details: options.showDetails, now: stats.status === "running" ? options.now : undefined
80
+ });
81
+ }
82
+ if (sidebarWidth > 0) {
83
+ const sidebarX = x + transcriptWidth + 3;
84
+ const sidebarHeight = Math.max(0, contentBottom - 3);
85
+ for (let row = 3; row < contentBottom; row++)
86
+ buffer.put(sidebarX - 2, row, "│", theme.muted);
87
+ renderWorkPanel(buffer, { x: sidebarX, y: 3, width: sidebarWidth, height: sidebarHeight }, stats);
88
+ }
89
+ const usage = stats.usageAvailable === false || (stats.usageAvailable === undefined && stats.tokensIn + stats.tokensOut === 0)
90
+ ? "Usage unavailable" : `${formatNumber(stats.tokensIn + stats.tokensOut)} tokens`;
91
+ const pendingMessages = queue.filter((item) => item.kind === "message" && item.status === "pending").length;
92
+ const metrics = `${formatElapsed(stats.elapsedMs)} · ${usage}${pendingMessages ? ` · ${pendingMessages} message${pendingMessages === 1 ? "" : "s"} queued` : ""}${scrollOffset > 0 && !options.showQueue ? " · History paused" : ""}`;
93
+ put(buffer, { x, y: Math.max(outputY, contentBottom), width: transcriptWidth, height: 1 }, 0, metrics, theme.muted);
94
+ let cursor;
95
+ if (composer && draft) {
96
+ const rect = { x, y: composerY, width: transcriptWidth, height: composerHeight };
97
+ const target = plans.find((plan) => plan.id === composer.afterPlanId) ?? activePlan;
98
+ const label = composer.kind === "plan" ? "ADD PLAN · joins the end of the queue"
99
+ : `AFTER ${target?.kind === "plan" ? basename(target.path) : "CURRENT PLAN"}`;
100
+ put(buffer, rect, 0, "─".repeat(transcriptWidth), theme.muted);
101
+ put(buffer, rect, 1, options.submitting ? "Adding to queue…" : label, theme.muted);
102
+ const start = Math.max(0, Math.min(draft.cursor.y - inputLines + 1, draft.lines.length - inputLines));
103
+ for (let index = 0; index < inputLines; index++) {
104
+ const text = composer.text.length === 0
105
+ ? composer.kind === "plan" ? "docs/plans/next-plan.md" : "Queue a message for this plan…"
106
+ : draft.lines[start + index] ?? "";
107
+ put(buffer, rect, index + 2, `${index === 0 ? "›" : " "} ${text}`, composer.text.length === 0 ? theme.muted : {});
108
+ }
109
+ if (composer.error)
110
+ put(buffer, rect, composerHeight - 1, composer.error, theme.error);
111
+ else if (options.feedback)
112
+ put(buffer, rect, composerHeight - 1, options.feedback, theme.success);
113
+ if (composer.focused && !options.submitting)
114
+ cursor = {
115
+ x: Math.min(buffer.width - 1, x + 3 + draft.cursor.x),
116
+ y: Math.max(0, Math.min(footerY - 1, composerY + 2 + draft.cursor.y - start))
117
+ };
118
+ }
119
+ for (const [index, line] of footerLines.entries()) {
120
+ put(buffer, { x, y: footerY + index, width, height: 1 }, 0, line, theme.muted);
121
+ }
122
+ return { scrollOffset, workOffset, outputRect, ...(cursor ? { cursor } : {}) };
123
+ }
124
+ function renderWorkList(buffer, rect, stats, requestedOffset) {
125
+ const theme = getTheme().styles;
126
+ const queue = stats.run?.queue ?? [];
127
+ const tasks = stats.run?.tasks ?? [];
128
+ const activeMessage = queue.findIndex((item) => item.kind === "message" && item.status === "running");
129
+ let currentLine = 1 + (activeMessage >= 0 ? activeMessage : queue.findIndex((item) => item.id === stats.run?.activePlanId));
130
+ let totalLines = queue.length + 3;
131
+ let completed = 0;
132
+ for (const task of tasks) {
133
+ if (task.status === "completed")
134
+ completed++;
135
+ if (requestedOffset === undefined && activeMessage < 0 && task.id === stats.run?.activeTaskId) {
136
+ const step = task.steps?.findIndex((step) => step.name === stats.run?.activeStep) ?? -1;
137
+ currentLine = totalLines + (step >= 0 ? step + 1 : 0);
138
+ }
139
+ totalLines += 1 + (task.steps?.length ?? 0);
140
+ }
141
+ const capacity = Math.max(0, rect.height - 2);
142
+ const offset = Math.max(0, Math.min(requestedOffset ?? Math.max(0, currentLine - 1), totalLines - capacity));
143
+ const end = offset + capacity;
144
+ let parent;
145
+ function paint(line, text, style) {
146
+ if (line >= offset && line < end)
147
+ put(buffer, rect, line - offset + 1, text, style);
148
+ }
149
+ paint(0, `PLANS · ${queue.filter((item) => item.kind === "plan").length}`, { bold: true });
150
+ let planNumber = 0;
151
+ let parentPlan;
152
+ for (let index = 0; index < Math.min(queue.length, end - 1); index++) {
153
+ const item = queue[index];
154
+ if (item.kind === "plan") {
155
+ planNumber++;
156
+ parentPlan = item.path;
157
+ }
158
+ if (index + 1 < offset)
159
+ continue;
160
+ const text = item.kind === "plan" ? `${planNumber}. ${displayPlanPath(item.path, stats.run?.cwd)}` : ` └ ${item.text}`;
161
+ paint(index + 1, `${marker(item.status)} ${text}`, tone(item.status));
162
+ if (index + 1 === offset && item.kind === "message" && parentPlan)
163
+ parent = `After ${displayPlanPath(parentPlan, stats.run?.cwd)}`;
164
+ }
165
+ paint(queue.length + 2, `TASKS · ${completed}/${tasks.length}`, { bold: true });
166
+ let line = queue.length + 3;
167
+ for (const [index, task] of tasks.entries()) {
168
+ if (line >= end)
169
+ break;
170
+ const next = line + 1 + (task.steps?.length ?? 0);
171
+ if (next <= offset) {
172
+ line = next;
173
+ continue;
174
+ }
175
+ const title = `${index + 1}. ${task.title}`;
176
+ const active = task.id === stats.run?.activeTaskId && stats.status === "running";
177
+ const status = active ? "running" : task.status;
178
+ paint(line, `${marker(status)} ${title}`, tone(status));
179
+ for (let stepIndex = Math.max(0, offset - line - 1); stepIndex < (task.steps?.length ?? 0) && line + stepIndex + 1 < end; stepIndex++) {
180
+ const step = task.steps[stepIndex];
181
+ const name = step.name;
182
+ const status = active && name === stats.run?.activeStep ? "running" : step.status;
183
+ paint(line + stepIndex + 1, ` ${marker(status)} ${name}`, tone(status));
184
+ if (line + stepIndex + 1 === offset)
185
+ parent = title;
186
+ }
187
+ line = next;
188
+ }
189
+ if (offset > 0)
190
+ put(buffer, rect, 0, `↑ earlier · ${parent ?? "tasks and plans"}`, theme.muted);
191
+ if (end < totalLines)
192
+ put(buffer, rect, rect.height - 1, "↓ more tasks and plans", theme.muted);
193
+ return offset;
194
+ }
195
+ function renderWorkPanel(buffer, rect, stats) {
196
+ const theme = getTheme().styles;
197
+ const queue = stats.run?.queue ?? [];
198
+ const tasks = stats.run?.tasks ?? [];
199
+ const plans = queue.filter((item) => item.kind === "plan");
200
+ const planIndex = Math.max(0, plans.findIndex((item) => item.id === stats.run?.activePlanId));
201
+ let row = 0;
202
+ put(buffer, rect, row++, `PLANS · ${plans.length > 0 ? planIndex + 1 : 0}/${plans.length}`, { bold: true });
203
+ const planRows = Math.min(Math.max(3, Math.floor(rect.height * .4)), Math.max(0, rect.height - 4));
204
+ const runningIndex = queue.findIndex((item) => item.status === "running");
205
+ const activeIndex = Math.max(0, runningIndex >= 0 ? runningIndex : queue.findIndex((item) => item.id === stats.run?.activePlanId));
206
+ const queueStart = Math.max(0, Math.min(activeIndex, queue.length - Math.max(1, planRows - 2)));
207
+ if (queueStart > 0) {
208
+ const first = queue[queueStart];
209
+ const parent = first?.kind === "message" ? plans.find((plan) => plan.id === first.afterPlanId) : undefined;
210
+ put(buffer, rect, row++, parent ? `↑ After ${basename(parent.path)}` : `↑ ${queueStart} earlier`, theme.muted);
211
+ }
212
+ let queueIndex = queueStart;
213
+ for (; queueIndex < queue.length && row < planRows; queueIndex++) {
214
+ const item = queue[queueIndex];
215
+ const number = item.kind === "plan" ? plans.findIndex((plan) => plan.id === item.id) + 1 : undefined;
216
+ const text = item.kind === "plan" ? `${number}. ${basename(item.path)}` : ` └ ${item.text}`;
217
+ put(buffer, rect, row++, `${marker(item.status)} ${text}`, tone(item.status));
218
+ }
219
+ if (queueIndex < queue.length)
220
+ put(buffer, rect, row++, " ↓ more queued work", theme.muted);
221
+ row++;
222
+ if (tasks.length === 0) {
223
+ put(buffer, rect, row++, "TASKS", { bold: true });
224
+ put(buffer, rect, row, "Waiting for plan details…", theme.muted);
225
+ return;
226
+ }
227
+ const done = tasks.filter((task) => task.status === "completed").length;
228
+ put(buffer, rect, row++, `TASKS · ${done}/${tasks.length}`, { bold: true });
229
+ const active = tasks.findIndex((task) => task.id === stats.run?.activeTaskId);
230
+ const pending = tasks.findIndex((task) => task.status !== "completed");
231
+ const focus = active >= 0 ? active : Math.max(0, pending);
232
+ const capacity = Math.max(1, rect.height - row - 1);
233
+ const start = Math.max(0, Math.min(focus - 1, tasks.length - capacity));
234
+ if (start > 0)
235
+ put(buffer, rect, row++, ` ↑ ${start} earlier tasks`, theme.muted);
236
+ for (let index = start; index < tasks.length && row < rect.height; index++) {
237
+ if (row === rect.height - 1 && index < tasks.length - 1) {
238
+ put(buffer, rect, row, " ↓ more tasks · v View all", theme.muted);
239
+ break;
240
+ }
241
+ const task = tasks[index];
242
+ const isActive = task.id === stats.run?.activeTaskId;
243
+ const status = isActive && stats.status === "running" ? "running" : task.status;
244
+ put(buffer, rect, row++, `${marker(status)} ${index + 1}. ${task.title}`, tone(status));
245
+ if (isActive && task.steps && row < rect.height - (index < tasks.length - 1 ? 1 : 0)) {
246
+ const steps = task.steps.map((step) => `${step.name === stats.run?.activeStep ? "›" : marker(step.status)} ${step.name}`).join(" ");
247
+ put(buffer, rect, row++, ` ${steps}`, theme.muted);
248
+ }
249
+ }
250
+ }
251
+ function marker(status) {
252
+ return ({ completed: "✓", running: "›", failed: "!", cancelled: "×", paused: "Ⅱ", pending: "○" })[status];
253
+ }
254
+ function tone(status) {
255
+ const theme = getTheme().styles;
256
+ if (status === "running")
257
+ return { ...theme.info, bold: true };
258
+ if (status === "failed")
259
+ return theme.error;
260
+ return theme.muted;
261
+ }
262
+ function basename(value) {
263
+ return value.split("/").at(-1) ?? value;
264
+ }
265
+ function displayPlanPath(value, cwd) {
266
+ if (!cwd)
267
+ return value;
268
+ const prefix = cwd.endsWith("/") ? cwd : `${cwd}/`;
269
+ return value.startsWith(prefix) ? value.slice(prefix.length) : value;
270
+ }
271
+ function put(buffer, rect, row, text, style) {
272
+ if (row < 0 || row >= rect.height || rect.y + row >= buffer.height)
273
+ return;
274
+ buffer.putInRect(rect, row, truncateToWidth(plainTerminalText(text), rect.width), style);
275
+ }
@@ -1,8 +1,8 @@
1
1
  import { ScreenBuffer } from "../buffer.js";
2
2
  import type { DashboardStats, Rect } from "../types.js";
3
3
  import { type VisualLine } from "./output-pane.js";
4
+ export { formatElapsed } from "../elapsed.js";
4
5
  export declare function renderCompactStatsPane(buffer: ScreenBuffer, rect: Rect, stats: DashboardStats): void;
5
6
  export declare function renderStatsPane(buffer: ScreenBuffer, rect: Rect, stats: DashboardStats): void;
6
- export declare function formatElapsed(ms: number): string;
7
7
  export declare function formatNumber(n: number): string;
8
8
  export declare function statsToLines(stats: DashboardStats, width: number): VisualLine[];