@mu-crew/mu 3.13.3 → 4.0.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 (119) hide show
  1. package/AGENTS.md +73 -35
  2. package/README.md +71 -57
  3. package/dist/chunk-5HECDBK3.js +4482 -0
  4. package/dist/chunk-5HECDBK3.js.map +1 -0
  5. package/dist/chunk-AW2NONYU.js +301 -0
  6. package/dist/chunk-AW2NONYU.js.map +1 -0
  7. package/dist/chunk-GQNHIPJN.js +4060 -0
  8. package/dist/chunk-GQNHIPJN.js.map +1 -0
  9. package/dist/{chunk-3BARPJLY.js → chunk-PZ5GOCWH.js} +16 -2
  10. package/dist/chunk-PZ5GOCWH.js.map +1 -0
  11. package/dist/chunk-S424D5EP.js +13766 -0
  12. package/dist/chunk-S424D5EP.js.map +1 -0
  13. package/dist/{cli-CKOMHBXV.js → cli-GB2CAVVW.js} +6776 -4350
  14. package/dist/cli-GB2CAVVW.js.map +1 -0
  15. package/dist/cli.js +2 -3
  16. package/dist/cli.js.map +1 -1
  17. package/dist/errors-KHOYKD7D.js +65 -0
  18. package/dist/extension/mu-pi.js +1617 -836
  19. package/dist/extension/mu-pi.js.map +1 -1
  20. package/dist/index.d.ts +4015 -2561
  21. package/dist/index.js +614 -297
  22. package/dist/index.js.map +1 -1
  23. package/dist/tui-ALPHYTA2.js +1773 -0
  24. package/dist/tui-ALPHYTA2.js.map +1 -0
  25. package/docs/ARCHITECTURE.md +117 -66
  26. package/docs/VOCABULARY.md +112 -49
  27. package/docs/guide/README.md +3 -3
  28. package/docs/guide/backends.md +8 -4
  29. package/docs/guide/cleanup.md +61 -33
  30. package/docs/guide/delegate.md +141 -66
  31. package/docs/guide/dispatch.md +148 -53
  32. package/docs/guide/getting-started.md +37 -44
  33. package/docs/guide/other-agent-clis.md +102 -0
  34. package/docs/guide/recovery.md +31 -50
  35. package/docs/guide/remote.md +115 -45
  36. package/docs/guide/sql.md +104 -24
  37. package/docs/guide/stop-a-worker.md +30 -37
  38. package/docs/guide/sync.md +11 -3
  39. package/docs/guide/upgrade.md +93 -43
  40. package/docs/reference/env.md +23 -32
  41. package/docs/reference/naming.md +12 -7
  42. package/docs/reference/surface-contract.md +153 -0
  43. package/package.json +33 -11
  44. package/prompts/mu-debug.md +3 -1
  45. package/prompts/mu-refute.md +2 -1
  46. package/prompts/mu-research.md +3 -1
  47. package/prompts/mu-review.md +2 -1
  48. package/prompts/mu-rules-audit.md +5 -4
  49. package/prompts/mu-sweep.md +2 -1
  50. package/prompts/mu-tournament.md +4 -2
  51. package/prompts/mu-until.md +3 -1
  52. package/prompts/ultrathink.md +1 -1
  53. package/skills/mu/SKILL.md +60 -291
  54. package/skills/mu/recipes/adversarial-review.md +12 -103
  55. package/skills/mu/recipes/brief.md +14 -61
  56. package/skills/mu/recipes/course-correct.md +23 -0
  57. package/skills/mu/recipes/findings.md +17 -111
  58. package/skills/mu/recipes/models.md +17 -92
  59. package/skills/mu/recipes/move.md +14 -0
  60. package/skills/mu/recipes/orchestrator-loop.md +20 -119
  61. package/skills/mu/recipes/recovery.md +8 -29
  62. package/skills/mu/recipes/remote-workers.md +16 -318
  63. package/skills/mu/recipes/tasks-or-calls.md +15 -78
  64. package/skills/mu/recipes/test-health.md +19 -77
  65. package/skills/mu/recipes/worker.md +17 -24
  66. package/dist/chunk-45KQLMEL.js +0 -2908
  67. package/dist/chunk-45KQLMEL.js.map +0 -1
  68. package/dist/chunk-G434JKV2.js +0 -641
  69. package/dist/chunk-G434JKV2.js.map +0 -1
  70. package/dist/chunk-KUGUFBWH.js +0 -12527
  71. package/dist/chunk-KUGUFBWH.js.map +0 -1
  72. package/dist/chunk-QITLNZEM.js +0 -87
  73. package/dist/chunk-QITLNZEM.js.map +0 -1
  74. package/dist/chunk-VIEYQMQA.js +0 -393
  75. package/dist/chunk-VIEYQMQA.js.map +0 -1
  76. package/dist/chunk-WI6AWCMK.js +0 -19
  77. package/dist/chunk-WI6AWCMK.js.map +0 -1
  78. package/dist/cli-CKOMHBXV.js.map +0 -1
  79. package/dist/devtools-LS4ZEPWM.js +0 -3772
  80. package/dist/devtools-LS4ZEPWM.js.map +0 -1
  81. package/dist/tui-LFJ5P4Y2.js +0 -40956
  82. package/dist/tui-LFJ5P4Y2.js.map +0 -1
  83. package/dist/tui-sync-worker.js +0 -17
  84. package/dist/tui-sync-worker.js.map +0 -1
  85. package/dist/yank-XDOONQSL.js +0 -13
  86. package/dist/yank-XDOONQSL.js.map +0 -1
  87. package/docs/ROADMAP.md +0 -123
  88. package/docs/VISION.md +0 -298
  89. package/docs/architecture/control-socket.md +0 -142
  90. package/docs/architecture/dag.md +0 -117
  91. package/docs/architecture/mux.md +0 -167
  92. package/docs/architecture/ops-log.md +0 -160
  93. package/docs/architecture/sdk.md +0 -187
  94. package/docs/architecture/sync.md +0 -159
  95. package/docs/architecture/tui.md +0 -165
  96. package/docs/guide/tui.md +0 -98
  97. package/docs/history/CHANGELOG-pre-3.md +0 -4866
  98. package/docs/img/tui-dashboard.png +0 -0
  99. package/docs/specs/2026-09-28-agent-state-from-murmur.md +0 -333
  100. package/docs/specs/2026-09-29-task-substates.md +0 -247
  101. package/docs/specs/2026-10-03-keep-driving-nudge.md +0 -163
  102. package/docs/specs/2026-10-21-fast-and-slow-gates.md +0 -229
  103. package/skills/mu/recipes/backlog-triage.md +0 -45
  104. package/skills/mu/recipes/codemode-driver.md +0 -118
  105. package/skills/mu/recipes/deep-research.md +0 -69
  106. package/skills/mu/recipes/drift-audit.md +0 -75
  107. package/skills/mu/recipes/fan-out.md +0 -42
  108. package/skills/mu/recipes/hypothesis-panel.md +0 -48
  109. package/skills/mu/recipes/long-run.md +0 -32
  110. package/skills/mu/recipes/loop-until-done.md +0 -45
  111. package/skills/mu/recipes/plan.md +0 -67
  112. package/skills/mu/recipes/refute.md +0 -55
  113. package/skills/mu/recipes/review-panel.md +0 -65
  114. package/skills/mu/recipes/rules-audit.md +0 -89
  115. package/skills/mu/recipes/tournament.md +0 -44
  116. package/skills/mu/recipes/ultrathink.md +0 -86
  117. package/skills/mu/recipes/watcher.md +0 -28
  118. package/skills/mu/recipes/waves.md +0 -21
  119. /package/dist/{chunk-3BARPJLY.js.map → errors-KHOYKD7D.js.map} +0 -0
