@narumitw/pi-statusline 0.25.0 → 0.31.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.
@@ -3,8 +3,8 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
3
3
  import process from "node:process";
4
4
  import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
5
5
  import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
6
+ import { powerlineExtensionSeparator } from "./powerline.js";
6
7
  import { DEFAULT_EXTENSION_STATUS_ICONS } from "./settings.js";
7
- import { tokyoNightExtensionSeparator } from "./tokyo-night.js";
8
8
  import type { StatuslineConfig } from "./types.js";
9
9
 
10
10
  export type ExtensionStatusIconAliasMap = ReadonlyMap<string, readonly string[]>;
@@ -14,9 +14,15 @@ export interface ExtensionStatusRuntime {
14
14
  }
15
15
 
16
16
  const STATUSLINE_KEY = "statusline";
17
+ const COMPATIBLE_STATUS_ICON_KEYS: Readonly<Record<string, string>> = {
18
+ retry: "unknown-error-retry",
19
+ sync: "pisync",
20
+ "unknown-error-retry": "retry",
21
+ pisync: "sync",
22
+ };
17
23
  const EMPTY_EXTENSION_STATUS_ICON_ALIASES: ExtensionStatusIconAliasMap = new Map();
18
24
  function extensionStatusSeparator(config: StatuslineConfig, theme: Theme): string {
19
- return tokyoNightExtensionSeparator(theme, config.palette);
25
+ return powerlineExtensionSeparator(theme, config.palettePreset);
20
26
  }
21
27
 
