tmux-ide 2.6.0 → 2.7.0

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 (271) hide show
  1. package/README.md +14 -9
  2. package/bin/cli.js +1024 -519
  3. package/bin/cli.ts +63 -6
  4. package/bunfig.toml +4 -0
  5. package/package.json +18 -7
  6. package/packages/contracts/package.json +22 -0
  7. package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
  8. package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
  9. package/packages/contracts/src/actions-contract.ts +310 -0
  10. package/packages/contracts/src/actions-errors.ts +41 -0
  11. package/packages/contracts/src/domain.ts +36 -0
  12. package/packages/contracts/src/ide-config.ts +170 -0
  13. package/packages/contracts/src/index.ts +24 -0
  14. package/packages/contracts/src/lib-internal/auth.ts +13 -0
  15. package/packages/contracts/src/lib-internal/hq.ts +38 -0
  16. package/packages/contracts/src/terminals.ts +116 -0
  17. package/packages/contracts/src/tmux.ts +60 -0
  18. package/packages/contracts/src/workspace.ts +67 -0
  19. package/packages/daemon/dist/agent-explain.d.ts +8 -1
  20. package/packages/daemon/dist/agent-explain.js +19 -3
  21. package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
  22. package/packages/daemon/dist/lib/tui-binary.js +130 -0
  23. package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
  24. package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
  25. package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
  26. package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
  27. package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
  28. package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
  29. package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
  30. package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
  31. package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
  32. package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
  33. package/packages/daemon/src/agent-explain.ts +298 -0
  34. package/packages/daemon/src/attach.ts +20 -0
  35. package/packages/daemon/src/bin.ts +4 -0
  36. package/packages/daemon/src/canonical.ts +7 -0
  37. package/packages/daemon/src/cli.ts +499 -0
  38. package/packages/daemon/src/command-center/actions/contract.ts +2 -0
  39. package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
  40. package/packages/daemon/src/command-center/actions/errors.ts +105 -0
  41. package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
  42. package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
  43. package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
  44. package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
  45. package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
  46. package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
  47. package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
  48. package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
  49. package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
  50. package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
  51. package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
  52. package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
  53. package/packages/daemon/src/command-center/actions/registry.ts +149 -0
  54. package/packages/daemon/src/command-center/discovery.ts +96 -0
  55. package/packages/daemon/src/command-center/index.ts +31 -0
  56. package/packages/daemon/src/command-center/schemas.ts +85 -0
  57. package/packages/daemon/src/command-center/server.ts +1260 -0
  58. package/packages/daemon/src/command-center/ws-events.ts +316 -0
  59. package/packages/daemon/src/config.ts +549 -0
  60. package/packages/daemon/src/detect.ts +248 -0
  61. package/packages/daemon/src/doctor.ts +242 -0
  62. package/packages/daemon/src/embed.ts +5 -0
  63. package/packages/daemon/src/index.ts +12 -0
  64. package/packages/daemon/src/init.ts +211 -0
  65. package/packages/daemon/src/inspect.ts +178 -0
  66. package/packages/daemon/src/js-yaml.d.ts +10 -0
  67. package/packages/daemon/src/launch.ts +349 -0
  68. package/packages/daemon/src/lib/active-projects.ts +49 -0
  69. package/packages/daemon/src/lib/agent-discovery.ts +121 -0
  70. package/packages/daemon/src/lib/app-config.ts +427 -0
  71. package/packages/daemon/src/lib/app-settings.ts +53 -0
  72. package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
  73. package/packages/daemon/src/lib/auth/middleware.ts +56 -0
  74. package/packages/daemon/src/lib/auth/types.ts +2 -0
  75. package/packages/daemon/src/lib/auth-token.ts +5 -0
  76. package/packages/daemon/src/lib/authorship.ts +280 -0
  77. package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
  78. package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
  79. package/packages/daemon/src/lib/daemon-embed.ts +782 -0
  80. package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
  81. package/packages/daemon/src/lib/daemon.ts +79 -0
  82. package/packages/daemon/src/lib/dot-path.ts +17 -0
  83. package/packages/daemon/src/lib/errors.ts +67 -0
  84. package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
  85. package/packages/daemon/src/lib/launch-plan.ts +90 -0
  86. package/packages/daemon/src/lib/log.ts +134 -0
  87. package/packages/daemon/src/lib/output.ts +76 -0
  88. package/packages/daemon/src/lib/project-init-runner.ts +150 -0
  89. package/packages/daemon/src/lib/project-inspect.ts +80 -0
  90. package/packages/daemon/src/lib/project-onboard.ts +149 -0
  91. package/packages/daemon/src/lib/project-probe.ts +92 -0
  92. package/packages/daemon/src/lib/project-registry.ts +296 -0
  93. package/packages/daemon/src/lib/session-monitor.ts +122 -0
  94. package/packages/daemon/src/lib/session-options.ts +100 -0
  95. package/packages/daemon/src/lib/shell.ts +8 -0
  96. package/packages/daemon/src/lib/sizes.ts +36 -0
  97. package/packages/daemon/src/lib/skill-sync.ts +155 -0
  98. package/packages/daemon/src/lib/slugify.ts +10 -0
  99. package/packages/daemon/src/lib/terminals-store.ts +125 -0
  100. package/packages/daemon/src/lib/tui-binary.ts +165 -0
  101. package/packages/daemon/src/lib/update-check.ts +298 -0
  102. package/packages/daemon/src/lib/update.ts +158 -0
  103. package/packages/daemon/src/lib/workspace-registry.ts +229 -0
  104. package/packages/daemon/src/lib/worktree.ts +289 -0
  105. package/packages/daemon/src/lib/yaml-io.ts +27 -0
  106. package/packages/daemon/src/ls.ts +40 -0
  107. package/packages/daemon/src/restart.ts +24 -0
  108. package/packages/daemon/src/restore.ts +514 -0
  109. package/packages/daemon/src/schemas/domain.ts +2 -0
  110. package/packages/daemon/src/schemas/filesystem.ts +34 -0
  111. package/packages/daemon/src/schemas/ide-config.ts +2 -0
  112. package/packages/daemon/src/schemas/index.ts +59 -0
  113. package/packages/daemon/src/schemas/inspect.ts +67 -0
  114. package/packages/daemon/src/schemas/registry.ts +55 -0
  115. package/packages/daemon/src/schemas/ws-events.ts +135 -0
  116. package/packages/daemon/src/send.ts +171 -0
  117. package/packages/daemon/src/server/README.md +15 -0
  118. package/packages/daemon/src/server/index.ts +74 -0
  119. package/packages/daemon/src/server/pty-bridge.ts +532 -0
  120. package/packages/daemon/src/server/standalone.ts +18 -0
  121. package/packages/daemon/src/server/ws-route.ts +483 -0
  122. package/packages/daemon/src/status.ts +59 -0
  123. package/packages/daemon/src/stop.ts +28 -0
  124. package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
  125. package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
  126. package/packages/daemon/src/terminal/README.md +92 -0
  127. package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
  128. package/packages/daemon/src/tui/chrome/chip.ts +30 -0
  129. package/packages/daemon/src/tui/chrome/events.ts +119 -0
  130. package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
  131. package/packages/daemon/src/tui/chrome/menu.ts +289 -0
  132. package/packages/daemon/src/tui/chrome/notify.ts +382 -0
  133. package/packages/daemon/src/tui/chrome/panels.ts +111 -0
  134. package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
  135. package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
  136. package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
  137. package/packages/daemon/src/tui/chrome/updater.ts +510 -0
  138. package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
  139. package/packages/daemon/src/tui/compiled.ts +121 -0
  140. package/packages/daemon/src/tui/detect/classify.ts +208 -0
  141. package/packages/daemon/src/tui/detect/manifest-loader.ts +193 -0
  142. package/packages/daemon/src/tui/detect/manifest.ts +199 -0
  143. package/packages/daemon/src/tui/detect/manifests.ts +354 -0
  144. package/packages/daemon/src/tui/detect/process-tree.ts +217 -0
  145. package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
  146. package/packages/daemon/src/tui/integrations/claude.ts +176 -0
  147. package/packages/daemon/src/tui/integrations/offer.ts +145 -0
  148. package/packages/daemon/src/tui/main.ts +82 -0
  149. package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
  150. package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
  151. package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
  152. package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
  153. package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
  154. package/packages/daemon/src/tui/mirror/blit.ts +186 -0
  155. package/packages/daemon/src/tui/mirror/control-client.ts +214 -0
  156. package/packages/daemon/src/tui/mirror/control.ts +97 -0
  157. package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
  158. package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
  159. package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
  160. package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
  161. package/packages/daemon/src/tui/mirror/file-tree.ts +97 -0
  162. package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
  163. package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
  164. package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
  165. package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
  166. package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
  167. package/packages/daemon/src/tui/mirror/palette.ts +274 -0
  168. package/packages/daemon/src/tui/mirror/pane-mirror.ts +561 -0
  169. package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
  170. package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -0
  171. package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
  172. package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
  173. package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
  174. package/packages/daemon/src/tui/mirror/selection.ts +262 -0
  175. package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
  176. package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
  177. package/packages/daemon/src/tui/mirror/size-truth.ts +77 -0
  178. package/packages/daemon/src/tui/mirror/spans.ts +46 -0
  179. package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
  180. package/packages/daemon/src/tui/team/CONTROL.md +50 -0
  181. package/packages/daemon/src/tui/team/entry.ts +38 -0
  182. package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
  183. package/packages/daemon/src/tui/team/home.ts +170 -0
  184. package/packages/daemon/src/tui/team/index.tsx +1521 -0
  185. package/packages/daemon/src/tui/team/input.ts +34 -0
  186. package/packages/daemon/src/tui/team/keymap.ts +127 -0
  187. package/packages/daemon/src/tui/team/mouse.ts +29 -0
  188. package/packages/daemon/src/tui/team/nav.ts +31 -0
  189. package/packages/daemon/src/tui/team/preview.ts +34 -0
  190. package/packages/daemon/src/tui/team/projects.ts +191 -0
  191. package/packages/daemon/src/tui/team/report.ts +83 -0
  192. package/packages/daemon/src/tui/team/sessions.ts +433 -0
  193. package/packages/daemon/src/tui/team/tree.ts +62 -0
  194. package/packages/daemon/src/types.ts +13 -0
  195. package/packages/daemon/src/ui/index.ts +32 -0
  196. package/packages/daemon/src/ui/terminal/index.ts +9 -0
  197. package/packages/daemon/src/ui/types.ts +91 -0
  198. package/packages/daemon/src/ui/web/base.css +80 -0
  199. package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
  200. package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
  201. package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
  202. package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
  203. package/packages/daemon/src/ui/web/hooks.ts +106 -0
  204. package/packages/daemon/src/ui/web/index.ts +27 -0
  205. package/packages/daemon/src/ui/web/render.ts +77 -0
  206. package/packages/daemon/src/ui/web/utils/color.ts +27 -0
  207. package/packages/daemon/src/validate.ts +217 -0
  208. package/packages/daemon/src/widgets/changes/README.md +3 -0
  209. package/packages/daemon/src/widgets/changes/index.tsx +691 -0
  210. package/packages/daemon/src/widgets/config/README.md +3 -0
  211. package/packages/daemon/src/widgets/config/index.tsx +481 -0
  212. package/packages/daemon/src/widgets/explorer/README.md +3 -0
  213. package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
  214. package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
  215. package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
  216. package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
  217. package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
  218. package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
  219. package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
  220. package/packages/daemon/src/widgets/lib/files.ts +88 -0
  221. package/packages/daemon/src/widgets/lib/git.ts +88 -0
  222. package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
  223. package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
  224. package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
  225. package/packages/daemon/src/widgets/lib/theme.ts +194 -0
  226. package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
  227. package/packages/daemon/src/widgets/preview/README.md +3 -0
  228. package/packages/daemon/src/widgets/preview/index.tsx +416 -0
  229. package/packages/daemon/src/widgets/resolve.ts +121 -0
  230. package/packages/daemon/src/widgets/setup/README.md +3 -0
  231. package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
  232. package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
  233. package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
  234. package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
  235. package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
  236. package/packages/daemon/src/widgets/setup/index.tsx +341 -0
  237. package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
  238. package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
  239. package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
  240. package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
  241. package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
  242. package/packages/tmux-bridge/package.json +22 -0
  243. package/packages/tmux-bridge/src/errors.ts +28 -0
  244. package/packages/tmux-bridge/src/index.ts +31 -0
  245. package/packages/tmux-bridge/src/monitor.ts +77 -0
  246. package/packages/tmux-bridge/src/panes.ts +136 -0
  247. package/packages/tmux-bridge/src/runner.test.ts +501 -0
  248. package/packages/tmux-bridge/src/runner.ts +91 -0
  249. package/packages/tmux-bridge/src/sessions.ts +126 -0
  250. package/packages/tmux-bridge/src/targeting.test.ts +107 -0
  251. package/packages/tmux-bridge/src/targeting.ts +90 -0
  252. package/scripts/build-tui.mjs +11 -4
  253. package/scripts/perf-mirror.mjs +313 -0
  254. package/scripts/postinstall.js +26 -2
  255. package/skill/SKILL.md +22 -0
  256. package/templates/AGENTS.md +14 -7
  257. package/templates/agent-team-monorepo.yml +8 -0
  258. package/templates/agent-team-nextjs.yml +8 -0
  259. package/templates/agent-team.yml +10 -0
  260. package/templates/convex.yml +2 -0
  261. package/templates/default.yml +11 -5
  262. package/templates/go.yml +4 -0
  263. package/templates/missions.yml +6 -0
  264. package/templates/nextjs.yml +4 -0
  265. package/templates/python.yml +4 -0
  266. package/templates/skills/backend.md +5 -12
  267. package/templates/skills/frontend.md +5 -12
  268. package/templates/skills/general-worker.md +5 -12
  269. package/templates/skills/researcher.md +7 -12
  270. package/templates/skills/reviewer.md +7 -16
  271. package/templates/vite.yml +4 -0
