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