@xpufx/paseo-top 0.4.4 → 0.4.7

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 +18 -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 +94 -1
  6. package/client/pill.tsx +692 -616
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +1 -1
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +308 -276
  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} +9 -3
  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} +51 -18
  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/{surface.tsx → lifecycle/surface.tsx} +6 -6
  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} +50 -58
  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 -234
  48. package/npm-shrinkwrap.json +44 -0
  49. package/package.json +8 -4
  50. package/paseo-plugin.json +2 -0
  51. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  52. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  53. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  54. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  55. package/screenshots/paseo-top-dashboard.jpg +0 -0
  56. package/screenshots/paseo-top-general-view.jpg +0 -0
  57. package/server/resources.ts +98 -0
  58. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  59. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  60. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  61. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  62. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  63. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  64. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  65. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  66. package/server/vendor/permission-audit/index.ts +1 -0
  67. package/server/vendor/permission-audit/server.ts +839 -0
  68. package/shared/helper-version.ts +1 -1
  69. package/shared/multi-host.ts +153 -159
  70. package/shared/resources.ts +145 -0
  71. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  72. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  73. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  74. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  75. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  76. package/shared/vendor/permission-audit/filter.ts +61 -0
  77. package/shared/vendor/permission-audit/index.ts +2 -0
  78. package/shared/vendor/permission-audit/shared.ts +119 -0
  79. package/shared/version.ts +1 -1
  80. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  81. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  82. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  83. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  84. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  85. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  86. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  87. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  88. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  89. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  90. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  91. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  92. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  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 -132
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  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 -26
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  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 -30
  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 -368
  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/theme/flair.ts +0 -76
  121. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  122. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  123. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  124. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  125. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  126. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  127. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  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
@@ -17,4 +17,4 @@
17
17
  // resolves on any machine holding the branch worktree.
18
18
  export const HELPER_VERSION = "0.4.0-beta.12";
19
19
  export const HELPER_SERVED_FROM = "checkout";
20
- export const HELPER_REVISION = "sha256:8ef1cb13c04a";
20
+ export const HELPER_REVISION = "sha256:8ac23e85d016";
@@ -1,4 +1,4 @@
1
- import { TimeoutError, withTimeout } from "./vendor/paseo-plugin-helper/index";
1
+ import { isTimeoutError, withTimeout } from "./vendor/paseo-plugin-helper/index";
2
2
 
