@dreb/coding-agent 2.51.0 → 2.53.0
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 +8 -6
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +11 -6
- package/dist/cli/args.js.map +1 -1
- package/dist/core/extensions/types.d.ts +31 -11
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/slash-commands.d.ts +14 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +28 -3
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/ask-user.d.ts +28 -20
- package/dist/core/tools/ask-user.d.ts.map +1 -1
- package/dist/core/tools/ask-user.js +130 -106
- package/dist/core/tools/ask-user.js.map +1 -1
- package/dist/core/tools/index.d.ts +9 -7
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/modes/interactive/components/ask-wizard.d.ts +83 -0
- package/dist/modes/interactive/components/ask-wizard.d.ts.map +1 -0
- package/dist/modes/interactive/components/ask-wizard.js +464 -0
- package/dist/modes/interactive/components/ask-wizard.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +4 -2
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +7 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +11 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +15 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +3 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +230 -47
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +66 -11
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/dashboard.md +1 -3
- package/docs/extensions.md +39 -22
- package/docs/rpc.md +84 -25
- package/examples/extensions/rpc-demo.ts +29 -0
- package/examples/rpc-extension-ui.ts +162 -6
- package/package.json +1 -1
- package/dist/modes/interactive/components/ask-user.d.ts +0 -59
- package/dist/modes/interactive/components/ask-user.d.ts.map +0 -1
- package/dist/modes/interactive/components/ask-user.js +0 -240
- package/dist/modes/interactive/components/ask-user.js.map +0 -1
package/docs/extensions.md
CHANGED
|
@@ -162,24 +162,33 @@ export default function (dreb: ExtensionAPI) {
|
|
|
162
162
|
ctx.ui.setStatus("my-ext", "Processing..."); // Footer status
|
|
163
163
|
ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor (default)
|
|
164
164
|
|
|
165
|
-
// ctx.ui.ask —
|
|
166
|
-
// rendered natively in the TUI and Dashboard (and over RPC).
|
|
167
|
-
// { selected: string[], customText?: string
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
165
|
+
// ctx.ui.ask — one or more rich clarifying questions asked together as a
|
|
166
|
+
// single wizard, rendered natively in the TUI and Dashboard (and over RPC).
|
|
167
|
+
// Resolves to { answers: Array<{ selected: string[], customText?: string,
|
|
168
|
+
// skipped?: boolean }> } with one answer per question, in order. Dismissing
|
|
169
|
+
// or timing out an ask stops the current agent turn and resolves undefined.
|
|
170
|
+
// This is the same primitive that powers the built-in `ask_user` tool.
|
|
171
|
+
const result = await ctx.ui.ask(
|
|
171
172
|
{
|
|
172
173
|
title: "Choose a database",
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
questions: [
|
|
175
|
+
{
|
|
176
|
+
question: "Which persistence strategy should I use?",
|
|
177
|
+
options: ["SQLite", "PostgreSQL", "Keep the JSON file"],
|
|
178
|
+
allowFreeText: true, // default; offers a "type your own answer" field
|
|
179
|
+
multiSelect: false, // true → checkboxes, combined with any free text
|
|
180
|
+
multiline: false, // true → multi-line free-text area
|
|
181
|
+
},
|
|
182
|
+
{ question: "Any migration constraints I should know about?", multiline: true },
|
|
183
|
+
],
|
|
178
184
|
},
|
|
179
185
|
{ signal, timeout: 60000 }, // both optional; absent user never deadlocks
|
|
180
186
|
);
|
|
181
|
-
if (!
|
|
187
|
+
if (!result) {
|
|
182
188
|
// The ask was dismissed or timed out; the current agent turn is stopping.
|
|
189
|
+
} else {
|
|
190
|
+
const [dbAnswer] = result.answers; // one entry per question, in order
|
|
191
|
+
// dbAnswer.selected / dbAnswer.customText / dbAnswer.skipped
|
|
183
192
|
}
|
|
184
193
|
});
|
|
185
194
|
|
|
@@ -1167,8 +1176,9 @@ dreb.registerCommand("deploy", {
|
|
|
1167
1176
|
|
|
1168
1177
|
### dreb.getCommands()
|
|
1169
1178
|
|
|
1170
|
-
Get the slash commands available for invocation via `prompt` in the current session. Includes extension commands, prompt templates, and skill commands.
|
|
1171
|
-
|
|
1179
|
+
Get the slash commands available for invocation via `prompt` in the current session. Includes extension commands, prompt templates, and skill commands, in that order.
|
|
1180
|
+
|
|
1181
|
+
This extension-SDK contract is intentionally narrower than RPC `get_commands`, which also exposes client-handled built-ins for alternate frontends.
|
|
1172
1182
|
|
|
1173
1183
|
```typescript
|
|
1174
1184
|
const commands = dreb.getCommands();
|
|
@@ -1195,8 +1205,7 @@ Each entry has this shape:
|
|
|
1195
1205
|
|
|
1196
1206
|
Use `sourceInfo` as the canonical provenance field. Do not infer ownership from command names or from ad hoc path parsing.
|
|
1197
1207
|
|
|
1198
|
-
Built-in
|
|
1199
|
-
mode and would not execute if sent via `prompt`.
|
|
1208
|
+
Built-in commands (like `/model` and `/settings`) are not included here because they are not prompt-invokable and have no resource provenance. RPC clients can discover them separately through `get_commands`; sending one through an RPC prompt command is rejected rather than passed to the model.
|
|
1200
1209
|
|
|
1201
1210
|
### dreb.registerMessageRenderer(customType, renderer)
|
|
1202
1211
|
|
|
@@ -1804,14 +1813,21 @@ const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
|
|
|
1804
1813
|
// Text input
|
|
1805
1814
|
const name = await ctx.ui.input("Name:", "placeholder");
|
|
1806
1815
|
|
|
1807
|
-
// Rich question:
|
|
1808
|
-
//
|
|
1809
|
-
|
|
1816
|
+
// Rich question wizard: one or more questions asked together, question text
|
|
1817
|
+
// supports Markdown and options can be combined with free text. Resolves to
|
|
1818
|
+
// { answers: [...] } (one per question, in order); dismissal stops the current
|
|
1819
|
+
// agent turn and returns undefined.
|
|
1820
|
+
const result = await ctx.ui.ask({
|
|
1810
1821
|
title: "Choose a database",
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1822
|
+
questions: [
|
|
1823
|
+
{
|
|
1824
|
+
question: "Which persistence strategy should I use?",
|
|
1825
|
+
options: ["SQLite", "PostgreSQL"],
|
|
1826
|
+
allowFreeText: true,
|
|
1827
|
+
},
|
|
1828
|
+
],
|
|
1814
1829
|
});
|
|
1830
|
+
const answer = result?.answers[0];
|
|
1815
1831
|
|
|
1816
1832
|
// Multi-line editor
|
|
1817
1833
|
const text = await ctx.ui.editor("Edit:", "prefilled text");
|
|
@@ -2156,6 +2172,7 @@ All examples in [examples/extensions/](../examples/extensions/).
|
|
|
2156
2172
|
| `overlay-qa-tests.ts` | Comprehensive overlay tests | `ui.custom`, all overlay options |
|
|
2157
2173
|
| `notify.ts` | Simple notifications | `ui.notify` |
|
|
2158
2174
|
| `timed-confirm.ts` | Dialogs with timeout | `ui.confirm` with timeout/signal |
|
|
2175
|
+
| `rpc-demo.ts` | Complete RPC extension-UI demo, including the batch question wizard | `ui.select`, `ui.confirm`, `ui.input`, `ui.editor`, `ui.ask`, status/widget/title methods |
|
|
2159
2176
|
| `mac-system-theme.ts` | Auto-switch theme | `setTheme`, `exec` |
|
|
2160
2177
|
| **Complex Extensions** |||
|
|
2161
2178
|
| `plan-mode/` | Full plan mode implementation | All event types, `registerCommand`, `registerShortcut`, `registerFlag`, `setStatus`, `setWidget`, `sendMessage`, `setActiveTools` |
|
package/docs/rpc.md
CHANGED
|
@@ -82,6 +82,8 @@ If the agent is streaming and no `streamingBehavior` is specified, the command r
|
|
|
82
82
|
|
|
83
83
|
**Input expansion**: Skill commands (`/skill:name`) and prompt templates (`/template`) are expanded before sending/queueing.
|
|
84
84
|
|
|
85
|
+
**Built-in commands are client actions, not model prompts.** Registered built-ins such as `/fork` are rejected by `prompt`, `steer`, and `follow_up` with `success: false`; the message is not queued or sent to the model. Discover them through [`get_commands`](#get_commands), then invoke the corresponding RPC operation or client UI. Matching uses the complete first slash token: `/fork` and `/fork anything` are recognized, while `/forklift` and unknown slash commands remain ordinary prompt text.
|
|
86
|
+
|
|
85
87
|
Response:
|
|
86
88
|
```json
|
|
87
89
|
{"id": "req-1", "type": "response", "command": "prompt", "success": true}
|
|
@@ -169,6 +171,34 @@ If an extension cancelled:
|
|
|
169
171
|
{"type": "response", "command": "new_session", "success": true, "data": {"cancelled": true}}
|
|
170
172
|
```
|
|
171
173
|
|
|
174
|
+
#### reload
|
|
175
|
+
|
|
176
|
+
Reload session resources (extensions, skills, prompt templates, context, and themes) using the same core reload operation as the interactive command.
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{"type": "reload"}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Response:
|
|
183
|
+
```json
|
|
184
|
+
{"type": "response", "command": "reload", "success": true}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### dream
|
|
188
|
+
|
|
189
|
+
Run memory consolidation or manage its archive path. `args` is the text after `/dream`: omit it to run consolidation, use `"backup"` to read the archive path, or `"backup <path>"` to set it.
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{"type": "dream", "args": "backup"}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Response:
|
|
196
|
+
```json
|
|
197
|
+
{"type": "response", "command": "dream", "success": true, "data": {"message": "Dream backup path: /home/user/.dreb/memory-archive"}}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
A consolidation run performs and verifies the backup before prompting the agent. Backup, lock, path-validation, settings-write, or consolidation failures are explicit RPC errors.
|
|
201
|
+
|
|
172
202
|
### State
|
|
173
203
|
|
|
174
204
|
#### get_state
|
|
@@ -800,6 +830,19 @@ Response:
|
|
|
800
830
|
}
|
|
801
831
|
```
|
|
802
832
|
|
|
833
|
+
#### import_jsonl
|
|
834
|
+
|
|
835
|
+
Import a JSONL session into the current runtime. This can be cancelled by a session-switch extension hook.
|
|
836
|
+
|
|
837
|
+
```json
|
|
838
|
+
{"type": "import_jsonl", "inputPath": "/tmp/session.jsonl"}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
Response:
|
|
842
|
+
```json
|
|
843
|
+
{"type": "response", "command": "import_jsonl", "success": true, "data": {"cancelled": false}}
|
|
844
|
+
```
|
|
845
|
+
|
|
803
846
|
#### switch_session
|
|
804
847
|
|
|
805
848
|
Load a different session file. Can be cancelled by a `session_before_switch` extension event handler.
|
|
@@ -945,7 +988,7 @@ The current session name is available via `get_state` in the `sessionName` field
|
|
|
945
988
|
|
|
946
989
|
#### get_commands
|
|
947
990
|
|
|
948
|
-
|
|
991
|
+
Discover extension commands, prompt templates, skills, and registered built-ins. Resource commands can be invoked through `prompt`; built-ins have `source: "builtin"` and require client-side handling through the corresponding RPC operation or UI. Sending a recognized built-in through `prompt`, `steer`, or `follow_up` is rejected fail-closed.
|
|
949
992
|
|
|
950
993
|
```json
|
|
951
994
|
{"type": "get_commands"}
|
|
@@ -959,28 +1002,26 @@ Response:
|
|
|
959
1002
|
"success": true,
|
|
960
1003
|
"data": {
|
|
961
1004
|
"commands": [
|
|
962
|
-
{"name": "session-name", "description": "Set or clear session name", "source": "extension", "path": "/home/user/.dreb/agent/extensions/session.ts"},
|
|
963
|
-
{"name": "fix-tests", "description": "Fix failing tests", "source": "prompt", "
|
|
964
|
-
{"name": "skill:brave-search", "description": "Web search via Brave API", "source": "skill", "
|
|
1005
|
+
{"name": "session-name", "description": "Set or clear session name", "source": "extension", "sourceInfo": {"path": "/home/user/.dreb/agent/extensions/session.ts"}},
|
|
1006
|
+
{"name": "fix-tests", "description": "Fix failing tests", "source": "prompt", "sourceInfo": {"path": "/home/user/myproject/.dreb/agent/prompts/fix-tests.md"}},
|
|
1007
|
+
{"name": "skill:brave-search", "description": "Web search via Brave API", "source": "skill", "sourceInfo": {"path": "/home/user/.dreb/agent/skills/brave-search/SKILL.md"}},
|
|
1008
|
+
{"name": "fork", "description": "Create a new fork from a previous message", "source": "builtin", "dashboard": true},
|
|
1009
|
+
{"name": "copy", "description": "Copy last agent message to clipboard", "source": "builtin", "dashboard": false}
|
|
965
1010
|
]
|
|
966
1011
|
}
|
|
967
1012
|
}
|
|
968
1013
|
```
|
|
969
1014
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
- `
|
|
973
|
-
- `
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
- `"skill"`: Loaded from a skill directory (name is prefixed with `skill:`)
|
|
977
|
-
- `location`: Where it was loaded from (optional, not present for extensions):
|
|
978
|
-
- `"user"`: User-level (`~/.dreb/agent/`)
|
|
979
|
-
- `"project"`: Project-level (`./.dreb/agent/`)
|
|
980
|
-
- `"path"`: Explicit path via CLI or settings
|
|
981
|
-
- `path`: Absolute file path to the command source (optional)
|
|
1015
|
+
Every command has `name`, optional `description`, and a `source`:
|
|
1016
|
+
|
|
1017
|
+
- `"extension"`: registered via `dreb.registerCommand()`; prompt-invokable and includes `sourceInfo`.
|
|
1018
|
+
- `"prompt"`: loaded prompt template; prompt-invokable and includes `sourceInfo`.
|
|
1019
|
+
- `"skill"`: loaded skill (name prefixed with `skill:`); prompt-invokable and includes `sourceInfo`.
|
|
1020
|
+
- `"builtin"`: core slash command; not prompt-invokable, has no file `sourceInfo`, and includes `dashboard`. A false value means dashboard clients should omit it from autocomplete while still intercepting typed use with terminal-only guidance.
|
|
982
1021
|
|
|
983
|
-
|
|
1022
|
+
Names are deduplicated, with a built-in taking precedence over a colliding resource command. Future registry entries appear automatically. Hidden development commands are intentionally not registered and do not appear.
|
|
1023
|
+
|
|
1024
|
+
The extension SDK's `dreb.getCommands()` contract is unchanged: it continues to return only commands invokable via `prompt`, with required resource provenance. Built-ins are added only to this RPC discovery surface.
|
|
984
1025
|
|
|
985
1026
|
### Session Listing
|
|
986
1027
|
|
|
@@ -2015,7 +2056,7 @@ Expected response: `extension_ui_response` with `value` (the edited text) or `ca
|
|
|
2015
2056
|
|
|
2016
2057
|
#### ask
|
|
2017
2058
|
|
|
2018
|
-
Ask the user
|
|
2059
|
+
Ask the user one or more rich clarifying questions together as a single wizard. Each question has Markdown-formatted question text, optional single- or multi-select options, and an optional free-text field. This powers the built-in `ask_user` tool. The request carries a `questions` array (1-10 entries); per question, `options` (2-4 nonblank strings) is optional and `allowFreeText` (default `true`), `multiSelect`, and `multiline` are optional booleans. An overall `title` defaults to `"Question"`.
|
|
2019
2060
|
|
|
2020
2061
|
```json
|
|
2021
2062
|
{
|
|
@@ -2023,11 +2064,20 @@ Ask the user a rich clarifying question with Markdown-formatted question text, o
|
|
|
2023
2064
|
"id": "uuid-5",
|
|
2024
2065
|
"method": "ask",
|
|
2025
2066
|
"title": "Choose a database",
|
|
2026
|
-
"
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2067
|
+
"questions": [
|
|
2068
|
+
{
|
|
2069
|
+
"question": "Which persistence strategy should I use?",
|
|
2070
|
+
"title": "Storage",
|
|
2071
|
+
"options": ["SQLite", "PostgreSQL", "Keep the JSON file"],
|
|
2072
|
+
"allowFreeText": true,
|
|
2073
|
+
"multiSelect": false,
|
|
2074
|
+
"multiline": false
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
"question": "Any migration constraints I should know about?",
|
|
2078
|
+
"multiline": true
|
|
2079
|
+
}
|
|
2080
|
+
],
|
|
2031
2081
|
"timeout": 60000,
|
|
2032
2082
|
"expiresAt": 1785434460000
|
|
2033
2083
|
}
|
|
@@ -2035,12 +2085,21 @@ Ask the user a rich clarifying question with Markdown-formatted question text, o
|
|
|
2035
2085
|
|
|
2036
2086
|
`timeout` is the original duration in milliseconds. `expiresAt` is the corresponding absolute Unix timestamp in milliseconds; Dashboard clients should use it for the visible countdown so reload, resync, or drill-in recovery does not restart the full duration.
|
|
2037
2087
|
|
|
2038
|
-
Expected response: `extension_ui_response` with `selected` (an array of strings) and optional string `customText` (the typed answer)
|
|
2088
|
+
Expected response: `extension_ui_response` with `answers` — an array with one entry per question, in the same order. Each answer has `selected` (an array of strings) and optional string `customText` (the typed answer); an answer with an empty `selected` and no nonblank `customText` is treated as skipped (an explicit `skipped: true` is also honored). Answering submits the batch even when some questions are skipped. Sending `cancelled: true` stops the current agent turn rather than continuing; a timeout has the same stop semantics. A missing/non-array `answers`, or a malformed `selected`/`customText`, is rejected as a protocol failure.
|
|
2039
2089
|
|
|
2040
2090
|
```json
|
|
2041
|
-
{
|
|
2091
|
+
{
|
|
2092
|
+
"type": "extension_ui_response",
|
|
2093
|
+
"id": "uuid-5",
|
|
2094
|
+
"answers": [
|
|
2095
|
+
{ "selected": ["SQLite"], "customText": "with WAL enabled" },
|
|
2096
|
+
{ "selected": [], "skipped": true }
|
|
2097
|
+
]
|
|
2098
|
+
}
|
|
2042
2099
|
```
|
|
2043
2100
|
|
|
2101
|
+
`ask` requests are single-flight per RPC runtime. If parallel tool execution starts several calls concurrently, RPC emits only the first request and queues the rest in FIFO order. The next request is emitted only after the active request settles; a queued call that is aborted before it starts settles without emitting. Hosts therefore need to render at most one pending `ask` wizard at a time.
|
|
2102
|
+
|
|
2044
2103
|
#### notify
|
|
2045
2104
|
|
|
2046
2105
|
Display a notification. Fire-and-forget, no response expected.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - confirm() - on session_before_switch
|
|
11
11
|
* - input() - via /rpc-input command
|
|
12
12
|
* - editor() - via /rpc-editor command
|
|
13
|
+
* - ask() - via /rpc-ask command
|
|
13
14
|
* - notify() - after each dialog completes
|
|
14
15
|
* - setStatus() - on turn_start/turn_end
|
|
15
16
|
* - setWidget() - on session_start
|
|
@@ -112,6 +113,34 @@ export default function (dreb: ExtensionAPI) {
|
|
|
112
113
|
},
|
|
113
114
|
});
|
|
114
115
|
|
|
116
|
+
// -- batch ask wizard via command --
|
|
117
|
+
|
|
118
|
+
dreb.registerCommand("rpc-ask", {
|
|
119
|
+
description: "Open a batch question wizard (demonstrates ctx.ui.ask in RPC)",
|
|
120
|
+
handler: async (_args, ctx) => {
|
|
121
|
+
const result = await ctx.ui.ask({
|
|
122
|
+
title: "Configure the RPC demo",
|
|
123
|
+
questions: [
|
|
124
|
+
{
|
|
125
|
+
question: "Which **transport checks** should run?",
|
|
126
|
+
title: "Checks",
|
|
127
|
+
options: ["Protocol", "Reconnect", "Timeout"],
|
|
128
|
+
multiSelect: true,
|
|
129
|
+
allowFreeText: true,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
question: "Anything else the RPC host should display?",
|
|
133
|
+
title: "Notes",
|
|
134
|
+
multiline: true,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
if (!result) return;
|
|
139
|
+
const answered = result.answers.filter((answer) => !answer.skipped).length;
|
|
140
|
+
ctx.ui.notify(`Submitted ${answered} of ${result.answers.length} answers`, "info");
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
115
144
|
// -- setEditorText via command --
|
|
116
145
|
|
|
117
146
|
dreb.registerCommand("rpc-prefill", {
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A lightweight TUI chat client that spawns the agent in RPC mode.
|
|
5
5
|
* Demonstrates how to build a custom UI on top of the RPC protocol,
|
|
6
|
-
* including handling extension UI requests (select, confirm, input, editor).
|
|
6
|
+
* including handling extension UI requests (select, confirm, input, editor, ask).
|
|
7
7
|
*
|
|
8
8
|
* Usage: npx tsx examples/rpc-extension-ui.ts
|
|
9
9
|
*
|
|
10
10
|
* Slash commands:
|
|
11
|
-
* /select
|
|
12
|
-
* /confirm
|
|
13
|
-
* /input
|
|
14
|
-
* /editor
|
|
11
|
+
* /select - demo select dialog
|
|
12
|
+
* /confirm - demo confirm dialog
|
|
13
|
+
* /input - demo input dialog
|
|
14
|
+
* /editor - demo editor dialog
|
|
15
|
+
* /rpc-ask - demo batch ask wizard from the companion extension
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import { spawn } from "node:child_process";
|
|
@@ -39,12 +40,28 @@ const RESET = "\x1b[0m";
|
|
|
39
40
|
// Extension UI request type (subset of rpc-types.ts)
|
|
40
41
|
// ============================================================================
|
|
41
42
|
|
|
43
|
+
interface AskQuestion {
|
|
44
|
+
question: string;
|
|
45
|
+
title?: string;
|
|
46
|
+
options?: string[];
|
|
47
|
+
allowFreeText?: boolean;
|
|
48
|
+
multiSelect?: boolean;
|
|
49
|
+
multiline?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface AskAnswer {
|
|
53
|
+
selected: string[];
|
|
54
|
+
customText?: string;
|
|
55
|
+
skipped?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
interface ExtensionUIRequest {
|
|
43
59
|
type: "extension_ui_request";
|
|
44
60
|
id: string;
|
|
45
61
|
method: string;
|
|
46
62
|
title?: string;
|
|
47
63
|
options?: string[];
|
|
64
|
+
questions?: AskQuestion[];
|
|
48
65
|
message?: string;
|
|
49
66
|
placeholder?: string;
|
|
50
67
|
prefill?: string;
|
|
@@ -239,6 +256,98 @@ class InputDialog implements Component {
|
|
|
239
256
|
}
|
|
240
257
|
}
|
|
241
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Compact batch-question wizard for the RPC example. Each page accepts option
|
|
261
|
+
* numbers and optional free text separated by commas, then sends one ordered
|
|
262
|
+
* answers[] response after the final question.
|
|
263
|
+
*/
|
|
264
|
+
class AskDialog implements Component {
|
|
265
|
+
private readonly input = new Input();
|
|
266
|
+
private readonly answers: AskAnswer[] = [];
|
|
267
|
+
private questionIndex = 0;
|
|
268
|
+
onSubmit?: (answers: AskAnswer[]) => void;
|
|
269
|
+
onCancel?: () => void;
|
|
270
|
+
onCtrlD?: () => void;
|
|
271
|
+
|
|
272
|
+
constructor(
|
|
273
|
+
private readonly title: string,
|
|
274
|
+
private readonly questions: AskQuestion[],
|
|
275
|
+
) {
|
|
276
|
+
this.input.onSubmit = (value) => this.acceptAnswer(value);
|
|
277
|
+
this.input.onEscape = () => this.onCancel?.();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private acceptAnswer(value: string): void {
|
|
281
|
+
const question = this.questions[this.questionIndex];
|
|
282
|
+
if (!question) return;
|
|
283
|
+
const raw = value.trim();
|
|
284
|
+
const options = question.options ?? [];
|
|
285
|
+
const selected: string[] = [];
|
|
286
|
+
const customParts: string[] = [];
|
|
287
|
+
|
|
288
|
+
if (options.length === 0) {
|
|
289
|
+
if (raw) customParts.push(raw);
|
|
290
|
+
} else if (raw) {
|
|
291
|
+
for (const token of raw
|
|
292
|
+
.split(",")
|
|
293
|
+
.map((part) => part.trim())
|
|
294
|
+
.filter(Boolean)) {
|
|
295
|
+
const numericIndex = /^\d+$/.test(token) ? Number(token) - 1 : -1;
|
|
296
|
+
const matched =
|
|
297
|
+
options[numericIndex] ?? options.find((option) => option.toLowerCase() === token.toLowerCase());
|
|
298
|
+
if (matched && (question.multiSelect || selected.length === 0)) {
|
|
299
|
+
if (!selected.includes(matched)) selected.push(matched);
|
|
300
|
+
} else if (!matched && question.allowFreeText !== false) {
|
|
301
|
+
customParts.push(token);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const customText = customParts.join(", ").trim() || undefined;
|
|
307
|
+
this.answers.push(selected.length > 0 || customText ? { selected, customText } : { selected: [], skipped: true });
|
|
308
|
+
this.questionIndex++;
|
|
309
|
+
if (this.questionIndex >= this.questions.length) {
|
|
310
|
+
this.onSubmit?.(this.answers);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.input.setValue("");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
handleInput(data: string): void {
|
|
317
|
+
if (matchesKey(data, "ctrl+d")) {
|
|
318
|
+
this.onCtrlD?.();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
this.input.handleInput(data);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
invalidate(): void {
|
|
325
|
+
this.input.invalidate();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
render(width: number): string[] {
|
|
329
|
+
const question = this.questions[this.questionIndex];
|
|
330
|
+
if (!question) return [`${RED}Invalid empty ask request${RESET}`];
|
|
331
|
+
const options = question.options ?? [];
|
|
332
|
+
const optionLines = options.map((option, index) => ` ${index + 1}. ${option}`);
|
|
333
|
+
const answerHint =
|
|
334
|
+
options.length === 0
|
|
335
|
+
? "Type an answer"
|
|
336
|
+
: question.multiSelect
|
|
337
|
+
? "Type option numbers separated by commas"
|
|
338
|
+
: "Type one option number";
|
|
339
|
+
const customHint = options.length > 0 && question.allowFreeText !== false ? ", plus optional text" : "";
|
|
340
|
+
return [
|
|
341
|
+
`${MAGENTA}${BOLD}${this.title} — ${this.questionIndex + 1}/${this.questions.length}${RESET}`,
|
|
342
|
+
`${BOLD}${question.title ?? question.question}${RESET}`,
|
|
343
|
+
...(question.title ? [question.question] : []),
|
|
344
|
+
...optionLines,
|
|
345
|
+
...this.input.render(width),
|
|
346
|
+
`${DIM}${answerHint}${customHint}. Enter to continue, Esc to cancel.${RESET}`,
|
|
347
|
+
];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
242
351
|
// ============================================================================
|
|
243
352
|
// Main
|
|
244
353
|
// ============================================================================
|
|
@@ -300,6 +409,7 @@ async function main() {
|
|
|
300
409
|
// These helpers swap between them.
|
|
301
410
|
|
|
302
411
|
let activeDialog: Component | null = null;
|
|
412
|
+
let activeDialogId: string | null = null;
|
|
303
413
|
|
|
304
414
|
function setBottomComponent(component: Component): void {
|
|
305
415
|
root.clear();
|
|
@@ -312,6 +422,7 @@ async function main() {
|
|
|
312
422
|
|
|
313
423
|
function showPrompt(): void {
|
|
314
424
|
activeDialog = null;
|
|
425
|
+
activeDialogId = null;
|
|
315
426
|
setBottomComponent(promptInput);
|
|
316
427
|
tui.setFocus(promptInput.input);
|
|
317
428
|
}
|
|
@@ -374,8 +485,27 @@ async function main() {
|
|
|
374
485
|
tui.setFocus(dialog.inputComponent);
|
|
375
486
|
}
|
|
376
487
|
|
|
488
|
+
function showAskDialog(
|
|
489
|
+
title: string,
|
|
490
|
+
questions: AskQuestion[],
|
|
491
|
+
onDone: (answers: AskAnswer[] | undefined) => void,
|
|
492
|
+
): void {
|
|
493
|
+
const dialog = new AskDialog(title, questions);
|
|
494
|
+
dialog.onSubmit = (answers) => {
|
|
495
|
+
showPrompt();
|
|
496
|
+
onDone(answers);
|
|
497
|
+
};
|
|
498
|
+
dialog.onCancel = () => {
|
|
499
|
+
showPrompt();
|
|
500
|
+
onDone(undefined);
|
|
501
|
+
};
|
|
502
|
+
dialog.onCtrlD = exit;
|
|
503
|
+
showDialog(dialog);
|
|
504
|
+
}
|
|
505
|
+
|
|
377
506
|
function handleExtensionUI(req: ExtensionUIRequest): void {
|
|
378
507
|
const { id, method } = req;
|
|
508
|
+
if (["select", "confirm", "input", "editor", "ask"].includes(method)) activeDialogId = id;
|
|
379
509
|
|
|
380
510
|
switch (method) {
|
|
381
511
|
// Dialog methods: replace prompt with interactive component
|
|
@@ -422,6 +552,23 @@ async function main() {
|
|
|
422
552
|
break;
|
|
423
553
|
}
|
|
424
554
|
|
|
555
|
+
case "ask": {
|
|
556
|
+
const questions = req.questions ?? [];
|
|
557
|
+
if (questions.length === 0) {
|
|
558
|
+
showPrompt();
|
|
559
|
+
send({ type: "extension_ui_response", id, cancelled: true });
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
showAskDialog(req.title ?? "Question", questions, (answers) => {
|
|
563
|
+
if (answers) {
|
|
564
|
+
send({ type: "extension_ui_response", id, answers });
|
|
565
|
+
} else {
|
|
566
|
+
send({ type: "extension_ui_response", id, cancelled: true });
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
|
|
425
572
|
// Fire-and-forget methods: display as notification
|
|
426
573
|
case "notify": {
|
|
427
574
|
const notifyType = (req.notifyType as string) ?? "info";
|
|
@@ -450,6 +597,10 @@ async function main() {
|
|
|
450
597
|
break;
|
|
451
598
|
}
|
|
452
599
|
|
|
600
|
+
case "setTitle":
|
|
601
|
+
terminal.setTitle(req.title ?? "");
|
|
602
|
+
break;
|
|
603
|
+
|
|
453
604
|
case "set_editor_text":
|
|
454
605
|
promptInput.input.setValue((req.text as string) ?? "");
|
|
455
606
|
tui.requestRender();
|
|
@@ -535,6 +686,11 @@ async function main() {
|
|
|
535
686
|
return;
|
|
536
687
|
}
|
|
537
688
|
|
|
689
|
+
if (data.type === "extension_ui_response_handled") {
|
|
690
|
+
if (data.id === activeDialogId) showPrompt();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
538
694
|
if (data.type === "message_update") {
|
|
539
695
|
const evt = data.assistantMessageEvent as Record<string, unknown> | undefined;
|
|
540
696
|
if (evt?.type === "text_delta") {
|
|
@@ -620,7 +776,7 @@ async function main() {
|
|
|
620
776
|
|
|
621
777
|
outputLog.append(`${BOLD}RPC Chat${RESET}`);
|
|
622
778
|
outputLog.append(`${DIM}Type a message and press Enter. Esc to abort or exit. Ctrl+D to quit.${RESET}`);
|
|
623
|
-
outputLog.append(`${DIM}Slash commands: /select /confirm /input /editor${RESET}`);
|
|
779
|
+
outputLog.append(`${DIM}Slash commands: /select /confirm /input /editor /rpc-ask${RESET}`);
|
|
624
780
|
outputLog.append("");
|
|
625
781
|
|
|
626
782
|
tui.start();
|
package/package.json
CHANGED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ask_user question component.
|
|
3
|
-
*
|
|
4
|
-
* Renders a clarifying question with optional single- or multi-select options
|
|
5
|
-
* and an optional free-text field, matching the approved cross-surface design.
|
|
6
|
-
*
|
|
7
|
-
* Keyboard model (settled in issue 396 discussion):
|
|
8
|
-
* ↑/↓ move a single cursor through the options and, last, the free-text field
|
|
9
|
-
* Space toggle the highlighted checkbox (multi-select only)
|
|
10
|
-
* Enter single-select: pick the highlighted option and submit;
|
|
11
|
-
* free-text field: submit the typed answer;
|
|
12
|
-
* multi-select: submit all checked options plus any free text
|
|
13
|
-
* Shift+Enter insert a newline in the multiline free-text field
|
|
14
|
-
* Esc stop the current agent turn
|
|
15
|
-
*/
|
|
16
|
-
import { Container, type Focusable, type TUI } from "@dreb/tui";
|
|
17
|
-
import type { AskRequest, AskResult } from "../../../core/extensions/types.js";
|
|
18
|
-
export interface AskUserComponentOptions {
|
|
19
|
-
tui?: TUI;
|
|
20
|
-
timeout?: number;
|
|
21
|
-
}
|
|
22
|
-
export declare class AskUserComponent extends Container implements Focusable {
|
|
23
|
-
private options;
|
|
24
|
-
private allowFreeText;
|
|
25
|
-
private multiSelect;
|
|
26
|
-
private multiline;
|
|
27
|
-
/** Cursor over [options..., freeTextRow?]. */
|
|
28
|
-
private cursorIndex;
|
|
29
|
-
/** Checkbox state for multi-select, aligned to options. */
|
|
30
|
-
private checked;
|
|
31
|
-
private onSubmitCallback;
|
|
32
|
-
private onStopCallback;
|
|
33
|
-
private titleText;
|
|
34
|
-
private baseTitle;
|
|
35
|
-
private optionsContainer;
|
|
36
|
-
private fieldLabel;
|
|
37
|
-
private input;
|
|
38
|
-
private editor;
|
|
39
|
-
private countdown;
|
|
40
|
-
private submitted;
|
|
41
|
-
private _focused;
|
|
42
|
-
get focused(): boolean;
|
|
43
|
-
set focused(value: boolean);
|
|
44
|
-
private get freeTextRow();
|
|
45
|
-
private get lastRow();
|
|
46
|
-
private cursorOnField;
|
|
47
|
-
constructor(request: AskRequest, onSubmit: (result: AskResult) => void, onStop: () => void, opts?: AskUserComponentOptions);
|
|
48
|
-
private buildHint;
|
|
49
|
-
private renderRows;
|
|
50
|
-
private syncFieldFocus;
|
|
51
|
-
private moveCursor;
|
|
52
|
-
private fieldText;
|
|
53
|
-
private currentAnswer;
|
|
54
|
-
private submit;
|
|
55
|
-
private stop;
|
|
56
|
-
handleInput(keyData: string): void;
|
|
57
|
-
dispose(): void;
|
|
58
|
-
}
|
|
59
|
-
//# sourceMappingURL=ask-user.d.ts.map
|