terminal-pilot 0.0.42 → 0.0.44

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 (82) hide show
  1. package/README.md +7 -3
  2. package/dist/cli.js +3435 -3165
  3. package/dist/cli.js.map +4 -4
  4. package/dist/commands/close-session.js +5 -0
  5. package/dist/commands/close-session.js.map +2 -2
  6. package/dist/commands/create-session.js +5 -0
  7. package/dist/commands/create-session.js.map +2 -2
  8. package/dist/commands/daemon-runtime.js.map +1 -1
  9. package/dist/commands/fill.js +5 -0
  10. package/dist/commands/fill.js.map +2 -2
  11. package/dist/commands/get-session.js +5 -0
  12. package/dist/commands/get-session.js.map +2 -2
  13. package/dist/commands/index.js +5 -0
  14. package/dist/commands/index.js.map +3 -3
  15. package/dist/commands/install.js +5 -0
  16. package/dist/commands/install.js.map +2 -2
  17. package/dist/commands/installer.js.map +2 -2
  18. package/dist/commands/list-sessions.js +5 -0
  19. package/dist/commands/list-sessions.js.map +2 -2
  20. package/dist/commands/press-key.js +5 -0
  21. package/dist/commands/press-key.js.map +2 -2
  22. package/dist/commands/read-history.js +5 -0
  23. package/dist/commands/read-history.js.map +2 -2
  24. package/dist/commands/read-screen.js +5 -0
  25. package/dist/commands/read-screen.js.map +2 -2
  26. package/dist/commands/resize.js +5 -0
  27. package/dist/commands/resize.js.map +2 -2
  28. package/dist/commands/runtime.js.map +1 -1
  29. package/dist/commands/screenshot.js +5 -0
  30. package/dist/commands/screenshot.js.map +2 -2
  31. package/dist/commands/send-signal.js +5 -0
  32. package/dist/commands/send-signal.js.map +2 -2
  33. package/dist/commands/type.js +5 -0
  34. package/dist/commands/type.js.map +2 -2
  35. package/dist/commands/uninstall.js +5 -0
  36. package/dist/commands/uninstall.js.map +2 -2
  37. package/dist/commands/wait-for-exit.js +5 -0
  38. package/dist/commands/wait-for-exit.js.map +2 -2
  39. package/dist/commands/wait-for.js +5 -0
  40. package/dist/commands/wait-for.js.map +2 -2
  41. package/dist/composition.json +35 -0
  42. package/dist/templates/terminal-pilot.md +1 -1
  43. package/dist/testing/cli-repl.js +3435 -3165
  44. package/dist/testing/cli-repl.js.map +4 -4
  45. package/dist/testing/qa-cli.js +3435 -3165
  46. package/dist/testing/qa-cli.js.map +4 -4
  47. package/node_modules/@poe-code/agent-defs/LICENSE +21 -0
  48. package/node_modules/@poe-code/agent-defs/package.json +9 -2
  49. package/node_modules/@poe-code/agent-skill-config/README.md +25 -17
  50. package/node_modules/@poe-code/agent-skill-config/dist/templates/poe-generate.md +16 -22
  51. package/node_modules/@poe-code/agent-skill-config/dist/templates/terminal-pilot.md +1 -1
  52. package/node_modules/@poe-code/agent-skill-config/package.json +1 -0
  53. package/node_modules/@poe-code/config-mutations/LICENSE +21 -0
  54. package/node_modules/@poe-code/config-mutations/README.md +8 -8
  55. package/node_modules/@poe-code/config-mutations/package.json +8 -1
  56. package/node_modules/@poe-code/frontmatter/LICENSE +21 -0
  57. package/node_modules/@poe-code/frontmatter/README.md +2 -2
  58. package/node_modules/@poe-code/frontmatter/package.json +8 -1
  59. package/node_modules/toolcraft-design/LICENSE +21 -0
  60. package/node_modules/toolcraft-design/README.md +8 -4
  61. package/node_modules/toolcraft-design/dist/components/file-changes.d.ts +16 -0
  62. package/node_modules/toolcraft-design/dist/components/file-changes.js +162 -0
  63. package/node_modules/toolcraft-design/dist/components/index.d.ts +2 -0
  64. package/node_modules/toolcraft-design/dist/components/index.js +1 -0
  65. package/node_modules/toolcraft-design/dist/dashboard/terminal.js +48 -6
  66. package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +4 -0
  67. package/node_modules/toolcraft-design/dist/explorer/actions.js +1 -0
  68. package/node_modules/toolcraft-design/dist/explorer/events.d.ts +4 -0
  69. package/node_modules/toolcraft-design/dist/explorer/jobs.d.ts +1 -0
  70. package/node_modules/toolcraft-design/dist/explorer/jobs.js +28 -0
  71. package/node_modules/toolcraft-design/dist/explorer/keymap.js +2 -2
  72. package/node_modules/toolcraft-design/dist/explorer/reducer.js +66 -6
  73. package/node_modules/toolcraft-design/dist/explorer/render/detail.js +1 -1
  74. package/node_modules/toolcraft-design/dist/explorer/render/list.js +48 -21
  75. package/node_modules/toolcraft-design/dist/explorer/render/modal.js +15 -2
  76. package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +1 -1
  77. package/node_modules/toolcraft-design/dist/explorer/runtime.js +3 -0
  78. package/node_modules/toolcraft-design/dist/explorer/state.d.ts +9 -0
  79. package/node_modules/toolcraft-design/dist/index.d.ts +2 -0
  80. package/node_modules/toolcraft-design/dist/index.js +1 -0
  81. package/node_modules/toolcraft-design/package.json +3 -1
  82. package/package.json +1 -1
