@gigaflow/gmux 0.15.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 (229) hide show
  1. package/CHANGELOG.md +654 -0
  2. package/LICENSE +21 -0
  3. package/README.md +351 -0
  4. package/dist/adapters/claude-code.d.ts +52 -0
  5. package/dist/adapters/claude-code.js +315 -0
  6. package/dist/adapters/claude-code.js.map +1 -0
  7. package/dist/adapters/codex.d.ts +39 -0
  8. package/dist/adapters/codex.js +233 -0
  9. package/dist/adapters/codex.js.map +1 -0
  10. package/dist/adapters/jsonl.d.ts +40 -0
  11. package/dist/adapters/jsonl.js +92 -0
  12. package/dist/adapters/jsonl.js.map +1 -0
  13. package/dist/adapters/registry.d.ts +11 -0
  14. package/dist/adapters/registry.js +21 -0
  15. package/dist/adapters/registry.js.map +1 -0
  16. package/dist/adapters/types.d.ts +43 -0
  17. package/dist/adapters/types.js +2 -0
  18. package/dist/adapters/types.js.map +1 -0
  19. package/dist/cli/border-client.d.ts +8 -0
  20. package/dist/cli/border-client.js +11 -0
  21. package/dist/cli/border-client.js.map +1 -0
  22. package/dist/cli/commands/__ask-cancel.d.ts +35 -0
  23. package/dist/cli/commands/__ask-cancel.js +91 -0
  24. package/dist/cli/commands/__ask-cancel.js.map +1 -0
  25. package/dist/cli/commands/__ask-refresh.d.ts +40 -0
  26. package/dist/cli/commands/__ask-refresh.js +54 -0
  27. package/dist/cli/commands/__ask-refresh.js.map +1 -0
  28. package/dist/cli/commands/__ask-run.d.ts +43 -0
  29. package/dist/cli/commands/__ask-run.js +126 -0
  30. package/dist/cli/commands/__ask-run.js.map +1 -0
  31. package/dist/cli/commands/__ask-send.d.ts +50 -0
  32. package/dist/cli/commands/__ask-send.js +138 -0
  33. package/dist/cli/commands/__ask-send.js.map +1 -0
  34. package/dist/cli/commands/__preview-card.d.ts +31 -0
  35. package/dist/cli/commands/__preview-card.js +72 -0
  36. package/dist/cli/commands/__preview-card.js.map +1 -0
  37. package/dist/cli/commands/ask.d.ts +50 -0
  38. package/dist/cli/commands/ask.js +240 -0
  39. package/dist/cli/commands/ask.js.map +1 -0
  40. package/dist/cli/commands/auto.d.ts +12 -0
  41. package/dist/cli/commands/auto.js +28 -0
  42. package/dist/cli/commands/auto.js.map +1 -0
  43. package/dist/cli/commands/cockpit.d.ts +14 -0
  44. package/dist/cli/commands/cockpit.js +52 -0
  45. package/dist/cli/commands/cockpit.js.map +1 -0
  46. package/dist/cli/commands/daemon.d.ts +75 -0
  47. package/dist/cli/commands/daemon.js +258 -0
  48. package/dist/cli/commands/daemon.js.map +1 -0
  49. package/dist/cli/commands/doctor.d.ts +6 -0
  50. package/dist/cli/commands/doctor.js +159 -0
  51. package/dist/cli/commands/doctor.js.map +1 -0
  52. package/dist/cli/commands/grep.d.ts +2 -0
  53. package/dist/cli/commands/grep.js +52 -0
  54. package/dist/cli/commands/grep.js.map +1 -0
  55. package/dist/cli/commands/index-cmd.d.ts +2 -0
  56. package/dist/cli/commands/index-cmd.js +29 -0
  57. package/dist/cli/commands/index-cmd.js.map +1 -0
  58. package/dist/cli/commands/ls.d.ts +30 -0
  59. package/dist/cli/commands/ls.js +95 -0
  60. package/dist/cli/commands/ls.js.map +1 -0
  61. package/dist/cli/commands/overlay.d.ts +26 -0
  62. package/dist/cli/commands/overlay.js +216 -0
  63. package/dist/cli/commands/overlay.js.map +1 -0
  64. package/dist/cli/commands/pick.d.ts +59 -0
  65. package/dist/cli/commands/pick.js +254 -0
  66. package/dist/cli/commands/pick.js.map +1 -0
  67. package/dist/cli/commands/picker-rows.d.ts +17 -0
  68. package/dist/cli/commands/picker-rows.js +47 -0
  69. package/dist/cli/commands/picker-rows.js.map +1 -0
  70. package/dist/cli/commands/resume.d.ts +25 -0
  71. package/dist/cli/commands/resume.js +88 -0
  72. package/dist/cli/commands/resume.js.map +1 -0
  73. package/dist/cli/commands/run.d.ts +13 -0
  74. package/dist/cli/commands/run.js +78 -0
  75. package/dist/cli/commands/run.js.map +1 -0
  76. package/dist/cli/commands/setup.d.ts +37 -0
  77. package/dist/cli/commands/setup.js +200 -0
  78. package/dist/cli/commands/setup.js.map +1 -0
  79. package/dist/cli/commands/show.d.ts +2 -0
  80. package/dist/cli/commands/show.js +24 -0
  81. package/dist/cli/commands/show.js.map +1 -0
  82. package/dist/cli/commands/summarize.d.ts +2 -0
  83. package/dist/cli/commands/summarize.js +59 -0
  84. package/dist/cli/commands/summarize.js.map +1 -0
  85. package/dist/cli/commands/tmux.d.ts +11 -0
  86. package/dist/cli/commands/tmux.js +90 -0
  87. package/dist/cli/commands/tmux.js.map +1 -0
  88. package/dist/cli/commands/watch.d.ts +2 -0
  89. package/dist/cli/commands/watch.js +76 -0
  90. package/dist/cli/commands/watch.js.map +1 -0
  91. package/dist/cli/format.d.ts +74 -0
  92. package/dist/cli/format.js +242 -0
  93. package/dist/cli/format.js.map +1 -0
  94. package/dist/cli/gmux-render.d.ts +11 -0
  95. package/dist/cli/gmux-render.js +60 -0
  96. package/dist/cli/gmux-render.js.map +1 -0
  97. package/dist/cli/main.d.ts +8 -0
  98. package/dist/cli/main.js +145 -0
  99. package/dist/cli/main.js.map +1 -0
  100. package/dist/cli/overlay-ask.d.ts +13 -0
  101. package/dist/cli/overlay-ask.js +32 -0
  102. package/dist/cli/overlay-ask.js.map +1 -0
  103. package/dist/cli/overlay.d.ts +26 -0
  104. package/dist/cli/overlay.js +149 -0
  105. package/dist/cli/overlay.js.map +1 -0
  106. package/dist/cli/picker.d.ts +308 -0
  107. package/dist/cli/picker.js +642 -0
  108. package/dist/cli/picker.js.map +1 -0
  109. package/dist/cli/preview.d.ts +87 -0
  110. package/dist/cli/preview.js +292 -0
  111. package/dist/cli/preview.js.map +1 -0
  112. package/dist/cli/tmux-label.d.ts +50 -0
  113. package/dist/cli/tmux-label.js +146 -0
  114. package/dist/cli/tmux-label.js.map +1 -0
  115. package/dist/core/errors.d.ts +54 -0
  116. package/dist/core/errors.js +91 -0
  117. package/dist/core/errors.js.map +1 -0
  118. package/dist/core/fingerprint.d.ts +21 -0
  119. package/dist/core/fingerprint.js +59 -0
  120. package/dist/core/fingerprint.js.map +1 -0
  121. package/dist/core/gmux-types.d.ts +102 -0
  122. package/dist/core/gmux-types.js +13 -0
  123. package/dist/core/gmux-types.js.map +1 -0
  124. package/dist/core/pane-state.d.ts +5 -0
  125. package/dist/core/pane-state.js +15 -0
  126. package/dist/core/pane-state.js.map +1 -0
  127. package/dist/core/paths.d.ts +68 -0
  128. package/dist/core/paths.js +106 -0
  129. package/dist/core/paths.js.map +1 -0
  130. package/dist/core/proc-tree.d.ts +9 -0
  131. package/dist/core/proc-tree.js +33 -0
  132. package/dist/core/proc-tree.js.map +1 -0
  133. package/dist/core/text.d.ts +34 -0
  134. package/dist/core/text.js +126 -0
  135. package/dist/core/text.js.map +1 -0
  136. package/dist/core/types.d.ts +322 -0
  137. package/dist/core/types.js +10 -0
  138. package/dist/core/types.js.map +1 -0
  139. package/dist/services/ask-transcript.d.ts +256 -0
  140. package/dist/services/ask-transcript.js +544 -0
  141. package/dist/services/ask-transcript.js.map +1 -0
  142. package/dist/services/ask.d.ts +54 -0
  143. package/dist/services/ask.js +161 -0
  144. package/dist/services/ask.js.map +1 -0
  145. package/dist/services/auto-summarize.d.ts +188 -0
  146. package/dist/services/auto-summarize.js +415 -0
  147. package/dist/services/auto-summarize.js.map +1 -0
  148. package/dist/services/concurrency.d.ts +2 -0
  149. package/dist/services/concurrency.js +16 -0
  150. package/dist/services/concurrency.js.map +1 -0
  151. package/dist/services/config.d.ts +131 -0
  152. package/dist/services/config.js +325 -0
  153. package/dist/services/config.js.map +1 -0
  154. package/dist/services/daemon-client.d.ts +9 -0
  155. package/dist/services/daemon-client.js +34 -0
  156. package/dist/services/daemon-client.js.map +1 -0
  157. package/dist/services/daemon-socket.d.ts +30 -0
  158. package/dist/services/daemon-socket.js +76 -0
  159. package/dist/services/daemon-socket.js.map +1 -0
  160. package/dist/services/daemon.d.ts +43 -0
  161. package/dist/services/daemon.js +106 -0
  162. package/dist/services/daemon.js.map +1 -0
  163. package/dist/services/distill.d.ts +39 -0
  164. package/dist/services/distill.js +93 -0
  165. package/dist/services/distill.js.map +1 -0
  166. package/dist/services/guardian.d.ts +26 -0
  167. package/dist/services/guardian.js +71 -0
  168. package/dist/services/guardian.js.map +1 -0
  169. package/dist/services/index-store.d.ts +36 -0
  170. package/dist/services/index-store.js +125 -0
  171. package/dist/services/index-store.js.map +1 -0
  172. package/dist/services/log-rotation.d.ts +3 -0
  173. package/dist/services/log-rotation.js +28 -0
  174. package/dist/services/log-rotation.js.map +1 -0
  175. package/dist/services/pane-links.d.ts +10 -0
  176. package/dist/services/pane-links.js +47 -0
  177. package/dist/services/pane-links.js.map +1 -0
  178. package/dist/services/pane-process.d.ts +95 -0
  179. package/dist/services/pane-process.js +183 -0
  180. package/dist/services/pane-process.js.map +1 -0
  181. package/dist/services/pane-registry.d.ts +24 -0
  182. package/dist/services/pane-registry.js +40 -0
  183. package/dist/services/pane-registry.js.map +1 -0
  184. package/dist/services/provider-process.d.ts +37 -0
  185. package/dist/services/provider-process.js +105 -0
  186. package/dist/services/provider-process.js.map +1 -0
  187. package/dist/services/providers.d.ts +76 -0
  188. package/dist/services/providers.js +104 -0
  189. package/dist/services/providers.js.map +1 -0
  190. package/dist/services/resolve.d.ts +3 -0
  191. package/dist/services/resolve.js +18 -0
  192. package/dist/services/resolve.js.map +1 -0
  193. package/dist/services/resources.d.ts +17 -0
  194. package/dist/services/resources.js +113 -0
  195. package/dist/services/resources.js.map +1 -0
  196. package/dist/services/search.d.ts +29 -0
  197. package/dist/services/search.js +135 -0
  198. package/dist/services/search.js.map +1 -0
  199. package/dist/services/semantic-gate.d.ts +13 -0
  200. package/dist/services/semantic-gate.js +23 -0
  201. package/dist/services/semantic-gate.js.map +1 -0
  202. package/dist/services/semantic.d.ts +113 -0
  203. package/dist/services/semantic.js +216 -0
  204. package/dist/services/semantic.js.map +1 -0
  205. package/dist/services/sensors.d.ts +92 -0
  206. package/dist/services/sensors.js +246 -0
  207. package/dist/services/sensors.js.map +1 -0
  208. package/dist/services/summarize.d.ts +84 -0
  209. package/dist/services/summarize.js +230 -0
  210. package/dist/services/summarize.js.map +1 -0
  211. package/dist/services/tmux-gateway.d.ts +17 -0
  212. package/dist/services/tmux-gateway.js +10 -0
  213. package/dist/services/tmux-gateway.js.map +1 -0
  214. package/dist/services/tmux-resolve.d.ts +54 -0
  215. package/dist/services/tmux-resolve.js +0 -0
  216. package/dist/services/tmux-resolve.js.map +1 -0
  217. package/dist/services/tmux.d.ts +32 -0
  218. package/dist/services/tmux.js +107 -0
  219. package/dist/services/tmux.js.map +1 -0
  220. package/dist/services/views.d.ts +7 -0
  221. package/dist/services/views.js +18 -0
  222. package/dist/services/views.js.map +1 -0
  223. package/dist/services/watch.d.ts +36 -0
  224. package/dist/services/watch.js +98 -0
  225. package/dist/services/watch.js.map +1 -0
  226. package/dist/services/workspace.d.ts +19 -0
  227. package/dist/services/workspace.js +88 -0
  228. package/dist/services/workspace.js.map +1 -0
  229. package/package.json +64 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The preview pane — both halves of it.
