toolcraft 0.0.93 → 0.0.95

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 (76) hide show
  1. package/README.md +74 -4
  2. package/composition.json +1 -1
  3. package/dist/agent-defs.d.ts +1 -0
  4. package/dist/agent-defs.js +1 -0
  5. package/dist/agent-human-in-loop.d.ts +1 -0
  6. package/dist/agent-human-in-loop.js +1 -0
  7. package/dist/agent-mcp-config.d.ts +1 -0
  8. package/dist/agent-mcp-config.js +1 -0
  9. package/dist/auth-store.d.ts +1 -0
  10. package/dist/auth-store.js +1 -0
  11. package/dist/cli.d.ts +4 -1
  12. package/dist/cli.js +24 -65
  13. package/dist/composition.json +1 -1
  14. package/dist/config-mutations.d.ts +1 -0
  15. package/dist/config-mutations.js +1 -0
  16. package/dist/frontmatter.d.ts +1 -0
  17. package/dist/frontmatter.js +1 -0
  18. package/dist/human-in-loop/gate.d.ts +5 -1
  19. package/dist/human-in-loop/gate.js +10 -8
  20. package/dist/human-in-loop/runner.js +1 -28
  21. package/dist/mcp.d.ts +4 -1
  22. package/dist/mcp.js +8 -51
  23. package/dist/process-runner.d.ts +1 -0
  24. package/dist/process-runner.js +1 -0
  25. package/dist/runtime/io.d.ts +5 -0
  26. package/dist/runtime/io.js +50 -0
  27. package/dist/sdk.d.ts +12 -8
  28. package/dist/sdk.js +7 -52
  29. package/dist/task-list.d.ts +1 -0
  30. package/dist/task-list.js +1 -0
  31. package/dist/testing/fakes.d.ts +20 -0
  32. package/dist/testing/fakes.js +83 -0
  33. package/dist/testing/fixtures.d.ts +8 -0
  34. package/dist/testing/fixtures.js +248 -0
  35. package/dist/testing/harness.d.ts +76 -0
  36. package/dist/testing/harness.js +391 -0
  37. package/dist/testing/index.d.ts +4 -0
  38. package/dist/testing/index.js +3 -0
  39. package/dist/testing/memory-fs.d.ts +11 -0
  40. package/dist/testing/memory-fs.js +61 -0
  41. package/dist/testing/parity.d.ts +25 -0
  42. package/dist/testing/parity.js +384 -0
  43. package/dist/testing/render-capture.d.ts +6 -0
  44. package/dist/testing/render-capture.js +54 -0
  45. package/dist/tiny-mcp-client.d.ts +1 -0
  46. package/dist/tiny-mcp-client.js +1 -0
  47. package/node_modules/@poe-code/agent-defs/package.json +2 -0
  48. package/node_modules/@poe-code/agent-human-in-loop/README.md +12 -2
  49. package/node_modules/@poe-code/agent-human-in-loop/package.json +2 -0
  50. package/node_modules/@poe-code/agent-mcp-config/README.md +7 -7
  51. package/node_modules/@poe-code/agent-mcp-config/package.json +2 -0
  52. package/node_modules/@poe-code/config-mutations/README.md +8 -8
  53. package/node_modules/@poe-code/config-mutations/package.json +2 -0
  54. package/node_modules/@poe-code/frontmatter/README.md +2 -2
  55. package/node_modules/@poe-code/frontmatter/package.json +2 -0
  56. package/node_modules/@poe-code/process-runner/README.md +1 -1
  57. package/node_modules/@poe-code/process-runner/package.json +2 -0
  58. package/node_modules/@poe-code/task-list/README.md +15 -3
  59. package/node_modules/@poe-code/task-list/package.json +2 -0
  60. package/node_modules/auth-store/README.md +15 -0
  61. package/node_modules/auth-store/package.json +2 -0
  62. package/node_modules/tiny-mcp-client/README.md +36 -36
  63. package/node_modules/tiny-mcp-client/package.json +2 -0
  64. package/node_modules/toolcraft-design/README.md +8 -4
  65. package/node_modules/toolcraft-design/dist/dashboard/terminal.js +48 -6
  66. package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +4 -0
  67. package/node_modules/toolcraft-design/dist/explorer/actions.js +1 -0
  68. package/node_modules/toolcraft-design/dist/explorer/events.d.ts +4 -0
  69. package/node_modules/toolcraft-design/dist/explorer/reducer.js +52 -4
  70. package/node_modules/toolcraft-design/dist/explorer/render/list.js +47 -20
  71. package/node_modules/toolcraft-design/dist/explorer/render/modal.js +13 -1
  72. package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +1 -1
  73. package/node_modules/toolcraft-design/dist/explorer/runtime.js +3 -0
  74. package/node_modules/toolcraft-design/dist/explorer/state.d.ts +9 -0
  75. package/node_modules/toolcraft-design/package.json +5 -0
  76. package/package.json +42 -2
