@crouton-kit/crouter 0.3.70 → 0.3.79
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/build-root.d.ts +12 -4
- package/dist/build-root.js +25 -6
- 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-memory/crouter-development/plugins.md +82 -5
- 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 +1115 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +234 -71
- 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 +2 -516
- 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/clients/attach/__tests__/attach-chrome-remote.test.js +11 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +113 -0
- 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 +28 -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 +9 -1
- package/dist/clients/attach/attach-cmd.js +841 -792
- package/dist/clients/attach/auth-pickers.d.ts +0 -12
- package/dist/clients/attach/auth-pickers.js +64 -15
- 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/graph-overlay.d.ts +12 -2
- package/dist/clients/attach/graph-overlay.js +83 -33
- 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 +11 -17
- package/dist/clients/attach/slash-commands.d.ts +9 -0
- package/dist/clients/attach/slash-commands.js +127 -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/human/queue.js +3 -4
- package/dist/commands/memory/lint.js +39 -5
- package/dist/commands/memory/write.js +1 -0
- package/dist/commands/node.js +9 -2
- package/dist/commands/pkg/plugin-inspect.js +22 -1
- package/dist/commands/pkg/plugin-manage.js +31 -9
- 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/__tests__/setup-core.test.js +158 -1
- package/dist/commands/sys/config.js +18 -21
- package/dist/commands/sys/doctor.js +42 -4
- package/dist/commands/sys/setup-core.d.ts +49 -1
- package/dist/commands/sys/setup-core.js +161 -4
- package/dist/commands/sys/setup.d.ts +88 -0
- package/dist/commands/sys/setup.js +943 -169
- package/dist/commands/view-pick.d.ts +4 -0
- package/dist/commands/view-pick.js +17 -7
- 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__/canvas-inbox-watcher.test.js +34 -9
- package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins-surfaces.test.js +298 -0
- package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins.test.js +444 -0
- package/dist/core/__tests__/fault-classifier.test.js +15 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +9 -1
- package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
- package/dist/core/__tests__/preview-registry-sync.test.js +30 -1
- package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
- package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
- 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/browse/__tests__/model.test.js +23 -5
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.d.ts +6 -0
- package/dist/core/canvas/browse/app.js +230 -41
- 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/command-plugins/adapter.d.ts +15 -0
- package/dist/core/command-plugins/adapter.js +145 -0
- package/dist/core/command-plugins/compose.d.ts +5 -0
- package/dist/core/command-plugins/compose.js +56 -0
- package/dist/core/command-plugins/discovery.d.ts +104 -0
- package/dist/core/command-plugins/discovery.js +565 -0
- package/dist/core/config.d.ts +48 -0
- 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__/bespoke-consumers.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
- 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 +257 -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/profiles/select.d.ts +6 -0
- package/dist/core/profiles/select.js +86 -52
- package/dist/core/provider-management.d.ts +12 -0
- package/dist/core/provider-management.js +24 -0
- package/dist/core/runtime/broker-protocol.d.ts +33 -5
- package/dist/core/runtime/broker.js +164 -14
- package/dist/core/runtime/launch.d.ts +39 -6
- package/dist/core/runtime/launch.js +78 -20
- 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/pi-vendored.d.ts +8 -0
- package/dist/core/runtime/pi-vendored.js +14 -0
- 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 +75 -47
- package/dist/core/runtime/recycle.js +6 -1
- package/dist/core/runtime/reset.js +5 -0
- package/dist/core/runtime/session-list-cache.d.ts +33 -0
- package/dist/core/runtime/session-list-cache.js +338 -0
- package/dist/core/runtime/spawn.js +52 -45
- 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 +217 -110
- package/dist/core/scope.js +27 -4
- package/dist/core/subscription-state.d.ts +90 -0
- package/dist/core/subscription-state.js +762 -0
- 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/contract.d.ts +17 -3
- package/dist/daemon/crtrd.js +253 -12
- 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 +73 -46
- package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
- package/dist/pi-extensions/canvas-stophook.js +33 -4
- package/dist/types.d.ts +9 -26
- package/dist/types.js +1 -28
- package/dist/web-client/assets/{index-DiFuLcp6.js → index--md2ylfi.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +8 -4
- 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
|
@@ -52,7 +52,7 @@ const MAX_FRAME_BYTES = BROKER_READ_CAPS.maxLineBytes - 4 * 1024 * 1024;
|
|
|
52
52
|
* see slash-commands.ts's REMOTE_SAFE_BUILTIN_NAMES gate). */
|
|
53
53
|
const DRIVE_FRAME_TYPES = new Set([
|
|
54
54
|
'prompt', 'steer', 'follow_up', 'abort', 'bash',
|
|
55
|
-
'set_model', 'cycle_model', 'cycle_thinking', 'set_thinking_level', 'dequeue',
|
|
55
|
+
'set_model', 'cycle_model', 'cycle_ladder', 'cycle_thinking', 'set_thinking_level', 'dequeue',
|
|
56
56
|
'set_auto_retry', 'set_auto_compaction', 'compact', 'new_session',
|
|
57
57
|
'switch_session', 'fork', 'set_session_name', 'navigate_tree', 'reload',
|
|
58
58
|
'export', 'clone', 'share', 'request_control', 'release_control', 'reload_auth',
|
|
@@ -64,6 +64,10 @@ export class InputController {
|
|
|
64
64
|
hooks;
|
|
65
65
|
/** The currently-rendered blocking dialog, if any (for supersede/dismiss). */
|
|
66
66
|
dialog;
|
|
67
|
+
/** The request id of {@link dialog} — so a broker-driven `extension_ui_dismiss`
|
|
68
|
+
* tears down ONLY the matching overlay (correlated dismissal), never an
|
|
69
|
+
* unrelated one. */
|
|
70
|
+
dialogId;
|
|
67
71
|
/** The currently-open native picker overlay, if any (for supersede/dismiss). */
|
|
68
72
|
picker;
|
|
69
73
|
/** Monotonic open counter — a slow read-op reply whose `seq` is no longer the
|
|
@@ -92,8 +96,11 @@ export class InputController {
|
|
|
92
96
|
}
|
|
93
97
|
/** Render extension UI requests from the broker. Blocking dialogs route their
|
|
94
98
|
* answer back to the broker; non-blocking notify requests become the viewer's
|
|
95
|
-
* normal notice line
|
|
96
|
-
*
|
|
99
|
+
* normal notice line and NOTHING else — a notify must never tear down an
|
|
100
|
+
* unrelated blocking dialog whose broker request is still pending. A dialog is
|
|
101
|
+
* torn down only by (a) a NEW blocking dialog superseding it (control handoff
|
|
102
|
+
* re-route), (b) its own answer, or (c) a correlated {@link dismissDialog}
|
|
103
|
+
* from the broker (the request was aborted/timed out out-of-band). */
|
|
97
104
|
attachDialog(req) {
|
|
98
105
|
if (req.method === 'notify') {
|
|
99
106
|
const prefix = req.notifyType === 'error' ? 'Error: ' : req.notifyType === 'warning' ? 'Warning: ' : '';
|
|
@@ -102,12 +109,26 @@ export class InputController {
|
|
|
102
109
|
return;
|
|
103
110
|
}
|
|
104
111
|
this.dialog?.dismiss();
|
|
112
|
+
this.dialogId = req.id;
|
|
105
113
|
this.dialog = renderDialog(this.tui, req, (resp) => {
|
|
106
114
|
this.dialog = undefined;
|
|
115
|
+
this.dialogId = undefined;
|
|
107
116
|
this.hooks.onDialogResponse(resp);
|
|
108
117
|
}, this.keybindings);
|
|
109
118
|
this.tui.requestRender();
|
|
110
119
|
}
|
|
120
|
+
/** Broker-driven correlated dismissal: the broker resolved this request itself
|
|
121
|
+
* (its request was aborted out-of-band — e.g. a local OAuth loopback callback
|
|
122
|
+
* won the race against this manual-paste dialog — or its timeout fired). Tear
|
|
123
|
+
* down ONLY the overlay whose id matches; leave any other dialog untouched. */
|
|
124
|
+
dismissDialog(id) {
|
|
125
|
+
if (this.dialogId !== id)
|
|
126
|
+
return;
|
|
127
|
+
this.dialog?.dismiss();
|
|
128
|
+
this.dialog = undefined;
|
|
129
|
+
this.dialogId = undefined;
|
|
130
|
+
this.tui.requestRender();
|
|
131
|
+
}
|
|
111
132
|
/** Feed the latest engine state so read-only commands (e.g. `/session`) report
|
|
112
133
|
* current values. Optional; the controller works without it. */
|
|
113
134
|
setState(state) {
|
|
@@ -174,6 +195,7 @@ export class InputController {
|
|
|
174
195
|
onGraph: this.hooks.onGraph,
|
|
175
196
|
onQuit: this.hooks.onQuit,
|
|
176
197
|
onCopy: this.hooks.onCopy,
|
|
198
|
+
onColor: this.hooks.onColor,
|
|
177
199
|
openModelPicker: () => void this.openModelPicker(),
|
|
178
200
|
openSessionPicker: () => void this.openSessionPicker(),
|
|
179
201
|
openForkPicker: () => void this.openForkPicker(),
|
|
@@ -182,6 +204,7 @@ export class InputController {
|
|
|
182
204
|
openScopedModelsPicker: () => void this.openScopedModelsPicker(),
|
|
183
205
|
openLoginPicker: this.hooks.openLoginPicker ? () => this.hooks.openLoginPicker() : undefined,
|
|
184
206
|
openLogoutPicker: this.hooks.openLogoutPicker ? () => this.hooks.openLogoutPicker() : undefined,
|
|
207
|
+
onExplicitModelSet: this.hooks.onExplicitModelSet,
|
|
185
208
|
};
|
|
186
209
|
}
|
|
187
210
|
// -------------------------------------------------------------------------
|
|
@@ -299,10 +322,20 @@ export class InputController {
|
|
|
299
322
|
this.tui.requestRender();
|
|
300
323
|
}
|
|
301
324
|
openModelPicker() {
|
|
302
|
-
return this.openPicker({ type: 'list_models' }, 'list_models', 'model', (data, close) => buildModelPicker(this.tui, data, this.send, close));
|
|
325
|
+
return this.openPicker({ type: 'list_models' }, 'list_models', 'model', (data, close) => buildModelPicker(this.tui, data, this.send, close, this.hooks.onExplicitModelSet));
|
|
303
326
|
}
|
|
327
|
+
/** Unlike the other pickers, the session picker opens IMMEDIATELY — its
|
|
328
|
+
* component owns async loaders (with a Loading header), so the multi-second
|
|
329
|
+
* session-dir scan runs off the open path instead of blocking it. */
|
|
304
330
|
openSessionPicker() {
|
|
305
|
-
|
|
331
|
+
const request = this.hooks.onRequest;
|
|
332
|
+
if (request === undefined) {
|
|
333
|
+
this.notify("The session picker isn't available in this viewer");
|
|
334
|
+
return Promise.resolve();
|
|
335
|
+
}
|
|
336
|
+
this.openSeq++; // supersede any in-flight fetch-first picker open
|
|
337
|
+
this.showPicker((controls) => buildSessionPicker(this.tui, this.state?.sessionFile, request, this.keybindings, this.send, controls.close));
|
|
338
|
+
return Promise.resolve();
|
|
306
339
|
}
|
|
307
340
|
openTreePicker() {
|
|
308
341
|
return this.openPicker({ type: 'get_tree' }, 'get_tree', 'tree', (data, close) => buildTreePicker(this.tui, data, this.send, close));
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
//
|
|
19
19
|
// Scope (v1): only ```mermaid fences. The wider termrender directive vocabulary
|
|
20
20
|
// is deliberately out of scope — see the base prompt's Diagrams note.
|
|
21
|
-
import { isRendererReady, renderMarkdown } from '@crouton-kit/humanloop';
|
|
21
|
+
import { ensureRenderer, isRendererReady, renderMarkdown } from '@crouton-kit/humanloop';
|
|
22
22
|
/** A complete mermaid fence: ```mermaid (opening info string) ... closing ```.
|
|
23
23
|
* Line-anchored (m flag), non-greedy body (captured). An UNCLOSED fence (still
|
|
24
24
|
* streaming) does not match, so mid-stream text is left raw and only settles to
|
|
@@ -71,7 +71,10 @@ function stripBlankEdges(lines) {
|
|
|
71
71
|
* diagrams have no width control and overflow rather than truncate), which is
|
|
72
72
|
* stripped here, so an approximate pane width is fine. */
|
|
73
73
|
export function transformMermaidFences(text, width) {
|
|
74
|
-
if (!textHasMermaid(text)
|
|
74
|
+
if (!textHasMermaid(text))
|
|
75
|
+
return text;
|
|
76
|
+
ensureRenderer();
|
|
77
|
+
if (!isRendererReady())
|
|
75
78
|
return text;
|
|
76
79
|
return text.replace(MERMAID_FENCE, (whole, body) => {
|
|
77
80
|
try {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Component, type KeybindingsManager, type TUI } from '@earendil-works/pi-tui';
|
|
2
|
-
import type { ClientToBroker, GetSettingsData, GetTreeData, ListModelsData, ListScopedModelsData
|
|
2
|
+
import type { ClientToBroker, GetSettingsData, GetTreeData, ListModelsData, ListScopedModelsData } from '../../core/runtime/broker-protocol.js';
|
|
3
3
|
import type { ReadOpRequest } from './view-socket.js';
|
|
4
4
|
/** A built picker overlay: the `component` to show, and the `focus` target the
|
|
5
5
|
* caller must `setFocus` (the component itself, or the inner list that owns
|
|
@@ -28,12 +28,13 @@ type Notify = (message: string) => void;
|
|
|
28
28
|
/** `/model` (and ctrl+l) — the real model selector with search + scope toggle.
|
|
29
29
|
* Select → `set_model` with the resolved `provider/id` (the broker's
|
|
30
30
|
* `findModelSpec` requires that form). */
|
|
31
|
-
export declare function buildModelPicker(tui: TUI, data: ListModelsData, send: Send, close: Close): Picker;
|
|
32
|
-
/** `/resume` — the real session selector.
|
|
33
|
-
* fetch each scope on demand
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
31
|
+
export declare function buildModelPicker(tui: TUI, data: ListModelsData, send: Send, close: Close, onSelect?: (model: string) => void): Picker;
|
|
32
|
+
/** `/resume` — the real session selector. Opens INSTANTLY and reconstructs from
|
|
33
|
+
* async loaders that fetch each scope on demand — the component's own Loading
|
|
34
|
+
* header covers the scan (listing a large session dir takes seconds, so the
|
|
35
|
+
* fetch must stay off the open path). Select → `switch_session`. Rename/delete
|
|
36
|
+
* are out of scope (no command frame). */
|
|
37
|
+
export declare function buildSessionPicker(tui: TUI, currentSessionFile: string | undefined, request: Request, keybindings: KeybindingsManager, send: Send, close: Close): Picker;
|
|
37
38
|
/** `/tree` — the real tree navigator (pure data). Select → `navigate_tree`.
|
|
38
39
|
* Label-edit is out of scope (no command frame) → no `onLabelChange`. */
|
|
39
40
|
export declare function buildTreePicker(tui: TUI, data: GetTreeData, send: Send, close: Close): Picker;
|
|
@@ -51,7 +51,7 @@ function modelRegistryAdapter(all, available) {
|
|
|
51
51
|
/** `/model` (and ctrl+l) — the real model selector with search + scope toggle.
|
|
52
52
|
* Select → `set_model` with the resolved `provider/id` (the broker's
|
|
53
53
|
* `findModelSpec` requires that form). */
|
|
54
|
-
export function buildModelPicker(tui, data, send, close) {
|
|
54
|
+
export function buildModelPicker(tui, data, send, close, onSelect) {
|
|
55
55
|
const all = data.models;
|
|
56
56
|
const availableSet = new Set(data.availableIds);
|
|
57
57
|
const available = all.filter((m) => availableSet.has(`${m.provider}/${m.id}`));
|
|
@@ -60,29 +60,23 @@ export function buildModelPicker(tui, data, send, close) {
|
|
|
60
60
|
: undefined;
|
|
61
61
|
const component = new ModelSelectorComponent(tui, current, SettingsManager.inMemory(), modelRegistryAdapter(all, available), data.scopedModels, (model) => {
|
|
62
62
|
send({ type: 'set_model', model: `${model.provider}/${model.id}` });
|
|
63
|
+
onSelect?.(`${model.provider}/${model.id}`);
|
|
63
64
|
close();
|
|
64
65
|
}, () => close());
|
|
65
66
|
return { component, focus: component };
|
|
66
67
|
}
|
|
67
|
-
/** `/resume` — the real session selector.
|
|
68
|
-
* fetch each scope on demand
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
|
|
68
|
+
/** `/resume` — the real session selector. Opens INSTANTLY and reconstructs from
|
|
69
|
+
* async loaders that fetch each scope on demand — the component's own Loading
|
|
70
|
+
* header covers the scan (listing a large session dir takes seconds, so the
|
|
71
|
+
* fetch must stay off the open path). Select → `switch_session`. Rename/delete
|
|
72
|
+
* are out of scope (no command frame). */
|
|
73
|
+
export function buildSessionPicker(tui, currentSessionFile, request, keybindings, send, close) {
|
|
72
74
|
const revive = (d) => d.sessions.map((s) => ({ ...s, created: new Date(s.created), modified: new Date(s.modified) }));
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
if (firstCwd) {
|
|
76
|
-
firstCwd = false;
|
|
77
|
-
return revive(prefetchedCwd);
|
|
78
|
-
}
|
|
79
|
-
return revive((await request({ type: 'list_sessions', scope: 'cwd' })));
|
|
80
|
-
};
|
|
81
|
-
const allLoader = async () => revive((await request({ type: 'list_sessions', scope: 'all' })));
|
|
82
|
-
const component = new SessionSelectorComponent(currentLoader, allLoader, (path) => {
|
|
75
|
+
const loader = (scope) => async () => revive((await request({ type: 'list_sessions', scope })));
|
|
76
|
+
const component = new SessionSelectorComponent(loader('cwd'), loader('all'), (path) => {
|
|
83
77
|
send({ type: 'switch_session', path });
|
|
84
78
|
close();
|
|
85
|
-
}, () => close(), () => close(), () => tui.requestRender(), { showRenameHint: false, keybindings: keybindings },
|
|
79
|
+
}, () => close(), () => close(), () => tui.requestRender(), { showRenameHint: false, keybindings: keybindings }, currentSessionFile);
|
|
86
80
|
return { component, focus: component };
|
|
87
81
|
}
|
|
88
82
|
/** `/tree` — the real tree navigator (pure data). Select → `navigate_tree`.
|
|
@@ -46,10 +46,19 @@ export interface SlashContext {
|
|
|
46
46
|
openTreePicker?: () => void;
|
|
47
47
|
openSettingsPicker?: () => void;
|
|
48
48
|
openScopedModelsPicker?: () => void;
|
|
49
|
+
/** An EXPLICIT model choice was just issued via `/model <arg>` — mirrors
|
|
50
|
+
* `InputControllerHooks.onExplicitModelSet` (the picker path fires it from
|
|
51
|
+
* `buildModelPicker`). attach-cmd persists the choice as the node kind's
|
|
52
|
+
* default once the broker confirms with `model_changed`. */
|
|
53
|
+
onExplicitModelSet?: (model: string) => void;
|
|
49
54
|
/** Open the native /login provider selector. */
|
|
50
55
|
openLoginPicker?: () => void;
|
|
51
56
|
/** Open the native /logout provider selector. */
|
|
52
57
|
openLogoutPicker?: () => void;
|
|
58
|
+
/** `/color` — recolor the editor's name chip (the "bubble" /rename names).
|
|
59
|
+
* `null` clears back to the default chip. Wired by attach-cmd (it owns the
|
|
60
|
+
* TitledEditor); absent → only the tmux pane outline is painted. */
|
|
61
|
+
onColor?: (color: string | null) => void;
|
|
53
62
|
}
|
|
54
63
|
/** Canvas slash-commands — implemented NATIVE in the viewer, the ONLY live host:
|
|
55
64
|
* the broker binds mode:'print', so broker pi-extensions never run their
|
|
@@ -37,6 +37,8 @@ export const CANVAS_SLASH_COMMANDS = [
|
|
|
37
37
|
{ name: 'resume-node', description: 'Open the canvas navigator (search/scope/sort/tree) and resume the chosen node' },
|
|
38
38
|
{ name: 'view', description: 'Open a view in a popup — bare for the picker, or /view <name> to open that view directly' },
|
|
39
39
|
{ name: 'context', description: 'Browse this node + its subscriptions’ context dirs in a read-only nvim popup' },
|
|
40
|
+
{ name: 'rename', description: 'Rename this node everywhere — node row, viewer window, session chip, tmux pane: /rename <name>' },
|
|
41
|
+
{ name: 'color', description: 'Color this node — tmux pane outline + name chip: /color <color> (red, blue, #rrggbb, …) or /color none' },
|
|
40
42
|
];
|
|
41
43
|
const CANVAS_NAMES = new Set(CANVAS_SLASH_COMMANDS.map((c) => c.name));
|
|
42
44
|
/** Native canvas commands that shell a LOCAL `crtr` mutation or read against
|
|
@@ -46,7 +48,15 @@ const CANVAS_NAMES = new Set(CANVAS_SLASH_COMMANDS.map((c) => c.name));
|
|
|
46
48
|
* local node, `/context` opens local `crtr node inspect context`. `/graph`
|
|
47
49
|
* (the in-process GRAPH overlay, already remote-safe — see graph-overlay.ts)
|
|
48
50
|
* and `/view` (a self-contained popup unrelated to `nodeId`) stay available. */
|
|
49
|
-
const REMOTE_UNSAFE_CANVAS_NAMES = new Set(['promote', 'resume-node', 'context']);
|
|
51
|
+
const REMOTE_UNSAFE_CANVAS_NAMES = new Set(['promote', 'resume-node', 'context', 'rename']);
|
|
52
|
+
/** tmux named colours accepted by `/color` (plus `colourN`/`#rrggbb`, validated
|
|
53
|
+
* by pattern). Mirrors tmux's colour-name table so a typo is rejected with the
|
|
54
|
+
* options rather than silently ignored by tmux. */
|
|
55
|
+
const TMUX_NAMED_COLORS = new Set([
|
|
56
|
+
'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
|
|
57
|
+
'brightred', 'brightgreen', 'brightyellow', 'brightblue', 'brightmagenta', 'brightcyan',
|
|
58
|
+
'orange', 'grey', 'gray', 'default',
|
|
59
|
+
]);
|
|
50
60
|
/** Builtins with no Phase-4 engine method — scoped out (review m2). */
|
|
51
61
|
const SCOPED_OUT = new Set(['trust']);
|
|
52
62
|
/** Valid `/settings thinking` levels — the `SetThinkingLevelFrame['level']`
|
|
@@ -153,6 +163,7 @@ export function dispatchSlashCommand(text, ctx) {
|
|
|
153
163
|
if (!arg)
|
|
154
164
|
return openOrHint(ctx.openModelPicker, ctx, 'model', '/model <provider/id>');
|
|
155
165
|
ctx.send({ type: 'set_model', model: arg });
|
|
166
|
+
ctx.onExplicitModelSet?.(arg);
|
|
156
167
|
return true;
|
|
157
168
|
case 'resume':
|
|
158
169
|
if (!arg)
|
|
@@ -232,7 +243,7 @@ export function dispatchSlashCommand(text, ctx) {
|
|
|
232
243
|
}
|
|
233
244
|
/** Append the native canvas commands to a command list, skipping any already
|
|
234
245
|
* present (so they survive whatever list Unit Q feeds in — builtins, or the
|
|
235
|
-
* broker's `get_commands` result). The
|
|
246
|
+
* broker's `get_commands` result). The canvas commands are viewer-owned,
|
|
236
247
|
* so they are always surfaced in autocomplete regardless of the source list —
|
|
237
248
|
* EXCEPT `REMOTE_UNSAFE_CANVAS_NAMES` when `remote`, which are structurally
|
|
238
249
|
* omitted rather than offered-then-blocked, so a remote attach's autocomplete
|
|
@@ -256,7 +267,7 @@ export function commandAutocompleteItems(commands = BUILTIN_SLASH_COMMANDS, remo
|
|
|
256
267
|
// ---------------------------------------------------------------------------
|
|
257
268
|
// Canvas commands — native reimplementations of the canvas chrome extensions.
|
|
258
269
|
// ---------------------------------------------------------------------------
|
|
259
|
-
/** Dispatch one of the
|
|
270
|
+
/** Dispatch one of the native canvas commands. Always returns `true` (the
|
|
260
271
|
* command is viewer-owned — never forwarded to the engine as a prompt). */
|
|
261
272
|
function dispatchCanvasCommand(name, arg, ctx) {
|
|
262
273
|
// Structural omission (withCanvasCommands) keeps these out of remote-mode
|
|
@@ -269,10 +280,8 @@ function dispatchCanvasCommand(name, arg, ctx) {
|
|
|
269
280
|
}
|
|
270
281
|
switch (name) {
|
|
271
282
|
case 'graph':
|
|
272
|
-
// `/graph`
|
|
273
|
-
//
|
|
274
|
-
// unit registers the command + autocomplete entry and triggers the overlay
|
|
275
|
-
// via the `onGraph` hook the integration unit wires in.
|
|
283
|
+
// `/graph` opens the attach viewer's in-process GRAPH overlay, not a tmux
|
|
284
|
+
// popup. Dispatch reaches it through the viewer integration's `onGraph` hook.
|
|
276
285
|
if (ctx.onGraph)
|
|
277
286
|
ctx.onGraph();
|
|
278
287
|
else
|
|
@@ -293,6 +302,11 @@ function dispatchCanvasCommand(name, arg, ctx) {
|
|
|
293
302
|
// A read-only nvim explorer over this node's + its subscriptions' context
|
|
294
303
|
// dirs, as a self-contained popup.
|
|
295
304
|
return openContext(ctx);
|
|
305
|
+
case 'rename':
|
|
306
|
+
renameNode(arg, ctx);
|
|
307
|
+
return true;
|
|
308
|
+
case 'color':
|
|
309
|
+
return colorNode(arg, ctx);
|
|
296
310
|
default:
|
|
297
311
|
ctx.notify(`/${name} is not available in attach`);
|
|
298
312
|
return true;
|
|
@@ -351,6 +365,112 @@ function promoteNode(arg, ctx) {
|
|
|
351
365
|
});
|
|
352
366
|
});
|
|
353
367
|
}
|
|
368
|
+
/** `/rename <name>` — rename this node EVERYWHERE the name shows (à la Claude
|
|
369
|
+
* Code's rename): (1) the pi session name, which is what the editor's title
|
|
370
|
+
* chip ("bubble") paints — sent as a `set_session_name` frame so the chip
|
|
371
|
+
* updates on the session_info_changed round-trip; (2) the tmux pane title
|
|
372
|
+
* (visible with pane-border-status / terminal titles); (3) the canvas node row
|
|
373
|
+
* via `crtr node config --name`, which also renames the live viewer window. */
|
|
374
|
+
function renameNode(arg, ctx) {
|
|
375
|
+
const nodeId = (ctx.nodeId ?? process.env['CRTR_NODE_ID'] ?? '').trim();
|
|
376
|
+
if (nodeId === '') {
|
|
377
|
+
ctx.notify('/rename: no node to rename (viewer has no node id)');
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (arg === '') {
|
|
381
|
+
ctx.notify('Usage: /rename <name>');
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// 1. Session name → the editor title chip repaints when the state echoes back.
|
|
385
|
+
ctx.send({ type: 'set_session_name', name: arg });
|
|
386
|
+
// 2. tmux pane title (best-effort; no-op outside tmux).
|
|
387
|
+
const pane = process.env['TMUX_PANE'];
|
|
388
|
+
if (process.env['TMUX'] !== undefined && pane !== undefined && pane !== '') {
|
|
389
|
+
execFile('tmux', ['select-pane', '-t', pane, '-T', arg], () => {
|
|
390
|
+
/* best-effort */
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
// 3. Canvas node row + live viewer window.
|
|
394
|
+
execFile('crtr', ['node', 'config', '--node', nodeId, '--name', arg, '--json'], { timeout: 15_000, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
|
|
395
|
+
if (err) {
|
|
396
|
+
// On a non-zero exit crtr still prints the structured error to stdout.
|
|
397
|
+
let msg = null;
|
|
398
|
+
try {
|
|
399
|
+
const payload = JSON.parse(stdout);
|
|
400
|
+
msg = typeof payload.message === 'string' ? payload.message : null;
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
msg = null;
|
|
404
|
+
}
|
|
405
|
+
ctx.notify(`rename failed: ${msg ?? err.message}`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
ctx.notify(`Renamed node → ${arg}`);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/** Promisified one-shot tmux call — resolves stdout ('' on error; best-effort). */
|
|
412
|
+
function tmuxCall(args) {
|
|
413
|
+
return new Promise((resolve) => {
|
|
414
|
+
execFile('tmux', args, { timeout: 5_000 }, (err, stdout) => resolve(err ? '' : stdout));
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/** Paint (or clear) THIS pane's border color. `pane-border-style` is a WINDOW
|
|
418
|
+
* option — tmux silently hoists a `set-option -p` to window scope, painting
|
|
419
|
+
* every pane — so per-pane color rides a per-pane user option instead: the
|
|
420
|
+
* color lands in `@crtr_color` on this pane only, and the window's border
|
|
421
|
+
* styles are set ONCE to a format tmux expands per pane (`fg=#{@crtr_color}`
|
|
422
|
+
* when set, else the style that was in effect before — baked in at install
|
|
423
|
+
* time so uncolored panes keep the user's theme). Clearing just unsets
|
|
424
|
+
* `@crtr_color`; the format's fallback branch restores the original look. */
|
|
425
|
+
async function paintPaneBorder(pane, value, clear) {
|
|
426
|
+
if (clear) {
|
|
427
|
+
await tmuxCall(['set-option', '-p', '-u', '-t', pane, '@crtr_color']);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
await tmuxCall(['set-option', '-p', '-t', pane, '@crtr_color', value]);
|
|
431
|
+
for (const opt of ['pane-border-style', 'pane-active-border-style']) {
|
|
432
|
+
const cur = (await tmuxCall(['show-options', '-wqv', '-t', pane, opt])).trim();
|
|
433
|
+
if (cur.includes('@crtr_color'))
|
|
434
|
+
continue; // our format is already installed on this window
|
|
435
|
+
// Bake the pre-existing effective style into the fallback branch (window
|
|
436
|
+
// value, else global, else tmux's defaults) — commas escaped so they don't
|
|
437
|
+
// split the #{?,,} conditional.
|
|
438
|
+
let base = cur !== '' ? cur : (await tmuxCall(['show-options', '-gqv', opt])).trim();
|
|
439
|
+
if (base === '')
|
|
440
|
+
base = opt === 'pane-active-border-style' ? 'fg=green' : 'default';
|
|
441
|
+
const fmt = `#{?#{!=:#{@crtr_color},},fg=#{@crtr_color},${base.replace(/,/g, '#,')}}`;
|
|
442
|
+
await tmuxCall(['set-option', '-w', '-t', pane, opt, fmt]);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/** `/color <color>` — color this node's two identity surfaces in one stroke:
|
|
446
|
+
* the tmux PANE OUTLINE (via `paintPaneBorder` above — per-pane, not the whole
|
|
447
|
+
* window) and the editor's NAME CHIP (the bubble `/rename` names), via the
|
|
448
|
+
* `onColor` hook attach-cmd wires. Ephemeral by design: the border rides the
|
|
449
|
+
* tmux pane and the chip lives in this viewer process. `/color none` clears
|
|
450
|
+
* both. Remote-SAFE: both surfaces are local to this viewer. */
|
|
451
|
+
function colorNode(arg, ctx) {
|
|
452
|
+
const value = arg.trim().toLowerCase();
|
|
453
|
+
if (value === '') {
|
|
454
|
+
ctx.notify('Usage: /color <color> — a name (red, blue, orange…), colourN, or #rrggbb; /color none to clear');
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
const clear = value === 'none' || value === 'clear';
|
|
458
|
+
const valid = clear || TMUX_NAMED_COLORS.has(value) || /^colour\d{1,3}$/.test(value) || /^#[0-9a-f]{6}$/.test(value);
|
|
459
|
+
if (!valid) {
|
|
460
|
+
ctx.notify(`Invalid color "${value}" — use a tmux colour name (${['red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'orange'].join(', ')}, …), colourN, or #rrggbb`);
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
// Pane outline — per-pane @crtr_color + a per-pane-expanded window format,
|
|
464
|
+
// so only THIS node's pane recolors (fire-and-forget; best-effort).
|
|
465
|
+
const pane = process.env['TMUX_PANE'];
|
|
466
|
+
const inTmux = process.env['TMUX'] !== undefined && pane !== undefined && pane !== '';
|
|
467
|
+
if (inTmux)
|
|
468
|
+
void paintPaneBorder(pane, value, clear);
|
|
469
|
+
// Name chip — the viewer owns the editor, so this rides the onColor hook.
|
|
470
|
+
ctx.onColor?.(clear ? null : value);
|
|
471
|
+
ctx.notify(clear ? 'Node color cleared' : `Node colored ${value}${inTmux ? '' : ' (chip only — no tmux pane)'}`);
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
354
474
|
/** `/resume-node`. Opens `crtr canvas browse` as a tmux popup,
|
|
355
475
|
* scoped to this node's cwd; on Enter it focuses the chosen node back into this
|
|
356
476
|
* pane via `crtr node focus --pane` (which `canvas browse` shells). */
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { CustomEditor } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { type EditorOptions, type EditorTheme, type TUI } from '@earendil-works/pi-tui';
|
|
3
3
|
/** Thinking levels pi cycles through (shift+tab), lowest → highest budget. */
|
|
4
|
-
export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
|
4
|
+
export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
5
|
+
/** Build a title-chip styler for a `/color` token (a tmux colour name, `#rrggbb`,
|
|
6
|
+
* or `colourN`): the colour as the chip BACKGROUND with contrast-picked bold
|
|
7
|
+
* text, matching the thinking-chip treatment. `undefined` for an unrenderable
|
|
8
|
+
* token (e.g. `default`) — the caller keeps its fallback style. */
|
|
9
|
+
export declare function colorChipStyle(token: string): ((s: string) => string) | undefined;
|
|
5
10
|
/** The default (thinking `off`) title chip: reverse video + a space of padding
|
|
6
11
|
* each side, so the name reads as a label sitting on the border rule. Used as
|
|
7
12
|
* the fallback when no thinking color applies. */
|
|
@@ -20,6 +25,7 @@ export declare function thinkingTitleStyle(level: string | undefined, fallback:
|
|
|
20
25
|
* and truncates when it only partially fits. Exported pure for the overflow
|
|
21
26
|
* regression test. */
|
|
22
27
|
export declare function composeTopBorder(width: number, title: string, info: string, titleStyle: (s: string) => string, borderColor: (s: string) => string): string;
|
|
28
|
+
export declare function outlineCursorLine(line: string): string | undefined;
|
|
23
29
|
export declare class TitledEditor extends CustomEditor {
|
|
24
30
|
/** crtr's OWN keybindings manager — the same one CustomEditor matches `app.*`
|
|
25
31
|
* against. We keep a reference so `handleInput` can resolve the newline chord
|
|
@@ -51,5 +57,10 @@ export declare class TitledEditor extends CustomEditor {
|
|
|
51
57
|
titleStyle: (s: string) => string;
|
|
52
58
|
/** Mode badge colorizer, matched to the current mode type. */
|
|
53
59
|
modeStyle: (s: string) => string;
|
|
60
|
+
/** Whether the hosting tmux pane / terminal window has FOCUS (terminal focus
|
|
61
|
+
* reporting, DECSET 1004 — attach-cmd flips this on `\x1b[I` / `\x1b[O`).
|
|
62
|
+
* Distinct from TUI component focus: the editor keeps component focus while
|
|
63
|
+
* its pane is blurred. False → the fake cursor renders as an outline. */
|
|
64
|
+
paneFocused: boolean;
|
|
54
65
|
render(width: number): string[];
|
|
55
66
|
}
|
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
// on each state update), mirroring pi's `getThinkingBorderColor`.
|
|
7
7
|
// Both are pure render-layer chrome; nothing here touches the socket or session.
|
|
8
8
|
import { CustomEditor } from '@earendil-works/pi-coding-agent';
|
|
9
|
-
import { truncateToWidth, visibleWidth, } from '@earendil-works/pi-tui';
|
|
10
|
-
/** The thinking
|
|
11
|
-
* its orange complement
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* contrast — a lighter blue/orange washes the label out. */
|
|
9
|
+
import { CURSOR_MARKER, truncateToWidth, visibleWidth, } from '@earendil-works/pi-tui';
|
|
10
|
+
/** The standard thinking levels form a deliberate cool→warm ramp between blue
|
|
11
|
+
* and its orange complement. `max` is a distinct, intense cyan endpoint so it
|
|
12
|
+
* never falls back to the neutral default border. `off` alone falls back to
|
|
13
|
+
* the caller's default border color. The colors double as title-chip
|
|
14
|
+
* BACKGROUNDS under bold white text, so each must remain dark enough for the
|
|
15
|
+
* label to read clearly. */
|
|
17
16
|
const THINKING_START = [46, 92, 190]; // deep blue
|
|
18
17
|
const THINKING_END = [166, 98, 24]; // deep amber
|
|
18
|
+
const THINKING_MAX = [0, 145, 170]; // intense cyan
|
|
19
19
|
function interpolateRgb(start, end, t) {
|
|
20
20
|
return [
|
|
21
21
|
Math.round(start[0] + (end[0] - start[0]) * t),
|
|
@@ -30,7 +30,55 @@ const THINKING_RGB = {
|
|
|
30
30
|
medium: interpolateRgb(THINKING_START, THINKING_END, 2 / 4),
|
|
31
31
|
high: interpolateRgb(THINKING_START, THINKING_END, 3 / 4),
|
|
32
32
|
xhigh: interpolateRgb(THINKING_START, THINKING_END, 4 / 4),
|
|
33
|
+
max: THINKING_MAX,
|
|
33
34
|
};
|
|
35
|
+
/** Named colours `/color` accepts, as RGB — mirrors the tmux colour-name table
|
|
36
|
+
* (so the chip and the pane border read as the same hue). */
|
|
37
|
+
const NAMED_RGB = {
|
|
38
|
+
black: [0, 0, 0],
|
|
39
|
+
red: [205, 49, 49],
|
|
40
|
+
green: [13, 188, 121],
|
|
41
|
+
yellow: [229, 229, 16],
|
|
42
|
+
blue: [36, 114, 200],
|
|
43
|
+
magenta: [188, 63, 188],
|
|
44
|
+
cyan: [17, 168, 205],
|
|
45
|
+
white: [229, 229, 229],
|
|
46
|
+
orange: [228, 152, 68],
|
|
47
|
+
grey: [128, 128, 128],
|
|
48
|
+
gray: [128, 128, 128],
|
|
49
|
+
brightred: [241, 76, 76],
|
|
50
|
+
brightgreen: [35, 209, 139],
|
|
51
|
+
brightyellow: [245, 245, 67],
|
|
52
|
+
brightblue: [59, 142, 234],
|
|
53
|
+
brightmagenta: [214, 112, 214],
|
|
54
|
+
brightcyan: [41, 184, 219],
|
|
55
|
+
};
|
|
56
|
+
/** Build a title-chip styler for a `/color` token (a tmux colour name, `#rrggbb`,
|
|
57
|
+
* or `colourN`): the colour as the chip BACKGROUND with contrast-picked bold
|
|
58
|
+
* text, matching the thinking-chip treatment. `undefined` for an unrenderable
|
|
59
|
+
* token (e.g. `default`) — the caller keeps its fallback style. */
|
|
60
|
+
export function colorChipStyle(token) {
|
|
61
|
+
const t = token.trim().toLowerCase();
|
|
62
|
+
const c256 = /^colour(\d{1,3})$/.exec(t);
|
|
63
|
+
if (c256 !== null)
|
|
64
|
+
return (s) => `\x1b[48;5;${c256[1]}m\x1b[97m\x1b[1m ${s} \x1b[0m`;
|
|
65
|
+
let rgb;
|
|
66
|
+
const hex = /^#([0-9a-f]{6})$/.exec(t);
|
|
67
|
+
if (hex !== null) {
|
|
68
|
+
const h = hex[1];
|
|
69
|
+
rgb = [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
rgb = NAMED_RGB[t];
|
|
73
|
+
}
|
|
74
|
+
if (rgb === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
const [r, g, b] = rgb;
|
|
77
|
+
// Pick black-on-light / white-on-dark so the name stays readable on any hue.
|
|
78
|
+
const luma = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
79
|
+
const fg = luma > 160 ? '\x1b[30m' : '\x1b[97m';
|
|
80
|
+
return (s) => `\x1b[48;2;${r};${g};${b}m${fg}\x1b[1m ${s} \x1b[0m`;
|
|
81
|
+
}
|
|
34
82
|
/** The default (thinking `off`) title chip: reverse video + a space of padding
|
|
35
83
|
* each side, so the name reads as a label sitting on the border rule. Used as
|
|
36
84
|
* the fallback when no thinking color applies. */
|
|
@@ -71,6 +119,39 @@ export function composeTopBorder(width, title, info, titleStyle, borderColor) {
|
|
|
71
119
|
const fill = Math.max(0, width - chipW - visibleWidth(infoChip));
|
|
72
120
|
return chip + borderColor('─'.repeat(fill)) + infoChip;
|
|
73
121
|
}
|
|
122
|
+
/** The stock pi-tui editor paints its fake cursor as a solid reverse-video
|
|
123
|
+
* block (`\x1b[7m…\x1b[0m`) unconditionally — in a wall of attach panes every
|
|
124
|
+
* pane shows an identical solid white cursor, and nothing tells you which one
|
|
125
|
+
* keys actually go to. When the hosting pane/terminal is UNFOCUSED we rewrite
|
|
126
|
+
* that cell into a MUTED cursor, mirroring how real terminals de-emphasize
|
|
127
|
+
* the hardware cursor on blur. A pixel-true full-cell outline isn't possible
|
|
128
|
+
* in text cells, and hollow-box glyphs (▯/⍘/□) render undersized, so the
|
|
129
|
+
* muted form is a full-size block in a dim gray background — same footprint
|
|
130
|
+
* as the focused cursor, clearly not the bright active one (ruling: tried the
|
|
131
|
+
* glyphs, Silas preferred the dim block). The rewrite anchors on pi-tui's
|
|
132
|
+
* zero-width CURSOR_MARKER (emitted right before the fake cursor while the
|
|
133
|
+
* editor component has TUI focus) so it can never touch reverse video
|
|
134
|
+
* elsewhere in the line. Returns undefined when the line carries no cursor.
|
|
135
|
+
* Exported pure for tests. */
|
|
136
|
+
const SOLID_CURSOR_PREFIX = '\x1b[7m';
|
|
137
|
+
const SOLID_CURSOR_RESET = '\x1b[0m';
|
|
138
|
+
/** Dim gray bg, default fg — the char under the cursor stays readable. */
|
|
139
|
+
const MUTED_CURSOR_ON = '\x1b[48;5;240m';
|
|
140
|
+
export function outlineCursorLine(line) {
|
|
141
|
+
const m = line.indexOf(CURSOR_MARKER);
|
|
142
|
+
if (m === -1)
|
|
143
|
+
return undefined;
|
|
144
|
+
const start = m + CURSOR_MARKER.length;
|
|
145
|
+
if (!line.startsWith(SOLID_CURSOR_PREFIX, start))
|
|
146
|
+
return undefined;
|
|
147
|
+
const gStart = start + SOLID_CURSOR_PREFIX.length;
|
|
148
|
+
const end = line.indexOf(SOLID_CURSOR_RESET, gStart);
|
|
149
|
+
if (end === -1)
|
|
150
|
+
return undefined;
|
|
151
|
+
const grapheme = line.slice(gStart, end);
|
|
152
|
+
const replacement = `${MUTED_CURSOR_ON}${grapheme}\x1b[0m`;
|
|
153
|
+
return line.slice(0, start) + replacement + line.slice(end + SOLID_CURSOR_RESET.length);
|
|
154
|
+
}
|
|
74
155
|
export class TitledEditor extends CustomEditor {
|
|
75
156
|
/** crtr's OWN keybindings manager — the same one CustomEditor matches `app.*`
|
|
76
157
|
* against. We keep a reference so `handleInput` can resolve the newline chord
|
|
@@ -114,8 +195,22 @@ export class TitledEditor extends CustomEditor {
|
|
|
114
195
|
titleStyle = defaultTitleStyle;
|
|
115
196
|
/** Mode badge colorizer, matched to the current mode type. */
|
|
116
197
|
modeStyle = defaultTitleStyle;
|
|
198
|
+
/** Whether the hosting tmux pane / terminal window has FOCUS (terminal focus
|
|
199
|
+
* reporting, DECSET 1004 — attach-cmd flips this on `\x1b[I` / `\x1b[O`).
|
|
200
|
+
* Distinct from TUI component focus: the editor keeps component focus while
|
|
201
|
+
* its pane is blurred. False → the fake cursor renders as an outline. */
|
|
202
|
+
paneFocused = true;
|
|
117
203
|
render(width) {
|
|
118
204
|
const lines = super.render(width);
|
|
205
|
+
if (!this.paneFocused) {
|
|
206
|
+
for (let i = 0; i < lines.length; i++) {
|
|
207
|
+
const hollowed = outlineCursorLine(lines[i]);
|
|
208
|
+
if (hollowed !== undefined) {
|
|
209
|
+
lines[i] = hollowed;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
119
214
|
if ((this.title || this.info) && lines.length > 0) {
|
|
120
215
|
// Replace the stock top border; dashes in the current (thinking-aware)
|
|
121
216
|
// border color.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// `crtr canvas browse` — the interactive full-screen canvas navigator.
|
|
2
2
|
//
|
|
3
|
-
// A raw-mode TUI over the WHOLE canvas: tabs (All/Live/Dormant/
|
|
3
|
+
// A raw-mode TUI over the WHOLE canvas: tabs (All/Live/Dormant/Attention/Pinned), an
|
|
4
4
|
// auto-collapsed tree, and `/` fuzzy search. Enter resumes the chosen node via
|
|
5
5
|
// `crtr node focus` (the ONLY sanctioned open — reviveNode, never `pi --session`).
|
|
6
6
|
// Owns the screen, so it returns void and writes nothing to stdout itself.
|
|
@@ -14,7 +14,7 @@ import { readState } from '../core/config.js';
|
|
|
14
14
|
export const browseLeaf = defineLeaf({
|
|
15
15
|
name: 'browse',
|
|
16
16
|
description: 'open the interactive canvas navigator (tabs/tree/search)',
|
|
17
|
-
whenToUse: 'you want to VIEW and NAVIGATE the whole canvas interactively — a full-screen TUI with tabs (All/Live/Dormant/
|
|
17
|
+
whenToUse: 'you want to VIEW and NAVIGATE the whole canvas interactively — a full-screen TUI with tabs (All/Live/Dormant/Attention/Pinned), an expandable tree (children auto-collapsed; → to expand), `/` fuzzy search, `f` to pin, `m` to message and `R` to rename a node in place, and `?` for the full keymap; Enter resumes the chosen node. Use this to find your way around a large canvas. Use `canvas dashboard` instead for a one-shot ASCII tree you can pipe, and `node inspect list` for a flat machine-readable roster',
|
|
18
18
|
help: {
|
|
19
19
|
name: 'canvas browse',
|
|
20
20
|
summary: 'interactive full-screen canvas navigator — tabs, an auto-collapsed tree, and `/` fuzzy search; Enter resumes the chosen node via `crtr node focus`. Outside a TTY it prints the static forest and exits',
|
|
@@ -9,7 +9,7 @@ import { appendInbox } from '../../core/feed/inbox.js';
|
|
|
9
9
|
import { closeSync, existsSync, linkSync, openSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { basename, join } from 'node:path';
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
|
-
import {
|
|
12
|
+
import { openInbox, scanInbox, parseDeck, deckPath, responsePath, progressPath, isResolved, atomicWriteJson, ask, launchReview, readJson, display, } from '@crouton-kit/humanloop';
|
|
13
13
|
import { killPane } from './shared.js';
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// stranded-answer healing
|
|
@@ -158,8 +158,8 @@ export const humanInbox = defineLeaf({
|
|
|
158
158
|
],
|
|
159
159
|
},
|
|
160
160
|
run: async () => {
|
|
161
|
-
await
|
|
162
|
-
// humanloop's
|
|
161
|
+
await openInbox({ roots: [interactionsRoot(process.cwd())] });
|
|
162
|
+
// humanloop's openInbox() writes response.json but never calls pushFinal — so a
|
|
163
163
|
// deck drained here would strand (no answer-back, leaked bridge node). Heal
|
|
164
164
|
// every resolved-but-live interaction: deliver its answer to the asker + reap.
|
|
165
165
|
const delivered = await healStrandedInteractions(process.cwd());
|
|
@@ -766,7 +766,6 @@ export const humanRun = defineLeaf({
|
|
|
766
766
|
try {
|
|
767
767
|
const res = await launchReview(rc.file, {
|
|
768
768
|
output: rc.output,
|
|
769
|
-
noTmux: true,
|
|
770
769
|
});
|
|
771
770
|
await pushFinal(rc.job_id, JSON.stringify(res));
|
|
772
771
|
}
|