@narumitw/pi-statusline 0.1.28 → 0.1.30

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.
package/README.md CHANGED
@@ -8,14 +8,14 @@ Use it to monitor model selection, thinking level, git branch, working directory
8
8
 
9
9
  ## ✨ Features
10
10
 
11
- - Replaces the default Pi footer with a compact rich statusline.
11
+ - Replaces the default Pi footer with a compact preset-based statusline.
12
12
  - Shows model, thinking level, git branch, project directory, active tool, context usage, tokens, cost, and clock.
13
13
  - Displays compact statuses published through Pi's generic extension status API.
14
14
  - Preserves extension-provided status icons when the status text starts with one.
15
15
  - Warns when the same extension package is installed from multiple sources.
16
- - Uses emoji-labeled segments for readability.
16
+ - Uses emoji-labeled segments for readability in both classic and Tokyo Night presets.
17
17
  - Adapts to terminal width and truncates safely.
18
- - Requires no configuration.
18
+ - Requires no configuration, with optional preset selection through `PI_STATUSLINE_PRESET`.
19
19
 
20
20
  ## 📦 Install
21
21
 
@@ -35,22 +35,38 @@ Try this package locally from the repository root:
35
35
  pi -e ./extensions/pi-statusline
36
36
  ```
37
37
 
38
+ ## 🎨 Presets
39
+
40
+ `pi-statusline` supports presets through the `PI_STATUSLINE_PRESET` environment variable:
41
+
42
+ ```bash
43
+ PI_STATUSLINE_PRESET=tokyo-night pi
44
+ PI_STATUSLINE_PRESET=classic pi
45
+ ```
46
+
47
+ Supported presets:
48
+
49
+ - `tokyo-night` — the default, inspired by the [Starship Tokyo Night preset](https://starship.rs/presets/tokyo-night), using `░▒▓` / `` powerline blocks and the Tokyo Night color ramp.
50
+ - `classic` — a compact Pi-themed statusline with left-aligned `•` separators.
51
+
52
+ Unset or invalid values fall back to `tokyo-night`. Both presets keep the same emoji-labeled information and preserve extension-provided status icons.
53
+
38
54
  ## 👀 What it shows
39
55
 
40
- The default statusline includes:
56
+ The default `tokyo-night` statusline uses a Starship-inspired `░▒▓` / `` powerline layout and includes:
41
57
 
42
58
  - `π` brand marker.
43
59
  - 🤖 current model.
44
60
  - 🧠 thinking level.
45
- - 🌿 git branch.
46
61
  - 📁 current project directory.
47
- - 🔧 active or last tool.
48
- - 📊 context usage percentage.
62
+ - 🌿 git branch.
63
+ - ⚙ active or last tool.
64
+ - 🪟 context usage percentage.
49
65
  - 🔢 token totals.
50
- - 💰 estimated cost.
66
+ - 💸 estimated cost.
51
67
  - 🕒 clock.
52
68
 
53
- Statuses from other extensions appear on their own compact line below the main statusline and are separated with ``.
69
+ Statuses from other extensions appear on their own compact line below the main statusline and use each preset's separator.
54
70
 
55
71
  `pi-statusline` is extension-agnostic: it consumes Pi's generic extension status API and does not import or depend on status-producing extensions. If an extension wants a custom icon, it should include that icon at the start of its status text, for example `ctx.ui.setStatus("goal", "🎯 active")`. Statuses without a leading icon use the generic `🔌` icon.
56
72
 
@@ -76,6 +92,11 @@ Examples:
76
92
  extensions/pi-statusline/
77
93
  ├── src/
78
94
  │ └── statusline.ts
95
+ ├── presets/
96
+ │ ├── ansi.ts
97
+ │ ├── classic.ts
98
+ │ ├── tokyo-night.ts
99
+ │ └── types.ts
79
100
  ├── README.md
80
101
  ├── LICENSE
81
102
  ├── tsconfig.json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-statusline",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Pi extension that replaces the footer with an information-rich statusline.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,6 +14,7 @@
14
14
  ],
15
15
  "files": [
16
16
  "src",
17
+ "presets",
17
18
  "README.md",
18
19
  "LICENSE"
19
20
  ],
@@ -0,0 +1,26 @@
1
+ export function ansiStyle(text: string, colors: { fg?: string; bg?: string }): string {
2
+ const codes = [
3
+ colors.fg ? truecolorCode("38", colors.fg) : undefined,
4
+ colors.bg ? truecolorCode("48", colors.bg) : undefined,
5
+ ].filter((code): code is string => code !== undefined);
6
+ if (codes.length === 0) return text;
7
+ return `\u001b[${codes.join(";")}m${text}\u001b[0m`;
8
+ }
9
+
10
+ export function ansiFg(hex: string, text: string): string {
11
+ return ansiStyle(text, { fg: hex });
12
+ }
13
+
14
+ function truecolorCode(prefix: "38" | "48", hex: string): string {
15
+ const { red, green, blue } = hexToRgb(hex);
16
+ return `${prefix};2;${red};${green};${blue}`;
17
+ }
18
+
19
+ function hexToRgb(hex: string): { red: number; green: number; blue: number } {
20
+ const normalized = hex.replace(/^#/, "");
21
+ return {
22
+ red: Number.parseInt(normalized.slice(0, 2), 16),
23
+ green: Number.parseInt(normalized.slice(2, 4), 16),
24
+ blue: Number.parseInt(normalized.slice(4, 6), 16),
25
+ };
26
+ }
@@ -0,0 +1,55 @@
1
+ import type { Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
2
+ import { truncateToWidth } from "@mariozechner/pi-tui";
3
+ import type { RenderSegment, SeparatorName, StatuslineConfig } from "./types.js";
4
+
5
+ export function renderClassicStatusline(
6
+ width: number,
7
+ segments: RenderSegment[],
8
+ theme: Theme,
9
+ config: StatuslineConfig,
10
+ ): string {
11
+ return truncateToWidth(joinSegments(segments, theme, config), width, "");
12
+ }
13
+
14
+ export function classicExtensionSeparator(theme: Theme): string {
15
+ return theme.fg("dim", " • ");
16
+ }
17
+
18
+ function joinSegments(segments: RenderSegment[], theme: Theme, config: StatuslineConfig): string {
19
+ const separator = separatorText(config.separator);
20
+ return segments
21
+ .map((segment, index) => styleSegment(segment, index, theme, config))
22
+ .join(theme.fg("dim", separator));
23
+ }
24
+
25
+ function styleSegment(
26
+ segment: RenderSegment,
27
+ index: number,
28
+ theme: Theme,
29
+ config: StatuslineConfig,
30
+ ): string {
31
+ const padding = config.density === "cozy" ? " " : "";
32
+ const text = `${padding}${segment.text}${padding}`;
33
+ const styledText = segment.emphasis ? theme.bold(text) : text;
34
+
35
+ if (config.palette === "mono") {
36
+ return index === 0 ? theme.fg("muted", styledText) : theme.fg("dim", styledText);
37
+ }
38
+
39
+ return theme.fg(segment.color as ThemeColor, styledText);
40
+ }
41
+
42
+ function separatorText(separator: SeparatorName): string {
43
+ switch (separator) {
44
+ case "powerline":
45
+ return "  ";
46
+ case "bar":
47
+ return " │ ";
48
+ case "round":
49
+ return " ❯ ";
50
+ case "none":
51
+ return " ";
52
+ case "dot":
53
+ return " • ";
54
+ }
55
+ }
@@ -0,0 +1,84 @@
1
+ import type { Theme } from "@mariozechner/pi-coding-agent";
2
+ import { truncateToWidth } from "@mariozechner/pi-tui";
3
+ import { ansiFg, ansiStyle } from "./ansi.js";
4
+ import type { RenderSegment, TokyoNightBlockName } from "./types.js";
5
+
6
+ interface TokyoNightBlock {
7
+ name: TokyoNightBlockName;
8
+ segments: RenderSegment[];
9
+ }
10
+
11
+ interface BlockColors {
12
+ fg: string;
13
+ bg: string;
14
+ }
15
+
16
+ const TOKYO_NIGHT_COLORS = {
17
+ lead: "#a3aed2",
18
+ header: { fg: "#090c0c", bg: "#a3aed2" },
19
+ directory: { fg: "#e3e5e5", bg: "#769ff0" },
20
+ git: { fg: "#769ff0", bg: "#394260" },
21
+ runtime: { fg: "#769ff0", bg: "#212736" },
22
+ meter: { fg: "#a0a9cb", bg: "#1d2230" },
23
+ extensionSeparator: "#394260",
24
+ } as const satisfies Record<string, string | BlockColors>;
25
+
26
+ const TOKYO_NIGHT_BLOCK_ORDER: TokyoNightBlockName[] = [
27
+ "header",
28
+ "directory",
29
+ "git",
30
+ "runtime",
31
+ "meter",
32
+ ];
33
+
34
+ export function renderTokyoNightStatusline(width: number, segments: RenderSegment[]): string {
35
+ return truncateToWidth(joinTokyoNightSegments(segments), width, "");
36
+ }
37
+
38
+ export function tokyoNightExtensionSeparator(_theme: Theme): string {
39
+ return ansiFg(TOKYO_NIGHT_COLORS.extensionSeparator, "  ");
40
+ }
41
+
42
+ function joinTokyoNightSegments(segments: RenderSegment[]): string {
43
+ const blocks = groupTokyoNightBlocks(segments);
44
+ let line = ansiFg(TOKYO_NIGHT_COLORS.lead, "░▒▓");
45
+
46
+ for (const [index, block] of blocks.entries()) {
47
+ const colors = getTokyoNightBlockColors(block.name);
48
+ const previous =
49
+ index === 0 ? undefined : getTokyoNightBlockColors(blocks[index - 1]?.name ?? "header");
50
+ if (previous) line += ansiStyle("", { fg: previous.bg, bg: colors.bg });
51
+ line += ansiStyle(formatTokyoNightBlockText(block), colors);
52
+ }
53
+
54
+ const lastBlock = blocks.at(-1);
55
+ if (lastBlock) line += ansiFg(getTokyoNightBlockColors(lastBlock.name).bg, "");
56
+
57
+ return line;
58
+ }
59
+
60
+ function groupTokyoNightBlocks(segments: RenderSegment[]): TokyoNightBlock[] {
61
+ const blocksByName = new Map<TokyoNightBlockName, RenderSegment[]>();
62
+ for (const segment of segments) {
63
+ const blockSegments = blocksByName.get(segment.block) ?? [];
64
+ blockSegments.push(segment);
65
+ blocksByName.set(segment.block, blockSegments);
66
+ }
67
+
68
+ return TOKYO_NIGHT_BLOCK_ORDER.flatMap((name) => {
69
+ const blockSegments = blocksByName.get(name);
70
+ return blockSegments ? [{ name, segments: blockSegments }] : [];
71
+ });
72
+ }
73
+
74
+ function formatTokyoNightBlockText(block: TokyoNightBlock): string {
75
+ return ` ${block.segments.map(formatTokyoNightSegmentText).join(" ")}`;
76
+ }
77
+
78
+ function formatTokyoNightSegmentText(segment: RenderSegment): string {
79
+ return segment.emphasis ? `\u001b[1m${segment.text}\u001b[22m` : segment.text;
80
+ }
81
+
82
+ function getTokyoNightBlockColors(block: TokyoNightBlockName): BlockColors {
83
+ return TOKYO_NIGHT_COLORS[block];
84
+ }
@@ -0,0 +1,37 @@
1
+ import type { ThemeColor } from "@mariozechner/pi-coding-agent";
2
+
3
+ export type SegmentName =
4
+ | "brand"
5
+ | "model"
6
+ | "thinking"
7
+ | "cwd"
8
+ | "branch"
9
+ | "tools"
10
+ | "context"
11
+ | "tokens"
12
+ | "cost"
13
+ | "time"
14
+ | "turn";
15
+
16
+ export type StatuslinePresetName = "classic" | "tokyo-night";
17
+ export type TokyoNightBlockName = "header" | "directory" | "git" | "runtime" | "meter";
18
+ export type PaletteName = "ocean" | "sunset" | "forest" | "candy" | "neon" | "mono";
19
+ export type Density = "compact" | "cozy";
20
+ export type SeparatorName = "dot" | "bar" | "powerline" | "round" | "none";
21
+
22
+ export interface StatuslineConfig {
23
+ preset: StatuslinePresetName;
24
+ palette: PaletteName;
25
+ density: Density;
26
+ separator: SeparatorName;
27
+ showLabels: boolean;
28
+ segments: SegmentName[];
29
+ }
30
+
31
+ export interface RenderSegment {
32
+ name: SegmentName;
33
+ text: string;
34
+ color: ThemeColor;
35
+ block: TokyoNightBlockName;
36
+ emphasis?: boolean;
37
+ }
package/src/statusline.ts CHANGED
@@ -8,35 +8,20 @@ import type {
8
8
  Theme,
9
9
  ThemeColor,
10
10
  } from "@mariozechner/pi-coding-agent";
11
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
12
-
13
- type SegmentName =
14
- | "brand"
15
- | "model"
16
- | "thinking"
17
- | "branch"
18
- | "cwd"
19
- | "tools"
20
- | "context"
21
- | "tokens"
22
- | "cost"
23
- | "time"
24
- | "turn";
25
-
26
- type PaletteName = "ocean" | "sunset" | "forest" | "candy" | "neon" | "mono";
27
- type Density = "compact" | "cozy";
28
- type SeparatorName = "dot" | "bar" | "powerline" | "round" | "none";
11
+ import { truncateToWidth } from "@mariozechner/pi-tui";
12
+ import { classicExtensionSeparator, renderClassicStatusline } from "../presets/classic.js";
13
+ import { renderTokyoNightStatusline, tokyoNightExtensionSeparator } from "../presets/tokyo-night.js";
14
+ import type {
15
+ PaletteName,
16
+ RenderSegment,
17
+ SegmentName,
18
+ StatuslineConfig,
19
+ StatuslinePresetName,
20
+ TokyoNightBlockName,
21
+ } from "../presets/types.js";
29
22
 
30
23
  type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
31
24
 
32
- interface StatuslineConfig {
33
- palette: PaletteName;
34
- density: Density;
35
- separator: SeparatorName;
36
- showLabels: boolean;
37
- segments: SegmentName[];
38
- }
39
-
40
25
  interface RuntimeState {
41
26
  turnCount: number;
42
27
  activeTools: Map<string, number>;
@@ -54,21 +39,15 @@ interface TokenTotals {
54
39
  cost: number;
55
40
  }
56
41
 
57
- interface RenderSegment {
58
- name: SegmentName;
59
- text: string;
60
- color: ThemeColor;
61
- emphasis?: boolean;
62
- }
63
-
64
42
  const STATUSLINE_KEY = "statusline";
43
+ const DEFAULT_PRESET: StatuslinePresetName = "tokyo-night";
65
44
 
66
45
  const DEFAULT_SEGMENTS: SegmentName[] = [
67
46
  "brand",
68
47
  "model",
69
48
  "thinking",
70
- "branch",
71
49
  "cwd",
50
+ "branch",
72
51
  "tools",
73
52
  "context",
74
53
  "tokens",
@@ -76,8 +55,6 @@ const DEFAULT_SEGMENTS: SegmentName[] = [
76
55
  "time",
77
56
  ];
78
57
 
79
- const RIGHT_SEGMENTS = new Set<SegmentName>(["context", "tokens", "cost", "time", "turn"]);
80
-
81
58
  const PALETTES: Record<PaletteName, ThemeColor[]> = {
82
59
  ocean: ["accent", "muted", "success", "warning"],
83
60
  sunset: ["warning", "accent", "success", "muted"],
@@ -116,7 +93,13 @@ export default function statusline(pi: ExtensionAPI) {
116
93
  invalidate() {},
117
94
  render(width: number): string[] {
118
95
  const lines = [renderStatusline(width, ctx, footerData, theme, config, runtime)];
119
- const extensionStatusLine = renderExtensionStatusline(width, footerData, theme, runtime);
96
+ const extensionStatusLine = renderExtensionStatusline(
97
+ width,
98
+ footerData,
99
+ theme,
100
+ config,
101
+ runtime,
102
+ );
120
103
  if (extensionStatusLine) lines.push(extensionStatusLine);
121
104
  return lines;
122
105
  },
@@ -184,14 +167,21 @@ export default function statusline(pi: ExtensionAPI) {
184
167
 
185
168
  function createDefaultConfig(): StatuslineConfig {
186
169
  return {
170
+ preset: readStatuslinePreset(),
187
171
  palette: "candy",
188
172
  density: "compact",
189
- separator: "powerline",
173
+ separator: "dot",
190
174
  showLabels: false,
191
175
  segments: [...DEFAULT_SEGMENTS],
192
176
  };
193
177
  }
194
178
 
179
+ function readStatuslinePreset(): StatuslinePresetName {
180
+ const preset = process.env.PI_STATUSLINE_PRESET?.trim().toLowerCase();
181
+ if (preset === "classic" || preset === "tokyo-night") return preset;
182
+ return DEFAULT_PRESET;
183
+ }
184
+
195
185
  function renderStatusline(
196
186
  width: number,
197
187
  ctx: ExtensionContext,
@@ -208,38 +198,24 @@ function renderStatusline(
208
198
  (segment): segment is RenderSegment => segment !== undefined && segment.text.length > 0,
209
199
  );
210
200
 
211
- const left = joinSegments(
212
- segments.filter((segment) => !RIGHT_SEGMENTS.has(segment.name)),
213
- theme,
214
- config,
215
- );
216
- const right = joinSegments(
217
- segments.filter((segment) => RIGHT_SEGMENTS.has(segment.name)),
218
- theme,
219
- config,
220
- );
221
-
222
- if (!left && !right) return truncateToWidth(theme.fg("dim", "pi-statusline"), width);
223
- if (!right) return truncateToWidth(left, width);
224
- if (!left) return truncateToWidth(right, width);
225
-
226
- const rightWidth = visibleWidth(right);
227
- if (rightWidth + 1 >= width) return truncateToWidth(right, width);
201
+ if (segments.length === 0) return truncateToWidth(theme.fg("dim", "pi-statusline"), width);
228
202
 
229
- const leftWidth = Math.max(0, width - rightWidth - 1);
230
- const trimmedLeft = truncateToWidth(left, leftWidth, "…");
231
- const padding = " ".repeat(Math.max(1, width - visibleWidth(trimmedLeft) - rightWidth));
232
-
233
- return truncateToWidth(`${trimmedLeft}${padding}${right}`, width, "");
203
+ switch (config.preset) {
204
+ case "classic":
205
+ return renderClassicStatusline(width, segments, theme, config);
206
+ case "tokyo-night":
207
+ return renderTokyoNightStatusline(width, segments);
208
+ }
234
209
  }
235
210
 
236
211
  function renderExtensionStatusline(
237
212
  width: number,
238
213
  footerData: ReadonlyFooterDataProvider,
239
214
  theme: Theme,
215
+ config: StatuslineConfig,
240
216
  runtime: RuntimeState,
241
217
  ): string | undefined {
242
- const status = formatExtensionStatuses(footerData.getExtensionStatuses(), theme, runtime);
218
+ const status = formatExtensionStatuses(footerData.getExtensionStatuses(), theme, config, runtime);
243
219
  if (!status) return undefined;
244
220
 
245
221
  return truncateToWidth(status, width, "");
@@ -257,98 +233,68 @@ function buildSegment(
257
233
 
258
234
  switch (name) {
259
235
  case "brand":
260
- return { name, text: "π", color: "accent", emphasis: true };
236
+ return segment(name, "π", "accent", "header", true);
261
237
  case "model":
262
- return labeled(name, `🤖 ${shortenModel(ctx.model?.id ?? "no-model")}`, color, config);
238
+ return segment(name, `🤖 ${shortenModel(ctx.model?.id ?? "no-model")}`, color, "header");
263
239
  case "thinking":
264
- return labeled(
240
+ return segment(
265
241
  name,
266
242
  `🧠 ${runtime.thinkingLevel}`,
267
243
  thinkingColor(runtime.thinkingLevel),
268
- config,
244
+ "header",
269
245
  );
270
246
  case "branch":
271
- return labeled(name, `🌿 ${footerData.getGitBranch() ?? "no-git"}`, color, config);
247
+ return segment(name, `🌿 ${footerData.getGitBranch() ?? "no-git"}`, color, "git");
272
248
  case "cwd":
273
- return labeled(name, `📁 ${basename(ctx.cwd) || ctx.cwd}`, color, config);
249
+ return segment(name, `📁 ${basename(ctx.cwd) || ctx.cwd}`, color, "directory");
274
250
  case "tools":
275
- return labeled(name, formatToolActivity(runtime), color, config);
251
+ return segment(name, formatToolActivity(runtime), color, "runtime");
276
252
  case "context": {
277
253
  const usage = ctx.getContextUsage();
278
254
  const value =
279
255
  usage?.percent === null || usage?.percent === undefined
280
256
  ? "🪟 ctx ?"
281
257
  : `🪟 ctx ${usage.percent.toFixed(0)}%`;
282
- return labeled(name, value, contextColor(usage?.percent), config);
258
+ return segment(name, value, contextColor(usage?.percent), "runtime");
283
259
  }
284
260
  case "tokens": {
285
261
  const totals = getTokenTotals(ctx);
286
262
  if (totals.input === 0 && totals.output === 0)
287
- return labeled(name, "🔢 tok 0", color, config);
288
- return labeled(
263
+ return segment(name, "🔢 tok 0", color, "runtime");
264
+ return segment(
289
265
  name,
290
266
  `🔢 ↑${formatCount(totals.input)} ↓${formatCount(totals.output)}`,
291
267
  color,
292
- config,
268
+ "runtime",
293
269
  );
294
270
  }
295
271
  case "cost": {
296
272
  const totals = getTokenTotals(ctx);
297
- return labeled(name, `💸 $${totals.cost.toFixed(totals.cost >= 1 ? 2 : 3)}`, color, config);
273
+ return segment(name, `💸 $${totals.cost.toFixed(totals.cost >= 1 ? 2 : 3)}`, color, "meter");
298
274
  }
299
275
  case "time":
300
- return labeled(name, `🕒 ${formatTime()}`, color, config);
276
+ return segment(name, `🕒 ${formatTime()}`, color, "meter");
301
277
  case "turn":
302
- return labeled(name, `🔁 #${runtime.turnCount}`, color, config);
278
+ return segment(name, `🔁 #${runtime.turnCount}`, color, "meter");
303
279
  }
304
280
  }
305
281
 
306
- function labeled(
282
+ function segment(
307
283
  name: SegmentName,
308
- value: string,
284
+ text: string,
309
285
  color: ThemeColor,
310
- config: StatuslineConfig,
286
+ block: TokyoNightBlockName,
287
+ emphasis = false,
311
288
  ): RenderSegment {
312
- if (!config.showLabels || name === "brand") return { name, text: value, color };
313
- return { name, text: `${name} ${value}`, color };
289
+ return { name, text, color, block, emphasis };
314
290
  }
315
291
 
316
- function joinSegments(segments: RenderSegment[], theme: Theme, config: StatuslineConfig): string {
317
- const separator = separatorText(config.separator);
318
- return segments
319
- .map((segment, index) => styleSegment(segment, index, theme, config))
320
- .join(theme.fg("dim", separator));
321
- }
322
-
323
- function styleSegment(
324
- segment: RenderSegment,
325
- index: number,
326
- theme: Theme,
327
- config: StatuslineConfig,
328
- ): string {
329
- const padding = config.density === "cozy" ? " " : "";
330
- const text = `${padding}${segment.text}${padding}`;
331
- const styledText = segment.emphasis ? theme.bold(text) : text;
332
-
333
- if (config.palette === "mono") {
334
- return index === 0 ? theme.fg("muted", styledText) : theme.fg("dim", styledText);
335
- }
336
-
337
- return theme.fg(segment.color, styledText);
338
- }
339
-
340
- function separatorText(separator: SeparatorName): string {
341
- switch (separator) {
342
- case "powerline":
343
- return "  ";
344
- case "bar":
345
- return " │ ";
346
- case "round":
347
- return " ❯ ";
348
- case "none":
349
- return " ";
350
- case "dot":
351
- return " • ";
292
+ function extensionStatusSeparator(presetName: StatuslinePresetName, theme: Theme): string {
293
+ switch (presetName) {
294
+ case "classic":
295
+ return classicExtensionSeparator(theme);
296
+ case "tokyo-night":
297
+ return tokyoNightExtensionSeparator(theme);
352
298
  }
353
299
  }
354
300
 
@@ -397,9 +343,10 @@ function formatToolActivity(runtime: RuntimeState): string {
397
343
  function formatExtensionStatuses(
398
344
  statuses: ReadonlyMap<string, string>,
399
345
  theme: Theme,
346
+ config: StatuslineConfig,
400
347
  runtime: RuntimeState,
401
348
  ): string {
402
- const separator = theme.fg("dim", "  ");
349
+ const separator = extensionStatusSeparator(config.preset, theme);
403
350
  const visibleStatuses = [
404
351
  ...formatDuplicateExtensionStatus(runtime, theme),
405
352
  ...[...statuses.entries()]
@@ -421,7 +368,8 @@ function formatExtensionStatus(key: string, value: string, theme: Theme): string
421
368
  function formatDuplicateExtensionStatus(runtime: RuntimeState, theme: Theme): string[] {
422
369
  if (runtime.duplicateExtensions.length === 0) return [];
423
370
  const names = runtime.duplicateExtensions.slice(0, 2).join(", ");
424
- const suffix = runtime.duplicateExtensions.length > 2 ? ` +${runtime.duplicateExtensions.length - 2}` : "";
371
+ const suffix =
372
+ runtime.duplicateExtensions.length > 2 ? ` +${runtime.duplicateExtensions.length - 2}` : "";
425
373
  return [`${theme.fg("warning", "⚠️")} ${theme.fg("warning", `dup ${names}${suffix}`)}`];
426
374
  }
427
375
 
@@ -494,7 +442,11 @@ function readPackageSources(settingsFile: string): string[] {
494
442
  return (settings.packages ?? [])
495
443
  .map((entry) => {
496
444
  if (typeof entry === "string") return entry;
497
- if (entry && typeof entry === "object" && typeof (entry as { source?: unknown }).source === "string") {
445
+ if (
446
+ entry &&
447
+ typeof entry === "object" &&
448
+ typeof (entry as { source?: unknown }).source === "string"
449
+ ) {
498
450
  return (entry as { source: string }).source;
499
451
  }
500
452
  return undefined;