@crouton-kit/crouter 0.3.18 → 0.3.19
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 +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +3 -2
- package/dist/commands/human/prompts.js +1 -1
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +2 -0
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +607 -61
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/loader.d.ts +7 -0
- package/dist/core/personas/loader.js +36 -5
- package/dist/core/personas/resolve.js +13 -3
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +32 -14
- package/dist/core/runtime/placement.js +206 -52
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +5 -0
- package/dist/core/spawn.js +20 -2
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +87 -23
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
// chat-view.ts — the render half of `crtr attach` (Phase 4, T5).
|
|
2
|
+
//
|
|
3
|
+
// A direct port of pi's `InteractiveMode.handleEvent()`
|
|
4
|
+
// (interactive-mode.js:2169-2462) + its rebuild path
|
|
5
|
+
// (`addMessageToChat`/`renderSessionContext`, :2483-2648) that DRIVES pi's own
|
|
6
|
+
// exported TUI components rather than reimplementing them. The viewer never
|
|
7
|
+
// spawns pi and never opens the session — it only consumes the relayed
|
|
8
|
+
// `AgentSessionEvent` stream + the `welcome` snapshot over `view.sock`.
|
|
9
|
+
//
|
|
10
|
+
// Ownership boundary: ChatView owns the chat scroll `Container` it is handed,
|
|
11
|
+
// plus an activity/status area pinned to the bottom of that container (working
|
|
12
|
+
// spinner, compaction/retry indicators, inline status/error lines). It does NOT
|
|
13
|
+
// own the editor, the footer, or the terminal title — those are chrome laid out
|
|
14
|
+
// by the attach command (T7). Events that only affect that chrome
|
|
15
|
+
// (queue_update / session_info_changed / thinking_level_changed) are forwarded
|
|
16
|
+
// to the optional `onFooterEvent` sink instead of rendered here.
|
|
17
|
+
//
|
|
18
|
+
// Custom-tool rendering (plan §3.3): extensions run in the broker, so the viewer
|
|
19
|
+
// has no `ToolDefinition`. Every tool renders through the DEFAULT
|
|
20
|
+
// `ToolExecutionComponent` path (toolDef = undefined) — builtin tools, diffs, and
|
|
21
|
+
// partial-result streaming all work; only custom `renderCall`/`renderResult`
|
|
22
|
+
// extensions degrade to the default, which is the decided Phase-4 behavior.
|
|
23
|
+
import { Container, Loader, Spacer, Text } from '@earendil-works/pi-tui';
|
|
24
|
+
import { AssistantMessageComponent, BashExecutionComponent, BranchSummaryMessageComponent, CompactionSummaryMessageComponent, CustomMessageComponent, getMarkdownTheme, parseSkillBlock, SkillInvocationMessageComponent, ToolExecutionComponent, UserMessageComponent, } from '@earendil-works/pi-coding-agent';
|
|
25
|
+
/** `theme` (the live instance) is NOT re-exported by pi's strict `.` map, so the
|
|
26
|
+
* loaders use small local ANSI styling rather than `theme.fg(...)`. Theme parity
|
|
27
|
+
* for activity spinners is cosmetic; the message components themselves still use
|
|
28
|
+
* `getMarkdownTheme()` for full-fidelity rendering. */
|
|
29
|
+
const spinnerStyle = (s) => `\x1b[36m${s}\x1b[39m`; // cyan
|
|
30
|
+
const dimStyle = (s) => `\x1b[2m${s}\x1b[22m`; // dim
|
|
31
|
+
export class ChatView {
|
|
32
|
+
tui;
|
|
33
|
+
/** The chat scroll area handed in by the attach command. */
|
|
34
|
+
container;
|
|
35
|
+
/** Activity/status area, pinned as the LAST child of `container` (mirrors pi's
|
|
36
|
+
* statusContainer sitting directly below chatContainer). Holds the working
|
|
37
|
+
* spinner / compaction+retry loaders. */
|
|
38
|
+
statusContainer = new Container();
|
|
39
|
+
cwd;
|
|
40
|
+
showImages;
|
|
41
|
+
imageWidthCells;
|
|
42
|
+
hideThinking;
|
|
43
|
+
hiddenThinkingLabel;
|
|
44
|
+
toolOutputExpanded;
|
|
45
|
+
onFooterEvent;
|
|
46
|
+
/** The assistant message component currently being streamed (between
|
|
47
|
+
* message_start and message_end for an assistant turn). */
|
|
48
|
+
streamingComponent;
|
|
49
|
+
/** Tool-call id → its execution component, for the duration of the call. */
|
|
50
|
+
pendingTools = new Map();
|
|
51
|
+
/** The single active activity loader (working/compaction/retry), if any. */
|
|
52
|
+
activityLoader;
|
|
53
|
+
/** While true, `append` skips re-pinning the status container (bulk rebuild). */
|
|
54
|
+
bulkMode = false;
|
|
55
|
+
constructor(tui, container, opts = {}) {
|
|
56
|
+
this.tui = tui;
|
|
57
|
+
this.container = container;
|
|
58
|
+
this.cwd = opts.cwd ?? process.cwd();
|
|
59
|
+
this.showImages = opts.showImages ?? true;
|
|
60
|
+
this.imageWidthCells = opts.imageWidthCells ?? 60;
|
|
61
|
+
this.hideThinking = opts.hideThinking ?? false;
|
|
62
|
+
this.hiddenThinkingLabel = opts.hiddenThinkingLabel ?? 'Thinking...';
|
|
63
|
+
this.toolOutputExpanded = opts.toolOutputExpanded ?? false;
|
|
64
|
+
this.onFooterEvent = opts.onFooterEvent;
|
|
65
|
+
this.container.addChild(this.statusContainer);
|
|
66
|
+
}
|
|
67
|
+
// -------------------------------------------------------------------------
|
|
68
|
+
// Catch-up: render the welcome snapshot, then live events resume.
|
|
69
|
+
// -------------------------------------------------------------------------
|
|
70
|
+
/** Render the full message history from a `welcome` snapshot. Ports pi's
|
|
71
|
+
* `renderSessionContext` (interactive-mode.js:2553): assistant messages spawn
|
|
72
|
+
* tool-execution components matched to their toolResult messages; everything
|
|
73
|
+
* else routes through `addMessageToChat`. The TUI's differential renderer +
|
|
74
|
+
* reflow-on-resize is automatic — no scrollback reconstruction needed. */
|
|
75
|
+
applySnapshot(snapshot) {
|
|
76
|
+
this.resetChat();
|
|
77
|
+
const renderedPendingTools = new Map();
|
|
78
|
+
const messages = snapshot.messages;
|
|
79
|
+
const lastIndex = messages.length - 1;
|
|
80
|
+
// Rebuild in bulk: detach the pinned status area, append children directly,
|
|
81
|
+
// re-pin ONCE at the end. Keeps the rebuild O(N) instead of O(N²) re-pins for
|
|
82
|
+
// a long transcript (see `append`).
|
|
83
|
+
this.container.removeChild(this.statusContainer);
|
|
84
|
+
this.bulkMode = true;
|
|
85
|
+
try {
|
|
86
|
+
for (let i = 0; i < messages.length; i++) {
|
|
87
|
+
const message = messages[i];
|
|
88
|
+
if (message.role === 'assistant') {
|
|
89
|
+
// If the snapshot caught an IN-FLIGHT assistant turn (state.isStreaming
|
|
90
|
+
// and this is the trailing, non-terminal assistant message), bind it as
|
|
91
|
+
// the live streaming component so the relayed message_update/message_end
|
|
92
|
+
// frames that follow `welcome` land on it. Otherwise the in-flight
|
|
93
|
+
// message would freeze at its snapshot value until the next turn — the
|
|
94
|
+
// common case, since you typically attach to a node that is working.
|
|
95
|
+
const isStreamingTail = i === lastIndex &&
|
|
96
|
+
snapshot.state.isStreaming &&
|
|
97
|
+
message.stopReason !== 'aborted' &&
|
|
98
|
+
message.stopReason !== 'error';
|
|
99
|
+
if (isStreamingTail) {
|
|
100
|
+
this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinking, getMarkdownTheme(), this.hiddenThinkingLabel);
|
|
101
|
+
this.append(this.streamingComponent);
|
|
102
|
+
this.streamingComponent.updateContent(message);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.addMessageToChat(message);
|
|
106
|
+
}
|
|
107
|
+
for (const content of this.assistantToolCalls(message)) {
|
|
108
|
+
const component = this.makeToolComponent(content.name, content.id, content.arguments);
|
|
109
|
+
this.append(component);
|
|
110
|
+
// (isStreamingTail is never aborted/error, so its tools fall through to
|
|
111
|
+
// renderedPendingTools → pendingTools, where live tool events find them.)
|
|
112
|
+
if (message.stopReason === 'aborted' || message.stopReason === 'error') {
|
|
113
|
+
const errorMessage = message.stopReason === 'aborted'
|
|
114
|
+
? 'Operation aborted'
|
|
115
|
+
: (message.errorMessage ?? 'Error');
|
|
116
|
+
component.updateResult({ content: [{ type: 'text', text: errorMessage }], isError: true });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
renderedPendingTools.set(content.id, component);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (message.role === 'toolResult') {
|
|
124
|
+
const component = renderedPendingTools.get(message.toolCallId);
|
|
125
|
+
if (component) {
|
|
126
|
+
component.updateResult(message);
|
|
127
|
+
renderedPendingTools.delete(message.toolCallId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this.addMessageToChat(message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
this.bulkMode = false;
|
|
137
|
+
this.container.addChild(this.statusContainer);
|
|
138
|
+
}
|
|
139
|
+
for (const [id, component] of renderedPendingTools) {
|
|
140
|
+
this.pendingTools.set(id, component);
|
|
141
|
+
}
|
|
142
|
+
this.tui.requestRender();
|
|
143
|
+
}
|
|
144
|
+
/** Stop any spinning activity loader. A one-shot `crtr attach` process lets its
|
|
145
|
+
* timers die with the process, but if T7 reuses a ChatView across reconnects in
|
|
146
|
+
* one process it should call this on detach to avoid a leaked interval. */
|
|
147
|
+
dispose() {
|
|
148
|
+
this.setActivity(undefined);
|
|
149
|
+
}
|
|
150
|
+
// -------------------------------------------------------------------------
|
|
151
|
+
// Live stream: the 15-of-17-case handleEvent port (turn_start/turn_end no-op,
|
|
152
|
+
// exactly as interactive pi falls through default for those two).
|
|
153
|
+
// -------------------------------------------------------------------------
|
|
154
|
+
handleEvent(event) {
|
|
155
|
+
switch (event.type) {
|
|
156
|
+
case 'agent_start': {
|
|
157
|
+
this.pendingTools.clear();
|
|
158
|
+
this.setActivity(this.makeLoader('Working...'));
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case 'agent_end': {
|
|
162
|
+
// Drop a dangling streaming component (e.g. aborted before message_end).
|
|
163
|
+
if (this.streamingComponent) {
|
|
164
|
+
this.container.removeChild(this.streamingComponent);
|
|
165
|
+
this.streamingComponent = undefined;
|
|
166
|
+
}
|
|
167
|
+
this.pendingTools.clear();
|
|
168
|
+
this.setActivity(undefined);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case 'message_start': {
|
|
172
|
+
if (event.message.role === 'custom' || event.message.role === 'user') {
|
|
173
|
+
this.addMessageToChat(event.message);
|
|
174
|
+
}
|
|
175
|
+
else if (event.message.role === 'assistant') {
|
|
176
|
+
this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinking, getMarkdownTheme(), this.hiddenThinkingLabel);
|
|
177
|
+
this.append(this.streamingComponent);
|
|
178
|
+
this.streamingComponent.updateContent(event.message);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case 'message_update': {
|
|
183
|
+
if (this.streamingComponent && event.message.role === 'assistant') {
|
|
184
|
+
this.streamingComponent.updateContent(event.message);
|
|
185
|
+
for (const content of this.assistantToolCalls(event.message)) {
|
|
186
|
+
const existing = this.pendingTools.get(content.id);
|
|
187
|
+
if (existing) {
|
|
188
|
+
existing.updateArgs(content.arguments);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const component = this.makeToolComponent(content.name, content.id, content.arguments);
|
|
192
|
+
this.append(component);
|
|
193
|
+
this.pendingTools.set(content.id, component);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case 'message_end': {
|
|
200
|
+
if (event.message.role !== 'assistant')
|
|
201
|
+
break;
|
|
202
|
+
if (this.streamingComponent) {
|
|
203
|
+
const stopReason = event.message.stopReason;
|
|
204
|
+
let errorMessage = event.message.errorMessage;
|
|
205
|
+
if (stopReason === 'aborted' && !errorMessage) {
|
|
206
|
+
errorMessage = 'Operation aborted';
|
|
207
|
+
// Surface the abort on the assistant bubble itself, mirroring pi
|
|
208
|
+
// (interactive-mode.js:2280 sets streamingMessage.errorMessage before
|
|
209
|
+
// updateContent) so the rendered message shows the annotation.
|
|
210
|
+
event.message.errorMessage = errorMessage;
|
|
211
|
+
}
|
|
212
|
+
this.streamingComponent.updateContent(event.message);
|
|
213
|
+
if (stopReason === 'aborted' || stopReason === 'error') {
|
|
214
|
+
const text = errorMessage ?? 'Error';
|
|
215
|
+
for (const component of this.pendingTools.values()) {
|
|
216
|
+
component.updateResult({ content: [{ type: 'text', text }], isError: true });
|
|
217
|
+
}
|
|
218
|
+
this.pendingTools.clear();
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// Args complete → trigger diff computation for edit tools.
|
|
222
|
+
for (const component of this.pendingTools.values()) {
|
|
223
|
+
component.setArgsComplete();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
this.streamingComponent = undefined;
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case 'tool_execution_start': {
|
|
231
|
+
let component = this.pendingTools.get(event.toolCallId);
|
|
232
|
+
if (!component) {
|
|
233
|
+
component = this.makeToolComponent(event.toolName, event.toolCallId, event.args);
|
|
234
|
+
this.append(component);
|
|
235
|
+
this.pendingTools.set(event.toolCallId, component);
|
|
236
|
+
}
|
|
237
|
+
component.markExecutionStarted();
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case 'tool_execution_update': {
|
|
241
|
+
const component = this.pendingTools.get(event.toolCallId);
|
|
242
|
+
if (component) {
|
|
243
|
+
component.updateResult({ ...event.partialResult, isError: false }, true);
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case 'tool_execution_end': {
|
|
248
|
+
const component = this.pendingTools.get(event.toolCallId);
|
|
249
|
+
if (component) {
|
|
250
|
+
component.updateResult({ ...event.result, isError: event.isError });
|
|
251
|
+
this.pendingTools.delete(event.toolCallId);
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case 'compaction_start': {
|
|
256
|
+
const label = event.reason === 'manual' ? 'Compacting context...' : 'Auto-compacting...';
|
|
257
|
+
this.setActivity(this.makeLoader(label));
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case 'compaction_end': {
|
|
261
|
+
this.setActivity(undefined);
|
|
262
|
+
if (event.aborted) {
|
|
263
|
+
this.showStatus(event.reason === 'manual' ? 'Compaction cancelled' : 'Auto-compaction cancelled');
|
|
264
|
+
}
|
|
265
|
+
else if (event.result) {
|
|
266
|
+
// 0.78.1 divergence from the §3.2 sketch: `compaction_end` carries only
|
|
267
|
+
// the CompactionResult (summary + tokensBefore), NOT the post-compaction
|
|
268
|
+
// message list — so the viewer cannot clear+rebuild the transcript the
|
|
269
|
+
// way in-process pi does (it rebuilds from its own SessionManager). We
|
|
270
|
+
// append the compaction summary marker instead and KEEP the
|
|
271
|
+
// pre-compaction scrollback for the rest of this attach session; a fresh
|
|
272
|
+
// re-attach gets the already-compacted history via `welcome`. (Reported
|
|
273
|
+
// in T5's final.)
|
|
274
|
+
this.addMessageToChat({
|
|
275
|
+
role: 'compactionSummary',
|
|
276
|
+
summary: event.result.summary,
|
|
277
|
+
tokensBefore: event.result.tokensBefore,
|
|
278
|
+
timestamp: Date.now(),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
else if (event.errorMessage) {
|
|
282
|
+
this.showError(event.errorMessage);
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case 'auto_retry_start': {
|
|
287
|
+
const seconds = Math.ceil(event.delayMs / 1000);
|
|
288
|
+
this.setActivity(this.makeLoader(`Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s...`));
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case 'auto_retry_end': {
|
|
292
|
+
this.setActivity(undefined);
|
|
293
|
+
if (!event.success) {
|
|
294
|
+
this.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError ?? 'Unknown error'}`);
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
// Footer/header-only — ChatView owns no footer; forward to the sink.
|
|
299
|
+
case 'queue_update':
|
|
300
|
+
case 'session_info_changed':
|
|
301
|
+
case 'thinking_level_changed':
|
|
302
|
+
this.onFooterEvent?.(event);
|
|
303
|
+
break;
|
|
304
|
+
// turn_start / turn_end — interactive pi ignores these (falls through
|
|
305
|
+
// default); the viewer does too.
|
|
306
|
+
default:
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
this.tui.requestRender();
|
|
310
|
+
}
|
|
311
|
+
// -------------------------------------------------------------------------
|
|
312
|
+
// Rebuild path (ports addMessageToChat, interactive-mode.js:2483).
|
|
313
|
+
// -------------------------------------------------------------------------
|
|
314
|
+
addMessageToChat(message) {
|
|
315
|
+
switch (message.role) {
|
|
316
|
+
case 'bashExecution': {
|
|
317
|
+
const component = new BashExecutionComponent(message.command, this.tui, message.excludeFromContext);
|
|
318
|
+
if (message.output)
|
|
319
|
+
component.appendOutput(message.output);
|
|
320
|
+
component.setComplete(message.exitCode, message.cancelled,
|
|
321
|
+
// pi's interactive mode passes a minimal `{truncated:true}` marker here
|
|
322
|
+
// (interactive-mode.js:2489) where the typed param is the full
|
|
323
|
+
// TruncationResult; the snapshot message only carries the boolean, so we
|
|
324
|
+
// mirror pi and cast.
|
|
325
|
+
message.truncated ? { truncated: true } : undefined, message.fullOutputPath);
|
|
326
|
+
this.append(component);
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
case 'custom': {
|
|
330
|
+
if (message.display) {
|
|
331
|
+
// No extension renderer in the viewer → default custom-message render.
|
|
332
|
+
const component = new CustomMessageComponent(message, undefined, getMarkdownTheme());
|
|
333
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
334
|
+
this.append(component);
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case 'compactionSummary': {
|
|
339
|
+
this.append(new Spacer(1));
|
|
340
|
+
const component = new CompactionSummaryMessageComponent(message, getMarkdownTheme());
|
|
341
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
342
|
+
this.append(component);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case 'branchSummary': {
|
|
346
|
+
this.append(new Spacer(1));
|
|
347
|
+
const component = new BranchSummaryMessageComponent(message, getMarkdownTheme());
|
|
348
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
349
|
+
this.append(component);
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case 'user': {
|
|
353
|
+
const textContent = this.userMessageText(message);
|
|
354
|
+
if (!textContent)
|
|
355
|
+
break;
|
|
356
|
+
if (this.chatChildCount() > 0)
|
|
357
|
+
this.append(new Spacer(1));
|
|
358
|
+
const skillBlock = parseSkillBlock(textContent);
|
|
359
|
+
if (skillBlock) {
|
|
360
|
+
const component = new SkillInvocationMessageComponent(skillBlock, getMarkdownTheme());
|
|
361
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
362
|
+
this.append(component);
|
|
363
|
+
if (skillBlock.userMessage) {
|
|
364
|
+
this.append(new UserMessageComponent(skillBlock.userMessage, getMarkdownTheme()));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
this.append(new UserMessageComponent(textContent, getMarkdownTheme()));
|
|
369
|
+
}
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
case 'assistant': {
|
|
373
|
+
this.append(new AssistantMessageComponent(message, this.hideThinking, getMarkdownTheme(), this.hiddenThinkingLabel));
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
// toolResult is rendered inline with its tool call (handled in applySnapshot).
|
|
377
|
+
default:
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// -------------------------------------------------------------------------
|
|
382
|
+
// Helpers
|
|
383
|
+
// -------------------------------------------------------------------------
|
|
384
|
+
/** Default-path tool component (toolDef undefined → builtin/default rendering). */
|
|
385
|
+
makeToolComponent(name, id, args) {
|
|
386
|
+
const component = new ToolExecutionComponent(name, id, args, { showImages: this.showImages, imageWidthCells: this.imageWidthCells }, undefined, this.tui, this.cwd);
|
|
387
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
388
|
+
return component;
|
|
389
|
+
}
|
|
390
|
+
/** Append a child while keeping the activity/status area pinned to the bottom. */
|
|
391
|
+
append(child) {
|
|
392
|
+
if (this.bulkMode) {
|
|
393
|
+
this.container.addChild(child);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
this.container.removeChild(this.statusContainer);
|
|
397
|
+
this.container.addChild(child);
|
|
398
|
+
this.container.addChild(this.statusContainer);
|
|
399
|
+
}
|
|
400
|
+
/** Number of chat children excluding the pinned status container. */
|
|
401
|
+
chatChildCount() {
|
|
402
|
+
return this.container.children.filter((c) => c !== this.statusContainer).length;
|
|
403
|
+
}
|
|
404
|
+
/** Clear all chat content and re-pin an empty status container. */
|
|
405
|
+
resetChat() {
|
|
406
|
+
this.setActivity(undefined);
|
|
407
|
+
this.streamingComponent = undefined;
|
|
408
|
+
this.pendingTools.clear();
|
|
409
|
+
this.container.clear();
|
|
410
|
+
this.container.addChild(this.statusContainer);
|
|
411
|
+
}
|
|
412
|
+
makeLoader(message) {
|
|
413
|
+
return new Loader(this.tui, spinnerStyle, dimStyle, message);
|
|
414
|
+
}
|
|
415
|
+
/** Swap the single active activity indicator (working/compaction/retry). */
|
|
416
|
+
setActivity(loader) {
|
|
417
|
+
this.activityLoader?.stop();
|
|
418
|
+
this.statusContainer.clear();
|
|
419
|
+
this.activityLoader = loader;
|
|
420
|
+
if (loader)
|
|
421
|
+
this.statusContainer.addChild(loader);
|
|
422
|
+
}
|
|
423
|
+
showStatus(message) {
|
|
424
|
+
this.append(new Spacer(1));
|
|
425
|
+
this.append(new Text(dimStyle(message), 1, 0));
|
|
426
|
+
}
|
|
427
|
+
showError(message) {
|
|
428
|
+
this.append(new Spacer(1));
|
|
429
|
+
this.append(new Text(`\x1b[31m${message}\x1b[39m`, 1, 0));
|
|
430
|
+
}
|
|
431
|
+
/** Extract the tool-call content items from an assistant message. */
|
|
432
|
+
assistantToolCalls(message) {
|
|
433
|
+
const content = message.content;
|
|
434
|
+
if (!Array.isArray(content))
|
|
435
|
+
return [];
|
|
436
|
+
return content.filter((c) => typeof c === 'object' && c !== null && c.type === 'toolCall');
|
|
437
|
+
}
|
|
438
|
+
/** Concatenate the text blocks of a user message (string or content array). */
|
|
439
|
+
userMessageText(message) {
|
|
440
|
+
const content = message.content;
|
|
441
|
+
if (typeof content === 'string')
|
|
442
|
+
return content;
|
|
443
|
+
if (!Array.isArray(content))
|
|
444
|
+
return '';
|
|
445
|
+
return content
|
|
446
|
+
.filter((c) => typeof c === 'object' && c !== null && c.type === 'text')
|
|
447
|
+
.map((c) => c.text)
|
|
448
|
+
.join('');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ImageContent } from '@earendil-works/pi-ai';
|
|
2
|
+
/** A clipboard image ready to attach, plus an optional human-readable note about
|
|
3
|
+
* the resize (from pi's `formatDimensionNote`) for the controller to surface. */
|
|
4
|
+
export interface ClipboardImageResult {
|
|
5
|
+
/** Ready to push into a `prompt`/`steer`/`follow_up` frame's `images?`. ABSENT
|
|
6
|
+
* when the image was read but DROPPED (a fallback exceeded MAX_BYTES base64);
|
|
7
|
+
* `note` then carries the drop reason and the caller attaches nothing. */
|
|
8
|
+
image?: ImageContent;
|
|
9
|
+
/** Either a resize note ("Resized from 4032×3024 to 1568×1176", present only
|
|
10
|
+
* when resized) or, when `image` is absent, the reason the image was dropped. */
|
|
11
|
+
note?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Read + resize the current clipboard image. Returns `null` when there is no
|
|
14
|
+
* image, no clipboard tool, or the read fails (all best-effort — the caller
|
|
15
|
+
* shows a brief notice and carries on). */
|
|
16
|
+
export declare function readClipboardImage(): Promise<ClipboardImageResult | null>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// clipboard-image.ts — read the clipboard image for `crtr attach` (T6, review M1).
|
|
2
|
+
//
|
|
3
|
+
// pi's `readClipboardImage` is NOT re-exported (it lives behind a native
|
|
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
|
+
// (pngpaste on macOS, wl-paste on Wayland, xclip on X11) — small + best-effort:
|
|
7
|
+
// no tool / empty clipboard → `null` and the caller shows a brief notice.
|
|
8
|
+
//
|
|
9
|
+
// The bytes are then resized AGGRESSIVELY through pi's exported `resizeImage`
|
|
10
|
+
// (which is reusable, unlike the reader) so the base64 stays well within the
|
|
11
|
+
// broker's client-read line cap (`BROKER_READ_CAPS.maxLineBytes` = 24 MiB).
|
|
12
|
+
// `resizeImage`'s `maxBytes` is the BASE64-PAYLOAD ceiling (it compares the
|
|
13
|
+
// encoded size, not raw bytes — see image-resize-core), so MAX_BYTES (3 MiB)
|
|
14
|
+
// bounds the base64 itself, far under 24 MiB. `resizeImage` already tries PNG and
|
|
15
|
+
// JPEG and picks the smaller, so it doubles as format normalization;
|
|
16
|
+
// `convertToPng` is the fallback when the Photon resizer is unavailable. The
|
|
17
|
+
// fallbacks (convertToPng / raw bytes) are NOT size-bounded by `resizeImage`, so
|
|
18
|
+
// each is gated on the SAME MAX_BYTES base64 ceiling (review M1): over it → the
|
|
19
|
+
// image is DROPPED with a user-visible note rather than shipped as an over-cap
|
|
20
|
+
// frame that would overflow BROKER_READ_CAPS.maxLineBytes (24 MiB) and destroy
|
|
21
|
+
// the viewer socket. The result is an `ImageContent` (pi-ai) ready to drop
|
|
22
|
+
// straight into a `prompt`/`steer`/`follow_up` frame's `images?` array (or, when
|
|
23
|
+
// dropped, a `note`-only result and no image).
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
import { convertToPng, formatDimensionNote, resizeImage, } from '@earendil-works/pi-coding-agent';
|
|
26
|
+
/** Longest edge (px) the pasted image is resized to — aggressive, matches the
|
|
27
|
+
* ~1568px long-edge guidance vision models use. */
|
|
28
|
+
const MAX_EDGE = 1568;
|
|
29
|
+
/** Largest BASE64 payload per image — the ceiling for BOTH paths: passed as
|
|
30
|
+
* `resizeImage`'s `maxBytes` (which bounds the encoded base64, not raw bytes)
|
|
31
|
+
* on the primary path, and compared against the fallbacks' base64 length. 3 MiB
|
|
32
|
+
* base64, far under BROKER_READ_CAPS.maxLineBytes (24 MiB). */
|
|
33
|
+
const MAX_BYTES = 3 * 1024 * 1024;
|
|
34
|
+
/** Bound the shell read so a giant/garbage clipboard can't blow up memory. */
|
|
35
|
+
const SPAWN_MAX_BUFFER = 64 * 1024 * 1024;
|
|
36
|
+
const SPAWN_TIMEOUT_MS = 3000;
|
|
37
|
+
/** Read + resize the current clipboard image. Returns `null` when there is no
|
|
38
|
+
* image, no clipboard tool, or the read fails (all best-effort — the caller
|
|
39
|
+
* shows a brief notice and carries on). */
|
|
40
|
+
export async function readClipboardImage() {
|
|
41
|
+
const raw = readRawClipboardImage();
|
|
42
|
+
if (!raw)
|
|
43
|
+
return null;
|
|
44
|
+
// Primary path: resize aggressively. Handles format (PNG/JPEG) + size bound.
|
|
45
|
+
const resized = await resizeImage(new Uint8Array(raw.bytes), raw.mimeType, {
|
|
46
|
+
maxWidth: MAX_EDGE,
|
|
47
|
+
maxHeight: MAX_EDGE,
|
|
48
|
+
maxBytes: MAX_BYTES,
|
|
49
|
+
});
|
|
50
|
+
if (resized) {
|
|
51
|
+
return {
|
|
52
|
+
image: { type: 'image', data: resized.data, mimeType: resized.mimeType },
|
|
53
|
+
note: resized.wasResized ? formatDimensionNote(resized) : undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Fallback: the Photon resizer is unavailable — at least normalize to PNG so
|
|
57
|
+
// the engine gets a format it accepts. Capped at MAX_BYTES (base64).
|
|
58
|
+
const png = await convertToPng(raw.bytes.toString('base64'), raw.mimeType);
|
|
59
|
+
if (png)
|
|
60
|
+
return capped({ type: 'image', data: png.data, mimeType: png.mimeType });
|
|
61
|
+
// Last resort: ship the raw bytes — also capped, never an over-cap frame.
|
|
62
|
+
return capped({ type: 'image', data: raw.bytes.toString('base64'), mimeType: raw.mimeType });
|
|
63
|
+
}
|
|
64
|
+
/** Gate a FALLBACK image on the per-image base64 ceiling (MAX_BYTES): under it →
|
|
65
|
+
* attach; over → DROP with a user-visible note rather than ship a frame that
|
|
66
|
+
* would overflow the broker's 24 MiB read cap and destroy the viewer socket.
|
|
67
|
+
* base64 is ASCII, so its byte length equals the bytes the frame puts on the
|
|
68
|
+
* wire. */
|
|
69
|
+
function capped(image) {
|
|
70
|
+
const encodedBytes = Buffer.byteLength(image.data);
|
|
71
|
+
if (encodedBytes <= MAX_BYTES)
|
|
72
|
+
return { image };
|
|
73
|
+
const mib = (n) => Math.round(n / (1024 * 1024));
|
|
74
|
+
return {
|
|
75
|
+
note: `Image not attached: ${mib(encodedBytes)} MiB exceeds the ${mib(MAX_BYTES)} MiB attach limit (clipboard resizer unavailable)`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Shell out to the platform clipboard tool for raw image bytes. */
|
|
79
|
+
function readRawClipboardImage() {
|
|
80
|
+
if (process.platform === 'darwin') {
|
|
81
|
+
const out = run('pngpaste', ['-']);
|
|
82
|
+
return out ? { bytes: out, mimeType: 'image/png' } : null;
|
|
83
|
+
}
|
|
84
|
+
// Linux: Wayland first (wl-paste), then X11 (xclip). Try PNG then JPEG.
|
|
85
|
+
if (isWayland()) {
|
|
86
|
+
const out = run('wl-paste', ['--type', 'image/png', '--no-newline']);
|
|
87
|
+
if (out)
|
|
88
|
+
return { bytes: out, mimeType: 'image/png' };
|
|
89
|
+
}
|
|
90
|
+
for (const mimeType of ['image/png', 'image/jpeg']) {
|
|
91
|
+
const out = run('xclip', ['-selection', 'clipboard', '-t', mimeType, '-o']);
|
|
92
|
+
if (out)
|
|
93
|
+
return { bytes: out, mimeType };
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function isWayland() {
|
|
98
|
+
return Boolean(process.env.WAYLAND_DISPLAY) || process.env.XDG_SESSION_TYPE === 'wayland';
|
|
99
|
+
}
|
|
100
|
+
/** Run a clipboard tool; return its stdout Buffer, or `null` on any failure
|
|
101
|
+
* (missing binary, non-zero exit, timeout, empty output). */
|
|
102
|
+
function run(command, args) {
|
|
103
|
+
const result = spawnSync(command, args, {
|
|
104
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
105
|
+
maxBuffer: SPAWN_MAX_BUFFER,
|
|
106
|
+
});
|
|
107
|
+
if (result.error || result.status !== 0)
|
|
108
|
+
return null;
|
|
109
|
+
const stdout = result.stdout;
|
|
110
|
+
if (!stdout || stdout.length === 0)
|
|
111
|
+
return null;
|
|
112
|
+
return Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout);
|
|
113
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { KeybindingsManager, type KeybindingsConfig } from '@earendil-works/pi-tui';
|
|
2
|
+
/** pi's user config dir: `~/.pi/agent/`. */
|
|
3
|
+
export declare function defaultAgentDir(): string;
|
|
4
|
+
/**
|
|
5
|
+
* Read `~/.pi/agent/keybindings.json` (if present) and return the user's binding
|
|
6
|
+
* overrides. Tolerates a flat `{action: keys}` map or a `{keybindings: {...}}`
|
|
7
|
+
* wrapper; a missing/malformed file → `undefined` (fall back to defaults).
|
|
8
|
+
*/
|
|
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
|
+
export declare function createKeybindingsManager(agentDir?: string): KeybindingsManager;
|
|
18
|
+
/**
|
|
19
|
+
* The user's theme name from pi settings — project (`<cwd>/.pi/settings.json`)
|
|
20
|
+
* overrides global (`~/.pi/agent/settings.json`). `undefined` → pi's default.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadThemeName(opts?: {
|
|
23
|
+
agentDir?: string;
|
|
24
|
+
cwd?: string;
|
|
25
|
+
}): string | undefined;
|
|
26
|
+
/** Resolve + activate the user's theme so the reused pi components (markdown,
|
|
27
|
+
* editor, select-list themes) render at parity with the user's pi. */
|
|
28
|
+
export declare function applyTheme(opts?: {
|
|
29
|
+
agentDir?: string;
|
|
30
|
+
cwd?: string;
|
|
31
|
+
}): void;
|