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
@@ -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 (direction > 0)
117
- heldOutput ??= getStore().getState().output;
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
- renderBorder(nextBuffer, layout, {
187
- leftTitle: title,
188
- rightTitle: statsTitle,
189
- style: { dim: true }
190
- });
191
- if (state.stats.context?.length) {
192
- const contextRect = layout.summary ?? layout.leftPane;
193
- // Context spans both panes on wide terminals.
194
- const remaining = renderContextPane(nextBuffer, {
195
- x: contextRect.x, y: contextRect.y, width: Math.max(0, cols - 2),
196
- height: layout.summary ? layout.summary.height + layout.leftPane.height : layout.leftPane.height
197
- }, state.stats.context);
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
- else {
206
- layout.leftPane.y += height;
207
- layout.leftPane.height -= height;
208
- layout.rightPane.y += height;
209
- layout.rightPane.height -= height;
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
- outputViewportHeight = Math.max(0, layout.leftPane.height - (showPerformance ? 1 : 0));
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
- renderStatsPane(nextBuffer, layout.rightPane, state.stats);
217
- if (layout.summary)
218
- renderCompactStatsPane(nextBuffer, layout.summary, state.stats);
219
- renderFooter(nextBuffer, layout.footer, footerHints, state.stats.session);
220
- if (showPerformance && layout.leftPane.height > 0) {
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 retainedItem = preview === item.text ? item : { ...item, text: preview };
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
  }