@crouton-kit/crouter 0.3.28 → 0.3.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/dist/build-root.js +4 -2
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  6. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  7. package/dist/builtin-memory/design.md +1 -1
  8. package/dist/builtin-memory/development.md +1 -1
  9. package/dist/builtin-memory/internal/INDEX.md +1 -1
  10. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  12. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  13. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  14. package/dist/builtin-memory/planning.md +1 -1
  15. package/dist/builtin-memory/spec.md +6 -4
  16. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  17. package/dist/builtin-personas/runtime-base.md +5 -4
  18. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  19. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  20. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  21. package/dist/builtin-views/canvas/core.mjs +586 -0
  22. package/dist/builtin-views/canvas/text.mjs +58 -0
  23. package/dist/builtin-views/canvas/tui.mjs +165 -0
  24. package/dist/builtin-views/canvas/web.jsx +120 -0
  25. package/dist/builtin-views/git-pr/core.mjs +673 -0
  26. package/dist/builtin-views/git-pr/text.mjs +84 -0
  27. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  28. package/dist/builtin-views/git-pr/web.jsx +216 -0
  29. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  30. package/dist/builtin-views/inbox/core.mjs +1273 -0
  31. package/dist/builtin-views/inbox/text.mjs +73 -0
  32. package/dist/builtin-views/inbox/tui.mjs +312 -0
  33. package/dist/builtin-views/inbox/web.jsx +188 -0
  34. package/dist/builtin-views/linkedin/core.mjs +906 -0
  35. package/dist/builtin-views/linkedin/text.mjs +69 -0
  36. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  37. package/dist/builtin-views/linkedin/web.jsx +206 -0
  38. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  39. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  40. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  41. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  42. package/dist/cli.js +3 -3
  43. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  44. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  45. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  46. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  47. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  48. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  49. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  50. package/dist/clients/attach/attach-cmd.js +282 -46
  51. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  52. package/dist/clients/attach/auth-pickers.js +194 -0
  53. package/dist/clients/attach/canvas-panels.d.ts +4 -2
  54. package/dist/clients/attach/canvas-panels.js +45 -17
  55. package/dist/clients/attach/chat-view.d.ts +27 -2
  56. package/dist/clients/attach/chat-view.js +98 -9
  57. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  58. package/dist/clients/attach/clipboard-image.js +150 -26
  59. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  60. package/dist/clients/attach/clipboard-text.js +39 -0
  61. package/dist/clients/attach/config-load.d.ts +54 -7
  62. package/dist/clients/attach/config-load.js +99 -3
  63. package/dist/clients/attach/context-message.d.ts +12 -0
  64. package/dist/clients/attach/context-message.js +72 -0
  65. package/dist/clients/attach/git-info.d.ts +20 -0
  66. package/dist/clients/attach/git-info.js +44 -0
  67. package/dist/clients/attach/graph-overlay.d.ts +13 -4
  68. package/dist/clients/attach/graph-overlay.js +59 -19
  69. package/dist/clients/attach/input-controller.d.ts +75 -13
  70. package/dist/clients/attach/input-controller.js +305 -67
  71. package/dist/clients/attach/pickers.d.ts +58 -0
  72. package/dist/clients/attach/pickers.js +171 -0
  73. package/dist/clients/attach/slash-commands.d.ts +22 -0
  74. package/dist/clients/attach/slash-commands.js +91 -25
  75. package/dist/clients/attach/titled-editor.d.ts +33 -0
  76. package/dist/clients/attach/titled-editor.js +84 -0
  77. package/dist/clients/attach/view-socket.d.ts +18 -1
  78. package/dist/clients/attach/view-socket.js +70 -1
  79. package/dist/clients/web/dev-server.d.ts +7 -0
  80. package/dist/clients/web/dev-server.js +59 -0
  81. package/dist/clients/web/events.d.ts +14 -0
  82. package/dist/clients/web/events.js +151 -0
  83. package/dist/clients/web/server.d.ts +18 -0
  84. package/dist/clients/web/server.js +450 -0
  85. package/dist/clients/web/web-cmd.d.ts +2 -0
  86. package/dist/clients/web/web-cmd.js +120 -0
  87. package/dist/commands/canvas.js +1 -2
  88. package/dist/commands/chord.js +1 -1
  89. package/dist/commands/dashboard.js +5 -1
  90. package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
  91. package/dist/commands/memory/lint.js +14 -2
  92. package/dist/commands/memory/read.js +2 -2
  93. package/dist/commands/memory/shared.d.ts +7 -1
  94. package/dist/commands/memory/shared.js +18 -3
  95. package/dist/commands/memory/write.js +8 -4
  96. package/dist/commands/memory.js +4 -4
  97. package/dist/commands/node.d.ts +1 -1
  98. package/dist/commands/node.js +64 -65
  99. package/dist/commands/pkg.js +1 -2
  100. package/dist/commands/revive.js +11 -3
  101. package/dist/commands/sys/sync.d.ts +1 -0
  102. package/dist/commands/sys/sync.js +187 -0
  103. package/dist/commands/sys.js +3 -2
  104. package/dist/commands/view-cycle.js +2 -2
  105. package/dist/commands/view-list.js +8 -8
  106. package/dist/commands/view-new.js +21 -17
  107. package/dist/commands/view-pick.js +1 -1
  108. package/dist/commands/view-run.js +35 -14
  109. package/dist/commands/view.js +8 -6
  110. package/dist/commands/workspace.d.ts +2 -0
  111. package/dist/commands/workspace.js +161 -0
  112. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  114. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  116. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  117. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  118. package/dist/core/__tests__/close.test.js +14 -20
  119. package/dist/core/__tests__/context-intro.test.js +19 -19
  120. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  121. package/dist/core/__tests__/draw-style.test.js +1 -1
  122. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  123. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  124. package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
  125. package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
  126. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  127. package/dist/core/__tests__/focuses.test.js +25 -13
  128. package/dist/core/__tests__/fork.test.js +22 -5
  129. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  130. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  131. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  132. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  133. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  134. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  135. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  136. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  137. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  138. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  139. package/dist/core/__tests__/helpers/harness.js +19 -36
  140. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  141. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  142. package/dist/core/__tests__/lifecycle.test.js +1 -19
  143. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  144. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  145. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
  146. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  147. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  148. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  149. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  150. package/dist/core/__tests__/reset.test.js +19 -79
  151. package/dist/core/__tests__/steer-note.test.js +9 -7
  152. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  153. package/dist/core/bootstrap.d.ts +0 -3
  154. package/dist/core/bootstrap.js +1 -143
  155. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  156. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  157. package/dist/core/canvas/browse/app.js +131 -8
  158. package/dist/core/canvas/browse/model.d.ts +23 -5
  159. package/dist/core/canvas/browse/model.js +68 -13
  160. package/dist/core/canvas/browse/render.d.ts +7 -1
  161. package/dist/core/canvas/browse/render.js +178 -47
  162. package/dist/core/canvas/canvas.js +1 -1
  163. package/dist/core/canvas/focuses.d.ts +4 -4
  164. package/dist/core/canvas/focuses.js +7 -7
  165. package/dist/core/canvas/nav-model.js +8 -7
  166. package/dist/core/canvas/render.d.ts +68 -9
  167. package/dist/core/canvas/render.js +195 -61
  168. package/dist/core/canvas/types.d.ts +16 -23
  169. package/dist/core/config.js +1 -4
  170. package/dist/core/memory-resolver.js +43 -5
  171. package/dist/core/runtime/bearings.d.ts +1 -1
  172. package/dist/core/runtime/bearings.js +8 -8
  173. package/dist/core/runtime/branded-host.d.ts +16 -0
  174. package/dist/core/runtime/branded-host.js +127 -0
  175. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  176. package/dist/core/runtime/broker.d.ts +4 -1
  177. package/dist/core/runtime/broker.js +494 -58
  178. package/dist/core/runtime/close.d.ts +12 -2
  179. package/dist/core/runtime/close.js +37 -16
  180. package/dist/core/runtime/front-door.js +6 -14
  181. package/dist/core/runtime/host.d.ts +14 -34
  182. package/dist/core/runtime/host.js +14 -50
  183. package/dist/core/runtime/launch.d.ts +12 -9
  184. package/dist/core/runtime/launch.js +27 -15
  185. package/dist/core/runtime/lifecycle.d.ts +1 -1
  186. package/dist/core/runtime/lifecycle.js +15 -19
  187. package/dist/core/runtime/nodes.d.ts +0 -55
  188. package/dist/core/runtime/nodes.js +6 -74
  189. package/dist/core/runtime/placement.d.ts +91 -335
  190. package/dist/core/runtime/placement.js +262 -840
  191. package/dist/core/runtime/promote.d.ts +2 -0
  192. package/dist/core/runtime/promote.js +24 -6
  193. package/dist/core/runtime/recap.d.ts +8 -0
  194. package/dist/core/runtime/recap.js +107 -0
  195. package/dist/core/runtime/recycle.js +25 -61
  196. package/dist/core/runtime/reset.d.ts +43 -37
  197. package/dist/core/runtime/reset.js +131 -218
  198. package/dist/core/runtime/revive.d.ts +9 -29
  199. package/dist/core/runtime/revive.js +40 -139
  200. package/dist/core/runtime/spawn.d.ts +15 -16
  201. package/dist/core/runtime/spawn.js +165 -194
  202. package/dist/core/runtime/surface-bg.d.ts +11 -0
  203. package/dist/core/runtime/surface-bg.js +68 -0
  204. package/dist/core/runtime/tmux.d.ts +13 -50
  205. package/dist/core/runtime/tmux.js +33 -87
  206. package/dist/core/scope.d.ts +1 -2
  207. package/dist/core/scope.js +1 -5
  208. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  209. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  210. package/dist/core/skill-sync/builtins.d.ts +42 -0
  211. package/dist/core/skill-sync/builtins.js +112 -0
  212. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  213. package/dist/core/skill-sync/claude-plugins.js +71 -0
  214. package/dist/core/skill-sync/engine.d.ts +42 -0
  215. package/dist/core/skill-sync/engine.js +633 -0
  216. package/dist/core/skill-sync/export.d.ts +23 -0
  217. package/dist/core/skill-sync/export.js +86 -0
  218. package/dist/core/skill-sync/manifest.d.ts +64 -0
  219. package/dist/core/skill-sync/manifest.js +181 -0
  220. package/dist/core/skill-sync/profile.d.ts +76 -0
  221. package/dist/core/skill-sync/profile.js +173 -0
  222. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  223. package/dist/core/skill-sync/snapshot.js +120 -0
  224. package/dist/core/substrate/index.d.ts +1 -1
  225. package/dist/core/substrate/index.js +1 -1
  226. package/dist/core/substrate/injected-store.js +3 -3
  227. package/dist/core/substrate/on-read.js +68 -6
  228. package/dist/core/substrate/render.d.ts +8 -11
  229. package/dist/core/substrate/render.js +29 -43
  230. package/dist/core/substrate/schema.d.ts +10 -3
  231. package/dist/core/substrate/schema.js +6 -3
  232. package/dist/core/tui/host.d.ts +12 -4
  233. package/dist/core/tui/host.js +280 -149
  234. package/dist/core/view/bridge.d.ts +10 -0
  235. package/dist/core/view/bridge.js +31 -0
  236. package/dist/core/view/chrome.d.ts +9 -0
  237. package/dist/core/view/chrome.js +22 -0
  238. package/dist/core/view/contract.d.ts +171 -0
  239. package/dist/core/view/contract.js +23 -0
  240. package/dist/core/view/loader.d.ts +31 -0
  241. package/dist/core/view/loader.js +188 -0
  242. package/dist/core/view/transport-local.d.ts +7 -0
  243. package/dist/core/view/transport-local.js +70 -0
  244. package/dist/core/view/transport.d.ts +4 -0
  245. package/dist/core/view/transport.js +15 -0
  246. package/dist/daemon/crtrd.d.ts +38 -27
  247. package/dist/daemon/crtrd.js +303 -376
  248. package/dist/daemon/manage.js +6 -1
  249. package/dist/index.js +1 -1
  250. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  251. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  252. package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
  253. package/dist/pi-extensions/canvas-nav.js +5 -0
  254. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  255. package/dist/pi-extensions/canvas-recap.js +236 -0
  256. package/dist/pi-extensions/canvas-resume.js +2 -1
  257. package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
  258. package/dist/pi-extensions/canvas-stophook.js +87 -101
  259. package/dist/pi-extensions/canvas-view.js +2 -1
  260. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  261. package/dist/pi-extensions/widget-order-bus.js +34 -0
  262. package/dist/prompts/view.d.ts +2 -2
  263. package/dist/prompts/view.js +145 -73
  264. package/dist/types.d.ts +1 -6
  265. package/dist/types.js +1 -3
  266. package/dist/web/ViewChrome.d.ts +7 -0
  267. package/dist/web/ViewChrome.js +28 -0
  268. package/dist/web/ViewPane.d.ts +39 -0
  269. package/dist/web/ViewPane.js +48 -0
  270. package/dist/web/index.d.ts +6 -0
  271. package/dist/web/index.js +16 -0
  272. package/dist/web/runtime.d.ts +39 -0
  273. package/dist/web/runtime.js +133 -0
  274. package/dist/web/states.d.ts +24 -0
  275. package/dist/web/states.js +24 -0
  276. package/dist/web/transport-http.d.ts +5 -0
  277. package/dist/web/transport-http.js +28 -0
  278. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  279. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  280. package/dist/web-client/index.html +13 -0
  281. package/package.json +20 -6
  282. package/dist/builtin-views/canvas/client.mjs +0 -303
  283. package/dist/builtin-views/canvas/view.mjs +0 -576
  284. package/dist/builtin-views/git-pr/client.mjs +0 -440
  285. package/dist/builtin-views/git-pr/view.mjs +0 -675
  286. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  287. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  288. package/dist/builtin-views/inbox/view.mjs +0 -889
  289. package/dist/builtin-views/linkedin/client.mjs +0 -610
  290. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  291. package/dist/commands/pkg/bridge.d.ts +0 -1
  292. package/dist/commands/pkg/bridge.js +0 -137
  293. package/dist/commands/skill/author.d.ts +0 -3
  294. package/dist/commands/skill/author.js +0 -140
  295. package/dist/commands/skill/shared.d.ts +0 -3
  296. package/dist/commands/skill/shared.js +0 -19
  297. package/dist/commands/skill.d.ts +0 -2
  298. package/dist/commands/skill.js +0 -21
  299. package/dist/commands/tmux-spread.d.ts +0 -2
  300. package/dist/commands/tmux-spread.js +0 -144
  301. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  302. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  303. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  304. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  305. package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
  306. package/dist/core/__tests__/home-session.test.js +0 -186
  307. package/dist/core/__tests__/relaunch.test.js +0 -335
  308. package/dist/core/bridge-map.d.ts +0 -19
  309. package/dist/core/bridge-map.js +0 -73
  310. package/dist/core/tui/contract.d.ts +0 -83
  311. package/dist/core/tui/contract.js +0 -8
  312. package/dist/core/tui/loader.d.ts +0 -16
  313. package/dist/core/tui/loader.js +0 -94
  314. package/dist/prompts/skill.d.ts +0 -2
  315. package/dist/prompts/skill.js +0 -650
  316. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  317. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  318. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  319. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  320. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  321. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
  322. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
