@linxiraos/pi-tui 1.2.0 → 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 (276) hide show
  1. package/CHANGELOG.md +188 -120
  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 +22 -26
  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 +35 -43
  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/agent-hub.ts +2 -2
  157. package/src/overlays/annotation-overlay.ts +422 -168
  158. package/src/overlays/approval-dialog.ts +389 -0
  159. package/src/overlays/ask-dialog.ts +244 -105
  160. package/src/overlays/btw-history-panel.ts +208 -147
  161. package/src/overlays/extensions/extension-list.ts +4 -6
  162. package/src/overlays/hook-editor.ts +90 -32
  163. package/src/overlays/hook-input.ts +105 -19
  164. package/src/overlays/job-tree.ts +177 -0
  165. package/src/overlays/jobs-panel.ts +222 -142
  166. package/src/overlays/login-dialog.ts +201 -91
  167. package/src/overlays/mcp-add-wizard.ts +562 -279
  168. package/src/overlays/model-browser.ts +32 -7
  169. package/src/overlays/model-hub.ts +4 -10
  170. package/src/overlays/model-picker.ts +58 -21
  171. package/src/overlays/pause-screen.ts +81 -23
  172. package/src/overlays/plan-review-overlay.ts +16 -0
  173. package/src/overlays/plugin-selector.ts +17 -1
  174. package/src/overlays/plugin-settings.ts +8 -2
  175. package/src/overlays/queue-mode-selector.ts +10 -0
  176. package/src/overlays/report-panel.ts +6 -2
  177. package/src/overlays/reset-usage-selector.ts +130 -21
  178. package/src/overlays/session-account-selector.ts +36 -0
  179. package/src/overlays/session-info-overlay.ts +199 -76
  180. package/src/overlays/session-selector.ts +15 -10
  181. package/src/overlays/settings-selector.ts +3 -0
  182. package/src/overlays/show-images-selector.ts +1 -0
  183. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  184. package/src/overlays/theme-selector.ts +6 -1
  185. package/src/overlays/thinking-selector.ts +8 -4
  186. package/src/overlays/tree-selector.ts +117 -2
  187. package/src/overlays/usage-dashboard.ts +498 -144
  188. package/src/prompt/composer-cache.ts +4 -6
  189. package/src/prompt/composer.ts +196 -96
  190. package/src/prompt/custom-editor.ts +168 -65
  191. package/src/prompt/image-references.d.ts +22 -0
  192. package/src/prompt/image-references.ts +79 -2
  193. package/src/prompt/internal-url-autocomplete.ts +1 -0
  194. package/src/prompt/macos-spelling.ts +28 -5
  195. package/src/prompt/prose-gate.ts +3 -0
  196. package/src/prompt/queued-messages.ts +9 -2
  197. package/src/prompt/tips.txt +1 -2
  198. package/src/prompt/typing-word.ts +59 -0
  199. package/src/prompt/welcome.ts +256 -423
  200. package/src/render/code-cell.ts +18 -5
  201. package/src/render/hanging-wrap.d.ts +7 -0
  202. package/src/render/hanging-wrap.ts +58 -0
  203. package/src/render/hyperlink.d.ts +2 -0
  204. package/src/render/hyperlink.ts +5 -0
  205. package/src/render/render-utils.d.ts +9 -1
  206. package/src/render/render-utils.ts +16 -2
  207. package/src/render/utils.d.ts +1 -1
  208. package/src/render/utils.ts +1 -1
  209. package/src/render/width-aware-text.ts +6 -5
  210. package/src/setup/scenes/splash.ts +137 -60
  211. package/src/status-line/component.ts +23 -10
  212. package/src/status-line/context-usage.ts +176 -73
  213. package/src/status-line/footer.ts +18 -35
  214. package/src/status-line/host.ts +6 -1
  215. package/src/status-line/segments.ts +16 -5
  216. package/src/status-line/startup.ts +6 -6
  217. package/src/status-line/types.ts +1 -0
  218. package/src/stdin-buffer.d.ts +7 -0
  219. package/src/stdin-buffer.ts +11 -1
  220. package/src/terminal-capabilities.d.ts +16 -1
  221. package/src/terminal-capabilities.ts +66 -28
  222. package/src/terminal-multiplexer.d.ts +7 -0
  223. package/src/terminal-multiplexer.ts +14 -0
  224. package/src/terminal.d.ts +8 -4
  225. package/src/terminal.ts +160 -46
  226. package/src/theme/symbols.d.ts +1 -1
  227. package/src/theme/symbols.ts +1 -0
  228. package/src/theme/theme-class.ts +1 -0
  229. package/src/theme/theme.d.ts +11 -0
  230. package/src/theme/theme.ts +46 -14
  231. package/src/thinking.d.ts +5 -0
  232. package/src/thinking.ts +9 -1
  233. package/src/tmux.d.ts +39 -4
  234. package/src/tmux.ts +110 -16
  235. package/src/tools/ask.ts +20 -9
  236. package/src/tools/ast-edit.d.ts +2 -2
  237. package/src/tools/ast-edit.ts +30 -7
  238. package/src/tools/ast-grep.d.ts +2 -2
  239. package/src/tools/ast-grep.ts +39 -35
  240. package/src/tools/bash.d.ts +6 -1
  241. package/src/tools/bash.ts +15 -9
  242. package/src/tools/debug.ts +2 -1
  243. package/src/tools/default-renderer.ts +3 -2
  244. package/src/tools/edit.d.ts +2 -0
  245. package/src/tools/edit.ts +100 -21
  246. package/src/tools/eval.d.ts +53 -4
  247. package/src/tools/eval.ts +173 -137
  248. package/src/tools/find.d.ts +14 -4
  249. package/src/tools/find.ts +71 -18
  250. package/src/tools/glob.d.ts +3 -3
  251. package/src/tools/glob.ts +51 -17
  252. package/src/tools/grep.d.ts +2 -2
  253. package/src/tools/grep.ts +99 -22
  254. package/src/tools/grouped-file-output.d.ts +9 -4
  255. package/src/tools/grouped-file-output.ts +21 -6
  256. package/src/tools/lsp.ts +30 -18
  257. package/src/tools/mcp.ts +2 -2
  258. package/src/tools/native-view.d.ts +1 -1
  259. package/src/tools/native-view.ts +2 -2
  260. package/src/tools/read.d.ts +32 -10
  261. package/src/tools/read.ts +89 -43
  262. package/src/tools/renderer.d.ts +2 -0
  263. package/src/tools/renderer.ts +2 -0
  264. package/src/tools/task.d.ts +31 -0
  265. package/src/tools/task.ts +40 -7
  266. package/src/tools/todo.ts +3 -2
  267. package/src/tools/wait.ts +16 -30
  268. package/src/tools/web-search.ts +2 -1
  269. package/src/tools/write.d.ts +2 -0
  270. package/src/tools/write.ts +36 -12
  271. package/src/tools/yield.ts +2 -2
  272. package/src/tui.d.ts +20 -3
  273. package/src/tui.ts +140 -36
  274. package/src/utils.ts +5 -3
  275. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  276. package/src/tools/eval-format/code-mode-declarations.ts +0 -61
