@linxiraos/pi-tui 1.3.2 → 1.4.1

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 (116) hide show
  1. package/CHANGELOG.md +181 -181
  2. package/dist/types/apps/session-picker.d.ts +13 -1
  3. package/dist/types/autocomplete.d.ts +6 -0
  4. package/dist/types/components/sparkline.d.ts +34 -0
  5. package/dist/types/index.d.ts +1 -0
  6. package/dist/types/native/describe.d.ts +7 -1
  7. package/dist/types/native/overlay.d.ts +2 -0
  8. package/dist/types/native/tone.d.ts +2 -0
  9. package/dist/types/overlays/agent-hub.d.ts +3 -3
  10. package/dist/types/overlays/approval-dialog.d.ts +15 -1
  11. package/dist/types/overlays/jobs-panel.d.ts +3 -0
  12. package/dist/types/overlays/model-browser.d.ts +1 -1
  13. package/dist/types/overlays/resume-picker.d.ts +293 -0
  14. package/dist/types/overlays/resume-tree.d.ts +353 -0
  15. package/dist/types/overlays/session-selector.d.ts +22 -4
  16. package/dist/types/render/output-block.d.ts +2 -0
  17. package/dist/types/render/tool-card.d.ts +2 -0
  18. package/dist/types/sixel.d.ts +28 -0
  19. package/dist/types/status-line/host.d.ts +3 -0
  20. package/dist/types/terminal-capabilities.d.ts +6 -1
  21. package/dist/types/tools/bash.d.ts +2 -0
  22. package/dist/types/tools/default-renderer.d.ts +3 -0
  23. package/dist/types/tools/eval.d.ts +9 -8
  24. package/dist/types/tools/hub.d.ts +3 -1
  25. package/dist/types/tools/irc.d.ts +11 -1
  26. package/dist/types/tools/job-widget.d.ts +226 -0
  27. package/dist/types/tools/json-tree.d.ts +2 -0
  28. package/dist/types/tools/line-ranges.d.ts +8 -1
  29. package/dist/types/tools/mcp.d.ts +23 -0
  30. package/dist/types/tools/output-meta.d.ts +1 -1
  31. package/dist/types/tools/read.d.ts +8 -2
  32. package/dist/types/tools/renderer.d.ts +8 -2
  33. package/dist/types/tools/result-card.d.ts +1 -1
  34. package/dist/types/tools/streaming-output.d.ts +1 -0
  35. package/dist/types/tools/task.d.ts +12 -0
  36. package/dist/types/tools/wait.d.ts +11 -0
  37. package/package.json +9 -9
  38. package/src/apps/session-picker.ts +20 -2
  39. package/src/autocomplete.d.ts +6 -0
  40. package/src/autocomplete.ts +21 -6
  41. package/src/chat/skill-message.ts +16 -17
  42. package/src/chat/tool-execution.ts +35 -0
  43. package/src/chrome/transcript-container.ts +85 -13
  44. package/src/components/editor.ts +17 -7
  45. package/src/components/image.ts +5 -4
  46. package/src/components/markdown.ts +11 -3
  47. package/src/components/sparkline.d.ts +34 -0
  48. package/src/components/sparkline.ts +89 -0
  49. package/src/index.d.ts +1 -0
  50. package/src/index.ts +1 -0
  51. package/src/native/backend.ts +32 -6
  52. package/src/native/describe.d.ts +7 -1
  53. package/src/native/describe.ts +18 -1
  54. package/src/native/overlay.d.ts +2 -0
  55. package/src/native/overlay.ts +3 -0
  56. package/src/native/tone.d.ts +2 -0
  57. package/src/native/tone.ts +22 -0
  58. package/src/overlays/agent-hub.ts +14 -10
  59. package/src/overlays/agent-transcript-viewer.ts +2 -2
  60. package/src/overlays/agents-hub.ts +2 -1
  61. package/src/overlays/approval-dialog.ts +59 -13
  62. package/src/overlays/hub-frame.ts +2 -1
  63. package/src/overlays/jobs-panel.ts +24 -2
  64. package/src/overlays/model-browser.ts +3 -0
  65. package/src/overlays/model-hub.ts +2 -1
  66. package/src/overlays/resume-picker.ts +706 -0
  67. package/src/overlays/resume-tree.ts +1901 -0
  68. package/src/overlays/session-selector.ts +579 -39
  69. package/src/prompt/attachment-chips.ts +1 -1
  70. package/src/render/output-block.d.ts +2 -0
  71. package/src/render/output-block.ts +25 -12
  72. package/src/render/tool-card.d.ts +2 -0
  73. package/src/render/tool-card.ts +6 -0
  74. package/src/sixel.d.ts +28 -0
  75. package/src/sixel.ts +185 -0
  76. package/src/status-line/component.ts +19 -2
  77. package/src/status-line/host.ts +3 -0
  78. package/src/terminal-capabilities.d.ts +6 -1
  79. package/src/terminal-capabilities.ts +54 -3
  80. package/src/theme/symbols.ts +5 -3
  81. package/src/tools/bash.d.ts +2 -0
  82. package/src/tools/bash.ts +25 -9
  83. package/src/tools/default-renderer.d.ts +3 -0
  84. package/src/tools/default-renderer.ts +54 -17
  85. package/src/tools/eval.d.ts +9 -8
  86. package/src/tools/eval.ts +32 -14
  87. package/src/tools/find.ts +4 -5
  88. package/src/tools/glob.ts +4 -6
  89. package/src/tools/grep.ts +2 -4
  90. package/src/tools/hub.ts +4 -1
  91. package/src/tools/irc.d.ts +11 -1
  92. package/src/tools/irc.ts +11 -1
  93. package/src/tools/job-widget.d.ts +226 -0
  94. package/src/tools/job-widget.ts +1036 -0
  95. package/src/tools/json-tree.d.ts +2 -0
  96. package/src/tools/json-tree.ts +8 -0
  97. package/src/tools/line-ranges.d.ts +8 -1
  98. package/src/tools/line-ranges.ts +23 -13
  99. package/src/tools/mcp.d.ts +23 -0
  100. package/src/tools/mcp.ts +44 -1
  101. package/src/tools/output-meta.d.ts +1 -1
  102. package/src/tools/output-meta.ts +1 -1
  103. package/src/tools/proc-render.ts +1 -0
  104. package/src/tools/read.d.ts +8 -2
  105. package/src/tools/read.ts +64 -21
  106. package/src/tools/renderer.d.ts +8 -2
  107. package/src/tools/renderer.ts +8 -2
  108. package/src/tools/result-card.d.ts +1 -1
  109. package/src/tools/result-card.ts +1 -2
  110. package/src/tools/streaming-output.d.ts +1 -0
  111. package/src/tools/streaming-output.ts +11 -9
  112. package/src/tools/task.d.ts +12 -0
  113. package/src/tools/task.ts +25 -1
  114. package/src/tools/wait.d.ts +11 -0
  115. package/src/tools/wait.ts +296 -147
  116. package/src/tui.ts +45 -5
