@crouton-kit/crouter 0.3.18 → 0.3.20

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.
Files changed (289) hide show
  1. package/dist/build-root.js +6 -0
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
  5. package/dist/builtin-memory/crouter-development/personas.md +117 -0
  6. package/dist/builtin-memory/crouter-development/plugins.md +163 -0
  7. package/dist/builtin-memory/design.md +60 -0
  8. package/dist/builtin-memory/development.md +116 -0
  9. package/dist/builtin-memory/planning.md +66 -0
  10. package/dist/builtin-memory/spec.md +99 -0
  11. package/dist/builtin-personas/design/PERSONA.md +1 -0
  12. package/dist/builtin-personas/design/orchestrator.md +2 -1
  13. package/dist/builtin-personas/developer/PERSONA.md +1 -0
  14. package/dist/builtin-personas/developer/orchestrator.md +2 -1
  15. package/dist/builtin-personas/explore/PERSONA.md +1 -0
  16. package/dist/builtin-personas/explore/orchestrator.md +4 -0
  17. package/dist/builtin-personas/general/PERSONA.md +1 -0
  18. package/dist/builtin-personas/general/orchestrator.md +4 -0
  19. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  20. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  21. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  22. package/dist/builtin-personas/plan/PERSONA.md +1 -0
  23. package/dist/builtin-personas/plan/orchestrator.md +2 -1
  24. package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -0
  25. package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -0
  26. package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -0
  27. package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -0
  28. package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -0
  29. package/dist/builtin-personas/review/PERSONA.md +1 -0
  30. package/dist/builtin-personas/review/orchestrator.md +4 -0
  31. package/dist/builtin-personas/runtime-base.md +6 -3
  32. package/dist/builtin-personas/spec/PERSONA.md +1 -0
  33. package/dist/builtin-personas/spec/orchestrator.md +2 -1
  34. package/dist/builtin-personas/waiting.md +8 -0
  35. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  36. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  37. package/dist/builtin-views/_lib/states.mjs +161 -0
  38. package/dist/builtin-views/canvas/client.mjs +303 -0
  39. package/dist/builtin-views/canvas/view.mjs +576 -0
  40. package/dist/builtin-views/git-pr/client.mjs +440 -0
  41. package/dist/builtin-views/git-pr/view.mjs +675 -0
  42. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  43. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  44. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  45. package/dist/builtin-views/inbox/view.mjs +889 -0
  46. package/dist/builtin-views/linkedin/client.mjs +610 -0
  47. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  48. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  49. package/dist/clients/attach/attach-cmd.js +354 -0
  50. package/dist/clients/attach/chat-view.d.ts +77 -0
  51. package/dist/clients/attach/chat-view.js +450 -0
  52. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  53. package/dist/clients/attach/clipboard-image.js +113 -0
  54. package/dist/clients/attach/config-load.d.ts +31 -0
  55. package/dist/clients/attach/config-load.js +113 -0
  56. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  57. package/dist/clients/attach/extension-dialogs.js +101 -0
  58. package/dist/clients/attach/input-controller.d.ts +54 -0
  59. package/dist/clients/attach/input-controller.js +204 -0
  60. package/dist/clients/attach/slash-commands.d.ts +36 -0
  61. package/dist/clients/attach/slash-commands.js +200 -0
  62. package/dist/clients/attach/view-socket.d.ts +48 -0
  63. package/dist/clients/attach/view-socket.js +126 -0
  64. package/dist/commands/attention.js +3 -3
  65. package/dist/commands/canvas-prune.js +1 -1
  66. package/dist/commands/daemon.js +3 -2
  67. package/dist/commands/human/prompts.js +1 -1
  68. package/dist/commands/human/queue.js +54 -8
  69. package/dist/commands/human/shared.d.ts +2 -0
  70. package/dist/commands/memory/find.d.ts +1 -0
  71. package/dist/commands/memory/find.js +180 -0
  72. package/dist/commands/memory/lint.d.ts +1 -0
  73. package/dist/commands/memory/lint.js +140 -0
  74. package/dist/commands/memory/list.d.ts +1 -0
  75. package/dist/commands/memory/list.js +79 -0
  76. package/dist/commands/memory/read.js +103 -0
  77. package/dist/commands/memory/shared.d.ts +30 -0
  78. package/dist/commands/memory/shared.js +122 -0
  79. package/dist/commands/memory/write.d.ts +1 -0
  80. package/dist/commands/memory/write.js +85 -0
  81. package/dist/commands/memory.d.ts +2 -0
  82. package/dist/commands/memory.js +27 -0
  83. package/dist/commands/node.d.ts +3 -2
  84. package/dist/commands/node.js +610 -61
  85. package/dist/commands/pkg/market-manage.js +1 -1
  86. package/dist/commands/push.js +6 -6
  87. package/dist/commands/revive.js +1 -1
  88. package/dist/commands/skill/author.js +1 -1
  89. package/dist/commands/skill/shared.d.ts +1 -8
  90. package/dist/commands/skill/shared.js +2 -55
  91. package/dist/commands/skill.js +9 -14
  92. package/dist/commands/sys/doctor.js +1 -1
  93. package/dist/commands/sys/update.js +1 -1
  94. package/dist/commands/view-cycle.d.ts +2 -0
  95. package/dist/commands/view-cycle.js +125 -0
  96. package/dist/commands/view-list.d.ts +2 -0
  97. package/dist/commands/view-list.js +66 -0
  98. package/dist/commands/view-new.d.ts +2 -0
  99. package/dist/commands/view-new.js +70 -0
  100. package/dist/commands/view-pick.d.ts +2 -0
  101. package/dist/commands/view-pick.js +119 -0
  102. package/dist/commands/view-run.d.ts +2 -0
  103. package/dist/commands/view-run.js +191 -0
  104. package/dist/commands/view.d.ts +2 -0
  105. package/dist/commands/view.js +29 -0
  106. package/dist/core/__tests__/broker-attach-limits.test.d.ts +1 -0
  107. package/dist/core/__tests__/broker-attach-limits.test.js +157 -0
  108. package/dist/core/__tests__/broker-attach-stream.test.d.ts +1 -0
  109. package/dist/core/__tests__/broker-attach-stream.test.js +125 -0
  110. package/dist/core/__tests__/broker-crash-teardown.test.d.ts +1 -0
  111. package/dist/core/__tests__/broker-crash-teardown.test.js +116 -0
  112. package/dist/core/__tests__/broker-dialogs.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-dialogs.test.js +126 -0
  114. package/dist/core/__tests__/broker-dormant-wake.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-dormant-wake.test.js +51 -0
  116. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  117. package/dist/core/__tests__/broker-lifecycle.test.js +87 -0
  118. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  119. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  120. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.d.ts +1 -0
  121. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +102 -0
  122. package/dist/core/__tests__/canvas-inbox-watcher.test.js +2 -36
  123. package/dist/core/__tests__/cascade-close.test.js +12 -2
  124. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  125. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  126. package/dist/core/__tests__/context-intro.test.js +76 -62
  127. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  128. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  129. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  130. package/dist/core/__tests__/detach-focus.test.js +206 -0
  131. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  132. package/dist/core/__tests__/draw-style.test.js +258 -0
  133. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  134. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  135. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  136. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  137. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  138. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  139. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  140. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  141. package/dist/core/__tests__/helpers/broker-clients.d.ts +43 -0
  142. package/dist/core/__tests__/helpers/broker-clients.js +178 -0
  143. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  144. package/dist/core/__tests__/helpers/harness.js +111 -1
  145. package/dist/core/__tests__/home-session.test.js +41 -8
  146. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  147. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  148. package/dist/core/__tests__/kickoff.test.js +37 -3
  149. package/dist/core/__tests__/live-mutation-verbs.test.d.ts +1 -0
  150. package/dist/core/__tests__/live-mutation-verbs.test.js +175 -0
  151. package/dist/core/__tests__/live-mutation.test.js +6 -132
  152. package/dist/core/__tests__/memory.test.js +23 -115
  153. package/dist/core/__tests__/placement-focus.test.js +5 -0
  154. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  155. package/dist/core/__tests__/relaunch.test.js +4 -3
  156. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  157. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  158. package/dist/core/__tests__/spawn-root.test.js +10 -0
  159. package/dist/core/__tests__/spike-harness.test.js +1 -0
  160. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  161. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  162. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  163. package/dist/core/__tests__/wake-origin.test.js +110 -0
  164. package/dist/core/bootstrap.js +1 -1
  165. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  166. package/dist/core/canvas/browse/app.js +24 -2
  167. package/dist/core/canvas/browse/model.d.ts +38 -2
  168. package/dist/core/canvas/browse/model.js +134 -10
  169. package/dist/core/canvas/browse/render.d.ts +6 -12
  170. package/dist/core/canvas/browse/render.js +72 -104
  171. package/dist/core/canvas/canvas.js +16 -23
  172. package/dist/core/canvas/db.js +46 -0
  173. package/dist/core/canvas/index.d.ts +1 -0
  174. package/dist/core/canvas/index.js +1 -0
  175. package/dist/core/canvas/pid.d.ts +4 -0
  176. package/dist/core/canvas/pid.js +23 -0
  177. package/dist/core/canvas/render.d.ts +11 -2
  178. package/dist/core/canvas/render.js +69 -0
  179. package/dist/core/canvas/types.d.ts +91 -3
  180. package/dist/core/canvas/wakeups.d.ts +76 -0
  181. package/dist/core/canvas/wakeups.js +185 -0
  182. package/dist/core/config.js +4 -1
  183. package/dist/core/frontmatter.js +37 -124
  184. package/dist/core/memory-resolver.d.ts +49 -0
  185. package/dist/core/memory-resolver.js +141 -0
  186. package/dist/core/personas/loader.d.ts +7 -0
  187. package/dist/core/personas/loader.js +36 -5
  188. package/dist/core/personas/resolve.js +13 -3
  189. package/dist/core/predicate.d.ts +63 -0
  190. package/dist/core/predicate.js +189 -0
  191. package/dist/core/resolver.js +26 -5
  192. package/dist/core/runtime/bearings.d.ts +53 -12
  193. package/dist/core/runtime/bearings.js +132 -59
  194. package/dist/core/runtime/broker-cli.d.ts +1 -0
  195. package/dist/core/runtime/broker-cli.js +46 -0
  196. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  197. package/dist/core/runtime/broker-protocol.js +153 -0
  198. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  199. package/dist/core/runtime/broker-sdk.js +72 -0
  200. package/dist/core/runtime/broker.d.ts +55 -0
  201. package/dist/core/runtime/broker.js +1128 -0
  202. package/dist/core/runtime/close.js +35 -6
  203. package/dist/core/runtime/host.d.ts +53 -0
  204. package/dist/core/runtime/host.js +186 -0
  205. package/dist/core/runtime/kickoff.d.ts +2 -1
  206. package/dist/core/runtime/kickoff.js +91 -5
  207. package/dist/core/runtime/launch.d.ts +54 -4
  208. package/dist/core/runtime/launch.js +85 -5
  209. package/dist/core/runtime/lifecycle.js +23 -6
  210. package/dist/core/runtime/memory.d.ts +2 -42
  211. package/dist/core/runtime/memory.js +11 -162
  212. package/dist/core/runtime/nodes.d.ts +37 -0
  213. package/dist/core/runtime/nodes.js +60 -1
  214. package/dist/core/runtime/persona.js +21 -11
  215. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  216. package/dist/core/runtime/pi-vendored.js +49 -0
  217. package/dist/core/runtime/placement.d.ts +32 -14
  218. package/dist/core/runtime/placement.js +206 -52
  219. package/dist/core/runtime/promote.d.ts +0 -6
  220. package/dist/core/runtime/promote.js +4 -12
  221. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  222. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  223. package/dist/core/runtime/reset.js +11 -8
  224. package/dist/core/runtime/revive.d.ts +2 -0
  225. package/dist/core/runtime/revive.js +34 -34
  226. package/dist/core/runtime/spawn.d.ts +23 -0
  227. package/dist/core/runtime/spawn.js +77 -23
  228. package/dist/core/runtime/stop-guard.d.ts +1 -1
  229. package/dist/core/runtime/stop-guard.js +6 -1
  230. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  231. package/dist/core/runtime/tmux-chrome.js +1 -1
  232. package/dist/core/runtime/tmux.d.ts +28 -0
  233. package/dist/core/runtime/tmux.js +80 -6
  234. package/dist/core/scope.d.ts +11 -0
  235. package/dist/core/scope.js +39 -0
  236. package/dist/core/spawn.d.ts +5 -0
  237. package/dist/core/spawn.js +20 -2
  238. package/dist/core/substrate/gate.d.ts +13 -0
  239. package/dist/core/substrate/gate.js +21 -0
  240. package/dist/core/substrate/index.d.ts +7 -0
  241. package/dist/core/substrate/index.js +18 -0
  242. package/dist/core/substrate/on-read.d.ts +14 -0
  243. package/dist/core/substrate/on-read.js +292 -0
  244. package/dist/core/substrate/render.d.ts +25 -0
  245. package/dist/core/substrate/render.js +256 -0
  246. package/dist/core/substrate/schema.d.ts +76 -0
  247. package/dist/core/substrate/schema.js +124 -0
  248. package/dist/core/substrate/session-cache.d.ts +30 -0
  249. package/dist/core/substrate/session-cache.js +77 -0
  250. package/dist/core/substrate/subject.d.ts +41 -0
  251. package/dist/core/substrate/subject.js +54 -0
  252. package/dist/core/tui/contract.d.ts +83 -0
  253. package/dist/core/tui/contract.js +8 -0
  254. package/dist/core/tui/draw.d.ts +96 -0
  255. package/dist/core/tui/draw.js +339 -0
  256. package/dist/core/tui/host.d.ts +29 -0
  257. package/dist/core/tui/host.js +379 -0
  258. package/dist/core/tui/loader.d.ts +16 -0
  259. package/dist/core/tui/loader.js +94 -0
  260. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  261. package/dist/core/wake.d.ts +86 -0
  262. package/dist/core/wake.js +308 -0
  263. package/dist/daemon/crtrd.d.ts +29 -4
  264. package/dist/daemon/crtrd.js +662 -46
  265. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  266. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  267. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  268. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  269. package/dist/pi-extensions/canvas-commands.js +10 -0
  270. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  271. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  272. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  273. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  274. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  275. package/dist/pi-extensions/canvas-nav.js +87 -23
  276. package/dist/pi-extensions/canvas-stophook.js +17 -8
  277. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  278. package/dist/pi-extensions/canvas-view.js +75 -0
  279. package/dist/prompts/skill.js +19 -26
  280. package/dist/prompts/view.d.ts +7 -0
  281. package/dist/prompts/view.js +101 -0
  282. package/dist/types.d.ts +4 -0
  283. package/dist/types.js +1 -0
  284. package/package.json +9 -4
  285. package/dist/commands/skill/find.d.ts +0 -4
  286. package/dist/commands/skill/find.js +0 -257
  287. package/dist/commands/skill/read.js +0 -91
  288. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  289. /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;