@@ -1,24 +1,52 @@
1
1
  /**
2
- * Simple text input component for hooks.
2
+ * One-line text prompt an extension (hook) asks the user.
3
3
  */
4
- import { Spacer, type TUI } from "../index";
4
+ import { type Component, Spacer, Text, type TUI } from "../index";
5
5
  import { matchesAppInterrupt } from "../keybinding-matchers";
6
6
  import { CountdownTimer } from "../chrome/countdown-timer";
7
7
  import { formTheme } from "../chrome/form-theme";
8
8
  import { OverlayPanel } from "../chrome/overlay-box";
9
9
  import { Form, TextFormField } from "../components/form";
10
- import { editorKey, interruptKey } from "../chrome/keybinding-hints";
11
- import { node } from "../native/describe";
12
- import type { DescribeContext, NativeNode } from "../native/node";
13
- import { overlayCard } from "../native/overlay";
10
+ import { boundKeys, editorKey, interruptKey } from "../chrome/keybinding-hints";
11
+ import { getKeybindings } from "../keybindings";
12
+ import { col, node, span } from "../native/describe";
13
+ import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
14
+ import { actionBar, actionButton } from "../native/overlay";
14
15
  import { plainText } from "../native/spans";
15
16
 
17
+ /** Role prefix of the sheet's parts (`zeta.hook-input.head`, `.title`, `.timer`, `.detail`, `.field`). */
18
+ const PART = "zeta.hook-input";
19
+
16
20
  export interface HookInputOptions {
17
21
  tui?: TUI;
18
22
  timeout?: number;
19
23
  onTimeout?: () => void;
20
24
  }
