rinco-pi-hud 1.1.0 → 1.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/README.md CHANGED
@@ -70,6 +70,10 @@ The HUD footer is enabled by default. Run `/zentui` to open the interactive sett
70
70
  /zentui statusline enable
71
71
  /zentui statusline disable
72
72
  /zentui statusline toggle
73
+ /zentui row project toggle
74
+ /zentui row session disable
75
+ /zentui row activity enable
76
+ /zentui row usage toggle
73
77
  /zentui format clear
74
78
  /zentui format "$model · $context · $cost · $git_branch( $git_commit) · $session_duration"
75
79
  /codex-status
@@ -121,6 +125,14 @@ Open `/zentui`, then select **Built-in segments** to enable or disable individua
121
125
 
122
126
  Existing configs using the former aggregate `tokens`, `toolActivity`, or `agentActivity` switches are migrated automatically. The whole footer can still be controlled with `/zentui statusline enable|disable|toggle`.
123
127
 
128
+ In the default four-row layout, each semantic row can also be controlled directly:
129
+
130
+ ```text
131
+ /zentui row <project|session|activity|usage> <enable|disable|toggle>
132
+ ```
133
+
134
+ Row switches are persisted in `footerRows`. They do not affect a custom single-line `footerFormat`.
135
+
124
136
  ### Extension statuses
125
137
 
126
138
  Third-party extensions publish statuses that the HUD footer can display. Configure placement (`left`, `middle`, `right`, `off`) and color mode (`zentui`, `original`) for each extension status key.
@@ -164,11 +176,10 @@ Check types:
164
176
  npm run typecheck
165
177
  ```
166
178
 
167
- Verify tests and package contents:
179
+ Verify formatting, types, lint rules, tests, and package contents:
168
180
 
169
181
  ```bash
170
- npm test
171
- npm run typecheck
182
+ npm run verify
172
183
  npm run pack:check
173
184
  ```
174
185
 
@@ -39,11 +39,12 @@ Pi loads the TypeScript extension directly, so this project has no separate buil
39
39
  Before submitting a change, run:
40
40
 
41
41
  ```bash
42
- npm test
43
- npm run typecheck
42
+ npm run verify
44
43
  npm run pack:check
45
44
  ```
46
45
 
46
+ `npm run verify` checks Prettier formatting, TypeScript types, ESLint rules, and the Vitest suite.
47
+
47
48
  Tests live in `tests/`:
48
49
 
49
50
  - `zentui-status.test.ts` — Config counts, MCP status parsing, and extension status helpers.
@@ -51,6 +52,7 @@ Tests live in `tests/`:
51
52
  - `footer-layout.test.ts` — Four-group categorized layout, segment wrapping, and narrow-width budget.
52
53
  - `footer-groups.test.ts` — Semantic Project, Session, Activity, and Usage segment ordering.
53
54
  - `footer-segment-controls.test.ts` — Independent status controls and legacy-config migration.
55
+ - `footer-row-controls.test.ts` — Per-row command parsing, persistence, and configuration defaults.
54
56
  - `model-switch-statusline.test.ts` — Model switch statusline integration.
55
57
  - `model-usage-refresh.test.ts` — Model usage refresh integration.
56
58
  - `token-switch-usage.test.ts` — Token Switch balance formatting.
@@ -67,10 +69,19 @@ In addition to automated tests, validate HUD behavior in a real Pi TUI across di
67
69
 
68
70
  ## Code style
69
71
 
70
- The project currently has no configured linter, formatter, or pre-commit hook. Follow the existing conventions:
72
+ The project uses ESLint for code-quality checks and Prettier for deterministic formatting:
73
+
74
+ ```bash
75
+ npm run lint
76
+ npm run lint:fix
77
+ npm run format:check
78
+ npm run format
79
+ ```
80
+
81
+ Follow the configured conventions:
71
82
 
72
83
  - Use TypeScript ESM and explicit types.
73
- - Use two-space indentation, double quotes, and semicolons.
84
+ - Use tab indentation, double quotes, and semicolons.
74
85
  - Keep extension lifecycle handling clear and release timers and UI state during `session_shutdown`.
75
86
  - Do not edit content enclosed by `AUTO-GENERATED` markers manually; regenerate it from the source identified in the marker.
76
87
 
@@ -116,8 +127,7 @@ Before opening a pull request, confirm that:
116
127
 
117
128
  - [ ] The change has a focused scope and contains no unrelated files.
118
129
  - [ ] New behavior or bug fixes include appropriate tests.
119
- - [ ] `npm test` passes.
120
- - [ ] `npm run typecheck` passes.
130
+ - [ ] `npm run verify` passes (format, types, lint, and tests).
121
131
  - [ ] `npm run pack:check` passes and the package contains the expected files.
122
132
  - [ ] Affected HUD behavior was verified in a real Pi TUI.
123
133
  - [ ] Related documentation was synchronized from its source of truth.
@@ -70,6 +70,10 @@ HUD 脚注默认启用。运行 `/zentui` 打开交互式设置。
70
70
  /zentui statusline enable
71
71
  /zentui statusline disable
72
72
  /zentui statusline toggle
73
+ /zentui row project toggle
74
+ /zentui row session disable
75
+ /zentui row activity enable
76
+ /zentui row usage toggle
73
77
  /zentui format clear
74
78
  /zentui format "$model · $context · $cost · $git_branch( $git_commit) · $session_duration"
75
79
  /codex-status
@@ -121,6 +125,14 @@ HUD 脚注默认启用。运行 `/zentui` 打开交互式设置。
121
125
 
122
126
  使用旧版聚合开关 `tokens`、`toolActivity` 或 `agentActivity` 的配置会自动迁移。仍可通过 `/zentui statusline enable|disable|toggle` 控制整个脚注。
123
127
 
128
+ 默认四行布局中的每个语义行也可以通过命令单独控制:
129
+
130
+ ```text
131
+ /zentui row <project|session|activity|usage> <enable|disable|toggle>
132
+ ```
133
+
134
+ 行开关会持久化到 `footerRows`。使用自定义单行 `footerFormat` 时,这些开关不生效。
135
+
124
136
  ### 扩展状态
125
137
 
126
138
  第三方扩展发布的状态可由 HUD 脚注显示。可为每个扩展状态键配置位置(`left`、`middle`、`right`、`off`)和颜色模式(`zentui`、`original`)。
@@ -164,11 +176,10 @@ npm test
164
176
  npm run typecheck
165
177
  ```
