@h-rig/cli 0.0.6-alpha.83 → 0.0.6-alpha.85

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 (163) hide show
  1. package/dist/bin/rig.js +22060 -14620
  2. package/dist/src/app/board.js +155 -28
  3. package/dist/src/app-opentui/adapters/doctor.d.ts +0 -2
  4. package/dist/src/app-opentui/adapters/doctor.js +1 -25
  5. package/dist/src/app-opentui/adapters/family.d.ts +62 -0
  6. package/dist/src/app-opentui/adapters/family.js +14305 -0
  7. package/dist/src/app-opentui/adapters/fleet.d.ts +0 -2
  8. package/dist/src/app-opentui/adapters/fleet.js +6 -40
  9. package/dist/src/app-opentui/adapters/inbox.d.ts +12 -2
  10. package/dist/src/app-opentui/adapters/inbox.js +54 -59
  11. package/dist/src/app-opentui/adapters/init.d.ts +0 -2
  12. package/dist/src/app-opentui/adapters/init.js +0 -26
  13. package/dist/src/app-opentui/adapters/inspect.d.ts +52 -0
  14. package/dist/src/app-opentui/adapters/inspect.js +1024 -0
  15. package/dist/src/app-opentui/adapters/pi-attach.d.ts +15 -6
  16. package/dist/src/app-opentui/adapters/pi-attach.js +348 -93
  17. package/dist/src/app-opentui/adapters/pi.d.ts +23 -0
  18. package/dist/src/app-opentui/adapters/pi.js +363 -0
  19. package/dist/src/app-opentui/adapters/plugin.d.ts +84 -0
  20. package/dist/src/app-opentui/adapters/plugin.js +544 -0
  21. package/dist/src/app-opentui/adapters/repo.d.ts +37 -0
  22. package/dist/src/app-opentui/adapters/repo.js +186 -0
  23. package/dist/src/app-opentui/adapters/run-detail.d.ts +9 -2
  24. package/dist/src/app-opentui/adapters/run-detail.js +97 -44
  25. package/dist/src/app-opentui/adapters/server.d.ts +10 -2
  26. package/dist/src/app-opentui/adapters/server.js +96 -27
  27. package/dist/src/app-opentui/adapters/tasks.d.ts +0 -2
  28. package/dist/src/app-opentui/adapters/tasks.js +437 -105
  29. package/dist/src/app-opentui/adapters/workspace.d.ts +49 -0
  30. package/dist/src/app-opentui/adapters/workspace.js +333 -0
  31. package/dist/src/app-opentui/autocomplete.d.ts +20 -0
  32. package/dist/src/app-opentui/autocomplete.js +576 -0
  33. package/dist/src/app-opentui/bootstrap.js +24313 -16963
  34. package/dist/src/app-opentui/command-palette.d.ts +3 -0
  35. package/dist/src/app-opentui/command-palette.js +1010 -0
  36. package/dist/src/app-opentui/drone.js +8 -6
  37. package/dist/src/app-opentui/fleet-stats.d.ts +32 -0
  38. package/dist/src/app-opentui/fleet-stats.js +114 -0
  39. package/dist/src/app-opentui/index.js +3438 -1724
  40. package/dist/src/app-opentui/intent.js +71 -48
  41. package/dist/src/app-opentui/keymap.d.ts +6 -5
  42. package/dist/src/app-opentui/keymap.js +1064 -23
  43. package/dist/src/app-opentui/layout.d.ts +7 -0
  44. package/dist/src/app-opentui/layout.js +13 -6
  45. package/dist/src/app-opentui/list-search.d.ts +24 -0
  46. package/dist/src/app-opentui/list-search.js +88 -1
  47. package/dist/src/app-opentui/pi-host-child.js +33 -1
  48. package/dist/src/app-opentui/pi-pty-host.d.ts +5 -0
  49. package/dist/src/app-opentui/pi-pty-host.js +21 -3
  50. package/dist/src/app-opentui/react/App.d.ts +9 -0
  51. package/dist/src/app-opentui/react/App.js +5099 -0
  52. package/dist/src/app-opentui/react/Backdrop.d.ts +5 -0
  53. package/dist/src/app-opentui/react/Backdrop.js +829 -0
  54. package/dist/src/app-opentui/react/ChromeHost.d.ts +5 -0
  55. package/dist/src/app-opentui/react/ChromeHost.js +2942 -0
  56. package/dist/src/app-opentui/react/SceneFrameView.d.ts +7 -0
  57. package/dist/src/app-opentui/react/SceneFrameView.js +529 -0
  58. package/dist/src/app-opentui/react/context.d.ts +17 -0
  59. package/dist/src/app-opentui/react/context.js +37 -0
  60. package/dist/src/app-opentui/react/launch.d.ts +2 -0
  61. package/dist/src/app-opentui/react/launch.js +5698 -0
  62. package/dist/src/app-opentui/react/nav.d.ts +18 -0
  63. package/dist/src/app-opentui/react/nav.js +54 -0
  64. package/dist/src/app-opentui/react/scroll.d.ts +12 -0
  65. package/dist/src/app-opentui/react/scroll.js +21 -0
  66. package/dist/src/app-opentui/react/syntax.d.ts +2 -0
  67. package/dist/src/app-opentui/react/syntax.js +64 -0
  68. package/dist/src/app-opentui/registry.js +19923 -5151
  69. package/dist/src/app-opentui/render/constants.d.ts +31 -0
  70. package/dist/src/app-opentui/render/constants.js +66 -0
  71. package/dist/src/app-opentui/render/graphics.d.ts +2 -1
  72. package/dist/src/app-opentui/render/graphics.js +106 -39
  73. package/dist/src/app-opentui/render/image-visual-layer-worker.js +108 -20
  74. package/dist/src/app-opentui/render/image-visual-layer.d.ts +7 -0
  75. package/dist/src/app-opentui/render/image-visual-layer.js +109 -20
  76. package/dist/src/app-opentui/render/native-host.d.ts +37 -0
  77. package/dist/src/app-opentui/render/native-host.js +179 -0
  78. package/dist/src/app-opentui/render/panels.js +18 -11
  79. package/dist/src/app-opentui/render/preloader.js +8 -6
  80. package/dist/src/app-opentui/render/scene.d.ts +50 -1
  81. package/dist/src/app-opentui/render/scene.js +183 -6
  82. package/dist/src/app-opentui/render/text.d.ts +7 -1
  83. package/dist/src/app-opentui/render/text.js +10 -7
  84. package/dist/src/app-opentui/render/type-bar.js +69 -30
  85. package/dist/src/app-opentui/runtime.d.ts +44 -1
  86. package/dist/src/app-opentui/runtime.js +3231 -1363
  87. package/dist/src/app-opentui/scenes/command.js +20 -6
  88. package/dist/src/app-opentui/scenes/doctor.js +176 -11
  89. package/dist/src/app-opentui/scenes/error.js +20 -10
  90. package/dist/src/app-opentui/scenes/family-domains/agent.d.ts +2 -0
  91. package/dist/src/app-opentui/scenes/family-domains/agent.js +348 -0
  92. package/dist/src/app-opentui/scenes/family-domains/browser.d.ts +2 -0
  93. package/dist/src/app-opentui/scenes/family-domains/browser.js +195 -0
  94. package/dist/src/app-opentui/scenes/family-domains/dist.d.ts +2 -0
  95. package/dist/src/app-opentui/scenes/family-domains/dist.js +243 -0
  96. package/dist/src/app-opentui/scenes/family-domains/git.d.ts +2 -0
  97. package/dist/src/app-opentui/scenes/family-domains/git.js +195 -0
  98. package/dist/src/app-opentui/scenes/family-domains/github.d.ts +2 -0
  99. package/dist/src/app-opentui/scenes/family-domains/github.js +274 -0
  100. package/dist/src/app-opentui/scenes/family-domains/harness.d.ts +2 -0
  101. package/dist/src/app-opentui/scenes/family-domains/harness.js +152 -0
  102. package/dist/src/app-opentui/scenes/family-domains/index.d.ts +4 -0
  103. package/dist/src/app-opentui/scenes/family-domains/index.js +1679 -0
  104. package/dist/src/app-opentui/scenes/family-domains/kit.d.ts +76 -0
  105. package/dist/src/app-opentui/scenes/family-domains/kit.js +305 -0
  106. package/dist/src/app-opentui/scenes/family-domains/profile.d.ts +2 -0
  107. package/dist/src/app-opentui/scenes/family-domains/profile.js +212 -0
  108. package/dist/src/app-opentui/scenes/family-domains/queue.d.ts +2 -0
  109. package/dist/src/app-opentui/scenes/family-domains/queue.js +146 -0
  110. package/dist/src/app-opentui/scenes/family-domains/remote.d.ts +2 -0
  111. package/dist/src/app-opentui/scenes/family-domains/remote.js +518 -0
  112. package/dist/src/app-opentui/scenes/family-domains/review.d.ts +2 -0
  113. package/dist/src/app-opentui/scenes/family-domains/review.js +280 -0
  114. package/dist/src/app-opentui/scenes/family-domains/setup.d.ts +2 -0
  115. package/dist/src/app-opentui/scenes/family-domains/setup.js +267 -0
  116. package/dist/src/app-opentui/scenes/family-domains/stats.d.ts +2 -0
  117. package/dist/src/app-opentui/scenes/family-domains/stats.js +370 -0
  118. package/dist/src/app-opentui/scenes/family.d.ts +3 -0
  119. package/dist/src/app-opentui/scenes/family.js +2144 -0
  120. package/dist/src/app-opentui/scenes/fleet.js +521 -31
  121. package/dist/src/app-opentui/scenes/handoff.js +204 -12
  122. package/dist/src/app-opentui/scenes/help.js +609 -40
  123. package/dist/src/app-opentui/scenes/inbox.js +278 -17
  124. package/dist/src/app-opentui/scenes/init.js +84 -39
  125. package/dist/src/app-opentui/scenes/inspect.d.ts +3 -0
  126. package/dist/src/app-opentui/scenes/inspect.js +793 -0
  127. package/dist/src/app-opentui/scenes/main.js +218 -39
  128. package/dist/src/app-opentui/scenes/pi.d.ts +3 -0
  129. package/dist/src/app-opentui/scenes/pi.js +508 -0
  130. package/dist/src/app-opentui/scenes/plugin.d.ts +3 -0
  131. package/dist/src/app-opentui/scenes/plugin.js +486 -0
  132. package/dist/src/app-opentui/scenes/repo.d.ts +3 -0
  133. package/dist/src/app-opentui/scenes/repo.js +424 -0
  134. package/dist/src/app-opentui/scenes/run-detail.js +333 -20
  135. package/dist/src/app-opentui/scenes/server.js +181 -15
  136. package/dist/src/app-opentui/scenes/tasks.js +486 -31
  137. package/dist/src/app-opentui/scenes/workspace.d.ts +3 -0
  138. package/dist/src/app-opentui/scenes/workspace.js +426 -0
  139. package/dist/src/app-opentui/selectable.d.ts +19 -0
  140. package/dist/src/app-opentui/selectable.js +79 -0
  141. package/dist/src/app-opentui/state.js +83 -30
  142. package/dist/src/app-opentui/surface-catalog.d.ts +20 -0
  143. package/dist/src/app-opentui/surface-catalog.js +540 -0
  144. package/dist/src/app-opentui/theme.d.ts +28 -6
  145. package/dist/src/app-opentui/theme.js +61 -8
  146. package/dist/src/app-opentui/types.d.ts +121 -4
  147. package/dist/src/commands/_authority-runs.d.ts +1 -1
  148. package/dist/src/commands/_authority-runs.js +2 -12
  149. package/dist/src/commands/_help-catalog.js +95 -15
  150. package/dist/src/commands/_operator-view.js +34 -2
  151. package/dist/src/commands/_pi-frontend.d.ts +2 -0
  152. package/dist/src/commands/_pi-frontend.js +34 -0
  153. package/dist/src/commands/_server-events.d.ts +26 -0
  154. package/dist/src/commands/_server-events.js +310 -0
  155. package/dist/src/commands/agent.js +2 -12
  156. package/dist/src/commands/run.js +34 -2
  157. package/dist/src/commands/server.js +2 -12
  158. package/dist/src/commands/stats.js +95 -15
  159. package/dist/src/commands/task-run-driver.js +2 -12
  160. package/dist/src/commands/task.js +131 -29
  161. package/dist/src/commands.js +192 -31
  162. package/dist/src/index.js +192 -31
  163. package/package.json +11 -9
