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 +2 -2
- package/dist/composition.json +2 -2
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +2 -1
- package/node_modules/toolcraft-design/dist/explorer/actions.js +33 -5
- package/node_modules/toolcraft-design/dist/explorer/events.d.ts +7 -2
- package/node_modules/toolcraft-design/dist/explorer/index.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/reducer.js +68 -17
- package/node_modules/toolcraft-design/dist/explorer/render/detail.js +4 -2
- package/node_modules/toolcraft-design/dist/explorer/render/footer.js +12 -0
- package/node_modules/toolcraft-design/dist/explorer/render/modal.js +11 -11
- package/node_modules/toolcraft-design/dist/explorer/runtime.js +52 -15
- package/node_modules/toolcraft-design/dist/explorer/state.d.ts +32 -3
- package/node_modules/toolcraft-design/dist/explorer/state.js +21 -13
- package/node_modules/toolcraft-design/dist/index.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/terminal/driver.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/terminal/driver.js +7 -3
- package/node_modules/toolcraft-design/dist/terminal/output.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/terminal/output.js +7 -5
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +2 -2
package/composition.json
CHANGED
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "toolcraft",
|
|
116
|
-
"version": "0.0.
|
|
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.
|
|
126
|
+
"version": "0.0.156",
|
|
127
127
|
"license": "MIT"
|
|
128
128
|
},
|
|
129
129
|
{
|
package/dist/composition.json
CHANGED
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "toolcraft",
|
|
116
|
-
"version": "0.0.
|
|
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.
|
|
126
|
+
"version": "0.0.156",
|
|
127
127
|
"license": "MIT"
|
|
128
128
|
},
|
|
129
129
|
{
|
|
@@ -13,7 +13,8 @@ export type ActionRuntimeHandles = {
|
|
|
13
13
|
content: string;
|
|
14
14
|
}) => void;
|
|
15
15
|
toast: (msg: string, tone?: Tone) => void;
|
|
16
|
-
confirm:
|
|
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] ??
|
|
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
|
|
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 => ({
|
|
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
|
-
{
|
|
57
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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 = {
|
|
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
|
|
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: {
|
|
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 => ({
|
|
448
|
-
|
|
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
|
|
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: {
|
|
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,
|
|
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,
|
|
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.
|
|
33
|
-
|
|
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
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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", {
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
? {
|
|
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
|
-
? {
|
|
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 }) => ({
|
|
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 [
|
|
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";
|
|
@@ -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 () => {
|
|
49
|
+
return () => {
|
|
50
|
+
eventListeners.delete(fn);
|
|
51
|
+
};
|
|
50
52
|
},
|
|
51
53
|
onResize(fn) {
|
|
52
54
|
resizeListeners.add(fn);
|
|
53
|
-
return () => {
|
|
55
|
+
return () => {
|
|
56
|
+
resizeListeners.delete(fn);
|
|
57
|
+
};
|
|
54
58
|
},
|
|
55
59
|
getSize,
|
|
56
60
|
writeFrame: writer.writeFrame
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
const OPEN = "\u001b[?1049h\u001b[?25l\u001b[?2004h
|
|
2
|
-
const
|
|
3
|
-
|
|
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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
161
|
+
"toolcraft-schema": "0.0.156",
|
|
162
162
|
"toolcraft-design": "*",
|
|
163
163
|
"@poe-code/frontmatter": "*",
|
|
164
164
|
"@poe-code/agent-mcp-config": "*",
|