@xpufx/paseo-top 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/client/pill-labels.ts +480 -0
  4. package/client/pill.tsx +2461 -0
  5. package/client/resources-query.ts +68 -0
  6. package/client/settings-ui.tsx +50 -0
  7. package/client/surface.tsx +364 -0
  8. package/client/telemetry-copy.ts +105 -0
  9. package/client/telemetry.tsx +486 -0
  10. package/client/turn-counter.ts +151 -0
  11. package/client/turn-panel.tsx +182 -0
  12. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  13. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  14. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  15. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  16. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  17. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  18. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  19. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  20. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  21. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  22. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  23. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  24. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  25. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  26. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  27. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  28. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  29. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  30. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  31. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  32. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  33. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  34. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  35. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  36. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  37. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  38. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  39. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  40. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  41. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  42. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  43. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  44. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  45. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  46. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  47. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  48. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  49. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  50. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  51. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  52. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  53. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  54. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  55. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  56. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  57. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  58. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  59. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  60. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  61. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  62. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  63. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  64. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  65. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  66. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  67. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  68. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  69. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  70. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  71. package/examples/pills/battery.jsonc +23 -0
  72. package/examples/pills/disk-usage.jsonc +18 -0
  73. package/examples/pills/docker-containers.jsonc +22 -0
  74. package/examples/pills/git-dirty.jsonc +22 -0
  75. package/examples/pills/gpu-nvidia.jsonc +22 -0
  76. package/package.json +37 -0
  77. package/paseo-plugin.json +4 -0
  78. package/server/resources.ts +784 -0
  79. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  80. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  81. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  82. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  83. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  84. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  85. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  86. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  87. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  88. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  89. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  90. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  91. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  92. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  93. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  94. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  95. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  96. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  97. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  98. package/shared/resources.ts +686 -0
  99. package/shared/vendor/paseo-plugin-helper/README.md +11 -0
  100. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  101. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  102. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  103. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  104. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  105. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  106. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  107. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  108. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  109. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  110. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
  111. package/shared/version.ts +2 -0
@@ -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;