@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,122 @@
1
+ import { normalizeColor } from "../colors.js";
2
+ import type { WidgetOptions } from "../types.js";
3
+ import type { WidgetProperty, WidgetPropertyDefault, WidgetSpec } from "./types.js";
4
+
5
+ const SYSTEM_BASE_OPTION_DEFAULTS = {
6
+ raw: false,
7
+ hideWhenEmpty: false,
8
+ hideWhenZero: false,
9
+ text: "-",
10
+ icon: "",
11
+ } as const;
12
+
13
+ // The options-relevant slice of a spec, derived from WidgetSpec (excludes render so concrete
14
+ // widget specs stay assignable — render's generic params make the full spec invariant).
15
+ type WidgetOptionsSpec = Pick<
16
+ WidgetSpec,
17
+ "baseOptions" | "baseOptionDefaults" | "properties" | "defaultStyle" | "createOptionDefaults"
18
+ >;
19
+
20
+ export function defaultOptionsFromSpec(spec: WidgetOptionsSpec): WidgetOptions {
21
+ const base: WidgetOptions = {};
22
+
23
+ for (const option of spec.baseOptions) {
24
+ const optionId: string = option;
25
+ base[optionId] = SYSTEM_BASE_OPTION_DEFAULTS[option];
26
+ }
27
+ Object.assign(base, spec.baseOptionDefaults ?? {});
28
+
29
+ for (const property of spec.properties) {
30
+ base[property.id] = property.default;
31
+ }
32
+
33
+ const dynamicDefaults = spec.createOptionDefaults?.();
34
+ if (dynamicDefaults) Object.assign(base, dynamicDefaults);
35
+
36
+ if (spec.defaultStyle.fg !== undefined) base.fg = spec.defaultStyle.fg;
37
+ if (spec.defaultStyle.bg !== undefined) base.bg = spec.defaultStyle.bg;
38
+ if (spec.defaultStyle.bold !== undefined) base.bold = spec.defaultStyle.bold;
39
+
40
+ return base;
41
+ }
42
+
43
+ export function sanitizeOptionsFromSpec(
44
+ spec: WidgetOptionsSpec,
45
+ input: Record<string, unknown>,
46
+ ): WidgetOptions {
47
+ const defaults = defaultOptionsFromSpec(spec);
48
+ const merged: Record<string, unknown> = { ...defaults, ...input };
49
+ const next: WidgetOptions = {};
50
+
51
+ for (const option of spec.baseOptions) {
52
+ if (!(option in defaults)) continue;
53
+ const optionId: string = option;
54
+ next[optionId] = sanitizeMetadataValue(merged[optionId], defaults[optionId]);
55
+ }
56
+
57
+ const fg = normalizeColor(merged.fg);
58
+ if (fg) next.fg = fg;
59
+ const bg = normalizeColor(merged.bg);
60
+ if (bg) next.bg = bg;
61
+ next.bold = typeof merged.bold === "boolean" ? merged.bold : Boolean(defaults.bold);
62
+
63
+ for (const property of spec.properties) {
64
+ next[property.id] = sanitizeWidgetProperty(property, merged[property.id]);
65
+ }
66
+
67
+ return next;
68
+ }
69
+
70
+ function sanitizeWidgetProperty(
71
+ property: WidgetProperty,
72
+ value: unknown,
73
+ ): string | number | boolean {
74
+ switch (property.kind) {
75
+ case "boolean":
76
+ return typeof value === "boolean" ? value : property.default;
77
+ case "number":
78
+ return sanitizeMetadataNumber(
79
+ value,
80
+ property.default,
81
+ property.options?.min,
82
+ property.options?.max,
83
+ );
84
+ case "text":
85
+ if (
86
+ property.id === "warningFg" ||
87
+ property.id === "warningBg" ||
88
+ property.id === "dangerFg" ||
89
+ property.id === "dangerBg"
90
+ )
91
+ return normalizeColor(value) ?? property.default;
92
+ return typeof value === "string" ? value : property.default;
93
+ case "choice": {
94
+ const choices = property.options?.choices ?? [];
95
+ return typeof value === "string" && choices.some((choice) => choice.id === value)
96
+ ? value
97
+ : property.default;
98
+ }
99
+ }
100
+ }
101
+
102
+ function sanitizeMetadataValue(
103
+ value: unknown,
104
+ defaultValue: WidgetOptions[keyof WidgetOptions],
105
+ ): WidgetOptions[keyof WidgetOptions] {
106
+ if (typeof defaultValue === "boolean") return typeof value === "boolean" ? value : defaultValue;
107
+ if (typeof defaultValue === "number")
108
+ return typeof value === "number" && Number.isInteger(value) ? value : defaultValue;
109
+ if (typeof defaultValue === "string") return typeof value === "string" ? value : defaultValue;
110
+ return defaultValue;
111
+ }
112
+
113
+ function sanitizeMetadataNumber(
114
+ value: unknown,
115
+ defaultValue: WidgetPropertyDefault,
116
+ min = Number.NEGATIVE_INFINITY,
117
+ max = Number.POSITIVE_INFINITY,
118
+ ): number {
119
+ const numberValue = typeof value === "number" && Number.isInteger(value) ? value : defaultValue;
120
+ const safeNumber = typeof numberValue === "number" ? numberValue : 0;
121
+ return Math.min(max, Math.max(min, safeNumber));
122
+ }
@@ -0,0 +1,361 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readdir } from "node:fs/promises";
3
+
4
+ import type { ColorName } from "../../colors.js";
5
+ import { CACHE_NAMESPACES, asyncCache } from "../../cache.js";
6
+ import type { WidgetIconSet } from "../types.js";
7
+ import { defineWidget } from "../types.js";
8
+
9
+ const VERSION_TIMEOUT_MS = 1_000;
10
+ const RUNTIME_CACHE_TTL_MS = 10_000;
11
+
12
+ interface VersionCommand {
13
+ command: string;
14
+ args: readonly string[];
15
+ parse: (output: string) => string | undefined;
16
+ }
17
+
18
+ interface RuntimeDefinition<TName extends string = string> {
19
+ name: TName;
20
+ files: readonly string[];
21
+ matchesEntry?: (entry: CwdEntry) => boolean;
22
+ versionCommands: readonly VersionCommand[];
23
+ icons: WidgetIconSet;
24
+ color: ColorName;
25
+ }
26
+
27
+ interface CwdEntry {
28
+ name: string;
29
+ isFile: boolean;
30
+ }
31
+
32
+ const RUNTIME_DEFINITIONS = [
33
+ {
34
+ name: "bun",
35
+ files: ["bun.lock", "bun.lockb"],
36
+ versionCommands: [
37
+ {
38
+ command: "bun",
39
+ args: ["--version"],
40
+ parse: (output) => /\bv?([0-9][^\s]*)/i.exec(output)?.[1],
41
+ },
42
+ ],
43
+ icons: { emoji: "🥟", nerd: "", text: "" },
44
+ color: "pi:warning",
45
+ },
46
+ {
47
+ name: "deno",
48
+ files: ["deno.json", "deno.jsonc", "deno.lock"],
49
+ versionCommands: [
50
+ {
51
+ command: "deno",
52
+ args: ["--version"],
53
+ parse: (output) => /deno\s+([0-9][^\s]*)/i.exec(output)?.[1],
54
+ },
55
+ ],
56
+ icons: { emoji: "🦕", nerd: "", text: "" },
57
+ color: "pi:syntaxType",
58
+ },
59
+ {
60
+ name: "lua",
61
+ files: ["stylua.toml", ".stylua.toml", ".luarc.json", ".luarc.jsonc", "init.lua"],
62
+ matchesEntry: (entry) => entry.name === "lua" || (entry.isFile && entry.name.endsWith(".lua")),
63
+ versionCommands: [
64
+ { command: "lua", args: ["-v"], parse: (output) => /Lua\s+([0-9][^\s]*)/i.exec(output)?.[1] },
65
+ {
66
+ command: "luajit",
67
+ args: ["-v"],
68
+ parse: (output) => /LuaJIT\s+([0-9][^\s]*)/i.exec(output)?.[1],
69
+ },
70
+ ],
71
+ icons: { emoji: "🌙", nerd: "", text: "" },
72
+ color: "pi:accent",
73
+ },
74
+ {
75
+ name: "node",
76
+ files: ["package.json", ".nvmrc", ".node-version"],
77
+ versionCommands: [
78
+ {
79
+ command: "node",
80
+ args: ["--version"],
81
+ parse: (output) => /\bv?([0-9][^\s]*)/i.exec(output)?.[1],
82
+ },
83
+ ],
84
+ icons: { emoji: "⬢", nerd: "", text: "" },
85
+ color: "pi:success",
86
+ },
87
+ {
88
+ name: "python",
89
+ files: [
90
+ "pyproject.toml",
91
+ "requirements.txt",
92
+ "setup.py",
93
+ "setup.cfg",
94
+ "Pipfile",
95
+ ".python-version",
96
+ ],
97
+ versionCommands: [
98
+ {
99
+ command: "python3",
100
+ args: ["--version"],
101
+ parse: (output) => /Python\s+([0-9][^\s]*)/i.exec(output)?.[1],
102
+ },
103
+ {
104
+ command: "python",
105
+ args: ["--version"],
106
+ parse: (output) => /Python\s+([0-9][^\s]*)/i.exec(output)?.[1],
107
+ },
108
+ ],
109
+ icons: { emoji: "🐍", nerd: "", text: "" },
110
+ color: "pi:warning",
111
+ },
112
+ {
113
+ name: "go",
114
+ files: ["go.mod"],
115
+ versionCommands: [
116
+ {
117
+ command: "go",
118
+ args: ["version"],
119
+ parse: (output) => /go version go([0-9][^\s]*)/i.exec(output)?.[1],
120
+ },
121
+ ],
122
+ icons: { emoji: "🐹", nerd: "", text: "" },
123
+ color: "pi:syntaxType",
124
+ },
125
+ {
126
+ name: "rust",
127
+ files: ["Cargo.toml"],
128
+ versionCommands: [
129
+ {
130
+ command: "rustc",
131
+ args: ["--version"],
132
+ parse: (output) => /rustc\s+([0-9][^\s]*)/i.exec(output)?.[1],
133
+ },
134
+ ],
135
+ icons: { emoji: "🦀", nerd: "", text: "" },
136
+ color: "pi:error",
137
+ },
138
+ {
139
+ name: "java",
140
+ files: ["pom.xml", "build.gradle", "build.gradle.kts"],
141
+ versionCommands: [
142
+ {
143
+ command: "java",
144
+ args: ["-version"],
145
+ parse: (output) => /(?:openjdk|java)\s+version\s+"?([0-9][^"\s]*)/i.exec(output)?.[1],
146
+ },
147
+ ],
148
+ icons: { emoji: "☕", nerd: "", text: "" },
149
+ color: "pi:warning",
150
+ },
151
+ {
152
+ name: "ruby",
153
+ files: ["Gemfile", ".ruby-version"],
154
+ versionCommands: [
155
+ {
156
+ command: "ruby",
157
+ args: ["--version"],
158
+ parse: (output) => /ruby\s+([0-9][^\s]*)/i.exec(output)?.[1],
159
+ },
160
+ ],
161
+ icons: { emoji: "💎", nerd: "", text: "" },
162
+ color: "pi:error",
163
+ },
164
+ {
165
+ name: "php",
166
+ files: ["composer.json"],
167
+ versionCommands: [
168
+ {
169
+ command: "php",
170
+ args: ["--version"],
171
+ parse: (output) => /PHP\s+([0-9][^\s]*)/i.exec(output)?.[1],
172
+ },
173
+ ],
174
+ icons: { emoji: "🐘", nerd: "", text: "" },
175
+ color: "pi:accent",
176
+ },
177
+ ] as const satisfies readonly RuntimeDefinition[];
178
+
179
+ export type RuntimeName = (typeof RUNTIME_DEFINITIONS)[number]["name"];
180
+
181
+ interface RuntimeInfo {
182
+ name: RuntimeName;
183
+ version?: string;
184
+ icons: WidgetIconSet;
185
+ color: ColorName;
186
+ }
187
+
188
+ interface RuntimeDetection {
189
+ cwd: string;
190
+ displayVersion: boolean;
191
+ }
192
+
193
+ export const RuntimeWidget = defineWidget({
194
+ type: "runtime",
195
+ label: "Runtime",
196
+ category: "Project",
197
+ description: "Current project runtime (e.g. Bun, Python, Go, Rust)",
198
+ dependencies: ["cwd"],
199
+ baseOptions: ["raw", "hideWhenEmpty", "icon", "text"],
200
+ baseOptionDefaults: { hideWhenEmpty: true },
201
+ properties: [
202
+ {
203
+ id: "style",
204
+ label: "Style",
205
+ kind: "choice",
206
+ description: "Display style",
207
+ default: "compact",
208
+ options: {
209
+ choices: [
210
+ { id: "default", label: "Default", list: "default" },
211
+ { id: "compact", label: "Compact", list: "compact" },
212
+ ],
213
+ showInWidgets: true,
214
+ showInColors: false,
215
+ listProperty: "style",
216
+ },
217
+ },
218
+ {
219
+ id: "displayVersion",
220
+ label: "Display version",
221
+ kind: "boolean",
222
+ description: "Show runtime version",
223
+ default: true,
224
+ options: {
225
+ label: "with-version",
226
+ showInWidgets: true,
227
+ showInColors: true,
228
+ listProperty: "",
229
+ },
230
+ },
231
+ ],
232
+ icons: { emoji: "⚙️", nerd: "", text: "runtime" },
233
+ defaultStyle: { fg: "default", bg: "default", bold: false },
234
+ render({ ctx, options, renderWidget }) {
235
+ const cwd = ctx.cwd;
236
+ if (cwd.length === 0) return renderWidget("");
237
+
238
+ const info = asyncCache.get(
239
+ CACHE_NAMESPACES.runtime,
240
+ `${cwd}:${options.displayVersion ? "version" : "no-version"}`,
241
+ RUNTIME_CACHE_TTL_MS,
242
+ { cwd, displayVersion: options.displayVersion },
243
+ detectRuntime,
244
+ ctx.requestRender,
245
+ );
246
+ if (!info) return renderWidget("", { icons: { emoji: "", nerd: "", text: "" } });
247
+
248
+ return renderWidget(renderRuntimeValue(info, options.displayVersion, options.style), {
249
+ icons: info.icons,
250
+ fg: options.fg && options.fg !== "default" ? options.fg : info.color,
251
+ });
252
+ },
253
+ });
254
+
255
+ function renderRuntimeValue(
256
+ info: RuntimeInfo,
257
+ displayVersion: boolean,
258
+ style: "default" | "compact",
259
+ ) {
260
+ if (!displayVersion || !info.version) return info.name;
261
+ if (style === "compact") return compactRuntimeVersion(info.name, info.version);
262
+ return `${info.name} ${info.version}`;
263
+ }
264
+
265
+ function prefixRuntimeVersion(version: string) {
266
+ const trimmed = version.trim();
267
+ if (!trimmed) return trimmed;
268
+ return trimmed.toLowerCase().startsWith("v") ? trimmed : `v${trimmed}`;
269
+ }
270
+
271
+ function compactRuntimeVersion(name: RuntimeName, version: string) {
272
+ const prefixed = prefixRuntimeVersion(version);
273
+ const match = /^v([0-9]+)(?:\.([0-9]+))?/i.exec(prefixed);
274
+ if (!match) return prefixed;
275
+
276
+ const major = match[1] ?? "";
277
+ const minor = match[2];
278
+ if ((name === "python" || name === "go" || name === "bun") && minor !== undefined)
279
+ return `v${major}.${minor}`;
280
+ return `v${major}`;
281
+ }
282
+
283
+ async function detectRuntime({
284
+ cwd,
285
+ displayVersion,
286
+ }: RuntimeDetection): Promise<RuntimeInfo | null> {
287
+ const entries = await readCwdEntries(cwd);
288
+ if (!entries) return null;
289
+
290
+ for (const definition of RUNTIME_DEFINITIONS) {
291
+ if (!matchesRuntime(definition, entries)) continue;
292
+
293
+ const version = displayVersion ? await detectVersion(definition) : undefined;
294
+ return version
295
+ ? {
296
+ name: definition.name,
297
+ version,
298
+ icons: definition.icons,
299
+ color: definition.color,
300
+ }
301
+ : {
302
+ name: definition.name,
303
+ icons: definition.icons,
304
+ color: definition.color,
305
+ };
306
+ }
307
+
308
+ return null;
309
+ }
310
+
311
+ async function readCwdEntries(cwd: string) {
312
+ try {
313
+ const entries = await readdir(cwd, { withFileTypes: true });
314
+ return entries.map((entry) => ({ name: entry.name, isFile: entry.isFile() }));
315
+ } catch {
316
+ return undefined;
317
+ }
318
+ }
319
+
320
+ function matchesRuntime(definition: RuntimeDefinition, entries: readonly CwdEntry[]) {
321
+ if (hasAnyFile(entries, definition.files)) return true;
322
+ return definition.matchesEntry
323
+ ? entries.some((entry) => definition.matchesEntry?.(entry) === true)
324
+ : false;
325
+ }
326
+
327
+ function hasAnyFile(entries: readonly CwdEntry[], files: readonly string[]) {
328
+ const fileNames = new Set(entries.filter((entry) => entry.isFile).map((entry) => entry.name));
329
+ return files.some((file) => fileNames.has(file));
330
+ }
331
+
332
+ async function detectVersion(definition: RuntimeDefinition) {
333
+ for (const command of definition.versionCommands) {
334
+ const version = await runVersion(command);
335
+ if (version) return version;
336
+ }
337
+ return undefined;
338
+ }
339
+
340
+ async function runVersion(versionCommand: VersionCommand) {
341
+ return new Promise<string | undefined>((resolve) => {
342
+ execFile(
343
+ versionCommand.command,
344
+ [...versionCommand.args],
345
+ { encoding: "utf8", timeout: VERSION_TIMEOUT_MS, windowsHide: true },
346
+ (error, stdout, stderr) => {
347
+ const output = `${stringOutput(stdout)}\n${stringOutput(stderr)}`;
348
+ if (error && output.trim().length === 0) {
349
+ resolve(undefined);
350
+ return;
351
+ }
352
+ const parsed = versionCommand.parse(output);
353
+ resolve(parsed ? prefixRuntimeVersion(parsed) : undefined);
354
+ },
355
+ );
356
+ });
357
+ }
358
+
359
+ function stringOutput(value: string | Buffer) {
360
+ return typeof value === "string" ? value : value.toString("utf8");
361
+ }
@@ -0,0 +1,223 @@
1
+ import type { WidgetEntry, WidgetOptions } from "../types.js";
2
+ import { ActiveToolsWidget } from "./core/active-tools.js";
3
+ import { ContextWindowWidget } from "./core/context-window.js";
4
+ import { CwdBasenameWidget } from "./core/cwd-basename.js";
5
+ import { CwdWidget } from "./core/cwd.js";
6
+ import { EventValueWidget } from "./core/event.js";
7
+ import { ExtensionStatusWidget } from "./core/external-status.js";
8
+ import { ModelProviderWidget } from "./core/model-provider.js";
9
+ import { ModelWidget } from "./core/model.js";
10
+ import { ProviderWidget } from "./core/provider.js";
11
+ import { SessionNameWidget } from "./core/session-name.js";
12
+ import { TextVerbosityWidget } from "./core/text-verbosity.js";
13
+ import { ThinkingLevelWidget } from "./core/thinking-level.js";
14
+ import { GitAheadBehindWidget } from "./git/ahead-behind.js";
15
+ import { GitBranchWidget } from "./git/branch.js";
16
+ import { GitCleanStatusWidget } from "./git/clean.js";
17
+ import { GitDeletionsWidget } from "./git/deletions.js";
18
+ import { GitDiffWidget } from "./git/diff.js";
19
+ import { GitInsertionsWidget } from "./git/insertions.js";
20
+ import { GitRemoteWidget } from "./git/remote.js";
21
+ import { GitRootDirWidget } from "./git/root.js";
22
+ import { GitShaWidget } from "./git/sha.js";
23
+ import { GitStagedWidget } from "./git/staged.js";
24
+ import { GitStatusWidget } from "./git/status.js";
25
+ import { GitUnstagedWidget } from "./git/unstaged.js";
26
+ import { GitUntrackedWidget } from "./git/untracked.js";
27
+ import { WidgetInstance } from "./instance.js";
28
+ import { CustomTextWidget } from "./layout/custom-text.js";
29
+ import { FlexSeparatorWidget } from "./layout/flex-separator.js";
30
+ import { SeparatorWidget } from "./layout/separator.js";
31
+ import { SpacerWidget } from "./layout/spacer.js";
32
+ import { sanitizeOptionsFromSpec } from "./options.js";
33
+ import { RuntimeWidget } from "./project/runtime.js";
34
+ import { AssistantMessagesWidget } from "./session/assistant-messages.js";
35
+ import { CompactionsWidget } from "./session/compactions.js";
36
+ import { ElapsedWidget } from "./session/elapsed.js";
37
+ import { LastActivityWidget } from "./session/last-activity.js";
38
+ import { MessagesWidget } from "./session/messages.js";
39
+ import { SessionIdWidget } from "./session/session-id.js";
40
+ import { SessionStartWidget } from "./session/session-start.js";
41
+ import { ToolResultsWidget } from "./session/tool-results.js";
42
+ import { TotalMessagesWidget } from "./session/total-messages.js";
43
+ import { TotalTimeWidget } from "./session/total-time.js";
44
+ import { UserMessagesWidget } from "./session/user-messages.js";
45
+ import { CacheHitRateWidget } from "./tokens/cache-hit-rate.js";
46
+ import { CacheReadWidget } from "./tokens/cache-read.js";
47
+ import { CacheWriteWidget } from "./tokens/cache-write.js";
48
+ import { ContextBarWidget } from "./tokens/context-bar.js";
49
+ import { ContextLengthWidget } from "./tokens/context-length.js";
50
+ import { ContextRemainingWidget } from "./tokens/context-remaining.js";
51
+ import { ContextWidget } from "./tokens/context.js";
52
+ import { CostWidget } from "./tokens/cost.js";
53
+ import { InputSpeedWidget } from "./tokens/input-speed.js";
54
+ import { InputTokensWidget } from "./tokens/input-tokens.js";
55
+ import { OutputSpeedWidget } from "./tokens/output-speed.js";
56
+ import { OutputTokensWidget } from "./tokens/output-tokens.js";
57
+ import { TokensWidget } from "./tokens/tokens.js";
58
+ import { TotalSpeedWidget } from "./tokens/total-speed.js";
59
+ import { TotalTokensWidget } from "./tokens/total-tokens.js";
60
+ import type { Widget } from "./types.js";
61
+
62
+ const WIDGETS = [
63
+ ModelWidget,
64
+ ProviderWidget,
65
+ ModelProviderWidget,
66
+ ThinkingLevelWidget,
67
+ TextVerbosityWidget,
68
+ ContextWindowWidget,
69
+ ActiveToolsWidget,
70
+ SessionNameWidget,
71
+ CwdWidget,
72
+ CwdBasenameWidget,
73
+ EventValueWidget,
74
+ ExtensionStatusWidget,
75
+ CustomTextWidget,
76
+ SeparatorWidget,
77
+ SpacerWidget,
78
+ FlexSeparatorWidget,
79
+ RuntimeWidget,
80
+ AssistantMessagesWidget,
81
+ CompactionsWidget,
82
+ ElapsedWidget,
83
+ LastActivityWidget,
84
+ MessagesWidget,
85
+ SessionIdWidget,
86
+ SessionStartWidget,
87
+ ToolResultsWidget,
88
+ TotalMessagesWidget,
89
+ TotalTimeWidget,
90
+ UserMessagesWidget,
91
+ GitAheadBehindWidget,
92
+ GitBranchWidget,
93
+ GitCleanStatusWidget,
94
+ GitDeletionsWidget,
95
+ GitDiffWidget,
96
+ GitInsertionsWidget,
97
+ GitRemoteWidget,
98
+ GitRootDirWidget,
99
+ GitShaWidget,
100
+ GitStagedWidget,
101
+ GitStatusWidget,
102
+ GitUnstagedWidget,
103
+ GitUntrackedWidget,
104
+ ContextBarWidget,
105
+ ContextLengthWidget,
106
+ ContextWidget,
107
+ ContextRemainingWidget,
108
+ CostWidget,
109
+ CacheReadWidget,
110
+ CacheWriteWidget,
111
+ CacheHitRateWidget,
112
+ TokensWidget,
113
+ InputTokensWidget,
114
+ OutputTokensWidget,
115
+ TotalTokensWidget,
116
+ InputSpeedWidget,
117
+ OutputSpeedWidget,
118
+ TotalSpeedWidget,
119
+ ] as const;
120
+
121
+ export type WidgetSpecUnion = (typeof WIDGETS)[number];
122
+ export type WidgetType = WidgetSpecUnion["type"];
123
+ export type WidgetCategory = WidgetSpecUnion["category"];
124
+
125
+ export interface WidgetDefinition {
126
+ readonly type: WidgetType;
127
+ readonly label: string;
128
+ readonly category: WidgetCategory;
129
+ readonly description: string;
130
+ }
131
+
132
+ interface WidgetRegistry {
133
+ readonly specs: readonly WidgetSpecUnion[];
134
+ readonly definitions: readonly WidgetDefinition[];
135
+ readonly types: readonly WidgetType[];
136
+ readonly typeSet: ReadonlySet<WidgetType>;
137
+
138
+ spec(type: WidgetType): WidgetSpecUnion;
139
+ maybeSpec(type: string): WidgetSpecUnion | undefined;
140
+ createEntry(type: WidgetType, options?: Record<string, unknown>): WidgetEntry;
141
+ cloneEntry(entry: WidgetEntry): WidgetEntry;
142
+ normalizeOptions(type: WidgetType, input: Record<string, unknown>): WidgetOptions;
143
+ createWidget(type: WidgetType, options?: Record<string, unknown>): Widget;
144
+ cloneWidget(widget: Widget): Widget;
145
+ hydrateWidget(entry: WidgetEntry): Widget;
146
+ }
147
+
148
+ function createWidgetRegistry(widgets: readonly WidgetSpecUnion[]): WidgetRegistry {
149
+ const specs = [...widgets];
150
+ const specsByType = new Map<WidgetType, WidgetSpecUnion>(
151
+ specs.map((spec) => [spec.type as WidgetType, spec]),
152
+ );
153
+ const definitions = specs.map(definitionFromWidgetSpec);
154
+ const types = definitions.map((definition) => definition.type);
155
+ const typeSet = new Set<WidgetType>(types);
156
+
157
+ const specFor = (type: WidgetType): WidgetSpecUnion => {
158
+ const spec = specsByType.get(type);
159
+ if (!spec) throw new Error(`Unsupported widget type: ${type}`);
160
+ return spec;
161
+ };
162
+
163
+ const buildEntry = (
164
+ type: WidgetType,
165
+ options: Record<string, unknown> = {},
166
+ enabled = true,
167
+ ): WidgetEntry => ({
168
+ id: `${type}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
169
+ type,
170
+ enabled,
171
+ options: sanitizeOptionsFromSpec(specFor(type), options),
172
+ });
173
+
174
+ return {
175
+ specs,
176
+ definitions,
177
+ types,
178
+ typeSet,
179
+ spec(type) {
180
+ return specFor(type);
181
+ },
182
+ maybeSpec(type) {
183
+ return specsByType.get(type as WidgetType);
184
+ },
185
+ createEntry(type, options = {}) {
186
+ return buildEntry(type, options);
187
+ },
188
+ normalizeOptions(type, input) {
189
+ return sanitizeOptionsFromSpec(specFor(type), input);
190
+ },
191
+ cloneEntry(entry) {
192
+ return buildEntry(entry.type, entry.options, entry.enabled);
193
+ },
194
+ createWidget(type, options = {}) {
195
+ return new WidgetInstance(specFor(type), buildEntry(type, options));
196
+ },
197
+ cloneWidget(widget) {
198
+ return new WidgetInstance(
199
+ specFor(widget.type),
200
+ buildEntry(widget.type, widget.options, widget.enabled),
201
+ );
202
+ },
203
+ hydrateWidget(entry) {
204
+ return new WidgetInstance(specFor(entry.type), {
205
+ id: entry.id,
206
+ type: entry.type,
207
+ enabled: entry.enabled,
208
+ options: { ...entry.options },
209
+ });
210
+ },
211
+ };
212
+ }
213
+
214
+ function definitionFromWidgetSpec(spec: WidgetSpecUnion): WidgetDefinition {
215
+ return {
216
+ type: spec.type as WidgetType,
217
+ label: spec.label,
218
+ category: spec.category as WidgetCategory,
219
+ description: spec.description,
220
+ };
221
+ }
222
+
223
+ export const registry = createWidgetRegistry(WIDGETS);