terminal-pilot 0.0.32 → 0.0.34

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 (61) hide show
  1. package/dist/cli.js +610 -163
  2. package/dist/cli.js.map +4 -4
  3. package/dist/commands/close-session.js +10 -0
  4. package/dist/commands/close-session.js.map +3 -3
  5. package/dist/commands/create-session.js +10 -0
  6. package/dist/commands/create-session.js.map +3 -3
  7. package/dist/commands/fill.js +10 -0
  8. package/dist/commands/fill.js.map +3 -3
  9. package/dist/commands/get-session.js +10 -0
  10. package/dist/commands/get-session.js.map +3 -3
  11. package/dist/commands/index.js +22 -17
  12. package/dist/commands/index.js.map +4 -4
  13. package/dist/commands/install.js +22 -17
  14. package/dist/commands/install.js.map +4 -4
  15. package/dist/commands/installer.js +22 -17
  16. package/dist/commands/installer.js.map +4 -4
  17. package/dist/commands/list-sessions.js +10 -0
  18. package/dist/commands/list-sessions.js.map +3 -3
  19. package/dist/commands/press-key.js +10 -0
  20. package/dist/commands/press-key.js.map +3 -3
  21. package/dist/commands/read-history.js +10 -0
  22. package/dist/commands/read-history.js.map +3 -3
  23. package/dist/commands/read-screen.js +10 -0
  24. package/dist/commands/read-screen.js.map +3 -3
  25. package/dist/commands/resize.js +10 -0
  26. package/dist/commands/resize.js.map +3 -3
  27. package/dist/commands/runtime.js +10 -0
  28. package/dist/commands/runtime.js.map +3 -3
  29. package/dist/commands/screenshot.js +10 -0
  30. package/dist/commands/screenshot.js.map +3 -3
  31. package/dist/commands/send-signal.js +10 -0
  32. package/dist/commands/send-signal.js.map +3 -3
  33. package/dist/commands/type.js +10 -0
  34. package/dist/commands/type.js.map +3 -3
  35. package/dist/commands/uninstall.js +13 -4
  36. package/dist/commands/uninstall.js.map +4 -4
  37. package/dist/commands/wait-for-exit.js +10 -0
  38. package/dist/commands/wait-for-exit.js.map +3 -3
  39. package/dist/commands/wait-for.js +10 -0
  40. package/dist/commands/wait-for.js.map +3 -3
  41. package/dist/testing/cli-repl.js +610 -163
  42. package/dist/testing/cli-repl.js.map +4 -4
  43. package/dist/testing/qa-cli.js +610 -163
  44. package/dist/testing/qa-cli.js.map +4 -4
  45. package/node_modules/toolcraft-design/dist/components/help-formatter-plain.js +1 -1
  46. package/node_modules/toolcraft-design/dist/components/help-formatter.js +1 -1
  47. package/node_modules/toolcraft-design/dist/components/inspector-card.d.ts +21 -0
  48. package/node_modules/toolcraft-design/dist/components/inspector-card.js +42 -0
  49. package/node_modules/toolcraft-design/dist/components/resource-browser.d.ts +21 -0
  50. package/node_modules/toolcraft-design/dist/components/resource-browser.js +98 -0
  51. package/node_modules/toolcraft-design/dist/explorer/index.d.ts +2 -0
  52. package/node_modules/toolcraft-design/dist/explorer/index.js +1 -0
  53. package/node_modules/toolcraft-design/dist/explorer/two-pane.d.ts +102 -0
  54. package/node_modules/toolcraft-design/dist/explorer/two-pane.js +510 -0
  55. package/node_modules/toolcraft-design/dist/index.d.ts +6 -2
  56. package/node_modules/toolcraft-design/dist/index.js +3 -1
  57. package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.d.ts +3 -3
  58. package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.js +3 -3
  59. package/node_modules/toolcraft-design/dist/prompts/interactive/keys.js +3 -0
  60. package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.js +8 -4
  61. package/package.json +4 -3
