@xpufx/paseo-top 0.4.3 → 0.4.5

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 (130) hide show
  1. package/OVERVIEW.md +17 -0
  2. package/client/host-ui.tsx +184 -0
  3. package/client/multi-host-view.tsx +57 -43
  4. package/client/multi-host.ts +36 -13
  5. package/client/pill-labels.ts +124 -7
  6. package/client/pill.tsx +724 -669
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +33 -0
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +310 -274
  11. package/client/telemetry-copy.ts +8 -2
  12. package/client/telemetry.tsx +372 -321
  13. package/client/turn-panel.tsx +126 -104
  14. package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +21 -61
  15. package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
  16. package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
  17. package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
  18. package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
  19. package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
  20. package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
  21. package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
  22. package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
  23. package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
  24. package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
  25. package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
  26. package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
  27. package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
  28. package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +118 -338
  29. package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
  30. package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
  31. package/client/vendor/paseo-plugin-helper/lifecycle/surface.tsx +140 -0
  32. package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
  33. package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
  34. package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
  35. package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +55 -47
  36. package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
  37. package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
  38. package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
  39. package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
  40. package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
  41. package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
  42. package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
  43. package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
  44. package/client/vendor/permission-audit/client.tsx +706 -0
  45. package/client/vendor/permission-audit/index.tsx +1 -0
  46. package/index.client.tsx +58 -37
  47. package/index.server.ts +256 -227
  48. package/package.json +14 -2
  49. package/paseo-plugin.json +2 -0
  50. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  51. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  52. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  53. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  54. package/screenshots/paseo-top-dashboard.jpg +0 -0
  55. package/screenshots/paseo-top-general-view.jpg +0 -0
  56. package/server/resources.ts +98 -0
  57. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  58. package/server/vendor/paseo-plugin-helper/index.ts +4 -0
  59. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  60. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
  61. package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
  62. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  63. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  64. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  65. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  66. package/server/vendor/paseo-plugin-helper/tickets.ts +1232 -0
  67. package/server/vendor/permission-audit/index.ts +1 -0
  68. package/server/vendor/permission-audit/server.ts +839 -0
  69. package/shared/helper-version.ts +20 -0
  70. package/shared/multi-host.ts +156 -155
  71. package/shared/resources.ts +146 -1
  72. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  73. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  74. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  75. package/shared/vendor/paseo-plugin-helper/index.ts +2 -0
  76. package/shared/vendor/paseo-plugin-helper/tickets.ts +1104 -0
  77. package/shared/vendor/permission-audit/filter.ts +61 -0
  78. package/shared/vendor/permission-audit/index.ts +2 -0
  79. package/shared/vendor/permission-audit/shared.ts +119 -0
  80. package/shared/version.ts +1 -1
  81. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  82. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  83. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -156
  84. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -184
  85. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -225
  86. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  87. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  88. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  89. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  90. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  91. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  92. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -75
  93. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  94. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  95. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  96. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  97. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -118
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -80
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -215
  100. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  101. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  102. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  103. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  104. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  105. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -25
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -224
  107. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  108. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  109. package/client/vendor/paseo-plugin-helper/index.ts +0 -29
  110. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  111. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  112. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  113. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -378
  114. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  115. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  117. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  118. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  119. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  120. package/client/vendor/paseo-plugin-helper/surface.tsx +0 -80
  121. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  122. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  123. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  124. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  125. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  126. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  127. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  128. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  129. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  130. /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
@@ -3,54 +3,42 @@ import { Text, View } from "react-native";
3
3
  import { useAgent, type PluginTimelineItemProps } from "@getpaseo/plugin/client";
4
4
  import { Icon } from "@getpaseo/plugin/client/react-native";