@@ -0,0 +1,1773 @@
1
+ import { createRequire as __muCreateRequire } from "node:module"; const require = __muCreateRequire(import.meta.url);
2
+ import {
3
+ formatElapsed,
4
+ loadDashboard,
5
+ matches
6
+ } from "./chunk-AW2NONYU.js";
7
+ import {
8
+ KNOWN_VERBS,
9
+ ambientIngest,
10
+ currentCallers,
11
+ parseOpKey,
12
+ syncEnabled
13
+ } from "./chunk-GQNHIPJN.js";
14
+ import {
15
+ DEFAULT_SUBSTATE,
16
+ TASK_STATUSES,
17
+ TASK_SUBSTATES,
18
+ activeMux,
19
+ attachShellLine,
20
+ formatPair,
21
+ getTask,
22
+ isTaskStatus,
23
+ openDb,
24
+ resolveMuxSessionWorkstreamName,
25
+ selectTaskNotes,
26
+ shellWord,
27
+ spawnerFromEnv,
28
+ tmuxAttachMode
29
+ } from "./chunk-S424D5EP.js";
30
+ import "./chunk-PZ5GOCWH.js";
31
+
32
+ // src/cli/tui/index.ts
33
+ import { render } from "ink";
34
+ import { createElement } from "react";
35
+
36
+ // src/cli/tui/app.tsx
37
+ import { Box as Box4, Text as Text3, useApp, useInput as useInput2, useStdin, useStdout as useStdout2, useWindowSize as useWindowSize2 } from "ink";
38
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
39
+
40
+ // node_modules/ansi-regex/index.js
41
+ function ansiRegex({ onlyFirst = false } = {}) {
42
+ const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
43
+ const osc = `(?:\\u001B\\][^\\u0007\\u001B\\u009C]*${ST})`;
44
+ const csi = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]";
45
+ const pattern = `${osc}|${csi}`;
46
+ return new RegExp(pattern, onlyFirst ? void 0 : "g");
47
+ }
48
+
49
+ // node_modules/strip-ansi/index.js
50
+ var regex = ansiRegex();
51
+ function stripAnsi(string) {
52
+ if (typeof string !== "string") {
53
+ throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``);
54
+ }
55
+ if (!string.includes("\x1B") && !string.includes("\x9B")) {
56
+ return string;
57
+ }
58
+ return string.replace(regex, "");
59
+ }
60
+
61
+ // node_modules/get-east-asian-width/lookup-data.js
62
+ var ambiguousMinimalCodePoint = 161;
63
+ var ambiguousMaximumCodePoint = 1114109;
64
+ var ambiguousRanges = [161, 161, 164, 164, 167, 168, 170, 170, 173, 174, 176, 180, 182, 186, 188, 191, 198, 198, 208, 208, 215, 216, 222, 225, 230, 230, 232, 234, 236, 237, 240, 240, 242, 243, 247, 250, 252, 252, 254, 254, 257, 257, 273, 273, 275, 275, 283, 283, 294, 295, 299, 299, 305, 307, 312, 312, 319, 322, 324, 324, 328, 331, 333, 333, 338, 339, 358, 359, 363, 363, 462, 462, 464, 464, 466, 466, 468, 468, 470, 470, 472, 472, 474, 474, 476, 476, 593, 593, 609, 609, 708, 708, 711, 711, 713, 715, 717, 717, 720, 720, 728, 731, 733, 733, 735, 735, 768, 879, 913, 929, 931, 937, 945, 961, 963, 969, 1025, 1025, 1040, 1103, 1105, 1105, 8208, 8208, 8211, 8214, 8216, 8217, 8220, 8221, 8224, 8226, 8228, 8231, 8240, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8254, 8254, 8308, 8308, 8319, 8319, 8321, 8324, 8364, 8364, 8451, 8451, 8453, 8453, 8457, 8457, 8467, 8467, 8470, 8470, 8481, 8482, 8486, 8486, 8491, 8491, 8531, 8532, 8539, 8542, 8544, 8555, 8560, 8569, 8585, 8585, 8592, 8601, 8632, 8633, 8658, 8658, 8660, 8660, 8679, 8679, 8704, 8704, 8706, 8707, 8711, 8712, 8715, 8715, 8719, 8719, 8721, 8721, 8725, 8725, 8730, 8730, 8733, 8736, 8739, 8739, 8741, 8741, 8743, 8748, 8750, 8750, 8756, 8759, 8764, 8765, 8776, 8776, 8780, 8780, 8786, 8786, 8800, 8801, 8804, 8807, 8810, 8811, 8814, 8815, 8834, 8835, 8838, 8839, 8853, 8853, 8857, 8857, 8869, 8869, 8895, 8895, 8978, 8978, 9312, 9449, 9451, 9547, 9552, 9587, 9600, 9615, 9618, 9621, 9632, 9633, 9635, 9641, 9650, 9651, 9654, 9655, 9660, 9661, 9664, 9665, 9670, 9672, 9675, 9675, 9678, 9681, 9698, 9701, 9711, 9711, 9733, 9734, 9737, 9737, 9742, 9743, 9756, 9756, 9758, 9758, 9792, 9792, 9794, 9794, 9824, 9825, 9827, 9829, 9831, 9834, 9836, 9837, 9839, 9839, 9886, 9887, 9919, 9919, 9926, 9933, 9935, 9939, 9941, 9953, 9955, 9955, 9960, 9961, 9963, 9969, 9972, 9972, 9974, 9977, 9979, 9980, 9982, 9983, 10045, 10045, 10102, 10111, 11094, 11097, 12872, 12879, 57344, 63743, 65024, 65039, 65533, 65533, 127232, 127242, 127248, 127277, 127280, 127337, 127344, 127373, 127375, 127376, 127387, 127404, 917760, 917999, 983040, 1048573, 1048576, 1114109];
65
+ var fullwidthMinimalCodePoint = 12288;
66
+ var fullwidthMaximumCodePoint = 65510;
67
+ var fullwidthRanges = [12288, 12288, 65281, 65376, 65504, 65510];
68
+ var wideMinimalCodePoint = 4352;
69
+ var wideMaximumCodePoint = 262141;
70
+ var wideRanges = [4352, 4447, 8986, 8987, 9001, 9002, 9193, 9196, 9200, 9200, 9203, 9203, 9725, 9726, 9748, 9749, 9776, 9783, 9800, 9811, 9855, 9855, 9866, 9871, 9875, 9875, 9889, 9889, 9898, 9899, 9917, 9918, 9924, 9925, 9934, 9934, 9940, 9940, 9962, 9962, 9970, 9971, 9973, 9973, 9978, 9978, 9981, 9981, 9989, 9989, 9994, 9995, 10024, 10024, 10060, 10060, 10062, 10062, 10067, 10069, 10071, 10071, 10133, 10135, 10160, 10160, 10175, 10175, 11035, 11036, 11088, 11088, 11093, 11093, 11904, 11929, 11931, 12019, 12032, 12245, 12272, 12287, 12289, 12350, 12353, 12438, 12441, 12543, 12549, 12591, 12593, 12686, 12688, 12773, 12783, 12830, 12832, 12871, 12880, 42124, 42128, 42182, 43360, 43388, 44032, 55203, 63744, 64255, 65040, 65049, 65072, 65106, 65108, 65126, 65128, 65131, 94176, 94180, 94192, 94198, 94208, 101594, 101631, 101664, 101760, 101874, 101888, 102801, 102816, 102866, 110576, 110579, 110581, 110587, 110589, 110590, 110592, 110888, 110898, 110898, 110928, 110930, 110933, 110933, 110948, 110952, 110960, 111355, 119552, 119638, 119648, 119670, 126980, 126980, 127183, 127183, 127374, 127374, 127377, 127386, 127406, 127406, 127488, 127490, 127504, 127547, 127552, 127560, 127568, 127569, 127584, 127589, 127744, 127776, 127789, 127797, 127799, 127868, 127870, 127891, 127904, 127946, 127951, 127955, 127968, 127984, 127988, 127988, 127992, 128062, 128064, 128064, 128066, 128252, 128255, 128317, 128331, 128334, 128336, 128359, 128378, 128378, 128405, 128406, 128420, 128420, 128507, 128591, 128640, 128709, 128716, 128716, 128720, 128722, 128725, 128729, 128732, 128735, 128747, 128748, 128756, 128764, 128986, 128986, 128992, 129003, 129008, 129008, 129292, 129338, 129340, 129349, 129351, 129535, 129648, 129660, 129664, 129734, 129736, 129736, 129740, 129757, 129759, 129771, 129775, 129786, 131072, 196605, 196608, 262141];
71
+
72
+ // node_modules/get-east-asian-width/utilities.js
73
+ var isInRange = (ranges, codePoint) => {
74
+ let low = 0;
75
+ let high = Math.floor(ranges.length / 2) - 1;
76
+ while (low <= high) {
77
+ const mid = Math.floor((low + high) / 2);
78
+ const i = mid * 2;
79
+ if (codePoint < ranges[i]) {
80
+ high = mid - 1;
81
+ } else if (codePoint > ranges[i + 1]) {
82
+ low = mid + 1;
83
+ } else {
84
+ return true;
85
+ }
86
+ }
87
+ return false;
88
+ };
89
+
90
+ // node_modules/get-east-asian-width/lookup.js
91
+ var commonCjkCodePoint = 19968;
92
+ var [wideFastPathStart, wideFastPathEnd] = /* @__PURE__ */ findWideFastPathRange(wideRanges);
93
+ function findWideFastPathRange(ranges) {
94
+ let fastPathStart = ranges[0];
95
+ let fastPathEnd = ranges[1];
96
+ for (let index = 0; index < ranges.length; index += 2) {
97
+ const start = ranges[index];
98
+ const end = ranges[index + 1];
99
+ if (commonCjkCodePoint >= start && commonCjkCodePoint <= end) {
100
+ return [start, end];
101
+ }
102
+ if (end - start > fastPathEnd - fastPathStart) {
103
+ fastPathStart = start;
104
+ fastPathEnd = end;
105
+ }
106
+ }
107
+ return [fastPathStart, fastPathEnd];
108
+ }
109
+ var isAmbiguous = (codePoint) => {
110
+ if (codePoint < ambiguousMinimalCodePoint || codePoint > ambiguousMaximumCodePoint) {
111
+ return false;
112
+ }
113
+ return isInRange(ambiguousRanges, codePoint);
114
+ };
115
+ var isFullwidth = (codePoint) => {
116
+ if (codePoint < fullwidthMinimalCodePoint || codePoint > fullwidthMaximumCodePoint) {
117
+ return false;
118
+ }
119
+ return isInRange(fullwidthRanges, codePoint);
120
+ };
121
+ var isWide = (codePoint) => {
122
+ if (codePoint >= wideFastPathStart && codePoint <= wideFastPathEnd) {
123
+ return true;
124
+ }
125
+ if (codePoint < wideMinimalCodePoint || codePoint > wideMaximumCodePoint) {
126
+ return false;
127
+ }
128
+ return isInRange(wideRanges, codePoint);
129
+ };
130
+
131
+ // node_modules/get-east-asian-width/index.js
132
+ function validate(codePoint) {
133
+ if (!Number.isSafeInteger(codePoint)) {
134
+ throw new TypeError(`Expected a code point, got \`${typeof codePoint}\`.`);
135
+ }
136
+ }
137
+ function eastAsianWidth(codePoint, { ambiguousAsWide = false } = {}) {
138
+ validate(codePoint);
139
+ if (isFullwidth(codePoint) || isWide(codePoint) || ambiguousAsWide && isAmbiguous(codePoint)) {
140
+ return 2;
141
+ }
142
+ return 1;
143
+ }
144
+
145
+ // node_modules/string-width/index.js
146
+ var segmenter = new Intl.Segmenter();
147
+ var visibleCharacterRegex = new RegExp("[^\\p{Default_Ignorable_Code_Point}\\p{Control}\\p{Format}\\p{Nonspacing_Mark}\\p{Enclosing_Mark}\\p{Surrogate}]", "v");
148
+ var spacingMarkRegex = new RegExp("\\p{Spacing_Mark}", "v");
149
+ var rgiEmojiRegex = new RegExp("^\\p{RGI_Emoji}$", "v");
150
+ var unqualifiedKeycapRegex = /^[\d#*]\u20E3$/;
151
+ var extendedPictographicRegex = new RegExp("\\p{Extended_Pictographic}", "gu");
152
+ function isDoubleWidthNonRgiEmojiSequence(segment) {
153
+ if (segment.length > 50) {
154
+ return false;
155
+ }
156
+ if (unqualifiedKeycapRegex.test(segment)) {
157
+ return true;
158
+ }
159
+ if (segment.includes("\u200D")) {
160
+ const pictographics = segment.match(extendedPictographicRegex);
161
+ return pictographics !== null && pictographics.length >= 2;
162
+ }
163
+ return false;
164
+ }
165
+ function baseVisible(segment) {
166
+ const index = segment.search(visibleCharacterRegex);
167
+ return index === -1 ? void 0 : segment.slice(index);
168
+ }
169
+ function isHangulLeadingJamo(codePoint) {
170
+ return codePoint >= 4352 && codePoint <= 4447 || codePoint >= 43360 && codePoint <= 43388;
171
+ }
172
+ function isHangulVowelJamo(codePoint) {
173
+ return codePoint >= 4448 && codePoint <= 4519 || codePoint >= 55216 && codePoint <= 55238;
174
+ }
175
+ function isHangulTrailingJamo(codePoint) {
176
+ return codePoint >= 4520 && codePoint <= 4607 || codePoint >= 55243 && codePoint <= 55291;
177
+ }
178
+ function isHangulJamo(codePoint) {
179
+ return isHangulLeadingJamo(codePoint) || isHangulVowelJamo(codePoint) || isHangulTrailingJamo(codePoint);
180
+ }
181
+ function hangulClusterWidth(visibleSegment, eastAsianWidthOptions) {
182
+ const codePoints = [];
183
+ for (const character of visibleSegment) {
184
+ if (!visibleCharacterRegex.test(character)) {
185
+ continue;
186
+ }
187
+ codePoints.push(character.codePointAt(0));
188
+ }
189
+ if (codePoints.length === 0) {
190
+ return void 0;
191
+ }
192
+ let width = 0;
193
+ for (let index = 0; index < codePoints.length; index++) {
194
+ const codePoint = codePoints[index];
195
+ if (!isHangulJamo(codePoint)) {
196
+ if (width === 0) {
197
+ return void 0;
198
+ }
199
+ for (let remaining = index; remaining < codePoints.length; remaining++) {
200
+ width += eastAsianWidth(codePoints[remaining], eastAsianWidthOptions);
201
+ }
202
+ return width;
203
+ }
204
+ if (isHangulLeadingJamo(codePoint) && isHangulVowelJamo(codePoints[index + 1])) {
205
+ width += 2;
206
+ index += isHangulTrailingJamo(codePoints[index + 2]) ? 2 : 1;
207
+ continue;
208
+ }
209
+ width += eastAsianWidth(codePoint, eastAsianWidthOptions);
210
+ }
211
+ return width;
212
+ }
213
+ function trailingWidth(visibleSegment, eastAsianWidthOptions) {
214
+ let extra = 0;
215
+ let first = true;
216
+ for (const character of visibleSegment) {
217
+ if (first) {
218
+ first = false;
219
+ continue;
220
+ }
221
+ if (spacingMarkRegex.test(character) || character >= "\uFF00" && character <= "\uFFEF") {
222
+ extra += eastAsianWidth(character.codePointAt(0), eastAsianWidthOptions);
223
+ }
224
+ }
225
+ return extra;
226
+ }
227
+ function stringWidth(input, options = {}) {
228
+ if (typeof input !== "string" || input.length === 0) {
229
+ return 0;
230
+ }
231
+ const {
232
+ ambiguousIsNarrow = true,
233
+ countAnsiEscapeCodes = false
234
+ } = options;
235
+ let string = input;
236
+ if (!countAnsiEscapeCodes && (string.includes("\x1B") || string.includes("\x9B"))) {
237
+ string = stripAnsi(string);
238
+ }
239
+ if (string.length === 0) {
240
+ return 0;
241
+ }
242
+ if (/^[\u0020-\u007E]*$/.test(string)) {
243
+ return string.length;
244
+ }
245
+ let width = 0;
246
+ const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
247
+ for (const { segment } of segmenter.segment(string)) {
248
+ const visibleSegment = baseVisible(segment);
249
+ if (visibleSegment === void 0) {
250
+ continue;
251
+ }
252
+ if (rgiEmojiRegex.test(segment) || isDoubleWidthNonRgiEmojiSequence(segment)) {
253
+ width += 2;
254
+ continue;
255
+ }
256
+ const hangulWidth = hangulClusterWidth(visibleSegment, eastAsianWidthOptions);
257
+ if (hangulWidth !== void 0) {
258
+ width += hangulWidth;
259
+ continue;
260
+ }
261
+ const codePoint = visibleSegment.codePointAt(0);
262
+ width += eastAsianWidth(codePoint, eastAsianWidthOptions);
263
+ width += trailingWidth(visibleSegment, eastAsianWidthOptions);
264
+ }
265
+ return width;
266
+ }
267
+
268
+ // src/cli/tui/commands.ts
269
+ import { spawn } from "child_process";
270
+ var message = (e) => e instanceof Error ? e.message : String(e);
271
+ var insideClient = () => Boolean(process.env.TMUX);
272
+ function backHint(backend) {
273
+ if (backend !== "tmux") return { short: "", text: "" };
274
+ switch (tmuxAttachMode()) {
275
+ case "switch":
276
+ return { short: "back: C-b L", text: "back: C-b L" };
277
+ case "nested":
278
+ return { short: "detach: C-b C-b d", text: "detach: C-b d (nested: C-b C-b d)" };
279
+ case "plain":
280
+ return { short: "detach: C-b d", text: "detach: C-b d (nested: C-b C-b d)" };
281
+ }
282
+ }
283
+ async function attachKeyHint() {
284
+ try {
285
+ const { short } = backHint((await activeMux()).name);
286
+ return short === "" ? "a attach" : `a attach (${short})`;
287
+ } catch {
288
+ return "a attach";
289
+ }
290
+ }
291
+ async function planAttach(t) {
292
+ try {
293
+ const mux = await activeMux();
294
+ const target = { session: `mu-${t.workstream}`, window: t.tab ?? t.name };
295
+ const inside = insideClient();
296
+ const steps = await mux.attachCommands({ ...target, inside });
297
+ const outside = await mux.attachCommands({ ...target, inside: false, outside: true });
298
+ const { text } = backHint(mux.name);
299
+ const notice2 = `attaching ${target.session}:${target.window}${text === "" ? "" : ` \xB7 ${text}`}`;
300
+ return { ok: true, steps, line: attachShellLine(outside), notice: notice2 };
301
+ } catch (e) {
302
+ return { ok: false, reason: message(e), line: null };
303
+ }
304
+ }
305
+ var spawnStep = (step) => new Promise((resolve) => {
306
+ let stderr = "";
307
+ const env = { ...process.env };
308
+ if (step.nested === true) delete env.TMUX;
309
+ const child = spawn(step.command, [...step.args], {
310
+ stdio: ["inherit", "inherit", "pipe"],
311
+ env
312
+ });
313
+ child.stderr.on("data", (d) => {
314
+ stderr = (stderr + d.toString()).slice(-400);
315
+ });
316
+ child.on("error", (e) => resolve({ code: null, error: e.message, stderr }));
317
+ child.on("close", (code) => resolve({ code, stderr }));
318
+ });
319
+ var wordsOf = (step) => [step.command, ...step.args].map(shellWord).join(" ");
320
+ async function runAttach(steps, run) {
321
+ const warnings = [];
322
+ for (const step of steps) {
323
+ let r;
324
+ try {
325
+ r = await run(step);
326
+ } catch (e) {
327
+ r = { code: null, error: message(e), stderr: "" };
328
+ }
329
+ if (r.code === 0) continue;
330
+ const why = r.error ?? `exit ${r.code ?? "?"}${r.stderr.trim() === "" ? "" : `: ${r.stderr.trim()}`}`;
331
+ if (step.optional === true) {
332
+ warnings.push(`${step.reason ?? wordsOf(step)} (${why})`);
333
+ continue;
334
+ }
335
+ return { ok: false, reason: `${wordsOf(step)} failed (${why})`, warnings };
336
+ }
337
+ return { ok: true, warnings };
338
+ }
339
+ var YANK_MAX_BYTES = 1024;
340
+ var CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
341
+ function yankSequence(command) {
342
+ if (CONTROL.test(command)) return { ok: false, why: "it has control characters" };
343
+ if (Buffer.byteLength(command) > YANK_MAX_BYTES) return { ok: false, why: "it is too long" };
344
+ return { ok: true, sequence: `\x1B]52;c;${Buffer.from(command).toString("base64")}\x07` };
345
+ }
346
+ function copyCommand(t, opts = {}) {
347
+ switch (t.kind) {
348
+ case "notes":
349
+ return `mu task show ${shellWord(t.id)} -w ${shellWord(t.workstream)}${opts.all === true ? " --all" : ""}`;
350
+ case "scrollback":
351
+ return opts.attachLine ?? `mu agent read ${shellWord(t.name)} -w ${shellWord(t.workstream)}`;
352
+ case "log":
353
+ return `mu log --all --since ${Math.max(0, t.item.seq - 1)} -n 1`;
354
+ }
355
+ }
356
+
357
+ // src/cli/tui/drill.tsx
358
+ import { Box as Box2, Text as Text2, useInput, useStdout, useWindowSize } from "ink";
359
+ import { useEffect, useMemo, useRef, useState } from "react";
360
+
361
+ // src/cli/tui/frame.tsx
362
+ import { Box, Text } from "ink";
363
+
364
+ // src/cli/tui/text.ts
365
+ var ESCAPES = /\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?|\u001b./g;
366
+ var CONTROL2 = /[\u0000-\u001f\u007f-\u009f]/g;
367
+ function clean(s) {
368
+ return s.replace(ESCAPES, "").replace(CONTROL2, " ");
369
+ }
370
+ var segmenter2 = new Intl.Segmenter();
371
+ function cell(s, width) {
372
+ if (width <= 0) return "";
373
+ const text = clean(s);
374
+ const w = stringWidth(text);
375
+ if (w <= width) return text + " ".repeat(width - w);
376
+ let out = "";
377
+ let used = 0;
378
+ for (const { segment } of segmenter2.segment(text)) {
379
+ const sw = stringWidth(segment);
380
+ if (used + sw > width - 1) break;
381
+ out += segment;
382
+ used += sw;
383
+ }
384
+ return `${out}\u2026${" ".repeat(width - used - 1)}`;
385
+ }
386
+ function scrollTop(top, selected, count, height) {
387
+ const h = Math.max(1, height);
388
+ let next = top;
389
+ if (selected < next) next = selected;
390
+ else if (selected >= next + h) next = selected - h + 1;
391
+ return Math.max(0, Math.min(next, count - h));
392
+ }
393
+ var lineText = (line) => line.map((s) => s.text).join("");
394
+ function fitLine(line, width) {
395
+ if (width <= 0) return [];
396
+ const runs = line.map((s) => ({ ...s, text: clean(s.text) }));
397
+ const total = stringWidth(runs.map((s) => s.text).join(""));
398
+ if (total <= width) return [...runs, { text: " ".repeat(width - total) }];
399
+ const out = [];
400
+ let used = 0;
401
+ for (const run of runs) {
402
+ let text = "";
403
+ for (const { segment } of segmenter2.segment(run.text)) {
404
+ const sw = stringWidth(segment);
405
+ if (used + sw > width - 1) {
406
+ out.push({ ...run, text: `${text}\u2026` });
407
+ return [...out, { text: " ".repeat(width - used - 1) }];
408
+ }
409
+ text += segment;
410
+ used += sw;
411
+ }
412
+ out.push({ ...run, text });
413
+ }
414
+ return out;
415
+ }
416
+
417
+ // src/cli/tui/theme.ts
418
+ var STYLES = {
419
+ /** Plain text (titles in lists, prose). */
420
+ text: {},
421
+ /** Frame borders (rounded, gray). */
422
+ border: { color: "gray" },
423
+ /** A frame's title, the selected tab and workstream. */
424
+ title: { color: "cyan", bold: true },
425
+ /** Secondary text: subtitles, `+N more`, owners, times, hosts, hint, filter, next. */
426
+ muted: { dimColor: true },
427
+ /** A row's id or name. */
428
+ id: { bold: true },
429
+ /** Emphasis without colour: a drill's pinned notice (an attach failure). */
430
+ strong: { bold: true },
431
+ /** A log line's verb. */
432
+ verb: { color: "cyan" },
433
+ /** A drill's title line. */
434
+ heading: { color: "magenta", bold: true },
435
+ /** Task OPEN (ready). */
436
+ open: { color: "cyan" },
437
+ /** Task OPEN/parked and OPEN/triage: out of the ready set. */
438
+ parked: { color: "gray" },
439
+ /** Task IN_PROGRESS. */
440
+ progress: { color: "yellow" },
441
+ /** Task CLOSED/done. */
442
+ done: { color: "green" },
443
+ /** Task CLOSED as anything but done (wontfix, duplicate, superseded). */
444
+ undone: { color: "red" },
445
+ /** Agent busy. */
446
+ busy: { color: "cyan" },
447
+ /** Agent waiting on a permission dialog. */
448
+ permission: { color: "magenta" },
449
+ /** "all quiet". */
450
+ good: { color: "green" },
451
+ /** Needs a look: an attention item, a stalled tool, a sync warning. */
452
+ warn: { color: "yellow" },
453
+ /** Broken: a dead agent, an error. */
454
+ bad: { color: "red" }
455
+ };
456
+ var colorOn = () => process.env.NO_COLOR === void 0;
457
+ function style(role = "text") {
458
+ const s = STYLES[role];
459
+ if (colorOn() || s.color === void 0) return s;
460
+ const { color: _, ...rest } = s;
461
+ return rest;
462
+ }
463
+ function statusRole(status, substate) {
464
+ if (status === "OPEN") return substate === "parked" || substate === "triage" ? "parked" : "open";
465
+ if (status === "IN_PROGRESS") return "progress";
466
+ if (status === "CLOSED") {
467
+ const dflt = isTaskStatus(status) ? DEFAULT_SUBSTATE[status] : "done";
468
+ return substate === "" || substate === dflt ? "done" : "undone";
469
+ }
470
+ return "text";
471
+ }
472
+ function agentStateRole(state) {
473
+ if (state === "busy") return "busy";
474
+ if (state === "needs_permission") return "permission";
475
+ return "muted";
476
+ }
477
+
478
+ // src/cli/tui/frame.tsx
479
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
480
+ var FRAME_CHROME = 2;
481
+ var FRAME_SIDES = 4;
482
+ function borderParts(width, left, right, label) {
483
+ if (width < 2) return { head: "\u2500".repeat(Math.max(0, width)), shown: "", tail: "" };
484
+ const inner = width - 2;
485
+ const text = clean(label);
486
+ if (text === "") return { head: left + "\u2500".repeat(inner) + right, shown: "", tail: "" };
487
+ const room = Math.max(0, inner - 4);
488
+ const shown = stringWidth(text) <= room ? text : cell(text, room).trimEnd();
489
+ const fill = "\u2500".repeat(Math.max(1, inner - 3 - stringWidth(shown)));
490
+ return { head: `${left}\u2500 `, shown, tail: ` ${fill}${right}` };
491
+ }
492
+ function Segs({ line }) {
493
+ return /* @__PURE__ */ jsx(Fragment, { children: line.map((seg, i) => (
494
+ // biome-ignore lint/suspicious/noArrayIndexKey: runs of one line are positional
495
+ /* @__PURE__ */ jsx(Text, { ...style(seg.role), children: seg.text }, i)
496
+ )) });
497
+ }
498
+ function borderSegs(width, left, right, title, rest) {
499
+ const label = title !== "" && rest !== "" ? `${title} \xB7 ${rest}` : title || rest;
500
+ const { head, shown, tail } = borderParts(width, left, right, label);
501
+ const t = title === "" ? 0 : Math.min(shown.length, clean(title).length);
502
+ const sep = title !== "" && rest !== "" ? shown.slice(t, t + 3) : "";
503
+ const parts = [
504
+ { text: head, role: "border" },
505
+ { text: shown.slice(0, t), role: "title" },
506
+ { text: sep, role: "border" },
507
+ { text: shown.slice(t + sep.length), role: "muted" },
508
+ { text: tail, role: "border" }
509
+ ];
510
+ return parts.filter((p) => p.text !== "");
511
+ }
512
+ function Frame({ body, selected, ...spec }) {
513
+ if (spec.height < FRAME_CHROME || spec.width < FRAME_SIDES) return null;
514
+ const rows = Math.max(0, spec.height - FRAME_CHROME);
515
+ const room = Math.max(0, spec.width - FRAME_SIDES);
516
+ const lineOf = (b) => typeof b === "string" ? [{ text: b }] : b ?? [];
517
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: spec.width, height: spec.height, flexShrink: 0, children: [
518
+ /* @__PURE__ */ jsx(Text, { wrap: "truncate", children: /* @__PURE__ */ jsx(Segs, { line: borderSegs(spec.width, "\u256D", "\u256E", spec.title, spec.subtitle ?? "") }) }),
519
+ Array.from({ length: rows }, (_, i) => {
520
+ const fitted = fitLine(lineOf(body[i]), room);
521
+ return (
522
+ // biome-ignore lint/suspicious/noArrayIndexKey: fixed-height frame, rows are positional
523
+ /* @__PURE__ */ jsxs(Text, { wrap: "truncate", children: [
524
+ /* @__PURE__ */ jsx(Text, { ...style("border"), children: "\u2502 " }),
525
+ i === selected ? /* @__PURE__ */ jsx(Text, { inverse: true, children: fitted.map((s) => s.text).join("") }) : /* @__PURE__ */ jsx(Segs, { line: fitted }),
526
+ /* @__PURE__ */ jsx(Text, { ...style("border"), children: " \u2502" })
527
+ ] }, i)
528
+ );
529
+ }),
530
+ /* @__PURE__ */ jsx(Text, { wrap: "truncate", children: /* @__PURE__ */ jsx(Segs, { line: borderSegs(spec.width, "\u2570", "\u256F", "", spec.bottom ?? "") }) })
531
+ ] });
532
+ }
533
+
534
+ // src/cli/tui/nav.ts
535
+ function navKey(input, key, page, space = false) {
536
+ const full = Math.max(1, page);
537
+ const half = Math.max(1, Math.floor(full / 2));
538
+ if (key.ctrl && input === "d") return { by: half };
539
+ if (key.ctrl && input === "u") return { by: -half };
540
+ if (key.ctrl || key.meta) return null;
541
+ if (key.downArrow || input === "j") return { by: 1 };
542
+ if (key.upArrow || input === "k") return { by: -1 };
543
+ if (key.pageDown || space && input === " ") return { by: full };
544
+ if (key.pageUp) return { by: -full };
545
+ if (input === "g" || key.home) return { to: "top" };
546
+ if (input === "G" || key.end) return { to: "bottom" };
547
+ return null;
548
+ }
549
+ var NAV_HINT = "j/k ^d/^u g/G";
550
+
551
+ // src/cli/tui/drill.tsx
552
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
553
+ var SCROLLBACK_LINES = 2e3;
554
+ var END = Number.MAX_SAFE_INTEGER;
555
+ var segmenter3 = new Intl.Segmenter();
556
+ function wrap(text, width) {
557
+ const max = Math.max(1, width);
558
+ const out = [];
559
+ for (const raw of text.split(/\r\n|\n|\r/)) {
560
+ let cur = "";
561
+ let curW = 0;
562
+ let breakAt = -1;
563
+ let broke = false;
564
+ for (const { segment } of segmenter3.segment(clean(raw.replace(/\t/g, " ")))) {
565
+ const sw = stringWidth(segment);
566
+ if (curW + sw > max) {
567
+ if (segment === " ") {
568
+ out.push(cur.trimEnd());
569
+ cur = "";
570
+ curW = 0;
571
+ breakAt = -1;
572
+ broke = true;
573
+ continue;
574
+ }
575
+ if (breakAt > 0) {
576
+ out.push(cur.slice(0, breakAt).trimEnd());
577
+ cur = cur.slice(breakAt);
578
+ } else {
579
+ if (cur !== "") out.push(cur);
580
+ cur = "";
581
+ }
582
+ curW = stringWidth(cur);
583
+ breakAt = -1;
584
+ broke = true;
585
+ }
586
+ if (broke && cur === "" && segment === " ") continue;
587
+ cur += segment;
588
+ curW += sw;
589
+ if (segment === " " && cur.trim() !== "") breakAt = cur.length;
590
+ }
591
+ out.push(cur.trimEnd());
592
+ }
593
+ return out;
594
+ }
595
+ var clampTop = (top, count, height) => Math.max(0, Math.min(top, count - Math.max(1, height)));
596
+ function notesText(sel) {
597
+ return sel.notes.map(
598
+ (n) => `#${n.n} ${n.kind}${sel.pinned.includes(n.n) ? " *" : ""} ${n.author ?? "-"} ${n.createdAt}
599
+ ${n.content}
600
+ `
601
+ ).join("\n");
602
+ }
603
+ function titleOf(t) {
604
+ switch (t.kind) {
605
+ case "notes":
606
+ return `Notes ${t.workstream}/${t.id} ${t.title}`;
607
+ case "scrollback":
608
+ return `Scrollback ${t.workstream}/${t.name} ${t.paneId} (last ${SCROLLBACK_LINES} lines)`;
609
+ case "log":
610
+ return `Log #${t.item.seq}`;
611
+ }
612
+ }
613
+ function logText(t) {
614
+ const i = t.item;
615
+ return [
616
+ t.item.rendered,
617
+ "",
618
+ `seq: ${i.seq}`,
619
+ `time: ${i.created_at}`,
620
+ `scope: ${t.item.scope ?? "-"}`,
621
+ `actor: ${i.actor}`,
622
+ `intent: ${i.intent ?? "-"}`,
623
+ `op: ${i.op}`,
624
+ `entity: ${i.entity}`,
625
+ `key: ${i.key === "" ? "-" : i.key}`,
626
+ `payload: ${i.payload}`,
627
+ ...i.cut !== void 0 && i.cut > 0 ? [`(${i.cut} characters cut from this item)`] : []
628
+ ].join("\n");
629
+ }
630
+ function taskLine(t) {
631
+ if (t === void 0) return "task: gone";
632
+ return `${formatPair(t)} ${t.ownerName === null ? "unowned" : `@${t.ownerName}`} ${t.title}`;
633
+ }
634
+ function useYank() {
635
+ const { stdout } = useStdout();
636
+ return async (target, all = false) => {
637
+ const plan = target.kind === "scrollback" ? await planAttach(target) : null;
638
+ const command = copyCommand(target, { all, attachLine: plan?.line ?? null });
639
+ const y = yankSequence(command);
640
+ const why = plan !== null && !plan.ok ? ` (attach unavailable: ${plan.reason})` : "";
641
+ if (!y.ok) return `not copied (${y.why}): ${command}${why}`;
642
+ stdout.write(y.sequence);
643
+ return `copied: ${command}${why}`;
644
+ };
645
+ }
646
+ function Drill({
647
+ target,
648
+ runtime,
649
+ notice: notice2,
650
+ strip = null,
651
+ busy,
652
+ onClose,
653
+ onAttach,
654
+ nonce = 0,
655
+ slowNonce = 0
656
+ }) {
657
+ const { columns, rows } = useWindowSize();
658
+ const yank = useYank();
659
+ const [all, setAll] = useState(false);
660
+ const [reload, setReload] = useState(0);
661
+ const [load, setLoad] = useState({ state: "loading" });
662
+ const [flash, setFlash] = useState(null);
663
+ const [top, setTop] = useState(target.kind === "scrollback" ? END : 0);
664
+ const topSeen = useRef(0);
665
+ const entityAll = useRef(false);
666
+ const tick = target.kind === "scrollback" ? slowNonce : nonce;
667
+ const entity = useRef(null);
668
+ useEffect(() => {
669
+ let live = true;
670
+ if (entity.current !== target || all !== entityAll.current) {
671
+ entity.current = target;
672
+ entityAll.current = all;
673
+ setLoad({ state: "loading" });
674
+ }
675
+ const done = (l) => {
676
+ if (live)
677
+ setLoad((prev) => JSON.stringify(prev) === JSON.stringify(l) ? prev : l);
678
+ };
679
+ try {
680
+ if (target.kind === "notes") {
681
+ const sel = runtime.notes(target.workstream, target.id, all);
682
+ done({
683
+ state: "ready",
684
+ text: sel.notes.length === 0 ? "(no notes)" : notesText(sel),
685
+ notices: sel.more > 0 ? [`${sel.more} of ${sel.total} notes not shown: A shows them all (oldest first)`] : [],
686
+ header: taskLine(runtime.task(target.workstream, target.id))
687
+ });
688
+ } else if (target.kind === "log") {
689
+ const { workstream, local } = parseOpKey(target.item.key === "" ? null : target.item.key);
690
+ const ctx = workstream !== void 0 && local !== void 0 && local !== "" ? `
691
+
692
+ task now: ${taskLine(runtime.task(workstream, local))}` : "";
693
+ done({ state: "ready", text: `${logText(target)}${ctx}`, notices: [] });
694
+ } else {
695
+ void (async () => {
696
+ try {
697
+ const mux = await activeMux();
698
+ const text = await mux.capture(target.paneId, { lines: SCROLLBACK_LINES });
699
+ done({ state: "ready", text: text.replace(/\s+$/, ""), notices: [] });
700
+ } catch (e) {
701
+ done({ state: "error", message: e instanceof Error ? e.message : String(e) });
702
+ }
703
+ })();
704
+ }
705
+ } catch (e) {
706
+ done({ state: "error", message: e instanceof Error ? e.message : String(e) });
707
+ }
708
+ return () => {
709
+ live = false;
710
+ };
711
+ }, [target, runtime, all, reload, tick]);
712
+ const lines = useMemo(() => {
713
+ const above = [...notice2, ...flash === null ? [] : [flash]];
714
+ const ready = load.state === "ready" ? load : null;
715
+ const head = [
716
+ ...ready?.header !== void 0 ? [ready.header] : [],
717
+ ...ready?.notices ?? [],
718
+ ...above
719
+ ].flatMap((l) => wrap(l, columns));
720
+ const body = load.state === "loading" ? ["loading\u2026"] : load.state === "error" ? wrap(`error: ${load.message}`, columns) : wrap(load.text, columns);
721
+ return { head, body };
722
+ }, [load, notice2, flash, columns]);
723
+ const showStrip = strip !== null && rows >= 4;
724
+ const room = Math.max(0, rows - 1 - (rows >= 3 ? 1 : 0) - (showStrip ? 1 : 0));
725
+ const pinned = lines.head.slice(0, Math.floor(room / 2));
726
+ const height = room - pinned.length;
727
+ const first = clampTop(top, lines.body.length, height);
728
+ topSeen.current = first;
729
+ const hint = target.kind === "notes" ? `${NAV_HINT} scroll A all notes r reload y copy cmd Esc back` : target.kind === "scrollback" ? `${NAV_HINT} scroll r reload a attach y copy cmd Esc back` : `${NAV_HINT} scroll y copy cmd Esc back`;
730
+ useInput((input, key) => {
731
+ if (busy.current) return;
732
+ setFlash(null);
733
+ const at = topSeen.current;
734
+ if (key.escape || input === "q") return onClose();
735
+ const nav = navKey(input, key, height, true);
736
+ const end = Math.max(0, lines.body.length - Math.max(1, height));
737
+ if (nav !== null)
738
+ setTop(
739
+ "by" in nav ? at + nav.by >= end ? END : Math.max(0, at + nav.by) : nav.to === "top" ? 0 : END
740
+ );
741
+ else if (input === "r") setReload((n) => n + 1);
742
+ else if (input === "A" && target.kind === "notes") setAll((a) => !a);
743
+ else if (input === "a" && target.kind === "scrollback") onAttach(target);
744
+ else if (input === "y") void yank(target, all).then(setFlash);
745
+ });
746
+ const visible = lines.body.slice(first, first + height);
747
+ const where = lines.body.length === 0 ? "" : `${first + 1}-${Math.min(lines.body.length, first + height)}/${lines.body.length} `;
748
+ return (
749
+ // Exactly `rows` lines: a short body is followed by the spacer, so the hint stays on the last row.
750
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: rows, children: [
751
+ /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", ...style("heading"), children: cell(titleOf(target), columns) }),
752
+ showStrip ? /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", children: /* @__PURE__ */ jsx2(Segs, { line: fitLine(strip, columns) }) }) : null,
753
+ pinned.map((l, i) => (
754
+ // biome-ignore lint/suspicious/noArrayIndexKey: lines have no identity; the index IS the position
755
+ /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", ...style("strong"), children: cell(l, columns) }, `p${i}`)
756
+ )),
757
+ visible.map((l, i) => (
758
+ // biome-ignore lint/suspicious/noArrayIndexKey: lines have no identity; the index IS the position
759
+ /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", ...style(load.state === "error" ? "bad" : "text"), children: cell(l, columns) }, first + i)
760
+ )),
761
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
762
+ rows >= 3 ? /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", ...style("muted"), children: cell(`${where}${hint}`, columns) }) : null
763
+ ] })
764
+ );
765
+ }
766
+
767
+ // src/cli/tui/overview.tsx
768
+ import { Box as Box3 } from "ink";
769
+
770
+ // src/cli/tui/grid.ts
771
+ var COLUMN_GAP = 1;
772
+ function columnCount(cols) {
773
+ if (cols >= 180) return 3;
774
+ if (cols >= 120) return 2;
775
+ return 1;
776
+ }
777
+ function gridWidths(total, n) {
778
+ if (n <= 0) return [];
779
+ const usable = Math.max(n, total - COLUMN_GAP * (n - 1));
780
+ const base = Math.floor(usable / n);
781
+ const extra = usable - base * n;
782
+ return Array.from({ length: n }, (_, i) => base + (i < extra ? 1 : 0));
783
+ }
784
+ var PLACEMENT = {
785
+ 1: {},
786
+ 2: { attention: 0, agents: 0, progress: 1, open: 1, log: 0 },
787
+ 3: { attention: 0, agents: 0, progress: 1, open: 1, log: 2 }
788
+ };
789
+ function placeCards(ids, n) {
790
+ const table = PLACEMENT[n];
791
+ const cols = Array.from({ length: n }, () => []);
792
+ for (const id of ids) cols[Math.min(n - 1, table[id] ?? n - 1)]?.push(id);
793
+ return cols.filter((c) => c.length > 0);
794
+ }
795
+ var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
796
+ function allocateRowBudgets(availableRows, cards) {
797
+ const out = /* @__PURE__ */ new Map();
798
+ const entries = cards.map((c) => {
799
+ const min = Math.max(0, c.min);
800
+ const max = Math.max(min, c.max);
801
+ const count = Math.max(0, Math.floor(c.count));
802
+ return { id: c.id, min, max, count, natural: clamp(count, min, max) };
803
+ });
804
+ if (entries.length === 0) return out;
805
+ const body = Math.max(0, availableRows - FRAME_CHROME * entries.length);
806
+ const set = (id, n) => void out.set(id, n);
807
+ const naturalTotal = entries.reduce((n, e) => n + e.natural, 0);
808
+ if (naturalTotal <= body) {
809
+ for (const e of entries) set(e.id, e.natural);
810
+ return fillHidden(out, entries, body);
811
+ }
812
+ const minTotal = entries.reduce((n, e) => n + e.min, 0);
813
+ for (const e of entries) set(e.id, e.min);
814
+ if (body <= minTotal) return out;
815
+ const left = body - minTotal;
816
+ const weight = entries.reduce((n, e) => n + e.natural, 0);
817
+ const shares = entries.map((e, index) => {
818
+ const raw = weight === 0 ? left / entries.length : left * e.natural / weight;
819
+ return { e, index, raw, add: Math.min(e.max - e.min, Math.floor(raw)) };
820
+ });
821
+ let remaining = left;
822
+ for (const s of shares) {
823
+ set(s.e.id, s.e.min + s.add);
824
+ remaining -= s.add;
825
+ }
826
+ const byRemainder = [...shares].sort(
827
+ (a, b) => b.raw - Math.floor(b.raw) - (a.raw - Math.floor(a.raw)) || a.index - b.index
828
+ );
829
+ while (remaining > 0) {
830
+ let changed = false;
831
+ for (const s of byRemainder) {
832
+ if (remaining <= 0) break;
833
+ const have = out.get(s.e.id) ?? 0;
834
+ if (have >= s.e.max) continue;
835
+ set(s.e.id, have + 1);
836
+ remaining -= 1;
837
+ changed = true;
838
+ }
839
+ if (!changed) break;
840
+ }
841
+ return fillHidden(out, entries, body);
842
+ }
843
+ function fillHidden(out, entries, body) {
844
+ let spare = body - entries.reduce((n, e) => n + (out.get(e.id) ?? 0), 0);
845
+ while (spare > 0) {
846
+ const hidden = entries.map((e, index) => ({ id: e.id, index, rows: Math.max(0, e.count - (out.get(e.id) ?? 0)) })).filter((h) => h.rows > 0);
847
+ const total = hidden.reduce((n, h) => n + h.rows, 0);
848
+ if (total === 0) break;
849
+ const pool = Math.min(spare, total);
850
+ const shares = hidden.map((h) => {
851
+ const raw = pool * h.rows / total;
852
+ return { ...h, add: Math.floor(raw), rem: raw - Math.floor(raw) };
853
+ });
854
+ let given = 0;
855
+ for (const s of shares) {
856
+ out.set(s.id, (out.get(s.id) ?? 0) + s.add);
857
+ given += s.add;
858
+ }
859
+ shares.sort((a, b) => b.rem - a.rem || a.index - b.index);
860
+ for (const s of shares) {
861
+ if (given >= pool) break;
862
+ const count = entries[s.index]?.count ?? 0;
863
+ if ((out.get(s.id) ?? 0) >= count) continue;
864
+ out.set(s.id, (out.get(s.id) ?? 0) + 1);
865
+ given += 1;
866
+ }
867
+ if (given === 0) break;
868
+ spare -= given;
869
+ }
870
+ return out;
871
+ }
872
+ function fitColumn(availableRows, cards) {
873
+ const budgets = allocateRowBudgets(availableRows, cards);
874
+ const out = [];
875
+ let left = Math.max(0, Math.floor(availableRows));
876
+ for (const c of cards) {
877
+ const rows = Math.min(budgets.get(c.id) ?? 0, Math.max(0, left - FRAME_CHROME));
878
+ if (rows === 0 && c.min > 0 && out.length > 0) continue;
879
+ if (left < FRAME_CHROME) break;
880
+ out.push({ id: c.id, height: FRAME_CHROME + rows, rows, hidden: Math.max(0, c.count - rows) });
881
+ left -= FRAME_CHROME + rows;
882
+ }
883
+ return out;
884
+ }
885
+
886
+ // src/cli/tui/columns.ts
887
+ var GUTTER = 2;
888
+ function naturalWidths(rows, ncols) {
889
+ const widths = new Array(ncols).fill(0);
890
+ for (const row of rows) {
891
+ for (let i = 0; i < ncols; i++) {
892
+ const w = stringWidth(clean(row[i] ?? ""));
893
+ if (w > (widths[i] ?? 0)) widths[i] = w;
894
+ }
895
+ }
896
+ return widths;
897
+ }
898
+ function columnWidths(rows, specs) {
899
+ const natural = naturalWidths(rows, specs.length);
900
+ return specs.map((s, i) => {
901
+ let w = natural[i] ?? 0;
902
+ if (s.min !== void 0) w = Math.max(w, s.min);
903
+ if (s.max !== void 0) w = Math.min(w, s.max);
904
+ return w;
905
+ });
906
+ }
907
+ function fitCell(s, width, align = "left") {
908
+ const text = clean(s);
909
+ const w = stringWidth(text);
910
+ if (align === "right" && w < width) return " ".repeat(width - w) + text;
911
+ return cell(text, width);
912
+ }
913
+ function renderCells(cells, widths, specs) {
914
+ return specs.map((s, i) => fitCell(cells[i] ?? "", widths[i] ?? 0, s.align));
915
+ }
916
+
917
+ // src/cli/tui/rows.ts
918
+ var TABS = [
919
+ { id: "overview", label: "Overview" },
920
+ { id: "tasks", label: "Tasks" },
921
+ { id: "dag", label: "DAG" },
922
+ { id: "agents", label: "Agents" },
923
+ { id: "log", label: "Log" }
924
+ ];
925
+ var STATUS_LABELS = ["active", "open", "in_progress", "closed", "all"];
926
+ var SORT_LABELS = ["activity", "roi", "age", "id"];
927
+ var SORT_ARROWS = {
928
+ activity: "\u2193",
929
+ // newest first
930
+ roi: "\u2193",
931
+ // highest first
932
+ age: "\u2191",
933
+ // oldest first
934
+ id: "\u2191"
935
+ };
936
+ var NO_FILTER = { status: "active", text: "", sort: "activity" };
937
+ var STATUS_SETS = {
938
+ active: ["OPEN", "IN_PROGRESS"],
939
+ open: ["OPEN"],
940
+ in_progress: ["IN_PROGRESS"],
941
+ closed: ["CLOSED"],
942
+ all: ["OPEN", "IN_PROGRESS", "CLOSED"]
943
+ };
944
+ var statusesOf = (label) => STATUS_SETS[label];
945
+ function nextStatusLabel(label) {
946
+ const i = STATUS_LABELS.indexOf(label);
947
+ return STATUS_LABELS[(i + 1) % STATUS_LABELS.length] ?? "active";
948
+ }
949
+ function nextSortLabel(label) {
950
+ const i = SORT_LABELS.indexOf(label);
951
+ return SORT_LABELS[(i + 1) % SORT_LABELS.length] ?? "activity";
952
+ }
953
+ var scopeKey = (tab, workstream) => `${tab}\0${workstream ?? ""}`;
954
+ function pickWorkstream(workstreams, hint) {
955
+ if (hint !== void 0 && workstreams.some((w) => w.name === hint)) return hint;
956
+ let best;
957
+ for (const w of workstreams) {
958
+ if (best === void 0 || w.latest_seq > best.latest_seq) best = w;
959
+ }
960
+ return best?.name ?? null;
961
+ }
962
+ var workstreamLabel = (w) => w.scratch ? `${w.name}*` : w.name;
963
+ function nextLine(snap) {
964
+ if (snap.next_source !== "computed") return "next: unavailable";
965
+ if (snap.next === null) return "next: no authorised work";
966
+ return `next (${snap.next.mode}): ${snap.next.commands.join(" ; ")}`;
967
+ }
968
+ var statusText = (status, substate) => substate === "" ? status : formatPair({ status, substate });
969
+ var scrollbackOf = (a) => ({
970
+ kind: "scrollback",
971
+ workstream: a.workstream,
972
+ name: a.name,
973
+ paneId: a.pane_id,
974
+ tab: a.tab
975
+ });
976
+ var STATUS_WIDTH = Math.max(
977
+ ...TASK_STATUSES.flatMap((st) => TASK_SUBSTATES[st].map((sub) => statusText(st, sub).length))
978
+ );
979
+ var STATE_WIDTH = "needs_permission".length;
980
+ var HOST_WIDTH = 6;
981
+ var LOCAL = "local";
982
+ var TIME_WIDTH = 8;
983
+ var protect = (min = 0, max) => ({
984
+ kind: "protect",
985
+ min,
986
+ ...max !== void 0 ? { max } : {}
987
+ });
988
+ var fixed = (n) => ({ kind: "protect", min: n, max: n });
989
+ var CLIP = { kind: "clip" };
990
+ function layoutRows(basis, specs) {
991
+ const widths = columnWidths(basis, specs);
992
+ const last = specs.length - 1;
993
+ return (cells, roles) => {
994
+ const fitted = renderCells(cells, widths, specs);
995
+ const shown = specs.flatMap(
996
+ (_, i) => i === last ? [{ text: clean(cells[last] ?? ""), role: roles[last] }] : (widths[i] ?? 0) > 0 ? [{ text: fitted[i] ?? "", role: roles[i] }] : []
997
+ );
998
+ const out = shown.flatMap((seg, i) => [
999
+ ...i > 0 ? [{ text: " ".repeat(GUTTER) }] : [],
1000
+ seg.role === void 0 ? { text: seg.text } : { text: seg.text, role: seg.role }
1001
+ ]);
1002
+ while (out.length > 0) {
1003
+ const tail = out[out.length - 1];
1004
+ const trimmed = tail.text.trimEnd();
1005
+ if (trimmed !== "") {
1006
+ out[out.length - 1] = { ...tail, text: trimmed };
1007
+ break;
1008
+ }
1009
+ out.pop();
1010
+ }
1011
+ return out;
1012
+ };
1013
+ }
1014
+ var rowOf = (key, line, rest = {}) => ({
1015
+ key,
1016
+ text: lineText(line),
1017
+ line,
1018
+ ...rest
1019
+ });
1020
+ var ownerLabel = (t) => t.owner === null ? "" : `@${t.owner_workstream !== null && t.owner_workstream !== t.workstream ? `${t.owner_workstream}/` : ""}${t.owner}`;
1021
+ function sortTasks(tasks, sort) {
1022
+ const roi = (t) => t.impact / Math.max(t.effort_days, 1e-9);
1023
+ const out = [...tasks];
1024
+ if (sort === "roi") out.sort((a, b) => roi(b) - roi(a) || a.effort_days - b.effort_days);
1025
+ else if (sort === "age")
1026
+ out.sort((a, b) => a.created_at < b.created_at ? -1 : a.created_at > b.created_at ? 1 : 0);
1027
+ else if (sort === "id") out.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
1028
+ return out;
1029
+ }
1030
+ function taskRows(snap, ws, f) {
1031
+ const statuses = statusesOf(f.status);
1032
+ const inWs = snap.tasks.filter((t) => t.workstream === ws);
1033
+ const tasks = sortTasks(
1034
+ inWs.filter(
1035
+ (t) => statuses.includes(t.status) && matches(f.text, t.key, t.title, t.owner, t.substate)
1036
+ ),
1037
+ f.sort
1038
+ );
1039
+ const specs = [protect(0, 24), fixed(STATUS_WIDTH), protect(0, 24), CLIP];
1040
+ const agentNames = snap.agents.filter((a) => a.workstream === ws).map((a) => `@${a.name}`);
1041
+ const line = layoutRows(
1042
+ [...inWs.map((t) => [t.id, "", ownerLabel(t)]), ...agentNames.map((n) => ["", "", n])],
1043
+ specs
1044
+ );
1045
+ return tasks.map(
1046
+ (t) => rowOf(
1047
+ t.key,
1048
+ line(
1049
+ [t.id, statusText(t.status, t.substate), ownerLabel(t), t.title],
1050
+ ["id", statusRole(t.status, t.substate), "muted", void 0]
1051
+ ),
1052
+ { drill: { kind: "notes", workstream: t.workstream, id: t.id, title: t.title } }
1053
+ )
1054
+ );
1055
+ }
1056
+ function edgeNotes(e) {
1057
+ const notes = [
1058
+ e.again ? "again, listed above" : "",
1059
+ e.kids > 0 ? `+${e.kids} open` : "",
1060
+ e.closed > 0 ? `${e.closed} closed` : "",
1061
+ e.cap_cut > 0 ? `${e.cap_cut} cut by cap` : ""
1062
+ ].filter((n) => n !== "");
1063
+ return notes.length > 0 ? ` (${notes.join(", ")})` : "";
1064
+ }
1065
+ var notice = (key, text) => rowOf(key, [{ text, role: "warn" }]);
1066
+ var edgeId = (e) => `${" ".repeat(e.depth)}${e.id}`;
1067
+ function dagRows(snap, ws, f) {
1068
+ const rows = [];
1069
+ const mine = snap.edges.filter((e) => e.workstream === ws);
1070
+ const specs = [protect(0, 40), fixed(STATUS_WIDTH), CLIP];
1071
+ const line = layoutRows(
1072
+ mine.map((e) => [edgeId(e), "", ""]),
1073
+ specs
1074
+ );
1075
+ for (const e of mine) {
1076
+ if (matches(f.text, e.key, e.title, e.substate)) {
1077
+ rows.push(
1078
+ rowOf(
1079
+ `${e.parent ?? ""}>${e.key}`,
1080
+ [
1081
+ ...line(
1082
+ [edgeId(e), statusText(e.status, e.substate), e.title],
1083
+ ["id", statusRole(e.status, e.substate), void 0]
1084
+ ),
1085
+ ...edgeNotes(e) === "" ? [] : [{ text: edgeNotes(e), role: "muted" }]
1086
+ ],
1087
+ { drill: { kind: "notes", workstream: e.workstream, id: e.id, title: e.title } }
1088
+ )
1089
+ );
1090
+ } else if (e.cap_cut > 0) {
1091
+ rows.push(
1092
+ notice(
1093
+ `cut:cap:${e.key}`,
1094
+ `\u2026 ${e.cap_cut} open task${e.cap_cut === 1 ? "" : "s"} under ${e.id} left out by the node cap`
1095
+ )
1096
+ );
1097
+ }
1098
+ }
1099
+ const cut = snap.frontier.find((c) => c.workstream === ws);
1100
+ if (cut !== void 0 && cut.roots_cut > 0) {
1101
+ rows.push(
1102
+ notice(
1103
+ "cut:roots",
1104
+ `\u2026 ${cut.roots_cut} open start task${cut.roots_cut === 1 ? "" : "s"} left out by the node cap`
1105
+ )
1106
+ );
1107
+ }
1108
+ if (cut !== void 0 && cut.again_cut > 0) {
1109
+ rows.push(
1110
+ notice(
1111
+ "cut:again",
1112
+ `\u2026 ${cut.again_cut} repeat line${cut.again_cut === 1 ? "" : "s"} left out by the node cap`
1113
+ )
1114
+ );
1115
+ }
1116
+ return rows;
1117
+ }
1118
+ function toolText(a, now) {
1119
+ if (a.tool === void 0) return "tool ?";
1120
+ if (a.tool === null) return "tool -";
1121
+ const ms = a.tool.elapsed_ms ?? now - a.tool.since;
1122
+ return `${a.tool.name} ${formatElapsed(ms)}`;
1123
+ }
1124
+ function agentRows(snap, ws, f, now) {
1125
+ const mine = snap.agents.filter((a) => a.workstream === ws);
1126
+ const agents = mine.filter((a) => matches(f.text, a.key, a.state, a.tool?.name, ...a.tasks));
1127
+ const taskOf = (a) => a.task === null ? "-" : a.task.startsWith(`${a.workstream}/`) ? a.task.slice(a.workstream.length + 1) : a.task;
1128
+ const specs = [protect(0, 32), fixed(HOST_WIDTH), fixed(STATE_WIDTH), protect(0, 32), CLIP];
1129
+ const line = layoutRows(
1130
+ // The task column is as wide as the workstream's task ids, not just the tasks owned
1131
+ // right now: an agent claiming or finishing a task must not move the tool column.
1132
+ [
1133
+ ...mine.map((a) => [a.name, "", "", taskOf(a)]),
1134
+ ...snap.tasks.filter((t) => t.workstream === ws).map((t) => ["", "", "", t.id])
1135
+ ],
1136
+ specs
1137
+ );
1138
+ const flagged = (kind, key) => snap.attention.some((x) => x.kind === kind && x.target === key);
1139
+ return agents.map(
1140
+ (a) => rowOf(
1141
+ a.key,
1142
+ line(
1143
+ [a.name, a.host ?? LOCAL, a.state ?? "?", taskOf(a), toolText(a, now)],
1144
+ [
1145
+ "id",
1146
+ "muted",
1147
+ flagged("dead", a.key) ? "bad" : agentStateRole(a.state),
1148
+ "muted",
1149
+ flagged("stalled", a.key) ? "warn" : "muted"
1150
+ ]
1151
+ ),
1152
+ { drill: scrollbackOf(a) }
1153
+ )
1154
+ );
1155
+ }
1156
+ var timeOf = (iso) => /T(\d\d:\d\d:\d\d)/.exec(iso)?.[1] ?? iso;
1157
+ function logRows(snap, ws, f) {
1158
+ const mine = snap.log.filter((l) => l.scope === ws);
1159
+ const specs = [{ kind: "protect", align: "right" }, fixed(TIME_WIDTH), CLIP];
1160
+ const line = layoutRows(
1161
+ mine.map((l) => [String(l.seq), "", ""]),
1162
+ specs
1163
+ );
1164
+ return mine.filter((l) => matches(f.text, l.rendered, l.actor, l.scope)).map((l) => {
1165
+ const head = line([String(l.seq), timeOf(l.created_at), ""], ["muted", "muted", void 0]);
1166
+ return rowOf(String(l.seq), [...head, { text: " ".repeat(GUTTER) }, ...logProse(l)], {
1167
+ drill: { kind: "log", item: l }
1168
+ });
1169
+ });
1170
+ }
1171
+ var LOG_VERBS = [...KNOWN_VERBS, "task status", "op put", "op del", "op"].sort(
1172
+ (a, b) => b.length - a.length
1173
+ );
1174
+ function logProse(l) {
1175
+ const text = clean(l.rendered);
1176
+ const verb = l.intent ? LOG_VERBS.find((v) => text === v || text.startsWith(`${v} `)) : void 0;
1177
+ if (verb === void 0) return [{ text }];
1178
+ return [
1179
+ { text: verb, role: "verb" },
1180
+ ...text.length > verb.length ? [{ text: text.slice(verb.length) }] : []
1181
+ ];
1182
+ }
1183
+ function attentionLabel(a, ws) {
1184
+ const where = a.workstream !== null && a.workstream !== ws ? `${a.workstream}/` : "";
1185
+ return `${where}${a.kind === "human" ? `human: ${a.text}` : a.text}`;
1186
+ }
1187
+ function attentionStrip(attention, ws) {
1188
+ if (attention.length === 0) return [{ text: "\u2713 all quiet", role: "good" }];
1189
+ return [
1190
+ { text: `\u26A0 ${attention.length} need you: `, role: "warn" },
1191
+ ...attention.flatMap((a, i) => [
1192
+ ...i > 0 ? [{ text: " \xB7 ", role: "muted" }] : [],
1193
+ { text: attentionLabel(a, ws), role: attentionRole(a) }
1194
+ ])
1195
+ ];
1196
+ }
1197
+ var attentionRole = (a) => a.kind === "dead" ? "bad" : "warn";
1198
+ function attentionRows(snap, ws) {
1199
+ return snap.attention.map((a) => {
1200
+ const agent = snap.agents.find((x) => x.key === a.target);
1201
+ const id = a.kind === "human" ? a.target?.slice((a.workstream?.length ?? 0) + 1) : void 0;
1202
+ const drill = agent ? scrollbackOf(agent) : a.workstream !== null && id ? { kind: "notes", workstream: a.workstream, id, title: a.text } : void 0;
1203
+ return rowOf(
1204
+ a.key,
1205
+ [{ text: attentionLabel(a, ws), role: attentionRole(a) }],
1206
+ drill ? { drill } : {}
1207
+ );
1208
+ });
1209
+ }
1210
+ function tabRows(tab, snap, ws, f, now) {
1211
+ switch (tab) {
1212
+ case "overview":
1213
+ return attentionRows(snap, ws);
1214
+ case "tasks":
1215
+ return taskRows(snap, ws, f);
1216
+ case "dag":
1217
+ return dagRows(snap, ws, f);
1218
+ case "agents":
1219
+ return agentRows(snap, ws, f, now);
1220
+ case "log":
1221
+ return logRows(snap, ws, f);
1222
+ }
1223
+ }
1224
+ function cursorIndex(rows, key, tab, was) {
1225
+ const i = key === null ? -1 : rows.findIndex((r) => r.key === key);
1226
+ if (i >= 0) return i;
1227
+ if (key !== null && was !== void 0) return Math.max(0, Math.min(was, rows.length - 1));
1228
+ return tab === "log" ? Math.max(0, rows.length - 1) : 0;
1229
+ }
1230
+ function moveCursor(rows, key, tab, delta, was) {
1231
+ if (rows.length === 0) return null;
1232
+ const i = Math.max(0, Math.min(rows.length - 1, cursorIndex(rows, key, tab, was) + delta));
1233
+ return rows[i]?.key ?? null;
1234
+ }
1235
+
1236
+ // src/cli/tui/overview.tsx
1237
+ import { jsx as jsx3 } from "react/jsx-runtime";
1238
+ function overviewCards(snap, ws, now) {
1239
+ const lines = (tab, f = NO_FILTER) => tabRows(tab, snap, ws, f, now).map((r) => r.line);
1240
+ return [
1241
+ {
1242
+ id: "attention",
1243
+ title: "Attention",
1244
+ lines: lines("overview"),
1245
+ empty: "\u2713 all quiet",
1246
+ emptyRole: "good",
1247
+ max: 6
1248
+ },
1249
+ { id: "agents", title: "Agents", lines: lines("agents"), empty: "no agents", max: 6 },
1250
+ {
1251
+ id: "progress",
1252
+ title: "In progress",
1253
+ lines: lines("tasks", { ...NO_FILTER, status: "in_progress" }),
1254
+ empty: "nothing in progress",
1255
+ max: 8
1256
+ },
1257
+ {
1258
+ id: "open",
1259
+ title: "Open \xB7 by ROI",
1260
+ lines: lines("tasks", { ...NO_FILTER, status: "open", sort: "roi" }),
1261
+ empty: "nothing open",
1262
+ max: 12
1263
+ },
1264
+ { id: "log", title: "Log", lines: lines("log"), empty: "no activity", max: 10, tail: true }
1265
+ ];
1266
+ }
1267
+ function Overview(p) {
1268
+ const columns = placeCards(
1269
+ p.cards.map((c) => c.id),
1270
+ columnCount(p.width)
1271
+ );
1272
+ const widths = gridWidths(p.width, columns.length);
1273
+ return /* @__PURE__ */ jsx3(Box3, { height: p.height, width: p.width, columnGap: COLUMN_GAP, children: columns.map((ids, ci) => {
1274
+ const cards = ids.flatMap((id) => p.cards.find((c) => c.id === id) ?? []);
1275
+ const placed = fitColumn(
1276
+ p.height,
1277
+ cards.map((c) => ({
1278
+ id: c.id,
1279
+ // Attention keeps up to 3 rows even on a short screen: it is why you look.
1280
+ min: c.id === "attention" ? Math.max(1, Math.min(3, c.lines.length)) : 1,
1281
+ max: c.max,
1282
+ count: c.lines.length
1283
+ }))
1284
+ );
1285
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", width: widths[ci], flexShrink: 0, children: placed.map((pl) => {
1286
+ const card = cards.find((c) => c.id === pl.id);
1287
+ if (card === void 0) return null;
1288
+ const attention = card.id === "attention";
1289
+ const from = card.tail ? Math.max(0, card.lines.length - pl.rows) : attention ? scrollTop(0, p.sel, card.lines.length, pl.rows) : 0;
1290
+ const body = card.lines.slice(from, from + pl.rows).map(
1291
+ (l, i) => attention ? [{ text: from + i === p.sel ? "> " : " " }, ...l] : l
1292
+ );
1293
+ return /* @__PURE__ */ jsx3(
1294
+ Frame,
1295
+ {
1296
+ width: widths[ci] ?? 0,
1297
+ height: pl.height,
1298
+ title: card.title,
1299
+ subtitle: card.lines.length > 0 ? String(card.lines.length) : "",
1300
+ bottom: pl.hidden > 0 ? `+${pl.hidden} ${card.tail ? "older" : "more"}` : "",
1301
+ body: card.lines.length === 0 ? [[{ text: card.empty, role: card.emptyRole ?? "muted" }]] : body,
1302
+ ...attention && card.lines.length > 0 ? { selected: p.sel - from } : {}
1303
+ },
1304
+ card.id
1305
+ );
1306
+ }) }, ids.join());
1307
+ }) });
1308
+ }
1309
+
1310
+ // src/cli/tui/use-snapshot.ts
1311
+ import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
1312
+ var SLOW_TICK_MS = 1e4;
1313
+ var TICK_FLOOR_MS = 100;
1314
+ var TICK_CEILING_MS = 1e4;
1315
+ var clampTick = (ms) => Math.max(TICK_FLOOR_MS, Math.min(TICK_CEILING_MS, ms));
1316
+ var fasterTick = (ms) => clampTick(Math.floor(ms / 2));
1317
+ var slowerTick = (ms) => clampTick(ms * 2);
1318
+ var snapshotKey = (s) => JSON.stringify(
1319
+ s,
1320
+ (k, v) => k === "elapsed_ms" && typeof v === "number" ? Math.floor(v / 1e3) : v
1321
+ );
1322
+ function useSnapshot(runtime, query, pollMs, slowMs = SLOW_TICK_MS) {
1323
+ const [state, setState] = useState2({
1324
+ loading: true,
1325
+ error: null,
1326
+ snapshot: null,
1327
+ key: "",
1328
+ nonce: 0,
1329
+ slowNonce: 0
1330
+ });
1331
+ const queryKey = JSON.stringify(query);
1332
+ const wake = useRef2(null);
1333
+ const forceSlow = useRef2(false);
1334
+ const published = useRef2("");
1335
+ const lastSlow = useRef2(Number.NEGATIVE_INFINITY);
1336
+ useEffect2(() => {
1337
+ let cancelled = false;
1338
+ let timer;
1339
+ const q = JSON.parse(queryKey);
1340
+ const loop = async () => {
1341
+ while (!cancelled) {
1342
+ const slow = forceSlow.current || Date.now() - lastSlow.current >= slowMs;
1343
+ forceSlow.current = false;
1344
+ if (slow) lastSlow.current = Date.now();
1345
+ try {
1346
+ const r = await runtime.refresh(q, { slow });
1347
+ if (cancelled) return;
1348
+ const key2 = `${queryKey}\0${snapshotKey(r.snapshot)}`;
1349
+ const changed = key2 !== published.current;
1350
+ published.current = key2;
1351
+ if (changed || slow)
1352
+ setState((s) => {
1353
+ const next = {
1354
+ ...s,
1355
+ loading: false,
1356
+ error: null,
1357
+ key: queryKey,
1358
+ slowNonce: slow ? s.slowNonce + 1 : s.slowNonce
1359
+ };
1360
+ if (!changed && s.error === null && !s.loading)
1361
+ return { ...next, snapshot: s.snapshot };
1362
+ return { ...next, snapshot: r.snapshot, nonce: s.nonce + 1 };
1363
+ });
1364
+ else
1365
+ setState(
1366
+ (s) => (
1367
+ // Byte-equal and nothing to clear: the same state object, no re-render.
1368
+ s.error === null && !s.loading ? s : { ...s, loading: false, error: null }
1369
+ )
1370
+ );
1371
+ } catch (err) {
1372
+ if (cancelled) return;
1373
+ const error = err instanceof Error ? err.message : String(err);
1374
+ setState((s) => s.error === error && !s.loading ? s : { ...s, loading: false, error });
1375
+ }
1376
+ if (pollMs === null) return;
1377
+ await new Promise((resolve) => {
1378
+ timer = setTimeout(resolve, pollMs);
1379
+ wake.current = resolve;
1380
+ });
1381
+ wake.current = null;
1382
+ }
1383
+ };
1384
+ void loop();
1385
+ return () => {
1386
+ cancelled = true;
1387
+ if (timer !== void 0) clearTimeout(timer);
1388
+ wake.current?.();
1389
+ };
1390
+ }, [runtime, queryKey, pollMs, slowMs]);
1391
+ const refresh = useCallback(() => {
1392
+ forceSlow.current = true;
1393
+ wake.current?.();
1394
+ }, []);
1395
+ const { key, ...rest } = state;
1396
+ return { ...rest, stale: state.snapshot !== null && key !== queryKey, refresh };
1397
+ }
1398
+
1399
+ // src/cli/tui/app.tsx
1400
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1401
+ var FIXED_BY_PRIORITY = ["footer", "strip", "filter", "next"];
1402
+ var TOP_LINES = 2;
1403
+ function layout(rows, lines = FIXED_BY_PRIORITY) {
1404
+ const spare = Math.max(0, rows - TOP_LINES - 1);
1405
+ const shown = new Set(lines.slice(0, spare));
1406
+ return { shown, height: Math.max(0, rows - TOP_LINES - shown.size) };
1407
+ }
1408
+ var HINT_HEAD = `\u2190/\u2192 tab q quit [/] workstream ${NAV_HINT} move`;
1409
+ var HINT_OVERVIEW = `${HINT_HEAD} Enter open y copy +/- rate`;
1410
+ var HINT = `${HINT_HEAD} Enter open / filter c clear y copy +/- rate`;
1411
+ var HINT_TASKS = `${HINT_HEAD} Enter open s sort o status / filter c clear y copy +/- rate`;
1412
+ var hintAgents = (attachKey) => `${HINT_HEAD} Enter scrollback ${attachKey} / filter c clear y copy +/- rate`;
1413
+ function workstreamSegs(labels, selected, width) {
1414
+ const cell2 = (i) => i === selected ? `[${labels[i]}]` : ` ${labels[i]} `;
1415
+ let first = 0;
1416
+ const total = (from) => (from > 0 ? 1 : 0) + labels.slice(from).reduce((n, _, k) => n + stringWidth(clean(cell2(from + k))), 0);
1417
+ while (first < selected && total(first) > width) first++;
1418
+ return [
1419
+ ...first > 0 ? [{ text: "\u2026", role: "muted" }] : [],
1420
+ ...labels.flatMap(
1421
+ (_, i) => i >= first ? [{ text: cell2(i), role: i === selected ? "title" : "muted" }] : []
1422
+ )
1423
+ ];
1424
+ }
1425
+ function App({
1426
+ runtime,
1427
+ initialWorkstream,
1428
+ initialTab = "overview",
1429
+ pollMs,
1430
+ slowMs,
1431
+ now = Date.now,
1432
+ runStep = spawnStep
1433
+ }) {
1434
+ const { exit, suspendTerminal } = useApp();
1435
+ const { stdin } = useStdin();
1436
+ const { stdout } = useStdout2();
1437
+ const yank = useYank();
1438
+ const [drill, setDrill] = useState3(null);
1439
+ const busy = useRef3(false);
1440
+ const [flash, setFlash] = useState3(null);
1441
+ const [attachKey, setAttachKey] = useState3("a attach");
1442
+ useEffect3(() => {
1443
+ void attachKeyHint().then(setAttachKey);
1444
+ }, []);
1445
+ const { columns, rows: screenRows } = useWindowSize2();
1446
+ const [tab, setTab] = useState3(initialTab);
1447
+ const [chosen, setChosen] = useState3(null);
1448
+ const [views, setViews] = useState3({});
1449
+ const [editing, setEditing] = useState3(null);
1450
+ const tops = useRef3({});
1451
+ const lastSel = useRef3({});
1452
+ const [tick, setTick] = useState3(pollMs);
1453
+ const viewOf = (t, w) => views[scopeKey(t, w)] ?? { filter: NO_FILTER, cursor: null };
1454
+ const known = useRef3(null);
1455
+ const pickShown = (list) => list === null ? chosen ?? initialWorkstream ?? null : chosen !== null && list.some((w) => w.name === chosen) ? chosen : pickWorkstream(list, initialWorkstream);
1456
+ const queryWs = pickShown(known.current);
1457
+ const wantsClosed = tab === "tasks" && statusesOf(viewOf("tasks", queryWs).filter.status).includes("CLOSED");
1458
+ const query = {
1459
+ scopeWorkstream: queryWs,
1460
+ ...wantsClosed ? { status: [...statusesOf("all")] } : {},
1461
+ ...queryWs !== null ? { logWorkstream: queryWs } : {}
1462
+ };
1463
+ const state = useSnapshot(runtime, query, tick, slowMs);
1464
+ const snap = state.snapshot;
1465
+ const names = snap?.workstreams ?? [];
1466
+ known.current = snap?.workstreams ?? known.current;
1467
+ const ws = snap ? pickShown(names) : queryWs;
1468
+ useEffect3(() => {
1469
+ if (ws !== null && ws !== chosen) setChosen(ws);
1470
+ }, [ws, chosen]);
1471
+ const view = viewOf(tab, ws);
1472
+ const all = snap ? tabRows(tab, snap, ws, view.filter, now()) : [];
1473
+ const overview = tab === "overview";
1474
+ const { shown, height: area } = layout(
1475
+ screenRows,
1476
+ // The Overview has no filter; the strip shows on every view, next to the Attention card.
1477
+ FIXED_BY_PRIORITY.filter((l) => !(overview && l === "filter"))
1478
+ );
1479
+ const framed = !overview && area > FRAME_CHROME && columns > FRAME_SIDES + 2;
1480
+ const height = framed ? area - FRAME_CHROME : area;
1481
+ const scope = scopeKey(tab, ws);
1482
+ const sel = cursorIndex(all, view.cursor, tab, lastSel.current[scope]);
1483
+ lastSel.current[scope] = sel;
1484
+ const top = scrollTop(tops.current[scope] ?? 0, sel, all.length, height);
1485
+ tops.current[scope] = top;
1486
+ const patch = (change, t = tab) => setViews((v) => {
1487
+ const k = scopeKey(t, ws);
1488
+ return { ...v, [k]: { filter: NO_FILTER, cursor: null, ...v[k], ...change } };
1489
+ });
1490
+ const setFilter = (change) => patch({ filter: { ...view.filter, ...change }, cursor: null });
1491
+ const move = (delta) => patch({ cursor: moveCursor(all, view.cursor, tab, delta, lastSel.current[scope]) });
1492
+ useEffect3(() => {
1493
+ const key = all[sel]?.key;
1494
+ if (view.cursor === null && tab !== "log" && !state.stale && key !== void 0)
1495
+ setViews((v) => {
1496
+ const k = scopeKey(tab, ws);
1497
+ if (v[k]?.cursor != null) return v;
1498
+ return { ...v, [k]: { filter: NO_FILTER, ...v[k], cursor: key } };
1499
+ });
1500
+ });
1501
+ const cycle = (list, cur, d) => list.length === 0 ? null : list[(Math.max(0, list.indexOf(cur ?? "")) + d + list.length) % list.length] ?? null;
1502
+ const attach = async (t) => {
1503
+ if (busy.current) return;
1504
+ busy.current = true;
1505
+ setFlash(null);
1506
+ let outcome;
1507
+ const plan = await planAttach(t);
1508
+ if (!plan.ok) outcome = { ok: false, reason: plan.reason, warnings: [] };
1509
+ else {
1510
+ outcome = { ok: false, reason: "terminal unavailable", warnings: [] };
1511
+ try {
1512
+ await suspendTerminal(async () => {
1513
+ stdout.write(`${plan.notice}
1514
+ `);
1515
+ outcome = await runAttach(plan.steps, runStep);
1516
+ await new Promise((r) => setImmediate(r));
1517
+ while (stdin.read() !== null) {
1518
+ }
1519
+ busy.current = false;
1520
+ });
1521
+ } catch (e) {
1522
+ outcome = { ok: false, reason: e instanceof Error ? e.message : String(e), warnings: [] };
1523
+ }
1524
+ }
1525
+ busy.current = false;
1526
+ if (!outcome.ok)
1527
+ setDrill({
1528
+ target: t,
1529
+ notice: [
1530
+ `could not attach: ${outcome.reason}`,
1531
+ ...plan.line === null ? [] : [`from a shell outside the mux: ${plan.line}`]
1532
+ ]
1533
+ });
1534
+ else if (outcome.warnings.length > 0) setFlash(`attach: ${outcome.warnings.join("; ")}`);
1535
+ };
1536
+ useInput2(
1537
+ (input, key) => {
1538
+ if (busy.current) return;
1539
+ setFlash(null);
1540
+ if (editing !== null) {
1541
+ if (key.return) setEditing(null);
1542
+ else if (key.escape) {
1543
+ setFilter({ text: editing.before });
1544
+ setEditing(null);
1545
+ } else if (key.backspace || key.delete) setFilter({ text: view.filter.text.slice(0, -1) });
1546
+ else if (input !== "" && !key.ctrl && !key.meta)
1547
+ setFilter({ text: view.filter.text + clean(input) });
1548
+ return;
1549
+ }
1550
+ if (input === "q" || key.escape) return exit();
1551
+ const nav = navKey(input, key, height);
1552
+ const ids = TABS.map((t) => t.id);
1553
+ if (key.tab || key.rightArrow || key.leftArrow) {
1554
+ const back = key.leftArrow || key.tab && key.shift;
1555
+ setTab(cycle(ids, tab, back ? -1 : 1) ?? tab);
1556
+ state.refresh();
1557
+ } else if (input === "[" || input === "]") {
1558
+ const next = cycle(
1559
+ names.map((w) => w.name),
1560
+ ws,
1561
+ input === "]" ? 1 : -1
1562
+ );
1563
+ if (next !== null) setChosen(next);
1564
+ } else if (nav !== null)
1565
+ move("by" in nav ? nav.by : nav.to === "top" ? -all.length : all.length);
1566
+ else if (input === "s" && tab === "tasks")
1567
+ setFilter({ sort: nextSortLabel(view.filter.sort) });
1568
+ else if (input === "o" && tab === "tasks")
1569
+ setFilter({ status: nextStatusLabel(view.filter.status) });
1570
+ else if (input === "/" && !overview) setEditing({ before: view.filter.text });
1571
+ else if (input === "c" && !overview)
1572
+ setFilter({
1573
+ text: "",
1574
+ ...tab === "tasks" ? { status: "active", sort: "activity" } : {}
1575
+ });
1576
+ else if (input === "r") state.refresh();
1577
+ else if ((input === "+" || input === "=" || input === "-") && tick !== null) {
1578
+ const next = input === "-" ? slowerTick(tick) : fasterTick(tick);
1579
+ setTick(next);
1580
+ setFlash(`refresh every ${next >= 1e3 ? `${next / 1e3}s` : `${next}ms`}`);
1581
+ } else if (key.return) {
1582
+ const target = all[sel]?.drill;
1583
+ if (target !== void 0) setDrill({ target, notice: [] });
1584
+ } else if (input === "a" && tab === "agents") {
1585
+ const target = all[sel]?.drill;
1586
+ if (target?.kind === "scrollback") void attach(target);
1587
+ } else if (input === "y") {
1588
+ const target = all[sel]?.drill;
1589
+ if (target !== void 0) void yank(target).then(setFlash);
1590
+ }
1591
+ },
1592
+ // A drill owns the keys while it is open: App reads none.
1593
+ { isActive: drill === null }
1594
+ );
1595
+ const text = (s) => cell(s, columns);
1596
+ const attention = snap?.attention ?? [];
1597
+ const lineOf = (r, i) => [{ text: i === sel ? "> " : " " }, ...r.line];
1598
+ const visible = all.slice(top, top + height);
1599
+ const f = view.filter;
1600
+ const filterLine = [
1601
+ tab === "tasks" ? `status: ${f.status}` : "",
1602
+ tab === "tasks" ? `sort: ${f.sort} ${SORT_ARROWS[f.sort]}` : "",
1603
+ `filter: ${f.text}${editing ? "_" : ""}`,
1604
+ snap ? `${all.length} row${all.length === 1 ? "" : "s"}` : "",
1605
+ state.stale || state.loading && !snap ? "loading\u2026" : ""
1606
+ ].filter((p) => p !== "").join(" ");
1607
+ const tabIndex = TABS.findIndex((t) => t.id === tab);
1608
+ const tabsFit = TABS.reduce((n, t) => n + t.label.length + 2, 0) <= columns;
1609
+ const wsLabels = names.map(workstreamLabel);
1610
+ const wsIndex = Math.max(
1611
+ 0,
1612
+ names.findIndex((w) => w.name === ws)
1613
+ );
1614
+ const strip = attentionStrip(attention, ws);
1615
+ const empty = snap === null ? state.error ? "" : "loading\u2026" : all.length === 0 ? f.text !== "" ? "no rows match the filter" : "nothing here" : "";
1616
+ const footer = flash ? { text: flash, role: "text" } : state.error ? { text: `error: ${state.error}`, role: "bad" } : {
1617
+ text: editing ? "type to filter Enter keep Esc cancel" : overview ? HINT_OVERVIEW : tab === "agents" ? hintAgents(attachKey) : tab === "tasks" ? HINT_TASKS : HINT,
1618
+ role: "muted"
1619
+ };
1620
+ if (drill !== null)
1621
+ return /* @__PURE__ */ jsx4(
1622
+ Drill,
1623
+ {
1624
+ target: drill.target,
1625
+ notice: drill.notice,
1626
+ strip,
1627
+ runtime,
1628
+ busy,
1629
+ nonce: state.nonce,
1630
+ slowNonce: state.slowNonce,
1631
+ onClose: () => setDrill(null),
1632
+ onAttach: (t) => void attach(t)
1633
+ }
1634
+ );
1635
+ const bodyLines = [
1636
+ ...visible.map((r, i) => lineOf(r, top + i)),
1637
+ ...empty ? [[{ text: ` ${empty}`, role: "muted" }]] : []
1638
+ ];
1639
+ const tabLabel = TABS[tabIndex]?.label ?? "";
1640
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", height: screenRows, children: [
1641
+ /* @__PURE__ */ jsx4(Text3, { wrap: "truncate", children: /* @__PURE__ */ jsx4(
1642
+ Segs,
1643
+ {
1644
+ line: fitLine(
1645
+ wsLabels.length > 0 ? workstreamSegs(wsLabels, wsIndex, columns) : [{ text: "no workstreams", role: "muted" }],
1646
+ columns
1647
+ )
1648
+ }
1649
+ ) }),
1650
+ tabsFit ? /* @__PURE__ */ jsx4(Box4, { children: TABS.map((t) => /* @__PURE__ */ jsx4(Text3, { inverse: t.id === tab, ...style(t.id === tab ? "title" : "muted"), children: ` ${t.label} ` }, t.id)) }) : (
1651
+ // Too narrow for the labels (they would wrap onto a second row): show the
1652
+ // active one and its place.
1653
+ /* @__PURE__ */ jsx4(Text3, { wrap: "truncate", inverse: true, ...style("title"), children: cell(` ${tabLabel} ${tabIndex + 1}/${TABS.length}`, columns) })
1654
+ ),
1655
+ shown.has("strip") ? /* @__PURE__ */ jsx4(Text3, { wrap: "truncate", children: /* @__PURE__ */ jsx4(Segs, { line: fitLine(strip, columns) }) }) : null,
1656
+ shown.has("filter") ? /* @__PURE__ */ jsx4(Text3, { wrap: "truncate", ...style("muted"), children: text(filterLine) }) : null,
1657
+ overview ? /* @__PURE__ */ jsx4(
1658
+ Overview,
1659
+ {
1660
+ cards: snap ? overviewCards(snap, ws, now()) : [],
1661
+ sel,
1662
+ width: columns,
1663
+ height: area
1664
+ }
1665
+ ) : framed ? /* @__PURE__ */ jsx4(
1666
+ Frame,
1667
+ {
1668
+ width: columns,
1669
+ height: area,
1670
+ title: tabLabel,
1671
+ subtitle: snap ? String(all.length) : "",
1672
+ bottom: all.length > height ? `${sel + 1}/${all.length}` : "",
1673
+ body: bodyLines,
1674
+ selected: sel - top
1675
+ }
1676
+ ) : bodyLines.map((l, i) => (
1677
+ // biome-ignore lint/suspicious/noArrayIndexKey: positional rows of a fixed-height viewport
1678
+ /* @__PURE__ */ jsx4(Text3, { wrap: "truncate", children: i === sel - top ? /* @__PURE__ */ jsx4(Text3, { inverse: true, children: text(l.map((s) => s.text).join("")) }) : /* @__PURE__ */ jsx4(Segs, { line: fitLine(l, columns) }) }, i)
1679
+ )),
1680
+ /* @__PURE__ */ jsx4(Box4, { flexGrow: 1 }),
1681
+ shown.has("next") ? /* @__PURE__ */ jsx4(Text3, { wrap: "truncate", ...style("muted"), children: text(snap ? nextLine(snap) : "next: unavailable") }) : null,
1682
+ shown.has("footer") ? /* @__PURE__ */ jsx4(Text3, { wrap: "truncate", ...style(footer.role), children: text(footer.text) }) : null
1683
+ ] });
1684
+ }
1685
+
1686
+ // src/cli/tui/runtime.ts
1687
+ var DEFAULT_SYNC_EVERY_MS = 1e4;
1688
+ function createRuntime(opts) {
1689
+ const ro = openDb({ path: opts.dbPath, readonly: true });
1690
+ const now = opts.now ?? Date.now;
1691
+ const every = opts.syncEveryMs ?? DEFAULT_SYNC_EVERY_MS;
1692
+ let inflight = null;
1693
+ let inflightQuery = "";
1694
+ let lastSync = Number.NEGATIVE_INFINITY;
1695
+ let warnings = [];
1696
+ let seenNames = "";
1697
+ let closed = false;
1698
+ const agentStates = /* @__PURE__ */ new Map();
1699
+ async function run(query, slow) {
1700
+ if (syncEnabled() && now() - lastSync >= every) {
1701
+ lastSync = now();
1702
+ warnings = (await ambientIngest(opts.syncDb, { quiet: true })).warnings;
1703
+ }
1704
+ const names = ro.prepare("SELECT name FROM workstreams WHERE torn_down_at IS NULL ORDER BY name").all().map((w) => w.name).join("\n");
1705
+ const activity = names !== seenNames;
1706
+ seenNames = names;
1707
+ const snapshot = await loadDashboard(ro, {
1708
+ ...query,
1709
+ activity,
1710
+ caller: opts.caller,
1711
+ callers: opts.callers ?? currentCallers(),
1712
+ syncWarnings: warnings,
1713
+ agentStates: { cache: agentStates, probe: slow }
1714
+ });
1715
+ return { snapshot, warnings };
1716
+ }
1717
+ return {
1718
+ refresh(query = {}, { slow = false } = {}) {
1719
+ if (closed) return Promise.reject(new Error("dashboard runtime is closed"));
1720
+ const key = `${slow ? "slow" : "fast"}:${JSON.stringify(query)}`;
1721
+ if (inflight !== null && inflightQuery === key) return inflight;
1722
+ const prior = inflight?.catch(() => void 0) ?? Promise.resolve();
1723
+ inflightQuery = key;
1724
+ const mine = prior.then(() => run(query, slow)).finally(() => {
1725
+ if (inflight === mine) inflight = null;
1726
+ });
1727
+ inflight = mine;
1728
+ return mine;
1729
+ },
1730
+ notes(workstream, id, all) {
1731
+ if (closed) throw new Error("dashboard runtime is closed");
1732
+ return selectTaskNotes(ro, id, { workstream, all });
1733
+ },
1734
+ task(workstream, id) {
1735
+ if (closed) throw new Error("dashboard runtime is closed");
1736
+ return getTask(ro, id, workstream);
1737
+ },
1738
+ async close() {
1739
+ if (closed) return;
1740
+ closed = true;
1741
+ try {
1742
+ await inflight?.catch(() => void 0);
1743
+ } finally {
1744
+ ro.close();
1745
+ }
1746
+ }
1747
+ };
1748
+ }
1749
+
1750
+ // src/cli/tui/index.ts
1751
+ async function runDashboard(opts) {
1752
+ const runtime = createRuntime({
1753
+ dbPath: opts.dbPath,
1754
+ syncDb: opts.syncDb,
1755
+ caller: spawnerFromEnv()
1756
+ });
1757
+ let app;
1758
+ opts.shutdown.add(() => app?.unmount());
1759
+ opts.shutdown.add(() => runtime.close());
1760
+ app = render(
1761
+ createElement(App, {
1762
+ runtime,
1763
+ initialWorkstream: opts.initialWorkstream ?? await resolveMuxSessionWorkstreamName() ?? void 0,
1764
+ pollMs: opts.pollMs ?? 1e3
1765
+ }),
1766
+ { alternateScreen: true, interactive: true }
1767
+ );
1768
+ await app.waitUntilExit();
1769
+ }
1770
+ export {
1771
+ runDashboard
1772
+ };
1773
+ //# sourceMappingURL=tui-ALPHYTA2.js.map