@tt-a1i/openpi 0.2.0 → 0.3.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.
@@ -18,6 +18,29 @@ import type { SubagentSnapshot, TranscriptItem } from "../domain.ts";
18
18
 
19
19
  const MAX_CACHED_WIDTHS_PER_ITEM = 2;
20
20
 
21
+ export const SPINNER_FRAMES = [
22
+ "⠋",
23
+ "⠙",
24
+ "⠹",
25
+ "⠸",
26
+ "⠼",
27
+ "⠴",
28
+ "⠦",
29
+ "⠧",
30
+ "⠇",
31
+ "⠏",
32
+ ] as const;
33
+
34
+ /** Frame cadence, shared with the dashboard and takeover headers. */
35
+ export const SPINNER_INTERVAL_MS = 120;
36
+
37
+ export function spinnerFrame(now: number) {
38
+ const frame = Math.floor(now / SPINNER_INTERVAL_MS) % SPINNER_FRAMES.length;
39
+ return SPINNER_FRAMES[
40
+ (frame + SPINNER_FRAMES.length) % SPINNER_FRAMES.length
41
+ ];
42
+ }
43
+
21
44
  /**
22
45
  * Strip raw ANSI codes, expand tabs, and drop control chars. Terminal-expanded
23
46
  * tabs (and stray escapes) make lines wider than the width we declare to the
@@ -146,10 +169,82 @@ function renderThinking(theme: Theme, text: string, width: number) {
146
169
  return out;
147
170
  }
148
171
 
172
+ function renderToolBody(theme: Theme, name: string, argsPreview?: string) {
173
+ const toolName = sanitizeText(name);
174
+ const preview = summarizeToolArgs(toolName, argsPreview);
175
+ // The `$` form only earns its prompt when there is a command to show; a bare
176
+ // `$ ` would read as an empty shell line.
177
+ if (toolName === "bash" && preview) return theme.fg("dim", `$ ${preview}`);
178
+ return (
179
+ theme.fg("dim", "→ ") +
180
+ theme.fg("toolTitle", toolName) +
181
+ (preview ? theme.fg("dim", ` ${preview}`) : "")
182
+ );
183
+ }
184
+
185
+ function firstOutputPreview(outputPreview?: string) {
186
+ return (
187
+ sanitizeText(outputPreview ?? "")
188
+ .split("\n")
189
+ .find((line) => line.trim()) ?? ""
190
+ );
191
+ }
192
+
193
+ /**
194
+ * One execution owns exactly one glyph column, on its command line. Output
195
+ * lines are plain indented text so a block keeps identical columns from the
196
+ * moment the command starts to the moment it settles.
197
+ */
198
+ export type ToolPhase = "live" | "ok" | "error" | "pending";
199
+
200
+ function phaseGlyph(theme: Theme, phase: ToolPhase, now: number) {
201
+ switch (phase) {
202
+ case "live":
203
+ return theme.fg("warning", spinnerFrame(now));
204
+ case "ok":
205
+ return theme.fg("success", "✓");
206
+ case "error":
207
+ return theme.fg("error", "✗");
208
+ case "pending":
209
+ return theme.fg("dim", "·");
210
+ }
211
+ }
212
+
213
+ /** Command line: `<glyph> $ cmd` for bash, `<glyph> → name args` otherwise. */
214
+ function renderToolLine(
215
+ theme: Theme,
216
+ phase: ToolPhase,
217
+ name: string,
218
+ argsPreview: string | undefined,
219
+ width: number,
220
+ now: number,
221
+ ) {
222
+ return truncateToWidth(
223
+ `${phaseGlyph(theme, phase, now)} ${renderToolBody(theme, name, argsPreview)}`,
224
+ width,
225
+ );
226
+ }
227
+
228
+ /** Output line: indented under the command, no second glyph. */
229
+ function renderOutputLine(
230
+ theme: Theme,
231
+ isError: boolean,
232
+ outputPreview: string,
233
+ width: number,
234
+ ) {
235
+ const preview = outputPreview || "(no output)";
236
+ const content = isError
237
+ ? theme.fg(outputPreview ? "error" : "dim", preview)
238
+ : theme.fg("dim", preview);
239
+ return truncateToWidth(` ${content}`, width);
240
+ }
241
+
149
242
  function renderAssistantItem(
150
243
  theme: Theme,
151
244
  item: Extract<TranscriptItem, { kind: "assistant" }>,
152
245
  width: number,
246
+ phases: ReadonlyMap<string, ToolPhase>,
247
+ now: number,
153
248
  ) {
154
249
  const out: string[] = [];
155
250
  for (const part of item.parts) {
@@ -164,12 +259,14 @@ function renderAssistantItem(
164
259
  ),
165
260
  );
166
261
  } else if (part.type === "toolCall") {
167
- const preview = summarizeToolArgs(part.name, part.argsPreview);
168
- const line =
169
- theme.fg("muted", "→ ") +
170
- theme.fg("toolTitle", part.name) +
171
- (preview ? theme.fg("dim", ` ${preview}`) : "");
172
- out.push(truncateToWidth(line, width));
262
+ const phase = phases.get(part.toolId) ?? "pending";
263
+ // A live tool is rendered by the live block, which owns the spinner and
264
+ // the streaming output; rendering the call here too would show the same
265
+ // command twice and make the block reflow when the tool settles.
266
+ if (phase === "live") continue;
267
+ out.push(
268
+ renderToolLine(theme, phase, part.name, part.argsPreview, width, now),
269
+ );
173
270
  }
