@herbertgao/pi-extensions 2026.8.3 → 2026.8.4

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 (122) hide show
  1. package/README.md +41 -0
  2. package/examples/pi-footer.json +275 -0
  3. package/node_modules/pi-footer/CHANGELOG.md +145 -0
  4. package/node_modules/pi-footer/LICENSE +21 -0
  5. package/node_modules/pi-footer/README.md +354 -0
  6. package/node_modules/pi-footer/package.json +68 -0
  7. package/node_modules/pi-footer/src/cache.ts +106 -0
  8. package/node_modules/pi-footer/src/colors.ts +237 -0
  9. package/node_modules/pi-footer/src/config.ts +202 -0
  10. package/node_modules/pi-footer/src/event-widgets.ts +41 -0
  11. package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
  12. package/node_modules/pi-footer/src/git.ts +135 -0
  13. package/node_modules/pi-footer/src/index.ts +268 -0
  14. package/node_modules/pi-footer/src/metrics.ts +141 -0
  15. package/node_modules/pi-footer/src/presets.ts +383 -0
  16. package/node_modules/pi-footer/src/render.ts +114 -0
  17. package/node_modules/pi-footer/src/separators.ts +52 -0
  18. package/node_modules/pi-footer/src/types.ts +122 -0
  19. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
  20. package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
  21. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
  22. package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
  23. package/node_modules/pi-footer/src/ui/events.ts +35 -0
  24. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
  25. package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
  26. package/node_modules/pi-footer/src/ui/fields.ts +252 -0
  27. package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
  28. package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
  29. package/node_modules/pi-footer/src/ui/layout.ts +17 -0
  30. package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
  31. package/node_modules/pi-footer/src/ui/model.ts +64 -0
  32. package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
  33. package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
  34. package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
  35. package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
  36. package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
  37. package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
  38. package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
  39. package/node_modules/pi-footer/src/ui/screen.ts +243 -0
  40. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
  41. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
  42. package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
  43. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
  44. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
  45. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
  46. package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
  47. package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
  48. package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
  49. package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
  50. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
  51. package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
  52. package/node_modules/pi-footer/src/ui/theme.ts +48 -0
  53. package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
  54. package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
  55. package/node_modules/pi-footer/src/ui.ts +66 -0
  56. package/node_modules/pi-footer/src/widgets/context.ts +28 -0
  57. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
  58. package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
  59. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
  60. package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
  61. package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
  62. package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
  63. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
  64. package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
  65. package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
  66. package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
  67. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
  68. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
  69. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
  70. package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
  71. package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
  72. package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
  73. package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
  74. package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
  75. package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
  76. package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
  77. package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
  78. package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
  79. package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
  80. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
  81. package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
  82. package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
  83. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
  84. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
  85. package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
  86. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
  87. package/node_modules/pi-footer/src/widgets/options.ts +122 -0
  88. package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
  89. package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
  90. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
  91. package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
  92. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
  93. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
  94. package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
  95. package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
  96. package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
  97. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
  98. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
  99. package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
  100. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
  101. package/node_modules/pi-footer/src/widgets/store.ts +26 -0
  102. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
  103. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
  104. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
  105. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
  106. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
  107. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
  108. package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
  109. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
  110. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
  111. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
  112. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
  113. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
  114. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
  115. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
  116. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
  117. package/node_modules/pi-footer/src/widgets/types.ts +181 -0
  118. package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
  119. package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
  120. package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
  121. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
  122. package/package.json +7 -2