@@ -0,0 +1,510 @@
1
+ import { ScreenBuffer, cellToAnsi } from "../dashboard/buffer.js";
2
+ import { createTerminalDriver } from "../dashboard/terminal.js";
3
+ const TOAST_MS = 2500;
4
+ export async function runTwoPaneExplorer(config) {
5
+ if (process.stdout.isTTY !== true) {
6
+ throw new Error("two-pane explorer requires a TTY");
7
+ }
8
+ return new TwoPaneExplorerRuntime(config, createTerminalDriver()).run();
9
+ }
10
+ export class TwoPaneExplorerRuntime {
11
+ config;
12
+ driver;
13
+ state;
14
+ unsubscribeKeypress;
15
+ unsubscribeResize;
16
+ toastTimer;
17
+ stopped = false;
18
+ rowsRequestToken = 0;
19
+ settle;
20
+ constructor(config, driver) {
21
+ this.config = config;
22
+ this.driver = driver;
23
+ const size = normalizeSize(driver.getSize());
24
+ this.state = {
25
+ title: config.title,
26
+ panes: [
27
+ initialPaneState(config.panes[0]),
28
+ initialPaneState(config.panes[1])
29
+ ],
30
+ activePaneIndex: 0,
31
+ filterFocused: false,
32
+ toast: null,
33
+ size
34
+ };
35
+ }
36
+ run() {
37
+ return new Promise((resolve, reject) => {
38
+ this.settle = { resolve, reject };
39
+ try {
40
+ this.startTerminal();
41
+ this.render();
42
+ this.loadRows().catch((error) => this.fail(error));
43
+ }
44
+ catch (error) {
45
+ this.fail(error);
46
+ }
47
+ });
48
+ }
49
+ startTerminal() {
50
+ this.driver.enterRawMode();
51
+ this.driver.enterAltScreen();
52
+ this.driver.disableLineWrap();
53
+ this.driver.hideCursor();
54
+ this.subscribeKeypress();
55
+ this.unsubscribeResize = this.driver.onResize(() => {
56
+ this.state = { ...this.state, size: normalizeSize(this.driver.getSize()) };
57
+ this.render();
58
+ });
59
+ }
60
+ subscribeKeypress() {
61
+ this.unsubscribeKeypress = this.driver.onKeypress((key) => {
62
+ this.dispatchKey(key);
63
+ });
64
+ }
65
+ pauseKeypress() {
66
+ this.unsubscribeKeypress?.();
67
+ this.unsubscribeKeypress = undefined;
68
+ }
69
+ async loadRows(requestToken = ++this.rowsRequestToken) {
70
+ const [leftRows, rightRows] = await Promise.all([
71
+ this.config.panes[0].rows(),
72
+ this.config.panes[1].rows()
73
+ ]);
74
+ if (requestToken !== this.rowsRequestToken) {
75
+ return;
76
+ }
77
+ this.state = {
78
+ ...this.state,
79
+ panes: [
80
+ rowsLoaded(this.state.panes[0], leftRows),
81
+ rowsLoaded(this.state.panes[1], rightRows)
82
+ ]
83
+ };
84
+ this.render();
85
+ }
86
+ async refresh() {
87
+ await this.config.refresh?.();
88
+ await this.loadRows(++this.rowsRequestToken);
89
+ }
90
+ dispatchKey(key) {
91
+ if (this.stopped) {
92
+ return;
93
+ }
94
+ this.trace("key", {
95
+ key: traceKey(key),
96
+ filterFocused: this.state.filterFocused,
97
+ pane: this.activePane().id,
98
+ cursor: this.activePane().cursor,
99
+ selected: this.activePane().selected.size,
100
+ filter: this.activePane().filter
101
+ });
102
+ if (this.state.filterFocused) {
103
+ this.dispatchFilterKey(key);
104
+ return;
105
+ }
106
+ if (isQuitKey(key)) {
107
+ this.exit(null);
108
+ return;
109
+ }
110
+ if (isTabKey(key)) {
111
+ this.state = {
112
+ ...this.state,
113
+ activePaneIndex: this.state.activePaneIndex === 0 ? 1 : 0
114
+ };
115
+ this.render();
116
+ return;
117
+ }
118
+ if (key.name === "up" || key.name === "down") {
119
+ this.moveCursor(key.name === "up" ? -1 : 1);
120
+ return;
121
+ }
122
+ if (key.name === "home") {
123
+ this.setCursor(0);
124
+ return;
125
+ }
126
+ if (key.name === "end") {
127
+ this.setCursor(filteredRows(this.activePane()).length - 1);
128
+ return;
129
+ }
130
+ if (key.ch === " " || key.name === "space") {
131
+ this.toggleSelection();
132
+ return;
133
+ }
134
+ if (key.ch === "/") {
135
+ this.state = {
136
+ ...this.state,
137
+ filterFocused: true,
138
+ panes: updateActivePane(this.state, (pane) => ({ ...pane, filter: "", cursor: 0 }))
139
+ };
140
+ this.render();
141
+ return;
142
+ }
143
+ const action = this.config.actions.find((candidate) => actionMatchesKey(candidate, key));
144
+ if (action !== undefined) {
145
+ this.runAction(action);
146
+ }
147
+ }
148
+ dispatchFilterKey(key) {
149
+ if (key.name === "escape" || key.name === "return" || key.name === "enter") {
150
+ this.state = { ...this.state, filterFocused: false };
151
+ this.trace("filter.submit", {
152
+ key: traceKey(key),
153
+ pane: this.activePane().id,
154
+ filter: this.activePane().filter,
155
+ rows: filteredRows(this.activePane()).length
156
+ });
157
+ this.render();
158
+ return;
159
+ }
160
+ if (key.name === "backspace") {
161
+ this.state = {
162
+ ...this.state,
163
+ panes: updateActivePane(this.state, (pane) => ({
164
+ ...pane,
165
+ filter: Array.from(pane.filter).slice(0, -1).join(""),
166
+ cursor: 0
167
+ }))
168
+ };
169
+ this.trace("filter.update", {
170
+ key: traceKey(key),
171
+ pane: this.activePane().id,
172
+ filter: this.activePane().filter,
173
+ rows: filteredRows(this.activePane()).length
174
+ });
175
+ this.render();
176
+ return;
177
+ }
178
+ if (key.ch !== undefined && key.ch.length > 0 && key.ch !== "/") {
179
+ this.state = {
180
+ ...this.state,
181
+ panes: updateActivePane(this.state, (pane) => ({
182
+ ...pane,
183
+ filter: `${pane.filter}${key.ch}`,
184
+ cursor: 0
185
+ }))
186
+ };
187
+ this.trace("filter.update", {
188
+ key: traceKey(key),
189
+ pane: this.activePane().id,
190
+ filter: this.activePane().filter,
191
+ rows: filteredRows(this.activePane()).length
192
+ });
193
+ this.render();
194
+ }
195
+ }
196
+ moveCursor(delta) {
197
+ const pane = this.activePane();
198
+ const rows = filteredRows(pane);
199
+ if (rows.length === 0) {
200
+ return;
201
+ }
202
+ this.setCursor(Math.max(0, Math.min(rows.length - 1, pane.cursor + delta)));
203
+ }
204
+ setCursor(cursor) {
205
+ this.state = {
206
+ ...this.state,
207
+ panes: updateActivePane(this.state, (pane) => ({
208
+ ...pane,
209
+ cursor: Math.max(0, cursor)
210
+ }))
211
+ };
212
+ this.render();
213
+ }
214
+ toggleSelection() {
215
+ const pane = this.activePane();
216
+ const row = currentRow(pane);
217
+ if (row === undefined) {
218
+ return;
219
+ }
220
+ const selected = new Set(pane.selected);
221
+ if (selected.has(row.id)) {
222
+ selected.delete(row.id);
223
+ }
224
+ else {
225
+ selected.add(row.id);
226
+ }
227
+ this.state = {
228
+ ...this.state,
229
+ panes: updateActivePane(this.state, (candidate) => ({ ...candidate, selected }))
230
+ };
231
+ this.trace("selection.toggle", {
232
+ pane: pane.id,
233
+ row: row.id,
234
+ selected: selected.size,
235
+ checked: selected.has(row.id),
236
+ filter: pane.filter,
237
+ cursor: pane.cursor
238
+ });
239
+ this.render();
240
+ }
241
+ runAction(action) {
242
+ const activePane = this.activePane();
243
+ const inactivePane = this.inactivePane();
244
+ const row = currentRow(activePane);
245
+ if (row === undefined) {
246
+ this.showToast("Select an item first", "warning");
247
+ return;
248
+ }
249
+ const selectedRows = activePane.rows.filter((candidate) => activePane.selected.has(candidate.id));
250
+ const context = {
251
+ activePane,
252
+ inactivePane,
253
+ row,
254
+ rows: selectedRows.length > 0 ? selectedRows : [row],
255
+ refresh: async () => this.refresh(),
256
+ suspendAnd: async (fn) => this.suspendAnd(fn),
257
+ toast: (message, tone) => this.showToast(message, tone),
258
+ exit: (result) => this.exit(result ?? null)
259
+ };
260
+ Promise.resolve()
261
+ .then(() => action.handler(context))
262
+ .catch((error) => {
263
+ this.showToast(error instanceof Error ? error.message : "Action failed", "error");
264
+ });
265
+ }
266
+ async suspendAnd(fn) {
267
+ this.pauseKeypress();
268
+ this.driver.exitAltScreen();
269
+ this.driver.enableLineWrap();
270
+ this.driver.showCursor();
271
+ this.driver.exitRawMode();
272
+ try {
273
+ return await fn();
274
+ }
275
+ finally {
276
+ if (!this.stopped) {
277
+ this.driver.enterRawMode();
278
+ this.driver.enterAltScreen();
279
+ this.driver.disableLineWrap();
280
+ this.driver.hideCursor();
281
+ this.subscribeKeypress();
282
+ this.state = { ...this.state, size: normalizeSize(this.driver.getSize()) };
283
+ this.render();
284
+ }
285
+ }
286
+ }
287
+ showToast(message, tone = "info") {
288
+ if (this.toastTimer !== undefined) {
289
+ clearTimeout(this.toastTimer);
290
+ }
291
+ this.state = { ...this.state, toast: { message, tone } };
292
+ this.render();
293
+ this.toastTimer = setTimeout(() => {
294
+ this.state = { ...this.state, toast: null };
295
+ this.render();
296
+ }, TOAST_MS);
297
+ }
298
+ activePane() {
299
+ return this.state.panes[this.state.activePaneIndex];
300
+ }
301
+ inactivePane() {
302
+ return this.state.panes[this.state.activePaneIndex === 0 ? 1 : 0];
303
+ }
304
+ render() {
305
+ if (this.stopped) {
306
+ return;
307
+ }
308
+ const size = normalizeSize(this.driver.getSize());
309
+ if (size.cols !== this.state.size.cols || size.rows !== this.state.size.rows) {
310
+ this.state = { ...this.state, size };
311
+ }
312
+ const next = new ScreenBuffer(this.state.size.cols, this.state.size.rows);
313
+ renderTwoPaneExplorer(this.state, this.config.actions, next);
314
+ this.driver.write(screenToAnsi(next));
315
+ }
316
+ exit(result) {
317
+ if (this.stopped) {
318
+ return;
319
+ }
320
+ this.stopped = true;
321
+ this.unsubscribeKeypress?.();
322
+ this.unsubscribeResize?.();
323
+ if (this.toastTimer !== undefined) {
324
+ clearTimeout(this.toastTimer);
325
+ }
326
+ this.driver.destroy();
327
+ this.settle?.resolve(result);
328
+ }
329
+ fail(error) {
330
+ if (!this.stopped) {
331
+ this.stopped = true;
332
+ this.driver.destroy();
333
+ }
334
+ this.settle?.reject(error);
335
+ }
336
+ trace(event, fields = {}) {
337
+ const trace = this.config.trace;
338
+ if (trace === undefined) {
339
+ return;
340
+ }
341
+ void Promise.resolve(trace({ event, ...fields })).catch(() => {
342
+ // Diagnostic tracing must not interfere with TUI input handling.
343
+ });
344
+ }
345
+ }
346
+ export function renderTwoPaneExplorer(state, actions, screen) {
347
+ screen.clear();
348
+ if (state.size.cols < 60 || state.size.rows < 8) {
349
+ screen.put(0, 0, fit(` ${state.title} `, state.size.cols), { bold: true });
350
+ screen.put(0, 2, fit("Terminal is too small for two-pane view.", state.size.cols));
351
+ screen.put(0, state.size.rows - 1, fit("q quit", state.size.cols), { dim: true });
352
+ return;
353
+ }
354
+ const gap = 1;
355
+ const paneWidth = Math.floor((state.size.cols - gap) / 2);
356
+ const rightWidth = state.size.cols - paneWidth - gap;
357
+ const paneHeight = Math.max(5, state.size.rows - 2);
358
+ renderPane(screen, state.panes[0], {
359
+ x: 0,
360
+ y: 0,
361
+ width: paneWidth,
362
+ height: paneHeight,
363
+ active: state.activePaneIndex === 0
364
+ });
365
+ renderPane(screen, state.panes[1], {
366
+ x: paneWidth + gap,
367
+ y: 0,
368
+ width: rightWidth,
369
+ height: paneHeight,
370
+ active: state.activePaneIndex === 1
371
+ });
372
+ renderFooter(screen, state, actions, state.size.rows - 2);
373
+ if (state.toast !== null) {
374
+ screen.put(0, state.size.rows - 1, fit(` ${state.toast.message} `, state.size.cols), toneStyle(state.toast.tone));
375
+ }
376
+ }
377
+ function renderPane(screen, pane, layout) {
378
+ const borderStyle = layout.active ? { bold: true, fg: "cyan" } : { dim: true };
379
+ const title = `${layout.active ? ">" : " "} ${pane.title}`;
380
+ screen.put(layout.x, layout.y, `+${fit(title, layout.width - 2, "-")}+`, borderStyle);
381
+ for (let y = 1; y < layout.height - 1; y += 1) {
382
+ screen.put(layout.x, layout.y + y, "|", borderStyle);
383
+ screen.put(layout.x + layout.width - 1, layout.y + y, "|", borderStyle);
384
+ }
385
+ const filterLabel = pane.filter.length > 0 ? ` /${pane.filter}` : "";
386
+ screen.put(layout.x, layout.y + layout.height - 1, `+${fit(`${pane.selected.size} selected${filterLabel}`, layout.width - 2, "-")}+`, borderStyle);
387
+ const visibleRows = filteredRows(pane);
388
+ const bodyHeight = layout.height - 2;
389
+ if (visibleRows.length === 0) {
390
+ screen.put(layout.x + 2, layout.y + 2, fit(pane.emptyHint, Math.max(0, layout.width - 4)), { dim: true });
391
+ return;
392
+ }
393
+ const start = Math.max(0, Math.min(pane.cursor - Math.floor(bodyHeight / 2), visibleRows.length - bodyHeight));
394
+ for (let index = 0; index < bodyHeight; index += 1) {
395
+ const row = visibleRows[start + index];
396
+ if (row === undefined) {
397
+ continue;
398
+ }
399
+ const selected = pane.selected.has(row.id);
400
+ const cursor = start + index === pane.cursor;
401
+ const marker = selected ? "*" : " ";
402
+ const pointer = cursor && layout.active ? ">" : " ";
403
+ const badge = row.badge ? ` [${row.badge.text}]` : "";
404
+ const subtitle = row.subtitle ? ` ${row.subtitle}` : "";
405
+ const style = cursor && layout.active ? { inverse: true } : {};
406
+ screen.put(layout.x + 1, layout.y + 1 + index, fit(`${pointer}${marker} ${row.title}${badge}${subtitle}`, layout.width - 2), style);
407
+ }
408
+ }
409
+ function renderFooter(screen, state, actions, y) {
410
+ const actionHelp = actions.map((action) => `${firstKey(action.key)} ${action.label}`).join(" ");
411
+ const filter = state.filterFocused ? "search: typing, enter done" : "/ search";
412
+ const text = `tab pane ${filter} space select ${actionHelp} q quit`;
413
+ screen.put(0, y, fit(text, state.size.cols), { dim: true });
414
+ }
415
+ function initialPaneState(definition) {
416
+ return {
417
+ id: definition.id,
418
+ title: definition.title,
419
+ rows: [],
420
+ cursor: 0,
421
+ selected: new Set(),
422
+ filter: "",
423
+ emptyHint: definition.emptyHint ?? "No items"
424
+ };
425
+ }
426
+ function rowsLoaded(pane, rows) {
427
+ const ids = new Set(rows.map((row) => row.id));
428
+ return {
429
+ ...pane,
430
+ rows,
431
+ selected: new Set([...pane.selected].filter((id) => ids.has(id))),
432
+ cursor: Math.max(0, Math.min(pane.cursor, Math.max(0, filteredRows({ ...pane, rows }).length - 1)))
433
+ };
434
+ }
435
+ function filteredRows(pane) {
436
+ const query = pane.filter.trim().toLowerCase();
437
+ if (query.length === 0) {
438
+ return pane.rows;
439
+ }
440
+ return pane.rows.filter((row) => {
441
+ const haystack = `${row.title} ${row.subtitle ?? ""} ${row.badge?.text ?? ""}`.toLowerCase();
442
+ return haystack.includes(query);
443
+ });
444
+ }
445
+ function currentRow(pane) {
446
+ return filteredRows(pane)[pane.cursor];
447
+ }
448
+ function updateActivePane(state, update) {
449
+ return state.activePaneIndex === 0
450
+ ? [update(state.panes[0]), state.panes[1]]
451
+ : [state.panes[0], update(state.panes[1])];
452
+ }
453
+ function actionMatchesKey(action, key) {
454
+ const keys = Array.isArray(action.key) ? action.key : [action.key];
455
+ return keys.some((candidate) => key.ch === candidate || key.name === candidate);
456
+ }
457
+ function isTabKey(key) {
458
+ return key.name === "tab" || key.ch === "\t" || (key.name === "i" && key.ctrl);
459
+ }
460
+ function firstKey(key) {
461
+ return Array.isArray(key) ? key[0] ?? "" : key;
462
+ }
463
+ function traceKey(key) {
464
+ return {
465
+ name: key.name,
466
+ ch: key.ch,
467
+ ctrl: key.ctrl,
468
+ meta: key.meta,
469
+ shift: key.shift
470
+ };
471
+ }
472
+ function isQuitKey(key) {
473
+ return key.ch === "q" || (key.name === "c" && key.ctrl);
474
+ }
475
+ function normalizeSize(size) {
476
+ return {
477
+ cols: Math.max(0, Math.floor(size.cols)),
478
+ rows: Math.max(0, Math.floor(size.rows))
479
+ };
480
+ }
481
+ function fit(text, width, pad = " ") {
482
+ if (width <= 0) {
483
+ return "";
484
+ }
485
+ const normalized = text.length > width ? text.slice(0, width) : text;
486
+ return normalized.padEnd(width, pad);
487
+ }
488
+ function toneStyle(tone) {
489
+ if (tone === "success") {
490
+ return { fg: "green", bold: true };
491
+ }
492
+ if (tone === "warning") {
493
+ return { fg: "yellow", bold: true };
494
+ }
495
+ if (tone === "error") {
496
+ return { fg: "red", bold: true };
497
+ }
498
+ return tone === "muted" ? { dim: true } : { fg: "cyan", bold: true };
499
+ }
500
+ function screenToAnsi(screen) {
501
+ let output = "";
502
+ for (let y = 0; y < screen.height; y += 1) {
503
+ output += `\u001b[${y + 1};1H`;
504
+ for (let x = 0; x < screen.width; x += 1) {
505
+ const cell = screen.get(x, y);
506
+ output += cellToAnsi(cell);
507
+ }
508
+ }
509
+ return output;
510
+ }
@@ -23,6 +23,10 @@ export { renderCatalog } from "./components/catalog.js";
23
23
  export type { CatalogGroup, CatalogItem, CatalogMetric, CatalogTone, RenderCatalogOptions } from "./components/catalog.js";