@@ -16,21 +16,25 @@
16
16
  // is therefore a BRANCH wrapping a single leaf: `crtr attach to <node>`.
17
17
  import { randomUUID } from 'node:crypto';
18
18
  import { execFile } from 'node:child_process';
19
- import { CombinedAutocompleteProvider, Container, ProcessTerminal, Text, TUI, matchesKey, } from '@earendil-works/pi-tui';
20
- import { CustomEditor, getSelectListTheme, } from '@earendil-works/pi-coding-agent';
19
+ import { CombinedAutocompleteProvider, Container, ProcessTerminal, Text, TUI, matchesKey, truncateToWidth, } from '@earendil-works/pi-tui';
20
+ import { DynamicBorder, getSelectListTheme, } from '@earendil-works/pi-coding-agent';
21
21
  import { defineBranch, defineLeaf } from '../../core/command.js';
22
22
  import { InputError } from '../../core/io.js';
23
23
  import { getNode, getRow } from '../../core/canvas/index.js';
24
24
  // tmux driver verbs only through placement (the §5.1 model-over-driver seam) —
25
25
  // never `core/runtime/tmux.js` directly.
26
- import { setPaneOption, currentTmux } from '../../core/runtime/placement.js';
27
- import { climbRoot } from '../../core/canvas/nav-model.js';
26
+ import { setPaneOption, currentTmux, inTmux, installMenuBinding } from '../../core/runtime/placement.js';
27
+ import { climbRoot, visibleWidth } from '../../core/canvas/nav-model.js';
28
28
  import { ChatView } from './chat-view.js';
