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