@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
@@ -0,0 +1,194 @@
1
+ // auth-pickers.ts — native /login and /logout overlays for `crtr attach`.
2
+ //
3
+ // Runs pi's REAL OAuth/API-key login flow locally in the viewer process.
4
+ // Since crtr attach is tmux-local only (viewer + broker = same machine, same
5
+ // filesystem), we can construct a local AuthStorage pointing at the SAME
6
+ // auth.json the broker uses — no socket round-trip needed for auth mutations.
7
+ //
8
+ // The OAuthSelectorComponent (provider picker) and LoginDialogComponent (the
9
+ // actual login flow UI) are pi's own exported components, used verbatim.
10
+ import { join } from 'node:path';
11
+ import { execFile } from 'node:child_process';
12
+ import { AuthStorage, ModelRegistry, OAuthSelectorComponent, LoginDialogComponent, } from '@earendil-works/pi-coding-agent';
13
+ /** Build an AuthStorage + ModelRegistry from the broker's agentDir (or pi's
14
+ * default). Both point at the SAME auth.json the broker uses — the viewer and
15
+ * broker share the filesystem (tmux-local invariant). */
16
+ function buildLocalAuth(agentDir) {
17
+ const authPath = join(agentDir, 'auth.json');
18
+ const modelsPath = join(agentDir, 'models.json');
19
+ const authStorage = AuthStorage.create(authPath);
20
+ const registry = ModelRegistry.create(authStorage, modelsPath);
21
+ return { authStorage, registry };
22
+ }
23
+ /** Assemble the AuthSelectorProvider list for the OAuth selector: OAuth providers
24
+ * from authStorage.getOAuthProviders() (authType 'oauth') + API-key providers
25
+ * derived from the registry's known providers that aren't OAuth (authType 'api_key').
26
+ * Mirrors how pi's interactive mode composes its login list. */
27
+ function buildProviderList(authStorage, registry) {
28
+ const oauthProviders = authStorage.getOAuthProviders();
29
+ const oauthIds = new Set(oauthProviders.map((p) => p.id));
30
+ const providers = oauthProviders.map((p) => ({
31
+ id: p.id,
32
+ name: p.name,
33
+ authType: 'oauth',
34
+ }));
35
+ // Add API-key providers from the registry that aren't OAuth providers.
36
+ const allModels = registry.getAll();
37
+ const seenProviders = new Set(oauthIds);
38
+ for (const m of allModels) {
39
+ if (!seenProviders.has(m.provider)) {
40
+ seenProviders.add(m.provider);
41
+ providers.push({
42
+ id: m.provider,
43
+ name: registry.getProviderDisplayName(m.provider),
44
+ authType: 'api_key',
45
+ });
46
+ }
47
+ }
48
+ return providers;
49
+ }
50
+ /** Open the browser with a URL (best-effort; platform-specific). */
51
+ function openBrowser(url) {
52
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
53
+ execFile(cmd, [url], { timeout: 5_000 }, () => {
54
+ /* best-effort: browser open is fire-and-forget */
55
+ });
56
+ }
57
+ /** `/login` — show the provider selector, then run pi's native login flow in
58
+ * the viewer. On success, notifies the broker to reload auth via reload_auth.
59
+ * Returns a Picker (OAuthSelectorComponent as the overlay). */
60
+ export function buildLoginPicker(tui, agentDir, onAuthReloaded, notify, controls) {
61
+ const { close, replace } = controls;
62
+ const { authStorage, registry } = buildLocalAuth(agentDir);
63
+ const providers = buildProviderList(authStorage, registry);
64
+ const getAuthStatus = (id) => authStorage.getAuthStatus(id);
65
+ const onSelect = (providerId) => {
66
+ const provider = providers.find((p) => p.id === providerId);
67
+ if (provider === undefined) {
68
+ notify(`Unknown provider: ${providerId}`);
69
+ close();
70
+ return;
71
+ }
72
+ // `finish()` is the single idempotent exit for the login dialog: tear the
73
+ // whole picker down (which restores editor focus), exactly once, no matter
74
+ // which path (success, failure, or an Esc-cancel that aborts mid-flow) gets
75
+ // there first. The OAuth/cancel paths resolve through DIFFERENT channels (the
76
+ // login() promise vs the dialog's onComplete), so the guard prevents a double
77
+ // close + double notify.
78
+ let settled = false;
79
+ const finish = () => {
80
+ if (settled)
81
+ return;
82
+ settled = true;
83
+ close();
84
+ };
85
+ const dialog = new LoginDialogComponent(tui, providerId, (success, message) => {
86
+ // The dialog's own completion (chiefly an Esc-cancel). On a real OAuth
87
+ // success the login() promise below already finished + notified, so the
88
+ // settled guard makes this a no-op then.
89
+ if (settled)
90
+ return;
91
+ if (success) {
92
+ notify(`Logged in to ${provider.name}`);
93
+ onAuthReloaded();
94
+ }
95
+ else {
96
+ notify(message ? `Login failed: ${message}` : `Login to ${provider.name} was cancelled`);
97
+ }
98
+ finish();
99
+ }, provider.name);
100
+ // Swap the provider selector for the login dialog IN PLACE — it renders
101
+ // inline under the editor (same slot as the model picker), not as a centered
102
+ // overlay. `replace` focuses the dialog; `close()` (via finish) unmounts it.
103
+ replace(dialog, dialog);
104
+ if (provider.authType === 'oauth') {
105
+ const isKnownOAuth = authStorage.getOAuthProviders().some((p) => p.id === providerId);
106
+ if (!isKnownOAuth) {
107
+ // An extension-registered provider: its OAuth impl lives in the broker's
108
+ // registry, not the viewer's local one, so we cannot drive the flow here.
109
+ dialog.showInfo([
110
+ `Provider "${provider.name}" is extension-registered and cannot be logged in`,
111
+ `from the viewer (the extension runs in the broker, not here).`,
112
+ `Run /login directly in the node's terminal, or log in via the pi CLI.`,
113
+ ]);
114
+ // The dialog stays up; an Esc routes through onComplete → finish().
115
+ return;
116
+ }
117
+ authStorage
118
+ .login(providerId, {
119
+ onAuth: (info) => {
120
+ dialog.showAuth(info.url, info.instructions);
121
+ openBrowser(info.url);
122
+ },
123
+ onDeviceCode: (info) => {
124
+ dialog.showDeviceCode(info);
125
+ openBrowser(info.verificationUri);
126
+ },
127
+ onPrompt: (prompt) => dialog.showPrompt(prompt.message, prompt.placeholder),
128
+ onProgress: (message) => {
129
+ dialog.showProgress(message);
130
+ },
131
+ onManualCodeInput: () => dialog.showManualInput('Paste the code or URL:'),
132
+ onSelect: async (prompt) => {
133
+ const labels = prompt.options.map((o, i) => `${i + 1}. ${o.label}`).join('\n');
134
+ const answer = await dialog.showPrompt(`${prompt.message}\n${labels}`, '1');
135
+ const idx = parseInt(answer, 10) - 1;
136
+ return prompt.options[idx]?.id;
137
+ },
138
+ signal: dialog.signal,
139
+ })
140
+ .then(() => {
141
+ if (settled)
142
+ return; // a cancel/abort already closed it
143
+ notify(`Logged in to ${provider.name}`);
144
+ onAuthReloaded();
145
+ finish();
146
+ })
147
+ .catch((err) => {
148
+ // An Esc-cancel aborts the flow; the dialog's onComplete already
149
+ // notified + finished, so only report a genuine error here.
150
+ if (err?.name !== 'AbortError') {
151
+ notify(`Login error: ${String(err?.message ?? err)}`);
152
+ }
153
+ finish();
154
+ });
155
+ }
156
+ else {
157
+ // API-key provider: prompt for the key and persist it locally.
158
+ dialog
159
+ .showPrompt(`Enter API key for ${provider.name}:`, 'sk-...')
160
+ .then((key) => {
161
+ if (settled)
162
+ return;
163
+ if (key.trim()) {
164
+ authStorage.set(providerId, { type: 'api_key', key: key.trim() });
165
+ notify(`API key saved for ${provider.name}`);
166
+ onAuthReloaded();
167
+ }
168
+ else {
169
+ notify('No key entered — login cancelled');
170
+ }
171
+ finish();
172
+ })
173
+ .catch(() => finish());
174
+ }
175
+ };
176
+ const component = new OAuthSelectorComponent('login', authStorage, providers, onSelect, () => close(), getAuthStatus);
177
+ return { component, focus: component };
178
+ }
179
+ /** `/logout` — show the provider selector in logout mode. On select, calls
180
+ * authStorage.logout(providerId) directly, then notifies the broker to reload. */
181
+ export function buildLogoutPicker(agentDir, onAuthReloaded, notify, close) {
182
+ const { authStorage, registry } = buildLocalAuth(agentDir);
183
+ const providers = buildProviderList(authStorage, registry);
184
+ const getAuthStatus = (id) => authStorage.getAuthStatus(id);
185
+ const onSelect = (providerId) => {
186
+ const provider = providers.find((p) => p.id === providerId);
187
+ authStorage.logout(providerId);
188
+ close();
189
+ notify(`Logged out of ${provider?.name ?? providerId}`);
190
+ onAuthReloaded();
191
+ };
192
+ const component = new OAuthSelectorComponent('logout', authStorage, providers, onSelect, () => close(), getAuthStatus);
193
+ return { component, focus: component };
194
+ }
@@ -1,3 +1,4 @@
1
+ import type { AttachPalette } from './config-load.js';
1
2
  export interface CanvasPanelLines {
2
3
  /** Above-editor manager line(s) — empty for a root node (no manager). */
3
4
  managers: string[];
@@ -6,5 +7,6 @@ export interface CanvasPanelLines {
6
7
  }
7
8
  /** Build the BASE panel line stacks for `nodeId` from canvas.db (ported verbatim
8
9
  * from canvas-nav.ts renderBase). `asks` is the per-node pending-ask map the
9
- * caller polls; pass `{}` to omit the ⚑ badges. */
10
- export declare function buildCanvasPanelLines(nodeId: string, asks: Record<string, number>): CanvasPanelLines;
10
+ * caller polls; pass `{}` to omit the ⚑ badges. `palette` (the live theme)
11
+ * drives the headers + dim cells; omit it and the panels render uncolored. */
12
+ export declare function buildCanvasPanelLines(nodeId: string, asks: Record<string, number>, palette?: AttachPalette): CanvasPanelLines;
@@ -12,39 +12,67 @@
12
12
  // caller owns the Containers and the refresh trigger. beginFrame() memoizes the
13
13
  // canvas.db reads for this one build pass (mirrors canvas-nav's per-frame cache),
14
14
  // so a whole panel rebuild is a single fan-out of disk/db reads.
15
- import { beginFrame, cNode, managerOf, liveReports, climbRoot, computeSubtreeActivity, navLabel, coloredGlyph, truncate, tokensCell, cycleBadge, childBadge, liveBelowBadge, askBadge, activityCell, DIM, RESET, YELLOW, } from '../../core/canvas/nav-model.js';
15
+ import { beginFrame, cNode, managerOf, liveReports, climbRoot, computeSubtreeActivity, navLabel, coloredGlyph, truncate, tokensCell, cycleBadge, childBadge, liveBelowBadge, askBadge, activityCell, YELLOW, RESET, } from '../../core/canvas/nav-model.js';
16
+ /** Column widths for the aligned panel rows (visible columns). name is sized to
17
+ * the widest label across BOTH panels (capped); kind is wide enough for the
18
+ * longest persona kind (`orchestrator` = 12) so real kinds never truncate;
19
+ * tokens is fixed. Both panels share one grid. */
20
+ const KIND_W = 12;
21
+ const TOK_W = 6;
22
+ const NAME_CAP = 22;
23
+ /** Pad to `w`, or ellipsize with `…` when longer (plain text — callers pass
24
+ * un-styled labels). */
25
+ function nameCell(s, w) {
26
+ return s.length > w ? `${s.slice(0, w - 1)}…` : s + ' '.repeat(w - s.length);
27
+ }
28
+ function padStartVisible(s, w) {
29
+ return s.length >= w ? s.slice(0, w) : ' '.repeat(w - s.length) + s;
30
+ }
16
31
  /** Build the BASE panel line stacks for `nodeId` from canvas.db (ported verbatim
17
32
  * from canvas-nav.ts renderBase). `asks` is the per-node pending-ask map the
18
- * caller polls; pass `{}` to omit the ⚑ badges. */
19
- export function buildCanvasPanelLines(nodeId, asks) {
33
+ * caller polls; pass `{}` to omit the ⚑ badges. `palette` (the live theme)
34
+ * drives the headers + dim cells; omit it and the panels render uncolored. */
35
+ export function buildCanvasPanelLines(nodeId, asks, palette) {
20
36
  // Fresh snapshot: drop the previous build's memoized reads so this pass sees
21
37
  // current disk+db state, then read-once within it.
22
38
  beginFrame();
39
+ // Styling: theme-driven when a palette is passed, plain otherwise (so the
40
+ // panels still build for a non-attach caller / a test).
41
+ const muted = palette?.muted ?? ((s) => s);
42
+ const faint = palette?.faint ?? ((s) => s);
23
43
  // One subtree-activity pass (rooted at the ancestry root) feeds the ⇣N
24
44
  // live-work-below badge on both the manager line and every report row.
25
45
  const activity = computeSubtreeActivity(climbRoot(nodeId), nodeId);
26
- const managers = [];
46
+ // One shared name-column width across BOTH panels so the grids line up.
27
47
  const mgr = managerOf(nodeId);
48
+ const live = liveReports(nodeId);
49
+ const gridIds = [...(mgr !== undefined ? [mgr] : []), ...live];
50
+ const nameW = gridIds.length > 0
51
+ ? Math.min(NAME_CAP, Math.max(...gridIds.map((id) => navLabel(cNode(id), id).length)))
52
+ : NAME_CAP;
53
+ // A node row laid out on the shared grid: glyph · name · kind · tokens · badges.
54
+ const row = (id) => {
55
+ const n = cNode(id);
56
+ const name = nameCell(navLabel(n, id), nameW);
57
+ const kind = faint(nameCell(n?.kind ?? '', KIND_W));
58
+ const tokens = muted(padStartVisible(tokensCell(id), TOK_W));
59
+ const badges = `${cycleBadge(n)}${childBadge(n)}${liveBelowBadge(n, activity.activeBelow)}${askBadge(id, asks)}${activityCell(id, n)}`;
60
+ return truncate(` ${coloredGlyph(n)} ${name} ${kind} ${tokens}${badges}`);
61
+ };
62
+ // --- managers panel (↑) ---
63
+ const managers = [];
28
64
  if (mgr !== undefined) {
29
- const mn = cNode(mgr);
30
- const name = navLabel(mn, mgr);
31
- managers.push(truncate(`↑ ${name} ${coloredGlyph(mn)} ${DIM}${mn?.kind ?? ''}${RESET} ${DIM}${tokensCell(mgr)}${RESET}${cycleBadge(mn)}${childBadge(mn)}${liveBelowBadge(mn, activity.activeBelow)}${askBadge(mgr, asks)}${activityCell(mgr, mn)}`));
65
+ managers.push(row(mgr));
32
66
  }
67
+ // --- reports panel (↓) ---
33
68
  const reports = [];
34
- const live = liveReports(nodeId);
35
69
  if (live.length > 0) {
36
- const nameW = Math.min(20, Math.max(...live.map((id) => navLabel(cNode(id), id).length)));
37
- for (const id of live) {
38
- const n = cNode(id);
39
- const name = navLabel(n, id).padEnd(nameW);
40
- const kind = `${DIM}${(n?.kind ?? '').padEnd(6)}${RESET}`;
41
- const tokens = `${DIM}${tokensCell(id).padStart(5)}${RESET}`;
42
- reports.push(truncate(` ${coloredGlyph(n)} ${name} ${kind} ${tokens}${cycleBadge(n)}${childBadge(n)}${liveBelowBadge(n, activity.activeBelow)}${askBadge(id, asks)}${activityCell(id, n)}`));
43
- }
70
+ for (const id of live)
71
+ reports.push(row(id));
44
72
  }
45
73
  // Self's own pending asks (no self row in BASE) → a trailing inline line.
46
74
  const selfAsks = asks[nodeId] ?? 0;
47
75
  if (selfAsks > 0)
48
- reports.push(`${YELLOW}⚑${selfAsks}${RESET}`);
76
+ reports.push(` ${YELLOW}⚑ ${selfAsks} pending${RESET}`);
49
77
  return { managers, reports };
50
78
  }
@@ -1,6 +1,7 @@
1
1
  import { Container, type TUI } from '@earendil-works/pi-tui';
2
2
  import { type AgentSessionEvent } from '@earendil-works/pi-coding-agent';
3
3
  import type { BrokerSnapshot } from '../../core/runtime/broker-protocol.js';
4
+ import type { AttachPalette } from './config-load.js';
4
5
  export interface ChatViewOptions {
5
6
  /** Working dir used by `ToolExecutionComponent` for path/diff display. Defaults
6
7
  * to the viewer's cwd; T7 may pass the node's working dir for nicer paths. */
@@ -19,6 +20,9 @@ export interface ChatViewOptions {
19
20
  * (queue_update, session_info_changed, thinking_level_changed). The attach
20
21
  * command (T7) wires a footer here; absent → these events are no-ops. */
21
22
  onFooterEvent?: (event: AgentSessionEvent) => void;
23
+ /** Theme palette for the activity spinner / dim status / error lines. Absent →
24
+ * the module defaults (faint + ANSI red) are used. */
25
+ palette?: AttachPalette;
22
26
  }
23
27
  export declare class ChatView {
24
28
  private readonly tui;
@@ -31,10 +35,16 @@ export declare class ChatView {
31
35
  private readonly cwd;
32
36
  private readonly showImages;
33
37
  private readonly imageWidthCells;
34
- private readonly hideThinking;
38
+ private hideThinking;
35
39
  private readonly hiddenThinkingLabel;
36
- private readonly toolOutputExpanded;
40
+ private toolOutputExpanded;
37
41
  private readonly onFooterEvent;
42
+ /** Theme-derived colorizers for the activity/status area. */
43
+ private readonly spinnerStyle;
44
+ private readonly dimStyle;
45
+ private readonly errorStyle;
46
+ /** Bold accent for the expanded crtr-context label. */
47
+ private readonly labelStyle;
38
48
  /** The assistant message component currently being streamed (between
39
49
  * message_start and message_end for an assistant turn). */
40
50
  private streamingComponent;
@@ -44,6 +54,10 @@ export declare class ChatView {
44
54
  private activityLoader;
45
55
  /** While true, `append` skips re-pinning the status container (bulk rebuild). */
46
56
  private bulkMode;
57
+ /** Text of the most recent assistant message, kept current from the welcome
58
+ * snapshot AND the live event stream so `/copy` (getLastAssistantText) works
59
+ * whether the message arrived before or after attach. */
60
+ private lastAssistantText;
47
61
  constructor(tui: TUI, container: Container, opts?: ChatViewOptions);
48
62
  /** Render the full message history from a `welcome` snapshot. Ports pi's
49
63
  * `renderSessionContext` (interactive-mode.js:2553): assistant messages spawn
@@ -55,6 +69,14 @@ export declare class ChatView {
55
69
  * timers die with the process, but if T7 reuses a ChatView across reconnects in
56
70
  * one process it should call this on detach to avoid a leaked interval. */
57
71
  dispose(): void;
72
+ /** Ctrl+O / app.tools.expand — flip global tool-output expansion and apply it to
73
+ * every rendered component that supports it. Returns the new expanded state so
74
+ * the caller can surface a notice. */
75
+ toggleToolsExpanded(): boolean;
76
+ /** Ctrl+T / app.thinking.toggle — flip thinking-block visibility and apply it to
77
+ * every assistant message (the in-flight streaming bubble is itself a child of
78
+ * the container, so the loop covers it too). Returns the new HIDDEN state. */
79
+ toggleThinking(): boolean;
58
80
  handleEvent(event: AgentSessionEvent): void;
59
81
  private addMessageToChat;
60
82
  /** Default-path tool component (toolDef undefined → builtin/default rendering). */
@@ -72,6 +94,9 @@ export declare class ChatView {
72
94
  private showError;
73
95
  /** Extract the tool-call content items from an assistant message. */
74
96
  private assistantToolCalls;
97
+ /** The text of the most recent assistant message, for `/copy`. Undefined when
98
+ * no assistant message has been seen (or it had no text content). */
99
+ getLastAssistantText(): string | undefined;
75
100
  /** Concatenate the text blocks of a user message (string or content array). */
76
101
  private userMessageText;
77
102
  }
@@ -22,12 +22,31 @@
22
22
  // extensions degrade to the default, which is the decided Phase-4 behavior.
23
23
  import { Container, Loader, Spacer, Text } from '@earendil-works/pi-tui';
24
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
25
+ import { ContextMessageComponent } from './context-message.js';
26
+ /** customType stamped on the injected `<crtr-context>` bearings block (mirrors
27
+ * CONTEXT_INTRO_CUSTOM_TYPE in canvas-context-intro.ts kept as a local literal
28
+ * so the viewer never imports the in-process extension). */
29
+ const CONTEXT_INTRO_CUSTOM_TYPE = 'crtr-context';
30
+ /** Theme-derived chrome colors (spinner / dim / error) are supplied by the
31
+ * attach command via `ChatViewOptions.palette` (built from the live theme in
32
+ * config-load.ts). pi does not re-export the raw `theme` instance, so when no
33
+ * palette is passed (ChatView used standalone) these defaults stand in: a faint
34
+ * dim and ANSI red, matching the palette's own fallbacks. The message components
35
+ * themselves still use `getMarkdownTheme()` for full-fidelity rendering. */
36
+ const defaultSpinnerStyle = (s) => `\x1b[2m${s}\x1b[22m`;
37
+ const defaultDimStyle = (s) => `\x1b[2m${s}\x1b[22m`;
38
+ /** A component supports the Ctrl+O global tool-output toggle. Mirrors pi's
39
+ * `isExpandable` duck-type (interactive-mode.js): any rendered child carrying a
40
+ * `setExpanded(boolean)` method — tool executions, custom/compaction/branch/skill
41
+ * message components — participates; everything else (the status container, plain
42
+ * text, assistant/user bubbles) is skipped. */
43
+ function isExpandable(obj) {
44
+ return (typeof obj === 'object' &&
45
+ obj !== null &&
46
+ 'setExpanded' in obj &&
47
+ typeof obj.setExpanded === 'function');
48
+ }
49
+ const defaultErrorStyle = (s) => `\x1b[31m${s}\x1b[39m`;
31
50
  export class ChatView {
32
51
  tui;
33
52
  /** The chat scroll area handed in by the attach command. */
@@ -43,6 +62,12 @@ export class ChatView {
43
62
  hiddenThinkingLabel;
44
63
  toolOutputExpanded;
45
64
  onFooterEvent;
65
+ /** Theme-derived colorizers for the activity/status area. */
66
+ spinnerStyle;
67
+ dimStyle;
68
+ errorStyle;
69
+ /** Bold accent for the expanded crtr-context label. */
70
+ labelStyle = (s) => `\x1b[1m${s}\x1b[22m`;
46
71
  /** The assistant message component currently being streamed (between
47
72
  * message_start and message_end for an assistant turn). */
48
73
  streamingComponent;
@@ -52,6 +77,10 @@ export class ChatView {
52
77
  activityLoader;
53
78
  /** While true, `append` skips re-pinning the status container (bulk rebuild). */
54
79
  bulkMode = false;
80
+ /** Text of the most recent assistant message, kept current from the welcome
81
+ * snapshot AND the live event stream so `/copy` (getLastAssistantText) works
82
+ * whether the message arrived before or after attach. */
83
+ lastAssistantText;
55
84
  constructor(tui, container, opts = {}) {
56
85
  this.tui = tui;
57
86
  this.container = container;
@@ -62,6 +91,9 @@ export class ChatView {
62
91
  this.hiddenThinkingLabel = opts.hiddenThinkingLabel ?? 'Thinking...';
63
92
  this.toolOutputExpanded = opts.toolOutputExpanded ?? false;
64
93
  this.onFooterEvent = opts.onFooterEvent;
94
+ this.spinnerStyle = opts.palette?.active ?? defaultSpinnerStyle;
95
+ this.dimStyle = opts.palette?.muted ?? defaultDimStyle;
96
+ this.errorStyle = opts.palette?.error ?? defaultErrorStyle;
65
97
  this.container.addChild(this.statusContainer);
66
98
  }
67
99
  // -------------------------------------------------------------------------
@@ -86,6 +118,11 @@ export class ChatView {
86
118
  for (let i = 0; i < messages.length; i++) {
87
119
  const message = messages[i];
88
120
  if (message.role === 'assistant') {
121
+ // Keep the `/copy` source current from the catch-up transcript too, not
122
+ // only the live stream (you usually attach AFTER the last reply landed).
123
+ const text = this.userMessageText(message);
124
+ if (text.trim())
125
+ this.lastAssistantText = text;
89
126
  // If the snapshot caught an IN-FLIGHT assistant turn (state.isStreaming
90
127
  // and this is the trailing, non-terminal assistant message), bind it as
91
128
  // the live streaming component so the relayed message_update/message_end
@@ -148,6 +185,35 @@ export class ChatView {
148
185
  this.setActivity(undefined);
149
186
  }
150
187
  // -------------------------------------------------------------------------
188
+ // Global display toggles (Ctrl+O / Ctrl+T), driven by the input controller.
189
+ // Pure client-side render state — no broker round-trip — ported from pi's
190
+ // `setToolsExpanded` / `toggleThinkingBlockVisibility`.
191
+ // -------------------------------------------------------------------------
192
+ /** Ctrl+O / app.tools.expand — flip global tool-output expansion and apply it to
193
+ * every rendered component that supports it. Returns the new expanded state so
194
+ * the caller can surface a notice. */
195
+ toggleToolsExpanded() {
196
+ this.toolOutputExpanded = !this.toolOutputExpanded;
197
+ for (const child of this.container.children) {
198
+ if (isExpandable(child))
199
+ child.setExpanded(this.toolOutputExpanded);
200
+ }
201
+ this.tui.requestRender();
202
+ return this.toolOutputExpanded;
203
+ }
204
+ /** Ctrl+T / app.thinking.toggle — flip thinking-block visibility and apply it to
205
+ * every assistant message (the in-flight streaming bubble is itself a child of
206
+ * the container, so the loop covers it too). Returns the new HIDDEN state. */
207
+ toggleThinking() {
208
+ this.hideThinking = !this.hideThinking;
209
+ for (const child of this.container.children) {
210
+ if (child instanceof AssistantMessageComponent)
211
+ child.setHideThinkingBlock(this.hideThinking);
212
+ }
213
+ this.tui.requestRender();
214
+ return this.hideThinking;
215
+ }
216
+ // -------------------------------------------------------------------------
151
217
  // Live stream: the 15-of-17-case handleEvent port (turn_start/turn_end no-op,
152
218
  // exactly as interactive pi falls through default for those two).
153
219
  // -------------------------------------------------------------------------
@@ -199,6 +265,13 @@ export class ChatView {
199
265
  case 'message_end': {
200
266
  if (event.message.role !== 'assistant')
201
267
  break;
268
+ // Stash the completed reply's text for `/copy` (skip aborted/error turns,
269
+ // whose text is partial or absent).
270
+ if (event.message.stopReason !== 'aborted' && event.message.stopReason !== 'error') {
271
+ const text = this.userMessageText(event.message);
272
+ if (text.trim())
273
+ this.lastAssistantText = text;
274
+ }
202
275
  if (this.streamingComponent) {
203
276
  const stopReason = event.message.stopReason;
204
277
  let errorMessage = event.message.errorMessage;
@@ -328,6 +401,15 @@ export class ChatView {
328
401
  }
329
402
  case 'custom': {
330
403
  if (message.display) {
404
+ // The <crtr-context> bearings block folds to a one-liner under Ctrl+O
405
+ // (the broker runs its renderer extension, so reimplement it natively).
406
+ if (message.customType === CONTEXT_INTRO_CUSTOM_TYPE) {
407
+ this.append(new ContextMessageComponent(this.userMessageText(message), this.toolOutputExpanded, this.dimStyle,
408
+ // Bold + active accent, so the [crtr context] label reads as a
409
+ // distinct color against the dim rest of the line.
410
+ (s) => this.spinnerStyle(this.labelStyle(s))));
411
+ break;
412
+ }
331
413
  // No extension renderer in the viewer → default custom-message render.
332
414
  const component = new CustomMessageComponent(message, undefined, getMarkdownTheme());
333
415
  component.setExpanded(this.toolOutputExpanded);
@@ -406,11 +488,12 @@ export class ChatView {
406
488
  this.setActivity(undefined);
407
489
  this.streamingComponent = undefined;
408
490
  this.pendingTools.clear();
491
+ this.lastAssistantText = undefined;
409
492
  this.container.clear();
410
493
  this.container.addChild(this.statusContainer);
411
494
  }
412
495
  makeLoader(message) {
413
- return new Loader(this.tui, spinnerStyle, dimStyle, message);
496
+ return new Loader(this.tui, this.spinnerStyle, this.dimStyle, message);
414
497
  }
415
498
  /** Swap the single active activity indicator (working/compaction/retry). */
416
499
  setActivity(loader) {
@@ -422,11 +505,11 @@ export class ChatView {
422
505
  }
423
506
  showStatus(message) {
424
507
  this.append(new Spacer(1));
425
- this.append(new Text(dimStyle(message), 1, 0));
508
+ this.append(new Text(this.dimStyle(message), 1, 0));
426
509
  }
427
510
  showError(message) {
428
511
  this.append(new Spacer(1));
429
- this.append(new Text(`\x1b[31m${message}\x1b[39m`, 1, 0));
512
+ this.append(new Text(this.errorStyle(message), 1, 0));
430
513
  }
431
514
  /** Extract the tool-call content items from an assistant message. */
432
515
  assistantToolCalls(message) {
@@ -435,6 +518,12 @@ export class ChatView {
435
518
  return [];
436
519
  return content.filter((c) => typeof c === 'object' && c !== null && c.type === 'toolCall');
437
520
  }
521
+ /** The text of the most recent assistant message, for `/copy`. Undefined when
522
+ * no assistant message has been seen (or it had no text content). */
523
+ getLastAssistantText() {
524
+ const t = this.lastAssistantText?.trim();
525
+ return t ? t : undefined;
526
+ }
438
527
  /** Concatenate the text blocks of a user message (string or content array). */
439
528
  userMessageText(message) {
440
529
  const content = message.content;
@@ -1,16 +1,55 @@
1
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. */
2
+ /** A clipboard image ready to attach, plus an optional note for the controller
3
+ * to surface — either a resize note (from pi's `formatDimensionNote`) or, when
4
+ * no image was attached, the reason (dropped over the cap, or no clipboard
5
+ * tool / read failure). */
4
6
  export interface ClipboardImageResult {
5
7
  /** 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
+ * when the image was read but DROPPED (a fallback exceeded MAX_BYTES base64),
9
+ * or when there was no image to read but a precise reason is worth surfacing;
10
+ * `note` then carries that reason and the caller attaches nothing. */
8
11
  image?: ImageContent;
9
12
  /** 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. */
13
+ * when resized) or, when `image` is absent, the reason no image was attached. */
11
14
  note?: string;
12
15
  }
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
+ /** Persist a (already-resized) clipboard image to a stable temp file and return
17
+ * its absolute path, so the attach editor can drop the PATH inline into the
18
+ * prompt instead of inlining base64 the agent can't see. The broker runs on the
19
+ * same host, so a tmpdir path is readable by the engine. */
20
+ export declare function writeClipboardImageToFile(image: ImageContent): string;
21
+ /** Read + resize the current clipboard image. Returns `null` ONLY when the
22
+ * clipboard genuinely holds no image; a `note`-only result means there is a
23
+ * precise reason to surface (no clipboard tool, read failure, or an over-cap
24
+ * drop). All best-effort — the caller shows a brief notice and carries on. */
16
25
  export declare function readClipboardImage(): Promise<ClipboardImageResult | null>;
26
+ interface RawImage {
27
+ bytes: Buffer;
28
+ mimeType: string;
29
+ }
30
+ /** Outcome of the raw clipboard read, distinguishing the three cases the caller
31
+ * must tell apart: `image` present → got bytes; `note` present → no image AND a
32
+ * precise reason to surface (no clipboard tool / read failure); both absent →
33
+ * the clipboard genuinely holds no image. */
34
+ export interface RawReadOutcome {
35
+ image?: RawImage;
36
+ note?: string;
37
+ }
38
+ /** Result of probing one clipboard tool, distinguishing a MISSING binary (so the
39
+ * caller can say "no clipboard tool") from a tool that ran but produced no image
40
+ * (`bytes` absent, `missing` false → genuinely empty). */
41
+ export interface RunResult {
42
+ /** Tool stdout when it produced non-empty output. */
43
+ bytes?: Buffer;
44
+ /** The binary was not found on PATH (ENOENT). */
45
+ missing?: boolean;
46
+ }
47
+ /** Pure macOS dispatch (the regression-test seam): try `pngpaste` if present,
48
+ * else fall through to the native osascript read — a missing `pngpaste` must
49
+ * NOT collapse to "no image", which was the stock-Mac bug. */
50
+ export declare function selectMacOutcome(runTool: (command: string, args: string[]) => RunResult, readNative: () => RawReadOutcome): RawReadOutcome;
51
+ /** Pure Linux dispatch (the regression-test seam): Wayland (`wl-paste`) first,
52
+ * then X11 (`xclip`), PNG then JPEG. If NO tool binary was found, surface a
53
+ * precise "install …" note; if a tool ran but found no image, report empty. */
54
+ export declare function selectLinuxOutcome(runTool: (command: string, args: string[]) => RunResult, wayland: boolean): RawReadOutcome;
55
+ export {};