22
28
  export function formatExtensionStatuses(
@@ -69,12 +75,32 @@ function extensionStatusIcon(
69
75
  extensionStatusIconAliases: ExtensionStatusIconAliasMap,
70
76
  ) {
71
77
  if (Object.hasOwn(configuredIcons, key)) return configuredIcons[key];
78
+ const namespaceIcon = configuredNamespaceIcon(key, configuredIcons);
79
+ if (namespaceIcon !== undefined) return namespaceIcon;
80
+ const compatibleKey = COMPATIBLE_STATUS_ICON_KEYS[key];
81
+ if (compatibleKey && Object.hasOwn(configuredIcons, compatibleKey)) {
82
+ return configuredIcons[compatibleKey];
83
+ }
72
84
  for (const alias of extensionStatusAliasesForKey(key, extensionStatusIconAliases)) {
73
85
  if (Object.hasOwn(configuredIcons, alias)) return configuredIcons[alias];
74
86
  }
75
87
  return leadingIcon ?? DEFAULT_EXTENSION_STATUS_ICONS[key] ?? "🔌";
76
88
  }
77
89
 
90
+ function configuredNamespaceIcon(
91
+ key: string,
92
+ configuredIcons: Readonly<Record<string, string>>,
93
+ ): string | undefined {
94
+ let match: { baseLength: number; icon: string } | undefined;
95
+ for (const [selector, icon] of Object.entries(configuredIcons)) {
96
+ if (!selector.endsWith(":*")) continue;
97
+ const base = selector.slice(0, -2);
98
+ if (!base || !key.startsWith(`${base}:`)) continue;
99
+ if (!match || base.length > match.baseLength) match = { baseLength: base.length, icon };
100
+ }
101
+ return match?.icon;
102
+ }
103
+
78
104
  function extensionStatusAliasesForKey(
79
105
  key: string,
80
106
  extensionStatusIconAliases: ExtensionStatusIconAliasMap,
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./statusline.js";
@@ -0,0 +1,39 @@
1
+ import type { ConfigSegmentName, SegmentName } from "./types.js";
2
+
3
+ export const INFORMATION_PROFILE_NAMES = ["minimal", "balanced", "detailed"] as const;
4
+ export type InformationProfileName = (typeof INFORMATION_PROFILE_NAMES)[number];
5
+ export type InformationProfile = InformationProfileName | "custom";
6
+
7
+ export const INFORMATION_PROFILES: Readonly<
8
+ Record<InformationProfileName, readonly SegmentName[]>
9
+ > = {
10
+ minimal: ["model", "cwd", "branch", "context"],
11
+ balanced: ["model", "thinking", "cwd", "branch", "tools", "context", "cost"],
12
+ detailed: [
13
+ "provider",
14
+ "model",
15
+ "thinking",
16
+ "cwd",
17
+ "branch",
18
+ "tools",
19
+ "context",
20
+ "tokens",
21
+ "cost",
22
+ "time",
23
+ ],
24
+ };
25
+
26
+ export function inferInformationProfile(
27
+ segments: readonly ConfigSegmentName[],
28
+ ): InformationProfile {
29
+ for (const name of INFORMATION_PROFILE_NAMES) {
30
+ const profile = INFORMATION_PROFILES[name];
31
+ if (
32
+ segments.length === profile.length &&
33
+ segments.every((segment, index) => segment === profile[index])
34
+ ) {
35
+ return name;
36
+ }
37
+ }
38
+ return "custom";
39
+ }
@@ -0,0 +1,169 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { visibleWidth } from "@earendil-works/pi-tui";
3
+ import { ansiStyle } from "./ansi.js";
4
+ import { resolvePreset } from "./presets/index.js";
5
+ import type { BlockColors, PowerlinePreset } from "./presets/types.js";
6
+ import {
7
+ LINE_BREAK_SEGMENT_NAME,
8
+ type PalettePreset,
9
+ type PowerlineBlockName,
10
+ type RenderItem,
11
+ type RenderSegment,
12
+ type SegmentPalette,
13
+ type SeparatorName,
14
+ type StatuslineConfig,
15
+ } from "./types.js";
16
+
17
+ interface PowerlineBlock {
18
+ baseBlock: PowerlineBlockName;
19
+ colors: BlockColors;
20
+ segments: RenderSegment[];
21
+ }
22
+
23
+ export function renderPowerlineStatusline(
24
+ width: number,
25
+ items: RenderItem[],
26
+ config: Pick<StatuslineConfig, "palettePreset" | "palette" | "density" | "separator">,
27
+ ): string {
28
+ if (items.length === 0 || width <= 0) return "";
29
+ return splitLines(items)
30
+ .map((segments) => fitPowerlineSegments(segments, width, config))
31
+ .join("\n");
32
+ }
33
+
34
+ function splitLines(items: RenderItem[]): RenderSegment[][] {
35
+ const lines: RenderSegment[][] = [[]];
36
+ for (const item of items) {
37
+ if (item.name === LINE_BREAK_SEGMENT_NAME) lines.push([]);
38
+ else lines.at(-1)?.push(item);
39
+ }
40
+ return lines;
41
+ }
42
+
43
+ const SEGMENT_RETENTION_PRIORITY: Readonly<Record<RenderSegment["name"], number>> = {
44
+ context: 120,
45
+ model: 110,
46
+ branch: 100,
47
+ tools: 90,
48
+ cwd: 80,
49
+ thinking: 70,
50
+ cost: 60,
51
+ provider: 50,
52
+ tokens: 40,
53
+ time: 30,
54
+ turn: 20,
55
+ brand: 10,
56
+ };
57
+
58
+ function fitPowerlineSegments(
59
+ segments: readonly RenderSegment[],
60
+ width: number,
61
+ config: Pick<StatuslineConfig, "palettePreset" | "palette" | "density" | "separator">,
62
+ ): string {
63
+ if (segments.length === 0) return "";
64
+ const fitted = [...segments];
65
+ while (fitted.length > 1) {
66
+ const rendered = joinPowerlineSegments(fitted, config);
67
+ if (visibleWidth(rendered) <= width) return rendered;
68
+ let removalIndex = 0;
69
+ for (let index = 1; index < fitted.length; index += 1) {
70
+ const candidate = fitted[index];
71
+ const current = fitted[removalIndex];
72
+ if (
73
+ candidate &&
74
+ current &&
75
+ SEGMENT_RETENTION_PRIORITY[candidate.name] < SEGMENT_RETENTION_PRIORITY[current.name]
76
+ ) {
77
+ removalIndex = index;
78
+ }
79
+ }
80
+ fitted.splice(removalIndex, 1);
81
+ }
82
+ const rendered = joinPowerlineSegments(fitted, config);
83
+ return visibleWidth(rendered) <= width ? rendered : "";
84
+ }
85
+
86
+ export function powerlineExtensionSeparator(
87
+ _theme: Theme,
88
+ palettePreset: PalettePreset = "tokyo-night",
89
+ ): string {
90
+ return ansiStyle(" • ", { fg: resolvePreset(palettePreset).extensionSeparator });
91
+ }
92
+
93
+ function joinPowerlineSegments(
94
+ segments: RenderSegment[],
95
+ config: Pick<StatuslineConfig, "palettePreset" | "palette" | "density" | "separator">,
96
+ ): string {
97
+ const preset = resolvePreset(config.palettePreset);
98
+ const blocks = contiguousBlocks(segments, preset, config.palettePreset, config.palette);
99
+ let line = ansiStyle("░▒▓", { fg: preset.lead });
100
+
101
+ for (const [index, block] of blocks.entries()) {
102
+ const previous = index === 0 ? undefined : blocks[index - 1]?.colors;
103
+ if (previous) line += ansiStyle("", { fg: previous.bg, bg: block.colors.bg });
104
+ line += ansiStyle(formatBlockText(block, config), block.colors);
105
+ }
106
+
107
+ const lastBlock = blocks.at(-1);
108
+ if (lastBlock) line += ansiStyle("", { fg: lastBlock.colors.bg });
109
+ return line;
110
+ }
111
+
112
+ function contiguousBlocks(
113
+ segments: RenderSegment[],
114
+ preset: PowerlinePreset,
115
+ palettePreset: PalettePreset,
116
+ configuredPalette: SegmentPalette,
117
+ ): PowerlineBlock[] {
118
+ const blocks: PowerlineBlock[] = [];
119
+ const usesConfiguredColors = palettePreset === "custom";
120
+ for (const segment of segments) {
121
+ const colors = usesConfiguredColors
122
+ ? (configuredPalette[segment.name] ?? {})
123
+ : preset.blocks[segment.block];
124
+ const previous = blocks.at(-1);
125
+ const matchesPrevious =
126
+ previous !== undefined &&
127
+ (usesConfiguredColors
128
+ ? colorsEqual(previous.colors, colors)
129
+ : previous.baseBlock === segment.block);
130
+ if (matchesPrevious) previous.segments.push(segment);
131
+ else blocks.push({ baseBlock: segment.block, colors, segments: [segment] });
132
+ }
133
+ return blocks;
134
+ }
135
+
136
+ function colorsEqual(left: BlockColors, right: BlockColors): boolean {
137
+ return left.fg === right.fg && left.bg === right.bg;
138
+ }
139
+
140
+ function formatBlockText(
141
+ block: PowerlineBlock,
142
+ config: Pick<StatuslineConfig, "density" | "separator">,
143
+ ): string {
144
+ const texts = block.segments.map(formatSegmentText);
145
+ const separator = separatorText(config.separator, config.density === "cozy");
146
+ const leading = config.density === "cozy" ? " " : " ";
147
+ const trailing = config.density === "cozy" ? " " : "";
148
+ return `${leading}${texts.join(separator)}${trailing}`;
149
+ }
150
+
151
+ function formatSegmentText(segment: RenderSegment): string {
152
+ return segment.emphasis ? `\u001b[1m${segment.text}\u001b[22m` : segment.text;
153
+ }
154
+
155
+ function separatorText(separator: SeparatorName, cozy: boolean): string {
156
+ const padding = cozy ? " " : " ";
157
+ switch (separator) {
158
+ case "dot":
159
+ return `${padding}•${padding}`;
160
+ case "bar":
161
+ return `${padding}│${padding}`;
162
+ case "powerline":
163
+ return `${padding}${padding}`;
164
+ case "round":
165
+ return `${padding}❯${padding}`;
166
+ case "none":
167
+ return padding;
168
+ }
169
+ }
@@ -0,0 +1,9 @@
1
+ import { createRampPreset } from "./create-ramp.js";
2
+
3
+ export const CANDY_PRESET = createRampPreset([
4
+ "#f5c2e7",
5
+ "#cba6f7",
6
+ "#89b4fa",
7
+ "#745f9a",
8
+ "#403a5c",
9
+ ]);
@@ -0,0 +1,41 @@
1
+ import type { PowerlineBlockName } from "../types.js";
2
+ import type { PowerlinePreset } from "./types.js";
3
+
4
+ const BLOCK_NAMES: PowerlineBlockName[] = ["header", "directory", "git", "runtime", "meter"];
5
+
6
+ type PaletteRamp = readonly [string, string, string, string, string];
7
+
8
+ export function createRampPreset(backgrounds: PaletteRamp): PowerlinePreset {
9
+ return {
10
+ lead: backgrounds[0],
11
+ blocks: Object.fromEntries(
12
+ BLOCK_NAMES.map((block, index) => {
13
+ const background = backgrounds[index];
14
+ return [block, { fg: contrastColor(background), bg: background }];
15
+ }),
16
+ ) as Record<PowerlineBlockName, { fg: string; bg: string }>,
17
+ extensionSeparator: backgrounds[2],
18
+ };
19
+ }
20
+
21
+ function contrastColor(hex: string): string {
22
+ const background = relativeLuminance(hex);
23
+ const dark = relativeLuminance("#090c0c");
24
+ const light = relativeLuminance("#f0f0f0");
25
+ return contrastRatio(background, dark) >= contrastRatio(background, light)
26
+ ? "#090c0c"
27
+ : "#f0f0f0";
28
+ }
29
+
30
+ function contrastRatio(left: number, right: number): number {
31
+ return (Math.max(left, right) + 0.05) / (Math.min(left, right) + 0.05);
32
+ }
33
+
34
+ function relativeLuminance(hex: string): number {
35
+ const normalized = hex.slice(1);
36
+ const channels = [0, 2, 4].map((offset) => {
37
+ const value = Number.parseInt(normalized.slice(offset, offset + 2), 16) / 255;
38
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
39
+ });
40
+ return 0.2126 * (channels[0] ?? 0) + 0.7152 * (channels[1] ?? 0) + 0.0722 * (channels[2] ?? 0);
41
+ }
@@ -0,0 +1,11 @@
1
+ import type { PowerlinePreset } from "./types.js";
2
+
3
+ export const CUSTOM_PRESET: PowerlinePreset = {
4
+ blocks: {
5
+ header: {},
6
+ directory: {},
7
+ git: {},
8
+ runtime: {},
9
+ meter: {},
10
+ },
11
+ };
@@ -0,0 +1,9 @@
1
+ import { createRampPreset } from "./create-ramp.js";
2
+
3
+ export const FOREST_PRESET = createRampPreset([
4
+ "#a7c080",
5
+ "#83c092",
6
+ "#5f9f75",
7
+ "#3f6f55",
8
+ "#293f35",
9
+ ]);
@@ -0,0 +1,54 @@
1
+ import {
2
+ type PaletteName,
3
+ type PalettePreset,
4
+ type PowerlineBlockName,
5
+ SEGMENT_NAMES,
6
+ type SegmentName,
7
+ type SegmentPalette,
8
+ } from "../types.js";
9
+ import { CANDY_PRESET } from "./candy.js";
10
+ import { CUSTOM_PRESET } from "./custom.js";
11
+ import { FOREST_PRESET } from "./forest.js";
12
+ import { MONO_PRESET } from "./mono.js";
13
+ import { NEON_PRESET } from "./neon.js";
14
+ import { OCEAN_PRESET } from "./ocean.js";
15
+ import { SUNSET_PRESET } from "./sunset.js";
16
+ import { TOKYO_NIGHT_PRESET } from "./tokyo-night.js";
17
+ import type { PowerlinePreset } from "./types.js";
18
+
19
+ const PRESETS = {
20
+ "tokyo-night": TOKYO_NIGHT_PRESET,
21
+ ocean: OCEAN_PRESET,
22
+ sunset: SUNSET_PRESET,
23
+ forest: FOREST_PRESET,
24
+ candy: CANDY_PRESET,
25
+ neon: NEON_PRESET,
26
+ mono: MONO_PRESET,
27
+ custom: CUSTOM_PRESET,
28
+ } satisfies Record<PalettePreset, PowerlinePreset>;
29
+
30
+ const SEGMENT_BLOCKS: Record<SegmentName, PowerlineBlockName> = {
31
+ brand: "header",
32
+ provider: "header",
33
+ model: "header",
34
+ thinking: "header",
35
+ cwd: "directory",
36
+ branch: "git",
37
+ tools: "runtime",
38
+ context: "runtime",
39
+ tokens: "runtime",
40
+ cost: "meter",
41
+ time: "meter",
42
+ turn: "meter",
43
+ };
44
+
45
+ export function resolvePreset(preset: PalettePreset): PowerlinePreset {
46
+ return PRESETS[preset];
47
+ }
48
+
49
+ export function segmentPaletteForPreset(preset: PaletteName): SegmentPalette {
50
+ const blocks = PRESETS[preset].blocks;
51
+ return Object.fromEntries(
52
+ SEGMENT_NAMES.map((name) => [name, { ...blocks[SEGMENT_BLOCKS[name]] }]),
53
+ ) as SegmentPalette;
54
+ }
@@ -0,0 +1,9 @@
1
+ import { createRampPreset } from "./create-ramp.js";
2
+
3
+ export const MONO_PRESET = createRampPreset([
4
+ "#d4d4d4",
5
+ "#a3a3a3",
6
+ "#686868",
7
+ "#404040",
8
+ "#262626",
9
+ ]);
@@ -0,0 +1,9 @@
1
+ import { createRampPreset } from "./create-ramp.js";
2
+
3
+ export const NEON_PRESET = createRampPreset([
4
+ "#39ff14",
5
+ "#00f5ff",
6
+ "#ff4fd8",
7
+ "#7a2cf3",
8
+ "#29134f",
9
+ ]);
@@ -0,0 +1,9 @@
1
+ import { createRampPreset } from "./create-ramp.js";
2
+
3
+ export const OCEAN_PRESET = createRampPreset([
4
+ "#7dcfff",
5
+ "#4f9fba",
6
+ "#2d6f88",
7
+ "#23475b",
8
+ "#182b3a",
9
+ ]);
@@ -0,0 +1,9 @@
1
+ import { createRampPreset } from "./create-ramp.js";
2
+
3
+ export const SUNSET_PRESET = createRampPreset([
4
+ "#ffcf70",
5
+ "#f59e6f",
6
+ "#dc718a",
7
+ "#8f5b78",
8
+ "#493447",
9
+ ]);
@@ -0,0 +1,13 @@
1
+ import type { PowerlinePreset } from "./types.js";
2
+
3
+ export const TOKYO_NIGHT_PRESET: PowerlinePreset = {
4
+ lead: "#a3aed2",
5
+ blocks: {
6
+ header: { fg: "#090c0c", bg: "#a3aed2" },
7
+ directory: { fg: "#e3e5e5", bg: "#769ff0" },
8
+ git: { fg: "#769ff0", bg: "#394260" },
9
+ runtime: { fg: "#769ff0", bg: "#212736" },
10
+ meter: { fg: "#a0a9cb", bg: "#1d2230" },
11
+ },
12
+ extensionSeparator: "#394260",
13
+ };
@@ -0,0 +1,12 @@
1
+ import type { PowerlineBlockName } from "../types.js";
2
+
3
+ export interface BlockColors {
4
+ fg?: string;
5
+ bg?: string;
6
+ }
7
+
8
+ export interface PowerlinePreset {
9
+ lead?: string;
10
+ blocks: Record<PowerlineBlockName, BlockColors>;
11
+ extensionSeparator?: string;
12
+ }
package/src/render.ts CHANGED
@@ -12,22 +12,20 @@ import {
12
12
  wrapExtensionStatusline,
13
13
  } from "./extension-status.js";
14
14
  import { formatGitBranchValue, type GitStatusSummary } from "./git-status.js";
15
- import { renderTokyoNightStatusline } from "./tokyo-night.js";
15
+ import { renderPowerlineStatusline } from "./powerline.js";
16
16
  import {
17
17
  LINE_BREAK_SEGMENT_NAME,
18
+ type PowerlineBlockName,
18
19
  type RenderItem,
19
20
  type RenderSegment,
20
21
  type SegmentName,
21
22
  type StatuslineConfig,
22
- type TokyoNightBlockName,
23
23
  } from "./types.js";
24
24
 
25
25
  type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
26
26
  export interface RuntimeState extends ExtensionStatusRuntime {
27
27
  turnCount: number;
28
28
  activeTools: Map<string, number>;
29
- lastTool?: string;
30
- lastCompletedTool?: string;
31
29
  isStreaming: boolean;
32
30
  thinkingLevel: ThinkingLevel;
33
31
  gitStatus?: GitStatusSummary;
@@ -50,19 +48,32 @@ export function renderStatusline(
50
48
  ): string {
51
49
  if (width <= 0) return "";
52
50
 
53
- const segments = config.segments
54
- .map((name): RenderItem | undefined =>
55
- name === LINE_BREAK_SEGMENT_NAME
56
- ? { name: LINE_BREAK_SEGMENT_NAME }
57
- : buildSegment(name, ctx, footerData, config, runtime),
58
- )
59
- .filter(
60
- (segment): segment is RenderItem =>
61
- segment !== undefined &&
62
- (segment.name === LINE_BREAK_SEGMENT_NAME || segment.text.length > 0),
63
- );
51
+ const rows: Array<{ configuredSegments: number; segments: RenderSegment[] }> = [
52
+ { configuredSegments: 0, segments: [] },
53
+ ];
54
+ for (const name of config.segments) {
55
+ if (name === LINE_BREAK_SEGMENT_NAME) {
56
+ rows.push({ configuredSegments: 0, segments: [] });
57
+ continue;
58
+ }
59
+
60
+ const row = rows.at(-1);
61
+ if (!row) continue;
62
+ row.configuredSegments += 1;
63
+ const rendered = buildSegment(name, ctx, footerData, config, runtime);
64
+ if (rendered && rendered.text.length > 0) row.segments.push(rendered);
65
+ }
64
66
 
65
- return renderTokyoNightStatusline(width, segments, config);
67
+ const segments: RenderItem[] = [];
68
+ const renderedRows = rows.filter(
69
+ (row) => row.configuredSegments === 0 || row.segments.length > 0,
70
+ );
71
+ for (const [index, row] of renderedRows.entries()) {
72
+ if (index > 0) segments.push({ name: LINE_BREAK_SEGMENT_NAME });
73
+ segments.push(...row.segments);
74
+ }
75
+
76
+ return renderPowerlineStatusline(width, segments, config);
66
77
  }
67
78
 
68
79
  export function renderExtensionStatusline(
@@ -121,8 +132,10 @@ function buildSegment(
121
132
  }
122
133
  case "cwd":
123
134
  return segment(name, basename(ctx.cwd) || ctx.cwd, config, "accent", "directory");
124
- case "tools":
125
- return segment(name, formatToolActivity(runtime), config, "accent", "runtime");
135
+ case "tools": {
136
+ const activity = formatToolActivity(runtime);
137
+ return activity ? segment(name, activity, config, "accent", "runtime") : undefined;
138
+ }
126
139
  case "context": {
127
140
  const usage = ctx.getContextUsage();
128
141
  const value =
@@ -161,7 +174,7 @@ function segment(
161
174
  value: string,
162
175
  config: StatuslineConfig,
163
176
  color: RenderSegment["color"],
164
- block: TokyoNightBlockName,
177
+ block: PowerlineBlockName,
165
178
  emphasis = false,
166
179
  ): RenderSegment {
167
180
  return { name, text: formatConfiguredSegment(name, value, config), color, block, emphasis };
@@ -204,7 +217,7 @@ export function contextColor(percent: number | null | undefined): ThemeColor {
204
217
  return "success";
205
218
  }
206
219
 
207
- export function formatToolActivity(runtime: RuntimeState): string {
220
+ export function formatToolActivity(runtime: RuntimeState): string | undefined {
208
221
  const active = [...runtime.activeTools.entries()];
209
222
  if (active.length > 0) {
210
223
  const [name, count] = active[0] ?? ["tool", 1];
@@ -212,9 +225,7 @@ export function formatToolActivity(runtime: RuntimeState): string {
212
225
  return `⚙ ${name}${suffix}`;
213
226
  }
214
227
 
215
- if (runtime.isStreaming) return "💭 thinking";
216
- if (runtime.lastCompletedTool) return `✅ ${runtime.lastCompletedTool}`;
217
- return "💤 idle";
228
+ return runtime.isStreaming ? "💭 thinking" : undefined;
218
229
  }
219
230
 
220
231
  export function prLinkFromStatuses(statuses: ReadonlyMap<string, string>): string | undefined {