29
29
  import { InputController } from './input-controller.js';
30
+ import { writeClipboardText } from './clipboard-text.js';
30
31
  import { buildCanvasPanelLines } from './canvas-panels.js';
31
32
  import { GraphOverlay } from './graph-overlay.js';
32
33
  import { slashCommandList } from './slash-commands.js';
33
- import { applyTheme, createKeybindingsManager } from './config-load.js';
34
+ import { applyTheme, attachPalette, createKeybindingsManager, defaultAgentDir, mirrorKeybindingsToEditor, mirrorKittyProtocolToEditor, } from './config-load.js';
35
+ import { buildLoginPicker, buildLogoutPicker } from './auth-pickers.js';
36
+ import { TitledEditor, thinkingBorderColor, thinkingTitleStyle, defaultTitleStyle } from './titled-editor.js';
37
+ import { fetchGitInfo } from './git-info.js';
34
38
  import { BrokerUnavailableError, ViewSocketClient, reconnectShouldGiveUp } from './view-socket.js';
35
39
  /** Async per-node ask-map fetch (NON-blocking — the viewer must never block its
36
40
  * input pump on a shell-out, unlike canvas-nav's execFileSync). Buckets a whole
@@ -122,22 +126,47 @@ async function runAttach(nodeId, observer) {
122
126
  // 1. Theme + keybindings FIRST — pi's components throw "Theme not initialized"
123
127
  // otherwise (T5 contract). One KeybindingsManager feeds BOTH editor + input.
124
128
  applyTheme({ cwd: meta.cwd });
129
+ const pal = attachPalette();
125
130
  const km = createKeybindingsManager();
131
+ // pi-coding-agent's CustomEditor may resolve a SEPARATE pi-tui instance whose
132
+ // global keybindings we'd otherwise never set — mirror the manager onto it so
133
+ // the editor honors the user's bindings (e.g. alt+enter → newline). No-op on a
134
+ // deduped install. Awaited before tui.start so the editor never handles a key
135
+ // against stale defaults.
136
+ await mirrorKeybindingsToEditor(km);
137
+ // The front door installs the Alt+C menu binding before it ever loads a theme,
138
+ // so it lands unstyled. Now that this viewer process HAS themed (applyTheme
139
+ // above populated the live-theme global), re-install it so the menu picks up
140
+ // the distinct-surface background. Idempotent re-bind; tmux-only, best-effort.
141
+ if (inTmux()) {
142
+ try {
143
+ installMenuBinding();
144
+ }
145
+ catch { /* best-effort */ }
146
+ }
126
147
  // 2. TUI + layout containers + editor. Top-to-bottom render order (set at
