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.
- package/composition.json +2 -2
- package/dist/composition.json +2 -2
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-design/README.md +6 -0
- package/node_modules/toolcraft-design/dist/acp/index.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/acp/index.js +1 -0
- package/node_modules/toolcraft-design/dist/acp/plan.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/acp/plan.js +50 -0
- package/node_modules/toolcraft-design/dist/dashboard/components/output-pane.d.ts +6 -2
- package/node_modules/toolcraft-design/dist/dashboard/components/output-pane.js +118 -6
- package/node_modules/toolcraft-design/dist/dashboard/components/run-view.d.ts +27 -0
- package/node_modules/toolcraft-design/dist/dashboard/components/run-view.js +275 -0
- package/node_modules/toolcraft-design/dist/dashboard/components/stats-pane.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/dashboard/components/stats-pane.js +2 -8
- package/node_modules/toolcraft-design/dist/dashboard/composer-layout.d.ts +12 -0
- package/node_modules/toolcraft-design/dist/dashboard/composer-layout.js +45 -0
- package/node_modules/toolcraft-design/dist/dashboard/composer.d.ts +24 -0
- package/node_modules/toolcraft-design/dist/dashboard/composer.js +107 -0
- package/node_modules/toolcraft-design/dist/dashboard/dashboard.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/dashboard/dashboard.js +198 -37
- package/node_modules/toolcraft-design/dist/dashboard/elapsed.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/dashboard/elapsed.js +8 -0
- package/node_modules/toolcraft-design/dist/dashboard/index.d.ts +2 -1
- package/node_modules/toolcraft-design/dist/dashboard/store.js +2 -1
- package/node_modules/toolcraft-design/dist/dashboard/terminal-width.js +1 -1
- package/node_modules/toolcraft-design/dist/dashboard/terminal.js +22 -47
- package/node_modules/toolcraft-design/dist/dashboard/types.d.ts +37 -0
- package/node_modules/toolcraft-design/dist/terminal/input.js +24 -13
- package/node_modules/toolcraft-schema/package.json +1 -1
- 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.
|
|
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.
|
|
126
|
+
"version": "0.0.245",
|
|
127
127
|
"license": "MIT"
|
|
128
128
|
},
|
|
129
129
|
{
|
package/dist/composition.json
CHANGED
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "toolcraft",
|
|
116
|
-
"version": "0.0.
|
|
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.
|
|
126
|
+
"version": "0.0.245",
|
|
127
127
|
"license": "MIT"
|
|
128
128
|
},
|
|
129
129
|
{
|
|
@@ -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
|
|
12
|
-
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
|
|
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[];
|