@pi-unipi/footer 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.
package/src/events.ts ADDED
@@ -0,0 +1,256 @@
1
+ /**
2
+ * @pi-unipi/footer — Event subscription wiring
3
+ *
4
+ * Wires FooterRegistry to UNIPI_EVENTS for all relevant events.
5
+ * Each event handler updates the registry cache for the appropriate group.
6
+ *
7
+ * Note: pi.events.on() returns an unsubscribe function directly.
8
+ */
9
+
10
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
+ import { UNIPI_EVENTS } from "@pi-unipi/core";
12
+ import type { FooterRegistry } from "./registry/index.js";
13
+
14
+ /** Cleanup function returned by subscribeToEvents */
15
+ type UnsubscribeFn = () => void;
16
+
17
+ /**
18
+ * Subscribe to all relevant UNIPI_EVENTS and wire them to the registry.
19
+ * Returns an unsubscribe function for cleanup on session shutdown.
20
+ */
21
+ export function subscribeToEvents(
22
+ pi: ExtensionAPI,
23
+ registry: FooterRegistry,
24
+ ): UnsubscribeFn {
25
+ const unsubscribers: UnsubscribeFn[] = [];
26
+
27
+ // ─── Compactor events ───────────────────────────────────────────────────
28
+
29
+ unsubscribers.push(
30
+ pi.events.on(UNIPI_EVENTS.COMPACTOR_STATS_UPDATED, (event: unknown) => {
31
+ try {
32
+ registry.updateData("compactor", event);
33
+ } catch (err) {
34
+ console.error("[footer] Compactor stats handler error:", err);
35
+ }
36
+ })
37
+ );
38
+
39
+ unsubscribers.push(
40
+ pi.events.on(UNIPI_EVENTS.COMPACTOR_COMPACTED, (event: unknown) => {
41
+ try {
42
+ const existing = registry.getGroupData("compactor") as Record<string, unknown> | undefined;
43
+ registry.updateData("compactor", { ...existing, lastCompaction: event });
44
+ } catch (err) {
45
+ console.error("[footer] Compaction handler error:", err);
46
+ }
47
+ })
48
+ );
49
+
50
+ // ─── Memory events ─────────────────────────────────────────────────────
51
+
52
+ unsubscribers.push(
53
+ pi.events.on(UNIPI_EVENTS.MEMORY_STORED, (event: unknown) => {
54
+ try {
55
+ const existing = registry.getGroupData("memory") as Record<string, unknown> | undefined;
56
+ registry.updateData("memory", { ...existing, lastStored: event });
57
+ } catch (err) {
58
+ console.error("[footer] Memory stored handler error:", err);
59
+ }
60
+ })
61
+ );
62
+
63
+ unsubscribers.push(
64
+ pi.events.on(UNIPI_EVENTS.MEMORY_DELETED, (event: unknown) => {
65
+ try {
66
+ const existing = registry.getGroupData("memory") as Record<string, unknown> | undefined;
67
+ registry.updateData("memory", { ...existing, lastDeleted: event });
68
+ } catch (err) {
69
+ console.error("[footer] Memory deleted handler error:", err);
70
+ }
71
+ })
72
+ );
73
+
74
+ unsubscribers.push(
75
+ pi.events.on(UNIPI_EVENTS.MEMORY_CONSOLIDATED, (event: unknown) => {
76
+ try {
77
+ const existing = registry.getGroupData("memory") as Record<string, unknown> | undefined;
78
+ registry.updateData("memory", { ...existing, lastConsolidated: event });
79
+ } catch (err) {
80
+ console.error("[footer] Memory consolidated handler error:", err);
81
+ }
82
+ })
83
+ );
84
+
85
+ // ─── MCP events ────────────────────────────────────────────────────────
86
+
87
+ unsubscribers.push(
88
+ pi.events.on(UNIPI_EVENTS.MCP_SERVER_STARTED, (event: unknown) => {
89
+ try {
90
+ const existing = registry.getGroupData("mcp") as Record<string, unknown> | undefined;
91
+ const evt = event as Record<string, unknown> | undefined;
92
+ const toolCount = typeof evt?.toolCount === "number" ? evt.toolCount : 0;
93
+ const serversTotal = (typeof existing?.serversTotal === "number" ? existing.serversTotal : 0) + 1;
94
+ const serversActive = (typeof existing?.serversActive === "number" ? existing.serversActive : 0) + 1;
95
+ const toolsTotal = (typeof existing?.toolsTotal === "number" ? existing.toolsTotal : 0) + toolCount;
96
+ registry.updateData("mcp", { ...existing, serversTotal, serversActive, toolsTotal, lastServerStarted: event });
97
+ } catch (err) {
98
+ console.error("[footer] MCP server started handler error:", err);
99
+ }
100
+ })
101
+ );
102
+
103
+ unsubscribers.push(
104
+ pi.events.on(UNIPI_EVENTS.MCP_SERVER_STOPPED, (event: unknown) => {
105
+ try {
106
+ const existing = registry.getGroupData("mcp") as Record<string, unknown> | undefined;
107
+ const evt = event as Record<string, unknown> | undefined;
108
+ const stoppedName = typeof evt?.name === "string" ? evt.name : "";
109
+ const serversActive = Math.max(0, (typeof existing?.serversActive === "number" ? existing.serversActive : 1) - 1);
110
+ // Subtract tools for this server if tracked
111
+ const lastStartedTools = existing?.lastServerStarted as Record<string, unknown> | undefined;
112
+ const lastStartedName = typeof lastStartedTools?.name === "string" ? lastStartedTools.name : "";
113
+ const lastStartedCount = typeof lastStartedTools?.toolCount === "number" ? lastStartedTools.toolCount : 0;
114
+ let toolsTotal = typeof existing?.toolsTotal === "number" ? existing.toolsTotal : 0;
115
+ if (stoppedName && stoppedName === lastStartedName) {
116
+ toolsTotal = Math.max(0, toolsTotal - lastStartedCount);
117
+ }
118
+ registry.updateData("mcp", { ...existing, serversActive, toolsTotal, lastServerStopped: event });
119
+ } catch (err) {
120
+ console.error("[footer] MCP server stopped handler error:", err);
121
+ }
122
+ })
123
+ );
124
+
125
+ unsubscribers.push(
126
+ pi.events.on(UNIPI_EVENTS.MCP_SERVER_ERROR, (event: unknown) => {
127
+ try {
128
+ const existing = registry.getGroupData("mcp") as Record<string, unknown> | undefined;
129
+ const serversTotal = (typeof existing?.serversTotal === "number" ? existing.serversTotal : 0) + 1;
130
+ const serversFailed = (typeof existing?.serversFailed === "number" ? existing.serversFailed : 0) + 1;
131
+ registry.updateData("mcp", { ...existing, serversTotal, serversFailed, lastServerError: event });
132
+ } catch (err) {
133
+ console.error("[footer] MCP server error handler error:", err);
134
+ }
135
+ })
136
+ );
137
+
138
+ unsubscribers.push(
139
+ pi.events.on(UNIPI_EVENTS.MCP_TOOLS_REGISTERED, (event: unknown) => {
140
+ try {
141
+ const existing = registry.getGroupData("mcp") as Record<string, unknown> | undefined;
142
+ const evt = event as Record<string, unknown> | undefined;
143
+ const toolNames = Array.isArray(evt?.toolNames) ? evt.toolNames : [];
144
+ const toolsTotal = (typeof existing?.toolsTotal === "number" ? existing.toolsTotal : 0) + toolNames.length;
145
+ registry.updateData("mcp", { ...existing, toolsTotal, lastToolsRegistered: event });
146
+ } catch (err) {
147
+ console.error("[footer] MCP tools registered handler error:", err);
148
+ }
149
+ })
150
+ );
151
+
152
+ unsubscribers.push(
153
+ pi.events.on(UNIPI_EVENTS.MCP_TOOLS_UNREGISTERED, (event: unknown) => {
154
+ try {
155
+ const existing = registry.getGroupData("mcp") as Record<string, unknown> | undefined;
156
+ const evt = event as Record<string, unknown> | undefined;
157
+ const toolNames = Array.isArray(evt?.toolNames) ? evt.toolNames : [];
158
+ const toolsTotal = Math.max(0, (typeof existing?.toolsTotal === "number" ? existing.toolsTotal : 0) - toolNames.length);
159
+ registry.updateData("mcp", { ...existing, toolsTotal, lastToolsUnregistered: event });
160
+ } catch (err) {
161
+ console.error("[footer] MCP tools unregistered handler error:", err);
162
+ }
163
+ })
164
+ );
165
+
166
+ // ─── Ralph events ──────────────────────────────────────────────────────
167
+
168
+ unsubscribers.push(
169
+ pi.events.on(UNIPI_EVENTS.RALPH_LOOP_START, (event: unknown) => {
170
+ try {
171
+ registry.updateData("ralph", { ...(event as Record<string, unknown>), active: true });
172
+ } catch (err) {
173
+ console.error("[footer] Ralph loop start handler error:", err);
174
+ }
175
+ })
176
+ );
177
+
178
+ unsubscribers.push(
179
+ pi.events.on(UNIPI_EVENTS.RALPH_LOOP_END, (event: unknown) => {
180
+ try {
181
+ registry.updateData("ralph", { ...(event as Record<string, unknown>), active: false });
182
+ } catch (err) {
183
+ console.error("[footer] Ralph loop end handler error:", err);
184
+ }
185
+ })
186
+ );
187
+
188
+ unsubscribers.push(
189
+ pi.events.on(UNIPI_EVENTS.RALPH_ITERATION_DONE, (event: unknown) => {
190
+ try {
191
+ const existing = registry.getGroupData("ralph") as Record<string, unknown> | undefined;
192
+ registry.updateData("ralph", { ...existing, lastIteration: event });
193
+ } catch (err) {
194
+ console.error("[footer] Ralph iteration handler error:", err);
195
+ }
196
+ })
197
+ );
198
+
199
+ // ─── Workflow events ───────────────────────────────────────────────────
200
+
201
+ unsubscribers.push(
202
+ pi.events.on(UNIPI_EVENTS.WORKFLOW_START, (event: unknown) => {
203
+ try {
204
+ registry.updateData("workflow", { ...(event as Record<string, unknown>), active: true, startTime: Date.now() });
205
+ } catch (err) {
206
+ console.error("[footer] Workflow start handler error:", err);
207
+ }
208
+ })
209
+ );
210
+
211
+ unsubscribers.push(
212
+ pi.events.on(UNIPI_EVENTS.WORKFLOW_END, (event: unknown) => {
213
+ try {
214
+ registry.updateData("workflow", { ...(event as Record<string, unknown>), active: false });
215
+ } catch (err) {
216
+ console.error("[footer] Workflow end handler error:", err);
217
+ }
218
+ })
219
+ );
220
+
221
+ // ─── Notification events ───────────────────────────────────────────────
222
+
223
+ unsubscribers.push(
224
+ pi.events.on(UNIPI_EVENTS.NOTIFICATION_SENT, (event: unknown) => {
225
+ try {
226
+ registry.updateData("notify", event);
227
+ } catch (err) {
228
+ console.error("[footer] Notification handler error:", err);
229
+ }
230
+ })
231
+ );
232
+
233
+ // ─── Module ready events ───────────────────────────────────────────────
234
+
235
+ unsubscribers.push(
236
+ pi.events.on(UNIPI_EVENTS.MODULE_READY, (_event: unknown) => {
237
+ try {
238
+ // Invalidate all caches when new modules load — they may bring fresh data
239
+ registry.invalidateAll();
240
+ } catch (err) {
241
+ console.error("[footer] Module ready handler error:", err);
242
+ }
243
+ })
244
+ );
245
+
246
+ // Return composite unsubscribe function
247
+ return () => {
248
+ for (const unsub of unsubscribers) {
249
+ try {
250
+ unsub();
251
+ } catch {
252
+ // Ignore cleanup errors
253
+ }
254
+ }
255
+ };
256
+ }
package/src/index.ts ADDED
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @pi-unipi/footer — Extension entry point
3
+ *
4
+ * Main extension function that registers commands, subscribes to events,
5
+ * initializes renderer on session_start.
6
+ */
7
+
8
+ import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
9
+ import { UNIPI_EVENTS, emitEvent, UNIPI_PREFIX, FOOTER_COMMANDS } from "@pi-unipi/core";
10
+ import { FooterRegistry, getFooterRegistry } from "./registry/index.js";
11
+ import { FooterRenderer } from "./rendering/renderer.js";
12
+ import { subscribeToEvents } from "./events.js";
13
+ import { loadFooterSettings, saveFooterSettings } from "./config.js";
14
+ import { getPreset } from "./presets.js";
15
+ import { registerCommands } from "./commands.js";
16
+
17
+ // Import segment groups
18
+ import { CORE_SEGMENTS } from "./segments/core.js";
19
+ import { COMPACTOR_SEGMENTS } from "./segments/compactor.js";
20
+ import { MEMORY_SEGMENTS } from "./segments/memory.js";
21
+ import { MCP_SEGMENTS } from "./segments/mcp.js";
22
+ import { RALPH_SEGMENTS } from "./segments/ralph.js";
23
+ import { WORKFLOW_SEGMENTS } from "./segments/workflow.js";
24
+ import { KANBOARD_SEGMENTS } from "./segments/kanboard.js";
25
+ import { NOTIFY_SEGMENTS } from "./segments/notify.js";
26
+ import { STATUS_EXT_SEGMENTS } from "./segments/status-ext.js";
27
+
28
+ import type { FooterGroup, FooterSegment } from "./types.js";
29
+ import { getThinkingLevel, rainbowBorder } from "./segments/core.js";
30
+
31
+ /** All segment groups */
32
+ const ALL_GROUPS: FooterGroup[] = [
33
+ { id: "core", name: "Core", icon: "", segments: CORE_SEGMENTS, defaultShow: true },
34
+ { id: "compactor", name: "Compactor", icon: "", segments: COMPACTOR_SEGMENTS, defaultShow: true },
35
+ { id: "memory", name: "Memory", icon: "", segments: MEMORY_SEGMENTS, defaultShow: true },
36
+ { id: "mcp", name: "MCP", icon: "", segments: MCP_SEGMENTS, defaultShow: true },
37
+ { id: "ralph", name: "Ralph", icon: "", segments: RALPH_SEGMENTS, defaultShow: true },
38
+ { id: "workflow", name: "Workflow", icon: "", segments: WORKFLOW_SEGMENTS, defaultShow: true },
39
+ { id: "kanboard", name: "Kanboard", icon: "", segments: KANBOARD_SEGMENTS, defaultShow: true },
40
+ { id: "notify", name: "Notify", icon: "", segments: NOTIFY_SEGMENTS, defaultShow: false },
41
+ { id: "status_ext", name: "Extensions", icon: "", segments: STATUS_EXT_SEGMENTS, defaultShow: true },
42
+ ];
43
+
44
+ /** Build a segment lookup from all groups */
45
+ function buildSegmentLookup(): Map<string, FooterSegment> {
46
+ const map = new Map<string, FooterSegment>();
47
+ for (const group of ALL_GROUPS) {
48
+ for (const segment of group.segments) {
49
+ map.set(segment.id, segment);
50
+ }
51
+ }
52
+ return map;
53
+ }
54
+
55
+ /** Extension state */
56
+ interface FooterState {
57
+ enabled: boolean;
58
+ registry: FooterRegistry;
59
+ renderer: FooterRenderer;
60
+ segmentLookup: Map<string, FooterSegment>;
61
+ unsubscribeEvents: (() => void) | null;
62
+ piContext: unknown;
63
+ footerData: unknown;
64
+ tuiRef: any;
65
+ }
66
+
67
+ export default function footerExtension(pi: ExtensionAPI): void {
68
+ // Build segment lookup
69
+ const segmentLookup = buildSegmentLookup();
70
+
71
+ // Create state
72
+ const state: FooterState = {
73
+ enabled: true,
74
+ registry: getFooterRegistry(),
75
+ renderer: new FooterRenderer(
76
+ getFooterRegistry(),
77
+ { get: (id: string) => segmentLookup.get(id) },
78
+ loadFooterSettings().preset,
79
+ ),
80
+ segmentLookup,
81
+ unsubscribeEvents: null,
82
+ piContext: null,
83
+ footerData: null,
84
+ tuiRef: null,
85
+ };
86
+
87
+ // Register all groups in the registry
88
+ for (const group of ALL_GROUPS) {
89
+ state.registry.registerGroup(group);
90
+ }
91
+
92
+ // ─── Session lifecycle ──────────────────────────────────────────────────
93
+
94
+ pi.on("session_start", async (_event, ctx) => {
95
+ const settings = loadFooterSettings();
96
+ state.enabled = settings.enabled;
97
+ state.piContext = ctx;
98
+ state.renderer.setPreset(settings.preset);
99
+ state.renderer.setActive(settings.enabled);
100
+
101
+ if (!settings.enabled || !ctx.hasUI) return;
102
+
103
+ // Subscribe to events
104
+ state.unsubscribeEvents = subscribeToEvents(pi, state.registry);
105
+
106
+ // Setup footer + widgets
107
+ setupFooterUI(pi, ctx, state);
108
+ });
109
+
110
+ pi.on("session_shutdown", async () => {
111
+ state.renderer.setActive(false);
112
+ state.unsubscribeEvents?.();
113
+ state.unsubscribeEvents = null;
114
+ state.piContext = null;
115
+ state.footerData = null;
116
+ state.tuiRef = null;
117
+ });
118
+
119
+ // ─── Register commands ──────────────────────────────────────────────────
120
+
121
+ registerCommands(pi, state, ALL_GROUPS);
122
+
123
+ // ─── Emit MODULE_READY ──────────────────────────────────────────────────
124
+
125
+ pi.on("session_start", async () => {
126
+ emitEvent(pi as any, UNIPI_EVENTS.MODULE_READY, {
127
+ name: "@pi-unipi/footer",
128
+ version: "0.1.0",
129
+ commands: [`${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER}`, `${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER_SETTINGS}`],
130
+ tools: [],
131
+ });
132
+ });
133
+ }
134
+
135
+ // ─── Footer UI setup ────────────────────────────────────────────────────────
136
+
137
+ function setupFooterUI(pi: ExtensionAPI, ctx: any, state: FooterState): void {
138
+ // Register footer (minimal — handles branch changes)
139
+ ctx.ui.setFooter((tui: any, _theme: Theme, footerData: any) => {
140
+ state.tuiRef = tui;
141
+ state.footerData = footerData;
142
+ state.renderer.setContext(state.piContext, footerData);
143
+
144
+ const unsub = footerData.onBranchChange(() => {
145
+ state.renderer.resetLayoutCache();
146
+ });
147
+
148
+ return {
149
+ dispose: unsub,
150
+ invalidate() {
151
+ state.renderer.resetLayoutCache();
152
+ },
153
+ render(): string[] {
154
+ return [];
155
+ },
156
+ };
157
+ });
158
+
159
+ // Top row widget
160
+ ctx.ui.setWidget("footer-top", (_tui: any, theme: Theme) => {
161
+ // Update the renderer's theme-like
162
+ const themeLike = { fg: (color: string, text: string) => theme.fg(color as any, text) };
163
+ // We need to patch the context with proper theme
164
+ state.renderer.setContext(state.piContext, state.footerData);
165
+
166
+ return {
167
+ dispose() {},
168
+ invalidate() {
169
+ state.renderer.resetLayoutCache();
170
+ },
171
+ render(width: number): string[] {
172
+ if (!state.enabled || !state.piContext) return [];
173
+
174
+ // Build layout with proper theme by creating segment contexts
175
+ const layout = state.renderer.computeLayout(width);
176
+ return layout.topContent ? [layout.topContent] : [];
177
+ },
178
+ };
179
+ }, { placement: "aboveEditor" });
180
+
181
+ // Secondary row widget + rainbow input border for xhigh thinking
182
+ ctx.ui.setWidget("footer-secondary", (_tui: any, _theme: Theme) => {
183
+ return {
184
+ dispose() {},
185
+ invalidate() {
186
+ state.renderer.resetLayoutCache();
187
+ },
188
+ render(width: number): string[] {
189
+ if (!state.enabled || !state.piContext) return [];
190
+
191
+ const lines: string[] = [];
192
+
193
+ // Rainbow border for input bar when thinking level is xhigh
194
+ const thinkingLevel = getThinkingLevel(state.piContext);
195
+ if (thinkingLevel === "xhigh") {
196
+ lines.push(rainbowBorder(width));
197
+ }
198
+
199
+ const layout = state.renderer.computeLayout(width);
200
+ if (layout.secondaryContent) {
201
+ lines.push(layout.secondaryContent);
202
+ }
203
+
204
+ return lines;
205
+ },
206
+ };
207
+ }, { placement: "belowEditor" });
208
+ }
package/src/presets.ts ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * @pi-unipi/footer — Presets system
3
+ *
4
+ * Preset definitions: default, minimal, compact, full, nerd, ascii.
5
+ * Each preset defines which segments appear on left, right, and secondary rows,
6
+ * plus separator style and color scheme.
7
+ */
8
+
9
+ import type { PresetDef, SeparatorStyle, ColorScheme } from "./types.js";
10
+ import { getDefaultColors } from "./rendering/theme.js";
11
+
12
+ /** Default preset — balanced view */
13
+ const DEFAULT_PRESET: PresetDef = {
14
+ leftSegments: [
15
+ "model", "thinking", "path", "git", "context_pct", "cost",
16
+ ],
17
+ rightSegments: [
18
+ "compactions", "tokens_saved", "project_count", "loop_status",
19
+ ],
20
+ secondarySegments: [
21
+ "current_command", "session_events",
22
+ ],
23
+ separator: "powerline-thin",
24
+ colors: getDefaultColors(),
25
+ };
26
+
27
+ /** Minimal preset — just the essentials */
28
+ const MINIMAL_PRESET: PresetDef = {
29
+ leftSegments: [
30
+ "path", "git", "context_pct",
31
+ ],
32
+ rightSegments: [],
33
+ secondarySegments: [],
34
+ separator: "pipe",
35
+ colors: getDefaultColors(),
36
+ };
37
+
38
+ /** Compact preset — core + key stats */
39
+ const COMPACT_PRESET: PresetDef = {
40
+ leftSegments: [
41
+ "model", "git", "cost", "context_pct",
42
+ ],
43
+ rightSegments: [
44
+ "compactions", "total_count",
45
+ ],
46
+ secondarySegments: [],
47
+ separator: "dot",
48
+ colors: getDefaultColors(),
49
+ };
50
+
51
+ /** Full preset — everything */
52
+ const FULL_PRESET: PresetDef = {
53
+ leftSegments: [
54
+ "model", "thinking", "path", "git", "context_pct", "cost",
55
+ "tokens_total", "tokens_in", "tokens_out",
56
+ ],
57
+ rightSegments: [
58
+ "session_events", "compactions", "tokens_saved", "compression_ratio",
59
+ "indexed_docs", "sandbox_runs", "search_queries",
60
+ "project_count", "total_count", "consolidations",
61
+ "servers_total", "servers_active", "tools_total", "servers_failed",
62
+ "active_loops", "total_iterations", "loop_status",
63
+ "current_command", "command_duration",
64
+ "docs_count", "tasks_done", "tasks_total", "task_pct",
65
+ "extension_statuses",
66
+ ],
67
+ secondarySegments: [
68
+ "hostname", "time",
69
+ "platforms_enabled", "last_sent",
70
+ ],
71
+ separator: "powerline-thin",
72
+ colors: getDefaultColors(),
73
+ };
74
+
75
+ /** Nerd preset — maximum detail for Nerd Font users */
76
+ const NERD_PRESET: PresetDef = {
77
+ leftSegments: [
78
+ "model", "thinking", "path", "git", "context_pct", "cost",
79
+ "tokens_total",
80
+ ],
81
+ rightSegments: [
82
+ "session_events", "compactions", "tokens_saved",
83
+ "project_count", "total_count",
84
+ "servers_total", "servers_active", "tools_total",
85
+ "active_loops", "loop_status",
86
+ "current_command",
87
+ "docs_count", "tasks_done", "tasks_total", "task_pct",
88
+ "extension_statuses",
89
+ ],
90
+ secondarySegments: [
91
+ "hostname", "time",
92
+ "compression_ratio", "indexed_docs",
93
+ "platforms_enabled", "last_sent",
94
+ ],
95
+ separator: "powerline",
96
+ colors: getDefaultColors(),
97
+ };
98
+
99
+ /** ASCII preset — safe for any terminal */
100
+ const ASCII_PRESET: PresetDef = {
101
+ leftSegments: [
102
+ "model", "path", "git", "context_pct", "cost",
103
+ ],
104
+ rightSegments: [
105
+ "compactions", "tokens_saved", "project_count",
106
+ ],
107
+ secondarySegments: [],
108
+ separator: "ascii",
109
+ colors: getDefaultColors(),
110
+ };
111
+
112
+ /** All preset definitions */
113
+ export const PRESETS: Record<string, PresetDef> = {
114
+ default: DEFAULT_PRESET,
115
+ minimal: MINIMAL_PRESET,
116
+ compact: COMPACT_PRESET,
117
+ full: FULL_PRESET,
118
+ nerd: NERD_PRESET,
119
+ ascii: ASCII_PRESET,
120
+ };
121
+
122
+ /** Valid preset names */
123
+ export const PRESET_NAMES = Object.keys(PRESETS);
124
+
125
+ /**
126
+ * Get a preset definition by name.
127
+ * Falls back to "default" if the name is not recognized.
128
+ */
129
+ export function getPreset(name: string): PresetDef {
130
+ return PRESETS[name] ?? PRESETS.default ?? DEFAULT_PRESET;
131
+ }