166
178
 
167
- 验证测试与发布内容:
179
+ 验证格式、类型、Lint、测试与发布内容:
168
180
 
169
181
  ```bash
170
- npm test
171
- npm run typecheck
182
+ npm run verify
172
183
  npm run pack:check
173
184
  ```
174
185
 
package/docs/footer.md CHANGED
@@ -13,6 +13,17 @@ Usage · 35%/200k · ↑ 4.2k · ↓ 1.1k · $ 0.030
13
13
 
14
14
  The category column is sized from visible text so every separator aligns. When a row exceeds the terminal width, complete status segments move to an indented continuation line. Only a single segment that is wider than the available content area is truncated. Third-party extension statuses appear on the Activity group in configured `left`, `middle`, then `right` order.
15
15
 
16
+ Control each default-layout row independently:
17
+
18
+ ```text
19
+ /zentui row project enable
20
+ /zentui row session disable
21
+ /zentui row activity toggle
22
+ /zentui row usage toggle
23
+ ```
24
+
25
+ The command shape is `/zentui row <project|session|activity|usage> <enable|disable|toggle>`. Values persist under `footerRows`; all four default to `true`. Row switches apply only to the default layout and do not alter a custom single-line `footerFormat`.
26
+
16
27
  ## Built-in status segments
17
28
 
18
29
  | Key | Default | Group | Description |
