toolcraft-openapi 0.0.151 → 0.0.153

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 (55) hide show
  1. package/dist/composition.json +2 -2
  2. package/node_modules/toolcraft-design/README.md +30 -0
  3. package/node_modules/toolcraft-design/dist/dashboard/terminal-width.d.ts +1 -0
  4. package/node_modules/toolcraft-design/dist/dashboard/terminal-width.js +18 -1
  5. package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +1 -1
  6. package/node_modules/toolcraft-design/dist/explorer/actions.js +22 -4
  7. package/node_modules/toolcraft-design/dist/explorer/filter.js +1 -1
  8. package/node_modules/toolcraft-design/dist/explorer/index.d.ts +2 -4
  9. package/node_modules/toolcraft-design/dist/explorer/index.js +1 -2
  10. package/node_modules/toolcraft-design/dist/explorer/jobs.d.ts +2 -2
  11. package/node_modules/toolcraft-design/dist/explorer/jobs.js +3 -5
  12. package/node_modules/toolcraft-design/dist/explorer/keymap.d.ts +13 -3
  13. package/node_modules/toolcraft-design/dist/explorer/keymap.js +89 -348
  14. package/node_modules/toolcraft-design/dist/explorer/layout.d.ts +1 -0
  15. package/node_modules/toolcraft-design/dist/explorer/layout.js +14 -4
  16. package/node_modules/toolcraft-design/dist/explorer/reducer.js +58 -9
  17. package/node_modules/toolcraft-design/dist/explorer/render/detail.d.ts +1 -1
  18. package/node_modules/toolcraft-design/dist/explorer/render/detail.js +43 -7
  19. package/node_modules/toolcraft-design/dist/explorer/render/footer.d.ts +1 -1
  20. package/node_modules/toolcraft-design/dist/explorer/render/footer.js +6 -3
  21. package/node_modules/toolcraft-design/dist/explorer/render/header.d.ts +1 -1
  22. package/node_modules/toolcraft-design/dist/explorer/render/header.js +6 -2
  23. package/node_modules/toolcraft-design/dist/explorer/render/index.d.ts +1 -1
  24. package/node_modules/toolcraft-design/dist/explorer/render/index.js +2 -1
  25. package/node_modules/toolcraft-design/dist/explorer/render/list.d.ts +16 -2
  26. package/node_modules/toolcraft-design/dist/explorer/render/list.js +13 -34
  27. package/node_modules/toolcraft-design/dist/explorer/render/modal.d.ts +1 -1
  28. package/node_modules/toolcraft-design/dist/explorer/render/pane.d.ts +5 -2
  29. package/node_modules/toolcraft-design/dist/explorer/render/pane.js +9 -6
  30. package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +2 -3
  31. package/node_modules/toolcraft-design/dist/explorer/runtime.js +68 -63
  32. package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.d.ts +23 -37
  33. package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.js +32 -83
  34. package/node_modules/toolcraft-design/dist/explorer/state.d.ts +47 -5
  35. package/node_modules/toolcraft-design/dist/explorer/state.js +56 -12
  36. package/node_modules/toolcraft-design/dist/index.d.ts +8 -2
  37. package/node_modules/toolcraft-design/dist/index.js +5 -1
  38. package/node_modules/toolcraft-design/dist/screen/ansi-text.d.ts +2 -0
  39. package/node_modules/toolcraft-design/dist/screen/ansi-text.js +35 -0
  40. package/node_modules/toolcraft-design/dist/screen/screen.d.ts +40 -0
  41. package/node_modules/toolcraft-design/dist/screen/screen.js +122 -0
  42. package/node_modules/toolcraft-design/dist/screen/style.d.ts +16 -0
  43. package/node_modules/toolcraft-design/dist/screen/style.js +55 -0
  44. package/node_modules/toolcraft-design/dist/terminal/driver.d.ts +33 -0
  45. package/node_modules/toolcraft-design/dist/terminal/driver.js +61 -0
  46. package/node_modules/toolcraft-design/dist/terminal/input.d.ts +25 -0
  47. package/node_modules/toolcraft-design/dist/terminal/input.js +181 -0
  48. package/node_modules/toolcraft-design/dist/terminal/output.d.ts +8 -0
  49. package/node_modules/toolcraft-design/dist/terminal/output.js +42 -0
  50. package/node_modules/toolcraft-schema/package.json +1 -1
  51. package/package.json +3 -3
  52. package/node_modules/toolcraft-design/dist/explorer/two-pane.d.ts +0 -102
  53. package/node_modules/toolcraft-design/dist/explorer/two-pane.js +0 -510
  54. package/node_modules/toolcraft-design/dist/run-two-pane-explorer.d.ts +0 -2
  55. package/node_modules/toolcraft-design/dist/run-two-pane-explorer.js +0 -1
