toolcraft-openapi 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.
package/README.md CHANGED
@@ -1,8 +1,7 @@
1
1
  # toolcraft-openapi
2
2
 
3
- tools for agents and humans
4
-
5
- Scaffold for OpenAPI-driven toolcraft clients.
3
+ Generate Toolcraft command clients from OpenAPI documents and share the same
4
+ surface across CLI, MCP, and SDK usage.
6
5
 
7
6
  ## Usage
8
7
 
@@ -18,20 +17,21 @@ const auth = bearerTokenAuth({
18
17
 
19
18
  ## Generator CLI
20
19
 
21
- `toolcraft-openapi-generate` reads an OpenAPI document from disk or a URL, writes generated
22
- command files.
20
+ `toolcraft-openapi-generate` reads an OpenAPI document from disk or a URL and
21
+ writes generated command files.
23
22
 
24
- - `--input <path-or-url>` OpenAPI document to read. Defaults to `openapi.json`.
25
- - `--output <dir>` directory for generated files. Defaults to `src/generated`.
26
- - `--check` exits non-zero when generated files would change.
27
- - `--diff` prints the generated file changes without writing them.
23
+ - `--input <path-or-url>` - OpenAPI document to read. Defaults to `openapi.json`.
24
+ - `--output <dir>` - directory for generated files. Defaults to `src/generated`.
25
+ - `--check` - exits non-zero when generated files would change.
26
+ - `--diff` - prints generated file changes without writing them.
28
27
 
29
- When `toolcraft.yml` is present next to the input file, the generator validates it, prints
30
- diagnostics, and uses it to shape generated command names for mapped resources.
28
+ When `toolcraft.yml` is present next to the input file, the generator validates
29
+ it, prints diagnostics, and uses it to shape generated command names for mapped
30
+ resources.
31
31
 
32
- The default generated integration point is `src/generated/client.ts`. Application code should
33
- provide deployment-specific configuration and let the generated client own the full OpenAPI-derived
34
- command surface:
32
+ The default generated integration point is `src/generated/client.ts`.
33
+ Application code provides deployment-specific configuration and lets the
34
+ generated client own the OpenAPI-derived command surface:
35
35
 
36
36
  ```ts
37
37
  import { defineGeneratedClient } from "./generated/client.js";
@@ -47,7 +47,7 @@ export const client = defineGeneratedClient({
47
47
  });
48
48
  ```
49
49
 
50
- Generated lower-level group and operation exports remain available for consumers that intentionally
50
+ Generated lower-level group and operation exports remain available when callers
51
51
  want a curated command surface with `defineClient()`.
52
52
 
53
53
  ### CI drift check
@@ -73,11 +73,11 @@ toolcraft-openapi-generate --check
73
73
  - `CommandContributor`
74
74
  - `AuthProvider`
75
75
 
76
- ## Environment variables
76
+ ## Environment Variables
77
77
 
78
- - `<envVar passed to bearerTokenAuth>` bearer token override; wins over stored credentials.
79
- - `AUTH_BACKEND` forwarded to `auth-store` to select `file` or `keychain` storage.
80
- - `TOOLCRAFT_OPENAPI_ENV` selects a configured OpenAPI environment when using
78
+ - `<envVar passed to bearerTokenAuth>` - bearer token override; wins over stored credentials.
79
+ - `AUTH_BACKEND` - forwarded to `auth-store` to select `file` or `keychain` storage.
80
+ - `TOOLCRAFT_OPENAPI_ENV` - selects a configured OpenAPI environment when using
81
81
  `resolveOpenApiBaseUrl`.
82
82
 
83
83
  ## Configuration
@@ -110,15 +110,15 @@ unspecified_endpoints:
110
110
 
111
111
  Supported keys:
112
112
 
113
- - `edition` required when `toolcraft.yml` exists. Currently `2026-05-16`.
114
- - `environments` named base URLs for `resolveOpenApiBaseUrl`.
115
- - `client_settings.idempotency_header` header used for generated idempotency keys.
116
- - `client_settings.auth.*.env` declarative auth environment metadata.
117
- - `pagination` named request/response field mappings. Schemes are validated today; iterator
113
+ - `edition` - required when `toolcraft.yml` exists. Currently `2026-05-16`.
114
+ - `environments` - named base URLs for `resolveOpenApiBaseUrl`.
115
+ - `client_settings.idempotency_header` - header used for generated idempotency keys.
116
+ - `client_settings.auth.*.env` - declarative auth environment metadata.
117
+ - `pagination` - named request/response field mappings. Schemes are validated today; iterator
118
118
  generation is not implemented yet.
119
- - `resources` resource/method mapping. Mapped methods shape generated names and file paths.
120
- - `readme.examples` examples emitted into generated command help and MCP descriptions.
121
- - `unspecified_endpoints` endpoints intentionally omitted from `resources`.
119
+ - `resources` - resource/method mapping. Mapped methods shape generated names and file paths.
120
+ - `readme.examples` - examples emitted into generated command help and MCP descriptions.
121
+ - `unspecified_endpoints` - endpoints intentionally omitted from `resources`.
122
122
 
123
123
  Mapped idempotent methods with `client_settings.idempotency_header` get an optional
124
124
  `idempotencyKey` param across CLI, MCP, and SDK surfaces. Mapped methods also get a CLI/SDK
@@ -127,20 +127,20 @@ Mapped idempotent methods with `client_settings.idempotency_header` get an optio
127
127
 
128
128
  Diagnostics use stable codes:
129
129
 
130
- - `TOOLCRAFT_OPENAPI_001` endpoint is not mapped or listed in `unspecified_endpoints`.
131
- - `TOOLCRAFT_OPENAPI_002` duplicate configured method path.
132
- - `TOOLCRAFT_OPENAPI_003` unknown pagination scheme.
133
- - `TOOLCRAFT_OPENAPI_004` reserved for spec drift.
134
- - `TOOLCRAFT_OPENAPI_005` reserved method name.
135
- - `TOOLCRAFT_OPENAPI_006` missing or unsupported edition.
136
- - `TOOLCRAFT_OPENAPI_007` invalid config shape.
130
+ - `TOOLCRAFT_OPENAPI_001` - endpoint is not mapped or listed in `unspecified_endpoints`.
131
+ - `TOOLCRAFT_OPENAPI_002` - duplicate configured method path.
132
+ - `TOOLCRAFT_OPENAPI_003` - unknown pagination scheme.
133
+ - `TOOLCRAFT_OPENAPI_004` - reserved for spec drift.
134
+ - `TOOLCRAFT_OPENAPI_005` - reserved method name.
135
+ - `TOOLCRAFT_OPENAPI_006` - missing or unsupported edition.
136
+ - `TOOLCRAFT_OPENAPI_007` - invalid config shape.
137
137
 
138
138
  ### `bearerTokenAuth(opts)`
139
139
 
140
- - `serviceName: string` auth-store service name and file-store key.
141
- - `envVar: string` environment variable checked before stored credentials.
142
- - `whoamiPath?: string` optional authenticated endpoint used by `login` and `status`.
143
- - `commandPrefix?: string` CLI auth group name. Defaults to `auth`.
140
+ - `serviceName: string` - auth-store service name and file-store key.
141
+ - `envVar: string` - environment variable checked before stored credentials.
142
+ - `whoamiPath?: string` - optional authenticated endpoint used by `login` and `status`.
143
+ - `commandPrefix?: string` - CLI auth group name. Defaults to `auth`.
144
144
 
145
145
  ## Auth commands
146
146
 
@@ -18,7 +18,7 @@
18
18
  },
19
19
  {
20
20
  "name": "toolcraft-openapi",
21
- "version": "0.0.93",
21
+ "version": "0.0.95",
22
22
  "license": "MIT"
23
23
  }
24
24
  ]