@@ -6,6 +6,7 @@ import {
6
6
  saveExtensionStatusColorMode,
7
7
  saveExtensionStatusPlacement,
8
8
  saveFooterFormatPatch,
9
+ saveFooterRowsPatch,
9
10
  saveFooterSegmentsPatch,
10
11
  saveGitBranchPatch,
11
12
  saveIconsModePatch,
@@ -39,6 +40,9 @@ export function registerSettingsController(pi: ExtensionAPI, deps: SettingsContr
39
40
  updateConfig(saveUiFeaturesPatch(patch));
40
41
  deps.applyConfiguredUi(ctx);
41
42
  },
43
+ setFooterRows(patch) {
44
+ updateConfig(saveFooterRowsPatch(patch));
45
+ },
42
46
  setFooterSegments(patch) {
43
47
  updateConfig(saveFooterSegmentsPatch(patch));
44
48
  deps.restartSessionTimer();
@@ -15,6 +15,7 @@ import {
15
15
  type ContextStyle,
16
16
  type ExtensionStatusColorMode,
17
17
  type ExtensionStatusPlacement,
18
+ type FooterRowsConfig,
18
19
  type FooterSegmentsConfig,
19
20
  type GitBranchConfig,
20
21
  type GitBranchMaxLength,
@@ -64,20 +65,13 @@ type FeatureSettingId = keyof UiFeaturesConfig;
64
65
  type FooterSegmentSettingId = keyof FooterSegmentsConfig;
65
66
  type SettingsSection = (typeof settingsSections)[number];
66
67
  type LayoutSettingId =
67
- | "contextStyle"
68
- | "separator"
69
- | "pathDisplay"
70
- | "pathDepth"
71
- | "branchLength"
72
- | "iconMode";
68
+ "contextStyle" | "separator" | "pathDisplay" | "pathDepth" | "branchLength" | "iconMode";
73
69
 
74
70
  type SettingsCommandDeps = {
75
71
  getConfig: () => PolishedTuiConfig;
76
72
  setColorSources: (patch: Partial<ColorSourcesConfig>) => void;
77
- setUiFeatures: (
78
- patch: Partial<UiFeaturesConfig>,
79
- ctx: ExtensionContext,
80
- ) => void;
73
+ setUiFeatures: (patch: Partial<UiFeaturesConfig>, ctx: ExtensionContext) => void;
74
+ setFooterRows: (patch: Partial<FooterRowsConfig>) => void;
81
75
  setFooterSegments: (patch: Partial<FooterSegmentsConfig>) => void;
82
76
  setFooterFormat: (value: string) => void;
83
77
  setIconMode: (mode: IconMode) => void;
@@ -109,6 +103,13 @@ const featureSettingDescriptions: Record<FeatureSettingId, string> = {
109
103
  statusLine: "Enable or disable Zentui's custom footer/status line.",
110
104
  };
111
105
 
106
+ const footerRowLabels: Record<keyof FooterRowsConfig, string> = {
107
+ project: "Project row",
108
+ session: "Session row",
109
+ activity: "Activity row",
110
+ usage: "Usage row",
111
+ };
112
+
112
113
  const footerSegmentSettingLabels: Record<FooterSegmentSettingId, string> = {
113
114
  cwd: "Current directory",
114
115
  gitBranch: "Git branch",
@@ -183,6 +184,11 @@ const directCommandSuggestions = [
183
184
  "statusline enable",
184
185
  "statusline disable",
185
186
  "statusline toggle",
187
+ ...Object.keys(footerRowLabels).flatMap((row) => [
188
+ `row ${row} enable`,
189
+ `row ${row} disable`,
190
+ `row ${row} toggle`,
191
+ ]),
186
192
  "format clear",
187
193
  "format $cwd on $git_branch $fill $context",
188
194
  "format $cwd( on $git_branch)($git_status)$fill($context)( | $cost)",
@@ -317,7 +323,28 @@ function footerSegmentPatch(
317
323
  }
318
324
 
319
325
  function usageText(): string {
320
- return 'Usage: /zentui statusline [enable|disable|toggle] or /zentui format "<template>"';
326
+ return 'Usage: /zentui statusline [enable|disable|toggle], /zentui row [project|session|activity|usage] [enable|disable|toggle], or /zentui format "<template>"';
327
+ }
328
+
329
+ function isFooterRow(value: string): value is keyof FooterRowsConfig {
330
+ return value === "project" || value === "session" || value === "activity" || value === "usage";
331
+ }
332
+
333
+ function parseDirectRowCommand(
334
+ args: string,
335
+ config: PolishedTuiConfig,
336
+ ): { row: keyof FooterRowsConfig; enabled: boolean } | undefined {
337
+ const normalized = args.trim().toLowerCase().replaceAll(/[_-]+/g, " ");
338
+ const [command, row, action, ...rest] = normalized.split(/\s+/g);
339
+ if (command !== "row" || !row || !isFooterRow(row) || !action || rest.length > 0) {
340
+ return undefined;
341
+ }
342
+ if (action !== "enable" && action !== "disable" && action !== "toggle") return undefined;
343
+
344
+ return {
345
+ row,
346
+ enabled: action === "toggle" ? !config.footerRows[row] : action === "enable",
347
+ };
321
348
  }
322
349
 
323
350
  function parseDirectFeatureCommand(
@@ -329,9 +356,8 @@ function parseDirectFeatureCommand(
329
356
 
330
357
  const words = normalized.split(/\s+/g).filter(Boolean);
331
358
  const hasWord = (value: string) => words.includes(value);
332
- const feature = hasWord("footer") || hasWord("statusline") || hasWord("status")
333
- ? "statusLine"
334
- : undefined;
359
+ const feature =
360
+ hasWord("footer") || hasWord("statusline") || hasWord("status") ? "statusLine" : undefined;
335
361
  const action = hasWord("toggle")
336
362
  ? "toggle"
337
363
  : hasWord("enable") || hasWord("enabled") || hasWord("on")
@@ -568,6 +594,24 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
568
594
  return;
569
595
  }
570
596
 
597
+ const directRowCommand = parseDirectRowCommand(args, deps.getConfig());
598
+ if (directRowCommand) {
599
+ try {
600
+ deps.setFooterRows({ [directRowCommand.row]: directRowCommand.enabled });
601
+ deps.requestRender();
602
+ if (ctx.hasUI) {
603
+ ctx.ui.notify(
604
+ `${footerRowLabels[directRowCommand.row]}: ${featureValue(directRowCommand.enabled)}`,
605
+ "info",
606
+ );
607
+ }
608
+ } catch (error) {
609
+ const message = error instanceof Error ? error.message : String(error);
610
+ if (ctx.hasUI) ctx.ui.notify(`Could not update Zentui settings: ${message}`, "error");
611
+ }
612
+ return;
613
+ }
614
+
571
615
  const directCommand = parseDirectFeatureCommand(args, deps.getConfig());
572
616
  if (directCommand) {
573
617
  try {
@@ -35,11 +35,12 @@ function getPiAgentDir(): string {
35
35
  function resolveAgentDir(options: ConfigCountOptions): string {
36
36
  let agentDir = "";
37
37
  try {
38
- agentDir = typeof options.agentDir === "string"
39
- ? options.agentDir
40
- : options.getAgentDir
41
- ? options.getAgentDir()
42
- : getPiAgentDir();
38
+ agentDir =
39
+ typeof options.agentDir === "string"
40
+ ? options.agentDir
41
+ : options.getAgentDir
42
+ ? options.getAgentDir()
43
+ : getPiAgentDir();
43
44
  } catch {
44
45
  return "";
45
46
  }
@@ -19,6 +19,7 @@ export type {
19
19
  ExtensionStatusColorMode,
20
20
  ExtensionStatusesConfig,
21
21
  ExtensionStatusPlacement,
22
+ FooterRowsConfig,
22
23
  FooterSegmentsConfig,
23
24
  GitBranchConfig,
24
25
  GitBranchMaxLength,
@@ -56,6 +57,7 @@ export {
56
57
  saveExtensionStatusColorMode,
57
58
  saveExtensionStatusPlacement,
58
59
  saveFooterFormatPatch,
60
+ saveFooterRowsPatch,
59
61
  saveFooterSegmentsPatch,
60
62
  saveGitBranchPatch,
61
63
  saveIconsModePatch,
@@ -25,6 +25,7 @@ import {
25
25
  type ExtensionStatusColorMode,
26
26
  type ExtensionStatusesConfig,
27
27
  type ExtensionStatusPlacement,
28
+ type FooterRowsConfig,
28
29
  type FooterSegmentsConfig,
29
30
  type GitBranchConfig,
30
31
  type GitBranchMaxLength,
@@ -218,6 +219,18 @@ function normalizeUiFeatures(record: Record<string, unknown>): UiFeaturesConfig
218
219
  return { statusLine: booleanValue(record, "statusLine") };
219
220
  }
220
221
 
222
+ function normalizeFooterRows(record: Record<string, unknown>): FooterRowsConfig {
223
+ return {
224
+ project:
225
+ typeof record.project === "boolean" ? record.project : defaultConfig.footerRows.project,
226
+ session:
227
+ typeof record.session === "boolean" ? record.session : defaultConfig.footerRows.session,
228
+ activity:
229
+ typeof record.activity === "boolean" ? record.activity : defaultConfig.footerRows.activity,
230
+ usage: typeof record.usage === "boolean" ? record.usage : defaultConfig.footerRows.usage,
231
+ };
232
+ }
233
+
221
234
  function normalizeFooterSegments(record: Record<string, unknown>): FooterSegmentsConfig {
222
235
  return {
223
236
  cwd: footerSegmentValue(record, "cwd"),
@@ -335,6 +348,9 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
335
348
  const features = isRecord(config.features)
336
349
  ? normalizeUiFeatures(config.features as Record<string, unknown>)
337
350
  : defaultConfig.features;
351
+ const footerRows = isRecord(config.footerRows)
352
+ ? normalizeFooterRows(config.footerRows as Record<string, unknown>)
353
+ : defaultConfig.footerRows;
338
354
  const footerSegments = isRecord(config.footerSegments)
339
355
  ? normalizeFooterSegments(config.footerSegments as Record<string, unknown>)
340
356
  : defaultConfig.footerSegments;
@@ -363,6 +379,7 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
363
379
  },
364
380
  colorSources: { ...colorSources },
365
381
  features: { ...features },
382
+ footerRows: { ...footerRows },
366
383
  footerSegments: { ...footerSegments },
367
384
  gitCommit,
368
385
  gitMetrics,
@@ -24,6 +24,7 @@ import {
24
24
  type ContextThresholds,
25
25
  type ExtensionStatusColorMode,
26
26
  type ExtensionStatusPlacement,
27
+ type FooterRowsConfig,
27
28
  type FooterSegmentsConfig,
28
29
  type GitBranchConfig,
29
30
  type IconMode,
@@ -41,6 +42,10 @@ function isUiFeatureKey(value: string): value is keyof UiFeaturesConfig {
41
42
  return value === "statusLine";
42
43
  }
43
44
 
45
+ function isFooterRowKey(value: string): value is keyof FooterRowsConfig {
46
+ return value === "project" || value === "session" || value === "activity" || value === "usage";
47
+ }
48
+
44
49
  function isFooterSegmentKey(value: string): value is keyof FooterSegmentsConfig {
45
50
  return (
46
51
  value === "cwd" ||
@@ -94,6 +99,15 @@ function validUiFeatureEntries(record: Record<string, unknown>): Partial<UiFeatu
94
99
  ) as Partial<UiFeaturesConfig>;
95
100
  }
96
101
 
102
+ function validFooterRowEntries(record: Record<string, unknown>): Partial<FooterRowsConfig> {
103
+ return Object.fromEntries(
104
+ Object.entries(record).filter((entry): entry is [keyof FooterRowsConfig, boolean] => {
105
+ const [key, value] = entry;
106
+ return isFooterRowKey(key) && typeof value === "boolean";
107
+ }),
108
+ ) as Partial<FooterRowsConfig>;
109
+ }
110
+
97
111
  function validFooterSegmentEntries(record: Record<string, unknown>): Partial<FooterSegmentsConfig> {
98
112
  return Object.fromEntries(
99
113
  Object.entries(record).filter((entry): entry is [keyof FooterSegmentsConfig, boolean] => {
@@ -133,6 +147,21 @@ export function saveUiFeaturesPatch(
133
147
  });
134
148
  }
135
149
 
150
+ export function saveFooterRowsPatch(
151
+ patch: Partial<FooterRowsConfig>,
152
+ path = configPath,
153
+ ): PolishedTuiConfig {
154
+ return mutateConfig(path, (record) => {
155
+ const existing = isRecord(record.footerRows)
156
+ ? { ...(record.footerRows as Record<string, unknown>) }
157
+ : {};
158
+ record.footerRows = {
159
+ ...existing,
160
+ ...validFooterRowEntries(patch),
161
+ };
162
+ });
163
+ }
164
+
136
165
  export function saveFooterSegmentsPatch(
137
166
  patch: Partial<FooterSegmentsConfig>,
138
167
  path = configPath,
@@ -43,6 +43,13 @@ export type UiFeaturesConfig = {
43
43
  statusLine: boolean;
44
44
  };
45
45
 
46
+ export type FooterRowsConfig = {
47
+ project: boolean;
48
+ session: boolean;
49
+ activity: boolean;
50
+ usage: boolean;
51
+ };
52
+
46
53
  export type FooterSegmentsConfig = {
47
54
  cwd: boolean;
48
55
  gitBranch: boolean;
@@ -142,6 +149,7 @@ export type PolishedTuiConfig = {
142
149
  };
143
150
  colorSources: ColorSourcesConfig;
144
151
  features: UiFeaturesConfig;
152
+ footerRows: FooterRowsConfig;
145
153
  footerSegments: FooterSegmentsConfig;
146
154
  gitCommit: GitCommitConfig;
147
155
  gitMetrics: GitMetricsConfig;
@@ -258,6 +266,12 @@ export const defaultConfig: PolishedTuiConfig = {
258
266
  features: {
259
267
  statusLine: true,
260
268
  },
269
+ footerRows: {
270
+ project: true,
271
+ session: true,
272
+ activity: true,
273
+ usage: true,
274
+ },
261
275
  footerSegments: {
262
276
  cwd: true,
263
277
  gitBranch: true,
@@ -184,11 +184,7 @@ export function installFooter(
184
184
  return gitStateBlock;
185
185
  case "runtime": {
186
186
  if (!safeRuntime) return "";
187
- const symbol = resolveRuntimeSymbol(
188
- safeRuntime.name,
189
- safeRuntime.symbol,
190
- iconMode,
191
- );
187
+ const symbol = resolveRuntimeSymbol(safeRuntime.name, safeRuntime.symbol, iconMode);
192
188
  const label = safeRuntime.version ? `${symbol} ${safeRuntime.version}` : symbol;
193
189
  return renderStyleForSource(theme, colorSource, safeRuntime.style, label);
194
190
  }
@@ -263,12 +259,7 @@ export function installFooter(
263
259
  );
264
260
  case "git_tag":
265
261
  return config.gitCommit.showTag && safeCommit?.tag
266
- ? renderStyleForSource(
267
- theme,
268
- colorSource,
269
- config.colors.gitCommit,
270
- safeCommit.tag,
271
- )
262
+ ? renderStyleForSource(theme, colorSource, config.colors.gitCommit, safeCommit.tag)
272
263
  : "";
273
264
  case "git_metrics":
274
265
  return formatGitMetricsSegment(
@@ -533,7 +524,12 @@ export function installFooter(
533
524
  time: timeSegment,
534
525
  },
535
526
  });
536
- const contents = composeCategorizedFooterRows(groups, separator, innerWidth);
527
+ const contents = composeCategorizedFooterRows(
528
+ groups,
529
+ separator,
530
+ innerWidth,
531
+ config.footerRows,
532
+ );
537
533
  return contents.map(frame);
538
534
  },
539
535
  };
@@ -7,6 +7,14 @@ export type CategorizedFooterRows = {
7
7
  usage: string[];
8
8
  };
9
9
 
10
+ export type CategorizedFooterRowVisibility = Record<keyof CategorizedFooterRows, boolean>;
11
+
12
+ const DEFAULT_ROW_VISIBILITY: CategorizedFooterRowVisibility = {
13
+ project: true,
14
+ session: true,
15
+ activity: true,
16
+ usage: true,
17
+ };
10
18
  const MIN_WRAPPED_CONTENT_WIDTH = 4;
11
19
 
12
20
  function joinStatusTexts(statusTexts: string[], separator: string): string {
@@ -45,7 +53,11 @@ function prependStatusArea(base: string, statusText: string, separator: string):
45
53
  return `${statusText}${separator}${base}`;
46
54
  }
47
55
 
48
- export function composeBuiltInFooterContent(left: string, right: string, innerWidth: number): string {
56
+ export function composeBuiltInFooterContent(
57
+ left: string,
58
+ right: string,
59
+ innerWidth: number,
60
+ ): string {
49
61
  const leftWidth = visibleWidth(left);
50
62
  const rightWidth = visibleWidth(right);
51
63
  if (!right) return truncateToWidth(left, innerWidth, "");
@@ -69,17 +81,11 @@ export function composeBuiltInFooterContent(left: string, right: string, innerWi
69
81
  let fittedLeft = truncateToWidth(left, leftBudget, "");
70
82
  let fittedRight = truncateToWidth(right, rightBudget, "");
71
83
  for (let pass = 0; pass < 2; pass += 1) {
72
- let spare = Math.max(
73
- 0,
74
- available - visibleWidth(fittedLeft) - visibleWidth(fittedRight),
75
- );
84
+ let spare = Math.max(0, available - visibleWidth(fittedLeft) - visibleWidth(fittedRight));
76
85
  if (spare === 0) break;
77
86
  if (visibleWidth(fittedLeft) < leftWidth) {
78
87
  fittedLeft = truncateToWidth(left, visibleWidth(fittedLeft) + spare, "");
79
- spare = Math.max(
80
- 0,
81
- available - visibleWidth(fittedLeft) - visibleWidth(fittedRight),
82
- );
88
+ spare = Math.max(0, available - visibleWidth(fittedLeft) - visibleWidth(fittedRight));
83
89
  }
84
90
  if (spare > 0 && visibleWidth(fittedRight) < rightWidth) {
85
91
  fittedRight = truncateToWidth(right, visibleWidth(fittedRight) + spare, "");
@@ -231,10 +237,21 @@ export function composeCategorizedFooterRows(
231
237
  rows: CategorizedFooterRows,
232
238
  separator: string,
233
239
  innerWidth: number,
240
+ visibility: CategorizedFooterRowVisibility = DEFAULT_ROW_VISIBILITY,
234
241
  ): string[] {
235
- const groups = [rows.project, rows.session, rows.activity, rows.usage];
236
- const categoryWidth = Math.max(...groups.map((parts) => visibleWidth(categoryLabel(parts))));
237
- return groups.flatMap((parts) =>
242
+ const groups = [
243
+ { id: "project", parts: rows.project },
244
+ { id: "session", parts: rows.session },
245
+ { id: "activity", parts: rows.activity },
246
+ { id: "usage", parts: rows.usage },
247
+ ] as const;
248
+ const visibleGroups = groups.filter(({ id }) => visibility[id]);
249
+ if (visibleGroups.length === 0) return [];
250
+
251
+ const categoryWidth = Math.max(
252
+ ...visibleGroups.map(({ parts }) => visibleWidth(categoryLabel(parts))),
253
+ );
254
+ return visibleGroups.flatMap(({ parts }) =>
238
255
  composeWrappedCategorizedRow(parts, separator, innerWidth, categoryWidth),
239
256
  );
240
257
  }
@@ -27,12 +27,9 @@ export function safeStatusText(value: string | null | undefined, maxLength = 256
27
27
  export type SanitizedFooterInputs = {
28
28
  safeCwd: string;
29
29
  safeRuntime:
30
- | { name: string; symbol: string; style: string; version: string | undefined }
31
- | undefined;
30
+ { name: string; symbol: string; style: string; version: string | undefined } | undefined;
32
31
  safePackageVersion: { ecosystem: string; version: string } | undefined;
33
- safeCommit:
34
- | { oid: string | null; detached: boolean; tag: string | null }
35
- | undefined;
32
+ safeCommit: { oid: string | null; detached: boolean; tag: string | null } | undefined;
36
33
  };
37
34
 
38
35
  /** Sanitize the state/ctx fields that flow into the terminal so a malicious
@@ -178,13 +175,19 @@ export function buildTelemetryLabels(
178
175
  const usageCategoryLabel = statusStyle(config.colors.contextNormal, "Usage");
179
176
 
180
177
  const sessionName = safeStatusText(state.telemetry.sessionName, 256);
181
- const sessionNameLabel = statusStyle(config.colors.gitBranch, sessionName ? `◈ ${sessionName}` : "");
178
+ const sessionNameLabel = statusStyle(
179
+ config.colors.gitBranch,
180
+ sessionName ? `◈ ${sessionName}` : "",
181
+ );
182
182
 
183
183
  const providerId = safeStatusText(ctx.model?.provider, 128);
184
184
  const modelId = safeStatusText(ctx.model?.id, 256);
185
185
  const modelText = providerId && modelId ? `${providerId}/${modelId}` : modelId || providerId;
186
186
  const modelValueLabel = statusStyle(config.colors.runtimePrefix, modelText);
187
- const modelStatusLabel = statusStyle(config.colors.runtimePrefix, modelText ? `λ ${modelText}` : "");
187
+ const modelStatusLabel = statusStyle(
188
+ config.colors.runtimePrefix,
189
+ modelText ? `λ ${modelText}` : "",
190
+ );
188
191
 
189
192
  const thinkingLabel = state.telemetry.modelSupportsReasoning
190
193
  ? statusStyle(config.colors.extensionStatus, `Thinking level: ${state.telemetry.thinkingLevel}`)
@@ -99,7 +99,8 @@ export default function (pi: ExtensionAPI) {
99
99
  if (
100
100
  !currentConfig.features.statusLine ||
101
101
  !(needsWallClock || needsDuration || needsActivityClock)
102
- ) return;
102
+ )
103
+ return;
103
104
 
104
105
  const timer = setInterval(() => {
105
106
  if (!sessionLifecycle.isCurrent()) return;
@@ -7,11 +7,7 @@ const execFileAsync = promisify(execFile);
7
7
  const GIT_COMMAND_TIMEOUT_MS = 2_000;
8
8
 
9
9
  export type GitOperationState =
10
- | "REBASING"
11
- | "MERGING"
12
- | "CHERRY-PICKING"
13
- | "REVERTING"
14
- | "BISECTING";
10
+ "REBASING" | "MERGING" | "CHERRY-PICKING" | "REVERTING" | "BISECTING";
15
11
 
16
12
  /**
17
13
  * Starship `git_commit`-style info derived from the existing porcelain probe.
@@ -57,9 +53,7 @@ export type GitStatusSummary = {
57
53
  };
58
54
 
59
55
  export type GitReadResult =
60
- | { kind: "ok"; status: GitStatusSummary }
61
- | { kind: "not_a_repo" }
62
- | { kind: "error" };
56
+ { kind: "ok"; status: GitStatusSummary } | { kind: "not_a_repo" } | { kind: "error" };
63
57
 
64
58
  export type GitStatePaths = {
65
59
  rebaseMerge?: string;
@@ -18,7 +18,8 @@ export function parseMcpStatus(text: string | undefined): McpStatus | undefined
18
18
  const total = Number(enabledMatch ? enabledMatch[1] : legacyMatch![2]);
19
19
  const disabled = enabledMatch?.[3] === undefined ? 0 : Number(enabledMatch[3]);
20
20
  if (![connected, total, disabled].every(Number.isSafeInteger)) return undefined;
21
- if (connected < 0 || total < 0 || connected > total || total > 10_000 || disabled > 10_000) return undefined;
21
+ if (connected < 0 || total < 0 || connected > total || total > 10_000 || disabled > 10_000)
22
+ return undefined;
22
23
  return { connected, total };
23
24
  }
24
25
 
@@ -41,8 +41,7 @@ export type PackageVersionResult = {
41
41
  };
42
42
 
43
43
  export type PackageVersionReadResult =
44
- | { kind: "ok"; result: PackageVersionResult | null }
45
- | { kind: "error" };
44
+ { kind: "ok"; result: PackageVersionResult | null } | { kind: "error" };
46
45
 
47
46
  /**
48
47
  * A single manifest source. `kind` selects the lookup mode:
@@ -103,9 +103,9 @@ function matchesDetection(
103
103
  if (spec.excludedFiles && hasAnyFile(cwd, spec.excludedFiles)) return false;
104
104
  return Boolean(
105
105
  (spec.files && hasAnyFile(cwd, spec.files)) ||
106
- (spec.folders && hasAnyFolder(cwd, spec.folders)) ||
107
- (spec.extensions && hasAnyExtension(entries, spec.extensions)) ||
108
- spec.env?.(env),
106
+ (spec.folders && hasAnyFolder(cwd, spec.folders)) ||
107
+ (spec.extensions && hasAnyExtension(entries, spec.extensions)) ||
108
+ spec.env?.(env),
109
109
  );
110
110
  }
111
111
 
@@ -119,7 +119,11 @@ export class SkillActivityTracker {
119
119
  }
120
120
  continue;
121
121
  }
122
- if (message.role !== "toolResult" || message.toolName !== "read" || message.isError === true) {
122
+ if (
123
+ message.role !== "toolResult" ||
124
+ message.toolName !== "read" ||
125
+ message.isError === true
126
+ ) {
123
127
  continue;
124
128
  }
125
129
  const id = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
@@ -46,7 +46,7 @@ export type TelemetryUpdate =
46
46
  name: string;
47
47
  args?: Record<string, unknown> | null;
48
48
  at: number;
49
- }
49
+ }
50
50
  | { type: "tool-result"; toolCallId: string; isError: boolean; at: number }
51
51
  | { type: "agent-start"; at: number }
52
52
  | { type: "agent-end"; at: number };
@@ -80,7 +80,9 @@ function normalizeThinkingLevel(value: string | undefined): string {
80
80
 
81
81
  export function createTelemetryState(initial: TelemetryInitialValues = {}): TelemetryState {
82
82
  return {
83
- sessionName: initial.sessionName ? sanitizeExternalText(initial.sessionName) || undefined : undefined,
83
+ sessionName: initial.sessionName
84
+ ? sanitizeExternalText(initial.sessionName) || undefined
85
+ : undefined,
84
86
  turnIndex: initial.turnIndex ?? 0,
85
87
  thinkingLevel: normalizeThinkingLevel(initial.thinkingLevel),
86
88
  modelSupportsReasoning: initial.modelSupportsReasoning ?? false,
@@ -118,11 +120,12 @@ export function findToolTarget(state: TelemetryState, toolCallId: string): strin
118
120
 
119
121
  function getToolTarget(args: Record<string, unknown> | null | undefined): string | undefined {
120
122
  if (!args) return undefined;
121
- const value = typeof args.path === "string"
122
- ? args.path
123
- : typeof args.filePath === "string"
124
- ? args.filePath
125
- : undefined;
123
+ const value =
124
+ typeof args.path === "string"
125
+ ? args.path
126
+ : typeof args.filePath === "string"
127
+ ? args.filePath
128
+ : undefined;
126
129
  return value === undefined ? undefined : sanitizeToolTarget(value);
127
130
  }
128
131
 
@@ -205,7 +208,10 @@ function finishLatestAgentRun(state: TelemetryState, at: number): TelemetryState
205
208
  return { ...state, agentRuns };
206
209
  }
207
210
 
208
- export function updateTelemetryState(state: TelemetryState, update: TelemetryUpdate): TelemetryState {
211
+ export function updateTelemetryState(
212
+ state: TelemetryState,
213
+ update: TelemetryUpdate,
214
+ ): TelemetryState {
209
215
  switch (update.type) {
210
216
  case "metadata":
211
217
  return updateMetadata(state, update);
@@ -63,7 +63,8 @@ class CodexAppServerClient {
63
63
 
64
64
  start(): Promise<void> {
65
65
  if (this.startPromise) return this.startPromise;
66
- if (this.signal?.aborted) return Promise.reject(new Error("Codex app-server query was cancelled."));
66
+ if (this.signal?.aborted)
67
+ return Promise.reject(new Error("Codex app-server query was cancelled."));
67
68
  this.signal?.addEventListener("abort", this.abortHandler, { once: true });
68
69
 
69
70
  this.startPromise = new Promise((resolve, reject) => {
@@ -100,7 +101,10 @@ class CodexAppServerClient {
100
101
 
101
102
  child.stderr.setEncoding("utf8");
102
103
  child.stderr.on("data", (chunk: string) => {
103
- this.stderr = truncateEnd(`${this.stderr}${chunk.slice(0, MAX_ERROR_BODY_CHARS)}`, MAX_ERROR_BODY_CHARS);
104
+ this.stderr = truncateEnd(
105
+ `${this.stderr}${chunk.slice(0, MAX_ERROR_BODY_CHARS)}`,
106
+ MAX_ERROR_BODY_CHARS,
107
+ );
104
108
  });
105
109
  child.stderr.on("error", () => undefined);
106
110
  child.stdin.on("error", (error) => this.rejectAll(error));
@@ -205,10 +205,7 @@ function isPrimaryCodexSnapshot(snapshot: NormalizedRateLimitSnapshot): boolean
205
205
  );
206
206
  }
207
207
 
208
- function formatWindowLine(
209
- window: NormalizedRateLimitWindow,
210
- fallback: "5h" | "weekly",
211
- ): string {
208
+ function formatWindowLine(window: NormalizedRateLimitWindow, fallback: "5h" | "weekly"): string {
212
209
  const label = `${formatWindowLabel(window, fallback, false)} limit:`;
213
210
  return ` ${label.padEnd(LIMIT_VALUE_COLUMN)}${formatWindow(window)}`;
214
211
  }
@@ -8,18 +8,18 @@ const SECRET_QUERY_PATTERN =
8
8
  /([?&](?:access_token|refresh_token|id_token|session_token|client_secret|api_key|password)=)[^&#\s]+/gi;
9
9
 
10
10
  export function sanitizeCodexText(value: unknown, maxChars = 600): string {
11
- let text = typeof value === "string" ? value : value instanceof Error ? value.message : String(value);
12
- text = stripVTControlCharacters(text.slice(0, Math.max(maxChars * 4, maxChars)))
13
- .replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
11
+ let text =
12
+ typeof value === "string" ? value : value instanceof Error ? value.message : String(value);
13
+ text = stripVTControlCharacters(text.slice(0, Math.max(maxChars * 4, maxChars))).replace(
14
+ /[\u0000-\u001f\u007f-\u009f]/g,
15
+ "",
16
+ );
14
17
  text = text
15
18
  .replace(BEARER_PATTERN, "Bearer <redacted>")
16
19
  .replace(SECRET_FIELD_PATTERN, "$1<redacted>")
17
20
  .replace(COOKIE_PATTERN, "$1<redacted>")
18
21
  .replace(SECRET_QUERY_PATTERN, "$1<redacted>");
19
- return text
20
- .replace(/\s+/g, " ")
21
- .trim()
22
- .slice(0, maxChars);
22
+ return text.replace(/\s+/g, " ").trim().slice(0, maxChars);
23
23
  }
24
24
 
25
25
  export function safeCodexErrorMessage(error: unknown): string {
@@ -17,8 +17,7 @@ export type CachedReport = {
17
17
  };
18
18
 
19
19
  export type QueryUsageResult =
20
- | { ok: true; report: CodexUsageReport }
21
- | { ok: false; errors: UsageQueryError[] };
20
+ { ok: true; report: CodexUsageReport } | { ok: false; errors: UsageQueryError[] };
22
21
 
23
22
  export type UsageQueryError = {
24
23
  source: UsageSource;
@@ -13,9 +13,7 @@ type QueryTokenSwitchBalanceOptions = {
13
13
  signal?: AbortSignal;
14
14
  };
15
15
 
16
- export function isTokenSwitchModel(
17
- model: Pick<TokenSwitchModel, "provider"> | undefined,
18
- ): boolean {
16
+ export function isTokenSwitchModel(model: Pick<TokenSwitchModel, "provider"> | undefined): boolean {
19
17
  return model?.provider === TOKEN_SWITCH_PROVIDER_ID;
20
18
  }
21
19
 
@@ -92,10 +90,7 @@ export async function queryTokenSwitchBalance({
92
90
  };
93
91
 
94
92
  try {
95
- const subscriptionPayload = await fetchPayload(
96
- TOKEN_SWITCH_SUBSCRIPTION_URL,
97
- "subscription",
98
- );
93
+ const subscriptionPayload = await fetchPayload(TOKEN_SWITCH_SUBSCRIPTION_URL, "subscription");
99
94
  const usagePayload = await fetchPayload(TOKEN_SWITCH_USAGE_URL, "usage");
100
95
  return parseTokenSwitchBalance(subscriptionPayload, usagePayload);
101
96
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rinco-pi-hud",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "A dynamic HUD footer for Pi, ported from Sakura Zentui Footer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,11 +30,11 @@
30
30
  "test": "vitest run",
31
31
  "test:watch": "vitest",
32
32
  "typecheck": "tsc --noEmit",
33
- "lint": "biome lint .",
34
- "lint:fix": "biome lint --write .",
35
- "format:check": "biome format .",
36
- "format": "biome format --write .",
37
- "verify": "npm run typecheck && npm run lint && npm run test",
33
+ "lint": "eslint extensions tests",
34
+ "lint:fix": "eslint extensions tests --fix",
35
+ "format:check": "prettier --check \"extensions/**/*.ts\" \"tests/**/*.ts\" \"eslint.config.js\" \"prettier.config.js\"",
36
+ "format": "prettier --write \"extensions/**/*.ts\" \"tests/**/*.ts\" \"eslint.config.js\" \"prettier.config.js\"",
37
+ "verify": "npm run format:check && npm run typecheck && npm run lint && npm run test",
38
38
  "pack:check": "npm pack --dry-run"
39
39
  },
40
40
  "peerDependencies": {
@@ -47,10 +47,14 @@
47
47
  ]
48
48
  },
49
49
  "devDependencies": {
50
- "@biomejs/biome": "^2.5.7",
51
50
  "@earendil-works/pi-ai": "^0.83.0",
51
+ "@eslint/js": "^10.0.1",
52
52
  "@types/node": "^24.12.4",
53
+ "eslint": "^10.8.1",
54
+ "eslint-config-prettier": "^10.1.8",
55
+ "prettier": "^3.9.6",
53
56
  "typescript": "^5.9.3",
57
+ "typescript-eslint": "^8.67.0",
54
58
  "vitest": "^4.1.9"
55
59
  }
56
60
  }