24
24
  export { renderDetailCard } from "./components/detail-card.js";
25
25
  export type { DetailCardRow, DetailCardSection, RenderDetailCardOptions } from "./components/detail-card.js";
26
+ export { renderInspectorCard } from "./components/inspector-card.js";
27
+ export type { InspectorField, InspectorSection, RenderInspectorCardOptions } from "./components/inspector-card.js";
28
+ export { renderResourceBrowser } from "./components/resource-browser.js";
29
+ export type { RenderResourceBrowserOptions, ResourceBrowserGroup, ResourceBrowserItem } from "./components/resource-browser.js";
26
30
  export { getTemplatePartialNames, renderTemplate, resolveTemplatePartials } from "./components/template.js";
27
31
  export type { RenderTemplateOptions, TemplateEscape } from "./components/template.js";
28
32
  export { openExternal } from "./components/browser.js";
@@ -31,8 +35,8 @@ export * as dashboard from "./dashboard/index.js";
31
35
  export { createDashboard, shouldUseInteractiveDashboard } from "./dashboard/index.js";
32
36
  export type { Dashboard, DashboardOptions } from "./dashboard/index.js";
33
37
  export * as explorer from "./explorer/index.js";
34
- export { runExplorer, singleDetail } from "./explorer/index.js";
35
- export type { Row, DetailItem, Detail, DetailCtx, Action, ActionContext, ExplorerConfig, ReorderContext, Tone } from "./explorer/index.js";
38
+ export { runExplorer, runTwoPaneExplorer, singleDetail } from "./explorer/index.js";
39
+ export type { Row, DetailItem, Detail, DetailCtx, Action, ActionContext, TwoPaneAction, TwoPaneActionContext, TwoPaneDefinition, TwoPaneExplorerConfig, TwoPanePaneState, TwoPaneRow, ExplorerConfig, ReorderContext, Tone } from "./explorer/index.js";
36
40
  export * as prompts from "./prompts/index.js";
