@crouton-kit/crouter 0.3.27 → 0.3.29
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/dist/build-root.js +4 -2
- package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
- package/dist/builtin-memory/crouter-development/personas.md +1 -1
- package/dist/builtin-memory/crouter-development/plugins.md +20 -18
- package/dist/builtin-memory/design.md +1 -1
- package/dist/builtin-memory/development.md +1 -1
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/planning.md +1 -1
- package/dist/builtin-memory/spec.md +6 -4
- package/dist/builtin-personas/orchestration-kernel.md +4 -5
- package/dist/builtin-personas/runtime-base.md +8 -4
- package/dist/builtin-personas/spec/PERSONA.md +8 -4
- package/dist/builtin-personas/spec/orchestrator.md +5 -3
- package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
- package/dist/builtin-views/canvas/core.mjs +586 -0
- package/dist/builtin-views/canvas/text.mjs +58 -0
- package/dist/builtin-views/canvas/tui.mjs +165 -0
- package/dist/builtin-views/canvas/web.jsx +120 -0
- package/dist/builtin-views/git-pr/core.mjs +673 -0
- package/dist/builtin-views/git-pr/text.mjs +84 -0
- package/dist/builtin-views/git-pr/tui.mjs +302 -0
- package/dist/builtin-views/git-pr/web.jsx +216 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
- package/dist/builtin-views/inbox/core.mjs +1273 -0
- package/dist/builtin-views/inbox/text.mjs +73 -0
- package/dist/builtin-views/inbox/tui.mjs +312 -0
- package/dist/builtin-views/inbox/web.jsx +188 -0
- package/dist/builtin-views/linkedin/core.mjs +906 -0
- package/dist/builtin-views/linkedin/text.mjs +69 -0
- package/dist/builtin-views/linkedin/tui.mjs +427 -0
- package/dist/builtin-views/linkedin/web.jsx +206 -0
- package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
- package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
- package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
- package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
- package/dist/cli.js +3 -3
- package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
- package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
- package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
- package/dist/clients/attach/__tests__/git-info.test.js +31 -0
- package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
- package/dist/clients/attach/__tests__/reconnect-giveup.test.js +30 -0
- package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
- package/dist/clients/attach/__tests__/titled-editor.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
- package/dist/clients/attach/attach-cmd.js +445 -41
- package/dist/clients/attach/auth-pickers.d.ts +9 -0
- package/dist/clients/attach/auth-pickers.js +194 -0
- package/dist/clients/attach/canvas-panels.d.ts +12 -0
- package/dist/clients/attach/canvas-panels.js +78 -0
- package/dist/clients/attach/chat-view.d.ts +27 -2
- package/dist/clients/attach/chat-view.js +98 -9
- package/dist/clients/attach/clipboard-image.d.ts +47 -8
- package/dist/clients/attach/clipboard-image.js +150 -26
- package/dist/clients/attach/clipboard-text.d.ts +3 -0
- package/dist/clients/attach/clipboard-text.js +39 -0
- package/dist/clients/attach/config-load.d.ts +54 -7
- package/dist/clients/attach/config-load.js +99 -3
- package/dist/clients/attach/context-message.d.ts +12 -0
- package/dist/clients/attach/context-message.js +72 -0
- package/dist/clients/attach/git-info.d.ts +20 -0
- package/dist/clients/attach/git-info.js +44 -0
- package/dist/clients/attach/graph-overlay.d.ts +43 -0
- package/dist/clients/attach/graph-overlay.js +306 -0
- package/dist/clients/attach/input-controller.d.ts +81 -13
- package/dist/clients/attach/input-controller.js +307 -67
- package/dist/clients/attach/pickers.d.ts +58 -0
- package/dist/clients/attach/pickers.js +171 -0
- package/dist/clients/attach/slash-commands.d.ts +44 -1
- package/dist/clients/attach/slash-commands.js +250 -27
- package/dist/clients/attach/titled-editor.d.ts +33 -0
- package/dist/clients/attach/titled-editor.js +84 -0
- package/dist/clients/attach/view-socket.d.ts +37 -2
- package/dist/clients/attach/view-socket.js +131 -7
- package/dist/clients/web/dev-server.d.ts +7 -0
- package/dist/clients/web/dev-server.js +59 -0
- package/dist/clients/web/events.d.ts +14 -0
- package/dist/clients/web/events.js +151 -0
- package/dist/clients/web/server.d.ts +18 -0
- package/dist/clients/web/server.js +450 -0
- package/dist/clients/web/web-cmd.d.ts +2 -0
- package/dist/clients/web/web-cmd.js +120 -0
- package/dist/commands/canvas.js +1 -2
- package/dist/commands/chord.js +1 -1
- package/dist/commands/dashboard.js +5 -1
- package/dist/commands/human/prompts.js +3 -3
- package/dist/commands/human/queue.d.ts +17 -0
- package/dist/commands/human/queue.js +111 -4
- package/dist/commands/memory/__tests__/lint-schema.test.js +27 -4
- package/dist/commands/memory/lint.d.ts +5 -4
- package/dist/commands/memory/lint.js +23 -7
- package/dist/commands/memory/read.js +2 -2
- package/dist/commands/memory/shared.d.ts +7 -1
- package/dist/commands/memory/shared.js +18 -3
- package/dist/commands/memory/write.js +20 -7
- package/dist/commands/memory.js +4 -4
- package/dist/commands/node.d.ts +1 -1
- package/dist/commands/node.js +64 -65
- package/dist/commands/pkg.js +1 -2
- package/dist/commands/revive.js +11 -3
- package/dist/commands/sys/feedback.d.ts +1 -0
- package/dist/commands/sys/feedback.js +163 -0
- package/dist/commands/sys/sync.d.ts +1 -0
- package/dist/commands/sys/sync.js +187 -0
- package/dist/commands/sys.js +4 -2
- package/dist/commands/view-cycle.js +2 -2
- package/dist/commands/view-list.js +8 -8
- package/dist/commands/view-new.js +21 -17
- package/dist/commands/view-pick.js +1 -1
- package/dist/commands/view-run.js +35 -14
- package/dist/commands/view.js +8 -6
- package/dist/commands/workspace.d.ts +2 -0
- package/dist/commands/workspace.js +161 -0
- package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
- package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
- package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
- package/dist/core/__tests__/broker-snapshot-history.test.d.ts +1 -0
- package/dist/core/__tests__/broker-snapshot-history.test.js +105 -0
- package/dist/core/__tests__/child-death-wake.test.js +56 -111
- package/dist/core/__tests__/close.test.js +14 -20
- package/dist/core/__tests__/context-intro.test.js +19 -19
- package/dist/core/__tests__/daemon-boot.test.js +30 -23
- package/dist/core/__tests__/draw-style.test.js +1 -1
- package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +32 -2
- package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
- package/dist/core/__tests__/focuses.test.js +25 -13
- package/dist/core/__tests__/fork.test.js +22 -5
- package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
- package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
- package/dist/core/__tests__/full/cascade-close.test.js +22 -8
- package/dist/core/__tests__/full/detach-focus.test.js +66 -169
- package/dist/core/__tests__/helpers/harness.d.ts +6 -9
- package/dist/core/__tests__/helpers/harness.js +19 -36
- package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
- package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
- package/dist/core/__tests__/human-stranded-deliver.test.d.ts +1 -0
- package/dist/core/__tests__/human-stranded-deliver.test.js +108 -0
- package/dist/core/__tests__/lifecycle.test.js +1 -19
- package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
- package/dist/core/__tests__/memory-resolver.test.js +1 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.d.ts +1 -0
- package/dist/core/__tests__/on-read-dedup-resume.test.js +81 -0
- package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
- package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch-root.test.js +114 -0
- package/dist/core/__tests__/reset.test.js +19 -79
- package/dist/core/__tests__/steer-note.test.js +9 -7
- package/dist/core/__tests__/tmux-surface.test.js +13 -7
- package/dist/core/bootstrap.d.ts +0 -3
- package/dist/core/bootstrap.js +1 -143
- package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
- package/dist/core/canvas/browse/app.js +131 -8
- package/dist/core/canvas/browse/model.d.ts +23 -5
- package/dist/core/canvas/browse/model.js +68 -13
- package/dist/core/canvas/browse/render.d.ts +7 -1
- package/dist/core/canvas/browse/render.js +178 -47
- package/dist/core/canvas/canvas.js +1 -1
- package/dist/core/canvas/focuses.d.ts +4 -4
- package/dist/core/canvas/focuses.js +7 -7
- package/dist/core/canvas/nav-model.d.ts +162 -0
- package/dist/core/canvas/nav-model.js +487 -0
- package/dist/core/canvas/paths.d.ts +7 -0
- package/dist/core/canvas/paths.js +9 -0
- package/dist/core/canvas/render.d.ts +68 -9
- package/dist/core/canvas/render.js +195 -61
- package/dist/core/canvas/types.d.ts +16 -23
- package/dist/core/config.js +1 -4
- package/dist/core/memory-resolver.js +43 -5
- package/dist/core/runtime/bearings.d.ts +1 -1
- package/dist/core/runtime/bearings.js +8 -8
- package/dist/core/runtime/branded-host.d.ts +16 -0
- package/dist/core/runtime/branded-host.js +127 -0
- package/dist/core/runtime/broker-protocol.d.ts +212 -6
- package/dist/core/runtime/broker-sdk.d.ts +0 -12
- package/dist/core/runtime/broker-sdk.js +77 -6
- package/dist/core/runtime/broker.d.ts +5 -1
- package/dist/core/runtime/broker.js +520 -59
- package/dist/core/runtime/close.d.ts +12 -2
- package/dist/core/runtime/close.js +37 -16
- package/dist/core/runtime/front-door.js +14 -7
- package/dist/core/runtime/host.d.ts +14 -34
- package/dist/core/runtime/host.js +14 -50
- package/dist/core/runtime/launch.d.ts +12 -9
- package/dist/core/runtime/launch.js +27 -15
- package/dist/core/runtime/lifecycle.d.ts +1 -1
- package/dist/core/runtime/lifecycle.js +15 -19
- package/dist/core/runtime/nodes.d.ts +0 -55
- package/dist/core/runtime/nodes.js +6 -74
- package/dist/core/runtime/placement.d.ts +91 -334
- package/dist/core/runtime/placement.js +262 -828
- package/dist/core/runtime/promote.d.ts +2 -0
- package/dist/core/runtime/promote.js +24 -6
- package/dist/core/runtime/recap.d.ts +8 -0
- package/dist/core/runtime/recap.js +107 -0
- package/dist/core/runtime/recycle.js +25 -61
- package/dist/core/runtime/reset.d.ts +43 -37
- package/dist/core/runtime/reset.js +131 -218
- package/dist/core/runtime/revive.d.ts +9 -29
- package/dist/core/runtime/revive.js +45 -135
- package/dist/core/runtime/spawn.d.ts +15 -11
- package/dist/core/runtime/spawn.js +168 -136
- package/dist/core/runtime/surface-bg.d.ts +11 -0
- package/dist/core/runtime/surface-bg.js +68 -0
- package/dist/core/runtime/tmux.d.ts +15 -43
- package/dist/core/runtime/tmux.js +38 -80
- package/dist/core/scope.d.ts +1 -2
- package/dist/core/scope.js +1 -5
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
- package/dist/core/skill-sync/builtins.d.ts +42 -0
- package/dist/core/skill-sync/builtins.js +112 -0
- package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
- package/dist/core/skill-sync/claude-plugins.js +71 -0
- package/dist/core/skill-sync/engine.d.ts +42 -0
- package/dist/core/skill-sync/engine.js +633 -0
- package/dist/core/skill-sync/export.d.ts +23 -0
- package/dist/core/skill-sync/export.js +86 -0
- package/dist/core/skill-sync/manifest.d.ts +64 -0
- package/dist/core/skill-sync/manifest.js +181 -0
- package/dist/core/skill-sync/profile.d.ts +76 -0
- package/dist/core/skill-sync/profile.js +173 -0
- package/dist/core/skill-sync/snapshot.d.ts +57 -0
- package/dist/core/skill-sync/snapshot.js +120 -0
- package/dist/core/spawn.d.ts +14 -0
- package/dist/core/spawn.js +29 -9
- package/dist/core/substrate/index.d.ts +2 -2
- package/dist/core/substrate/index.js +7 -7
- package/dist/core/substrate/injected-store.d.ts +10 -0
- package/dist/core/substrate/injected-store.js +55 -0
- package/dist/core/substrate/on-read.js +68 -6
- package/dist/core/substrate/render.d.ts +8 -11
- package/dist/core/substrate/render.js +29 -43
- package/dist/core/substrate/schema.d.ts +16 -11
- package/dist/core/substrate/schema.js +32 -31
- package/dist/core/tui/host.d.ts +12 -4
- package/dist/core/tui/host.js +280 -149
- package/dist/core/view/bridge.d.ts +10 -0
- package/dist/core/view/bridge.js +31 -0
- package/dist/core/view/chrome.d.ts +9 -0
- package/dist/core/view/chrome.js +22 -0
- package/dist/core/view/contract.d.ts +171 -0
- package/dist/core/view/contract.js +23 -0
- package/dist/core/view/loader.d.ts +31 -0
- package/dist/core/view/loader.js +188 -0
- package/dist/core/view/transport-local.d.ts +7 -0
- package/dist/core/view/transport-local.js +70 -0
- package/dist/core/view/transport.d.ts +4 -0
- package/dist/core/view/transport.js +15 -0
- package/dist/daemon/crtrd.d.ts +38 -27
- package/dist/daemon/crtrd.js +303 -376
- package/dist/daemon/manage.js +6 -1
- package/dist/index.js +1 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
- package/dist/pi-extensions/canvas-context-intro.js +4 -4
- package/dist/pi-extensions/canvas-doc-substrate.js +29 -20
- package/dist/pi-extensions/canvas-nav.js +35 -385
- package/dist/pi-extensions/canvas-recap.d.ts +37 -0
- package/dist/pi-extensions/canvas-recap.js +236 -0
- package/dist/pi-extensions/canvas-resume.js +2 -1
- package/dist/pi-extensions/canvas-stophook.d.ts +15 -13
- package/dist/pi-extensions/canvas-stophook.js +119 -103
- package/dist/pi-extensions/canvas-view.js +2 -1
- package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
- package/dist/pi-extensions/widget-order-bus.js +34 -0
- package/dist/prompts/view.d.ts +2 -2
- package/dist/prompts/view.js +145 -73
- package/dist/types.d.ts +1 -6
- package/dist/types.js +1 -3
- package/dist/web/ViewChrome.d.ts +7 -0
- package/dist/web/ViewChrome.js +28 -0
- package/dist/web/ViewPane.d.ts +39 -0
- package/dist/web/ViewPane.js +48 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +16 -0
- package/dist/web/runtime.d.ts +39 -0
- package/dist/web/runtime.js +133 -0
- package/dist/web/states.d.ts +24 -0
- package/dist/web/states.js +24 -0
- package/dist/web/transport-http.d.ts +5 -0
- package/dist/web/transport-http.js +28 -0
- package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
- package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
- package/dist/web-client/index.html +13 -0
- package/package.json +20 -6
- package/dist/builtin-views/canvas/client.mjs +0 -303
- package/dist/builtin-views/canvas/view.mjs +0 -576
- package/dist/builtin-views/git-pr/client.mjs +0 -440
- package/dist/builtin-views/git-pr/view.mjs +0 -675
- package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
- package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
- package/dist/builtin-views/inbox/view.mjs +0 -889
- package/dist/builtin-views/linkedin/client.mjs +0 -610
- package/dist/builtin-views/linkedin/view.mjs +0 -1171
- package/dist/commands/pkg/bridge.d.ts +0 -1
- package/dist/commands/pkg/bridge.js +0 -137
- package/dist/commands/skill/author.d.ts +0 -3
- package/dist/commands/skill/author.js +0 -140
- package/dist/commands/skill/shared.d.ts +0 -3
- package/dist/commands/skill/shared.js +0 -19
- package/dist/commands/skill.d.ts +0 -2
- package/dist/commands/skill.js +0 -21
- package/dist/commands/tmux-spread.d.ts +0 -2
- package/dist/commands/tmux-spread.js +0 -144
- package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
- package/dist/core/__tests__/full/placement-focus.test.js +0 -309
- package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
- package/dist/core/__tests__/full/placement-revive.test.js +0 -238
- package/dist/core/__tests__/full/placement-teardown.test.js +0 -221
- package/dist/core/__tests__/home-session.test.js +0 -186
- package/dist/core/__tests__/relaunch.test.js +0 -335
- package/dist/core/bridge-map.d.ts +0 -19
- package/dist/core/bridge-map.js +0 -73
- package/dist/core/tui/contract.d.ts +0 -83
- package/dist/core/tui/contract.js +0 -8
- package/dist/core/tui/loader.d.ts +0 -16
- package/dist/core/tui/loader.js +0 -94
- package/dist/prompts/skill.d.ts +0 -2
- package/dist/prompts/skill.js +0 -650
- /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
- /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/reconnect-giveup.test.d.ts} +0 -0
- /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
|
@@ -15,18 +15,44 @@
|
|
|
15
15
|
// `crtr attach <node>` form is not expressible without a framework change. attach
|
|
16
16
|
// is therefore a BRANCH wrapping a single leaf: `crtr attach to <node>`.
|
|
17
17
|
import { randomUUID } from 'node:crypto';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { execFile } from 'node:child_process';
|
|
19
|
+
import { CombinedAutocompleteProvider, Container, ProcessTerminal, Text, TUI, matchesKey, truncateToWidth, } from '@earendil-works/pi-tui';
|
|
20
|
+
import { DynamicBorder, getSelectListTheme, } from '@earendil-works/pi-coding-agent';
|
|
20
21
|
import { defineBranch, defineLeaf } from '../../core/command.js';
|
|
21
22
|
import { InputError } from '../../core/io.js';
|
|
22
|
-
import { getNode } from '../../core/canvas/index.js';
|
|
23
|
+
import { getNode, getRow } from '../../core/canvas/index.js';
|
|
23
24
|
// tmux driver verbs only through placement (the §5.1 model-over-driver seam) —
|
|
24
25
|
// never `core/runtime/tmux.js` directly.
|
|
25
|
-
import { setPaneOption, currentTmux } from '../../core/runtime/placement.js';
|
|
26
|
+
import { setPaneOption, currentTmux, inTmux, installMenuBinding } from '../../core/runtime/placement.js';
|
|
27
|
+
import { climbRoot, visibleWidth } from '../../core/canvas/nav-model.js';
|
|
26
28
|
import { ChatView } from './chat-view.js';
|
|
27
29
|
import { InputController } from './input-controller.js';
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
+
import { writeClipboardText } from './clipboard-text.js';
|
|
31
|
+
import { buildCanvasPanelLines } from './canvas-panels.js';
|
|
32
|
+
import { GraphOverlay } from './graph-overlay.js';
|
|
33
|
+
import { slashCommandList } from './slash-commands.js';
|
|
34
|
+
import { applyTheme, attachPalette, createKeybindingsManager, defaultAgentDir, mirrorKeybindingsToEditor, mirrorKittyProtocolToEditor, } from './config-load.js';
|
|
35
|
+
import { buildLoginPicker, buildLogoutPicker } from './auth-pickers.js';
|
|
36
|
+
import { TitledEditor, thinkingBorderColor, thinkingTitleStyle, defaultTitleStyle } from './titled-editor.js';
|
|
37
|
+
import { fetchGitInfo } from './git-info.js';
|
|
38
|
+
import { BrokerUnavailableError, ViewSocketClient, reconnectShouldGiveUp } from './view-socket.js';
|
|
39
|
+
/** Async per-node ask-map fetch (NON-blocking — the viewer must never block its
|
|
40
|
+
* input pump on a shell-out, unlike canvas-nav's execFileSync). Buckets a whole
|
|
41
|
+
* sub-DAG's pending asks in one `crtr` process; on any error the callback is not
|
|
42
|
+
* invoked, so the caller keeps its last good map. */
|
|
43
|
+
function fetchAsksAsync(rootId, cb) {
|
|
44
|
+
execFile('crtr', ['canvas', 'attention', 'map', '--view', rootId, '--json'], { timeout: 2_500, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
|
|
45
|
+
if (err)
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(stdout.trim());
|
|
49
|
+
cb(parsed.counts ?? {});
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* malformed — keep last map */
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
30
56
|
/** Run the interactive attach session against `nodeId`. Resolves when the
|
|
31
57
|
* viewer detaches (ctrl+c/ctrl+d), the broker goes away, or a signal lands —
|
|
32
58
|
* always restoring the terminal first. */
|
|
@@ -100,49 +126,241 @@ async function runAttach(nodeId, observer) {
|
|
|
100
126
|
// 1. Theme + keybindings FIRST — pi's components throw "Theme not initialized"
|
|
101
127
|
// otherwise (T5 contract). One KeybindingsManager feeds BOTH editor + input.
|
|
102
128
|
applyTheme({ cwd: meta.cwd });
|
|
129
|
+
const pal = attachPalette();
|
|
103
130
|
const km = createKeybindingsManager();
|
|
104
|
-
//
|
|
105
|
-
|
|
131
|
+
// pi-coding-agent's CustomEditor may resolve a SEPARATE pi-tui instance whose
|
|
132
|
+
// global keybindings we'd otherwise never set — mirror the manager onto it so
|
|
133
|
+
// the editor honors the user's bindings (e.g. alt+enter → newline). No-op on a
|
|
134
|
+
// deduped install. Awaited before tui.start so the editor never handles a key
|
|
135
|
+
// against stale defaults.
|
|
136
|
+
await mirrorKeybindingsToEditor(km);
|
|
137
|
+
// The front door installs the Alt+C menu binding before it ever loads a theme,
|
|
138
|
+
// so it lands unstyled. Now that this viewer process HAS themed (applyTheme
|
|
139
|
+
// above populated the live-theme global), re-install it so the menu picks up
|
|
140
|
+
// the distinct-surface background. Idempotent re-bind; tmux-only, best-effort.
|
|
141
|
+
if (inTmux()) {
|
|
142
|
+
try {
|
|
143
|
+
installMenuBinding();
|
|
144
|
+
}
|
|
145
|
+
catch { /* best-effort */ }
|
|
146
|
+
}
|
|
147
|
+
// 2. TUI + layout containers + editor. Top-to-bottom render order (set at
|
|
148
|
+
// step 8): chat · rule · badge · managers (↑ subscriber) · editor (themed
|
|
149
|
+
// border) · reports (↓ subscriptions) · status bar. A themed DynamicBorder
|
|
150
|
+
// rule divides the scrolling chat from the fixed chrome below it. The badge
|
|
151
|
+
// sits in the BOTTOM-anchored chrome
|
|
152
|
+
// stack (directly above managers), NOT as the topmost child: pi-tui anchors
|
|
153
|
+
// the viewport to the cursor at the bottom, so a topmost badge scrolls off
|
|
154
|
+
// into scrollback the instant the chat exceeds one screen. The badge + the
|
|
155
|
+
// two canvas panels reproduce the pi-extension chrome natively (Unit Q).
|
|
156
|
+
const procTerminal = new ProcessTerminal();
|
|
157
|
+
const tui = new TUI(procTerminal);
|
|
106
158
|
const chatContainer = new Container();
|
|
159
|
+
const chrome = new Container(); // a themed rule dividing scrolling chat from the fixed chrome below
|
|
160
|
+
const managers = new Container();
|
|
161
|
+
const reports = new Container();
|
|
162
|
+
const queued = new Container(); // pending steering/follow-up messages, above the editor
|
|
163
|
+
const pickerPanel = new Container(); // native pickers (/model etc.), inline directly under the editor
|
|
107
164
|
const footer = new Container();
|
|
108
|
-
|
|
109
|
-
const
|
|
165
|
+
chrome.addChild(new DynamicBorder(pal.border));
|
|
166
|
+
const editorTheme = { borderColor: pal.border, selectList: getSelectListTheme() };
|
|
167
|
+
// TitledEditor paints the session name into its top border (solid-background
|
|
168
|
+
// chip) and tracks a thinking-level border color, both set from live state below.
|
|
169
|
+
const editor = new TitledEditor(tui, editorTheme, km, { paddingX: 1 });
|
|
170
|
+
// Slash-command autocomplete: builtins + native canvas commands now; enriched
|
|
171
|
+
// with the broker's engine/extension/skill commands on the get_commands ack.
|
|
172
|
+
const setCommands = (commands) => {
|
|
173
|
+
editor.setAutocompleteProvider(new CombinedAutocompleteProvider(slashCommandList(commands), meta.cwd));
|
|
174
|
+
};
|
|
175
|
+
setCommands();
|
|
110
176
|
// 3. Render layer (T5). ChatView owns the chat scroll + activity spinner; the
|
|
111
177
|
// footer/status line below is ours, so onFooterEvent is left unset (footer
|
|
112
178
|
// updates flow through handleBrokerFrame's single update path instead).
|
|
113
|
-
const chatView = new ChatView(tui, chatContainer, { cwd: meta.cwd });
|
|
179
|
+
const chatView = new ChatView(tui, chatContainer, { cwd: meta.cwd, palette: pal });
|
|
114
180
|
// 4. Footer/status line — surfaces role + live state + transient notices.
|
|
115
181
|
const clientId = randomUUID();
|
|
116
182
|
let role = observer ? 'observer' : 'controller';
|
|
183
|
+
let brokerAgentDir;
|
|
117
184
|
let liveState;
|
|
118
185
|
let notice = '';
|
|
186
|
+
// Live context occupancy for the footer: `contextTokens` is the prompt-token
|
|
187
|
+
// count of the most recent assistant turn (input + cacheRead + cacheWrite =
|
|
188
|
+
// everything sent to the model, since with prompt caching bare `input` is just
|
|
189
|
+
// the uncached slice and badly understates the context), read off the relayed
|
|
190
|
+
// message_end stream. It rides data the viewer already receives, so the
|
|
191
|
+
// indicator needs no broker/protocol change.
|
|
192
|
+
let contextTokens;
|
|
193
|
+
const formatTokens = (n) => n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k` : `${n}`;
|
|
194
|
+
// A themed, aligned status bar: live state on the LEFT (role · model · streaming
|
|
195
|
+
// · context · queued), the transient notice pushed to the RIGHT edge — not jammed inline.
|
|
119
196
|
const renderFooter = () => {
|
|
120
|
-
const
|
|
197
|
+
const segs = [
|
|
198
|
+
role === 'controller' ? pal.active('● drive') : pal.muted('○ read-only'),
|
|
199
|
+
];
|
|
121
200
|
if (liveState?.model)
|
|
122
|
-
|
|
201
|
+
segs.push(pal.info(liveState.model));
|
|
123
202
|
if (liveState?.isStreaming)
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
203
|
+
segs.push(pal.active('streaming'));
|
|
204
|
+
if (contextTokens !== undefined) {
|
|
205
|
+
segs.push(pal.muted(`${formatTokens(contextTokens)} ctx`));
|
|
206
|
+
}
|
|
207
|
+
if (liveState && liveState.pendingMessageCount > 0) {
|
|
208
|
+
segs.push(pal.muted(`queued ${liveState.pendingMessageCount}`));
|
|
209
|
+
}
|
|
210
|
+
const left = segs.join(pal.muted(' · '));
|
|
211
|
+
const right = notice ? pal.warning(`⚠ ${notice}`) : '';
|
|
212
|
+
// Text(_, 1, 0) pads 1 col each side, so the usable span is columns - 2.
|
|
213
|
+
const span = Math.max(1, (process.stdout.columns ?? 80) - 2);
|
|
214
|
+
const rightW = visibleWidth(right);
|
|
215
|
+
// Truncate the left segment (ANSI-aware) so a long model/notice never wraps;
|
|
216
|
+
// the right-aligned notice keeps its slot.
|
|
217
|
+
const maxLeft = right ? Math.max(0, span - rightW - 1) : span;
|
|
218
|
+
const leftFit = visibleWidth(left) > maxLeft ? truncateToWidth(left, maxLeft, '…') : left;
|
|
219
|
+
const gap = Math.max(right ? 1 : 0, span - visibleWidth(leftFit) - rightW);
|
|
220
|
+
const line = right ? leftFit + ' '.repeat(gap) + right : leftFit;
|
|
132
221
|
footer.clear();
|
|
133
222
|
footer.addChild(new Text(line, 1, 0));
|
|
134
223
|
tui.requestRender();
|
|
135
224
|
};
|
|
136
|
-
|
|
225
|
+
// Transient notices (errors, command feedback) auto-clear after a few seconds so
|
|
226
|
+
// a stale "error: …" doesn't linger in the footer forever. `sticky` keeps a notice
|
|
227
|
+
// up until explicitly replaced (used by the reconnect indicator, which is cleared
|
|
228
|
+
// by hand once the socket redials).
|
|
229
|
+
const NOTICE_CLEAR_MS = 3_000;
|
|
230
|
+
let noticeTimer;
|
|
231
|
+
const setNotice = (msg, opts) => {
|
|
137
232
|
notice = msg;
|
|
233
|
+
if (noticeTimer !== undefined) {
|
|
234
|
+
clearTimeout(noticeTimer);
|
|
235
|
+
noticeTimer = undefined;
|
|
236
|
+
}
|
|
237
|
+
if (msg && !opts?.sticky) {
|
|
238
|
+
noticeTimer = setTimeout(() => {
|
|
239
|
+
noticeTimer = undefined;
|
|
240
|
+
notice = '';
|
|
241
|
+
renderFooter();
|
|
242
|
+
}, NOTICE_CLEAR_MS);
|
|
243
|
+
}
|
|
138
244
|
renderFooter();
|
|
139
245
|
};
|
|
140
|
-
//
|
|
246
|
+
// Subscribed-node panel — manager line above the editor, live reports below,
|
|
247
|
+
// read straight from canvas.db via nav-model (Feature 2). asksMap is refreshed
|
|
248
|
+
// by the low-rate poll below; beginFrame() inside buildCanvasPanelLines memoizes
|
|
249
|
+
// the db reads for each rebuild.
|
|
250
|
+
let asksMap = {};
|
|
251
|
+
const renderPanels = () => {
|
|
252
|
+
const { managers: mLines, reports: rLines } = buildCanvasPanelLines(nodeId, asksMap, pal);
|
|
253
|
+
managers.clear();
|
|
254
|
+
for (const l of mLines)
|
|
255
|
+
managers.addChild(new Text(l, 1, 0));
|
|
256
|
+
reports.clear();
|
|
257
|
+
for (const l of rLines)
|
|
258
|
+
reports.addChild(new Text(l, 1, 0));
|
|
259
|
+
tui.requestRender();
|
|
260
|
+
};
|
|
261
|
+
// The alt+g GRAPH overlay (Feature 3) — a bounded, centered modal navigator
|
|
262
|
+
// over the same nav-model graph, reading the live asksMap.
|
|
263
|
+
const graphOverlay = new GraphOverlay(tui, nodeId, () => asksMap, pal);
|
|
264
|
+
// Refresh all canvas chrome (panels + overlay if open) from canvas.db.
|
|
265
|
+
const refreshChrome = () => {
|
|
266
|
+
renderPanels();
|
|
267
|
+
graphOverlay.refresh();
|
|
268
|
+
};
|
|
269
|
+
// Git context painted into the RIGHT of the editor's top border: cwd leaf ·
|
|
270
|
+
// branch · status symbols (● dirty, ⇡ ahead, ⇣ behind). Refreshed by the poll.
|
|
271
|
+
const renderGitInfo = (info) => {
|
|
272
|
+
const parts = [pal.muted(info.dir)];
|
|
273
|
+
if (info.branch)
|
|
274
|
+
parts.push(pal.info(`⎇ ${info.branch}`));
|
|
275
|
+
const sym = [];
|
|
276
|
+
if (info.dirty)
|
|
277
|
+
sym.push(pal.warning('●'));
|
|
278
|
+
if (info.ahead > 0)
|
|
279
|
+
sym.push(pal.active(`⇡${info.ahead}`));
|
|
280
|
+
if (info.behind > 0)
|
|
281
|
+
sym.push(pal.active(`⇣${info.behind}`));
|
|
282
|
+
if (sym.length > 0)
|
|
283
|
+
parts.push(sym.join(''));
|
|
284
|
+
// A trailing space keeps the symbols off the very last border cell.
|
|
285
|
+
editor.info = ` ${parts.join(pal.muted(' · '))} `;
|
|
286
|
+
tui.requestRender();
|
|
287
|
+
};
|
|
288
|
+
const pollGitInfo = () => fetchGitInfo(meta.cwd, renderGitInfo);
|
|
289
|
+
// Queued steering/follow-up messages, shown above the editor so you can SEE
|
|
290
|
+
// what is waiting (not just the footer count). Texts arrive on `queue_update`;
|
|
291
|
+
// the welcome snapshot carries only a count, so the panel fills from the first
|
|
292
|
+
// queue change after attach.
|
|
293
|
+
let queuedMessages = [];
|
|
294
|
+
const renderQueued = () => {
|
|
295
|
+
queued.clear();
|
|
296
|
+
if (queuedMessages.length > 0) {
|
|
297
|
+
const span = Math.max(1, (process.stdout.columns ?? 80) - 4);
|
|
298
|
+
queued.addChild(new Text(pal.muted(`queued (${queuedMessages.length}):`), 1, 0));
|
|
299
|
+
for (const msg of queuedMessages) {
|
|
300
|
+
const oneLine = msg.replace(/\s+/g, ' ').trim();
|
|
301
|
+
queued.addChild(new Text(pal.faint(` ⋯ ${truncateToWidth(oneLine, span, '…')}`), 1, 0));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
tui.requestRender();
|
|
305
|
+
};
|
|
306
|
+
// 5. Input layer (T6). onCommand/onDialogResponse → socket; onNotice → footer;
|
|
307
|
+
// nodeId/onGraph feed the slash context (/promote targets this node, /graph
|
|
308
|
+
// toggles the overlay); onRequest is the correlated read-op channel the
|
|
309
|
+
// native pickers ride on.
|
|
141
310
|
const input = new InputController(tui, editor, km, {
|
|
142
311
|
onCommand: (frame) => socket.send(frame),
|
|
143
312
|
onDialogResponse: (resp) => socket.send(resp),
|
|
144
313
|
onNotice: (msg) => setNotice(msg),
|
|
314
|
+
nodeId,
|
|
315
|
+
onGraph: () => graphOverlay.toggle(),
|
|
316
|
+
// `/quit` — detach this viewer (the engine keeps running). Must tear down
|
|
317
|
+
// locally; a bare `bye` would just trigger the reconnect loop.
|
|
318
|
+
onQuit: () => teardown('detach'),
|
|
319
|
+
// `/copy` — last assistant message → system clipboard.
|
|
320
|
+
onCopy: () => copyLastAssistant(),
|
|
321
|
+
// Global display toggles (Ctrl+O tools / Ctrl+T thinking) — pure render state
|
|
322
|
+
// owned by ChatView, surfaced as a footer notice mirroring pi's status line.
|
|
323
|
+
onToggleToolsExpand: () => setNotice(chatView.toggleToolsExpanded() ? 'Tool output: expanded' : 'Tool output: collapsed'),
|
|
324
|
+
onToggleThinking: () => setNotice(chatView.toggleThinking() ? 'Thinking blocks: hidden' : 'Thinking blocks: visible'),
|
|
325
|
+
// Wire the correlated read-op channel so the native pickers (/model,
|
|
326
|
+
// /resume, /fork, /tree, /settings, /scoped-models) can fetch their payloads.
|
|
327
|
+
// Without this every picker degrades to the "isn't available in this viewer"
|
|
328
|
+
// notice (see slash-commands.ts + InputController.openPicker).
|
|
329
|
+
onRequest: (frame) => socket.request(frame),
|
|
330
|
+
// Mount the native pickers inline in the chrome stack, directly under the
|
|
331
|
+
// editor (queued-panel pattern) — not as a floating centered modal — so a
|
|
332
|
+
// picker reads as part of the editor, like pi's own selectors.
|
|
333
|
+
onMountPicker: (component) => {
|
|
334
|
+
pickerPanel.addChild(component);
|
|
335
|
+
tui.requestRender();
|
|
336
|
+
},
|
|
337
|
+
onUnmountPicker: () => {
|
|
338
|
+
pickerPanel.clear();
|
|
339
|
+
tui.requestRender();
|
|
340
|
+
},
|
|
341
|
+
// Auth pickers — viewer-local OAuth/API-key flows. Auth operations target the
|
|
342
|
+
// SAME auth.json the broker uses (broker + viewer share a filesystem in the
|
|
343
|
+
// tmux-local attach model), so these run entirely in the viewer process and
|
|
344
|
+
// then notify the broker to reload via a reload_auth frame.
|
|
345
|
+
openLoginPicker: () => {
|
|
346
|
+
const dir = brokerAgentDir ?? defaultAgentDir();
|
|
347
|
+
input.openAuthPicker((controls) => buildLoginPicker(tui, dir, () => socket.send({ type: 'reload_auth' }), (m) => setNotice(m), controls));
|
|
348
|
+
},
|
|
349
|
+
openLogoutPicker: () => {
|
|
350
|
+
const dir = brokerAgentDir ?? defaultAgentDir();
|
|
351
|
+
input.openAuthPicker((controls) => buildLogoutPicker(dir, () => socket.send({ type: 'reload_auth' }), (m) => setNotice(m), controls.close));
|
|
352
|
+
},
|
|
145
353
|
});
|
|
354
|
+
// `/copy` — copy the last assistant message (held by ChatView, kept current
|
|
355
|
+
// from both the welcome snapshot and the live stream) to the system clipboard.
|
|
356
|
+
const copyLastAssistant = () => {
|
|
357
|
+
const text = chatView.getLastAssistantText();
|
|
358
|
+
if (!text) {
|
|
359
|
+
setNotice('Nothing to copy — no assistant message yet');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
setNotice(writeClipboardText(text) ? 'Copied last message to clipboard' : 'Copy failed — no clipboard tool found');
|
|
363
|
+
};
|
|
146
364
|
// Feed fresh state into the input controller AND the footer. Steer-vs-prompt
|
|
147
365
|
// routing (T6) reads `state.isStreaming`, so this must stay current — the
|
|
148
366
|
// broker only ships full state in `welcome`, so we patch isStreaming/name/etc
|
|
@@ -150,7 +368,18 @@ async function runAttach(nodeId, observer) {
|
|
|
150
368
|
const setLiveState = (state) => {
|
|
151
369
|
liveState = state;
|
|
152
370
|
input.setState(state);
|
|
371
|
+
// The session name lives in the editor's top border (solid chip); the border
|
|
372
|
+
// color tracks the agent's thinking level. Both follow live state.
|
|
373
|
+
editor.title = state.sessionName ? `⬢ ${state.sessionName}` : '';
|
|
374
|
+
editor.borderColor = thinkingBorderColor(state.thinkingLevel, pal.border);
|
|
375
|
+
// Title chip background = the same thinking-level color (bold white text), so
|
|
376
|
+
// the name chip and the border rule read as one hue.
|
|
377
|
+
editor.titleStyle = thinkingTitleStyle(state.thinkingLevel, defaultTitleStyle);
|
|
153
378
|
renderFooter();
|
|
379
|
+
// Panel liveness rides the relayed event stream (agent_start/agent_end/
|
|
380
|
+
// queue_update/session_info_changed all patch state through here) plus the
|
|
381
|
+
// low-rate poll below for changes that emit no event (child status flips).
|
|
382
|
+
refreshChrome();
|
|
154
383
|
};
|
|
155
384
|
const patchState = (patch) => {
|
|
156
385
|
if (liveState === undefined)
|
|
@@ -162,8 +391,16 @@ async function runAttach(nodeId, observer) {
|
|
|
162
391
|
switch (frame.type) {
|
|
163
392
|
case 'welcome': {
|
|
164
393
|
role = frame.role;
|
|
394
|
+
brokerAgentDir = frame.agentDir;
|
|
165
395
|
chatView.applySnapshot(frame.snapshot);
|
|
396
|
+
// Seed the footer's context indicator from the catch-up snapshot.
|
|
397
|
+
const cu = frame.snapshot.stats.contextUsage;
|
|
398
|
+
if (cu && cu.tokens != null)
|
|
399
|
+
contextTokens = cu.tokens;
|
|
166
400
|
setLiveState(frame.snapshot.state);
|
|
401
|
+
// Ask the broker for its merged command list (engine/extension/skill
|
|
402
|
+
// commands) to enrich slash autocomplete beyond the builtins+canvas set.
|
|
403
|
+
socket.send({ type: 'get_commands' });
|
|
167
404
|
if (frame.pending_dialog != null)
|
|
168
405
|
input.attachDialog(frame.pending_dialog);
|
|
169
406
|
break;
|
|
@@ -175,6 +412,12 @@ async function runAttach(nodeId, observer) {
|
|
|
175
412
|
renderFooter();
|
|
176
413
|
break;
|
|
177
414
|
}
|
|
415
|
+
case 'model_changed': {
|
|
416
|
+
// The broker's own post-set_model/cycle_model announcement (pi emits no
|
|
417
|
+
// engine event for a model switch) — keeps the footer's model current.
|
|
418
|
+
patchState({ model: frame.model });
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
178
421
|
case 'error': {
|
|
179
422
|
if (frame.code === 'not_controller') {
|
|
180
423
|
setNotice('read-only — another viewer is the controller');
|
|
@@ -185,13 +428,51 @@ async function runAttach(nodeId, observer) {
|
|
|
185
428
|
break;
|
|
186
429
|
}
|
|
187
430
|
case 'ack': {
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
431
|
+
// get_commands replies with the broker's merged command list as JSON in
|
|
432
|
+
// `detail` — rebuild the autocomplete provider with it (slashCommandList
|
|
433
|
+
// re-appends the native canvas commands, deduped).
|
|
434
|
+
if (frame.for === 'get_commands' && frame.ok && frame.detail) {
|
|
435
|
+
try {
|
|
436
|
+
const list = JSON.parse(frame.detail);
|
|
437
|
+
setCommands(list);
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
/* malformed — keep the current provider */
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
// navigate_tree's detail is the navigated-to user message's text (pi
|
|
445
|
+
// parity: the tree navigator restores it to the editor for re-editing);
|
|
446
|
+
// the rewound transcript itself arrives via the broker's re-welcome.
|
|
447
|
+
if (frame.for === 'navigate_tree' && frame.ok) {
|
|
448
|
+
if (frame.detail) {
|
|
449
|
+
editor.setText(frame.detail);
|
|
450
|
+
tui.requestRender();
|
|
451
|
+
}
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
if (frame.for === 'share') {
|
|
455
|
+
if (frame.ok && frame.detail) {
|
|
456
|
+
setNotice(`Shared: ${frame.detail}`);
|
|
457
|
+
writeClipboardText(frame.detail);
|
|
458
|
+
}
|
|
459
|
+
else if (!frame.ok) {
|
|
460
|
+
setNotice(`share failed: ${frame.detail ?? 'unknown error'}`);
|
|
461
|
+
}
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
if (frame.for === 'clone' && !frame.ok) {
|
|
465
|
+
setNotice(`clone failed: ${frame.detail ?? 'unknown error'}`);
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
192
468
|
if (!frame.ok) {
|
|
193
469
|
setNotice(`command failed: ${frame.for}${frame.detail ? ` — ${frame.detail}` : ''}`);
|
|
470
|
+
break;
|
|
194
471
|
}
|
|
472
|
+
// Confirm commands whose effect is otherwise invisible — without this a
|
|
473
|
+
// `/reload` looks like it did nothing (the engine reloads silently).
|
|
474
|
+
if (frame.for === 'reload')
|
|
475
|
+
setNotice('Reloaded keybindings, extensions, skills, prompts, and themes');
|
|
195
476
|
break;
|
|
196
477
|
}
|
|
197
478
|
case 'extension_ui_request': {
|
|
@@ -202,16 +483,27 @@ async function runAttach(nodeId, observer) {
|
|
|
202
483
|
// A relayed AgentSessionEvent — render it, and keep local state fresh.
|
|
203
484
|
const event = frame;
|
|
204
485
|
chatView.handleEvent(event);
|
|
486
|
+
// The most recent assistant turn's prompt tokens ARE the live context
|
|
487
|
+
// size — read straight off the relayed message, no broker round-trip.
|
|
488
|
+
if (event.type === 'message_end' && event.message.role === 'assistant') {
|
|
489
|
+
const u = event.message.usage;
|
|
490
|
+
contextTokens = u.input + u.cacheRead + u.cacheWrite;
|
|
491
|
+
renderFooter();
|
|
492
|
+
}
|
|
205
493
|
if (event.type === 'agent_start')
|
|
206
494
|
patchState({ isStreaming: true });
|
|
207
495
|
else if (event.type === 'agent_end')
|
|
208
496
|
patchState({ isStreaming: false });
|
|
209
497
|
else if (event.type === 'session_info_changed')
|
|
210
498
|
patchState({ sessionName: event.name });
|
|
211
|
-
else if (event.type === 'thinking_level_changed')
|
|
499
|
+
else if (event.type === 'thinking_level_changed') {
|
|
212
500
|
patchState({ thinkingLevel: event.level });
|
|
501
|
+
setNotice(`Thinking level: ${event.level}`);
|
|
502
|
+
}
|
|
213
503
|
else if (event.type === 'queue_update') {
|
|
214
|
-
|
|
504
|
+
queuedMessages = [...event.steering, ...event.followUp];
|
|
505
|
+
renderQueued();
|
|
506
|
+
patchState({ pendingMessageCount: queuedMessages.length });
|
|
215
507
|
}
|
|
216
508
|
break;
|
|
217
509
|
}
|
|
@@ -239,10 +531,16 @@ async function runAttach(nodeId, observer) {
|
|
|
239
531
|
const done = new Promise((r) => {
|
|
240
532
|
resolveDone = r;
|
|
241
533
|
});
|
|
534
|
+
let asksTimer;
|
|
242
535
|
const teardown = (reason) => {
|
|
243
536
|
if (tornDown)
|
|
244
537
|
return;
|
|
245
538
|
tornDown = true;
|
|
539
|
+
if (asksTimer !== undefined)
|
|
540
|
+
clearInterval(asksTimer);
|
|
541
|
+
if (noticeTimer !== undefined)
|
|
542
|
+
clearTimeout(noticeTimer);
|
|
543
|
+
graphOverlay.close();
|
|
246
544
|
clearPaneTag();
|
|
247
545
|
removeKeyListener();
|
|
248
546
|
// A clean detach tells the broker to drop this viewer (the engine runs on);
|
|
@@ -264,6 +562,60 @@ async function runAttach(nodeId, observer) {
|
|
|
264
562
|
}
|
|
265
563
|
resolveDone();
|
|
266
564
|
};
|
|
565
|
+
// The handshake the viewer sends on connect AND on every successful redial.
|
|
566
|
+
// After a broker refresh / crash-revive the fresh process has no controller and
|
|
567
|
+
// no memory of us, so re-`hello` reclaims our role and the `welcome` snapshot
|
|
568
|
+
// repaints full scrollback/state. Sends the CURRENT `role`, not the constructor
|
|
569
|
+
// `observer` flag: if we were preempted to observer at runtime (a
|
|
570
|
+
// `control_changed` demoted us), re-hello'ing as 'controller' would let us
|
|
571
|
+
// reclaim control from the new controller after a refresh — so the demotion
|
|
572
|
+
// must survive the reconnect.
|
|
573
|
+
const sendHello = () => {
|
|
574
|
+
socket.send({
|
|
575
|
+
type: 'hello',
|
|
576
|
+
role,
|
|
577
|
+
client_id: clientId,
|
|
578
|
+
term: { cols: process.stdout.columns ?? 80, rows: process.stdout.rows ?? 24 },
|
|
579
|
+
});
|
|
580
|
+
};
|
|
581
|
+
// Reconnect supervisor. A resident broker node that yields EXITS its broker
|
|
582
|
+
// (socket close) and is revived FRESH by the daemon on the SAME `view.sock`
|
|
583
|
+
// path within a few seconds. So on close we HOLD the pane and re-dial while
|
|
584
|
+
// the canvas-db row says the node is still alive (status active/idle), giving
|
|
585
|
+
// up only when it is genuinely terminal (done/dead/canceled or row gone) or a
|
|
586
|
+
// bounded ~30s of failed redials elapses (stale-row safety). A successful
|
|
587
|
+
// redial re-sends `hello`; the new broker's `welcome` runs applySnapshot,
|
|
588
|
+
// which resetChat()s the pane and rebuilds from the persisted .jsonl — a clean
|
|
589
|
+
// "clear + continuation" for free.
|
|
590
|
+
const RECONNECT_GIVEUP_MS = 30_000;
|
|
591
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
592
|
+
let reconnecting = false;
|
|
593
|
+
const attemptReconnect = async () => {
|
|
594
|
+
reconnecting = true;
|
|
595
|
+
setNotice('↻ node refreshing — reconnecting…', { sticky: true });
|
|
596
|
+
const deadline = Date.now() + RECONNECT_GIVEUP_MS;
|
|
597
|
+
let delay = 200;
|
|
598
|
+
while (!tornDown && Date.now() < deadline) {
|
|
599
|
+
if (reconnectShouldGiveUp(getRow(nodeId))) {
|
|
600
|
+
break; // genuinely gone → fall through to broker-gone
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
await socket.redial();
|
|
604
|
+
reconnecting = false;
|
|
605
|
+
sendHello();
|
|
606
|
+
setNotice('');
|
|
607
|
+
return; // reconnected; the welcome snapshot clears + continues the pane
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
// ENOENT (socket not recreated yet) / ECONNREFUSED (listen not ready) → retry.
|
|
611
|
+
await sleep(Math.min(delay, 1000));
|
|
612
|
+
delay *= 2;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
reconnecting = false;
|
|
616
|
+
if (!tornDown)
|
|
617
|
+
teardown('broker-gone');
|
|
618
|
+
};
|
|
267
619
|
// Permanent socket wiring (the connect-race listeners are gone).
|
|
268
620
|
socket.on('frame', handleBrokerFrame);
|
|
269
621
|
socket.on('error', (err) => {
|
|
@@ -271,15 +623,44 @@ async function runAttach(nodeId, observer) {
|
|
|
271
623
|
// broker-gone message prefers this over the generic text.
|
|
272
624
|
lastSocketError = err.message;
|
|
273
625
|
});
|
|
274
|
-
socket.on('close', () =>
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
626
|
+
socket.on('close', () => {
|
|
627
|
+
if (tornDown)
|
|
628
|
+
return; // a real detach/signal already won
|
|
629
|
+
if (reconnecting)
|
|
630
|
+
return; // the dead socket's own close during the redial loop
|
|
631
|
+
void attemptReconnect();
|
|
632
|
+
});
|
|
633
|
+
// ctrl+d → detach immediately (lifecycle is ours). ctrl+c is gentler: a single
|
|
634
|
+
// tap CLEARS the editor (and arms a window); a second tap within that window
|
|
635
|
+
// detaches. A global TUI input listener fires BEFORE the focused component (and
|
|
636
|
+
// before any dialog overlay), so this is consistent regardless of focus.
|
|
637
|
+
const CTRL_C_DETACH_MS = 1000;
|
|
638
|
+
let lastCtrlCAt = 0;
|
|
278
639
|
const removeKeyListener = tui.addInputListener((data) => {
|
|
279
|
-
if (matchesKey(data, 'ctrl+
|
|
640
|
+
if (matchesKey(data, 'ctrl+d')) {
|
|
280
641
|
teardown('detach');
|
|
281
642
|
return { consume: true };
|
|
282
643
|
}
|
|
644
|
+
if (matchesKey(data, 'ctrl+c')) {
|
|
645
|
+
const now = Date.now();
|
|
646
|
+
if (now - lastCtrlCAt < CTRL_C_DETACH_MS) {
|
|
647
|
+
lastCtrlCAt = 0;
|
|
648
|
+
teardown('detach');
|
|
649
|
+
return { consume: true };
|
|
650
|
+
}
|
|
651
|
+
lastCtrlCAt = now;
|
|
652
|
+
editor.setText('');
|
|
653
|
+
setNotice('Editor cleared — press Ctrl+C again to detach');
|
|
654
|
+
tui.requestRender();
|
|
655
|
+
return { consume: true };
|
|
656
|
+
}
|
|
657
|
+
// alt+g toggles the GRAPH overlay. The global listener fires BEFORE the
|
|
658
|
+
// focused component, so this works whether the editor or the overlay holds
|
|
659
|
+
// focus (closing it from inside the overlay too).
|
|
660
|
+
if (matchesKey(data, 'alt+g')) {
|
|
661
|
+
graphOverlay.toggle();
|
|
662
|
+
return { consume: true };
|
|
663
|
+
}
|
|
283
664
|
return undefined;
|
|
284
665
|
});
|
|
285
666
|
// Pane kill / daemon teardown arrives as a signal (raw mode swallows ctrl+c as
|
|
@@ -291,17 +672,40 @@ async function runAttach(nodeId, observer) {
|
|
|
291
672
|
// 8. Lay out, focus the editor, start, then handshake. Starting before the
|
|
292
673
|
// handshake means the welcome's applySnapshot renders into a live TUI.
|
|
293
674
|
tui.addChild(chatContainer);
|
|
294
|
-
tui.addChild(
|
|
675
|
+
tui.addChild(chrome);
|
|
676
|
+
tui.addChild(managers);
|
|
677
|
+
tui.addChild(queued);
|
|
295
678
|
tui.addChild(editor);
|
|
679
|
+
tui.addChild(pickerPanel);
|
|
680
|
+
tui.addChild(reports);
|
|
681
|
+
tui.addChild(footer);
|
|
296
682
|
tui.setFocus(editor);
|
|
297
683
|
renderFooter();
|
|
684
|
+
renderPanels();
|
|
685
|
+
renderQueued();
|
|
686
|
+
pollGitInfo();
|
|
298
687
|
tui.start();
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
688
|
+
// ProcessTerminal negotiated the keyboard protocol on OUR pi-tui instance; the
|
|
689
|
+
// editor reads the flag from its own (possibly separate) instance, so mirror it.
|
|
690
|
+
void mirrorKittyProtocolToEditor(procTerminal.kittyProtocolActive);
|
|
691
|
+
sendHello();
|
|
692
|
+
// Low-rate canvas-chrome refresh: repaint panels/overlay from canvas.db (child
|
|
693
|
+
// status flips emit no broker event) and re-poll the ask map asynchronously.
|
|
694
|
+
const ASKS_POLL_MS = 5_000;
|
|
695
|
+
const pollChrome = () => {
|
|
696
|
+
refreshChrome();
|
|
697
|
+
pollGitInfo();
|
|
698
|
+
fetchAsksAsync(climbRoot(nodeId), (counts) => {
|
|
699
|
+
if (JSON.stringify(counts) !== JSON.stringify(asksMap)) {
|
|
700
|
+
asksMap = counts;
|
|
701
|
+
refreshChrome();
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
};
|
|
705
|
+
pollChrome();
|
|
706
|
+
asksTimer = setInterval(pollChrome, ASKS_POLL_MS);
|
|
707
|
+
if (typeof asksTimer.unref === 'function')
|
|
708
|
+
asksTimer.unref();
|
|
305
709
|
await done;
|
|
306
710
|
}
|
|
307
711
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TUI } from '@earendil-works/pi-tui';
|
|
2
|
+
import type { Picker, PickerControls } from './pickers.js';
|
|
3
|
+
/** `/login` — show the provider selector, then run pi's native login flow in
|
|
4
|
+
* the viewer. On success, notifies the broker to reload auth via reload_auth.
|
|
5
|
+
* Returns a Picker (OAuthSelectorComponent as the overlay). */
|
|
6
|
+
export declare function buildLoginPicker(tui: TUI, agentDir: string, onAuthReloaded: () => void, notify: (msg: string) => void, controls: PickerControls): Picker;
|
|
7
|
+
/** `/logout` — show the provider selector in logout mode. On select, calls
|
|
8
|
+
* authStorage.logout(providerId) directly, then notifies the broker to reload. */
|
|
9
|
+
export declare function buildLogoutPicker(agentDir: string, onAuthReloaded: () => void, notify: (msg: string) => void, close: () => void): Picker;
|