rinco-pi-hud 1.3.0 → 1.3.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
@@ -113,7 +113,7 @@ See [docs/footer.md](docs/footer.md) for the full variable and segment reference
113
113
  | [`extensions/hud/ui/`](extensions/hud/ui/) | Icons and terminal style utilities |
114
114
  | [`docs/footer.md`](docs/footer.md) | Complete footer configuration reference |
115
115
  | [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) | Local development, testing guidance, and pull request checklist |
116
- | [`tests/`](tests/) | Vitest suite: footer layout, telemetry, provider balances, MCP parsing |
116
+ | [`tests/`](tests/) | Vitest suite: footer layout, telemetry, provider balances, MCP configuration counts |
117
117
 
118
118
  ## Development
119
119
 
@@ -50,7 +50,8 @@ npm run pack:check
50
50
 
51
51
  Tests live in `tests/`:
52
52
 
53
- - `zentui-status.test.ts` — Config counts, MCP status parsing, and extension status helpers.
53
+ - `zentui-status.test.ts` — Config counts, MCP configuration counts, and extension status helpers.
54
+ - `mcp-footer.test.ts` — MCP count rendering, segment controls, custom formats, and legacy third-party statuses.
54
55
  - `codex-usage.test.ts` — Codex payload normalization and model-specific quota formatting.
55
56
  - `footer-layout.test.ts` — Four-group categorized layout, segment wrapping, and narrow-width budget.
56
57
  - `footer-groups.test.ts` — Semantic Project, Session, Activity, and Usage segment ordering.
@@ -113,7 +113,7 @@ Usage 行跟随当前模型的 Provider:
113
113
  | [`extensions/hud/ui/`](../extensions/hud/ui/) | 图标与终端样式工具 |
114
114
  | [`docs/footer.md`](footer.md) | 完整脚注配置参考 |
115
115
  | [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) | 本地开发、测试规范和 PR 检查清单 |
116
- | [`tests/`](../tests/) | Vitest 测试套件:脚注布局、遥测、各家余额与 MCP 解析 |
116
+ | [`tests/`](../tests/) | Vitest 测试套件:脚注布局、遥测、各家余额与 MCP 配置计数 |
117
117
 
118
118
  ## 开发
119
119
 
package/docs/footer.md CHANGED
@@ -7,10 +7,12 @@ When `footerFormat` is empty, the HUD displays four semantic groups:
7
7
  ```text
8
8
  Project · pi-sakura ·  main ·  24.18
9
9
  Session · gpt-5.3 · Thinking level: high · Turn 8
10
- Activity · Agent idle · Skill 2/3 · MCP 2/2
10
+ Activity · Agent idle · Skill 2/3 · MCP 2
11
11
  Usage · 35%/200k · ↑ 4.2k · ↓ 1.1k · $ 0.030
12
12
  ```
13
13
 
14
+ The MCP segment counts server entries in Pi's agent-directory `mcp.json` and the project's `.pi/mcp.json`. Same-name entries count once; disabled entries are included. Zero entries hide the segment. Counts refresh with the project state polling interval. This does not report connection state, validate individual server settings, or include servers registered dynamically by extensions. Missing, unreadable, malformed, or larger-than-1-MiB files contribute no entries. Legacy adapter footer statuses remain ordinary third-party statuses.
15
+
14
16
  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
17
 
16
18
  Control each default-layout row independently:
@@ -50,7 +52,7 @@ The command shape is `/zentui row <project|session|activity|usage> <enable|disab
50
52
  | `activeAgents` | on | Activity | Active primary agent runs |
51
53
  | `agentIdle` | on | Activity | `Agent idle` when no primary agent run is active |
52
54
  | `skills` | on | Activity | Active/available skills count, e.g. `Skill 1/3` |
53
- | `mcp` | on | Activity | MCP connected/total servers |
55
+ | `mcp` | on | Activity | File-configured MCP servers, e.g. `MCP 2` |
54
56
  | `context` | on | Usage | Context percentage, window size, and/or gauge |
55
57
  | `inputTokens` | on | Usage | Cumulative input tokens |
56
58
  | `outputTokens` | on | Usage | Cumulative output tokens |
@@ -119,7 +121,7 @@ Clear it:
119
121
  | `$skills` | Active/available skills, e.g. `Skill 1/3` |
120
122
  | `$active_skills` | Active skills count only |
121
123
  | `$extensions` | Installed Pi packages count |
122
- | `$mcp` | MCP connected/total |
124
+ | `$mcp` | File-configured MCP server count |
123
125
  | `$tool_counts` | Native tool completion counts |
124
126
  | `$running_tools` | Recently running tools |
125
127
  | `$active_agents` | Active primary agent runs |
@@ -133,7 +133,7 @@ const footerSegmentSettingLabels: Record<FooterSegmentSettingId, string> = {
133
133
  activeAgents: "Active agents",
134
134
  agentIdle: "Agent idle status",
135
135
  skills: "Skill activity",
136
- mcp: "MCP connections",
136
+ mcp: "MCP configured servers",
137
137
  context: "Context usage",
138
138
  inputTokens: "Input tokens",
139
139
  outputTokens: "Output tokens",
@@ -173,7 +173,7 @@ const footerSegmentSettingDescriptions: Record<FooterSegmentSettingId, string> =
173
173
  codexUsage: "Show OpenAI Codex subscription rate limits on the usage row when available.",
174
174
  configCounts: "Show instruction-file and installed Pi package counts on the project row.",
175
175
  skills: "Show active/available Pi skill counts on the activity row.",
176
- mcp: "Show parsed MCP connected/total server counts on the activity row.",
176
+ mcp: "Show the configured server count from global and project mcp.json, including disabled entries.",
177
177
  runningTools: "Show currently running native tools and their elapsed time.",
178
178
  toolCounts: "Show cumulative completed native-tool counts.",
179
179
  activeAgents: "Show active main agent runs on the activity row (not subagent count).",
@@ -1,6 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { readFileSync, statSync } from "node:fs";
3
3
  import { isAbsolute, join } from "node:path";
4
+ import { countConfiguredMcpServers } from "../segments/mcp-status.js";
4
5
 
5
6
  export interface ConfigCounts {
6
7
  instructionFiles: {
@@ -9,6 +10,7 @@ export interface ConfigCounts {
9
10
  total: number;
10
11
  };
11
12
  packages: number;
13
+ mcpServers: number;
12
14
  }
13
15
 
14
16
  export interface ConfigCountOptions {
@@ -82,6 +84,7 @@ export function countConfigEntries(cwd: string, options: ConfigCountOptions = {}
82
84
  total: agentsMd + claudeMd,
83
85
  },
84
86
  packages: countPackages(agentDir),
87
+ mcpServers: countConfiguredMcpServers(cwd, agentDir),
85
88
  };
86
89
  }
87
90
 
@@ -31,7 +31,6 @@ import {
31
31
  } from "../telemetry/format.js";
32
32
  import { resolveRuntimeSymbol } from "../ui/icons.js";
33
33
  import type { LiveContextOverride } from "../session/live-context.js";
34
- import { parseMcpStatus } from "../segments/mcp-status.js";
35
34
  import type { FooterState } from "../state/index.js";
36
35
  import { renderStyleForSource } from "../ui/style.js";
37
36
 
@@ -74,7 +73,6 @@ export function installFooter(
74
73
  const colorSource = config.colorSources.starship;
75
74
  const iconMode = config.icons.mode;
76
75
  const rawExtensionStatuses = footerData.getExtensionStatuses();
77
- const mcpStatus = parseMcpStatus(rawExtensionStatuses.get("mcp"));
78
76
  const { safeCwd, safeRuntime, safePackageVersion, safeCommit } = sanitizeFooterInputs(
79
77
  ctx,
80
78
  state,
@@ -166,7 +164,7 @@ export function installFooter(
166
164
  runningToolsLabel,
167
165
  activeAgentsLabel,
168
166
  agentIdleLabel,
169
- } = buildTelemetryLabels(ctx, state, config, theme, colorSource, mcpStatus);
167
+ } = buildTelemetryLabels(ctx, state, config, theme, colorSource);
170
168
  const renderVariable = (name: string): string => {
171
169
  const canonical = FOOTER_FORMAT_ALIASES[name] ?? name;
172
170
  switch (canonical) {
@@ -439,14 +437,7 @@ export function installFooter(
439
437
  formatTimeLabel(""),
440
438
  )}`
441
439
  : "";