37
41
  export { intro, introPlain, outro, note, select, multiselect, text as promptText, confirm, confirmOrCancel, password, spinner, withSpinner, isCancel, cancel, log, PromptCancelledError } from "./prompts/index.js";
38
42
  export type { SelectOptions, MultiselectOptions, TextOptions, ConfirmOptions, PasswordOptions, SpinnerOptions, WithSpinnerOptions } from "./prompts/index.js";
@@ -17,6 +17,8 @@ export { formatCommandNotFoundPanel } from "./components/command-errors.js";
17
17
  export { renderTable } from "./components/table.js";
18
18
  export { renderCatalog } from "./components/catalog.js";
19
19
  export { renderDetailCard } from "./components/detail-card.js";
20
+ export { renderInspectorCard } from "./components/inspector-card.js";
21
+ export { renderResourceBrowser } from "./components/resource-browser.js";
20
22
  export { getTemplatePartialNames, renderTemplate, resolveTemplatePartials } from "./components/template.js";
21
23
  export { openExternal } from "./components/browser.js";
22
24
  // ACP rendering
@@ -26,7 +28,7 @@ export * as dashboard from "./dashboard/index.js";
26
28
  export { createDashboard, shouldUseInteractiveDashboard } from "./dashboard/index.js";
27
29
  // Explorer
