toolcraft 0.0.154 → 0.0.156

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 CHANGED
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.154",
116
+ "version": "0.0.156",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.154",
126
+ "version": "0.0.156",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.154",
116
+ "version": "0.0.156",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.154",
126
+ "version": "0.0.156",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.154",
11
+ "version": "0.0.156",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -13,7 +13,8 @@ export type ActionRuntimeHandles = {
13
13
  content: string;
14
14
  }) => void;
15
15
  toast: (msg: string, tone?: Tone) => void;
16
- confirm: (prompt: string) => Promise<boolean>;
16
+ confirm: ActionContext<unknown>["confirm"];
17
+ promptText: ActionContext<unknown>["promptText"];
17
18
  exit: (after?: () => void | Promise<void>) => void;
18
19
  };
19
20
  export declare function resolveAction<R>(state: ExplorerState, keyEvent: ExplorerKeypressEvent): Action<R> | null;
@@ -13,7 +13,8 @@ export function resolveAction(state, keyEvent) {
13
13
  }
14
14
  export function buildActionContext(state, _action, source, runtimeHandles, rowsOverride) {
15
15
  const detailActive = source === "both" && state.focused === "detail";
16
- const row = rowsOverride?.[0] ?? (detailActive ? currentDetailRow(state) : currentRow(state)) ?? { id: "", title: "" };
16
+ const row = rowsOverride?.[0] ??
17
+ (detailActive ? currentDetailRow(state) : currentRow(state)) ?? { id: "", title: "" };
17
18
  const panes = runtimePanes(state);
18
19
  return {
19
20
  row,
@@ -26,6 +27,7 @@ export function buildActionContext(state, _action, source, runtimeHandles, rowsO
26
27
  openModal: runtimeHandles.openModal,
27
28
  toast: runtimeHandles.toast,
28
29
  confirm: runtimeHandles.confirm,
30
+ promptText: runtimeHandles.promptText,
29
31
  exit: runtimeHandles.exit,
30
32
  activePane: state.focused === "detail" ? panes[1] : panes[0],
31
33
  inactivePane: state.focused === "detail" ? panes[0] : panes[1]
@@ -39,21 +41,47 @@ function currentDetailItem(state) {
39
41
  }
40
42
  function currentDetailRow(state) {
41
43
  const item = currentDetailItem(state);
42
- return item === undefined ? undefined : { id: item.id, title: item.title ?? item.id, subtitle: item.subtitle, badge: item.badge };
44
+ return item === undefined
45
+ ? undefined
46
+ : { id: item.id, title: item.title ?? item.id, subtitle: item.subtitle, badge: item.badge };
43
47
  }
44
48
  function selectedRows(state, fallback) {
45
49
  if (!state.multiSelect || state.selected.size === 0) {
46
50
  return fallback.id === "" ? [] : [fallback];
47
51
  }
48
52
  const source = state.focused === "detail"
49
- ? (state.detail.items ?? []).map(item => ({ id: item.id, title: item.title ?? item.id, subtitle: item.subtitle, badge: item.badge }))
53
+ ? (state.detail.items ?? []).map((item) => ({
54
+ id: item.id,
55
+ title: item.title ?? item.id,
56
+ subtitle: item.subtitle,
57
+ badge: item.badge
58
+ }))
50
59
  : state.rows;
51
60
  return source.filter((row) => state.selected.has(row.id));
52
61
  }
53
62
  function runtimePanes(state) {
54
63
  const definitions = state.paneDefinitions;
55
64
  return [
56
- { id: definitions[0]?.id ?? "list", title: definitions[0]?.title ?? state.title, rows: state.rows, cursor: state.cursor, selected: state.selected, filter: state.filter },
57
- { id: definitions[1]?.id ?? "detail", title: definitions[1]?.title ?? "Preview", rows: (state.detail.items ?? []).map(item => ({ id: item.id, title: item.title ?? item.id, subtitle: item.subtitle, badge: item.badge })), cursor: state.detail.cursor, selected: state.selected, filter: "" }
65
+ {
66
+ id: definitions[0]?.id ?? "list",
67
+ title: definitions[0]?.title ?? state.title,
68
+ rows: state.rows,
69
+ cursor: state.cursor,
70
+ selected: state.selected,
71
+ filter: state.filter
72
+ },
73
+ {
74
+ id: definitions[1]?.id ?? "detail",
75
+ title: definitions[1]?.title ?? "Preview",
76
+ rows: (state.detail.items ?? []).map((item) => ({
77
+ id: item.id,
78
+ title: item.title ?? item.id,
79
+ subtitle: item.subtitle,
80
+ badge: item.badge
81
+ })),
82
+ cursor: state.detail.cursor,
83
+ selected: state.selected,
84
+ filter: ""
85
+ }
58
86
  ];
59
87
  }
@@ -65,7 +65,12 @@ export type ExplorerEvent = {
65
65
  };
66
66
  export type ConfirmModal = {
67
67
  kind: "confirm";
68
- action: Action<unknown>;
69
- rows: Row[];
68
+ title: string;
69
+ message: string;
70
+ confirmLabel: string;
71
+ cancelLabel: string;
72
+ destructive: boolean;
73
+ action?: Action<unknown>;
74
+ rows?: Row[];
70
75
  resolver: (ok: boolean) => void;
71
76
  };
@@ -4,5 +4,5 @@ export { createInitialState, normalizeExplorerConfig } from "./state.js";
4
4
  export { resolveBindings } from "./keymap.js";
5
5
  export type { Effect, ExplorerEvent } from "./events.js";
6
6
  export type { BindingTarget, ExplorerBindingDefaults, ExplorerBuiltinCommand, ResolvedBindings } from "./keymap.js";
7
- export type { Action, ActionContext, Detail, DetailCtx, DetailItem, Dirty, ExplorerConfig, PaneConfig, ListPaneConfig, DetailPaneConfig, PaneRuntimeState, ExplorerLayoutMode, ExplorerSize, ExplorerState, ReorderContext, Row, Tone } from "./state.js";
7
+ export type { Action, ActionContext, ConfirmPromptOptions, Detail, DetailCtx, DetailItem, Dirty, ExplorerConfig, PaneConfig, ListPaneConfig, DetailPaneConfig, PaneRuntimeState, ExplorerLayoutMode, ExplorerSize, ExplorerState, ReorderContext, Row, Tone } from "./state.js";
8
8
  export declare function singleDetail<R>(fn: (row: Row, ctx: DetailCtx) => string | Promise<string>): Detail<R>;
@@ -9,6 +9,7 @@ const DEFAULT_ACTION_HANDLES = {
9
9
  openModal: () => undefined,
10
10
  toast: () => undefined,
11
11
  confirm: async () => false,
12
+ promptText: async () => null,
12
13
  exit: () => undefined
13
14
  };
14
15
  export function step(state, event, runtimeHandles = DEFAULT_ACTION_HANDLES) {
@@ -157,8 +158,23 @@ function stepFilterKey(state, key, target, runtimeHandles) {
157
158
  return mark(state, 0);
158
159
  }
159
160
  function stepModalKey(state, key, target, runtimeHandles) {
161
+ if (state.modal?.kind === "input") {
162
+ if (target?.type === "builtin" && (target.id === "escape" || target.id === "quit")) {
163
+ return modalDismissed(state, null, runtimeHandles);
164
+ }
165
+ if (target?.type === "builtin" && target.id === "confirm") {
166
+ return modalDismissed(state, state.modal.value, runtimeHandles);
167
+ }
168
+ if (isBackspace(key)) {
169
+ return setModal(state, { ...state.modal, value: state.modal.value.slice(0, -1) });
170
+ }
171
+ if (isPrintable(key)) {
172
+ return setModal(state, { ...state.modal, value: `${state.modal.value}${key.ch}` });
173
+ }
174
+ return mark(state, 0);
175
+ }
160
176
  if (state.modal?.kind === "confirm") {
161
- if (target?.type === "builtin" && target.id === "escape") {
177
+ if (target?.type === "builtin" && (target.id === "escape" || target.id === "quit")) {
162
178
  return modalDismissed(state, false, runtimeHandles);
163
179
  }
164
180
  if (target?.type === "builtin" && target.id === "confirm") {
@@ -240,17 +256,25 @@ function rowsLoaded(state, rows) {
240
256
  const matchPositions = createMatchPositions(matches);
241
257
  const identityCursor = previousRowId === undefined
242
258
  ? -1
243
- : filtered.findIndex(index => rows[index]?.id === previousRowId);
244
- const cursor = identityCursor >= 0
245
- ? identityCursor
246
- : clamp(state.cursor, 0, Math.max(0, filtered.length - 1));
259
+ : filtered.findIndex((index) => rows[index]?.id === previousRowId);
260
+ const cursor = identityCursor >= 0 ? identityCursor : clamp(state.cursor, 0, Math.max(0, filtered.length - 1));
247
261
  const selected = state.multiSelect ? pruneSelection(state.selected, rows) : new Set();
248
262
  const detail = resetDetailForCursor(state, rows, filtered, cursor);
249
263
  const modal = modalStillValid(state.modal, rows);
250
264
  if (state.modal?.kind === "confirm" && modal === null) {
251
265
  state.modal.resolver(false);
252
266
  }
253
- const nextView = { ...state, rows, rowsLoading: false, filtered, matchPositions, cursor, selected, detail, modal };
267
+ const nextView = {
268
+ ...state,
269
+ rows,
270
+ rowsLoading: false,
271
+ filtered,
272
+ matchPositions,
273
+ cursor,
274
+ selected,
275
+ detail,
276
+ modal
277
+ };
254
278
  const next = {
255
279
  ...nextView,
256
280
  actionState: recomputeActionState(nextView),
@@ -348,10 +372,13 @@ function modalDismissed(state, result, runtimeHandles) {
348
372
  if (modal?.kind === "confirm") {
349
373
  modal.resolver(result === true);
350
374
  }
351
- if (modal?.kind !== "confirm" || result !== true) {
375
+ if (modal?.kind === "input") {
376
+ modal.resolver(typeof result === "string" ? result : null);
377
+ }
378
+ if (modal?.kind !== "confirm" || result !== true || modal.action === undefined) {
352
379
  return { state: closed, effects: NO_EFFECTS };
353
380
  }
354
- return dispatchAction(closed, modal.action, true, runtimeHandles, modal.rows);
381
+ return dispatchAction(closed, modal.action, true, runtimeHandles, modal.rows ?? []);
355
382
  }
356
383
  function moveCursor(state, delta) {
357
384
  if (state.focused === "detail" && hasDetailCursor(state)) {
@@ -363,9 +390,7 @@ function moveCursor(state, delta) {
363
390
  return setCursor(state, state.cursor + delta);
364
391
  }
365
392
  function isDetailBlobFocused(state) {
366
- return (state.focused === "detail" &&
367
- !hasDetailCursor(state) &&
368
- (state.detail.items?.length ?? 0) > 0);
393
+ return (state.focused === "detail" && !hasDetailCursor(state) && (state.detail.items?.length ?? 0) > 0);
369
394
  }
370
395
  function moveDetailCursor(state, delta) {
371
396
  const max = Math.max(0, (state.detail.items?.length ?? 0) - 1);
@@ -439,13 +464,24 @@ function updateDetailFilter(state, filter) {
439
464
  const items = filterDetailItems(allItems, filter);
440
465
  const detail = { ...state.detail, filter, items, cursor: 0, scroll: 0 };
441
466
  return {
442
- state: { ...state, detail, actionState: recomputeActionState({ ...state, detail }), dirty: REGION_HEADER | REGION_DETAIL | REGION_FOOTER },
467
+ state: {
468
+ ...state,
469
+ detail,
470
+ actionState: recomputeActionState({ ...state, detail }),
471
+ dirty: REGION_HEADER | REGION_DETAIL | REGION_FOOTER
472
+ },
443
473
  effects: NO_EFFECTS
444
474
  };
445
475
  }
446
476
  function filterDetailItems(items, query) {
447
- const rows = items.map(item => ({ id: item.id, title: item.title ?? item.id, subtitle: item.subtitle }));
448
- return filterRows(query, rows).map(match => items[match.index]).filter((item) => item !== undefined);
477
+ const rows = items.map((item) => ({
478
+ id: item.id,
479
+ title: item.title ?? item.id,
480
+ subtitle: item.subtitle
481
+ }));
482
+ return filterRows(query, rows)
483
+ .map((match) => items[match.index])
484
+ .filter((item) => item !== undefined);
449
485
  }
450
486
  function isSecondListFocused(state) {
451
487
  return state.focused === "detail" && state.paneDefinitions[1]?.kind === "list";
@@ -498,7 +534,7 @@ function confirmKey(state, runtimeHandles) {
498
534
  if (state.modal?.kind === "confirm") {
499
535
  return modalDismissed(state, true, runtimeHandles);
500
536
  }
501
- const hasActions = [...state.actionState.values()].some(entry => entry.available && entry.running !== true);
537
+ const hasActions = [...state.actionState.values()].some((entry) => entry.available && entry.running !== true);
502
538
  return hasActions
503
539
  ? setModal(state, { kind: "palette", query: "", cursor: 0 })
504
540
  : dispatchPrimary(state, runtimeHandles);
@@ -508,7 +544,9 @@ function toggleSelect(state) {
508
544
  return mark(state, 0);
509
545
  }
510
546
  const detailItem = state.focused === "detail" ? state.detail.items?.[state.detail.cursor] : undefined;
511
- const row = detailItem === undefined ? currentRow(state) : { id: detailItem.id, title: detailItem.title ?? detailItem.id };
547
+ const row = detailItem === undefined
548
+ ? currentRow(state)
549
+ : { id: detailItem.id, title: detailItem.title ?? detailItem.id };
512
550
  if (row === undefined) {
513
551
  return mark(state, 0);
514
552
  }
@@ -748,10 +786,21 @@ function dispatchAction(state, action, confirmed, runtimeHandles, modalRows) {
748
786
  return mark(state, 0);
749
787
  }
750
788
  if (action.destructive === true && !confirmed) {
789
+ const actionLabel = typeof action.label === "function" ? action.label() : action.label;
751
790
  return {
752
791
  state: {
753
792
  ...state,
754
- modal: { kind: "confirm", action, rows, resolver: () => undefined },
793
+ modal: {
794
+ kind: "confirm",
795
+ title: action.destructive ? "Confirm destructive action" : "Confirm action",
796
+ message: `${actionLabel} ${rows.length === 1 ? rows[0].title : `${rows.length} items`}?`,
797
+ confirmLabel: actionLabel,
798
+ cancelLabel: "Cancel",
799
+ destructive: action.destructive === true,
800
+ action,
801
+ rows,
802
+ resolver: () => undefined
803
+ },
755
804
  dirty: REGION_MODAL | REGION_FOOTER
756
805
  },
757
806
  effects: NO_EFFECTS
@@ -865,6 +914,8 @@ function modalStillValid(modal, rows) {
865
914
  if (modal?.kind !== "confirm") {
866
915
  return modal;
867
916
  }
917
+ if (modal.rows === undefined)
918
+ return modal;
868
919
  const ids = new Set(rows.map((row) => row.id));
869
920
  return modal.rows.every((row) => ids.has(row.id)) ? modal : null;
870
921
  }
@@ -12,12 +12,14 @@ export function renderDetail(state, screen, layout) {
12
12
  return;
13
13
  }
14
14
  const row = state.rows.find((r) => r.id === state.detail.rowId) ?? null;
15
+ const pane = state.paneDefinitions[1];
16
+ const title = pane?.titleForRow?.(row ?? undefined) ?? pane?.title ?? "Preview";
15
17
  if (layout.mode === "narrow-vertical") {
16
- drawPaneFrame(screen, rect, state.paneDefinitions[1]?.title ?? "Preview", state.focused === "detail" ? styles.borderFocused : styles.border, { focused: state.focused === "detail", indicator: scrollIndicator(state) });
18
+ drawPaneFrame(screen, rect, title, state.focused === "detail" ? styles.borderFocused : styles.border, { focused: state.focused === "detail", indicator: scrollIndicator(state) });
17
19
  renderDetailBody(state, screen, paneBodyRect(rect), row);
18
20
  return;
19
21
  }
20
- drawPaneFrame(screen, rect, state.paneDefinitions[1]?.title ?? "Preview", state.focused === "detail" ? styles.borderFocused : styles.border, { focused: state.focused === "detail", indicator: scrollIndicator(state) });
22
+ drawPaneFrame(screen, rect, title, state.focused === "detail" ? styles.borderFocused : styles.border, { focused: state.focused === "detail", indicator: scrollIndicator(state) });
21
23
  renderDetailBody(state, screen, paneBodyRect(rect), row);
22
24
  }
23
25
  function renderDetailBody(state, screen, rect, row) {
@@ -36,6 +36,18 @@ function putFooterText(screen, x, y, endX, text, style = {}) {
36
36
  return cellWidth(fitted, x);
37
37
  }
38
38
  function footerHints(state) {
39
+ if (state.modal?.kind === "input") {
40
+ return [
41
+ { key: "Enter", label: "submit", running: false },
42
+ { key: "Esc", label: "cancel", running: false }
43
+ ];
44
+ }
45
+ if (state.modal?.kind === "confirm") {
46
+ return [
47
+ { key: "Y/Enter", label: state.modal.confirmLabel, running: false },
48
+ { key: "N/Esc", label: state.modal.cancelLabel, running: false }
49
+ ];
50
+ }
39
51
  const hints = [];
40
52
  if (state.focused === "detail") {
41
53
  hints.push({ key: "Tab", label: "focus", running: false });
@@ -29,16 +29,16 @@ function modalLines(state) {
29
29
  }
30
30
  if (state.modal?.kind === "confirm") {
31
31
  return [
32
- state.modal.action.destructive ? "Destructive action" : "Confirm action",
33
- `${labelFor(state.modal.action)} ${state.modal.rows.length} row(s)?`,
34
- "Enter confirms · Esc cancels"
32
+ state.modal.message,
33
+ ` [ ${state.modal.confirmLabel} ] ${state.modal.cancelLabel}`
35
34
  ];
36
35
  }
36
+ if (state.modal?.kind === "input") {
37
+ const value = state.modal.value.length > 0 ? state.modal.value : (state.modal.placeholder ?? "");
38
+ return [state.modal.label, ` ${value}▌`];
39
+ }
37
40
  if (state.modal?.kind === "palette") {
38
- return [
39
- `Query: ${state.modal.query}`,
40
- ...paletteLines(state)
41
- ];
41
+ return [`Query: ${state.modal.query}`, ...paletteLines(state)];
42
42
  }
43
43
  if (state.modal?.kind === "content") {
44
44
  return state.modal.content.split("\n").slice(state.modal.scroll);
@@ -50,7 +50,10 @@ function title(state) {
50
50
  return "Keybindings";
51
51
  }
52
52
  if (state.modal?.kind === "confirm") {
53
- return "Confirm";
53
+ return state.modal.title;
54
+ }
55
+ if (state.modal?.kind === "input") {
56
+ return state.modal.title;
54
57
  }
55
58
  if (state.modal?.kind === "content") {
56
59
  return state.modal.title;
@@ -95,6 +98,3 @@ function paletteLines(state) {
95
98
  }
96
99
  return lines.length === 0 ? [" No actions"] : lines;
97
100
  }
98
- function labelFor(action) {
99
- return typeof action.label === "function" ? action.label() : action.label;
100
- }
@@ -5,13 +5,13 @@ import { createDetailJobs } from "./jobs.js";
5
5
  import { computeExplorerLayout } from "./layout.js";
6
6
  import { renderExplorer } from "./render/index.js";
7
7
  import { step } from "./reducer.js";
8
- import { createInitialState, REGION_ALL, REGION_MODAL, REGION_TOAST, normalizeExplorerConfig } from "./state.js";
8
+ import { createInitialState, REGION_ALL, REGION_FOOTER, REGION_MODAL, REGION_TOAST, normalizeExplorerConfig } from "./state.js";
9
9
  const TOAST_MS = 2500;
10
10
  export async function runExplorer(config) {
11
11
  if (process.stdout.isTTY !== true) {
12
12
  throw new Error("explorer requires a TTY");
13
13
  }
14
- const driver = createTerminalDriver();
14
+ const driver = createTerminalDriver({ mouse: config.mouse });
15
15
  const runtime = new ExplorerRuntime(config, driver);
16
16
  return runtime.run();
17
17
  }
@@ -59,6 +59,7 @@ class ExplorerRuntime {
59
59
  this.showToast(msg, tone);
60
60
  },
61
61
  confirm: async (prompt) => this.confirm(prompt),
62
+ promptText: async (options) => this.promptText(options),
62
63
  exit: (after) => {
63
64
  this.exit(null, after);
64
65
  }
@@ -98,15 +99,30 @@ class ExplorerRuntime {
98
99
  this.trace("input", { event });
99
100
  if (event.type === "paste") {
100
101
  for (const ch of event.text.replaceAll("\n", "").replaceAll("\r", "")) {
101
- this.dispatch({ type: "key", key: { ch, name: ch, ctrl: false, meta: false, shift: false } });
102
+ this.dispatch({
103
+ type: "key",
104
+ key: { ch, name: ch, ctrl: false, meta: false, shift: false }
105
+ });
102
106
  }
103
107
  return;
104
108
  }
105
109
  if (event.type === "wheel") {
106
- this.dispatch({ type: "key", key: { name: event.direction, ctrl: false, meta: false, shift: false } });
110
+ this.dispatch({
111
+ type: "key",
112
+ key: { name: event.direction, ctrl: false, meta: false, shift: false }
113
+ });
107
114
  return;
108
115
  }
109
- this.dispatch({ type: "key", key: { name: event.name, ch: event.ch, ctrl: event.ctrl, meta: event.alt, shift: event.shift } });
116
+ this.dispatch({
117
+ type: "key",
118
+ key: {
119
+ name: event.name,
120
+ ch: event.ch,
121
+ ctrl: event.ctrl,
122
+ meta: event.alt,
123
+ shift: event.shift
124
+ }
125
+ });
110
126
  });
111
127
  }
112
128
  pauseKeypress() {
@@ -279,21 +295,38 @@ class ExplorerRuntime {
279
295
  }
280
296
  confirm(prompt) {
281
297
  return new Promise((resolve) => {
282
- const action = {
283
- id: "__confirm__",
284
- label: prompt,
285
- handler: () => undefined
286
- };
287
- const row = this.currentRow();
298
+ const options = typeof prompt === "string"
299
+ ? { title: "Confirm", message: prompt }
300
+ : prompt;
288
301
  this.state = {
289
302
  ...this.state,
290
303
  modal: {
291
304
  kind: "confirm",
292
- action,
293
- rows: row === undefined ? [] : [row],
305
+ title: options.title,
306
+ message: options.message,
307
+ confirmLabel: options.confirmLabel ?? "Yes",
308
+ cancelLabel: options.cancelLabel ?? "No",
309
+ destructive: options.destructive ?? false,
294
310
  resolver: resolve
295
311
  },
296
- dirty: REGION_MODAL
312
+ dirty: REGION_MODAL | REGION_FOOTER
313
+ };
314
+ this.scheduleRender();
315
+ });
316
+ }
317
+ promptText(options) {
318
+ return new Promise((resolve) => {
319
+ this.state = {
320
+ ...this.state,
321
+ modal: {
322
+ kind: "input",
323
+ title: options.title,
324
+ label: options.label,
325
+ value: options.initialValue ?? "",
326
+ placeholder: options.placeholder,
327
+ resolver: resolve
328
+ },
329
+ dirty: REGION_MODAL | REGION_FOOTER
297
330
  };
298
331
  this.scheduleRender();
299
332
  });
@@ -340,7 +373,11 @@ class ExplorerRuntime {
340
373
  renderExplorer({ ...this.state, dirty: REGION_ALL }, this.screen);
341
374
  const frame = this.screen.flush();
342
375
  this.driver.writeFrame(frame);
343
- this.trace("frame", { bytes: Buffer.byteLength(frame), cols: this.state.size.cols, rows: this.state.size.rows });
376
+ this.trace("frame", {
377
+ bytes: Buffer.byteLength(frame),
378
+ cols: this.state.size.cols,
379
+ rows: this.state.size.rows
380
+ });
344
381
  this.state = { ...this.state, dirty: 0 };
345
382
  }
346
383
  track(promise) {
@@ -1,5 +1,12 @@
1
1
  import { type ResolvedBindings } from "./keymap.js";
2
2
  export type Tone = "success" | "warning" | "error" | "info" | "muted";
3
+ export interface ConfirmPromptOptions {
4
+ title: string;
5
+ message: string;
6
+ confirmLabel?: string;
7
+ cancelLabel?: string;
8
+ destructive?: boolean;
9
+ }
3
10
  export interface Row {
4
11
  id: string;
5
12
  title: string;
@@ -59,7 +66,13 @@ export interface ActionContext<R> {
59
66
  content: string;
60
67
  }) => void;
61
68
  toast: (msg: string, tone?: Tone) => void;
62
- confirm: (prompt: string) => Promise<boolean>;
69
+ confirm: (prompt: string | ConfirmPromptOptions) => Promise<boolean>;
70
+ promptText: (options: {
71
+ title: string;
72
+ label: string;
73
+ initialValue?: string;
74
+ placeholder?: string;
75
+ }) => Promise<string | null>;
63
76
  exit: (after?: () => void | Promise<void>) => void;
64
77
  activePane?: PaneRuntimeState;
65
78
  inactivePane?: PaneRuntimeState;
@@ -76,6 +89,7 @@ export interface DetailPaneConfig {
76
89
  id: string;
77
90
  kind: "detail";
78
91
  title: string;
92
+ titleForRow?: (row: Row | undefined) => string;
79
93
  render: (row: Row | undefined, ctx: DetailCtx) => string | Promise<string>;
80
94
  }
81
95
  export type PaneConfig = ListPaneConfig | DetailPaneConfig;
@@ -107,6 +121,8 @@ export interface ExplorerConfig<R> {
107
121
  initialFilter?: string;
108
122
  /** Synchronous first paint rows; still refreshed via `rows()` after start. */
109
123
  initialRows?: Row[];
124
+ /** Disable terminal mouse reporting when native text selection is more useful than wheel input. */
125
+ mouse?: boolean;
110
126
  }
111
127
  export type NormalizedExplorerConfig<R> = ExplorerConfig<R> & {
112
128
  rows: () => Promise<Row[]>;
@@ -153,9 +169,21 @@ export interface ExplorerState {
153
169
  kind: "help";
154
170
  } | {
155
171
  kind: "confirm";
156
- action: Action<unknown>;
157
- rows: Row[];
172
+ title: string;
173
+ message: string;
174
+ confirmLabel: string;
175
+ cancelLabel: string;
176
+ destructive: boolean;
158
177
  resolver: (ok: boolean) => void;
178
+ action?: Action<unknown>;
179
+ rows?: Row[];
180
+ } | {
181
+ kind: "input";
182
+ title: string;
183
+ label: string;
184
+ value: string;
185
+ placeholder?: string;
186
+ resolver: (value: string | null) => void;
159
187
  } | {
160
188
  kind: "palette";
161
189
  query: string;
@@ -181,6 +209,7 @@ export interface ExplorerState {
181
209
  id: string;
182
210
  title: string;
183
211
  kind: "list" | "detail";
212
+ titleForRow?: (row: Row | undefined) => string;
184
213
  }>;
185
214
  }
186
215
  export interface ActionStateEntry {
@@ -10,16 +10,21 @@ export function normalizeExplorerConfig(config) {
10
10
  const list = config.panes.find((pane) => pane.kind === "list");
11
11
  if (list === undefined)
12
12
  throw new Error("Explorer requires a list pane");
13
- const companion = config.panes.find(pane => pane !== list);
13
+ const companion = config.panes.find((pane) => pane !== list);
14
14
  const detail = companion?.kind === "detail"
15
- ? { items: async (row, ctx) => {
15
+ ? {
16
+ items: async (row, ctx) => {
16
17
  const content = await companion.render(row, ctx);
17
18
  if (ctx.signal.aborted)
18
19
  return [];
19
20
  return [{ id: row.id, render: () => content, renderedContent: content }];
20
- } }
21
+ }
22
+ }
21
23
  : companion?.kind === "list"
22
- ? { items: async () => (await companion.rows()).map(row => ({ ...row, render: () => "" })), actions: config.actions }
24
+ ? {
25
+ items: async () => (await companion.rows()).map((row) => ({ ...row, render: () => "" })),
26
+ actions: config.actions
27
+ }
23
28
  : { items: async () => [] };
24
29
  return {
25
30
  ...config,
@@ -35,12 +40,7 @@ export const REGION_DETAIL = 1 << 2;
35
40
  export const REGION_FOOTER = 1 << 3;
36
41
  export const REGION_MODAL = 1 << 4;
37
42
  export const REGION_TOAST = 1 << 5;
38
- export const REGION_ALL = REGION_HEADER |
39
- REGION_LIST |
40
- REGION_DETAIL |
41
- REGION_FOOTER |
42
- REGION_MODAL |
43
- REGION_TOAST;
43
+ export const REGION_ALL = REGION_HEADER | REGION_LIST | REGION_DETAIL | REGION_FOOTER | REGION_MODAL | REGION_TOAST;
44
44
  export function createInitialState(config, size) {
45
45
  const normalizedConfig = normalizeExplorerConfig(config);
46
46
  const normalizedSize = {
@@ -82,7 +82,12 @@ export function createInitialState(config, size) {
82
82
  bindings: resolveBindings(normalizedConfig),
83
83
  actionState: createInitialActionState(normalizedConfig),
84
84
  suspended: false,
85
- paneDefinitions: normalizedConfig.panes?.map(({ id, title, kind }) => ({ id, title, kind })) ?? [
85
+ paneDefinitions: normalizedConfig.panes?.map(({ id, title, kind, ...pane }) => ({
86
+ id,
87
+ title,
88
+ kind,
89
+ ...(kind === "detail" && "titleForRow" in pane ? { titleForRow: pane.titleForRow } : {})
90
+ })) ?? [
86
91
  { id: "list", title: normalizedConfig.title, kind: "list" },
87
92
  { id: "detail", title: "Preview", kind: "detail" }
88
93
  ]
@@ -90,10 +95,13 @@ export function createInitialState(config, size) {
90
95
  }
91
96
  function createInitialActionState(config) {
92
97
  const state = new Map();
93
- for (const [source, actions] of [["row", config.actions], ["detail", config.detail?.actions ?? []]]) {
98
+ for (const [source, actions] of [
99
+ ["row", config.actions],
100
+ ["detail", config.detail?.actions ?? []]
101
+ ]) {
94
102
  for (const action of actions) {
95
103
  if (state.has(action.id)) {
96
- const isSharedListAction = config.panes?.some(pane => pane.kind === "list" && pane !== config.panes?.[0]) === true;
104
+ const isSharedListAction = config.panes?.some((pane) => pane.kind === "list" && pane !== config.panes?.[0]) === true;
97
105
  if (!isSharedListAction)
98
106
  throw new Error(`Duplicate explorer action id: ${action.id}`);
99
107
  const existing = state.get(action.id);
@@ -38,7 +38,7 @@ export { createDashboard, shouldUseInteractiveDashboard } from "./dashboard/inde
38
38
  export type { Dashboard, DashboardOptions } from "./dashboard/index.js";
39
39
  export * as explorer from "./explorer/index.js";
40
40
  export { runExplorer, singleDetail, normalizeExplorerConfig } from "./explorer/index.js";
41
- export type { Row, DetailItem, Detail, DetailCtx, Action, ActionContext, ExplorerConfig, PaneConfig, ListPaneConfig, DetailPaneConfig, PaneRuntimeState, ReorderContext, Tone } from "./explorer/index.js";
41
+ export type { Row, DetailItem, Detail, DetailCtx, Action, ActionContext, ConfirmPromptOptions, ExplorerConfig, PaneConfig, ListPaneConfig, DetailPaneConfig, PaneRuntimeState, ReorderContext, Tone } from "./explorer/index.js";
42
42
  export * as prompts from "./prompts/index.js";
43
43
  export { intro, introPlain, outro, note, select, multiselect, text as promptText, confirm, confirmOrCancel, password, spinner, withSpinner, isCancel, cancel, log, PromptCancelledError } from "./prompts/index.js";
44
44
  export type { SelectOptions, MultiselectOptions, TextOptions, ConfirmOptions, PasswordOptions, SpinnerOptions, WithSpinnerOptions } from "./prompts/index.js";
@@ -29,5 +29,6 @@ export declare function createTerminalDriver(options?: {
29
29
  input?: InputStream;
30
30
  output?: OutputStream;
31
31
  escTimeoutMs?: number;
32
+ mouse?: boolean;
32
33
  }): TerminalDriver;
33
34
  export type { TerminalInputEvent } from "./input.js";
@@ -10,7 +10,7 @@ export function createTerminalDriver(options = {}) {
10
10
  listener(event);
11
11
  };
12
12
  const parser = createInputParser({ escTimeoutMs: options.escTimeoutMs, onEvent: emit });
13
- const writer = createFrameWriter(output);
13
+ const writer = createFrameWriter(output, { mouse: options.mouse });
14
14
  let started = false;
15
15
  const onData = (chunk) => {
16
16
  for (const event of parser.feed(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)))
@@ -46,11 +46,15 @@ export function createTerminalDriver(options = {}) {
46
46
  },
47
47
  onEvent(fn) {
48
48
  eventListeners.add(fn);
49
- return () => { eventListeners.delete(fn); };
49
+ return () => {
50
+ eventListeners.delete(fn);
51
+ };
50
52
  },
51
53
  onResize(fn) {
52
54
  resizeListeners.add(fn);
53
- return () => { resizeListeners.delete(fn); };
55
+ return () => {
56
+ resizeListeners.delete(fn);
57
+ };
54
58
  },
55
59
  getSize,
56
60
  writeFrame: writer.writeFrame
@@ -5,4 +5,6 @@ export interface FrameWriter {
5
5
  }
6
6
  export declare function createFrameWriter(stream: {
7
7
  write(value: string): boolean;
8
+ }, options?: {
9
+ mouse?: boolean;
8
10
  }): FrameWriter;
@@ -1,6 +1,8 @@
1
- const OPEN = "\u001b[?1049h\u001b[?25l\u001b[?2004h\u001b[?1000h\u001b[?1006h\u001b[?7l";
2
- const CLOSE = "\u001b[0m\u001b[?7h\u001b[?1006l\u001b[?1000l\u001b[?2004l\u001b[?25h\u001b[?1049l";
3
- export function createFrameWriter(stream) {
1
+ const OPEN = "\u001b[?1049h\u001b[?25l\u001b[?2004h";
2
+ const OPEN_MOUSE = "\u001b[?1000h\u001b[?1006h";
3
+ const CLOSE_MOUSE = "\u001b[?1006l\u001b[?1000l";
4
+ const CLOSE = "\u001b[?2004l\u001b[?25h\u001b[?1049l";
5
+ export function createFrameWriter(stream, options = {}) {
4
6
  let opened = false;
5
7
  const interrupt = () => terminateWith("SIGINT");
6
8
  const terminate = () => terminateWith("SIGTERM");
@@ -16,7 +18,7 @@ export function createFrameWriter(stream) {
16
18
  if (opened)
17
19
  return;
18
20
  opened = true;
19
- stream.write(OPEN);
21
+ stream.write(`${OPEN}${options.mouse === false ? CLOSE_MOUSE : OPEN_MOUSE}\u001b[?7l`);
20
22
  process.once("SIGINT", interrupt);
21
23
  process.once("SIGTERM", terminate);
22
24
  process.once("uncaughtException", fatal);
@@ -28,7 +30,7 @@ export function createFrameWriter(stream) {
28
30
  process.removeListener("SIGINT", interrupt);
29
31
  process.removeListener("SIGTERM", terminate);
30
32
  process.removeListener("uncaughtException", fatal);
31
- stream.write(CLOSE);
33
+ stream.write(`\u001b[0m\u001b[?7h${options.mouse === false ? "" : CLOSE_MOUSE}${CLOSE}`);
32
34
  }
33
35
  return {
34
36
  open,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.154",
3
+ "version": "0.0.156",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.154",
3
+ "version": "0.0.156",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -158,7 +158,7 @@
158
158
  "yaml"
159
159
  ],
160
160
  "optionalDependencies": {
161
- "toolcraft-schema": "0.0.154",
161
+ "toolcraft-schema": "0.0.156",
162
162
  "toolcraft-design": "*",
163
163
  "@poe-code/frontmatter": "*",
164
164
  "@poe-code/agent-mcp-config": "*",