@@ -45,3 +45,18 @@ await store.delete();
45
45
 
46
46
  - Uses the `security` CLI (`add-generic-password`, `find-generic-password`, `delete-generic-password`)
47
47
  - Configurable service and account names
48
+
49
+ ## Configuration Options
50
+
51
+ `createSecretStore()` accepts:
52
+
53
+ - `backendEnvVar`: optional env var name that selects `file` or `keychain`.
54
+ - `fileStore.salt`: application-specific encryption salt.
55
+ - `fileStore.defaultDirectory`: default credential directory.
56
+ - `fileStore.defaultFileName`: encrypted file name.
57
+ - `keychainStore.service`: macOS Keychain service name.
58
+ - `keychainStore.account`: macOS Keychain account name.
59
+
60
+ ## Environment Variables
61
+
62
+ This package reads only the caller-selected `backendEnvVar`. It does not define a fixed public env var.
@@ -1,4 +1,6 @@
1
1
  {
2
+ "name": "auth-store",
3
+ "version": "0.0.1",
2
4
  "private": true,
3
5
  "license": "MIT",
4
6
  "type": "module",
@@ -8,7 +8,7 @@
8
8
  import { HttpTransport, McpClient } from "tiny-mcp-client";
9
9
 
10
10
  const client = new McpClient({
11
- clientInfo: { name: "demo", version: "0.1.0" },
11
+ clientInfo: { name: "demo", version: "0.1.0" }
12
12
  });
13
13
 
14
14
  await client.connect(new HttpTransport({ url: "http://127.0.0.1:3000/mcp" }));
@@ -16,7 +16,7 @@ const tools = await client.listTools();
16
16
  console.log(tools);
17
17
  const result = await client.callTool({
18
18
  name: "search",
19
- arguments: { query: "typed outputs" },
19
+ arguments: { query: "typed outputs" }
20
20
  });
21
21
  console.log(result.structuredContent);
22
22
  await client.close();
@@ -26,11 +26,11 @@ await client.close();
26
26
 
27
27
  ## Transports
28
28
 
29
- | Transport | Description |
30
- |-----------|-------------|
31
- | `StdioTransport` | Spawns an MCP server process and communicates over stdio. |
32
- | `HttpTransport` | Connects to streamable HTTP MCP endpoints, including session IDs, SSE GET streams, and session termination. |
33
- | `createInMemoryTransportPair()` | Creates paired streams for in-process tests. |
29
+ | Transport | Description |
30
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
31
+ | `StdioTransport` | Spawns an MCP server process and communicates over stdio. |
32
+ | `HttpTransport` | Connects to streamable HTTP MCP endpoints, including session IDs, SSE GET streams, and session termination. |
33
+ | `createInMemoryTransportPair()` | Creates paired streams for in-process tests. |
34
34
 
35
35
  ## OAuth HTTP support
36
36
 
@@ -46,16 +46,16 @@ const transport = new HttpTransport({
46
46
  client: {
47
47
  mode: "dynamic",
48
48
  metadata: {
49
- clientName: "tiny-client",
50
- },
49
+ clientName: "tiny-client"
50
+ }
51
51
  },
52
52
  browser: {
53
53
  openBrowser: async (url) => {
54
54
  console.log(`Open ${url}`);
55
- },
56
- },
57
- }),
58
- },
55
+ }
56
+ }
57
+ })
58
+ }
59
59
  });