@@ -118,19 +118,18 @@ export function createTerminalDriver(opts) {
118
118
  if (destroyed) {
119
119
  return () => { };
120
120
  }
121
- const listener = (str, key) => {
122
- const event = toKeypressEvent(str, key);
123
- if (event !== undefined) {
121
+ const listener = (chunk) => {
122
+ for (const event of parseKeypressChunk(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))) {
124
123
  handler(event);
125
124
  }
126
125
  };
127
126
  keypressListeners.add(listener);
128
- stdin.on("keypress", listener);
127
+ stdin.on("data", listener);
129
128
  return () => {
130
129
  if (!keypressListeners.delete(listener)) {
131
130
  return;
132
131
  }
133
- stdin.off("keypress", listener);
132
+ stdin.off("data", listener);
134
133
  };
135
134
  }
136
135
  function destroy() {
@@ -138,7 +137,7 @@ export function createTerminalDriver(opts) {
138
137
  return;
139
138
  }
140
139
  for (const listener of keypressListeners) {
141
- stdin.off("keypress", listener);
140
+ stdin.off("data", listener);
142
141
  }
143
142
  keypressListeners.clear();
144
143
  for (const listener of resizeListeners) {
@@ -173,6 +172,9 @@ export function parseKeypress(data) {
173
172
  if (data.length === 0) {
174
173
  return undefined;
175
174
  }
175
+ if (data.length === 1 && data[0] === 0x1b) {
176
+ return { name: "escape", ctrl: false, meta: false, shift: false };
177
+ }
176
178
  const stream = new PassThrough();
177
179
  const stdin = stream;
178
180
  let event;
@@ -184,6 +186,46 @@ export function parseKeypress(data) {
184
186
  stream.destroy();
185
187
  return event;
186
188
  }
189
+ function parseKeypressChunk(data) {
190
+ const events = [];
191
+ const input = data.toString("utf8");
192
+ let index = 0;
193
+ while (index < input.length) {
194
+ const sequence = nextKeySequence(input, index);
195
+ if (sequence.length === 0) {
196
+ break;
197
+ }
198
+ const event = parseKeypress(Buffer.from(sequence));
199
+ if (event !== undefined) {
200
+ events.push(event);
201
+ }
202
+ index += sequence.length;
203
+ }
204
+ return events;
205
+ }
206
+ function nextKeySequence(input, index) {
207
+ const character = input[index];
208
+ if (character === undefined) {
209
+ return "";
210
+ }
211
+ if (character !== "\u001b") {
212
+ return character;
213
+ }
214
+ const next = input[index + 1];
215
+ if (next === undefined) {
216
+ return character;
217
+ }
218
+ if (next !== "[") {
219
+ return input.slice(index, index + 2);
220
+ }
221
+ for (let cursor = index + 2; cursor < input.length; cursor += 1) {
222
+ const code = input.charCodeAt(cursor);
223
+ if ((code >= 0x40 && code <= 0x7e) || input[cursor] === "~") {
224
+ return input.slice(index, cursor + 1);
225
+ }
226
+ }
227
+ return input.slice(index);
228
+ }
187
229
  function toKeypressEvent(str, key) {
188
230
  const controlCharacter = controlCharacterToKeypress(key?.sequence);
189
231
  if (controlCharacter !== undefined) {
@@ -7,6 +7,10 @@ type ExplorerKeypressEvent = Extract<ExplorerEvent, {
7
7
  export type ActionRuntimeHandles = {
8
8
  refresh: () => Promise<void>;
9
9
  suspendAnd: <T>(fn: () => Promise<T>) => Promise<T>;
10
+ openModal: (content: {
11
+ title: string;
12
+ content: string;
13
+ }) => void;
10
14
  toast: (msg: string, tone?: Tone) => void;
11
15
  confirm: (prompt: string) => Promise<boolean>;
12
16
  exit: (after?: () => void | Promise<void>) => void;
@@ -20,6 +20,7 @@ export function buildActionContext(state, _action, source, runtimeHandles, rowsO
20
20
  filter: state.filter,
21
21
  refresh: runtimeHandles.refresh,
22
22
  suspendAnd: runtimeHandles.suspendAnd,
23
+ openModal: runtimeHandles.openModal,
23
24
  toast: runtimeHandles.toast,
24
25
  confirm: runtimeHandles.confirm,
25
26
  exit: runtimeHandles.exit
@@ -55,6 +55,10 @@ export type ExplorerEvent = {
55
55
  type: "suspendResumed";
56
56
  value: unknown;
57
57
  emit: ExplorerEvent;
58
+ } | {
59
+ type: "modalOpened";
60
+ title: string;
61
+ content: string;
58
62
  } | {
59
63
  type: "modalDismissed";
60
64
  result: unknown;
@@ -1,6 +1,7 @@
1
1
  import type { ExplorerEvent } from "./events.js";
2
2
  import type { DetailCtx, DetailItem } from "./state.js";
3
3
  export declare const LOADING_INDICATOR_MS = 150;
4
+ export declare const DETAIL_DEBOUNCE_MS = 100;
4
5
  export declare function createDetailJobs(emit: (event: ExplorerEvent) => void): {
5
6
  schedule: (rowId: string, items: (ctx: DetailCtx) => Promise<DetailItem[]>, ctx: DetailCtx) => Promise<void>;
6
7
  abort: () => void;
@@ -1,6 +1,8 @@
1
1
  export const LOADING_INDICATOR_MS = 150;
2
+ export const DETAIL_DEBOUNCE_MS = 100;
2
3
  export function createDetailJobs(emit) {
3
4
  let token = 0;
5
+ let lastScheduleAt = 0;
4
6
  let current = null;
5
7
  const abortedTokens = new Set();
6
8
  return {
@@ -9,6 +11,9 @@ export function createDetailJobs(emit) {
9
11
  current.controller.abort();
10
12
  clearTimeout(current.loadingTimer);
11
13
  }
14
+ const scheduledAt = Date.now();
15
+ const debounce = scheduledAt - lastScheduleAt < DETAIL_DEBOUNCE_MS;
16
+ lastScheduleAt = scheduledAt;
12
17
  const nextToken = ++token;
13
18
  const controller = new AbortController();
14
19
  let finished = false;
@@ -19,6 +24,12 @@ export function createDetailJobs(emit) {
19
24
  }, LOADING_INDICATOR_MS);
20
25
  current = { controller, loadingTimer, token: nextToken };
21
26
  try {
27
+ if (debounce) {
28
+ await waitUnlessAborted(DETAIL_DEBOUNCE_MS, controller.signal);
29
+ if (controller.signal.aborted || nextToken !== token) {
30
+ return;
31
+ }
32
+ }
22
33
  const loadedItems = await items({ ...ctx, signal: controller.signal });
23
34
  finished = true;
24
35
  if (!abortedTokens.has(nextToken)) {
@@ -51,6 +62,23 @@ export function createDetailJobs(emit) {
51
62
  }
52
63
  };
53
64
  }
65
+ function waitUnlessAborted(ms, signal) {
66
+ return new Promise((resolve) => {
67
+ if (signal.aborted) {
68
+ resolve();
69
+ return;
70
+ }
71
+ const onAbort = () => {
72
+ clearTimeout(timer);
73
+ resolve();
74
+ };
75
+ const timer = setTimeout(() => {
76
+ signal.removeEventListener("abort", onAbort);
77
+ resolve();
78
+ }, ms);
79
+ signal.addEventListener("abort", onAbort, { once: true });
80
+ });
81
+ }
54
82
  function toError(error) {
55
83
  if (error instanceof Error) {
56
84
  return error;
@@ -7,8 +7,8 @@ const builtinBindings = {
7
7
  cursorDown: ["down", "j"],
8
8
  top: ["home", "gg"],
9
9
  bottom: ["end", "G"],
10
- pageUp: ["Ctrl+u"],
11
- pageDown: ["Ctrl+d"],
10
+ pageUp: ["pageup", "Ctrl+u"],
11
+ pageDown: ["pagedown", "Ctrl+d"],
12
12
  focusNext: ["tab"],
13
13
  escape: ["escape"],
14
14
  confirm: ["return", "enter"],
@@ -5,6 +5,7 @@ const NO_EFFECTS = [];
5
5
  const DEFAULT_ACTION_HANDLES = {
6
6
  refresh: async () => undefined,
7
7
  suspendAnd: async (fn) => fn(),
8
+ openModal: () => undefined,
8
9
  toast: () => undefined,
9
10
  confirm: async () => false,
10
11
  exit: () => undefined
@@ -31,6 +32,13 @@ export function step(state, event, runtimeHandles = DEFAULT_ACTION_HANDLES) {
31
32
  return expireToast(state);
32
33
  case "suspendResumed":
33
34
  return suspendResumed(state, event.emit, runtimeHandles);
35
+ case "modalOpened":
36
+ return setModal(state, {
37
+ kind: "content",
38
+ title: event.title,
39
+ content: event.content,
40
+ scroll: 0
41
+ });
34
42
  case "modalDismissed":
35
43
  return modalDismissed(state, event.result, runtimeHandles);
36
44
  }
@@ -69,9 +77,13 @@ function stepKey(state, key, runtimeHandles) {
69
77
  case "bottom":
70
78
  return setCursor(state, state.filtered.length - 1);
71
79
  case "pageUp":
72
- return moveCursor(state, -pageSize(state));
80
+ return isDetailBlobFocused(state)
81
+ ? detailScroll(state, -detailBodyHeight(state))
82
+ : moveCursor(state, -pageSize(state));
73
83
  case "pageDown":
74
- return moveCursor(state, pageSize(state));
84
+ return isDetailBlobFocused(state)
85
+ ? detailScroll(state, detailBodyHeight(state))
86
+ : moveCursor(state, pageSize(state));
75
87
  case "focusNext":
76
88
  return focusNext(state);
77
89
  case "escape":
@@ -164,6 +176,26 @@ function stepModalKey(state, key, target, runtimeHandles) {
164
176
  }
165
177
  return paletteInput(state, key);
166
178
  }
179
+ if (state.modal?.kind === "content") {
180
+ if (target?.type === "builtin") {
181
+ switch (target.id) {
182
+ case "escape":
183
+ case "confirm":
184
+ return modalDismissed(state, false, runtimeHandles);
185
+ case "cursorDown":
186
+ case "detailScrollDown":
187
+ return scrollContentModal(state, 1);
188
+ case "cursorUp":
189
+ case "detailScrollUp":
190
+ return scrollContentModal(state, -1);
191
+ case "pageDown":
192
+ return scrollContentModal(state, modalBodyHeight(state));
193
+ case "pageUp":
194
+ return scrollContentModal(state, -modalBodyHeight(state));
195
+ }
196
+ }
197
+ return mark(state, 0);
198
+ }
167
199
  return mark(state, 0);
168
200
  }
169
201
  function resize(state, cols, rows) {
@@ -287,7 +319,7 @@ function suspendResumed(state, emit, runtimeHandles) {
287
319
  }
288
320
  function modalDismissed(state, result, runtimeHandles) {
289
321
  const modal = state.modal;
290
- const closed = { ...state, modal: null, dirty: REGION_MODAL | REGION_FOOTER };
322
+ const closed = { ...state, modal: null, dirty: REGION_ALL };
291
323
  if (modal?.kind === "confirm") {
292
324
  modal.resolver(result === true);
293
325
  }
@@ -300,8 +332,16 @@ function moveCursor(state, delta) {
300
332
  if (state.focused === "detail" && hasDetailCursor(state)) {
301
333
  return moveDetailCursor(state, delta);
302
334
  }
335
+ if (isDetailBlobFocused(state)) {
336
+ return detailScroll(state, delta);
337
+ }
303
338
  return setCursor(state, state.cursor + delta);
304
339
  }
340
+ function isDetailBlobFocused(state) {
341
+ return (state.focused === "detail" &&
342
+ !hasDetailCursor(state) &&
343
+ (state.detail.items?.length ?? 0) > 0);
344
+ }
305
345
  function moveDetailCursor(state, delta) {
306
346
  const max = Math.max(0, (state.detail.items?.length ?? 0) - 1);
307
347
  const cursor = clamp(state.detail.cursor + delta, 0, max);
@@ -466,9 +506,6 @@ function selectionChanged(state, selected) {
466
506
  return { state: next, effects: NO_EFFECTS };
467
507
  }
468
508
  function detailScroll(state, delta) {
469
- if (state.focused !== "detail") {
470
- return mark(state, 0);
471
- }
472
509
  const scroll = clamp(state.detail.scroll + delta, 0, maxDetailScroll(state));
473
510
  if (scroll === state.detail.scroll) {
474
511
  return mark(state, 0);
@@ -478,6 +515,29 @@ function detailScroll(state, delta) {
478
515
  effects: NO_EFFECTS
479
516
  };
480
517
  }
518
+ function scrollContentModal(state, delta) {
519
+ if (state.modal?.kind !== "content") {
520
+ return mark(state, 0);
521
+ }
522
+ const scroll = clamp(state.modal.scroll + delta, 0, maxContentModalScroll(state));
523
+ if (scroll === state.modal.scroll) {
524
+ return mark(state, 0);
525
+ }
526
+ return setModal(state, { ...state.modal, scroll });
527
+ }
528
+ function maxContentModalScroll(state) {
529
+ if (state.modal?.kind !== "content") {
530
+ return 0;
531
+ }
532
+ return Math.max(0, state.modal.content.split("\n").length - modalBodyHeight(state));
533
+ }
534
+ function modalBodyHeight(state) {
535
+ const screenHeight = normalizeSize(state.size.rows);
536
+ if (screenHeight <= 2) {
537
+ return 0;
538
+ }
539
+ return Math.max(0, screenHeight - 4);
540
+ }
481
541
  function clampDetailScroll(state) {
482
542
  const scroll = clamp(state.detail.scroll, 0, maxDetailScroll(state));
483
543
  if (scroll === state.detail.scroll) {
@@ -26,7 +26,7 @@ function renderDetailBody(state, screen, rect, row) {
26
26
  return;
27
27
  }
28
28
  if (items === null) {
29
- writeLine(screen, rect, 0, state.detail.loading ? "Loading detail..." : state.emptyHint, styles.muted);
29
+ writeLine(screen, rect, 0, row === null ? state.emptyHint : "Loading detail...", styles.muted);
30
30
  return;
31
31
  }
32
32
  if (items.length === 0) {
@@ -13,7 +13,7 @@ export function renderList(state, screen, layout) {
13
13
  writeLine(screen, rect, 0, "Terminal too narrow", styles.muted);
14
14
  return;
15
15
  }
16
- drawPaneFrame(screen, rect, "Plans", state.focused === "list" ? styles.borderFocused : styles.border);
16
+ drawPaneFrame(screen, rect, state.title, state.focused === "list" ? styles.borderFocused : styles.border);
17
17
  const bodyRect = paneBodyRect(rect);
18
18
  if (bodyRect.width <= 0 || bodyRect.height <= 0) {
19
19
  return;
@@ -28,28 +28,34 @@ export function renderList(state, screen, layout) {
28
28
  cache.clear();
29
29
  return;
30
30
  }
31
- let lastGroup;
31
+ const lines = buildDisplayLines(state);
32
+ const start = visibleStart(lines, bodyRect.height);
32
33
  let y = 0;
33
- for (const rowIndex of state.filtered) {
34
- if (y >= bodyRect.height) {
35
- break;
36
- }
37
- const row = state.rows[rowIndex];
38
- if (!row) {
39
- continue;
40
- }
41
- if (row.group && row.group !== lastGroup && y < bodyRect.height) {
42
- const hash = `group:${row.group}`;
34
+ for (let lineIndex = start; lineIndex < lines.length && y < bodyRect.height; lineIndex += 1) {
35
+ const line = lines[lineIndex];
36
+ if (line.kind === "group") {
37
+ const hash = `group:${line.label}`;
43
38
  if (cache.get(y) !== hash) {
44
- writeLine(screen, bodyRect, y, formatGroupHeader(row.group, bodyRect), styles.muted);
39
+ writeLine(screen, bodyRect, y, formatGroupHeader(line.label, bodyRect), styles.muted);
45
40
  cache.set(y, hash);
46
41
  }
47
42
  y += 1;
48
- lastGroup = row.group;
43
+ continue;
44
+ }
45
+ if (line.kind === "subtitle") {
46
+ const subtitleHash = `subtitle:${line.row.id}:${line.text}`;
47
+ if (cache.get(y) !== subtitleHash) {
48
+ writeLine(screen, bodyRect, y, ` ${line.text}`, styles.muted);
49
+ cache.set(y, subtitleHash);
50
+ }
51
+ y += 1;
52
+ continue;
49
53
  }
54
+ const rowIndex = line.rowIndex;
50
55
  if (y >= bodyRect.height) {
51
56
  break;
52
57
  }
58
+ const row = line.row;
53
59
  const selected = state.multiSelect && state.selected.has(row.id);
54
60
  const cursor = rowIndex === state.filtered[state.cursor];
55
61
  const positions = state.matchPositions.get(rowIndex) ?? [];
@@ -64,15 +70,36 @@ export function renderList(state, screen, layout) {
64
70
  cache.set(y, hash);
65
71
  }
66
72
  y += 1;
67
- if (row.subtitle && y < bodyRect.height) {
68
- const subtitleHash = `${hash}:subtitle:${row.subtitle}`;
69
- if (cache.get(y) !== subtitleHash) {
70
- writeLine(screen, bodyRect, y, ` ${row.subtitle}`, styles.muted);
71
- cache.set(y, subtitleHash);
72
- }
73
- y += 1;
73
+ }
74
+ }
75
+ function buildDisplayLines(state) {
76
+ const lines = [];
77
+ let lastGroup;
78
+ for (const rowIndex of state.filtered) {
79
+ const row = state.rows[rowIndex];
80
+ if (!row) {
81
+ continue;
82
+ }
83
+ if (row.group && row.group !== lastGroup) {
84
+ lines.push({ kind: "group", label: row.group });
85
+ lastGroup = row.group;
74
86
  }
87
+ lines.push({ kind: "row", rowIndex, row, cursor: rowIndex === state.filtered[state.cursor] });
88
+ if (row.subtitle) {
89
+ lines.push({ kind: "subtitle", row, text: row.subtitle });
90
+ }
91
+ }
92
+ return lines;
93
+ }
94
+ function visibleStart(lines, height) {
95
+ if (height <= 0) {
96
+ return 0;
97
+ }
98
+ const cursorLine = lines.findIndex((line) => line.kind === "row" && line.cursor);
99
+ if (cursorLine < 0) {
100
+ return 0;
75
101
  }
102
+ return Math.max(0, Math.min(cursorLine, cursorLine - height + 1));
76
103
  }
77
104
  function formatGroupHeader(label, rect) {
78
105
  const text = ` ${label} `;
@@ -1,10 +1,11 @@
1
+ import { stripAnsi } from "../../internal/strip-ansi.js";
1
2
  import { getExplorerStyles } from "../theme.js";
2
3
  import { fitToWidth, padEndCells } from "./text.js";
3
4
  export function renderModal(state, screen) {
4
5
  if (state.modal === null || screen.width <= 0 || screen.height <= 0) {
5
6
  return;
6
7
  }
7
- const width = Math.min(screen.width - 2, Math.max(34, Math.floor(screen.width * 0.62)));
8
+ const width = Math.min(screen.width - 2, Math.max(34, Math.floor(screen.width * modalWidthRatio(state))));
8
9
  const height = Math.min(screen.height - 2, modalHeight(state));
9
10
  const x = Math.max(0, Math.floor((screen.width - width) / 2));
10
11
  const y = Math.max(0, Math.floor((screen.height - height) / 2));
@@ -12,7 +13,7 @@ export function renderModal(state, screen) {
12
13
  drawBox(screen, x, y, width, height, title(state), styles.borderFocused);
13
14
  const lines = modalLines(state);
14
15
  for (let row = 0; row < Math.min(lines.length, height - 2); row += 1) {
15
- screen.put(x + 2, y + 1 + row, fitToWidth(lines[row], width - 4, x + 2), row === 1 ? styles.accent : {});
16
+ screen.put(x + 2, y + 1 + row, fitToWidth(stripAnsi(lines[row]), width - 4, x + 2), row === 1 ? styles.accent : {});
16
17
  }
17
18
  }
18
19
  function modalLines(state) {
@@ -39,6 +40,9 @@ function modalLines(state) {
39
40
  ...paletteLines(state)
40
41
  ];
41
42
  }
43
+ if (state.modal?.kind === "content") {
44
+ return state.modal.content.split("\n").slice(state.modal.scroll);
45
+ }
42
46
  return [];
43
47
  }
44
48
  function title(state) {
@@ -48,11 +52,20 @@ function title(state) {
48
52
  if (state.modal?.kind === "confirm") {
49
53
  return "Confirm";
50
54
  }
55
+ if (state.modal?.kind === "content") {
56
+ return state.modal.title;
57
+ }
51
58
  return "Command Palette";
52
59
  }
53
60
  function modalHeight(state) {
61
+ if (state.modal?.kind === "content") {
62
+ return Math.max(5, state.modal.content.split("\n").length + 2);
63
+ }
54
64
  return Math.max(5, modalLines(state).length + 2);
55
65
  }
66
+ function modalWidthRatio(state) {
67
+ return state.modal?.kind === "content" ? 0.86 : 0.62;
68
+ }
56
69
  function drawBox(screen, x, y, width, height, boxTitle, style) {
57
70
  screen.clearRect({ x, y, width, height });
58
71
  const innerWidth = Math.max(0, width - 2);
@@ -6,7 +6,7 @@ import { renderExplorer } from "./index.js";
6
6
  export function renderStateSnapshot(state) {
7
7
  const screen = new ScreenBuffer(state.size.cols, state.size.rows);
8
8
  renderExplorer(state, screen);
9
- return dumpScreen(screen);
9
+ return dumpScreen(screen).split("\n").map((line) => line.trimEnd()).join("\n");
10
10
  }
11
11
  export function dumpScreen(screen) {
12
12
  const lines = [];
@@ -45,6 +45,9 @@ class ExplorerRuntime {
45
45
  await this.refreshRowsFromSource();
46
46
  },
47
47
  suspendAnd: async (fn) => this.suspendAnd(fn),
48
+ openModal: (content) => {
49
+ this.dispatch({ type: "modalOpened", title: content.title, content: content.content });
50
+ },
48
51
  toast: (msg, tone) => {
49
52
  this.showToast(msg, tone);
50
53
  },
@@ -48,6 +48,10 @@ export interface ActionContext<R> {
48
48
  filter: string;
49
49
  refresh: () => Promise<void>;
50
50
  suspendAnd: <T>(fn: () => Promise<T>) => Promise<T>;
51
+ openModal: (content: {
52
+ title: string;
53
+ content: string;
54
+ }) => void;
51
55
  toast: (msg: string, tone?: Tone) => void;
52
56
  confirm: (prompt: string) => Promise<boolean>;
53
57
  exit: (after?: () => void | Promise<void>) => void;
@@ -114,6 +118,11 @@ export interface ExplorerState {
114
118
  kind: "palette";
115
119
  query: string;
116
120
  cursor: number;
121
+ } | {
122
+ kind: "content";
123
+ title: string;
124
+ content: string;
125
+ scroll: number;
117
126
  };
118
127
  toast: {
119
128
  message: string;
@@ -19,6 +19,8 @@ export { formatCommandNotFound } from "./components/command-errors.js";
19
19
  export { formatCommandNotFoundPanel } from "./components/command-errors.js";
20
20
  export { renderTable } from "./components/table.js";
21
21
  export type { TableColumn, RenderTableOptions } from "./components/table.js";
22
+ export { renderFileChanges } from "./components/file-changes.js";
23
+ export type { FileChange, FileChangeDisplayMode, FileChangeKind, FileChangeOutputFormat, RenderFileChangesOptions } from "./components/file-changes.js";
22
24
  export { renderCatalog } from "./components/catalog.js";
23
25
  export type { CatalogGroup, CatalogItem, CatalogMetric, CatalogTone, RenderCatalogOptions } from "./components/catalog.js";
24
26
  export { renderDetailCard } from "./components/detail-card.js";
@@ -15,6 +15,7 @@ export * as helpFormatterPlain from "./components/help-formatter-plain.js";
15
15
  export { formatCommandNotFound } from "./components/command-errors.js";
16
16
  export { formatCommandNotFoundPanel } from "./components/command-errors.js";
17
17
  export { renderTable } from "./components/table.js";
18
+ export { renderFileChanges } from "./components/file-changes.js";
18
19
  export { renderCatalog } from "./components/catalog.js";
19
20
  export { renderDetailCard } from "./components/detail-card.js";
20
21
  export { renderInspectorCard } from "./components/inspector-card.js";
@@ -2,6 +2,7 @@
2
2
  "name": "toolcraft-design",
3
3
  "version": "0.0.2",
4
4
  "private": true,
5
+ "license": "MIT",
5
6
  "type": "module",
6
7
  "main": "dist/index.js",
7
8
  "types": "dist/index.d.ts",
@@ -17,7 +18,8 @@
17
18
  "generate:design-docs:all": "tsx scripts/generate-docs.ts all"
18
19
  },
19
20
  "files": [
20
- "dist"
21
+ "dist",
22
+ "LICENSE"
21
23
  ],
22
24
  "dependencies": {
23
25
  "fast-string-width": "^3.0.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-pilot",
3
- "version": "0.0.42",
3
+ "version": "0.0.44",
4
4
  "description": "Playwright-like SDK and CLI for automating interactive CLI apps through a real PTY",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",