@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,486 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { Text, View } from "react-native";
|
|
3
|
+
import { useAgent, type PluginTimelineItemProps } from "@getpaseo/plugin/client";
|
|
4
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
5
|
+
import {
|
|
6
|
+
Badge,
|
|
7
|
+
Collapsible,
|
|
8
|
+
CopyButton,
|
|
9
|
+
PluginThemeProvider,
|
|
10
|
+
ProgressBar,
|
|
11
|
+
Row,
|
|
12
|
+
Stack,
|
|
13
|
+
usePluginSettings,
|
|
14
|
+
} from "paseo-plugin-helper/client";
|
|
15
|
+
import { formatBytes, formatUptime, truncatePath } from "paseo-plugin-helper/shared";
|
|
16
|
+
import { buildTelemetryCopyText } from "./telemetry-copy";
|
|
17
|
+
import {
|
|
18
|
+
isTimelineEnabled,
|
|
19
|
+
isMcpSurfaceEnabled,
|
|
20
|
+
topSettingsContract,
|
|
21
|
+
TIMELINE_RENDERED_METRICS,
|
|
22
|
+
type MetricId,
|
|
23
|
+
type TopTimelineTelemetryData,
|
|
24
|
+
} from "../shared/resources";
|
|
25
|
+
import { formatCompactTokens, type TopAgentSnapshot } from "./pill-labels";
|
|
26
|
+
|
|
27
|
+
export { TIMELINE_RENDERED_METRICS };
|
|
28
|
+
|
|
29
|
+
function Vital({
|
|
30
|
+
icon,
|
|
31
|
+
color,
|
|
32
|
+
children,
|
|
33
|
+
}: {
|
|
34
|
+
icon: string;
|
|
35
|
+
color: string;
|
|
36
|
+
children: React.ReactNode;
|
|
37
|
+
}) {
|
|
38
|
+
return (
|
|
39
|
+
<Row gap={4} align="center">
|
|
40
|
+
<Icon name={icon} size={12} color={color} />
|
|
41
|
+
{/*
|
|
42
|
+
* Color MUST be applied to the label as well as the icon. A <Text> with
|
|
43
|
+
* no `color` falls back to React Native's default black and disappears on
|
|
44
|
+
* dark surfaces (xpufx-org/paseo#208). Reusing the icon's `color` keeps
|
|
45
|
+
* threshold states (e.g. CPU/RAM warning/danger) consistent for both.
|
|
46
|
+
*/}
|
|
47
|
+
<Text numberOfLines={1} style={{ fontSize: 11, fontWeight: "500", color }}>
|
|
48
|
+
{children}
|
|
49
|
+
</Text>
|
|
50
|
+
</Row>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function TopTimelineTelemetryCard({
|
|
55
|
+
item,
|
|
56
|
+
theme,
|
|
57
|
+
layout,
|
|
58
|
+
timestamp,
|
|
59
|
+
}: PluginTimelineItemProps<TopTimelineTelemetryData>) {
|
|
60
|
+
const data = item.data;
|
|
61
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
62
|
+
const liveUsage = useAgent(data.agentId, (a: TopAgentSnapshot) => a.lastUsage);
|
|
63
|
+
const inputTokens = data.inputTokens ?? liveUsage?.inputTokens;
|
|
64
|
+
const outputTokens = data.outputTokens ?? liveUsage?.outputTokens;
|
|
65
|
+
const cachedTokens =
|
|
66
|
+
data.cachedTokens ??
|
|
67
|
+
data.cachedInputTokens ??
|
|
68
|
+
liveUsage?.cachedInputTokens ??
|
|
69
|
+
liveUsage?.cachedTokens;
|
|
70
|
+
const contextUsedTokens =
|
|
71
|
+
data.contextUsedTokens ??
|
|
72
|
+
liveUsage?.contextWindowUsedTokens ??
|
|
73
|
+
liveUsage?.contextUsedTokens;
|
|
74
|
+
const contextMaxTokens =
|
|
75
|
+
data.contextMaxTokens ??
|
|
76
|
+
liveUsage?.contextWindowMaxTokens ??
|
|
77
|
+
liveUsage?.contextMaxTokens;
|
|
78
|
+
const costUsd =
|
|
79
|
+
data.costUsd ?? liveUsage?.totalCostUsd ?? liveUsage?.costUsd;
|
|
80
|
+
const { settings } = usePluginSettings(topSettingsContract);
|
|
81
|
+
const surfaces = settings.metricSurfaces;
|
|
82
|
+
const show = (id: MetricId) => !surfaces || isTimelineEnabled(surfaces[id]);
|
|
83
|
+
const showMcp = isMcpSurfaceEnabled(settings, "timeline", data.mcpInstalled, data.mcpRunning);
|
|
84
|
+
|
|
85
|
+
const outcomeConfig = useMemo(() => {
|
|
86
|
+
switch (data.outcomeKind) {
|
|
87
|
+
case "completed":
|
|
88
|
+
return {
|
|
89
|
+
icon: "CheckCircle2",
|
|
90
|
+
color: theme.colors.statusSuccess,
|
|
91
|
+
label: "Turn Completed",
|
|
92
|
+
};
|
|
93
|
+
case "failed":
|
|
94
|
+
return {
|
|
95
|
+
icon: "AlertCircle",
|
|
96
|
+
color: theme.colors.statusDanger,
|
|
97
|
+
label: "Turn Failed",
|
|
98
|
+
};
|
|
99
|
+
case "canceled":
|
|
100
|
+
return {
|
|
101
|
+
icon: "MinusCircle",
|
|
102
|
+
color: theme.colors.statusWarning,
|
|
103
|
+
label: "Turn Canceled",
|
|
104
|
+
};
|
|
105
|
+
default:
|
|
106
|
+
return {
|
|
107
|
+
icon: "Activity",
|
|
108
|
+
color: theme.colors.foregroundMuted,
|
|
109
|
+
label: "Turn Ended",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}, [data.outcomeKind, theme.colors]);
|
|
113
|
+
|
|
114
|
+
const cpuColor = useMemo(() => {
|
|
115
|
+
if (data.cpuPercent >= 85) return theme.colors.statusDanger;
|
|
116
|
+
if (data.cpuPercent >= 70) return theme.colors.statusWarning;
|
|
117
|
+
return theme.colors.foreground;
|
|
118
|
+
}, [data.cpuPercent, theme.colors]);
|
|
119
|
+
|
|
120
|
+
const memColor = useMemo(() => {
|
|
121
|
+
if (data.memPercent >= 90) return theme.colors.statusDanger;
|
|
122
|
+
if (data.memPercent >= 75) return theme.colors.statusWarning;
|
|
123
|
+
return theme.colors.foreground;
|
|
124
|
+
}, [data.memPercent, theme.colors]);
|
|
125
|
+
|
|
126
|
+
const timeLabel = useMemo(() => {
|
|
127
|
+
try {
|
|
128
|
+
const d = data.timestamp ? new Date(data.timestamp) : timestamp;
|
|
129
|
+
return d.toLocaleTimeString([], {
|
|
130
|
+
hour: "2-digit",
|
|
131
|
+
minute: "2-digit",
|
|
132
|
+
second: "2-digit",
|
|
133
|
+
});
|
|
134
|
+
} catch {
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
}, [data.timestamp, timestamp]);
|
|
138
|
+
|
|
139
|
+
const totalTokens =
|
|
140
|
+
inputTokens != null || outputTokens != null
|
|
141
|
+
? (inputTokens ?? 0) + (outputTokens ?? 0)
|
|
142
|
+
: undefined;
|
|
143
|
+
|
|
144
|
+
const contextPercent = useMemo(() => {
|
|
145
|
+
if (!contextMaxTokens || contextMaxTokens <= 0) return null;
|
|
146
|
+
return Math.round(((contextUsedTokens ?? 0) / contextMaxTokens) * 100);
|
|
147
|
+
}, [contextUsedTokens, contextMaxTokens]);
|
|
148
|
+
|
|
149
|
+
const getCopyText = useCallback(
|
|
150
|
+
() => buildTelemetryCopyText({ data, liveUsage, timeLabel }),
|
|
151
|
+
[data, liveUsage, timeLabel],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const hasTokenDetails =
|
|
155
|
+
inputTokens != null ||
|
|
156
|
+
outputTokens != null ||
|
|
157
|
+
cachedTokens != null ||
|
|
158
|
+
contextUsedTokens != null ||
|
|
159
|
+
contextMaxTokens != null ||
|
|
160
|
+
costUsd != null;
|
|
161
|
+
|
|
162
|
+
const canceledText =
|
|
163
|
+
data.outcomeKind === "canceled"
|
|
164
|
+
? !/^cancel/i.test(data.outcomeError ?? "")
|
|
165
|
+
? `Canceled${data.outcomeError ? `: ${data.outcomeError}` : ""}`
|
|
166
|
+
: (data.outcomeError ?? "Canceled")
|
|
167
|
+
: "";
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<PluginThemeProvider theme={theme} layout={layout}>
|
|
171
|
+
<Collapsible
|
|
172
|
+
variant="elevated"
|
|
173
|
+
isExpanded={isExpanded}
|
|
174
|
+
onToggle={setIsExpanded}
|
|
175
|
+
style={data.outcomeKind === "failed" ? { borderColor: theme.colors.statusDanger } : undefined}
|
|
176
|
+
title={
|
|
177
|
+
<Row gap={6} align="center">
|
|
178
|
+
<Icon name={outcomeConfig.icon} size={14} color={outcomeConfig.color} />
|
|
179
|
+
<Text style={{ fontSize: 12, fontWeight: "600", color: theme.colors.foreground }}>
|
|
180
|
+
{outcomeConfig.label}
|
|
181
|
+
</Text>
|
|
182
|
+
{data.durationMs != null ? (
|
|
183
|
+
<Badge label={`${(data.durationMs / 1000).toFixed(1)}s`} variant="neutral" />
|
|
184
|
+
) : null}
|
|
185
|
+
</Row>
|
|
186
|
+
}
|
|
187
|
+
headerRight={
|
|
188
|
+
<Row gap={6} align="center">
|
|
189
|
+
{timeLabel !== "" ? (
|
|
190
|
+
<Text style={{ fontSize: 10, color: theme.colors.foregroundMuted }}>{timeLabel}</Text>
|
|
191
|
+
) : null}
|
|
192
|
+
<Text
|
|
193
|
+
style={{
|
|
194
|
+
fontSize: 10,
|
|
195
|
+
color: theme.colors.foregroundMuted,
|
|
196
|
+
fontStyle: "italic",
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
via top
|
|
200
|
+
</Text>
|
|
201
|
+
{/*
|
|
202
|
+
* Explicit copy affordance. Web's selection-copy handler only
|
|
203
|
+
* rebuilds clipboard content for `[data-testid="assistant-message"]`
|
|
204
|
+
* selections, so styled timeline items copy nothing
|
|
205
|
+
* (xpufx-org/paseo#278). The helper CopyButton bypasses that gate
|
|
206
|
+
* with the helper's own clipboard path. Empty labels keep the
|
|
207
|
+
* header compact: the icon flips Copy -> Check on success.
|
|
208
|
+
*/}
|
|
209
|
+
<CopyButton
|
|
210
|
+
getText={getCopyText}
|
|
211
|
+
label=""
|
|
212
|
+
copiedLabel=""
|
|
213
|
+
accessibilityLabel="Copy timeline card"
|
|
214
|
+
toastMessage="timeline card"
|
|
215
|
+
/>
|
|
216
|
+
</Row>
|
|
217
|
+
}
|
|
218
|
+
summary={
|
|
219
|
+
!isExpanded && data.outcomeKind === "canceled" ? (
|
|
220
|
+
<Text numberOfLines={2} style={{ fontSize: 11, color: theme.colors.statusDanger }}>
|
|
221
|
+
{canceledText}
|
|
222
|
+
</Text>
|
|
223
|
+
) : undefined
|
|
224
|
+
}
|
|
225
|
+
>
|
|
226
|
+
<Stack gap={6}>
|
|
227
|
+
<Row wrap gap={8} align="center">
|
|
228
|
+
{show("cpu_ram") && (
|
|
229
|
+
<Vital icon="Cpu" color={cpuColor}>
|
|
230
|
+
CPU {data.cpuPercent}%
|
|
231
|
+
</Vital>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
{show("cpu_ram") && (
|
|
235
|
+
<Vital icon="Database" color={memColor}>
|
|
236
|
+
RAM {formatBytes(data.memUsedBytes)} ({data.memPercent}%)
|
|
237
|
+
</Vital>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
{show("load") && (
|
|
241
|
+
<Vital icon="Activity" color={theme.colors.foreground}>
|
|
242
|
+
Load {data.loadAvg1m.toFixed(2)}
|
|
243
|
+
</Vital>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{showMcp && (
|
|
247
|
+
<Vital
|
|
248
|
+
icon="Server"
|
|
249
|
+
color={
|
|
250
|
+
data.mcpTotal == null
|
|
251
|
+
? theme.colors.foregroundMuted
|
|
252
|
+
: (data.mcpHealthy ?? 0) === data.mcpTotal
|
|
253
|
+
? theme.colors.statusSuccess
|
|
254
|
+
: theme.colors.statusWarning
|
|
255
|
+
}
|
|
256
|
+
>
|
|
257
|
+
{data.mcpTotal != null ? `MCP ${data.mcpHealthy ?? 0}/${data.mcpTotal}` : "MCP --"}
|
|
258
|
+
</Vital>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{show("agent_id") && (
|
|
262
|
+
<Vital icon="Fingerprint" color={theme.colors.foreground}>
|
|
263
|
+
{data.agentId && data.agentId.length > 7
|
|
264
|
+
? data.agentId.slice(0, 7)
|
|
265
|
+
: (data.agentId ?? "--")}
|
|
266
|
+
</Vital>
|
|
267
|
+
)}
|
|
268
|
+
|
|
269
|
+
{show("agent") && (
|
|
270
|
+
<Vital
|
|
271
|
+
icon="Bot"
|
|
272
|
+
color={data.agentModel ? theme.colors.foreground : theme.colors.foregroundMuted}
|
|
273
|
+
>
|
|
274
|
+
{data.agentModel ?? "model --"}
|
|
275
|
+
</Vital>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{show("agent_provider") && (
|
|
279
|
+
<Vital
|
|
280
|
+
icon="Globe"
|
|
281
|
+
color={
|
|
282
|
+
data.agentProvider ? theme.colors.foreground : theme.colors.foregroundMuted
|
|
283
|
+
}
|
|
284
|
+
>
|
|
285
|
+
{data.agentProvider ?? "provider --"}
|
|
286
|
+
</Vital>
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
{show("agent_title") && (
|
|
290
|
+
<Vital
|
|
291
|
+
icon="Tag"
|
|
292
|
+
color={data.agentTitle ? theme.colors.foreground : theme.colors.foregroundMuted}
|
|
293
|
+
>
|
|
294
|
+
{data.agentTitle ?? "title --"}
|
|
295
|
+
</Vital>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
{show("branch") && (
|
|
299
|
+
<Vital
|
|
300
|
+
icon="GitBranch"
|
|
301
|
+
color={data.branch ? theme.colors.foreground : theme.colors.foregroundMuted}
|
|
302
|
+
>
|
|
303
|
+
{data.branch ?? "branch --"}
|
|
304
|
+
</Vital>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{show("worktree") && (
|
|
308
|
+
<Vital
|
|
309
|
+
icon="Folder"
|
|
310
|
+
color={data.worktree ? theme.colors.foreground : theme.colors.foregroundMuted}
|
|
311
|
+
>
|
|
312
|
+
{data.worktree ? truncatePath(data.worktree, 20) : "worktree --"}
|
|
313
|
+
</Vital>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{show("uptime") && (
|
|
317
|
+
<Vital icon="Clock" color={theme.colors.foreground}>
|
|
318
|
+
{data.uptimeSeconds ? formatUptime(data.uptimeSeconds) : "--"}
|
|
319
|
+
</Vital>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{show("changes") && (
|
|
323
|
+
<Vital icon="GitCommitHorizontal" color={theme.colors.foreground}>
|
|
324
|
+
{(data.gitFilesChanged ?? 0) > 0
|
|
325
|
+
? `±${data.gitFilesChanged} files +${data.gitInsertions ?? 0}/-${data.gitDeletions ?? 0}`
|
|
326
|
+
: "No changes"}
|
|
327
|
+
</Vital>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{show("tokens") && (
|
|
331
|
+
<Vital
|
|
332
|
+
icon="Coins"
|
|
333
|
+
color={
|
|
334
|
+
totalTokens != null || contextUsedTokens != null
|
|
335
|
+
? theme.colors.foreground
|
|
336
|
+
: theme.colors.foregroundMuted
|
|
337
|
+
}
|
|
338
|
+
>
|
|
339
|
+
{contextUsedTokens != null && contextMaxTokens
|
|
340
|
+
? `${formatCompactTokens(contextUsedTokens)}/${formatCompactTokens(contextMaxTokens)}${contextPercent != null ? ` (${contextPercent}% ctx)` : ""}`
|
|
341
|
+
: totalTokens != null
|
|
342
|
+
? `${formatCompactTokens(totalTokens)} tok`
|
|
343
|
+
: contextUsedTokens != null
|
|
344
|
+
? `${formatCompactTokens(contextUsedTokens)} ctx`
|
|
345
|
+
: "tok --"}
|
|
346
|
+
</Vital>
|
|
347
|
+
)}
|
|
348
|
+
|
|
349
|
+
{show("tools") && (
|
|
350
|
+
<Vital icon="Sigma" color={theme.colors.foreground}>
|
|
351
|
+
{data.toolCalls != null
|
|
352
|
+
? `${data.toolCalls}${data.toolErrors ? ` (${data.toolErrors} err)` : ""}`
|
|
353
|
+
: "tools --"}
|
|
354
|
+
</Vital>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{show("turns") && (
|
|
358
|
+
<Vital
|
|
359
|
+
icon="Repeat"
|
|
360
|
+
color={data.turnCount != null ? theme.colors.foreground : theme.colors.foregroundMuted}
|
|
361
|
+
>
|
|
362
|
+
{data.turnCount != null ? `${data.turnCount} turns` : "turns --"}
|
|
363
|
+
</Vital>
|
|
364
|
+
)}
|
|
365
|
+
</Row>
|
|
366
|
+
|
|
367
|
+
{show("tokens") && hasTokenDetails ? (
|
|
368
|
+
<Stack gap={8}>
|
|
369
|
+
<Row justify="space-between" align="center">
|
|
370
|
+
<Row gap={4} align="center">
|
|
371
|
+
<Icon name="Coins" size={12} color={theme.colors.foregroundMuted} />
|
|
372
|
+
<Text style={sectionTitleStyle(theme.colors)}>Tokens & Context</Text>
|
|
373
|
+
</Row>
|
|
374
|
+
{costUsd != null && (
|
|
375
|
+
<Text style={{ fontSize: 10, fontWeight: "600", color: theme.colors.foreground }}>
|
|
376
|
+
${costUsd < 0.01 ? costUsd.toFixed(4) : costUsd.toFixed(2)}
|
|
377
|
+
</Text>
|
|
378
|
+
)}
|
|
379
|
+
</Row>
|
|
380
|
+
|
|
381
|
+
{contextMaxTokens != null && contextMaxTokens > 0 ? (
|
|
382
|
+
<Stack gap={4}>
|
|
383
|
+
<Row justify="space-between" align="center">
|
|
384
|
+
<Text style={{ fontSize: 10, color: theme.colors.foregroundMuted }}>
|
|
385
|
+
Context Window
|
|
386
|
+
</Text>
|
|
387
|
+
<Text style={{ fontSize: 10, fontWeight: "600", color: theme.colors.foreground }}>
|
|
388
|
+
{formatCompactTokens(contextUsedTokens ?? 0)} /{" "}
|
|
389
|
+
{formatCompactTokens(contextMaxTokens)} ({contextPercent}%)
|
|
390
|
+
</Text>
|
|
391
|
+
</Row>
|
|
392
|
+
<ProgressBar
|
|
393
|
+
value={contextPercent ?? 0}
|
|
394
|
+
thresholds={{ warning: 70, danger: 85 }}
|
|
395
|
+
height={6}
|
|
396
|
+
/>
|
|
397
|
+
</Stack>
|
|
398
|
+
) : contextUsedTokens != null ? (
|
|
399
|
+
<Text style={{ fontSize: 10, color: theme.colors.foreground }}>
|
|
400
|
+
{formatCompactTokens(contextUsedTokens)} tokens
|
|
401
|
+
</Text>
|
|
402
|
+
) : null}
|
|
403
|
+
|
|
404
|
+
<Row wrap gap={6} align="center">
|
|
405
|
+
{inputTokens != null && (
|
|
406
|
+
<Badge label={`In: ${inputTokens.toLocaleString()}`} variant="neutral" />
|
|
407
|
+
)}
|
|
408
|
+
{outputTokens != null && (
|
|
409
|
+
<Badge label={`Out: ${outputTokens.toLocaleString()}`} variant="neutral" />
|
|
410
|
+
)}
|
|
411
|
+
{cachedTokens != null && (
|
|
412
|
+
<Badge label={`Cache: ${cachedTokens.toLocaleString()}`} variant="neutral" />
|
|
413
|
+
)}
|
|
414
|
+
</Row>
|
|
415
|
+
</Stack>
|
|
416
|
+
) : show("tokens") ? (
|
|
417
|
+
<Row justify="space-between" align="center">
|
|
418
|
+
<Text style={sectionTitleStyle(theme.colors)}>Tokens & Context</Text>
|
|
419
|
+
<Text
|
|
420
|
+
style={{
|
|
421
|
+
fontSize: 10,
|
|
422
|
+
fontStyle: "italic",
|
|
423
|
+
color: theme.colors.foregroundMuted,
|
|
424
|
+
}}
|
|
425
|
+
>
|
|
426
|
+
Not reported by provider
|
|
427
|
+
</Text>
|
|
428
|
+
</Row>
|
|
429
|
+
) : null}
|
|
430
|
+
|
|
431
|
+
<Stack gap={4}>
|
|
432
|
+
<Text style={sectionTitleStyle(theme.colors)}>Turn Details</Text>
|
|
433
|
+
<Row wrap gap={12} align="center">
|
|
434
|
+
<Vital icon="Cpu" color={theme.colors.foreground}>
|
|
435
|
+
{data.agentModel ?? "Unknown model"} ({data.agentProvider ?? "default"})
|
|
436
|
+
</Vital>
|
|
437
|
+
{data.toolCalls != null && (
|
|
438
|
+
<Vital icon="Sigma" color={theme.colors.foreground}>
|
|
439
|
+
{data.toolCalls} calls
|
|
440
|
+
{data.toolErrors ? `, ${data.toolErrors} failed` : ""}
|
|
441
|
+
</Vital>
|
|
442
|
+
)}
|
|
443
|
+
{(data.gitInsertions != null || data.gitDeletions != null) && (
|
|
444
|
+
<Vital icon="GitCommitHorizontal" color={theme.colors.foreground}>
|
|
445
|
+
+{data.gitInsertions ?? 0} -{data.gitDeletions ?? 0}
|
|
446
|
+
</Vital>
|
|
447
|
+
)}
|
|
448
|
+
</Row>
|
|
449
|
+
</Stack>
|
|
450
|
+
|
|
451
|
+
{data.outcomeError && (
|
|
452
|
+
<View
|
|
453
|
+
style={{
|
|
454
|
+
padding: 6,
|
|
455
|
+
borderRadius: 4,
|
|
456
|
+
backgroundColor: theme.colors.surface2,
|
|
457
|
+
borderLeftWidth: 3,
|
|
458
|
+
borderLeftColor: theme.colors.statusDanger,
|
|
459
|
+
}}
|
|
460
|
+
>
|
|
461
|
+
<Text numberOfLines={2} style={{ fontSize: 11, color: theme.colors.statusDanger }}>
|
|
462
|
+
{data.outcomeError}
|
|
463
|
+
</Text>
|
|
464
|
+
</View>
|
|
465
|
+
)}
|
|
466
|
+
</Stack>
|
|
467
|
+
</Collapsible>
|
|
468
|
+
</PluginThemeProvider>
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const styles = {
|
|
473
|
+
// Layout-only. Color MUST come from the theme: a raw style without `color`
|
|
474
|
+
// falls back to React Native's default black and vanishes in dark mode
|
|
475
|
+
// (xpufx-org/paseo#208). Use `sectionTitleStyle(colors)` at call sites.
|
|
476
|
+
sectionTitle: {
|
|
477
|
+
fontSize: 10,
|
|
478
|
+
fontWeight: "600",
|
|
479
|
+
textTransform: "uppercase",
|
|
480
|
+
letterSpacing: 0.5,
|
|
481
|
+
},
|
|
482
|
+
} as const;
|
|
483
|
+
|
|
484
|
+
function sectionTitleStyle(colors: { foregroundMuted: string }) {
|
|
485
|
+
return [styles.sectionTitle, { color: colors.foregroundMuted }];
|
|
486
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic conversation turn counter.
|
|
3
|
+
*
|
|
4
|
+
* Source of truth is the daemon timeline, reached through the plugin-facing
|
|
5
|
+
* `paseo.agents.ref(id).timeline.refetch()` path (xpufx-org/paseo#248). Each
|
|
6
|
+
* `user_message` entry maps 1:1 to a canonical timeline row, so filtering to
|
|
7
|
+
* `user_message` and sorting by `seqEnd` yields a stable 1-based turn number
|
|
8
|
+
* plus the exact `seq` an agent can resolve against the same timeline. The UI
|
|
9
|
+
* and the agent therefore agree on `Turn #N (seq S)` without a bespoke server.
|
|
10
|
+
*
|
|
11
|
+
* No `@getpaseo/*` import: the input/output shapes are structural so the module
|
|
12
|
+
* stays dependency-free and unit-testable without the Paseo runtime.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface TimelineEntryLike {
|
|
16
|
+
seqStart?: number;
|
|
17
|
+
seqEnd?: number;
|
|
18
|
+
item?: { type?: string; text?: string } | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TimelineCursor {
|
|
22
|
+
epoch: string;
|
|
23
|
+
seq: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TimelinePageLike {
|
|
27
|
+
entries: readonly TimelineEntryLike[];
|
|
28
|
+
hasOlder?: boolean;
|
|
29
|
+
startCursor?: TimelineCursor | null;
|
|
30
|
+
staleCursor?: boolean;
|
|
31
|
+
reset?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TimelineRefetchOptionsLike {
|
|
35
|
+
direction?: "tail" | "before" | "after";
|
|
36
|
+
cursor?: TimelineCursor;
|
|
37
|
+
limit?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AgentTimelineRefLike {
|
|
41
|
+
timeline: {
|
|
42
|
+
refetch(options?: TimelineRefetchOptionsLike): Promise<TimelinePageLike>;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PaseoAgentApiLike {
|
|
47
|
+
agents: { ref(agentId: string): AgentTimelineRefLike };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TurnRef {
|
|
51
|
+
/** 1-based conversation turn number (Turn #1 is the first user message). */
|
|
52
|
+
turn: number;
|
|
53
|
+
/** Canonical timeline seq of the user_message row; shared with the agent. */
|
|
54
|
+
seq: number;
|
|
55
|
+
/** Verbatim user message text (may be empty). */
|
|
56
|
+
preview: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Default page size while walking history backwards. */
|
|
60
|
+
export const TURN_PAGE_LIMIT = 200;
|
|
61
|
+
/** Safety bound so a pathological timeline cannot loop forever. */
|
|
62
|
+
export const TURN_MAX_PAGES = 50;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `Turn #3 (seq 12)` — the single address format the panel renders and tests
|
|
66
|
+
* assert, so a human reference and the agent's own resolution cannot drift.
|
|
67
|
+
*/
|
|
68
|
+
export function formatTurnAddress(turn: number, seq: number): string {
|
|
69
|
+
return `Turn #${turn} (seq ${seq})`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Shortest unambiguous tail of a turn, for list rows and copy actions. */
|
|
73
|
+
export function formatTurnSummary(turn: TurnRef): string {
|
|
74
|
+
return formatTurnAddress(turn.turn, turn.seq);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Derives 1-based turns from timeline entries. `user_message` rows are filtered
|
|
79
|
+
* and sorted by `seq`, so a caller's page-concatenation order is irrelevant.
|
|
80
|
+
* Entries without a usable numeric seq are dropped rather than silently
|
|
81
|
+
* shifting later turn numbers.
|
|
82
|
+
*/
|
|
83
|
+
export function deriveTurns(entries: readonly TimelineEntryLike[]): TurnRef[] {
|
|
84
|
+
const rows: Array<{ seq: number; preview: string }> = [];
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const item = entry?.item;
|
|
87
|
+
if (!item || item.type !== "user_message") continue;
|
|
88
|
+
const seq = typeof entry.seqEnd === "number" ? entry.seqEnd : entry.seqStart;
|
|
89
|
+
if (typeof seq !== "number" || !Number.isFinite(seq)) continue;
|
|
90
|
+
rows.push({ seq, preview: typeof item.text === "string" ? item.text : "" });
|
|
91
|
+
}
|
|
92
|
+
rows.sort((a, b) => a.seq - b.seq);
|
|
93
|
+
return rows.map((row, index) => ({ turn: index + 1, seq: row.seq, preview: row.preview }));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Current/last turn, or undefined for an empty conversation. */
|
|
97
|
+
export function currentTurn(turns: readonly TurnRef[]): TurnRef | undefined {
|
|
98
|
+
return turns.length > 0 ? turns[turns.length - 1] : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Full-history turns via cursor paging. `refetch()` defaults to a 200-entry
|
|
103
|
+
* tail page, so walk backwards with `direction: "before"` using the page's
|
|
104
|
+
* `startCursor` until the daemon reports no older history. A stale/reset epoch
|
|
105
|
+
* (agent replacement) stops the walk and returns what was collected.
|
|
106
|
+
*/
|
|
107
|
+
export async function fetchAllTurns(
|
|
108
|
+
paseo: PaseoAgentApiLike,
|
|
109
|
+
agentId: string,
|
|
110
|
+
pageLimit: number = TURN_PAGE_LIMIT,
|
|
111
|
+
): Promise<TurnRef[]> {
|
|
112
|
+
const handle = paseo.agents.ref(agentId);
|
|
113
|
+
let page = await handle.timeline.refetch({ limit: pageLimit });
|
|
114
|
+
const bySeq = new Map<number, TimelineEntryLike>();
|
|
115
|
+
storeEntries(bySeq, page.entries);
|
|
116
|
+
|
|
117
|
+
let pages = 0;
|
|
118
|
+
while (page.hasOlder && page.startCursor && pages < TURN_MAX_PAGES) {
|
|
119
|
+
const next = await handle.timeline.refetch({
|
|
120
|
+
direction: "before",
|
|
121
|
+
cursor: page.startCursor,
|
|
122
|
+
limit: pageLimit,
|
|
123
|
+
});
|
|
124
|
+
if (next.staleCursor || next.reset) break;
|
|
125
|
+
storeEntries(bySeq, next.entries);
|
|
126
|
+
page = next;
|
|
127
|
+
pages += 1;
|
|
128
|
+
}
|
|
129
|
+
return deriveTurns([...bySeq.values()]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function storeEntries(
|
|
133
|
+
bySeq: Map<number, TimelineEntryLike>,
|
|
134
|
+
entries: readonly TimelineEntryLike[],
|
|
135
|
+
): void {
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
const seq = entrySeq(entry);
|
|
138
|
+
if (seq !== undefined) bySeq.set(seq, entry);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Stable dedupe key for an entry. `user_message` is not identity-merged, so
|
|
144
|
+
* `seqEnd` is the row seq; fall back to `seqStart` if a host omits it. Returns
|
|
145
|
+
* undefined for a malformed entry so it is dropped, never a bogus `seq -1`.
|
|
146
|
+
*/
|
|
147
|
+
function entrySeq(entry: TimelineEntryLike): number | undefined {
|
|
148
|
+
if (typeof entry.seqEnd === "number" && Number.isFinite(entry.seqEnd)) return entry.seqEnd;
|
|
149
|
+
if (typeof entry.seqStart === "number" && Number.isFinite(entry.seqStart)) return entry.seqStart;
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|