28
30
  export * as explorer from "./explorer/index.js";
29
- export { runExplorer, singleDetail } from "./explorer/index.js";
31
+ export { runExplorer, runTwoPaneExplorer, singleDetail } from "./explorer/index.js";
30
32
  // Prompts
31
33
  export * as prompts from "./prompts/index.js";
32
34
  export { intro, introPlain, outro, note, select, multiselect, text as promptText, confirm, confirmOrCancel, password, spinner, withSpinner, isCancel, cancel, log, PromptCancelledError } from "./prompts/index.js";
@@ -10,9 +10,9 @@ export declare const GLYPHS: {
10
10
  readonly barEnd: string;
11
11
  readonly radioActive: string;
12
12
  readonly radioInactive: string;
13
- readonly checkboxActive: string;
14
- readonly checkboxSelected: string;
15
- readonly checkboxInactive: string;
13
+ readonly checkboxActive: "[ ]";
14
+ readonly checkboxSelected: "[x]";
15
+ readonly checkboxInactive: "[ ]";
16
16
  readonly passwordMask: string;
17
17
  readonly ellipsis: "...";
18
18
  };
@@ -27,9 +27,9 @@ export const GLYPHS = {
27
27
  barEnd: glyph("└", "-"),
28
28
  radioActive: glyph("●", ">"),
29
29
  radioInactive: glyph("○", " "),
30
- checkboxActive: glyph("◻", "[ ]"),
31
- checkboxSelected: glyph("◼", "[+]"),
32
- checkboxInactive: glyph("◻", "[ ]"),
30
+ checkboxActive: "[ ]",
31
+ checkboxSelected: "[x]",
32
+ checkboxInactive: "[ ]",
33
33
  passwordMask: glyph("•", "*"),
34
34
  ellipsis: "..."
35
35
  };
