@crouton-kit/crouter 0.3.28 → 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 +5 -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__/slash-quit-copy.test.js +42 -0
- package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
- package/dist/clients/attach/attach-cmd.js +282 -46
- 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 +4 -2
- package/dist/clients/attach/canvas-panels.js +45 -17
- 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 +13 -4
- package/dist/clients/attach/graph-overlay.js +59 -19
- package/dist/clients/attach/input-controller.d.ts +75 -13
- package/dist/clients/attach/input-controller.js +305 -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 +22 -0
- package/dist/clients/attach/slash-commands.js +91 -25
- 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 +18 -1
- package/dist/clients/attach/view-socket.js +70 -1
- 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/memory/__tests__/lint-schema.test.js +6 -6
- package/dist/commands/memory/lint.js +14 -2
- 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 +8 -4
- 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/sync.d.ts +1 -0
- package/dist/commands/sys/sync.js +187 -0
- package/dist/commands/sys.js +3 -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__/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 +17 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +22 -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__/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.js +2 -2
- 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.js +8 -7
- 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.d.ts +4 -1
- package/dist/core/runtime/broker.js +494 -58
- package/dist/core/runtime/close.d.ts +12 -2
- package/dist/core/runtime/close.js +37 -16
- package/dist/core/runtime/front-door.js +6 -14
- 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 -335
- package/dist/core/runtime/placement.js +262 -840
- 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 +40 -139
- package/dist/core/runtime/spawn.d.ts +15 -16
- package/dist/core/runtime/spawn.js +165 -194
- 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 +13 -50
- package/dist/core/runtime/tmux.js +33 -87
- 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/substrate/index.d.ts +1 -1
- package/dist/core/substrate/index.js +1 -1
- package/dist/core/substrate/injected-store.js +3 -3
- 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 +10 -3
- package/dist/core/substrate/schema.js +6 -3
- 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 +16 -16
- package/dist/pi-extensions/canvas-nav.js +5 -0
- 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 +14 -12
- package/dist/pi-extensions/canvas-stophook.js +87 -101
- 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 -297
- 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__/slash-quit-copy.test.d.ts} +0 -0
- /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
//
|
|
3
3
|
// pi's `readClipboardImage` is NOT re-exported (it lives behind a native
|
|
4
4
|
// clipboard binding + Photon worker the package gates off `.`-only exports), so
|
|
5
|
-
// we reimplement the READ by shelling out to the platform clipboard tool
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// we reimplement the READ by shelling out to the platform clipboard tool. macOS:
|
|
6
|
+
// `pngpaste` is a faster path IF present, but it is NOT a built-in (`brew install
|
|
7
|
+
// pngpaste`), so the authoritative read is the dependency-free `osascript` path
|
|
8
|
+
// (`the clipboard as «class PNGf»` → temp PNG), which works on a stock Mac.
|
|
9
|
+
// Linux: `wl-paste` on Wayland, `xclip` on X11. The reader DISTINGUISHES three
|
|
10
|
+
// outcomes — got bytes, clipboard genuinely empty, or no clipboard tool / read
|
|
11
|
+
// failure — so the caller's notice is accurate ("No image in the clipboard" vs a
|
|
12
|
+
// precise "install xclip" / "osascript failed") instead of collapsing every
|
|
13
|
+
// miss to "no image".
|
|
8
14
|
//
|
|
9
15
|
// The bytes are then resized AGGRESSIVELY through pi's exported `resizeImage`
|
|
10
16
|
// (which is reusable, unlike the reader) so the base64 stays well within the
|
|
@@ -22,6 +28,9 @@
|
|
|
22
28
|
// straight into a `prompt`/`steer`/`follow_up` frame's `images?` array (or, when
|
|
23
29
|
// dropped, a `note`-only result and no image).
|
|
24
30
|
import { spawnSync } from 'node:child_process';
|
|
31
|
+
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
32
|
+
import { tmpdir } from 'node:os';
|
|
33
|
+
import { join } from 'node:path';
|
|
25
34
|
import { convertToPng, formatDimensionNote, resizeImage, } from '@earendil-works/pi-coding-agent';
|
|
26
35
|
/** Longest edge (px) the pasted image is resized to — aggressive, matches the
|
|
27
36
|
* ~1568px long-edge guidance vision models use. */
|
|
@@ -34,13 +43,32 @@ const MAX_BYTES = 3 * 1024 * 1024;
|
|
|
34
43
|
/** Bound the shell read so a giant/garbage clipboard can't blow up memory. */
|
|
35
44
|
const SPAWN_MAX_BUFFER = 64 * 1024 * 1024;
|
|
36
45
|
const SPAWN_TIMEOUT_MS = 3000;
|
|
37
|
-
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
46
|
+
/** Persist a (already-resized) clipboard image to a stable temp file and return
|
|
47
|
+
* its absolute path, so the attach editor can drop the PATH inline into the
|
|
48
|
+
* prompt instead of inlining base64 the agent can't see. The broker runs on the
|
|
49
|
+
* same host, so a tmpdir path is readable by the engine. */
|
|
50
|
+
export function writeClipboardImageToFile(image) {
|
|
51
|
+
const dir = join(tmpdir(), 'crtr-clip-images');
|
|
52
|
+
mkdirSync(dir, { recursive: true });
|
|
53
|
+
const ext = image.mimeType === 'image/jpeg' ? 'jpg' : 'png';
|
|
54
|
+
const path = join(dir, `paste-${Date.now()}-${process.pid}.${ext}`);
|
|
55
|
+
writeFileSync(path, Buffer.from(image.data, 'base64'));
|
|
56
|
+
return path;
|
|
57
|
+
}
|
|
58
|
+
/** Read + resize the current clipboard image. Returns `null` ONLY when the
|
|
59
|
+
* clipboard genuinely holds no image; a `note`-only result means there is a
|
|
60
|
+
* precise reason to surface (no clipboard tool, read failure, or an over-cap
|
|
61
|
+
* drop). All best-effort — the caller shows a brief notice and carries on. */
|
|
40
62
|
export async function readClipboardImage() {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
63
|
+
const outcome = readRawClipboardImage();
|
|
64
|
+
const raw = outcome.image;
|
|
65
|
+
if (!raw) {
|
|
66
|
+
// No bytes. A `note` means a precise reason worth surfacing (no clipboard
|
|
67
|
+
// tool, or a read failure) — return it so the caller shows it instead of the
|
|
68
|
+
// generic "No image in the clipboard". No note → the clipboard is genuinely
|
|
69
|
+
// empty → null (caller shows the generic notice).
|
|
70
|
+
return outcome.note ? { note: outcome.note } : null;
|
|
71
|
+
}
|
|
44
72
|
// Primary path: resize aggressively. Handles format (PNG/JPEG) + size bound.
|
|
45
73
|
const resized = await resizeImage(new Uint8Array(raw.bytes), raw.mimeType, {
|
|
46
74
|
maxWidth: MAX_EDGE,
|
|
@@ -78,36 +106,132 @@ function capped(image) {
|
|
|
78
106
|
/** Shell out to the platform clipboard tool for raw image bytes. */
|
|
79
107
|
function readRawClipboardImage() {
|
|
80
108
|
if (process.platform === 'darwin') {
|
|
81
|
-
|
|
82
|
-
return out ? { bytes: out, mimeType: 'image/png' } : null;
|
|
109
|
+
return selectMacOutcome(run, readMacClipboardViaOsascript);
|
|
83
110
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
return selectLinuxOutcome(run, isWayland());
|
|
112
|
+
}
|
|
113
|
+
/** Pure macOS dispatch (the regression-test seam): try `pngpaste` if present,
|
|
114
|
+
* else fall through to the native osascript read — a missing `pngpaste` must
|
|
115
|
+
* NOT collapse to "no image", which was the stock-Mac bug. */
|
|
116
|
+
export function selectMacOutcome(runTool, readNative) {
|
|
117
|
+
const fast = runTool('pngpaste', ['-']);
|
|
118
|
+
if (fast.bytes)
|
|
119
|
+
return { image: { bytes: fast.bytes, mimeType: 'image/png' } };
|
|
120
|
+
// pngpaste missing OR clipboard empty → osascript is authoritative.
|
|
121
|
+
return readNative();
|
|
122
|
+
}
|
|
123
|
+
/** Pure Linux dispatch (the regression-test seam): Wayland (`wl-paste`) first,
|
|
124
|
+
* then X11 (`xclip`), PNG then JPEG. If NO tool binary was found, surface a
|
|
125
|
+
* precise "install …" note; if a tool ran but found no image, report empty. */
|
|
126
|
+
export function selectLinuxOutcome(runTool, wayland) {
|
|
127
|
+
let anyToolPresent = false;
|
|
128
|
+
if (wayland) {
|
|
129
|
+
const out = runTool('wl-paste', ['--type', 'image/png', '--no-newline']);
|
|
130
|
+
if (out.bytes)
|
|
131
|
+
return { image: { bytes: out.bytes, mimeType: 'image/png' } };
|
|
132
|
+
if (!out.missing)
|
|
133
|
+
anyToolPresent = true;
|
|
89
134
|
}
|
|
90
135
|
for (const mimeType of ['image/png', 'image/jpeg']) {
|
|
91
|
-
const out =
|
|
92
|
-
if (out)
|
|
93
|
-
return { bytes: out, mimeType };
|
|
136
|
+
const out = runTool('xclip', ['-selection', 'clipboard', '-t', mimeType, '-o']);
|
|
137
|
+
if (out.bytes)
|
|
138
|
+
return { image: { bytes: out.bytes, mimeType } };
|
|
139
|
+
if (!out.missing)
|
|
140
|
+
anyToolPresent = true;
|
|
141
|
+
}
|
|
142
|
+
if (!anyToolPresent) {
|
|
143
|
+
return {
|
|
144
|
+
note: wayland
|
|
145
|
+
? 'No clipboard tool found — install wl-clipboard (wl-paste) to paste images'
|
|
146
|
+
: 'No clipboard tool found — install xclip to paste images',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return {}; // a tool ran but the clipboard holds no image
|
|
150
|
+
}
|
|
151
|
+
/** AppleScript that writes the clipboard's PNG representation to the temp path
|
|
152
|
+
* in `argv`. `the clipboard as «class PNGf»` THROWS when the clipboard has no
|
|
153
|
+
* PNG-convertible image — caught → "NO_IMAGE" (a clean empty signal, distinct
|
|
154
|
+
* from a failure). A write failure (after `set eof` already truncated the file)
|
|
155
|
+
* closes the handle and returns "WRITE_FAILED" rather than swallowing the error
|
|
156
|
+
* and reporting a false "OK" over a 0-byte/partial file — so "OK" reliably means
|
|
157
|
+
* the full bytes are in the temp file, preserving the empty-vs-failure split. */
|
|
158
|
+
const MAC_PNG_SCRIPT = `on run argv
|
|
159
|
+
set outPath to item 1 of argv
|
|
160
|
+
try
|
|
161
|
+
set pngData to (the clipboard as «class PNGf»)
|
|
162
|
+
on error
|
|
163
|
+
return "NO_IMAGE"
|
|
164
|
+
end try
|
|
165
|
+
set fh to open for access (POSIX file outPath) with write permission
|
|
166
|
+
try
|
|
167
|
+
set eof fh to 0
|
|
168
|
+
write pngData to fh
|
|
169
|
+
close access fh
|
|
170
|
+
on error
|
|
171
|
+
try
|
|
172
|
+
close access fh
|
|
173
|
+
end try
|
|
174
|
+
return "WRITE_FAILED"
|
|
175
|
+
end try
|
|
176
|
+
return "OK"
|
|
177
|
+
end run`;
|
|
178
|
+
/** Native, dependency-free macOS read: osascript (always present) writes the
|
|
179
|
+
* clipboard PNG to a temp file we then read. Distinguishes empty ("NO_IMAGE")
|
|
180
|
+
* from a genuine read failure so the caller's notice is accurate. */
|
|
181
|
+
function readMacClipboardViaOsascript() {
|
|
182
|
+
const tmpFile = join(tmpdir(), `crtr-clip-${process.pid}-${Date.now()}.png`);
|
|
183
|
+
try {
|
|
184
|
+
const result = spawnSync('osascript', ['-e', MAC_PNG_SCRIPT, tmpFile], {
|
|
185
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
186
|
+
maxBuffer: SPAWN_MAX_BUFFER,
|
|
187
|
+
encoding: 'utf-8',
|
|
188
|
+
});
|
|
189
|
+
if (result.error || result.status !== 0) {
|
|
190
|
+
return { note: 'Could not read the clipboard (osascript failed)' };
|
|
191
|
+
}
|
|
192
|
+
const signal = (result.stdout ?? '').trim();
|
|
193
|
+
if (signal === 'NO_IMAGE')
|
|
194
|
+
return {}; // clipboard genuinely holds no image
|
|
195
|
+
if (signal !== 'OK')
|
|
196
|
+
return { note: 'Could not read the clipboard image' };
|
|
197
|
+
let bytes;
|
|
198
|
+
try {
|
|
199
|
+
bytes = readFileSync(tmpFile);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return { note: 'Could not read the clipboard image' };
|
|
203
|
+
}
|
|
204
|
+
return bytes.length > 0 ? { image: { bytes, mimeType: 'image/png' } } : {};
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
try {
|
|
208
|
+
unlinkSync(tmpFile);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
/* ignore temp-file cleanup errors (NO_IMAGE never created it) */
|
|
212
|
+
}
|
|
94
213
|
}
|
|
95
|
-
return null;
|
|
96
214
|
}
|
|
97
215
|
function isWayland() {
|
|
98
216
|
return Boolean(process.env.WAYLAND_DISPLAY) || process.env.XDG_SESSION_TYPE === 'wayland';
|
|
99
217
|
}
|
|
100
|
-
/** Run a clipboard tool; return its stdout
|
|
101
|
-
* (
|
|
218
|
+
/** Run a clipboard tool; return its stdout bytes, or a `missing` flag when the
|
|
219
|
+
* binary isn't on PATH (so the caller can distinguish "no tool" from "no
|
|
220
|
+
* image"). Any other failure (non-zero exit, timeout, empty output) → an empty
|
|
221
|
+
* result ({}), i.e. the tool ran but yielded no image. */
|
|
102
222
|
function run(command, args) {
|
|
103
223
|
const result = spawnSync(command, args, {
|
|
104
224
|
timeout: SPAWN_TIMEOUT_MS,
|
|
105
225
|
maxBuffer: SPAWN_MAX_BUFFER,
|
|
106
226
|
});
|
|
107
|
-
if (result.error
|
|
108
|
-
|
|
227
|
+
if (result.error) {
|
|
228
|
+
const missing = result.error.code === 'ENOENT';
|
|
229
|
+
return { missing };
|
|
230
|
+
}
|
|
231
|
+
if (result.status !== 0)
|
|
232
|
+
return {};
|
|
109
233
|
const stdout = result.stdout;
|
|
110
234
|
if (!stdout || stdout.length === 0)
|
|
111
|
-
return
|
|
112
|
-
return Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout);
|
|
235
|
+
return {};
|
|
236
|
+
return { bytes: Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout) };
|
|
113
237
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// clipboard-text.ts — write text to the system clipboard for `crtr attach` (/copy).
|
|
2
|
+
//
|
|
3
|
+
// pi's clipboard binding is not re-exported (see clipboard-image.ts), so we shell
|
|
4
|
+
// out to the platform clipboard tool, mirroring the read path: macOS `pbcopy`
|
|
5
|
+
// (always present), Linux `wl-copy` (Wayland) or `xclip` (X11). Best-effort —
|
|
6
|
+
// returns false (with no throw) when no tool is available or the write fails, so
|
|
7
|
+
// the caller can surface an accurate notice.
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
const SPAWN_TIMEOUT_MS = 3000;
|
|
10
|
+
/** Ordered list of [command, args] clipboard-write tools to try for this
|
|
11
|
+
* platform. The first one present on PATH that succeeds wins. */
|
|
12
|
+
function writers() {
|
|
13
|
+
if (process.platform === 'darwin')
|
|
14
|
+
return [['pbcopy', []]];
|
|
15
|
+
const wayland = Boolean(process.env.WAYLAND_DISPLAY) || process.env.XDG_SESSION_TYPE === 'wayland';
|
|
16
|
+
const xclip = ['xclip', ['-selection', 'clipboard']];
|
|
17
|
+
const wlcopy = ['wl-copy', []];
|
|
18
|
+
return wayland ? [wlcopy, xclip] : [xclip, wlcopy];
|
|
19
|
+
}
|
|
20
|
+
/** Write `text` to the system clipboard. Returns true on success, false when no
|
|
21
|
+
* clipboard tool is available or every attempt failed. Never throws. */
|
|
22
|
+
export function writeClipboardText(text) {
|
|
23
|
+
for (const [command, args] of writers()) {
|
|
24
|
+
const result = spawnSync(command, args, {
|
|
25
|
+
input: text,
|
|
26
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
});
|
|
29
|
+
// ENOENT → tool not installed; try the next one.
|
|
30
|
+
if (result.error) {
|
|
31
|
+
if (result.error.code === 'ENOENT')
|
|
32
|
+
continue;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (result.status === 0)
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
@@ -7,14 +7,15 @@ export declare function defaultAgentDir(): string;
|
|
|
7
7
|
* wrapper; a missing/malformed file → `undefined` (fall back to defaults).
|
|
8
8
|
*/
|
|
9
9
|
export declare function loadUserKeybindings(agentDir?: string): KeybindingsConfig | undefined;
|
|
10
|
-
/**
|
|
11
|
-
* Build the KeybindingsManager the attach viewer drives input with: TUI defaults
|
|
12
|
-
* + vendored `app.*` defs, with the user's overrides applied, registered as the
|
|
13
|
-
* global pi-tui keybindings so reused components pick up the same bindings.
|
|
14
|
-
* (`as unknown as` bridges the readonly `TUI_KEYBINDINGS`/string-literal defs to
|
|
15
|
-
* the mutable `KeybindingDefinitions` param — vendoring cast, no behavior change.)
|
|
16
|
-
*/
|
|
17
10
|
export declare function createKeybindingsManager(agentDir?: string): KeybindingsManager;
|
|
11
|
+
/** Register the manager on the editor's pi-tui instance too (see `editorPiTui`),
|
|
12
|
+
* so the editor's newline/submit handling honors the same (user-overridden)
|
|
13
|
+
* bindings as the rest of the viewer. Call once, after `createKeybindingsManager`. */
|
|
14
|
+
export declare function mirrorKeybindingsToEditor(km: KeybindingsManager): Promise<void>;
|
|
15
|
+
/** Mirror the negotiated kitty-keyboard-protocol flag onto the editor's pi-tui
|
|
16
|
+
* instance (ProcessTerminal sets it only on OUR copy). Call after the terminal
|
|
17
|
+
* has negotiated, i.e. after `tui.start()`. */
|
|
18
|
+
export declare function mirrorKittyProtocolToEditor(active: boolean): Promise<void>;
|
|
18
19
|
/**
|
|
19
20
|
* The user's theme name from pi settings — project (`<cwd>/.pi/settings.json`)
|
|
20
21
|
* overrides global (`~/.pi/agent/settings.json`). `undefined` → pi's default.
|
|
@@ -29,3 +30,49 @@ export declare function applyTheme(opts?: {
|
|
|
29
30
|
agentDir?: string;
|
|
30
31
|
cwd?: string;
|
|
31
32
|
}): void;
|
|
33
|
+
/**
|
|
34
|
+
* The named styling roles the attach chrome paints with, all theme-derived so the
|
|
35
|
+
* viewer matches whatever theme the user configured for pi. Each is a
|
|
36
|
+
* `str => str` colorizer applied to a whole token.
|
|
37
|
+
*/
|
|
38
|
+
export interface AttachPalette {
|
|
39
|
+
/** Headings / badges / panel titles — the theme accent (gold in the default). */
|
|
40
|
+
accent: (s: string) => string;
|
|
41
|
+
/** Live/active markers + the working spinner — the theme's bright accent (teal). */
|
|
42
|
+
active: (s: string) => string;
|
|
43
|
+
/** Informational values (model name, counts) — the theme link color (blue). */
|
|
44
|
+
info: (s: string) => string;
|
|
45
|
+
/** Secondary text — the theme muted gray. */
|
|
46
|
+
muted: (s: string) => string;
|
|
47
|
+
/** Least-important text — SGR faint (a style, not a hue). */
|
|
48
|
+
faint: (s: string) => string;
|
|
49
|
+
/** Border rules / frames — the theme border color. */
|
|
50
|
+
border: (s: string) => string;
|
|
51
|
+
/** Distinct-surface paint for a modal/overlay: wraps a WHOLE rendered line so
|
|
52
|
+
* it sits on the theme's `selectedBg` background, edge to edge, re-asserting
|
|
53
|
+
* the bg after every embedded full-reset so an inner `\x1b[0m` (a status dot,
|
|
54
|
+
* the cursor bar) can't punch a hole in the surface. Closes with a bg-only
|
|
55
|
+
* reset so the surface never bleeds past the line. */
|
|
56
|
+
surface: (s: string) => string;
|
|
57
|
+
/** Emphasis. */
|
|
58
|
+
bold: (s: string) => string;
|
|
59
|
+
/** Error text. pi's public theme API does NOT surface the `error` ThemeColor
|
|
60
|
+
* (only markdown/select-list/settings-list derived colors are re-exported), so
|
|
61
|
+
* this is the semantically-correct ANSI red — NOT an ad-hoc hardcode. */
|
|
62
|
+
error: (s: string) => string;
|
|
63
|
+
/** Warning / transient notices. Same constraint as `error` → ANSI yellow. */
|
|
64
|
+
warning: (s: string) => string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build the attach chrome's color palette from the LIVE theme. Call AFTER
|
|
68
|
+
* `applyTheme()` — the colors are pulled from pi's `getMarkdownTheme()` /
|
|
69
|
+
* `getSelectListTheme()`, which read the active theme singleton at call time.
|
|
70
|
+
*
|
|
71
|
+
* pi does not re-export the raw `Theme` instance (its `.` export map omits the
|
|
72
|
+
* `theme` const and `getEditorTheme`/`getTheme`), so the accent/border/muted hues
|
|
73
|
+
* are sourced from the markdown + select-list theme colorizers, which ARE
|
|
74
|
+
* re-exported and are themselves backed by `theme.fg(...)`. The two semantic
|
|
75
|
+
* colors pi never exposes through that surface — `error`/`warning` — fall back to
|
|
76
|
+
* standard ANSI red/yellow.
|
|
77
|
+
*/
|
|
78
|
+
export declare function attachPalette(): AttachPalette;
|
|
@@ -16,10 +16,13 @@
|
|
|
16
16
|
// never fire). We also register it globally via `setKeybindings` so the reused
|
|
17
17
|
// pi-tui components (SelectList autocomplete, the dialog editor) honor overrides.
|
|
18
18
|
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
19
20
|
import { homedir } from 'node:os';
|
|
20
21
|
import { join } from 'node:path';
|
|
22
|
+
import { pathToFileURL } from 'node:url';
|
|
21
23
|
import { KeybindingsManager, TUI_KEYBINDINGS, setKeybindings, } from '@earendil-works/pi-tui';
|
|
22
|
-
import { initTheme } from '@earendil-works/pi-coding-agent';
|
|
24
|
+
import { initTheme, getMarkdownTheme, getSelectListTheme } from '@earendil-works/pi-coding-agent';
|
|
25
|
+
import { surfaceBgAnsi } from '../../core/runtime/surface-bg.js';
|
|
23
26
|
/** pi's user config dir: `~/.pi/agent/`. */
|
|
24
27
|
export function defaultAgentDir() {
|
|
25
28
|
return join(homedir(), '.pi', 'agent');
|
|
@@ -44,7 +47,10 @@ const APP_KEYBINDINGS = {
|
|
|
44
47
|
'app.tools.expand': { defaultKeys: 'ctrl+o', description: 'Toggle tool output' },
|
|
45
48
|
'app.thinking.toggle': { defaultKeys: 'ctrl+t', description: 'Toggle thinking blocks' },
|
|
46
49
|
'app.editor.external': { defaultKeys: 'ctrl+g', description: 'Open external editor' },
|
|
47
|
-
|
|
50
|
+
// Alt+Enter inserts a newline in the attach editor (shift+enter is unreliable
|
|
51
|
+
// across terminals), so the follow-up-queue action drops its alt+enter default
|
|
52
|
+
// to free the chord — see ATTACH_KEYBINDING_OVERRIDES below.
|
|
53
|
+
'app.message.followUp': { defaultKeys: [], description: 'Queue follow-up message' },
|
|
48
54
|
'app.message.dequeue': { defaultKeys: 'alt+up', description: 'Restore queued messages' },
|
|
49
55
|
'app.clipboard.pasteImage': { defaultKeys: ['alt+v', 'ctrl+v'], description: 'Paste image from clipboard' },
|
|
50
56
|
'app.session.new': { defaultKeys: [], description: 'Start a new session' },
|
|
@@ -78,12 +84,66 @@ export function loadUserKeybindings(agentDir = defaultAgentDir()) {
|
|
|
78
84
|
* (`as unknown as` bridges the readonly `TUI_KEYBINDINGS`/string-literal defs to
|
|
79
85
|
* the mutable `KeybindingDefinitions` param — vendoring cast, no behavior change.)
|
|
80
86
|
*/
|
|
87
|
+
/**
|
|
88
|
+
* Attach-specific default overrides applied OVER pi's TUI defaults. Alt+Enter
|
|
89
|
+
* inserts a newline here (the follow-up-queue action gives up its alt+enter
|
|
90
|
+
* default above), because shift+enter — pi's stock newline chord — does not
|
|
91
|
+
* transmit a distinct sequence in many terminals. User `keybindings.json` still
|
|
92
|
+
* wins on top of these (applied as the manager's override config).
|
|
93
|
+
*/
|
|
94
|
+
const ATTACH_KEYBINDING_OVERRIDES = {
|
|
95
|
+
'tui.input.newLine': { defaultKeys: ['shift+enter', 'alt+enter'], description: 'Insert new line' },
|
|
96
|
+
};
|
|
81
97
|
export function createKeybindingsManager(agentDir = defaultAgentDir()) {
|
|
82
|
-
const definitions = {
|
|
98
|
+
const definitions = {
|
|
99
|
+
...TUI_KEYBINDINGS,
|
|
100
|
+
...APP_KEYBINDINGS,
|
|
101
|
+
...ATTACH_KEYBINDING_OVERRIDES,
|
|
102
|
+
};
|
|
83
103
|
const km = new KeybindingsManager(definitions, loadUserKeybindings(agentDir));
|
|
84
104
|
setKeybindings(km);
|
|
85
105
|
return km;
|
|
86
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* pi-coding-agent's `CustomEditor` resolves `@earendil-works/pi-tui` from its OWN
|
|
109
|
+
* `node_modules`, which can be a SEPARATE module instance from the one this file
|
|
110
|
+
* imports (a non-deduped install leaves a nested copy). The editor's newline /
|
|
111
|
+
* submit handling reads keybindings AND the kitty-protocol flag from THAT
|
|
112
|
+
* instance's module-globals via `getKeybindings()` / `isKittyProtocolActive()`,
|
|
113
|
+
* so state we set only on our copy is invisible to it — the user's `alt+enter`
|
|
114
|
+
* newline binding never applies and Enter falls through to submit. We mirror our
|
|
115
|
+
* state onto the editor's instance too. When the install IS deduped both resolve
|
|
116
|
+
* to one module and the mirror is a harmless re-set. Best-effort: a resolution /
|
|
117
|
+
* import failure leaves the editor on our copy's state (the deduped case).
|
|
118
|
+
*/
|
|
119
|
+
let editorPiTuiPromise;
|
|
120
|
+
function editorPiTui() {
|
|
121
|
+
if (!editorPiTuiPromise) {
|
|
122
|
+
editorPiTuiPromise = (async () => {
|
|
123
|
+
try {
|
|
124
|
+
const pcaEntry = import.meta.resolve('@earendil-works/pi-coding-agent');
|
|
125
|
+
const piTuiPath = createRequire(pcaEntry).resolve('@earendil-works/pi-tui');
|
|
126
|
+
return (await import(pathToFileURL(piTuiPath).href));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
})();
|
|
132
|
+
}
|
|
133
|
+
return editorPiTuiPromise;
|
|
134
|
+
}
|
|
135
|
+
/** Register the manager on the editor's pi-tui instance too (see `editorPiTui`),
|
|
136
|
+
* so the editor's newline/submit handling honors the same (user-overridden)
|
|
137
|
+
* bindings as the rest of the viewer. Call once, after `createKeybindingsManager`. */
|
|
138
|
+
export async function mirrorKeybindingsToEditor(km) {
|
|
139
|
+
(await editorPiTui())?.setKeybindings(km);
|
|
140
|
+
}
|
|
141
|
+
/** Mirror the negotiated kitty-keyboard-protocol flag onto the editor's pi-tui
|
|
142
|
+
* instance (ProcessTerminal sets it only on OUR copy). Call after the terminal
|
|
143
|
+
* has negotiated, i.e. after `tui.start()`. */
|
|
144
|
+
export async function mirrorKittyProtocolToEditor(active) {
|
|
145
|
+
(await editorPiTui())?.setKittyProtocolActive(active);
|
|
146
|
+
}
|
|
87
147
|
/**
|
|
88
148
|
* The user's theme name from pi settings — project (`<cwd>/.pi/settings.json`)
|
|
89
149
|
* overrides global (`~/.pi/agent/settings.json`). `undefined` → pi's default.
|
|
@@ -111,3 +171,39 @@ export function loadThemeName(opts) {
|
|
|
111
171
|
export function applyTheme(opts) {
|
|
112
172
|
initTheme(loadThemeName(opts));
|
|
113
173
|
}
|
|
174
|
+
const FAINT = (s) => `\x1b[2m${s}\x1b[22m`;
|
|
175
|
+
const RED = (s) => `\x1b[31m${s}\x1b[39m`;
|
|
176
|
+
const YELLOW = (s) => `\x1b[33m${s}\x1b[39m`;
|
|
177
|
+
/**
|
|
178
|
+
* Build the attach chrome's color palette from the LIVE theme. Call AFTER
|
|
179
|
+
* `applyTheme()` — the colors are pulled from pi's `getMarkdownTheme()` /
|
|
180
|
+
* `getSelectListTheme()`, which read the active theme singleton at call time.
|
|
181
|
+
*
|
|
182
|
+
* pi does not re-export the raw `Theme` instance (its `.` export map omits the
|
|
183
|
+
* `theme` const and `getEditorTheme`/`getTheme`), so the accent/border/muted hues
|
|
184
|
+
* are sourced from the markdown + select-list theme colorizers, which ARE
|
|
185
|
+
* re-exported and are themselves backed by `theme.fg(...)`. The two semantic
|
|
186
|
+
* colors pi never exposes through that surface — `error`/`warning` — fall back to
|
|
187
|
+
* standard ANSI red/yellow.
|
|
188
|
+
*/
|
|
189
|
+
export function attachPalette() {
|
|
190
|
+
const md = getMarkdownTheme();
|
|
191
|
+
const sel = getSelectListTheme();
|
|
192
|
+
// The distinct-surface bg-on SGR, captured once. Re-asserted after every
|
|
193
|
+
// embedded `\x1b[0m` (full reset) inside a line so a coloured cell can't drop
|
|
194
|
+
// back to the default background mid-row; the line ends with `\x1b[49m` (reset
|
|
195
|
+
// background only) so the surface never bleeds onto the next line.
|
|
196
|
+
const bgOn = surfaceBgAnsi();
|
|
197
|
+
return {
|
|
198
|
+
accent: md.heading,
|
|
199
|
+
active: md.code,
|
|
200
|
+
info: md.link,
|
|
201
|
+
muted: sel.description,
|
|
202
|
+
faint: FAINT,
|
|
203
|
+
border: md.hr,
|
|
204
|
+
bold: md.bold,
|
|
205
|
+
error: RED,
|
|
206
|
+
warning: YELLOW,
|
|
207
|
+
surface: (line) => `${bgOn}${line.replace(/\x1b\[0m/g, `\x1b[0m${bgOn}`)}\x1b[49m`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Component } from '@earendil-works/pi-tui';
|
|
2
|
+
export declare class ContextMessageComponent implements Component {
|
|
3
|
+
private readonly body;
|
|
4
|
+
private readonly dim;
|
|
5
|
+
private readonly label;
|
|
6
|
+
private expanded;
|
|
7
|
+
constructor(body: string, expanded: boolean, dim: (s: string) => string, label: (s: string) => string);
|
|
8
|
+
/** Ctrl+O global tool-output toggle (ChatView.isExpandable duck-type). */
|
|
9
|
+
setExpanded(expanded: boolean): void;
|
|
10
|
+
render(width: number): string[];
|
|
11
|
+
invalidate(): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// context-message.ts — the viewer's foldable renderer for the `<crtr-context>`
|
|
2
|
+
// bearings block (a `crtr-context` custom message). The broker runs the
|
|
3
|
+
// canvas-context-intro extension, so the VIEWER has no message renderer for it
|
|
4
|
+
// and would otherwise show the full block always. This reimplements the
|
|
5
|
+
// extension's `renderContextMessage` natively (src/pi-extensions/
|
|
6
|
+
// canvas-context-intro.ts): collapsed → one dim line; expanded (Ctrl+O) →
|
|
7
|
+
// label + full wrapped body. It implements `setExpanded`, so the Ctrl+O
|
|
8
|
+
// tool-output toggle (ChatView.toggleToolsExpanded → isExpandable) folds it
|
|
9
|
+
// alongside tool output.
|
|
10
|
+
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
11
|
+
/** The label shown in both states. Mirrors the extension's customType stamp. */
|
|
12
|
+
const LABEL = 'crtr context';
|
|
13
|
+
/** Char-based wrap that never emits an over-width line (pi-tui hard-crashes on
|
|
14
|
+
* one). Visible-width aware so wide glyphs in the bearings tree count right. */
|
|
15
|
+
function wrapLine(line, width) {
|
|
16
|
+
if (line === '')
|
|
17
|
+
return [''];
|
|
18
|
+
const out = [];
|
|
19
|
+
let cur = '';
|
|
20
|
+
for (const ch of line) {
|
|
21
|
+
if (cur !== '' && visibleWidth(cur + ch) > width) {
|
|
22
|
+
out.push(cur);
|
|
23
|
+
cur = ch;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
cur += ch;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (cur !== '')
|
|
30
|
+
out.push(cur);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
export class ContextMessageComponent {
|
|
34
|
+
body;
|
|
35
|
+
dim;
|
|
36
|
+
label;
|
|
37
|
+
expanded;
|
|
38
|
+
constructor(body, expanded, dim, label) {
|
|
39
|
+
this.body = body;
|
|
40
|
+
this.dim = dim;
|
|
41
|
+
this.label = label;
|
|
42
|
+
this.expanded = expanded;
|
|
43
|
+
}
|
|
44
|
+
/** Ctrl+O global tool-output toggle (ChatView.isExpandable duck-type). */
|
|
45
|
+
setExpanded(expanded) {
|
|
46
|
+
this.expanded = expanded;
|
|
47
|
+
}
|
|
48
|
+
render(width) {
|
|
49
|
+
const w = width > 0 ? width : 80;
|
|
50
|
+
if (!this.expanded) {
|
|
51
|
+
// Paint the [crtr context] label in the accent color and the rest dim, so
|
|
52
|
+
// the label reads distinctly in BOTH states. Truncate BEFORE painting so the
|
|
53
|
+
// ANSI wrappers never inflate the measured width (an over-wide line aborts
|
|
54
|
+
// the whole TUI render).
|
|
55
|
+
const labelText = `[${LABEL}]`;
|
|
56
|
+
const rest = ' orienting bearings — ctrl+o to expand';
|
|
57
|
+
const labelW = visibleWidth(labelText);
|
|
58
|
+
if (labelW >= w)
|
|
59
|
+
return [this.label(truncateToWidth(labelText, w, '…'))];
|
|
60
|
+
return [this.label(labelText) + this.dim(truncateToWidth(rest, w - labelW, '…'))];
|
|
61
|
+
}
|
|
62
|
+
const lines = [this.label(truncateToWidth(`[${LABEL}]`, w, '…')), ''];
|
|
63
|
+
for (const raw of this.body.split('\n')) {
|
|
64
|
+
for (const seg of wrapLine(raw, w))
|
|
65
|
+
lines.push(seg);
|
|
66
|
+
}
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
invalidate() {
|
|
70
|
+
/* stateless — nothing to clear */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface GitInfo {
|
|
2
|
+
/** Last path segment of the cwd (always present). */
|
|
3
|
+
dir: string;
|
|
4
|
+
/** Current branch (or short SHA when detached); undefined outside a repo. */
|
|
5
|
+
branch?: string;
|
|
6
|
+
/** Working tree has uncommitted changes (staged, unstaged, or untracked). */
|
|
7
|
+
dirty: boolean;
|
|
8
|
+
/** Commits ahead of upstream. */
|
|
9
|
+
ahead: number;
|
|
10
|
+
/** Commits behind upstream. */
|
|
11
|
+
behind: number;
|
|
12
|
+
}
|
|
13
|
+
/** Parse `git status --porcelain=v1 --branch` into {@link GitInfo} flags. The
|
|
14
|
+
* first `## ` line carries branch + `[ahead N, behind M]`; any further line is
|
|
15
|
+
* a changed/untracked path → dirty. Exported pure for unit tests. */
|
|
16
|
+
export declare function parseGitStatus(dir: string, stdout: string): GitInfo;
|
|
17
|
+
/** Fetch git context for `cwd` without blocking. On any error (not a repo, git
|
|
18
|
+
* missing, timeout) the callback still fires with dir-only info so the border
|
|
19
|
+
* shows the folder name regardless. */
|
|
20
|
+
export declare function fetchGitInfo(cwd: string, cb: (info: GitInfo) => void): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// git-info.ts — async, non-blocking git context for the attach editor border:
|
|
2
|
+
// the working dir's last path segment, current branch, and a couple of status
|
|
3
|
+
// symbols (dirty / ahead / behind). Shells out via execFile (NEVER sync — the
|
|
4
|
+
// viewer must never block its input pump on a subprocess) and reports a plain
|
|
5
|
+
// struct the caller styles into the editor's top border.
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { basename } from 'node:path';
|
|
8
|
+
/** Parse `git status --porcelain=v1 --branch` into {@link GitInfo} flags. The
|
|
9
|
+
* first `## ` line carries branch + `[ahead N, behind M]`; any further line is
|
|
10
|
+
* a changed/untracked path → dirty. Exported pure for unit tests. */
|
|
11
|
+
export function parseGitStatus(dir, stdout) {
|
|
12
|
+
const lines = stdout.split('\n').filter((l) => l.length > 0);
|
|
13
|
+
const info = { dir, dirty: false, ahead: 0, behind: 0 };
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
if (line.startsWith('## ')) {
|
|
16
|
+
// `## main...origin/main [ahead 1, behind 2]` or `## HEAD (no branch)`
|
|
17
|
+
const head = line.slice(3);
|
|
18
|
+
if (!head.startsWith('HEAD (no branch)')) {
|
|
19
|
+
const branch = head.split(/\.\.\.| /, 1)[0];
|
|
20
|
+
if (branch)
|
|
21
|
+
info.branch = branch;
|
|
22
|
+
}
|
|
23
|
+
info.ahead = Number(/ahead (\d+)/.exec(head)?.[1] ?? 0);
|
|
24
|
+
info.behind = Number(/behind (\d+)/.exec(head)?.[1] ?? 0);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
info.dirty = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return info;
|
|
31
|
+
}
|
|
32
|
+
/** Fetch git context for `cwd` without blocking. On any error (not a repo, git
|
|
33
|
+
* missing, timeout) the callback still fires with dir-only info so the border
|
|
34
|
+
* shows the folder name regardless. */
|
|
35
|
+
export function fetchGitInfo(cwd, cb) {
|
|
36
|
+
const dir = basename(cwd) || cwd;
|
|
37
|
+
execFile('git', ['-C', cwd, 'status', '--porcelain=v1', '--branch'], { timeout: 2_000, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
|
|
38
|
+
if (err) {
|
|
39
|
+
cb({ dir, dirty: false, ahead: 0, behind: 0 });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
cb(parseGitStatus(dir, stdout));
|
|
43
|
+
});
|
|
44
|
+
}
|