442
- const formatNeedsMcp = /\$\{?mcp\b/.test(config.footerFormat);
443
- const dedicatedMcpVisible = Boolean(
444
- mcpStatus && (config.footerFormat ? formatNeedsMcp : config.footerSegments.mcp),
445
- );
446
- const extensionStatusSource = dedicatedMcpVisible
447
- ? new Map([...rawExtensionStatuses].filter(([key]) => key !== "mcp"))
448
- : rawExtensionStatuses;
449
- const extensionStatuses = collectExtensionStatusSegments(extensionStatusSource, config);
440
+ const extensionStatuses = collectExtensionStatusSegments(rawExtensionStatuses, config);
450
441
  const renderExtensionStatus = (segment: ExtensionStatusSegment) =>
451
442
  segment.colorMode === "original"
452
443
  ? segment.text
@@ -11,7 +11,6 @@
11
11
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
12
12
  import { truncateToWidth } from "@earendil-works/pi-tui";
13
13
  import type { ColorSource, PolishedTuiConfig } from "../config/config.js";
14
- import type { McpStatus } from "../segments/mcp-status.js";
15
14
  import { formatSkillCounts } from "../segments/skill-activity.js";
16
15
  import type { FooterState } from "../state/index.js";
17
16
  import { getTelemetryStats } from "../state/telemetry.js";
@@ -164,7 +163,6 @@ export function buildTelemetryLabels(
164
163
  config: PolishedTuiConfig,
165
164
  theme: ThemeLike,
166
165
  colorSource: ColorSource,
167
- mcpStatus: McpStatus | undefined,
168
166
  ): TelemetryLabels {
169
167
  const statusStyle = (style: string, text: string) =>
170
168
  text ? renderStyleForSource(theme, colorSource, style, text) : "";
@@ -267,9 +265,10 @@ export function buildTelemetryLabels(
267
265
  : "";
268
266
  const configCountsLabel = [instructionFilesLabel, extensionsLabel].filter(Boolean).join(" ");
269
267
 
270
- const mcpLabel = mcpStatus
271
- ? statusStyle(config.colors.gitStatus, `MCP ${mcpStatus.connected}/${mcpStatus.total}`)
272
- : "";
268
+ const mcpLabel =
269
+ state.configCounts.mcpServers > 0
270
+ ? statusStyle(config.colors.gitStatus, `MCP ${state.configCounts.mcpServers}`)
271
+ : "";
273
272
 
274
273
  const telemetryStats = getTelemetryStats(state.telemetry);
275
274
  const toolCountsText = Object.entries(telemetryStats.completedToolCounts)
@@ -1,26 +1,31 @@
1
- import { stripVTControlCharacters } from "node:util";
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
2
3
 
3
- export interface McpStatus {
4
- connected: number;
5
- total: number;
4
+ const MAX_CONFIG_BYTES = 1024 * 1024;
5
+
6
+ function isRecord(value: unknown): value is Record<string, unknown> {
7
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6
8
  }
7
9
 
8
- /** Parses the adapter's public status text without intercepting or modifying UI methods. */
9
- export function parseMcpStatus(text: string | undefined): McpStatus | undefined {
10
- if (typeof text !== "string" || text.length === 0) return undefined;
11
- const status = stripVTControlCharacters(text.slice(0, 2048));
12
- const enabledMatch = status.match(
13
- /MCP:\s*(\d{1,5})\s+servers?\s+enabled(?:\s*\(\s*(\d{1,5})\s+connected\s*\))?(?:\s*\(\s*(\d{1,5})\s+disabled\s*\))?\s*$/i,
14
- );
15
- const legacyMatch = status.match(/MCP:\s*(\d{1,5})\s*\/\s*(\d{1,5})\s+servers\b/i);
16
- if (!enabledMatch && !legacyMatch) return undefined;
17
- const connected = Number(enabledMatch ? (enabledMatch[2] ?? 0) : legacyMatch![1]);
18
- const total = Number(enabledMatch ? enabledMatch[1] : legacyMatch![2]);
19
- const disabled = enabledMatch?.[3] === undefined ? 0 : Number(enabledMatch[3]);
20
- if (![connected, total, disabled].every(Number.isSafeInteger)) return undefined;
21
- if (connected < 0 || total < 0 || connected > total || total > 10_000 || disabled > 10_000)
22
- return undefined;
23
- return { connected, total };
10
+ function readServerNames(path: string): string[] {
11
+ try {
12
+ const stats = statSync(path);
13
+ if (!stats.isFile() || stats.size > MAX_CONFIG_BYTES) return [];
14
+ const config: unknown = JSON.parse(readFileSync(path, "utf8"));
15
+ if (!isRecord(config) || !isRecord(config.mcpServers)) return [];
16
+ return Object.entries(config.mcpServers)
17
+ .filter(([name, entry]) => /^[A-Za-z0-9_-]+$/.test(name) && isRecord(entry))
18
+ .map(([name]) => name);
19
+ } catch {
20
+ return [];
21
+ }
24
22
  }
25
23
 
26
- export const parseMcpAdapterStatus = parseMcpStatus;
24
+ /** Count file-configured server entries, including disabled ones, without resolving secrets
25
+ * or starting connections. Project entries override global entries with the same name.
26
+ * This is not a connection count or a validation of individual server settings. */
27
+ export function countConfiguredMcpServers(cwd: string, agentDir: string): number {
28
+ const globalNames = agentDir ? readServerNames(join(agentDir, "mcp.json")) : [];
29
+ const projectNames = readServerNames(join(cwd, ".pi", "mcp.json"));
30
+ return new Set([...globalNames, ...projectNames]).size;
31
+ }
@@ -42,6 +42,7 @@ export function createInitialState(gitDefaults: GitStatusSummary): FooterState {
42
42
  configCounts: {
43
43
  instructionFiles: { agentsMd: 0, claudeMd: 0, total: 0 },
44
44
  packages: 0,
45
+ mcpServers: 0,
45
46
  },
46
47
  skillCounts: undefined,
47
48
  codexUsageStatus: undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rinco-pi-hud",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "A dynamic HUD footer for Pi, ported from Sakura Zentui Footer.",
5
5
  "type": "module",
6
6
  "license": "MIT",