@@ -21,6 +21,9 @@ export function mapKey(name, char) {
21
21
  if (char === " ") {
22
22
  return "space";
23
23
  }
24
+ if (char !== undefined && aliases[char] !== undefined) {
25
+ return aliases[char];
26
+ }
24
27
  if (!name) {
25
28
  return undefined;
26
29
  }
@@ -102,10 +102,14 @@ function renderOption(option, values, active, submitted, cancelled) {
102
102
  }
103
103
  function renderMultiselectPrompt(prompt, opts) {
104
104
  if (prompt.state === "submit" || prompt.state === "cancel") {
105
- const labels = prompt.visibleOptions
106
- .filter((option) => hasValue(prompt.value, option.value))
107
- .map((option) => prompt.state === "submit" ? color.dim(option.label) : color.dim.strikethrough(option.label))
108
- .join(", ");
105
+ const selectedOptions = prompt.visibleOptions.filter((option) => hasValue(prompt.value, option.value));
106
+ const labels = selectedOptions.length > 3
107
+ ? prompt.state === "submit"
108
+ ? color.dim(`${selectedOptions.length} selected`)
109
+ : color.dim.strikethrough(`${selectedOptions.length} selected`)
110
+ : selectedOptions
111
+ .map((option) => prompt.state === "submit" ? color.dim(option.label) : color.dim.strikethrough(option.label))
112
+ .join(", ");
109
113
  const end = prompt.state === "submit" ? color.green(GLYPHS.barEnd) : color.red(GLYPHS.barEnd);
110
114
  return `${color.gray(GLYPHS.barStart)} ${symbol(prompt.state)} ${opts.message}\n${color.gray(GLYPHS.bar)} ${labels}\n${end}`;
111
115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-pilot",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
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",
@@ -38,7 +38,7 @@
38
38
  "dist"
39
39
  ],
40
40
  "engines": {
41
- "node": ">=20"
41
+ "node": ">=18.18"
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",
@@ -54,13 +54,14 @@
54
54
  "dependencies": {
55
55
  "@poe-code/agent-skill-config": "*",
56
56
  "@resvg/resvg-js": "^2.6.2",
57
- "commander": "^14.0.3",
57
+ "commander": "^13.1.0",
58
58
  "fast-string-width": "^3.0.2",
59
59
  "fast-wrap-ansi": "^0.2.0",
60
60
  "jetbrains-mono": "^1.0.6",
61
61
  "jose": "^6.1.2",
62
62
  "jsonc-parser": "^3.3.1",
63
63
  "node-pty": "^1.1.0",
64
+ "sisteransi": "^1.0.5",
64
65
  "smol-toml": "^1.6.0",
65
66
  "toolcraft": "^0.0.51",
66
67
  "yaml": "^2.8.2"