3
3
  /**
4
4
  * Bounded multi-host fleet polling state machine.
@@ -136,136 +136,71 @@ export function aggregateFleet(
136
136
  export const DEFAULT_MULTI_HOST_INTERVAL_MS = 15_000;
137
137
  export const DEFAULT_MULTI_HOST_TIMEOUT_MS = 4_000;
138
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
- }
139
+ export interface MultiHostPoller {
140
+ setHosts(hosts: readonly FleetHostInput[]): void;
141
+ start(): void;
142
+ stop(): void;
143
+ pollOnce(): Promise<void>;
144
+ getSnapshots(): FleetHostSnapshot[];
145
+ dispose(): void;
146
+ }
146
147
 
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>();
148
+ /**
149
+ * Closure-based on purpose: plugin client bundles are evaluated by Hermes from
150
+ * source, where a `class` expression binds as `undefined`. A class here made the
151
+ * Fleet tab crash with "Cannot read property 'prototype' of undefined"
152
+ * (#1066, #510). Never use `class` in client-reachable code.
153
+ */
154
+ export function createMultiHostPoller(opts: MultiHostPollerOptions): MultiHostPoller {
155
+ const options = {
156
+ timeoutMs: opts.timeoutMs ?? DEFAULT_MULTI_HOST_TIMEOUT_MS,
157
+ intervalMs: opts.intervalMs ?? DEFAULT_MULTI_HOST_INTERVAL_MS,
158
+ now: opts.now ?? (() => Date.now()),
159
+ acquire: opts.acquire,
160
+ onUpdate: opts.onUpdate,
161
+ };
162
+ const snapshots = new Map<string, FleetHostSnapshot>();
163
+ const clients = new Map<string, FleetHostClient>();
164
+ const inFlight = new Set<string>();
154
165
  /** 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
- };
166
+ const generation = new Map<string, number>();
167
+ let timer: ReturnType<typeof setInterval> | null = null;
168
+ let running = false;
169
+ let tickInFlight = false;
170
+ let disposed = false;
171
+
172
+ function currentGeneration(serverId: string): number {
173
+ return generation.get(serverId) ?? 0;
169
174
  }
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();
175
+ function bumpGeneration(serverId: string): void {
176
+ generation.set(serverId, currentGeneration(serverId) + 1);
220
177
  }
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);
178
+ function getSnapshots(): FleetHostSnapshot[] {
179
+ return sortFleetSnapshots([...snapshots.values()]);
228
180
  }
229
-
230
- stop(): void {
231
- this.running = false;
232
- if (this.timer !== null) {
233
- clearInterval(this.timer);
234
- this.timer = null;
235
- }
181
+ function emit(): void {
182
+ options.onUpdate?.(getSnapshots());
236
183
  }
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()]);
184
+ function acquireClient(serverId: string): FleetHostClient {
185
+ const existing = clients.get(serverId);
186
+ if (existing) return existing;
187
+ const client = options.acquire(serverId);
188
+ clients.set(serverId, client);
189
+ return client;
254
190
  }
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);
191
+ function disposeClient(serverId: string): void {
192
+ const client = clients.get(serverId);
193
+ if (!client) return;
194
+ clients.delete(serverId);
195
+ try {
196
+ void client.dispose();
197
+ } catch {
198
+ // A released borrowed client may already be disposed; ignore.
262
199
  }
263
- this.snapshots.clear();
264
- this.inFlight.clear();
265
200
  }
266
201
 
267
- private async probeHost(serverId: string): Promise<void> {
268
- const snapshot = this.snapshots.get(serverId);
202
+ async function probeHost(serverId: string): Promise<void> {
203
+ const snapshot = snapshots.get(serverId);
269
204
  if (!snapshot) return;
270
205
 
271
206
  if (snapshot.connectionStatus !== "online") {
@@ -274,75 +209,134 @@ export class MultiHostPoller {
274
209
  snapshot.connectionStatus === "error" ? "Host connection error" : null;
275
210
  return;
276
211
  }
277
- if (this.inFlight.has(serverId)) return;
212
+ if (inFlight.has(serverId)) return;
278
213
 
279
- this.inFlight.add(serverId);
280
- const generation = this.currentGeneration(serverId);
281
- const startedAt = this.options.now();
282
- let timedOut = false;
214
+ inFlight.add(serverId);
215
+ const startGeneration = currentGeneration(serverId);
216
+ const startedAt = options.now();
283
217
  try {
284
- const client = this.acquireClient(serverId);
218
+ const client = acquireClient(serverId);
285
219
  const counts = await withTimeout(
286
220
  client.readCounts(),
287
- this.options.timeoutMs,
221
+ options.timeoutMs,
288
222
  `fleet probe ${snapshot.label}`,
289
223
  );
290
- if (this.currentGeneration(serverId) !== generation) return;
224
+ if (currentGeneration(serverId) !== startGeneration) return;
291
225
  snapshot.status = "online";
292
226
  snapshot.counts = counts;
293
- snapshot.latencyMs = this.options.now() - startedAt;
294
- snapshot.lastUpdatedAt = this.options.now();
227
+ snapshot.latencyMs = options.now() - startedAt;
228
+ snapshot.lastUpdatedAt = options.now();
295
229
  snapshot.staleAt = null;
296
230
  snapshot.error = null;
297
231
  } catch (cause) {
298
- if (this.currentGeneration(serverId) !== generation) return;
299
- timedOut = cause instanceof TimeoutError;
300
- if (timedOut) {
232
+ if (currentGeneration(serverId) !== startGeneration) return;
233
+ if (isTimeoutError(cause)) {
301
234
  // Unresponsive: mark stale with a timestamp and keep the borrowed
302
235
  // client. The next 15s tick is the only retry.
303
236
  snapshot.status = "stale";
304
- snapshot.staleAt = this.options.now();
237
+ snapshot.staleAt = options.now();
305
238
  snapshot.error = "Timed out";
306
239
  } else {
307
240
  // A released/replaced connection surfaces here; drop the client so the
308
241
  // next probe reacquires a fresh one.
309
242
  snapshot.status = "error";
310
243
  snapshot.error = errorMessage(cause);
311
- this.disposeClient(serverId);
244
+ disposeClient(serverId);
312
245
  }
313
246
  } finally {
314
- this.inFlight.delete(serverId);
247
+ inFlight.delete(serverId);
315
248
  }
316
249
  }
317
250
 
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);
251
+ async function pollOnce(): Promise<void> {
252
+ if (disposed || tickInFlight) return;
253
+ tickInFlight = true;
330
254
  try {
331
- void client.dispose();
332
- } catch {
333
- // A released borrowed client may already be disposed; ignore.
255
+ await Promise.all([...snapshots.keys()].map((serverId) => probeHost(serverId)));
256
+ } finally {
257
+ tickInFlight = false;
258
+ emit();
334
259
  }
335
260
  }
336
261
 
337
- private currentGeneration(serverId: string): number {
338
- return this.generation.get(serverId) ?? 0;
339
- }
262
+ function setHosts(hosts: readonly FleetHostInput[]): void {
263
+ if (disposed) return;
264
+ const next = new Map(hosts.map((host) => [host.serverId, host]));
265
+
266
+ for (const serverId of [...snapshots.keys()]) {
267
+ if (!next.has(serverId)) {
268
+ bumpGeneration(serverId);
269
+ disposeClient(serverId);
270
+ snapshots.delete(serverId);
271
+ inFlight.delete(serverId);
272
+ }
273
+ }
340
274
 
341
- private bumpGeneration(serverId: string): void {
342
- this.generation.set(serverId, this.currentGeneration(serverId) + 1);
275
+ for (const host of next.values()) {
276
+ const existing = snapshots.get(host.serverId);
277
+ if (!existing) {
278
+ snapshots.set(host.serverId, {
279
+ serverId: host.serverId,
280
+ label: host.label,
281
+ connectionStatus: host.status,
282
+ status: fleetStatusFor(host.status),
283
+ latencyMs: null,
284
+ lastUpdatedAt: null,
285
+ staleAt: null,
286
+ error: null,
287
+ counts: null,
288
+ });
289
+ continue;
290
+ }
291
+ const transitioned = existing.connectionStatus !== host.status;
292
+ if (transitioned) {
293
+ // A status change (including reconnect/failover) invalidates the
294
+ // borrowed client; reacquire on the next probe.
295
+ bumpGeneration(host.serverId);
296
+ disposeClient(host.serverId);
297
+ existing.connectionStatus = host.status;
298
+ existing.status = fleetStatusFor(host.status);
299
+ if (host.status !== "online") {
300
+ // Disconnected hosts keep no counts; only a `stale` probe retains
301
+ // its last-known snapshot as a deliberate "unresponsive" state.
302
+ existing.staleAt = null;
303
+ existing.counts = null;
304
+ }
305
+ }
306
+ existing.label = host.label;
307
+ }
308
+
309
+ emit();
343
310
  }
344
311
 
345
- private emit(): void {
346
- this.options.onUpdate?.(this.getSnapshots());
312
+ function stop(): void {
313
+ running = false;
314
+ if (timer !== null) {
315
+ clearInterval(timer);
316
+ timer = null;
317
+ }
347
318
  }
319
+
320
+ return {
321
+ setHosts,
322
+ /** Start the immediate poll plus the fixed-cadence loop. */
323
+ start() {
324
+ if (disposed || running) return;
325
+ running = true;
326
+ void pollOnce();
327
+ timer = setInterval(() => void pollOnce(), options.intervalMs);
328
+ },
329
+ stop,
330
+ /** Run one bounded pass over all hosts. Safe to await in tests. */
331
+ pollOnce,
332
+ getSnapshots,
333
+ dispose() {
334
+ if (disposed) return;
335
+ disposed = true;
336
+ stop();
337
+ for (const serverId of [...clients.keys()]) disposeClient(serverId);
338
+ snapshots.clear();
339
+ inFlight.clear();
340
+ },
341
+ };
348
342
  }
