@linxiraos/pi-tui 1.2.1 → 1.3.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 (274) hide show
  1. package/CHANGELOG.md +188 -125
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.txt +55 -5
  4. package/dist/types/app-keybindings.d.ts +18 -8
  5. package/dist/types/apps/git/diff-pane.d.ts +5 -0
  6. package/dist/types/apps/git/git-tui.d.ts +16 -2
  7. package/dist/types/apps/git/state.d.ts +11 -0
  8. package/dist/types/autocomplete.d.ts +2 -0
  9. package/dist/types/charts/chart-svg.d.ts +7 -13
  10. package/dist/types/chat/advisor-message.d.ts +4 -1
  11. package/dist/types/chat/assistant-message.d.ts +9 -4
  12. package/dist/types/chat/figure-markdown.d.ts +7 -2
  13. package/dist/types/chat/messages.d.ts +3 -1
  14. package/dist/types/chat/read-tool-group.d.ts +2 -2
  15. package/dist/types/chat/table-chart.d.ts +11 -15
  16. package/dist/types/chat/tool-execution.d.ts +32 -20
  17. package/dist/types/chat/user-message.d.ts +3 -2
  18. package/dist/types/chrome/countdown-timer.d.ts +3 -2
  19. package/dist/types/chrome/diff.d.ts +2 -3
  20. package/dist/types/chrome/transcript-container.d.ts +2 -0
  21. package/dist/types/components/image.d.ts +2 -0
  22. package/dist/types/components/markdown.d.ts +49 -3
  23. package/dist/types/components/text.d.ts +2 -0
  24. package/dist/types/debug-server.d.ts +2 -0
  25. package/dist/types/keybinding-matchers.d.ts +6 -0
  26. package/dist/types/multiplexers/tmux.d.ts +1 -0
  27. package/dist/types/multiplexers/types.d.ts +6 -0
  28. package/dist/types/native/backend.d.ts +5 -4
  29. package/dist/types/native/blobs.d.ts +24 -12
  30. package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
  31. package/dist/types/overlays/annotation-overlay.d.ts +19 -0
  32. package/dist/types/overlays/approval-dialog.d.ts +79 -0
  33. package/dist/types/overlays/ask-dialog.d.ts +11 -6
  34. package/dist/types/overlays/btw-history-panel.d.ts +2 -6
  35. package/dist/types/overlays/hook-editor.d.ts +7 -2
  36. package/dist/types/overlays/hook-input.d.ts +12 -4
  37. package/dist/types/overlays/job-tree.d.ts +50 -0
  38. package/dist/types/overlays/jobs-panel.d.ts +25 -11
  39. package/dist/types/overlays/login-dialog.d.ts +8 -3
  40. package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
  41. package/dist/types/overlays/model-browser.d.ts +6 -1
  42. package/dist/types/overlays/model-picker.d.ts +2 -0
  43. package/dist/types/overlays/pause-screen.d.ts +5 -0
  44. package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
  45. package/dist/types/overlays/report-panel.d.ts +5 -1
  46. package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
  47. package/dist/types/overlays/session-account-selector.d.ts +4 -0
  48. package/dist/types/overlays/session-info-overlay.d.ts +7 -3
  49. package/dist/types/overlays/session-selector.d.ts +3 -1
  50. package/dist/types/overlays/usage-dashboard.d.ts +6 -6
  51. package/dist/types/prompt/composer.d.ts +21 -20
  52. package/dist/types/prompt/custom-editor.d.ts +20 -10
  53. package/dist/types/prompt/image-references.d.ts +22 -0
  54. package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
  55. package/dist/types/prompt/macos-spelling.d.ts +2 -1
  56. package/dist/types/prompt/prose-gate.d.ts +3 -0
  57. package/dist/types/prompt/queued-messages.d.ts +1 -1
  58. package/dist/types/prompt/typing-word.d.ts +25 -0
  59. package/dist/types/prompt/welcome.d.ts +26 -6
  60. package/dist/types/render/hanging-wrap.d.ts +7 -0
  61. package/dist/types/render/hyperlink.d.ts +2 -0
  62. package/dist/types/render/render-utils.d.ts +9 -1
  63. package/dist/types/render/utils.d.ts +1 -1
  64. package/dist/types/setup/scenes/splash.d.ts +6 -5
  65. package/dist/types/status-line/context-usage.d.ts +4 -1
  66. package/dist/types/status-line/host.d.ts +6 -4
  67. package/dist/types/status-line/startup.d.ts +6 -6
  68. package/dist/types/status-line/types.d.ts +3 -0
  69. package/dist/types/stdin-buffer.d.ts +7 -0
  70. package/dist/types/terminal-capabilities.d.ts +16 -1
  71. package/dist/types/terminal-multiplexer.d.ts +7 -0
  72. package/dist/types/terminal.d.ts +8 -4
  73. package/dist/types/theme/symbols.d.ts +1 -1
  74. package/dist/types/theme/theme.d.ts +11 -0
  75. package/dist/types/thinking.d.ts +5 -0
  76. package/dist/types/tmux.d.ts +39 -4
  77. package/dist/types/tools/ast-edit.d.ts +2 -2
  78. package/dist/types/tools/ast-grep.d.ts +2 -2
  79. package/dist/types/tools/bash.d.ts +6 -1
  80. package/dist/types/tools/edit.d.ts +2 -0
  81. package/dist/types/tools/eval.d.ts +53 -4
  82. package/dist/types/tools/find.d.ts +14 -4
  83. package/dist/types/tools/glob.d.ts +3 -3
  84. package/dist/types/tools/grep.d.ts +2 -2
  85. package/dist/types/tools/grouped-file-output.d.ts +9 -4
  86. package/dist/types/tools/native-view.d.ts +1 -1
  87. package/dist/types/tools/read.d.ts +32 -10
  88. package/dist/types/tools/renderer.d.ts +2 -0
  89. package/dist/types/tools/task.d.ts +31 -0
  90. package/dist/types/tools/write.d.ts +2 -0
  91. package/dist/types/tui.d.ts +20 -3
  92. package/package.json +10 -10
  93. package/src/app-keybindings.d.ts +18 -8
  94. package/src/app-keybindings.ts +20 -8
  95. package/src/apps/autoresearch-dashboard.ts +444 -27
  96. package/src/apps/debug/raw-sse.ts +2 -2
  97. package/src/apps/git/diff-pane.ts +39 -41
  98. package/src/apps/git/git-tui.ts +80 -14
  99. package/src/apps/git/sidebar.ts +10 -1
  100. package/src/apps/git/state.ts +12 -0
  101. package/src/autocomplete.d.ts +2 -0
  102. package/src/autocomplete.ts +21 -19
  103. package/src/bracketed-paste.ts +9 -2
  104. package/src/charts/chart-svg.d.ts +7 -13
  105. package/src/charts/chart-svg.ts +19 -22
  106. package/src/chat/advisor-message.d.ts +4 -1
  107. package/src/chat/advisor-message.ts +77 -24
  108. package/src/chat/assistant-message.d.ts +9 -4
  109. package/src/chat/assistant-message.ts +127 -31
  110. package/src/chat/bash-execution.ts +7 -2
  111. package/src/chat/figure-markdown.d.ts +7 -2
  112. package/src/chat/figure-markdown.ts +10 -3
  113. package/src/chat/image-loading.ts +9 -6
  114. package/src/chat/messages.d.ts +3 -1
  115. package/src/chat/messages.ts +4 -1
  116. package/src/chat/read-tool-group.d.ts +2 -2
  117. package/src/chat/read-tool-group.ts +97 -42
  118. package/src/chat/svg-figure.ts +3 -1
  119. package/src/chat/svg-source.ts +6 -3
  120. package/src/chat/table-chart.d.ts +11 -15
  121. package/src/chat/table-chart.ts +24 -57
  122. package/src/chat/tool-execution.d.ts +32 -20
  123. package/src/chat/tool-execution.ts +103 -36
  124. package/src/chat/user-message.d.ts +3 -2
  125. package/src/chat/user-message.ts +54 -34
  126. package/src/chrome/countdown-timer.ts +3 -3
  127. package/src/chrome/diff.d.ts +2 -3
  128. package/src/chrome/diff.ts +4 -9
  129. package/src/chrome/transcript-container.d.ts +2 -0
  130. package/src/chrome/transcript-container.ts +212 -28
  131. package/src/components/editor.ts +12 -12
  132. package/src/components/image.d.ts +2 -0
  133. package/src/components/image.ts +54 -20
  134. package/src/components/markdown.d.ts +49 -3
  135. package/src/components/markdown.ts +461 -11
  136. package/src/components/settings-list.ts +2 -4
  137. package/src/components/text.d.ts +2 -0
  138. package/src/components/text.ts +11 -1
  139. package/src/debug-server.d.ts +2 -0
  140. package/src/debug-server.ts +51 -69
  141. package/src/hotkeys-markdown.ts +2 -0
  142. package/src/keybinding-matchers.d.ts +6 -0
  143. package/src/keybinding-matchers.ts +19 -0
  144. package/src/multiplexers/tmux.d.ts +1 -0
  145. package/src/multiplexers/tmux.ts +3 -0
  146. package/src/multiplexers/types.d.ts +6 -0
  147. package/src/multiplexers/types.ts +6 -0
  148. package/src/native/backend.d.ts +5 -4
  149. package/src/native/backend.ts +13 -5
  150. package/src/native/blobs.d.ts +24 -12
  151. package/src/native/blobs.ts +56 -7
  152. package/src/native/reconcile.ts +43 -21
  153. package/src/overlays/advisor-config.ts +259 -8
  154. package/src/overlays/agent-hub-projection.d.ts +10 -5
  155. package/src/overlays/agent-hub-projection.ts +63 -12
  156. package/src/overlays/annotation-overlay.ts +422 -168
  157. package/src/overlays/approval-dialog.ts +389 -0
  158. package/src/overlays/ask-dialog.ts +244 -105
  159. package/src/overlays/btw-history-panel.ts +208 -147
  160. package/src/overlays/extensions/extension-list.ts +4 -6
  161. package/src/overlays/hook-editor.ts +90 -32
  162. package/src/overlays/hook-input.ts +105 -19
  163. package/src/overlays/job-tree.ts +177 -0
  164. package/src/overlays/jobs-panel.ts +222 -142
  165. package/src/overlays/login-dialog.ts +201 -91
  166. package/src/overlays/mcp-add-wizard.ts +562 -279
  167. package/src/overlays/model-browser.ts +32 -7
  168. package/src/overlays/model-hub.ts +4 -10
  169. package/src/overlays/model-picker.ts +58 -21
  170. package/src/overlays/pause-screen.ts +81 -23
  171. package/src/overlays/plan-review-overlay.ts +16 -0
  172. package/src/overlays/plugin-selector.ts +17 -1
  173. package/src/overlays/queue-mode-selector.ts +10 -0
  174. package/src/overlays/report-panel.ts +6 -2
  175. package/src/overlays/reset-usage-selector.ts +130 -21
  176. package/src/overlays/session-account-selector.ts +36 -0
  177. package/src/overlays/session-info-overlay.ts +199 -76
  178. package/src/overlays/session-selector.ts +15 -10
  179. package/src/overlays/settings-selector.ts +3 -0
  180. package/src/overlays/show-images-selector.ts +1 -0
  181. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  182. package/src/overlays/theme-selector.ts +6 -1
  183. package/src/overlays/thinking-selector.ts +8 -4
  184. package/src/overlays/tree-selector.ts +117 -2
  185. package/src/overlays/usage-dashboard.ts +498 -144
  186. package/src/prompt/composer-cache.ts +1 -1
  187. package/src/prompt/composer.ts +194 -74
  188. package/src/prompt/custom-editor.ts +168 -65
  189. package/src/prompt/image-references.d.ts +22 -0
  190. package/src/prompt/image-references.ts +79 -2
  191. package/src/prompt/internal-url-autocomplete.ts +1 -0
  192. package/src/prompt/macos-spelling.ts +28 -5
  193. package/src/prompt/prose-gate.ts +3 -0
  194. package/src/prompt/queued-messages.ts +9 -2
  195. package/src/prompt/tips.txt +1 -2
  196. package/src/prompt/typing-word.ts +59 -0
  197. package/src/prompt/welcome.ts +97 -45
  198. package/src/render/code-cell.ts +18 -5
  199. package/src/render/hanging-wrap.d.ts +7 -0
  200. package/src/render/hanging-wrap.ts +58 -0
  201. package/src/render/hyperlink.d.ts +2 -0
  202. package/src/render/hyperlink.ts +5 -0
  203. package/src/render/render-utils.d.ts +9 -1
  204. package/src/render/render-utils.ts +16 -2
  205. package/src/render/utils.d.ts +1 -1
  206. package/src/render/utils.ts +1 -1
  207. package/src/render/width-aware-text.ts +6 -5
  208. package/src/setup/scenes/splash.ts +22 -8
  209. package/src/status-line/component.ts +23 -10
  210. package/src/status-line/context-usage.ts +176 -73
  211. package/src/status-line/footer.ts +18 -35
  212. package/src/status-line/host.ts +6 -1
  213. package/src/status-line/segments.ts +16 -5
  214. package/src/status-line/startup.ts +6 -6
  215. package/src/status-line/types.ts +1 -0
  216. package/src/stdin-buffer.d.ts +7 -0
  217. package/src/stdin-buffer.ts +11 -1
  218. package/src/terminal-capabilities.d.ts +16 -1
  219. package/src/terminal-capabilities.ts +66 -28
  220. package/src/terminal-multiplexer.d.ts +7 -0
  221. package/src/terminal-multiplexer.ts +14 -0
  222. package/src/terminal.d.ts +8 -4
  223. package/src/terminal.ts +160 -46
  224. package/src/theme/symbols.d.ts +1 -1
  225. package/src/theme/symbols.ts +1 -0
  226. package/src/theme/theme-class.ts +1 -0
  227. package/src/theme/theme.d.ts +11 -0
  228. package/src/theme/theme.ts +46 -14
  229. package/src/thinking.d.ts +5 -0
  230. package/src/thinking.ts +9 -1
  231. package/src/tmux.d.ts +39 -4
  232. package/src/tmux.ts +110 -16
  233. package/src/tools/ask.ts +20 -9
  234. package/src/tools/ast-edit.d.ts +2 -2
  235. package/src/tools/ast-edit.ts +30 -7
  236. package/src/tools/ast-grep.d.ts +2 -2
  237. package/src/tools/ast-grep.ts +39 -35
  238. package/src/tools/bash.d.ts +6 -1
  239. package/src/tools/bash.ts +15 -9
  240. package/src/tools/debug.ts +2 -1
  241. package/src/tools/default-renderer.ts +3 -2
  242. package/src/tools/edit.d.ts +2 -0
  243. package/src/tools/edit.ts +100 -21
  244. package/src/tools/eval.d.ts +53 -4
  245. package/src/tools/eval.ts +173 -137
  246. package/src/tools/find.d.ts +14 -4
  247. package/src/tools/find.ts +71 -18
  248. package/src/tools/glob.d.ts +3 -3
  249. package/src/tools/glob.ts +51 -17
  250. package/src/tools/grep.d.ts +2 -2
  251. package/src/tools/grep.ts +99 -22
  252. package/src/tools/grouped-file-output.d.ts +9 -4
  253. package/src/tools/grouped-file-output.ts +21 -6
  254. package/src/tools/lsp.ts +30 -18
  255. package/src/tools/mcp.ts +2 -2
  256. package/src/tools/native-view.d.ts +1 -1
  257. package/src/tools/native-view.ts +2 -2
  258. package/src/tools/read.d.ts +32 -10
  259. package/src/tools/read.ts +89 -43
  260. package/src/tools/renderer.d.ts +2 -0
  261. package/src/tools/renderer.ts +2 -0
  262. package/src/tools/task.d.ts +31 -0
  263. package/src/tools/task.ts +40 -7
  264. package/src/tools/todo.ts +3 -2
  265. package/src/tools/wait.ts +16 -30
  266. package/src/tools/web-search.ts +2 -1
  267. package/src/tools/write.d.ts +2 -0
  268. package/src/tools/write.ts +36 -12
  269. package/src/tools/yield.ts +2 -2
  270. package/src/tui.d.ts +20 -3
  271. package/src/tui.ts +140 -36
  272. package/src/utils.ts +5 -3
  273. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  274. package/src/tools/eval-format/code-mode-declarations.ts +0 -61
