@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/client/pill.tsx
ADDED
|
@@ -0,0 +1,2461 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { Text, View } from "react-native";
|
|
3
|
+
import type { PluginWorkspaceSnapshot } from "@getpaseo/plugin";
|
|
4
|
+
import {
|
|
5
|
+
useWorkspace,
|
|
6
|
+
useAgent,
|
|
7
|
+
useRpc,
|
|
8
|
+
type PluginClientContext,
|
|
9
|
+
} from "@getpaseo/plugin/client";
|
|
10
|
+
import {
|
|
11
|
+
registerComposerPill,
|
|
12
|
+
registerSidebarSurface,
|
|
13
|
+
type ComposerPillRegistrar,
|
|
14
|
+
type SidebarSurfaceRegistrar,
|
|
15
|
+
type PillLiveContext,
|
|
16
|
+
type RegisterComposerPillOptions,
|
|
17
|
+
ModalBody,
|
|
18
|
+
Card,
|
|
19
|
+
CardHeader,
|
|
20
|
+
Button,
|
|
21
|
+
Row,
|
|
22
|
+
KeyValue,
|
|
23
|
+
KeyValueGroup,
|
|
24
|
+
ProgressBar,
|
|
25
|
+
MetricGauge,
|
|
26
|
+
Tabs,
|
|
27
|
+
Toggle,
|
|
28
|
+
Badge,
|
|
29
|
+
Icon,
|
|
30
|
+
AboutSection,
|
|
31
|
+
CustomPillBody,
|
|
32
|
+
CustomPillModalContent,
|
|
33
|
+
useRpcQuery,
|
|
34
|
+
usePluginSettings,
|
|
35
|
+
usePluginTheme,
|
|
36
|
+
getStatusColor,
|
|
37
|
+
triggerHaptic,
|
|
38
|
+
shouldEmitSnapshotUpdate,
|
|
39
|
+
type RenderModalProps,
|
|
40
|
+
type RenderPillProps,
|
|
41
|
+
type KeyValueProps,
|
|
42
|
+
type CardHeaderProps,
|
|
43
|
+
type BadgeProps,
|
|
44
|
+
} from "paseo-plugin-helper/client";
|
|
45
|
+
import {
|
|
46
|
+
formatBytes,
|
|
47
|
+
formatUptime,
|
|
48
|
+
resolveMetricStatus,
|
|
49
|
+
type MetricThresholds,
|
|
50
|
+
type CustomPillState,
|
|
51
|
+
} from "paseo-plugin-helper/shared";
|
|
52
|
+
import {
|
|
53
|
+
getSystemResourcesRpc,
|
|
54
|
+
topSettingsContract,
|
|
55
|
+
getCustomPillsRpc,
|
|
56
|
+
listCustomPillsRpc,
|
|
57
|
+
runCustomPillModalCommandRpc,
|
|
58
|
+
isPillEnabled,
|
|
59
|
+
isProviderDependent,
|
|
60
|
+
legacyFlagView,
|
|
61
|
+
isMcpSurfaceEnabled,
|
|
62
|
+
customPillEffectiveEnabled,
|
|
63
|
+
METRIC_DEFINITIONS,
|
|
64
|
+
DEFAULT_METRIC_SURFACES,
|
|
65
|
+
checkboxesFromTarget,
|
|
66
|
+
targetFromCheckboxes,
|
|
67
|
+
type SystemResources,
|
|
68
|
+
type TopSettings,
|
|
69
|
+
type CustomPillStateOutput,
|
|
70
|
+
type MetricId,
|
|
71
|
+
type SurfaceTarget,
|
|
72
|
+
} from "../shared/resources";
|
|
73
|
+
import { PLUGIN_VERSION } from "../shared/version";
|
|
74
|
+
import { TopDashboardSurface } from "./surface";
|
|
75
|
+
import { useTopResourceQuery, useCustomPillsQuery } from "./resources-query";
|
|
76
|
+
import { ChoiceChips } from "./settings-ui";
|
|
77
|
+
import {
|
|
78
|
+
buildAllLabel,
|
|
79
|
+
describeSegment,
|
|
80
|
+
enabledItemsForSettings,
|
|
81
|
+
extractTokenMetrics,
|
|
82
|
+
formatCompactTokens,
|
|
83
|
+
formatSegmentIcon,
|
|
84
|
+
formatSegmentLabel,
|
|
85
|
+
nextCycleItem,
|
|
86
|
+
type PillItemType,
|
|
87
|
+
type SegmentSnapshot,
|
|
88
|
+
type SegmentTone,
|
|
89
|
+
type TopAgentSnapshot,
|
|
90
|
+
} from "./pill-labels";
|
|
91
|
+
|
|
92
|
+
const EMPTY_PARAMS = {};
|
|
93
|
+
|
|
94
|
+
function formatWorktreeLocation(dir: string | null | undefined): string {
|
|
95
|
+
if (!dir) return "";
|
|
96
|
+
// Check for Paseo-managed worktree: ~/.paseo/worktrees/<hash>/<slug>
|
|
97
|
+
const wtMatch = dir.match(/[/\\]\.paseo[/\\]worktrees[/\\][^/\\]+[/\\]([^/\\]+)$/);
|
|
98
|
+
if (wtMatch && wtMatch[1]) {
|
|
99
|
+
return wtMatch[1];
|
|
100
|
+
}
|
|
101
|
+
const clean = dir.replace(/[/\\]+$/, "");
|
|
102
|
+
const segments = clean.split(/[/\\]/);
|
|
103
|
+
return segments[segments.length - 1] || clean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatTimeAgo(isoString: string | null | undefined): string {
|
|
107
|
+
if (!isoString) return "--";
|
|
108
|
+
const time = new Date(isoString).getTime();
|
|
109
|
+
if (isNaN(time)) return "--";
|
|
110
|
+
const diffMs = Math.max(0, Date.now() - time);
|
|
111
|
+
const secs = Math.floor(diffMs / 1000);
|
|
112
|
+
if (secs < 30) return "just now";
|
|
113
|
+
if (secs < 60) return `${secs}s ago`;
|
|
114
|
+
const mins = Math.floor(secs / 60);
|
|
115
|
+
if (mins < 60) return `${mins}m ago`;
|
|
116
|
+
const hours = Math.floor(mins / 60);
|
|
117
|
+
if (hours < 24) return `${hours}h ${mins % 60}m ago`;
|
|
118
|
+
const days = Math.floor(hours / 24);
|
|
119
|
+
return `${days}d ago`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatIdleDuration(isoString: string | null | undefined): string {
|
|
123
|
+
if (!isoString) return "--";
|
|
124
|
+
const time = new Date(isoString).getTime();
|
|
125
|
+
if (isNaN(time)) return "--";
|
|
126
|
+
const diffMs = Math.max(0, Date.now() - time);
|
|
127
|
+
const secs = Math.floor(diffMs / 1000);
|
|
128
|
+
if (secs < 60) return `${secs}s`;
|
|
129
|
+
const mins = Math.floor(secs / 60);
|
|
130
|
+
if (mins < 60) return `${mins}m ${secs % 60}s`;
|
|
131
|
+
const hours = Math.floor(mins / 60);
|
|
132
|
+
if (hours < 24) return `${hours}h ${mins % 60}m`;
|
|
133
|
+
const days = Math.floor(hours / 24);
|
|
134
|
+
return `${days}d ${hours % 24}h`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const CPU_THRESHOLDS: MetricThresholds = { warning: 60, danger: 85 };
|
|
138
|
+
const MEM_THRESHOLDS: MetricThresholds = { warning: 70, danger: 85 };
|
|
139
|
+
|
|
140
|
+
const TABS = [
|
|
141
|
+
{ id: "system", label: "System", shortLabel: "System", icon: "Activity" },
|
|
142
|
+
{ id: "context", label: "Workspace", shortLabel: "Workspace", icon: "GitBranch" },
|
|
143
|
+
{ id: "settings", label: "Settings", shortLabel: "Settings", icon: "Sliders" },
|
|
144
|
+
{ id: "about", label: "About", shortLabel: "About", icon: "Info" },
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const TIMELINE_CADENCE_OPTIONS = Array.from({ length: 11 }, (_, n) => ({
|
|
148
|
+
id: n,
|
|
149
|
+
label: n === 0 ? "Never" : n === 1 ? "Every turn" : `${n}`,
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
function getMetricColors(
|
|
153
|
+
data: SystemResources | undefined,
|
|
154
|
+
colors: ReturnType<typeof usePluginTheme>["colors"],
|
|
155
|
+
) {
|
|
156
|
+
if (
|
|
157
|
+
!data ||
|
|
158
|
+
data.cpuUsagePercent === undefined ||
|
|
159
|
+
data.memoryUsedPercent === undefined
|
|
160
|
+
) {
|
|
161
|
+
return {
|
|
162
|
+
cpuColor: colors.foregroundMuted,
|
|
163
|
+
memColor: colors.foregroundMuted,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const cpuStatus = resolveMetricStatus(data.cpuUsagePercent, CPU_THRESHOLDS);
|
|
168
|
+
const memStatus = resolveMetricStatus(data.memoryUsedPercent, MEM_THRESHOLDS);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
cpuColor: getStatusColor(cpuStatus, colors),
|
|
172
|
+
memColor: getStatusColor(memStatus, colors),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Canonical metric union lives in pill-labels (no RN imports); re-exported here
|
|
177
|
+
// so pill call sites keep a single import path.
|
|
178
|
+
export type { PillItemType } from "./pill-labels";
|
|
179
|
+
|
|
180
|
+
export type ModalTab = "system" | "context" | "settings" | "about";
|
|
181
|
+
|
|
182
|
+
export function getItemTab(item: PillItemType): "system" | "context" {
|
|
183
|
+
switch (item) {
|
|
184
|
+
case "cpu_ram":
|
|
185
|
+
case "load":
|
|
186
|
+
case "uptime":
|
|
187
|
+
case "mcp":
|
|
188
|
+
case "changes":
|
|
189
|
+
case "tokens":
|
|
190
|
+
case "tools":
|
|
191
|
+
case "turns":
|
|
192
|
+
return "system";
|
|
193
|
+
case "branch":
|
|
194
|
+
case "worktree":
|
|
195
|
+
case "agent_title":
|
|
196
|
+
case "agent":
|
|
197
|
+
case "agent_provider":
|
|
198
|
+
case "agent_activity":
|
|
199
|
+
case "agent_id":
|
|
200
|
+
return "context";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const currentCycleTabByAgent = new Map<string, ModalTab>();
|
|
205
|
+
|
|
206
|
+
// The 0.8 host popover is a narrow column, so every helper text component in
|
|
207
|
+
// the modal path renders through these compact wrappers (2-3pt under helper
|
|
208
|
+
// defaults). Call-site props still win via spread order.
|
|
209
|
+
function CompactKeyValue(props: KeyValueProps) {
|
|
210
|
+
return (
|
|
211
|
+
<KeyValue
|
|
212
|
+
labelStyle={styles.compactKvLabel}
|
|
213
|
+
valueStyle={styles.compactKvValue}
|
|
214
|
+
{...props}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function CompactCardHeader(props: CardHeaderProps) {
|
|
220
|
+
return (
|
|
221
|
+
<CardHeader titleStyle={styles.compactCardTitle} {...props} />
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function CompactBadge(props: BadgeProps) {
|
|
226
|
+
return <Badge textStyle={styles.compactBadgeText} {...props} />;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface LiveSnapshot extends SegmentSnapshot {
|
|
230
|
+
workspaceDirectory?: string | null;
|
|
231
|
+
updatedAt?: number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Host resources are identical for every agent in a workspace; scope the shared fetch by directory. */
|
|
235
|
+
function resourceScope(directory?: string | null): string {
|
|
236
|
+
return typeof directory === "string" ? directory.trim() : "";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const liveSnapshots = new Map<string, LiveSnapshot>();
|
|
240
|
+
const sharedResourceSnapshots = new Map<string, { data: SystemResources; at: number }>();
|
|
241
|
+
const liveSnapshotInflight = new Map<string, Promise<SystemResources | null>>();
|
|
242
|
+
let rpcInvoker: ((contract: any, input: any) => Promise<any>) | null = null;
|
|
243
|
+
|
|
244
|
+
export function updateLiveSnapshot(agentId: string, partial: Partial<LiveSnapshot>) {
|
|
245
|
+
const prev = liveSnapshots.get(agentId) ?? {};
|
|
246
|
+
const next = { ...prev, ...partial, updatedAt: Date.now() };
|
|
247
|
+
liveSnapshots.set(agentId, next);
|
|
248
|
+
// A mounted pill's fresh query result also fills the workspace-scoped cache,
|
|
249
|
+
// so every other agent in that workspace reads it instead of fetching again.
|
|
250
|
+
if (partial.data) {
|
|
251
|
+
sharedResourceSnapshots.set(resourceScope(next.workspaceDirectory), {
|
|
252
|
+
data: partial.data,
|
|
253
|
+
at: Date.now(),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const LIVE_SNAPSHOT_TTL_MS = 2500;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Shared full-snapshot fetch for button-host live labels.
|
|
262
|
+
*
|
|
263
|
+
* The snapshot is host/workspace data, and `branch` is the only per-workspace
|
|
264
|
+
* field in it, so it is cached and single-flighted by workspace directory
|
|
265
|
+
* rather than by agent. Keying on the agent gave every agent its own copy of
|
|
266
|
+
* the same resources, so a host with N agents polled `system-resources.get`
|
|
267
|
+
* N times per refresh.
|
|
268
|
+
*/
|
|
269
|
+
async function liveSnapshotFor(ctx: PillLiveContext): Promise<SegmentSnapshot> {
|
|
270
|
+
const cached = liveSnapshots.get(ctx.agentId);
|
|
271
|
+
const scope = resourceScope(cached?.workspaceDirectory);
|
|
272
|
+
const shared = sharedResourceSnapshots.get(scope);
|
|
273
|
+
let data = cached?.data ?? shared?.data;
|
|
274
|
+
const sharedAge = shared ? Date.now() - shared.at : Infinity;
|
|
275
|
+
try {
|
|
276
|
+
if (rpcInvoker) {
|
|
277
|
+
const params: Record<string, unknown> = {};
|
|
278
|
+
if (cached?.workspaceDirectory) params.directory = cached.workspaceDirectory;
|
|
279
|
+
let inflight = liveSnapshotInflight.get(scope);
|
|
280
|
+
if (!inflight && sharedAge > LIVE_SNAPSHOT_TTL_MS) {
|
|
281
|
+
inflight = (async () => {
|
|
282
|
+
try {
|
|
283
|
+
return (await rpcInvoker!(getSystemResourcesRpc, params)) as SystemResources | null;
|
|
284
|
+
} catch {
|
|
285
|
+
return null;
|
|
286
|
+
} finally {
|
|
287
|
+
liveSnapshotInflight.delete(scope);
|
|
288
|
+
}
|
|
289
|
+
})();
|
|
290
|
+
liveSnapshotInflight.set(scope, inflight);
|
|
291
|
+
}
|
|
292
|
+
const fresh = inflight ? await inflight : null;
|
|
293
|
+
if (fresh) {
|
|
294
|
+
data = { ...(data ?? {}), ...fresh } as SystemResources;
|
|
295
|
+
sharedResourceSnapshots.set(scope, { data: fresh, at: Date.now() });
|
|
296
|
+
updateLiveSnapshot(ctx.agentId, { data });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} catch {
|
|
300
|
+
// Keep cached data when the live fetch fails
|
|
301
|
+
}
|
|
302
|
+
const worktreeLocationText =
|
|
303
|
+
cached?.worktreeLocationText ??
|
|
304
|
+
(cached?.workspaceDirectory ? formatWorktreeLocation(cached.workspaceDirectory) : undefined);
|
|
305
|
+
return { data, agent: cached?.agent ?? null, agentId: ctx.agentId, worktreeLocationText };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function singleItemLabelResolver(item: PillItemType) {
|
|
309
|
+
return async (ctx: PillLiveContext) => {
|
|
310
|
+
const snap = await liveSnapshotFor(ctx);
|
|
311
|
+
return {
|
|
312
|
+
label: formatSegmentLabel(item, snap),
|
|
313
|
+
icon: formatSegmentIcon(item, snap),
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Custom-pill states are host-wide (not workspace data), so they get the same
|
|
320
|
+
* treatment as the resource snapshot: one TTL-cached, single-flighted fetch
|
|
321
|
+
* shared by every custom pill. Resolving each pill's label used to call
|
|
322
|
+
* `top.custom-pills.get` itself, so a host with N visible custom pills issued N
|
|
323
|
+
* identical RPCs per label tick.
|
|
324
|
+
*/
|
|
325
|
+
const CUSTOM_PILL_TTL_MS = 3000;
|
|
326
|
+
let customPillCache: { pills: CustomPillStateOutput[]; at: number } | null = null;
|
|
327
|
+
let customPillInflight: Promise<CustomPillStateOutput[]> | null = null;
|
|
328
|
+
|
|
329
|
+
async function customPillSnapshot(): Promise<CustomPillStateOutput[]> {
|
|
330
|
+
if (customPillCache && Date.now() - customPillCache.at < CUSTOM_PILL_TTL_MS) {
|
|
331
|
+
return customPillCache.pills;
|
|
332
|
+
}
|
|
333
|
+
if (customPillInflight) return customPillInflight;
|
|
334
|
+
if (!rpcInvoker) return customPillCache?.pills ?? [];
|
|
335
|
+
customPillInflight = (async () => {
|
|
336
|
+
try {
|
|
337
|
+
const res = await rpcInvoker!(getCustomPillsRpc, EMPTY_PARAMS);
|
|
338
|
+
const pills: CustomPillStateOutput[] = res?.pills ?? [];
|
|
339
|
+
customPillCache = { pills, at: Date.now() };
|
|
340
|
+
return pills;
|
|
341
|
+
} catch {
|
|
342
|
+
return customPillCache?.pills ?? [];
|
|
343
|
+
} finally {
|
|
344
|
+
customPillInflight = null;
|
|
345
|
+
}
|
|
346
|
+
})();
|
|
347
|
+
return customPillInflight;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Single registration path for every Top pill variant (main cycle/all pill,
|
|
352
|
+
* per-metric pills, custom metric pills) so the presentation model cannot
|
|
353
|
+
* diverge: all are centered surfaces with a pinned popover width. Call sites
|
|
354
|
+
* only describe what is pill-specific.
|
|
355
|
+
*/
|
|
356
|
+
function registerTopPill(
|
|
357
|
+
client: ComposerPillRegistrar | PluginClientContext,
|
|
358
|
+
options: Omit<RegisterComposerPillOptions<ModalTab>, "presentation" | "popoverWidth"> & {
|
|
359
|
+
presentation?: "popover" | "centered";
|
|
360
|
+
popoverWidth?: number;
|
|
361
|
+
},
|
|
362
|
+
) {
|
|
363
|
+
return registerComposerPill<ModalTab>(client as ComposerPillRegistrar, {
|
|
364
|
+
popoverWidth: 360,
|
|
365
|
+
...options,
|
|
366
|
+
presentation: options.presentation ?? "centered",
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
interface PillItemContentProps {
|
|
371
|
+
item: PillItemType;
|
|
372
|
+
data?: SystemResources;
|
|
373
|
+
agent?: {
|
|
374
|
+
title?: string | null;
|
|
375
|
+
model?: string | null;
|
|
376
|
+
provider?: string;
|
|
377
|
+
status?: string;
|
|
378
|
+
lastActivityAt?: string;
|
|
379
|
+
} | null;
|
|
380
|
+
agentId?: string;
|
|
381
|
+
worktreeLocationText?: string;
|
|
382
|
+
isOpen?: boolean;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function PillItemContent({
|
|
386
|
+
item,
|
|
387
|
+
data,
|
|
388
|
+
agent,
|
|
389
|
+
agentId,
|
|
390
|
+
worktreeLocationText,
|
|
391
|
+
isOpen,
|
|
392
|
+
}: PillItemContentProps) {
|
|
393
|
+
const { colors } = usePluginTheme();
|
|
394
|
+
const { cpuColor, memColor } = getMetricColors(data, colors);
|
|
395
|
+
const descriptor = describeSegment(item, { data, agent, agentId, worktreeLocationText });
|
|
396
|
+
const toneColor = (tone: SegmentTone): string => {
|
|
397
|
+
switch (tone) {
|
|
398
|
+
case "muted":
|
|
399
|
+
return colors.foregroundMuted;
|
|
400
|
+
case "accent":
|
|
401
|
+
return colors.accent;
|
|
402
|
+
case "success":
|
|
403
|
+
return colors.statusSuccess;
|
|
404
|
+
case "danger":
|
|
405
|
+
return colors.statusDanger;
|
|
406
|
+
case "warning":
|
|
407
|
+
return colors.statusWarning;
|
|
408
|
+
case "cpu":
|
|
409
|
+
return cpuColor;
|
|
410
|
+
case "mem":
|
|
411
|
+
return memColor;
|
|
412
|
+
default:
|
|
413
|
+
return colors.foreground;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<Row gap={4} align="center" style={styles.pillContainer}>
|
|
419
|
+
{descriptor.iconTone !== "none" ? (
|
|
420
|
+
<Icon name={descriptor.icon} size={12} color={toneColor(descriptor.iconTone)} />
|
|
421
|
+
) : null}
|
|
422
|
+
<Text numberOfLines={1} style={[styles.pillText, isOpen && styles.pillTextActive]}>
|
|
423
|
+
{descriptor.leading ? (
|
|
424
|
+
<Text style={{ color: toneColor(descriptor.leadingTone ?? "muted"), fontWeight: "700" }}>
|
|
425
|
+
{`${descriptor.leading} `}
|
|
426
|
+
</Text>
|
|
427
|
+
) : null}
|
|
428
|
+
{descriptor.prefix ? (
|
|
429
|
+
<Text style={{ color: toneColor(descriptor.prefixTone ?? "muted") }}>
|
|
430
|
+
{descriptor.prefix}
|
|
431
|
+
</Text>
|
|
432
|
+
) : null}
|
|
433
|
+
<Text style={{ color: toneColor(descriptor.tone), fontWeight: "600" }}>
|
|
434
|
+
{descriptor.text}
|
|
435
|
+
</Text>
|
|
436
|
+
{descriptor.separator ? (
|
|
437
|
+
<Text style={{ color: colors.foregroundMuted }}>{descriptor.separator}</Text>
|
|
438
|
+
) : null}
|
|
439
|
+
{descriptor.trailing ? (
|
|
440
|
+
<Text
|
|
441
|
+
style={{ color: toneColor(descriptor.trailingTone ?? "foreground"), fontWeight: "600" }}
|
|
442
|
+
>
|
|
443
|
+
{descriptor.trailing}
|
|
444
|
+
</Text>
|
|
445
|
+
) : null}
|
|
446
|
+
</Text>
|
|
447
|
+
</Row>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
type SettingsListener = (settings: TopSettings) => void;
|
|
452
|
+
const settingsListeners = new Set<SettingsListener>();
|
|
453
|
+
let lastNotifiedSettings: TopSettings | null = null;
|
|
454
|
+
|
|
455
|
+
export function notifySettingsChanged(settings: TopSettings) {
|
|
456
|
+
if (!shouldEmitSnapshotUpdate(
|
|
457
|
+
lastNotifiedSettings as unknown as Record<string, unknown> | null,
|
|
458
|
+
settings as unknown as Record<string, unknown>,
|
|
459
|
+
)) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
lastNotifiedSettings = settings;
|
|
463
|
+
for (const listener of settingsListeners) {
|
|
464
|
+
try {
|
|
465
|
+
listener(settings);
|
|
466
|
+
} catch {
|
|
467
|
+
// Ignore listener errors
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function PillOffline() {
|
|
473
|
+
const { colors } = usePluginTheme();
|
|
474
|
+
return (
|
|
475
|
+
<Row gap={4} align="center" style={styles.pillContainer}>
|
|
476
|
+
<Icon name="Ghost" size={13} color={colors.statusDanger} />
|
|
477
|
+
<Text numberOfLines={1} style={[styles.pillText, { color: colors.foregroundMuted }]}>
|
|
478
|
+
Offline
|
|
479
|
+
</Text>
|
|
480
|
+
</Row>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function PillLoading() {
|
|
485
|
+
const { colors } = usePluginTheme();
|
|
486
|
+
return (
|
|
487
|
+
<Text numberOfLines={1} style={[styles.pillText, { color: colors.foregroundMuted }]}>
|
|
488
|
+
top...
|
|
489
|
+
</Text>
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function PillEmpty() {
|
|
494
|
+
const { colors } = usePluginTheme();
|
|
495
|
+
return (
|
|
496
|
+
<Row gap={4} align="center" style={styles.pillContainer}>
|
|
497
|
+
<Icon name="Activity" size={12} color={colors.accent} />
|
|
498
|
+
<Text numberOfLines={1} style={[styles.pillText, { color: colors.foregroundMuted }]}>
|
|
499
|
+
top
|
|
500
|
+
</Text>
|
|
501
|
+
</Row>
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function McpLoadingPill() {
|
|
506
|
+
const { colors } = usePluginTheme();
|
|
507
|
+
return (
|
|
508
|
+
<Text numberOfLines={1} style={[styles.pillText, { color: colors.foregroundMuted }]}>
|
|
509
|
+
MCP…
|
|
510
|
+
</Text>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
interface PillSegmentProps {
|
|
515
|
+
item: PillItemType;
|
|
516
|
+
data?: SystemResources;
|
|
517
|
+
agent?: PillItemContentProps["agent"];
|
|
518
|
+
agentId?: string;
|
|
519
|
+
worktreeLocationText?: string;
|
|
520
|
+
isOpen?: boolean;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* The composer pill body is host-pressed: legacy hosts wrap `renderPill` in
|
|
525
|
+
* their own pressable and route the tap through `resolveDefaultPayload`, so the
|
|
526
|
+
* segment must not nest a second interaction handler.
|
|
527
|
+
*/
|
|
528
|
+
function PillSegment({
|
|
529
|
+
item,
|
|
530
|
+
data,
|
|
531
|
+
agent,
|
|
532
|
+
agentId,
|
|
533
|
+
worktreeLocationText,
|
|
534
|
+
isOpen,
|
|
535
|
+
}: PillSegmentProps) {
|
|
536
|
+
return (
|
|
537
|
+
<PillItemContent
|
|
538
|
+
item={item}
|
|
539
|
+
data={data}
|
|
540
|
+
agent={agent}
|
|
541
|
+
agentId={agentId}
|
|
542
|
+
worktreeLocationText={worktreeLocationText}
|
|
543
|
+
isOpen={isOpen}
|
|
544
|
+
/>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
interface AllInOnePillProps {
|
|
549
|
+
items: PillItemType[];
|
|
550
|
+
data?: SystemResources;
|
|
551
|
+
agent?: PillItemContentProps["agent"];
|
|
552
|
+
agentId?: string;
|
|
553
|
+
worktreeLocationText?: string;
|
|
554
|
+
isOpen?: boolean;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function AllInOnePill({
|
|
558
|
+
items,
|
|
559
|
+
data,
|
|
560
|
+
agent,
|
|
561
|
+
agentId,
|
|
562
|
+
worktreeLocationText,
|
|
563
|
+
isOpen,
|
|
564
|
+
}: AllInOnePillProps) {
|
|
565
|
+
const { colors } = usePluginTheme();
|
|
566
|
+
return (
|
|
567
|
+
<Row gap={6} align="center" style={styles.allInOneContainer}>
|
|
568
|
+
{items.map((item, idx) => (
|
|
569
|
+
<React.Fragment key={item}>
|
|
570
|
+
{idx > 0 && (
|
|
571
|
+
<Text style={[styles.dividerText, { color: colors.foregroundMuted }]}>│</Text>
|
|
572
|
+
)}
|
|
573
|
+
<PillSegment
|
|
574
|
+
item={item}
|
|
575
|
+
data={data}
|
|
576
|
+
agent={agent}
|
|
577
|
+
agentId={agentId}
|
|
578
|
+
worktreeLocationText={worktreeLocationText}
|
|
579
|
+
isOpen={isOpen}
|
|
580
|
+
/>
|
|
581
|
+
</React.Fragment>
|
|
582
|
+
))}
|
|
583
|
+
</Row>
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export interface SingleItemPillViewProps extends RenderPillProps<ModalTab> {
|
|
588
|
+
item: PillItemType;
|
|
589
|
+
defaultTab?: ModalTab;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export function SingleItemPillView({
|
|
593
|
+
item,
|
|
594
|
+
workspaceId,
|
|
595
|
+
agentId,
|
|
596
|
+
isOpen,
|
|
597
|
+
}: SingleItemPillViewProps) {
|
|
598
|
+
const workspaceDirectory = useWorkspace(workspaceId, (w: PluginWorkspaceSnapshot) => w?.directory);
|
|
599
|
+
const agent = useAgent(agentId, (a: TopAgentSnapshot) => ({
|
|
600
|
+
title: a?.title,
|
|
601
|
+
model: a?.model,
|
|
602
|
+
provider: a?.provider,
|
|
603
|
+
status: a?.status,
|
|
604
|
+
lastActivityAt: a?.lastActivityAt,
|
|
605
|
+
lastUsage: a?.lastUsage,
|
|
606
|
+
}));
|
|
607
|
+
|
|
608
|
+
const needsResource = useMemo(() => {
|
|
609
|
+
switch (item) {
|
|
610
|
+
case "cpu_ram":
|
|
611
|
+
case "load":
|
|
612
|
+
case "uptime":
|
|
613
|
+
case "mcp":
|
|
614
|
+
return true;
|
|
615
|
+
case "branch":
|
|
616
|
+
return Boolean(workspaceDirectory);
|
|
617
|
+
default:
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
}, [item, workspaceDirectory]);
|
|
621
|
+
|
|
622
|
+
const shouldPoll = needsResource;
|
|
623
|
+
const { data, isError, isLoading } = useTopResourceQuery(workspaceDirectory, {
|
|
624
|
+
enabled: shouldPoll,
|
|
625
|
+
staleTime: 2500,
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const worktreeLocationText = useMemo(
|
|
629
|
+
() => formatWorktreeLocation(workspaceDirectory),
|
|
630
|
+
[workspaceDirectory],
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
useEffect(() => {
|
|
634
|
+
updateLiveSnapshot(agentId, {
|
|
635
|
+
data,
|
|
636
|
+
agent,
|
|
637
|
+
agentId,
|
|
638
|
+
workspaceDirectory,
|
|
639
|
+
worktreeLocationText,
|
|
640
|
+
});
|
|
641
|
+
}, [agentId, data, agent, workspaceDirectory, worktreeLocationText]);
|
|
642
|
+
|
|
643
|
+
if (item === "mcp") {
|
|
644
|
+
if (data && !data.mcpInstalled) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
if (isLoading || !data) {
|
|
648
|
+
return <McpLoadingPill />;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (shouldPoll && isError) {
|
|
653
|
+
return <PillOffline />;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (shouldPoll && (isLoading || !data)) {
|
|
657
|
+
return <PillLoading />;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return (
|
|
661
|
+
<PillSegment
|
|
662
|
+
item={item}
|
|
663
|
+
data={data}
|
|
664
|
+
agent={agent}
|
|
665
|
+
agentId={agentId}
|
|
666
|
+
worktreeLocationText={worktreeLocationText}
|
|
667
|
+
isOpen={isOpen}
|
|
668
|
+
/>
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function PillView({ isOpen, open, workspaceId, agentId }: RenderPillProps<ModalTab>) {
|
|
673
|
+
// No background settings poll here: the hook re-verifies on mount and
|
|
674
|
+
// window focus, and mutations invalidate the shared settings cache.
|
|
675
|
+
// Polling from every mounted pill/modal/surface multiplied daemon reads
|
|
676
|
+
// with no value change; fan-out stays guarded by notifySettingsChanged.
|
|
677
|
+
const { settings, updateSettings } = usePluginSettings(topSettingsContract);
|
|
678
|
+
const flags = legacyFlagView(settings);
|
|
679
|
+
|
|
680
|
+
useEffect(() => {
|
|
681
|
+
notifySettingsChanged(settings);
|
|
682
|
+
}, [settings]);
|
|
683
|
+
|
|
684
|
+
const workspaceDirectory = useWorkspace(workspaceId, (w: PluginWorkspaceSnapshot) => w?.directory);
|
|
685
|
+
const agent = useAgent(agentId, (a: TopAgentSnapshot) => ({
|
|
686
|
+
title: a?.title,
|
|
687
|
+
model: a?.model,
|
|
688
|
+
provider: a?.provider,
|
|
689
|
+
status: a?.status,
|
|
690
|
+
lastActivityAt: a?.lastActivityAt,
|
|
691
|
+
lastUsage: a?.lastUsage,
|
|
692
|
+
}));
|
|
693
|
+
|
|
694
|
+
const hasAnyEnabled =
|
|
695
|
+
flags.showCpuRam ||
|
|
696
|
+
flags.showBranch ||
|
|
697
|
+
flags.showWorktree ||
|
|
698
|
+
flags.showAgentTitle ||
|
|
699
|
+
flags.showAgent ||
|
|
700
|
+
flags.showAgentProvider ||
|
|
701
|
+
flags.showAgentActivity ||
|
|
702
|
+
flags.showAgentId ||
|
|
703
|
+
flags.showLoad ||
|
|
704
|
+
flags.showUptime ||
|
|
705
|
+
(flags.showMcp);
|
|
706
|
+
|
|
707
|
+
// If no items are selected, fallback to CPU & RAM without mutating saved settings
|
|
708
|
+
const effectiveShowCpuRam = flags.showCpuRam || !hasAnyEnabled;
|
|
709
|
+
|
|
710
|
+
const shouldPoll = true;
|
|
711
|
+
const { data, isError, isLoading } = useTopResourceQuery(workspaceDirectory, {
|
|
712
|
+
staleTime: 2500,
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
const isMcpEnabled = isMcpSurfaceEnabled(settings, "pill", data?.mcpInstalled, data?.mcpRunning);
|
|
716
|
+
|
|
717
|
+
useEffect(() => {
|
|
718
|
+
const found = new Set<MetricId>();
|
|
719
|
+
const live = data?.liveUsage;
|
|
720
|
+
if (live && (live.inputTokens != null || live.outputTokens != null)) {
|
|
721
|
+
found.add("tokens");
|
|
722
|
+
}
|
|
723
|
+
const last = data?.lastTurn;
|
|
724
|
+
if (last && (last.inputTokens != null || last.outputTokens != null)) {
|
|
725
|
+
found.add("tokens");
|
|
726
|
+
}
|
|
727
|
+
const agentUsage = agent?.lastUsage;
|
|
728
|
+
if (
|
|
729
|
+
agentUsage &&
|
|
730
|
+
(agentUsage.inputTokens != null ||
|
|
731
|
+
agentUsage.outputTokens != null ||
|
|
732
|
+
agentUsage.contextWindowUsedTokens != null)
|
|
733
|
+
) {
|
|
734
|
+
found.add("tokens");
|
|
735
|
+
}
|
|
736
|
+
if (agent?.model || agent?.provider) {
|
|
737
|
+
found.add("agent");
|
|
738
|
+
found.add("agent_provider");
|
|
739
|
+
}
|
|
740
|
+
if (found.size > 0) {
|
|
741
|
+
const have = new Set(settings.provisionedMetrics ?? []);
|
|
742
|
+
const additions = [...found].filter((id) => !have.has(id));
|
|
743
|
+
if (additions.length > 0) {
|
|
744
|
+
updateSettings({ provisionedMetrics: [...have, ...additions] });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
748
|
+
}, [data?.lastTurn, data?.liveUsage, agent?.model, agent?.provider, agent?.lastUsage]);
|
|
749
|
+
|
|
750
|
+
const worktreeLocationText = useMemo(
|
|
751
|
+
() => formatWorktreeLocation(workspaceDirectory),
|
|
752
|
+
[workspaceDirectory],
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
useEffect(() => {
|
|
756
|
+
updateLiveSnapshot(agentId, {
|
|
757
|
+
data,
|
|
758
|
+
agent,
|
|
759
|
+
agentId,
|
|
760
|
+
workspaceDirectory,
|
|
761
|
+
worktreeLocationText,
|
|
762
|
+
});
|
|
763
|
+
}, [agentId, data, agent, workspaceDirectory, worktreeLocationText]);
|
|
764
|
+
|
|
765
|
+
// Collect available items enabled by user settings (or fallback to cpu_ram).
|
|
766
|
+
// Visibility is selector-only: components render placeholders when data is
|
|
767
|
+
// absent, so every mode agrees on what is shown.
|
|
768
|
+
const items: PillItemType[] = [];
|
|
769
|
+
if (effectiveShowCpuRam) items.push("cpu_ram");
|
|
770
|
+
if (flags.showBranch) items.push("branch");
|
|
771
|
+
if (flags.showWorktree) items.push("worktree");
|
|
772
|
+
if (flags.showAgentTitle) items.push("agent_title");
|
|
773
|
+
if (flags.showAgent) items.push("agent");
|
|
774
|
+
if (flags.showAgentProvider) items.push("agent_provider");
|
|
775
|
+
if (flags.showAgentActivity) items.push("agent_activity");
|
|
776
|
+
if (flags.showAgentId) items.push("agent_id");
|
|
777
|
+
if (flags.showLoad) items.push("load");
|
|
778
|
+
if (flags.showUptime) items.push("uptime");
|
|
779
|
+
if (isMcpEnabled) items.push("mcp");
|
|
780
|
+
if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.changes)) {
|
|
781
|
+
items.push("changes");
|
|
782
|
+
}
|
|
783
|
+
if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.tokens)) {
|
|
784
|
+
items.push("tokens");
|
|
785
|
+
}
|
|
786
|
+
if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.tools)) {
|
|
787
|
+
items.push("tools");
|
|
788
|
+
}
|
|
789
|
+
if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.turns)) {
|
|
790
|
+
items.push("turns");
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Legacy render fallback only. On the supported 0.8 host the pill is
|
|
794
|
+
// host-rendered from `resolveLabel`, which advances the cycle on the shared
|
|
795
|
+
// visibility-gated timer; a per-agent React interval here duplicated that
|
|
796
|
+
// ticker with no RPC value.
|
|
797
|
+
const activeMode = items.length > 0 ? items[0] : "cpu_ram";
|
|
798
|
+
|
|
799
|
+
useEffect(() => {
|
|
800
|
+
currentCycleTabByAgent.set(agentId, getItemTab(activeMode));
|
|
801
|
+
return () => {
|
|
802
|
+
currentCycleTabByAgent.delete(agentId);
|
|
803
|
+
};
|
|
804
|
+
}, [agentId, activeMode]);
|
|
805
|
+
|
|
806
|
+
if (shouldPoll && isError) {
|
|
807
|
+
return <PillOffline />;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (shouldPoll && (isLoading || !data)) {
|
|
811
|
+
return <PillLoading />;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (items.length === 0) {
|
|
815
|
+
return <PillEmpty />;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (settings.pillMode === "all") {
|
|
819
|
+
return (
|
|
820
|
+
<AllInOnePill
|
|
821
|
+
items={items}
|
|
822
|
+
data={data}
|
|
823
|
+
agent={agent}
|
|
824
|
+
agentId={agentId}
|
|
825
|
+
worktreeLocationText={worktreeLocationText}
|
|
826
|
+
isOpen={isOpen}
|
|
827
|
+
/>
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return (
|
|
832
|
+
<PillSegment
|
|
833
|
+
item={activeMode}
|
|
834
|
+
data={data}
|
|
835
|
+
agent={agent}
|
|
836
|
+
agentId={agentId}
|
|
837
|
+
worktreeLocationText={worktreeLocationText}
|
|
838
|
+
isOpen={isOpen}
|
|
839
|
+
/>
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
interface ResourceModalProps extends RenderModalProps<ModalTab> {
|
|
844
|
+
initialTab?: ModalTab;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function MetricSurfaceMatrix({
|
|
848
|
+
settings,
|
|
849
|
+
updateSettings,
|
|
850
|
+
notifySettingsChanged,
|
|
851
|
+
mcpInstalled,
|
|
852
|
+
mcpRunning,
|
|
853
|
+
customPills,
|
|
854
|
+
customOverrides,
|
|
855
|
+
onCustomToggle,
|
|
856
|
+
}: {
|
|
857
|
+
settings: TopSettings;
|
|
858
|
+
updateSettings: (updates: Partial<TopSettings>) => void;
|
|
859
|
+
notifySettingsChanged: (s: TopSettings) => void;
|
|
860
|
+
mcpInstalled: boolean;
|
|
861
|
+
mcpRunning?: boolean | null;
|
|
862
|
+
customPills: Array<{ id: string; title: string; sourceFile?: string; enabled: boolean }>;
|
|
863
|
+
customOverrides: Record<string, boolean> | undefined;
|
|
864
|
+
onCustomToggle: (id: string, val: boolean) => void;
|
|
865
|
+
}) {
|
|
866
|
+
const { colors, isCompact } = usePluginTheme();
|
|
867
|
+
const surfaces = settings.metricSurfaces ?? DEFAULT_METRIC_SURFACES;
|
|
868
|
+
const provisioned = new Set(settings.provisionedMetrics ?? []);
|
|
869
|
+
const matrixRowMinHeight = isCompact ? 44 : 32;
|
|
870
|
+
const setTarget = (id: MetricId, target: SurfaceTarget) => {
|
|
871
|
+
const next = { ...surfaces, [id]: target };
|
|
872
|
+
const s = { ...settings, metricSurfaces: next };
|
|
873
|
+
updateSettings({ metricSurfaces: next });
|
|
874
|
+
notifySettingsChanged(s);
|
|
875
|
+
};
|
|
876
|
+
return (
|
|
877
|
+
<View style={[styles.metricMatrix, isCompact && styles.metricMatrixCompact]}>
|
|
878
|
+
<View style={[styles.metricMatrixHeader, isCompact && styles.metricMatrixHeaderCompact]}>
|
|
879
|
+
<Text style={[styles.metricMatrixHeaderText, { color: colors.foregroundMuted }]}>
|
|
880
|
+
Metric
|
|
881
|
+
</Text>
|
|
882
|
+
<View style={[styles.metricMatrixTargetHeader, isCompact && styles.metricMatrixTargetHeaderCompact]}>
|
|
883
|
+
<Text style={[styles.metricMatrixHeaderText, { color: colors.foregroundMuted }]}>
|
|
884
|
+
Pill
|
|
885
|
+
</Text>
|
|
886
|
+
<Text style={[styles.metricMatrixHeaderText, { color: colors.foregroundMuted }]}>
|
|
887
|
+
Timeline
|
|
888
|
+
</Text>
|
|
889
|
+
</View>
|
|
890
|
+
</View>
|
|
891
|
+
{METRIC_DEFINITIONS.map((def, index) => {
|
|
892
|
+
const boxes = checkboxesFromTarget(surfaces[def.id]);
|
|
893
|
+
const unprovisioned =
|
|
894
|
+
isProviderDependent(def.id) && !provisioned.has(def.id);
|
|
895
|
+
const timelineDisabled = !!def.pillOnly;
|
|
896
|
+
const disabled = def.id === "mcp" && !mcpInstalled;
|
|
897
|
+
const note =
|
|
898
|
+
def.pillOnly
|
|
899
|
+
? "live only"
|
|
900
|
+
: def.id === "mcp" && !mcpInstalled
|
|
901
|
+
? "mcp-tools not installed"
|
|
902
|
+
: def.id === "mcp" && mcpRunning === false
|
|
903
|
+
? "mcp-tools disabled"
|
|
904
|
+
: unprovisioned
|
|
905
|
+
? "waiting for provider"
|
|
906
|
+
: null;
|
|
907
|
+
const setBox = (which: "pill" | "timeline", val: boolean) => {
|
|
908
|
+
const nextBoxes = { ...boxes, [which]: val };
|
|
909
|
+
if (def.pillOnly) nextBoxes.timeline = false;
|
|
910
|
+
setTarget(
|
|
911
|
+
def.id,
|
|
912
|
+
targetFromCheckboxes(nextBoxes.pill, nextBoxes.timeline),
|
|
913
|
+
);
|
|
914
|
+
};
|
|
915
|
+
return (
|
|
916
|
+
<View
|
|
917
|
+
key={def.id}
|
|
918
|
+
style={[
|
|
919
|
+
styles.metricMatrixRow,
|
|
920
|
+
isCompact && styles.metricMatrixRowCompact,
|
|
921
|
+
{ minHeight: matrixRowMinHeight },
|
|
922
|
+
index < METRIC_DEFINITIONS.length - 1
|
|
923
|
+
? {
|
|
924
|
+
borderBottomWidth: 1,
|
|
925
|
+
borderBottomColor: colors.border,
|
|
926
|
+
}
|
|
927
|
+
: undefined,
|
|
928
|
+
]}
|
|
929
|
+
>
|
|
930
|
+
<View style={styles.metricMatrixLabel}>
|
|
931
|
+
<Text style={[styles.metricMatrixTitle, { color: colors.foreground }]}>
|
|
932
|
+
{def.title}
|
|
933
|
+
</Text>
|
|
934
|
+
{note ? (
|
|
935
|
+
<Text style={[styles.metricMatrixNote, { color: colors.foregroundMuted }]}>
|
|
936
|
+
{note}
|
|
937
|
+
</Text>
|
|
938
|
+
) : null}
|
|
939
|
+
</View>
|
|
940
|
+
<View style={[styles.metricMatrixTargets, isCompact && styles.metricMatrixTargetsCompact]}>
|
|
941
|
+
<Toggle
|
|
942
|
+
value={boxes.pill}
|
|
943
|
+
disabled={disabled}
|
|
944
|
+
onValueChange={(val) => setBox("pill", val)}
|
|
945
|
+
style={styles.matrixToggle}
|
|
946
|
+
/>
|
|
947
|
+
<Toggle
|
|
948
|
+
value={boxes.timeline && !def.pillOnly}
|
|
949
|
+
disabled={disabled || timelineDisabled}
|
|
950
|
+
onValueChange={(val) => setBox("timeline", val)}
|
|
951
|
+
style={styles.matrixToggle}
|
|
952
|
+
/>
|
|
953
|
+
</View>
|
|
954
|
+
</View>
|
|
955
|
+
);
|
|
956
|
+
})}
|
|
957
|
+
{customPills.map((pill) => {
|
|
958
|
+
const masterOn = settings.showCustomPills ?? true;
|
|
959
|
+
const enabled = customPillEffectiveEnabled(masterOn, customOverrides, pill);
|
|
960
|
+
return (
|
|
961
|
+
<View
|
|
962
|
+
key={`custom-${pill.id}`}
|
|
963
|
+
style={styles.metricMatrixRow}
|
|
964
|
+
>
|
|
965
|
+
<View style={styles.metricMatrixLabel}>
|
|
966
|
+
<Text style={[styles.metricMatrixTitle, { color: colors.foreground }]}>
|
|
967
|
+
{pill.title}
|
|
968
|
+
</Text>
|
|
969
|
+
<Text style={[styles.metricMatrixNote, { color: colors.foregroundMuted }]}>
|
|
970
|
+
custom pill
|
|
971
|
+
</Text>
|
|
972
|
+
</View>
|
|
973
|
+
<View style={[styles.metricMatrixTargets, isCompact && styles.metricMatrixTargetsCompact]}>
|
|
974
|
+
<Toggle
|
|
975
|
+
value={enabled}
|
|
976
|
+
disabled={!masterOn}
|
|
977
|
+
onValueChange={(val) => onCustomToggle(pill.id, val)}
|
|
978
|
+
style={styles.matrixToggle}
|
|
979
|
+
/>
|
|
980
|
+
<Toggle
|
|
981
|
+
value={false}
|
|
982
|
+
disabled={true}
|
|
983
|
+
onValueChange={() => {}}
|
|
984
|
+
style={styles.matrixToggle}
|
|
985
|
+
/>
|
|
986
|
+
</View>
|
|
987
|
+
</View>
|
|
988
|
+
);
|
|
989
|
+
})}
|
|
990
|
+
</View>
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: ResourceModalProps) {
|
|
995
|
+
const { colors, padding } = usePluginTheme();
|
|
996
|
+
// Settings re-verify on mount/focus via hook defaults; the settings tab
|
|
997
|
+
// refetches explicitly below. No background poll: the modal only lives
|
|
998
|
+
// while open. Custom-pill definitions are a separate data source and keep
|
|
999
|
+
// their own lightweight poll while the modal is mounted.
|
|
1000
|
+
const { settings, updateSettings, resetSettings, refetch: refetchSettings } = usePluginSettings(
|
|
1001
|
+
topSettingsContract,
|
|
1002
|
+
);
|
|
1003
|
+
const { data: customPillList } = useRpcQuery(listCustomPillsRpc, EMPTY_PARAMS, {
|
|
1004
|
+
refetchInterval: 5000,
|
|
1005
|
+
});
|
|
1006
|
+
const flags = legacyFlagView(settings);
|
|
1007
|
+
const [selectedTab, setSelectedTab] = useState<ModalTab | null>(null);
|
|
1008
|
+
const activeTab = selectedTab ?? payload ?? initialTab ?? settings.defaultTab ?? "system";
|
|
1009
|
+
|
|
1010
|
+
useEffect(() => {
|
|
1011
|
+
if (payload) {
|
|
1012
|
+
setSelectedTab(payload);
|
|
1013
|
+
}
|
|
1014
|
+
}, [payload]);
|
|
1015
|
+
|
|
1016
|
+
// Ensure freshest settings are fetched whenever user views settings
|
|
1017
|
+
useEffect(() => {
|
|
1018
|
+
if (activeTab === "settings") {
|
|
1019
|
+
void refetchSettings();
|
|
1020
|
+
}
|
|
1021
|
+
}, [activeTab, refetchSettings]);
|
|
1022
|
+
|
|
1023
|
+
const workspace = useWorkspace(workspaceId, (w: PluginWorkspaceSnapshot) => ({
|
|
1024
|
+
name: w?.name,
|
|
1025
|
+
title: w?.title,
|
|
1026
|
+
projectDisplayName: w?.projectDisplayName,
|
|
1027
|
+
directory: w?.directory,
|
|
1028
|
+
kind: w?.kind,
|
|
1029
|
+
status: w?.status,
|
|
1030
|
+
statusEnteredAt: w?.statusEnteredAt,
|
|
1031
|
+
diffStat: w?.diffStat,
|
|
1032
|
+
}));
|
|
1033
|
+
|
|
1034
|
+
const agent = useAgent(agentId, (a: TopAgentSnapshot) => ({
|
|
1035
|
+
title: a?.title,
|
|
1036
|
+
model: a?.model,
|
|
1037
|
+
provider: a?.provider,
|
|
1038
|
+
status: a?.status,
|
|
1039
|
+
cwd: a?.cwd,
|
|
1040
|
+
lastActivityAt: a?.lastActivityAt,
|
|
1041
|
+
lastUsage: a?.lastUsage,
|
|
1042
|
+
}));
|
|
1043
|
+
|
|
1044
|
+
const { data, isError, error, isLoading, isRefetching, refetch } = useTopResourceQuery(
|
|
1045
|
+
workspace?.directory,
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
const tokenMetrics = useMemo(() => {
|
|
1049
|
+
return extractTokenMetrics({ data, agent });
|
|
1050
|
+
}, [data, agent]);
|
|
1051
|
+
|
|
1052
|
+
const contextPercent = useMemo(() => {
|
|
1053
|
+
if (!tokenMetrics?.contextMaxTokens || tokenMetrics.contextMaxTokens <= 0) return null;
|
|
1054
|
+
return Math.round(((tokenMetrics.contextUsedTokens ?? 0) / tokenMetrics.contextMaxTokens) * 100);
|
|
1055
|
+
}, [tokenMetrics]);
|
|
1056
|
+
|
|
1057
|
+
const isMcpEnabled = isMcpSurfaceEnabled(settings, "pill", data?.mcpInstalled, data?.mcpRunning);
|
|
1058
|
+
|
|
1059
|
+
useEffect(() => {
|
|
1060
|
+
updateLiveSnapshot(agentId, {
|
|
1061
|
+
data,
|
|
1062
|
+
agent,
|
|
1063
|
+
agentId,
|
|
1064
|
+
workspaceDirectory: workspace?.directory,
|
|
1065
|
+
worktreeLocationText: workspace?.directory
|
|
1066
|
+
? formatWorktreeLocation(workspace.directory)
|
|
1067
|
+
: undefined,
|
|
1068
|
+
});
|
|
1069
|
+
}, [agentId, data, agent, workspace?.directory]);
|
|
1070
|
+
|
|
1071
|
+
const hasAnyPillEnabled =
|
|
1072
|
+
flags.showCpuRam ||
|
|
1073
|
+
flags.showBranch ||
|
|
1074
|
+
flags.showWorktree ||
|
|
1075
|
+
flags.showAgentTitle ||
|
|
1076
|
+
flags.showAgent ||
|
|
1077
|
+
flags.showAgentProvider ||
|
|
1078
|
+
flags.showAgentActivity ||
|
|
1079
|
+
flags.showAgentId ||
|
|
1080
|
+
flags.showLoad ||
|
|
1081
|
+
flags.showUptime ||
|
|
1082
|
+
isMcpEnabled;
|
|
1083
|
+
|
|
1084
|
+
const handleRefresh = () => {
|
|
1085
|
+
triggerHaptic("light");
|
|
1086
|
+
refetch();
|
|
1087
|
+
void refetchSettings();
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
const handleTabChange = (tabId: string) => {
|
|
1091
|
+
triggerHaptic("light");
|
|
1092
|
+
setSelectedTab(tabId as ModalTab);
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
const navbar = (
|
|
1096
|
+
<Tabs
|
|
1097
|
+
tabs={TABS}
|
|
1098
|
+
activeTab={activeTab}
|
|
1099
|
+
onTabChange={handleTabChange}
|
|
1100
|
+
/>
|
|
1101
|
+
);
|
|
1102
|
+
const navbarStyle = {
|
|
1103
|
+
backgroundColor: colors.surface0,
|
|
1104
|
+
paddingHorizontal: padding.horizontal,
|
|
1105
|
+
paddingTop: padding.vertical,
|
|
1106
|
+
paddingBottom: Math.round(padding.gap / 2),
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
if (isError && !data) {
|
|
1110
|
+
return (
|
|
1111
|
+
<ModalBody
|
|
1112
|
+
style={{ backgroundColor: colors.surface0 }}
|
|
1113
|
+
headerMode="pinned"
|
|
1114
|
+
refreshing={isRefetching}
|
|
1115
|
+
onRefresh={handleRefresh}
|
|
1116
|
+
header={navbar}
|
|
1117
|
+
headerStyle={navbarStyle}
|
|
1118
|
+
contentContainerStyle={[{ paddingTop: Math.round(padding.gap / 2) }]}
|
|
1119
|
+
>
|
|
1120
|
+
<Card variant="elevated">
|
|
1121
|
+
<View style={styles.errorBox}>
|
|
1122
|
+
<Icon name="Ghost" size={24} color={colors.statusDanger} />
|
|
1123
|
+
<Text style={[styles.errorText, { color: colors.statusDanger }]}>
|
|
1124
|
+
{error instanceof Error ? error.message : "Failed to load metrics"}
|
|
1125
|
+
</Text>
|
|
1126
|
+
</View>
|
|
1127
|
+
</Card>
|
|
1128
|
+
</ModalBody>
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const { cpuColor, memColor } = getMetricColors(data, colors);
|
|
1133
|
+
|
|
1134
|
+
return (
|
|
1135
|
+
<ModalBody
|
|
1136
|
+
style={{ backgroundColor: colors.surface0 }}
|
|
1137
|
+
headerMode="pinned"
|
|
1138
|
+
refreshing={isLoading || isRefetching}
|
|
1139
|
+
onRefresh={handleRefresh}
|
|
1140
|
+
header={navbar}
|
|
1141
|
+
headerStyle={navbarStyle}
|
|
1142
|
+
contentContainerStyle={[
|
|
1143
|
+
{ paddingTop: Math.round(padding.gap / 2) },
|
|
1144
|
+
]}
|
|
1145
|
+
>
|
|
1146
|
+
{activeTab === "system" && (
|
|
1147
|
+
<>
|
|
1148
|
+
{/* Dual Metric Gauges Hero */}
|
|
1149
|
+
<Card variant="elevated">
|
|
1150
|
+
<View style={styles.gaugeContainer}>
|
|
1151
|
+
<MetricGauge
|
|
1152
|
+
value={data?.cpuUsagePercent ?? 0}
|
|
1153
|
+
thresholds={CPU_THRESHOLDS}
|
|
1154
|
+
label="CPU Load"
|
|
1155
|
+
size={72}
|
|
1156
|
+
/>
|
|
1157
|
+
<MetricGauge
|
|
1158
|
+
value={data?.memoryUsedPercent ?? 0}
|
|
1159
|
+
thresholds={MEM_THRESHOLDS}
|
|
1160
|
+
label="RAM Used"
|
|
1161
|
+
size={72}
|
|
1162
|
+
/>
|
|
1163
|
+
</View>
|
|
1164
|
+
</Card>
|
|
1165
|
+
|
|
1166
|
+
{/* Host Meta Card */}
|
|
1167
|
+
<Card variant="elevated">
|
|
1168
|
+
<KeyValueGroup columns={1}>
|
|
1169
|
+
<CompactKeyValue label="Host" value={data?.hostname ?? "Unknown"} copyable />
|
|
1170
|
+
<CompactKeyValue
|
|
1171
|
+
label="Uptime"
|
|
1172
|
+
value={data?.uptimeSeconds ? formatUptime(data?.uptimeSeconds) : "--"}
|
|
1173
|
+
/>
|
|
1174
|
+
</KeyValueGroup>
|
|
1175
|
+
<CompactKeyValue
|
|
1176
|
+
label="Processor"
|
|
1177
|
+
value={data?.cpuModel ?? "--"}
|
|
1178
|
+
subValue={data?.cpuCores ? `(${data?.cpuCores} cores)` : undefined}
|
|
1179
|
+
/>
|
|
1180
|
+
</Card>
|
|
1181
|
+
|
|
1182
|
+
{/* CPU Utilization Card */}
|
|
1183
|
+
<Card variant="elevated">
|
|
1184
|
+
<CompactCardHeader
|
|
1185
|
+
title="CPU Details"
|
|
1186
|
+
value={
|
|
1187
|
+
<Text style={[styles.metricHighlight, { color: cpuColor }]}>
|
|
1188
|
+
{data?.cpuUsagePercent ?? 0}%
|
|
1189
|
+
</Text>
|
|
1190
|
+
}
|
|
1191
|
+
/>
|
|
1192
|
+
|
|
1193
|
+
<ProgressBar
|
|
1194
|
+
value={data?.cpuUsagePercent ?? 0}
|
|
1195
|
+
thresholds={CPU_THRESHOLDS}
|
|
1196
|
+
height={8}
|
|
1197
|
+
/>
|
|
1198
|
+
|
|
1199
|
+
<CompactKeyValue
|
|
1200
|
+
label="Load Average (1m, 5m, 15m)"
|
|
1201
|
+
value={data?.loadAvg ? data?.loadAvg.map((n) => n.toFixed(2)).join(" ") : "-- -- --"}
|
|
1202
|
+
mono
|
|
1203
|
+
/>
|
|
1204
|
+
</Card>
|
|
1205
|
+
|
|
1206
|
+
{/* Memory Card */}
|
|
1207
|
+
<Card variant="elevated">
|
|
1208
|
+
<CompactCardHeader
|
|
1209
|
+
title="Memory Details"
|
|
1210
|
+
value={
|
|
1211
|
+
<Text style={[styles.metricHighlight, { color: memColor }]}>
|
|
1212
|
+
{data?.memoryUsedPercent ?? 0}%
|
|
1213
|
+
</Text>
|
|
1214
|
+
}
|
|
1215
|
+
/>
|
|
1216
|
+
|
|
1217
|
+
<ProgressBar
|
|
1218
|
+
value={data?.memoryUsedPercent ?? 0}
|
|
1219
|
+
thresholds={MEM_THRESHOLDS}
|
|
1220
|
+
height={8}
|
|
1221
|
+
/>
|
|
1222
|
+
|
|
1223
|
+
<CompactKeyValue
|
|
1224
|
+
label="Used / Total"
|
|
1225
|
+
value={`${formatBytes(data?.memoryUsedBytes ?? 0)} / ${formatBytes(data?.memoryTotalBytes ?? 0)}`}
|
|
1226
|
+
/>
|
|
1227
|
+
</Card>
|
|
1228
|
+
|
|
1229
|
+
{/* MCP Servers Card */}
|
|
1230
|
+
{Boolean(data?.mcpInstalled) && (
|
|
1231
|
+
<Card variant="elevated">
|
|
1232
|
+
<CompactCardHeader
|
|
1233
|
+
title="MCP Servers"
|
|
1234
|
+
subtitle="Source: paseo-mcp-tools"
|
|
1235
|
+
icon="Server"
|
|
1236
|
+
value={
|
|
1237
|
+
data?.mcp ? (
|
|
1238
|
+
<CompactBadge
|
|
1239
|
+
label={data?.mcp.isStale ? "Stale Snapshot" : "Live"}
|
|
1240
|
+
variant={data?.mcp.isStale ? "warning" : "success"}
|
|
1241
|
+
dot
|
|
1242
|
+
/>
|
|
1243
|
+
) : (
|
|
1244
|
+
<CompactBadge label="No Data" variant="neutral" />
|
|
1245
|
+
)
|
|
1246
|
+
}
|
|
1247
|
+
/>
|
|
1248
|
+
|
|
1249
|
+
{data?.mcp ? (
|
|
1250
|
+
<>
|
|
1251
|
+
<KeyValueGroup columns={1}>
|
|
1252
|
+
<CompactKeyValue
|
|
1253
|
+
label="Health"
|
|
1254
|
+
value={`${data?.mcp.healthy} healthy / ${data?.mcp.total} total`}
|
|
1255
|
+
/>
|
|
1256
|
+
<CompactKeyValue
|
|
1257
|
+
label="Snapshot Updated"
|
|
1258
|
+
value={formatTimeAgo(data?.mcp.updatedAt)}
|
|
1259
|
+
/>
|
|
1260
|
+
<CompactKeyValue
|
|
1261
|
+
label="Data Provider"
|
|
1262
|
+
value="paseo-mcp-tools"
|
|
1263
|
+
/>
|
|
1264
|
+
</KeyValueGroup>
|
|
1265
|
+
|
|
1266
|
+
{data?.mcp.servers && data?.mcp.servers.length > 0 ? (
|
|
1267
|
+
<View style={styles.mcpList}>
|
|
1268
|
+
{data?.mcp.servers.map((srv) => {
|
|
1269
|
+
const badgeVariant: "success" | "warning" | "danger" | "neutral" =
|
|
1270
|
+
srv.status === "healthy"
|
|
1271
|
+
? "success"
|
|
1272
|
+
: srv.status === "degraded"
|
|
1273
|
+
? "warning"
|
|
1274
|
+
: srv.status === "down"
|
|
1275
|
+
? "danger"
|
|
1276
|
+
: "neutral";
|
|
1277
|
+
return (
|
|
1278
|
+
<View key={srv.name} style={styles.mcpRow}>
|
|
1279
|
+
<View style={styles.mcpInfo}>
|
|
1280
|
+
<Text
|
|
1281
|
+
numberOfLines={1}
|
|
1282
|
+
style={[styles.mcpName, { color: colors.foreground }]}
|
|
1283
|
+
>
|
|
1284
|
+
{srv.name}
|
|
1285
|
+
</Text>
|
|
1286
|
+
<Text style={[styles.mcpLatency, { color: colors.foregroundMuted }]}>
|
|
1287
|
+
{srv.latencyMs >= 0 ? `${srv.latencyMs}ms` : "timeout"}
|
|
1288
|
+
</Text>
|
|
1289
|
+
</View>
|
|
1290
|
+
<CompactBadge label={srv.status} variant={badgeVariant} />
|
|
1291
|
+
</View>
|
|
1292
|
+
);
|
|
1293
|
+
})}
|
|
1294
|
+
</View>
|
|
1295
|
+
) : (
|
|
1296
|
+
<Text style={[styles.mcpEmpty, { color: colors.foregroundMuted }]}>
|
|
1297
|
+
No MCP servers configured
|
|
1298
|
+
</Text>
|
|
1299
|
+
)}
|
|
1300
|
+
</>
|
|
1301
|
+
) : (
|
|
1302
|
+
<Text style={[styles.mcpEmpty, { color: colors.foregroundMuted }]}>
|
|
1303
|
+
Waiting for status snapshot from paseo-mcp-tools...
|
|
1304
|
+
</Text>
|
|
1305
|
+
)}
|
|
1306
|
+
</Card>
|
|
1307
|
+
)}
|
|
1308
|
+
|
|
1309
|
+
{/* Custom Metric Pills Card */}
|
|
1310
|
+
{Boolean(data?.customPills && data?.customPills.length > 0) && (
|
|
1311
|
+
<Card variant="elevated">
|
|
1312
|
+
<CompactCardHeader
|
|
1313
|
+
title="Custom Metric Pills"
|
|
1314
|
+
subtitle="Discovered from ~/.paseo/top/pills"
|
|
1315
|
+
icon="Sliders"
|
|
1316
|
+
value={
|
|
1317
|
+
<CompactBadge
|
|
1318
|
+
label={`${data?.customPills?.length ?? 0} active`}
|
|
1319
|
+
variant="accent"
|
|
1320
|
+
/>
|
|
1321
|
+
}
|
|
1322
|
+
/>
|
|
1323
|
+
<KeyValueGroup columns={1}>
|
|
1324
|
+
{(data?.customPills ?? []).map((cp) => (
|
|
1325
|
+
<CompactKeyValue
|
|
1326
|
+
key={cp.id}
|
|
1327
|
+
label={cp.title}
|
|
1328
|
+
value={cp.displayValue}
|
|
1329
|
+
subValue={cp.status !== "neutral" ? `(${cp.status})` : undefined}
|
|
1330
|
+
/>
|
|
1331
|
+
))}
|
|
1332
|
+
</KeyValueGroup>
|
|
1333
|
+
</Card>
|
|
1334
|
+
)}
|
|
1335
|
+
</>
|
|
1336
|
+
)}
|
|
1337
|
+
|
|
1338
|
+
{activeTab === "context" && (
|
|
1339
|
+
<>
|
|
1340
|
+
{/* Workspace Information */}
|
|
1341
|
+
<Card variant="elevated">
|
|
1342
|
+
<CompactCardHeader
|
|
1343
|
+
title="Workspace & Git"
|
|
1344
|
+
icon="GitBranch"
|
|
1345
|
+
value={
|
|
1346
|
+
workspace?.status ? (
|
|
1347
|
+
<CompactBadge label={workspace.status} variant="info" />
|
|
1348
|
+
) : undefined
|
|
1349
|
+
}
|
|
1350
|
+
/>
|
|
1351
|
+
<KeyValueGroup columns={1}>
|
|
1352
|
+
<CompactKeyValue label="Git Branch" value={data?.branch || "Unknown"} />
|
|
1353
|
+
<CompactKeyValue label="Kind" value={workspace?.kind || "Unknown"} />
|
|
1354
|
+
</KeyValueGroup>
|
|
1355
|
+
{workspace?.directory ? (
|
|
1356
|
+
<CompactKeyValue label="Worktree Location" value={workspace.directory} copyable mono />
|
|
1357
|
+
) : null}
|
|
1358
|
+
{workspace?.name ? (
|
|
1359
|
+
<CompactKeyValue label="Workspace Name" value={workspace.name} />
|
|
1360
|
+
) : null}
|
|
1361
|
+
{workspace?.title && workspace.title !== workspace.name ? (
|
|
1362
|
+
<CompactKeyValue label="Workspace Title" value={workspace.title} />
|
|
1363
|
+
) : null}
|
|
1364
|
+
{workspace?.projectDisplayName ? (
|
|
1365
|
+
<CompactKeyValue label="Project" value={workspace.projectDisplayName} />
|
|
1366
|
+
) : null}
|
|
1367
|
+
{workspace?.diffStat ? (
|
|
1368
|
+
<CompactKeyValue
|
|
1369
|
+
label="Git Changes"
|
|
1370
|
+
value={`+${workspace.diffStat.additions} -${workspace.diffStat.deletions}`}
|
|
1371
|
+
/>
|
|
1372
|
+
) : null}
|
|
1373
|
+
</Card>
|
|
1374
|
+
|
|
1375
|
+
{/* Agent Information */}
|
|
1376
|
+
<Card variant="elevated">
|
|
1377
|
+
<CompactCardHeader
|
|
1378
|
+
title={agent?.title ? `Agent: ${agent.title}` : "Agent Session"}
|
|
1379
|
+
icon="Bot"
|
|
1380
|
+
value={
|
|
1381
|
+
agent?.status ? (
|
|
1382
|
+
<CompactBadge
|
|
1383
|
+
label={agent.status}
|
|
1384
|
+
variant={agent.status === "running" ? "success" : "info"}
|
|
1385
|
+
/>
|
|
1386
|
+
) : undefined
|
|
1387
|
+
}
|
|
1388
|
+
/>
|
|
1389
|
+
{agent?.title ? (
|
|
1390
|
+
<CompactKeyValue label="Agent Tab" value={agent.title} />
|
|
1391
|
+
) : null}
|
|
1392
|
+
{agentId ? (
|
|
1393
|
+
<CompactKeyValue label="Agent ID" value={agentId} copyable mono />
|
|
1394
|
+
) : null}
|
|
1395
|
+
<KeyValueGroup columns={1}>
|
|
1396
|
+
<CompactKeyValue label="Model" value={agent?.model || "Standard"} />
|
|
1397
|
+
<CompactKeyValue label="Provider" value={agent?.provider || "Default"} />
|
|
1398
|
+
</KeyValueGroup>
|
|
1399
|
+
<KeyValueGroup columns={1}>
|
|
1400
|
+
<CompactKeyValue
|
|
1401
|
+
label="Last Worked"
|
|
1402
|
+
value={
|
|
1403
|
+
agent?.status === "running"
|
|
1404
|
+
? "Active now"
|
|
1405
|
+
: formatTimeAgo(agent?.lastActivityAt)
|
|
1406
|
+
}
|
|
1407
|
+
/>
|
|
1408
|
+
<CompactKeyValue
|
|
1409
|
+
label="Inactivity"
|
|
1410
|
+
value={
|
|
1411
|
+
agent?.status === "running"
|
|
1412
|
+
? "0s (active)"
|
|
1413
|
+
: formatIdleDuration(agent?.lastActivityAt)
|
|
1414
|
+
}
|
|
1415
|
+
/>
|
|
1416
|
+
</KeyValueGroup>
|
|
1417
|
+
{agent?.cwd ? (
|
|
1418
|
+
<CompactKeyValue label="Working Directory" value={agent.cwd} copyable mono />
|
|
1419
|
+
) : null}
|
|
1420
|
+
</Card>
|
|
1421
|
+
|
|
1422
|
+
{/* Token Usage & Context Window */}
|
|
1423
|
+
<Card variant="elevated">
|
|
1424
|
+
<CompactCardHeader
|
|
1425
|
+
title="Tokens & Context Window"
|
|
1426
|
+
icon="Coins"
|
|
1427
|
+
value={
|
|
1428
|
+
contextPercent != null ? (
|
|
1429
|
+
<CompactBadge
|
|
1430
|
+
label={`${contextPercent}% ctx`}
|
|
1431
|
+
variant={contextPercent >= 85 ? "danger" : contextPercent >= 70 ? "warning" : "success"}
|
|
1432
|
+
/>
|
|
1433
|
+
) : tokenMetrics?.totalTokens != null ? (
|
|
1434
|
+
<CompactBadge
|
|
1435
|
+
label={`${formatCompactTokens(tokenMetrics.totalTokens)} tok`}
|
|
1436
|
+
variant="neutral"
|
|
1437
|
+
/>
|
|
1438
|
+
) : undefined
|
|
1439
|
+
}
|
|
1440
|
+
/>
|
|
1441
|
+
|
|
1442
|
+
{tokenMetrics?.contextMaxTokens != null && tokenMetrics.contextMaxTokens > 0 ? (
|
|
1443
|
+
<View style={styles.contextUsageBlock}>
|
|
1444
|
+
<View style={styles.contextUsageRow}>
|
|
1445
|
+
<Text style={[styles.compactKvLabel, { color: colors.foregroundMuted }]}>Context Utilization</Text>
|
|
1446
|
+
<Text style={[styles.compactKvValue, { color: colors.foreground, fontWeight: "600" }]}>
|
|
1447
|
+
{formatCompactTokens(tokenMetrics.contextUsedTokens ?? 0)} / {formatCompactTokens(tokenMetrics.contextMaxTokens)} ({contextPercent}%)
|
|
1448
|
+
</Text>
|
|
1449
|
+
</View>
|
|
1450
|
+
<ProgressBar
|
|
1451
|
+
value={contextPercent ?? 0}
|
|
1452
|
+
thresholds={{ warning: 70, danger: 85 }}
|
|
1453
|
+
height={8}
|
|
1454
|
+
/>
|
|
1455
|
+
</View>
|
|
1456
|
+
) : tokenMetrics?.contextUsedTokens != null ? (
|
|
1457
|
+
<CompactKeyValue
|
|
1458
|
+
label="Context Used"
|
|
1459
|
+
value={`${tokenMetrics.contextUsedTokens.toLocaleString()} (${formatCompactTokens(tokenMetrics.contextUsedTokens)})`}
|
|
1460
|
+
/>
|
|
1461
|
+
) : null}
|
|
1462
|
+
|
|
1463
|
+
<KeyValueGroup columns={1}>
|
|
1464
|
+
<CompactKeyValue
|
|
1465
|
+
label="Input Tokens"
|
|
1466
|
+
value={tokenMetrics?.inputTokens != null ? tokenMetrics.inputTokens.toLocaleString() : "--"}
|
|
1467
|
+
subValue={tokenMetrics?.inputTokens != null ? formatCompactTokens(tokenMetrics.inputTokens) : undefined}
|
|
1468
|
+
/>
|
|
1469
|
+
<CompactKeyValue
|
|
1470
|
+
label="Output Tokens"
|
|
1471
|
+
value={tokenMetrics?.outputTokens != null ? tokenMetrics.outputTokens.toLocaleString() : "--"}
|
|
1472
|
+
subValue={tokenMetrics?.outputTokens != null ? formatCompactTokens(tokenMetrics.outputTokens) : undefined}
|
|
1473
|
+
/>
|
|
1474
|
+
{tokenMetrics?.cachedTokens != null && (
|
|
1475
|
+
<CompactKeyValue
|
|
1476
|
+
label="Cached Tokens"
|
|
1477
|
+
value={tokenMetrics.cachedTokens.toLocaleString()}
|
|
1478
|
+
subValue={formatCompactTokens(tokenMetrics.cachedTokens)}
|
|
1479
|
+
/>
|
|
1480
|
+
)}
|
|
1481
|
+
{tokenMetrics?.costUsd != null && (
|
|
1482
|
+
<CompactKeyValue
|
|
1483
|
+
label="Session / Turn Cost"
|
|
1484
|
+
value={`$${tokenMetrics.costUsd < 0.01 ? tokenMetrics.costUsd.toFixed(4) : tokenMetrics.costUsd.toFixed(2)}`}
|
|
1485
|
+
/>
|
|
1486
|
+
)}
|
|
1487
|
+
</KeyValueGroup>
|
|
1488
|
+
{!tokenMetrics && (
|
|
1489
|
+
<Text style={[styles.mcpEmpty, { color: colors.foregroundMuted, marginTop: 4 }]}>
|
|
1490
|
+
No token usage recorded for this agent session yet
|
|
1491
|
+
</Text>
|
|
1492
|
+
)}
|
|
1493
|
+
</Card>
|
|
1494
|
+
</>
|
|
1495
|
+
)}
|
|
1496
|
+
|
|
1497
|
+
{activeTab === "settings" && (
|
|
1498
|
+
<>
|
|
1499
|
+
{/* Pill Display Mode */}
|
|
1500
|
+
<Card variant="elevated">
|
|
1501
|
+
<CompactCardHeader
|
|
1502
|
+
title="Pill Display Mode"
|
|
1503
|
+
icon="LayoutGrid"
|
|
1504
|
+
subtitle="How active items appear in the composer trackbar"
|
|
1505
|
+
/>
|
|
1506
|
+
<ChoiceChips
|
|
1507
|
+
options={[
|
|
1508
|
+
{ id: "cycle", label: "Cycle", description: "Rotate one at a time" },
|
|
1509
|
+
{ id: "all", label: "All in One", description: "Combined into one pill" },
|
|
1510
|
+
{ id: "multiple", label: "Multiple", description: "Dedicated pills" },
|
|
1511
|
+
]}
|
|
1512
|
+
value={settings.pillMode ?? "cycle"}
|
|
1513
|
+
showActiveDescription
|
|
1514
|
+
onChange={(nextMode) => {
|
|
1515
|
+
triggerHaptic("light");
|
|
1516
|
+
updateSettings({ pillMode: nextMode });
|
|
1517
|
+
notifySettingsChanged({ ...settings, pillMode: nextMode });
|
|
1518
|
+
}}
|
|
1519
|
+
/>
|
|
1520
|
+
<View style={styles.settingsSpacer}>
|
|
1521
|
+
<Toggle
|
|
1522
|
+
label="Show Composer Pill"
|
|
1523
|
+
description="Hide the composer pill entirely; the dashboard stays available from the sidebar"
|
|
1524
|
+
style={styles.toggleFullWidth}
|
|
1525
|
+
value={settings.showComposerPill ?? true}
|
|
1526
|
+
onValueChange={(val) => {
|
|
1527
|
+
const next = { ...settings, showComposerPill: val };
|
|
1528
|
+
updateSettings({ showComposerPill: val });
|
|
1529
|
+
notifySettingsChanged(next);
|
|
1530
|
+
}}
|
|
1531
|
+
/>
|
|
1532
|
+
</View>
|
|
1533
|
+
</Card>
|
|
1534
|
+
|
|
1535
|
+
{/* Active Pill Items Selectors */}
|
|
1536
|
+
<Card variant="elevated">
|
|
1537
|
+
<CompactCardHeader
|
|
1538
|
+
title="Active Pill Items"
|
|
1539
|
+
icon="Sliders"
|
|
1540
|
+
subtitle={
|
|
1541
|
+
!hasAnyPillEnabled
|
|
1542
|
+
? "None selected -- automatically showing CPU & RAM"
|
|
1543
|
+
: (settings.pillMode ?? "cycle") === "multiple"
|
|
1544
|
+
? "Choose which items appear as dedicated pills"
|
|
1545
|
+
: (settings.pillMode ?? "cycle") === "all"
|
|
1546
|
+
? "Choose which items appear together in the pill"
|
|
1547
|
+
: "Choose which items cycle in the composer pill"
|
|
1548
|
+
}
|
|
1549
|
+
/>
|
|
1550
|
+
<View style={styles.settingsToggles}>
|
|
1551
|
+
<MetricSurfaceMatrix
|
|
1552
|
+
settings={settings}
|
|
1553
|
+
updateSettings={updateSettings}
|
|
1554
|
+
notifySettingsChanged={notifySettingsChanged}
|
|
1555
|
+
mcpInstalled={Boolean(data?.mcpInstalled)}
|
|
1556
|
+
mcpRunning={data?.mcpRunning ?? null}
|
|
1557
|
+
customPills={(customPillList?.pills ?? []).map((pill) => ({
|
|
1558
|
+
id: pill.id,
|
|
1559
|
+
title: pill.title,
|
|
1560
|
+
sourceFile: pill.sourceFile,
|
|
1561
|
+
enabled: pill.enabled,
|
|
1562
|
+
}))}
|
|
1563
|
+
customOverrides={settings.customPillEnabled}
|
|
1564
|
+
onCustomToggle={(id, val) => {
|
|
1565
|
+
const next = { ...settings.customPillEnabled, [id]: val };
|
|
1566
|
+
const nextSettings = { ...settings, customPillEnabled: next };
|
|
1567
|
+
updateSettings({ customPillEnabled: next });
|
|
1568
|
+
notifySettingsChanged(nextSettings);
|
|
1569
|
+
}}
|
|
1570
|
+
/>
|
|
1571
|
+
</View>
|
|
1572
|
+
</Card>
|
|
1573
|
+
|
|
1574
|
+
{/* Custom Metric Pills */}
|
|
1575
|
+
<Card variant="elevated">
|
|
1576
|
+
<CompactCardHeader
|
|
1577
|
+
title="Custom Metric Pills"
|
|
1578
|
+
icon="Sliders"
|
|
1579
|
+
subtitle="Standalone pills discovered from ~/.paseo/top/pills"
|
|
1580
|
+
/>
|
|
1581
|
+
<View style={styles.settingsToggles}>
|
|
1582
|
+
<Toggle
|
|
1583
|
+
label="Show Custom Metric Pills"
|
|
1584
|
+
description="Display pills defined in ~/.paseo/top/pills as standalone composer pills"
|
|
1585
|
+
style={styles.toggleFullWidth}
|
|
1586
|
+
labelStyle={styles.compactToggleLabel}
|
|
1587
|
+
value={settings.showCustomPills ?? true}
|
|
1588
|
+
onValueChange={(val) => {
|
|
1589
|
+
const s = { ...settings, showCustomPills: val };
|
|
1590
|
+
updateSettings({ showCustomPills: val });
|
|
1591
|
+
notifySettingsChanged(s);
|
|
1592
|
+
}}
|
|
1593
|
+
/>
|
|
1594
|
+
|
|
1595
|
+
</View>
|
|
1596
|
+
</Card>
|
|
1597
|
+
|
|
1598
|
+
{/* Rotation Speed Setting (Cycle mode only) */}
|
|
1599
|
+
{(settings.pillMode ?? "cycle") === "cycle" && (
|
|
1600
|
+
<Card variant="elevated">
|
|
1601
|
+
<CompactCardHeader
|
|
1602
|
+
title="Rotation Speed"
|
|
1603
|
+
icon="Clock"
|
|
1604
|
+
value={
|
|
1605
|
+
<Text style={[styles.accentValueText, { color: colors.accent }]}>
|
|
1606
|
+
{`${settings.intervalSeconds}s`}
|
|
1607
|
+
</Text>
|
|
1608
|
+
}
|
|
1609
|
+
/>
|
|
1610
|
+
<ChoiceChips
|
|
1611
|
+
options={[2, 3, 4, 6].map((sec) => ({ id: sec, label: `${sec}s` }))}
|
|
1612
|
+
value={settings.intervalSeconds}
|
|
1613
|
+
onChange={(intervalSeconds) => {
|
|
1614
|
+
triggerHaptic("light");
|
|
1615
|
+
updateSettings({ intervalSeconds });
|
|
1616
|
+
}}
|
|
1617
|
+
/>
|
|
1618
|
+
</Card>
|
|
1619
|
+
)}
|
|
1620
|
+
|
|
1621
|
+
{/* Timeline Cadence Setting */}
|
|
1622
|
+
<Card variant="elevated">
|
|
1623
|
+
<CompactCardHeader
|
|
1624
|
+
title="Timeline Cadence"
|
|
1625
|
+
icon="Clock"
|
|
1626
|
+
value={
|
|
1627
|
+
<Text style={[styles.accentValueText, { color: colors.accent }]}>
|
|
1628
|
+
{(settings.timelineCadence ?? 1) === 0
|
|
1629
|
+
? "Never"
|
|
1630
|
+
: (settings.timelineCadence ?? 1) === 1
|
|
1631
|
+
? "Every turn"
|
|
1632
|
+
: `Every ${(settings.timelineCadence ?? 1)} turns`}
|
|
1633
|
+
</Text>
|
|
1634
|
+
}
|
|
1635
|
+
subtitle="How often a card is stamped into the timeline view"
|
|
1636
|
+
/>
|
|
1637
|
+
<ChoiceChips
|
|
1638
|
+
options={TIMELINE_CADENCE_OPTIONS}
|
|
1639
|
+
value={settings.timelineCadence ?? 1}
|
|
1640
|
+
onChange={(timelineCadence) => {
|
|
1641
|
+
triggerHaptic("light");
|
|
1642
|
+
updateSettings({ timelineCadence });
|
|
1643
|
+
notifySettingsChanged({ ...settings, timelineCadence });
|
|
1644
|
+
}}
|
|
1645
|
+
/>
|
|
1646
|
+
<Text style={[styles.modeDesc, styles.modeDescSpaced, { color: colors.foregroundMuted }]}>
|
|
1647
|
+
0 behaves as never; N above 1 stamps every Nth turn
|
|
1648
|
+
</Text>
|
|
1649
|
+
</Card>
|
|
1650
|
+
|
|
1651
|
+
{/* Default Modal Tab Setting */}
|
|
1652
|
+
<Card variant="elevated">
|
|
1653
|
+
<CompactCardHeader
|
|
1654
|
+
title="Default Modal Tab"
|
|
1655
|
+
icon="Sliders"
|
|
1656
|
+
value={
|
|
1657
|
+
<Text style={[styles.accentValueText, { color: colors.accent }]}>
|
|
1658
|
+
{TABS.find((t) => t.id === settings.defaultTab)?.label || "System"}
|
|
1659
|
+
</Text>
|
|
1660
|
+
}
|
|
1661
|
+
/>
|
|
1662
|
+
<ChoiceChips
|
|
1663
|
+
options={TABS.map((tab) => ({ id: tab.id as ModalTab, label: tab.label }))}
|
|
1664
|
+
value={settings.defaultTab ?? "system"}
|
|
1665
|
+
onChange={(defaultTab) => {
|
|
1666
|
+
triggerHaptic("light");
|
|
1667
|
+
updateSettings({ defaultTab });
|
|
1668
|
+
}}
|
|
1669
|
+
/>
|
|
1670
|
+
</Card>
|
|
1671
|
+
|
|
1672
|
+
<Button
|
|
1673
|
+
label="Reset to Defaults"
|
|
1674
|
+
variant="secondary"
|
|
1675
|
+
size="sm"
|
|
1676
|
+
onPress={() => {
|
|
1677
|
+
triggerHaptic("medium");
|
|
1678
|
+
resetSettings();
|
|
1679
|
+
notifySettingsChanged(topSettingsContract.defaultSettings);
|
|
1680
|
+
setSelectedTab(null);
|
|
1681
|
+
}}
|
|
1682
|
+
/>
|
|
1683
|
+
</>
|
|
1684
|
+
)}
|
|
1685
|
+
|
|
1686
|
+
{activeTab === "about" && (
|
|
1687
|
+
<AboutSection
|
|
1688
|
+
name="paseo-top"
|
|
1689
|
+
description="Live host system and workspace monitor for Paseo composer trackbar."
|
|
1690
|
+
version={data?.version ?? PLUGIN_VERSION}
|
|
1691
|
+
author="xpufx"
|
|
1692
|
+
repository="https://github.com/xpufx/paseo-top"
|
|
1693
|
+
issues="https://github.com/xpufx/paseo-top/issues"
|
|
1694
|
+
license="MIT"
|
|
1695
|
+
density="tiny"
|
|
1696
|
+
extraItems={[
|
|
1697
|
+
{
|
|
1698
|
+
label: "Host Platform",
|
|
1699
|
+
value: data?.platform ? `${data?.platform} (${data?.arch ?? "unknown"})` : "Linux",
|
|
1700
|
+
copyable: true,
|
|
1701
|
+
},
|
|
1702
|
+
{ label: "Host Name", value: data?.hostname ?? "localhost", copyable: true },
|
|
1703
|
+
{ label: "CPU Model", value: data?.cpuModel ?? "unknown", copyable: true },
|
|
1704
|
+
{
|
|
1705
|
+
label: "CPU Cores",
|
|
1706
|
+
value: `${data?.cpuCores ?? 0} cores`,
|
|
1707
|
+
},
|
|
1708
|
+
{
|
|
1709
|
+
label: "Total Memory",
|
|
1710
|
+
value: data?.memoryTotalBytes ? formatBytes(data?.memoryTotalBytes) : "unknown",
|
|
1711
|
+
},
|
|
1712
|
+
{
|
|
1713
|
+
label: "Host Uptime",
|
|
1714
|
+
value: data?.uptimeSeconds ? formatUptime(data?.uptimeSeconds) : "unknown",
|
|
1715
|
+
},
|
|
1716
|
+
]}
|
|
1717
|
+
/>
|
|
1718
|
+
)}
|
|
1719
|
+
|
|
1720
|
+
{/* Discrete Version Footer */}
|
|
1721
|
+
{activeTab !== "about" && (
|
|
1722
|
+
<View style={styles.footer}>
|
|
1723
|
+
<Text style={[styles.footerText, { color: colors.foregroundMuted }]}>
|
|
1724
|
+
top v{data?.version ?? PLUGIN_VERSION}
|
|
1725
|
+
</Text>
|
|
1726
|
+
</View>
|
|
1727
|
+
)}
|
|
1728
|
+
</ModalBody>
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
interface LiveCustomPillViewProps {
|
|
1733
|
+
pillId: string;
|
|
1734
|
+
initial: CustomPillState;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function LiveCustomPillView({ pillId, initial }: LiveCustomPillViewProps) {
|
|
1738
|
+
const { data } = useCustomPillsQuery();
|
|
1739
|
+
const liveState = data?.pills.find((p) => p.id === pillId) ?? initial;
|
|
1740
|
+
return <CustomPillBody state={liveState} />;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
interface LiveCustomPillModalProps {
|
|
1744
|
+
pillId: string;
|
|
1745
|
+
initial: CustomPillStateOutput;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function LiveCustomPillModal({ pillId, initial }: LiveCustomPillModalProps) {
|
|
1749
|
+
const { colors } = usePluginTheme();
|
|
1750
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
1751
|
+
const [outputOverride, setOutputOverride] = useState<string | undefined>(undefined);
|
|
1752
|
+
const { data, refetch } = useCustomPillsQuery();
|
|
1753
|
+
const liveState = data?.pills.find((p) => p.id === pillId) ?? initial;
|
|
1754
|
+
const runModalCommand = useRpc(runCustomPillModalCommandRpc);
|
|
1755
|
+
|
|
1756
|
+
const handleRefresh = async () => {
|
|
1757
|
+
setRefreshing(true);
|
|
1758
|
+
try {
|
|
1759
|
+
const res = await runModalCommand({ pillId });
|
|
1760
|
+
if (res?.output) {
|
|
1761
|
+
setOutputOverride(res.output);
|
|
1762
|
+
}
|
|
1763
|
+
void refetch();
|
|
1764
|
+
} finally {
|
|
1765
|
+
setRefreshing(false);
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
// Run the drilldown command once when the modal opens so it never shows stale pill output
|
|
1770
|
+
const didInitialRun = useRef(false);
|
|
1771
|
+
useEffect(() => {
|
|
1772
|
+
if (didInitialRun.current) return;
|
|
1773
|
+
didInitialRun.current = true;
|
|
1774
|
+
void handleRefresh();
|
|
1775
|
+
}, [pillId]);
|
|
1776
|
+
|
|
1777
|
+
const effectiveState: CustomPillStateOutput = {
|
|
1778
|
+
...liveState,
|
|
1779
|
+
modalOutput: outputOverride ?? liveState.modalOutput,
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
return (
|
|
1783
|
+
<View style={styles.modalRoot}>
|
|
1784
|
+
<CustomPillModalContent
|
|
1785
|
+
state={effectiveState}
|
|
1786
|
+
onRefresh={handleRefresh}
|
|
1787
|
+
isRefreshing={refreshing}
|
|
1788
|
+
/>
|
|
1789
|
+
<View style={styles.customPillHint}>
|
|
1790
|
+
<Text style={[styles.footerText, { color: colors.foregroundMuted }]}>
|
|
1791
|
+
Custom metric pill from paseo-top
|
|
1792
|
+
{effectiveState.sourceFile ? ` - defined in ${effectiveState.sourceFile}` : ""}
|
|
1793
|
+
</Text>
|
|
1794
|
+
</View>
|
|
1795
|
+
</View>
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// The host may mount the client contribution more than once without an
|
|
1800
|
+
// intervening cleanup (multi-pane, StrictMode, hot reload). Pill, surface,
|
|
1801
|
+
// and sidebar ids are global per plugin, so two live mounts double-register
|
|
1802
|
+
// every pill and a single tap fires two openers - the modal opens in both
|
|
1803
|
+
// locations at once. Last mount wins: mounting retires the previous mount.
|
|
1804
|
+
let liveContributionCleanup: (() => void) | null = null;
|
|
1805
|
+
|
|
1806
|
+
export function contributeClient(client: ComposerPillRegistrar | PluginClientContext) {
|
|
1807
|
+
if (liveContributionCleanup) {
|
|
1808
|
+
liveContributionCleanup();
|
|
1809
|
+
liveContributionCleanup = null;
|
|
1810
|
+
}
|
|
1811
|
+
let cleanupSidebar: (() => void) | null = null;
|
|
1812
|
+
if ("addSurface" in client && "addSidebarItem" in client) {
|
|
1813
|
+
cleanupSidebar = registerSidebarSurface(
|
|
1814
|
+
client as unknown as SidebarSurfaceRegistrar,
|
|
1815
|
+
{
|
|
1816
|
+
id: "paseo-top-dashboard",
|
|
1817
|
+
title: "Top Dashboard",
|
|
1818
|
+
icon: "Activity",
|
|
1819
|
+
Component: TopDashboardSurface,
|
|
1820
|
+
},
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
const activePills = new Map<string, () => void>();
|
|
1824
|
+
let latestSettings: TopSettings = topSettingsContract.defaultSettings;
|
|
1825
|
+
let mountDisposed = false;
|
|
1826
|
+
|
|
1827
|
+
function syncPills(settings: TopSettings) {
|
|
1828
|
+
if (mountDisposed) return;
|
|
1829
|
+
latestSettings = settings;
|
|
1830
|
+
if (settings.showComposerPill === false) {
|
|
1831
|
+
for (const [, cleanup] of activePills.entries()) {
|
|
1832
|
+
cleanup();
|
|
1833
|
+
}
|
|
1834
|
+
activePills.clear();
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
const flags = legacyFlagView(settings);
|
|
1838
|
+
const mode = settings.pillMode ?? "cycle";
|
|
1839
|
+
|
|
1840
|
+
if (mode === "cycle" || mode === "all") {
|
|
1841
|
+
// Remove all single-item pills
|
|
1842
|
+
for (const [key, cleanup] of activePills.entries()) {
|
|
1843
|
+
if (key !== "paseo-top") {
|
|
1844
|
+
cleanup();
|
|
1845
|
+
activePills.delete(key);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// Ensure main pill is registered
|
|
1850
|
+
if (!activePills.has("paseo-top")) {
|
|
1851
|
+
const cleanup = registerTopPill(client, {
|
|
1852
|
+
id: "paseo-top",
|
|
1853
|
+
title: "top",
|
|
1854
|
+
modalTitle: "Host System Resources",
|
|
1855
|
+
modalIcon: "Activity",
|
|
1856
|
+
icon: "Cpu",
|
|
1857
|
+
resolveDefaultPayload: ({ agentId }) => {
|
|
1858
|
+
if (latestSettings.pillMode === "all") {
|
|
1859
|
+
return latestSettings.defaultTab;
|
|
1860
|
+
}
|
|
1861
|
+
return currentCycleTabByAgent.get(agentId) ?? latestSettings.defaultTab;
|
|
1862
|
+
},
|
|
1863
|
+
resolveLabel: async (ctx) => {
|
|
1864
|
+
const snap = await liveSnapshotFor(ctx);
|
|
1865
|
+
const items = enabledItemsForSettings(latestSettings);
|
|
1866
|
+
if (items.length === 0) return { label: "top", icon: "Activity" };
|
|
1867
|
+
if ((latestSettings.pillMode ?? "cycle") === "all") {
|
|
1868
|
+
return { label: buildAllLabel(items, snap), icon: "Activity" };
|
|
1869
|
+
}
|
|
1870
|
+
const item = nextCycleItem(ctx.agentId, items);
|
|
1871
|
+
if (!item) return { label: "top", icon: "Activity" };
|
|
1872
|
+
currentCycleTabByAgent.set(ctx.agentId, getItemTab(item));
|
|
1873
|
+
return {
|
|
1874
|
+
label: formatSegmentLabel(item, snap),
|
|
1875
|
+
icon: formatSegmentIcon(item, snap),
|
|
1876
|
+
};
|
|
1877
|
+
},
|
|
1878
|
+
refreshIntervalMs: 3000,
|
|
1879
|
+
renderPill: (props) => <PillView {...props} />,
|
|
1880
|
+
renderModal: (props) => <ResourceModal {...props} />,
|
|
1881
|
+
});
|
|
1882
|
+
activePills.set("paseo-top", cleanup);
|
|
1883
|
+
}
|
|
1884
|
+
} else if (mode === "multiple") {
|
|
1885
|
+
// Remove main pill
|
|
1886
|
+
if (activePills.has("paseo-top")) {
|
|
1887
|
+
activePills.get("paseo-top")!();
|
|
1888
|
+
activePills.delete("paseo-top");
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
const hasAny =
|
|
1892
|
+
flags.showCpuRam ||
|
|
1893
|
+
flags.showBranch ||
|
|
1894
|
+
flags.showWorktree ||
|
|
1895
|
+
flags.showAgentTitle ||
|
|
1896
|
+
flags.showAgent ||
|
|
1897
|
+
flags.showAgentProvider ||
|
|
1898
|
+
flags.showAgentActivity ||
|
|
1899
|
+
flags.showAgentId ||
|
|
1900
|
+
flags.showLoad ||
|
|
1901
|
+
flags.showUptime;
|
|
1902
|
+
|
|
1903
|
+
const effectiveCpu = flags.showCpuRam || !hasAny;
|
|
1904
|
+
|
|
1905
|
+
const desiredPills: {
|
|
1906
|
+
id: string;
|
|
1907
|
+
item: PillItemType;
|
|
1908
|
+
title: string;
|
|
1909
|
+
modalTitle: string;
|
|
1910
|
+
defaultTab: "system" | "context";
|
|
1911
|
+
icon: string;
|
|
1912
|
+
}[] = [];
|
|
1913
|
+
|
|
1914
|
+
if (effectiveCpu) {
|
|
1915
|
+
desiredPills.push({
|
|
1916
|
+
id: "paseo-top-cpu",
|
|
1917
|
+
item: "cpu_ram",
|
|
1918
|
+
icon: "Cpu",
|
|
1919
|
+
title: "CPU & RAM",
|
|
1920
|
+
modalTitle: "Host System Resources",
|
|
1921
|
+
defaultTab: "system",
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
if (flags.showBranch) {
|
|
1925
|
+
desiredPills.push({
|
|
1926
|
+
id: "paseo-top-branch",
|
|
1927
|
+
item: "branch",
|
|
1928
|
+
icon: "GitBranch",
|
|
1929
|
+
title: "Git Branch",
|
|
1930
|
+
modalTitle: "Host System Resources",
|
|
1931
|
+
defaultTab: "context",
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
if (flags.showWorktree) {
|
|
1935
|
+
desiredPills.push({
|
|
1936
|
+
id: "paseo-top-worktree",
|
|
1937
|
+
item: "worktree",
|
|
1938
|
+
icon: "FolderGit2",
|
|
1939
|
+
title: "Worktree",
|
|
1940
|
+
modalTitle: "Host System Resources",
|
|
1941
|
+
defaultTab: "context",
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
if (flags.showAgentTitle) {
|
|
1945
|
+
desiredPills.push({
|
|
1946
|
+
id: "paseo-top-agent-title",
|
|
1947
|
+
item: "agent_title",
|
|
1948
|
+
icon: "Bot",
|
|
1949
|
+
title: "Agent Tab",
|
|
1950
|
+
modalTitle: "Host System Resources",
|
|
1951
|
+
defaultTab: "context",
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
if (flags.showAgent) {
|
|
1955
|
+
desiredPills.push({
|
|
1956
|
+
id: "paseo-top-agent",
|
|
1957
|
+
item: "agent",
|
|
1958
|
+
icon: "Bot",
|
|
1959
|
+
title: "Agent Model",
|
|
1960
|
+
modalTitle: "Host System Resources",
|
|
1961
|
+
defaultTab: "context",
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
if (flags.showAgentProvider) {
|
|
1965
|
+
desiredPills.push({
|
|
1966
|
+
id: "paseo-top-agent-provider",
|
|
1967
|
+
item: "agent_provider",
|
|
1968
|
+
icon: "Sparkles",
|
|
1969
|
+
title: "Provider",
|
|
1970
|
+
modalTitle: "Host System Resources",
|
|
1971
|
+
defaultTab: "context",
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
if (flags.showAgentActivity) {
|
|
1975
|
+
desiredPills.push({
|
|
1976
|
+
id: "paseo-top-agent-activity",
|
|
1977
|
+
item: "agent_activity",
|
|
1978
|
+
icon: "Activity",
|
|
1979
|
+
title: "Activity",
|
|
1980
|
+
modalTitle: "Host System Resources",
|
|
1981
|
+
defaultTab: "context",
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
if (flags.showAgentId ?? true) {
|
|
1985
|
+
desiredPills.push({
|
|
1986
|
+
id: "paseo-top-agent-id",
|
|
1987
|
+
item: "agent_id",
|
|
1988
|
+
icon: "Hash",
|
|
1989
|
+
title: "Agent ID",
|
|
1990
|
+
modalTitle: "Host System Resources",
|
|
1991
|
+
defaultTab: "context",
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
if (flags.showLoad) {
|
|
1995
|
+
desiredPills.push({
|
|
1996
|
+
id: "paseo-top-load",
|
|
1997
|
+
item: "load",
|
|
1998
|
+
icon: "Gauge",
|
|
1999
|
+
title: "Load",
|
|
2000
|
+
modalTitle: "Host System Resources",
|
|
2001
|
+
defaultTab: "system",
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
if (flags.showUptime) {
|
|
2005
|
+
desiredPills.push({
|
|
2006
|
+
id: "paseo-top-uptime",
|
|
2007
|
+
item: "uptime",
|
|
2008
|
+
icon: "Clock",
|
|
2009
|
+
title: "Uptime",
|
|
2010
|
+
modalTitle: "Host System Resources",
|
|
2011
|
+
defaultTab: "system",
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
if (flags.showMcp) {
|
|
2015
|
+
desiredPills.push({
|
|
2016
|
+
id: "paseo-top-mcp",
|
|
2017
|
+
item: "mcp",
|
|
2018
|
+
icon: "Server",
|
|
2019
|
+
title: "MCP Health",
|
|
2020
|
+
modalTitle: "Host System Resources",
|
|
2021
|
+
defaultTab: "system",
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.changes)) {
|
|
2025
|
+
desiredPills.push({
|
|
2026
|
+
id: "paseo-top-changes",
|
|
2027
|
+
item: "changes",
|
|
2028
|
+
icon: "GitCompare",
|
|
2029
|
+
title: "Git Changes",
|
|
2030
|
+
modalTitle: "Host System Resources",
|
|
2031
|
+
defaultTab: "system",
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.tokens)) {
|
|
2035
|
+
desiredPills.push({
|
|
2036
|
+
id: "paseo-top-tokens",
|
|
2037
|
+
item: "tokens",
|
|
2038
|
+
icon: "Coins",
|
|
2039
|
+
title: "Token Usage",
|
|
2040
|
+
modalTitle: "Host System Resources",
|
|
2041
|
+
defaultTab: "context",
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.tools)) {
|
|
2045
|
+
desiredPills.push({
|
|
2046
|
+
id: "paseo-top-tools",
|
|
2047
|
+
item: "tools",
|
|
2048
|
+
icon: "Sigma",
|
|
2049
|
+
title: "Tool Calls",
|
|
2050
|
+
modalTitle: "Host System Resources",
|
|
2051
|
+
defaultTab: "system",
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.turns)) {
|
|
2055
|
+
desiredPills.push({
|
|
2056
|
+
id: "paseo-top-turns",
|
|
2057
|
+
item: "turns",
|
|
2058
|
+
icon: "RotateCw",
|
|
2059
|
+
title: "Turn Count",
|
|
2060
|
+
modalTitle: "Host System Resources",
|
|
2061
|
+
defaultTab: "system",
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
const desiredIds = new Set(desiredPills.map((p) => p.id));
|
|
2066
|
+
|
|
2067
|
+
// Remove pills no longer desired
|
|
2068
|
+
for (const [key, cleanup] of activePills.entries()) {
|
|
2069
|
+
if (!desiredIds.has(key)) {
|
|
2070
|
+
cleanup();
|
|
2071
|
+
activePills.delete(key);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// Register newly desired pills
|
|
2076
|
+
for (const pillDef of desiredPills) {
|
|
2077
|
+
if (!activePills.has(pillDef.id)) {
|
|
2078
|
+
const cleanup = registerTopPill(client, {
|
|
2079
|
+
id: pillDef.id,
|
|
2080
|
+
title: pillDef.title,
|
|
2081
|
+
modalTitle: pillDef.modalTitle,
|
|
2082
|
+
icon: pillDef.icon,
|
|
2083
|
+
modalIcon: pillDef.icon,
|
|
2084
|
+
resolveDefaultPayload: () => pillDef.defaultTab,
|
|
2085
|
+
resolveLabel: singleItemLabelResolver(pillDef.item),
|
|
2086
|
+
refreshIntervalMs: 5000,
|
|
2087
|
+
renderPill: (props) => (
|
|
2088
|
+
<SingleItemPillView
|
|
2089
|
+
item={pillDef.item}
|
|
2090
|
+
defaultTab={pillDef.defaultTab}
|
|
2091
|
+
{...props}
|
|
2092
|
+
/>
|
|
2093
|
+
),
|
|
2094
|
+
renderModal: (props) => (
|
|
2095
|
+
<ResourceModal initialTab={pillDef.defaultTab} {...props} />
|
|
2096
|
+
),
|
|
2097
|
+
});
|
|
2098
|
+
activePills.set(pillDef.id, cleanup);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
// Register settings listener
|
|
2105
|
+
settingsListeners.add(syncPills);
|
|
2106
|
+
|
|
2107
|
+
// Initial sync synchronously
|
|
2108
|
+
syncPills(topSettingsContract.defaultSettings);
|
|
2109
|
+
|
|
2110
|
+
// Query settings from daemon if client supports rpc
|
|
2111
|
+
const clientWithRpc = client as {
|
|
2112
|
+
rpc?: (contract: any, input: any) => Promise<any>;
|
|
2113
|
+
};
|
|
2114
|
+
if (typeof clientWithRpc.rpc === "function") {
|
|
2115
|
+
const rawRpc = clientWithRpc.rpc.bind(clientWithRpc);
|
|
2116
|
+
rpcInvoker = (contract, input) => rawRpc(contract, input);
|
|
2117
|
+
}
|
|
2118
|
+
if (typeof clientWithRpc.rpc === "function") {
|
|
2119
|
+
void clientWithRpc
|
|
2120
|
+
.rpc(topSettingsContract.get, {})
|
|
2121
|
+
.then((fetchedSettings: any) => {
|
|
2122
|
+
if (mountDisposed) return;
|
|
2123
|
+
if (fetchedSettings) {
|
|
2124
|
+
syncPills(fetchedSettings as TopSettings);
|
|
2125
|
+
}
|
|
2126
|
+
})
|
|
2127
|
+
.catch(() => {
|
|
2128
|
+
// Ignore initial get errors
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// Dynamic discovery and lifecycle management for user custom metric pills
|
|
2133
|
+
const activeCustomPills = new Map<string, () => void>();
|
|
2134
|
+
|
|
2135
|
+
async function syncCustomPills() {
|
|
2136
|
+
if (mountDisposed) return;
|
|
2137
|
+
if (latestSettings.showComposerPill === false) {
|
|
2138
|
+
for (const [id, cleanup] of activeCustomPills.entries()) {
|
|
2139
|
+
cleanup();
|
|
2140
|
+
activeCustomPills.delete(id);
|
|
2141
|
+
}
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
const showCustom = latestSettings.showCustomPills ?? true;
|
|
2145
|
+
if (!showCustom) {
|
|
2146
|
+
// Master toggle off: unregister all custom pills
|
|
2147
|
+
for (const [id, cleanup] of activeCustomPills.entries()) {
|
|
2148
|
+
cleanup();
|
|
2149
|
+
activeCustomPills.delete(id);
|
|
2150
|
+
}
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
try {
|
|
2155
|
+
if (typeof clientWithRpc.rpc !== "function") return;
|
|
2156
|
+
const pills = await customPillSnapshot();
|
|
2157
|
+
if (mountDisposed) return;
|
|
2158
|
+
const pillIds = new Set(pills.map((p: CustomPillStateOutput) => p.id));
|
|
2159
|
+
|
|
2160
|
+
// Remove pills that are no longer configured
|
|
2161
|
+
for (const [id, cleanup] of activeCustomPills.entries()) {
|
|
2162
|
+
if (!pillIds.has(id)) {
|
|
2163
|
+
cleanup();
|
|
2164
|
+
activeCustomPills.delete(id);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// Register newly discovered custom metric pills
|
|
2169
|
+
for (const pill of pills) {
|
|
2170
|
+
if (!activeCustomPills.has(pill.id)) {
|
|
2171
|
+
const cleanup = registerTopPill(client, {
|
|
2172
|
+
id: `top-custom-${pill.id}`,
|
|
2173
|
+
title: pill.title,
|
|
2174
|
+
compactTitle: pill.compactTitle,
|
|
2175
|
+
icon: pill.icon,
|
|
2176
|
+
compactIcon: pill.compactIcon,
|
|
2177
|
+
modalTitle: pill.modalTitle ?? pill.title,
|
|
2178
|
+
resolveLabel: async () => {
|
|
2179
|
+
const states = await customPillSnapshot();
|
|
2180
|
+
return (
|
|
2181
|
+
states.find((p: CustomPillStateOutput) => p.id === pill.id)?.displayValue ??
|
|
2182
|
+
pill.displayValue
|
|
2183
|
+
);
|
|
2184
|
+
},
|
|
2185
|
+
refreshIntervalMs: 5000,
|
|
2186
|
+
renderPill: () => <LiveCustomPillView pillId={pill.id} initial={pill} />,
|
|
2187
|
+
renderModal: () => (
|
|
2188
|
+
<LiveCustomPillModal pillId={pill.id} initial={pill} />
|
|
2189
|
+
),
|
|
2190
|
+
});
|
|
2191
|
+
activeCustomPills.set(pill.id, cleanup);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
} catch {
|
|
2195
|
+
// Ignore initial get errors
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
void syncCustomPills();
|
|
2200
|
+
// Discovery only: labels refresh through `customPillSnapshot` on the shared
|
|
2201
|
+
// pill timer, so this host-wide timer can be slow without stale values. It is
|
|
2202
|
+
// one timer per plugin contribution, independent of agent count.
|
|
2203
|
+
const customPillInterval = setInterval(syncCustomPills, 15000);
|
|
2204
|
+
const onSettingsChanged = () => {
|
|
2205
|
+
void syncCustomPills();
|
|
2206
|
+
};
|
|
2207
|
+
settingsListeners.add(onSettingsChanged);
|
|
2208
|
+
|
|
2209
|
+
let disposed = false;
|
|
2210
|
+
const cleanup = () => {
|
|
2211
|
+
if (disposed) return;
|
|
2212
|
+
disposed = true;
|
|
2213
|
+
mountDisposed = true;
|
|
2214
|
+
if (cleanupSidebar) {
|
|
2215
|
+
cleanupSidebar();
|
|
2216
|
+
cleanupSidebar = null;
|
|
2217
|
+
}
|
|
2218
|
+
clearInterval(customPillInterval);
|
|
2219
|
+
settingsListeners.delete(syncPills);
|
|
2220
|
+
settingsListeners.delete(onSettingsChanged);
|
|
2221
|
+
for (const cleanup of activePills.values()) {
|
|
2222
|
+
cleanup();
|
|
2223
|
+
}
|
|
2224
|
+
activePills.clear();
|
|
2225
|
+
for (const cleanup of activeCustomPills.values()) {
|
|
2226
|
+
cleanup();
|
|
2227
|
+
}
|
|
2228
|
+
activeCustomPills.clear();
|
|
2229
|
+
};
|
|
2230
|
+
liveContributionCleanup = cleanup;
|
|
2231
|
+
return () => {
|
|
2232
|
+
if (liveContributionCleanup === cleanup) liveContributionCleanup = null;
|
|
2233
|
+
cleanup();
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
// Residual composition styles only (no RN StyleSheet): helper Row/Stack own
|
|
2238
|
+
// the flex layout, these are the tiny per-element text/spacing tweaks no helper
|
|
2239
|
+
// primitive covers.
|
|
2240
|
+
const styles = {
|
|
2241
|
+
modalRoot: {
|
|
2242
|
+
flex: 1,
|
|
2243
|
+
minHeight: 0,
|
|
2244
|
+
width: "100%",
|
|
2245
|
+
},
|
|
2246
|
+
pillContainer: {
|
|
2247
|
+
flexDirection: "row",
|
|
2248
|
+
alignItems: "center",
|
|
2249
|
+
gap: 4,
|
|
2250
|
+
overflow: "hidden",
|
|
2251
|
+
flexShrink: 1,
|
|
2252
|
+
minWidth: 0,
|
|
2253
|
+
},
|
|
2254
|
+
pillText: {
|
|
2255
|
+
fontSize: 11,
|
|
2256
|
+
flexShrink: 1,
|
|
2257
|
+
},
|
|
2258
|
+
pillTextActive: {
|
|
2259
|
+
opacity: 0.85,
|
|
2260
|
+
},
|
|
2261
|
+
metricHighlight: {
|
|
2262
|
+
fontSize: 12,
|
|
2263
|
+
fontWeight: "700",
|
|
2264
|
+
},
|
|
2265
|
+
compactKvLabel: {
|
|
2266
|
+
fontSize: 9,
|
|
2267
|
+
},
|
|
2268
|
+
compactKvValue: {
|
|
2269
|
+
fontSize: 10,
|
|
2270
|
+
},
|
|
2271
|
+
compactCardTitle: {
|
|
2272
|
+
fontSize: 11,
|
|
2273
|
+
},
|
|
2274
|
+
compactBadgeText: {
|
|
2275
|
+
fontSize: 9,
|
|
2276
|
+
},
|
|
2277
|
+
compactToggleLabel: {
|
|
2278
|
+
fontSize: 11,
|
|
2279
|
+
},
|
|
2280
|
+
footer: {
|
|
2281
|
+
alignItems: "center",
|
|
2282
|
+
paddingTop: 8,
|
|
2283
|
+
paddingBottom: 4,
|
|
2284
|
+
},
|
|
2285
|
+
customPillHint: {
|
|
2286
|
+
alignItems: "center",
|
|
2287
|
+
paddingTop: 8,
|
|
2288
|
+
paddingBottom: 12,
|
|
2289
|
+
},
|
|
2290
|
+
footerText: {
|
|
2291
|
+
fontSize: 8,
|
|
2292
|
+
opacity: 0.65,
|
|
2293
|
+
fontFamily: "monospace",
|
|
2294
|
+
},
|
|
2295
|
+
errorBox: {
|
|
2296
|
+
padding: 24,
|
|
2297
|
+
alignItems: "center",
|
|
2298
|
+
justifyContent: "center",
|
|
2299
|
+
gap: 8,
|
|
2300
|
+
},
|
|
2301
|
+
errorText: {
|
|
2302
|
+
fontSize: 11,
|
|
2303
|
+
fontWeight: "500",
|
|
2304
|
+
},
|
|
2305
|
+
gaugeContainer: {
|
|
2306
|
+
flexDirection: "row",
|
|
2307
|
+
flexWrap: "wrap",
|
|
2308
|
+
justifyContent: "space-around",
|
|
2309
|
+
alignItems: "center",
|
|
2310
|
+
paddingVertical: 12,
|
|
2311
|
+
width: "100%",
|
|
2312
|
+
flexShrink: 1,
|
|
2313
|
+
minWidth: 0,
|
|
2314
|
+
},
|
|
2315
|
+
settingsToggles: {
|
|
2316
|
+
gap: 8,
|
|
2317
|
+
width: "100%",
|
|
2318
|
+
},
|
|
2319
|
+
toggleFullWidth: {
|
|
2320
|
+
width: "100%",
|
|
2321
|
+
alignSelf: "stretch",
|
|
2322
|
+
},
|
|
2323
|
+
settingsSpacer: {
|
|
2324
|
+
marginTop: 12,
|
|
2325
|
+
},
|
|
2326
|
+
contextUsageBlock: {
|
|
2327
|
+
marginVertical: 4,
|
|
2328
|
+
},
|
|
2329
|
+
contextUsageRow: {
|
|
2330
|
+
flexDirection: "row",
|
|
2331
|
+
justifyContent: "space-between",
|
|
2332
|
+
marginBottom: 4,
|
|
2333
|
+
},
|
|
2334
|
+
accentValueText: {
|
|
2335
|
+
fontWeight: "600",
|
|
2336
|
+
},
|
|
2337
|
+
metricMatrix: {
|
|
2338
|
+
gap: 0,
|
|
2339
|
+
width: "100%",
|
|
2340
|
+
},
|
|
2341
|
+
metricMatrixCompact: {
|
|
2342
|
+
minWidth: 0,
|
|
2343
|
+
},
|
|
2344
|
+
metricMatrixHeader: {
|
|
2345
|
+
flexDirection: "row",
|
|
2346
|
+
alignItems: "center",
|
|
2347
|
+
justifyContent: "space-between",
|
|
2348
|
+
paddingLeft: 4,
|
|
2349
|
+
paddingRight: 4,
|
|
2350
|
+
paddingBottom: 2,
|
|
2351
|
+
},
|
|
2352
|
+
metricMatrixHeaderCompact: {
|
|
2353
|
+
paddingLeft: 2,
|
|
2354
|
+
paddingRight: 2,
|
|
2355
|
+
},
|
|
2356
|
+
metricMatrixHeaderText: {
|
|
2357
|
+
fontSize: 9,
|
|
2358
|
+
fontWeight: "700",
|
|
2359
|
+
textTransform: "uppercase",
|
|
2360
|
+
letterSpacing: 0.5,
|
|
2361
|
+
},
|
|
2362
|
+
metricMatrixTargetHeader: {
|
|
2363
|
+
flexDirection: "row",
|
|
2364
|
+
alignItems: "center",
|
|
2365
|
+
gap: 28,
|
|
2366
|
+
paddingRight: 10,
|
|
2367
|
+
},
|
|
2368
|
+
metricMatrixTargetHeaderCompact: {
|
|
2369
|
+
gap: 16,
|
|
2370
|
+
paddingRight: 8,
|
|
2371
|
+
},
|
|
2372
|
+
metricMatrixRow: {
|
|
2373
|
+
flexDirection: "row",
|
|
2374
|
+
alignItems: "center",
|
|
2375
|
+
justifyContent: "space-between",
|
|
2376
|
+
paddingLeft: 4,
|
|
2377
|
+
paddingRight: 4,
|
|
2378
|
+
},
|
|
2379
|
+
metricMatrixRowCompact: {
|
|
2380
|
+
paddingLeft: 2,
|
|
2381
|
+
paddingRight: 2,
|
|
2382
|
+
},
|
|
2383
|
+
metricMatrixLabel: {
|
|
2384
|
+
flex: 1,
|
|
2385
|
+
minWidth: 0,
|
|
2386
|
+
paddingVertical: 2,
|
|
2387
|
+
},
|
|
2388
|
+
metricMatrixTitle: {
|
|
2389
|
+
fontSize: 11,
|
|
2390
|
+
fontWeight: "600",
|
|
2391
|
+
},
|
|
2392
|
+
metricMatrixNote: {
|
|
2393
|
+
fontSize: 9,
|
|
2394
|
+
lineHeight: 12,
|
|
2395
|
+
marginTop: 1,
|
|
2396
|
+
},
|
|
2397
|
+
metricMatrixTargets: {
|
|
2398
|
+
flexDirection: "row",
|
|
2399
|
+
alignItems: "center",
|
|
2400
|
+
gap: 16,
|
|
2401
|
+
paddingLeft: 8,
|
|
2402
|
+
},
|
|
2403
|
+
metricMatrixTargetsCompact: {
|
|
2404
|
+
gap: 8,
|
|
2405
|
+
paddingLeft: 4,
|
|
2406
|
+
},
|
|
2407
|
+
matrixToggle: {
|
|
2408
|
+
width: 38,
|
|
2409
|
+
minHeight: 44,
|
|
2410
|
+
},
|
|
2411
|
+
allInOneContainer: {
|
|
2412
|
+
flexDirection: "row",
|
|
2413
|
+
alignItems: "center",
|
|
2414
|
+
gap: 6,
|
|
2415
|
+
overflow: "hidden",
|
|
2416
|
+
flexShrink: 1,
|
|
2417
|
+
minWidth: 0,
|
|
2418
|
+
},
|
|
2419
|
+
dividerText: {
|
|
2420
|
+
fontSize: 10,
|
|
2421
|
+
opacity: 0.6,
|
|
2422
|
+
},
|
|
2423
|
+
modeDesc: {
|
|
2424
|
+
fontSize: 8,
|
|
2425
|
+
lineHeight: 10,
|
|
2426
|
+
},
|
|
2427
|
+
modeDescSpaced: {
|
|
2428
|
+
marginTop: 8,
|
|
2429
|
+
},
|
|
2430
|
+
mcpList: {
|
|
2431
|
+
gap: 8,
|
|
2432
|
+
marginTop: 4,
|
|
2433
|
+
},
|
|
2434
|
+
mcpRow: {
|
|
2435
|
+
flexDirection: "row",
|
|
2436
|
+
alignItems: "center",
|
|
2437
|
+
justifyContent: "space-between",
|
|
2438
|
+
paddingVertical: 6,
|
|
2439
|
+
paddingHorizontal: 8,
|
|
2440
|
+
borderRadius: 6,
|
|
2441
|
+
},
|
|
2442
|
+
mcpInfo: {
|
|
2443
|
+
flexDirection: "row",
|
|
2444
|
+
alignItems: "center",
|
|
2445
|
+
gap: 8,
|
|
2446
|
+
flex: 1,
|
|
2447
|
+
},
|
|
2448
|
+
mcpName: {
|
|
2449
|
+
fontSize: 11,
|
|
2450
|
+
fontWeight: "500",
|
|
2451
|
+
},
|
|
2452
|
+
mcpLatency: {
|
|
2453
|
+
fontSize: 9,
|
|
2454
|
+
fontFamily: "monospace",
|
|
2455
|
+
},
|
|
2456
|
+
mcpEmpty: {
|
|
2457
|
+
fontSize: 10,
|
|
2458
|
+
fontStyle: "italic",
|
|
2459
|
+
paddingVertical: 4,
|
|
2460
|
+
},
|
|
2461
|
+
} as const;
|