@crouton-kit/crouter 0.3.68 → 0.3.78
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 +1 -1
- package/dist/builtin-memory/00-runtime-base.md +3 -10
- package/dist/builtin-memory/04-base-worker.md +18 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +13 -34
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +639 -741
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +241 -58
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +2 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +365 -94
- package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +2 -2
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +11 -39
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +20 -15
- package/dist/builtin-pi-packages/pi-mode-switch/package.json +1 -1
- package/dist/builtin-views/canvas/tui.mjs +8 -9
- package/dist/builtin-views/chat/tui.mjs +12 -12
- package/dist/builtin-views/git-pr/tui.mjs +7 -8
- package/dist/builtin-views/inbox/tui.mjs +27 -41
- package/dist/builtin-views/linkedin/tui.mjs +23 -37
- package/dist/builtin-views/prompt-review/tui.mjs +11 -11
- package/dist/builtin-views/settings/tui.mjs +11 -11
- package/dist/builtin-views/workspace-sidebar/tui.mjs +8 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -0
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +11 -7
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +1 -1
- package/dist/clients/attach/__tests__/mermaid-render.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/mermaid-render.test.js +20 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +110 -0
- package/dist/clients/attach/attach-cmd.d.ts +1 -1
- package/dist/clients/attach/attach-cmd.js +733 -730
- package/dist/clients/attach/auth-pickers.d.ts +12 -0
- package/dist/clients/attach/auth-pickers.js +11 -0
- package/dist/clients/attach/canvas-panels.js +2 -3
- package/dist/clients/attach/chat-view.d.ts +7 -8
- package/dist/clients/attach/chat-view.js +119 -79
- package/dist/clients/attach/edit-diff-render.d.ts +6 -0
- package/dist/clients/attach/edit-diff-render.js +263 -0
- package/dist/clients/attach/input-controller.d.ts +28 -2
- package/dist/clients/attach/input-controller.js +38 -5
- package/dist/clients/attach/mermaid-render.js +5 -2
- package/dist/clients/attach/pickers.d.ts +8 -7
- package/dist/clients/attach/pickers.js +13 -17
- package/dist/clients/attach/slash-commands.d.ts +9 -0
- package/dist/clients/attach/slash-commands.js +128 -7
- package/dist/clients/attach/titled-editor.d.ts +12 -1
- package/dist/clients/attach/titled-editor.js +103 -8
- package/dist/commands/canvas-browse.js +2 -2
- package/dist/commands/memory/lint.js +39 -5
- package/dist/commands/memory/write.js +1 -0
- package/dist/commands/node.js +12 -5
- package/dist/commands/profile/new.js +30 -5
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
- package/dist/commands/sys/config.js +18 -21
- package/dist/commands/sys/setup-core.d.ts +12 -1
- package/dist/commands/sys/setup-core.js +25 -3
- package/dist/commands/sys/setup.js +166 -20
- package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
- package/dist/core/__tests__/base-worker-prompt.test.js +24 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +232 -1
- package/dist/core/__tests__/fault-classifier.test.js +15 -0
- package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
- package/dist/core/__tests__/full/broker-pane-resolution.test.js +9 -6
- package/dist/core/__tests__/full/cascade-close.test.js +5 -2
- package/dist/core/__tests__/full/dead-pane-regression.test.js +6 -3
- package/dist/core/__tests__/full/detach-focus.test.js +10 -5
- package/dist/core/__tests__/full/human-new-window-regression.test.js +6 -3
- package/dist/core/__tests__/full/review-render-pane-regression.test.js +2 -2
- package/dist/core/__tests__/helpers/harness.d.ts +1 -0
- package/dist/core/__tests__/helpers/harness.js +28 -4
- package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
- package/dist/core/__tests__/live-mutation-verbs.test.js +5 -4
- package/dist/core/__tests__/review-model-floor.test.js +1 -1
- package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
- package/dist/core/__tests__/stream-watchdog.test.js +70 -0
- package/dist/core/__tests__/tmux-surface.test.js +72 -0
- package/dist/core/canvas/__tests__/remote-event-stream.test.js +11 -9
- package/dist/core/canvas/__tests__/render-remote.test.js +7 -4
- package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
- package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +30 -13
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +164 -5
- package/dist/core/canvas/browse/model.d.ts +10 -6
- package/dist/core/canvas/browse/model.js +20 -12
- package/dist/core/canvas/browse/pins.d.ts +4 -0
- package/dist/core/canvas/browse/pins.js +29 -0
- package/dist/core/canvas/browse/render.d.ts +13 -1
- package/dist/core/canvas/browse/render.js +72 -15
- package/dist/core/canvas/nav-model.js +5 -11
- package/dist/core/canvas/paths.d.ts +5 -0
- package/dist/core/canvas/paths.js +7 -0
- package/dist/core/canvas/pid.d.ts +14 -10
- package/dist/core/canvas/pid.js +14 -10
- package/dist/core/config.d.ts +53 -2
- package/dist/core/config.js +228 -110
- package/dist/core/fault-classifier.js +1 -1
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +15 -1
- package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/resolve.test.js +224 -0
- package/dist/core/keybindings/catalog.d.ts +14 -0
- package/dist/core/keybindings/catalog.js +256 -0
- package/dist/core/keybindings/index.d.ts +5 -0
- package/dist/core/keybindings/index.js +4 -0
- package/dist/core/keybindings/match.d.ts +29 -0
- package/dist/core/keybindings/match.js +99 -0
- package/dist/core/keybindings/persistence.d.ts +14 -0
- package/dist/core/keybindings/persistence.js +31 -0
- package/dist/core/keybindings/resolve.d.ts +14 -0
- package/dist/core/keybindings/resolve.js +305 -0
- package/dist/core/keybindings/types.d.ts +37 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/memory-resolver.d.ts +1 -1
- package/dist/core/memory-resolver.js +10 -3
- package/dist/core/predicate.d.ts +5 -3
- package/dist/core/predicate.js +5 -3
- package/dist/core/runtime/broker-protocol.d.ts +33 -5
- package/dist/core/runtime/broker.js +165 -14
- package/dist/core/runtime/launch.d.ts +39 -6
- package/dist/core/runtime/launch.js +79 -21
- package/dist/core/runtime/naming.js +3 -3
- package/dist/core/runtime/pi-cli.d.ts +6 -0
- package/dist/core/runtime/pi-cli.js +16 -2
- package/dist/core/runtime/placement.d.ts +2 -2
- package/dist/core/runtime/placement.js +4 -1
- package/dist/core/runtime/promote.js +4 -0
- package/dist/core/runtime/recap.d.ts +3 -3
- package/dist/core/runtime/recap.js +47 -44
- package/dist/core/runtime/recycle.js +6 -1
- package/dist/core/runtime/reset.js +5 -0
- package/dist/core/runtime/revive.js +28 -18
- package/dist/core/runtime/session-list-cache.d.ts +23 -0
- package/dist/core/runtime/session-list-cache.js +270 -0
- package/dist/core/runtime/spawn.js +51 -32
- package/dist/core/runtime/stream-watchdog.d.ts +26 -0
- package/dist/core/runtime/stream-watchdog.js +75 -0
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +2 -2
- package/dist/core/runtime/tmux.d.ts +18 -15
- package/dist/core/runtime/tmux.js +216 -110
- package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
- package/dist/core/tui/__tests__/host-keybindings.test.js +112 -0
- package/dist/core/tui/host.d.ts +30 -1
- package/dist/core/tui/host.js +148 -34
- package/dist/core/view/__tests__/transport-remote.test.js +9 -6
- package/dist/core/view/contract.d.ts +17 -3
- package/dist/daemon/crtrd-cli.d.ts +1 -1
- package/dist/daemon/crtrd-cli.js +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +41 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +1 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +203 -25
- package/dist/pi-extensions/canvas-recap.d.ts +4 -0
- package/dist/pi-extensions/canvas-recap.js +38 -37
- package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
- package/dist/pi-extensions/canvas-stophook.js +33 -4
- package/dist/suppress-experimental-warnings.d.ts +1 -0
- package/dist/suppress-experimental-warnings.js +15 -0
- package/dist/types.d.ts +3 -13
- package/dist/types.js +12 -36
- package/dist/web-client/assets/{index-CnF5r8ky.js → index--md2ylfi.js} +18 -17
- package/dist/web-client/assets/index-BUdm9s9s.css +2 -0
- package/dist/web-client/index.html +3 -3
- package/dist/web-client/sw.js +1 -1
- package/package.json +11 -8
- package/scripts/postinstall.mjs +1 -1
- package/dist/pi-extensions/canvas-nav.d.ts +0 -43
- package/dist/pi-extensions/canvas-nav.js +0 -640
- package/dist/pi-extensions/widget-order-bus.d.ts +0 -6
- package/dist/pi-extensions/widget-order-bus.js +0 -34
- package/dist/web-client/assets/index-BnmSLNLa.css +0 -2
package/dist/core/tui/host.js
CHANGED
|
@@ -22,6 +22,7 @@ import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from
|
|
|
22
22
|
import { createDraw, detectColorCaps } from './draw.js';
|
|
23
23
|
import { initialChrome, deriveState } from '../view/chrome.js';
|
|
24
24
|
import { createLocalTransport } from '../view/transport-local.js';
|
|
25
|
+
import { formatBinding, matchesTerminalInput, resolveUserKeybindings, } from '../keybindings/index.js';
|
|
25
26
|
// Numeric SGR codes only (a color NAME emits a broken CSI — see draw.ts guard).
|
|
26
27
|
const FG = { cyan: '36', green: '32', yellow: '33', red: '31', grey: '90' };
|
|
27
28
|
/** The persistent state signal: word + glyph + hue, derived from host signals so
|
|
@@ -179,15 +180,94 @@ function drawChrome(draw, size, manifest, c, now = Date.now()) {
|
|
|
179
180
|
return { row: top, col: 0, width: cols, height };
|
|
180
181
|
}
|
|
181
182
|
export { drawChrome };
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
183
|
+
/** True when some effective binding claims the `ctrl+h` gesture — used to decide
|
|
184
|
+
* whether the ambiguous 0x08 byte is a Ctrl-H remap or a plain Backspace. */
|
|
185
|
+
function bindingSetClaimsCtrlH(bindings) {
|
|
186
|
+
for (const eff of bindings.bindings.values()) {
|
|
187
|
+
if (eff.gestures.includes('ctrl+h'))
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
/** One-line summary of rejected hand-edited overrides, surfaced once per surface. */
|
|
193
|
+
function summarizeKeybindingDiagnostics(diags) {
|
|
194
|
+
if (diags.length === 0)
|
|
195
|
+
return null;
|
|
196
|
+
const extra = diags.length > 1 ? ` (+${diags.length - 1} more)` : '';
|
|
197
|
+
return `keybindings: ${diags[0].message}${extra}`;
|
|
198
|
+
}
|
|
199
|
+
/** Extend the shared legacy parser with the terminal gestures the catalog can
|
|
200
|
+
* express. The raw shape goes only to the catalog matcher; custom literals keep
|
|
201
|
+
* their established Key-based matching. When `bindings` is supplied the ambiguous
|
|
202
|
+
* 0x08 byte resolves to Ctrl-H only if an effective binding claims it. */
|
|
203
|
+
export function parseViewKeypress(data, bindings) {
|
|
204
|
+
const parsed = parseKeypress(data);
|
|
205
|
+
const raw = { ...parsed.key, input: parsed.input };
|
|
206
|
+
const sequence = data.toString('utf8');
|
|
207
|
+
const named = {
|
|
208
|
+
'\x1b[3~': 'delete', '\x1b[5~': 'pageUp', '\x1b[6~': 'pageDown',
|
|
209
|
+
'\x1b[H': 'home', '\x1b[F': 'end', '\x1b[1~': 'home', '\x1b[4~': 'end',
|
|
210
|
+
'\x1bOH': 'home', '\x1bOF': 'end',
|
|
211
|
+
};
|
|
212
|
+
const setModifiers = (code) => {
|
|
213
|
+
const value = Number(code);
|
|
214
|
+
raw.shift = value === 2 || value === 4 || value === 6 || value === 8;
|
|
215
|
+
raw.meta = value === 3 || value === 4 || value === 7 || value === 8;
|
|
216
|
+
raw.ctrl = value === 5 || value === 6 || value === 7 || value === 8;
|
|
217
|
+
};
|
|
218
|
+
const namedKey = named[sequence];
|
|
219
|
+
if (namedKey)
|
|
220
|
+
raw[namedKey] = true;
|
|
221
|
+
const arrow = /^\x1b\[(?:1;)?([2-8])?([ABCD])$/.exec(sequence);
|
|
222
|
+
if (arrow) {
|
|
223
|
+
setModifiers(arrow[1]);
|
|
224
|
+
raw[{ A: 'upArrow', B: 'downArrow', C: 'rightArrow', D: 'leftArrow' }[arrow[2]]] = true;
|
|
225
|
+
}
|
|
226
|
+
const modifiedNamed = /^\x1b\[(3|5|6|1|4);([2-8])~$/.exec(sequence);
|
|
227
|
+
if (modifiedNamed) {
|
|
228
|
+
raw[{ 3: 'delete', 5: 'pageUp', 6: 'pageDown', 1: 'home', 4: 'end' }[modifiedNamed[1]]] = true;
|
|
229
|
+
setModifiers(modifiedNamed[2]);
|
|
230
|
+
}
|
|
231
|
+
const modifiedHomeEnd = /^\x1b\[1;([2-8])([HF])$/.exec(sequence);
|
|
232
|
+
if (modifiedHomeEnd) {
|
|
233
|
+
raw[modifiedHomeEnd[2] === 'H' ? 'home' : 'end'] = true;
|
|
234
|
+
setModifiers(modifiedHomeEnd[1]);
|
|
235
|
+
}
|
|
236
|
+
// Alt+<char> gestures (ESC-prefixed printables/control letters) that the
|
|
237
|
+
// legacy parser leaves as a raw two-byte `input`. Only the raw catalog shape
|
|
238
|
+
// gains the alt modifier; `parsed` keeps its Key-based shape so custom literals
|
|
239
|
+
// still match exactly as before (Alt+<letter> must NOT match a bare literal).
|
|
240
|
+
// Special keys the legacy parser already decoded (Alt+Backspace, arrows, …)
|
|
241
|
+
// return `input: ''`, so `parsed.input === sequence` skips them here.
|
|
242
|
+
if (sequence.length === 2 && sequence.startsWith('\x1b') && parsed.input === sequence) {
|
|
243
|
+
const code = sequence.charCodeAt(1);
|
|
244
|
+
raw.input = code < 32 ? String.fromCharCode(code + 64).toLowerCase() : sequence[1];
|
|
245
|
+
raw.meta = true;
|
|
246
|
+
raw.ctrl = code < 32;
|
|
247
|
+
}
|
|
248
|
+
// Backspace family. \x7f, \b, and their Alt-prefixed forms all mean "delete one
|
|
249
|
+
// char" and canonicalize to plain backspace (there is no alt+backspace catalog
|
|
250
|
+
// action, so Alt+Backspace deletes exactly like Backspace, as before). The 0x08
|
|
251
|
+
// byte is ALSO Ctrl-H: honor a Ctrl-H remap only when an effective binding
|
|
252
|
+
// actually claims `ctrl+h`, otherwise it stays backspace so default deletion
|
|
253
|
+
// keeps working on ^H-erase terminals.
|
|
254
|
+
if (raw.backspace) {
|
|
255
|
+
const ctrlHByte = sequence === '\b' || sequence === '\x1b\b';
|
|
256
|
+
if (ctrlHByte && bindings && bindingSetClaimsCtrlH(bindings)) {
|
|
257
|
+
raw.input = 'h';
|
|
258
|
+
raw.ctrl = true;
|
|
259
|
+
raw.meta = false;
|
|
260
|
+
raw.backspace = false;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
raw.input = '';
|
|
264
|
+
raw.ctrl = false;
|
|
265
|
+
raw.meta = false;
|
|
266
|
+
raw.backspace = true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return { ...parsed, raw };
|
|
270
|
+
}
|
|
191
271
|
function keyTokens(input, key) {
|
|
192
272
|
const t = [];
|
|
193
273
|
if (key.upArrow)
|
|
@@ -228,21 +308,53 @@ function isPrintable(input, key) {
|
|
|
228
308
|
function sanitizePrintable(s) {
|
|
229
309
|
return Array.from(s).filter((ch) => ch.charCodeAt(0) >= 32).join('');
|
|
230
310
|
}
|
|
311
|
+
/** Resolve footer hints for the CURRENT state: only bindings whose `when(state)`
|
|
312
|
+
* passes contribute, so mode-gated affordances (compose/react send/cancel/pick)
|
|
313
|
+
* appear exactly while active. Catalog bindings render the effective key label
|
|
314
|
+
* (omitted entirely when disabled); custom-view literal hints stay verbatim. */
|
|
315
|
+
export function resolveViewKeyHints(keymap, bindings, state) {
|
|
316
|
+
const hints = [];
|
|
317
|
+
for (const binding of keymap) {
|
|
318
|
+
if (!binding.hint)
|
|
319
|
+
continue;
|
|
320
|
+
if (binding.when && !binding.when(state))
|
|
321
|
+
continue;
|
|
322
|
+
if ('bindingId' in binding) {
|
|
323
|
+
if (bindings.gestures(binding.bindingId).length === 0)
|
|
324
|
+
continue; // disabled: no hint
|
|
325
|
+
hints.push({ keys: formatBinding(bindings, binding.bindingId), label: binding.hint.label });
|
|
326
|
+
}
|
|
327
|
+
else if ('capture' in binding) {
|
|
328
|
+
hints.push('keys' in binding.hint ? binding.hint : { keys: 'type', label: binding.hint.label });
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
hints.push(binding.hint);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return hints;
|
|
335
|
+
}
|
|
231
336
|
/** The first non-capture binding whose key matches and whose `when` (if any)
|
|
232
|
-
*
|
|
233
|
-
|
|
337
|
+
* passes — catalog IDs use the terminal matcher; custom literals retain their
|
|
338
|
+
* presenter-authored matching. */
|
|
339
|
+
export function matchViewBinding(keymap, bindings, input, key, state, raw = { ...key, input }) {
|
|
234
340
|
const tokens = keyTokens(input, key);
|
|
235
|
-
for (const
|
|
236
|
-
if ('capture' in
|
|
341
|
+
for (const binding of keymap) {
|
|
342
|
+
if ('capture' in binding)
|
|
237
343
|
continue;
|
|
238
|
-
if (
|
|
344
|
+
if (binding.when && !binding.when(state))
|
|
239
345
|
continue;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
346
|
+
const matches = 'bindingId' in binding
|
|
347
|
+
? matchesTerminalInput(bindings, binding.bindingId, raw)
|
|
348
|
+
: binding.keys.some((literal) => tokens.includes(literal));
|
|
349
|
+
if (matches)
|
|
350
|
+
return { intent: binding.intent, payload: binding.payload ? binding.payload(state) : undefined };
|
|
243
351
|
}
|
|
244
352
|
return null;
|
|
245
353
|
}
|
|
354
|
+
/** Match a host-owned generic-view action through the same catalog snapshot. */
|
|
355
|
+
export function matchesViewHostBinding(bindings, id, raw) {
|
|
356
|
+
return matchesTerminalInput(bindings, id, raw);
|
|
357
|
+
}
|
|
246
358
|
/** The active text-capture binding for this state, if any (compose-mode entry). */
|
|
247
359
|
function activeCapture(keymap, state) {
|
|
248
360
|
for (const b of keymap) {
|
|
@@ -271,9 +383,13 @@ async function runRequest(transport, src, args) {
|
|
|
271
383
|
* (or Ctrl-C). `text` is the optional text presenter for the piped path. */
|
|
272
384
|
export async function runCoreView(core, tui, text, opts = {}) {
|
|
273
385
|
const options = Object.freeze({ ...(opts.options ?? {}) });
|
|
386
|
+
const bindings = resolveUserKeybindings();
|
|
387
|
+
const keybindingNotice = summarizeKeybindingDiagnostics(bindings.diagnostics);
|
|
274
388
|
const transport = createLocalTransport({ cwd: process.cwd() });
|
|
275
389
|
// ── Non-TTY / piped path: init, best-effort refresh, dump, exit 0. ──
|
|
276
390
|
if (!process.stdin.isTTY) {
|
|
391
|
+
if (keybindingNotice)
|
|
392
|
+
process.stderr.write(`${keybindingNotice}\n`);
|
|
277
393
|
let dstate = core.init(options);
|
|
278
394
|
const dchrome = initialChrome();
|
|
279
395
|
const dsignal = {
|
|
@@ -307,13 +423,9 @@ export async function runCoreView(core, tui, text, opts = {}) {
|
|
|
307
423
|
let state = core.init(options);
|
|
308
424
|
const chrome = initialChrome();
|
|
309
425
|
let tick = 0;
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
for (const b of tui.keymap) {
|
|
314
|
-
if (b.hint)
|
|
315
|
-
hints.push(b.hint);
|
|
316
|
-
}
|
|
426
|
+
// Report rejected hand-edited overrides once, as a chrome notice.
|
|
427
|
+
if (keybindingNotice)
|
|
428
|
+
chrome.banner = { msg: keybindingNotice, level: 'action' };
|
|
317
429
|
// Restore the terminal exactly once, however we leave.
|
|
318
430
|
let restored = false;
|
|
319
431
|
const cleanup = () => {
|
|
@@ -329,7 +441,9 @@ export async function runCoreView(core, tui, text, opts = {}) {
|
|
|
329
441
|
const render = () => {
|
|
330
442
|
const size = getTerminalSize();
|
|
331
443
|
const { draw, frame } = createDraw(size, caps);
|
|
332
|
-
|
|
444
|
+
// Footer hints are re-derived per draw so they track the active mode
|
|
445
|
+
// (compose/react affordances appear only while their `when(state)` holds).
|
|
446
|
+
const chromeManifest = { ...core.manifest, keymap: resolveViewKeyHints(tui.keymap, bindings, state) };
|
|
333
447
|
const tickChrome = { ...chrome, tick };
|
|
334
448
|
const content = drawChrome(draw, size, chromeManifest, tickChrome);
|
|
335
449
|
try {
|
|
@@ -525,14 +639,14 @@ export async function runCoreView(core, tui, text, opts = {}) {
|
|
|
525
639
|
return;
|
|
526
640
|
let parsed;
|
|
527
641
|
try {
|
|
528
|
-
parsed =
|
|
642
|
+
parsed = parseViewKeypress(data, bindings);
|
|
529
643
|
}
|
|
530
644
|
catch {
|
|
531
645
|
return;
|
|
532
646
|
}
|
|
533
|
-
const { input, key } = parsed;
|
|
534
|
-
//
|
|
535
|
-
if (
|
|
647
|
+
const { input, key, raw } = parsed;
|
|
648
|
+
// The catalog-backed escape hatch works even mid-flight.
|
|
649
|
+
if (matchesViewHostBinding(bindings, 'crtr.view.host.force-quit', raw)) {
|
|
536
650
|
finish();
|
|
537
651
|
return;
|
|
538
652
|
}
|
|
@@ -544,13 +658,13 @@ export async function runCoreView(core, tui, text, opts = {}) {
|
|
|
544
658
|
// fall through to the keymap so submit/cancel can be bound.
|
|
545
659
|
const cap = activeCapture(tui.keymap, state);
|
|
546
660
|
if (cap) {
|
|
547
|
-
if (
|
|
548
|
-
captureBuf
|
|
661
|
+
if (matchesViewHostBinding(bindings, 'crtr.view.host.editor.backspace', raw)) {
|
|
662
|
+
captureBuf = captureBuf.slice(0, -1);
|
|
549
663
|
await enqueueDispatch(cap.capture, captureBuf);
|
|
550
664
|
return;
|
|
551
665
|
}
|
|
552
|
-
if (key
|
|
553
|
-
captureBuf
|
|
666
|
+
if (isPrintable(input, key)) {
|
|
667
|
+
captureBuf += sanitizePrintable(input);
|
|
554
668
|
await enqueueDispatch(cap.capture, captureBuf);
|
|
555
669
|
return;
|
|
556
670
|
}
|
|
@@ -558,7 +672,7 @@ export async function runCoreView(core, tui, text, opts = {}) {
|
|
|
558
672
|
else {
|
|
559
673
|
captureBuf = '';
|
|
560
674
|
}
|
|
561
|
-
const m =
|
|
675
|
+
const m = matchViewBinding(tui.keymap, bindings, input, key, state, raw);
|
|
562
676
|
if (m)
|
|
563
677
|
await enqueueDispatch(m.intent, m.payload);
|
|
564
678
|
};
|
|
@@ -25,8 +25,12 @@ function startStalledServer() {
|
|
|
25
25
|
});
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
|
-
test('a stalled endpoint returns a timeout failure within the configured bound instead of hanging forever', async () => {
|
|
28
|
+
test('a stalled endpoint returns a timeout failure within the configured bound instead of hanging forever', async (t) => {
|
|
29
29
|
const server = await startStalledServer();
|
|
30
|
+
// Register the close BEFORE any assertion runs — a listening server left
|
|
31
|
+
// open by a thrown assertion keeps this test file's process alive forever
|
|
32
|
+
// (see the CI-hang postmortem on rebuild-coalescer.test.ts).
|
|
33
|
+
t.after(() => server.close());
|
|
30
34
|
const exec = createRemoteExec(server.url, 'tok', 100); // small timeout for the test
|
|
31
35
|
const started = Date.now();
|
|
32
36
|
// Race against a generous outer deadline: if the transport doesn't
|
|
@@ -40,9 +44,8 @@ test('a stalled endpoint returns a timeout failure within the configured bound i
|
|
|
40
44
|
assert.equal(result.ok, false);
|
|
41
45
|
assert.match(result.stderr, /timeout/i);
|
|
42
46
|
assert.ok(elapsed < 1000, `expected the timeout to fire near the configured 100ms bound, took ${elapsed}ms`);
|
|
43
|
-
await server.close();
|
|
44
47
|
});
|
|
45
|
-
test('a normal fast response is unaffected by the timeout wiring', async () => {
|
|
48
|
+
test('a normal fast response is unaffected by the timeout wiring', async (t) => {
|
|
46
49
|
const server = createServer((req, res) => {
|
|
47
50
|
const chunks = [];
|
|
48
51
|
req.on('data', (c) => chunks.push(c));
|
|
@@ -51,6 +54,7 @@ test('a normal fast response is unaffected by the timeout wiring', async () => {
|
|
|
51
54
|
res.end(JSON.stringify({ ok: true, exitCode: 0, stdout: JSON.stringify({ nodes: [] }), stderr: '' }));
|
|
52
55
|
});
|
|
53
56
|
});
|
|
57
|
+
t.after(() => new Promise((r) => server.close(() => r())));
|
|
54
58
|
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
55
59
|
const addr = server.address();
|
|
56
60
|
const port = typeof addr === 'object' && addr !== null ? addr.port : 0;
|
|
@@ -58,7 +62,6 @@ test('a normal fast response is unaffected by the timeout wiring', async () => {
|
|
|
58
62
|
const result = await exec('crtr', ['--json', 'node', 'inspect', 'list']);
|
|
59
63
|
assert.equal(result.ok, true);
|
|
60
64
|
assert.equal(result.stdout, JSON.stringify({ nodes: [] }));
|
|
61
|
-
await new Promise((r) => server.close(() => r()));
|
|
62
65
|
});
|
|
63
66
|
// --- Phase 3 review Minor 1: no token bytes may ever reach returned stderr ---
|
|
64
67
|
test('an unreachable endpoint returns a flat generic network-error message, never the raw exception text', async () => {
|
|
@@ -75,7 +78,7 @@ test('a malformed token (contains a newline) is rejected up front with a generic
|
|
|
75
78
|
assert.ok(!result.stderr.includes('bad\ntoken'));
|
|
76
79
|
assert.ok(!result.stderr.includes('bad'));
|
|
77
80
|
});
|
|
78
|
-
test('a valid token containing typical bearer-token punctuation (-, _, .) still round-trips against a real server', async () => {
|
|
81
|
+
test('a valid token containing typical bearer-token punctuation (-, _, .) still round-trips against a real server', async (t) => {
|
|
79
82
|
const server = createServer((req, res) => {
|
|
80
83
|
const chunks = [];
|
|
81
84
|
req.on('data', (c) => chunks.push(c));
|
|
@@ -84,6 +87,7 @@ test('a valid token containing typical bearer-token punctuation (-, _, .) still
|
|
|
84
87
|
res.end(JSON.stringify({ ok: true, exitCode: 0, stdout: JSON.stringify({ nodes: [] }), stderr: '' }));
|
|
85
88
|
});
|
|
86
89
|
});
|
|
90
|
+
t.after(() => new Promise((r) => server.close(() => r())));
|
|
87
91
|
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
88
92
|
const addr = server.address();
|
|
89
93
|
const port = typeof addr === 'object' && addr !== null ? addr.port : 0;
|
|
@@ -91,5 +95,4 @@ test('a valid token containing typical bearer-token punctuation (-, _, .) still
|
|
|
91
95
|
const result = await exec('crtr', ['--json', 'node', 'inspect', 'list']);
|
|
92
96
|
assert.equal(result.ok, true);
|
|
93
97
|
assert.equal(result.stdout, JSON.stringify({ nodes: [] }));
|
|
94
|
-
await new Promise((r) => server.close(() => r()));
|
|
95
98
|
});
|
|
@@ -154,7 +154,20 @@ export interface KeyHint {
|
|
|
154
154
|
keys: string;
|
|
155
155
|
label: string;
|
|
156
156
|
}
|
|
157
|
+
/** A catalog binding supplies only its semantic hint label: the host derives
|
|
158
|
+
* the rendered key label from the effective user binding snapshot. */
|
|
159
|
+
export interface CatalogKeyHint {
|
|
160
|
+
label: string;
|
|
161
|
+
}
|
|
157
162
|
export type KeyBinding<S> = {
|
|
163
|
+
/** Closed crouter catalog action. Shipped presenters use this form. */
|
|
164
|
+
bindingId: import('../keybindings/index.js').BindingId;
|
|
165
|
+
intent: string;
|
|
166
|
+
payload?: (state: S) => unknown;
|
|
167
|
+
when?: (state: S) => boolean;
|
|
168
|
+
hint?: CatalogKeyHint;
|
|
169
|
+
} | {
|
|
170
|
+
/** Literal keys remain owned by custom/plugin views, outside crouter's catalog. */
|
|
158
171
|
keys: string[];
|
|
159
172
|
intent: string;
|
|
160
173
|
payload?: (state: S) => unknown;
|
|
@@ -162,11 +175,12 @@ export type KeyBinding<S> = {
|
|
|
162
175
|
hint?: KeyHint;
|
|
163
176
|
} | {
|
|
164
177
|
/** Text-capture binding: while `when(state)` is true the host runs a
|
|
165
|
-
* built-in line-edit buffer over raw printable
|
|
166
|
-
*
|
|
178
|
+
* built-in line-edit buffer over raw printable keys and dispatches
|
|
179
|
+
* `capture` with the next draft value on each edit. Deletion is the
|
|
180
|
+
* host catalog action `crtr.view.host.editor.backspace`. */
|
|
167
181
|
capture: string;
|
|
168
182
|
when: (state: S) => boolean;
|
|
169
|
-
hint?: KeyHint;
|
|
183
|
+
hint?: KeyHint | CatalogKeyHint;
|
|
170
184
|
};
|
|
171
185
|
export interface TuiPresenter<S = unknown> {
|
|
172
186
|
/** Pure read of state, paints via draw.*; never ANSI. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import '../suppress-experimental-warnings.js';
|
package/dist/daemon/crtrd-cli.js
CHANGED
|
@@ -21,6 +21,7 @@ import { closeDb } from '../../core/canvas/db.js';
|
|
|
21
21
|
import { reportsDir } from '../../core/canvas/paths.js';
|
|
22
22
|
import { readInboxSince } from '../../core/feed/inbox.js';
|
|
23
23
|
import { STALL_REPROMPT } from '../../core/runtime/stop-guard.js';
|
|
24
|
+
import { recordFault, readFault } from '../../core/runtime/fault.js';
|
|
24
25
|
let home;
|
|
25
26
|
let origNode;
|
|
26
27
|
let origPane;
|
|
@@ -262,6 +263,46 @@ test('refresh-yield (intent=refresh) writes NO push — silent to subscribers',
|
|
|
262
263
|
assert.equal(readInboxSince('root').length, 0, 'a yield is silent: NO inbox pointer');
|
|
263
264
|
assert.equal(pi.injected.length, 0, 'no reprompt on a refresh-yield');
|
|
264
265
|
});
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// turn_end — stale-⚠ fix: reaching turn_end proves the current provider
|
|
268
|
+
// round-trip and any wedge episode are both over, so both fault links clear.
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
/** A turn_end event carrying one assistant text block. */
|
|
271
|
+
function turnEndEvent(text) {
|
|
272
|
+
return { message: { role: 'assistant', content: [{ type: 'text', text }] } };
|
|
273
|
+
}
|
|
274
|
+
test('turn_end clears a stale pi→provider fatal fault (a completed round-trip proves the episode is over)', () => {
|
|
275
|
+
createNode(node('leaf', { parent: null, lifecycle: 'terminal' }));
|
|
276
|
+
recordFault('leaf', {
|
|
277
|
+
link: 'pi→provider',
|
|
278
|
+
op: 'generation turn',
|
|
279
|
+
kind: 'other',
|
|
280
|
+
retry: { disposition: 'fatal' },
|
|
281
|
+
message: 'terminated',
|
|
282
|
+
});
|
|
283
|
+
assert.notEqual(readFault('leaf'), null, 'precondition: fault marker recorded');
|
|
284
|
+
process.env['CRTR_NODE_ID'] = 'leaf';
|
|
285
|
+
const pi = makeFakePi();
|
|
286
|
+
registerCanvasStophook(pi);
|
|
287
|
+
pi.fire('turn_end', turnEndEvent('all good now'), {});
|
|
288
|
+
assert.equal(readFault('leaf'), null, 'the pi→provider fault is cleared at turn_end');
|
|
289
|
+
});
|
|
290
|
+
test('turn_end still clears a daemon→node wedge fault (existing behavior, unchanged)', () => {
|
|
291
|
+
createNode(node('leaf', { parent: null, lifecycle: 'terminal' }));
|
|
292
|
+
recordFault('leaf', {
|
|
293
|
+
link: 'daemon→node',
|
|
294
|
+
op: 'wedge detection',
|
|
295
|
+
kind: 'wedged',
|
|
296
|
+
retry: { disposition: 'manual' },
|
|
297
|
+
message: 'no progress for 20m',
|
|
298
|
+
});
|
|
299
|
+
assert.notEqual(readFault('leaf'), null, 'precondition: wedge fault recorded');
|
|
300
|
+
process.env['CRTR_NODE_ID'] = 'leaf';
|
|
301
|
+
const pi = makeFakePi();
|
|
302
|
+
registerCanvasStophook(pi);
|
|
303
|
+
pi.fire('turn_end', turnEndEvent('turn completed'), {});
|
|
304
|
+
assert.equal(readFault('leaf'), null, 'the daemon→node wedge fault is still cleared at turn_end');
|
|
305
|
+
});
|
|
265
306
|
test('stalled leaf (nothing live to await, no final) is still reprompted', () => {
|
|
266
307
|
createNode(node('mgr', { parent: null, lifecycle: 'terminal', mode: 'orchestrator' }));
|
|
267
308
|
createNode(node('leaf', { parent: 'mgr', lifecycle: 'terminal', status: 'active' }));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/pi-extensions/__tests__/canvas-stophook-context-nudge.test.ts
|
|
2
|
+
//
|
|
3
|
+
// The context-size nudge's delivery mode splits on whether the agent run
|
|
4
|
+
// continues past the turn. The regression this pins: the nudge used to be an
|
|
5
|
+
// unconditional STEER, and pi's agent loop drains the steering queue right
|
|
6
|
+
// after turn_end — so a band crossing on the run's FINAL message (no tool
|
|
7
|
+
// calls) extended the run past the agent's closing reply, burying the summary
|
|
8
|
+
// a human comes back to read once the conversation goes quiet.
|
|
9
|
+
// • turn_end with toolCall content (mid-run) → sendUserMessage steer
|
|
10
|
+
// • turn_end with text-only content (final) → sendMessage 'nextTurn' hold
|
|
11
|
+
//
|
|
12
|
+
// Second regression pinned here: a tool-calling turn ends with stopReason
|
|
13
|
+
// 'toolUse' (pi-ai's StopReason), NEVER 'stop'. Gating the steer path on
|
|
14
|
+
// stopReason 'stop'/'length' made runContinues unconditionally false, so the
|
|
15
|
+
// nudge was ALWAYS appended as nextTurn and never steered mid-run.
|
|
16
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import registerCanvasStophook from '../canvas-stophook.js';
|
|
22
|
+
import { createNode } from '../../core/canvas/canvas.js';
|
|
23
|
+
import { closeDb } from '../../core/canvas/db.js';
|
|
24
|
+
let home;
|
|
25
|
+
let origNode;
|
|
26
|
+
function node(id) {
|
|
27
|
+
return {
|
|
28
|
+
node_id: id,
|
|
29
|
+
name: id,
|
|
30
|
+
created: new Date().toISOString(),
|
|
31
|
+
cwd: '/tmp/work',
|
|
32
|
+
kind: 'general',
|
|
33
|
+
mode: 'base',
|
|
34
|
+
lifecycle: 'terminal',
|
|
35
|
+
status: 'active',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function makeFakePi() {
|
|
39
|
+
const handlers = {};
|
|
40
|
+
return {
|
|
41
|
+
steered: [],
|
|
42
|
+
held: [],
|
|
43
|
+
on(e, h) { handlers[e] = h; },
|
|
44
|
+
sendUserMessage(content, options) { this.steered.push({ content, deliverAs: options?.deliverAs }); },
|
|
45
|
+
sendMessage(m, options) {
|
|
46
|
+
this.held.push({ customType: m.customType, content: m.content, deliverAs: options?.deliverAs, triggerTurn: options?.triggerTurn });
|
|
47
|
+
},
|
|
48
|
+
fire(e, ev, ctx) { handlers[e]?.(ev, ctx); },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** A turn_end event whose assistant message crossed a steering band. A
|
|
52
|
+
* tool-calling turn really ends with stopReason 'toolUse'; text-only finals
|
|
53
|
+
* end with 'stop'. */
|
|
54
|
+
function turnEnd(content, stopReason = 'stop') {
|
|
55
|
+
return { message: { role: 'assistant', stopReason, content, usage: { input: 1, output: 1 } }, toolResults: [] };
|
|
56
|
+
}
|
|
57
|
+
const ctxAt = (tokens) => ({ getContextUsage: () => ({ tokens }) });
|
|
58
|
+
before(() => { origNode = process.env['CRTR_NODE_ID']; });
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
closeDb();
|
|
61
|
+
if (home)
|
|
62
|
+
rmSync(home, { recursive: true, force: true });
|
|
63
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-stophook-nudge-'));
|
|
64
|
+
process.env['CRTR_HOME'] = home;
|
|
65
|
+
});
|
|
66
|
+
after(() => {
|
|
67
|
+
closeDb();
|
|
68
|
+
if (home)
|
|
69
|
+
rmSync(home, { recursive: true, force: true });
|
|
70
|
+
delete process.env['CRTR_HOME'];
|
|
71
|
+
if (origNode === undefined)
|
|
72
|
+
delete process.env['CRTR_NODE_ID'];
|
|
73
|
+
else
|
|
74
|
+
process.env['CRTR_NODE_ID'] = origNode;
|
|
75
|
+
});
|
|
76
|
+
test('band crossing mid-run (tool calls present) → context nudge is a STEER', () => {
|
|
77
|
+
createNode(node('midrun'));
|
|
78
|
+
process.env['CRTR_NODE_ID'] = 'midrun';
|
|
79
|
+
const pi = makeFakePi();
|
|
80
|
+
registerCanvasStophook(pi);
|
|
81
|
+
pi.fire('turn_end', turnEnd([{ type: 'toolCall', id: 't1', name: 'bash', arguments: {} }], 'toolUse'), ctxAt(150_000));
|
|
82
|
+
assert.equal(pi.steered.length, 1, 'exactly one steer delivered');
|
|
83
|
+
assert.equal(pi.steered[0].deliverAs, 'steer');
|
|
84
|
+
assert.match(pi.steered[0].content, /^\[crtr\] Context ~150k/);
|
|
85
|
+
assert.equal(pi.held.length, 0, 'nothing held for nextTurn');
|
|
86
|
+
});
|
|
87
|
+
test('band crossing on the FINAL message (no tool calls) → nudge is HELD as nextTurn, never a steer', () => {
|
|
88
|
+
createNode(node('final'));
|
|
89
|
+
process.env['CRTR_NODE_ID'] = 'final';
|
|
90
|
+
const pi = makeFakePi();
|
|
91
|
+
registerCanvasStophook(pi);
|
|
92
|
+
pi.fire('turn_end', turnEnd([{ type: 'text', text: 'here is my summary' }]), ctxAt(150_000));
|
|
93
|
+
assert.equal(pi.steered.length, 0, 'no steer — a steer here would extend the run past the closing reply');
|
|
94
|
+
assert.equal(pi.held.length, 1, 'nudge held for the next user message');
|
|
95
|
+
assert.equal(pi.held[0].deliverAs, 'nextTurn');
|
|
96
|
+
assert.equal(pi.held[0].customType, 'crtr-context-nudge');
|
|
97
|
+
assert.match(pi.held[0].content, /^\[crtr\] Context ~150k/);
|
|
98
|
+
assert.notEqual(pi.held[0].triggerTurn, true, 'must not trigger a turn');
|
|
99
|
+
});
|
|
100
|
+
test('a truncated tool-calling turn (stopReason length) still steers — the loop continues past failed calls', () => {
|
|
101
|
+
createNode(node('lengthrun'));
|
|
102
|
+
process.env['CRTR_NODE_ID'] = 'lengthrun';
|
|
103
|
+
const pi = makeFakePi();
|
|
104
|
+
registerCanvasStophook(pi);
|
|
105
|
+
pi.fire('turn_end', turnEnd([{ type: 'toolCall', id: 't1', name: 'bash', arguments: {} }], 'length'), ctxAt(150_000));
|
|
106
|
+
assert.equal(pi.steered.length, 1);
|
|
107
|
+
assert.equal(pi.steered[0].deliverAs, 'steer');
|
|
108
|
+
assert.equal(pi.held.length, 0);
|
|
109
|
+
});
|
|
110
|
+
test('an aborted final turn also holds (the loop ends unconditionally on abort)', () => {
|
|
111
|
+
createNode(node('aborted'));
|
|
112
|
+
process.env['CRTR_NODE_ID'] = 'aborted';
|
|
113
|
+
const pi = makeFakePi();
|
|
114
|
+
registerCanvasStophook(pi);
|
|
115
|
+
// Aborted mid-tool-call: toolCall content present, but the run ends anyway —
|
|
116
|
+
// a steer would sit queued and leak into the NEXT run's start instead.
|
|
117
|
+
pi.fire('turn_end', turnEnd([{ type: 'toolCall', id: 't1', name: 'bash', arguments: {} }], 'aborted'), ctxAt(150_000));
|
|
118
|
+
assert.equal(pi.steered.length, 0);
|
|
119
|
+
assert.equal(pi.held.length, 1);
|
|
120
|
+
assert.equal(pi.held[0].deliverAs, 'nextTurn');
|
|
121
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type PiEvents = 'session_start' | 'turn_end' | 'agent_start' | 'agent_end' | 'session_shutdown';
|
|
1
|
+
type PiEvents = 'session_start' | 'turn_end' | 'agent_start' | 'agent_end' | 'agent_settled' | 'session_shutdown';
|
|
2
2
|
interface PiLike {
|
|
3
3
|
on: (event: PiEvents, handler: (event: any, ctx: any) => void | Promise<void>) => void;
|
|
4
4
|
sendUserMessage: (content: string, options?: {
|