174
271
  }
175
272
  return out;
@@ -179,27 +276,117 @@ function renderToolResultItem(
179
276
  theme: Theme,
180
277
  item: Extract<TranscriptItem, { kind: "toolResult" }>,
181
278
  width: number,
279
+ paired: boolean,
280
+ now: number,
182
281
  ) {
183
- const firstLine =
184
- sanitizeText(item.outputPreview ?? "")
185
- .split("\n")
186
- .find((line) => line.trim()) ?? "";
187
- const label = item.isError
188
- ? theme.fg("error", " error: ")
189
- : theme.fg("dim", " output: ");
190
- return [
191
- truncateToWidth(label + theme.fg("dim", firstLine || "(no output)"), width),
192
- ];
282
+ const preview = firstOutputPreview(item.outputPreview);
283
+ // An orphan result (its call is not the previous item) still needs a glyph:
284
+ // there is no command line above it to carry one.
285
+ if (!paired) {
286
+ return [
287
+ renderToolLine(
288
+ theme,
289
+ item.isError ? "error" : "ok",
290
+ item.name,
291
+ undefined,
292
+ width,
293
+ now,
294
+ ),
295
+ ...(preview
296
+ ? [renderOutputLine(theme, item.isError, preview, width)]
297
+ : []),
298
+ ];
299
+ }
300
+ return [renderOutputLine(theme, item.isError, preview, width)];
301
+ }
302
+
303
+ function isPairedToolResult(
304
+ previous: TranscriptItem | undefined,
305
+ current: TranscriptItem,
306
+ ) {
307
+ if (
308
+ !previous ||
309
+ previous.kind !== "assistant" ||
310
+ current.kind !== "toolResult"
311
+ ) {
312
+ return false;
313
+ }
314
+ const lastPart = previous.parts[previous.parts.length - 1];
315
+ return lastPart?.type === "toolCall" && lastPart.toolId === current.toolId;
193
316
  }
194
317
 
