@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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// titled-editor.ts — the `crtr attach` input editor with two bits of chrome pi's
|
|
2
|
+
// stock editor doesn't give us:
|
|
3
|
+
// 1. the session name painted INTO the top border as a solid-background chip,
|
|
4
|
+
// so it stays pinned to the editor instead of scrolling off as a free badge;
|
|
5
|
+
// 2. a border color that tracks the agent's thinking level (set by attach-cmd
|
|
6
|
+
// on each state update), mirroring pi's `getThinkingBorderColor`.
|
|
7
|
+
// Both are pure render-layer chrome; nothing here touches the socket or session.
|
|
8
|
+
import { CustomEditor } from '@earendil-works/pi-coding-agent';
|
|
9
|
+
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
10
|
+
/** Per-level border/chip color as a 24-bit RGB triple — ONE blue-violet hue at
|
|
11
|
+
* CONSTANT perceived lightness (luma ≈ 120) whose SATURATION alone climbs with
|
|
12
|
+
* the budget: a near-gray blue at `minimal` deepening to a vivid violet at
|
|
13
|
+
* `xhigh`, never getting lighter or darker — only more colorful. (pi paints its
|
|
14
|
+
* own `thinking*` theme colors off its public palette surface, so we own this
|
|
15
|
+
* ramp directly — truecolor, not the 16-color ANSI hues, so the saturation
|
|
16
|
+
* climb actually reads.) `off` falls back to the caller's default border color. */
|
|
17
|
+
const THINKING_RGB = {
|
|
18
|
+
off: undefined, // default border color
|
|
19
|
+
minimal: [125, 115, 135], // near-gray blue-violet
|
|
20
|
+
low: [129, 109, 151], // muted blue-violet
|
|
21
|
+
medium: [134, 103, 168], // violet
|
|
22
|
+
high: [139, 98, 184], // deeper violet
|
|
23
|
+
xhigh: [144, 92, 200], // vivid violet
|
|
24
|
+
};
|
|
25
|
+
/** The default (thinking `off`) title chip: reverse video + a space of padding
|
|
26
|
+
* each side, so the name reads as a label sitting on the border rule. Used as
|
|
27
|
+
* the fallback when no thinking color applies. */
|
|
28
|
+
export const defaultTitleStyle = (s) => `\x1b[7m ${s} \x1b[27m`;
|
|
29
|
+
/** Resolve the editor border colorizer for a thinking level. Unknown / `off` →
|
|
30
|
+
* the supplied `fallback` (the theme's default border color). */
|
|
31
|
+
export function thinkingBorderColor(level, fallback) {
|
|
32
|
+
const rgb = level === undefined ? undefined : THINKING_RGB[level];
|
|
33
|
+
if (rgb === undefined)
|
|
34
|
+
return fallback;
|
|
35
|
+
const [r, g, b] = rgb;
|
|
36
|
+
return (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m`;
|
|
37
|
+
}
|
|
38
|
+
/** Resolve the title-chip styler for a thinking level: the level's color as the
|
|
39
|
+
* chip BACKGROUND with bold white text (a space of padding each side), so the
|
|
40
|
+
* session name reads as a solid label in the same hue as the border. Unknown /
|
|
41
|
+
* `off` → the supplied `fallback` (the reverse-video default chip). */
|
|
42
|
+
export function thinkingTitleStyle(level, fallback) {
|
|
43
|
+
const rgb = level === undefined ? undefined : THINKING_RGB[level];
|
|
44
|
+
if (rgb === undefined)
|
|
45
|
+
return fallback;
|
|
46
|
+
const [r, g, b] = rgb;
|
|
47
|
+
return (s) => `\x1b[48;2;${r};${g};${b}m\x1b[97m\x1b[1m ${s} \x1b[0m`;
|
|
48
|
+
}
|
|
49
|
+
/** Compose the replacement top-border line: solid title chip + border rule +
|
|
50
|
+
* info chip, never wider than `width` (pi-tui hard-crashes on an over-wide
|
|
51
|
+
* line). The info chip yields entirely when the chip leaves it almost no room,
|
|
52
|
+
* and truncates when it only partially fits. Exported pure for the overflow
|
|
53
|
+
* regression test. */
|
|
54
|
+
export function composeTopBorder(width, title, info, titleStyle, borderColor) {
|
|
55
|
+
const chip = title ? titleStyle(truncateToWidth(title, Math.max(1, width - 4), '…')) : '';
|
|
56
|
+
const chipW = visibleWidth(chip);
|
|
57
|
+
const avail = width - chipW;
|
|
58
|
+
let infoChip = '';
|
|
59
|
+
if (info && avail > 4) {
|
|
60
|
+
infoChip = visibleWidth(info) <= avail ? info : truncateToWidth(info, avail, '…');
|
|
61
|
+
}
|
|
62
|
+
const fill = Math.max(0, width - chipW - visibleWidth(infoChip));
|
|
63
|
+
return chip + borderColor('─'.repeat(fill)) + infoChip;
|
|
64
|
+
}
|
|
65
|
+
export class TitledEditor extends CustomEditor {
|
|
66
|
+
/** Session-name chip painted into the LEFT of the top border. Empty → plain. */
|
|
67
|
+
title = '';
|
|
68
|
+
/** Pre-styled context string painted into the RIGHT of the top border (cwd /
|
|
69
|
+
* branch / git status). Already colorized by the caller; empty → omitted. */
|
|
70
|
+
info = '';
|
|
71
|
+
/** Paint the chip solid so the name reads as a label sitting on the border
|
|
72
|
+
* rule. Defaults to the reverse-video chip; attach-cmd swaps in a
|
|
73
|
+
* thinking-colored background (bold white text) on each state update. */
|
|
74
|
+
titleStyle = defaultTitleStyle;
|
|
75
|
+
render(width) {
|
|
76
|
+
const lines = super.render(width);
|
|
77
|
+
if ((this.title || this.info) && lines.length > 0) {
|
|
78
|
+
// Replace the stock top border; dashes in the current (thinking-aware)
|
|
79
|
+
// border color.
|
|
80
|
+
lines[0] = composeTopBorder(width, this.title, this.info, this.titleStyle, (s) => this.borderColor(s));
|
|
81
|
+
}
|
|
82
|
+
return lines;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import
|
|
2
|
+
import type { NodeRow } from '../../core/canvas/types.js';
|
|
3
|
+
import { type BrokerDataFrame, type BrokerToClient, type ClientToBroker, type DequeueFrame, type GetSettingsFrame, type GetTreeFrame, type ListModelsFrame, type ListScopedModelsFrame, type ListSessionsFrame } from '../../core/runtime/broker-protocol.js';
|
|
4
|
+
/** A correlated read-op (or `dequeue`) request MINUS the client-chosen `id` —
|
|
5
|
+
* {@link ViewSocketClient.request} mints the `id`, sends the frame, and resolves
|
|
6
|
+
* with the matching `data` reply. The picker/operator code builds these; the
|
|
7
|
+
* socket owns the correlation token so callers never hand-roll one. */
|
|
8
|
+
export type ReadOpRequest = Omit<ListModelsFrame, 'id'> | Omit<ListSessionsFrame, 'id'> | Omit<GetTreeFrame, 'id'> | Omit<GetSettingsFrame, 'id'> | Omit<ListScopedModelsFrame, 'id'> | Omit<DequeueFrame, 'id'>;
|
|
3
9
|
/** Surfaced when the node has no reachable broker at connect time (no socket
|
|
4
10
|
* file, or a stale socket with nothing listening). The command catches this to
|
|
5
11
|
* exit non-zero with a focus/revive hint. */
|
|
12
|
+
/** The reconnect supervisor's give-up predicate (extracted pure so it is
|
|
13
|
+
* testable without a socket or TUI). After a broker close the viewer KEEPS
|
|
14
|
+
* re-dialing the same `view.sock` while the node is still alive — a yield
|
|
15
|
+
* leaves `status='active'` (intent='refresh') and the daemon revives a fresh
|
|
16
|
+
* broker on the same path. It gives up only when the node is genuinely gone:
|
|
17
|
+
* a terminal status (done/dead/canceled) or a reaped row (null). `idle` is NOT
|
|
18
|
+
* terminal — an idle-release node revives on its next inbox wake, so keep
|
|
19
|
+
* trying (the supervisor's own ~30s bound caps an indefinite wait). */
|
|
20
|
+
export declare function reconnectShouldGiveUp(row: NodeRow | null): boolean;
|
|
6
21
|
export declare class BrokerUnavailableError extends Error {
|
|
7
22
|
readonly nodeId: string;
|
|
8
23
|
constructor(nodeId: string);
|
|
@@ -24,15 +39,35 @@ export interface ViewSocketClient {
|
|
|
24
39
|
export declare class ViewSocketClient extends EventEmitter {
|
|
25
40
|
private readonly nodeId;
|
|
26
41
|
private socket;
|
|
27
|
-
private
|
|
42
|
+
private decoder;
|
|
28
43
|
private closeEmitted;
|
|
44
|
+
/** In-flight correlated read-ops, keyed by the `id` minted in {@link request}.
|
|
45
|
+
* Resolved by the matching `data` frame / rejected by the matching `error`
|
|
46
|
+
* frame in {@link onData}, the request's timeout, or socket teardown. */
|
|
47
|
+
private pending;
|
|
29
48
|
constructor(nodeId: string);
|
|
49
|
+
/** Issue a correlated read-op and resolve with the broker's `data` reply (or
|
|
50
|
+
* reject on the correlated `error`, a timeout, or socket teardown). Mints the
|
|
51
|
+
* `id`, sends `{...frame, id}`, and parks a resolver consumed by {@link onData}.
|
|
52
|
+
* The reply is narrowed by the caller on its `kind`. */
|
|
53
|
+
request(frame: ReadOpRequest): Promise<BrokerDataFrame>;
|
|
54
|
+
/** Reject + clear every in-flight request (socket gone / decode error) so a
|
|
55
|
+
* picker fetch never hangs past the connection it rode on. */
|
|
56
|
+
private rejectAllPending;
|
|
30
57
|
/** The broker binds `join(nodeDir(id), 'view.sock')` — resolve it the same way. */
|
|
31
58
|
get socketPath(): string;
|
|
32
59
|
/** Open the connection. Wire up listeners; emits `connect` on success or
|
|
33
60
|
* `error` (a {@link BrokerUnavailableError} for ECONNREFUSED/ENOENT) on
|
|
34
61
|
* failure. Idempotent guard is the caller's job — call once. */
|
|
35
62
|
connect(): void;
|
|
63
|
+
/** Re-establish the socket after a broker exit (a yield→revive cycle), on the
|
|
64
|
+
* SAME stable `view.sock` path. Installs a FRESH FrameDecoder (a half-frame
|
|
65
|
+
* from the dead stream must not corrupt the new one) and resets the close
|
|
66
|
+
* guard, then re-dials. Resolves on `connect` (caller re-sends `hello`);
|
|
67
|
+
* rejects on the dial's `error` (ECONNREFUSED while the new broker is
|
|
68
|
+
* mid-boot, or ENOENT before it re-binds the socket) — both retryable. A
|
|
69
|
+
* post-connect error flows through the normal `onError`→`close` path. */
|
|
70
|
+
redial(): Promise<void>;
|
|
36
71
|
/** Encode + write one client→broker frame. No-op on a dead/absent socket
|
|
37
72
|
* (a `close` event drives teardown); never throws. */
|
|
38
73
|
send(frame: ClientToBroker): void;
|
|
@@ -9,20 +9,40 @@
|
|
|
9
9
|
// what keeps a broker alive, not attach). It also never uses pi's `RpcClient`
|
|
10
10
|
// (which would spawn its own engine).
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// `
|
|
15
|
-
// ECONNREFUSED/ENOENT → a `BrokerUnavailableError` so the
|
|
16
|
-
// with a clear message
|
|
17
|
-
//
|
|
12
|
+
// Reconnect: a dropped broker emits `close`; the caller's reconnect supervisor
|
|
13
|
+
// decides whether to re-dial (a yield/revive keeps the SAME `view.sock` path —
|
|
14
|
+
// `redial()` re-establishes the socket on it) or give up ("broker gone"). On a
|
|
15
|
+
// connect-time ECONNREFUSED/ENOENT → a `BrokerUnavailableError` so the FIRST
|
|
16
|
+
// connect exits non-zero with a clear message; the same codes during a redial
|
|
17
|
+
// reject the redial promise as retryable. Decode is bounded by
|
|
18
|
+
// `CLIENT_READ_CAPS`; a broker that somehow sends an oversized frame surfaces as
|
|
19
|
+
// a clean error+exit, not a crash.
|
|
18
20
|
import { EventEmitter } from 'node:events';
|
|
21
|
+
import { randomUUID } from 'node:crypto';
|
|
19
22
|
import { createConnection } from 'node:net';
|
|
20
23
|
import { join } from 'node:path';
|
|
21
24
|
import { nodeDir } from '../../core/canvas/paths.js';
|
|
22
25
|
import { CLIENT_READ_CAPS, encodeFrame, FrameDecoder, FrameOverflowError, } from '../../core/runtime/broker-protocol.js';
|
|
26
|
+
/** How long {@link ViewSocketClient.request} waits for the correlated reply before
|
|
27
|
+
* rejecting — bounds a pending picker fetch if the broker drops the request
|
|
28
|
+
* (it should always reply with `data` or a correlated `error`). */
|
|
29
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
23
30
|
/** Surfaced when the node has no reachable broker at connect time (no socket
|
|
24
31
|
* file, or a stale socket with nothing listening). The command catches this to
|
|
25
32
|
* exit non-zero with a focus/revive hint. */
|
|
33
|
+
/** The reconnect supervisor's give-up predicate (extracted pure so it is
|
|
34
|
+
* testable without a socket or TUI). After a broker close the viewer KEEPS
|
|
35
|
+
* re-dialing the same `view.sock` while the node is still alive — a yield
|
|
36
|
+
* leaves `status='active'` (intent='refresh') and the daemon revives a fresh
|
|
37
|
+
* broker on the same path. It gives up only when the node is genuinely gone:
|
|
38
|
+
* a terminal status (done/dead/canceled) or a reaped row (null). `idle` is NOT
|
|
39
|
+
* terminal — an idle-release node revives on its next inbox wake, so keep
|
|
40
|
+
* trying (the supervisor's own ~30s bound caps an indefinite wait). */
|
|
41
|
+
export function reconnectShouldGiveUp(row) {
|
|
42
|
+
if (row === null)
|
|
43
|
+
return true;
|
|
44
|
+
return row.status === 'done' || row.status === 'dead' || row.status === 'canceled';
|
|
45
|
+
}
|
|
26
46
|
export class BrokerUnavailableError extends Error {
|
|
27
47
|
nodeId;
|
|
28
48
|
constructor(nodeId) {
|
|
@@ -36,10 +56,48 @@ export class ViewSocketClient extends EventEmitter {
|
|
|
36
56
|
socket;
|
|
37
57
|
decoder = new FrameDecoder(CLIENT_READ_CAPS);
|
|
38
58
|
closeEmitted = false;
|
|
59
|
+
/** In-flight correlated read-ops, keyed by the `id` minted in {@link request}.
|
|
60
|
+
* Resolved by the matching `data` frame / rejected by the matching `error`
|
|
61
|
+
* frame in {@link onData}, the request's timeout, or socket teardown. */
|
|
62
|
+
pending = new Map();
|
|
39
63
|
constructor(nodeId) {
|
|
40
64
|
super();
|
|
41
65
|
this.nodeId = nodeId;
|
|
42
66
|
}
|
|
67
|
+
/** Issue a correlated read-op and resolve with the broker's `data` reply (or
|
|
68
|
+
* reject on the correlated `error`, a timeout, or socket teardown). Mints the
|
|
69
|
+
* `id`, sends `{...frame, id}`, and parks a resolver consumed by {@link onData}.
|
|
70
|
+
* The reply is narrowed by the caller on its `kind`. */
|
|
71
|
+
request(frame) {
|
|
72
|
+
const id = randomUUID();
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
// Fail fast on a dead/absent socket rather than parking a promise that the
|
|
75
|
+
// reply will never reach (it would otherwise hang the full timeout). A
|
|
76
|
+
// post-`close` request lands here too — `rejectAllPending` already ran.
|
|
77
|
+
const sock = this.socket;
|
|
78
|
+
if (sock === undefined || sock.destroyed) {
|
|
79
|
+
reject(new Error(`cannot issue '${frame.type}': no live broker connection`));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const timer = setTimeout(() => {
|
|
83
|
+
this.pending.delete(id);
|
|
84
|
+
reject(new Error(`request '${frame.type}' timed out after ${REQUEST_TIMEOUT_MS}ms`));
|
|
85
|
+
}, REQUEST_TIMEOUT_MS);
|
|
86
|
+
if (typeof timer.unref === 'function')
|
|
87
|
+
timer.unref();
|
|
88
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
89
|
+
this.send({ ...frame, id });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Reject + clear every in-flight request (socket gone / decode error) so a
|
|
93
|
+
* picker fetch never hangs past the connection it rode on. */
|
|
94
|
+
rejectAllPending(reason) {
|
|
95
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
reject(new Error(reason));
|
|
98
|
+
}
|
|
99
|
+
this.pending.clear();
|
|
100
|
+
}
|
|
43
101
|
/** The broker binds `join(nodeDir(id), 'view.sock')` — resolve it the same way. */
|
|
44
102
|
get socketPath() {
|
|
45
103
|
return join(nodeDir(this.nodeId), 'view.sock');
|
|
@@ -55,6 +113,46 @@ export class ViewSocketClient extends EventEmitter {
|
|
|
55
113
|
socket.on('error', (err) => this.onError(err));
|
|
56
114
|
socket.on('close', () => this.onClose());
|
|
57
115
|
}
|
|
116
|
+
/** Re-establish the socket after a broker exit (a yield→revive cycle), on the
|
|
117
|
+
* SAME stable `view.sock` path. Installs a FRESH FrameDecoder (a half-frame
|
|
118
|
+
* from the dead stream must not corrupt the new one) and resets the close
|
|
119
|
+
* guard, then re-dials. Resolves on `connect` (caller re-sends `hello`);
|
|
120
|
+
* rejects on the dial's `error` (ECONNREFUSED while the new broker is
|
|
121
|
+
* mid-boot, or ENOENT before it re-binds the socket) — both retryable. A
|
|
122
|
+
* post-connect error flows through the normal `onError`→`close` path. */
|
|
123
|
+
redial() {
|
|
124
|
+
this.destroy();
|
|
125
|
+
this.decoder = new FrameDecoder(CLIENT_READ_CAPS);
|
|
126
|
+
this.closeEmitted = false;
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const socket = createConnection(this.socketPath);
|
|
129
|
+
this.socket = socket;
|
|
130
|
+
let settled = false;
|
|
131
|
+
socket.on('connect', () => {
|
|
132
|
+
if (settled)
|
|
133
|
+
return;
|
|
134
|
+
settled = true;
|
|
135
|
+
this.emit('connect');
|
|
136
|
+
resolve();
|
|
137
|
+
});
|
|
138
|
+
socket.on('data', (chunk) => this.onData(chunk));
|
|
139
|
+
socket.on('error', (err) => {
|
|
140
|
+
if (settled) {
|
|
141
|
+
this.onError(err);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
settled = true;
|
|
145
|
+
try {
|
|
146
|
+
socket.destroy();
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
/* ignore */
|
|
150
|
+
}
|
|
151
|
+
reject(err);
|
|
152
|
+
});
|
|
153
|
+
socket.on('close', () => this.onClose());
|
|
154
|
+
});
|
|
155
|
+
}
|
|
58
156
|
/** Encode + write one client→broker frame. No-op on a dead/absent socket
|
|
59
157
|
* (a `close` event drives teardown); never throws. */
|
|
60
158
|
send(frame) {
|
|
@@ -83,12 +181,37 @@ export class ViewSocketClient extends EventEmitter {
|
|
|
83
181
|
const msg = err instanceof FrameOverflowError
|
|
84
182
|
? `broker sent an oversized frame (${err.message}) — disconnecting`
|
|
85
183
|
: `failed to decode a broker frame: ${String(err)}`;
|
|
184
|
+
this.rejectAllPending(msg);
|
|
86
185
|
this.emitError(new Error(msg));
|
|
87
186
|
this.destroy();
|
|
88
187
|
return;
|
|
89
188
|
}
|
|
90
189
|
for (const raw of frames) {
|
|
91
|
-
|
|
190
|
+
const frame = raw;
|
|
191
|
+
// Correlated replies (read-ops + dequeue) are consumed by the pending-by-id
|
|
192
|
+
// resolver, NOT re-emitted as a generic 'frame' (the attach frame router
|
|
193
|
+
// would otherwise treat a `data` frame as an AgentSessionEvent). A `data`
|
|
194
|
+
// frame is ALWAYS a reply, so it is swallowed unconditionally — even a late
|
|
195
|
+
// post-timeout one (no pending entry) is dropped, never leaked to the
|
|
196
|
+
// router. An `error` is correlated only when its `id` matches an in-flight
|
|
197
|
+
// request; an uncorrelated error still flows through.
|
|
198
|
+
if (frame.type === 'data') {
|
|
199
|
+
const entry = typeof frame.id === 'string' ? this.pending.get(frame.id) : undefined;
|
|
200
|
+
if (entry) {
|
|
201
|
+
this.pending.delete(frame.id);
|
|
202
|
+
clearTimeout(entry.timer);
|
|
203
|
+
entry.resolve(frame);
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (frame.type === 'error' && typeof frame.id === 'string' && this.pending.has(frame.id)) {
|
|
208
|
+
const entry = this.pending.get(frame.id);
|
|
209
|
+
this.pending.delete(frame.id);
|
|
210
|
+
clearTimeout(entry.timer);
|
|
211
|
+
entry.reject(new Error(frame.message || `request failed: ${frame.code}`));
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
this.emit('frame', frame);
|
|
92
215
|
}
|
|
93
216
|
}
|
|
94
217
|
onError(err) {
|
|
@@ -104,6 +227,7 @@ export class ViewSocketClient extends EventEmitter {
|
|
|
104
227
|
if (this.closeEmitted)
|
|
105
228
|
return;
|
|
106
229
|
this.closeEmitted = true;
|
|
230
|
+
this.rejectAllPending('broker connection closed');
|
|
107
231
|
this.emit('close');
|
|
108
232
|
}
|
|
109
233
|
destroy() {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Server as HttpServer } from 'node:http';
|
|
2
|
+
import type { ViteDevServer } from 'vite';
|
|
3
|
+
/** Create a Vite dev server in middleware mode, HMR bound to `httpServer`.
|
|
4
|
+
* Mirrors the old `view serve` resolve setup so the shell's bare imports
|
|
5
|
+
* (react, the `@crouton-kit/crouter/web` self-subpath) resolve from the
|
|
6
|
+
* generated/shell root. Lazily imports vite so the shipped path never loads it. */
|
|
7
|
+
export declare function createDevServer(httpServer: HttpServer): Promise<ViteDevServer>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// dev-server.ts — `crtr web serve --dev` Vite middleware mode.
|
|
2
|
+
//
|
|
3
|
+
// The shipped server serves a prebuilt static shell bundle from dist/web-client/
|
|
4
|
+
// (§11). In --dev a contributor iterating on the shell or a builtin view gets
|
|
5
|
+
// HMR instead: a Vite dev server in MIDDLEWARE mode mounted on the SAME HTTP
|
|
6
|
+
// server as the bridge + WS relay. This is the one place Vite runs at runtime —
|
|
7
|
+
// the mainstream "Vite middleware in dev, static dist in prod" split.
|
|
8
|
+
//
|
|
9
|
+
// The bridge (POST /__crtr/source), the SSE lane (GET /__crtr/events) and the
|
|
10
|
+
// WS relay are owned by server.ts and checked BEFORE Vite's middlewares; this
|
|
11
|
+
// module only supplies the asset/HMR middleware. Vite's HMR WebSocket shares
|
|
12
|
+
// the HTTP server (server.hmr.server) — server.ts's upgrade handler routes
|
|
13
|
+
// `/node/<id>` to the broker relay and leaves every other upgrade for Vite.
|
|
14
|
+
import { resolve, dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
/** The shell SPA Vite root — the in-tree shell project (`src/clients/web/web-client/`).
|
|
18
|
+
* --dev is a from-source contributor loop, so this resolves against the source
|
|
19
|
+
* tree (HERE is `dist/clients/web` when built, `src/clients/web` under tsx);
|
|
20
|
+
* walk to the package root and into src. */
|
|
21
|
+
function resolveShellRoot(packageRoot) {
|
|
22
|
+
return join(packageRoot, 'src', 'clients', 'web', 'web-client');
|
|
23
|
+
}
|
|
24
|
+
/** Create a Vite dev server in middleware mode, HMR bound to `httpServer`.
|
|
25
|
+
* Mirrors the old `view serve` resolve setup so the shell's bare imports
|
|
26
|
+
* (react, the `@crouton-kit/crouter/web` self-subpath) resolve from the
|
|
27
|
+
* generated/shell root. Lazily imports vite so the shipped path never loads it. */
|
|
28
|
+
export async function createDevServer(httpServer) {
|
|
29
|
+
const packageRoot = resolve(HERE, '../../..');
|
|
30
|
+
const nodeModules = join(packageRoot, 'node_modules');
|
|
31
|
+
const shellRoot = resolveShellRoot(packageRoot);
|
|
32
|
+
const { createServer } = await import('vite');
|
|
33
|
+
const react = (await import('@vitejs/plugin-react')).default;
|
|
34
|
+
const tailwindcss = (await import('@tailwindcss/vite')).default;
|
|
35
|
+
return createServer({
|
|
36
|
+
configFile: false,
|
|
37
|
+
root: shellRoot,
|
|
38
|
+
plugins: [react(), tailwindcss()],
|
|
39
|
+
appType: 'spa',
|
|
40
|
+
resolve: {
|
|
41
|
+
alias: {
|
|
42
|
+
react: join(nodeModules, 'react'),
|
|
43
|
+
'react-dom': join(nodeModules, 'react-dom'),
|
|
44
|
+
// The crtr web runtime is a self-subpath of crouter's own package;
|
|
45
|
+
// pin it to the built barrel so the shell can import it from any root.
|
|
46
|
+
'@crouton-kit/crouter/web': join(packageRoot, 'dist', 'web', 'index.js'),
|
|
47
|
+
},
|
|
48
|
+
dedupe: ['react', 'react-dom'],
|
|
49
|
+
},
|
|
50
|
+
server: {
|
|
51
|
+
middlewareMode: true,
|
|
52
|
+
// Share our HTTP server for HMR; server.ts's upgrade handler hands every
|
|
53
|
+
// non-`/node/<id>` upgrade to Vite (the vite-hmr WebSocket).
|
|
54
|
+
hmr: { server: httpServer },
|
|
55
|
+
fs: { allow: [shellRoot, packageRoot] },
|
|
56
|
+
},
|
|
57
|
+
logLevel: 'warn',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ServerResponse } from 'node:http';
|
|
2
|
+
export type ChangeKind = 'nodes' | 'inbox';
|
|
3
|
+
export interface EventHub {
|
|
4
|
+
/** Register an SSE client: writes the event-stream headers, streams events
|
|
5
|
+
* until the connection closes, and self-removes on close. */
|
|
6
|
+
addClient: (res: ServerResponse) => void;
|
|
7
|
+
/** Tear down watchers + heartbeat and end all open streams. */
|
|
8
|
+
close: () => void;
|
|
9
|
+
}
|
|
10
|
+
/** Start the change-event hub: open the two fs watchers once, fan their
|
|
11
|
+
* debounced invalidations out to every connected SSE client. Watchers run for
|
|
12
|
+
* the server's lifetime (cheap — two inotify/FSEvents registrations), not
|
|
13
|
+
* per-client, so N browsers share one watch. */
|
|
14
|
+
export declare function startEventHub(): EventHub;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// events.ts — the `crtr web` realtime push lane (the §7 SSE seam, built for v1).
|
|
2
|
+
//
|
|
3
|
+
// ONE server-sent-events stream, `GET /__crtr/events`, that carries CHANGE
|
|
4
|
+
// INVALIDATIONS — never data. The shell / its ViewPanes subscribe and, on an
|
|
5
|
+
// event, re-pull through their existing Sources (the bridge) instead of waiting
|
|
6
|
+
// on a fixed poll tick. SSE carries "something in this class changed"; the
|
|
7
|
+
// consumer decides what to re-read. This keeps the event vocabulary tiny and
|
|
8
|
+
// generic and the contract unchanged (a ViewPane still just runs `refresh`).
|
|
9
|
+
//
|
|
10
|
+
// Event vocabulary (deliberately minimal): { kind: 'nodes' | 'inbox', ts }.
|
|
11
|
+
// - 'nodes' — the node graph changed (topology, status/intent transitions,
|
|
12
|
+
// naming). Source: canvas.db (+ its WAL sidecar) mtime. Every transition()
|
|
13
|
+
// and canvas mutation writes the WAL, so a watch on the home dir's
|
|
14
|
+
// `canvas.db*` files detects all of them with one cheap watcher.
|
|
15
|
+
// - 'inbox' — a node's inbox.jsonl gained a message. Source: a recursive watch
|
|
16
|
+
// on the nodes/ tree, filtered to `inbox.jsonl` writes.
|
|
17
|
+
//
|
|
18
|
+
// WHY these sources (the §7 seam left the event source open): they are the
|
|
19
|
+
// cheapest things that detect node-graph + inbox changes without a daemon hook
|
|
20
|
+
// or a db trigger — pure fs.watch on the two on-disk authorities (canvas.db for
|
|
21
|
+
// the graph, inbox.jsonl appends for messages). No polling, no canvas.db reads.
|
|
22
|
+
import { watch } from 'node:fs';
|
|
23
|
+
import { crtrHome, nodesRoot, ensureHome } from '../../core/canvas/paths.js';
|
|
24
|
+
/** Coalesce a burst of fs events into one emission per kind. WAL writes and
|
|
25
|
+
* inbox appends arrive in clusters (a single transition can touch -wal/-shm);
|
|
26
|
+
* a short trailing debounce collapses each cluster into a single invalidation,
|
|
27
|
+
* which is all a re-pull consumer needs. */
|
|
28
|
+
const DEBOUNCE_MS = 150;
|
|
29
|
+
/** SSE keepalive comment cadence — keeps proxies/clients from idling the stream
|
|
30
|
+
* shut. A bare `:` comment line is ignored by EventSource. */
|
|
31
|
+
const HEARTBEAT_MS = 25_000;
|
|
32
|
+
/** Start the change-event hub: open the two fs watchers once, fan their
|
|
33
|
+
* debounced invalidations out to every connected SSE client. Watchers run for
|
|
34
|
+
* the server's lifetime (cheap — two inotify/FSEvents registrations), not
|
|
35
|
+
* per-client, so N browsers share one watch. */
|
|
36
|
+
export function startEventHub() {
|
|
37
|
+
ensureHome();
|
|
38
|
+
const clients = new Set();
|
|
39
|
+
const watchers = [];
|
|
40
|
+
const timers = new Map();
|
|
41
|
+
const broadcast = (kind) => {
|
|
42
|
+
const line = `data: ${JSON.stringify({ kind, ts: Date.now() })}\n\n`;
|
|
43
|
+
for (const res of clients) {
|
|
44
|
+
try {
|
|
45
|
+
res.write(line);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* a dead stream is reaped by its own 'close' handler */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
// Debounce per kind: an fs event arms a trailing timer; a fresh event within
|
|
53
|
+
// the window resets it, so a write cluster emits exactly once.
|
|
54
|
+
const emit = (kind) => {
|
|
55
|
+
const existing = timers.get(kind);
|
|
56
|
+
if (existing !== undefined)
|
|
57
|
+
clearTimeout(existing);
|
|
58
|
+
timers.set(kind, setTimeout(() => {
|
|
59
|
+
timers.delete(kind);
|
|
60
|
+
broadcast(kind);
|
|
61
|
+
}, DEBOUNCE_MS));
|
|
62
|
+
};
|
|
63
|
+
// Watcher 1 — the canvas home dir (non-recursive). canvas.db lives in WAL
|
|
64
|
+
// mode, so graph writes land in canvas.db-wal and checkpoints in canvas.db;
|
|
65
|
+
// watching the dir catches every `canvas.db*` sidecar touch → 'nodes'.
|
|
66
|
+
try {
|
|
67
|
+
const homeWatcher = watch(crtrHome(), (_event, filename) => {
|
|
68
|
+
if (filename !== null && filename.startsWith('canvas.db'))
|
|
69
|
+
emit('nodes');
|
|
70
|
+
});
|
|
71
|
+
homeWatcher.on('error', () => {
|
|
72
|
+
/* watch dropped (dir replaced) — non-fatal; the poll cadence still backs us */
|
|
73
|
+
});
|
|
74
|
+
watchers.push(homeWatcher);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* fs.watch unavailable on this platform — degrade to poll-only (no SSE) */
|
|
78
|
+
}
|
|
79
|
+
// Watcher 2 — the nodes/ tree (recursive). An inbox.jsonl append is a message
|
|
80
|
+
// landing; a meta.json write is a graph/identity change (naming, polymorph).
|
|
81
|
+
// Recursive fs.watch is supported on macOS/Windows always and Linux ≥ v20.
|
|
82
|
+
try {
|
|
83
|
+
const nodesWatcher = watch(nodesRoot(), { recursive: true }, (_event, filename) => {
|
|
84
|
+
if (filename === null)
|
|
85
|
+
return;
|
|
86
|
+
if (filename.endsWith('inbox.jsonl'))
|
|
87
|
+
emit('inbox');
|
|
88
|
+
else if (filename.endsWith('meta.json'))
|
|
89
|
+
emit('nodes');
|
|
90
|
+
});
|
|
91
|
+
nodesWatcher.on('error', () => {
|
|
92
|
+
/* recursive watch refused/dropped — non-fatal */
|
|
93
|
+
});
|
|
94
|
+
watchers.push(nodesWatcher);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
/* recursive watch unsupported — inbox push degrades to poll-only */
|
|
98
|
+
}
|
|
99
|
+
const heartbeat = setInterval(() => {
|
|
100
|
+
for (const res of clients) {
|
|
101
|
+
try {
|
|
102
|
+
res.write(': ping\n\n');
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
/* reaped by its 'close' handler */
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}, HEARTBEAT_MS);
|
|
109
|
+
heartbeat.unref();
|
|
110
|
+
return {
|
|
111
|
+
addClient(res) {
|
|
112
|
+
res.writeHead(200, {
|
|
113
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
114
|
+
'cache-control': 'no-cache, no-transform',
|
|
115
|
+
connection: 'keep-alive',
|
|
116
|
+
// The browser EventSource is same-origin here; no CORS header needed.
|
|
117
|
+
});
|
|
118
|
+
// An initial comment flushes headers and confirms the stream is open.
|
|
119
|
+
res.write(': connected\n\n');
|
|
120
|
+
clients.add(res);
|
|
121
|
+
const drop = () => {
|
|
122
|
+
clients.delete(res);
|
|
123
|
+
};
|
|
124
|
+
res.on('close', drop);
|
|
125
|
+
res.on('error', drop);
|
|
126
|
+
},
|
|
127
|
+
close() {
|
|
128
|
+
clearInterval(heartbeat);
|
|
129
|
+
for (const t of timers.values())
|
|
130
|
+
clearTimeout(t);
|
|
131
|
+
timers.clear();
|
|
132
|
+
for (const w of watchers) {
|
|
133
|
+
try {
|
|
134
|
+
w.close();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* ignore */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const res of clients) {
|
|
141
|
+
try {
|
|
142
|
+
res.end();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
/* ignore */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
clients.clear();
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface WebServerOptions {
|
|
2
|
+
port: number;
|
|
3
|
+
host?: string;
|
|
4
|
+
/** --dev: mount a Vite dev server (middleware mode, HMR) for asset serving
|
|
5
|
+
* instead of the static dist/web-client/ bundle. The bridge + SSE + WS relay
|
|
6
|
+
* are identical in both modes. */
|
|
7
|
+
dev?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface RunningWebServer {
|
|
10
|
+
url: string;
|
|
11
|
+
port: number;
|
|
12
|
+
clientDir: string;
|
|
13
|
+
close: () => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/** Build + start the unified web server. Binds 127.0.0.1 only (remote access is
|
|
16
|
+
* out of scope — §9). Resolves once the server is listening. Async because
|
|
17
|
+
* --dev awaits a Vite middleware server before listening. */
|
|
18
|
+
export declare function startWebServer(opts: WebServerOptions): Promise<RunningWebServer>;
|