@xpufx/paseo-top 0.4.2 → 0.4.4

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 (43) hide show
  1. package/README.md +11 -12
  2. package/client/multi-host-view.tsx +177 -0
  3. package/client/multi-host.ts +98 -0
  4. package/client/pill-labels.ts +30 -6
  5. package/client/pill.tsx +42 -61
  6. package/client/settings-events.ts +33 -0
  7. package/client/surface.tsx +10 -9
  8. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +12 -0
  9. package/client/vendor/paseo-plugin-helper/components/Button.tsx +29 -3
  10. package/client/vendor/paseo-plugin-helper/components/Card.tsx +32 -0
  11. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +17 -1
  12. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +17 -2
  13. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +142 -0
  14. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +16 -2
  15. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +7 -0
  16. package/client/vendor/paseo-plugin-helper/components/Select.tsx +131 -63
  17. package/client/vendor/paseo-plugin-helper/components/index.ts +1 -0
  18. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +4 -3
  19. package/client/vendor/paseo-plugin-helper/host.ts +12 -58
  20. package/client/vendor/paseo-plugin-helper/index.ts +2 -0
  21. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +19 -29
  22. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +24 -22
  23. package/client/vendor/paseo-plugin-helper/panel.tsx +11 -3
  24. package/client/vendor/paseo-plugin-helper/pill.tsx +135 -339
  25. package/client/vendor/paseo-plugin-helper/styles/index.ts +1 -0
  26. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +550 -0
  27. package/client/vendor/paseo-plugin-helper/surface.tsx +62 -2
  28. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +3 -39
  29. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +40 -1
  30. package/client/vendor/paseo-plugin-helper/tickets.tsx +766 -0
  31. package/index.server.ts +7 -0
  32. package/package.json +12 -3
  33. package/paseo-plugin.json +1 -1
  34. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  35. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
  36. package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
  37. package/server/vendor/paseo-plugin-helper/tickets.ts +1281 -0
  38. package/shared/helper-version.ts +20 -0
  39. package/shared/multi-host.ts +348 -0
  40. package/shared/resources.ts +1 -1
  41. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  42. package/shared/vendor/paseo-plugin-helper/tickets.ts +1010 -0
  43. package/shared/version.ts +1 -1