195
318
  function renderTranscriptItem(
196
319
  theme: Theme,
197
320
  item: TranscriptItem,
198
321
  width: number,
322
+ context: ItemContext,
323
+ now: number,
199
324
  ) {
200
325
  if (item.kind === "user") return renderUserText(theme, item.text, width);
201
- if (item.kind === "assistant") return renderAssistantItem(theme, item, width);
202
- return renderToolResultItem(theme, item, width);
326
+ if (item.kind === "assistant") {
327
+ return renderAssistantItem(theme, item, width, context.phases, now);
328
+ }
329
+ return renderToolResultItem(theme, item, width, context.paired, now);
330
+ }
331
+
332
+ interface ItemContext {
333
+ readonly phases: ReadonlyMap<string, ToolPhase>;
334
+ readonly paired: boolean;
335
+ /** Cache discriminator: identity plus width is not enough on its own. */
336
+ readonly token: string;
337
+ }
338
+
339
+ /**
340
+ * An item's rendering depends on its neighbours (does a call have its result
341
+ * yet?) and on live state (is the call still running?), so the cache key has to
342
+ * carry that context or a stale glyph would outlive the phase it described.
343
+ */
344
+ function itemContext(
345
+ transcript: ReadonlyArray<TranscriptItem>,
346
+ index: number,
347
+ liveIds: ReadonlySet<string>,
348
+ ): ItemContext {
349
+ const item = transcript[index]!;
350
+ if (item.kind === "user")
351
+ return { phases: new Map(), paired: false, token: "" };
352
+ if (item.kind === "toolResult") {
353
+ const paired = isPairedToolResult(transcript[index - 1], item);
354
+ return { phases: new Map(), paired, token: paired ? "p" : "o" };
355
+ }
356
+
357
+ const phases = new Map<string, ToolPhase>();
358
+ for (const part of item.parts) {
359
+ if (part.type !== "toolCall") continue;
360
+ if (liveIds.has(part.toolId)) {
361
+ phases.set(part.toolId, "live");
362
+ continue;
363
+ }
364
+ const result = findResult(transcript, index, part.toolId);
365
+ phases.set(
366
+ part.toolId,
367
+ result ? (result.isError ? "error" : "ok") : "pending",
368
+ );
369
+ }
370
+ return {
371
+ phases,
372
+ paired: false,
373
+ token: [...phases].map(([id, phase]) => `${id}:${phase}`).join(","),
374
+ };
375
+ }
376
+
377
+ /** The result for a call, if it has already landed later in the transcript. */
378
+ function findResult(
379
+ transcript: ReadonlyArray<TranscriptItem>,
380
+ callIndex: number,
381
+ toolId: string,
382
+ ) {
383
+ for (let index = callIndex + 1; index < transcript.length; index++) {
384
+ const candidate = transcript[index];
385
+ if (candidate?.kind === "toolResult" && candidate.toolId === toolId) {
386
+ return candidate;
387
+ }
388
+ }
389
+ return undefined;
203
390
  }
204
391
 
205
392
  /**
@@ -208,24 +395,43 @@ function renderTranscriptItem(
208
395
  * from their component's invalidate() when Pi changes theme.
209
396
  */
