@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.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/client/pill-labels.ts +480 -0
- package/client/pill.tsx +2461 -0
- package/client/resources-query.ts +68 -0
- package/client/settings-ui.tsx +50 -0
- package/client/surface.tsx +364 -0
- package/client/telemetry-copy.ts +105 -0
- package/client/telemetry.tsx +486 -0
- package/client/turn-counter.ts +151 -0
- package/client/turn-panel.tsx +182 -0
- package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
- package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
- package/client/vendor/paseo-plugin-helper/host.ts +277 -0
- package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/index.ts +28 -0
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
- package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
- package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
- package/client/vendor/paseo-plugin-helper/query.ts +66 -0
- package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
- package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
- package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
- package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
- package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/examples/pills/battery.jsonc +23 -0
- package/examples/pills/disk-usage.jsonc +18 -0
- package/examples/pills/docker-containers.jsonc +22 -0
- package/examples/pills/git-dirty.jsonc +22 -0
- package/examples/pills/gpu-nvidia.jsonc +22 -0
- package/package.json +37 -0
- package/paseo-plugin.json +4 -0
- package/server/resources.ts +784 -0
- package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
- package/server/vendor/paseo-plugin-helper/index.ts +18 -0
- package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
- package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
- package/server/vendor/paseo-plugin-helper/network.ts +91 -0
- package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
- package/server/vendor/paseo-plugin-helper/process.ts +186 -0
- package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
- package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
- package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
- package/server/vendor/paseo-plugin-helper/system.ts +128 -0
- package/server/vendor/paseo-plugin-helper/task.ts +116 -0
- package/server/vendor/paseo-plugin-helper/version.ts +153 -0
- package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
- package/shared/resources.ts +686 -0
- package/shared/vendor/paseo-plugin-helper/README.md +11 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
- package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
- package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
- package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
- package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
- package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
- package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
- package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
- package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
- 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
|
+
| [](screenshots/paseo-top-general-view.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
|
+
}
|