@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,68 @@
1
+ import {
2
+ useAutoRefreshQuery,
3
+ useRpcQuery,
4
+ sharedSnapshotKey,
5
+ normalizeSnapshotScope,
6
+ type UseAutoRefreshQueryOptions,
7
+ } from "paseo-plugin-helper/client";
8
+ import {
9
+ getSystemResourcesRpc,
10
+ getCustomPillsRpc,
11
+ type SystemResources,
12
+ } from "../shared/resources";
13
+
14
+ interface TopResourceQueryInput {
15
+ directory?: string;
16
+ }
17
+
18
+ const EMPTY_PARAMS = {};
19
+
20
+ export { normalizeSnapshotScope };
21
+
22
+ /**
23
+ * The single workspace-scoped resource snapshot path for Top.
24
+ *
25
+ * Pill, modal, and surface consumers share one React Query cache identity
26
+ * per workspace (host-wide when no directory is known). The key carries no
27
+ * selective `fields`: field-specific keys fragmented the cache and gave
28
+ * every visible pill its own poller even though all views consume one host
29
+ * snapshot. Selective field collection stays available on the server RPC
30
+ * for genuinely cheaper paths, and the expensive MCP/plugin detection there
31
+ * remains behind its long-lived cache/in-flight guard.
32
+ *
33
+ * Custom-pill configuration/state polling is a different data source and
34
+ * keeps its own query (`top.custom-pills.get` / `top.custom-pills.list`).
35
+ *
36
+ * Polling is lifecycle-aware by mount: the query only runs while a consumer
37
+ * is mounted, and pass `isOpen: false` to pause a mounted-but-hidden view.
38
+ */
39
+ export function useTopResourceQuery(
40
+ directory?: string | null,
41
+ options?: Omit<
42
+ UseAutoRefreshQueryOptions<SystemResources>,
43
+ "defaultRate" | "isOpen"
44
+ > & {
45
+ defaultRate?: UseAutoRefreshQueryOptions<SystemResources>["defaultRate"];
46
+ isOpen?: boolean;
47
+ },
48
+ ) {
49
+ const [, keyInput] = sharedSnapshotKey(
50
+ getSystemResourcesRpc.name,
51
+ directory,
52
+ );
53
+ const input: TopResourceQueryInput = keyInput;
54
+ return useAutoRefreshQuery(getSystemResourcesRpc, input, {
55
+ defaultRate: "5s",
56
+ isOpen: true,
57
+ ...options,
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Custom-pill state snapshot shared by the live pill label and its
63
+ * drilldown modal. Both poll the same `top.custom-pills.get` payload on the
64
+ * same cadence, so they read one query instead of registering two pollers.
65
+ */
66
+ export function useCustomPillsQuery() {
67
+ return useRpcQuery(getCustomPillsRpc, EMPTY_PARAMS, { refetchInterval: 3000 });
68
+ }
@@ -0,0 +1,50 @@
1
+ import React from "react";
2
+ import { Text } from "react-native";
3
+ import { Button, Row, Stack, usePluginTheme } from "paseo-plugin-helper/client";
4
+
5
+ export interface ChipOption<T extends string | number> {
6
+ id: T;
7
+ label: string;
8
+ description?: string;
9
+ }
10
+
11
+ export interface ChoiceChipsProps<T extends string | number> {
12
+ options: ChipOption<T>[];
13
+ value: T;
14
+ onChange: (value: T) => void;
15
+ /** Show the selected option's description under the chips. */
16
+ showActiveDescription?: boolean;
17
+ }
18
+
19
+ /**
20
+ * Single-select chip row shared by every Top settings surface. Built from the
21
+ * helper `Button`/`Row` primitives so the modal and the sidebar dashboard
22
+ * cannot drift into two hand-rolled chip systems.
23
+ */
24
+ export function ChoiceChips<T extends string | number>({
25
+ options,
26
+ value,
27
+ onChange,
28
+ showActiveDescription = false,
29
+ }: ChoiceChipsProps<T>) {
30
+ const { colors } = usePluginTheme();
31
+ const selected = options.find((option) => option.id === value);
32
+ return (
33
+ <Stack gap={6}>
34
+ <Row wrap gap={6}>
35
+ {options.map((option) => (
36
+ <Button
37
+ key={String(option.id)}
38
+ label={option.label}
39
+ size="sm"
40
+ variant={option.id === value ? "primary" : "ghost"}
41
+ onPress={() => onChange(option.id)}
42
+ />
43
+ ))}
44
+ </Row>
45
+ {showActiveDescription && selected?.description ? (
46
+ <Text style={{ fontSize: 9, color: colors.foregroundMuted }}>{selected.description}</Text>
47
+ ) : null}
48
+ </Stack>
49
+ );
50
+ }
@@ -0,0 +1,364 @@
1
+ import React, { useState } from "react";
2
+ import { Text, View } from "react-native";
3
+ import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
4
+ import {
5
+ AboutSection,
6
+ Button,
7
+ Card,
8
+ KeyValue,
9
+ KeyValueGroup,
10
+ MetricGauge,
11
+ ModalBody,
12
+ Row,
13
+ Stack,
14
+ Tabs,
15
+ Toggle,
16
+ CardHeader,
17
+ usePluginSettings,
18
+ usePluginTheme,
19
+ } from "paseo-plugin-helper/client";
20
+ import { formatBytes, formatUptime } from "paseo-plugin-helper/shared";
21
+ import {
22
+ topSettingsContract,
23
+ checkboxesFromTarget,
24
+ targetFromCheckboxes,
25
+ METRIC_DEFINITIONS,
26
+ type PillMode,
27
+ type TopSettings,
28
+ } from "../shared/resources";
29
+ import { PLUGIN_VERSION } from "../shared/version";
30
+ import { notifySettingsChanged } from "./pill";
31
+ import { ChoiceChips } from "./settings-ui";
32
+ import { useTopResourceQuery } from "./resources-query";
33
+
34
+ type SurfaceTab = "system" | "settings" | "about";
35
+
36
+ const TABS = [
37
+ { id: "system", label: "Activity", shortLabel: "Activity", icon: "Activity" },
38
+ { id: "settings", label: "Settings", shortLabel: "Settings", icon: "Sliders" },
39
+ { id: "about", label: "About", shortLabel: "About", icon: "Info" },
40
+ ];
41
+
42
+ const TIMELINE_CADENCE_OPTIONS = Array.from({ length: 11 }, (_, n) => ({
43
+ id: n,
44
+ label: n === 0 ? "Never" : n === 1 ? "Every turn" : `${n}`,
45
+ }));
46
+
47
+ const CPU_THRESHOLDS = { warning: 60, danger: 85 };
48
+ const MEM_THRESHOLDS = { warning: 70, danger: 85 };
49
+
50
+ // Keep the dashboard a readable centered column instead of stretching
51
+ // edge-to-edge on large viewports. The cap lives in the helper
52
+ // (`ModalBody maxContentWidth`); this is the only place the surface picks
53
+ // the value.
54
+ const TOP_CONTENT_MAX_WIDTH = 600;
55
+
56
+ export function TopDashboardSurface(_props: PluginSurfaceProps) {
57
+ const { colors } = usePluginTheme();
58
+ const [activeTab, setActiveTab] = useState<SurfaceTab>("system");
59
+ // No background settings poll: mount/focus verification plus mutation
60
+ // invalidation keep the dashboard in sync without daemon churn.
61
+ const { settings, updateSettings, resetSettings } = usePluginSettings(
62
+ topSettingsContract,
63
+ );
64
+ // The dashboard surface carries no workspace scope, so it reads the
65
+ // host-wide entry of the same shared snapshot path the pill and modal use
66
+ // per workspace (see useTopResourceQuery).
67
+ const { data, isLoading } = useTopResourceQuery();
68
+
69
+ const pillHidden = settings.showComposerPill === false;
70
+
71
+ const applySettings = (updates: Partial<TopSettings>) => {
72
+ const changed = Object.entries(updates).some(
73
+ ([key, value]) =>
74
+ !Object.is((settings as Record<string, unknown>)[key], value),
75
+ );
76
+ if (!changed) return;
77
+ const next = { ...settings, ...updates };
78
+ updateSettings(updates);
79
+ notifySettingsChanged(next);
80
+ };
81
+
82
+ return (
83
+ <View style={[styles.root, { backgroundColor: colors.surface0 }]}>
84
+ <ModalBody
85
+ headerMode="pinned"
86
+ maxContentWidth={TOP_CONTENT_MAX_WIDTH}
87
+ header={
88
+ <Tabs
89
+ tabs={TABS}
90
+ activeTab={activeTab}
91
+ onTabChange={(id) => setActiveTab(id as SurfaceTab)}
92
+ />
93
+ }
94
+ headerStyle={{
95
+ backgroundColor: colors.surface0,
96
+ paddingHorizontal: 12,
97
+ paddingTop: 12,
98
+ paddingBottom: 6,
99
+ }}
100
+ contentContainerStyle={{
101
+ gap: 12,
102
+ paddingHorizontal: 12,
103
+ paddingBottom: 12,
104
+ paddingTop: 6,
105
+ }}
106
+ refreshing={isLoading}
107
+ >
108
+ {activeTab === "system" && (
109
+ <Stack gap={12}>
110
+ <Text style={{ fontSize: 16, fontWeight: "700", color: colors.foreground }}>
111
+ Host System Resources
112
+ </Text>
113
+
114
+ {pillHidden && (
115
+ <Card variant="elevated">
116
+ <Text style={{ fontSize: 12, fontWeight: "600", color: colors.foregroundMuted }}>
117
+ Composer pill is hidden in trackbar. You can re-enable it from the Settings tab above.
118
+ </Text>
119
+ </Card>
120
+ )}
121
+
122
+ <Card variant="elevated">
123
+ <Row justify="space-around" align="center" style={styles.gaugeContainer}>
124
+ <MetricGauge
125
+ value={data?.cpuUsagePercent ?? 0}
126
+ thresholds={CPU_THRESHOLDS}
127
+ label="CPU Load"
128
+ size={72}
129
+ />
130
+ <MetricGauge
131
+ value={data?.memoryUsedPercent ?? 0}
132
+ thresholds={MEM_THRESHOLDS}
133
+ label="RAM Used"
134
+ size={72}
135
+ />
136
+ </Row>
137
+ </Card>
138
+
139
+ <Card variant="elevated">
140
+ <KeyValueGroup>
141
+ <KeyValue
142
+ label="CPU"
143
+ value={data?.cpuUsagePercent !== undefined ? `${data.cpuUsagePercent}%` : "--"}
144
+ />
145
+ <KeyValue
146
+ label="Memory"
147
+ value={
148
+ data?.memoryUsedBytes !== undefined
149
+ ? `${formatBytes(data.memoryUsedBytes, { compact: true, decimals: 1 })} (${data.memoryUsedPercent ?? "--"}%)`
150
+ : "--"
151
+ }
152
+ />
153
+ <KeyValue
154
+ label="Load"
155
+ value={data?.loadAvg?.[0] !== undefined ? data.loadAvg[0].toFixed(2) : "--"}
156
+ />
157
+ <KeyValue
158
+ label="Uptime"
159
+ value={data?.uptimeSeconds ? formatUptime(data.uptimeSeconds) : "--"}
160
+ />
161
+ <KeyValue label="Branch" value={data?.branch ?? "--"} copyable />
162
+ <KeyValue
163
+ label="MCP"
164
+ value={
165
+ data?.mcp && typeof data.mcp.total === "number"
166
+ ? `${data.mcp.healthy}/${data.mcp.total} healthy`
167
+ : "MCP --"
168
+ }
169
+ />
170
+ </KeyValueGroup>
171
+ </Card>
172
+ {isLoading && !data ? (
173
+ <Text style={{ fontSize: 12, color: colors.foregroundMuted }}>
174
+ Loading live snapshot...
175
+ </Text>
176
+ ) : null}
177
+ </Stack>
178
+ )}
179
+
180
+ {activeTab === "settings" && (
181
+ <Stack gap={12}>
182
+ <Card variant="elevated">
183
+ <CardHeader
184
+ title="Pill Display Mode"
185
+ icon="LayoutGrid"
186
+ subtitle="How active items appear in the composer trackbar"
187
+ />
188
+ <ChoiceChips
189
+ options={[
190
+ { id: "cycle", label: "Cycle", description: "Rotate one at a time" },
191
+ { id: "all", label: "All in One", description: "Combined into one pill" },
192
+ { id: "multiple", label: "Multiple", description: "Dedicated pills" },
193
+ ]}
194
+ value={settings.pillMode ?? "cycle"}
195
+ showActiveDescription
196
+ onChange={(pillMode: PillMode) => applySettings({ pillMode })}
197
+ />
198
+ <View style={styles.settingsSpacer}>
199
+ <Toggle
200
+ label="Show Composer Pill"
201
+ description="Hide the composer pill entirely; the dashboard stays available from the sidebar"
202
+ value={settings.showComposerPill ?? true}
203
+ onValueChange={(val) => applySettings({ showComposerPill: val })}
204
+ />
205
+ </View>
206
+ </Card>
207
+
208
+ <Card variant="elevated">
209
+ <CardHeader
210
+ title="Metric Surfaces"
211
+ icon="Sliders"
212
+ subtitle="Choose where each metric appears (pill vs timeline)"
213
+ />
214
+ <Stack gap={12}>
215
+ {METRIC_DEFINITIONS.map((def) => {
216
+ const boxes = checkboxesFromTarget(settings.metricSurfaces?.[def.id]);
217
+ const setBox = (which: "pill" | "timeline", val: boolean) => {
218
+ const nextBoxes = { ...boxes, [which]: val };
219
+ if (def.pillOnly) nextBoxes.timeline = false;
220
+ applySettings({
221
+ metricSurfaces: {
222
+ ...settings.metricSurfaces,
223
+ [def.id]: targetFromCheckboxes(nextBoxes.pill, nextBoxes.timeline),
224
+ } as TopSettings["metricSurfaces"],
225
+ });
226
+ };
227
+ return (
228
+ <View key={def.id}>
229
+ <Text style={{ fontSize: 11, fontWeight: "700", color: colors.foreground }}>
230
+ {def.title}
231
+ </Text>
232
+ <Row gap={24} align="center" style={styles.metricTargets}>
233
+ <Row gap={6} align="center">
234
+ <Text style={[styles.metricTargetLabel, { color: colors.foreground }]}>
235
+ Pill
236
+ </Text>
237
+ <Toggle value={boxes.pill} onValueChange={(val) => setBox("pill", val)} />
238
+ </Row>
239
+ <Row gap={6} align="center">
240
+ <Text style={[styles.metricTargetLabel, { color: colors.foreground }]}>
241
+ Timeline
242
+ </Text>
243
+ <Toggle
244
+ value={boxes.timeline && !def.pillOnly}
245
+ disabled={!!def.pillOnly}
246
+ onValueChange={(val) => setBox("timeline", val)}
247
+ />
248
+ </Row>
249
+ </Row>
250
+ </View>
251
+ );
252
+ })}
253
+ </Stack>
254
+ </Card>
255
+
256
+ <Card variant="elevated">
257
+ <CardHeader
258
+ title="Rotation Speed"
259
+ icon="Clock"
260
+ value={
261
+ <Text style={{ color: colors.accent, fontWeight: "600" }}>
262
+ {`${settings.intervalSeconds}s`}
263
+ </Text>
264
+ }
265
+ />
266
+ <ChoiceChips
267
+ options={[2, 3, 4, 6].map((sec) => ({ id: sec, label: `${sec}s` }))}
268
+ value={settings.intervalSeconds}
269
+ onChange={(intervalSeconds) => applySettings({ intervalSeconds })}
270
+ />
271
+ </Card>
272
+
273
+ <Card variant="elevated">
274
+ <CardHeader
275
+ title="Timeline Cadence"
276
+ icon="Clock"
277
+ value={
278
+ <Text style={{ color: colors.accent, fontWeight: "600" }}>
279
+ {(settings.timelineCadence ?? 1) === 0
280
+ ? "Never"
281
+ : (settings.timelineCadence ?? 1) === 1
282
+ ? "Every turn"
283
+ : `Every ${(settings.timelineCadence ?? 1)} turns`}
284
+ </Text>
285
+ }
286
+ subtitle="How often a card is stamped into the timeline view (0 = never)"
287
+ />
288
+ <ChoiceChips
289
+ options={TIMELINE_CADENCE_OPTIONS}
290
+ value={settings.timelineCadence ?? 1}
291
+ onChange={(timelineCadence) => applySettings({ timelineCadence })}
292
+ />
293
+ </Card>
294
+
295
+ <Button
296
+ label="Reset to Defaults"
297
+ variant="secondary"
298
+ size="sm"
299
+ onPress={() => {
300
+ void resetSettings().then((defaults) => {
301
+ notifySettingsChanged(defaults ?? topSettingsContract.defaultSettings);
302
+ });
303
+ }}
304
+ />
305
+ </Stack>
306
+ )}
307
+
308
+ {activeTab === "about" && (
309
+ <AboutSection
310
+ name="paseo-top"
311
+ description="Live host system and workspace monitor for Paseo composer trackbar."
312
+ version={data?.version ?? PLUGIN_VERSION}
313
+ author="xpufx"
314
+ repository="https://github.com/xpufx/paseo-top"
315
+ issues="https://github.com/xpufx/paseo-top/issues"
316
+ license="MIT"
317
+ density="tiny"
318
+ extraItems={[
319
+ {
320
+ label: "Host Platform",
321
+ value: data?.platform ? `${data.platform} (${data.arch ?? "unknown"})` : "Linux",
322
+ copyable: true,
323
+ },
324
+ { label: "Host Name", value: data?.hostname ?? "localhost", copyable: true },
325
+ { label: "CPU Model", value: data?.cpuModel ?? "unknown", copyable: true },
326
+ { label: "CPU Cores", value: `${data?.cpuCores ?? 0} cores` },
327
+ {
328
+ label: "Total Memory",
329
+ value: data?.memoryTotalBytes ? formatBytes(data.memoryTotalBytes) : "unknown",
330
+ },
331
+ {
332
+ label: "Host Uptime",
333
+ value: data?.uptimeSeconds ? formatUptime(data.uptimeSeconds) : "unknown",
334
+ },
335
+ ]}
336
+ />
337
+ )}
338
+ </ModalBody>
339
+ </View>
340
+ );
341
+ }
342
+
343
+ const styles = {
344
+ root: {
345
+ flex: 1,
346
+ minHeight: 0,
347
+ width: "100%",
348
+ },
349
+ gaugeContainer: {
350
+ paddingVertical: 12,
351
+ width: "100%",
352
+ },
353
+ settingsSpacer: {
354
+ marginTop: 12,
355
+ },
356
+ metricTargets: {
357
+ paddingLeft: 4,
358
+ marginTop: 4,
359
+ },
360
+ metricTargetLabel: {
361
+ fontSize: 9,
362
+ fontWeight: "600",
363
+ },
364
+ } as const;
@@ -0,0 +1,105 @@
1
+ import { formatUptime } from "paseo-plugin-helper/shared";
2
+ import { formatCompactTokens, type TopAgentSnapshot } from "./pill-labels";
3
+ import type { TopTimelineTelemetryData } from "../shared/resources";
4
+
5
+ export interface TelemetryCopyInput {
6
+ data: TopTimelineTelemetryData;
7
+ liveUsage?: TopAgentSnapshot["lastUsage"] | null;
8
+ timeLabel?: string;
9
+ }
10
+
11
+ function outcomeLabel(kind: TopTimelineTelemetryData["outcomeKind"]): string {
12
+ switch (kind) {
13
+ case "completed":
14
+ return "Turn Completed";
15
+ case "failed":
16
+ return "Turn Failed";
17
+ case "canceled":
18
+ return "Turn Canceled";
19
+ default:
20
+ return "Turn Ended";
21
+ }
22
+ }
23
+
24
+ function shortAgentId(agentId: string | undefined): string | undefined {
25
+ if (!agentId) return undefined;
26
+ return agentId.length > 7 ? agentId.slice(0, 7) : agentId;
27
+ }
28
+
29
+ /**
30
+ * Plain-text digest of a styled timeline card. Web's copy handler only rebuilds
31
+ * selections inside `[data-testid="assistant-message"]`, so a plugin/timeline
32
+ * item's selection copies nothing (xpufx-org/paseo#278). This produces the
33
+ * clipboard payload for the card's explicit Copy control instead.
34
+ */
35
+ export function buildTelemetryCopyText({ data, liveUsage, timeLabel }: TelemetryCopyInput): string {
36
+ const inputTokens = data.inputTokens ?? liveUsage?.inputTokens;
37
+ const outputTokens = data.outputTokens ?? liveUsage?.outputTokens;
38
+ const cachedTokens =
39
+ data.cachedTokens ??
40
+ data.cachedInputTokens ??
41
+ liveUsage?.cachedInputTokens ??
42
+ liveUsage?.cachedTokens;
43
+ const contextUsedTokens =
44
+ data.contextUsedTokens ??
45
+ liveUsage?.contextWindowUsedTokens ??
46
+ liveUsage?.contextUsedTokens;
47
+ const contextMaxTokens =
48
+ data.contextMaxTokens ??
49
+ liveUsage?.contextWindowMaxTokens ??
50
+ liveUsage?.contextMaxTokens;
51
+ const costUsd = data.costUsd ?? liveUsage?.totalCostUsd ?? liveUsage?.costUsd;
52
+
53
+ const lines: string[] = [outcomeLabel(data.outcomeKind)];
54
+
55
+ const time = timeLabel ?? data.timestamp;
56
+ if (time) lines.push(time);
57
+
58
+ const id = shortAgentId(data.agentId);
59
+ if (id) lines.push(`Agent ${id}`);
60
+
61
+ const model = data.agentModel ?? undefined;
62
+ const provider = data.agentProvider ?? undefined;
63
+ if (model || provider) {
64
+ lines.push(`${model ?? "model --"} (${provider ?? "default"})`);
65
+ }
66
+
67
+ if (data.branch) lines.push(`Branch ${data.branch}`);
68
+ if (data.worktree) lines.push(`Worktree ${data.worktree}`);
69
+
70
+ const totalTokens =
71
+ inputTokens != null || outputTokens != null
72
+ ? (inputTokens ?? 0) + (outputTokens ?? 0)
73
+ : undefined;
74
+
75
+ if (contextUsedTokens != null && contextMaxTokens != null && contextMaxTokens > 0) {
76
+ const percent = Math.round((contextUsedTokens / contextMaxTokens) * 100);
77
+ lines.push(`Tokens ${formatCompactTokens(contextUsedTokens)}/${formatCompactTokens(contextMaxTokens)} (${percent}% ctx)`);
78
+ } else if (totalTokens != null) {
79
+ lines.push(`Tokens ${inputTokens ?? 0} in / ${outputTokens ?? 0} out (${totalTokens} total)`);
80
+ } else if (contextUsedTokens != null) {
81
+ lines.push(`Tokens ${formatCompactTokens(contextUsedTokens)} ctx`);
82
+ }
83
+
84
+ if (cachedTokens != null) lines.push(`Cache ${cachedTokens.toLocaleString()}`);
85
+ if (costUsd != null) {
86
+ lines.push(`Cost $${costUsd < 0.01 ? costUsd.toFixed(4) : costUsd.toFixed(2)}`);
87
+ }
88
+
89
+ if (data.durationMs != null) lines.push(`Duration ${(data.durationMs / 1000).toFixed(1)}s`);
90
+ if (data.toolCalls != null) {
91
+ lines.push(`Tools ${data.toolCalls}${data.toolErrors ? ` (${data.toolErrors} err)` : ""}`);
92
+ }
93
+ if (data.turnCount != null) lines.push(`Turns ${data.turnCount}`);
94
+ if (data.uptimeSeconds) lines.push(`Uptime ${formatUptime(data.uptimeSeconds)}`);
95
+ if (data.mcpTotal != null) lines.push(`MCP ${data.mcpHealthy ?? 0}/${data.mcpTotal}`);
96
+
97
+ if (data.gitFilesChanged != null || data.gitInsertions != null || data.gitDeletions != null) {
98
+ const files = data.gitFilesChanged ?? 0;
99
+ lines.push(files > 0 ? `Changes ±${files} files +${data.gitInsertions ?? 0}/-${data.gitDeletions ?? 0}` : "Changes none");
100
+ }
101
+
102
+ if (data.outcomeError) lines.push(`Error ${data.outcomeError}`);
103
+
104
+ return lines.join("\n");
105
+ }