21
25
 
26
+ /** Terminal-only key legend under the field; a native terminal puts the keys in the buttons' tooltips. */
27
+ class KeyLegend implements Component {
28
+ readonly #text = new Text("", 0, 0);
29
+
30
+ constructor(private readonly legend: string) {
31
+ this.invalidate();
32
+ }
33
+
34
+ render(width: number): readonly string[] {
35
+ return this.#text.render(width);
36
+ }
37
+
38
+ invalidate(): void {
39
+ this.#text.setText(formTheme.hint(this.legend));
40
+ this.#text.invalidate();
41
+ }
42
+ }
43
+
44
+ interface HookInputNativeMemo {
45
+ countdown: NativeNode | undefined;
46
+ error: boolean;
47
+ node: NativeNode;
48
+ }
49
+
22
50
  export class HookInputComponent extends OverlayPanel {
23
51
  #field: TextFormField;
24
52
  #form: Form;
@@ -26,11 +54,12 @@ export class HookInputComponent extends OverlayPanel {
26
54
  #onCancelCallback: () => void;
27
55
  #baseTitle: string;
28
56
  #countdown: CountdownTimer | undefined;
29
- #nativeMemo: { countdown: NativeNode | undefined; node: NativeNode } | undefined;
57
+ #nativeMemo: HookInputNativeMemo | undefined;
58
+ #nativeActions: NativeNode | undefined;
30
59
 
31
60
  constructor(
32
61
  title: string,
33
- _placeholder: string | undefined,
62
+ placeholder: string | undefined,
34
63
  onSubmit: (value: string) => void,
35
64
  onCancel: () => void,
36
65
  opts?: HookInputOptions,
@@ -55,17 +84,19 @@ export class HookInputComponent extends OverlayPanel {
55
84
 
56
85
  this.#field = new TextFormField({
57
86
  theme: formTheme,
58
- hint: `${editorKey("tui.input.submit")} submit ${interruptKey()} cancel`,
59
87
  empty: "submit",
60
88
  onSubmit: value => this.#onSubmitCallback(value),
61
89
  onCancel: () => this.#onCancelCallback(),
62
90
  });
91
+ // Native terminals draw it while the value is empty; the terminal row stays bare.
92
+ this.#field.input.placeholder = placeholder?.trim() || undefined;
63
93
  this.#form = new Form({
64
94
  fields: [this.#field],
65
95
  onCancel: () => this.#onCancelCallback(),
66
96
  isCancel: matchesAppInterrupt,
67
97
  });
68
98
  this.addChild(this.#form);
99
+ this.addChild(new KeyLegend(`${editorKey("tui.input.submit")} submit ${interruptKey()} cancel`));
69
100
  this.addChild(new Spacer(1));
70
101
  }
71
102
 
@@ -83,24 +114,79 @@ export class HookInputComponent extends OverlayPanel {
83
114
  this.#form.pasteText(text);
84
115
  }
85
116
 
117
+ /** Submit and Cancel run what Enter and the interrupt key run. */
118
+ handleNativeEvent(event: NativeUiEvent): void {
119
+ if (event.type !== "action") return;
120
+ if (event.act === "submit") {
121
+ this.#countdown?.reset();
122
+ this.#field.submit();
123
+ } else if (event.act === "cancel") {
124
+ this.#onCancelCallback();
125
+ }
126
+ }
127
+
86
128
  override dispose(): void {
87
129
  this.#countdown?.dispose();
88
130
  super.dispose();
89
131
  }
90
132
 
91
- /** A card over the form, headed by the title plus the countdown `elapsed` while a timeout runs. */
133
+ /**
134
+ * A sheet in the composer's place, built like the extension editor's: the
135
+ * title's first line (the countdown at its end while a timeout runs) over
136
+ * any further lines as detail, the field written on the sheet (tinted, the
137
+ * reason under it, while a submit was refused), and Cancel · Submit, whose
138
+ * keys live in their tooltips.
139
+ */
92
140
  override describe(_cx: DescribeContext): NativeNode {
93
141
  const countdown = this.#countdown?.describe();
142
+ const error = this.#field.hasError;
94
143
  const memo = this.#nativeMemo;
95
- if (memo && memo.countdown === countdown) return memo.node;
96
- const title = plainText(this.#baseTitle);
97
- const root = countdown
98
- ? overlayCard(this.nativeRole, undefined, [
99
- node("row", { gap: "sm", align: "baseline" }, [node("text", { text: title }), countdown], "head"),
100
- this.#form,
101
- ])
102
- : overlayCard(this.nativeRole, title, [this.#form]);
103
- this.#nativeMemo = { countdown, node: root };
144
+ if (memo && memo.countdown === countdown && memo.error === error) return memo.node;
145
+ const [titleLine = "", ...detailLines] = plainText(this.#baseTitle).trim().split(/\r?\n/);
146
+ const titleRow: NativeChild[] = [
147
+ node(
148
+ "text",
149
+ { spans: [span(titleLine.trim())], role: `${PART}.title`, wrap: "word", grow: 1 },
150
+ undefined,
151
+ "title",
152
+ ),
153
+ ];
154
+ if (countdown) {
155
+ titleRow.push(
156
+ node(
157
+ "row",
158
+ { role: `${PART}.timer`, title: "Cancels when the time runs out; typing restarts it" },
159
+ [countdown],
160
+ "timer",
161
+ ),
162
+ );
163
+ }
164
+ const head: NativeChild[] = [node("row", { gap: "md", align: "baseline" }, titleRow, "line")];
165
+ const detail = detailLines.join("\n").trim();
166
+ if (detail) {
167
+ head.push(node("text", { spans: [span(detail)], role: `${PART}.detail`, wrap: "word" }, undefined, "detail"));
168
+ }
169
+ const root = col(
170
+ [
171
+ node("col", { role: `${PART}.head`, gap: "xs" }, head, "head"),
172
+ node("col", { role: `${PART}.field`, ...(error ? { tone: "error" as const } : {}) }, [this.#form], "field"),
173
+ this.#describeActions(),
174
+ ],
175
+ { role: this.nativeRole },
176
+ );
177
+ this.#nativeMemo = { countdown, error, node: root };
104
178
  return root;
105
179
  }
180
+
181
+ #describeActions(): NativeNode {
182
+ if (this.#nativeActions) return this.#nativeActions;
183
+ const submitKey = getKeybindings().getKeys("tui.input.submit")[0];
184
+ const cancelKey = boundKeys("app.interrupt", ["escape"])[0];
185
+ this.#nativeActions = actionBar([
186
+ null,
187
+ actionButton("Cancel", "cancel", cancelKey ? { keys: cancelKey } : {}),
188
+ actionButton("Submit", "submit", { tone: "accent", ...(submitKey ? { keys: submitKey } : {}) }),
189
+ ]);
190
+ return this.#nativeActions;
191
+ }
106
192
  }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * The jobs of an agent and its subagents as a tree, shared by the jobs sheet
3
+ * and the `/jobs` report. The viewing agent's own jobs sit at the top level;
4
+ * a subagent's jobs nest under the task job that runs it, or under a header
5
+ * naming the subagent when that job is no longer listed (a foreground task, or
6
+ * one evicted from the recent list). Siblings sort branchiest first: the more
7
+ * rows a subtree holds the higher it sits, so leaf jobs settle to the bottom of
8
+ * their level; ties keep running before settled, then oldest first.
9
+ */
10
+ import { buildTreePrefix, getTreeBranch } from "../render/utils";
11
+ import type { Theme } from "../theme/theme";
12
+
13
+ /** What the tree reads off a job. */
14
+ export interface JobTreeJob {
15
+ readonly id: string;
16
+ readonly status: "running" | "completed" | "failed" | "cancelled";
17
+ readonly startTime: number;
18
+ /** Registry id of the agent that owns the job; absent or the root id: a top-level job. */
19
+ readonly ownerId?: string;
20
+ /** Registry id of the subagent a task job runs: the job heads that agent's subtree. */
21
+ readonly agentId?: string;
22
+ }
23
+
24
+ /** A subagent in the tree, by registry id: an owner of listed jobs, or an ancestor linking one to the root. */
25
+ export interface JobTreeAgent {
26
+ readonly id: string;
27
+ readonly parentId?: string;
28
+ }
29
+
30
+ /** Where a row sits: its depth and the rails drawn left of it. */
31
+ export interface JobTreePlace {
32
+ /** 0 for the top level. */
33
+ readonly depth: number;
34
+ /** Per enclosing level below the top, whether that level's rail continues past this row. */
35
+ readonly rails: readonly boolean[];
36
+ /** Whether this row is the last of its siblings. */
37
+ readonly last: boolean;
38
+ /** Whether rows nest under this one. */
39
+ readonly parent: boolean;
40
+ }
41
+
42
+ /** One tree row: a job (a task job also heads its subagent), or a header for a subagent with no listed task job. */
43
+ export type JobTreeRow<J extends JobTreeJob> =
44
+ | (JobTreePlace & { readonly kind: "job"; readonly job: J; readonly agent?: JobTreeAgent })
45
+ | (JobTreePlace & { readonly kind: "agent"; readonly agent: JobTreeAgent });
46
+
47
+ interface TreeNode<J> {
48
+ job?: J;
49
+ agent?: JobTreeAgent;
50
+ readonly children: TreeNode<J>[];
51
+ /** Rows nested under this node. */
52
+ size: number;
53
+ running: boolean;
54
+ startTime: number;
55
+ }
56
+
57
+ /**
58
+ * `jobs` as tree rows in display order. `rootId` is the viewing agent: its
59
+ * jobs, ownerless jobs and jobs of owners outside `agents` sit at the top
60
+ * level, as do agents whose parent is the root or unknown.
61
+ */
62
+ export function buildJobTree<J extends JobTreeJob>(
63
+ jobs: readonly J[],
64
+ agents: readonly JobTreeAgent[],
65
+ rootId: string | undefined,
66
+ ): JobTreeRow<J>[] {
67
+ const top: TreeNode<J>[] = [];
68
+ const agentNodes = new Map<string, TreeNode<J>>();
69
+ for (const agent of agents) {
70
+ if (agent.id === rootId) continue;
71
+ agentNodes.set(agent.id, { agent, children: [], size: 0, running: false, startTime: Infinity });
72
+ }
73
+ // A task job heads its subagent; with several, the running then newest one.
74
+ for (const job of jobs) {
75
+ const node = job.agentId === undefined ? undefined : agentNodes.get(job.agentId);
76
+ if (!node) continue;
77
+ const held = node.job;
78
+ if (!held || (held.status !== "running" && (job.status === "running" || job.startTime > held.startTime))) {
79
+ node.job = job;
80
+ }
81
+ }
82
+ for (const node of agentNodes.values()) {
83
+ (parentNode(agentNodes, node.agent!)?.children ?? top).push(node);
84
+ }
85
+ for (const job of jobs) {
86
+ if (job.agentId !== undefined && agentNodes.get(job.agentId)?.job === job) continue;
87
+ const owner = job.ownerId === undefined || job.ownerId === rootId ? undefined : agentNodes.get(job.ownerId);
88
+ (owner?.children ?? top).push({ job, children: [], size: 0, running: false, startTime: Infinity });
89
+ }
90
+ settle(top);
91
+ return flatten(top);
92
+ }
93
+
94
+ /** `agent`'s parent node, or undefined for the top level: the root, an unknown agent, or a link that would close a cycle. */
95
+ function parentNode<J>(agentNodes: ReadonlyMap<string, TreeNode<J>>, agent: JobTreeAgent): TreeNode<J> | undefined {
96
+ const parent = agent.parentId === undefined ? undefined : agentNodes.get(agent.parentId);
97
+ if (!parent) return undefined;
98
+ const seen = new Set<string>();
99
+ for (let cursor = agent.parentId; cursor !== undefined && !seen.has(cursor);) {
100
+ if (cursor === agent.id) return undefined;
101
+ seen.add(cursor);
102
+ cursor = agentNodes.get(cursor)?.agent?.parentId;
103
+ }
104
+ return parent;
105
+ }
106
+
107
+ /** Size, liveness and start time of each subtree, then siblings branchiest first; drops subagents with nothing listed. */
108
+ function settle<J extends JobTreeJob>(top: TreeNode<J>[]): void {
109
+ // Post-order without recursion: lineage depth is not ours to bound.
110
+ const order: TreeNode<J>[] = [];
111
+ const stack = [...top];
112
+ while (stack.length > 0) {
113
+ const node = stack.pop()!;
114
+ order.push(node);
115
+ stack.push(...node.children);
116
+ }
117
+ const prune = (nodes: TreeNode<J>[]): void => {
118
+ const kept = nodes.filter(node => node.job !== undefined || node.children.length > 0);
119
+ nodes.splice(0, nodes.length, ...kept);
120
+ nodes.sort(compareNodes);
121
+ };
122
+ for (let i = order.length - 1; i >= 0; i--) {
123
+ const node = order[i];
124
+ prune(node.children);
125
+ node.size = 0;
126
+ node.running = node.job?.status === "running";
127
+ node.startTime = node.job?.startTime ?? Infinity;
128
+ for (const child of node.children) {
129
+ node.size += 1 + child.size;
130
+ node.running ||= child.running;
131
+ node.startTime = Math.min(node.startTime, child.startTime);
132
+ }
133
+ }
134
+ prune(top);
135
+ }
136
+
137
+ /** Branchiest first: the larger subtree, then running before settled, then the older. */
138
+ function compareNodes<J>(a: TreeNode<J>, b: TreeNode<J>): number {
139
+ return b.size - a.size || Number(!a.running) - Number(!b.running) || a.startTime - b.startTime;
140
+ }
141
+
142
+ /** Depth-first rows of `top`, iteratively so lineage depth stays unbounded. */
143
+ function flatten<J extends JobTreeJob>(top: readonly TreeNode<J>[]): JobTreeRow<J>[] {
144
+ const out: JobTreeRow<J>[] = [];
145
+ const stack: { node: TreeNode<J>; depth: number; rails: readonly boolean[]; last: boolean }[] = [];
146
+ const push = (level: readonly TreeNode<J>[], depth: number, rails: readonly boolean[]): void => {
147
+ for (let i = level.length - 1; i >= 0; i--) {
148
+ stack.push({ node: level[i], depth, rails, last: i === level.length - 1 });
149
+ }
150
+ };
151
+ push(top, 0, []);
152
+ while (stack.length > 0) {
153
+ const { node, depth, rails, last } = stack.pop()!;
154
+ const place = { depth, rails, last, parent: node.children.length > 0 };
155
+ if (node.job) out.push({ ...place, kind: "job", job: node.job, agent: node.agent });
156
+ else if (node.agent) out.push({ ...place, kind: "agent", agent: node.agent });
157
+ // The top level draws no rail, so its children's rails start empty.
158
+ push(node.children, depth + 1, depth === 0 ? [] : [...rails, !last]);
159
+ }
160
+ return out;
161
+ }
162
+
163
+ /** The connector drawn before a row's head: nothing at the top level, else rails then a branch. */
164
+ export function jobTreeGuide(row: JobTreePlace, theme: Theme): string {
165
+ if (row.depth === 0) return "";
166
+ return `${buildTreePrefix(row.rails, theme)}${getTreeBranch(row.last, theme)} `;
167
+ }
168
+
169
+ /**
170
+ * The prefix of lines under a row's head (its label, a wrapped command): the
171
+ * rails that continue past it, then the rail down to its children or blank,
172
+ * two columns wide so a top-level leaf's lines indent by two.
173
+ */
174
+ export function jobTreeIndent(row: JobTreePlace, theme: Theme): string {
175
+ const rails = row.depth === 0 ? "" : buildTreePrefix([...row.rails, !row.last], theme);
176
+ return `${rails}${row.parent ? `${theme.tree.vertical} ` : " "}`;
177
+ }