@crouton-kit/crouter 0.3.28 → 0.3.30

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 (324) hide show
  1. package/dist/build-root.d.ts +15 -7
  2. package/dist/build-root.js +48 -34
  3. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  4. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  6. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  7. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  8. package/dist/builtin-memory/design.md +1 -1
  9. package/dist/builtin-memory/development.md +1 -1
  10. package/dist/builtin-memory/internal/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  12. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  13. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  14. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  15. package/dist/builtin-memory/planning.md +1 -1
  16. package/dist/builtin-memory/spec.md +6 -4
  17. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  18. package/dist/builtin-personas/runtime-base.md +5 -4
  19. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  20. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  21. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  22. package/dist/builtin-views/canvas/core.mjs +586 -0
  23. package/dist/builtin-views/canvas/text.mjs +58 -0
  24. package/dist/builtin-views/canvas/tui.mjs +165 -0
  25. package/dist/builtin-views/canvas/web.jsx +120 -0
  26. package/dist/builtin-views/git-pr/core.mjs +673 -0
  27. package/dist/builtin-views/git-pr/text.mjs +84 -0
  28. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  29. package/dist/builtin-views/git-pr/web.jsx +216 -0
  30. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  31. package/dist/builtin-views/inbox/core.mjs +1273 -0
  32. package/dist/builtin-views/inbox/text.mjs +73 -0
  33. package/dist/builtin-views/inbox/tui.mjs +312 -0
  34. package/dist/builtin-views/inbox/web.jsx +188 -0
  35. package/dist/builtin-views/linkedin/core.mjs +906 -0
  36. package/dist/builtin-views/linkedin/text.mjs +69 -0
  37. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  38. package/dist/builtin-views/linkedin/web.jsx +206 -0
  39. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  40. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  41. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  42. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  43. package/dist/cli.js +22 -17
  44. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  45. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  46. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  47. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  48. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  49. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  50. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  51. package/dist/clients/attach/attach-cmd.js +282 -46
  52. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  53. package/dist/clients/attach/auth-pickers.js +194 -0
  54. package/dist/clients/attach/canvas-panels.d.ts +4 -2
  55. package/dist/clients/attach/canvas-panels.js +45 -17
  56. package/dist/clients/attach/chat-view.d.ts +27 -2
  57. package/dist/clients/attach/chat-view.js +98 -9
  58. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  59. package/dist/clients/attach/clipboard-image.js +150 -26
  60. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  61. package/dist/clients/attach/clipboard-text.js +39 -0
  62. package/dist/clients/attach/config-load.d.ts +54 -7
  63. package/dist/clients/attach/config-load.js +99 -3
  64. package/dist/clients/attach/context-message.d.ts +12 -0
  65. package/dist/clients/attach/context-message.js +72 -0
  66. package/dist/clients/attach/git-info.d.ts +20 -0
  67. package/dist/clients/attach/git-info.js +44 -0
  68. package/dist/clients/attach/graph-overlay.d.ts +13 -4
  69. package/dist/clients/attach/graph-overlay.js +59 -19
  70. package/dist/clients/attach/input-controller.d.ts +75 -13
  71. package/dist/clients/attach/input-controller.js +305 -67
  72. package/dist/clients/attach/pickers.d.ts +58 -0
  73. package/dist/clients/attach/pickers.js +171 -0
  74. package/dist/clients/attach/slash-commands.d.ts +22 -0
  75. package/dist/clients/attach/slash-commands.js +91 -25
  76. package/dist/clients/attach/titled-editor.d.ts +33 -0
  77. package/dist/clients/attach/titled-editor.js +84 -0
  78. package/dist/clients/attach/view-socket.d.ts +18 -1
  79. package/dist/clients/attach/view-socket.js +70 -1
  80. package/dist/clients/web/dev-server.d.ts +7 -0
  81. package/dist/clients/web/dev-server.js +59 -0
  82. package/dist/clients/web/events.d.ts +14 -0
  83. package/dist/clients/web/events.js +151 -0
  84. package/dist/clients/web/server.d.ts +18 -0
  85. package/dist/clients/web/server.js +450 -0
  86. package/dist/clients/web/web-cmd.d.ts +2 -0
  87. package/dist/clients/web/web-cmd.js +120 -0
  88. package/dist/commands/canvas.js +1 -2
  89. package/dist/commands/chord.js +4 -25
  90. package/dist/commands/dashboard.js +5 -1
  91. package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
  92. package/dist/commands/memory/lint.js +14 -2
  93. package/dist/commands/memory/read.js +2 -2
  94. package/dist/commands/memory/shared.d.ts +7 -1
  95. package/dist/commands/memory/shared.js +18 -3
  96. package/dist/commands/memory/write.js +8 -4
  97. package/dist/commands/memory.js +4 -4
  98. package/dist/commands/node.d.ts +1 -1
  99. package/dist/commands/node.js +64 -65
  100. package/dist/commands/pkg.js +1 -2
  101. package/dist/commands/revive.js +11 -3
  102. package/dist/commands/sys/sync.d.ts +1 -0
  103. package/dist/commands/sys/sync.js +187 -0
  104. package/dist/commands/sys.js +3 -2
  105. package/dist/commands/view-cycle.js +2 -2
  106. package/dist/commands/view-list.js +8 -8
  107. package/dist/commands/view-new.js +21 -17
  108. package/dist/commands/view-pick.js +1 -1
  109. package/dist/commands/view-run.js +35 -14
  110. package/dist/commands/view.js +8 -6
  111. package/dist/commands/workspace.d.ts +2 -0
  112. package/dist/commands/workspace.js +161 -0
  113. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  114. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  115. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  116. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  117. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  118. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  119. package/dist/core/__tests__/close.test.js +14 -20
  120. package/dist/core/__tests__/context-intro.test.js +19 -19
  121. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  122. package/dist/core/__tests__/draw-style.test.js +1 -1
  123. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  124. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  125. package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
  126. package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
  127. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  128. package/dist/core/__tests__/focuses.test.js +25 -13
  129. package/dist/core/__tests__/fork.test.js +22 -5
  130. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  131. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  132. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  133. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  134. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  135. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  136. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  137. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  138. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  139. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  140. package/dist/core/__tests__/helpers/harness.js +19 -36
  141. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  142. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  143. package/dist/core/__tests__/lifecycle.test.js +1 -19
  144. package/dist/core/__tests__/listing-completeness.test.js +2 -2
  145. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  146. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  147. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
  148. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  149. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  150. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  151. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  152. package/dist/core/__tests__/reset.test.js +19 -79
  153. package/dist/core/__tests__/steer-note.test.js +9 -7
  154. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  155. package/dist/core/bootstrap.d.ts +0 -3
  156. package/dist/core/bootstrap.js +1 -143
  157. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  158. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  159. package/dist/core/canvas/browse/app.js +131 -8
  160. package/dist/core/canvas/browse/model.d.ts +23 -5
  161. package/dist/core/canvas/browse/model.js +68 -13
  162. package/dist/core/canvas/browse/render.d.ts +7 -1
  163. package/dist/core/canvas/browse/render.js +178 -47
  164. package/dist/core/canvas/canvas.js +1 -1
  165. package/dist/core/canvas/focuses.d.ts +4 -4
  166. package/dist/core/canvas/focuses.js +7 -7
  167. package/dist/core/canvas/nav-model.js +8 -7
  168. package/dist/core/canvas/render.d.ts +68 -9
  169. package/dist/core/canvas/render.js +195 -61
  170. package/dist/core/canvas/types.d.ts +16 -23
  171. package/dist/core/config.js +1 -4
  172. package/dist/core/memory-resolver.js +43 -5
  173. package/dist/core/runtime/bearings.d.ts +1 -1
  174. package/dist/core/runtime/bearings.js +8 -8
  175. package/dist/core/runtime/branded-host.d.ts +16 -0
  176. package/dist/core/runtime/branded-host.js +127 -0
  177. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  178. package/dist/core/runtime/broker.d.ts +4 -1
  179. package/dist/core/runtime/broker.js +494 -58
  180. package/dist/core/runtime/close.d.ts +12 -2
  181. package/dist/core/runtime/close.js +37 -16
  182. package/dist/core/runtime/front-door.js +6 -14
  183. package/dist/core/runtime/host.d.ts +14 -34
  184. package/dist/core/runtime/host.js +14 -50
  185. package/dist/core/runtime/launch.d.ts +12 -9
  186. package/dist/core/runtime/launch.js +27 -15
  187. package/dist/core/runtime/lifecycle.d.ts +1 -1
  188. package/dist/core/runtime/lifecycle.js +15 -19
  189. package/dist/core/runtime/nodes.d.ts +0 -55
  190. package/dist/core/runtime/nodes.js +6 -74
  191. package/dist/core/runtime/placement.d.ts +91 -335
  192. package/dist/core/runtime/placement.js +291 -858
  193. package/dist/core/runtime/promote.d.ts +2 -0
  194. package/dist/core/runtime/promote.js +24 -6
  195. package/dist/core/runtime/recap.d.ts +8 -0
  196. package/dist/core/runtime/recap.js +107 -0
  197. package/dist/core/runtime/recycle.js +25 -61
  198. package/dist/core/runtime/reset.d.ts +43 -37
  199. package/dist/core/runtime/reset.js +131 -218
  200. package/dist/core/runtime/revive.d.ts +9 -29
  201. package/dist/core/runtime/revive.js +40 -139
  202. package/dist/core/runtime/spawn.d.ts +15 -16
  203. package/dist/core/runtime/spawn.js +165 -194
  204. package/dist/core/runtime/surface-bg.d.ts +11 -0
  205. package/dist/core/runtime/surface-bg.js +68 -0
  206. package/dist/core/runtime/tmux.d.ts +15 -52
  207. package/dist/core/runtime/tmux.js +35 -99
  208. package/dist/core/scope.d.ts +1 -2
  209. package/dist/core/scope.js +1 -5
  210. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  211. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  212. package/dist/core/skill-sync/builtins.d.ts +42 -0
  213. package/dist/core/skill-sync/builtins.js +112 -0
  214. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  215. package/dist/core/skill-sync/claude-plugins.js +71 -0
  216. package/dist/core/skill-sync/engine.d.ts +42 -0
  217. package/dist/core/skill-sync/engine.js +633 -0
  218. package/dist/core/skill-sync/export.d.ts +23 -0
  219. package/dist/core/skill-sync/export.js +86 -0
  220. package/dist/core/skill-sync/manifest.d.ts +64 -0
  221. package/dist/core/skill-sync/manifest.js +181 -0
  222. package/dist/core/skill-sync/profile.d.ts +76 -0
  223. package/dist/core/skill-sync/profile.js +173 -0
  224. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  225. package/dist/core/skill-sync/snapshot.js +120 -0
  226. package/dist/core/substrate/index.d.ts +1 -1
  227. package/dist/core/substrate/index.js +1 -1
  228. package/dist/core/substrate/injected-store.js +3 -3
  229. package/dist/core/substrate/on-read.js +68 -6
  230. package/dist/core/substrate/render.d.ts +8 -11
  231. package/dist/core/substrate/render.js +29 -43
  232. package/dist/core/substrate/schema.d.ts +10 -3
  233. package/dist/core/substrate/schema.js +6 -3
  234. package/dist/core/tui/host.d.ts +12 -4
  235. package/dist/core/tui/host.js +280 -149
  236. package/dist/core/view/bridge.d.ts +10 -0
  237. package/dist/core/view/bridge.js +31 -0
  238. package/dist/core/view/chrome.d.ts +9 -0
  239. package/dist/core/view/chrome.js +22 -0
  240. package/dist/core/view/contract.d.ts +171 -0
  241. package/dist/core/view/contract.js +23 -0
  242. package/dist/core/view/loader.d.ts +31 -0
  243. package/dist/core/view/loader.js +188 -0
  244. package/dist/core/view/transport-local.d.ts +7 -0
  245. package/dist/core/view/transport-local.js +70 -0
  246. package/dist/core/view/transport.d.ts +4 -0
  247. package/dist/core/view/transport.js +15 -0
  248. package/dist/daemon/crtrd.d.ts +38 -27
  249. package/dist/daemon/crtrd.js +303 -376
  250. package/dist/daemon/manage.js +6 -1
  251. package/dist/index.js +1 -1
  252. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  253. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  254. package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
  255. package/dist/pi-extensions/canvas-nav.js +5 -0
  256. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  257. package/dist/pi-extensions/canvas-recap.js +236 -0
  258. package/dist/pi-extensions/canvas-resume.js +2 -1
  259. package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
  260. package/dist/pi-extensions/canvas-stophook.js +87 -101
  261. package/dist/pi-extensions/canvas-view.js +2 -1
  262. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  263. package/dist/pi-extensions/widget-order-bus.js +34 -0
  264. package/dist/prompts/view.d.ts +2 -2
  265. package/dist/prompts/view.js +145 -73
  266. package/dist/types.d.ts +1 -6
  267. package/dist/types.js +1 -3
  268. package/dist/web/ViewChrome.d.ts +7 -0
  269. package/dist/web/ViewChrome.js +28 -0
  270. package/dist/web/ViewPane.d.ts +39 -0
  271. package/dist/web/ViewPane.js +48 -0
  272. package/dist/web/index.d.ts +6 -0
  273. package/dist/web/index.js +16 -0
  274. package/dist/web/runtime.d.ts +39 -0
  275. package/dist/web/runtime.js +133 -0
  276. package/dist/web/states.d.ts +24 -0
  277. package/dist/web/states.js +24 -0
  278. package/dist/web/transport-http.d.ts +5 -0
  279. package/dist/web/transport-http.js +28 -0
  280. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  281. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  282. package/dist/web-client/index.html +13 -0
  283. package/package.json +20 -6
  284. package/dist/builtin-views/canvas/client.mjs +0 -303
  285. package/dist/builtin-views/canvas/view.mjs +0 -576
  286. package/dist/builtin-views/git-pr/client.mjs +0 -440
  287. package/dist/builtin-views/git-pr/view.mjs +0 -675
  288. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  289. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  290. package/dist/builtin-views/inbox/view.mjs +0 -889
  291. package/dist/builtin-views/linkedin/client.mjs +0 -610
  292. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  293. package/dist/commands/pkg/bridge.d.ts +0 -1
  294. package/dist/commands/pkg/bridge.js +0 -137
  295. package/dist/commands/skill/author.d.ts +0 -3
  296. package/dist/commands/skill/author.js +0 -140
  297. package/dist/commands/skill/shared.d.ts +0 -3
  298. package/dist/commands/skill/shared.js +0 -19
  299. package/dist/commands/skill.d.ts +0 -2
  300. package/dist/commands/skill.js +0 -21
  301. package/dist/commands/tmux-spread.d.ts +0 -2
  302. package/dist/commands/tmux-spread.js +0 -144
  303. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  304. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  305. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  306. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  307. package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
  308. package/dist/core/__tests__/home-session.test.js +0 -186
  309. package/dist/core/__tests__/relaunch.test.js +0 -335
  310. package/dist/core/bridge-map.d.ts +0 -19
  311. package/dist/core/bridge-map.js +0 -73
  312. package/dist/core/tui/contract.d.ts +0 -83
  313. package/dist/core/tui/contract.js +0 -8
  314. package/dist/core/tui/loader.d.ts +0 -16
  315. package/dist/core/tui/loader.js +0 -94
  316. package/dist/prompts/skill.d.ts +0 -2
  317. package/dist/prompts/skill.js +0 -650
  318. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  319. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  320. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  321. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  322. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  323. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
  324. /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
- // (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.
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
- /** 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). */
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 raw = readRawClipboardImage();
42
- if (!raw)
43
- return null;
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
- const out = run('pngpaste', ['-']);
82
- return out ? { bytes: out, mimeType: 'image/png' } : null;
109
+ return selectMacOutcome(run, readMacClipboardViaOsascript);
83
110
  }
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' };
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 = run('xclip', ['-selection', 'clipboard', '-t', mimeType, '-o']);
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 Buffer, or `null` on any failure
101
- * (missing binary, non-zero exit, timeout, empty output). */
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 || result.status !== 0)
108
- return null;
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 null;
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,3 @@
1
+ /** Write `text` to the system clipboard. Returns true on success, false when no
2
+ * clipboard tool is available or every attempt failed. Never throws. */
3
+ export declare function writeClipboardText(text: string): boolean;
@@ -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
- 'app.message.followUp': { defaultKeys: 'alt+enter', description: 'Queue follow-up message' },
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 = { ...TUI_KEYBINDINGS, ...APP_KEYBINDINGS };
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
+ }