pi-cc-extensions 0.1.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 (39) hide show
  1. package/README.md +141 -0
  2. package/assets/readme/context-preview.webp +0 -0
  3. package/assets/readme/context-usage.webp +0 -0
  4. package/assets/readme/hero.svg +101 -0
  5. package/assets/readme/session-reference.webp +0 -0
  6. package/assets/readme/showcase.webp +0 -0
  7. package/extensions/README.md +3 -0
  8. package/extensions/ccstyle-powerline/bash-mode/completion.ts +556 -0
  9. package/extensions/ccstyle-powerline/bash-mode/editor.ts +442 -0
  10. package/extensions/ccstyle-powerline/bash-mode/history.ts +151 -0
  11. package/extensions/ccstyle-powerline/bash-mode/shell-session.ts +286 -0
  12. package/extensions/ccstyle-powerline/bash-mode/transcript.ts +108 -0
  13. package/extensions/ccstyle-powerline/bash-mode/types.ts +59 -0
  14. package/extensions/ccstyle-powerline/colors.ts +69 -0
  15. package/extensions/ccstyle-powerline/context-usage.ts +46 -0
  16. package/extensions/ccstyle-powerline/fixed-editor/cluster.ts +107 -0
  17. package/extensions/ccstyle-powerline/fixed-editor/terminal-split.ts +1317 -0
  18. package/extensions/ccstyle-powerline/git-status.ts +218 -0
  19. package/extensions/ccstyle-powerline/icons.ts +181 -0
  20. package/extensions/ccstyle-powerline/powerline-config.ts +234 -0
  21. package/extensions/ccstyle-powerline/powerline.ts +3150 -0
  22. package/extensions/ccstyle-powerline/presets.ts +109 -0
  23. package/extensions/ccstyle-powerline/render-scheduler.ts +24 -0
  24. package/extensions/ccstyle-powerline/segments.ts +476 -0
  25. package/extensions/ccstyle-powerline/separators.ts +41 -0
  26. package/extensions/ccstyle-powerline/shortcuts.ts +57 -0
  27. package/extensions/ccstyle-powerline/theme.json +26 -0
  28. package/extensions/ccstyle-powerline/theme.ts +241 -0
  29. package/extensions/ccstyle-powerline/types.ts +197 -0
  30. package/extensions/ccstyle-powerline/welcome-dismiss.ts +34 -0
  31. package/extensions/ccstyle-powerline/welcome.ts +611 -0
  32. package/extensions/ccstyle-powerline/working-vibes.ts +695 -0
  33. package/extensions/claude-code-style.ts +987 -0
  34. package/extensions/context.ts +362 -0
  35. package/extensions/session-reference/README.md +28 -0
  36. package/extensions/session-reference/core.test.ts +74 -0
  37. package/extensions/session-reference/core.ts +258 -0
  38. package/extensions/session-reference/index.ts +379 -0
  39. package/package.json +33 -0