210
397
  export class TranscriptRenderer {
211
- private itemCache = new WeakMap<TranscriptItem, Map<number, string[]>>();
398
+ private itemCache = new WeakMap<TranscriptItem, Map<string, string[]>>();
212
399
 
213
- render(snap: SubagentSnapshot, width: number, theme: Theme) {
400
+ render(
401
+ snap: SubagentSnapshot,
402
+ width: number,
403
+ theme: Theme,
404
+ options?: { readonly now?: number },
405
+ ) {
214
406
  const out: string[] = [];
407
+ const now = options?.now ?? Date.now();
408
+ const liveIds = new Set(snap.liveTools.map((tool) => tool.toolId));
215
409
 
216
- for (const item of snap.transcript) {
217
- const cached = this.itemCache.get(item)?.get(width);
218
- const lines = cached ?? renderTranscriptItem(theme, item, width);
410
+ for (let index = 0; index < snap.transcript.length; index++) {
411
+ const item = snap.transcript[index];
412
+ const context = itemContext(snap.transcript, index, liveIds);
413
+ const key = `${width}|${context.token}`;
414
+ const cached = this.itemCache.get(item)?.get(key);
415
+ const lines =
416
+ cached ?? renderTranscriptItem(theme, item, width, context, now);
219
417
  if (!cached) {
220
- const widths = this.itemCache.get(item) ?? new Map<number, string[]>();
418
+ const widths = this.itemCache.get(item) ?? new Map<string, string[]>();
221
419
  if (widths.size >= MAX_CACHED_WIDTHS_PER_ITEM) {
222
420
  const oldestWidth = widths.keys().next().value;
223
421
  if (oldestWidth !== undefined) widths.delete(oldestWidth);
224
422
  }
225
- widths.set(width, lines);
423
+ widths.set(key, lines);
226
424
  this.itemCache.set(item, widths);
227
425
  }
228
- if (lines.length > 0) out.push(...lines, "");
426
+ if (lines.length > 0) {
427
+ if (
428
+ out.length > 0 &&
429
+ !isPairedToolResult(snap.transcript[index - 1], item)
430
+ ) {
431
+ out.push("");
432
+ }
433
+ out.push(...lines);
434
+ }
229
435
  }
230
436
  while (out.length > 0 && out[out.length - 1] === "") out.pop();
231
437
 
@@ -239,19 +445,22 @@ export class TranscriptRenderer {
239
445
  if (out.length === before + 1) out.pop();
240
446
  }
241
447
 
242
- // Live tool executions (present until the ToolEnd lands in the transcript).
448
+ // Live tool executions. The manager drops a live entry when its ToolEnd
449
+ // lands, and the transcript's call line then takes over with the settled
450
+ // glyph in the same column, so the block never reflows.
243
451
  for (const tool of snap.liveTools) {
244
452
  if (out.length > 0) out.push("");
245
- const marker = tool.done
453
+ const phase: ToolPhase = tool.done
246
454
  ? tool.isError
247
- ? theme.fg("error", "error")
248
- : theme.fg("success", "done")
249
- : theme.fg("warning", "running");
250
- const args = summarizeToolArgs(tool.name, tool.argsPreview);
251
- let line = `${theme.fg("toolTitle", tool.name)}${args ? theme.fg("dim", ` ${args}`) : ""} · ${marker}`;
252
- const preview = tool.outputPreview && compactPreview(tool.outputPreview);
253
- if (preview) line += theme.fg("dim", ` · ${preview}`);
254
- out.push(truncateToWidth(line, width));
455
+ ? "error"
456
+ : "ok"
457
+ : "live";
458
+ out.push(
459
+ renderToolLine(theme, phase, tool.name, tool.argsPreview, width, now),
460
+ );
461
+ const preview = firstOutputPreview(tool.outputPreview);
462
+ if (preview)
463
+ out.push(renderOutputLine(theme, !!tool.isError, preview, width));
255
464
  }
256
465
 
257
466
  // Queued steering/follow-up messages: show them immediately so Enter
@@ -288,6 +497,12 @@ export function buildTranscriptLines(
288
497
  width: number,
289
498
  theme: Theme,
290
499
  renderer?: TranscriptRenderer,
500
+ options?: { readonly now?: number },
291
501
  ) {
292
- return (renderer ?? new TranscriptRenderer()).render(snap, width, theme);
502
+ return (renderer ?? new TranscriptRenderer()).render(
503
+ snap,
504
+ width,
505
+ theme,
506
+ options,
507
+ );
293
508
  }
@@ -6,17 +6,16 @@ import {
6
6
  import { Markdown, Text } from "@earendil-works/pi-tui";
7
7
  import { sanitizeText } from "./transcript.ts";
8
8
 
9
- const COLLAPSED_MAX_LINES = 12;
10
9
  const MAX_STATUS_ROWS = 4;
11
10
 
12
11
  export interface WaitResultItem {
13
- id: string;
14
- title?: string;
15
- status?: string;
12
+ readonly id: string;
13
+ readonly title?: string;
14
+ readonly status?: string;
16
15
  }
17
16
 
18
17
  export interface WaitResultDetails {
19
- results?: WaitResultItem[];
18
+ readonly results?: readonly WaitResultItem[];
20
19
  }
21
20
 
22
21
  export function buildWaitResultPreview(
@@ -46,23 +45,11 @@ export function buildWaitResultPreview(
46
45
  lines.push(theme.fg("dim", ` … ${results.length - MAX_STATUS_ROWS} more`));
47
46
  }
48
47
 
49
- const cleanLines = sanitizeText(content)
50
- .split("\n")
51
- .map((line) => line.trimEnd())
52
- .filter((line) => line.trim() && line.trim() !== "---");
53
- const leadingHeader = cleanLines[0]?.startsWith("## ") ? 1 : 0;
54
- const body = cleanLines.slice(leadingHeader);
55
- const available = Math.max(1, COLLAPSED_MAX_LINES - lines.length - 1);
56
- for (const line of body.slice(0, available)) {
57
- lines.push(theme.fg("toolOutput", line));
58
- }
59
-
60
- const hidden = Math.max(0, body.length - available);
61
- if (hidden > 0) {
48
+ if (content.trim()) {
62
49
  lines.push(
63
50
  theme.fg(
64
51
  "dim",
65
- `… ${hidden} more line${hidden === 1 ? "" : "s"} · ${keyHint("app.tools.expand", "to expand")}`,
52
+ `Results passed to main agent · ${keyHint("app.tools.expand", "to expand")}`,
66
53
  ),
67
54
  );
68
55
  }
@@ -1,8 +1,11 @@
1
- import {
2
- CustomEditor,
3
- type ExtensionAPI,
4
- type ExtensionContext,
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
5
4
  } from "@earendil-works/pi-coding-agent";
5
+ import {
6
+ registerEditorLayer,
7
+ removeEditorLayer,
8
+ } from "../shared/editor-layers.ts";
6
9
  import {
7
10
  loadSetupConfig,
8
11
  SETUP_CONFIG_CHANGED_CHANNEL,
@@ -51,6 +54,7 @@ export default function (pi: ExtensionAPI) {
51
54
  let sessionActive = false;
52
55
  let statusContext: ExtensionContext | undefined;
53
56
  let requestEditorRender: (() => void) | undefined;
57
+ let editorLayerRegistered = false;
54
58
 
55
59
  const updateStatus = () => {
56
60
  statusContext?.ui.setStatus(
@@ -72,21 +76,22 @@ export default function (pi: ExtensionAPI) {
72
76
 
73
77
  const installSuggestionEditor = (ctx: ExtensionContext) => {
74
78
  if (ctx.mode !== "tui") return;
75
- const previous = ctx.ui.getEditorComponent();
76
- ctx.ui.setEditorComponent((tui, theme, keybindings) => {
77
- const base =
78
- previous?.(tui, theme, keybindings) ??
79
- new CustomEditor(tui, theme, keybindings);
80
- requestEditorRender = () => tui.requestRender();
81
- return new NextActionSuggestionEditor(
82
- base,
83
- keybindings,
84
- suggestionState,
85
- cancelPrediction,
86
- requestEditorRender,
87
- (text) => ctx.ui.theme.fg("dim", text),
88
- );
79
+ registerEditorLayer(pi, ctx, {
80
+ id: "suggestions",
81
+ order: 200,
82
+ wrap: (base, tui, _theme, keybindings) => {
83
+ requestEditorRender = () => tui.requestRender();
84
+ return new NextActionSuggestionEditor(
85
+ base,
86
+ keybindings,
87
+ suggestionState,
88
+ cancelPrediction,
89
+ requestEditorRender,
90
+ (text) => ctx.ui.theme.fg("dim", text),
91
+ );
92
+ },
89
93
  });
94
+ editorLayerRegistered = true;
90
95
  };
91
96
 
92
97
  pi.on("session_start", (_event, ctx) => {
@@ -160,6 +165,10 @@ export default function (pi: ExtensionAPI) {
160
165
  pi.events.on(SETUP_CONFIG_CHANGED_CHANNEL, cancelPrediction);
161
166
 
162
167
  pi.on("session_shutdown", async () => {
168
+ if (editorLayerRegistered) {
169
+ removeEditorLayer(pi, "suggestions");
170
+ editorLayerRegistered = false;
171
+ }
163
172
  sessionActive = false;
164
173
  runBoundary.reset();
165
174
  const task = activePrediction?.task;
@@ -99,6 +99,8 @@ export default function sessionTasks(pi: ExtensionAPI) {
99
99
  let notifiedProblem: string | undefined;
100
100
  let taskWidgetVisible = true;
101
101
  let taskWidgetExpanded = false;
102
+ let taskWidgetMounted = false;
103
+ let requestTaskWidgetRender: (() => void) | undefined;
102
104
  let ui: ExtensionContext["ui"] | undefined;
103
105
  let uiMode: ExtensionContext["mode"] | undefined;
104
106
  const hideLifecycleTools = () =>
@@ -124,22 +126,36 @@ export default function sessionTasks(pi: ExtensionAPI) {
124
126
 
125
127
  const updateTaskWidget = (ctx?: ExtensionContext) => {
126
128
  if (ctx?.hasUI) {
129
+ if (ui !== ctx.ui) {
130
+ taskWidgetMounted = false;
131
+ requestTaskWidgetRender = undefined;
132
+ }
127
133
  ui = ctx.ui;
128
134
  uiMode = ctx.mode;
129
135
  }
130
136
  if (!ui || uiMode !== "tui") return false;
131
- const current = snapshot();
132
137
  const shown =
133
138
  taskWidgetVisible && !problemMessage() && hasActionableTasks();
134
139
  if (!shown) {
140
+ if (!taskWidgetMounted) return false;
135
141
  ui.setWidget(TASK_WIDGET_KEY, undefined);
142
+ taskWidgetMounted = false;
143
+ requestTaskWidgetRender = undefined;
136
144
  return false;
137
145
  }
138
- ui.setWidget(TASK_WIDGET_KEY, (_tui, theme) => ({
139
- render: (width) =>
140
- renderTaskWidget(current, theme, width, taskWidgetExpanded),
141
- invalidate() {},
142
- }));
146
+ if (taskWidgetMounted) {
147
+ requestTaskWidgetRender?.();
148
+ return true;
149
+ }
150
+ ui.setWidget(TASK_WIDGET_KEY, (tui, theme) => {
151
+ requestTaskWidgetRender = () => tui.requestRender();
152
+ return {
153
+ render: (width) =>
154
+ renderTaskWidget(snapshot(), theme, width, taskWidgetExpanded),
155
+ invalidate() {},
156
+ };
157
+ });
158
+ taskWidgetMounted = true;
143
159
  return true;
144
160
  };
145
161
 
@@ -519,6 +535,8 @@ export default function sessionTasks(pi: ExtensionAPI) {
519
535
  } catch {
520
536
  // The interactive UI may already be disposed.
521
537
  }
538
+ taskWidgetMounted = false;
539
+ requestTaskWidgetRender = undefined;
522
540
  ui = undefined;
523
541
  uiMode = undefined;
524
542
  });
@@ -450,27 +450,76 @@ export function renderFooter(options: FooterRenderOptions) {
450
450
  formatPullRequest,
451
451
  );
452
452
  const lines: string[] = [];
453
+ const statusLines = options.statuses
454
+ ? Array.from(options.statuses).flatMap((status) => status.split("\n"))
455
+ : [];
456
+ const singleStatus = statusLines.length === 1 ? statusLines[0] : undefined;
457
+ const separator = options.theme.fg("dim", " · ");
458
+ const styledStatus =
459
+ singleStatus === undefined
460
+ ? undefined
461
+ : options.theme.fg("dim", singleStatus);
462
+ const statusWidth =
463
+ styledStatus === undefined
464
+ ? 0
465
+ : visibleWidth(separator) + visibleWidth(styledStatus);
466
+ let inlinedStatus = false;
453
467
 
454
468
  for (const layout of options.lines) {
455
- const line = renderFooterLine(
469
+ const resolved = resolveLineSegments(
456
470
  layout,
457
471
  catalog,
458
472
  options.style,
473
+ options.modelInfo.contextPercent,
474
+ );
475
+ const fitted = fitSegmentsToWidth(
476
+ resolved.left,
477
+ resolved.right,
459
478
  options.width,
479
+ options.style,
480
+ options.theme,
481
+ );
482
+ const canInlineStatus =
483
+ lines.length === 0 &&
484
+ styledStatus !== undefined &&
485
+ naturalLineWidth(
486
+ fitted.left,
487
+ fitted.right,
488
+ options.style,
489
+ options.theme,
490
+ ) +
491
+ statusWidth <=
492
+ options.width;
493
+ const line = renderFooterLine(
494
+ layout,
495
+ catalog,
496
+ options.style,
497
+ canInlineStatus ? options.width - statusWidth : options.width,
460
498
  options.theme,
461
499
  options.modelInfo.contextPercent,
462
500
  );
463
- if (line) lines.push(line);
501
+ if (!line) continue;
502
+ lines.push(
503
+ canInlineStatus
504
+ ? truncateToWidth(
505
+ `${line}${separator}${styledStatus}`,
506
+ options.width,
507
+ options.theme.fg("dim", "..."),
508
+ )
509
+ : line,
510
+ );
511
+ inlinedStatus ||= canInlineStatus;
464
512
  }
465
513
 
466
- if (options.statuses) {
467
- for (const statusLine of options.statuses) {
468
- for (const part of statusLine.split("\n")) {
469
- lines.push(
470
- truncateToWidth(part, options.width, options.theme.fg("dim", "...")),
471
- );
472
- }
473
- }
514
+ if (inlinedStatus) return lines;
515
+ for (const statusLine of statusLines) {
516
+ lines.push(
517
+ truncateToWidth(
518
+ statusLine,
519
+ options.width,
520
+ options.theme.fg("dim", "..."),
521
+ ),
522
+ );
474
523
  }
475
524
 
476
525
  return lines;