@xpufx/paseo-top 0.4.0

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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/client/pill-labels.ts +480 -0
  4. package/client/pill.tsx +2461 -0
  5. package/client/resources-query.ts +68 -0
  6. package/client/settings-ui.tsx +50 -0
  7. package/client/surface.tsx +364 -0
  8. package/client/telemetry-copy.ts +105 -0
  9. package/client/telemetry.tsx +486 -0
  10. package/client/turn-counter.ts +151 -0
  11. package/client/turn-panel.tsx +182 -0
  12. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  13. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  14. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  15. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  16. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  17. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  18. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  19. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  20. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  21. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  22. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  23. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  24. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  25. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  26. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  27. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  28. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  29. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  30. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  31. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  32. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  33. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  34. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  35. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  36. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  37. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  38. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  39. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  40. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  41. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  42. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  43. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  44. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  45. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  46. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  47. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  48. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  49. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  50. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  51. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  52. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  53. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  54. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  55. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  56. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  57. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  58. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  59. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  60. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  61. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  62. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  63. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  64. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  65. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  66. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  67. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  68. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  69. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  70. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  71. package/examples/pills/battery.jsonc +23 -0
  72. package/examples/pills/disk-usage.jsonc +18 -0
  73. package/examples/pills/docker-containers.jsonc +22 -0
  74. package/examples/pills/git-dirty.jsonc +22 -0
  75. package/examples/pills/gpu-nvidia.jsonc +22 -0
  76. package/package.json +37 -0
  77. package/paseo-plugin.json +4 -0
  78. package/server/resources.ts +784 -0
  79. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  80. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  81. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  82. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  83. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  84. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  85. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  86. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  87. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  88. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  89. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  90. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  91. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  92. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  93. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  94. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  95. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  96. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  97. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  98. package/shared/resources.ts +686 -0
  99. package/shared/vendor/paseo-plugin-helper/README.md +11 -0
  100. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  101. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  102. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  103. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  104. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  105. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  106. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  107. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  108. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  109. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  110. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
  111. package/shared/version.ts +2 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xpufx
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # paseo-top
2
+
3
+ Live host system resource monitor and telemetry provider for [Paseo](https://github.com/getpaseo/paseo) (v0.8+).
4
+
5
+ Displays real-time host metrics, session metadata, turn telemetry, and custom pill widgets directly in the composer track bar without cluttering the interface. Automatically stamps performance summaries into agent conversation timelines and provides an interactive modal dashboard with live gauges.
6
+
7
+ Built on [paseo-plugin-helper](https://github.com/xpufx/paseo/tree/main/packages/paseo-plugin-helper), the shared Paseo plugin runtime.
8
+
9
+ > [!NOTE]
10
+ > **Prerequisites & Platform Support**:
11
+ > - Developed and tested primarily on **Linux** (using `/proc` telemetry), with fallback support for **macOS** and **Windows** host platforms.
12
+
13
+ | General Composer View | Dashboard View |
14
+ | :---: | :---: |
15
+ | [![General Composer View](screenshots/paseo-top-general-view.jpg)](screenshots/paseo-top-general-view.jpg) | [![Dashboard View](screenshots/paseo-top-dashboard.jpg)](screenshots/paseo-top-dashboard.jpg) |
16
+
17
+ | System Resources Modal | Workspace Context Modal | Settings Modal | About Modal |
18
+ | :---: | :---: | :---: | :---: |
19
+ | <img src="screenshots/1-paseo-top-modal-system.jpg" alt="System Resources Modal" width="100%" /> | <img src="screenshots/2-paseo-top-modal-workspace.jpg" alt="Workspace Context Modal" width="100%" /> | <img src="screenshots/3-paseo-top-modal-settings.jpg" alt="Settings Modal" width="100%" /> | <img src="screenshots/4-paseo-top-modal-about.jpg" alt="About Modal" width="100%" /> |
20
+
21
+
22
+ ## Core Capabilities
23
+
24
+ ### 1. Host Resource Telemetry
25
+ - **CPU & Memory Tracking**: Continuous utilization monitoring with Linux `/proc/meminfo` available-RAM accuracy.
26
+ - **Load Averages & Architecture**: Reports 1m, 5m, and 15m load averages, physical and logical CPU core counts, platform architecture, and host uptime.
27
+ - **Three-Tier Status Indicators**:
28
+ - **Success / Normal**: Below 60% CPU and 70% RAM.
29
+ - **Warning / Elevated**: 60% to 84% CPU or 70% to 84% RAM.
30
+ - **Danger / Critical**: 85% or higher CPU or RAM.
31
+ - **Adaptive Polling**: Throttles telemetry refresh rates when the modal is closed to conserve CPU cycles.
32
+
33
+ ### 2. Workspace & Agent Session Context
34
+ - **Git Branch & Worktree**: Monitors active branch name and worktree directory path.
35
+ - **Git Diff Shortstats**: Summarizes uncommitted file changes, lines added, and lines deleted.
36
+ - **Agent Identity**: Displays active agent session ID, display name, model name, and LLM provider.
37
+ - **Agent Activity State**: Reflects lifecycle state (running, idle, waiting for input).
38
+
39
+ ### 3. Turn Telemetry & Token Accounting
40
+ - **Lifetime Turn Counter**: Tracks conversation turn counts throughout the session.
41
+ - **Token Accounting**: Captures input tokens, cached prompt tokens, output tokens, and context window utilization percentage.
42
+ - **Cost Estimation**: Reports cumulative estimated USD cost per turn when supported by the provider.
43
+ - **Tool Execution Auditing**: Counts tool invocations and execution error tallies per turn.
44
+ - **Timeline Turn Telemetry**: Automatically appends a structured telemetry card to the conversation timeline at the conclusion of each agent turn.
45
+
46
+ ### 4. Fleet & MCP Integration
47
+ - **MCP Server Health**: Bridges with `mcp-tools` to monitor connected MCP servers, distinguishing healthy, degraded, or offline servers alongside latency figures.
48
+
49
+ ### 5. Flexible Display Modes
50
+ - **Cycle Mode**: Automatically cycles through enabled metric segments on each refresh interval.
51
+ - **All Mode**: Combines all enabled metrics into a single unified ticker.
52
+ - **Multiple Mode**: Splits selected metrics into distinct, individual pills placed side-by-side in the composer track bar.
53
+
54
+ > [!NOTE]
55
+ > Displaying too many pills separately in Multiple Mode may cause UI performance issues. Consider using **Cycle Mode** or **All Mode** when monitoring multiple metrics simultaneously.
56
+
57
+ ### 6. Dual Surface Routing Matrix
58
+ Every metric can be routed independently via settings to:
59
+ - **Pill**: Render in the composer track bar.
60
+ - **Timeline**: Render in the turn completion card.
61
+ - **Both**: Display in both locations.
62
+ - **None**: Disable the metric entirely.
63
+
64
+ ### 7. Extensible Custom Metric Pills (JSONC Engine)
65
+ Add user-defined metrics by dropping declarative `.jsonc` definitions into `~/.paseo/plugin-data/xpufx/top/pills/`:
66
+ - **Shell Command Execution**: Executes commands or scripts at configurable intervals.
67
+ - **Threshold Matching**: Maps outputs or exit codes to visual statuses (`neutral`, `success`, `warning`, `danger`, `accent`, `info`).
68
+ - **Interactive Modal Drilldown**: Clicking a custom pill can trigger an on-demand secondary command (for example, `docker ps -a` or `df -h`) and render terminal output directly inside the detail modal.
69
+ - Built-in templates available in [`examples/pills/`](examples/pills/):
70
+ - `docker-containers.jsonc`: Active container tally and list
71
+ - `gpu-nvidia.jsonc`: GPU load and VRAM usage via `nvidia-smi`
72
+ - `disk-usage.jsonc`: Root filesystem utilization meter
73
+ - `battery.jsonc`: Battery percentage and AC charging state
74
+ - `git-dirty.jsonc`: Modified file count in working tree
75
+
76
+ ### 8. Native Modal Dashboard
77
+ Clicking the pill opens a native dashboard with:
78
+ - **Pinned Header Navbar**: Fixed tabs for System Resources, Activity Timeline, Custom Pills, Settings, and About. Content smoothly scrolls beneath the navbar.
79
+ - **Visual Gauges**: Responsive bars showing CPU and RAM distribution.
80
+ - **Full Settings Suite**: Live toggles for every metric and pill, with persistent state managed by `PluginStorage`.
81
+
82
+ ## Installation
83
+
84
+ Install using the native Paseo monorepo subpath syntax:
85
+
86
+ ```bash
87
+ paseo plugin add xpufx/paseo --path plugins/top
88
+ ```
89
+
90
+ For local development:
91
+
92
+ ```bash
93
+ git clone git@github.com:xpufx/paseo.git
94
+ cd paseo
95
+ paseo plugin add ./plugins/top
96
+ ```
97
+
98
+ ## Development
99
+
100
+ ```bash
101
+ # Typecheck
102
+ npm run typecheck --workspace=plugins/top
103
+
104
+ # Run test suite
105
+ npm test --workspace=plugins/top
106
+
107
+ # Reload in a running Paseo daemon
108
+ paseo plugin reload top
109
+ paseo plugin logs top
110
+ ```
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,480 @@
1
+ import type { PluginAgentSnapshot } from "@getpaseo/plugin";
2
+ import { formatBytes, formatUptime } from "paseo-plugin-helper/shared";
3
+ import {
4
+ isPillEnabled,
5
+ legacyFlagView,
6
+ METRIC_DEFINITIONS,
7
+ type LiveUsage,
8
+ type SystemResources,
9
+ type TopSettings,
10
+ } from "../shared/resources";
11
+
12
+ /**
13
+ * Agent usage record. The daemon populates `lastUsage` on agent snapshots but
14
+ * `@getpaseo/plugin`'s PluginAgentSnapshot omits it; raw provider usage may
15
+ * surface either the canonical AgentUsage names or their legacy aliases.
16
+ */
17
+ export type AgentUsageSnapshot = LiveUsage & {
18
+ cachedTokens?: number;
19
+ contextUsedTokens?: number;
20
+ contextMaxTokens?: number;
21
+ costUsd?: number;
22
+ };
23
+
24
+ export type TopAgentSnapshot = PluginAgentSnapshot & {
25
+ lastUsage?: AgentUsageSnapshot | null;
26
+ };
27
+
28
+ export type PillItemType =
29
+ | "cpu_ram"
30
+ | "branch"
31
+ | "worktree"
32
+ | "agent_title"
33
+ | "agent"
34
+ | "agent_provider"
35
+ | "agent_activity"
36
+ | "agent_id"
37
+ | "load"
38
+ | "uptime"
39
+ | "mcp"
40
+ | "changes"
41
+ | "tokens"
42
+ | "tools"
43
+ | "turns";
44
+
45
+ export interface SegmentSnapshot {
46
+ data?: SystemResources;
47
+ agent?: {
48
+ title?: string | null;
49
+ model?: string | null;
50
+ provider?: string;
51
+ status?: string;
52
+ lastActivityAt?: string;
53
+ lastUsage?: AgentUsageSnapshot | null;
54
+ } | null;
55
+ agentId?: string;
56
+ worktreeLocationText?: string;
57
+ }
58
+
59
+ export interface TokenMetrics {
60
+ inputTokens?: number;
61
+ outputTokens?: number;
62
+ cachedTokens?: number;
63
+ totalTokens?: number;
64
+ contextUsedTokens?: number;
65
+ contextMaxTokens?: number;
66
+ costUsd?: number;
67
+ }
68
+
69
+ export function formatCompactTokens(n: number): string {
70
+ if (!Number.isFinite(n) || n <= 0) return "0";
71
+ if (n >= 1_000_000) {
72
+ const val = n / 1_000_000;
73
+ return val >= 10 ? `${Math.round(val)}M` : `${parseFloat(val.toFixed(1))}M`;
74
+ }
75
+ if (n >= 1_000) {
76
+ const val = n / 1_000;
77
+ return val >= 10 ? `${Math.round(val)}k` : `${parseFloat(val.toFixed(1))}k`;
78
+ }
79
+ return `${Math.round(n)}`;
80
+ }
81
+
82
+ export function extractTokenMetrics(snap: SegmentSnapshot): TokenMetrics | null {
83
+ const live = snap.data?.liveUsage;
84
+ const last = snap.data?.lastTurn;
85
+ const agentUsage = snap.agent?.lastUsage;
86
+
87
+ const num = (v: unknown): number | undefined =>
88
+ typeof v === "number" && Number.isFinite(v) ? v : undefined;
89
+
90
+ const inputTokens =
91
+ num(live?.inputTokens) ??
92
+ num(last?.inputTokens) ??
93
+ num(agentUsage?.inputTokens);
94
+
95
+ const outputTokens =
96
+ num(live?.outputTokens) ??
97
+ num(last?.outputTokens) ??
98
+ num(agentUsage?.outputTokens);
99
+
100
+ const cachedTokens =
101
+ num(live?.cachedInputTokens) ??
102
+ num(last?.cachedTokens) ??
103
+ num(last?.cachedInputTokens) ??
104
+ num(agentUsage?.cachedInputTokens) ??
105
+ num(agentUsage?.cachedTokens);
106
+
107
+ const contextUsedTokens =
108
+ num(live?.contextWindowUsedTokens) ??
109
+ num(last?.contextUsedTokens) ??
110
+ num(agentUsage?.contextWindowUsedTokens) ??
111
+ num(agentUsage?.contextUsedTokens);
112
+
113
+ const contextMaxTokens =
114
+ num(live?.contextWindowMaxTokens) ??
115
+ num(last?.contextMaxTokens) ??
116
+ num(agentUsage?.contextWindowMaxTokens) ??
117
+ num(agentUsage?.contextMaxTokens);
118
+
119
+ const costUsd =
120
+ num(live?.totalCostUsd) ??
121
+ num(last?.costUsd) ??
122
+ num(agentUsage?.totalCostUsd) ??
123
+ num(agentUsage?.costUsd);
124
+
125
+ const totalTokens =
126
+ inputTokens != null || outputTokens != null
127
+ ? (inputTokens ?? 0) + (outputTokens ?? 0)
128
+ : undefined;
129
+
130
+ if (
131
+ inputTokens === undefined &&
132
+ outputTokens === undefined &&
133
+ cachedTokens === undefined &&
134
+ contextUsedTokens === undefined &&
135
+ contextMaxTokens === undefined &&
136
+ costUsd === undefined
137
+ ) {
138
+ return null;
139
+ }
140
+
141
+ return {
142
+ inputTokens,
143
+ outputTokens,
144
+ cachedTokens,
145
+ totalTokens,
146
+ contextUsedTokens,
147
+ contextMaxTokens,
148
+ costUsd,
149
+ };
150
+ }
151
+
152
+ export function formatTokensLabel(
153
+ metrics: TokenMetrics | null,
154
+ prefix: string | undefined = "tok",
155
+ ): string {
156
+ if (!metrics) {
157
+ return prefix ? `${prefix} --` : "--";
158
+ }
159
+
160
+ // 1. Both context used and max: "58k/1M"
161
+ if (
162
+ metrics.contextUsedTokens != null &&
163
+ metrics.contextMaxTokens != null &&
164
+ metrics.contextMaxTokens > 0
165
+ ) {
166
+ return `${formatCompactTokens(metrics.contextUsedTokens)}/${formatCompactTokens(metrics.contextMaxTokens)}`;
167
+ }
168
+
169
+ // 2. Context used without max (when no total tokens or total is 0): "58k ctx"
170
+ if (
171
+ metrics.contextUsedTokens != null &&
172
+ metrics.contextUsedTokens > 0 &&
173
+ (metrics.totalTokens == null || metrics.totalTokens === 0)
174
+ ) {
175
+ return `${formatCompactTokens(metrics.contextUsedTokens)} ctx`;
176
+ }
177
+
178
+ // 3. Total tokens (input + output): "888 tok", "58k tok"
179
+ if (metrics.totalTokens != null && metrics.totalTokens > 0) {
180
+ return `${formatCompactTokens(metrics.totalTokens)} tok`;
181
+ }
182
+
183
+ // 4. Context used fallback: "58k ctx"
184
+ if (metrics.contextUsedTokens != null && metrics.contextUsedTokens > 0) {
185
+ return `${formatCompactTokens(metrics.contextUsedTokens)} ctx`;
186
+ }
187
+
188
+ return prefix ? `${prefix} --` : "--";
189
+ }
190
+
191
+ export function formatIdlePillLabel(
192
+ status: string | undefined,
193
+ isoString: string | null | undefined,
194
+ ): string {
195
+ if (status === "running") return "active";
196
+ if (!isoString) return status || "--";
197
+ const time = new Date(isoString).getTime();
198
+ if (isNaN(time)) return "--";
199
+ const diffMs = Math.max(0, Date.now() - time);
200
+ const secs = Math.floor(diffMs / 1000);
201
+ if (secs < 60) return `idle ${secs}s`;
202
+ const mins = Math.floor(secs / 60);
203
+ if (mins < 60) return `idle ${mins}m`;
204
+ const hours = Math.floor(mins / 60);
205
+ if (hours < 24) return `idle ${hours}h`;
206
+ const days = Math.floor(hours / 24);
207
+ return `idle ${days}d`;
208
+ }
209
+
210
+ export function formatSegmentIcon(item: PillItemType, snap?: SegmentSnapshot): string {
211
+ if (item === "agent_activity") {
212
+ return snap?.agent?.status === "running" ? "Activity" : "Clock";
213
+ }
214
+ const def = METRIC_DEFINITIONS.find((d) => d.id === item);
215
+ return def?.icon ?? "Activity";
216
+ }
217
+
218
+ /**
219
+ * Theme-independent color slot for one piece of a pill segment. The renderer
220
+ * maps these to concrete theme colors so the composer pill (host-rendered) and
221
+ * the in-product pill fallback share one description of the same metric.
222
+ */
223
+ export type SegmentTone =
224
+ | "foreground"
225
+ | "muted"
226
+ | "accent"
227
+ | "success"
228
+ | "danger"
229
+ | "warning"
230
+ | "cpu"
231
+ | "mem"
232
+ | "none";
233
+
234
+ /**
235
+ * One pill metric split into its visual pieces. `describeSegment` is the single
236
+ * source of truth: `formatSegmentLabel` flattens it to the host label string and
237
+ * the pill renderer lays it out, so label text and inline rendering cannot drift.
238
+ */
239
+ export interface SegmentDescriptor {
240
+ icon: string;
241
+ /** "none" keeps the inline icon hidden (the metric is text-only in-pill). */
242
+ iconTone: SegmentTone;
243
+ /** Leading glyph, e.g. the MCP health dot. Excluded from the flat label. */
244
+ leading?: string;
245
+ leadingTone?: SegmentTone;
246
+ prefix?: string;
247
+ prefixTone?: SegmentTone;
248
+ text: string;
249
+ tone: SegmentTone;
250
+ separator?: string;
251
+ trailing?: string;
252
+ trailingTone?: SegmentTone;
253
+ }
254
+
255
+ export function describeSegment(
256
+ item: PillItemType,
257
+ snap: SegmentSnapshot = {},
258
+ ): SegmentDescriptor {
259
+ const { data, agent, agentId, worktreeLocationText } = snap;
260
+ const def = METRIC_DEFINITIONS.find((d) => d.id === item);
261
+ const icon = formatSegmentIcon(item, snap);
262
+ const prefix = def?.shortLabel ? `${def.shortLabel} ` : "";
263
+ switch (item) {
264
+ case "branch":
265
+ return { icon, iconTone: "accent", text: data?.branch ?? "--", tone: "foreground" };
266
+ case "worktree":
267
+ return {
268
+ icon,
269
+ iconTone: "accent",
270
+ text: worktreeLocationText || "--",
271
+ tone: "foreground",
272
+ };
273
+ case "agent_title":
274
+ return { icon, iconTone: "accent", text: agent?.title ?? "Agent", tone: "foreground" };
275
+ case "agent":
276
+ return {
277
+ icon,
278
+ iconTone: "accent",
279
+ text: agent?.model || agent?.provider || "Agent",
280
+ tone: "foreground",
281
+ };
282
+ case "agent_provider":
283
+ return {
284
+ icon,
285
+ iconTone: "accent",
286
+ text: agent?.provider ?? "Provider",
287
+ tone: "foreground",
288
+ };
289
+ case "agent_activity": {
290
+ const running = agent?.status === "running";
291
+ return {
292
+ icon,
293
+ iconTone: running ? "success" : "muted",
294
+ text: formatIdlePillLabel(agent?.status, agent?.lastActivityAt),
295
+ tone: running ? "success" : "foreground",
296
+ };
297
+ }
298
+ case "agent_id":
299
+ return {
300
+ icon,
301
+ iconTone: "accent",
302
+ text: agentId && agentId.length > 7 ? agentId.slice(0, 7) : agentId ?? "--",
303
+ tone: "foreground",
304
+ };
305
+ case "load":
306
+ return {
307
+ icon,
308
+ iconTone: "none",
309
+ prefix,
310
+ prefixTone: "muted",
311
+ text: data?.loadAvg?.[0] !== undefined ? data.loadAvg[0].toFixed(2) : "--",
312
+ tone: "cpu",
313
+ };
314
+ case "uptime":
315
+ return {
316
+ icon,
317
+ iconTone: "none",
318
+ prefix,
319
+ prefixTone: "muted",
320
+ text: data?.uptimeSeconds ? formatUptime(data.uptimeSeconds) : "--",
321
+ tone: "foreground",
322
+ };
323
+ case "mcp": {
324
+ const mcp = data?.mcp;
325
+ const live = mcp && !mcp.isStale;
326
+ const leadingTone: SegmentTone = live
327
+ ? mcp.down > 0
328
+ ? "danger"
329
+ : mcp.degraded > 0 || mcp.healthy !== mcp.total
330
+ ? "warning"
331
+ : "success"
332
+ : "muted";
333
+ return {
334
+ icon,
335
+ iconTone: "none",
336
+ leading: live ? "●" : "○",
337
+ leadingTone,
338
+ text: mcp ? `${mcp.healthy}/${mcp.total} MCP` : "MCP -",
339
+ tone: "foreground",
340
+ };
341
+ }
342
+ case "changes": {
343
+ const last = data?.lastTurn;
344
+ const hasData = last && (last.gitInsertions != null || last.gitDeletions != null);
345
+ return {
346
+ icon,
347
+ iconTone: "none",
348
+ prefix,
349
+ prefixTone: "muted",
350
+ text: hasData ? `+${last.gitInsertions ?? 0}/-${last.gitDeletions ?? 0}` : "--",
351
+ tone: "foreground",
352
+ };
353
+ }
354
+ case "tokens": {
355
+ const metrics = extractTokenMetrics(snap);
356
+ if (!metrics) {
357
+ return {
358
+ icon,
359
+ iconTone: "none",
360
+ prefix,
361
+ prefixTone: "muted",
362
+ text: formatTokensLabel(metrics, ""),
363
+ tone: "muted",
364
+ };
365
+ }
366
+ return {
367
+ icon,
368
+ iconTone: "none",
369
+ text: formatTokensLabel(metrics, ""),
370
+ tone: "foreground",
371
+ };
372
+ }
373
+ case "tools": {
374
+ const last = data?.lastTurn;
375
+ const hasData = last && last.toolCalls != null;
376
+ return {
377
+ icon,
378
+ iconTone: "none",
379
+ prefix,
380
+ prefixTone: "muted",
381
+ text: hasData
382
+ ? `${last.toolCalls}${last.toolErrors ? ` (${last.toolErrors} err)` : ""}`
383
+ : "--",
384
+ tone: "foreground",
385
+ };
386
+ }
387
+ case "turns": {
388
+ const last = data?.lastTurn;
389
+ return {
390
+ icon,
391
+ iconTone: "none",
392
+ prefix,
393
+ prefixTone: "muted",
394
+ text: last?.turnCount != null ? `${last.turnCount}` : "--",
395
+ tone: "foreground",
396
+ };
397
+ }
398
+ case "cpu_ram":
399
+ default: {
400
+ const ram =
401
+ data?.memoryUsedBytes !== undefined
402
+ ? formatBytes(data.memoryUsedBytes, { compact: true, decimals: 1 })
403
+ : "--";
404
+ const cpu = data?.cpuUsagePercent !== undefined ? `${data.cpuUsagePercent}%` : "--";
405
+ return {
406
+ icon,
407
+ iconTone: "none",
408
+ text: cpu,
409
+ tone: "cpu",
410
+ separator: " · ",
411
+ trailing: ram,
412
+ trailingTone: "mem",
413
+ };
414
+ }
415
+ }
416
+ }
417
+
418
+ export function formatSegmentLabel(item: PillItemType, snap: SegmentSnapshot): string {
419
+ const d = describeSegment(item, snap);
420
+ return `${d.prefix ?? ""}${d.text}${d.separator ?? ""}${d.trailing ?? ""}`;
421
+ }
422
+
423
+ export function enabledItemsForSettings(settings: TopSettings): PillItemType[] {
424
+ const flags = legacyFlagView(settings);
425
+ const items: PillItemType[] = [];
426
+ const hasAnyEnabled =
427
+ flags.showCpuRam ||
428
+ flags.showBranch ||
429
+ flags.showWorktree ||
430
+ flags.showAgentTitle ||
431
+ flags.showAgent ||
432
+ flags.showAgentProvider ||
433
+ flags.showAgentActivity ||
434
+ flags.showAgentId ||
435
+ flags.showLoad ||
436
+ flags.showUptime ||
437
+ flags.showMcp;
438
+ if (flags.showCpuRam || !hasAnyEnabled) items.push("cpu_ram");
439
+ if (flags.showBranch) items.push("branch");
440
+ if (flags.showWorktree) items.push("worktree");
441
+ if (flags.showAgentTitle) items.push("agent_title");
442
+ if (flags.showAgent) items.push("agent");
443
+ if (flags.showAgentProvider) items.push("agent_provider");
444
+ if (flags.showAgentActivity) items.push("agent_activity");
445
+ if (flags.showAgentId) items.push("agent_id");
446
+ if (flags.showLoad) items.push("load");
447
+ if (flags.showUptime) items.push("uptime");
448
+ if (flags.showMcp) items.push("mcp");
449
+ if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.changes)) {
450
+ items.push("changes");
451
+ }
452
+ if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.tokens)) {
453
+ items.push("tokens");
454
+ }
455
+ if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.tools)) {
456
+ items.push("tools");
457
+ }
458
+ if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.turns)) {
459
+ items.push("turns");
460
+ }
461
+ return items;
462
+ }
463
+
464
+ const cycleIndexByAgent = new Map<string, number>();
465
+
466
+ export function nextCycleItem(agentId: string, items: PillItemType[]): PillItemType | undefined {
467
+ if (items.length === 0) return undefined;
468
+ const current = cycleIndexByAgent.get(agentId) ?? 0;
469
+ const item = items[current % items.length];
470
+ cycleIndexByAgent.set(agentId, (current + 1) % items.length);
471
+ return item;
472
+ }
473
+
474
+ export function resetCycleState(): void {
475
+ cycleIndexByAgent.clear();
476
+ }
477
+
478
+ export function buildAllLabel(items: PillItemType[], snap: SegmentSnapshot): string {
479
+ return items.map((item) => formatSegmentLabel(item, snap)).join(" | ");
480
+ }