127
- // step 8): chat · badge · managers (↑ subscriber) · editor · reports
128
- // (↓ subscriptions) · footer. The badge sits in the BOTTOM-anchored chrome
148
+ // step 8): chat · rule · badge · managers (↑ subscriber) · editor (themed
149
+ // border) · reports (↓ subscriptions) · status bar. A themed DynamicBorder
150
+ // rule divides the scrolling chat from the fixed chrome below it. The badge
151
+ // sits in the BOTTOM-anchored chrome
129
152
  // stack (directly above managers), NOT as the topmost child: pi-tui anchors
130
153
  // the viewport to the cursor at the bottom, so a topmost badge scrolls off
131
154
  // into scrollback the instant the chat exceeds one screen. The badge + the
132
155
  // two canvas panels reproduce the pi-extension chrome natively (Unit Q).
133
- const tui = new TUI(new ProcessTerminal());
134
- const badge = new Container();
156
+ const procTerminal = new ProcessTerminal();
157
+ const tui = new TUI(procTerminal);
135
158
  const chatContainer = new Container();
159
+ const chrome = new Container(); // a themed rule dividing scrolling chat from the fixed chrome below
136
160
  const managers = new Container();
137
161
  const reports = new Container();
162
+ const queued = new Container(); // pending steering/follow-up messages, above the editor
163
+ const pickerPanel = new Container(); // native pickers (/model etc.), inline directly under the editor
138
164
  const footer = new Container();
139
- const editorTheme = { borderColor: (s) => s, selectList: getSelectListTheme() };
140
- const editor = new CustomEditor(tui, editorTheme, km, { paddingX: 1 });
165
+ chrome.addChild(new DynamicBorder(pal.border));
166
+ const editorTheme = { borderColor: pal.border, selectList: getSelectListTheme() };
167
+ // TitledEditor paints the session name into its top border (solid-background
168
+ // chip) and tracks a thinking-level border color, both set from live state below.
169
+ const editor = new TitledEditor(tui, editorTheme, km, { paddingX: 1 });
141
170
  // Slash-command autocomplete: builtins + native canvas commands now; enriched
142
171
  // with the broker's engine/extension/skill commands on the get_commands ack.
