toolcraft-openapi 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/bin/generate.js +7 -0
  2. package/dist/define-client.js +2 -2
  3. package/dist/generate.js +2 -2
  4. package/dist/http.d.ts +21 -2
  5. package/dist/http.js +147 -22
  6. package/dist/index.d.ts +1 -1
  7. package/dist/lock.d.ts +1 -1
  8. package/dist/lock.js +109 -5
  9. package/dist/mock/fetch.js +1 -1
  10. package/dist/network-error.d.ts +2 -0
  11. package/dist/network-error.js +83 -0
  12. package/dist/spec-source.js +103 -3
  13. package/node_modules/@poe-code/design-system/dist/acp/components.js +15 -13
  14. package/node_modules/@poe-code/design-system/dist/components/color.d.ts +31 -0
  15. package/node_modules/@poe-code/design-system/dist/components/color.js +101 -0
  16. package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.d.ts +1 -0
  17. package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.js +1 -1
  18. package/node_modules/@poe-code/design-system/dist/components/index.d.ts +4 -0
  19. package/node_modules/@poe-code/design-system/dist/components/index.js +2 -0
  20. package/node_modules/@poe-code/design-system/dist/components/logger.js +2 -2
  21. package/node_modules/@poe-code/design-system/dist/components/symbols.js +3 -3
  22. package/node_modules/@poe-code/design-system/dist/components/table.js +191 -40
  23. package/node_modules/@poe-code/design-system/dist/components/template.d.ts +6 -0
  24. package/node_modules/@poe-code/design-system/dist/components/template.js +271 -0
  25. package/node_modules/@poe-code/design-system/dist/components/text.d.ts +1 -0
  26. package/node_modules/@poe-code/design-system/dist/components/text.js +11 -3
  27. package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +20 -13
  28. package/node_modules/@poe-code/design-system/dist/dashboard/keymap.d.ts +5 -0
  29. package/node_modules/@poe-code/design-system/dist/dashboard/keymap.js +146 -12
  30. package/node_modules/@poe-code/design-system/dist/dashboard/terminal.js +31 -0
  31. package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
  32. package/node_modules/@poe-code/design-system/dist/explorer/actions.d.ts +16 -0
  33. package/node_modules/@poe-code/design-system/dist/explorer/actions.js +39 -0
  34. package/node_modules/@poe-code/design-system/dist/explorer/demo.d.ts +13 -0
  35. package/node_modules/@poe-code/design-system/dist/explorer/demo.js +297 -0
  36. package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +61 -0
  37. package/node_modules/@poe-code/design-system/dist/explorer/events.js +1 -0
  38. package/node_modules/@poe-code/design-system/dist/explorer/filter.d.ts +10 -0
  39. package/node_modules/@poe-code/design-system/dist/explorer/filter.js +95 -0
  40. package/node_modules/@poe-code/design-system/dist/explorer/index.d.ts +8 -0
  41. package/node_modules/@poe-code/design-system/dist/explorer/index.js +8 -0
  42. package/node_modules/@poe-code/design-system/dist/explorer/jobs.d.ts +7 -0
  43. package/node_modules/@poe-code/design-system/dist/explorer/jobs.js +59 -0
  44. package/node_modules/@poe-code/design-system/dist/explorer/keymap.d.ts +21 -0
  45. package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +363 -0
  46. package/node_modules/@poe-code/design-system/dist/explorer/layout.d.ts +20 -0
  47. package/node_modules/@poe-code/design-system/dist/explorer/layout.js +73 -0
  48. package/node_modules/@poe-code/design-system/dist/explorer/reducer.d.ts +9 -0
  49. package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +704 -0
  50. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.d.ts +4 -0
  51. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +96 -0
  52. package/node_modules/@poe-code/design-system/dist/explorer/render/footer.d.ts +4 -0
  53. package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +49 -0
  54. package/node_modules/@poe-code/design-system/dist/explorer/render/header.d.ts +4 -0
  55. package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +56 -0
  56. package/node_modules/@poe-code/design-system/dist/explorer/render/index.d.ts +8 -0
  57. package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +61 -0
  58. package/node_modules/@poe-code/design-system/dist/explorer/render/list.d.ts +4 -0
  59. package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +106 -0
  60. package/node_modules/@poe-code/design-system/dist/explorer/render/modal.d.ts +3 -0
  61. package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +91 -0
  62. package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.d.ts +8 -0
  63. package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.js +156 -0
  64. package/node_modules/@poe-code/design-system/dist/explorer/runtime.d.ts +2 -0
  65. package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +282 -0
  66. package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.d.ts +50 -0
  67. package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.js +101 -0
  68. package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +130 -0
  69. package/node_modules/@poe-code/design-system/dist/explorer/state.js +87 -0
  70. package/node_modules/@poe-code/design-system/dist/explorer/theme.d.ts +27 -0
  71. package/node_modules/@poe-code/design-system/dist/explorer/theme.js +97 -0
  72. package/node_modules/@poe-code/design-system/dist/index.d.ts +7 -0
  73. package/node_modules/@poe-code/design-system/dist/index.js +5 -0
  74. package/node_modules/@poe-code/design-system/dist/internal/color-support.d.ts +9 -0
  75. package/node_modules/@poe-code/design-system/dist/internal/color-support.js +12 -0
  76. package/node_modules/@poe-code/design-system/dist/prompts/index.js +2 -2
  77. package/node_modules/@poe-code/design-system/dist/prompts/primitives/cancel.js +2 -2
  78. package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -2
  79. package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +4 -4
  80. package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +5 -5
  81. package/node_modules/@poe-code/design-system/dist/prompts/primitives/outro.js +2 -2
  82. package/node_modules/@poe-code/design-system/dist/prompts/primitives/spinner.js +3 -3
  83. package/node_modules/@poe-code/design-system/dist/static/menu.js +5 -5
  84. package/node_modules/@poe-code/design-system/dist/static/spinner.js +8 -8
  85. package/node_modules/@poe-code/design-system/dist/tokens/colors.js +29 -29
  86. package/node_modules/@poe-code/design-system/dist/tokens/typography.js +6 -6
  87. package/node_modules/@poe-code/design-system/package.json +6 -3
  88. package/package.json +2 -4