@@ -1,34 +1,22 @@
1
1
  /**
2
2
  * The async background jobs of a session as {@link JobsSheet}: the dismissable
3
- * sheet the native jobs pill and `/jobs` open. It draws task jobs as `agent`
4
- * nodes and the others as a status dot, the one-line label and a live
5
- * `elapsed`, lists every job and inspects the selected one (command, cwd, live
6
- * pids, exit code, output tail).
3
+ * sheet the native jobs pill and `/jobs` open. A master–detail split lists
4
+ * every job of the session's agent and its subagents as a tree (see
5
+ * {@link buildJobTree}) beside the selected one's status, command, facts
6
+ * (cwd, live pids, exit code, artifact) and live output.
7
7
  */
8
- import type { TspAgentProps, TspSpan } from "@linxiraos/pi-wire";
8
+ import type { TspSpan, TspTone } from "@linxiraos/pi-wire";
9
9
  import { formatDuration } from "@linxiraos/pi-utils";
10
10
  import type { Component } from "../tui";
11
11
  import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../keybinding-matchers";
12
12
  import { matchesKey } from "../keys";
13
13
  import { truncateToWidth } from "../utils";
14
14
  import { collapseCommand } from "../apps/ps-data";
15
- import {
16
- ansi,
17
- card,
18
- code,
19
- col,
20
- compact,
21
- elapsed,
22
- item,
23
- kv,
24
- node,
25
- row,
26
- span,
27
- stableKey,
28
- text,
29
- } from "../native/describe";
15
+ import { ansi, code, col, compact, item, node, row, span, stableKey, text } from "../native/describe";
30
16
  import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
