toolcraft 0.0.243 → 0.0.244
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
|
@@ -2,6 +2,8 @@ import { getTheme } from "../../internal/theme-detect.js";
|
|
|
2
2
|
import { plainTerminalText } from "../ansi.js";
|
|
3
3
|
import { displayWidth, graphemes, graphemeWidth, truncateToWidth } from "../terminal-width.js";
|
|
4
4
|
import { computeVisualLines } from "./output-pane.js";
|
|
5
|
+
import { formatElapsed } from "../elapsed.js";
|
|
6
|
+
export { formatElapsed } from "../elapsed.js";
|
|
5
7
|
export function renderCompactStatsPane(buffer, rect, stats) {
|
|
6
8
|
buffer.clearRect(rect);
|
|
7
9
|
if (rect.width <= 0 || rect.height <= 0)
|
|
@@ -58,14 +60,6 @@ export function renderStatsPane(buffer, rect, stats) {
|
|
|
58
60
|
buffer.putInRect({ x: rect.x + textStart, y: rect.y + row, width: rect.width - textStart, height: 1 }, 0, line.text, line.style);
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
|
-
export function formatElapsed(ms) {
|
|
62
|
-
const safeMs = Number.isFinite(ms) ? ms : 0;
|
|
63
|
-
const totalSeconds = Math.max(0, Math.floor(safeMs / 1_000));
|
|
64
|
-
const hours = Math.floor(totalSeconds / 3_600);
|
|
65
|
-
const minutes = Math.floor((totalSeconds % 3_600) / 60);
|
|
66
|
-
const seconds = totalSeconds % 60;
|
|
67
|
-
return [hours, minutes, seconds].map((value) => value.toString().padStart(2, "0")).join(":");
|
|
68
|
-
}
|
|
69
63
|
export function formatNumber(n) {
|
|
70
64
|
return new Intl.NumberFormat("en-US").format(n);
|
|
71
65
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ComposerState } from "./composer.js";
|
|
2
|
+
type ComposerLayout = {
|
|
3
|
+
lines: string[];
|
|
4
|
+
starts: number[];
|
|
5
|
+
cursor: {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
/** Shared display rows keep input navigation aligned with the rendered caret. */
|
|
11
|
+
export declare function layoutComposer(state: ComposerState, width: number): ComposerLayout;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { graphemes, graphemeWidth } from "./terminal-width.js";
|
|
2
|
+
const layouts = new WeakMap();
|
|
3
|
+
/** Shared display rows keep input navigation aligned with the rendered caret. */
|
|
4
|
+
export function layoutComposer(state, width) {
|
|
5
|
+
width = Math.max(1, width);
|
|
6
|
+
const cached = layouts.get(state);
|
|
7
|
+
if (cached?.text === state.text && cached.cursor === state.cursor && cached.width === width)
|
|
8
|
+
return cached.layout;
|
|
9
|
+
const lines = [""];
|
|
10
|
+
const starts = [0];
|
|
11
|
+
let column = 0;
|
|
12
|
+
let offset = 0;
|
|
13
|
+
let cursor = { x: 0, y: 0 };
|
|
14
|
+
for (const segment of graphemes(state.text)) {
|
|
15
|
+
const cells = segment === "\t" ? 2 : graphemeWidth(segment);
|
|
16
|
+
if (segment !== "\n" && column + cells > width) {
|
|
17
|
+
lines.push("");
|
|
18
|
+
starts.push(offset);
|
|
19
|
+
column = 0;
|
|
20
|
+
}
|
|
21
|
+
if (offset === state.cursor)
|
|
22
|
+
cursor = { x: column, y: lines.length - 1 };
|
|
23
|
+
if (segment === "\n") {
|
|
24
|
+
lines.push("");
|
|
25
|
+
starts.push(offset + 1);
|
|
26
|
+
column = 0;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
lines[lines.length - 1] += segment === "\t" ? " " : segment;
|
|
30
|
+
column += cells;
|
|
31
|
+
}
|
|
32
|
+
offset += segment.length;
|
|
33
|
+
}
|
|
34
|
+
if (offset === state.cursor) {
|
|
35
|
+
if (column >= width) {
|
|
36
|
+
lines.push("");
|
|
37
|
+
starts.push(offset);
|
|
38
|
+
column = 0;
|
|
39
|
+
}
|
|
40
|
+
cursor = { x: column, y: lines.length - 1 };
|
|
41
|
+
}
|
|
42
|
+
const layout = { lines, starts, cursor };
|
|
43
|
+
layouts.set(state, { text: state.text, cursor: state.cursor, width, layout });
|
|
44
|
+
return layout;
|
|
45
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { KeypressEvent } from "./terminal.js";
|
|
2
|
+
export type DashboardSubmission = {
|
|
3
|
+
kind: "message" | "plan";
|
|
4
|
+
text: string;
|
|
5
|
+
afterPlanId?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ComposerState = {
|
|
8
|
+
kind: DashboardSubmission["kind"];
|
|
9
|
+
text: string;
|
|
10
|
+
/** UTF-16 offset at a grapheme boundary. */
|
|
11
|
+
cursor: number;
|
|
12
|
+
focused: boolean;
|
|
13
|
+
afterPlanId?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
/** Preserve the intended display column across shorter rows during vertical movement. */
|
|
16
|
+
preferredColumn?: number;
|
|
17
|
+
preferredWidth?: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function createComposerState(kind: DashboardSubmission["kind"], afterPlanId?: string): ComposerState;
|
|
20
|
+
export declare function editComposer(state: ComposerState, event: KeypressEvent, width?: number): {
|
|
21
|
+
state: ComposerState;
|
|
22
|
+
handled: boolean;
|
|
23
|
+
submit?: DashboardSubmission;
|
|
24
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { parseAnsi } from "./ansi.js";
|
|
2
|
+
import { graphemes, graphemeWidth } from "./terminal-width.js";
|
|
3
|
+
import { layoutComposer } from "./composer-layout.js";
|
|
4
|
+
const editingSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
5
|
+
export function createComposerState(kind, afterPlanId) {
|
|
6
|
+
return { kind, text: "", cursor: 0, focused: true, ...(afterPlanId ? { afterPlanId } : {}) };
|
|
7
|
+
}
|
|
8
|
+
export function editComposer(state, event, width = Number.MAX_SAFE_INTEGER) {
|
|
9
|
+
const key = event.name ?? event.ch;
|
|
10
|
+
if (!state.focused || (event.ctrl && key === "c"))
|
|
11
|
+
return { state, handled: false };
|
|
12
|
+
if (key === "escape")
|
|
13
|
+
return { state: { ...state, focused: false }, handled: true };
|
|
14
|
+
const next = { ...state, error: undefined, preferredColumn: undefined, preferredWidth: undefined };
|
|
15
|
+
if ((key === "up" || key === "down") && !event.ctrl && !event.meta) {
|
|
16
|
+
const layout = layoutComposer(state, width);
|
|
17
|
+
const row = Math.max(0, Math.min(layout.starts.length - 1, layout.cursor.y + (key === "up" ? -1 : 1)));
|
|
18
|
+
if (row === layout.cursor.y)
|
|
19
|
+
return { state, handled: true };
|
|
20
|
+
next.preferredColumn = (state.preferredWidth === width ? state.preferredColumn : undefined) ?? layout.cursor.x;
|
|
21
|
+
next.preferredWidth = width;
|
|
22
|
+
next.cursor = layout.starts[row];
|
|
23
|
+
let column = 0;
|
|
24
|
+
for (const segment of graphemes(state.text.slice(next.cursor, layout.starts[row + 1]))) {
|
|
25
|
+
const cells = segment === "\t" ? 2 : graphemeWidth(segment);
|
|
26
|
+
if (segment === "\n" || column + cells > next.preferredColumn)
|
|
27
|
+
break;
|
|
28
|
+
next.cursor += segment.length;
|
|
29
|
+
column += cells;
|
|
30
|
+
}
|
|
31
|
+
return { state: next, handled: true };
|
|
32
|
+
}
|
|
33
|
+
const segments = ["left", "right", "backspace", "delete"].includes(key ?? "") || (event.ctrl && (key === "d" || key === "w"))
|
|
34
|
+
? editingSegmenter.segment(state.text) : undefined;
|
|
35
|
+
const previous = segments?.containing(state.cursor - 1)?.index ?? state.cursor;
|
|
36
|
+
const current = segments?.containing(state.cursor);
|
|
37
|
+
const following = current ? current.index + current.segment.length : state.cursor;
|
|
38
|
+
const lineStart = state.cursor === 0 ? 0 : state.text.lastIndexOf("\n", state.cursor - 1) + 1;
|
|
39
|
+
const newline = state.text.indexOf("\n", state.cursor);
|
|
40
|
+
const lineEnd = newline === -1 ? state.text.length : newline;
|
|
41
|
+
function replace(start, end, text = "") {
|
|
42
|
+
next.text = state.text.slice(0, start) + text + state.text.slice(end);
|
|
43
|
+
next.cursor = start + text.length;
|
|
44
|
+
}
|
|
45
|
+
if (event.name === "paste") {
|
|
46
|
+
const text = parseAnsi((event.ch ?? "").replaceAll("\r\n", "\n").replaceAll("\r", "\n"))
|
|
47
|
+
.map((line) => line.segments.map((segment) => segment.text).join(""))
|
|
48
|
+
.join("\n");
|
|
49
|
+
replace(state.cursor, state.cursor, text);
|
|
50
|
+
}
|
|
51
|
+
else if ((key === "return" || key === "enter") && (event.meta || event.shift)) {
|
|
52
|
+
replace(state.cursor, state.cursor, "\n");
|
|
53
|
+
}
|
|
54
|
+
else if (key === "return" || key === "enter" || key === "tab") {
|
|
55
|
+
if (!state.text.trim())
|
|
56
|
+
return { state, handled: true };
|
|
57
|
+
return { state, handled: true, submit: {
|
|
58
|
+
kind: state.kind,
|
|
59
|
+
text: state.text.trim(),
|
|
60
|
+
...(state.kind === "message" && state.afterPlanId ? { afterPlanId: state.afterPlanId } : {})
|
|
61
|
+
} };
|
|
62
|
+
}
|
|
63
|
+
else if (key === "left" && !event.meta) {
|
|
64
|
+
next.cursor = previous;
|
|
65
|
+
}
|
|
66
|
+
else if (key === "right" && !event.meta) {
|
|
67
|
+
next.cursor = following;
|
|
68
|
+
}
|
|
69
|
+
else if (key === "home" || (event.ctrl && key === "a")) {
|
|
70
|
+
next.cursor = lineStart;
|
|
71
|
+
}
|
|
72
|
+
else if (key === "end" || (event.ctrl && key === "e")) {
|
|
73
|
+
next.cursor = lineEnd;
|
|
74
|
+
}
|
|
75
|
+
else if (key === "backspace") {
|
|
76
|
+
replace(previous, state.cursor);
|
|
77
|
+
}
|
|
78
|
+
else if (key === "delete" || (event.ctrl && key === "d")) {
|
|
79
|
+
replace(state.cursor, following);
|
|
80
|
+
}
|
|
81
|
+
else if (event.ctrl && key === "u") {
|
|
82
|
+
replace(lineStart, state.cursor);
|
|
83
|
+
}
|
|
84
|
+
else if (event.ctrl && key === "k") {
|
|
85
|
+
replace(state.cursor, lineEnd);
|
|
86
|
+
}
|
|
87
|
+
else if (event.ctrl && key === "w") {
|
|
88
|
+
let start = state.cursor;
|
|
89
|
+
let preceding = segments?.containing(start - 1);
|
|
90
|
+
while (preceding && preceding.segment.trim() === "") {
|
|
91
|
+
start = preceding.index;
|
|
92
|
+
preceding = segments?.containing(start - 1);
|
|
93
|
+
}
|
|
94
|
+
while (preceding && preceding.segment.trim() !== "") {
|
|
95
|
+
start = preceding.index;
|
|
96
|
+
preceding = segments?.containing(start - 1);
|
|
97
|
+
}
|
|
98
|
+
replace(start, state.cursor);
|
|
99
|
+
}
|
|
100
|
+
else if (event.ch !== undefined && !event.ctrl && !event.meta) {
|
|
101
|
+
replace(state.cursor, state.cursor, event.ch);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
return { state, handled: false };
|
|
105
|
+
}
|
|
106
|
+
return { state: next, handled: true };
|
|
107
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type RenderPerformanceSnapshot } from "../render-performance.js";
|
|
2
2
|
import type { FooterHint } from "./components/footer.js";
|
|
3
|
+
import { type DashboardSubmission } from "./composer.js";
|
|
3
4
|
import type { Command, DashboardStats, OutputItem } from "./types.js";
|
|
4
5
|
export type DashboardOptions = {
|
|
5
6
|
title?: string;
|
|
@@ -9,6 +10,9 @@ export type DashboardOptions = {
|
|
|
9
10
|
hints?: FooterHint[];
|
|
10
11
|
stdin?: NodeJS.ReadStream;
|
|
11
12
|
stdout?: NodeJS.WriteStream;
|
|
13
|
+
appearance?: "panels" | "conversation";
|
|
14
|
+
/** Resolves once the runtime accepts the input; rejection keeps the draft editable. */
|
|
15
|
+
onSubmit?: (submission: DashboardSubmission) => void | Promise<void>;
|
|
12
16
|
/** Observe bounded performance snapshots without forcing idle repaints. */
|
|
13
17
|
onPerformance?: (stats: RenderPerformanceSnapshot) => void;
|
|
14
18
|
};
|
|
@@ -3,9 +3,12 @@ import { createLogger } from "../components/logger.js";
|
|
|
3
3
|
import { resolveOutputFormat } from "../internal/output-format.js";
|
|
4
4
|
import { ScreenBuffer, diff } from "./buffer.js";
|
|
5
5
|
import { renderContextPane } from "./components/context-pane.js";
|
|
6
|
+
import { plainTerminalText } from "./ansi.js";
|
|
6
7
|
import { renderBorder } from "./components/border.js";
|
|
7
8
|
import { defaultHints, renderFooter } from "./components/footer.js";
|
|
8
9
|
import { renderOutputPane } from "./components/output-pane.js";
|
|
10
|
+
import { renderRunView } from "./components/run-view.js";
|
|
11
|
+
import { createComposerState, editComposer } from "./composer.js";
|
|
9
12
|
import { renderCompactStatsPane, renderStatsPane } from "./components/stats-pane.js";
|
|
10
13
|
import { createKeymap } from "./keymap.js";
|
|
11
14
|
import { computeDashboardLayout } from "./layout.js";
|
|
@@ -22,6 +25,7 @@ export function createDashboard(opts = {}) {
|
|
|
22
25
|
const title = opts.title ?? DEFAULT_TITLE;
|
|
23
26
|
const statsTitle = opts.statsTitle ?? DEFAULT_STATS_TITLE;
|
|
24
27
|
const rightPaneWidth = opts.rightPaneWidth ?? DEFAULT_RIGHT_PANE_WIDTH;
|
|
28
|
+
const conversation = opts.appearance === "conversation" || opts.onSubmit !== undefined;
|
|
25
29
|
const commandHandlers = new Set();
|
|
26
30
|
const fallbackLogger = createLogger((message) => {
|
|
27
31
|
stdout.write(`${message}\n`);
|
|
@@ -39,7 +43,17 @@ export function createDashboard(opts = {}) {
|
|
|
39
43
|
let scrollOffset = 0;
|
|
40
44
|
let outputViewportHeight = 0;
|
|
41
45
|
let heldOutput;
|
|
46
|
+
let heldAt;
|
|
42
47
|
let renderTimer;
|
|
48
|
+
let composer = opts.onSubmit ? createComposerState("message") : undefined;
|
|
49
|
+
let otherDraft = createComposerState("plan");
|
|
50
|
+
let submitting = false;
|
|
51
|
+
let showQueue = false;
|
|
52
|
+
let workOffset = 0;
|
|
53
|
+
let feedback;
|
|
54
|
+
let showDetails = false;
|
|
55
|
+
let lastActivePlanId;
|
|
56
|
+
let outputRect;
|
|
43
57
|
function appendOutput(item) {
|
|
44
58
|
if (destroyed) {
|
|
45
59
|
return;
|
|
@@ -67,6 +81,8 @@ export function createDashboard(opts = {}) {
|
|
|
67
81
|
driver.enterAltScreen();
|
|
68
82
|
driver.disableLineWrap();
|
|
69
83
|
driver.hideCursor();
|
|
84
|
+
if (opts.onSubmit)
|
|
85
|
+
driver.write("\u001b[?2004h");
|
|
70
86
|
render();
|
|
71
87
|
const activeStore = getStore();
|
|
72
88
|
let previousStats = activeStore.getState().stats;
|
|
@@ -90,18 +106,110 @@ export function createDashboard(opts = {}) {
|
|
|
90
106
|
}
|
|
91
107
|
});
|
|
92
108
|
unsubscribeKeypress = driver.onKeypress((event) => {
|
|
109
|
+
performanceMonitor.request("input");
|
|
110
|
+
if (composer && !(event.ctrl && event.name === "c")) {
|
|
111
|
+
if (submitting)
|
|
112
|
+
return;
|
|
113
|
+
if (event.ctrl && event.name === "p") {
|
|
114
|
+
feedback = undefined;
|
|
115
|
+
[composer, otherDraft] = [otherDraft, composer];
|
|
116
|
+
composer.focused = true;
|
|
117
|
+
render();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (composer.focused && event.meta && (event.name === "up" || event.name === "down")) {
|
|
121
|
+
feedback = undefined;
|
|
122
|
+
const run = getStore().getState().stats.run;
|
|
123
|
+
const plans = run?.queue?.filter((item) => item.kind === "plan") ?? [];
|
|
124
|
+
const activeIndex = Math.max(0, plans.findIndex((item) => item.id === run?.activePlanId));
|
|
125
|
+
const available = plans.slice(activeIndex);
|
|
126
|
+
if (available.length > 0) {
|
|
127
|
+
const index = available.findIndex((item) => item.id === composer.afterPlanId);
|
|
128
|
+
const next = index < 0 ? 0 : (index + (event.name === "up" ? -1 : 1) + available.length) % available.length;
|
|
129
|
+
composer = { ...composer, afterPlanId: available[next].id };
|
|
130
|
+
render();
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!composer.focused && (event.ch === "i" || event.ch === "p" || event.name === "return")) {
|
|
135
|
+
feedback = undefined;
|
|
136
|
+
const kind = event.ch === "p" ? "plan" : "message";
|
|
137
|
+
if (composer.kind !== kind)
|
|
138
|
+
[composer, otherDraft] = [otherDraft, composer];
|
|
139
|
+
composer.focused = true;
|
|
140
|
+
render();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const edit = editComposer(composer, event, Math.max(1, (outputRect?.width ?? 80) - 3));
|
|
144
|
+
if (edit.handled) {
|
|
145
|
+
feedback = undefined;
|
|
146
|
+
composer = edit.state;
|
|
147
|
+
if (edit.submit) {
|
|
148
|
+
const submission = edit.submit;
|
|
149
|
+
submitting = true;
|
|
150
|
+
render();
|
|
151
|
+
void (async () => {
|
|
152
|
+
try {
|
|
153
|
+
await opts.onSubmit(submission);
|
|
154
|
+
const run = store?.getState().stats.run;
|
|
155
|
+
const activeIndex = run?.queue?.findIndex((item) => item.id === run.activePlanId) ?? -1;
|
|
156
|
+
const targetIndex = run?.queue?.findIndex((item) => item.id === submission.afterPlanId) ?? -1;
|
|
157
|
+
const target = targetIndex < activeIndex ? run?.activePlanId : submission.afterPlanId;
|
|
158
|
+
composer = createComposerState(submission.kind, target);
|
|
159
|
+
const planNumber = (run?.queue?.filter((item) => item.kind === "plan").findIndex((item) => item.id === submission.afterPlanId) ?? -1) + 1;
|
|
160
|
+
feedback = submission.kind === "plan" ? "Plan queued" : `Message queued${planNumber > 0 ? ` after plan ${planNumber}` : ""}`;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
+
composer = { ...composer, error: message };
|
|
165
|
+
if (destroyed)
|
|
166
|
+
fallbackLogger.error(`Could not queue ${submission.kind}: ${plainTerminalText(message)}`);
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
submitting = false;
|
|
170
|
+
render();
|
|
171
|
+
}
|
|
172
|
+
})();
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
renderTimer ??= setTimeout(render, 16);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (conversation && !composer?.focused && (event.ch === "v" || event.ch === "d")) {
|
|
181
|
+
if (event.ch === "v") {
|
|
182
|
+
showQueue = !showQueue;
|
|
183
|
+
if (showQueue)
|
|
184
|
+
workOffset = undefined;
|
|
185
|
+
}
|
|
186
|
+
else
|
|
187
|
+
showDetails = !showDetails;
|
|
188
|
+
render();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (showQueue && !composer?.focused && event.name === "home") {
|
|
192
|
+
workOffset = 0;
|
|
193
|
+
render();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
93
196
|
const command = resolveCommand(event);
|
|
94
197
|
if (command === undefined) {
|
|
95
198
|
return;
|
|
96
199
|
}
|
|
97
|
-
performanceMonitor.request("input");
|
|
98
200
|
if (command === "render-stats") {
|
|
99
201
|
showPerformance = !showPerformance;
|
|
100
202
|
render();
|
|
101
203
|
return;
|
|
102
204
|
}
|
|
103
205
|
if (command === "follow") {
|
|
206
|
+
if (showQueue) {
|
|
207
|
+
workOffset = event.name === "end" ? Number.MAX_SAFE_INTEGER : undefined;
|
|
208
|
+
render();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
104
211
|
heldOutput = undefined;
|
|
212
|
+
heldAt = undefined;
|
|
105
213
|
scrollOffset = 0;
|
|
106
214
|
render();
|
|
107
215
|
return;
|
|
@@ -110,14 +218,23 @@ export function createDashboard(opts = {}) {
|
|
|
110
218
|
command === "scroll-down" ||
|
|
111
219
|
command === "page-up" ||
|
|
112
220
|
command === "page-down") {
|
|
113
|
-
const page = Math.max(1, outputViewportHeight);
|
|
221
|
+
const page = Math.max(1, outputRect?.height ?? outputViewportHeight);
|
|
114
222
|
const amount = command === "page-up" || command === "page-down" ? page : 1;
|
|
115
223
|
const direction = command === "scroll-up" || command === "page-up" ? 1 : -1;
|
|
116
|
-
if (
|
|
117
|
-
|
|
224
|
+
if (showQueue) {
|
|
225
|
+
workOffset = Math.max(0, (workOffset ?? 0) - amount * direction);
|
|
226
|
+
renderTimer ??= setTimeout(render, 16);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (direction > 0 && heldOutput === undefined) {
|
|
230
|
+
heldOutput = getStore().getState().output;
|
|
231
|
+
heldAt = Date.now();
|
|
232
|
+
}
|
|
118
233
|
scrollOffset = Math.max(0, scrollOffset + amount * direction);
|
|
119
|
-
if (scrollOffset === 0)
|
|
234
|
+
if (scrollOffset === 0) {
|
|
120
235
|
heldOutput = undefined;
|
|
236
|
+
heldAt = undefined;
|
|
237
|
+
}
|
|
121
238
|
renderTimer ??= setTimeout(render, 16);
|
|
122
239
|
return;
|
|
123
240
|
}
|
|
@@ -143,6 +260,8 @@ export function createDashboard(opts = {}) {
|
|
|
143
260
|
started = false;
|
|
144
261
|
return;
|
|
145
262
|
}
|
|
263
|
+
if (opts.onSubmit)
|
|
264
|
+
driver.write("\u001b[?2004l");
|
|
146
265
|
driver.destroy();
|
|
147
266
|
driver = undefined;
|
|
148
267
|
previousBuffer = new ScreenBuffer(0, 0);
|
|
@@ -159,6 +278,10 @@ export function createDashboard(opts = {}) {
|
|
|
159
278
|
return;
|
|
160
279
|
}
|
|
161
280
|
stop();
|
|
281
|
+
for (const draft of [composer, otherDraft]) {
|
|
282
|
+
if (draft?.error)
|
|
283
|
+
fallbackLogger.error(`Could not queue ${draft.kind}: ${plainTerminalText(draft.error)}`);
|
|
284
|
+
}
|
|
162
285
|
commandHandlers.clear();
|
|
163
286
|
store = undefined;
|
|
164
287
|
destroyed = true;
|
|
@@ -183,45 +306,83 @@ export function createDashboard(opts = {}) {
|
|
|
183
306
|
footerHeight: state.stats.session ? 2 : 1
|
|
184
307
|
});
|
|
185
308
|
const nextBuffer = new ScreenBuffer(cols, rows);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const height = remaining.y - contextRect.y;
|
|
199
|
-
if (layout.summary) {
|
|
200
|
-
layout.summary.y = remaining.y;
|
|
201
|
-
layout.summary.height = Math.min(layout.summary.height, remaining.height);
|
|
202
|
-
layout.leftPane.y = layout.summary.y + layout.summary.height;
|
|
203
|
-
layout.leftPane.height = Math.max(0, remaining.height - layout.summary.height);
|
|
309
|
+
let cursor;
|
|
310
|
+
if (conversation) {
|
|
311
|
+
const activePlanId = state.stats.run?.activePlanId;
|
|
312
|
+
if (activePlanId !== lastActivePlanId)
|
|
313
|
+
feedback = undefined;
|
|
314
|
+
if (composer && (composer.afterPlanId === undefined ||
|
|
315
|
+
(composer.text.length === 0 && composer.afterPlanId === lastActivePlanId))) {
|
|
316
|
+
composer = { ...composer, afterPlanId: activePlanId };
|
|
317
|
+
}
|
|
318
|
+
if (otherDraft.afterPlanId === undefined ||
|
|
319
|
+
(otherDraft.text.length === 0 && otherDraft.afterPlanId === lastActivePlanId)) {
|
|
320
|
+
otherDraft = { ...otherDraft, afterPlanId: activePlanId };
|
|
204
321
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
322
|
+
lastActivePlanId = activePlanId;
|
|
323
|
+
const view = renderRunView(nextBuffer, {
|
|
324
|
+
title, stats: state.stats, output: heldOutput ?? state.output, scrollOffset,
|
|
325
|
+
composer, submitting, showQueue, showDetails, workOffset, feedback, hints: opts.hints,
|
|
326
|
+
now: heldOutput === undefined ? Date.now() : heldAt
|
|
327
|
+
});
|
|
328
|
+
scrollOffset = view.scrollOffset;
|
|
329
|
+
workOffset = view.workOffset;
|
|
330
|
+
outputRect = view.outputRect;
|
|
331
|
+
cursor = view.cursor;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
renderBorder(nextBuffer, layout, {
|
|
335
|
+
leftTitle: title,
|
|
336
|
+
rightTitle: statsTitle,
|
|
337
|
+
style: { dim: true }
|
|
338
|
+
});
|
|
339
|
+
if (state.stats.context?.length) {
|
|
340
|
+
const contextRect = layout.summary ?? layout.leftPane;
|
|
341
|
+
// Context spans both panes on wide terminals.
|
|
342
|
+
const remaining = renderContextPane(nextBuffer, {
|
|
343
|
+
x: contextRect.x, y: contextRect.y, width: Math.max(0, cols - 2),
|
|
344
|
+
height: layout.summary ? layout.summary.height + layout.leftPane.height : layout.leftPane.height
|
|
345
|
+
}, state.stats.context);
|
|
346
|
+
const height = remaining.y - contextRect.y;
|
|
347
|
+
if (layout.summary) {
|
|
348
|
+
layout.summary.y = remaining.y;
|
|
349
|
+
layout.summary.height = Math.min(layout.summary.height, remaining.height);
|
|
350
|
+
layout.leftPane.y = layout.summary.y + layout.summary.height;
|
|
351
|
+
layout.leftPane.height = Math.max(0, remaining.height - layout.summary.height);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
layout.leftPane.y += height;
|
|
355
|
+
layout.leftPane.height -= height;
|
|
356
|
+
layout.rightPane.y += height;
|
|
357
|
+
layout.rightPane.height -= height;
|
|
358
|
+
}
|
|
210
359
|
}
|
|
360
|
+
outputViewportHeight = Math.max(0, layout.leftPane.height - (showPerformance ? 1 : 0));
|
|
361
|
+
scrollOffset = renderOutputPane(nextBuffer, { ...layout.leftPane, height: outputViewportHeight }, heldOutput ?? state.output, scrollOffset);
|
|
362
|
+
if (scrollOffset === 0)
|
|
363
|
+
heldOutput = undefined;
|
|
364
|
+
renderStatsPane(nextBuffer, layout.rightPane, state.stats);
|
|
365
|
+
if (layout.summary)
|
|
366
|
+
renderCompactStatsPane(nextBuffer, layout.summary, state.stats);
|
|
367
|
+
renderFooter(nextBuffer, layout.footer, footerHints, state.stats.session);
|
|
211
368
|
}
|
|
212
|
-
|
|
213
|
-
scrollOffset = renderOutputPane(nextBuffer, { ...layout.leftPane, height: outputViewportHeight }, heldOutput ?? state.output, scrollOffset);
|
|
214
|
-
if (scrollOffset === 0)
|
|
369
|
+
if (scrollOffset === 0) {
|
|
215
370
|
heldOutput = undefined;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
nextBuffer.putInRect(layout.leftPane, layout.leftPane.height - 1, formatRenderPerformance(performanceMonitor.snapshot(), layout.leftPane.width), { dim: true });
|
|
371
|
+
heldAt = undefined;
|
|
372
|
+
}
|
|
373
|
+
const performanceRect = outputRect ?? layout.leftPane;
|
|
374
|
+
if (showPerformance && performanceRect.height > 0) {
|
|
375
|
+
nextBuffer.putInRect(performanceRect, performanceRect.height - 1, formatRenderPerformance(performanceMonitor.snapshot(), performanceRect.width), { dim: true });
|
|
222
376
|
}
|
|
223
377
|
const changes = diff(previousBuffer, nextBuffer);
|
|
224
378
|
driver.flush(changes);
|
|
379
|
+
if (cursor) {
|
|
380
|
+
driver.moveTo(cursor.x, cursor.y);
|
|
381
|
+
driver.showCursor();
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
driver.hideCursor();
|
|
385
|
+
}
|
|
225
386
|
performanceMonitor.end(startedAt, { changedCells: changes.length });
|
|
226
387
|
opts.onPerformance?.(performanceMonitor.snapshot());
|
|
227
388
|
previousBuffer = nextBuffer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatElapsed(ms: number): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function formatElapsed(ms) {
|
|
2
|
+
const safeMs = Number.isFinite(ms) ? ms : 0;
|
|
3
|
+
const totalSeconds = Math.max(0, Math.floor(safeMs / 1_000));
|
|
4
|
+
const hours = Math.floor(totalSeconds / 3_600);
|
|
5
|
+
const minutes = Math.floor((totalSeconds % 3_600) / 60);
|
|
6
|
+
const seconds = totalSeconds % 60;
|
|
7
|
+
return [hours, minutes, seconds].map((value) => value.toString().padStart(2, "0")).join(":");
|
|
8
|
+
}
|
|
@@ -3,8 +3,9 @@ export { createDashboardLineBuffer, createStreamingDashboardLineBuffer } from ".
|
|
|
3
3
|
export { limitOutputPreview, createOutputPreviewBuffer } from "./output-preview.js";
|
|
4
4
|
export { shouldUseInteractiveDashboard } from "./should-use-dashboard.js";
|
|
5
5
|
export type { Dashboard, DashboardOptions } from "./dashboard.js";
|
|
6
|
+
export type { DashboardSubmission } from "./composer.js";
|
|
6
7
|
export { renderDashboardSnapshot } from "./snapshot.js";
|
|
7
8
|
export type { SnapshotOptions } from "./snapshot.js";
|
|
8
|
-
export type { OutputItem, OutputItemKind, DashboardStats, Command, DashboardState } from "./types.js";
|
|
9
|
+
export type { OutputItem, OutputItemKind, DashboardStats, DashboardTask, DashboardRunState, DashboardQueueItem, DashboardWorkStatus, Command, DashboardState } from "./types.js";
|
|
9
10
|
export { defaultHints } from "./components/footer.js";
|
|
10
11
|
export type { FooterHint } from "./components/footer.js";
|
|
@@ -22,7 +22,8 @@ export function createStore() {
|
|
|
22
22
|
}
|
|
23
23
|
function appendOutput(item) {
|
|
24
24
|
const preview = limitOutputPreview(item.text);
|
|
25
|
-
const
|
|
25
|
+
const detail = item.detail === undefined ? undefined : limitOutputPreview(item.detail);
|
|
26
|
+
const retainedItem = preview === item.text && detail === item.detail ? item : { ...item, text: preview, detail };
|
|
26
27
|
const existing = item.id === undefined ? -1 : state.output.findIndex((entry) => entry.id === item.id);
|
|
27
28
|
const next = existing !== -1
|
|
28
29
|
? state.output.map((entry, index) => index === existing ? retainedItem : entry)
|
|
@@ -2,7 +2,7 @@ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme
|
|
|
2
2
|
export function graphemes(value) {
|
|
3
3
|
for (let index = 0; index < value.length; index += 1) {
|
|
4
4
|
const code = value.charCodeAt(index);
|
|
5
|
-
if ((code < 0x20 || code > 0x7e) && (code < 0x4e00 || code > 0x9fff)) {
|
|
5
|
+
if ((code < 0x20 || code > 0x7e) && code !== 0x09 && code !== 0x0a && (code < 0x4e00 || code > 0x9fff)) {
|
|
6
6
|
return Array.from(graphemeSegmenter.segment(value), ({ segment }) => segment);
|
|
7
7
|
}
|
|
8
8
|
}
|