60
60
  ```
61
61
 
@@ -67,38 +67,38 @@ You can also call `discoverOAuthMetadata(resourceUrl, options)` directly, or ins
67
67
  - `createInMemoryTransportPair()` is useful for fast unit tests without opening sockets or spawning processes.
68
68
  - JSON-RPC error constants are exported for assertions: `ERROR_PARSE`, `ERROR_INVALID_REQUEST`, `ERROR_METHOD_NOT_FOUND`, `ERROR_INVALID_PARAMS`, and `ERROR_INTERNAL`.
69
69
 
70
- ## Config options
70
+ ## Configuration Options
71
71
 
72
72
  ### `McpClientOptions`
73
73
 
74
- | Option | Type | Description |
75
- |--------|------|-------------|
76
- | `clientInfo` | `{ name: string; version: string }` | Required client identity sent during initialize. |
77
- | `capabilities` | `ClientCapabilities` | Optional MCP client capabilities. |
78
- | `onToolsChanged`, `onResourcesChanged`, `onPromptsChanged` | callbacks | Optional notification handlers. |
79
- | `onResourceUpdated`, `onLog`, `onProgress` | callbacks | Optional resource/log/progress handlers. |
80
- | `onSamplingRequest`, `onRootsList` | callbacks | Optional server-to-client request handlers. |
74
+ | Option | Type | Description |
75
+ | ---------------------------------------------------------- | ----------------------------------- | ------------------------------------------------ |
76
+ | `clientInfo` | `{ name: string; version: string }` | Required client identity sent during initialize. |
77
+ | `capabilities` | `ClientCapabilities` | Optional MCP client capabilities. |
78
+ | `onToolsChanged`, `onResourcesChanged`, `onPromptsChanged` | callbacks | Optional notification handlers. |
79
+ | `onResourceUpdated`, `onLog`, `onProgress` | callbacks | Optional resource/log/progress handlers. |
80
+ | `onSamplingRequest`, `onRootsList` | callbacks | Optional server-to-client request handlers. |
81
81
 
82
82
  ### `HttpTransportOptions`
83
83
 
84
- | Option | Type | Description |
85
- |--------|------|-------------|
86
- | `url` | `string` | MCP HTTP endpoint URL. |
87
- | `headers` | `HeadersInit` | Static headers added to requests. |
88
- | `fetch` | `(input, init?) => Promise<Response>` | Custom fetch implementation. |
89
- | `oauth` | `OAuthClientProviderOptions` | Enables OAuth authorization handling. |
90
- | `oauthDiscoveryCache` | `OAuthDiscoveryCache` | Optional shared metadata cache. |
84
+ | Option | Type | Description |
85
+ | --------------------- | ------------------------------------- | ------------------------------------- |
86
+ | `url` | `string` | MCP HTTP endpoint URL. |
87
+ | `headers` | `HeadersInit` | Static headers added to requests. |
88
+ | `fetch` | `(input, init?) => Promise<Response>` | Custom fetch implementation. |
89
+ | `oauth` | `OAuthClientProviderOptions` | Enables OAuth authorization handling. |
90
+ | `oauthDiscoveryCache` | `OAuthDiscoveryCache` | Optional shared metadata cache. |
91
91
 
92
92
  ### `StdioTransportOptions`
93
93
 
94
- | Option | Type | Description |
95
- |--------|------|-------------|
96
- | `command` | `string` | Server executable. |
97
- | `args` | `string[]` | Server args. |
98
- | `cwd` | `string` | Server working directory. |
99
- | `env` | `NodeJS.ProcessEnv` | Server environment. |
100
- | `spawn` | `StdioSpawn` | Custom spawn function for tests. |
94
+ | Option | Type | Description |
95
+ | --------- | ------------------- | -------------------------------- |
96
+ | `command` | `string` | Server executable. |
97
+ | `args` | `string[]` | Server args. |
98
+ | `cwd` | `string` | Server working directory. |
99
+ | `env` | `NodeJS.ProcessEnv` | Server environment. |
100
+ | `spawn` | `StdioSpawn` | Custom spawn function for tests. |
101
101
 
102
- ## Environment variables
102
+ ## Environment Variables
103
103
 
104
104
  This package does not expose public environment variables. Pass process environment explicitly through `StdioTransportOptions.env` when a spawned MCP server needs it.
@@ -1,4 +1,6 @@
1
1
  {
2
+ "name": "tiny-mcp-client",
3
+ "version": "0.1.0",
2
4
  "private": true,
3
5
  "license": "MIT",
4
6
  "type": "module",
@@ -118,16 +118,16 @@ Raw HTML in Markdown is escaped by default. Pass `{ allowRawHtml: true }` only w
118
118
 
119
119
  Fenced code block syntax highlighting is opt-in:
120
120
 
121
- ```ts
121
+ ````ts
122
122
  import { renderMarkdown, renderMarkdownHtml } from "toolcraft-design";
123
123
 