@@ -0,0 +1,383 @@
1
+ import type { ColorName } from "./colors.js";
2
+ import type { SeparatorStyle, WidgetSeparatorStyle } from "./separators.js";
3
+ import type { IconMode, TerminalOptions, TerminalWidthMode, WidgetOptions } from "./types.js";
4
+ import type { WidgetType } from "./widgets/registry.js";
5
+
6
+ export interface PresetWidget {
7
+ type: WidgetType;
8
+ options?: WidgetOptions;
9
+ }
10
+
11
+ export interface PresetDefinition {
12
+ separator?: SeparatorStyle;
13
+ iconMode?: IconMode;
14
+ terminal?: Partial<TerminalOptions>;
15
+ lines: PresetWidget[][];
16
+ }
17
+
18
+ interface PowerlineSegment extends PresetWidget {
19
+ options: WidgetOptions & { fg: ColorName; bg: ColorName };
20
+ }
21
+
22
+ interface PowerlineTransitionContext {
23
+ left: PowerlineSegment;
24
+ right: PowerlineSegment;
25
+ index: number;
26
+ }
27
+
28
+ interface PowerlineCapOptions {
29
+ separator?: WidgetSeparatorStyle;
30
+ fg?: ColorName;
31
+ bg?: ColorName;
32
+ }
33
+
34
+ interface PowerlineLineOptions {
35
+ separator?: WidgetSeparatorStyle;
36
+ start?: boolean | PowerlineCapOptions;
37
+ end?: boolean | PowerlineCapOptions;
38
+ transition?: (
39
+ context: PowerlineTransitionContext,
40
+ ) => Partial<WidgetOptions> & { separator?: WidgetSeparatorStyle };
41
+ }
42
+
43
+ const FULL_WIDTH: Partial<TerminalOptions> = { widthMode: "full" satisfies TerminalWidthMode };
44
+
45
+ function widget(type: WidgetType, options: WidgetOptions = {}): PresetWidget {
46
+ return { type, options };
47
+ }
48
+
49
+ function powerSegment(
50
+ type: WidgetType,
51
+ options: WidgetOptions & { fg: ColorName; bg: ColorName },
52
+ ): PowerlineSegment {
53
+ return { type, options };
54
+ }
55
+
56
+ function powerlineLine(
57
+ segments: readonly PowerlineSegment[],
58
+ options: PowerlineLineOptions = {},
59
+ ): PresetWidget[] {
60
+ if (segments.length === 0) return [];
61
+
62
+ const separator = options.separator ?? "powerline-right-spaced";
63
+ const line: PresetWidget[] = [];
64
+ const first = segments[0];
65
+ if (!first) return [];
66
+
67
+ if (options.start) line.push(capWidget(first.options.bg, options.start, "powerline-start"));
68
+
69
+ for (let index = 0; index < segments.length; index += 1) {
70
+ const current = segments[index];
71
+ if (!current) continue;
72
+ line.push(current);
73
+
74
+ const next = segments[index + 1];
75
+ if (!next) continue;
76
+ const transition = options.transition?.({ left: current, right: next, index }) ?? {};
77
+ line.push({
78
+ type: "separator",
79
+ options: {
80
+ separator: transition.separator ?? separator,
81
+ fg: current.options.bg,
82
+ bg: next.options.bg,
83
+ ...transition,
84
+ },
85
+ });
86
+ }
87
+
88
+ const last = segments.at(-1);
89
+ if (last && options.end) line.push(capWidget(last.options.bg, options.end, "powerline-end"));
90
+
91
+ return line;
92
+ }
93
+
94
+ function capWidget(
95
+ color: ColorName,
96
+ cap: true | PowerlineCapOptions,
97
+ defaultSeparator: WidgetSeparatorStyle,
98
+ ): PresetWidget {
99
+ const options = cap === true ? {} : cap;
100
+ return {
101
+ type: "separator",
102
+ options: {
103
+ separator: options.separator ?? defaultSeparator,
104
+ fg: options.fg ?? color,
105
+ bg: options.bg ?? "default",
106
+ },
107
+ };
108
+ }
109
+
110
+ function demoLabelLine(preset: keyof typeof BASE_PRESET_DEFINITIONS): PresetWidget[] {
111
+ return [widget("custom-text", { raw: true, fg: "pi:success", text: `Preset '${preset}':` })];
112
+ }
113
+
114
+ function demoEmptyLine(): PresetWidget[] {
115
+ return [widget("custom-text", { raw: true, fg: "pi:success", text: "" })];
116
+ }
117
+
118
+ const BASE_PRESET_DEFINITIONS = {
119
+ default: {
120
+ separator: "dot",
121
+ terminal: FULL_WIDTH,
122
+ lines: [
123
+ [
124
+ widget("model-provider"),
125
+ widget("thinking-level"),
126
+ widget("text-verbosity"),
127
+ widget("context-length"),
128
+ widget("git-branch"),
129
+ widget("git-diff", { gitDiffMode: "compact" }),
130
+ widget("cost"),
131
+ widget("total-time"),
132
+ ],
133
+ ],
134
+ },
135
+ powerline: {
136
+ separator: "none",
137
+ iconMode: "nerd",
138
+ terminal: FULL_WIDTH,
139
+ lines: [
140
+ powerlineLine([
141
+ powerSegment("model-provider", { fg: "brightWhite", bg: "ansi256:33", bold: true }),
142
+ powerSegment("git-branch", { fg: "brightWhite", bg: "ansi256:61", bold: true }),
143
+ powerSegment("tokens", { fg: "brightWhite", bg: "ansi256:64" }),
144
+ powerSegment("context-bar", {
145
+ fg: "ansi256:234",
146
+ bg: "ansi256:136",
147
+ contextBarMode: "medium",
148
+ }),
149
+ powerSegment("output-speed", { fg: "black", bg: "ansi256:37" }),
150
+ powerSegment("total-time", { fg: "brightWhite", bg: "ansi256:236" }),
151
+ ]),
152
+ ],
153
+ },
154
+ "powerline-bright": {
155
+ separator: "none",
156
+ iconMode: "nerd",
157
+ terminal: FULL_WIDTH,
158
+ lines: [
159
+ powerlineLine(
160
+ [
161
+ powerSegment("model", { fg: "brightWhite", bg: "ansi256:131", icon: "Model: " }),
162
+ powerSegment("context-length", { fg: "black", bg: "ansi256:220", icon: "Ctx: " }),
163
+ powerSegment("git-branch", { fg: "brightWhite", bg: "ansi256:68" }),
164
+ powerSegment("git-diff", {
165
+ fg: "black",
166
+ bg: "ansi256:108",
167
+ raw: true,
168
+ gitDiffMode: "compact",
169
+ }),
170
+ powerSegment("total-time", { fg: "black", bg: "ansi256:176", raw: true }),
171
+ ],
172
+ { start: true, end: true },
173
+ ),
174
+ powerlineLine(
175
+ [
176
+ powerSegment("cwd", { fg: "brightWhite", bg: "ansi256:131", segments: 3, icon: "cwd: " }),
177
+ powerSegment("cost", { fg: "black", bg: "ansi256:220", icon: "Cost: " }),
178
+ ],
179
+ { start: true, end: true },
180
+ ),
181
+ ],
182
+ },
183
+ "powerline-blocks": {
184
+ separator: "none",
185
+ iconMode: "nerd",
186
+ terminal: FULL_WIDTH,
187
+ lines: [
188
+ powerlineLine(
189
+ [
190
+ powerSegment("model", { fg: "brightWhite", bg: "ansi256:131", icon: "Model: " }),
191
+ powerSegment("context-length", { fg: "black", bg: "ansi256:222", icon: "Ctx: " }),
192
+ powerSegment("git-branch", { fg: "brightWhite", bg: "ansi256:67" }),
193
+ powerSegment("git-diff", {
194
+ fg: "black",
195
+ bg: "ansi256:151",
196
+ raw: true,
197
+ gitDiffMode: "compact",
198
+ }),
199
+ ],
200
+ { start: true, end: true },
201
+ ),
202
+ powerlineLine(
203
+ [
204
+ powerSegment("input-tokens", { fg: "brightWhite", bg: "ansi256:131", icon: "In: " }),
205
+ powerSegment("output-tokens", { fg: "black", bg: "ansi256:222", icon: "Out: " }),
206
+ powerSegment("cache-read", { fg: "brightWhite", bg: "ansi256:67", icon: "Cached: " }),
207
+ powerSegment("total-tokens", { fg: "black", bg: "ansi256:151", icon: "Total: " }),
208
+ ],
209
+ { start: true, end: true },
210
+ ),
211
+ powerlineLine(
212
+ [
213
+ powerSegment("context-bar", {
214
+ fg: "brightWhite",
215
+ bg: "ansi256:131",
216
+ icon: "Ctx ",
217
+ contextBarMode: "medium",
218
+ }),
219
+ powerSegment("total-time", { fg: "brightWhite", bg: "ansi256:131", raw: true }),
220
+ ],
221
+ {
222
+ start: true,
223
+ end: true,
224
+ transition: () => ({ separator: "powerline-soft-right" }),
225
+ },
226
+ ),
227
+ ],
228
+ },
229
+ "powerline-mono": {
230
+ separator: "none",
231
+ iconMode: "nerd",
232
+ terminal: FULL_WIDTH,
233
+ lines: [
234
+ powerlineLine(
235
+ [
236
+ powerSegment("model", { fg: "brightWhite", bg: "ansi256:240", icon: "Model: " }),
237
+ powerSegment("context-length", { fg: "black", bg: "ansi256:252", icon: "Ctx: " }),
238
+ powerSegment("git-branch", { fg: "brightWhite", bg: "ansi256:233" }),
239
+ powerSegment("git-diff", {
240
+ fg: "black",
241
+ bg: "ansi256:250",
242
+ raw: true,
243
+ gitDiffMode: "compact",
244
+ }),
245
+ powerSegment("cwd-basename", { fg: "brightWhite", bg: "ansi256:236" }),
246
+ ],
247
+ { start: true, end: true },
248
+ ),
249
+ powerlineLine(
250
+ [
251
+ powerSegment("output-tokens", { fg: "brightWhite", bg: "ansi256:240", icon: "👾 Out: " }),
252
+ powerSegment("cache-read", { fg: "black", bg: "ansi256:252", icon: "💰 Cached: " }),
253
+ powerSegment("total-tokens", { fg: "brightWhite", bg: "ansi256:233", icon: "Total: " }),
254
+ powerSegment("context-length", { fg: "black", bg: "ansi256:250", icon: "Ctx: " }),
255
+ ],
256
+ { start: true, end: true },
257
+ ),
258
+ ],
259
+ },
260
+ "git-heavy": {
261
+ separator: "dot",
262
+ iconMode: "nerd",
263
+ terminal: FULL_WIDTH,
264
+ lines: [
265
+ [
266
+ widget("model-provider"),
267
+ widget("cwd-basename"),
268
+ widget("git-branch"),
269
+ widget("git-sha"),
270
+ widget("git-status"),
271
+ widget("git-diff", { gitDiffMode: "compact" }),
272
+ widget("git-ahead-behind"),
273
+ ],
274
+ ],
275
+ },
276
+ compact: {
277
+ separator: "space",
278
+ terminal: { widthMode: "full-minus-40" },
279
+ lines: [
280
+ [
281
+ widget("model"),
282
+ widget("thinking-level"),
283
+ widget("text-verbosity"),
284
+ widget("git-branch"),
285
+ widget("context"),
286
+ widget("cost"),
287
+ ],
288
+ ],
289
+ },
290
+ "pi-footer": {
291
+ separator: "none",
292
+ iconMode: "text",
293
+ terminal: FULL_WIDTH,
294
+ lines: [
295
+ [
296
+ widget("cwd", { raw: true, fg: "pi:dim", cwdDisplayStyle: "full-home" }),
297
+ widget("git-branch", {
298
+ icon: " ",
299
+ fg: "pi:dim",
300
+ hideWhenEmpty: true,
301
+ gitBranchDisplayStyle: "round-brackets",
302
+ }),
303
+ widget("session-name", { icon: " • ", fg: "pi:dim", hideWhenEmpty: true }),
304
+ ],
305
+ [
306
+ widget("tokens", { raw: true, fg: "pi:dim", tokenFormatStyle: "compact" }),
307
+ widget("cache-read", {
308
+ icon: " R",
309
+ fg: "pi:dim",
310
+ tokenFormatStyle: "compact",
311
+ hideWhenZero: true,
312
+ }),
313
+ widget("cache-write", {
314
+ icon: " W",
315
+ fg: "pi:dim",
316
+ tokenFormatStyle: "compact",
317
+ hideWhenZero: true,
318
+ }),
319
+ widget("cache-hit-rate", {
320
+ icon: " CH",
321
+ fg: "pi:dim",
322
+ hideWhenZero: true,
323
+ cacheHitSource: "turn",
324
+ }),
325
+ widget("cost", {
326
+ icon: " ",
327
+ fg: "pi:dim",
328
+ costFormatStyle: "compact",
329
+ showSubscription: true,
330
+ }),
331
+ widget("context", {
332
+ icon: " ",
333
+ fg: "pi:dim",
334
+ contextConditionalColors: true,
335
+ warningFg: "pi:warning",
336
+ dangerFg: "pi:error",
337
+ }),
338
+ widget("context-window", {
339
+ icon: "/",
340
+ fg: "pi:dim",
341
+ contextConditionalColors: true,
342
+ warningFg: "pi:warning",
343
+ dangerFg: "pi:error",
344
+ tokenFormatStyle: "compact",
345
+ }),
346
+ widget("flex-separator"),
347
+ widget("model", { raw: true, fg: "pi:dim" }),
348
+ widget("thinking-level", { icon: " • ", fg: "pi:dim", hideWhenEmpty: true }),
349
+ ],
350
+ ],
351
+ },
352
+ } satisfies Record<string, PresetDefinition>;
353
+
354
+ function demoLines(...presets: Array<keyof typeof BASE_PRESET_DEFINITIONS>): PresetWidget[][] {
355
+ return presets.flatMap((preset, index) => [
356
+ demoLabelLine(preset),
357
+ ...BASE_PRESET_DEFINITIONS[preset].lines,
358
+ ...(index === presets.length - 1 ? [] : [demoEmptyLine()]),
359
+ ]);
360
+ }
361
+
362
+ export const PRESET_DEFINITIONS = {
363
+ ...BASE_PRESET_DEFINITIONS,
364
+ demo: {
365
+ separator: "none",
366
+ iconMode: "nerd",
367
+ terminal: FULL_WIDTH,
368
+ lines: demoLines(
369
+ "pi-footer",
370
+ "powerline",
371
+ "powerline-bright",
372
+ "powerline-blocks",
373
+ "powerline-mono",
374
+ ),
375
+ },
376
+ "demo-standard": {
377
+ separator: "dot",
378
+ terminal: FULL_WIDTH,
379
+ lines: demoLines("default", "compact", "git-heavy"),
380
+ },
381
+ } satisfies Record<string, PresetDefinition>;
382
+
383
+ export type Preset = keyof typeof PRESET_DEFINITIONS;
@@ -0,0 +1,114 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+
4
+ import { applyColors } from "./colors.js";
5
+ import type { GetExtensionStatuses } from "./extension-statuses.js";
6
+ import { separatorText } from "./separators.js";
7
+ import type { StatuslineData, StatuslineSettings } from "./types.js";
8
+ import { contextForDependencies } from "./widgets/context.js";
9
+ import { registry } from "./widgets/registry.js";
10
+ import type { WidgetStore } from "./widgets/store.js";
11
+ import type { BaseWidgetContext, Widget } from "./widgets/types.js";
12
+
13
+ export interface RenderStatuslineOptions {
14
+ getExtensionStatuses?: GetExtensionStatuses;
15
+ theme?: Theme;
16
+ requestRender?: () => void;
17
+ }
18
+
19
+ export function renderStatuslines(
20
+ store: WidgetStore,
21
+ data: StatuslineData,
22
+ width: number,
23
+ options: RenderStatuslineOptions = {},
24
+ ): string[] {
25
+ const settings = store.settings;
26
+ if (!settings.enabled || width <= 0) return [];
27
+
28
+ const baseCtx: BaseWidgetContext = {
29
+ iconMode: settings.iconMode,
30
+ minimalist: settings.minimalist,
31
+ colorLevel: settings.terminal.colorLevel,
32
+ ...(options.theme ? { theme: options.theme } : {}),
33
+ ...(options.requestRender ? { requestRender: options.requestRender } : {}),
34
+ };
35
+ const lineWidth = effectiveWidth(settings, width);
36
+ return store.lines
37
+ .map((line) => renderLine(line, settings, lineWidth, { baseCtx, data, options }))
38
+ .filter((line) => line.trim().length > 0);
39
+ }
40
+
41
+ function padRight(left: string, right: string, width: number): string {
42
+ const spaces = Math.max(1, width - visibleWidth(left) - visibleWidth(right));
43
+ return truncateToWidth(`${left}${" ".repeat(spaces)}${right}`, width, "…");
44
+ }
45
+
46
+ interface RenderedSegment {
47
+ widget: Widget;
48
+ segment: string;
49
+ }
50
+
51
+ interface RenderLineContext {
52
+ baseCtx: BaseWidgetContext;
53
+ data: StatuslineData;
54
+ options: RenderStatuslineOptions;
55
+ }
56
+
57
+ function renderLine(
58
+ line: readonly Widget[],
59
+ settings: StatuslineSettings,
60
+ width: number,
61
+ ctx: RenderLineContext,
62
+ ): string {
63
+ const rendered = line
64
+ .filter((widget) => widget.enabled)
65
+ .map((widget) => ({
66
+ widget,
67
+ segment:
68
+ widget.render(
69
+ contextForDependencies(
70
+ ctx.baseCtx,
71
+ registry.spec(widget.type).dependencies,
72
+ ctx.data,
73
+ ctx.options,
74
+ ),
75
+ ) ?? "",
76
+ }));
77
+
78
+ const flexIndex = rendered.findIndex((entry) => entry.widget.type === "flex-separator");
79
+ if (flexIndex === -1) {
80
+ return truncateToWidth(joinSegments(rendered, settings), width, "…");
81
+ }
82
+
83
+ const left = joinSegments(rendered.slice(0, flexIndex), settings);
84
+ const right = joinSegments(rendered.slice(flexIndex + 1), settings);
85
+ return right ? padRight(left, right, width) : truncateToWidth(left, width, "…");
86
+ }
87
+
88
+ function effectiveWidth(settings: StatuslineSettings, width: number): number {
89
+ if (settings.terminal.widthMode === "full-minus-40") return Math.max(1, width - 40);
90
+ return width;
91
+ }
92
+
93
+ function joinSegments(entries: readonly RenderedSegment[], settings: StatuslineSettings): string {
94
+ const segments = entries.filter((entry) => entry.segment.length > 0);
95
+ if (segments.length === 0) return "";
96
+
97
+ let output = segments[0]?.segment ?? "";
98
+ for (let index = 1; index < segments.length; index += 1) {
99
+ const previous = segments[index - 1];
100
+ const current = segments[index];
101
+ if (!previous || !current) continue;
102
+ if (previous.widget.type !== "separator" && current.widget.type !== "separator") {
103
+ output += applyColors(
104
+ separatorText(settings.separator),
105
+ settings.separatorFg,
106
+ settings.separatorBg,
107
+ false,
108
+ settings.terminal.colorLevel,
109
+ );
110
+ }
111
+ output += current.segment;
112
+ }
113
+ return output;
114
+ }
@@ -0,0 +1,52 @@
1
+ export const SEPARATOR_VALUES = [
2
+ "none",
3
+ "dot",
4
+ "pipe",
5
+ "space",
6
+ "powerline",
7
+ "dash",
8
+ "comma",
9
+ ] as const;
10
+
11
+ export type SeparatorStyle = (typeof SEPARATOR_VALUES)[number];
12
+
13
+ export const WIDGET_SEPARATOR_VALUES = [
14
+ ...SEPARATOR_VALUES,
15
+ "powerline-right",
16
+ "powerline-right-spaced",
17
+ "powerline-left",
18
+ "powerline-left-spaced",
19
+ "powerline-soft-right",
20
+ "powerline-soft-left",
21
+ "powerline-start",
22
+ "powerline-end",
23
+ "custom",
24
+ ] as const;
25
+
26
+ export type WidgetSeparatorStyle = (typeof WIDGET_SEPARATOR_VALUES)[number];
27
+
28
+ export function separatorText(separator: SeparatorStyle): string {
29
+ if (separator === "none") return "";
30
+ if (separator === "space") return " ";
31
+ if (separator === "pipe") return " | ";
32
+ if (separator === "powerline") return "  ";
33
+ if (separator === "dash") return " - ";
34
+ if (separator === "comma") return ", ";
35
+ return " • ";
36
+ }
37
+
38
+ export function widgetSeparatorText(
39
+ separator: WidgetSeparatorStyle | undefined,
40
+ customText: string,
41
+ ): string {
42
+ if (separator === "custom") return customText;
43
+ if (separator === "powerline" || separator === "powerline-right-spaced") return " ";
44
+ if (separator === "powerline-right") return "";
45
+ if (separator === "powerline-left-spaced") return " ";
46
+ if (separator === "powerline-left") return "";
47
+ if (separator === "powerline-soft-right") return "";
48
+ if (separator === "powerline-soft-left") return "";
49
+ if (separator === "powerline-start") return "";
50
+ if (separator === "powerline-end") return "";
51
+ return separatorText(separator ?? "pipe");
52
+ }
@@ -0,0 +1,122 @@
1
+ import type { ColorLevel, ColorName } from "./colors.js";
2
+ import type { ExtensionStatusRowConfig } from "./extension-statuses.js";
3
+ import type { Preset } from "./presets.js";
4
+ import type { SeparatorStyle } from "./separators.js";
5
+ import type { WidgetType } from "./widgets/registry.js";
6
+
7
+ export interface WidgetEntry {
8
+ id: string;
9
+ type: WidgetType;
10
+ enabled: boolean;
11
+ options: WidgetOptions;
12
+ }
13
+
14
+ type WidgetOptionValue = string | number | boolean | undefined;
15
+
16
+ // Shared style options. These are renderer-level options, not widget-specific metadata.
17
+ export interface WidgetStyle {
18
+ fg?: ColorName;
19
+ bg?: ColorName;
20
+ bold?: boolean;
21
+ }
22
+
23
+ export interface WidgetOptions extends WidgetStyle {
24
+ [key: string]: WidgetOptionValue;
25
+
26
+ // Shared/base options. New widget-specific fields must be declared through widget metadata.
27
+ icon?: string;
28
+ raw?: boolean;
29
+ hideWhenEmpty?: boolean;
30
+ hideWhenZero?: boolean;
31
+ text?: string;
32
+ }
33
+
34
+ export const TERMINAL_WIDTH_MODE_VALUES = ["full", "full-minus-40"] as const;
35
+ export type TerminalWidthMode = (typeof TERMINAL_WIDTH_MODE_VALUES)[number];
36
+
37
+ export interface TerminalOptions {
38
+ widthMode: TerminalWidthMode;
39
+ colorLevel: ColorLevel;
40
+ }
41
+
42
+ export interface StatuslineSettings {
43
+ version: 1;
44
+ enabled: boolean;
45
+ preset: Preset;
46
+ separator: SeparatorStyle;
47
+ separatorFg: ColorName;
48
+ separatorBg: ColorName;
49
+ iconMode: IconMode;
50
+ minimalist: boolean;
51
+ terminal: TerminalOptions;
52
+ extensionStatusRow: ExtensionStatusRowConfig;
53
+ }
54
+
55
+ export interface StatuslineConfig extends StatuslineSettings {
56
+ lines: WidgetEntry[][];
57
+ }
58
+
59
+ export const ICON_MODE_VALUES = ["emoji", "nerd", "text"] as const;
60
+
61
+ export type IconMode = (typeof ICON_MODE_VALUES)[number];
62
+
63
+ export function isRecord(value: unknown): value is Record<string, unknown> {
64
+ return typeof value === "object" && value !== null;
65
+ }
66
+
67
+ export interface SessionMetrics {
68
+ inputTokens: number;
69
+ outputTokens: number;
70
+ cacheReadTokens: number;
71
+ cacheWriteTokens: number;
72
+ totalTokens: number;
73
+ costUsd: number;
74
+ userMessages: number;
75
+ assistantMessages: number;
76
+ toolResults: number;
77
+ firstTimestampMs: number | undefined;
78
+ lastTimestampMs: number | undefined;
79
+ compactions: number;
80
+ }
81
+
82
+ export interface TurnMetrics {
83
+ inputTokens: number;
84
+ outputTokens: number;
85
+ cacheReadTokens: number;
86
+ cacheWriteTokens: number;
87
+ totalTokens: number;
88
+ costUsd: number;
89
+ }
90
+
91
+ export interface GitInfo {
92
+ branch: string | null;
93
+ sha: string | null;
94
+ root: string | null;
95
+ staged: number;
96
+ unstaged: number;
97
+ untracked: number;
98
+ insertions: number;
99
+ deletions: number;
100
+ ahead: number;
101
+ behind: number;
102
+ remote: string | null;
103
+ isRepo: boolean;
104
+ }
105
+
106
+ export interface StatuslineData {
107
+ model: string | undefined;
108
+ provider: string | undefined;
109
+ sessionName: string | undefined;
110
+ sessionId: string | undefined;
111
+ thinkingLevel: string | undefined;
112
+ textVerbosity: string | undefined;
113
+ git: GitInfo;
114
+ cwd: string;
115
+ activeToolCount: number;
116
+ usingSubscription: boolean;
117
+ contextTokens: number | undefined;
118
+ contextMaxTokens: number | undefined;
119
+ metrics: SessionMetrics;
120
+ turnMetrics: TurnMetrics;
121
+ eventWidgets: ReadonlyMap<string, string>;
122
+ }
@@ -0,0 +1,22 @@
1
+ import type { ColorLevel } from "../colors.js";
2
+ import { COLOR_LEVEL_LABELS } from "./model.js";
3
+
4
+ type ColorLevelConfirmAction = "confirm" | "cancel";
5
+
6
+ export const COLOR_LEVEL_CONFIRM_HINT = "Press enter/y to proceed, esc/n to go back.";
7
+ export const COLOR_LEVEL_CONFIRM_WARNING =
8
+ "Changing color level will reset all custom ANSI256 widget colors.";
9
+
10
+ export function colorLevelConfirmValueLabel(colorLevel: ColorLevel | undefined): string {
11
+ return `New color level: ${colorLevel ? COLOR_LEVEL_LABELS[colorLevel] : "unknown"}`;
12
+ }
13
+
14
+ export function colorLevelConfirmAction(
15
+ data: string,
16
+ isEscape: boolean,
17
+ isEnter: boolean,
18
+ ): ColorLevelConfirmAction | undefined {
19
+ if (isEscape || data === "n") return "cancel";
20
+ if (isEnter || data === "y") return "confirm";
21
+ return undefined;
22
+ }