@@ -26,10 +26,10 @@ Duplicate mapping keys use YAML's last-wins behavior by default, matching the
26
26
  legacy frontmatter readers. Callers that need strict YAML mappings can pass
27
27
  `{ uniqueKeys: true }` to `parseFrontmatter` or `parseFrontmatterDocument`.
28
28
 
29
- ## Environment
29
+ ## Environment Variables
30
30
 
31
31
  This package reads no environment variables.
32
32
 
33
- ## Config
33
+ ## Configuration Options
34
34
 
35
35
  This package has no configuration options.
@@ -1,4 +1,6 @@
1
1
  {
2
+ "name": "@poe-code/frontmatter",
3
+ "version": "0.0.1",
2
4
  "private": true,
3
5
  "license": "MIT",
4
6
  "type": "module",
@@ -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",
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-openapi",
3
- "version": "0.0.93",
3
+ "version": "0.0.95",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,7 +30,7 @@
30
30
  "toolcraft-openapi-generate": "dist/bin/generate.js"
31
31
  },
32
32
  "dependencies": {
33
- "toolcraft": "0.0.93",
33
+ "toolcraft": "0.0.95",
34
34
  "auth-store": "^0.0.1",
35
35
  "fast-string-width": "^3.0.2",
36
36
  "fast-wrap-ansi": "^0.2.0",