5
5
  import {
6
- Badge,
7
- Collapsible,
8
- CopyButton,
9
- PluginThemeProvider,
10
- ProgressBar,
11
- Row,
12
- Stack,
13
- usePluginSettings,
14
- } from "./vendor/paseo-plugin-helper/index";
15
- import { formatBytes, formatUptime, truncatePath } from "../shared/vendor/paseo-plugin-helper/index";
6
+ HostBadge,
7
+ HostCollapsible,
8
+ HostCopyButton,
9
+ HostLayoutProvider,
10
+ HostProgressBar,
11
+ HostRow,
12
+ HostStack,
13
+ HostThemeProvider,
14
+ HostVital,
15
+ getStatusColor,
16
+ } from "./host-ui";
17
+ import { usePluginSettings } from "./vendor/paseo-plugin-helper/core/index";
18
+ import {
19
+ formatBytes,
20
+ formatUptime,
21
+ resolveMetricStatus,
22
+ truncatePath,
23
+ } from "../shared/vendor/paseo-plugin-helper/index";
16
24
  import { buildTelemetryCopyText } from "./telemetry-copy";
17
25
  import {
18
26
  isTimelineEnabled,
19
27
  isMcpSurfaceEnabled,
20
28
  topSettingsContract,
21
29
  TIMELINE_RENDERED_METRICS,
30
+ CPU_THRESHOLDS,
31
+ MEM_THRESHOLDS,
22
32
  type MetricId,
23
33
  type TopTimelineTelemetryData,
24
34
  } from "../shared/resources";
25
- import { formatCompactTokens, type TopAgentSnapshot } from "./pill-labels";
35
+ import {
36
+ formatCompactTokens,
37
+ type TopAgentSnapshot,
38
+ } from "./pill-labels";
26
39
 
27
40
  export { TIMELINE_RENDERED_METRICS };
28
41
 
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
42
  export function TopTimelineTelemetryCard({
55
43
  item,
56
44
  theme,
@@ -58,6 +46,7 @@ export function TopTimelineTelemetryCard({
58
46
  timestamp,
59
47
  }: PluginTimelineItemProps<TopTimelineTelemetryData>) {
60
48
  const data = item.data;
49
+ const compact = layout?.compact ?? false;
61
50
  const [isExpanded, setIsExpanded] = useState(false);
62
51
  const liveUsage = useAgent(data.agentId, (a: TopAgentSnapshot) => a.lastUsage);
63
52
  const inputTokens = data.inputTokens ?? liveUsage?.inputTokens;
@@ -111,17 +100,19 @@ export function TopTimelineTelemetryCard({
111
100
  }
112
101
  }, [data.outcomeKind, theme.colors]);
113
102
 
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]);
103
+ // CPU/RAM colors resolve through the shared canonical thresholds
104
+ // (shared/resources.ts) so the timeline card can never diverge from the
105
+ // pill and dashboard surfaces.
106
+ const cpuColor = useMemo(
107
+ () => getStatusColor(resolveMetricStatus(data.cpuPercent, CPU_THRESHOLDS), theme.colors),
108
+ [data.cpuPercent, theme.colors],
109
+ );
119
110
 
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]);
111
+ const memColor = useMemo(
112
+ () =>
113
+ getStatusColor(resolveMetricStatus(data.memPercent, MEM_THRESHOLDS), theme.colors),
114
+ [data.memPercent, theme.colors],
115
+ );
125
116
 