31
17
  import { actionBar, actionButton, selectList } from "../native/overlay";
18
+ import { theme } from "../theme/theme";
19
+ import { buildJobTree, type JobTreeAgent, type JobTreeRow, jobTreeGuide } from "./job-tree";
32
20
 
33
21
  /** One async job as the session snapshots it. */
34
22
  export interface JobsPanelJob {
@@ -38,14 +26,20 @@ export interface JobsPanelJob {
38
26
  readonly label: string;
39
27
  readonly startTime: number;
40
28
  readonly endTime?: number;
29
+ /** Registry id of the agent that owns the job. */
30
+ readonly ownerId?: string;
41
31
  /** Registry id of the subagent a task job runs. */
42
32
  readonly agentId?: string;
43
33
  }
44
34
 
45
- /** Running and recently settled jobs. */
35
+ /** Running and recently settled jobs of an agent and its subagents. */
46
36
  export interface JobsPanelSnapshot {
47
37
  readonly running: readonly JobsPanelJob[];
48
38
  readonly recent: readonly JobsPanelJob[];
39
+ /** The agent the sheet is for: its jobs sit at the top level, and only they can be cancelled. */
40
+ readonly rootId?: string;
41
+ /** Subagents owning listed jobs, with the ancestors linking them to the root. */
42
+ readonly agents?: readonly JobTreeAgent[];
49
43
  }
50
44
 
51
45
  /** What the sheet shows about the selected job beyond its row. */
@@ -68,37 +62,57 @@ export interface JobsSheetSource {
68
62
  load(): JobsPanelSnapshot;
69
63
  /** The selected job's detail; runs once per frame, undefined once the job is gone. */
70
64
  inspect(id: string): JobsPanelDetail | undefined;
71
- /** Cancel a running job. */
72
- cancel(id: string): void;
65
+ /**
66
+ * Cancel a running job. Omitted by a read-only sheet (a focused subagent
67
+ * view, which only watches that agent): it lists the jobs and offers no way
68
+ * to stop them.
69
+ */
70
+ cancel?(id: string): void;
73
71
  /** Dismiss the sheet. */
74
72
  close(): void;
75
73
  }
76
74
 
77
- const AGENT_STATUS: Record<JobsPanelJob["status"], TspAgentProps["status"]> = {
78
- running: "running",
79
- completed: "done",
80
- failed: "failed",
81
- cancelled: "aborted",
75
+ /** A job's status as the sheet names it. */
76
+ const STATUS_LABEL: Record<JobsPanelJob["status"], string> = {
77
+ running: "Running",
78
+ completed: "Done",
79
+ failed: "Failed",
80
+ cancelled: "Cancelled",
82
81
  };
83
82
 
84
- const DOT: Record<JobsPanelJob["status"], TspSpan> = {
85
- running: { t: "●", s: "accent", fx: "pulse" },
86
- completed: { t: "●", s: "success" },
87
- failed: { t: "●", s: "error" },
88
- cancelled: { t: "●", s: "dim" },
83
+ /** A job's status color: the list row's icon and the detail's status pill. */
84
+ const STATUS_TONE: Record<JobsPanelJob["status"], TspTone> = {
85
+ running: "pending",
86
+ completed: "success",
87
+ failed: "error",
88
+ cancelled: "muted",
89
89
  };
90
90
 
91
+ /** The status pill's lead glyph; a running job's pulses. */
92
+ const STATUS_GLYPH: Record<JobsPanelJob["status"], TspSpan> = {
93
+ running: { t: "●", fx: "pulse" },
94
+ completed: { t: "✓" },
95
+ failed: { t: "✕" },
96
+ cancelled: { t: "⊘" },
97
+ };
98
+
99
+ /** The list row's icon per job type (Tern icon names); other types read as a generic job. */
100
+ const TYPE_ICON: Record<string, string> = { bash: "bash", task: "agent" };
101
+
91
102
  /** Output tail rows the terminal fallback shows under the selected job. */
92
103
  const FALLBACK_TAIL_LINES = 8;
93
104
 
94
105
  /**
95
106
  * The jobs pill's sheet: a centred `lg` glass sheet titled Background jobs.
96
- * A selectable list of every job (running first) sits over the selected
97
- * job's detail: status and live elapsed (a task job's `agent` node), cwd,
98
- * live pids, exit code, the full command and a tail-following output pane.
99
- * It re-reads the session every frame, so the caller re-renders it on a
100
- * timer while it is open. ↑/↓ select, X cancels the selected running job,
101
- * Esc or Close dismisses; nothing enters the transcript.
107
+ * A master–detail split (it stacks in a narrow pane): the selectable job tree
108
+ * beside the selected job's detail — a status pill with live elapsed and
109
+ * Cancel job, its command or task, facts (agent, cwd, live pids, exit code,
110
+ * artifact) and a tail-following output pane. With no jobs it shows an empty
111
+ * state. It re-reads the session every frame, so the caller re-renders it on
112
+ * a timer while it is open. ↑/↓ select, X cancels the selected running job,
113
+ * Esc or Close dismisses; nothing enters the transcript. A source without
114
+ * `cancel` renders read-only, and a subagent's job never offers Cancel: only
115
+ * the root's own jobs can be stopped here.
102
116
  */
103
117
  export class JobsSheet implements Component {
104
118
  readonly nativeOverlay = {
@@ -117,17 +131,20 @@ export class JobsSheet implements Component {
117
131
  this.#source = source;
118
132
  }
119
133
 
120
- describe(cx: DescribeContext): NativeNode {
121
- const agent = cx.supports("agent");
122
- const { jobs, selected } = this.#current();
134
+ describe(_cx: DescribeContext): NativeNode {
135
+ const { rows, jobs, selected, cancellable } = this.#current();
123
136
  const detail = selected && this.#source.inspect(selected.id);
124
137
  const now = Date.now();
125
138
  // Elapsed nodes tick on their own; list values and the output pane redraw
126
- // when a job starts, settles or leaves, its process changes, its output
127
- // grows, or (while one runs) once a second.
139
+ // when a job starts, settles, leaves or moves in the tree, its process
140
+ // changes, its output grows, or (while one runs) once a second.
128
141
  const key = [
129
- agent,
130
- jobs.map(job => `${job.id}:${job.status}`).join(","),
142
+ rows
143
+ .map(
144
+ entry =>
145
+ `${jobTreeGuide(entry, theme)}${entry.kind === "job" ? `${entry.job.id}:${entry.job.status}` : entry.agent.id}`,
146
+ )
147
+ .join(","),
131
148
  selected?.id,
132
149
  detail?.pids.join(" "),
133
150
  detail?.exitCode,
@@ -136,38 +153,47 @@ export class JobsSheet implements Component {
136
153
  ].join("|");
137
154
  const output = detail?.output;
138
155
  if (this.#native?.key === key && this.#native.output === output) return this.#native.node;
139
- const actions = [null, actionButton("Close", "close", { keys: "escape" })];
140
- if (selected?.status === "running") {
141
- actions.unshift(actionButton("Cancel job", "cancel", { keys: "x", tone: "error" }));
142
- }
143
- const described = col(
144
- compact([
145
- selectList(
146
- "jobs",
147
- jobs.map(job => describeJobItem(job, now)),
148
- { selected: selected?.id ?? null, empty: "No background jobs", max: { lines: 6 } },
149
- ),
150
- selected && describeDetail(selected, detail, now, agent),
151
- actionBar(actions),
152
- ]),
153
- { gap: "md" },
154
- );
156
+ const close = actionButton("Close", "close", { keys: "escape" });
157
+ const described = selected
158
+ ? col([
159
+ node(
160
+ "row",
161
+ { role: "zeta.jobs.split" },
162
+ [
163
+ node("col", { role: "zeta.jobs.master" }, [describeJobList(rows, selected, now)], "master"),
164
+ describeDetail(selected, detail, now, cancellable),
165
+ ],
166
+ "split",
167
+ ),
168
+ actionBar([describeSummary(jobs), null, close]),
169
+ ])
170
+ : col([describeEmpty(), actionBar([null, close])]);
155
171
  this.#native = { key, output, node: described };
156
172
  return described;
157
173
  }
158
174
 
159
- /** Rows for a terminal without the native surface: one per job, then the selected job's facts and output tail. */
175
+ /** Rows for a terminal without the native surface: the job tree, then the selected job's facts and output tail. */
160
176
  render(width: number): readonly string[] {
161
- const { jobs, selected } = this.#current();
177
+ const { rows, jobs, selected } = this.#current();
162
178
  const now = Date.now();
163
179
  const running = jobs.filter(job => job.status === "running").length;
164
180
  const lines = [truncateToWidth(` Background jobs · ${running} running`, width)];
165
181
  if (jobs.length === 0) lines.push(" No background jobs");
166
- for (const job of jobs) {
182
+ for (const entry of rows) {
183
+ const guide = jobTreeGuide(entry, theme);
184
+ if (entry.kind === "agent") {
185
+ lines.push(truncateToWidth(` ${guide}${entry.agent.id}`, width));
186
+ continue;
187
+ }
188
+ const { job } = entry;
167
189
  const cursor = job === selected ? ">" : " ";
168
190
  const age = formatDuration(jobAge(job, now));
191
+ const agent = entry.agent && entry.agent.id !== job.label ? `${entry.agent.id}: ` : "";
169
192
  lines.push(
170
- truncateToWidth(`${cursor} ${job.status} ${job.type} ${age} ${collapseCommand(job.label)}`, width),
193
+ truncateToWidth(
194
+ `${cursor} ${guide}${job.status} ${job.type} ${age} ${agent}${collapseCommand(job.label)}`,
195
+ width,
196
+ ),
171
197
  );
172
198
  }
173
199
  const detail = selected && this.#source.inspect(selected.id);
@@ -204,13 +230,24 @@ export class JobsSheet implements Component {
204
230
  else if (event.type === "action" && event.act === "cancel") this.#cancelSelected();
205
231
  }
206
232
 
207
- /** Every job, running first, and the selected one (the first when the selection left). */
208
- #current(): { jobs: readonly JobsPanelJob[]; selected: JobsPanelJob | undefined } {
209
- const { running, recent } = this.#source.load();
210
- const jobs = [...running, ...recent];
233
+ /**
234
+ * The job tree, its jobs in tree order, the selected one (the first when
235
+ * the selection left) and whether the sheet can stop it: a job the root
236
+ * owns, with a source that cancels.
237
+ */
238
+ #current(): {
239
+ rows: readonly JobTreeRow<JobsPanelJob>[];
240
+ jobs: readonly JobsPanelJob[];
241
+ selected: JobsPanelJob | undefined;
242
+ cancellable: boolean;
243
+ } {
244
+ const { running, recent, rootId, agents } = this.#source.load();
245
+ const rows = buildJobTree([...running, ...recent], agents ?? [], rootId);
246
+ const jobs = rows.flatMap(entry => (entry.kind === "job" ? [entry.job] : []));
211
247
  const selected = jobs.find(job => job.id === this.#selectedId) ?? jobs[0];
212
248
  this.#selectedId = selected?.id;
213
- return { jobs, selected };
249
+ const owned = selected?.ownerId === undefined || selected.ownerId === rootId;
250
+ return { rows, jobs, selected, cancellable: this.#source.cancel !== undefined && owned };
214
251
  }
215
252
 
216
253
  #tailLines(id: string, output: string): readonly string[] {
@@ -229,8 +266,8 @@ export class JobsSheet implements Component {
229
266
  }
230
267
 
231
268
  #cancelSelected(): void {
232
- const { selected } = this.#current();
233
- if (selected?.status === "running") this.#source.cancel(selected.id);
269
+ const { selected, cancellable } = this.#current();
270
+ if (cancellable && selected?.status === "running") this.#source.cancel?.(selected.id);
234
271
  }
235
272
  }
236
273
 
@@ -256,94 +293,137 @@ function tailLines(text: string, count: number): string[] {
256
293
  return lines.reverse();
257
294
  }
258
295
 
259
- /** A task job as an `agent` node when the terminal draws them, else a one-line dot row. */
260
- function describeJob(job: JobsPanelJob, nowMs: number, agent: boolean): NativeNode {
261
- const settled = job.status !== "running";
262
- const age = jobAge(job, nowMs);
263
- if (agent && job.type === "task") {
296
+ /**
297
+ * The master list: the job tree, connectors leading each label. A subagent
298
+ * without a listed task job heads its jobs as a quiet label (a plain text
299
+ * child: no row behavior, so ↑/↓ and clicks pass it by).
300
+ */
301
+ function describeJobList(rows: readonly JobTreeRow<JobsPanelJob>[], selected: JobsPanelJob, nowMs: number): NativeNode {
302
+ const children: NativeChild[] = rows.map(entry => {
303
+ const guide = jobTreeGuide(entry, theme);
304
+ if (entry.kind === "job") return describeJobItem(entry.job, entry.agent?.id, guide, nowMs);
264
305
  return node(
265
- "agent",
306
+ "text",
266
307
  {
267
- name: job.agentId ?? job.id,
268
- agent: "task",
269
- task: job.label,
270
- status: AGENT_STATUS[job.status],
271
- stats: settled ? { took: age } : { age },
308
+ role: "zeta.jobs.group",
309
+ spans: compact([guide !== "" && span(guide, "mono dim"), span(entry.agent.id)]),
310
+ wrap: "none",
272
311
  },
273
312
  undefined,
274
- job.id,
313
+ `agent-${stableKey(entry.agent.id)}`,
275
314
  );
276
- }
277
- return node(
278
- "row",
279
- { role: "zeta.jobs.row", gap: "sm", align: "center", title: `${job.id} · ${job.type} · ${job.status}` },
280
- [
281
- text([DOT[job.status]], { aria: job.status, shrink: 0 }),
282
- text([span(collapseCommand(job.label))], { wrap: "none", truncate: "end", grow: 1 }),
283
- text([span(job.type, "dim")], { wrap: "none", shrink: 0 }),
284
- elapsed(age, settled),
285
- ],
286
- job.id,
287
- );
315
+ });
316
+ return selectList("jobs", children, { selected: selected.id, max: { lines: 12 } });
288
317
  }
289
318
 
290
- /** One sheet list row: status dot and one-line label, then type and age. */
291
- function describeJobItem(job: JobsPanelJob, nowMs: number): NativeNode {
319
+ /**
320
+ * One list row: the type icon tinted by status, the tree connector, the
321
+ * subagent a task job runs, the one-line label (a process job's command in
322
+ * mono, a task's work in prose) and how long it ran.
323
+ */
324
+ function describeJobItem(job: JobsPanelJob, agentName: string | undefined, guide: string, nowMs: number): NativeNode {
325
+ const status = STATUS_LABEL[job.status];
326
+ const age = formatDuration(jobAge(job, nowMs));
292
327
  return item(job.id, {
293
- label: [DOT[job.status], span(" "), span(collapseCommand(job.label))],
294
- value: [span(job.type, "dim"), span(` · ${formatDuration(jobAge(job, nowMs))}`, "muted")],
295
- title: `${job.id} · ${job.type} · ${job.status}`,
328
+ label: compact([
329
+ guide !== "" && span(guide, "mono dim"),
330
+ agentName !== undefined && agentName !== job.label && span(`${agentName} `, "strong"),
331
+ job.type === "task" ? span(job.label) : span(collapseCommand(job.label), "mono"),
332
+ ]),
333
+ value: [span(age, "mono")],
334
+ icon: TYPE_ICON[job.type] ?? "job",
335
+ tone: STATUS_TONE[job.status],
336
+ title: `${job.label}\n${status} · ${job.type} · ${job.id}`,
337
+ aria: `${job.label}, ${status.toLowerCase()}, ${age}`,
296
338
  });
297
339
  }
298
340
 
299
- /** The selected job: status line (or `agent` node), facts, command and output tail. */
341
+ /** The footer's at-a-glance count. */
342
+ function describeSummary(jobs: readonly JobsPanelJob[]): NativeNode {
343
+ const running = jobs.filter(job => job.status === "running").length;
344
+ const parts = compact([
345
+ running > 0 && `${running} running`,
346
+ jobs.length > running && `${jobs.length - running} recent`,
347
+ ]);
348
+ return node("text", { role: "zeta.jobs.summary", text: parts.join(" · "), wrap: "none" }, undefined, "summary");
349
+ }
350
+
351
+ /** No jobs: what the sheet is for, in place of the split. */
352
+ function describeEmpty(): NativeNode {
353
+ return node(
354
+ "col",
355
+ { role: "zeta.jobs.empty", align: "center" },
356
+ [
357
+ node("icon", { name: "jobs" }),
358
+ text("No background jobs", { role: "zeta.jobs.empty.title" }),
359
+ text("Commands and subagent tasks started in the background show up here while they run.", {
360
+ role: "zeta.jobs.empty.note",
361
+ wrap: "word",
362
+ }),
363
+ ],
364
+ "empty",
365
+ );
366
+ }
367
+
368
+ /**
369
+ * The selected job: a status line (pill, live elapsed, type · id, Cancel job
370
+ * when `cancellable`), its task or full command, facts and the output pane (or
371
+ * its empty state).
372
+ */
300
373
  function describeDetail(
301
374
  job: JobsPanelJob,
302
375
  detail: JobsPanelDetail | undefined,
303
376
  nowMs: number,
304
- agent: boolean,
377
+ cancellable: boolean,
305
378
  ): NativeNode {
306
379
  const settled = job.status !== "running";
307
380
  const exit = detail?.exitCode;
381
+ const age = Math.round(jobAge(job, nowMs));
382
+ const command = detail?.command ?? (job.type === "task" ? undefined : job.label);
383
+ const pids = detail?.pids ?? [];
384
+ const facts = compact([
385
+ job.agentId ? { k: "Agent", v: [span(job.agentId, "mono")] } : undefined,
386
+ detail?.cwd ? { k: "Directory", v: [span(detail.cwd, "path")] } : undefined,
387
+ pids.length > 0 && { k: pids.length > 1 ? "PIDs" : "PID", v: [span(pids.join(", "), "num")] },
388
+ exit !== undefined && { k: "Exit code", v: [span(String(exit), exit === 0 ? "success" : "error")] },
389
+ detail?.artifactId ? { k: "Full output", v: [span(`artifact://${detail.artifactId}`, "mono")] } : undefined,
390
+ ]);
308
391
  const children = compact<NativeChild>([
309
- agent && job.type === "task"
310
- ? describeJob(job, nowMs, agent)
311
- : row(
312
- [
313
- text(
314
- [DOT[job.status], span(` ${job.status}`, "strong"), span(` · ${job.type} · ${job.id}`, "muted")],
315
- {
316
- wrap: "none",
317
- truncate: "end",
318
- },
319
- ),
320
- elapsed(jobAge(job, nowMs), settled),
321
- ],
322
- { gap: "sm", align: "center" },
323
- ),
324
- detail &&
325
- kv([
326
- ["Directory", detail.cwd && [span(detail.cwd, "path")]],
327
- ["PID", detail.pids.length > 0 ? [span(detail.pids.join(", "), "num")] : undefined],
328
- ["Exit", exit === undefined ? undefined : [span(String(exit), exit === 0 ? "success" : "error")]],
329
- ["Full output", detail.artifactId && [span(`artifact://${detail.artifactId}`, "mono")]],
392
+ row(
393
+ compact<NativeChild>([
394
+ text([STATUS_GLYPH[job.status], span(` ${STATUS_LABEL[job.status]}`)], {
395
+ role: "zeta.jobs.state",
396
+ tone: STATUS_TONE[job.status],
397
+ wrap: "none",
398
+ }),
399
+ node("elapsed", {
400
+ age,
401
+ ...(settled ? { stopped: age } : {}),
402
+ format: "short",
403
+ role: "zeta.jobs.age",
404
+ title: settled ? "Ran for" : "Running for",
405
+ }),
406
+ text([span(`${job.type} · ${job.id}`, "mono")], { role: "zeta.jobs.id", wrap: "none", truncate: "end" }),
407
+ node("spacer", { grow: 1 }),
408
+ cancellable && !settled && actionButton("Cancel job", "cancel", { keys: "x", tone: "error" }),
330
409
  ]),
331
- // The row shows the command on one line; show it whole when that cut or collapsed it.
332
- detail?.command && detail.command !== collapseCommand(job.label)
333
- ? card(
334
- {
335
- head: [span("Command", "muted")],
336
- variant: "bare",
337
- collapsible: true,
338
- collapsed: true,
339
- preview: { lines: 3 },
340
- },
341
- [code(detail.command, { lang: "bash", wrap: true })],
342
- )
343
- : undefined,
344
- detail?.output
345
- ? ansi(detail.output, { follow: !settled, role: "zeta.jobs.output", max: { h: "16lines" } })
346
- : text([span(settled ? "No output" : "No output yet", "dim")]),
410
+ { role: "zeta.jobs.status", gap: "sm", align: "center" },
411
+ ),
412
+ command
413
+ ? code(command, { lang: "bash", wrap: true, preview: { lines: 4 }, role: "zeta.jobs.command" })
414
+ : text(job.label, { role: "zeta.jobs.title", wrap: "word" }),
415
+ facts.length > 0 && node("kv", { items: facts, role: "zeta.jobs.facts" }),
416
+ node(
417
+ "col",
418
+ { role: "zeta.jobs.out", gap: "sm" },
419
+ [
420
+ text("Output", { role: "zeta.jobs.label" }),
421
+ detail?.output
422
+ ? ansi(detail.output, { follow: !settled, role: "zeta.jobs.output", max: { h: "16lines" } })
423
+ : text(settled ? "No output" : "No output yet", { role: "zeta.jobs.nooutput" }),
424
+ ],
425
+ "out",
426
+ ),
347
427
  ]);
348
- return node("col", { gap: "sm", role: "zeta.jobs.detail" }, children, stableKey(job.id));
428
+ return node("col", { role: "zeta.jobs.detail" }, children, stableKey(job.id));
349
429
  }