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,88 @@
1
+ /**
2
+ * Pure scrollbar geometry (M19.5) — shared by the editor viewport, the diff
3
+ * pane, and any scrolled mirror pane.
4
+ *
5
+ * A 1-col track at the right edge of an always-present container; the thumb's
6
+ * position and size are pure functions of (viewportTop, contentLen, viewH).
7
+ * Both the RENDER (draws the track cells) and the ROUTER (classifies a press as
8
+ * thumb vs track, maps a thumb drag to an absolute scroll top, pages a track
9
+ * click) read the SAME math from here — the surface-bar discipline applied to a
10
+ * vertical strip. Deliberately opentui-free so it unit-tests under Node.
11
+ */
12
+
13
+ export interface ScrollThumb {
14
+ /** Whether the content overflows the viewport (nothing to scroll → hidden). */
15
+ overflow: boolean;
16
+ /** First track row (0-based, within [0, viewH)) the thumb occupies. */
17
+ start: number;
18
+ /** Thumb height in rows (≥1, ≤viewH). */
19
+ size: number;
20
+ }
21
+
22
+ /** The thumb size for a content/viewport pair — a viewport-fraction of the
23
+ * track, floored at one cell so a thumb always exists. Shared by every
24
+ * function below so their maths never drift. */
25
+ function thumbSize(contentLen: number, viewH: number): number {
26
+ return Math.max(1, Math.min(viewH, Math.round((viewH * viewH) / contentLen)));
27
+ }
28
+
29
+ /** The thumb geometry for a scroll position. `viewportTop` is the first visible
30
+ * content line; `contentLen` the total lines; `viewH` the visible rows. When
31
+ * the content fits, `overflow` is false and the caller draws no track. */
32
+ export function scrollThumb(viewportTop: number, contentLen: number, viewH: number): ScrollThumb {
33
+ if (viewH <= 0 || contentLen <= viewH) {
34
+ return { overflow: false, start: 0, size: Math.max(1, viewH) };
35
+ }
36
+ const size = thumbSize(contentLen, viewH);
37
+ const maxTop = contentLen - viewH;
38
+ const maxStart = viewH - size;
39
+ const top = Math.max(0, Math.min(maxTop, viewportTop));
40
+ const start = maxStart <= 0 ? 0 : Math.round((top / maxTop) * maxStart);
41
+ return { overflow: true, start: Math.max(0, Math.min(maxStart, start)), size };
42
+ }
43
+
44
+ /** Where a track row falls relative to the thumb — the fourth drag-origin's
45
+ * press classifier and the page-click direction source. */
46
+ export type TrackZone = "above" | "thumb" | "below";
47
+
48
+ export function trackZone(row: number, thumb: ScrollThumb): TrackZone {
49
+ if (row < thumb.start) return "above";
50
+ if (row >= thumb.start + thumb.size) return "below";
51
+ return "thumb";
52
+ }
53
+
54
+ /** Page the viewport toward a click at track `row` (above the thumb → up one
55
+ * viewport, below → down one; on the thumb → unchanged), clamped to range. */
56
+ export function pageTop(
57
+ row: number,
58
+ viewportTop: number,
59
+ contentLen: number,
60
+ viewH: number,
61
+ ): number {
62
+ const thumb = scrollThumb(viewportTop, contentLen, viewH);
63
+ if (!thumb.overflow) return viewportTop;
64
+ const maxTop = contentLen - viewH;
65
+ const zone = trackZone(row, thumb);
66
+ const next =
67
+ zone === "above" ? viewportTop - viewH : zone === "below" ? viewportTop + viewH : viewportTop;
68
+ return Math.max(0, Math.min(maxTop, next));
69
+ }
70
+
71
+ /** Map a dragged thumb to an absolute scroll top. `trackRow` is the current
72
+ * pointer row within the track; `grabOffset` is the row within the thumb where
73
+ * the drag began (so the thumb doesn't jump under the cursor). Returns the new
74
+ * first-visible line, clamped. */
75
+ export function dragTop(
76
+ trackRow: number,
77
+ grabOffset: number,
78
+ contentLen: number,
79
+ viewH: number,
80
+ ): number {
81
+ if (viewH <= 0 || contentLen <= viewH) return 0;
82
+ const size = thumbSize(contentLen, viewH);
83
+ const maxStart = viewH - size;
84
+ const maxTop = contentLen - viewH;
85
+ if (maxStart <= 0) return 0;
86
+ const start = Math.max(0, Math.min(maxStart, trackRow - grabOffset));
87
+ return Math.round((start / maxStart) * maxTop);
88
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Pure, io-free math for scrollback search (M20.3) — the copy-mode `/` finder,
3
+ * app-native. app.tsx owns the search-session state machine + the io (reading a
4
+ * pane's `bufferLines`, driving scrollOffset); this module owns the arithmetic,
5
+ * unit-tested without a terminal exactly like selection.ts / diff-model.ts.
6
+ *
7
+ * A match is a (line, col) into the pane's full buffer (0 = oldest scrollback
8
+ * line). The render maps a match line to a visible row and inverse/accent-tints
9
+ * its [col, col+len) span; a jump converts a match line to a scrollOffset.
10
+ */
11
+
12
+ /** One substring hit: `line` is the absolute buffer line, `col` the 0-based
13
+ * character column of the match start on that line. */
14
+ export interface SearchMatch {
15
+ line: number;
16
+ col: number;
17
+ }
18
+
19
+ /**
20
+ * Every case-insensitive substring occurrence of `query` across `lines`, in
21
+ * buffer order (top→bottom, left→right), one entry per occurrence (multiple per
22
+ * line allowed; non-overlapping — the scan advances past each hit). An empty
23
+ * query yields no matches.
24
+ */
25
+ export function findMatches(lines: readonly string[], query: string): SearchMatch[] {
26
+ const out: SearchMatch[] = [];
27
+ if (query.length === 0) return out;
28
+ const needle = query.toLowerCase();
29
+ for (let line = 0; line < lines.length; line++) {
30
+ const hay = (lines[line] ?? "").toLowerCase();
31
+ let from = 0;
32
+ for (;;) {
33
+ const idx = hay.indexOf(needle, from);
34
+ if (idx === -1) break;
35
+ out.push({ line, col: idx });
36
+ from = idx + needle.length;
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+
42
+ /**
43
+ * Matches in VISIT order — bottom-up (nearest the live viewport first), the
44
+ * order `/`-then-Enter lands on and n/N cycle through. So visit index 0 is the
45
+ * bottom-most match (shown as "1/N"), and `n` walks upward through the buffer.
46
+ * A fresh copy (does not mutate the input).
47
+ */
48
+ export function visitOrder(matches: readonly SearchMatch[]): SearchMatch[] {
49
+ return [...matches].reverse();
50
+ }
51
+
52
+ /** Cycle a match index by `dir` (+1 next / -1 prev) with wraparound; -1 stays
53
+ * -1 when there are no matches. */
54
+ export function stepMatch(current: number, dir: number, total: number): number {
55
+ if (total <= 0) return -1;
56
+ return (((current + dir) % total) + total) % total;
57
+ }
58
+
59
+ /**
60
+ * The scrollOffset (lines above the live viewport) that brings buffer `line`
61
+ * into view, positioned roughly at the middle of a `viewH`-row viewport, clamped
62
+ * to [0, depth]. `depth` is the scrollback budget (lines above the live top);
63
+ * the pane snapshot renders visible row `r` from buffer line `depth - offset + r`,
64
+ * so putting `line` at row `target` needs `offset = depth + target - line`.
65
+ */
66
+ export function offsetForMatch(line: number, depth: number, viewH: number): number {
67
+ const target = Math.floor(Math.max(0, viewH) / 2);
68
+ const raw = depth + target - line;
69
+ return Math.max(0, Math.min(depth, raw));
70
+ }
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Pure, io-free geometry + text math for mouse selection & clipboard (M19.4).
3
+ *
4
+ * Two surfaces select the same way — the session MIRROR (pane snapshot rows)
5
+ * and the built-in EDITOR (buffer lines) — so the coordinate math lives here,
6
+ * unit-tested without a terminal, exactly like spans.ts / diff-model.ts. app.tsx
7
+ * owns the mouse-gesture state machine and the io (OSC52 to stdout, tmux
8
+ * passthrough, send-keys); this module owns the arithmetic.
9
+ *
10
+ * The selection is LINEAR (terminal-style, reading order), not rectangular: a
11
+ * cell is selected if it falls between the anchor and head in row-major order.
12
+ */
13
+
14
+ /** A cell in surface-local coordinates: mirror = snapshot row + pane column;
15
+ * editor = buffer line + buffer column. */
16
+ export interface Cell {
17
+ row: number;
18
+ col: number;
19
+ }
20
+
21
+ /** An active selection on one surface. `anchor` is where the drag began, `head`
22
+ * the current/last pointer cell; either may precede the other. */
23
+ export type Selection =
24
+ | { surface: "mirror"; paneId: string; anchor: Cell; head: Cell }
25
+ | { surface: "editor"; anchor: Cell; head: Cell };
26
+
27
+ /** OpenTUI TextAttributes bit for inverse video (matches pane-mirror.ts). */
28
+ export const ATTR_INVERSE = 32;
29
+
30
+ /** Order two cells into reading order (row-major); both endpoints inclusive. */
31
+ export function orderCells(a: Cell, b: Cell): { start: Cell; end: Cell } {
32
+ if (a.row < b.row || (a.row === b.row && a.col <= b.col)) return { start: a, end: b };
33
+ return { start: b, end: a };
34
+ }
35
+
36
+ /**
37
+ * The inclusive [from,to] column interval selected on `row` for a linear
38
+ * selection between ordered endpoints, or null when the row is outside the
39
+ * selection or has no cells. `rowLen` is the row's character count.
40
+ */
41
+ export function rowSelectionRange(
42
+ row: number,
43
+ rowLen: number,
44
+ start: Cell,
45
+ end: Cell,
46
+ ): { from: number; to: number } | null {
47
+ if (row < start.row || row > end.row) return null;
48
+ if (rowLen <= 0) return null;
49
+ const rawFrom = row === start.row ? start.col : 0;
50
+ const rawTo = row === end.row ? end.col : rowLen - 1;
51
+ const from = Math.max(0, Math.min(rawFrom, rowLen - 1));
52
+ const to = Math.max(0, Math.min(rawTo, rowLen - 1));
53
+ if (to < from) return null;
54
+ return { from, to };
55
+ }
56
+
57
+ /**
58
+ * Extract the selected text from per-row plain strings between ordered
59
+ * endpoints. Rows join with "\n". When `trimTrailing` (mirror snapshot rows are
60
+ * space-padded to the pane width), trailing whitespace on each visual row is
61
+ * stripped — the terminal-copy convention; the editor passes false to keep
62
+ * buffer text exact.
63
+ */
64
+ export function extractSelection(
65
+ rowTexts: string[],
66
+ start: Cell,
67
+ end: Cell,
68
+ trimTrailing = true,
69
+ ): string {
70
+ const out: string[] = [];
71
+ for (let r = start.row; r <= end.row; r++) {
72
+ const text = rowTexts[r] ?? "";
73
+ const range = rowSelectionRange(r, text.length, start, end);
74
+ let seg = range ? text.slice(range.from, range.to + 1) : "";
75
+ if (trimTrailing) seg = seg.replace(/\s+$/u, "");
76
+ out.push(seg);
77
+ }
78
+ return out.join("\n");
79
+ }
80
+
81
+ /**
82
+ * The inclusive [from,to] char interval of the word at `col` in `text`,
83
+ * splitting on /\W/ (word chars are [A-Za-z0-9_]). A click on a non-word char
84
+ * selects just that char; an empty line selects {0,0}.
85
+ */
86
+ export function wordRangeAt(text: string, col: number): { from: number; to: number } {
87
+ const n = text.length;
88
+ if (n === 0) return { from: 0, to: 0 };
89
+ const c = Math.max(0, Math.min(col, n - 1));
90
+ const isWord = (ch: string): boolean => /\w/u.test(ch);
91
+ if (!isWord(text[c]!)) return { from: c, to: c };
92
+ let from = c;
93
+ let to = c;
94
+ while (from > 0 && isWord(text[from - 1]!)) from--;
95
+ while (to < n - 1 && isWord(text[to + 1]!)) to++;
96
+ return { from, to };
97
+ }
98
+
99
+ /** The inclusive [from,to] interval covering the line up to its last non-blank
100
+ * char (a blank line → {0,0}). */
101
+ export function lineRangeAt(text: string): { from: number; to: number } {
102
+ const trimmed = text.replace(/\s+$/u, "");
103
+ if (trimmed.length === 0) return { from: 0, to: 0 };
104
+ return { from: 0, to: trimmed.length - 1 };
105
+ }
106
+
107
+ /**
108
+ * The resulting click count (1/2/3, cycling to 1 after 3) for double/triple
109
+ * detection: a click at the same cell within `windowMs` of the previous one
110
+ * increments; anything else resets to 1.
111
+ */
112
+ export function clickCount(
113
+ prev: { row: number; col: number; ts: number; count: number } | null,
114
+ cell: Cell,
115
+ ts: number,
116
+ windowMs: number,
117
+ ): number {
118
+ if (prev && prev.row === cell.row && prev.col === cell.col && ts - prev.ts <= windowMs) {
119
+ return prev.count >= 3 ? 1 : prev.count + 1;
120
+ }
121
+ return 1;
122
+ }
123
+
124
+ /**
125
+ * Toggle the inverse attribute across chars [from,to] (inclusive, in row-char
126
+ * columns) of a row's styled runs, splitting runs at the boundaries so the
127
+ * selected span renders highlighted while every other run keeps its colors.
128
+ * XOR (not OR) so an already-inverse cell — e.g. the painted cursor — flips
129
+ * back, matching pane-mirror's cursor treatment.
130
+ */
131
+ export function tintRunsInverse<T extends { text: string; attributes: number }>(
132
+ runs: T[],
133
+ from: number,
134
+ to: number,
135
+ ): T[] {
136
+ if (to < from) return runs;
137
+ const out: T[] = [];
138
+ let col = 0;
139
+ for (const run of runs) {
140
+ const len = run.text.length;
141
+ const runStart = col;
142
+ const runEnd = col + len - 1;
143
+ col += len;
144
+ if (len === 0 || runEnd < from || runStart > to) {
145
+ out.push(run);
146
+ continue;
147
+ }
148
+ const a = Math.max(from, runStart) - runStart;
149
+ const b = Math.min(to, runEnd) - runStart;
150
+ if (a > 0) out.push({ ...run, text: run.text.slice(0, a) });
151
+ out.push({ ...run, text: run.text.slice(a, b + 1), attributes: run.attributes ^ ATTR_INVERSE });
152
+ if (b + 1 < len) out.push({ ...run, text: run.text.slice(b + 1) });
153
+ }
154
+ return out;
155
+ }
156
+
157
+ /**
158
+ * Set the background color across chars [from,to] (inclusive, in row-char
159
+ * columns) of a row's styled runs, splitting runs at the boundaries so the
160
+ * spanned cells render with `bg` while every other run keeps its colors. The
161
+ * twin of {@link tintRunsInverse} — search-match highlighting paints an accent
162
+ * background (a distinct treatment from the inverse-video selection) so the two
163
+ * can coexist on one row. `bg` is packed 0xRRGGBB (the run bg convention).
164
+ */
165
+ export function tintRunsBg<T extends { text: string; bg: number | null }>(
166
+ runs: T[],
167
+ from: number,
168
+ to: number,
169
+ bg: number,
170
+ ): T[] {
171
+ if (to < from) return runs;
172
+ const out: T[] = [];
173
+ let col = 0;
174
+ for (const run of runs) {
175
+ const len = run.text.length;
176
+ const runStart = col;
177
+ const runEnd = col + len - 1;
178
+ col += len;
179
+ if (len === 0 || runEnd < from || runStart > to) {
180
+ out.push(run);
181
+ continue;
182
+ }
183
+ const a = Math.max(from, runStart) - runStart;
184
+ const b = Math.min(to, runEnd) - runStart;
185
+ if (a > 0) out.push({ ...run, text: run.text.slice(0, a) });
186
+ out.push({ ...run, text: run.text.slice(a, b + 1), bg });
187
+ if (b + 1 < len) out.push({ ...run, text: run.text.slice(b + 1) });
188
+ }
189
+ return out;
190
+ }
191
+
192
+ // ── Select mode on app-mouse panes (M22.9) ──────────────────────────────────
193
+ // Panes whose app turned mouse reporting on (`appMouse`) normally get every
194
+ // press FORWARDED, so a drag can never start a local selection there. Two
195
+ // entries reopen selection: an explicit per-pane SELECT MODE (right-click →
196
+ // "Select text…", paused forwarding until esc / a completed copy / focus
197
+ // leaving the pane) and a SHIFT-modified press when the terminal passes shift
198
+ // through (SGR encodes it as +4 on the button code; many terminals keep
199
+ // shift+drag for native selection instead — then only select mode applies).
200
+
201
+ /** PURE — whether a left press on a pane begins a LOCAL drag selection
202
+ * (vs. being forwarded to the pane's app as an SGR press). */
203
+ export function pressStartsSelection(
204
+ appMouse: boolean,
205
+ selectModeOn: boolean,
206
+ shift: boolean,
207
+ ): boolean {
208
+ return !appMouse || selectModeOn || shift;
209
+ }
210
+
211
+ /** PURE — whether the wheel scrolls the LOCAL mirror scrollback (vs. being
212
+ * forwarded as SGR wheel events). Select mode reclaims the wheel so older
213
+ * output can be scrolled into view and selected. */
214
+ export function wheelScrollsLocal(appMouse: boolean, selectModeOn: boolean): boolean {
215
+ return !appMouse || selectModeOn;
216
+ }
217
+
218
+ /** PURE — the select-mode badge text for a pane width (rendered in the pane's
219
+ * top-right badge family, next to the scroll badge), degrading label → glyph
220
+ * → hidden on narrow panes exactly like the agent chip's budget. */
221
+ export function selectBadgeLabel(paneWidth: number): string | null {
222
+ if (paneWidth >= 16) return " ⧉ select ";
223
+ if (paneWidth >= 5) return " ⧉ ";
224
+ return null;
225
+ }
226
+
227
+ /** The raw OSC52 clipboard-set escape for a base64 payload (BEL-terminated). */
228
+ export function osc52Sequence(base64: string): string {
229
+ return `\x1b]52;c;${base64}\x07`;
230
+ }
231
+
232
+ /**
233
+ * Wrap a sequence in the tmux passthrough envelope (DCS `tmux;` … ST) with inner
234
+ * ESC bytes doubled, so it rides through an outer tmux (with allow-passthrough
235
+ * on) verbatim to the real terminal. Kept tested + available; the app prefers
236
+ * raw OSC52 so `set-clipboard on` captures it into tmux's paste buffer.
237
+ */
238
+ export function tmuxPassthrough(seq: string): string {
239
+ const doubled = seq.split("\x1b").join("\x1b\x1b");
240
+ return `\x1bPtmux;${doubled}\x1b\\`;
241
+ }
242
+
243
+ /** Split `text` into chunks each at most `maxBytes` UTF-8 bytes, never breaking
244
+ * a code point — for send-keys -H, whose per-command length tmux caps. */
245
+ export function chunkByBytes(text: string, maxBytes: number): string[] {
246
+ const enc = new TextEncoder();
247
+ const chunks: string[] = [];
248
+ let cur = "";
249
+ let curBytes = 0;
250
+ for (const ch of text) {
251
+ const b = enc.encode(ch).length;
252
+ if (curBytes + b > maxBytes && cur.length > 0) {
253
+ chunks.push(cur);
254
+ cur = "";
255
+ curBytes = 0;
256
+ }
257
+ cur += ch;
258
+ curBytes += b;
259
+ }
260
+ if (cur.length > 0) chunks.push(cur);
261
+ return chunks;
262
+ }