@@ -0,0 +1,282 @@
1
+ import { ScreenBuffer, cellToAnsi, diff } from "../dashboard/buffer.js";
2
+ import { createTerminalDriver } from "../dashboard/terminal.js";
3
+ import { createDetailJobs } from "./jobs.js";
4
+ import { computeExplorerLayout } from "./layout.js";
5
+ import { renderExplorer } from "./render/index.js";
6
+ import { step } from "./reducer.js";
7
+ import { createInitialState, REGION_ALL, REGION_MODAL, REGION_TOAST } from "./state.js";
8
+ const TOAST_MS = 2500;
9
+ export async function runExplorer(config) {
10
+ if (process.stdout.isTTY !== true) {
11
+ throw new Error("explorer requires a TTY");
12
+ }
13
+ const driver = createTerminalDriver();
14
+ const runtime = new ExplorerRuntime(config, driver);
15
+ return runtime.run();
16
+ }
17
+ class ExplorerRuntime {
18
+ config;
19
+ driver;
20
+ state;
21
+ previousBuffer = new ScreenBuffer(0, 0);
22
+ detailJobs;
23
+ runtimeHandles;
24
+ pendingEffects = new Set();
25
+ unsubscribeKeypress;
26
+ unsubscribeResize;
27
+ toastTimer;
28
+ stopped = false;
29
+ settle;
30
+ constructor(config, driver) {
31
+ this.config = config;
32
+ this.driver = driver;
33
+ this.state = createInitialState(config, driver.getSize());
34
+ this.detailJobs = createDetailJobs((event) => {
35
+ this.dispatch(event);
36
+ });
37
+ this.runtimeHandles = {
38
+ refresh: async () => {
39
+ await this.refreshRows();
40
+ },
41
+ suspendAnd: async (fn) => this.suspendAnd(fn),
42
+ toast: (msg, tone) => {
43
+ this.showToast(msg, tone);
44
+ },
45
+ confirm: async (prompt) => this.confirm(prompt),
46
+ exit: (after) => {
47
+ this.exit(null, after);
48
+ }
49
+ };
50
+ }
51
+ run() {
52
+ return new Promise((resolve, reject) => {
53
+ this.settle = { resolve, reject };
54
+ try {
55
+ this.startTerminal();
56
+ this.render();
57
+ this.refreshRows().catch((error) => {
58
+ this.fail(error);
59
+ });
60
+ }
61
+ catch (error) {
62
+ this.fail(error);
63
+ }
64
+ });
65
+ }
66
+ startTerminal() {
67
+ this.driver.enterRawMode();
68
+ this.driver.enterAltScreen();
69
+ this.driver.disableLineWrap();
70
+ this.driver.hideCursor();
71
+ this.unsubscribeKeypress = this.driver.onKeypress((key) => {
72
+ this.dispatch({ type: "key", key });
73
+ });
74
+ this.unsubscribeResize = this.driver.onResize(() => {
75
+ const size = this.driver.getSize();
76
+ this.dispatch({ type: "resize", cols: size.cols, rows: size.rows });
77
+ });
78
+ }
79
+ async refreshRows() {
80
+ const rows = await this.config.rows();
81
+ this.dispatch({ type: "rowsLoaded", rows });
82
+ }
83
+ dispatch(event) {
84
+ if (this.stopped) {
85
+ return;
86
+ }
87
+ const previousState = this.state;
88
+ const next = step(this.state, event, this.runtimeHandles);
89
+ this.state = next.state;
90
+ this.render();
91
+ this.applyEffects(next.effects, previousState);
92
+ }
93
+ applyEffects(effects, previousState) {
94
+ for (const effect of effects) {
95
+ if (effect.type === "renderDetail") {
96
+ this.renderDetail(effect.rowId);
97
+ continue;
98
+ }
99
+ if (effect.type === "persistOrder") {
100
+ this.track(this.persistOrder(effect.orderedIds, previousState.rows));
101
+ continue;
102
+ }
103
+ if (effect.type === "suspend") {
104
+ this.track(this.runActionEffect(effect));
105
+ continue;
106
+ }
107
+ if (effect.type === "exit") {
108
+ this.exit(effect.result, effect.after);
109
+ }
110
+ }
111
+ }
112
+ renderDetail(rowId) {
113
+ const row = this.state.rows.find((candidate) => candidate.id === rowId);
114
+ if (row === undefined) {
115
+ return;
116
+ }
117
+ const layout = computeExplorerLayout({
118
+ cols: this.state.size.cols,
119
+ rows: this.state.size.rows,
120
+ detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow"
121
+ });
122
+ void this.detailJobs.schedule(rowId, (ctx) => this.config.detail.items(row, ctx), {
123
+ width: layout.detail.width,
124
+ height: layout.detail.height,
125
+ row,
126
+ signal: new AbortController().signal
127
+ });
128
+ }
129
+ async persistOrder(orderedIds, previousRows) {
130
+ try {
131
+ await this.config.reorder?.onReorder(orderedIds);
132
+ }
133
+ catch (error) {
134
+ this.showToast(error instanceof Error ? error.message : "Could not persist order", "error");
135
+ this.dispatch({ type: "rowsLoaded", rows: previousRows });
136
+ }
137
+ }
138
+ async runActionEffect(effect) {
139
+ try {
140
+ const value = await effect.fn();
141
+ this.dispatch(effect.resumeWith(value));
142
+ }
143
+ catch (error) {
144
+ this.showToast(error instanceof Error ? error.message : "Action failed", "error");
145
+ this.dispatch(effect.resumeWith(error));
146
+ }
147
+ }
148
+ async suspendAnd(fn) {
149
+ this.driver.exitAltScreen();
150
+ this.driver.enableLineWrap();
151
+ this.driver.showCursor();
152
+ this.driver.exitRawMode();
153
+ try {
154
+ return await fn();
155
+ }
156
+ finally {
157
+ if (!this.stopped) {
158
+ this.driver.enterRawMode();
159
+ this.driver.enterAltScreen();
160
+ this.driver.disableLineWrap();
161
+ this.driver.hideCursor();
162
+ const size = this.driver.getSize();
163
+ this.dispatch({
164
+ type: "suspendResumed",
165
+ value: null,
166
+ emit: { type: "resize", cols: size.cols, rows: size.rows }
167
+ });
168
+ }
169
+ }
170
+ }
171
+ confirm(prompt) {
172
+ return new Promise((resolve) => {
173
+ const action = {
174
+ id: "__confirm__",
175
+ label: prompt,
176
+ handler: () => undefined
177
+ };
178
+ const row = this.currentRow();
179
+ this.state = {
180
+ ...this.state,
181
+ modal: {
182
+ kind: "confirm",
183
+ action,
184
+ rows: row === undefined ? [] : [row],
185
+ resolver: resolve
186
+ },
187
+ dirty: REGION_MODAL
188
+ };
189
+ this.render();
190
+ });
191
+ }
192
+ showToast(message, tone = "info") {
193
+ if (this.stopped) {
194
+ return;
195
+ }
196
+ if (this.toastTimer !== undefined) {
197
+ clearTimeout(this.toastTimer);
198
+ }
199
+ this.state = {
200
+ ...this.state,
201
+ toast: { message, tone, expiresAt: Date.now() + TOAST_MS },
202
+ dirty: REGION_TOAST
203
+ };
204
+ this.render();
205
+ this.toastTimer = setTimeout(() => {
206
+ this.dispatch({ type: "toastExpired" });
207
+ }, TOAST_MS);
208
+ }
209
+ currentRow() {
210
+ return this.state.rows[this.state.filtered[this.state.cursor] ?? -1];
211
+ }
212
+ render() {
213
+ if (this.stopped) {
214
+ return;
215
+ }
216
+ const size = this.driver.getSize();
217
+ if (size.cols !== this.state.size.cols || size.rows !== this.state.size.rows) {
218
+ this.state = step(this.state, { type: "resize", cols: size.cols, rows: size.rows }, this.runtimeHandles).state;
219
+ }
220
+ const nextBuffer = this.state.dirty === REGION_ALL
221
+ ? new ScreenBuffer(this.state.size.cols, this.state.size.rows)
222
+ : cloneBuffer(this.previousBuffer);
223
+ renderExplorer(this.state, nextBuffer);
224
+ this.driver.write(changesToAnsi(diff(this.previousBuffer, nextBuffer)));
225
+ this.previousBuffer = nextBuffer;
226
+ this.state = { ...this.state, dirty: 0 };
227
+ }
228
+ track(promise) {
229
+ this.pendingEffects.add(promise);
230
+ promise.finally(() => {
231
+ this.pendingEffects.delete(promise);
232
+ });
233
+ }
234
+ exit(result, after) {
235
+ if (this.stopped) {
236
+ return;
237
+ }
238
+ this.stopped = true;
239
+ this.unsubscribeKeypress?.();
240
+ this.unsubscribeResize?.();
241
+ this.detailJobs.abort();
242
+ if (this.toastTimer !== undefined) {
243
+ clearTimeout(this.toastTimer);
244
+ }
245
+ this.driver.destroy();
246
+ Promise.resolve()
247
+ .then(() => after?.())
248
+ .then(() => {
249
+ this.settle?.resolve(result);
250
+ })
251
+ .catch((error) => {
252
+ this.settle?.reject(error);
253
+ });
254
+ }
255
+ fail(error) {
256
+ if (!this.stopped) {
257
+ this.stopped = true;
258
+ this.driver.destroy();
259
+ }
260
+ this.settle?.reject(error);
261
+ }
262
+ }
263
+ function cloneBuffer(buffer) {
264
+ const next = new ScreenBuffer(buffer.width, buffer.height);
265
+ for (let y = 0; y < buffer.height; y += 1) {
266
+ for (let x = 0; x < buffer.width; x += 1) {
267
+ const cell = buffer.get(x, y);
268
+ next.put(x, y, cell.ch, cell.style);
269
+ }
270
+ }
271
+ return next;
272
+ }
273
+ function changesToAnsi(changes) {
274
+ let output = "";
275
+ for (const change of changes) {
276
+ output += `${cursorPositionAnsi(change.x, change.y)}${cellToAnsi(change.cell)}`;
277
+ }
278
+ return output;
279
+ }
280
+ function cursorPositionAnsi(x, y) {
281
+ return `\u001b[${Math.max(1, y + 1)};${Math.max(1, x + 1)}H`;
282
+ }
@@ -0,0 +1,50 @@
1
+ import type { Cell } from "../dashboard/types.js";
2
+ import type { KeypressEvent, TerminalDriver } from "../dashboard/terminal.js";
3
+ export declare class FakeTerminalDriver implements TerminalDriver {
4
+ private cols;
5
+ private rows;
6
+ readonly keyQueue: KeypressEvent[];
7
+ readonly writes: string[];
8
+ readonly flushes: Array<Array<{
9
+ x: number;
10
+ y: number;
11
+ cell: Cell;
12
+ }>>;
13
+ rawMode: boolean;
14
+ altScreen: boolean;
15
+ lineWrap: boolean;
16
+ cursorVisible: boolean;
17
+ destroyed: boolean;
18
+ enterAltScreenCount: number;
19
+ exitAltScreenCount: number;
20
+ enterRawModeCount: number;
21
+ exitRawModeCount: number;
22
+ private readonly keypressHandlers;
23
+ private readonly resizeHandlers;
24
+ constructor(cols?: number, rows?: number);
25
+ get output(): string;
26
+ enterRawMode(): void;
27
+ exitRawMode(): void;
28
+ enterAltScreen(): void;
29
+ exitAltScreen(): void;
30
+ disableLineWrap(): void;
31
+ enableLineWrap(): void;
32
+ hideCursor(): void;
33
+ showCursor(): void;
34
+ moveTo(x: number, y: number): void;
35
+ write(text: string): void;
36
+ flush(changes: Array<{
37
+ x: number;
38
+ y: number;
39
+ cell: Cell;
40
+ }>): void;
41
+ getSize(): {
42
+ cols: number;
43
+ rows: number;
44
+ };
45
+ resize(cols: number, rows: number): void;
46
+ onResize(handler: () => void): () => void;
47
+ onKeypress(handler: (key: KeypressEvent) => void): () => void;
48
+ press(key: KeypressEvent): void;
49
+ destroy(): void;
50
+ }
@@ -0,0 +1,101 @@
1
+ export class FakeTerminalDriver {
2
+ cols;
3
+ rows;
4
+ keyQueue = [];
5
+ writes = [];
6
+ flushes = [];
7
+ rawMode = false;
8
+ altScreen = false;
9
+ lineWrap = true;
10
+ cursorVisible = true;
11
+ destroyed = false;
12
+ enterAltScreenCount = 0;
13
+ exitAltScreenCount = 0;
14
+ enterRawModeCount = 0;
15
+ exitRawModeCount = 0;
16
+ keypressHandlers = new Set();
17
+ resizeHandlers = new Set();
18
+ constructor(cols = 120, rows = 24) {
19
+ this.cols = cols;
20
+ this.rows = rows;
21
+ }
22
+ get output() {
23
+ return this.writes.join("");
24
+ }
25
+ enterRawMode() {
26
+ this.rawMode = true;
27
+ this.enterRawModeCount += 1;
28
+ }
29
+ exitRawMode() {
30
+ this.rawMode = false;
31
+ this.exitRawModeCount += 1;
32
+ }
33
+ enterAltScreen() {
34
+ this.altScreen = true;
35
+ this.enterAltScreenCount += 1;
36
+ }
37
+ exitAltScreen() {
38
+ this.altScreen = false;
39
+ this.exitAltScreenCount += 1;
40
+ }
41
+ disableLineWrap() {
42
+ this.lineWrap = false;
43
+ }
44
+ enableLineWrap() {
45
+ this.lineWrap = true;
46
+ }
47
+ hideCursor() {
48
+ this.cursorVisible = false;
49
+ }
50
+ showCursor() {
51
+ this.cursorVisible = true;
52
+ }
53
+ moveTo(x, y) {
54
+ this.write(`\u001b[${Math.max(1, y + 1)};${Math.max(1, x + 1)}H`);
55
+ }
56
+ write(text) {
57
+ if (!this.destroyed) {
58
+ this.writes.push(text);
59
+ }
60
+ }
61
+ flush(changes) {
62
+ this.flushes.push(changes);
63
+ }
64
+ getSize() {
65
+ return { cols: this.cols, rows: this.rows };
66
+ }
67
+ resize(cols, rows) {
68
+ this.cols = cols;
69
+ this.rows = rows;
70
+ for (const handler of this.resizeHandlers) {
71
+ handler();
72
+ }
73
+ }
74
+ onResize(handler) {
75
+ this.resizeHandlers.add(handler);
76
+ return () => {
77
+ this.resizeHandlers.delete(handler);
78
+ };
79
+ }
80
+ onKeypress(handler) {
81
+ this.keypressHandlers.add(handler);
82
+ return () => {
83
+ this.keypressHandlers.delete(handler);
84
+ };
85
+ }
86
+ press(key) {
87
+ this.keyQueue.push(key);
88
+ for (const handler of this.keypressHandlers) {
89
+ handler(key);
90
+ }
91
+ }
92
+ destroy() {
93
+ this.destroyed = true;
94
+ this.rawMode = false;
95
+ this.lineWrap = true;
96
+ this.altScreen = false;
97
+ this.cursorVisible = true;
98
+ this.keypressHandlers.clear();
99
+ this.resizeHandlers.clear();
100
+ }
101
+ }
@@ -0,0 +1,130 @@
1
+ import { type ResolvedBindings } from "./keymap.js";
2
+ export type Tone = "success" | "warning" | "error" | "info" | "muted";
3
+ export interface Row {
4
+ id: string;
5
+ title: string;
6
+ subtitle?: string;
7
+ badge?: {
8
+ text: string;
9
+ tone?: Tone;
10
+ };
11
+ group?: string;
12
+ }
13
+ export interface DetailItem {
14
+ id: string;
15
+ title?: string;
16
+ subtitle?: string;
17
+ badge?: {
18
+ text: string;
19
+ tone?: Tone;
20
+ };
21
+ render: (ctx: DetailCtx) => string | Promise<string>;
22
+ }
23
+ export interface Detail<R> {
24
+ items: (row: Row, ctx: DetailCtx) => Promise<DetailItem[]>;
25
+ actions?: Action<R>[];
26
+ }
27
+ export interface DetailCtx {
28
+ width: number;
29
+ height: number;
30
+ signal: AbortSignal;
31
+ row: Row;
32
+ }
33
+ export interface Action<R> {
34
+ id: string;
35
+ label: string | (() => string);
36
+ key?: string | string[];
37
+ predicate?: (ctx: ActionContext<R>) => boolean;
38
+ handler: (ctx: ActionContext<R>) => void | Promise<void>;
39
+ destructive?: boolean;
40
+ primary?: boolean;
41
+ showInFooter?: boolean;
42
+ }
43
+ export interface ActionContext<R> {
44
+ row: Row;
45
+ rows: Row[];
46
+ item?: DetailItem;
47
+ filter: string;
48
+ refresh: () => Promise<void>;
49
+ suspendAnd: <T>(fn: () => Promise<T>) => Promise<T>;
50
+ toast: (msg: string, tone?: Tone) => void;
51
+ confirm: (prompt: string) => Promise<boolean>;
52
+ exit: (after?: () => void | Promise<void>) => void;
53
+ }
54
+ export interface ExplorerConfig<R> {
55
+ title: string;
56
+ rows: () => Promise<Row[]>;
57
+ detail: Detail<R>;
58
+ actions: Action<R>[];
59
+ reorder?: {
60
+ onReorder: (orderedIds: string[]) => void | Promise<void>;
61
+ };
62
+ multiSelect?: boolean;
63
+ keybindOverrides?: Record<string, string | string[]>;
64
+ emptyHint?: string;
65
+ initialFilter?: string;
66
+ }
67
+ export declare const REGION_HEADER: number;
68
+ export declare const REGION_LIST: number;
69
+ export declare const REGION_DETAIL: number;
70
+ export declare const REGION_FOOTER: number;
71
+ export declare const REGION_MODAL: number;
72
+ export declare const REGION_TOAST: number;
73
+ export declare const REGION_ALL: number;
74
+ export type Dirty = number;
75
+ export type ExplorerLayoutMode = "wide" | "medium" | "narrow-vertical" | "narrow-list-only" | "too-narrow";
76
+ export interface ExplorerSize {
77
+ cols: number;
78
+ rows: number;
79
+ }
80
+ export interface ExplorerState {
81
+ title: string;
82
+ emptyHint: string;
83
+ rows: Row[];
84
+ filtered: number[];
85
+ matchPositions: Map<number, number[]>;
86
+ cursor: number;
87
+ filter: string;
88
+ filterFocused: boolean;
89
+ focused: "list" | "detail";
90
+ detail: {
91
+ rowId: string | null;
92
+ items: DetailItem[] | null;
93
+ cursor: number;
94
+ scroll: number;
95
+ token: number;
96
+ loading: boolean;
97
+ };
98
+ selected: Set<string>;
99
+ modal: null | {
100
+ kind: "help";
101
+ } | {
102
+ kind: "confirm";
103
+ action: Action<unknown>;
104
+ rows: Row[];
105
+ resolver: (ok: boolean) => void;
106
+ } | {
107
+ kind: "palette";
108
+ query: string;
109
+ cursor: number;
110
+ };
111
+ toast: {
112
+ message: string;
113
+ tone: Tone;
114
+ expiresAt: number;
115
+ } | null;
116
+ dirty: Dirty;
117
+ size: ExplorerSize;
118
+ layout: ExplorerLayoutMode;
119
+ bindings: ResolvedBindings;
120
+ actionState: Map<string, ActionStateEntry>;
121
+ }
122
+ export interface ActionStateEntry {
123
+ available: boolean;
124
+ label: string;
125
+ running?: boolean;
126
+ action?: Action<unknown>;
127
+ source?: "row" | "detail";
128
+ }
129
+ export declare function createInitialState<R>(config: ExplorerConfig<R>, size: ExplorerSize): ExplorerState;
130
+ export declare function resolveExplorerLayoutMode(cols: number): ExplorerLayoutMode;
@@ -0,0 +1,87 @@
1
+ import { resolveBindings } from "./keymap.js";
2
+ export const REGION_HEADER = 1 << 0;
3
+ export const REGION_LIST = 1 << 1;
4
+ export const REGION_DETAIL = 1 << 2;
5
+ export const REGION_FOOTER = 1 << 3;
6
+ export const REGION_MODAL = 1 << 4;
7
+ export const REGION_TOAST = 1 << 5;
8
+ export const REGION_ALL = REGION_HEADER |
9
+ REGION_LIST |
10
+ REGION_DETAIL |
11
+ REGION_FOOTER |
12
+ REGION_MODAL |
13
+ REGION_TOAST;
14
+ export function createInitialState(config, size) {
15
+ const normalizedSize = {
16
+ cols: normalizeSize(size.cols),
17
+ rows: normalizeSize(size.rows)
18
+ };
19
+ return {
20
+ title: config.title,
21
+ emptyHint: config.emptyHint ?? "No detail",
22
+ rows: [],
23
+ filtered: [],
24
+ matchPositions: new Map(),
25
+ cursor: 0,
26
+ filter: config.initialFilter ?? "",
27
+ filterFocused: false,
28
+ focused: "list",
29
+ detail: {
30
+ rowId: null,
31
+ items: null,
32
+ cursor: 0,
33
+ scroll: 0,
34
+ token: 0,
35
+ loading: false
36
+ },
37
+ selected: new Set(),
38
+ modal: null,
39
+ toast: null,
40
+ dirty: REGION_ALL,
41
+ size: normalizedSize,
42
+ layout: resolveExplorerLayoutMode(normalizedSize.cols),
43
+ bindings: resolveBindings(config),
44
+ actionState: createInitialActionState(config)
45
+ };
46
+ }
47
+ function createInitialActionState(config) {
48
+ const state = new Map();
49
+ for (const action of config.actions) {
50
+ state.set(action.id, {
51
+ available: true,
52
+ label: typeof action.label === "function" ? action.id : action.label,
53
+ action: action,
54
+ source: "row"
55
+ });
56
+ }
57
+ for (const action of config.detail.actions ?? []) {
58
+ state.set(action.id, {
59
+ available: true,
60
+ label: typeof action.label === "function" ? action.id : action.label,
61
+ action: action,
62
+ source: "detail"
63
+ });
64
+ }
65
+ return state;
66
+ }
67
+ export function resolveExplorerLayoutMode(cols) {
68
+ if (cols < 40) {
69
+ return "too-narrow";
70
+ }
71
+ if (cols < 80) {
72
+ return "narrow-list-only";
73
+ }
74
+ if (cols < 100) {
75
+ return "narrow-vertical";
76
+ }
77
+ if (cols < 120) {
78
+ return "medium";
79
+ }
80
+ return "wide";
81
+ }
82
+ function normalizeSize(value) {
83
+ if (!Number.isFinite(value)) {
84
+ return 0;
85
+ }
86
+ return Math.max(0, Math.floor(value));
87
+ }
@@ -0,0 +1,27 @@
1
+ import type { Tone } from "./state.js";
2
+ type CellStyle = {
3
+ fg?: string;
4
+ bg?: string;
5
+ bold?: boolean;
6
+ dim?: boolean;
7
+ underline?: boolean;
8
+ };
9
+ export interface ExplorerTheme {
10
+ accent: (text: string) => string;
11
+ muted: (text: string) => string;
12
+ border: (text: string) => string;
13
+ borderFocused: (text: string) => string;
14
+ badge: (text: string, tone: Tone) => string;
15
+ matchHighlight: (text: string) => string;
16
+ }
17
+ export interface ExplorerStyles {
18
+ accent: CellStyle;
19
+ muted: CellStyle;
20
+ border: CellStyle;
21
+ borderFocused: CellStyle;
22
+ matchHighlight: CellStyle;
23
+ tones: Record<Tone, CellStyle>;
24
+ }
25
+ export declare function getExplorerTheme(): ExplorerTheme;
26
+ export declare function getExplorerStyles(): ExplorerStyles;
27
+ export {};