@@ -17,6 +17,8 @@ export interface BackgroundChrome {
17
17
  }
18
18
  /** Map an omp background token to card tone/role. */
19
19
  export declare function backgroundChrome(bg: ThemeBg | undefined): BackgroundChrome;
20
+ /** The omp theme colour an ANSI renderer draws a TSP tone with (the inverse of {@link colorTone}). */
21
+ export declare function toneColor(tone: TspTone): ThemeColor;
20
22
  /**
21
23
  * Tone of an omp colour used as a severity or border colour; undefined for
22
24
  * colours that carry no tone (callers pick their own fallback).
@@ -50,6 +50,28 @@ export function backgroundChrome(bg: ThemeBg | undefined): BackgroundChrome {
50
50
  }
51
51
  }
52
52
 
53
+ /** The omp theme colour an ANSI renderer draws a TSP tone with (the inverse of {@link colorTone}). */
54
+ export function toneColor(tone: TspTone): ThemeColor {
55
+ switch (tone) {
56
+ case "neutral":
57
+ return "text";
58
+ case "accent":
59
+ case "info":
60
+ return "accent";
61
+ case "success":
62
+ return "success";
63
+ case "warning":
64
+ case "pending":
65
+ return "warning";
66
+ case "error":
67
+ return "error";
68
+ case "muted":
69
+ return "muted";
70
+ case "user":
71
+ return "customMessageLabel";
72
+ }
73
+ }
74
+
53
75
  /**
54
76
  * Tone of an omp colour used as a severity or border colour; undefined for
55
77
  * colours that carry no tone (callers pick their own fallback).
@@ -5,11 +5,11 @@
5
5
  * - Table view: every registered agent except Main (Main IS the ambient
6
6
  * chat), live from the global AgentHubRegistry — status, unread irc count,
7
7
  * current/last task, last activity. Navigate with keys, wheel, hover, and
8
- * click; `r` revives a parked agent, `x` aborts + releases one.
8
+ * click; `r` revives a parked or killed agent, `x` aborts + releases one.
9
9
  * - Chat view: per-agent transcript (incremental session-file tail, absorbed
10
10
  * from the old session observer overlay) plus an input line. Submitting
11
- * revives a parked agent, then prompts/steers it; the message lands in the
12
- * agent's persisted history via the normal prompt path.
11
+ * revives a parked or killed agent, then prompts/steers it; the message
12
+ * lands in the agent's persisted history via the normal prompt path.
13
13
  *
14
14
  * Replaces the old SessionObserverOverlayComponent (ctrl+s observer).
15
15
  */