@@ -1,10 +1,11 @@
1
- import { ScreenBuffer, cellToAnsi, diff } from "../dashboard/buffer.js";
2
- import { createTerminalDriver } from "../dashboard/terminal.js";
1
+ import { Screen } from "../screen/screen.js";
2
+ import { appendFileSync } from "node:fs";
3
+ import { createTerminalDriver } from "../terminal/driver.js";
3
4
  import { createDetailJobs } from "./jobs.js";
4
5
  import { computeExplorerLayout } from "./layout.js";
5
6
  import { renderExplorer } from "./render/index.js";
6
7
  import { step } from "./reducer.js";
7
- import { createInitialState, REGION_ALL, REGION_MODAL, REGION_TOAST } from "./state.js";
8
+ import { createInitialState, REGION_ALL, REGION_MODAL, REGION_TOAST, normalizeExplorerConfig } from "./state.js";
8
9
  const TOAST_MS = 2500;
9
10
  export async function runExplorer(config) {
10
11
  if (process.stdout.isTTY !== true) {
@@ -15,10 +16,10 @@ export async function runExplorer(config) {
15
16
  return runtime.run();
16
17
  }
17
18
  class ExplorerRuntime {
18
- config;
19
19
  driver;
20
+ config;
20
21
  state;
21
- previousBuffer = new ScreenBuffer(0, 0);
22
+ screen;
22
23
  detailJobs;
23
24
  runtimeHandles;
24
25
  pendingEffects = new Set();
@@ -28,11 +29,14 @@ class ExplorerRuntime {
28
29
  rowsRequestToken = 0;
29
30
  reorderToken = 0;
30
31
  stopped = false;
32
+ renderScheduled = false;
33
+ tracePath = process.env.POE_CODE_TUI_TRACE;
31
34
  settle;
32
35
  constructor(config, driver) {
33
- this.config = config;
34
36
  this.driver = driver;
35
- this.state = createInitialState(config, driver.getSize());
37
+ this.config = normalizeExplorerConfig(config);
38
+ this.state = createInitialState(this.config, driver.getSize());
39
+ this.screen = new Screen(driver.getSize());
36
40
  this.detailJobs = createDetailJobs((event) => {
37
41
  if (event.type === "detailLoaded") {
38
42
  this.loadDetailContent(event.rowId, event.token, event.items);
@@ -69,7 +73,7 @@ class ExplorerRuntime {
69
73
  // Seed list/detail before first paint so callers with known rows avoid an empty flash.
70
74
  this.dispatch({ type: "rowsLoaded", rows: this.config.initialRows });
71
75
  }
72
- this.render();
76
+ this.renderNow();
73
77
  this.loadRows().catch((error) => {
74
78
  this.fail(error);
75
79
  });
@@ -80,13 +84,9 @@ class ExplorerRuntime {
80
84
  });
81
85
  }
82
86
  startTerminal() {
83
- this.driver.enterRawMode();
84
- this.driver.enterAltScreen();
85
- this.driver.disableLineWrap();
86
- this.driver.hideCursor();
87
+ this.driver.start();
87
88
  this.subscribeKeypress();
88
- this.unsubscribeResize = this.driver.onResize(() => {
89
- const size = this.driver.getSize();
89
+ this.unsubscribeResize = this.driver.onResize((size) => {
90
90
  this.dispatch({ type: "resize", cols: size.cols, rows: size.rows });
91
91
  });
92
92
  }
@@ -94,8 +94,19 @@ class ExplorerRuntime {
94
94
  if (this.unsubscribeKeypress !== undefined) {
95
95
  return;
96
96
  }
97
- this.unsubscribeKeypress = this.driver.onKeypress((key) => {
98
- this.dispatch({ type: "key", key });
97
+ this.unsubscribeKeypress = this.driver.onEvent((event) => {
98
+ this.trace("input", { event });
99
+ if (event.type === "paste") {
100
+ 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
+ }
103
+ return;
104
+ }
105
+ if (event.type === "wheel") {
106
+ this.dispatch({ type: "key", key: { name: event.direction, ctrl: false, meta: false, shift: false } });
107
+ return;
108
+ }
109
+ this.dispatch({ type: "key", key: { name: event.name, ch: event.ch, ctrl: event.ctrl, meta: event.alt, shift: event.shift } });
99
110
  });
100
111
  }
101
112
  pauseKeypress() {
@@ -120,13 +131,13 @@ class ExplorerRuntime {
120
131
  const previousState = this.state;
121
132
  const next = step(this.state, event, this.runtimeHandles);
122
133
  this.state = next.state;
123
- this.render();
134
+ this.scheduleRender();
124
135
  this.applyEffects(next.effects, previousState);
125
136
  }
126
137
  applyEffects(effects, previousState) {
127
138
  for (const effect of effects) {
128
139
  if (effect.type === "renderDetail") {
129
- this.renderDetail(effect.rowId);
140
+ this.renderDetail(effect.rowId, effect.token);
130
141
  continue;
131
142
  }
132
143
  if (effect.type === "persistOrder") {
@@ -142,7 +153,7 @@ class ExplorerRuntime {
142
153
  }
143
154
  }
144
155
  }
145
- renderDetail(rowId) {
156
+ renderDetail(rowId, token) {
146
157
  const row = this.state.rows.find((candidate) => candidate.id === rowId);
147
158
  if (row === undefined) {
148
159
  return;
@@ -150,10 +161,11 @@ class ExplorerRuntime {
150
161
  const layout = computeExplorerLayout({
151
162
  cols: this.state.size.cols,
152
163
  rows: this.state.size.rows,
153
- detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow"
164
+ detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow",
165
+ focused: this.state.focused
154
166
  });
155
167
  const reloadDetail = () => this.reloadFocusedDetail(rowId);
156
- void this.detailJobs.schedule(rowId, (ctx) => this.config.detail.items(row, { ...ctx, reloadDetail }), {
168
+ void this.detailJobs.schedule(rowId, token, (ctx) => this.config.detail.items(row, { ...ctx, reloadDetail }), {
157
169
  width: layout.detail.width,
158
170
  height: layout.detail.height,
159
171
  row,
@@ -169,7 +181,9 @@ class ExplorerRuntime {
169
181
  if (rowId !== undefined && rowId !== focusedId) {
170
182
  return;
171
183
  }
172
- this.renderDetail(focusedId);
184
+ const token = this.state.detail.token + 1;
185
+ this.state = { ...this.state, detail: { ...this.state.detail, token } };
186
+ this.renderDetail(focusedId, token);
173
187
  }
174
188
  loadDetailContent(rowId, token, items) {
175
189
  const row = this.state.rows.find((candidate) => candidate.id === rowId);
@@ -179,7 +193,8 @@ class ExplorerRuntime {
179
193
  const layout = computeExplorerLayout({
180
194
  cols: this.state.size.cols,
181
195
  rows: this.state.size.rows,
182
- detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow"
196
+ detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow",
197
+ focused: this.state.focused
183
198
  });
184
199
  const context = {
185
200
  width: layout.detail.width,
@@ -243,19 +258,15 @@ class ExplorerRuntime {
243
258
  }
244
259
  async suspendAnd(fn) {
245
260
  this.pauseKeypress();
246
- this.driver.exitAltScreen();
247
- this.driver.enableLineWrap();
248
- this.driver.showCursor();
249
- this.driver.exitRawMode();
261
+ this.state = { ...this.state, suspended: true };
262
+ this.driver.stop();
250
263
  try {
251
264
  return await fn();
252
265
  }
253
266
  finally {
254
267
  if (!this.stopped) {
255
- this.driver.enterRawMode();
256
- this.driver.enterAltScreen();
257
- this.driver.disableLineWrap();
258
- this.driver.hideCursor();
268
+ this.driver.start();
269
+ this.state = { ...this.state, suspended: false };
259
270
  this.subscribeKeypress();
260
271
  const size = this.driver.getSize();
261
272
  this.dispatch({
@@ -284,7 +295,7 @@ class ExplorerRuntime {
284
295
  },
285
296
  dirty: REGION_MODAL
286
297
  };
287
- this.render();
298
+ this.scheduleRender();
288
299
  });
289
300
  }
290
301
  showToast(message, tone = "info") {
@@ -299,7 +310,7 @@ class ExplorerRuntime {
299
310
  toast: { message, tone, expiresAt: Date.now() + TOAST_MS },
300
311
  dirty: REGION_TOAST
301
312
  };
302
- this.render();
313
+ this.scheduleRender();
303
314
  this.toastTimer = setTimeout(() => {
304
315
  this.dispatch({ type: "toastExpired" });
305
316
  }, TOAST_MS);
@@ -307,20 +318,29 @@ class ExplorerRuntime {
307
318
  currentRow() {
308
319
  return this.state.rows[this.state.filtered[this.state.cursor] ?? -1];
309
320
  }
310
- render() {
311
- if (this.stopped) {
321
+ scheduleRender() {
322
+ if (this.stopped || this.state.suspended || this.renderScheduled) {
312
323
  return;
313
324
  }
325
+ this.renderScheduled = true;
326
+ setImmediate(() => {
327
+ this.renderScheduled = false;
328
+ this.renderNow();
329
+ });
330
+ }
331
+ renderNow() {
332
+ if (this.stopped || this.state.suspended)
333
+ return;
314
334
  const size = this.driver.getSize();
315
335
  if (size.cols !== this.state.size.cols || size.rows !== this.state.size.rows) {
316
336
  this.state = step(this.state, { type: "resize", cols: size.cols, rows: size.rows }, this.runtimeHandles).state;
317
337
  }
318
- const nextBuffer = this.state.dirty === REGION_ALL
319
- ? new ScreenBuffer(this.state.size.cols, this.state.size.rows)
320
- : cloneBuffer(this.previousBuffer);
321
- renderExplorer(this.state, nextBuffer);
322
- this.driver.write(changesToAnsi(diff(this.previousBuffer, nextBuffer)));
323
- this.previousBuffer = nextBuffer;
338
+ if (this.screen.width !== this.state.size.cols || this.screen.height !== this.state.size.rows)
339
+ this.screen.resize(this.state.size);
340
+ renderExplorer({ ...this.state, dirty: REGION_ALL }, this.screen);
341
+ const frame = this.screen.flush();
342
+ this.driver.writeFrame(frame);
343
+ this.trace("frame", { bytes: Buffer.byteLength(frame), cols: this.state.size.cols, rows: this.state.size.rows });
324
344
  this.state = { ...this.state, dirty: 0 };
325
345
  }
326
346
  track(promise) {
@@ -340,8 +360,8 @@ class ExplorerRuntime {
340
360
  if (this.toastTimer !== undefined) {
341
361
  clearTimeout(this.toastTimer);
342
362
  }
343
- this.driver.destroy();
344
- Promise.resolve()
363
+ this.driver.stop();
364
+ Promise.allSettled([...this.pendingEffects])
345
365
  .then(() => after?.())
346
366
  .then(() => {
347
367
  this.settle?.resolve(result);
@@ -353,28 +373,13 @@ class ExplorerRuntime {
353
373
  fail(error) {
354
374
  if (!this.stopped) {
355
375
  this.stopped = true;
356
- this.driver.destroy();
376
+ this.driver.stop();
357
377
  }
358
378
  this.settle?.reject(error);
359
379
  }
360
- }
361
- function cloneBuffer(buffer) {
362
- const next = new ScreenBuffer(buffer.width, buffer.height);
363
- for (let y = 0; y < buffer.height; y += 1) {
364
- for (let x = 0; x < buffer.width; x += 1) {
365
- const cell = buffer.get(x, y);
366
- next.put(x, y, cell.ch, cell.style);
367
- }
368
- }
369
- return next;
370
- }
371
- function changesToAnsi(changes) {
372
- let output = "";
373
- for (const change of changes) {
374
- output += `${cursorPositionAnsi(change.x, change.y)}${cellToAnsi(change.cell)}`;
380
+ trace(type, fields) {
381
+ if (this.tracePath === undefined || this.tracePath.length === 0)
382
+ return;
383
+ appendFileSync(this.tracePath, `${JSON.stringify({ type, timestamp: new Date().toISOString(), ...fields })}\n`);
375
384
  }
376
- return output;
377
- }
378
- function cursorPositionAnsi(x, y) {
379
- return `\u001b[${Math.max(1, y + 1)};${Math.max(1, x + 1)}H`;
380
385
  }
@@ -1,50 +1,36 @@
1
- import type { Cell } from "../dashboard/types.js";
2
- import type { KeypressEvent, TerminalDriver } from "../dashboard/terminal.js";
1
+ import type { TerminalDriver, TerminalInputEvent } from "../terminal/driver.js";
3
2
  export declare class FakeTerminalDriver implements TerminalDriver {
4
3
  private cols;
5
4
  private rows;
6
- readonly keyQueue: KeypressEvent[];
7
5
  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;
6
+ started: boolean;
7
+ startCount: number;
8
+ stopCount: number;
9
+ private readonly eventHandlers;
23
10
  private readonly resizeHandlers;
24
11
  constructor(cols?: number, rows?: number);
25
12
  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;
13
+ get destroyed(): boolean;
14
+ get altScreen(): boolean;
15
+ get enterAltScreenCount(): number;
16
+ start(): void;
17
+ stop(): void;
18
+ onEvent(handler: (event: TerminalInputEvent) => void): () => void;
19
+ onResize(handler: (size: {
20
+ cols: number;
21
+ rows: number;
22
+ }) => void): () => void;
41
23
  getSize(): {
42
24
  cols: number;
43
25
  rows: number;
44
26
  };
27
+ writeFrame(ansi: string): void;
45
28
  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;
29
+ press(key: {
30
+ name?: string;
31
+ ch?: string;
32
+ ctrl: boolean;
33
+ meta: boolean;
34
+ shift: boolean;
35
+ }): void;
50
36
  }
@@ -1,101 +1,50 @@
1
1
  export class FakeTerminalDriver {
2
2
  cols;
3
3
  rows;
4
- keyQueue = [];
5
4
  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();
5
+ started = false;
6
+ startCount = 0;
7
+ stopCount = 0;
8
+ eventHandlers = new Set();
17
9
  resizeHandlers = new Set();
18
10
  constructor(cols = 120, rows = 24) {
19
11
  this.cols = cols;
20
12
  this.rows = rows;
21
13
  }
22
- get output() {
23
- return this.writes.join("");
14
+ get output() { return this.writes.join(""); }
15
+ get destroyed() { return !this.started && this.stopCount > 0; }
16
+ get altScreen() { return this.started; }
17
+ get enterAltScreenCount() { return this.startCount; }
18
+ start() { if (!this.started) {
19
+ this.started = true;
20
+ this.startCount += 1;
21
+ } }
22
+ stop() { if (this.started) {
23
+ this.started = false;
24
+ this.stopCount += 1;
25
+ } }
26
+ onEvent(handler) {
27
+ this.eventHandlers.add(handler);
28
+ return () => { this.eventHandlers.delete(handler); };
24
29
  }
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 };
30
+ onResize(handler) {
31
+ this.resizeHandlers.add(handler);
32
+ return () => { this.resizeHandlers.delete(handler); };
66
33
  }
34
+ getSize() { return { cols: this.cols, rows: this.rows }; }
35
+ writeFrame(ansi) { if (this.started)
36
+ this.writes.push(ansi); }
67
37
  resize(cols, rows) {
68
38
  this.cols = cols;
69
39
  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
- };
40
+ for (const handler of this.resizeHandlers)
41
+ handler(this.getSize());
85
42
  }
86
43
  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();
44
+ if (!this.started)
45
+ return;
46
+ const event = { type: "key", name: key.name ?? key.ch ?? "", ch: key.ch, ctrl: key.ctrl, alt: key.meta, shift: key.shift };
47
+ for (const handler of this.eventHandlers)
48
+ handler(event);
100
49
  }
101
50
  }
@@ -37,7 +37,9 @@ export interface Action<R> {
37
37
  id: string;
38
38
  label: string | (() => string);
39
39
  key?: string | string[];
40
+ accelerator?: string;
40
41
  predicate?: (ctx: ActionContext<R>) => boolean;
42
+ visible?: (row: Row) => boolean;
41
43
  handler: (ctx: ActionContext<R>) => void | Promise<void>;
42
44
  destructive?: boolean;
43
45
  primary?: boolean;
@@ -59,6 +61,31 @@ export interface ActionContext<R> {
59
61
  toast: (msg: string, tone?: Tone) => void;
60
62
  confirm: (prompt: string) => Promise<boolean>;
61
63
  exit: (after?: () => void | Promise<void>) => void;
64
+ activePane?: PaneRuntimeState;
65
+ inactivePane?: PaneRuntimeState;
66
+ }
67
+ export interface ListPaneConfig {
68
+ id: string;
69
+ kind: "list";
70
+ title: string;
71
+ rows: () => Promise<Row[]>;
72
+ emptyHint?: string;
73
+ multiSelect?: boolean;
74
+ }
75
+ export interface DetailPaneConfig {
76
+ id: string;
77
+ kind: "detail";
78
+ title: string;
79
+ render: (row: Row | undefined, ctx: DetailCtx) => string | Promise<string>;
80
+ }
81
+ export type PaneConfig = ListPaneConfig | DetailPaneConfig;
82
+ export interface PaneRuntimeState {
83
+ id: string;
84
+ title: string;
85
+ rows: Row[];
86
+ cursor: number;
87
+ selected: Set<string>;
88
+ filter: string;
62
89
  }
63
90
  export interface ReorderContext {
64
91
  refresh: () => Promise<void>;
@@ -66,9 +93,10 @@ export interface ReorderContext {
66
93
  }
67
94
  export interface ExplorerConfig<R> {
68
95
  title: string;
69
- rows: () => Promise<Row[]>;
70
- refresh?: () => Promise<void>;
71
- detail: Detail<R>;
96
+ panes?: PaneConfig[];
97
+ rows?: () => Promise<Row[]>;
98
+ refresh?: () => void | Promise<void>;
99
+ detail?: Detail<R>;
72
100
  actions: Action<R>[];
73
101
  reorder?: {
74
102
  onReorder: (orderedIds: string[], ctx?: ReorderContext) => void | Promise<void>;
@@ -80,6 +108,11 @@ export interface ExplorerConfig<R> {
80
108
  /** Synchronous first paint rows; still refreshed via `rows()` after start. */
81
109
  initialRows?: Row[];
82
110
  }
111
+ export type NormalizedExplorerConfig<R> = ExplorerConfig<R> & {
112
+ rows: () => Promise<Row[]>;
113
+ detail: Detail<R>;
114
+ };
115
+ export declare function normalizeExplorerConfig<R>(config: ExplorerConfig<R>): NormalizedExplorerConfig<R>;
83
116
  export declare const REGION_HEADER: number;
84
117
  export declare const REGION_LIST: number;
85
118
  export declare const REGION_DETAIL: number;
@@ -97,6 +130,7 @@ export interface ExplorerState {
97
130
  title: string;
98
131
  emptyHint: string;
99
132
  rows: Row[];
133
+ rowsLoading: boolean;
100
134
  filtered: number[];
101
135
  matchPositions: Map<number, number[]>;
102
136
  cursor: number;
@@ -106,6 +140,8 @@ export interface ExplorerState {
106
140
  detail: {
107
141
  rowId: string | null;
108
142
  items: DetailItem[] | null;
143
+ allItems?: DetailItem[] | null;
144
+ filter?: string;
109
145
  cursor: number;
110
146
  scroll: number;
111
147
  token: number;
@@ -140,13 +176,19 @@ export interface ExplorerState {
140
176
  layout: ExplorerLayoutMode;
141
177
  bindings: ResolvedBindings;
142
178
  actionState: Map<string, ActionStateEntry>;
179
+ suspended: boolean;
180
+ paneDefinitions: Array<{
181
+ id: string;
182
+ title: string;
183
+ kind: "list" | "detail";
184
+ }>;
143
185
  }
144
186
  export interface ActionStateEntry {
145
187
  available: boolean;
146
188
  label: string;
147
189
  running?: boolean;
148
190
  action?: Action<unknown>;
149
- source?: "row" | "detail";
191
+ source?: "row" | "detail" | "both";
150
192
  }
151
193
  export declare function createInitialState<R>(config: ExplorerConfig<R>, size: ExplorerSize): ExplorerState;
152
- export declare function resolveExplorerLayoutMode(cols: number): ExplorerLayoutMode;
194
+ export declare function resolveExplorerLayoutMode(cols: number, rows?: number): ExplorerLayoutMode;