@@ -3,6 +3,9 @@ import {
3
3
  defineSettingsContract,
4
4
  type RpcOutput,
5
5
  type CustomPillState,
6
+ type MetricThresholds,
7
+ formatBytes,
8
+ formatUptime,
6
9
  } from "./vendor/paseo-plugin-helper/index";
7
10
  import { z } from "zod";
8
11
 
@@ -142,6 +145,12 @@ export const topTimelineTelemetrySchema = z.object({
142
145
  contextUsedTokens: z.number().optional(),
143
146
  contextMaxTokens: z.number().optional(),
144
147
  costUsd: z.number().optional(),
148
+ /** Tokens per second of generation (output + reasoning / durationSec). Zero for zero-duration or interrupted turns. */
149
+ tokensPerSecond: z.number().optional(),
150
+ /** Context pressure level: "normal" (<60%), "warning" (60-74%), "high" (>=75%). */
151
+ contextPressureLevel: z.enum(["normal", "warning", "high"]).optional(),
152
+ /** Context utilization percentage (0-100). */
153
+ contextUtilizationPct: z.number().optional(),
145
154
  });
146
155
  export type TopTimelineTelemetryData = z.infer<typeof topTimelineTelemetrySchema>;
147
156
 
@@ -177,6 +186,12 @@ export const getSystemResourcesRpc = defineContract({
177
186
  customPills: z.array(CustomPillStateSchema).optional(),
178
187
  lastTurn: topTimelineTelemetrySchema.nullable().optional(),
179
188
  liveUsage: liveUsageSchema.nullable().optional(),
189
+ /** Tokens per second of generation from the most recent turn. */
190
+ tokensPerSecond: z.number().optional(),
191
+ /** Context pressure level from the most recent turn. */
192
+ contextPressureLevel: z.enum(["normal", "warning", "high"]).optional(),
193
+ /** Context utilization percentage from the most recent turn. */
194
+ contextUtilizationPct: z.number().optional(),
180
195
  }),