143
172
  const setCommands = (commands) => {
@@ -147,47 +176,80 @@ async function runAttach(nodeId, observer) {
147
176
  // 3. Render layer (T5). ChatView owns the chat scroll + activity spinner; the
148
177
  // footer/status line below is ours, so onFooterEvent is left unset (footer
149
178
  // updates flow through handleBrokerFrame's single update path instead).
150
- const chatView = new ChatView(tui, chatContainer, { cwd: meta.cwd });
179
+ const chatView = new ChatView(tui, chatContainer, { cwd: meta.cwd, palette: pal });
151
180
  // 4. Footer/status line — surfaces role + live state + transient notices.
152
181
  const clientId = randomUUID();
153
182
  let role = observer ? 'observer' : 'controller';
183
+ let brokerAgentDir;
154
184
  let liveState;
155
185
  let notice = '';
186
+ // Live context occupancy for the footer: `contextTokens` is the prompt-token
187
+ // count of the most recent assistant turn (input + cacheRead + cacheWrite =
188
+ // everything sent to the model, since with prompt caching bare `input` is just
189
+ // the uncached slice and badly understates the context), read off the relayed
190
+ // message_end stream. It rides data the viewer already receives, so the
191
+ // indicator needs no broker/protocol change.
192
+ let contextTokens;
193
+ const formatTokens = (n) => n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k` : `${n}`;
194
+ // A themed, aligned status bar: live state on the LEFT (role · model · streaming
195
+ // · context · queued), the transient notice pushed to the RIGHT edge — not jammed inline.
156
196
  const renderFooter = () => {
157
- const bits = [role === 'controller' ? 'drive' : 'read-only'];
197
+ const segs = [
198
+ role === 'controller' ? pal.active('● drive') : pal.muted('○ read-only'),
199
+ ];
158
200
  if (liveState?.model)
159
- bits.push(liveState.model);
201
+ segs.push(pal.info(liveState.model));
160
202
  if (liveState?.isStreaming)
161
- bits.push('streaming');
162
- if (liveState && liveState.pendingMessageCount > 0)
163
- bits.push(`queued ${liveState.pendingMessageCount}`);
164
- let line = `\x1b[2m${bits.join(' · ')}\x1b[22m`;
165
- if (notice)
166
- line += ` \x1b[33m${notice}\x1b[39m`;
203
+ segs.push(pal.active('streaming'));
204
+ if (contextTokens !== undefined) {
205
+ segs.push(pal.muted(`${formatTokens(contextTokens)} ctx`));
206
+ }
207
+ if (liveState && liveState.pendingMessageCount > 0) {
208
+ segs.push(pal.muted(`queued ${liveState.pendingMessageCount}`));
209
+ }
210
+ const left = segs.join(pal.muted(' · '));
211
+ const right = notice ? pal.warning(`⚠ ${notice}`) : '';
212
+ // Text(_, 1, 0) pads 1 col each side, so the usable span is columns - 2.
213
+ const span = Math.max(1, (process.stdout.columns ?? 80) - 2);
214
+ const rightW = visibleWidth(right);
215
+ // Truncate the left segment (ANSI-aware) so a long model/notice never wraps;
216
+ // the right-aligned notice keeps its slot.
217
+ const maxLeft = right ? Math.max(0, span - rightW - 1) : span;
218
+ const leftFit = visibleWidth(left) > maxLeft ? truncateToWidth(left, maxLeft, '…') : left;
219
+ const gap = Math.max(right ? 1 : 0, span - visibleWidth(leftFit) - rightW);
220
+ const line = right ? leftFit + ' '.repeat(gap) + right : leftFit;
167
221
  footer.clear();
168
222
  footer.addChild(new Text(line, 1, 0));
169
223
  tui.requestRender();
170
224
  };
171
- const setNotice = (msg) => {
225
+ // Transient notices (errors, command feedback) auto-clear after a few seconds so
226
+ // a stale "error: …" doesn't linger in the footer forever. `sticky` keeps a notice
227
+ // up until explicitly replaced (used by the reconnect indicator, which is cleared
228
+ // by hand once the socket redials).
229
+ const NOTICE_CLEAR_MS = 3_000;
230
+ let noticeTimer;
231
+ const setNotice = (msg, opts) => {
172
232
  notice = msg;
233
+ if (noticeTimer !== undefined) {
234
+ clearTimeout(noticeTimer);
235
+ noticeTimer = undefined;
236
+ }
237
+ if (msg && !opts?.sticky) {
238
+ noticeTimer = setTimeout(() => {
239
+ noticeTimer = undefined;
240
+ notice = '';
241
+ renderFooter();
242
+ }, NOTICE_CLEAR_MS);
243
+ }
173
244
  renderFooter();
174
245
  };
175
- // Name badge — the session name (= the canvas editor label `kind (mode)
176
- // name`), relayed over the socket and patched live (Feature 1). Rendered just
177
- // above the managers line in the bottom chrome so it stays in-viewport.
178
- const renderBadge = () => {
179
- badge.clear();
180
- if (liveState?.sessionName)
181
- badge.addChild(new Text(`\x1b[1m${liveState.sessionName}\x1b[22m`, 1, 0));
182
- tui.requestRender();
183
- };
184
246
  // Subscribed-node panel — manager line above the editor, live reports below,
185
247
  // read straight from canvas.db via nav-model (Feature 2). asksMap is refreshed
186
248
  // by the low-rate poll below; beginFrame() inside buildCanvasPanelLines memoizes
187
249
  // the db reads for each rebuild.
188
250
  let asksMap = {};
189
251
  const renderPanels = () => {
190
- const { managers: mLines, reports: rLines } = buildCanvasPanelLines(nodeId, asksMap);
252
+ const { managers: mLines, reports: rLines } = buildCanvasPanelLines(nodeId, asksMap, pal);
191
253
  managers.clear();
192
254
  for (const l of mLines)
193
255
  managers.addChild(new Text(l, 1, 0));
@@ -196,24 +258,109 @@ async function runAttach(nodeId, observer) {
196
258
  reports.addChild(new Text(l, 1, 0));
197
259
  tui.requestRender();
198
260
  };
199
- // The alt+g GRAPH overlay (Feature 3) — a full-screen navigator over the same
200
- // nav-model graph, reading the live asksMap.
201
- const graphOverlay = new GraphOverlay(tui, nodeId, () => asksMap);
261
+ // The alt+g GRAPH overlay (Feature 3) — a bounded, centered modal navigator
262
+ // over the same nav-model graph, reading the live asksMap.
263
+ const graphOverlay = new GraphOverlay(tui, nodeId, () => asksMap, pal);
202
264
  // Refresh all canvas chrome (panels + overlay if open) from canvas.db.
203
265
  const refreshChrome = () => {
204
266
  renderPanels();
205
267
  graphOverlay.refresh();
206
268
  };
269
+ // Git context painted into the RIGHT of the editor's top border: cwd leaf ·
270
+ // branch · status symbols (● dirty, ⇡ ahead, ⇣ behind). Refreshed by the poll.
271
+ const renderGitInfo = (info) => {
272
+ const parts = [pal.muted(info.dir)];
273
+ if (info.branch)
274
+ parts.push(pal.info(`⎇ ${info.branch}`));
275
+ const sym = [];
276
+ if (info.dirty)
277
+ sym.push(pal.warning('●'));
278
+ if (info.ahead > 0)
279
+ sym.push(pal.active(`⇡${info.ahead}`));
280
+ if (info.behind > 0)
281
+ sym.push(pal.active(`⇣${info.behind}`));
282
+ if (sym.length > 0)
283
+ parts.push(sym.join(''));
284
+ // A trailing space keeps the symbols off the very last border cell.
285
+ editor.info = ` ${parts.join(pal.muted(' · '))} `;
286
+ tui.requestRender();
287
+ };
288
+ const pollGitInfo = () => fetchGitInfo(meta.cwd, renderGitInfo);
289
+ // Queued steering/follow-up messages, shown above the editor so you can SEE
290
+ // what is waiting (not just the footer count). Texts arrive on `queue_update`;
291
+ // the welcome snapshot carries only a count, so the panel fills from the first
292
+ // queue change after attach.
293
+ let queuedMessages = [];
294
+ const renderQueued = () => {
295
+ queued.clear();
296
+ if (queuedMessages.length > 0) {
297
+ const span = Math.max(1, (process.stdout.columns ?? 80) - 4);
298
+ queued.addChild(new Text(pal.muted(`queued (${queuedMessages.length}):`), 1, 0));
299
+ for (const msg of queuedMessages) {
300
+ const oneLine = msg.replace(/\s+/g, ' ').trim();
301
+ queued.addChild(new Text(pal.faint(` ⋯ ${truncateToWidth(oneLine, span, '…')}`), 1, 0));
302
+ }
303
+ }
304
+ tui.requestRender();
305
+ };
207
306
  // 5. Input layer (T6). onCommand/onDialogResponse → socket; onNotice → footer;
208
307
  // nodeId/onGraph feed the slash context (/promote targets this node, /graph
209
- // toggles the overlay).
308
+ // toggles the overlay); onRequest is the correlated read-op channel the
309
+ // native pickers ride on.
210
310
  const input = new InputController(tui, editor, km, {
211
311
  onCommand: (frame) => socket.send(frame),
212
312
  onDialogResponse: (resp) => socket.send(resp),
213
313
  onNotice: (msg) => setNotice(msg),
214
314
  nodeId,
215
315
  onGraph: () => graphOverlay.toggle(),
316
+ // `/quit` — detach this viewer (the engine keeps running). Must tear down
317
+ // locally; a bare `bye` would just trigger the reconnect loop.
318
+ onQuit: () => teardown('detach'),
319
+ // `/copy` — last assistant message → system clipboard.
320
+ onCopy: () => copyLastAssistant(),
321
+ // Global display toggles (Ctrl+O tools / Ctrl+T thinking) — pure render state
322
+ // owned by ChatView, surfaced as a footer notice mirroring pi's status line.
323
+ onToggleToolsExpand: () => setNotice(chatView.toggleToolsExpanded() ? 'Tool output: expanded' : 'Tool output: collapsed'),
324
+ onToggleThinking: () => setNotice(chatView.toggleThinking() ? 'Thinking blocks: hidden' : 'Thinking blocks: visible'),
325
+ // Wire the correlated read-op channel so the native pickers (/model,
326
+ // /resume, /fork, /tree, /settings, /scoped-models) can fetch their payloads.
327
+ // Without this every picker degrades to the "isn't available in this viewer"
328
+ // notice (see slash-commands.ts + InputController.openPicker).
329
+ onRequest: (frame) => socket.request(frame),
330
+ // Mount the native pickers inline in the chrome stack, directly under the
331
+ // editor (queued-panel pattern) — not as a floating centered modal — so a
332
+ // picker reads as part of the editor, like pi's own selectors.
333
+ onMountPicker: (component) => {
334
+ pickerPanel.addChild(component);
335
+ tui.requestRender();
336
+ },
337
+ onUnmountPicker: () => {
338
+ pickerPanel.clear();
339
+ tui.requestRender();
340
+ },
341
+ // Auth pickers — viewer-local OAuth/API-key flows. Auth operations target the
342
+ // SAME auth.json the broker uses (broker + viewer share a filesystem in the
343
+ // tmux-local attach model), so these run entirely in the viewer process and
344
+ // then notify the broker to reload via a reload_auth frame.
345
+ openLoginPicker: () => {
346
+ const dir = brokerAgentDir ?? defaultAgentDir();
347
+ input.openAuthPicker((controls) => buildLoginPicker(tui, dir, () => socket.send({ type: 'reload_auth' }), (m) => setNotice(m), controls));
348
+ },
349
+ openLogoutPicker: () => {
350
+ const dir = brokerAgentDir ?? defaultAgentDir();
351
+ input.openAuthPicker((controls) => buildLogoutPicker(dir, () => socket.send({ type: 'reload_auth' }), (m) => setNotice(m), controls.close));
352
+ },
216
353
  });
354
+ // `/copy` — copy the last assistant message (held by ChatView, kept current
355
+ // from both the welcome snapshot and the live stream) to the system clipboard.
356
+ const copyLastAssistant = () => {
357
+ const text = chatView.getLastAssistantText();
358
+ if (!text) {
359
+ setNotice('Nothing to copy — no assistant message yet');
360
+ return;
361
+ }
362
+ setNotice(writeClipboardText(text) ? 'Copied last message to clipboard' : 'Copy failed — no clipboard tool found');
363
+ };
217
364
  // Feed fresh state into the input controller AND the footer. Steer-vs-prompt
218
365
  // routing (T6) reads `state.isStreaming`, so this must stay current — the
219
366
  // broker only ships full state in `welcome`, so we patch isStreaming/name/etc
@@ -221,8 +368,14 @@ async function runAttach(nodeId, observer) {
221
368
  const setLiveState = (state) => {
222
369
  liveState = state;
223
370
  input.setState(state);
371
+ // The session name lives in the editor's top border (solid chip); the border
372
+ // color tracks the agent's thinking level. Both follow live state.
373
+ editor.title = state.sessionName ? `⬢ ${state.sessionName}` : '';
374
+ editor.borderColor = thinkingBorderColor(state.thinkingLevel, pal.border);
375
+ // Title chip background = the same thinking-level color (bold white text), so
376
+ // the name chip and the border rule read as one hue.
377
+ editor.titleStyle = thinkingTitleStyle(state.thinkingLevel, defaultTitleStyle);
224
378
  renderFooter();
225
- renderBadge();
226
379
  // Panel liveness rides the relayed event stream (agent_start/agent_end/
227
380
  // queue_update/session_info_changed all patch state through here) plus the
228
381
  // low-rate poll below for changes that emit no event (child status flips).
@@ -238,7 +391,12 @@ async function runAttach(nodeId, observer) {
238
391
  switch (frame.type) {
239
392
  case 'welcome': {
240
393
  role = frame.role;
394
+ brokerAgentDir = frame.agentDir;
241
395
  chatView.applySnapshot(frame.snapshot);
396
+ // Seed the footer's context indicator from the catch-up snapshot.
397
+ const cu = frame.snapshot.stats.contextUsage;
398
+ if (cu && cu.tokens != null)
399
+ contextTokens = cu.tokens;
242
400
  setLiveState(frame.snapshot.state);
243
401
  // Ask the broker for its merged command list (engine/extension/skill
244
402
  // commands) to enrich slash autocomplete beyond the builtins+canvas set.
@@ -254,6 +412,12 @@ async function runAttach(nodeId, observer) {
254
412
  renderFooter();
255
413
  break;
256
414
  }
415
+ case 'model_changed': {
416
+ // The broker's own post-set_model/cycle_model announcement (pi emits no
417
+ // engine event for a model switch) — keeps the footer's model current.
418
+ patchState({ model: frame.model });
419
+ break;
420
+ }
257
421
  case 'error': {
258
422
  if (frame.code === 'not_controller') {
259
423
  setNotice('read-only — another viewer is the controller');
@@ -277,9 +441,38 @@ async function runAttach(nodeId, observer) {
277
441
  }
278
442
  break;
279
443
  }
444
+ // navigate_tree's detail is the navigated-to user message's text (pi
445
+ // parity: the tree navigator restores it to the editor for re-editing);
446
+ // the rewound transcript itself arrives via the broker's re-welcome.
447
+ if (frame.for === 'navigate_tree' && frame.ok) {
448
+ if (frame.detail) {
449
+ editor.setText(frame.detail);
450
+ tui.requestRender();
451
+ }
452
+ break;
453
+ }
454
+ if (frame.for === 'share') {
455
+ if (frame.ok && frame.detail) {
456
+ setNotice(`Shared: ${frame.detail}`);
457
+ writeClipboardText(frame.detail);
458
+ }
459
+ else if (!frame.ok) {
460
+ setNotice(`share failed: ${frame.detail ?? 'unknown error'}`);
461
+ }
462
+ break;
463
+ }
464
+ if (frame.for === 'clone' && !frame.ok) {
465
+ setNotice(`clone failed: ${frame.detail ?? 'unknown error'}`);
466
+ break;
467
+ }
280
468
  if (!frame.ok) {
281
469
  setNotice(`command failed: ${frame.for}${frame.detail ? ` — ${frame.detail}` : ''}`);
470
+ break;
282
471
  }
472
+ // Confirm commands whose effect is otherwise invisible — without this a
473
+ // `/reload` looks like it did nothing (the engine reloads silently).
474
+ if (frame.for === 'reload')
475
+ setNotice('Reloaded keybindings, extensions, skills, prompts, and themes');
283
476
  break;
284
477
  }
285
478
  case 'extension_ui_request': {
@@ -290,16 +483,27 @@ async function runAttach(nodeId, observer) {
290
483
  // A relayed AgentSessionEvent — render it, and keep local state fresh.
291
484
  const event = frame;
292
485
  chatView.handleEvent(event);
486
+ // The most recent assistant turn's prompt tokens ARE the live context
487
+ // size — read straight off the relayed message, no broker round-trip.
488
+ if (event.type === 'message_end' && event.message.role === 'assistant') {
489
+ const u = event.message.usage;
490
+ contextTokens = u.input + u.cacheRead + u.cacheWrite;
491
+ renderFooter();
492
+ }
293
493
  if (event.type === 'agent_start')
294
494
  patchState({ isStreaming: true });
295
495
  else if (event.type === 'agent_end')
296
496
  patchState({ isStreaming: false });
297
497
  else if (event.type === 'session_info_changed')
298
498
  patchState({ sessionName: event.name });
299
- else if (event.type === 'thinking_level_changed')
499
+ else if (event.type === 'thinking_level_changed') {
300
500
  patchState({ thinkingLevel: event.level });
501
+ setNotice(`Thinking level: ${event.level}`);
502
+ }
301
503
  else if (event.type === 'queue_update') {
302
- patchState({ pendingMessageCount: event.steering.length + event.followUp.length });
504
+ queuedMessages = [...event.steering, ...event.followUp];
505
+ renderQueued();
506
+ patchState({ pendingMessageCount: queuedMessages.length });
303
507
  }
304
508
  break;
305
509
  }
@@ -334,6 +538,8 @@ async function runAttach(nodeId, observer) {
334
538
  tornDown = true;
335
539
  if (asksTimer !== undefined)
336
540
  clearInterval(asksTimer);
541
+ if (noticeTimer !== undefined)
542
+ clearTimeout(noticeTimer);
337
543
  graphOverlay.close();
338
544
  clearPaneTag();
339
545
  removeKeyListener();
@@ -357,10 +563,17 @@ async function runAttach(nodeId, observer) {
357
563
  resolveDone();
358
564
  };
359
565
  // The handshake the viewer sends on connect AND on every successful redial.
566
+ // After a broker refresh / crash-revive the fresh process has no controller and
567
+ // no memory of us, so re-`hello` reclaims our role and the `welcome` snapshot
568
+ // repaints full scrollback/state. Sends the CURRENT `role`, not the constructor
569
+ // `observer` flag: if we were preempted to observer at runtime (a
570
+ // `control_changed` demoted us), re-hello'ing as 'controller' would let us
571
+ // reclaim control from the new controller after a refresh — so the demotion
572
+ // must survive the reconnect.
360
573
  const sendHello = () => {
361
574
  socket.send({
362
575
  type: 'hello',
363
- role: observer ? 'observer' : 'controller',
576
+ role,
364
577
  client_id: clientId,
365
578
  term: { cols: process.stdout.columns ?? 80, rows: process.stdout.rows ?? 24 },
366
579
  });
@@ -379,7 +592,7 @@ async function runAttach(nodeId, observer) {
379
592
  let reconnecting = false;
380
593
  const attemptReconnect = async () => {
381
594
  reconnecting = true;
382
- setNotice('↻ node refreshing — reconnecting…');
595
+ setNotice('↻ node refreshing — reconnecting…', { sticky: true });
383
596
  const deadline = Date.now() + RECONNECT_GIVEUP_MS;
384
597
  let delay = 200;
385
598
  while (!tornDown && Date.now() < deadline) {
@@ -417,14 +630,30 @@ async function runAttach(nodeId, observer) {
417
630
  return; // the dead socket's own close during the redial loop
418
631
  void attemptReconnect();
419
632
  });
420
- // ctrl+c / ctrl+d → detach (T6 left these unwired; lifecycle is ours). A
421
- // global TUI input listener fires BEFORE the focused component (and before any
422
- // dialog overlay), so detach is unconditional and consistent.
633
+ // ctrl+d → detach immediately (lifecycle is ours). ctrl+c is gentler: a single
634
+ // tap CLEARS the editor (and arms a window); a second tap within that window
635
+ // detaches. A global TUI input listener fires BEFORE the focused component (and
636
+ // before any dialog overlay), so this is consistent regardless of focus.
637
+ const CTRL_C_DETACH_MS = 1000;
638
+ let lastCtrlCAt = 0;
423
639
  const removeKeyListener = tui.addInputListener((data) => {
424
- if (matchesKey(data, 'ctrl+c') || matchesKey(data, 'ctrl+d')) {
640
+ if (matchesKey(data, 'ctrl+d')) {
425
641
  teardown('detach');
426
642
  return { consume: true };
427
643
  }
644
+ if (matchesKey(data, 'ctrl+c')) {
645
+ const now = Date.now();
646
+ if (now - lastCtrlCAt < CTRL_C_DETACH_MS) {
647
+ lastCtrlCAt = 0;
648
+ teardown('detach');
649
+ return { consume: true };
650
+ }
651
+ lastCtrlCAt = now;
652
+ editor.setText('');
653
+ setNotice('Editor cleared — press Ctrl+C again to detach');
654
+ tui.requestRender();
655
+ return { consume: true };
656
+ }
428
657
  // alt+g toggles the GRAPH overlay. The global listener fires BEFORE the
429
658
  // focused component, so this works whether the editor or the overlay holds
430
659
  // focus (closing it from inside the overlay too).
@@ -443,22 +672,29 @@ async function runAttach(nodeId, observer) {
443
672
  // 8. Lay out, focus the editor, start, then handshake. Starting before the
444
673
  // handshake means the welcome's applySnapshot renders into a live TUI.
445
674
  tui.addChild(chatContainer);
446
- tui.addChild(badge);
675
+ tui.addChild(chrome);
447
676
  tui.addChild(managers);
677
+ tui.addChild(queued);
448
678
  tui.addChild(editor);
679
+ tui.addChild(pickerPanel);
449
680
  tui.addChild(reports);
450
681
  tui.addChild(footer);
451
682
  tui.setFocus(editor);
452
683
  renderFooter();
453
- renderBadge();
454
684
  renderPanels();
685
+ renderQueued();
686
+ pollGitInfo();
455
687
  tui.start();
688
+ // ProcessTerminal negotiated the keyboard protocol on OUR pi-tui instance; the
689
+ // editor reads the flag from its own (possibly separate) instance, so mirror it.
690
+ void mirrorKittyProtocolToEditor(procTerminal.kittyProtocolActive);
456
691
  sendHello();
457
692
  // Low-rate canvas-chrome refresh: repaint panels/overlay from canvas.db (child
458
693
  // status flips emit no broker event) and re-poll the ask map asynchronously.
459
694
  const ASKS_POLL_MS = 5_000;
460
695
  const pollChrome = () => {
461
696
  refreshChrome();
697
+ pollGitInfo();
462
698
  fetchAsksAsync(climbRoot(nodeId), (counts) => {
463
699
  if (JSON.stringify(counts) !== JSON.stringify(asksMap)) {
464
700
  asksMap = counts;
@@ -0,0 +1,9 @@
1
+ import type { TUI } from '@earendil-works/pi-tui';
2
+ import type { Picker, PickerControls } from './pickers.js';
3
+ /** `/login` — show the provider selector, then run pi's native login flow in
4
+ * the viewer. On success, notifies the broker to reload auth via reload_auth.
5
+ * Returns a Picker (OAuthSelectorComponent as the overlay). */
6
+ export declare function buildLoginPicker(tui: TUI, agentDir: string, onAuthReloaded: () => void, notify: (msg: string) => void, controls: PickerControls): Picker;
7
+ /** `/logout` — show the provider selector in logout mode. On select, calls
8
+ * authStorage.logout(providerId) directly, then notifies the broker to reload. */
9
+ export declare function buildLogoutPicker(agentDir: string, onAuthReloaded: () => void, notify: (msg: string) => void, close: () => void): Picker;