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.
- package/README.md +74 -4
- package/composition.json +1 -1
- package/dist/agent-defs.d.ts +1 -0
- package/dist/agent-defs.js +1 -0
- package/dist/agent-human-in-loop.d.ts +1 -0
- package/dist/agent-human-in-loop.js +1 -0
- package/dist/agent-mcp-config.d.ts +1 -0
- package/dist/agent-mcp-config.js +1 -0
- package/dist/auth-store.d.ts +1 -0
- package/dist/auth-store.js +1 -0
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +24 -65
- package/dist/composition.json +1 -1
- package/dist/config-mutations.d.ts +1 -0
- package/dist/config-mutations.js +1 -0
- package/dist/frontmatter.d.ts +1 -0
- package/dist/frontmatter.js +1 -0
- package/dist/human-in-loop/gate.d.ts +5 -1
- package/dist/human-in-loop/gate.js +10 -8
- package/dist/human-in-loop/runner.js +1 -28
- package/dist/mcp.d.ts +4 -1
- package/dist/mcp.js +8 -51
- package/dist/process-runner.d.ts +1 -0
- package/dist/process-runner.js +1 -0
- package/dist/runtime/io.d.ts +5 -0
- package/dist/runtime/io.js +50 -0
- package/dist/sdk.d.ts +12 -8
- package/dist/sdk.js +7 -52
- package/dist/task-list.d.ts +1 -0
- package/dist/task-list.js +1 -0
- package/dist/testing/fakes.d.ts +20 -0
- package/dist/testing/fakes.js +83 -0
- package/dist/testing/fixtures.d.ts +8 -0
- package/dist/testing/fixtures.js +248 -0
- package/dist/testing/harness.d.ts +76 -0
- package/dist/testing/harness.js +391 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/memory-fs.d.ts +11 -0
- package/dist/testing/memory-fs.js +61 -0
- package/dist/testing/parity.d.ts +25 -0
- package/dist/testing/parity.js +384 -0
- package/dist/testing/render-capture.d.ts +6 -0
- package/dist/testing/render-capture.js +54 -0
- package/dist/tiny-mcp-client.d.ts +1 -0
- package/dist/tiny-mcp-client.js +1 -0
- package/node_modules/@poe-code/agent-defs/package.json +2 -0
- package/node_modules/@poe-code/agent-human-in-loop/README.md +12 -2
- package/node_modules/@poe-code/agent-human-in-loop/package.json +2 -0
- package/node_modules/@poe-code/agent-mcp-config/README.md +7 -7
- package/node_modules/@poe-code/agent-mcp-config/package.json +2 -0
- package/node_modules/@poe-code/config-mutations/README.md +8 -8
- package/node_modules/@poe-code/config-mutations/package.json +2 -0
- package/node_modules/@poe-code/frontmatter/README.md +2 -2
- package/node_modules/@poe-code/frontmatter/package.json +2 -0
- package/node_modules/@poe-code/process-runner/README.md +1 -1
- package/node_modules/@poe-code/process-runner/package.json +2 -0
- package/node_modules/@poe-code/task-list/README.md +15 -3
- package/node_modules/@poe-code/task-list/package.json +2 -0
- package/node_modules/auth-store/README.md +15 -0
- package/node_modules/auth-store/package.json +2 -0
- package/node_modules/tiny-mcp-client/README.md +36 -36
- package/node_modules/tiny-mcp-client/package.json +2 -0
- package/node_modules/toolcraft-design/README.md +8 -4
- package/node_modules/toolcraft-design/dist/dashboard/terminal.js +48 -6
- package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/explorer/actions.js +1 -0
- package/node_modules/toolcraft-design/dist/explorer/events.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/explorer/reducer.js +52 -4
- package/node_modules/toolcraft-design/dist/explorer/render/list.js +47 -20
- package/node_modules/toolcraft-design/dist/explorer/render/modal.js +13 -1
- package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +1 -1
- package/node_modules/toolcraft-design/dist/explorer/runtime.js +3 -0
- package/node_modules/toolcraft-design/dist/explorer/state.d.ts +9 -0
- package/node_modules/toolcraft-design/package.json +5 -0
- 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.
|
|
@@ -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
|
|
30
|
-
|
|
31
|
-
| `StdioTransport`
|
|
32
|
-
| `HttpTransport`
|
|
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
|
-
##
|
|
70
|
+
## Configuration Options
|
|
71
71
|
|
|
72
72
|
### `McpClientOptions`
|
|
73
73
|
|
|
74
|
-
| Option
|
|
75
|
-
|
|
76
|
-
| `clientInfo`
|
|
77
|
-
| `capabilities`
|
|
78
|
-
| `onToolsChanged`, `onResourcesChanged`, `onPromptsChanged` | callbacks
|
|
79
|
-
| `onResourceUpdated`, `onLog`, `onProgress`
|
|
80
|
-
| `onSamplingRequest`, `onRootsList`
|
|
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
|
|
85
|
-
|
|
86
|
-
| `url`
|
|
87
|
-
| `headers`
|
|
88
|
-
| `fetch`
|
|
89
|
-
| `oauth`
|
|
90
|
-
| `oauthDiscoveryCache` | `OAuthDiscoveryCache`
|
|
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
|
|
95
|
-
|
|
96
|
-
| `command` | `string`
|
|
97
|
-
| `args`
|
|
98
|
-
| `cwd`
|
|
99
|
-
| `env`
|
|
100
|
-
| `spawn`
|
|
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
|
|
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.
|
|
@@ -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
|
-
|
|
121
|
+
````ts
|
|
122
122
|
import { renderMarkdown, renderMarkdownHtml } from "toolcraft-design";
|
|
123
123
|
|
|
124
|
-
const html = renderMarkdownHtml(
|
|
124
|
+
const html = renderMarkdownHtml('```ts\nconst value = "hello";\n```', {
|
|
125
125
|
syntaxHighlight: true
|
|
126
126
|
});
|
|
127
|
-
const terminal = renderMarkdown(
|
|
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 = (
|
|
122
|
-
const event
|
|
123
|
-
if (event !== undefined) {
|
|
121
|
+
const listener = (chunk) => {
|
|
122
|
+
for (const event of parseKeypressChunk(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))) {
|
|
124
123
|
handler(event);
|
|
125
124
|
}
|
|
126
125
|
};
|
|
127
126
|
keypressListeners.add(listener);
|
|
128
|
-
stdin.on("
|
|
127
|
+
stdin.on("data", listener);
|
|
129
128
|
return () => {
|
|
130
129
|
if (!keypressListeners.delete(listener)) {
|
|
131
130
|
return;
|
|
132
131
|
}
|
|
133
|
-
stdin.off("
|
|
132
|
+
stdin.off("data", listener);
|
|
134
133
|
};
|
|
135
134
|
}
|
|
136
135
|
function destroy() {
|
|
@@ -138,7 +137,7 @@ export function createTerminalDriver(opts) {
|
|
|
138
137
|
return;
|
|
139
138
|
}
|
|
140
139
|
for (const listener of keypressListeners) {
|
|
141
|
-
stdin.off("
|
|
140
|
+
stdin.off("data", listener);
|
|
142
141
|
}
|
|
143
142
|
keypressListeners.clear();
|
|
144
143
|
for (const listener of resizeListeners) {
|
|
@@ -173,6 +172,9 @@ export function parseKeypress(data) {
|
|
|
173
172
|
if (data.length === 0) {
|
|
174
173
|
return undefined;
|
|
175
174
|
}
|
|
175
|
+
if (data.length === 1 && data[0] === 0x1b) {
|
|
176
|
+
return { name: "escape", ctrl: false, meta: false, shift: false };
|
|
177
|
+
}
|
|
176
178
|
const stream = new PassThrough();
|
|
177
179
|
const stdin = stream;
|
|
178
180
|
let event;
|
|
@@ -184,6 +186,46 @@ export function parseKeypress(data) {
|
|
|
184
186
|
stream.destroy();
|
|
185
187
|
return event;
|
|
186
188
|
}
|
|
189
|
+
function parseKeypressChunk(data) {
|
|
190
|
+
const events = [];
|
|
191
|
+
const input = data.toString("utf8");
|
|
192
|
+
let index = 0;
|
|
193
|
+
while (index < input.length) {
|
|
194
|
+
const sequence = nextKeySequence(input, index);
|
|
195
|
+
if (sequence.length === 0) {
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
const event = parseKeypress(Buffer.from(sequence));
|
|
199
|
+
if (event !== undefined) {
|
|
200
|
+
events.push(event);
|
|
201
|
+
}
|
|
202
|
+
index += sequence.length;
|
|
203
|
+
}
|
|
204
|
+
return events;
|
|
205
|
+
}
|
|
206
|
+
function nextKeySequence(input, index) {
|
|
207
|
+
const character = input[index];
|
|
208
|
+
if (character === undefined) {
|
|
209
|
+
return "";
|
|
210
|
+
}
|
|
211
|
+
if (character !== "\u001b") {
|
|
212
|
+
return character;
|
|
213
|
+
}
|
|
214
|
+
const next = input[index + 1];
|
|
215
|
+
if (next === undefined) {
|
|
216
|
+
return character;
|
|
217
|
+
}
|
|
218
|
+
if (next !== "[") {
|
|
219
|
+
return input.slice(index, index + 2);
|
|
220
|
+
}
|
|
221
|
+
for (let cursor = index + 2; cursor < input.length; cursor += 1) {
|
|
222
|
+
const code = input.charCodeAt(cursor);
|
|
223
|
+
if ((code >= 0x40 && code <= 0x7e) || input[cursor] === "~") {
|
|
224
|
+
return input.slice(index, cursor + 1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return input.slice(index);
|
|
228
|
+
}
|
|
187
229
|
function toKeypressEvent(str, key) {
|
|
188
230
|
const controlCharacter = controlCharacterToKeypress(key?.sequence);
|
|
189
231
|
if (controlCharacter !== undefined) {
|
|
@@ -7,6 +7,10 @@ type ExplorerKeypressEvent = Extract<ExplorerEvent, {
|
|
|
7
7
|
export type ActionRuntimeHandles = {
|
|
8
8
|
refresh: () => Promise<void>;
|
|
9
9
|
suspendAnd: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
10
|
+
openModal: (content: {
|
|
11
|
+
title: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}) => void;
|
|
10
14
|
toast: (msg: string, tone?: Tone) => void;
|
|
11
15
|
confirm: (prompt: string) => Promise<boolean>;
|
|
12
16
|
exit: (after?: () => void | Promise<void>) => void;
|
|
@@ -20,6 +20,7 @@ export function buildActionContext(state, _action, source, runtimeHandles, rowsO
|
|
|
20
20
|
filter: state.filter,
|
|
21
21
|
refresh: runtimeHandles.refresh,
|
|
22
22
|
suspendAnd: runtimeHandles.suspendAnd,
|
|
23
|
+
openModal: runtimeHandles.openModal,
|
|
23
24
|
toast: runtimeHandles.toast,
|
|
24
25
|
confirm: runtimeHandles.confirm,
|
|
25
26
|
exit: runtimeHandles.exit
|
|
@@ -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:
|
|
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
|
-
|
|
31
|
+
const lines = buildDisplayLines(state);
|
|
32
|
+
const start = visibleStart(lines, bodyRect.height);
|
|
32
33
|
let y = 0;
|
|
33
|
-
for (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const row = state.rows[rowIndex];
|
|
38
|
-
if (!row) {
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
if (row.group && row.group !== lastGroup && y < bodyRect.height) {
|
|
42
|
-
const hash = `group:${row.group}`;
|
|
34
|
+
for (let lineIndex = start; lineIndex < lines.length && y < bodyRect.height; lineIndex += 1) {
|
|
35
|
+
const line = lines[lineIndex];
|
|
36
|
+
if (line.kind === "group") {
|
|
37
|
+
const hash = `group:${line.label}`;
|
|
43
38
|
if (cache.get(y) !== hash) {
|
|
44
|
-
writeLine(screen, bodyRect, y, formatGroupHeader(
|
|
39
|
+
writeLine(screen, bodyRect, y, formatGroupHeader(line.label, bodyRect), styles.muted);
|
|
45
40
|
cache.set(y, hash);
|
|
46
41
|
}
|
|
47
42
|
y += 1;
|
|
48
|
-
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (line.kind === "subtitle") {
|
|
46
|
+
const subtitleHash = `subtitle:${line.row.id}:${line.text}`;
|
|
47
|
+
if (cache.get(y) !== subtitleHash) {
|
|
48
|
+
writeLine(screen, bodyRect, y, ` ${line.text}`, styles.muted);
|
|
49
|
+
cache.set(y, subtitleHash);
|
|
50
|
+
}
|
|
51
|
+
y += 1;
|
|
52
|
+
continue;
|
|
49
53
|
}
|
|
54
|
+
const rowIndex = line.rowIndex;
|
|
50
55
|
if (y >= bodyRect.height) {
|
|
51
56
|
break;
|
|
52
57
|
}
|
|
58
|
+
const row = line.row;
|
|
53
59
|
const selected = state.multiSelect && state.selected.has(row.id);
|
|
54
60
|
const cursor = rowIndex === state.filtered[state.cursor];
|
|
55
61
|
const positions = state.matchPositions.get(rowIndex) ?? [];
|
|
@@ -64,15 +70,36 @@ export function renderList(state, screen, layout) {
|
|
|
64
70
|
cache.set(y, hash);
|
|
65
71
|
}
|
|
66
72
|
y += 1;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function buildDisplayLines(state) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
let lastGroup;
|
|
78
|
+
for (const rowIndex of state.filtered) {
|
|
79
|
+
const row = state.rows[rowIndex];
|
|
80
|
+
if (!row) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (row.group && row.group !== lastGroup) {
|
|
84
|
+
lines.push({ kind: "group", label: row.group });
|
|
85
|
+
lastGroup = row.group;
|
|
74
86
|
}
|
|
87
|
+
lines.push({ kind: "row", rowIndex, row, cursor: rowIndex === state.filtered[state.cursor] });
|
|
88
|
+
if (row.subtitle) {
|
|
89
|
+
lines.push({ kind: "subtitle", row, text: row.subtitle });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
94
|
+
function visibleStart(lines, height) {
|
|
95
|
+
if (height <= 0) {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
const cursorLine = lines.findIndex((line) => line.kind === "row" && line.cursor);
|
|
99
|
+
if (cursorLine < 0) {
|
|
100
|
+
return 0;
|
|
75
101
|
}
|
|
102
|
+
return Math.max(0, Math.min(cursorLine, cursorLine - height + 1));
|
|
76
103
|
}
|
|
77
104
|
function formatGroupHeader(label, rect) {
|
|
78
105
|
const text = ` ${label} `;
|
|
@@ -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 *
|
|
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
|
}
|