@@ -894,7 +894,11 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
894
894
  id: "revive",
895
895
  label: "Revive",
896
896
  keys: ["r"],
897
- disabled: readOnly ?? (selected?.status === "parked" ? undefined : "Only parked agents can be revived"),
897
+ disabled:
898
+ readOnly ??
899
+ (selected?.status === "parked" || selected?.status === "aborted"
900
+ ? undefined
901
+ : "Only parked or killed agents can be revived"),
898
902
  },
899
903
  { id: "kill", label: "Kill", keys: ["x"], danger: true, disabled: readOnly ?? (selected ? undefined : true) },
900
904
  { id: "view", label: "By parent", keys: ["t"], on: this.#viewMode === "tree" },
@@ -2498,15 +2502,15 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
2498
2502
  #activateAgent(ref: TRecord): void {
2499
2503
  this.#notice = undefined;
2500
2504
  const focusAgent = this.#focusAgent;
2501
- // Aborted agents and advisor refs are read-only transcripts with no
2502
- // revivable session; open the in-hub viewer instead of failing ensureLive.
2503
- if (ref.kind === "advisor" || ref.status === "aborted" || this.#remote || !focusAgent) {
2505
+ // Advisor refs are read-only transcripts with no revivable session; open
2506
+ // the in-hub viewer instead of failing ensureLive.
2507
+ if (ref.kind === "advisor" || this.#remote || !focusAgent) {
2504
2508
  this.openChat(ref.id);
2505
2509
  return;
2506
2510
  }
2507
2511
  void (async () => {
2508
2512
  try {
2509
- await focusAgent(ref.id); // ensureLive inside revives parked agents; no parking, no session files
2513
+ await focusAgent(ref.id); // ensureLive inside revives parked and killed agents
2510
2514
  this.#onDone();
2511
2515
  } catch (error) {
2512
2516
  this.#notice = error instanceof Error ? error.message : String(error);
@@ -2523,8 +2527,8 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
2523
2527
  this.#requestRender();
2524
2528
  return;
2525
2529
  }
2526
- if (ref.status !== "parked") {
2527
- this.#notice = `Agent "${ref.id}" is ${ref.status} — only parked agents can be revived.`;
2530
+ if (ref.status !== "parked" && ref.status !== "aborted") {
2531
+ this.#notice = `Agent "${ref.id}" is ${ref.status} — only parked or killed agents can be revived.`;
2528
2532
  this.#requestRender();
2529
2533
  return;
2530
2534
  }
@@ -235,10 +235,10 @@ export class AgentTranscriptViewer implements Component {
235
235
  this.#pollTimer.unref?.();
236
236
  }
237
237
 
238
- /** Advisor and aborted-agent transcripts are read-only. */
238
+ /** Advisor transcripts are read-only; every other agent revives on send. */
239
239
  get #sendable(): boolean {
240
240
  const ref = this.#deps.registry.get(this.#deps.agentId);
241
- if (!ref || ref.kind === "advisor" || ref.status === "aborted") return false;
241
+ if (!ref || ref.kind === "advisor") return false;
242
242
  return Boolean(this.#deps.remote || this.#deps.lifecycle);
243
243
  }
244
244
 
@@ -1583,7 +1583,8 @@ export class AgentsHubComponent implements Component {
1583
1583
  } else {
1584
1584
  children.push(...this.#describeList());
1585
1585
  }
1586
- return node("col", { gap: "sm", grow: 1 }, children, "body");
1586
+ // Zero basis: the body takes the space left of the sidebar instead of sizing to its widest line.
1587
+ return node("col", { gap: "sm", grow: 1, basis: 0, min: { w: 0 } }, children, "body");
1587
1588
  }
1588
1589
 
1589
1590
  #statusSpans(): TspSpan[] {
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Tool-approval prompt in the composer's place, presented like the ask
3
3
  * dialog: the approval title and its details over a list of choices —
4
- * Approve, an optional "approve for this session", and Deny — and an action
4
+ * optional grants (led by a toggle that keeps them across sessions), Approve,
5
+ * an optional "approve for this session", and Deny — and an action
5
6
  * bar. The arrows move between the choices and Enter takes the highlighted
6
7
  * one; `app.approval.approve` (Ctrl+Enter by default) approves this call
7
8
  * outright and Esc denies. Enter and the approve chord only arm
@@ -45,7 +46,8 @@ export const APPROVE_ARM_MS = 500;
45
46
  /** The Deny row while a composer draft holds Enter: sending it denies the call and steers the turn. */
46
47
  const DENY_AND_SEND_LABEL = "Deny and send message";
47
48
 
48
- type ApprovalAnswer = "once" | "session" | "deny";
49
+ /** `grant<N>` takes the Nth of {@link ApprovalDialogOptions.grants}; `remember` toggles their lifetime. */
50
+ type ApprovalAnswer = "once" | "session" | "deny" | "remember" | `grant${number}`;
49
51
 
50
52
  interface ApprovalChoice {
51
53
  readonly label: string;
@@ -57,6 +59,14 @@ interface ApprovalDialogCallbacks {
57
59
  onDeny(): void;
58
60
  }
59
61
 
62
+ /** A choice that approves the call and remembers more than it: a folder, or the exact target. */
63
+ export interface ApprovalDialogGrant {
64
+ /** Label while the grant is remembered for this session. */
65
+ readonly label: string;
66
+ /** Label once the remember toggle keeps it across sessions. */
67
+ readonly alwaysLabel: string;
68
+ }
69
+
60
70
  /** The composer mounted beneath the dialog: it receives every key the dialog does not own. */
61
71
  export interface ApprovalDialogDraft {
62
72
  /** True while the composer holds a draft — text, attachments, or a paste still landing: keys then edit and send it. */
@@ -80,6 +90,12 @@ interface ApprovalDialogOptions {
80
90
  draft?: ApprovalDialogDraft;
81
91
  /** Offer to approve this and later like calls for the rest of the session, as the row between Approve and Deny. */
82
92
  session?: { label: string; onApprove(): void };
93
+ /**
94
+ * Grants to offer above Approve, led by a toggle that keeps them across sessions; Approve then
95
+ * reads "Approve Once". `onGrant` gets the chosen grant and whether the toggle was on.
96
+ */
97
+ grants?: readonly ApprovalDialogGrant[];
98
+ onGrant?(grant: ApprovalDialogGrant, always: boolean): void;
83
99
  }
84
100
 
85
101
  /** A prompt's detail rows: every line after the title, trailing blanks dropped. */
@@ -100,9 +116,11 @@ export class ApprovalDialogComponent implements Component {
100
116
  readonly #footer = new PanelRows();
101
117
  #scrollOffset = 0;
102
118
  #bodyRows = MAX_DETAIL_ROWS;
103
- /** In list order; the first (Approve) is highlighted on mount and Deny is last. */
104
- readonly #choices: readonly ApprovalChoice[];
119
+ /** In list order: the remember toggle and grants (when offered), Approve (highlighted on mount), the session row, Deny last. */
120
+ #choices: readonly ApprovalChoice[];
105
121
  #choice = 0;
122
+ /** The remember toggle: grants are kept across sessions while it is on. */
123
+ #rememberAlways = false;
106
124
  #closed = false;
107
125
  #native: NativeNode | undefined;
108
126
  /** Whether a draft held the keys when {@link #native} was described: it moves the highlight. */
@@ -122,11 +140,8 @@ export class ApprovalDialogComponent implements Component {
122
140
  this.#summary = promptDetails(lines);
123
141
  this.#full =
124
142
  options.fullPrompt === undefined ? undefined : promptDetails(sanitizeDisplayLines(options.fullPrompt));
125
- this.#choices = [
126
- { label: "Approve", answer: "once" },
127
- ...(options.session ? [{ label: options.session.label, answer: "session" as const }] : []),
128
- { label: "Deny", answer: "deny" },
129
- ];
143
+ this.#choices = this.#buildChoices();
144
+ this.#choice = this.#choices.findIndex(choice => choice.answer === "once");
130
145
  this.#panel = new OverlayPanel(title, "zeta.overlay.approval");
131
146
  this.#footer.setHeight(1);
132
147
  this.#panel.addChild(this.#body);
@@ -149,6 +164,33 @@ export class ApprovalDialogComponent implements Component {
149
164
  }
150
165
  }
151
166
 
167
+ #buildChoices(): ApprovalChoice[] {
168
+ const grants = this.options.grants ?? [];
169
+ const glyph = this.#rememberAlways ? theme.checkbox.checked : theme.checkbox.unchecked;
170
+ return [
171
+ ...(grants.length > 0 ? [{ label: `${glyph} Remember across sessions`, answer: "remember" as const }] : []),
172
+ ...grants.map((grant, index) => ({
173
+ label: this.#rememberAlways ? grant.alwaysLabel : grant.label,
174
+ answer: `grant${index}` as const,
175
+ })),
176
+ { label: grants.length > 0 ? "Approve Once" : "Approve", answer: "once" },
177
+ ...(this.options.session ? [{ label: this.options.session.label, answer: "session" as const }] : []),
178
+ { label: "Deny", answer: "deny" },
179
+ ];
180
+ }
181
+
182
+ /** Run a choice: the remember toggle flips the grants' lifetime in place, every other choice answers. */
183
+ #take(answer: ApprovalAnswer): void {
184
+ if (answer !== "remember") {
185
+ this.#finish(answer);
186
+ return;
187
+ }
188
+ this.#rememberAlways = !this.#rememberAlways;
189
+ this.#choices = this.#buildChoices();
190
+ this.#native = undefined;
191
+ this.options.tui?.requestRender();
192
+ }
193
+
152
194
  invalidate(): void {
153
195
  this.#native = undefined;
154
196
  this.#panel.invalidate();
@@ -191,7 +233,7 @@ export class ApprovalDialogComponent implements Component {
191
233
  else if (matchesSelectPageUp(keyData)) this.#scroll(-page);
192
234
  else if (matchesSelectPageDown(keyData)) this.#scroll(page);
193
235
  else if (getKeybindings().matches(keyData, "tui.select.confirm")) {
194
- if (performance.now() >= this.#armedAt) this.#finish(this.#choices[this.#choice]!.answer);
236
+ if (performance.now() >= this.#armedAt) this.#take(this.#choices[this.#choice]!.answer);
195
237
  }
196
238
  // Enter never reaches an empty composer: it would submit nothing, or interrupt the run.
197
239
  else if (draft && !getKeybindings().matches(keyData, "tui.input.submit")) this.#editDraft(draft, keyData);
@@ -295,7 +337,7 @@ export class ApprovalDialogComponent implements Component {
295
337
  tone: "error",
296
338
  title: `Deny this call${cancelKey ? ` ${formatTooltipKey(cancelKey)}` : ""}`,
297
339
  });
298
- const approve = actionButton("Approve", "approve", {
340
+ const approve = actionButton(this.options.grants?.length ? "Approve Once" : "Approve", "approve", {
299
341
  ...(approveKey ? { keys: approveKey } : {}),
300
342
  tone: "accent",
301
343
  title: `Approve this call${approveKey ? ` ${formatTooltipKey(approveKey)}` : ""}`,
@@ -350,7 +392,7 @@ export class ApprovalDialogComponent implements Component {
350
392
  }
351
393
  if ((event.type !== "select" && event.type !== "activate") || leafKey(event.key) !== "choices") return;
352
394
  const choice = this.#choices.find(candidate => candidate.answer === event.item);
353
- if (choice) this.#finish(choice.answer);
395
+ if (choice) this.#take(choice.answer);
354
396
  }
355
397
 
356
398
  /** Detail rows at `width`: one clipped row per detail, or wrapped rows once expanded. */
@@ -384,6 +426,10 @@ export class ApprovalDialogComponent implements Component {
384
426
  this.#countdown?.dispose();
385
427
  if (answer === "once") this.callbacks.onApprove();
386
428
  else if (answer === "session") this.options.session?.onApprove();
387
- else this.callbacks.onDeny();
429
+ else if (answer.startsWith("grant")) {
430
+ const grant = this.options.grants?.[Number(answer.slice("grant".length))];
431
+ if (grant) this.options.onGrant?.(grant, this.#rememberAlways);
432
+ else this.callbacks.onDeny();
433
+ } else this.callbacks.onDeny();
388
434
  }
389
435
  }
@@ -96,7 +96,8 @@ export function describeHubSidebar<TEntry extends SidebarEntry<string>>(
96
96
  ),
97
97
  );
98
98
  }
99
- return node("list", { selected: selectedId }, items, key);
99
+ // The sidebar keeps its width: a body line wider than the pane must not squeeze it.
100
+ return node("list", { selected: selectedId, shrink: 0 }, items, key);
100
101
  }
101
102
 
102
103
  /**
@@ -6,17 +6,19 @@
6
6
  * (cwd, live pids, exit code, artifact) and live output.
7
7
  */
8
8
  import type { TspSpan, TspTone } from "@linxiraos/pi-wire";
9
- import { formatDuration } from "@linxiraos/pi-utils";
9
+ import { formatDuration, sanitizeText } 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
- import { truncateToWidth } from "../utils";
13
+ import { replaceTabs, truncateToWidth, wrapTextWithAnsi } from "../utils";
14
14
  import { collapseCommand } from "../apps/ps-data";
15
+ import { PREVIEW_LIMITS, shortenEmbeddedPaths } from "../render/render-utils";
15
16
  import { ansi, code, col, compact, item, node, row, span, stableKey, text } from "../native/describe";
16
17
  import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
17
18
  import { actionBar, actionButton, selectList } from "../native/overlay";
18
19
  import { theme } from "../theme/theme";
19
20
  import { buildJobTree, type JobTreeAgent, type JobTreeRow, jobTreeGuide } from "./job-tree";
21
+ import { describeJobWidget, type JobWidgetView, renderJobWidgetLines } from "../tools/job-widget";
20
22
 
21
23
  /** One async job as the session snapshots it. */
22
24
  export interface JobsPanelJob {
@@ -54,6 +56,8 @@ export interface JobsPanelDetail {
54
56
  readonly output?: string;
55
57
  /** Artifact holding the full output when `output` is cut. */
56
58
  readonly artifactId?: string;
59
+ /** The job's live widget (frozen once it settles), drawn above the output. */
60
+ readonly widget?: JobWidgetView;
57
61
  }
58
62
 
59
63
  /** The session behind a {@link JobsSheet}. */
@@ -149,6 +153,7 @@ export class JobsSheet implements Component {
149
153
  detail?.pids.join(" "),
150
154
  detail?.exitCode,
151
155
  detail?.artifactId,
156
+ detail?.widget ? `${detail.widget.updatedAt}:${detail.widget.state}` : "",
152
157
  jobs.some(job => job.status === "running") ? Math.floor(now / 1000) : "",
153
158
  ].join("|");
154
159
  const output = detail?.output;
@@ -198,6 +203,17 @@ export class JobsSheet implements Component {
198
203
  }
199
204
  const detail = selected && this.#source.inspect(selected.id);
200
205
  if (detail) {
206
+ // Rows show the cut label; the selected job's command wraps here,
207
+ // bounded so a long script never pushes the facts and output off the sheet.
208
+ if (detail.command) {
209
+ const commandRows = wrapTextWithAnsi(
210
+ shortenEmbeddedPaths(replaceTabs(sanitizeText(detail.command))),
211
+ Math.max(1, width - 3),
212
+ );
213
+ for (const line of commandRows.slice(0, PREVIEW_LIMITS.EXPANDED_LINES)) lines.push(` ${line}`);
214
+ const hidden = commandRows.length - PREVIEW_LIMITS.EXPANDED_LINES;
215
+ if (hidden > 0) lines.push(truncateToWidth(` … ${hidden} more command lines`, width));
216
+ }
201
217
  const facts = compact([
202
218
  detail.cwd && `cwd ${detail.cwd}`,
203
219
  detail.pids.length > 0 && `pid ${detail.pids.join(", ")}`,
@@ -205,6 +221,11 @@ export class JobsSheet implements Component {
205
221
  detail.artifactId && `artifact://${detail.artifactId}`,
206
222
  ]);
207
223
  if (facts.length > 0) lines.push(truncateToWidth(` ${facts.join(" · ")}`, width));
224
+ if (detail.widget) {
225
+ for (const line of renderJobWidgetLines(detail.widget, Math.max(0, width - 3), theme)) {
226
+ lines.push(` ${line}`);
227
+ }
228
+ }
208
229
  for (const line of this.#tailLines(selected.id, detail.output ?? "")) {
209
230
  if (line) lines.push(truncateToWidth(` ${line}`, width));
210
231
  }
@@ -413,6 +434,7 @@ function describeDetail(
413
434
  ? code(command, { lang: "bash", wrap: true, preview: { lines: 4 }, role: "zeta.jobs.command" })
414
435
  : text(job.label, { role: "zeta.jobs.title", wrap: "word" }),
415
436
  facts.length > 0 && node("kv", { items: facts, role: "zeta.jobs.facts" }),
437
+ detail?.widget && node("col", { role: "zeta.jobs.widget" }, [describeJobWidget(detail.widget)], "widget"),
416
438
  node(
417
439
  "col",
418
440
  { role: "zeta.jobs.out", gap: "sm" },
@@ -63,6 +63,7 @@ export type ModelRole =
63
63
  | "memory"
64
64
  | "task"
65
65
  | "advisor"
66
+ | "widget"
66
67
  | "image"
67
68
  | "web"
68
69
  | "speech"
@@ -79,6 +80,7 @@ export const MODEL_ROLE_IDS: ModelRole[] = [
79
80
  "memory",
80
81
  "task",
81
82
  "advisor",
83
+ "widget",
82
84
  "image",
83
85
  "web",
84
86
  "speech",
@@ -96,6 +98,7 @@ export const CHAT_MODEL_ROLE_IDS: ModelRole[] = [
96
98
  "memory",
97
99
  "task",
98
100
  "advisor",
101
+ "widget",
99
102
  ];
100
103
  export const KIND_ROLE_IDS: ModelRole[] = ["image", "web", "speech", "dictation", "judge"];
101
104
 
@@ -3046,7 +3046,8 @@ export class ModelHubComponent implements Component {
3046
3046
  this.#browser,
3047
3047
  );
3048
3048
  }
3049
- return node("col", { gap: "sm", grow: 1 }, children, "body");
3049
+ // Zero basis: the body takes the space left of the sidebar instead of sizing to its widest line.
3050
+ return node("col", { gap: "sm", grow: 1, basis: 0, min: { w: 0 } }, children, "body");
3050
3051
  }
3051
3052
 
3052
3053
  #describeStatus(): NativeNode {