@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
package/OVERVIEW.md ADDED
@@ -0,0 +1,17 @@
1
+ Top adds real-time host resource telemetry and session performance metrics directly to the Paseo composer trackbar, conversation timeline, and an interactive dashboard modal.
2
+
3
+ The plugin displays live CPU and memory utilization, system load averages (1m, 5m, 15m), active agent turns, token consumption, and cost telemetry. An interactive dashboard modal provides visual resource gauges, multi-host fleet telemetry, workspace context, and configurable metric routing.
4
+
5
+ ## Capabilities and Data Access
6
+
7
+ - **Host Resource Telemetry**: Samples CPU usage, memory utilization (including Linux `/proc/meminfo` available memory), load averages, host uptime, and core counts via local OS interfaces.
8
+ - **Agent and Turn Telemetry**: Monitors active agent session metadata (model, provider, turn counts, input/output/cached tokens, execution error counts) and can stamp a summary card into the session timeline upon turn completion.
9
+ - **Display Modes**: Supports Cycle Mode (rotates through metrics on a configurable interval), All-in-One Mode (unified ticker), and Multiple Mode (side-by-side pills).
10
+ - **Multi-Host Telemetry**: When configured with multiple daemon hosts, queries registered hosts bounded to one probe per host on a 15-second cadence with a 4-second timeout to report online/offline status and host latency.
11
+ - **Custom Metrics**: Supports user-defined metrics via declarative `.jsonc` definitions in scoped plugin storage (`~/.paseo/plugin-data/xpufx/top/pills/`) with optional periodic command execution and threshold status mapping.
12
+
13
+ ## Setup and Requirements
14
+
15
+ - Runs on the Paseo daemon and requires Paseo 0.9.0 or later.
16
+ - Telemetry sampling runs locally on the daemon host (Linux, macOS, Windows).
17
+ - All plugin persistent configuration is stored within scoped plugin storage.
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Re-export the shared UI composition package for `plugins/top`
3
+ * (xpufx-org/paseo#976, #1218, #1258).
4
+ *
5
+ * All host-delegating cards, buttons, badges, inputs, form rows, action bars,
6
+ * tabs, collapsibles, code blocks, gauges, about sections, and theme providers
7
+ * come directly from `@xpufx/paseo-plugin-ui`.
8
+ */
9
+
10
+ import React from "react";
11
+ import {
12
+ HostModalSection as BaseHostModalSection,
13
+ ModalBodyScrollOwnerContext,
14
+ type HostModalSectionProps,
15
+ } from "@xpufx/paseo-plugin-ui";
16
+
17
+ /**
18
+ * Top pill modal sections operate under the host single-scroller model
19
+ * (`hostScroll: true`, xpufx-org/paseo#975, #1054, #1258).
20
+ * Ensures fluid, unconstrained height when rendered with or without an outer
21
+ * host provider in testing harnesses.
22
+ */
23
+ export function HostModalSection({ children, style, ...props }: HostModalSectionProps) {
24
+ return (
25
+ <ModalBodyScrollOwnerContext.Provider value="host">
26
+ <BaseHostModalSection style={style} {...props}>
27
+ {children}
28
+ </BaseHostModalSection>
29
+ </ModalBodyScrollOwnerContext.Provider>
30
+ );
31
+ }
32
+
33
+ export {
34
+ HostAboutSection,
35
+ HostActionBar,
36
+ HostActionBar as ActionBar,
37
+ HostAttentionBeacon,
38
+ HostAttentionBeacon as AttentionBeacon,
39
+ HostBadge,
40
+ HostBadge as Badge,
41
+ HostButton,
42
+ HostButton as Button,
43
+ HostCard,
44
+ HostCard as Card,
45
+ HostCardHeader,
46
+ HostCardHeader as CardHeader,
47
+ HostCodeBlock,
48
+ HostCodeBlock as CodeBlock,
49
+ HostCollapsible,
50
+ HostCollapsible as Collapsible,
51
+ HostCommandBox,
52
+ HostCommandBox as CommandBox,
53
+ HostCopyButton,
54
+ HostEmptyState,
55
+ HostEmptyState as EmptyState,
56
+ HostFormRow,
57
+ HostFormRow as FormRow,
58
+ HostGrid,
59
+ HostHighlightedText,
60
+ HostHighlightedText as HighlightedText,
61
+ HostInlineButton,
62
+ HostInlineButton as InlineButton,
63
+ HostKeyValue,
64
+ HostKeyValue as KeyValue,
65
+ HostKeyValueGroup,
66
+ HostKeyValueGroup as KeyValueGroup,
67
+ HostLayoutProvider,
68
+ HostMetricGauge,
69
+ HostModalBody,
70
+ HostModalBody as ModalBody,
71
+ HostProgressBar,
72
+ HostRow,
73
+ HostScroll,
74
+ HostSearchInput,
75
+ HostSectionHeader,
76
+ HostSelect,
77
+ HostSelect as Select,
78
+ HostStack,
79
+ HostStatusDot,
80
+ HostStatusDot as StatusDot,
81
+ HostTabs,
82
+ HostTabs as Tabs,
83
+ HostTextInput,
84
+ HostTextInput as TextInput,
85
+ HostThemeProvider,
86
+ HostToggle,
87
+ HostToggle as Toggle,
88
+ HostVital,
89
+ ModalBodyScrollOwnerContext,
90
+ alpha,
91
+ getStatusColor,
92
+ sanitizeFluidStyle,
93
+ useHostLayout,
94
+ useHostTheme,
95
+ useModalBodyScrollOwner,
96
+ } from "@xpufx/paseo-plugin-ui";
97
+
98
+ export type {
99
+ HighlightOptions,
100
+ HighlightPart,
101
+ HostAboutLink,
102
+ HostAboutSectionProps,
103
+ HostActionBarProps,
104
+ HostActionBarProps as ActionBarProps,
105
+ HostAttentionBeaconMode,
106
+ HostAttentionBeaconMode as AttentionBeaconMode,
107
+ HostAttentionBeaconProps,
108
+ HostAttentionBeaconProps as AttentionBeaconProps,
109
+ HostAttentionBeaconTone,
110
+ HostAttentionBeaconTone as AttentionBeaconTone,
111
+ HostBadgeProps,
112
+ HostBadgeProps as BadgeProps,
113
+ HostBadgeSize,
114
+ HostBadgeSize as BadgeSize,
115
+ HostBadgeStyle,
116
+ HostBadgeStyle as BadgeStyle,
117
+ HostButtonProps,
118
+ HostButtonProps as ButtonProps,
119
+ HostButtonSize,
120
+ HostButtonSize as ButtonSize,
121
+ HostButtonVariant,
122
+ HostButtonVariant as ButtonVariant,
123
+ HostCardHeaderProps,
124
+ HostCardHeaderProps as CardHeaderProps,
125
+ HostCardProps,
126
+ HostCardProps as CardProps,
127
+ HostCodeBlockProps,
128
+ HostCodeBlockProps as CodeBlockProps,
129
+ HostCollapsibleProps,
130
+ HostCollapsibleProps as CollapsibleProps,
131
+ HostCommandBoxProps,
132
+ HostCommandBoxProps as CommandBoxProps,
133
+ HostCopyButtonProps,
134
+ HostCopyButtonSize,
135
+ HostCopyButtonVariant,
136
+ HostEmptyStateProps,
137
+ HostEmptyStateProps as EmptyStateProps,
138
+ HostFormRowProps,
139
+ HostFormRowProps as FormRowProps,
140
+ HostGridProps,
141
+ HostHighlightedTextProps,
142
+ HostInlineButtonProps,
143
+ HostInlineButtonProps as InlineButtonProps,
144
+ HostKeyValueGroupProps,
145
+ HostKeyValueGroupProps as KeyValueGroupProps,
146
+ HostKeyValueProps,
147
+ HostKeyValueProps as KeyValueProps,
148
+ HostKeyValueTruncateMode,
149
+ HostKeyValueTruncateMode as KeyValueTruncateMode,
150
+ HostLayoutProviderProps,
151
+ HostMetricGaugeProps,
152
+ HostModalBodyProps,
153
+ HostModalBodyProps as ModalBodyProps,
154
+ HostModalSectionProps,
155
+ HostProgressBarProps,
156
+ HostRowProps,
157
+ HostScrollProps,
158
+ HostSearchInputProps,
159
+ HostSectionHeaderProps,
160
+ HostSelectOption,
161
+ HostSelectProps,
162
+ HostSelectProps as SelectProps,
163
+ HostStackProps,
164
+ HostStatusDotProps,
165
+ HostStatusDotProps as StatusDotProps,
166
+ HostSurfaceVariant,
167
+ HostTabItem,
168
+ HostTabsProps,
169
+ HostTabsProps as TabsProps,
170
+ HostTextInputProps,
171
+ HostTextInputProps as TextInputProps,
172
+ HostThemeProviderProps,
173
+ HostToggleProps,
174
+ HostToggleProps as ToggleProps,
175
+ HostVitalProps,
176
+ ModalBodyScrollOwner,
177
+ PluginTheme,
178
+ PluginUiTheme,
179
+ PluginUiTheme as TopTheme,
180
+ ResponsiveLayout,
181
+ ScrollViewInstance,
182
+ StatusVariant,
183
+ ThemeColors,
184
+ } from "@xpufx/paseo-plugin-ui";
@@ -1,24 +1,25 @@
1
1
  import React, { useMemo } from "react";
2
2
  import { Text } from "react-native";
3
3
  import {
4
- Badge,
5
- Card,
6
- CardHeader,
7
- EmptyState,
8
- KeyValue,
9
- KeyValueGroup,
10
- Row,
11
- SectionHeader,
12
- Stack,
13
- StatusDot,
14
- usePluginTheme,
15
- } from "./vendor/paseo-plugin-helper/index";
4
+ HostBadge,
5
+ HostCard,
6
+ HostCardHeader,
7
+ HostEmptyState,
8
+ HostGrid,
9
+ HostKeyValue,
10
+ HostRow,
11
+ HostSectionHeader,
12
+ HostStack,
13
+ HostStatusDot,
14
+ useHostLayout,
15
+ useHostTheme,
16
+ } from "./host-ui";
16
17
  import {
17
18
  aggregateFleet,
18
19
  type FleetHostSnapshot,
19
20
  type FleetHostStatus,
20
21
  } from "../shared/multi-host";