126
117
  const timeLabel = useMemo(() => {
127
118
  try {
@@ -166,306 +157,358 @@ export function TopTimelineTelemetryCard({
166
157
  : (data.outcomeError ?? "Canceled")
167
158
  : "";
168
159
 
160
+ // Velocity badge from computed tokensPerSecond.
161
+ const velocityBadge = useMemo(() => {
162
+ if (data.tokensPerSecond == null || !Number.isFinite(data.tokensPerSecond)) return null;
163
+ const tps = Math.round(data.tokensPerSecond);
164
+ let label = `⚡ ${tps} tok/s`;
165
+ if (data.contextPressureLevel === "high") {
166
+ label += " · ⚠️ high ctx";
167
+ } else if (data.contextPressureLevel === "warning") {
168
+ label += " · ⚠️ warn ctx";
169
+ }
170
+ return label;
171
+ }, [data.tokensPerSecond, data.contextPressureLevel]);
172
+
169
173
  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
- )}
174
+ <HostThemeProvider theme={theme}>
175
+ <HostLayoutProvider layout={layout}>
176
+ <HostCollapsible
177
+ variant="elevated"
178
+ isExpanded={isExpanded}
179
+ onToggle={setIsExpanded}
180
+ style={data.outcomeKind === "failed" ? { borderColor: theme.colors.statusDanger } : undefined}
181
+ title={
182
+ <HostRow gap={6} align="center" style={styles.headerInlineRow}>
183
+ <Icon name={outcomeConfig.icon} size={14} color={outcomeConfig.color} />
184
+ <Text style={{ fontSize: 12, fontWeight: "600", color: theme.colors.foreground, flexShrink: 0 }}>
185
+ {outcomeConfig.label}
186
+ </Text>
187
+ {data.durationMs != null ? (
188
+ <HostBadge
189
+ label={`${(data.durationMs / 1000).toFixed(1)}s`}
190
+ variant="neutral"
191
+ style={{ flexShrink: 0 }}
192
+ />
193
+ ) : null}
194
+ {show("velocity") && velocityBadge ? (
195
+ <HostBadge
196
+ label={velocityBadge}
197
+ variant={
198
+ data.contextPressureLevel === "high"
199
+ ? "danger"
200
+ : data.contextPressureLevel === "warning"
201
+ ? "warning"
202
+ : "neutral"
203
+ }
204
+ style={{ flexShrink: 0 }}
205
+ />
206
+ ) : null}
207
+ </HostRow>
208
+ }
209
+ subtitle={
210
+ !isExpanded && data.outcomeKind === "canceled" ? (
211
+ <Text numberOfLines={2} style={{ fontSize: 11, color: theme.colors.statusDanger }}>
212
+ {canceledText}
213
+ </Text>
214
+ ) : undefined
215
+ }
216
+ headerRight={
217
+ <HostRow gap={6} align="center" style={styles.headerInlineRow}>
218
+ {timeLabel !== "" ? (
219
+ <Text style={{ fontSize: 10, color: theme.colors.foregroundMuted }}>{timeLabel}</Text>
220
+ ) : null}
221
+ <Text
222
+ style={{
223
+ fontSize: 10,
224
+ color: theme.colors.foregroundMuted,
225
+ fontStyle: "italic",
226
+ }}
227
+ >
228
+ via top
229
+ </Text>
230
+ {/*
231
+ * Explicit copy affordance. Web's selection-copy handler only
232
+ * rebuilds clipboard content for `[data-testid="assistant-message"]`
233
+ * selections, so styled timeline items copy nothing
234
+ * (xpufx-org/paseo#278). The helper CopyButton bypasses that gate
235
+ * with the helper's own clipboard path. Empty labels keep the
236
+ * header compact: the icon flips Copy -> Check on success.
237
+ */}
238
+ <HostCopyButton
239
+ getText={getCopyText}
240
+ label=""
241
+ copiedLabel=""
242
+ accessibilityLabel="Copy timeline card"
243
+ toastMessage="timeline card"
244
+ />
245
+ </HostRow>
246
+ }
247
+ summary={
248
+ <HostRow wrap gap={compact ? 6 : 8} align="center">
249
+ {show("cpu_ram") && (
250
+ <HostVital icon="Cpu" color={cpuColor}>
251
+ CPU {data.cpuPercent}%
252
+ </HostVital>
253
+ )}
233
254
 
234
- {show("cpu_ram") && (
235
- <Vital icon="Database" color={memColor}>
236
- RAM {formatBytes(data.memUsedBytes)} ({data.memPercent}%)
237
- </Vital>
238
- )}
255
+ {show("cpu_ram") && (
256
+ <HostVital icon="Database" color={memColor}>
257
+ RAM {formatBytes(data.memUsedBytes)} ({data.memPercent}%)
258
+ </HostVital>
259
+ )}
239
260
 
240
- {show("load") && (
241
- <Vital icon="Activity" color={theme.colors.foreground}>
242
- Load {data.loadAvg1m.toFixed(2)}
243
- </Vital>
244
- )}
261
+ {show("load") && (
262
+ <HostVital icon="Activity" color={theme.colors.foreground}>
263
+ Load {data.loadAvg1m.toFixed(2)}
264
+ </HostVital>
265
+ )}
245
266
 
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
- )}
267
+ {showMcp && (
268
+ <HostVital
269
+ icon="Server"
270
+ color={
271
+ data.mcpTotal == null
272
+ ? theme.colors.foregroundMuted
273
+ : (data.mcpHealthy ?? 0) === data.mcpTotal
274
+ ? theme.colors.statusSuccess
275
+ : theme.colors.statusWarning
276
+ }
277
+ >
278
+ {data.mcpTotal != null ? `MCP ${data.mcpHealthy ?? 0}/${data.mcpTotal}` : "MCP --"}
279
+ </HostVital>
280
+ )}
260
281
 
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
- )}
282
+ {show("agent_id") && (
283
+ <HostVital icon="Fingerprint" color={theme.colors.foreground}>
284
+ {data.agentId && data.agentId.length > 7
285
+ ? data.agentId.slice(0, 7)
286
+ : (data.agentId ?? "--")}
287
+ </HostVital>
288
+ )}
268
289
 
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
- )}
290
+ {show("agent") && (
291
+ <HostVital
292
+ icon="Bot"
293
+ color={data.agentModel ? theme.colors.foreground : theme.colors.foregroundMuted}
294
+ >
295
+ {data.agentModel ?? "model --"}
296
+ </HostVital>
297
+ )}
277
298
 
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
- )}
299
+ {show("agent_provider") && (
300
+ <HostVital
301
+ icon="Globe"
302
+ color={
303
+ data.agentProvider ? theme.colors.foreground : theme.colors.foregroundMuted
304
+ }
305
+ >
306
+ {data.agentProvider ?? "provider --"}
307
+ </HostVital>
308
+ )}
288
309
 
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
- )}
310
+ {show("agent_title") && (
311
+ <HostVital
312
+ icon="Tag"
313
+ color={data.agentTitle ? theme.colors.foreground : theme.colors.foregroundMuted}
314
+ >
315
+ {data.agentTitle ?? "title --"}
316
+ </HostVital>
317
+ )}
297
318
 
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
- )}
319
+ {show("branch") && (
320
+ <HostVital
321
+ icon="GitBranch"
322
+ color={data.branch ? theme.colors.foreground : theme.colors.foregroundMuted}
323
+ >
324
+ {data.branch ?? "branch --"}
325
+ </HostVital>
326
+ )}
306
327
 
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
- )}
328
+ {show("worktree") && (
329
+ <HostVital
330
+ icon="Folder"
331
+ color={data.worktree ? theme.colors.foreground : theme.colors.foregroundMuted}
332
+ >
333
+ {data.worktree ? truncatePath(data.worktree, 20) : "worktree --"}
334
+ </HostVital>
335
+ )}
315
336
 