@@ -0,0 +1,260 @@
1
+ /**
2
+ * The tmux-ide cheat-sheet — an iPadOS-style "all the keys" command sheet.
3
+ *
4
+ * Pressing `⌥k` (or clicking the `[ ? keys ]` trigger in the chrome row) on any
5
+ * adopted session floats a `display-popup` running `tmux-ide cheatsheet`, which
6
+ * prints this grouped, styled sheet and then blocks until ANY key closes it.
7
+ *
8
+ * The sheet is STATIC content, so it renders as a plain ANSI CLI print — no
9
+ * OpenTUI/bun boot — which keeps the popup instant. To stop the sheet drifting
10
+ * from reality, its dynamic groups are SOURCED from live inputs: the configured
11
+ * key binds + theme tokens (`keys`/`theme`, defaulting to the app-config
12
+ * defaults) and the team app's `DEFAULT_KEYMAP`/`ACTION_ORDER`. Only the
13
+ * inherently-static tmux essentials group is hardcoded.
14
+ *
15
+ * `buildCheatsheet` and the bind/unbind command builders are PURE (tested); the
16
+ * CLI `cheatsheet` command wires the io (print + wait-for-key).
17
+ */
18
+ import { DEFAULT_KEYS, DEFAULT_THEME, type AppKeys, type AppTheme } from "../../lib/app-config.ts";
19
+ import { ACTION_ORDER, DEFAULT_KEYMAP } from "../team/keymap.ts";
20
+ import { GRAMMAR_HELP } from "../../widgets/lib/grammar.ts";
21
+ import { PANEL_POPUPS, panelKey } from "./panels.ts";
22
+
23
+ /**
24
+ * The root-table key that floats this sheet: `M-k` (Alt+k). Like the switcher's
25
+ * popup key it lives in the ROOT table so it fires without the prefix from any adopted
26
+ * session, and Alt-letter keys are low-collision in terminal apps. Chosen to sit
27
+ * next to `M-p` (switcher) so the two chrome popups share an ergonomic home.
28
+ */
29
+ export const CHEATSHEET_KEY = "M-k";
30
+
31
+ // --- ANSI styling — the CLI's bold/cyan/dim pattern (see bin/cli.ts). ---
32
+ const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
33
+ const dim = (s: string) => `\x1b[2m${s}\x1b[22m`;
34
+ const cyan = (s: string) => `\x1b[36m${s}\x1b[39m`;
35
+ const head = (s: string) => `\x1b[1;36m${s}\x1b[0m`;
36
+ const color = (code: number, s: string) => `\x1b[38;5;${code}m${s}\x1b[39m`;
37
+
38
+ /**
39
+ * The 256-color index of a `colourN` theme token for the ANSI legend, or null
40
+ * for a non-`colourN` token (e.g. a hex color) — the caller then renders it dim
41
+ * rather than mis-mapping the code.
42
+ */
43
+ function tokenCode(token: string): number | null {
44
+ const m = /^colou?r(\d+)$/.exec(token);
45
+ if (!m) return null;
46
+ const n = Number(m[1]);
47
+ return Number.isInteger(n) && n >= 0 && n <= 255 ? n : null;
48
+ }
49
+
50
+ /** Render a glyph in a theme status token's color for the legend (dim fallback). */
51
+ function legendMark(token: string, glyph: string): string {
52
+ const code = tokenCode(token);
53
+ return code === null ? dim(glyph) : color(code, glyph);
54
+ }
55
+
56
+ /**
57
+ * Render a tmux key name for humans: `M-` → `⌥`, `C-` → `^`, `S-` → `⇧`. Keeps
58
+ * the sheet's key hints sourced from the real `M-…` constants instead of a
59
+ * separately-maintained pretty string.
60
+ */
61
+ function renderKey(tmuxKey: string): string {
62
+ return tmuxKey.replace(/M-/g, "⌥").replace(/C-/g, "^").replace(/S-/g, "⇧");
63
+ }
64
+
65
+ /**
66
+ * Clip a styled line to `width` VISIBLE columns, passing ANSI escapes through
67
+ * untouched (they cost no width) and closing any open style with a reset so a
68
+ * mid-style cut can't bleed into the next line. Guarantees the sheet respects a
69
+ * narrow popup without mangling color codes.
70
+ */
71
+ function clip(line: string, width: number): string {
72
+ let out = "";
73
+ let visible = 0;
74
+ let i = 0;
75
+ while (i < line.length) {
76
+ if (line[i] === "\x1b") {
77
+ // eslint-disable-next-line no-control-regex -- matching the ANSI SGR escape
78
+ const m = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
79
+ if (m) {
80
+ out += m[0];
81
+ i += m[0].length;
82
+ continue;
83
+ }
84
+ }
85
+ if (visible >= width) break;
86
+ out += line[i];
87
+ visible++;
88
+ i++;
89
+ }
90
+ return `${out}\x1b[0m`;
91
+ }
92
+
93
+ /** Visible-width of a styled string (ANSI escapes stripped). */
94
+ function visibleWidth(s: string): number {
95
+ // eslint-disable-next-line no-control-regex -- stripping ANSI SGR escapes
96
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
97
+ }
98
+
99
+ /**
100
+ * Build the full ANSI cheat sheet, one screenful for a ~100x28 popup.
101
+ *
102
+ * Groups (each under a bold-cyan heading):
103
+ * 1. dock — the ⌥p / ⌥k popups, the clickable bar, and the glyph legend.
104
+ * 2. picker — the keys live INSIDE the ⌥p switcher popup.
105
+ * 3. team app — DERIVED from ACTION_ORDER/DEFAULT_KEYMAP (two columns if the
106
+ * width allows), so it never drifts from the real keymap.
107
+ * 4. tmux — the prefix essentials (inherently static → hardcoded).
108
+ * 5. cli — the handful of scriptable commands worth remembering.
109
+ *
110
+ * Every line is clipped to `width` so nothing overflows a narrow popup.
111
+ */
112
+ export function buildCheatsheet(opts: { width: number; keys?: AppKeys; theme?: AppTheme }): string {
113
+ const width = Math.max(20, opts.width);
114
+ const keys = opts.keys ?? DEFAULT_KEYS;
115
+ const theme = opts.theme ?? DEFAULT_THEME;
116
+ const lines: string[] = [];
117
+ const pad = (s: string) => ` ${s}`; // two-space indent under each heading
118
+
119
+ // Title.
120
+ lines.push(`${head(" tmux-ide")} ${dim("cheat sheet — press any key to close")}`);
121
+ lines.push("");
122
+
123
+ // 1. dock — sourced key hints from the configured keys.
124
+ lines.push(head("dock"));
125
+ lines.push(
126
+ pad(
127
+ `${bold(renderKey(keys.home))} home cockpit ${bold(renderKey(keys.popup))} switcher popup ${bold(renderKey(keys.cheatsheet))} this sheet ${bold(renderKey(keys.menu))} actions menu ${bold(renderKey(keys.sidebar))} sidebar`,
128
+ ),
129
+ );
130
+ lines.push(
131
+ pad(
132
+ dim(
133
+ `bar: click a project tab = switch there · [ ⌂ home ${renderKey(keys.home)} ] = home · [ ⧉ switch ${renderKey(keys.popup)} ] = switcher · right-click anywhere = menu`,
134
+ ),
135
+ ),
136
+ );
137
+ const active = theme.glyphs.active;
138
+ const legend =
139
+ `${legendMark(theme.status.blocked, active)} blocked ${legendMark(theme.status.working, active)} working ` +
140
+ `${legendMark(theme.status.done, active)} done ${legendMark(theme.status.idle, active)} idle ` +
141
+ `${dim("·")} unknown ${dim(theme.glyphs.inactive)} stopped`;
142
+ lines.push(pad(legend));
143
+ lines.push("");
144
+
145
+ // 1a. prefix twins — the RELIABLE bindings (the tmux prefix survives every
146
+ // keyboard protocol; Alt keys don't always). Letters pinned against
147
+ // prefixKeyBinds by a statusline test so they can't drift.
148
+ lines.push(head("prefix keys (always work)"));
149
+ lines.push(
150
+ pad(
151
+ `${bold("prefix h")} home ${bold("prefix j")} switcher ${bold("prefix k")} keys ${bold("prefix u")} menu ${bold("prefix b")} sidebar ${bold("prefix e")} files ${bold("prefix g")} changes ${bold("prefix v")} config`,
152
+ ),
153
+ );
154
+ lines.push(
155
+ pad(dim(`prefix = your tmux prefix (usually C-b) — use these when Alt keys don't reach tmux`)),
156
+ );
157
+ lines.push("");
158
+
159
+ // 1b. panels — every widget floats as a popup, one key away, esc to close.
160
+ lines.push(head("panels"));
161
+ const panelHints = PANEL_POPUPS.map(
162
+ (p) => `${bold(renderKey(panelKey(p, keys.panels)))} ${p.label}`,
163
+ ).join(" ");
164
+ lines.push(pad(`${panelHints} ${dim("esc/q closes any panel")}`));
165
+ lines.push("");
166
+
167
+ // 1c. grammar — the ONE interaction grammar every panel + the sidebar share,
168
+ // sourced from GRAMMAR_HELP so it can never drift from the widgets.
169
+ lines.push(head("in panels & sidebar"));
170
+ const gKeyW = Math.max(...GRAMMAR_HELP.map((r) => r.keys.length));
171
+ for (const row of GRAMMAR_HELP) {
172
+ lines.push(pad(`${bold(row.keys.padEnd(gKeyW))} ${dim(row.label)}`));
173
+ }
174
+ lines.push("");
175
+
176
+ // 2. picker — keys inside the switcher popup.
177
+ lines.push(head(`picker ${dim(`(inside the ${renderKey(keys.popup)} popup)`)}`));
178
+ lines.push(
179
+ pad(`${bold("↵")} switch ${bold("l")} launch ${bold("/")} find ${bold("esc")} close`),
180
+ );
181
+ lines.push("");
182
+
183
+ // 3. team app — derived from the real keymap, two columns when it fits.
184
+ lines.push(head("team app"));
185
+ const cells = ACTION_ORDER.map((action) => {
186
+ const binding = DEFAULT_KEYMAP[action];
187
+ return { keys: binding.keys.join("/"), desc: binding.description };
188
+ });
189
+ const keyW = Math.max(...cells.map((c) => c.keys.length));
190
+ const descW = Math.max(...cells.map((c) => c.desc.length));
191
+ const cellW = keyW + 2 + descW; // "keys" + gap + "desc" visible width
192
+ const renderCell = (c: { keys: string; desc: string }): string => {
193
+ const text = `${bold(c.keys.padEnd(keyW))} ${dim(c.desc)}`;
194
+ return text + " ".repeat(Math.max(0, cellW - visibleWidth(text)));
195
+ };
196
+ const twoCols = width >= cellW * 2 + 4;
197
+ if (twoCols) {
198
+ const half = Math.ceil(cells.length / 2);
199
+ for (let i = 0; i < half; i++) {
200
+ const left = cells[i];
201
+ const right = cells[i + half];
202
+ const rendered = left ? renderCell(left) : "";
203
+ lines.push(pad(right ? `${rendered} ${renderCell(right)}` : rendered));
204
+ }
205
+ } else {
206
+ for (const c of cells) lines.push(pad(renderCell(c)));
207
+ }
208
+ lines.push("");
209
+
210
+ // 4. tmux essentials — static.
211
+ lines.push(head("tmux essentials"));
212
+ lines.push(
213
+ pad(
214
+ `${bold("prefix d")} detach ${bold("prefix z")} zoom pane ${bold("prefix [")} copy mode`,
215
+ ),
216
+ );
217
+ lines.push(
218
+ pad(
219
+ `${bold("prefix c")} new window ${bold("prefix n/p")} next/prev ${bold('prefix % / "')} splits`,
220
+ ),
221
+ );
222
+ lines.push("");
223
+
224
+ // 5. cli — scriptable commands.
225
+ lines.push(head("cli"));
226
+ lines.push(pad(cyan("tmux-ide team --json")));
227
+ lines.push(pad(cyan("tmux-ide wait agent-status <s> --status done")));
228
+ lines.push(pad(cyan("tmux-ide adopt/unadopt <session>")));
229
+ lines.push(pad(cyan("tmux-ide worktree create <branch>") + dim(" (⎇ in the menu)")));
230
+
231
+ return lines.map((line) => clip(line, width)).join("\n");
232
+ }
233
+
234
+ /**
235
+ * PURE — the `display-popup` command STRING that floats the cheat sheet (shared
236
+ * by the M-k bind, the bar's `[ ? keys ]` left-click router, and the actions
237
+ * menu's "Cheat sheet" item, so all three open an identical popup). Mirror of the
238
+ * sizing in {@link cheatsheetBindCommand}.
239
+ */
240
+ export function cheatsheetPopupCommand(cheatsheetCmd = "tmux-ide cheatsheet"): string {
241
+ return `display-popup -E -w 90% -h 80% "${cheatsheetCmd}"`;
242
+ }
243
+
244
+ /**
245
+ * PURE — the tmux argv that binds {@link CHEATSHEET_KEY}: `M-k` opens a
246
+ * `display-popup` running the cheat sheet (which waits for any key, then exits,
247
+ * closing the popup). Server-wide root-table bind, mirroring the switcher's
248
+ * popup key — see the note on `unadoptSession`.
249
+ */
250
+ export function cheatsheetBindCommand(
251
+ cheatsheetCmd = "tmux-ide cheatsheet",
252
+ key = CHEATSHEET_KEY,
253
+ ): string[] {
254
+ return ["bind-key", "-n", key, "display-popup", "-E", "-w", "90%", "-h", "80%", cheatsheetCmd];
255
+ }
256
+
257
+ /** PURE — the tmux argv that removes the cheat-sheet key binding. */
258
+ export function cheatsheetUnbindCommand(key = CHEATSHEET_KEY): string[] {
259
+ return ["unbind-key", "-n", key];
260
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Per-pane agent chips — the border label rendered INSIDE an adopted session.
3
+ *
4
+ * Where the status bar ({@link ./statusline.ts}) shows the fleet rollup (one
5
+ * glyph per session), a chip shows per-PANE truth: what agent is running in
6
+ * THIS pane and its live state — `claude · working`, `codex · blocked`. The
7
+ * updater writes each adopted pane's chip into a `@tmux_ide_chip` pane option
8
+ * and `adoptSession` points `pane-border-format` at it (falling back to the
9
+ * pane title when empty). `paneChip` is pure (tested); the io lives in the
10
+ * updater.
11
+ */
12
+ import { DEFAULT_THEME, type AppTheme } from "../../lib/app-config.ts";
13
+ import type { AgentStatus } from "../detect/classify.ts";
14
+ import { statusStyle } from "./statusline.ts";
15
+
16
+ /**
17
+ * PURE — build a pane's chip string with tmux `#[...]` markup, or `""` for a
18
+ * non-agent pane (`agent === null`, e.g. a raw shell). An empty chip clears the
19
+ * pane option so the border format falls back to the pane title. The chip is
20
+ * `<agent> · <status>` styled via the shared {@link statusStyle} so it reads the
21
+ * same as its session's rollup glyph — one palette across the bar and the chips.
22
+ */
23
+ export function paneChip(
24
+ agent: string | null,
25
+ status: AgentStatus,
26
+ theme: AppTheme = DEFAULT_THEME,
27
+ ): string {
28
+ if (!agent) return "";
29
+ return `${statusStyle(status, theme)}${agent} · ${status}#[default]`;
30
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Agent-state TRANSITION event log — the fleet's history layer.
3
+ *
4
+ * The chrome {@link ./updater.ts updater} sees every session's status EACH
5
+ * tick, but a status bar is a snapshot: it can't tell you that
6
+ * `prototyper-platform` went `working → done` at 12:31. This module turns the
7
+ * per-tick fleet into a stream of transition events and appends them to a
8
+ * JSONL log that `tmux-ide events` tails.
9
+ *
10
+ * Split as usual: {@link diffFleet} + {@link formatEventLine} +
11
+ * {@link shouldRotate} are PURE (unit-tested without a filesystem), while
12
+ * {@link appendEvents} is the thin io wrapper that stamps `ts`, rotates the
13
+ * file, and appends.
14
+ */
15
+ import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { join } from "node:path";
18
+ import type { AgentStatus } from "../detect/classify.ts";
19
+
20
+ /** A single session-level status transition. `from` is null the first time a session is seen. */
21
+ export interface AgentEvent {
22
+ ts: string;
23
+ session: string;
24
+ from: AgentStatus | null;
25
+ to: AgentStatus;
26
+ }
27
+
28
+ /** A transition before it's been stamped with a timestamp (what {@link diffFleet} emits). */
29
+ export type AgentEventInit = Omit<AgentEvent, "ts">;
30
+
31
+ /** Rotate the log once it grows past this many bytes (crude one-generation rotation). */
32
+ export const EVENTS_MAX_BYTES = 1024 * 1024;
33
+
34
+ /**
35
+ * PURE — diff the previous fleet state against the current one.
36
+ *
37
+ * For each session in `next` we emit a transition when its status changed, and
38
+ * a first-sight event (`from: null`) the first time we see it. A session that
39
+ * DISAPPEARS from `next` emits NOTHING — we don't synthesize an `idle`/gone
40
+ * event, we just drop it from the returned `state` (keeps the log honest: an
41
+ * absent session simply stops producing events until it returns).
42
+ *
43
+ * Returns both the new events and the fresh state map to thread into the next
44
+ * tick. The caller stamps `ts` on the events (see {@link appendEvents}).
45
+ */
46
+ export function diffFleet(
47
+ prev: Map<string, AgentStatus>,
48
+ next: Array<{ name: string; status: AgentStatus }>,
49
+ ): { events: AgentEventInit[]; state: Map<string, AgentStatus> } {
50
+ const state = new Map<string, AgentStatus>();
51
+ const events: AgentEventInit[] = [];
52
+ for (const { name, status } of next) {
53
+ const before = prev.has(name) ? prev.get(name)! : null;
54
+ state.set(name, status);
55
+ if (before === null) {
56
+ events.push({ session: name, from: null, to: status });
57
+ } else if (before !== status) {
58
+ events.push({ session: name, from: before, to: status });
59
+ }
60
+ }
61
+ return { events, state };
62
+ }
63
+
64
+ /** PURE — whether a log of `sizeBytes` is due for rotation. */
65
+ export function shouldRotate(sizeBytes: number): boolean {
66
+ return sizeBytes > EVENTS_MAX_BYTES;
67
+ }
68
+
69
+ /** Extract `HH:MM:SS` from an ISO-8601 timestamp (UTC); falls back to the raw string. */
70
+ function isoTime(ts: string): string {
71
+ const m = /T(\d{2}:\d{2}:\d{2})/.exec(ts);
72
+ return m ? m[1]! : ts;
73
+ }
74
+
75
+ /**
76
+ * PURE — render one event for the human-readable `tmux-ide events` view, e.g.
77
+ * `12:31:07 prototyper-platform working → done`. First-sight events (from=null)
78
+ * render the origin as `·`. `paint` colors a status token (defaults to
79
+ * identity so tests get plain text); the CLI passes an ANSI painter.
80
+ */
81
+ export function formatEventLine(
82
+ ev: AgentEvent,
83
+ paint: (status: AgentStatus | null, text: string) => string = (_s, t) => t,
84
+ ): string {
85
+ const from = ev.from === null ? "·" : paint(ev.from, ev.from);
86
+ return `${isoTime(ev.ts)} ${ev.session} ${from} → ${paint(ev.to, ev.to)}`;
87
+ }
88
+
89
+ /** Absolute path to the fleet event log. */
90
+ export function eventsPath(): string {
91
+ return join(homedir(), ".tmux-ide", "events.jsonl");
92
+ }
93
+
94
+ /**
95
+ * io — stamp each event with `now()` and append it to the log as JSONL.
96
+ *
97
+ * Before appending, rotates the log to `events.jsonl.1` (overwriting any prior
98
+ * generation) when it's grown past {@link EVENTS_MAX_BYTES}. Best-effort: a
99
+ * filesystem failure must never break the updater loop, so every step is
100
+ * guarded. No-op when there are no events.
101
+ */
102
+ export function appendEvents(
103
+ events: AgentEventInit[],
104
+ now: () => string = () => new Date().toISOString(),
105
+ ): void {
106
+ if (events.length === 0) return;
107
+ const path = eventsPath();
108
+ try {
109
+ mkdirSync(join(homedir(), ".tmux-ide"), { recursive: true });
110
+ if (existsSync(path) && shouldRotate(statSync(path).size)) {
111
+ renameSync(path, `${path}.1`);
112
+ }
113
+ const ts = now();
114
+ const lines = events.map((e) => `${JSON.stringify({ ts, ...e })}\n`).join("");
115
+ appendFileSync(path, lines);
116
+ } catch {
117
+ // Stale/failed log write just means a gap in history — never fatal.
118
+ }
119
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Kitty-keyboard-protocol fallback binds for the root-table Alt keys.
3
+ *
4
+ * Claude Code enables the Kitty keyboard protocol with event-type reporting.
5
+ * Under it, the FOCUSED pane's terminal encodes e.g. Alt-m not as the legacy
6
+ * `ESC m` but as the CSI-u form `ESC[109;3:1u` (109 = 'm', 3 = the Alt modifier,
7
+ * `:1` = a key-PRESS event-type subparameter). tmux 3.6 does NOT normalize that
8
+ * `:1`-carrying full form back to `M-m`, so a root-table `bind-key -n M-m` never
9
+ * fires while a Claude Code pane is focused — every dock shortcut (⌥m/⌥p/⌥k/⌥b/
10
+ * ⌥e/⌥g/⌥,/⌥h) goes dead. Plain `ESC m` and the basic CSI-u `ESC[109;3u` both
11
+ * DO reach tmux as `M-m`; only the `:1` full form misses (proven live by
12
+ * injecting each encoding into a nested client).
13
+ *
14
+ * The fix is a tmux USER-KEY fallback per Alt bind: teach tmux the exact kitty
15
+ * escape as `user-keys[N]`, then bind `UserN` to the SAME action argv as the
16
+ * `M-…` key. tmux matches the raw escape against the user-key and fires the
17
+ * bind, so the shortcut works whether the focused pane speaks legacy or full
18
+ * kitty. Registering the fallback is harmless for panes that never send the
19
+ * `:1` form (nothing else emits `UserN`).
20
+ *
21
+ * Pure — tested as a table; the io (registering the binds) lives in
22
+ * {@link ./statusline.ts}.
23
+ */
24
+
25
+ /**
26
+ * PURE — the full-kitty key-PRESS encoding for a single-char Alt key (`M-<c>`),
27
+ * or `null` for anything else (multi-char key names, non-Alt keys — they don't
28
+ * get a fallback). The sequence is `ESC[<code>;3:1u` where `<code>` is the code
29
+ * point of the (lowercased) character, `3` is the Alt modifier, and `:1` is the
30
+ * key-press event-type subparameter. Examples: `M-m` → `ESC[109;3:1u`, `M-h` →
31
+ * `ESC[104;3:1u`, `M-,` → `ESC[44;3:1u`.
32
+ */
33
+ export function kittyEscapeFor(key: string): string | null {
34
+ const m = /^M-(.)$/.exec(key);
35
+ const ch = m?.[1];
36
+ if (ch === undefined) return null;
37
+ const code = ch.toLowerCase().codePointAt(0);
38
+ if (code === undefined) return null;
39
+ return `\x1b[${code};3:1u`;
40
+ }
41
+
42
+ /**
43
+ * PURE — the tmux `user-keys` array index for the Nth Alt fallback. We claim the
44
+ * range `[100, …)` (offset 100) so we never clobber a user's own
45
+ * `user-keys[0..]` entries — anyone hand-setting `user-keys` should keep their
46
+ * indices below 100.
47
+ */
48
+ export function kittyUserKeyIndex(slot: number): number {
49
+ return 100 + slot;
50
+ }
51
+
52
+ /** PURE — the tmux key name a `user-keys[idx]` entry surfaces as (`UserN`). */
53
+ export function kittyUserKeyName(slot: number): string {
54
+ return `User${kittyUserKeyIndex(slot)}`;
55
+ }