@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.
- package/OVERVIEW.md +18 -0
- package/client/host-ui.tsx +184 -0
- package/client/multi-host-view.tsx +57 -43
- package/client/multi-host.ts +36 -13
- package/client/pill-labels.ts +94 -1
- package/client/pill.tsx +692 -616
- package/client/resources-query.ts +1 -1
- package/client/settings-events.ts +1 -1
- package/client/settings-ui.tsx +86 -9
- package/client/surface.tsx +308 -276
- package/client/telemetry-copy.ts +8 -2
- package/client/telemetry.tsx +372 -321
- package/client/turn-panel.tsx +126 -104
- package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +9 -3
- package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
- package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
- package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
- package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
- package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
- package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
- package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
- package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
- package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +51 -18
- package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
- package/client/vendor/paseo-plugin-helper/{surface.tsx → lifecycle/surface.tsx} +6 -6
- package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
- package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
- package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
- package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +50 -58
- package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
- package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
- package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
- package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
- package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
- package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
- package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
- package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
- package/client/vendor/permission-audit/client.tsx +706 -0
- package/client/vendor/permission-audit/index.tsx +1 -0
- package/index.client.tsx +58 -37
- package/index.server.ts +256 -234
- package/npm-shrinkwrap.json +44 -0
- package/package.json +8 -4
- package/paseo-plugin.json +2 -0
- package/screenshots/1-paseo-top-modal-system.jpg +0 -0
- package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
- package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
- package/screenshots/4-paseo-top-modal-about.jpg +0 -0
- package/screenshots/paseo-top-dashboard.jpg +0 -0
- package/screenshots/paseo-top-general-view.jpg +0 -0
- package/server/resources.ts +98 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
- package/server/vendor/paseo-plugin-helper/index.ts +2 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
- package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
- package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
- package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
- package/server/vendor/permission-audit/index.ts +1 -0
- package/server/vendor/permission-audit/server.ts +839 -0
- package/shared/helper-version.ts +1 -1
- package/shared/multi-host.ts +153 -159
- package/shared/resources.ts +145 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
- package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
- package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
- package/shared/vendor/permission-audit/filter.ts +61 -0
- package/shared/vendor/permission-audit/index.ts +2 -0
- package/shared/vendor/permission-audit/shared.ts +119 -0
- package/shared/version.ts +1 -1
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
- package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
- package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
- package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/index.ts +0 -30
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
- package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
- package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
- package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
- package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
- /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
- /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
- /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
package/shared/helper-version.ts
CHANGED
|
@@ -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:
|
|
20
|
+
export const HELPER_REVISION = "sha256:8ac23e85d016";
|
package/shared/multi-host.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
268
|
-
const snapshot =
|
|
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 (
|
|
212
|
+
if (inFlight.has(serverId)) return;
|
|
278
213
|
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
const startedAt =
|
|
282
|
-
let timedOut = false;
|
|
214
|
+
inFlight.add(serverId);
|
|
215
|
+
const startGeneration = currentGeneration(serverId);
|
|
216
|
+
const startedAt = options.now();
|
|
283
217
|
try {
|
|
284
|
-
const client =
|
|
218
|
+
const client = acquireClient(serverId);
|
|
285
219
|
const counts = await withTimeout(
|
|
286
220
|
client.readCounts(),
|
|
287
|
-
|
|
221
|
+
options.timeoutMs,
|
|
288
222
|
`fleet probe ${snapshot.label}`,
|
|
289
223
|
);
|
|
290
|
-
if (
|
|
224
|
+
if (currentGeneration(serverId) !== startGeneration) return;
|
|
291
225
|
snapshot.status = "online";
|
|
292
226
|
snapshot.counts = counts;
|
|
293
|
-
snapshot.latencyMs =
|
|
294
|
-
snapshot.lastUpdatedAt =
|
|
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 (
|
|
299
|
-
|
|
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 =
|
|
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
|
-
|
|
244
|
+
disposeClient(serverId);
|
|
312
245
|
}
|
|
313
246
|
} finally {
|
|
314
|
-
|
|
247
|
+
inFlight.delete(serverId);
|
|
315
248
|
}
|
|
316
249
|
}
|
|
317
250
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
255
|
+
await Promise.all([...snapshots.keys()].map((serverId) => probeHost(serverId)));
|
|
256
|
+
} finally {
|
|
257
|
+
tickInFlight = false;
|
|
258
|
+
emit();
|
|
334
259
|
}
|
|
335
260
|
}
|
|
336
261
|
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
}
|
package/shared/resources.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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(
|
|
31
|
+
reject(createTimeoutError(`${label} timed out after ${timeoutMs}ms`, timeoutMs));
|
|
25
32
|
}, timeoutMs);
|
|
26
33
|
});
|
|
27
34
|
|