3
+ *
4
+ * fzf has exactly ONE preview pane and cannot split it. So gmux renders the card
5
+ * and the chat into one command's output and owns the boundary itself: fzf does
6
+ * not gain a pane, it gains a longer string.
7
+ *
8
+ * ```
9
+ * ┌─ sessions ─────┬─ preview ──────────────────┐
10
+ * │ > webshop 2h │ 4998a936 webshop 2h ago │
11
+ * │ api 4h │ WHERE IT LANDED … │ ← formatCard, clipped
12
+ * │ docs 1d │ ── ask ────────────────────│ ← the divider
13
+ * │ │ you │
14
+ * │ │ why did this one fail? │ ← the transcript
15
+ * │ │ gmux │
16
+ * │ │ The run died in apply_… │
17
+ * └────────────────┴────────────────────────────┘
18
+ * ```
19
+ *
20
+ * **No conversation ⇒ the card gets the whole pane, byte-identical to today.**
21
+ * That is the deal, not a nicety: a picker run where nobody presses ctrl-o must
22
+ * render exactly what it rendered before this file existed. It needs no flag and
23
+ * no branch — a missing transcript reads as zero events, `splitPreview` hands the
24
+ * card every row, and `formatPreview` returns `formatCard` and nothing else.
25
+ *
26
+ * **Nothing here calls a model, and nothing here may ever learn how.** The
27
+ * preview command re-runs on every cursor move, so a model call in this file is
28
+ * a model call per keystroke. The chat renders from the transcript file or it
29
+ * does not render.
30
+ *
31
+ * **Colour is an accent, never the message.** The preview's stdout is a pipe,
32
+ * not a tty, so `format.ts`'s gated `dim`/`cyan` are no-ops here and the CARD
33
+ * stays monochrome, exactly as it shipped. The divider and the `you`/`gmux`
34
+ * speakers use the `paneCyan`/`paneBold` variants instead, which fzf renders
35
+ * with `--ansi` — so the seam between card and chat pops in colour. But the
36
+ * divider is still carried by GLYPHS and the speakers by LAYOUT, so `NO_COLOR`,
37
+ * `TERM=dumb` or a colourless pane loses only the accent, never the structure.
38
+ */
39
+ import type { SessionView } from "../core/types.js";
40
+ import type { AskTranscript } from "../services/ask-transcript.js";
41
+ /** Row budget for one render. Sums to the pane height. */
42
+ export interface PreviewSplit {
43
+ cardRows: number;
44
+ dividerRows: 0 | 1;
45
+ chatRows: number;
46
+ }
47
+ /**
48
+ * How the pane's rows are divided.
49
+ *
50
+ * `paneRows` arrives from `$FZF_PREVIEW_LINES` via a shell and a commander flag,
51
+ * which makes it environment-controlled text. Treat it as hostile: anything
52
+ * non-positive is the guess, and no arithmetic below may go negative or throw.
53
+ */
54
+ export declare function splitPreview(paneRows: number, hasChat: boolean): PreviewSplit;
55
+ /** `Math.max(0, …)`: `String.repeat` throws RangeError on a negative count, and
56
+ * the width came from the environment. A narrow pane must not crash the pane.
57
+ * The colour is zero display columns, so the rule is still exactly `width`. */
58
+ export declare function askDivider(width: number): string;
59
+ /**
60
+ * Is there a conversation at all?
61
+ *
62
+ * A question is the only thing that makes one. `meta` alone is a transcript the
63
+ * sender created and nothing more, and it must still render the empty state —
64
+ * otherwise a `no-provider` send would put a divider over an empty half.
65
+ */
66
+ export declare function hasChatContent(transcript: AskTranscript | null): boolean;
67
+ /**
68
+ * The chat half: the last `rows` wrapped rows of the thread.
69
+ *
70
+ * **Auto-tail.** New text appears at the bottom, old text slides up. That is what
71
+ * makes it read as a chat rather than a document, it composes with the refresh
72
+ * loop for free, and it means the common case needs no scrolling at all. There is
73
+ * deliberately no scroll: fzf's own `shift-up` scrolls the WHOLE preview, which
74
+ * in split mode drags the card off the top and reveals nothing, because there is
75
+ * only one pane. If you hit the ceiling, `cat` the transcript.
76
+ *
77
+ * `now` is a parameter so the `thinking… Ns` count is testable without a clock.
78
+ */
79
+ export declare function formatChat(transcript: AskTranscript, rows: number, width: number, now?: Date): string;
80
+ /**
81
+ * One preview command's whole output: the card, and the chat if there is one.
82
+ *
83
+ * **The empty state returns `formatCard` and NOTHING else** — not "the card plus
84
+ * an empty tail", not "the card with a trailing divider". Byte-identical, because
85
+ * that is what decision 4 promises to everyone who never presses ctrl-o.
86
+ */
87
+ export declare function formatPreview(view: SessionView, transcript: AskTranscript | null, split: PreviewSplit, width: number, now?: Date): string;
@@ -0,0 +1,292 @@
1
+ /**
2
+ * The preview pane — both halves of it.
3
+ *
4
+ * fzf has exactly ONE preview pane and cannot split it. So gmux renders the card
5
+ * and the chat into one command's output and owns the boundary itself: fzf does
6
+ * not gain a pane, it gains a longer string.
7
+ *
8
+ * ```
9
+ * ┌─ sessions ─────┬─ preview ──────────────────┐
10
+ * │ > webshop 2h │ 4998a936 webshop 2h ago │
11
+ * │ api 4h │ WHERE IT LANDED … │ ← formatCard, clipped
12
+ * │ docs 1d │ ── ask ────────────────────│ ← the divider
13
+ * │ │ you │
14
+ * │ │ why did this one fail? │ ← the transcript
15
+ * │ │ gmux │
16
+ * │ │ The run died in apply_… │
17
+ * └────────────────┴────────────────────────────┘
18
+ * ```
19
+ *
20
+ * **No conversation ⇒ the card gets the whole pane, byte-identical to today.**
21
+ * That is the deal, not a nicety: a picker run where nobody presses ctrl-o must
22
+ * render exactly what it rendered before this file existed. It needs no flag and
23
+ * no branch — a missing transcript reads as zero events, `splitPreview` hands the
24
+ * card every row, and `formatPreview` returns `formatCard` and nothing else.
25
+ *
26
+ * **Nothing here calls a model, and nothing here may ever learn how.** The
27
+ * preview command re-runs on every cursor move, so a model call in this file is
28
+ * a model call per keystroke. The chat renders from the transcript file or it
29
+ * does not render.
30
+ *
31
+ * **Colour is an accent, never the message.** The preview's stdout is a pipe,
32
+ * not a tty, so `format.ts`'s gated `dim`/`cyan` are no-ops here and the CARD
33
+ * stays monochrome, exactly as it shipped. The divider and the `you`/`gmux`
34
+ * speakers use the `paneCyan`/`paneBold` variants instead, which fzf renders
35
+ * with `--ansi` — so the seam between card and chat pops in colour. But the
36
+ * divider is still carried by GLYPHS and the speakers by LAYOUT, so `NO_COLOR`,
37
+ * `TERM=dumb` or a colourless pane loses only the accent, never the structure.
38
+ */
39
+ import { wrapText } from "../core/text.js";
40
+ import { formatCard, indent, paneBold, paneCyan, paneDim } from "./format.js";
41
+ /** Below this the card is a fragment and the chat is unreadable. See COLLAPSED. */
42
+ const MIN_SPLIT = 15;
43
+ /** Enough card to be a card: label, meta, blank, a heading and two lines under it. */
44
+ const CARD_MIN = 6;
45
+ /** Enough chat to read as one: two speakers, two bodies, the blank between them. */
46
+ const CHAT_MIN = 8;
47
+ const DIVIDER_ROWS = 1;
48
+ /**
49
+ * What we assume when fzf did not tell us the pane height.
50
+ *
51
+ * A guess rather than a refusal to split, deliberately: the chat auto-tails, so
52
+ * an over-guess costs a little fzf scrolling, while refusing shows no chat at all
53
+ * to someone who just asked for one.
54
+ */
55
+ const ASSUMED_PANE_ROWS = 24;
56
+ /**
57
+ * How the pane's rows are divided.
58
+ *
59
+ * `paneRows` arrives from `$FZF_PREVIEW_LINES` via a shell and a commander flag,
60
+ * which makes it environment-controlled text. Treat it as hostile: anything
61
+ * non-positive is the guess, and no arithmetic below may go negative or throw.
62
+ */
63
+ export function splitPreview(paneRows, hasChat) {
64
+ const rows = Number.isFinite(paneRows) && paneRows > 0 ? Math.floor(paneRows) : ASSUMED_PANE_ROWS;
65
+ // The empty state, and the whole of it. `cardRows` is the pane because there
66
+ // is nothing to share it with.
67
+ if (!hasChat)
68
+ return { cardRows: rows, dividerRows: 0, chatRows: 0 };
69
+ if (rows < MIN_SPLIT) {
70
+ // COLLAPSED — the answer to "what about a 20-line terminal?", where the pane
71
+ // is 14 rows and two 7-row halves are useless to everybody. The card drops to
72
+ // `formatCard`'s first line (`bold(sessionLabel)`) and the chat takes the
73
+ // rest: you still know which session "this" is, which is the entire point of
74
+ // the focus model, and the chat stays readable. A better failure than an
75
+ // 8-row card fragment.
76
+ const chatRows = Math.max(0, rows - 1 - DIVIDER_ROWS);
77
+ const dividerRows = chatRows > 0 ? 1 : 0;
78
+ return { cardRows: rows - chatRows - dividerRows, dividerRows, chatRows };
79
+ }
80
+ const chatRows = Math.min(Math.max(Math.floor(rows / 2), CHAT_MIN), rows - DIVIDER_ROWS - CARD_MIN);
81
+ return { cardRows: rows - chatRows - DIVIDER_ROWS, dividerRows: 1, chatRows };
82
+ }
83
+ /**
84
+ * `"── ask "` is 7 display columns, so the rendered rule is exactly `width`.
85
+ *
86
+ * Labelled because `format.ts`'s idiom is named sections (`WHERE IT LANDED`,
87
+ * `NEXT STEP`) and an anonymous rule would be the only unnamed boundary on
88
+ * screen. `paneCyan` — gmux's own accent, the `gmux` speaker's colour — so the seam
89
+ * between the monochrome card and the chat is the one line that pops, which is
90
+ * the whole reason it is coloured rather than dim. `paneCyan` not `cyan`: the
91
+ * pane is a pipe, so the gated `cyan` is a no-op here; `paneCyan` drops only the
92
+ * TTY test and still honours `NO_COLOR`. The unicode needs no ASCII fallback —
93
+ * `format.ts` already ships `⚠ ○ ◐ ·` unconditionally.
94
+ */
95
+ const DIVIDER_LABEL = "── ask ";
96
+ /** `Math.max(0, …)`: `String.repeat` throws RangeError on a negative count, and
97
+ * the width came from the environment. A narrow pane must not crash the pane.
98
+ * The colour is zero display columns, so the rule is still exactly `width`. */
99
+ export function askDivider(width) {
100
+ const fill = Number.isFinite(width) ? Math.floor(width) - DIVIDER_LABEL.length : 0;
101
+ return paneCyan(`${DIVIDER_LABEL}${"─".repeat(Math.max(0, fill))}`);
102
+ }
103
+ /**
104
+ * Is there a conversation at all?
105
+ *
106
+ * A question is the only thing that makes one. `meta` alone is a transcript the
107
+ * sender created and nothing more, and it must still render the empty state —
108
+ * otherwise a `no-provider` send would put a divider over an empty half.
109
+ */
110
+ export function hasChatContent(transcript) {
111
+ return transcript !== null && transcript.events.some((event) => event.t === "question");
112
+ }
113
+ function foldBySeq(events) {
114
+ const questions = [];
115
+ const chunks = new Map();
116
+ const settled = new Map();
117
+ for (const event of events) {
118
+ switch (event.t) {
119
+ case "question":
120
+ questions.push(event);
121
+ break;
122
+ case "chunk": {
123
+ const existing = chunks.get(event.seq);
124
+ if (existing)
125
+ existing.push(event.text);
126
+ else
127
+ chunks.set(event.seq, [event.text]);
128
+ break;
129
+ }
130
+ case "end":
131
+ case "aborted":
132
+ settled.set(event.seq, { t: event.t });
133
+ break;
134
+ case "error":
135
+ settled.set(event.seq, { t: "error", message: event.message });
136
+ break;
137
+ default:
138
+ break;
139
+ }
140
+ }
141
+ // Keyed by `seq`, never file order — the same rule `foldCompletedTurns` folds
142
+ // by, and for the same reason: correctness here must not depend on a lock in
143
+ // another module.
144
+ questions.sort((a, b) => a.seq - b.seq);
145
+ return { questions, chunks, settled };
146
+ }
147
+ /** Elapsed whole seconds since a question landed. Computed at render time and
148
+ * never stored — a number written into the transcript is stale on arrival. */
149
+ function elapsedSeconds(at, now) {
150
+ const started = Date.parse(at);
151
+ if (!Number.isFinite(started))
152
+ return 0;
153
+ return Math.max(0, Math.floor((now.getTime() - started) / 1000));
154
+ }
155
+ /**
156
+ * The answer slot, in every state a turn can be in.
157
+ *
158
+ * The in-flight render is specified rather than implied because it IS the UX for
159
+ * most of a turn's life: the provider buffers, so `thinking… 14s` is what the
160
+ * pane says for ~9–20s and the answer then lands in one paint.
161
+ */
162
+ function answerText(seq, question, folded, now) {
163
+ const partial = (folded.chunks.get(seq) ?? []).join("").trim();
164
+ const settled = folded.settled.get(seq);
165
+ if (!settled) {
166
+ return partial === ""
167
+ ? `thinking… ${elapsedSeconds(question.at, now)}s (esc to cancel)`
168
+ : partial;
169
+ }
170
+ if (settled.t === "error")
171
+ return settled.message ?? "the answer failed";
172
+ if (settled.t === "aborted") {
173
+ // The human still SEES an aborted turn here; only the model's view loses it
174
+ // (`foldCompletedTurns` drops it, so "ok but briefly" after an esc has no
175
+ // antecedent — which beats telling the model it failed and being apologized
176
+ // at instead of answered).
177
+ return partial === "" ? "(cancelled)" : `${partial}\n(cancelled)`;
178
+ }
179
+ return partial;
180
+ }
181
+ /**
182
+ * `you` / `gmux`, with the body indented under it.
183
+ *
184
+ * Legible with colour off, which the pane requires: the speaker sits on its own
185
+ * line above an indented body, so the LAYOUT distinguishes them and the colour
186
+ * only accelerates it. `paneBold` is the section-heading idiom; `paneCyan` is
187
+ * already gmux's own colour (the `where` column, the `○` marker) — the forced
188
+ * variants, because the pane is a pipe and the gated ones would be no-ops here.
189
+ */
190
+ function speaker(head, body, width) {
191
+ const wrapped = body
192
+ .split("\n")
193
+ .flatMap((line) => wrapText(line, Math.max(1, Math.floor(width) - 2)));
194
+ return [head, ...indent(wrapped.join("\n")).split("\n")];
195
+ }
196
+ /**
197
+ * The `· re:` suffix marks a CHANGE of focus, and nothing else.
198
+ *
199
+ * Focus re-points per question, so a thread of five questions can span five
200
+ * sessions and an answer rendered under a cursor that has since moved reads as
201
+ * being about the wrong one. But stamping every question with the same id when
202
+ * focus never moved is noise on the half of the pane with least room for it. The
203
+ * change is the only moment it carries information — plus the first question,
204
+ * which has nothing to differ from and everything to establish.
205
+ */
206
+ function questionHead(question, previousFocus, first) {
207
+ const focus = question.focus;
208
+ if (!focus || (!first && focus === previousFocus))
209
+ return paneBold("you");
210
+ return `${paneBold("you")} ${paneDim(`· re: ${focus.slice(0, 8)}`)}`;
211
+ }
212
+ /**
213
+ * The chat half: the last `rows` wrapped rows of the thread.
214
+ *
215
+ * **Auto-tail.** New text appears at the bottom, old text slides up. That is what
216
+ * makes it read as a chat rather than a document, it composes with the refresh
217
+ * loop for free, and it means the common case needs no scrolling at all. There is
218
+ * deliberately no scroll: fzf's own `shift-up` scrolls the WHOLE preview, which
219
+ * in split mode drags the card off the top and reveals nothing, because there is
220
+ * only one pane. If you hit the ceiling, `cat` the transcript.
221
+ *
222
+ * `now` is a parameter so the `thinking… Ns` count is testable without a clock.
223
+ */
224
+ export function formatChat(transcript, rows, width, now = new Date()) {
225
+ const folded = foldBySeq(transcript.events);
226
+ const lines = [];
227
+ let previousFocus = null;
228
+ for (const [index, question] of folded.questions.entries()) {
229
+ if (index > 0)
230
+ lines.push("");
231
+ lines.push(...speaker(questionHead(question, previousFocus, index === 0), question.text, width));
232
+ lines.push("");
233
+ lines.push(...speaker(paneCyan("gmux"), answerText(question.seq, question, folded, now), width));
234
+ previousFocus = question.focus;
235
+ }
236
+ const budget = Number.isFinite(rows) && rows > 0 ? Math.floor(rows) : 0;
237
+ return lines.slice(-budget || lines.length).join("\n");
238
+ }
239
+ /**
240
+ * The card, clipped at the divider, counted in DISPLAY rows.
241
+ *
242
+ * `--preview-window=…,wrap` breaks a card line at the pane's right edge, and
243
+ * `formatCard` emits lines up to 647 columns — so a divider placed after
244
+ * `cardRows` *logical* lines would be shoved off the pane it exists to split.
245
+ * Breaking at the same column fzf would gives the same picture and puts the
246
+ * divider where we said it goes.
247
+ *
248
+ * The card is clipped and not rewritten: measured, it renders 23–83 rows against
249
+ * a 14–41 row pane, so it already overflows the FULL pane by 1.2x–6x at every
250
+ * realistic size. The split does not break the card; the card was already
251
+ * clipped, and fixing that is `gmux show`'s bug, not this pane's.
252
+ */
253
+ function clipToRows(lines, rows, width) {
254
+ const out = [];
255
+ for (const line of lines) {
256
+ if (out.length >= rows)
257
+ break;
258
+ for (const piece of hardWrap(line, width)) {
259
+ if (out.length >= rows)
260
+ break;
261
+ out.push(piece);
262
+ }
263
+ }
264
+ return out;
265
+ }
266
+ function hardWrap(line, width) {
267
+ const w = Math.floor(width);
268
+ if (!Number.isFinite(w) || w <= 0 || line.length <= w)
269
+ return [line];
270
+ const pieces = [];
271
+ for (let i = 0; i < line.length; i += w)
272
+ pieces.push(line.slice(i, i + w));
273
+ return pieces;
274
+ }
275
+ /**
276
+ * One preview command's whole output: the card, and the chat if there is one.
277
+ *
278
+ * **The empty state returns `formatCard` and NOTHING else** — not "the card plus
279
+ * an empty tail", not "the card with a trailing divider". Byte-identical, because
280
+ * that is what decision 4 promises to everyone who never presses ctrl-o.
281
+ */
282
+ export function formatPreview(view, transcript, split, width, now = new Date()) {
283
+ const card = formatCard(view, now);
284
+ if (transcript === null || split.chatRows <= 0 || !hasChatContent(transcript))
285
+ return card;
286
+ return [
287
+ ...clipToRows(card.split("\n"), split.cardRows, width),
288
+ askDivider(width),
289
+ formatChat(transcript, split.chatRows, width, now),
290
+ ].join("\n");
291
+ }
292
+ //# sourceMappingURL=preview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.js","sourceRoot":"","sources":["../../src/cli/preview.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The pane-border-label HUD: each pane's session headline, written into its tmux
3
+ * pane title and shown on the pane border. The `gmux watch` service repaints these
4
+ * on a loop; `Alt-g` toggles the service.
5
+ *
6
+ * Labels live in a per-pane `@gm_label` option, NOT `#{pane_title}`: a running
7
+ * agent sets its pane title via OSC escape sequences (a progress spinner), which
8
+ * would clobber a title we wrote. A `@`-prefixed user option is ours alone.
9
+ */
10
+ import type { PaneEntry, PaneState } from "../core/gmux-types.js";
11
+ import type { SessionRecord, SessionView, TmuxPane } from "../core/types.js";
12
+ /**
13
+ * The label for a pane: `project — headline`, `project — gmux summaries loading…`
14
+ * while a refresh is in flight, `○ project` when resolved but not yet summarised,
15
+ * or empty for a pane with no resolvable agent. Not truncated — tmux clips it to
16
+ * the pane's width at render.
17
+ */
18
+ export declare function paneLabel(view: SessionView | null, refreshing?: boolean): string;
19
+ /** Phase 0 state glyph, shown in the daemon-driven border label ahead of the project name. */
20
+ export declare function stateGlyph(state: PaneState): string;
21
+ /**
22
+ * The daemon-driven border label for one pane: `<glyph> <project> — <headline>`.
23
+ * Zero sensing — reads only the model entry. Falls back to the pane's command
24
+ * when the cwd is empty, and to the raw state name until semantics arrive.
25
+ */
26
+ export declare function snapshotLabel(entry: PaneEntry): string;
27
+ /**
28
+ * Resolve a set of panes and write each one's `@gm_label`. Returns the resolved
29
+ * session records, so the caller can feed exactly the on-screen sessions to the
30
+ * summariser. No model calls here — labels render from the cache.
31
+ */
32
+ export declare function labelPanes(panes: readonly TmuxPane[]): Promise<SessionRecord[]>;
33
+ /** Label every pane in a window (or all windows when `windowId` is null). */
34
+ export declare function paintLabels(windowId: string | null): Promise<SessionRecord[]>;
35
+ /** Show the label borders across all windows, driven purely by the global option. */
36
+ export declare function enableBorder(): Promise<void>;
37
+ /**
38
+ * Hide the label borders. Sets the global off, then clears any per-window
39
+ * override (older versions set `pane-border-status` per window, which would
40
+ * override the global and keep the border up) and wipes every pane's `@gm_label`,
41
+ * so no headline lingers regardless of theme or leftover settings.
42
+ */
43
+ export declare function disableBorder(): Promise<void>;
44
+ /**
45
+ * `Alt-g`: toggle the background label agent. On when off (enable the borders,
46
+ * paint an immediate first frame for the current window, start the watcher); off
47
+ * when on (stop the watcher, hide the borders). "On" is the service running, not
48
+ * the border option — a theme may set the border for its own reasons.
49
+ */
50
+ export declare function toggleWatch(windowId: string): Promise<"on" | "off">;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The pane-border-label HUD: each pane's session headline, written into its tmux
3
+ * pane title and shown on the pane border. The `gmux watch` service repaints these
4
+ * on a loop; `Alt-g` toggles the service.
5
+ *
6
+ * Labels live in a per-pane `@gm_label` option, NOT `#{pane_title}`: a running
7
+ * agent sets its pane title via OSC escape sequences (a progress spinner), which
8
+ * would clobber a title we wrote. A `@`-prefixed user option is ours alone.
9
+ */
10
+ import { execFile } from "node:child_process";
11
+ import { basename } from "node:path";
12
+ import { promisify } from "node:util";
13
+ import { inProgressIds } from "../services/auto-summarize.js";
14
+ import { prunePaneLinks } from "../services/pane-links.js";
15
+ import { listAllPanes, listPanes } from "../services/tmux.js";
16
+ import { resolvePanesLive } from "../services/tmux-resolve.js";
17
+ import { attachSummaries, loadCachedRecords } from "../services/views.js";
18
+ import { isWatchRunning, startWatch, stopWatch, } from "../services/watch.js";
19
+ const run = promisify(execFile);
20
+ /**
21
+ * Active pane emphasised, label centred. Forces its own foreground colour so it
22
+ * doesn't inherit a themed, dimmed `pane-border-style` (which would leave every
23
+ * inactive pane's label invisible).
24
+ */
25
+ const PANE_BORDER_FORMAT = "#[align=centre]#[fg=colour252]#{?pane_active,#[reverse],} #{@gm_label} #[default]";
26
+ /**
27
+ * The label for a pane: `project — headline`, `project — gmux summaries loading…`
28
+ * while a refresh is in flight, `○ project` when resolved but not yet summarised,
29
+ * or empty for a pane with no resolvable agent. Not truncated — tmux clips it to
30
+ * the pane's width at render.
31
+ */
32
+ export function paneLabel(view, refreshing = false) {
33
+ if (!view)
34
+ return "";
35
+ const project = view.record.project ?? view.record.harness;
36
+ if (refreshing)
37
+ return `${project} — gmux summaries loading…`;
38
+ if (!view.summary)
39
+ return `○ ${project}`;
40
+ return `${project} — ${view.summary.headline}`;
41
+ }
42
+ /** Phase 0 state glyph, shown in the daemon-driven border label ahead of the project name. */
43
+ export function stateGlyph(state) {
44
+ switch (state) {
45
+ case "working": return "●";
46
+ case "waiting": return "◔";
47
+ case "error": return "✗";
48
+ case "done": return "✓";
49
+ case "idle": return "○";
50
+ }
51
+ }
52
+ /**
53
+ * The daemon-driven border label for one pane: `<glyph> <project> — <headline>`.
54
+ * Zero sensing — reads only the model entry. Falls back to the pane's command
55
+ * when the cwd is empty, and to the raw state name until semantics arrive.
56
+ */
57
+ export function snapshotLabel(entry) {
58
+ const name = entry.identity.cwd ? basename(entry.identity.cwd) : entry.identity.command;
59
+ const text = entry.semantics?.label ?? entry.state;
60
+ return `${stateGlyph(entry.state)} ${name} — ${text}`;
61
+ }
62
+ async function tmux(args) {
63
+ const { stdout } = await run("tmux", args);
64
+ return stdout;
65
+ }
66
+ /**
67
+ * Resolve a set of panes and write each one's `@gm_label`. Returns the resolved
68
+ * session records, so the caller can feed exactly the on-screen sessions to the
69
+ * summariser. No model calls here — labels render from the cache.
70
+ */
71
+ export async function labelPanes(panes) {
72
+ const links = await prunePaneLinks(panes.map((p) => p.paneId));
73
+ const records = await loadCachedRecords();
74
+ const resolved = await resolvePanesLive(panes, records, links);
75
+ const present = resolved
76
+ .map((r) => r.record)
77
+ .filter((r) => r !== null);
78
+ const views = await attachSummaries(present);
79
+ const bySession = new Map(views.map((v) => [v.record.sessionId, v]));
80
+ const refreshing = await inProgressIds();
81
+ for (const { pane, record } of resolved) {
82
+ const view = record
83
+ ? bySession.get(record.sessionId) ?? { record, summary: null }
84
+ : null;
85
+ const isRefreshing = record ? refreshing.has(record.sessionId) : false;
86
+ await tmux(["set-option", "-p", "-t", pane.paneId, "@gm_label", paneLabel(view, isRefreshing)]);
87
+ }
88
+ return present;
89
+ }
90
+ /** Label every pane in a window (or all windows when `windowId` is null). */
91
+ export async function paintLabels(windowId) {
92
+ const panes = windowId === null ? await listAllPanes() : await listPanes(windowId);
93
+ return labelPanes(panes);
94
+ }
95
+ async function windowIds() {
96
+ const out = await tmux(["list-windows", "-a", "-F", "#{window_id}"]);
97
+ return out.split("\n").map((line) => line.trim()).filter(Boolean);
98
+ }
99
+ /** Clear any per-window `pane-border-status` so the global option governs every window. */
100
+ async function clearWindowOverrides() {
101
+ for (const w of await windowIds()) {
102
+ await tmux(["set-option", "-w", "-u", "-t", w, "pane-border-status"]);
103
+ }
104
+ }
105
+ /** Show the label borders across all windows, driven purely by the global option. */
106
+ export async function enableBorder() {
107
+ await clearWindowOverrides();
108
+ await tmux(["set-option", "-g", "pane-border-format", PANE_BORDER_FORMAT]);
109
+ await tmux(["set-option", "-g", "pane-border-status", "top"]);
110
+ }
111
+ /**
112
+ * Hide the label borders. Sets the global off, then clears any per-window
113
+ * override (older versions set `pane-border-status` per window, which would
114
+ * override the global and keep the border up) and wipes every pane's `@gm_label`,
115
+ * so no headline lingers regardless of theme or leftover settings.
116
+ */
117
+ export async function disableBorder() {
118
+ await tmux(["set-option", "-g", "pane-border-status", "off"]);
119
+ try {
120
+ await clearWindowOverrides();
121
+ for (const pane of await listAllPanes()) {
122
+ await tmux(["set-option", "-p", "-u", "-t", pane.paneId, "@gm_label"]);
123
+ }
124
+ }
125
+ catch {
126
+ // Best-effort cleanup; the global 'off' has already hidden the borders.
127
+ }
128
+ }
129
+ /**
130
+ * `Alt-g`: toggle the background label agent. On when off (enable the borders,
131
+ * paint an immediate first frame for the current window, start the watcher); off
132
+ * when on (stop the watcher, hide the borders). "On" is the service running, not
133
+ * the border option — a theme may set the border for its own reasons.
134
+ */
135
+ export async function toggleWatch(windowId) {
136
+ if (await isWatchRunning()) {
137
+ await stopWatch();
138
+ await disableBorder();
139
+ return "off";
140
+ }
141
+ await enableBorder();
142
+ await paintLabels(windowId); // instant first paint, before the loop's first tick
143
+ await startWatch();
144
+ return "on";
145
+ }
146
+ //# sourceMappingURL=tmux-label.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tmux-label.js","sourceRoot":"","sources":["../../src/cli/tmux-label.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAItC,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,EACL,cAAc,EACd,UAAU,EACV,SAAS,GACV,MAAM,sBAAsB,CAAC;AAE9B,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAEhC;;;;GAIG;AACH,MAAM,kBAAkB,GACtB,mFAAmF,CAAC;AAEtF;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,IAAwB,EAAE,UAAU,GAAG,KAAK;IACpE,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;IAC3D,IAAI,UAAU;QAAE,OAAO,GAAG,OAAO,4BAA4B,CAAC;IAC9D,IAAI,CAAC,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,OAAO,EAAE,CAAC;IACzC,OAAO,GAAG,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;AACjD,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,UAAU,CAAC,KAAgB;IACzC,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,SAAS,CAAC,CAAC,OAAO,GAAG,CAAC;QAC3B,KAAK,SAAS,CAAC,CAAC,OAAO,GAAG,CAAC;QAC3B,KAAK,OAAO,CAAC,CAAC,OAAO,GAAG,CAAC;QACzB,KAAK,MAAM,CAAC,CAAC,OAAO,GAAG,CAAC;QACxB,KAAK,MAAM,CAAC,CAAC,OAAO,GAAG,CAAC;IAC1B,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAgB;IAC5C,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;IACxF,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,EAAE,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC;IACnD,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC;AACxD,CAAC;AAED,KAAK,UAAU,IAAI,CAAC,IAAc;IAChC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC3C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,KAA0B;IACzD,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/D,MAAM,OAAO,GAAG,MAAM,iBAAiB,EAAE,CAAC;IAC1C,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAE/D,MAAM,OAAO,GAAG,QAAQ;SACrB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAA8B,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IACzD,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACrE,MAAM,UAAU,GAAG,MAAM,aAAa,EAAE,CAAC;IAEzC,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;QACxC,MAAM,IAAI,GAAuB,MAAM;YACrC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;YAC9D,CAAC,CAAC,IAAI,CAAC;QACT,MAAM,YAAY,GAAG,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QACvE,MAAM,IAAI,CAAC,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAClG,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAuB;IACvD,MAAM,KAAK,GAAG,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,YAAY,EAAE,CAAC,CAAC,CAAC,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC;IACnF,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC;IACrE,OAAO,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AACpE,CAAC;AAED,2FAA2F;AAC3F,KAAK,UAAU,oBAAoB;IACjC,KAAK,MAAM,CAAC,IAAI,MAAM,SAAS,EAAE,EAAE,CAAC;QAClC,MAAM,IAAI,CAAC,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC;IACxE,CAAC;AACH,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,MAAM,oBAAoB,EAAE,CAAC;IAC7B,MAAM,IAAI,CAAC,CAAC,YAAY,EAAE,IAAI,EAAE,oBAAoB,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAC3E,MAAM,IAAI,CAAC,CAAC,YAAY,EAAE,IAAI,EAAE,oBAAoB,EAAE,KAAK,CAAC,CAAC,CAAC;AAChE,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,MAAM,IAAI,CAAC,CAAC,YAAY,EAAE,IAAI,EAAE,oBAAoB,EAAE,KAAK,CAAC,CAAC,CAAC;IAC9D,IAAI,CAAC;QACH,MAAM,oBAAoB,EAAE,CAAC;QAC7B,KAAK,MAAM,IAAI,IAAI,MAAM,YAAY,EAAE,EAAE,CAAC;YACxC,MAAM,IAAI,CAAC,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;IAC1E,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAgB;IAChD,IAAI,MAAM,cAAc,EAAE,EAAE,CAAC;QAC3B,MAAM,SAAS,EAAE,CAAC;QAClB,MAAM,aAAa,EAAE,CAAC;QACtB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,YAAY,EAAE,CAAC;IACrB,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,oDAAoD;IACjF,MAAM,UAAU,EAAE,CAAC;IACnB,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Error taxonomy.
3
+ *
4
+ * Harness-engineering rule: an error message carries the fix inline. A user —
5
+ * or an agent — should never have to guess the next command. `fix` is printed
6
+ * directly beneath the message and included in `--json` output.
7
+ */
8
+ export declare class GmuxError extends Error {
9
+ /** The exact command or action that resolves this error. */
10
+ readonly fix: string | undefined;
11
+ /** Process exit code. */
12
+ readonly exitCode: number;
13
+ constructor(message: string, options?: {
14
+ fix?: string;
15
+ exitCode?: number;
16
+ });
17
+ }
18
+ /** No session matched the given id or prefix. */
19
+ export declare class SessionNotFoundError extends GmuxError {
20
+ constructor(id: string);
21
+ }
22
+ /** A prefix matched more than one session. */
23
+ export declare class AmbiguousSessionError extends GmuxError {
24
+ constructor(id: string, matches: string[]);
25
+ }
26
+ /** A required external binary is missing. */
27
+ export declare class MissingDependencyError extends GmuxError {
28
+ constructor(binary: string, fix: string);
29
+ }
30
+ /** A summary provider returned something unusable. */
31
+ export declare class SummaryProviderError extends GmuxError {
32
+ constructor(provider: string, detail: string);
33
+ }
34
+ /**
35
+ * The chat provider behind `gmux ask` failed or said nothing.
36
+ *
37
+ * The fix names the provider that actually failed. It used to hardcode
38
+ * `claude -p`, which told a user who had configured Codex to go and test a
39
+ * binary they may not even have — non-negotiable #5 asks for the fix to the
40
+ * problem in front of you, not to the common case.
41
+ */
42
+ export declare class AskProviderError extends GmuxError {
43
+ constructor(provider: string, detail: string);
44
+ }
45
+ /**
46
+ * No provider is configured — because the user chose "none" in `gmux setup`.
47
+ *
48
+ * Distinct from a missing binary, and it must stay distinct: this is a choice
49
+ * being honored, not a fault. The fix says how to change your mind, not how to
50
+ * install something.
51
+ */
52
+ export declare class NoProviderError extends GmuxError {
53
+ constructor(what: string);
54
+ }