181
196
  });
182
197
 
@@ -204,6 +219,7 @@ export const MetricIdSchema = z.enum([
204
219
  "tokens",
205
220
  "tools",
206
221
  "turns",
222
+ "velocity",
207
223
  ]);
208
224
  export type MetricId = z.infer<typeof MetricIdSchema>;
209
225
 
@@ -223,6 +239,7 @@ export const METRIC_IDS: readonly MetricId[] = [
223
239
  "tokens",
224
240
  "tools",
225
241
  "turns",
242
+ "velocity",
226
243
  ];
227
244
 
228
245
  export const CORE_TIMELINE_METRICS: readonly MetricId[] = [
@@ -233,6 +250,7 @@ export const CORE_TIMELINE_METRICS: readonly MetricId[] = [
233
250
  "agent_id",
234
251
  "changes",
235
252
  "tokens",
253
+ "velocity",
236
254
  ];
237
255
 
238
256
  export const DEFAULT_METRIC_SURFACES: Record<MetricId, SurfaceTarget> = {
@@ -251,6 +269,7 @@ export const DEFAULT_METRIC_SURFACES: Record<MetricId, SurfaceTarget> = {
251
269
  tokens: "timeline",
252
270
  tools: "timeline",
253
271
  turns: "none",
272
+ velocity: "both",
254
273
  };
255
274
 
256
275
  export interface MetricDefinition {
@@ -263,6 +282,12 @@ export interface MetricDefinition {
263
282
  pillOnly?: boolean;
264
283
  }
265
284
 
285
+ /** Context pressure thresholds matching the velocity pill's bloat logic. */
286
+ export const CONTEXT_PRESSURE_THRESHOLDS: Record<string, number> = {
287
+ normalUpper: 60,
288
+ warningUpper: 75,
289
+ };
290
+
266
291
  export const METRIC_DEFINITIONS: MetricDefinition[] = [
267
292
  {
268
293
  id: "cpu_ram",
@@ -366,6 +391,14 @@ export const METRIC_DEFINITIONS: MetricDefinition[] = [
366
391
  icon: "Repeat",
367
392
  shortLabel: "turns",
368
393
  },
394
+ {
395
+ id: "velocity",
396
+ title: "Generation Velocity",
397
+ description: "Live tokens-per-second tachometer with cache hit rate and context pressure",
398
+ icon: "Zap",
399
+ shortLabel: "tps",
400
+ core: true,
401
+ },
369
402
  ];
370
403
 
371
404
  export function isPillEnabled(target?: SurfaceTarget): boolean {
@@ -481,6 +514,7 @@ export const TIMELINE_RENDERED_METRICS: readonly MetricId[] = [
481
514
  "tokens",
482
515
  "tools",
483
516
  "turns",
517
+ "velocity",
484
518
  ];
485
519
 
486
520
  export interface LegacyFlagView {
@@ -549,6 +583,116 @@ export function legacyFlagView(settings: {
549
583
  };
550
584
  }
551
585
 
586
+ // --- Canonical metric thresholds -------------------------------------------
587
+
588
+ /**
589
+ * Single source of truth for CPU/RAM warning/danger color thresholds. The
590
+ * pill, the dashboard surface, and the timeline card all resolve through
591
+ * these constants so a metric never changes color depending on where it is
592
+ * displayed (pill.tsx and telemetry.tsx used to carry divergent copies).
593
+ */
594
+ export const CPU_THRESHOLDS: MetricThresholds = { warning: 60, danger: 85 };
595
+ export const MEM_THRESHOLDS: MetricThresholds = { warning: 70, danger: 85 };
596
+
597
+ // --- Tabs -------------------------------------------------------------------
598
+
599
+ export interface TopTabItem {
600
+ id: string;
601
+ label: string;
602
+ shortLabel: string;
603
+ icon: string;
604
+ }
605
+
606
+ const SYSTEM_TAB: TopTabItem = {
607
+ id: "system",
608
+ label: "System",
609
+ shortLabel: "System",
610
+ icon: "Activity",
611
+ };
612
+ const PERMISSIONS_TAB: TopTabItem = {
613
+ id: "permissions",
614
+ label: "Permissions",
615
+ shortLabel: "Permissions",
616
+ icon: "ShieldCheck",
617
+ };
618
+ const SETTINGS_TAB: TopTabItem = {
619
+ id: "settings",
620
+ label: "Settings",
621
+ shortLabel: "Settings",
622
+ icon: "Sliders",
623
+ };
624
+ const ABOUT_TAB: TopTabItem = { id: "about", label: "About", shortLabel: "About", icon: "Info" };
625
+
626
+ /** Sidebar dashboard tabs (client/surface.tsx). */
627
+ export const SURFACE_TABS: readonly TopTabItem[] = [
628
+ { ...SYSTEM_TAB, label: "Activity", shortLabel: "Activity" },
629
+ { id: "fleet", label: "Fleet", shortLabel: "Fleet", icon: "Server" },
630
+ PERMISSIONS_TAB,
631
+ SETTINGS_TAB,
632
+ ABOUT_TAB,
633
+ ];
634
+
635
+ /** Pill modal tabs (client/pill.tsx). */
636
+ export const MODAL_TABS: readonly TopTabItem[] = [
637
+ SYSTEM_TAB,
638
+ { id: "context", label: "Workspace", shortLabel: "Workspace", icon: "GitBranch" },
639
+ PERMISSIONS_TAB,
640
+ SETTINGS_TAB,
641
+ ABOUT_TAB,
642
+ ];
643
+
644
+ // --- About section extras ---------------------------------------------------
645
+
646
+ /**
647
+ * Host facts for the About tab. Shared verbatim by the dashboard surface and
648
+ * the pill modal so the two About sections cannot drift.
649
+ */
650
+ export function aboutExtraItems(
651
+ data: SystemResources | undefined,
652
+ ): Array<{ label: string; value: string; copyable?: boolean }> {
653
+ return [
654
+ {
655
+ label: "Host Platform",
656
+ value: data?.platform ? `${data.platform} (${data.arch ?? "unknown"})` : "Linux",
657
+ copyable: true,
658
+ },
659
+ { label: "Host Name", value: data?.hostname ?? "localhost", copyable: true },
660
+ { label: "CPU Model", value: data?.cpuModel ?? "unknown", copyable: true },
661
+ { label: "CPU Cores", value: `${data?.cpuCores ?? 0} cores` },
662
+ {
663
+ label: "Total Memory",
664
+ value: data?.memoryTotalBytes ? formatBytes(data.memoryTotalBytes) : "unknown",
665
+ },
666
+ {
667
+ label: "Host Uptime",
668
+ value: data?.uptimeSeconds ? formatUptime(data.uptimeSeconds) : "unknown",
669
+ },
670
+ ];
671
+ }
672
+
673
+ // --- Enabled-flag OR chain ---------------------------------------------------
674
+
675
+ /**
676
+ * The legacy pill-flag OR chain, formerly copy-pasted at every call site that
677
+ * asks "is any pill item enabled" (pill view, modal settings, multiple-mode
678
+ * sync). MCP is presence-gated separately, so it is not part of the base
679
+ * chain — callers OR it in with their own MCP resolution.
680
+ */
681
+ export function hasAnyLegacyPillFlagEnabled(flags: LegacyFlagView): boolean {
682
+ return (
683
+ flags.showCpuRam ||
684
+ flags.showBranch ||
685
+ flags.showWorktree ||
686
+ flags.showAgentTitle ||
687
+ flags.showAgent ||
688
+ flags.showAgentProvider ||
689
+ flags.showAgentActivity ||
690
+ flags.showAgentId ||
691
+ flags.showLoad ||
692
+ flags.showUptime
693
+ );
694
+ }
695
+
552
696
  /**
553
697
  * Migrates legacy boolean toggles and single recordTurnTelemetry switch into the
554
698
  * per-metric SurfaceTarget model: Record<MetricId, "pill" | "timeline" | "both" | "none">.
@@ -603,6 +747,7 @@ export function migrateLegacyMetricSurfaces(raw: Record<string, unknown>): Recor
603
747
  tokens: false,
604
748
  tools: false,
605
749
  turns: false,
750
+ velocity: false,
606
751
  };
607
752
 
608
753
  const result: Record<MetricId, SurfaceTarget> = { ...DEFAULT_METRIC_SURFACES };
@@ -1,11 +1,18 @@
1
- export class TimeoutError extends Error {
1
+ // NOTE: no `class` here. Hermes eval of plugin client bundles binds class
2
+ // expressions as `undefined`, so errors are plain Error objects with a brand.
3
+ export interface TimeoutError extends Error {
2
4
  readonly timeoutMs: number;
5
+ }
3
6
 
4
- constructor(message: string, timeoutMs: number) {
5
- super(message);
6
- this.name = "TimeoutError";
7
- this.timeoutMs = timeoutMs;
8
- }
7
+ export function createTimeoutError(message: string, timeoutMs: number): TimeoutError {
8
+ const err = new Error(message) as Error & { timeoutMs: number };
9
+ err.name = "TimeoutError";
10
+ err.timeoutMs = timeoutMs;
11
+ return err;
12
+ }
13
+
14
+ export function isTimeoutError(e: unknown): e is TimeoutError {
15
+ return e instanceof Error && e.name === "TimeoutError";
9
16
  }
10
17
 
11
18
  /**
@@ -21,7 +28,7 @@ export async function withTimeout<T>(
21
28
  let timer: any;
22
29
  const timeoutPromise = new Promise<never>((_, reject) => {
23
30
  timer = setTimeout(() => {
24
- reject(new TimeoutError(`${label} timed out after ${timeoutMs}ms`, timeoutMs));
31
+ reject(createTimeoutError(`${label} timed out after ${timeoutMs}ms`, timeoutMs));
25
32
  }, timeoutMs);
26
33
  });
27
34