@@ -0,0 +1,20 @@
1
+ // Declared paseo-plugin-helper expectation for top.
2
+ //
3
+ // This plugin carries a committed vendored helper tree, but its bare
4
+ // `paseo-plugin-helper/<tree>` imports still resolve through this checkout's
5
+ // tsconfig `paths` alias into ../../packages/paseo-plugin-helper/src, so part
6
+ // of what it runs comes from the checkout rather than from the copy it carries.
7
+ // Resolution is therefore "checkout", and this file records what that resolution is
8
+ // supposed to be, so scripts/helper-resolution.test.mjs can contradict it if the
9
+ // two ever disagree (#633, #649).
10
+ //
11
+ // HELPER_VERSION is the helper's own package.json `version` — the same value the
12
+ // vendored READMEs pin, not a scheme invented for this plugin.
13
+ // HELPER_REVISION is a content digest of packages/paseo-plugin-helper/src at
14
+ // stamp time. It is the authoritative check, and it is preferred over the
15
+ // checkout sha in shared/version.ts because a squash-merge deletes the very
16
+ // commit that sha names, orphaning it in every clean clone while it still
17
+ // resolves on any machine holding the branch worktree.
18
+ export const HELPER_VERSION = "0.4.0-beta.12";
19
+ export const HELPER_SERVED_FROM = "checkout";
20
+ export const HELPER_REVISION = "sha256:8ef1cb13c04a";
@@ -0,0 +1,348 @@
1
+ import { TimeoutError, withTimeout } from "./vendor/paseo-plugin-helper/index";
2
+
3
+ /**
4
+ * Bounded multi-host fleet polling state machine.
5
+ *
6
+ * Pure, framework-free logic shared by the client hook and exercised directly
7
+ * by unit tests. It owns the exact contract from #299: at most one in-flight
8
+ * probe per host, a slower cadence than the local five-second poll, a
9
+ * per-host timeout that marks the host `stale` with a timestamp, and
10
+ * dispose/reacquire of the borrowed host client across connection
11
+ * transitions.
12
+ *
13
+ * It deliberately does not model CPU/RAM/disk gauges: the Paseo plugin SDK
14
+ * exposes no host-metrics RPC on a borrowed host client (verified against
15
+ * 0.9.0 and 0.9.2), and fabricating them would be mock data. Only counts are
16
+ * aggregated (never summed/averaged gauges), matching the #299 rule.
17
+ */
18
+
19
+ export type HostConnectionStatus =
20
+ | "idle"
21
+ | "connecting"
22
+ | "online"
23
+ | "offline"
24
+ | "error";
25
+
26
+ export type FleetHostStatus = "online" | "stale" | "offline" | "error";
27
+
28
+ export interface FleetHostInput {
29
+ serverId: string;
30
+ label: string;
31
+ status: HostConnectionStatus;
32
+ }
33
+
34
+ /** Cross-host counts. These are the only fleet-additive metrics. */
35
+ export interface FleetHostCounts {
36
+ agents: number;
37
+ activeAgents: number;
38
+ workspaces: number;
39
+ }
40
+
41
+ export interface FleetHostSnapshot {
42
+ serverId: string;
43
+ label: string;
44
+ connectionStatus: HostConnectionStatus;
45
+ status: FleetHostStatus;
46
+ /** Round-trip latency of the last successful probe, in milliseconds. */
47
+ latencyMs: number | null;
48
+ /** Epoch ms of the last successful probe. */
49
+ lastUpdatedAt: number | null;
50
+ /** Epoch ms when the host was first marked stale; cleared on recovery. */
51
+ staleAt: number | null;
52
+ error: string | null;
53
+ counts: FleetHostCounts | null;
54
+ }
55
+
56
+ /**
57
+ * A borrowed host client. Mirrors the structural slice of `PaseoApi` the
58
+ * fleet needs, so `getPaseoClient(serverId)` satisfies it without importing
59
+ * the Paseo SDK types into shared code.
60
+ */
61
+ export interface FleetHostClient {
62
+ readCounts(): Promise<FleetHostCounts>;
63
+ dispose(): Promise<void> | void;
64
+ }
65
+
66
+ export interface MultiHostPollerOptions {
67
+ acquire(serverId: string): FleetHostClient;
68
+ /** Per-host request timeout. Default 4000ms (#299). */
69
+ timeoutMs?: number;
70
+ /** Polling cadence. Default 15000ms, slower than the local 5s poll (#299). */
71
+ intervalMs?: number;
72
+ now?: () => number;
73
+ onUpdate?: (snapshots: FleetHostSnapshot[]) => void;
74
+ }
75
+
76
+ export interface FleetAggregate {
77
+ hosts: number;
78
+ online: number;
79
+ stale: number;
80
+ offline: number;
81
+ error: number;
82
+ /** Hosts currently returning real data (online or stale). */
83
+ responsive: number;
84
+ /** Sum of counts across responsive hosts. Gauges are never aggregated. */
85
+ counts: FleetHostCounts;
86
+ }
87
+
88
+ function emptyCounts(): FleetHostCounts {
89
+ return { agents: 0, activeAgents: 0, workspaces: 0 };
90
+ }
91
+
92
+ function fleetStatusFor(connection: HostConnectionStatus): FleetHostStatus {
93
+ if (connection === "online") return "online";
94
+ if (connection === "error") return "error";
95
+ return "offline";
96
+ }
97
+
98
+ function errorMessage(cause: unknown): string {
99
+ return cause instanceof Error ? cause.message : String(cause);
100
+ }
101
+
102
+ export function sortFleetSnapshots(
103
+ snapshots: readonly FleetHostSnapshot[],
104
+ ): FleetHostSnapshot[] {
105
+ return [...snapshots].sort((a, b) => {
106
+ const byLabel = a.label.localeCompare(b.label);
107
+ return byLabel !== 0 ? byLabel : a.serverId.localeCompare(b.serverId);
108
+ });
109
+ }
110
+
111
+ /** Sum counts only; hardware gauges are per-host and never aggregated. */
112
+ export function aggregateFleet(
113
+ snapshots: readonly FleetHostSnapshot[],
114
+ ): FleetAggregate {
115
+ const aggregate: FleetAggregate = {
116
+ hosts: snapshots.length,
117
+ online: 0,
118
+ stale: 0,
119
+ offline: 0,
120
+ error: 0,
121
+ responsive: 0,
122
+ counts: emptyCounts(),
123
+ };
124
+ for (const snapshot of snapshots) {
125
+ aggregate[snapshot.status] += 1;
126
+ if (snapshot.counts) {
127
+ aggregate.responsive += 1;
128
+ aggregate.counts.agents += snapshot.counts.agents;
129
+ aggregate.counts.activeAgents += snapshot.counts.activeAgents;
130
+ aggregate.counts.workspaces += snapshot.counts.workspaces;
131
+ }
132
+ }
133
+ return aggregate;
134
+ }
135
+
136
+ export const DEFAULT_MULTI_HOST_INTERVAL_MS = 15_000;
137
+ export const DEFAULT_MULTI_HOST_TIMEOUT_MS = 4_000;
138
+
139
+ export class MultiHostPoller {
140
+ static get DEFAULT_INTERVAL_MS(): number {
141
+ return DEFAULT_MULTI_HOST_INTERVAL_MS;
142
+ }
143
+ static get DEFAULT_TIMEOUT_MS(): number {
144
+ return DEFAULT_MULTI_HOST_TIMEOUT_MS;
145
+ }
146
+
147
+ private readonly options: Required<
148
+ Pick<MultiHostPollerOptions, "timeoutMs" | "intervalMs" | "now">
149
+ > &
150
+ Pick<MultiHostPollerOptions, "acquire" | "onUpdate">;
151
+ private readonly snapshots = new Map<string, FleetHostSnapshot>();
152
+ private readonly clients = new Map<string, FleetHostClient>();
153
+ private readonly inFlight = new Set<string>();
154
+ /** Bumped on any connection transition so in-flight results are discarded. */
155
+ private readonly generation = new Map<string, number>();
156
+ private timer: ReturnType<typeof setInterval> | null = null;
157
+ private running = false;
158
+ private tickInFlight = false;
159
+ private disposed = false;
160
+
161
+ constructor(options: MultiHostPollerOptions) {
162
+ this.options = {
163
+ timeoutMs: options.timeoutMs ?? DEFAULT_MULTI_HOST_TIMEOUT_MS,
164
+ intervalMs: options.intervalMs ?? DEFAULT_MULTI_HOST_INTERVAL_MS,
165
+ now: options.now ?? (() => Date.now()),
166
+ acquire: options.acquire,
167
+ onUpdate: options.onUpdate,
168
+ };
169
+ }
170
+
171
+ /** Reconcile the live host list, releasing clients across transitions. */
172
+ setHosts(hosts: readonly FleetHostInput[]): void {
173
+ if (this.disposed) return;
174
+ const next = new Map(hosts.map((host) => [host.serverId, host]));
175
+
176
+ for (const serverId of [...this.snapshots.keys()]) {
177
+ if (!next.has(serverId)) {
178
+ this.bumpGeneration(serverId);
179
+ this.disposeClient(serverId);
180
+ this.snapshots.delete(serverId);
181
+ this.inFlight.delete(serverId);
182
+ }
183
+ }
184
+
185
+ for (const host of next.values()) {
186
+ const existing = this.snapshots.get(host.serverId);
187
+ if (!existing) {
188
+ this.snapshots.set(host.serverId, {
189
+ serverId: host.serverId,
190
+ label: host.label,
191
+ connectionStatus: host.status,
192
+ status: fleetStatusFor(host.status),
193
+ latencyMs: null,
194
+ lastUpdatedAt: null,
195
+ staleAt: null,
196
+ error: null,
197
+ counts: null,
198
+ });
199
+ continue;
200
+ }
201
+ const transitioned = existing.connectionStatus !== host.status;
202
+ if (transitioned) {
203
+ // A status change (including reconnect/failover) invalidates the
204
+ // borrowed client; reacquire on the next probe.
205
+ this.bumpGeneration(host.serverId);
206
+ this.disposeClient(host.serverId);
207
+ existing.connectionStatus = host.status;
208
+ existing.status = fleetStatusFor(host.status);
209
+ if (host.status !== "online") {
210
+ // Disconnected hosts keep no counts; only a `stale` probe retains
211
+ // its last-known snapshot as a deliberate "unresponsive" state.
212
+ existing.staleAt = null;
213
+ existing.counts = null;
214
+ }
215
+ }
216
+ existing.label = host.label;
217
+ }
218
+
219
+ this.emit();
220
+ }
221
+
222
+ /** Start the immediate poll plus the fixed-cadence loop. */
223
+ start(): void {
224
+ if (this.disposed || this.running) return;
225
+ this.running = true;
226
+ void this.pollOnce();
227
+ this.timer = setInterval(() => void this.pollOnce(), this.options.intervalMs);
228
+ }
229
+
230
+ stop(): void {
231
+ this.running = false;
232
+ if (this.timer !== null) {
233
+ clearInterval(this.timer);
234
+ this.timer = null;
235
+ }
236
+ }
237
+
238
+ /** Run one bounded pass over all hosts. Safe to await in tests. */
239
+ async pollOnce(): Promise<void> {
240
+ if (this.disposed || this.tickInFlight) return;
241
+ this.tickInFlight = true;
242
+ try {
243
+ await Promise.all(
244
+ [...this.snapshots.keys()].map((serverId) => this.probeHost(serverId)),
245
+ );
246
+ } finally {
247
+ this.tickInFlight = false;
248
+ this.emit();
249
+ }
250
+ }
251
+
252
+ getSnapshots(): FleetHostSnapshot[] {
253
+ return sortFleetSnapshots([...this.snapshots.values()]);
254
+ }
255
+
256
+ dispose(): void {
257
+ if (this.disposed) return;
258
+ this.disposed = true;
259
+ this.stop();
260
+ for (const serverId of [...this.clients.keys()]) {
261
+ this.disposeClient(serverId);
262
+ }
263
+ this.snapshots.clear();
264
+ this.inFlight.clear();
265
+ }
266
+
267
+ private async probeHost(serverId: string): Promise<void> {
268
+ const snapshot = this.snapshots.get(serverId);
269
+ if (!snapshot) return;
270
+
271
+ if (snapshot.connectionStatus !== "online") {
272
+ snapshot.status = fleetStatusFor(snapshot.connectionStatus);
273
+ snapshot.error =
274
+ snapshot.connectionStatus === "error" ? "Host connection error" : null;
275
+ return;
276
+ }
277
+ if (this.inFlight.has(serverId)) return;
278
+
279
+ this.inFlight.add(serverId);
280
+ const generation = this.currentGeneration(serverId);
281
+ const startedAt = this.options.now();
282
+ let timedOut = false;
283
+ try {
284
+ const client = this.acquireClient(serverId);
285
+ const counts = await withTimeout(
286
+ client.readCounts(),
287
+ this.options.timeoutMs,
288
+ `fleet probe ${snapshot.label}`,
289
+ );
290
+ if (this.currentGeneration(serverId) !== generation) return;
291
+ snapshot.status = "online";
292
+ snapshot.counts = counts;
293
+ snapshot.latencyMs = this.options.now() - startedAt;
294
+ snapshot.lastUpdatedAt = this.options.now();
295
+ snapshot.staleAt = null;
296
+ snapshot.error = null;
297
+ } catch (cause) {
298
+ if (this.currentGeneration(serverId) !== generation) return;
299
+ timedOut = cause instanceof TimeoutError;
300
+ if (timedOut) {
301
+ // Unresponsive: mark stale with a timestamp and keep the borrowed
302
+ // client. The next 15s tick is the only retry.
303
+ snapshot.status = "stale";
304
+ snapshot.staleAt = this.options.now();
305
+ snapshot.error = "Timed out";
306
+ } else {
307
+ // A released/replaced connection surfaces here; drop the client so the
308
+ // next probe reacquires a fresh one.
309
+ snapshot.status = "error";
310
+ snapshot.error = errorMessage(cause);
311
+ this.disposeClient(serverId);
312
+ }
313
+ } finally {
314
+ this.inFlight.delete(serverId);
315
+ }
316
+ }
317
+
318
+ private acquireClient(serverId: string): FleetHostClient {
319
+ const existing = this.clients.get(serverId);
320
+ if (existing) return existing;
321
+ const client = this.options.acquire(serverId);
322
+ this.clients.set(serverId, client);
323
+ return client;
324
+ }
325
+
326
+ private disposeClient(serverId: string): void {
327
+ const client = this.clients.get(serverId);
328
+ if (!client) return;
329
+ this.clients.delete(serverId);
330
+ try {
331
+ void client.dispose();
332
+ } catch {
333
+ // A released borrowed client may already be disposed; ignore.
334
+ }
335
+ }
336
+
337
+ private currentGeneration(serverId: string): number {
338
+ return this.generation.get(serverId) ?? 0;
339
+ }
340
+
341
+ private bumpGeneration(serverId: string): void {
342
+ this.generation.set(serverId, this.currentGeneration(serverId) + 1);
343
+ }
344
+
345
+ private emit(): void {
346
+ this.options.onUpdate?.(this.getSnapshots());
347
+ }
348
+ }
@@ -654,7 +654,7 @@ export const TopSettingsSchema = z.preprocess(
654
654
  recordTurnTelemetry: z.boolean().optional(),
655
655
  timelineCadence: z.number().min(0).max(10).default(1),
656
656
  intervalSeconds: z.number().min(1).max(60).default(3),
657
- defaultTab: z.enum(["system", "context", "settings", "about"]).default("system"),
657
+ defaultTab: z.enum(["system", "context", "permissions", "settings", "about"]).default("system"),
658
658
  })
659
659
  );
660
660
 
@@ -8,3 +8,4 @@ export * from "./suppressed";
8
8
  export * from "./custom-pills";
9
9
  export * from "./forge";
10
10
  export * from "./highlight";
11
+ export * from "./tickets";