124
- const html = renderMarkdownHtml("```ts\nconst value = \"hello\";\n```", {
124
+ const html = renderMarkdownHtml('```ts\nconst value = "hello";\n```', {
125
125
  syntaxHighlight: true
126
126
  });
127
- const terminal = renderMarkdown("```ts\nconst value = \"hello\";\n```", {
127
+ const terminal = renderMarkdown('```ts\nconst value = "hello";\n```', {
128
128
  syntaxHighlight: true
129
129
  });
130
- ```
130
+ ````
131
131
 
132
132
  HTML highlighting emits escaped code text with neutral Toolcraft-owned `<span>` wrappers. The renderer does not ship or inject CSS, so consumers control the appearance:
133
133
 
@@ -340,3 +340,7 @@ nvm exec 18.18 node packages/toolcraft-design/scripts/check-node18.mjs
340
340
  - `NO_COLOR`: disables color when `FORCE_COLOR` does not enable it.
341
341
  - `TERM`: participates in color support detection; an unset or `dumb` terminal disables color unless forced.
342
342
  - `POE_NO_SPINNER`: uses non-animated spinner behavior when set to `1`.
343
+
344
+ ## Configuration Options
345
+
346
+ Use `configureTheme({ brand?, label? })` for process-local brand settings, `withOutputFormat(format, fn)` for scoped output-format overrides, and component-specific options such as `RenderTableOptions`, `RenderMarkdownOptions`, prompt options, dashboard options, and explorer config. Environment variables above are process defaults, not config files.
@@ -118,19 +118,18 @@ export function createTerminalDriver(opts) {
118
118
  if (destroyed) {
119
119
  return () => { };
120
120
  }
121
- const listener = (str, key) => {
122
- const event = toKeypressEvent(str, key);
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("keypress", listener);
127
+ stdin.on("data", listener);
129
128
  return () => {
130
129
  if (!keypressListeners.delete(listener)) {
131
130
  return;
132
131
  }
133
- stdin.off("keypress", listener);
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("keypress", listener);
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
@@ -55,6 +55,10 @@ export type ExplorerEvent = {
55
55
  type: "suspendResumed";
56
56
  value: unknown;
57
57
  emit: ExplorerEvent;
58
+ } | {
59
+ type: "modalOpened";
60
+ title: string;
61
+ content: string;
58
62
  } | {
59
63
  type: "modalDismissed";
60
64
  result: unknown;
@@ -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
  }
@@ -164,6 +172,26 @@ function stepModalKey(state, key, target, runtimeHandles) {
164
172
  }
165
173
  return paletteInput(state, key);
166
174
  }
175
+ if (state.modal?.kind === "content") {
176
+ if (target?.type === "builtin") {
177
+ switch (target.id) {
178
+ case "escape":
179
+ case "confirm":
180
+ return modalDismissed(state, false, runtimeHandles);
181
+ case "cursorDown":
182
+ case "detailScrollDown":
183
+ return scrollContentModal(state, 1);
184
+ case "cursorUp":
185
+ case "detailScrollUp":
186
+ return scrollContentModal(state, -1);
187
+ case "pageDown":
188
+ return scrollContentModal(state, modalBodyHeight(state));
189
+ case "pageUp":
190
+ return scrollContentModal(state, -modalBodyHeight(state));
191
+ }
192
+ }
193
+ return mark(state, 0);
194
+ }
167
195
  return mark(state, 0);
168
196
  }
169
197
  function resize(state, cols, rows) {
@@ -287,7 +315,7 @@ function suspendResumed(state, emit, runtimeHandles) {
287
315
  }
288
316
  function modalDismissed(state, result, runtimeHandles) {
289
317
  const modal = state.modal;
290
- const closed = { ...state, modal: null, dirty: REGION_MODAL | REGION_FOOTER };
318
+ const closed = { ...state, modal: null, dirty: REGION_ALL };
291
319
  if (modal?.kind === "confirm") {
292
320
  modal.resolver(result === true);
293
321
  }
@@ -466,9 +494,6 @@ function selectionChanged(state, selected) {
466
494
  return { state: next, effects: NO_EFFECTS };
467
495
  }
468
496
  function detailScroll(state, delta) {
469
- if (state.focused !== "detail") {
470
- return mark(state, 0);
471
- }
472
497
  const scroll = clamp(state.detail.scroll + delta, 0, maxDetailScroll(state));
473
498
  if (scroll === state.detail.scroll) {
474
499
  return mark(state, 0);
@@ -478,6 +503,29 @@ function detailScroll(state, delta) {
478
503
  effects: NO_EFFECTS
479
504
  };
480
505
  }
506
+ function scrollContentModal(state, delta) {
507
+ if (state.modal?.kind !== "content") {
508
+ return mark(state, 0);
509
+ }
510
+ const scroll = clamp(state.modal.scroll + delta, 0, maxContentModalScroll(state));
511
+ if (scroll === state.modal.scroll) {
512
+ return mark(state, 0);
513
+ }
514
+ return setModal(state, { ...state.modal, scroll });
515
+ }
516
+ function maxContentModalScroll(state) {
517
+ if (state.modal?.kind !== "content") {
518
+ return 0;
519
+ }
520
+ return Math.max(0, state.modal.content.split("\n").length - modalBodyHeight(state));
521
+ }
522
+ function modalBodyHeight(state) {
523
+ const screenHeight = normalizeSize(state.size.rows);
524
+ if (screenHeight <= 2) {
525
+ return 0;
526
+ }
527
+ return Math.max(0, screenHeight - 4);
528
+ }
481
529
  function clampDetailScroll(state) {
482
530
  const scroll = clamp(state.detail.scroll, 0, maxDetailScroll(state));
483
531
  if (scroll === state.detail.scroll) {
@@ -28,28 +28,34 @@ export function renderList(state, screen, layout) {
28
28
  cache.clear();
29
29
  return;
30
30
  }
31
- let lastGroup;
31
+ const lines = buildDisplayLines(state);
32
+ const start = visibleStart(lines, bodyRect.height);
32
33
  let y = 0;
33
- for (const rowIndex of state.filtered) {
34
- if (y >= bodyRect.height) {
35
- break;
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(row.group, bodyRect), styles.muted);
39
+ writeLine(screen, bodyRect, y, formatGroupHeader(line.label, bodyRect), styles.muted);
45
40
  cache.set(y, hash);
46
41
  }
47
42
  y += 1;
48
- lastGroup = row.group;
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
- if (row.subtitle && y < bodyRect.height) {
68
- const subtitleHash = `${hash}:subtitle:${row.subtitle}`;
69
- if (cache.get(y) !== subtitleHash) {
70
- writeLine(screen, bodyRect, y, ` ${row.subtitle}`, styles.muted);
71
- cache.set(y, subtitleHash);
72
- }
73
- y += 1;
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} `;
@@ -4,7 +4,7 @@ export function renderModal(state, screen) {
4
4
  if (state.modal === null || screen.width <= 0 || screen.height <= 0) {
5
5
  return;
6
6
  }
7
- const width = Math.min(screen.width - 2, Math.max(34, Math.floor(screen.width * 0.62)));
7
+ const width = Math.min(screen.width - 2, Math.max(34, Math.floor(screen.width * modalWidthRatio(state))));
8
8
  const height = Math.min(screen.height - 2, modalHeight(state));
9
9
  const x = Math.max(0, Math.floor((screen.width - width) / 2));
10
10
  const y = Math.max(0, Math.floor((screen.height - height) / 2));
@@ -39,6 +39,9 @@ function modalLines(state) {
39
39
  ...paletteLines(state)
40
40
  ];
41
41
  }
42
+ if (state.modal?.kind === "content") {
43
+ return state.modal.content.split("\n").slice(state.modal.scroll);
44
+ }
42
45
  return [];
43
46
  }
44
47
  function title(state) {
@@ -48,11 +51,20 @@ function title(state) {
48
51
  if (state.modal?.kind === "confirm") {
49
52
  return "Confirm";
50
53
  }
54
+ if (state.modal?.kind === "content") {
55
+ return state.modal.title;
56
+ }
51
57
  return "Command Palette";
52
58
  }
53
59
  function modalHeight(state) {
60
+ if (state.modal?.kind === "content") {
61
+ return Math.max(5, state.modal.content.split("\n").length + 2);
62
+ }
54
63
  return Math.max(5, modalLines(state).length + 2);
55
64
  }
65
+ function modalWidthRatio(state) {
66
+ return state.modal?.kind === "content" ? 0.86 : 0.62;
67
+ }
56
68
  function drawBox(screen, x, y, width, height, boxTitle, style) {
57
69
  screen.clearRect({ x, y, width, height });
58
70
  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;
@@ -1,4 +1,6 @@
1
1
  {
2
+ "name": "toolcraft-design",
3
+ "version": "0.0.2",
2
4
  "private": true,
3
5
  "license": "MIT",
4
6
  "type": "module",
@@ -45,5 +47,8 @@
45
47
  "type": "git",
46
48
  "url": "git+https://github.com/poe-platform/poe-code.git",
47
49
  "directory": "packages/toolcraft-design"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
48
53
  }
49
54
  }