316
- {show("uptime") && (
317
- <Vital icon="Clock" color={theme.colors.foreground}>
318
- {data.uptimeSeconds ? formatUptime(data.uptimeSeconds) : "--"}
319
- </Vital>
320
- )}
337
+ {show("uptime") && (
338
+ <HostVital icon="Clock" color={theme.colors.foreground}>
339
+ {data.uptimeSeconds ? formatUptime(data.uptimeSeconds) : "--"}
340
+ </HostVital>
341
+ )}
321
342
 
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
- )}
343
+ {show("changes") && (
344
+ <HostVital icon="GitCommitHorizontal" color={theme.colors.foreground}>
345
+ {(data.gitFilesChanged ?? 0) > 0
346
+ ? `±${data.gitFilesChanged} files +${data.gitInsertions ?? 0}/-${data.gitDeletions ?? 0}`
347
+ : "No changes"}
348
+ </HostVital>
349
+ )}
329
350
 
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
- )}
351
+ {show("tokens") && (
352
+ <HostVital
353
+ icon="Coins"
354
+ color={
355
+ totalTokens != null || contextUsedTokens != null
356
+ ? theme.colors.foreground
357
+ : theme.colors.foregroundMuted
358
+ }
359
+ >
360
+ {contextUsedTokens != null && contextMaxTokens
361
+ ? `${formatCompactTokens(contextUsedTokens)}/${formatCompactTokens(contextMaxTokens)}${contextPercent != null ? ` (${contextPercent}% ctx)` : ""}`
362
+ : totalTokens != null
363
+ ? `${formatCompactTokens(totalTokens)} tok`
364
+ : contextUsedTokens != null
365
+ ? `${formatCompactTokens(contextUsedTokens)} ctx`
366
+ : "tok --"}
367
+ </HostVital>
368
+ )}
348
369
 
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
- )}
370
+ {show("velocity") && (
371
+ <HostVital
372
+ icon="Zap"
373
+ color={
374
+ data.tokensPerSecond != null && Number.isFinite(data.tokensPerSecond)
375
+ ? data.contextPressureLevel === "high"
376
+ ? theme.colors.statusDanger
377
+ : data.contextPressureLevel === "warning"
378
+ ? theme.colors.statusWarning
379
+ : theme.colors.foreground
380
+ : theme.colors.foregroundMuted
381
+ }
382
+ >
383
+ {data.tokensPerSecond != null && Number.isFinite(data.tokensPerSecond)
384
+ ? `${Math.round(data.tokensPerSecond)} tok/s`
385
+ : "vel --"}
386
+ </HostVital>
387
+ )}
356
388
 
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>
389
+ {show("tools") && (
390
+ <HostVital icon="Sigma" color={theme.colors.foreground}>
391
+ {data.toolCalls != null
392
+ ? `${data.toolCalls}${data.toolErrors ? ` (${data.toolErrors} err)` : ""}`
393
+ : "tools --"}
394
+ </HostVital>
395
+ )}
380
396
 
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>
397
+ {show("turns") && (
398
+ <HostVital
399
+ icon="Repeat"
400
+ color={data.turnCount != null ? theme.colors.foreground : theme.colors.foregroundMuted}
401
+ >
402
+ {data.turnCount != null ? `${data.turnCount} turns` : "turns --"}
403
+ </HostVital>
404
+ )}
405
+ </HostRow>
406
+ }
407
+ >
408
+ <HostStack gap={6}>
409
+ {show("tokens") && hasTokenDetails ? (
410
+ <HostStack gap={8}>
411
+ <HostRow justify="between" align="center">
412
+ <HostRow gap={4} align="center">
413
+ <Icon name="Coins" size={12} color={theme.colors.foregroundMuted} />
414
+ <Text style={sectionTitleStyle(theme.colors)}>Tokens & Context</Text>
415
+ </HostRow>
416
+ {costUsd != null && (
387
417
  <Text style={{ fontSize: 10, fontWeight: "600", color: theme.colors.foreground }}>
388
- {formatCompactTokens(contextUsedTokens ?? 0)} /{" "}
389
- {formatCompactTokens(contextMaxTokens)} ({contextPercent}%)
418
+ ${costUsd < 0.01 ? costUsd.toFixed(4) : costUsd.toFixed(2)}
390
419
  </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
420
+ )}
421
+ </HostRow>
422
+
423
+ {contextMaxTokens != null && contextMaxTokens > 0 ? (
424
+ <HostStack gap={4}>
425
+ <HostRow justify="between" align="center">
426
+ <Text style={{ fontSize: 10, color: theme.colors.foregroundMuted }}>
427
+ Context Window
428
+ </Text>
429
+ <Text style={{ fontSize: 10, fontWeight: "600", color: theme.colors.foreground }}>
430
+ {formatCompactTokens(contextUsedTokens ?? 0)} /{" "}
431
+ {formatCompactTokens(contextMaxTokens)} ({contextPercent}%)
432
+ </Text>
433
+ </HostRow>
434
+ <HostProgressBar
435
+ value={contextPercent ?? 0}
436
+ thresholds={{ warning: 70, danger: 85 }}
437
+ height={6}
438
+ />
439
+ </HostStack>
440
+ ) : contextUsedTokens != null ? (
441
+ <Text style={{ fontSize: 10, color: theme.colors.foreground }}>
442
+ {formatCompactTokens(contextUsedTokens)} tokens
443
+ </Text>
444
+ ) : null}
445
+
446
+ <HostRow wrap gap={6} align="center">
447
+ {inputTokens != null && (
448
+ <HostBadge label={`In: ${inputTokens.toLocaleString('en-US')}`} variant="neutral" />
449
+ )}
450
+ {outputTokens != null && (
451
+ <HostBadge label={`Out: ${outputTokens.toLocaleString('en-US')}`} variant="neutral" />
452
+ )}
453
+ {cachedTokens != null && (
454
+ <HostBadge label={`Cache: ${cachedTokens.toLocaleString('en-US')}`} variant="neutral" />
455
+ )}
456
+ </HostRow>
457
+ </HostStack>
458
+ ) : show("tokens") ? (
459
+ <HostRow justify="between" align="center">
460
+ <Text style={sectionTitleStyle(theme.colors)}>Tokens & Context</Text>
461
+ <Text
462
+ style={{
463
+ fontSize: 10,
464
+ fontStyle: "italic",
465
+ color: theme.colors.foregroundMuted,
466
+ }}
467
+ >
468
+ Not reported by provider
401
469
  </Text>
402
- ) : null}
470
+ </HostRow>
471
+ ) : null}
403
472
 
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" />
473
+ <HostStack gap={4}>
474
+ <Text style={sectionTitleStyle(theme.colors)}>Turn Details</Text>
475
+ <HostRow wrap gap={12} align="center">
476
+ <HostVital grow={false} icon="Cpu" color={theme.colors.foreground}>
477
+ {data.agentModel ?? "Unknown model"} ({data.agentProvider ?? "default"})
478
+ </HostVital>
479
+ {data.toolCalls != null && (
480
+ <HostVital grow={false} icon="Sigma" color={theme.colors.foreground}>
481
+ {data.toolCalls} calls
482
+ {data.toolErrors ? `, ${data.toolErrors} failed` : ""}
483
+ </HostVital>
410
484
  )}