@@ -11,15 +11,17 @@ var RIG_UI = {
11
11
  bg: "#070809",
12
12
  bg2: "#0b0c0e",
13
13
  panel: "#101115",
14
- panel2: "#101115",
15
- glass: "#14161b",
16
- ink: "#f2f3f6",
17
- ink2: "#aeb0ba",
18
- ink3: "#6c6e79",
19
- ink4: "#44464f",
14
+ panel2: "#14161b",
15
+ glass: "#1e2230",
16
+ border: "#2a2e3a",
17
+ ink: "#f4f5f8",
18
+ ink2: "#c7c9d3",
19
+ ink3: "#9aa0ae",
20
+ ink4: "#787b86",
20
21
  lime: "#ccff4d",
21
22
  limeDim: "#a9d63f",
22
23
  cyan: "#56d8ff",
24
+ cyanDim: "#3f9fbd",
23
25
  red: "#ff5d5d",
24
26
  yellow: "#ffd24d",
25
27
  magenta: "#ff79b0"
@@ -38,11 +40,11 @@ var styles = {
38
40
  };
39
41
  function statusColor(status) {
40
42
  switch (status) {
41
- case "running":
42
43
  case "success":
43
44
  case "completed":
44
45
  case "merged":
45
46
  return RIG_UI.lime;
47
+ case "running":
46
48
  case "preparing":
47
49
  case "created":
48
50
  case "validating":
@@ -62,11 +64,182 @@ function statusColor(status) {
62
64
  return RIG_UI.ink3;
63
65
  }
64
66
  }
67
+ function statusGlyph(status) {
68
+ switch (status) {
69
+ case "success":
70
+ case "completed":
71
+ case "merged":
72
+ return "\u2713";
73
+ case "running":
74
+ return "\u25CF";
75
+ case "preparing":
76
+ case "created":
77
+ case "validating":
78
+ case "reviewing":
79
+ case "closing-out":
80
+ case "stopping":
81
+ case "loading":
82
+ case "action":
83
+ return "\u25D0";
84
+ case "needs-attention":
85
+ case "needs_attention":
86
+ return "\u26A0";
87
+ case "failed":
88
+ case "error":
89
+ return "\u2717";
90
+ default:
91
+ return "\xB7";
92
+ }
93
+ }
94
+ function noColorActive() {
95
+ const noColor = process.env.NO_COLOR;
96
+ if (noColor !== undefined && noColor !== "") {
97
+ return true;
98
+ }
99
+ return process.env.RIG_NO_COLOR !== undefined;
100
+ }
101
+ function statusLabel(status) {
102
+ return {
103
+ glyph: statusGlyph(status),
104
+ color: noColorActive() ? RIG_UI.ink : statusColor(status)
105
+ };
106
+ }
107
+
108
+ // packages/cli/src/app-opentui/drone.ts
109
+ import { RGBA, StyledText, TextAttributes as TextAttributes2 } from "@opentui/core";
110
+ var MINI_DRONE = [
111
+ "(!!!) (!!!)",
112
+ " \\%==%/ ",
113
+ " %%?%% ",
114
+ " /%==%\\ ",
115
+ "(!!!) (!!!)"
116
+ ];
117
+ var LEAD_MARK = [
118
+ " .-=-. ",
119
+ "=%%?%%=",
120
+ " '-=-' "
121
+ ];
122
+ var BLADE_FRAMES = ["---", "\\\\\\", "|||", "///"];
123
+ var EYE_FRAMES = ["o", "@", "\u2022", "."];
124
+ var COLOR = {
125
+ body: RGBA.fromHex(RIG_UI.lime),
126
+ mini: RGBA.fromHex(RIG_UI.limeDim),
127
+ rotor: RGBA.fromHex(RIG_UI.cyan),
128
+ path: RGBA.fromHex(RIG_UI.cyan),
129
+ eye: RGBA.fromHex(RIG_UI.ink),
130
+ dim: RGBA.fromHex(RIG_UI.ink4),
131
+ ink: RGBA.fromHex(RIG_UI.ink2)
132
+ };
133
+ function bladeForTick(tick, phase = 0) {
134
+ return BLADE_FRAMES[(Math.floor(tick / 3) + phase) % BLADE_FRAMES.length];
135
+ }
136
+ function eyeForTick(tick, phase = 0) {
137
+ const pulse = Math.sin(tick * 0.09 + phase);
138
+ return pulse > 0.55 ? EYE_FRAMES[1] : pulse > 0.1 ? EYE_FRAMES[0] : pulse > -0.45 ? EYE_FRAMES[2] : EYE_FRAMES[3];
139
+ }
140
+ function chunk(text, fg, bold = false, dim = false) {
141
+ let attributes = TextAttributes2.NONE;
142
+ if (bold)
143
+ attributes |= TextAttributes2.BOLD;
144
+ if (dim)
145
+ attributes |= TextAttributes2.DIM;
146
+ return { __isChunk: true, text, fg, ...attributes !== TextAttributes2.NONE ? { attributes } : {} };
147
+ }
148
+ function styledLine(text, colorFor) {
149
+ const chunks = [];
150
+ let run = "";
151
+ let runColor = null;
152
+ const flush = () => {
153
+ if (!run || !runColor)
154
+ return;
155
+ chunks.push(chunk(run, COLOR[runColor], runColor === "body" || runColor === "eye", runColor === "dim"));
156
+ run = "";
157
+ };
158
+ for (const char of text) {
159
+ const next = colorFor(char);
160
+ if (next !== runColor) {
161
+ flush();
162
+ runColor = next;
163
+ }
164
+ run += char;
165
+ }
166
+ flush();
167
+ return new StyledText(chunks.length > 0 ? chunks : [chunk("", COLOR.ink)]);
168
+ }
169
+ function droneColor(char) {
170
+ if (char === "?" || char === "o" || char === "@" || char === "\u2022")
171
+ return "eye";
172
+ if (char === "%" || char === "=")
173
+ return "mini";
174
+ if (char === "$")
175
+ return "body";
176
+ if (char === "(" || char === ")" || char === "/" || char === "\\" || char === "|" || char === "-")
177
+ return "rotor";
178
+ if (char === "\xB7" || char === "\u2022" || char === "'" || char === ".")
179
+ return "path";
180
+ if (char.trim() === "")
181
+ return "dim";
182
+ return "ink";
183
+ }
184
+ function motionColor(char) {
185
+ if (char === "\u25CF" || char === "\u25C6" || char === "\u25C9")
186
+ return "body";
187
+ if (char === "\u2022" || char === "\xB7" || char === "\u2500" || char === "\u2502" || char === "\u2571" || char === "\u2572" || char === "\u256D" || char === "\u256E" || char === "\u2570" || char === "\u256F")
188
+ return "path";
189
+ if (char === "\u258C" || char === "\u2590" || char === "\u2581" || char === "\u2594")
190
+ return "mini";
191
+ return droneColor(char);
192
+ }
193
+ function materializeDrone(lines, tick, phase = 0) {
194
+ const blade = bladeForTick(tick, phase);
195
+ const eye = eyeForTick(tick, phase);
196
+ return lines.map((line) => line.replaceAll("!!!", blade).replaceAll("?", eye));
197
+ }
198
+ function miniDroneLines(tick, align = "left", phase = 0) {
199
+ return materializeDrone(MINI_DRONE, tick, phase).map((text) => ({
200
+ text,
201
+ styledText: styledLine(text, droneColor),
202
+ fg: RIG_UI.limeDim,
203
+ align
204
+ }));
205
+ }
206
+ function brandFleetLines(tick, align = "center") {
207
+ const left = materializeDrone(MINI_DRONE, tick, 0);
208
+ const right = materializeDrone(MINI_DRONE, tick, 2);
209
+ const lead = materializeDrone(LEAD_MARK, tick, 1);
210
+ const packet = ["\xB7", "\u2022", "\u25CF", "\u2022"][Math.floor(tick / 2) % 4];
211
+ const bridge = [
212
+ ` ${left[0]} ${lead[0]} ${right[0]}`,
213
+ ` ${left[1]} \xB7\u2500\u2500${packet}\u2500\u2500\xB7 ${lead[1]} \xB7\u2500\u2500${packet}\u2500\u2500\xB7 ${right[1]}`,
214
+ ` ${left[2]} \xB7' '${lead[2]}' '\xB7 ${right[2]}`,
215
+ ` ${left[3]} \u2572 signal bus \u2571 ${right[3]}`,
216
+ ` ${left[4]} \xB7\u2500\u2500\u2500${packet}\u2500\u2500\u2500\u2500\u2500\u2500${packet}\u2500\u2500\u2500\xB7 ${right[4]}`
217
+ ];
218
+ return bridge.map((text) => ({
219
+ text,
220
+ styledText: styledLine(text, motionColor),
221
+ fg: RIG_UI.limeDim,
222
+ align
223
+ }));
224
+ }
225
+ function actionDrone(tick, phase = 0) {
226
+ const blade = bladeForTick(tick, phase);
227
+ const eye = eyeForTick(tick, phase);
228
+ return `(${blade})\u2572${eye}\u2571(${blade})`;
229
+ }
230
+ function actionDroneLine(tick, text, fg = RIG_UI.ink2) {
231
+ const prefix = actionDrone(tick);
232
+ const content = `${prefix} ${text}`;
233
+ return { text: content, styledText: styledLine(content, motionColor), fg, align: "center" };
234
+ }
65
235
 
66
236
  // packages/cli/src/app-opentui/render/scene.ts
67
237
  function line(text, options = {}) {
68
238
  return { text, ...options };
69
239
  }
240
+ function blank() {
241
+ return { text: "" };
242
+ }
70
243
  function deckRow(input) {
71
244
  const prefix = input.active ? "\u25B8" : " ";
72
245
  const label = input.label.toUpperCase().padEnd(10);
@@ -77,10 +250,39 @@ function deckRow(input) {
77
250
  activateOnClick: input.activateOnClick
78
251
  });
79
252
  }
253
+ function loadingRows(label, tick = 0, compact = false) {
254
+ const mascot = compact ? miniDroneLines(tick, "center") : brandFleetLines(tick, "center");
255
+ return [
256
+ blank(),
257
+ ...mascot,
258
+ blank(),
259
+ { ...actionDroneLine(tick, `loading ${label}\u2026`, RIG_UI.cyan), align: "center" }
260
+ ];
261
+ }
262
+ function errorBanner(message, hint) {
263
+ return [
264
+ line(`\u2717 failed to load \u2014 ${message}`, { fg: RIG_UI.red, bold: true }),
265
+ ...hint ? [line(` ${hint}`, { fg: RIG_UI.ink3 })] : [],
266
+ line(" select refresh/retry below", { fg: RIG_UI.ink4 }),
267
+ line("", { fg: RIG_UI.ink3 })
268
+ ];
269
+ }
270
+ function lastRefreshError(state) {
271
+ const value = state.data.lastRefreshError;
272
+ return typeof value === "string" && value.trim() ? value : undefined;
273
+ }
80
274
  function makeSceneFrame(input) {
81
275
  return input;
82
276
  }
83
277
 
278
+ // packages/cli/src/app-opentui/selectable.ts
279
+ function selectableDeckRow(deck, item) {
280
+ return { ...deckRow({ ...deck, activateOnClick: true }), selectable: item };
281
+ }
282
+ function withSelectable(line2, item) {
283
+ return { ...line2, selectable: item, activateOnClick: true };
284
+ }
285
+
84
286
  // packages/cli/src/app-opentui/list-search.ts
85
287
  function isListSearchScene(scene) {
86
288
  return scene === "fleet" || scene === "tasks";
@@ -113,8 +315,86 @@ function filterTasksForSearch(tasks, query) {
113
315
  function searchSummary(kind, query, visible, total) {
114
316
  return query.trim() ? `search ${JSON.stringify(query)} \xB7 ${visible}/${total} ${kind}` : `${total} ${kind}`;
115
317
  }
318
+ function taskSortSpec(value) {
319
+ if (!value || typeof value !== "object" || Array.isArray(value))
320
+ return null;
321
+ const record = value;
322
+ const key = record.key;
323
+ const dir = record.dir;
324
+ if ((key === "status" || key === "id" || key === "priority" || key === "assignee" || key === "title") && (dir === "asc" || dir === "desc")) {
325
+ return { key, dir };
326
+ }
327
+ return null;
328
+ }
329
+ function cycleTaskSort(current, key) {
330
+ if (!current || current.key !== key)
331
+ return { key, dir: "asc" };
332
+ if (current.dir === "asc")
333
+ return { key, dir: "desc" };
334
+ return null;
335
+ }
336
+ function applyDir(rows, compare, dir) {
337
+ const sorted = [...rows].sort(compare);
338
+ return dir === "desc" ? sorted.reverse() : sorted;
339
+ }
340
+ function sortTasks(tasks, spec) {
341
+ if (!spec)
342
+ return [...tasks];
343
+ const { key, dir } = spec;
344
+ const compare = (a, b) => {
345
+ if (key === "status")
346
+ return a.status.localeCompare(b.status) || a.id.localeCompare(b.id, undefined, { numeric: true });
347
+ if (key === "priority") {
348
+ const left = a.priority ?? Number.NEGATIVE_INFINITY;
349
+ const right = b.priority ?? Number.NEGATIVE_INFINITY;
350
+ return right - left || a.id.localeCompare(b.id, undefined, { numeric: true });
351
+ }
352
+ if (key === "assignee")
353
+ return (a.assignees[0] ?? "").localeCompare(b.assignees[0] ?? "") || a.id.localeCompare(b.id, undefined, { numeric: true });
354
+ if (key === "title")
355
+ return a.title.localeCompare(b.title, undefined, { sensitivity: "base" }) || a.id.localeCompare(b.id, undefined, { numeric: true });
356
+ return a.id.localeCompare(b.id, undefined, { numeric: true });
357
+ };
358
+ return applyDir(tasks, compare, dir);
359
+ }
116
360
 
117
361
  // packages/cli/src/app-opentui/scenes/tasks.ts
362
+ var TASK_RECOVERY_ITEMS = [
363
+ { id: "refresh", label: "refresh", intent: { scene: "tasks", argv: ["tasks"], action: { kind: "refresh", label: "Refresh tasks" } }, message: "refresh task source" },
364
+ { id: "next", label: "next", intent: { scene: "command", argv: ["task", "run", "--next"], action: { kind: "command-run", label: "Run next task" } }, message: "dispatch next runnable task" },
365
+ { id: "repair", label: "repair", intent: { scene: "command", argv: ["init", "--repair"], action: { kind: "command-run", label: "Repair task source" } }, message: "repair project/task source" },
366
+ { id: "server", label: "server", intent: { scene: "server", argv: ["server", "status"], action: { kind: "refresh", label: "Open server controls" } }, message: "server controls" },
367
+ { id: "doctor", label: "doctor", intent: { scene: "doctor", argv: ["doctor"], action: { kind: "doctor-run", label: "Run doctor" } }, message: "diagnose task source" }
368
+ ];
369
+ var COL = {
370
+ glyph: 1,
371
+ id: 11,
372
+ status: 12,
373
+ priority: 5,
374
+ assignee: 12
375
+ };
376
+ var GUTTER = " ";
377
+ var TASK_COLUMNS = [
378
+ { key: "status", label: "status", hint: "by task state" },
379
+ { key: "id", label: "task-id", hint: "by id" },
380
+ { key: "priority", label: "priority", hint: "by priority" },
381
+ { key: "assignee", label: "assignee", hint: "by assignee" },
382
+ { key: "title", label: "title", hint: "by title" }
383
+ ];
384
+ function taskSelectableItem(task) {
385
+ const taskId = task.id?.trim();
386
+ if (!taskId)
387
+ return null;
388
+ const title = (task.title || "task").trim() || "task";
389
+ const activeRunId = task.activeRun?.runId?.trim();
390
+ return {
391
+ id: taskId,
392
+ label: title,
393
+ data: { taskDetailId: taskId, selectedTaskId: taskId, ...activeRunId ? { selectedRunId: activeRunId } : {} },
394
+ intent: { scene: "tasks", argv: ["tasks", taskId], action: { kind: "refresh", label: `Open ${title}`, payload: { taskDetailId: taskId } } },
395
+ message: `open ${title} detail`
396
+ };
397
+ }
118
398
  function taskView(state) {
119
399
  const tasks = state.data.tasks;
120
400
  return tasks && typeof tasks === "object" && !Array.isArray(tasks) ? tasks : null;
@@ -123,6 +403,32 @@ function taskRecords(state) {
123
403
  const records = taskView(state)?.records;
124
404
  return Array.isArray(records) ? records : [];
125
405
  }
406
+ function dispatchingFor(state) {
407
+ const value = state.data.dispatchingRun;
408
+ if (!value || typeof value !== "object" || Array.isArray(value))
409
+ return null;
410
+ const record = value;
411
+ if (typeof record.taskId !== "string" || !record.taskId.trim())
412
+ return null;
413
+ return { taskId: record.taskId, status: typeof record.status === "string" ? record.status : "dispatching" };
414
+ }
415
+ function updatedAgoLabel(state) {
416
+ const refreshedAt = taskView(state)?.refreshedAt;
417
+ if (typeof refreshedAt !== "string")
418
+ return;
419
+ const parsed = Date.parse(refreshedAt);
420
+ if (!Number.isFinite(parsed))
421
+ return;
422
+ const seconds = Math.max(0, Math.round((Date.now() - parsed) / 1000));
423
+ if (seconds < 1)
424
+ return "updated just now";
425
+ if (seconds < 60)
426
+ return `updated ${seconds}s ago`;
427
+ const minutes = Math.round(seconds / 60);
428
+ if (minutes < 60)
429
+ return `updated ${minutes}m ago`;
430
+ return `updated ${Math.round(minutes / 60)}h ago`;
431
+ }
126
432
  function panelWidth(layout) {
127
433
  return layout?.centerWidth ?? 100;
128
434
  }
@@ -134,9 +440,19 @@ function clip(value, width) {
134
440
  return "";
135
441
  return value.length <= width ? value : `${value.slice(0, Math.max(0, width - 1))}\u2026`;
136
442
  }
443
+ function cell(value, width) {
444
+ return clip(value, width).padEnd(width);
445
+ }
137
446
  function formatAssignee(value) {
138
447
  return value.startsWith("@") ? value : `@${value}`;
139
448
  }
449
+ function assigneeCell(task) {
450
+ const first = task.assignees[0];
451
+ return first ? formatAssignee(first) : "\u2014";
452
+ }
453
+ function priorityCell(task) {
454
+ return task.priority === undefined ? "\u2014" : `P${String(task.priority).slice(0, 3)}`;
455
+ }
140
456
  function taskViewLabel(view) {
141
457
  const filters = view?.filters && typeof view.filters === "object" && !Array.isArray(view.filters) ? view.filters : {};
142
458
  const filter = typeof filters.preset === "string" && filters.preset !== "custom" ? filters.preset : [
@@ -152,31 +468,75 @@ function taskViewLabel(view) {
152
468
  const count = visible !== undefined && total !== undefined ? `${visible}/${total}` : "syncing";
153
469
  return `filter ${filter} \xB7 sort ${sort} \xB7 ${count}`;
154
470
  }
155
- function taskRow(width, task, index, active) {
156
- const id = task.id.slice(0, 10).padEnd(11);
157
- const status = task.status.replace(/[_-]/g, " ").slice(0, 11).padEnd(12);
158
- const priority = task.priority === undefined ? "prio --" : `prio ${String(task.priority).slice(0, 3).padStart(2)}`;
471
+ function sortArrow(spec, key) {
472
+ if (!spec || spec.key !== key)
473
+ return "";
474
+ return spec.dir === "asc" ? "\u25B2" : "\u25BC";
475
+ }
476
+ function headerRow(spec) {
477
+ const text = [
478
+ " ",
479
+ cell(`task-id${sortArrow(spec, "id")}`, COL.id),
480
+ cell(`status${sortArrow(spec, "status")}`, COL.status),
481
+ cell(`prio${sortArrow(spec, "priority")}`, COL.priority),
482
+ cell(`assignee${sortArrow(spec, "assignee")}`, COL.assignee),
483
+ `title${sortArrow(spec, "title")}`
484
+ ].join(GUTTER);
485
+ return line(text, { fg: RIG_UI.ink, bold: true, bg: RIG_UI.glass });
486
+ }
487
+ function sortToggleRow(column, current) {
488
+ const arrow = sortArrow(current, column.key);
489
+ const active = current?.key === column.key;
490
+ const next = cycleTaskSort(current, column.key);
491
+ const marker = active ? "\u25B8" : " ";
492
+ const label = `${column.label}${arrow ? ` ${arrow}` : ""}`.padEnd(12);
493
+ const row = line(`${marker} ${label} ${column.hint}`, {
494
+ fg: active ? RIG_UI.lime : RIG_UI.ink2,
495
+ bold: active
496
+ });
497
+ return withSelectable(row, {
498
+ id: `task-sort-${column.key}`,
499
+ label: `sort by ${column.label}`,
500
+ data: { taskSort: next },
501
+ message: `sort tasks ${next ? `${column.label} ${next.dir}` : "default"}`
502
+ });
503
+ }
504
+ function sortStrip(spec) {
505
+ return [
506
+ line("SORT", { fg: RIG_UI.ink3, bold: true }),
507
+ ...TASK_COLUMNS.map((column) => sortToggleRow(column, spec)),
508
+ line("", { fg: RIG_UI.ink3 })
509
+ ];
510
+ }
511
+ function taskRow(width, task, active, optimistic) {
512
+ const effectiveStatus = optimistic ?? task.status;
513
+ const { glyph } = statusLabel(effectiveStatus);
159
514
  const marker = active ? "\u258C" : " ";
160
- const prefix = `${marker} ${id} ${status} ${priority.padEnd(8)} `;
515
+ const status = effectiveStatus.replace(/[_-]/g, " ");
516
+ const fixed = `${marker} ${glyph}${GUTTER}${cell(task.id, COL.id)}${GUTTER}${cell(status, COL.status)}${GUTTER}${cell(priorityCell(task), COL.priority)}${GUTTER}${cell(assigneeCell(task), COL.assignee)}${GUTTER}`;
161
517
  const activeRun = task.activeRun;
162
518
  const activeSuffix = activeRun ? ` \xB7 attach ${activeRun.runId.slice(0, 8)}${activeRun.status ? ` ${activeRun.status}` : ""}` : "";
163
- const titleWidth = Math.max(12, width - prefix.length - 2);
164
- return line(`${prefix}${clip(`${task.title}${activeSuffix}`, titleWidth)}`, {
165
- fg: active ? RIG_UI.ink : statusColor(task.status),
519
+ const titleWidth = Math.max(8, width - fixed.length - 2);
520
+ const row = line(`${fixed}${clip(`${task.title}${activeSuffix}`, titleWidth)}`, {
521
+ fg: active ? RIG_UI.ink : RIG_UI.ink2,
166
522
  bold: active,
167
- selectableIndex: index,
168
- activateOnClick: true
523
+ ...active ? { bg: RIG_UI.glass } : {}
169
524
  });
525
+ const item = taskSelectableItem(task);
526
+ return item ? withSelectable(row, item) : row;
170
527
  }
528
+ var RECOVERY_DECKS = [
529
+ { label: "refresh", detail: "reload task source" },
530
+ { label: "next", detail: "dispatch next runnable task" },
531
+ { label: "repair", detail: "repair project/task source link" },
532
+ { label: "server", detail: "open server controls" },
533
+ { label: "doctor", detail: "diagnose task source" }
534
+ ];
171
535
  function recoveryRows(selected, reason) {
172
536
  return [
173
537
  ...reason ? [line(reason, { fg: RIG_UI.yellow }), line("", { fg: RIG_UI.ink3 })] : [],
174
538
  line("ACTIONS", { fg: RIG_UI.ink3, bold: true }),
175
- deckRow({ label: "refresh", detail: "reload task source", index: 0, active: selected === 0, activateOnClick: true }),
176
- deckRow({ label: "next", detail: "dispatch next runnable task", index: 1, active: selected === 1, activateOnClick: true }),
177
- deckRow({ label: "repair", detail: "repair project/task source link", index: 2, active: selected === 2, activateOnClick: true }),
178
- deckRow({ label: "server", detail: "open server controls", index: 3, active: selected === 3, activateOnClick: true }),
179
- deckRow({ label: "doctor", detail: "diagnose task source", index: 4, active: selected === 4, activateOnClick: true })
539
+ ...RECOVERY_DECKS.map((deck, index) => selectableDeckRow({ ...deck, index, active: selected === index }, TASK_RECOVERY_ITEMS[index]))
180
540
  ];
181
541
  }
182
542
  function emptyRows(query, total, selected) {
@@ -194,27 +554,119 @@ function degradedRows(state, selected) {
194
554
  return [];
195
555
  return recoveryRows(selected, state.error.message);
196
556
  }
557
+ function rawString(raw, key) {
558
+ const value = raw?.[key];
559
+ return typeof value === "string" ? value : "";
560
+ }
561
+ function taskBody(state, task) {
562
+ const preview = state.data.taskPreviewRaw;
563
+ if (preview && typeof preview === "object" && !Array.isArray(preview)) {
564
+ const fromPreview = rawString(preview, "body");
565
+ if (fromPreview.trim())
566
+ return fromPreview;
567
+ }
568
+ return rawString(task?.raw, "body");
569
+ }
570
+ function renderTaskDetail(state, taskId, layout) {
571
+ const task = taskRecords(state).find((entry) => entry.id === taskId);
572
+ const title = (task?.title || taskId).trim();
573
+ const status = task?.status ?? "unknown";
574
+ const { glyph, color } = statusLabel(status);
575
+ const body = taskBody(state, task);
576
+ const activeRunId = task?.activeRun?.runId?.trim();
577
+ const width = panelWidth(layout);
578
+ const selected = Math.max(0, state.selection.index);
579
+ const headerLines = [
580
+ line(`${glyph} ${title}`, { fg: color, bold: true }),
581
+ line(`task ${taskId} \xB7 ${status}`, { fg: RIG_UI.ink3 }),
582
+ ...task && task.labels.length > 0 ? [line(`labels: ${task.labels.join(", ")}`, { fg: RIG_UI.ink4 })] : [],
583
+ ...task && task.assignees.length > 0 ? [line(`assignees: ${task.assignees.join(", ")}`, { fg: RIG_UI.ink4 })] : [],
584
+ line("", { fg: RIG_UI.ink3 }),
585
+ line("ACTIONS", { fg: RIG_UI.ink3, bold: true })
586
+ ];
587
+ let index = 0;
588
+ const actionRows = [];
589
+ actionRows.push(selectableDeckRow({ label: "back", detail: "return to the task list", index, active: selected === index }, { id: "task-detail-back", label: "back", data: { taskDetailId: undefined }, message: "back to task list" }));
590
+ index += 1;
591
+ actionRows.push(selectableDeckRow({ label: "dispatch", detail: "submit a Pi run for this task", index, active: selected === index }, { id: `task-detail-run:${taskId}`, label: `dispatch ${taskId}`, intent: { scene: "tasks", argv: ["run", taskId], action: { kind: "task-run-id", payload: { task: taskId }, label: `Dispatching ${taskId}` } }, message: `dispatch task ${taskId}` }));
592
+ index += 1;
593
+ if (activeRunId) {
594
+ actionRows.push(selectableDeckRow({ label: "attach", detail: `attach the Pi console to run ${activeRunId.slice(0, 8)}`, index, active: selected === index }, { id: `task-detail-attach:${taskId}`, label: `attach ${activeRunId.slice(0, 8)}`, intent: { scene: "handoff", argv: ["attach", activeRunId], action: { kind: "run-attach", payload: { runId: activeRunId }, label: `Attach Pi ${activeRunId.slice(0, 8)}` } }, message: `attach Pi to ${activeRunId.slice(0, 8)}` }));
595
+ index += 1;
596
+ }
597
+ const bodyHeading = [line("", { fg: RIG_UI.ink3 }), line("BODY", { fg: RIG_UI.ink3, bold: true })];
598
+ const bodyLines = body.trim() ? body.split(`
599
+ `).slice(0, 400).map((text) => line(text, { fg: RIG_UI.ink2 })) : [line("(no description)", { fg: RIG_UI.ink4 })];
600
+ const panelLines = [...headerLines, ...actionRows, ...bodyHeading, ...bodyLines];
601
+ const paddingX = layout?.compact ? 2 : 3;
602
+ const chromeTopInset = 3 + 1;
603
+ const headerCount = headerLines.length + actionRows.length + bodyHeading.length;
604
+ const native = body.trim() ? {
605
+ kind: "markdown",
606
+ content: body,
607
+ top: (layout?.centerTop ?? 1) + chromeTopInset + headerCount,
608
+ left: (layout?.centerLeft ?? 2) + paddingX,
609
+ width: Math.max(20, width - paddingX * 2),
610
+ height: Math.max(6, panelHeight(layout, 0) - chromeTopInset - headerCount - 1)
611
+ } : undefined;
612
+ return makeSceneFrame({
613
+ scene: "tasks",
614
+ title: "Task",
615
+ lines: [],
616
+ ...native ? { native } : {},
617
+ panels: [{
618
+ id: "task-detail",
619
+ top: 0,
620
+ width,
621
+ height: panelHeight(layout, 0),
622
+ lines: panelLines,
623
+ backgroundColor: RIG_UI.panel,
624
+ backgroundAlpha: 184,
625
+ opacity: 1,
626
+ border: false,
627
+ stickyScroll: true,
628
+ stickyStart: "top",
629
+ chrome: "ad-terminal",
630
+ headerText: `task ${taskId}`,
631
+ headerHeight: 3,
632
+ paddingX,
633
+ paddingY: 1
634
+ }],
635
+ footer: { run: activeRunId ? `${activeRunId.slice(0, 8)} active` : undefined, message: "back returns to the list \xB7 dispatch runs the task" },
636
+ live: state.status === "action"
637
+ });
638
+ }
197
639
  function renderTasksScene(state, layout) {
640
+ const detailId = typeof state.data.taskDetailId === "string" ? state.data.taskDetailId.trim() : "";
641
+ if (detailId)
642
+ return renderTaskDetail(state, detailId, layout);
198
643
  const width = panelWidth(layout);
199
644
  const view = taskView(state);
200
645
  const allTasks = taskRecords(state);
201
646
  const query = listSearchQuery(state);
202
- const tasks = filterTasksForSearch(allTasks, query);
647
+ const spec = taskSortSpec(state.data.taskSort);
648
+ const tasks = sortTasks(filterTasksForSearch(allTasks, query), spec);
203
649
  const selected = typeof state.data.selectedTaskId === "string" && tasks.some((task) => task.id === state.data.selectedTaskId) ? state.data.selectedTaskId : tasks[0]?.id;
204
650
  const selectedIndex = Math.max(0, tasks.findIndex((task) => task.id === selected));
205
651
  const recoveryIndex = Math.max(0, Math.min(4, state.selection.index));
206
- const dispatch = state.data.dispatchingRun;
207
- const dispatchLine = dispatch && typeof dispatch === "object" && !Array.isArray(dispatch) ? ` \xB7 ${dispatch.runId ?? "new"} ${dispatch.status ?? "dispatching"}` : "";
652
+ const dispatching = dispatchingFor(state);
653
+ const dispatchLine = dispatching ? ` \xB7 ${state.data.dispatchingRun?.runId ?? "new"} ${dispatching.status}` : "";
654
+ const updatedAgo = updatedAgoLabel(state);
208
655
  const panelTop = 0;
209
656
  const contentWidth = Math.max(12, width - (layout?.compact ? 8 : 10));
657
+ const loading = state.status === "loading" && allTasks.length === 0;
658
+ const refreshError = lastRefreshError(state);
659
+ const banner = refreshError && !state.error ? errorBanner(refreshError) : [];
210
660
  const panelLines = [
211
- line("TASK STATUS PRIORITY TITLE", { fg: RIG_UI.ink3, bold: true }),
212
- line("", { fg: RIG_UI.ink3 }),
661
+ ...banner,
213
662
  ...tasks.length > 0 ? [
214
663
  ...degradedRows(state, recoveryIndex),
215
664
  ...query.trim() ? [line(searchSummary("tasks", query, tasks.length, allTasks.length), { fg: RIG_UI.ink4 }), line("", { fg: RIG_UI.ink3 })] : [],
216
- ...tasks.map((task, index) => taskRow(contentWidth, task, index, index === selectedIndex))
217
- ] : state.error ? degradedRows(state, recoveryIndex) : [
665
+ ...sortStrip(spec),
666
+ headerRow(spec),
667
+ line("", { fg: RIG_UI.ink3 }),
668
+ ...tasks.map((task, index) => taskRow(contentWidth, task, index === selectedIndex, dispatching && dispatching.taskId === task.id ? dispatching.status : undefined))
669
+ ] : state.error ? degradedRows(state, recoveryIndex) : loading ? loadingRows("tasks", state.tick) : [
218
670
  ...query.trim() ? [line(searchSummary("tasks", query, tasks.length, allTasks.length), { fg: RIG_UI.ink4 }), line("", { fg: RIG_UI.ink3 })] : [],
219
671
  ...emptyRows(query, allTasks.length, recoveryIndex)
220
672
  ]
@@ -224,7 +676,7 @@ function renderTasksScene(state, layout) {
224
676
  title: "Tasks",
225
677
  lines: [
226
678
  line("", { fg: RIG_UI.ink4 }),
227
- line(query.trim() ? ` tasks --search ${JSON.stringify(query)}` : ` tasks \xB7 ${taskViewLabel(view)}${dispatchLine}`, { fg: RIG_UI.ink3 })
679
+ line(`${query.trim() ? ` tasks --search ${JSON.stringify(query)}` : ` tasks \xB7 ${taskViewLabel(view)}${dispatchLine}`}${updatedAgo ? ` \xB7 ${updatedAgo}` : ""}`, { fg: RIG_UI.ink3 })
228
680
  ],
229
681
  panels: [{
230
682
  id: "tasks-list",
@@ -242,7 +694,10 @@ function renderTasksScene(state, layout) {
242
694
  paddingX: layout?.compact ? 2 : 3,
243
695
  paddingY: 1
244
696
  }],
245
- footer: { message: query.trim() ? searchSummary("tasks", query, tasks.length, allTasks.length) : taskViewLabel(view) },
697
+ footer: {
698
+ message: query.trim() ? searchSummary("tasks", query, tasks.length, allTasks.length) : taskViewLabel(view),
699
+ ...updatedAgo ? { updated: updatedAgo } : {}
700
+ },
246
701
  typeBarPlaceholder: "search tasks\u2026",
247
702
  live: true
248
703
  });
@@ -0,0 +1,3 @@
1
+ import type { AppSceneFrame, AppState } from "../types";
2
+ import type { StageLayout } from "../layout";
3
+ export declare function renderWorkspaceScene(state: AppState, layout?: StageLayout): AppSceneFrame;