@@ -0,0 +1,109 @@
1
+ import type { ColorScheme, PresetDef, StatusLinePreset } from "./types.ts";
2
+ import { getDefaultColors } from "./theme.ts";
3
+
4
+ // Get base colors from theme.ts (single source of truth)
5
+ const DEFAULT_COLORS: ColorScheme = getDefaultColors();
6
+
7
+ // Minimal - more muted, less colorful
8
+ const MINIMAL_COLORS: ColorScheme = {
9
+ ...DEFAULT_COLORS,
10
+ model: "text",
11
+ path: "text",
12
+ gitClean: "dim",
13
+ };
14
+
15
+ // Nerd - vibrant colors
16
+ const NERD_COLORS: ColorScheme = {
17
+ ...DEFAULT_COLORS,
18
+ model: "accent",
19
+ path: "success",
20
+ tokens: "muted",
21
+ cost: "warning",
22
+ };
23
+
24
+ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
25
+ default: {
26
+ leftSegments: ["model", "thinking", "shell_mode", "path", "git", "context_pct", "cache_read", "cost"],
27
+ rightSegments: [],
28
+ secondarySegments: ["extension_statuses"],
29
+ separator: "powerline-thin",
30
+ colors: DEFAULT_COLORS,
31
+ segmentOptions: {
32
+ model: { showThinkingLevel: false },
33
+ path: { mode: "basename" },
34
+ git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
35
+ },
36
+ },
37
+
38
+ minimal: {
39
+ leftSegments: ["shell_mode", "path", "git"],
40
+ rightSegments: ["context_pct"],
41
+ separator: "slash",
42
+ colors: MINIMAL_COLORS,
43
+ segmentOptions: {
44
+ path: { mode: "basename" },
45
+ git: { showBranch: true, showStaged: false, showUnstaged: false, showUntracked: false },
46
+ },
47
+ },
48
+
49
+ compact: {
50
+ leftSegments: ["model", "shell_mode", "git"],
51
+ rightSegments: ["cost", "context_pct"],
52
+ separator: "pipe",
53
+ colors: DEFAULT_COLORS,
54
+ segmentOptions: {
55
+ model: { showThinkingLevel: true },
56
+ git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: false },
57
+ },
58
+ },
59
+
60
+ full: {
61
+ leftSegments: ["hostname", "model", "thinking", "shell_mode", "path", "git", "subagents"],
62
+ rightSegments: ["token_in", "token_out", "cache_read", "cost", "context_pct", "time_spent", "time", "extension_statuses"],
63
+ separator: "powerline",
64
+ colors: DEFAULT_COLORS,
65
+ segmentOptions: {
66
+ model: { showThinkingLevel: false },
67
+ path: { mode: "abbreviated", maxLength: 50 },
68
+ git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
69
+ time: { format: "24h", showSeconds: false },
70
+ },
71
+ },
72
+
73
+ nerd: {
74
+ leftSegments: ["hostname", "model", "thinking", "shell_mode", "path", "git", "session", "subagents"],
75
+ rightSegments: ["token_in", "token_out", "cache_read", "cache_write", "cost", "context_pct", "context_total", "time_spent", "time", "extension_statuses"],
76
+ separator: "powerline",
77
+ colors: NERD_COLORS,
78
+ segmentOptions: {
79
+ model: { showThinkingLevel: false },
80
+ path: { mode: "abbreviated", maxLength: 60 },
81
+ git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
82
+ time: { format: "24h", showSeconds: true },
83
+ },
84
+ },
85
+
86
+ ascii: {
87
+ leftSegments: ["model", "shell_mode", "path", "git"],
88
+ rightSegments: ["token_total", "cost", "context_pct"],
89
+ separator: "ascii",
90
+ colors: MINIMAL_COLORS,
91
+ segmentOptions: {
92
+ model: { showThinkingLevel: true },
93
+ path: { mode: "abbreviated", maxLength: 40 },
94
+ git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
95
+ },
96
+ },
97
+
98
+ custom: {
99
+ leftSegments: ["model", "shell_mode", "path", "git"],
100
+ rightSegments: ["token_total", "cost", "context_pct"],
101
+ separator: "pipe",
102
+ colors: DEFAULT_COLORS,
103
+ segmentOptions: {},
104
+ },
105
+ };
106
+
107
+ export function getPreset(name: StatusLinePreset): PresetDef {
108
+ return PRESETS[name] ?? PRESETS.default;
109
+ }
@@ -0,0 +1,24 @@
1
+ export interface RenderScheduler {
2
+ schedule(delayMs?: number): void;
3
+ cancel(): void;
4
+ }
5
+
6
+ export function createRenderScheduler(render: () => void, defaultDelayMs: number): RenderScheduler {
7
+ let timer: ReturnType<typeof setTimeout> | null = null;
8
+
9
+ return {
10
+ schedule(delayMs = defaultDelayMs) {
11
+ if (timer) return;
12
+
13
+ timer = setTimeout(() => {
14
+ timer = null;
15
+ render();
16
+ }, delayMs);
17
+ },
18
+ cancel() {
19
+ if (!timer) return;
20
+ clearTimeout(timer);
21
+ timer = null;
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,476 @@
1
+ import { hostname as osHostname } from "node:os";
2
+ import { basename } from "node:path";
3
+ import { visibleWidth } from "@earendil-works/pi-tui";
4
+ import type { BuiltinStatusLineSegmentId, RenderedSegment, SegmentContext, SemanticColor, StatusLineSegment, StatusLineSegmentId } from "./types.ts";
5
+ import { normalizeCompactExtensionStatus, normalizeExtensionStatusValue } from "./powerline-config.ts";
6
+ import { fg, colorThinking, applyColor } from "./theme.ts";
7
+ import { getIcons, SEP_DOT, getThinkingText } from "./icons.ts";
8
+
9
+ function color(ctx: SegmentContext, semantic: SemanticColor, text: string): string {
10
+ return fg(ctx.theme, semantic, text, ctx.colors);
11
+ }
12
+
13
+ // ═══════════════════════════════════════════════════════════════════════════
14
+ // Helpers
15
+ // ═══════════════════════════════════════════════════════════════════════════
16
+
17
+ function withIcon(icon: string, text: string): string {
18
+ return icon ? `${icon} ${text}` : text;
19
+ }
20
+
21
+ function formatTokens(n: number): string {
22
+ if (n < 1000) return n.toString();
23
+ if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
24
+ if (n < 1000000) return `${Math.round(n / 1000)}k`;
25
+ if (n < 10000000) return `${(n / 1000000).toFixed(1)}M`;
26
+ return `${Math.round(n / 1000000)}M`;
27
+ }
28
+
29
+ function formatDuration(ms: number): string {
30
+ const seconds = Math.floor(ms / 1000);
31
+ const minutes = Math.floor(seconds / 60);
32
+ const hours = Math.floor(minutes / 60);
33
+
34
+ if (hours > 0) return `${hours}h${minutes % 60}m`;
35
+ if (minutes > 0) return `${minutes}m${seconds % 60}s`;
36
+ return `${seconds}s`;
37
+ }
38
+
39
+ // ═══════════════════════════════════════════════════════════════════════════
40
+ // Segment Implementations
41
+ // ═══════════════════════════════════════════════════════════════════════════
42
+
43
+ const modelSegment: StatusLineSegment = {
44
+ id: "model",
45
+ render(ctx) {
46
+ const icons = getIcons();
47
+ const opts = ctx.options.model ?? {};
48
+
49
+ let modelName = ctx.model?.name || ctx.model?.id || "no-model";
50
+ // Strip "Claude " prefix for brevity
51
+ if (modelName.startsWith("Claude ")) {
52
+ modelName = modelName.slice(7);
53
+ }
54
+
55
+ let content = color(ctx, "model", withIcon(icons.model, modelName));
56
+
57
+ // Keep the model and thinking level together without an extra dot marker:
58
+ // `model [med]`. The thinking label uses pi's native level color.
59
+ if (opts.showThinkingLevel !== false) {
60
+ const level = ctx.thinkingLevel || "off";
61
+ if (level !== "off") {
62
+ const thinkingText = getThinkingText(level) ?? `[${level}]`;
63
+ content += ` ${colorThinking(ctx.theme, level, thinkingText, ctx.colors)}`;
64
+ }
65
+ }
66
+
67
+ return { content, visible: true };
68
+ },
69
+ };
70
+
71
+ const shellModeSegment: StatusLineSegment = {
72
+ id: "shell_mode",
73
+ render(ctx) {
74
+ if (!ctx.shellModeActive) {
75
+ return { content: "", visible: false };
76
+ }
77
+
78
+ const shellName = ctx.shellName ?? "shell";
79
+ const state = ctx.shellRunning ? "run" : "idle";
80
+ const cwd = ctx.shellCwd ? basename(ctx.shellCwd) : null;
81
+ const parts = [shellName, state];
82
+ if (cwd) {
83
+ parts.push(cwd);
84
+ }
85
+
86
+ return { content: color(ctx, "shellMode", parts.join(SEP_DOT)), visible: true };
87
+ },
88
+ };
89
+
90
+ const pathSegment: StatusLineSegment = {
91
+ id: "path",
92
+ render(ctx) {
93
+ const icons = getIcons();
94
+ const opts = ctx.options.path ?? {};
95
+ const mode = opts.mode ?? "basename";
96
+
97
+ let pwd = ctx.shellModeActive && ctx.shellCwd ? ctx.shellCwd : (ctx.cwd ?? process.cwd());
98
+ const home = process.env.HOME || process.env.USERPROFILE;
99
+
100
+ if (mode === "basename") {
101
+ // Just the last directory component (cross-platform)
102
+ pwd = basename(pwd) || pwd;
103
+ } else {
104
+ // Abbreviate home directory for abbreviated/full modes
105
+ if (home && pwd.startsWith(home)) {
106
+ pwd = `~${pwd.slice(home.length)}`;
107
+ }
108
+
109
+ // Strip /work/ prefix (common in containers)
110
+ if (pwd.startsWith("/work/")) {
111
+ pwd = pwd.slice(6);
112
+ }
113
+
114
+ // Truncate if too long (only for abbreviated mode)
115
+ if (mode === "abbreviated") {
116
+ const maxLen = opts.maxLength ?? 40;
117
+ if (pwd.length > maxLen) {
118
+ pwd = `…${pwd.slice(-(maxLen - 1))}`;
119
+ }
120
+ }
121
+ }
122
+
123
+ const content = withIcon(icons.folder, pwd);
124
+ return { content: color(ctx, "path", content), visible: true };
125
+ },
126
+ };
127
+
128
+ const gitSegment: StatusLineSegment = {
129
+ id: "git",
130
+ render(ctx) {
131
+ const icons = getIcons();
132
+ const opts = ctx.options.git ?? {};
133
+ const { branch, staged, unstaged, untracked } = ctx.git;
134
+ const gitStatus = (staged > 0 || unstaged > 0 || untracked > 0)
135
+ ? { staged, unstaged, untracked }
136
+ : null;
137
+
138
+ if (!branch && !gitStatus) return { content: "", visible: false };
139
+
140
+ const isDirty = gitStatus && (gitStatus.staged > 0 || gitStatus.unstaged > 0 || gitStatus.untracked > 0);
141
+ const showBranch = opts.showBranch !== false;
142
+ const branchColor: SemanticColor = isDirty ? "gitDirty" : "gitClean";
143
+
144
+ // Build content - color branch separately from indicators
145
+ let content = "";
146
+ if (showBranch && branch) {
147
+ // Color just the branch name (icon + branch text)
148
+ content = color(ctx, branchColor, withIcon(icons.branch, branch));
149
+ }
150
+
151
+ // Add status indicators (each with their own color, not wrapped)
152
+ if (gitStatus) {
153
+ const indicators: string[] = [];
154
+ if (opts.showUnstaged !== false && gitStatus.unstaged > 0) {
155
+ indicators.push(applyColor(ctx.theme, "warning", `*${gitStatus.unstaged}`));
156
+ }
157
+ if (opts.showStaged !== false && gitStatus.staged > 0) {
158
+ indicators.push(applyColor(ctx.theme, "success", `+${gitStatus.staged}`));
159
+ }
160
+ if (opts.showUntracked !== false && gitStatus.untracked > 0) {
161
+ indicators.push(applyColor(ctx.theme, "muted", `?${gitStatus.untracked}`));
162
+ }
163
+ if (indicators.length > 0) {
164
+ const indicatorText = indicators.join(" ");
165
+ if (!content && showBranch === false) {
166
+ // No branch shown, color the git icon with branch color
167
+ content = color(ctx, branchColor, icons.git ? `${icons.git} ` : "") + indicatorText;
168
+ } else {
169
+ content += content ? ` ${indicatorText}` : indicatorText;
170
+ }
171
+ }
172
+ }
173
+
174
+ if (!content) return { content: "", visible: false };
175
+
176
+ return { content, visible: true };
177
+ },
178
+ };
179
+
180
+ const thinkingSegment: StatusLineSegment = {
181
+ id: "thinking",
182
+ render(ctx) {
183
+ const level = ctx.thinkingLevel || "off";
184
+
185
+ const levelText: Record<string, string> = {
186
+ off: "off",
187
+ minimal: "min",
188
+ low: "low",
189
+ medium: "med",
190
+ high: "high",
191
+ xhigh: "xhigh",
192
+ max: "max",
193
+ };
194
+ const label = levelText[level] || level;
195
+ const content = `think:${label}`;
196
+
197
+ return {
198
+ content: colorThinking(ctx.theme, level, content, ctx.colors),
199
+ visible: true,
200
+ };
201
+ },
202
+ };
203
+
204
+ const subagentsSegment: StatusLineSegment = {
205
+ id: "subagents",
206
+ render() {
207
+ // Note: pi-mono doesn't have subagent tracking built-in
208
+ // This would require extension state management
209
+ // For now, return not visible
210
+ return { content: "", visible: false };
211
+ },
212
+ };
213
+
214
+ const tokenInSegment: StatusLineSegment = {
215
+ id: "token_in",
216
+ render(ctx) {
217
+ const icons = getIcons();
218
+ const { input } = ctx.usageStats;
219
+ if (!input) return { content: "", visible: false };
220
+
221
+ const content = withIcon(icons.input, formatTokens(input));
222
+ return { content: color(ctx, "tokens", content), visible: true };
223
+ },
224
+ };
225
+
226
+ const tokenOutSegment: StatusLineSegment = {
227
+ id: "token_out",
228
+ render(ctx) {
229
+ const icons = getIcons();
230
+ const { output } = ctx.usageStats;
231
+ if (!output) return { content: "", visible: false };
232
+
233
+ const content = withIcon(icons.output, formatTokens(output));
234
+ return { content: color(ctx, "tokens", content), visible: true };
235
+ },
236
+ };
237
+
238
+ const tokenTotalSegment: StatusLineSegment = {
239
+ id: "token_total",
240
+ render(ctx) {
241
+ const icons = getIcons();
242
+ const { input, output, cacheRead, cacheWrite } = ctx.usageStats;
243
+ const total = input + output + cacheRead + cacheWrite;
244
+ if (!total) return { content: "", visible: false };
245
+
246
+ const content = withIcon(icons.tokens, formatTokens(total));
247
+ return { content: color(ctx, "tokens", content), visible: true };
248
+ },
249
+ };
250
+
251
+ const costSegment: StatusLineSegment = {
252
+ id: "cost",
253
+ render(ctx) {
254
+ const { cost } = ctx.usageStats;
255
+ const usingSubscription = ctx.usingSubscription;
256
+
257
+ if (!cost && !usingSubscription) {
258
+ return { content: "", visible: false };
259
+ }
260
+
261
+ const costDisplay = usingSubscription ? "(sub)" : `$${cost.toFixed(2)}`;
262
+ return { content: color(ctx, "cost", costDisplay), visible: true };
263
+ },
264
+ };
265
+
266
+ const contextPctSegment: StatusLineSegment = {
267
+ id: "context_pct",
268
+ render(ctx) {
269
+ if (ctx.customCompactionEnabled) return { content: "", visible: false };
270
+
271
+ const icons = getIcons();
272
+ const pct = ctx.contextPercent;
273
+ const window = ctx.contextWindow;
274
+
275
+ const autoIcon = ctx.autoCompactEnabled && icons.auto ? ` ${icons.auto}` : "";
276
+ const text = `${pct === null ? "?" : pct.toFixed(1)}%/${formatTokens(window)}${autoIcon}`;
277
+
278
+ // A null percentage is pi's intentional post-compaction "unknown" state.
279
+ // Show it neutrally until the next model response provides fresh usage.
280
+ let content: string;
281
+ if (pct !== null && pct > 90) {
282
+ content = withIcon(icons.context, color(ctx, "contextError", text));
283
+ } else if (pct !== null && pct > 70) {
284
+ content = withIcon(icons.context, color(ctx, "contextWarn", text));
285
+ } else {
286
+ content = withIcon(icons.context, color(ctx, "context", text));
287
+ }
288
+
289
+ return { content, visible: true };
290
+ },
291
+ };
292
+
293
+ const contextTotalSegment: StatusLineSegment = {
294
+ id: "context_total",
295
+ render(ctx) {
296
+ if (ctx.customCompactionEnabled) return { content: "", visible: false };
297
+
298
+ const icons = getIcons();
299
+ const window = ctx.contextWindow;
300
+ if (!window) return { content: "", visible: false };
301
+
302
+ return {
303
+ content: color(ctx, "context", withIcon(icons.context, formatTokens(window))),
304
+ visible: true,
305
+ };
306
+ },
307
+ };
308
+
309
+ const timeSpentSegment: StatusLineSegment = {
310
+ id: "time_spent",
311
+ render(ctx) {
312
+ const icons = getIcons();
313
+ const elapsed = Date.now() - ctx.sessionStartTime;
314
+ if (elapsed < 1000) return { content: "", visible: false };
315
+
316
+ return { content: withIcon(icons.time, formatDuration(elapsed)), visible: true };
317
+ },
318
+ };
319
+
320
+ const timeSegment: StatusLineSegment = {
321
+ id: "time",
322
+ render(ctx) {
323
+ const icons = getIcons();
324
+ const opts = ctx.options.time ?? {};
325
+ const now = new Date();
326
+
327
+ let hours = now.getHours();
328
+ let suffix = "";
329
+ if (opts.format === "12h") {
330
+ suffix = hours >= 12 ? "pm" : "am";
331
+ hours = hours % 12 || 12;
332
+ }
333
+
334
+ const mins = now.getMinutes().toString().padStart(2, "0");
335
+ let timeStr = `${hours}:${mins}`;
336
+ if (opts.showSeconds) {
337
+ timeStr += `:${now.getSeconds().toString().padStart(2, "0")}`;
338
+ }
339
+ timeStr += suffix;
340
+
341
+ return { content: withIcon(icons.time, timeStr), visible: true };
342
+ },
343
+ };
344
+
345
+ const sessionSegment: StatusLineSegment = {
346
+ id: "session",
347
+ render(ctx) {
348
+ const icons = getIcons();
349
+ const sessionId = ctx.sessionId;
350
+ const display = sessionId?.slice(0, 8) || "new";
351
+
352
+ return { content: withIcon(icons.session, display), visible: true };
353
+ },
354
+ };
355
+
356
+ const hostnameSegment: StatusLineSegment = {
357
+ id: "hostname",
358
+ render() {
359
+ const icons = getIcons();
360
+ const name = osHostname().split(".")[0];
361
+ return { content: withIcon(icons.host, name), visible: true };
362
+ },
363
+ };
364
+
365
+ const cacheReadSegment: StatusLineSegment = {
366
+ id: "cache_read",
367
+ render(ctx) {
368
+ const icons = getIcons();
369
+ const { cacheRead } = ctx.usageStats;
370
+ if (!cacheRead) return { content: "", visible: false };
371
+
372
+ const parts = [icons.cache, icons.input, formatTokens(cacheRead)].filter(Boolean);
373
+ const content = parts.join(" ");
374
+ return { content: color(ctx, "tokens", content), visible: true };
375
+ },
376
+ };
377
+
378
+ const cacheWriteSegment: StatusLineSegment = {
379
+ id: "cache_write",
380
+ render(ctx) {
381
+ const icons = getIcons();
382
+ const { cacheWrite } = ctx.usageStats;
383
+ if (!cacheWrite) return { content: "", visible: false };
384
+
385
+ const parts = [icons.cache, icons.output, formatTokens(cacheWrite)].filter(Boolean);
386
+ const content = parts.join(" ");
387
+ return { content: color(ctx, "tokens", content), visible: true };
388
+ },
389
+ };
390
+
391
+ const extensionStatusesSegment: StatusLineSegment = {
392
+ id: "extension_statuses",
393
+ render(ctx) {
394
+ const statuses = ctx.extensionStatuses;
395
+ if (!statuses || statuses.size === 0) return { content: "", visible: false };
396
+
397
+ // Join compact statuses with a separator
398
+ // Skip: empty strings, notification-style ("[...") shown above editor,
399
+ // and strings that are only ANSI codes with no visible text.
400
+ // Also skip statuses explicitly elevated into dedicated custom segments.
401
+ const parts: string[] = [];
402
+ for (const [statusKey, value] of statuses.entries()) {
403
+ if (ctx.hiddenExtensionStatusKeys.has(statusKey)) continue;
404
+ const normalized = value ? normalizeCompactExtensionStatus(value) : null;
405
+ if (normalized) {
406
+ parts.push(normalized);
407
+ }
408
+ }
409
+
410
+ if (parts.length === 0) return { content: "", visible: false };
411
+
412
+ // Statuses already have their own styling applied by the extensions
413
+ const content = parts.join(` ${SEP_DOT} `);
414
+ return { content, visible: true };
415
+ },
416
+ };
417
+
418
+ // ═══════════════════════════════════════════════════════════════════════════
419
+ // Segment Registry
420
+ // ═══════════════════════════════════════════════════════════════════════════
421
+
422
+ export const SEGMENTS: Record<BuiltinStatusLineSegmentId, StatusLineSegment> = {
423
+ model: modelSegment,
424
+ shell_mode: shellModeSegment,
425
+ path: pathSegment,
426
+ git: gitSegment,
427
+ thinking: thinkingSegment,
428
+ subagents: subagentsSegment,
429
+ token_in: tokenInSegment,
430
+ token_out: tokenOutSegment,
431
+ token_total: tokenTotalSegment,
432
+ cost: costSegment,
433
+ context_pct: contextPctSegment,
434
+ context_total: contextTotalSegment,
435
+ time_spent: timeSpentSegment,
436
+ time: timeSegment,
437
+ session: sessionSegment,
438
+ hostname: hostnameSegment,
439
+ cache_read: cacheReadSegment,
440
+ cache_write: cacheWriteSegment,
441
+ extension_statuses: extensionStatusesSegment,
442
+ };
443
+
444
+ function renderCustomSegment(id: `custom:${string}`, ctx: SegmentContext): RenderedSegment {
445
+ const customItemId = id.slice("custom:".length);
446
+ const custom = ctx.customItemsById.get(customItemId);
447
+ if (!custom) return { content: "", visible: false };
448
+
449
+ const rawStatus = ctx.extensionStatuses.get(custom.statusKey);
450
+ const normalizedStatus = rawStatus ? normalizeExtensionStatusValue(rawStatus) : null;
451
+ if (!normalizedStatus) {
452
+ return custom.hideWhenMissing ? { content: "", visible: false } : { content: custom.prefix ?? custom.id, visible: true };
453
+ }
454
+
455
+ let content = normalizedStatus;
456
+ if (custom.prefix) {
457
+ content = `${custom.prefix}${SEP_DOT}${content}`;
458
+ }
459
+ if (custom.color) {
460
+ content = applyColor(ctx.theme, custom.color, content);
461
+ }
462
+
463
+ return { content, visible: true };
464
+ }
465
+
466
+ export function renderSegment(id: StatusLineSegmentId, ctx: SegmentContext): RenderedSegment {
467
+ if (id.startsWith("custom:")) {
468
+ return renderCustomSegment(id, ctx);
469
+ }
470
+
471
+ const segment = SEGMENTS[id];
472
+ if (!segment) {
473
+ return { content: "", visible: false };
474
+ }
475
+ return segment.render(ctx);
476
+ }
@@ -0,0 +1,41 @@
1
+ import type { SeparatorDef, StatusLineSeparatorStyle } from "./types.ts";
2
+ import { getSeparatorChars } from "./icons.ts";
3
+
4
+ export function getSeparator(style: StatusLineSeparatorStyle): SeparatorDef {
5
+ const chars = getSeparatorChars();
6
+
7
+ switch (style) {
8
+ // Keep the preset names for compatibility, but use plain pipe separators
9
+ // so this bundled footer never emits Nerd Font arrow glyphs.
10
+ case "powerline":
11
+ case "powerline-thin":
12
+ return { left: ` ${chars.pipe} `, right: ` ${chars.pipe} ` };
13
+
14
+ case "slash":
15
+ return { left: ` ${chars.slash} `, right: ` ${chars.slash} ` };
16
+
17
+ case "pipe":
18
+ return { left: ` ${chars.pipe} `, right: ` ${chars.pipe} ` };
19
+
20
+ case "block":
21
+ return { left: chars.block, right: chars.block };
22
+
23
+ case "none":
24
+ return { left: chars.space, right: chars.space };
25
+
26
+ case "ascii":
27
+ return { left: chars.asciiLeft, right: chars.asciiRight };
28
+
29
+ case "dot":
30
+ return { left: chars.dot, right: chars.dot };
31
+
32
+ case "chevron":
33
+ return { left: "›", right: "‹" };
34
+
35
+ case "star":
36
+ return { left: "✦", right: "✦" };
37
+
38
+ default:
39
+ return getSeparator("powerline-thin");
40
+ }
41
+ }