411
- {cachedTokens != null && (
412
- <Badge label={`Cache: ${cachedTokens.toLocaleString()}`} variant="neutral" />
485
+ {(data.gitInsertions != null || data.gitDeletions != null) && (
486
+ <HostVital grow={false} icon="GitCommitHorizontal" color={theme.colors.foreground}>
487
+ +{data.gitInsertions ?? 0} -{data.gitDeletions ?? 0}
488
+ </HostVital>
413
489
  )}
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
490
+ </HostRow>
491
+ </HostStack>
492
+
493
+ {data.outcomeError && (
494
+ <View
420
495
  style={{
421
- fontSize: 10,
422
- fontStyle: "italic",
423
- color: theme.colors.foregroundMuted,
496
+ padding: 6,
497
+ borderRadius: 4,
498
+ backgroundColor: theme.colors.surface2,
499
+ borderLeftWidth: 3,
500
+ borderLeftColor: theme.colors.statusDanger,
424
501
  }}
425
502
  >
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>
503
+ <Text numberOfLines={2} style={{ fontSize: 11, color: theme.colors.statusDanger }}>
504
+ {data.outcomeError}
505
+ </Text>
506
+ </View>
507
+ )}
508
+ </HostStack>
509
+ </HostCollapsible>
510
+ </HostLayoutProvider>
511
+ </HostThemeProvider>
469
512
  );
470
513
  }
471
514
 
@@ -479,6 +522,14 @@ const styles = {
479
522
  textTransform: "uppercase",
480
523
  letterSpacing: 0.5,
481
524
  },
525
+ // Header slots are auto-width: a `HostRow`'s base `width: 100%` resolves to
526
+ // zero inside the collapsible's `flex: 1` title column on native Yoga, which
527
+ // collapsed the header and stranded the outcome/time row mid-card
528
+ // (xpufx-org/paseo#1010). Opting these rows out of the full-width default
529
+ // keeps the header on one content-sized line.
530
+ headerInlineRow: {
531
+ width: "auto",
532
+ },
482
533
  } as const;
483
534
 
484
535
  function sectionTitleStyle(colors: { foregroundMuted: string }) {