21
- import { useFleetPolling } from "./multi-host";
22
+ import { multiHostSupported, useFleetPolling } from "./multi-host";
22
23
 
23
24
  const STATUS_VARIANT: Record<
24
25
  FleetHostStatus,
@@ -59,7 +60,8 @@ function FleetHostCard({
59
60
  snapshot: FleetHostSnapshot;
60
61
  now: number;
61
62
  }) {
62
- const { colors } = usePluginTheme();
63
+ const { colors } = useHostTheme();
64
+ const { compact } = useHostLayout();
63
65
  const variant = STATUS_VARIANT[snapshot.status];
64
66
  const staleAge = formatStaleAge(snapshot.staleAt, now);
65
67
  const subtitle =
@@ -68,47 +70,47 @@ function FleetHostCard({
68
70
  : (staleAge ?? snapshot.error ?? "Not connected");
69
71
 
70
72
  return (
71
- <Card variant="elevated">
72
- <CardHeader
73
+ <HostCard variant="elevated">
74
+ <HostCardHeader
73
75
  icon="Server"
74
76
  title={snapshot.label}
75
77
  subtitle={subtitle}
76
78
  badge={
77
- <Row gap={6} align="center">
78
- <StatusDot
79
+ <HostRow gap={6} align="center">
80
+ <HostStatusDot
79
81
  variant={snapshot.status === "online" ? "success" : variant}
80
82
  pulse={snapshot.status === "online"}
81
83
  />
82
- <Badge
84
+ <HostBadge
83
85
  label={STATUS_LABEL[snapshot.status]}
84
86
  variant={variant}
85
87
  size="sm"
86
88
  />
87
- </Row>
89
+ </HostRow>
88
90
  }
89
91
  />
90
- <KeyValueGroup>
91
- <KeyValue label="Host" value={snapshot.serverId} truncate="middle" copyable />
92
- <KeyValue label="Latency" value={formatLatency(snapshot.latencyMs)} />
93
- <KeyValue
92
+ <HostGrid columns={compact ? 1 : 2} gap={12}>
93
+ <HostKeyValue label="Host" value={snapshot.serverId} truncate="middle" copyable />
94
+ <HostKeyValue label="Latency" value={formatLatency(snapshot.latencyMs)} />
95
+ <HostKeyValue
94
96
  label="Agents"
95
97
  value={snapshot.counts ? String(snapshot.counts.agents) : "--"}
96
98
  />
97
- <KeyValue
99
+ <HostKeyValue
98
100
  label="Active agents"
99
101
  value={snapshot.counts ? String(snapshot.counts.activeAgents) : "--"}
100
102
  />
101
- <KeyValue
103
+ <HostKeyValue
102
104
  label="Workspaces"
103
105
  value={snapshot.counts ? String(snapshot.counts.workspaces) : "--"}
104
106
  />
105
- </KeyValueGroup>
107
+ </HostGrid>
106
108
  {snapshot.status === "error" && snapshot.error ? (
107
109
  <Text style={{ marginTop: 8, fontSize: 11, color: colors.statusDanger }}>
108
110
  {snapshot.error}
109
111
  </Text>
110
112
  ) : null}
111
- </Card>
113
+ </HostCard>
112
114
  );
113
115
  }
114
116
 
@@ -122,14 +124,26 @@ function FleetHostCard({
122
124
  * Top behavior is unaffected whenever the tab is not selected.
123
125
  */
124
126
  export function FleetView() {
125
- const { colors } = usePluginTheme();
127
+ const { colors } = useHostTheme();
126
128
  const snapshots = useFleetPolling();
127
129
  const now = Date.now();
128
130
  const aggregate = useMemo(() => aggregateFleet(snapshots), [snapshots]);
129
131
 
132
+ // A host that does not supply `useHosts`/`getPaseoClient` (the mobile bundle)
133
+ // cannot borrow remote clients. Say so instead of throwing.
134
+ if (!multiHostSupported()) {
135
+ return (
136
+ <HostEmptyState
137
+ icon="Server"
138
+ title="Multi-host fleet unavailable"
139
+ description="This host build does not provide multi-host access. Open Paseo on desktop to see other hosts."
140
+ />
141
+ );
142
+ }
143
+
130
144
  if (snapshots.length === 0) {
131
145
  return (
132
- <EmptyState
146
+ <HostEmptyState
133
147
  icon="Server"
134
148
  title="No other hosts"
135
149
  description="Configure additional hosts in Paseo to see their fleet status here."
@@ -138,33 +152,33 @@ export function FleetView() {
138
152
  }
139
153
 
140
154
  return (
141
- <Stack gap={12}>
155
+ <HostStack gap={12}>
142
156
  <Text style={{ fontSize: 16, fontWeight: "700", color: colors.foreground }}>
143
157
  Multi-Host Fleet
144
158
  </Text>
145
159
 
146
- <Card variant="elevated">
147
- <CardHeader
160
+ <HostCard variant="elevated">
161
+ <HostCardHeader
148
162
  title="Fleet Totals"
149
163
  icon="Activity"
150
164
  subtitle="Counts only. CPU/RAM/load stay per-host."
151
165
  />
152
- <KeyValueGroup>
153
- <KeyValue label="Hosts" value={`${aggregate.responsive}/${aggregate.hosts} responsive`} />
154
- <KeyValue
166
+ <HostGrid columns={2} gap={12}>
167
+ <HostKeyValue label="Hosts" value={`${aggregate.responsive}/${aggregate.hosts} responsive`} />
168
+ <HostKeyValue
155
169
  label="Online / Stale"
156
170
  value={`${aggregate.online} / ${aggregate.stale}`}
157
171
  />
158
- <KeyValue label="Agents" value={String(aggregate.counts.agents)} />
159
- <KeyValue
172
+ <HostKeyValue label="Agents" value={String(aggregate.counts.agents)} />
173
+ <HostKeyValue
160
174
  label="Active agents"
161
175
  value={String(aggregate.counts.activeAgents)}
162
176
  />
163
- <KeyValue label="Workspaces" value={String(aggregate.counts.workspaces)} />
164
- </KeyValueGroup>
165
- </Card>
177
+ <HostKeyValue label="Workspaces" value={String(aggregate.counts.workspaces)} />
178
+ </HostGrid>
179
+ </HostCard>
166
180
 
167
- <SectionHeader title="Hosts" count={snapshots.length} />
181
+ <HostSectionHeader title="Hosts" count={snapshots.length} />
168
182
  {snapshots.map((snapshot) => (
169
183
  <FleetHostCard
170
184
  key={snapshot.serverId}
@@ -172,6 +186,6 @@ export function FleetView() {
172
186
  now={now}
173
187
  />
174
188
  ))}
175
- </Stack>
189
+ </HostStack>
176
190
  );
177
191
  }
@@ -1,24 +1,43 @@
1
- import { getPaseoClient, useHosts } from "@getpaseo/plugin/client";
2
1
  import { useEffect, useMemo, useRef, useState } from "react";
3
2
  import {
4
- MultiHostPoller,
3
+ getOptionalPaseoClient,
4
+ isMultiHostSupported,
5
+ useOptionalHosts,
6
+ } from "./vendor/paseo-plugin-helper/lifecycle/index";
7
+ import {
8
+ createMultiHostPoller,
9
+ type MultiHostPoller,
5
10
  type FleetHostClient,
6
11
  type FleetHostCounts,
7
12
  type FleetHostSnapshot,
8
13
  } from "../shared/multi-host";
9
14
 
10
15
  /**
11
- * Wrap a borrowed `getPaseoClient(serverId)` as a fleet client.
16
+ * True when the running host supplies the multi-host primitives.
17
+ *
18
+ * Feature detection lives in `paseo-plugin-helper/lifecycle` so top and
19
+ * x-comms share one seam; this wrapper keeps the Fleet tab's local name.
20
+ */
21
+ export function multiHostSupported(): boolean {
22
+ return isMultiHostSupported();
23
+ }
24
+
25
+ /**
26
+ * Wrap a borrowed `getOptionalPaseoClient(serverId)` as a fleet client.
12
27
  *
13
28
  * Only counts are read. The borrowed API exposes agents/workspaces/projects/
14
29
  * terminals/providers/config and no host-metrics RPC, so the fleet view cannot
15
30
  * show remote CPU/RAM/load/uptime; fabricating them would be mock data.
16
31
  */
17
32
  export function createFleetClient(serverId: string): FleetHostClient {
18
- // getPaseoClient throws for a non-online/unknown host; the poller only
19
- // acquires for online hosts, and the throw is caught per host so one bad
20
- // host cannot break the others.
21
- const client = getPaseoClient(serverId);
33
+ // getOptionalPaseoClient throws for a non-online/unknown host; the poller
34
+ // only acquires for online hosts, and the throw is caught per host so one bad
35
+ // host cannot break the others. It returns undefined only when the host omits
36
+ // the seam entirely, which the poller never reaches.
37
+ const client = getOptionalPaseoClient(serverId);
38
+ if (!client) {
39
+ throw new Error("multi-host fleet is not available on this host");
40
+ }
22
41
  return {
23
42
  async readCounts(): Promise<FleetHostCounts> {
24
43
  const [agents, workspaces] = await Promise.all([
@@ -47,15 +66,19 @@ export interface UseFleetPollingOptions {
47
66
  /**
48
67
  * Bounded multi-host fleet polling hook.
49
68
  *
50
- * Enumerates online hosts with `useHosts()`, acquires a borrowed
51
- * `getPaseoClient(serverId)` per online host, and drives the pure
69
+ * Enumerates online hosts with `useOptionalHosts()`, acquires a borrowed
70
+ * `getOptionalPaseoClient(serverId)` per online host, and drives the pure
52
71
  * {@link MultiHostPoller} state machine: max one in-flight probe per host,
53
72
  * 15s cadence, 4s per-host timeout, stale marking, and dispose/reacquire on
54
73
  * host status transitions. Local Top behavior is untouched because nothing
55
74
  * starts unless the fleet view is mounted and enabled.
56
75
  */
57
76
  export function useFleetPolling({ enabled = true }: UseFleetPollingOptions = {}) {
58
- const hosts = useHosts();
77
+ // The mobile host omits the primitives; nothing to poll, so no hooks run and
78
+ // the caller renders an unavailable state. Both checks are module-constant,
79
+ // so the hook order is stable for a given host.
80
+ const supported = isMultiHostSupported();
81
+ const hosts = useOptionalHosts();
59
82
  const [snapshots, setSnapshots] = useState<FleetHostSnapshot[]>([]);
60
83
  const pollerRef = useRef<MultiHostPoller | null>(null);
61
84
 
@@ -75,8 +98,8 @@ export function useFleetPolling({ enabled = true }: UseFleetPollingOptions = {})
75
98
  hostInputsRef.current = hostInputs;
76
99
 
77
100
  useEffect(() => {
78
- if (!enabled) return;
79
- const poller = new MultiHostPoller({
101
+ if (!enabled || !supported) return;
102
+ const poller = createMultiHostPoller({
80
103
  acquire: createFleetClient,
81
104
  onUpdate: setSnapshots,
82
105
  });
@@ -88,7 +111,7 @@ export function useFleetPolling({ enabled = true }: UseFleetPollingOptions = {})
88
111
  pollerRef.current = null;
89
112
  setSnapshots([]);
90
113
  };
91
- }, [enabled]);
114
+ }, [enabled, supported]);
92
115
 
93
116
  useEffect(() => {
94
117
  pollerRef.current?.setHosts(hostInputs);
@@ -40,7 +40,8 @@ export type PillItemType =
40
40
  | "changes"
41
41
  | "tokens"
42
42
  | "tools"
43
- | "turns";
43
+ | "turns"
44
+ | "velocity";
44
45
 
45
46
  export interface SegmentSnapshot {
46
47
  data?: SystemResources;
@@ -56,6 +57,20 @@ export interface SegmentSnapshot {
56
57
  worktreeLocationText?: string;
57
58
  }
58
59
 
60
+ /**
61
+ * First non-blank string wins, else the fallback. `??` only guards null and
62
+ * undefined, so a present-but-empty host field (blank `title`, `provider`,
63
+ * `branch`, `agentId`) slipped through as an empty label. On a button-shaped
64
+ * host an empty label renders icon-only, which reads as the pill collapsing to
65
+ * a skinny/empty line (xpufx-org/paseo#507).
66
+ */
67
+ function firstNonBlank(fallback: string, ...values: Array<string | null | undefined>): string {
68
+ for (const value of values) {
69
+ if (typeof value === "string" && value.trim().length > 0) return value;
70
+ }
71
+ return fallback;
72
+ }
73
+
59
74
  export interface TokenMetrics {
60
75
  inputTokens?: number;
61
76
  outputTokens?: number;
@@ -66,6 +81,9 @@ export interface TokenMetrics {
66
81
  costUsd?: number;
67
82
  }
68
83
 
84
+ /** Context pressure level classification. */
85
+ export type ContextPressureLevel = "normal" | "warning" | "high";
86
+
69
87
  export function formatCompactTokens(n: number): string {
70
88
  if (!Number.isFinite(n) || n <= 0) return "0";
71
89
  if (n >= 1_000_000) {
@@ -79,6 +97,28 @@ export function formatCompactTokens(n: number): string {
79
97
  return `${Math.round(n)}`;
80
98
  }
81
99
 
100
+ export function formatTurnTokensPerSecond(
101
+ outputTokens: number | undefined,
102
+ durationMs: number | undefined,
103
+ ): string | null {
104
+ if (
105
+ outputTokens == null ||
106
+ durationMs == null ||
107
+ !Number.isFinite(outputTokens) ||
108
+ !Number.isFinite(durationMs) ||
109
+ outputTokens <= 0 ||
110
+ durationMs <= 0
111
+ ) {
112
+ return null;
113
+ }
114
+ const rate = outputTokens / (durationMs / 1000);
115
+ if (!Number.isFinite(rate) || rate <= 0) return null;
116
+ if (rate < 10) {
117
+ return `${parseFloat(rate.toFixed(1))} tok/s`;
118
+ }
119
+ return `${Math.round(rate)} tok/s`;
120
+ }
121
+
82
122
  export function extractTokenMetrics(snap: SegmentSnapshot): TokenMetrics | null {
83
123
  const live = snap.data?.liveUsage;
84
124
  const last = snap.data?.lastTurn;
@@ -262,28 +302,38 @@ export function describeSegment(
262
302
  const prefix = def?.shortLabel ? `${def.shortLabel} ` : "";
263
303
  switch (item) {
264
304
  case "branch":
265
- return { icon, iconTone: "accent", text: data?.branch ?? "--", tone: "foreground" };
305
+ return {
306
+ icon,
307
+ iconTone: "accent",
308
+ text: firstNonBlank("--", data?.branch),
309
+ tone: "foreground",
310
+ };
266
311
  case "worktree":
267
312
  return {
268
313
  icon,
269
314
  iconTone: "accent",
270
- text: worktreeLocationText || "--",
315
+ text: firstNonBlank("--", worktreeLocationText),
271
316
  tone: "foreground",
272
317
  };
273
318
  case "agent_title":
274
- return { icon, iconTone: "accent", text: agent?.title ?? "Agent", tone: "foreground" };
319
+ return {
320
+ icon,
321
+ iconTone: "accent",
322
+ text: firstNonBlank("Agent", agent?.title),
323
+ tone: "foreground",
324
+ };
275
325
  case "agent":
276
326
  return {
277
327
  icon,
278
328
  iconTone: "accent",
279
- text: agent?.model || agent?.provider || "Agent",
329
+ text: firstNonBlank("Agent", agent?.model, agent?.provider),
280
330
  tone: "foreground",
281
331
  };
282
332
  case "agent_provider":
283
333
  return {
284
334
  icon,
285
335
  iconTone: "accent",
286
- text: agent?.provider ?? "Provider",
336
+ text: firstNonBlank("Provider", agent?.provider),
287
337
  tone: "foreground",
288
338
  };
289
339
  case "agent_activity": {
@@ -299,7 +349,7 @@ export function describeSegment(
299
349
  return {
300
350
  icon,
301
351
  iconTone: "accent",
302
- text: agentId && agentId.length > 7 ? agentId.slice(0, 7) : agentId ?? "--",
352
+ text: agentId && agentId.length > 7 ? agentId.slice(0, 7) : firstNonBlank("--", agentId),
303
353
  tone: "foreground",
304
354
  };
305
355
  case "load":
@@ -395,6 +445,70 @@ export function describeSegment(
395
445
  tone: "foreground",
396
446
  };
397
447
  }
448
+ case "velocity": {
449
+ // Pull velocity from liveUsage (real-time) or lastTurn (post-turn).
450
+ const tps =
451
+ data?.tokensPerSecond ?? data?.lastTurn?.tokensPerSecond;
452
+ const ctxPct =
453
+ data?.contextUtilizationPct ?? data?.lastTurn?.contextUtilizationPct;
454
+ const ctxLevel =
455
+ data?.contextPressureLevel ?? data?.lastTurn?.contextPressureLevel;
456
+
457
+ if (tps == null || !Number.isFinite(tps)) {
458
+ return {
459
+ icon,
460
+ iconTone: "none",
461
+ prefix,
462
+ prefixTone: "muted",
463
+ text: "--",
464
+ tone: "muted",
465
+ };
466
+ }
467
+
468
+ // Cache hit rate from liveUsage.
469
+ const live = data?.liveUsage;
470
+ let cachePct: number | undefined;
471
+ if (
472
+ live &&
473
+ live.inputTokens != null &&
474
+ live.cachedInputTokens != null &&
475
+ live.inputTokens > 0
476
+ ) {
477
+ cachePct = Math.round((live.cachedInputTokens / live.inputTokens) * 100);
478
+ }
479
+
480
+ // Build label: ⚡ N tok/s · M% cache
481
+ let text = `${Math.round(tps)} tok/s`;
482
+ let tone: SegmentTone = "success";
483
+ if (cachePct != null) {
484
+ text += ` · ${cachePct}% cache`;
485
+ }
486
+
487
+ // Context pressure badge as trailing.
488
+ let trailing: string | undefined;
489
+ let trailingTone: SegmentTone = "foreground";
490
+ if (ctxLevel === "high") {
491
+ trailing = "⚠️ high ctx";
492
+ trailingTone = "danger";
493
+ tone = "danger";
494
+ } else if (ctxLevel === "warning") {
495
+ trailing = "⚠️ warn ctx";
496
+ trailingTone = "warning";
497
+ tone = "warning";
498
+ } else if (ctxPct != null) {
499
+ trailing = `${ctxPct}% ctx`;
500
+ trailingTone = "foreground";
501
+ }
502
+
503
+ return {
504
+ icon,
505
+ iconTone: "accent",
506
+ text,
507
+ tone,
508
+ trailing,
509
+ trailingTone,
510
+ };
511
+ }
398
512
  case "cpu_ram":
399
513
  default: {
400
514
  const ram =
@@ -458,6 +572,9 @@ export function enabledItemsForSettings(settings: TopSettings): PillItemType[] {
458
572
  if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.turns)) {
459
573
  items.push("turns");
460
574
  }
575
+ if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.velocity)) {
576
+ items.push("velocity");
577
+ }
461
578
  return items;
462
579
  }
463
580