@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.
- package/OVERVIEW.md +17 -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 +124 -7
- package/client/pill.tsx +724 -669
- package/client/resources-query.ts +1 -1
- package/client/settings-events.ts +33 -0
- package/client/settings-ui.tsx +86 -9
- package/client/surface.tsx +310 -274
- 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} +21 -61
- 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} +118 -338
- 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/lifecycle/surface.tsx +140 -0
- 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} +55 -47
- 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 -227
- package/package.json +14 -2
- 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 +4 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
- package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
- 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 +1232 -0
- package/server/vendor/permission-audit/index.ts +1 -0
- package/server/vendor/permission-audit/server.ts +839 -0
- package/shared/helper-version.ts +20 -0
- package/shared/multi-host.ts +156 -155
- package/shared/resources.ts +146 -1
- 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 +2 -0
- package/shared/vendor/paseo-plugin-helper/tickets.ts +1104 -0
- 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 -156
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -184
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -225
- 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 -75
- 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 -118
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -80
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -215
- 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 -25
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -224
- 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 -29
- 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 -378
- 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/surface.tsx +0 -80
- 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/{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
|
@@ -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: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.
|
|
@@ -133,132 +133,74 @@ export function aggregateFleet(
|
|
|
133
133
|
return aggregate;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
export
|
|
137
|
-
|
|
138
|
-
static readonly DEFAULT_TIMEOUT_MS = 4_000;
|
|
136
|
+
export const DEFAULT_MULTI_HOST_INTERVAL_MS = 15_000;
|
|
137
|
+
export const DEFAULT_MULTI_HOST_TIMEOUT_MS = 4_000;
|
|
139
138
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
private readonly generation = new Map<string, number>();
|
|
149
|
-
private timer: ReturnType<typeof setInterval> | null = null;
|
|
150
|
-
private running = false;
|
|
151
|
-
private tickInFlight = false;
|
|
152
|
-
private disposed = false;
|
|
153
|
-
|
|
154
|
-
constructor(options: MultiHostPollerOptions) {
|
|
155
|
-
this.options = {
|
|
156
|
-
timeoutMs: options.timeoutMs ?? MultiHostPoller.DEFAULT_TIMEOUT_MS,
|
|
157
|
-
intervalMs: options.intervalMs ?? MultiHostPoller.DEFAULT_INTERVAL_MS,
|
|
158
|
-
now: options.now ?? (() => Date.now()),
|
|
159
|
-
acquire: options.acquire,
|
|
160
|
-
onUpdate: options.onUpdate,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Reconcile the live host list, releasing clients across transitions. */
|
|
165
|
-
setHosts(hosts: readonly FleetHostInput[]): void {
|
|
166
|
-
if (this.disposed) return;
|
|
167
|
-
const next = new Map(hosts.map((host) => [host.serverId, host]));
|
|
168
|
-
|
|
169
|
-
for (const serverId of [...this.snapshots.keys()]) {
|
|
170
|
-
if (!next.has(serverId)) {
|
|
171
|
-
this.bumpGeneration(serverId);
|
|
172
|
-
this.disposeClient(serverId);
|
|
173
|
-
this.snapshots.delete(serverId);
|
|
174
|
-
this.inFlight.delete(serverId);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
for (const host of next.values()) {
|
|
179
|
-
const existing = this.snapshots.get(host.serverId);
|
|
180
|
-
if (!existing) {
|
|
181
|
-
this.snapshots.set(host.serverId, {
|
|
182
|
-
serverId: host.serverId,
|
|
183
|
-
label: host.label,
|
|
184
|
-
connectionStatus: host.status,
|
|
185
|
-
status: fleetStatusFor(host.status),
|
|
186
|
-
latencyMs: null,
|
|
187
|
-
lastUpdatedAt: null,
|
|
188
|
-
staleAt: null,
|
|
189
|
-
error: null,
|
|
190
|
-
counts: null,
|
|
191
|
-
});
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
const transitioned = existing.connectionStatus !== host.status;
|
|
195
|
-
if (transitioned) {
|
|
196
|
-
// A status change (including reconnect/failover) invalidates the
|
|
197
|
-
// borrowed client; reacquire on the next probe.
|
|
198
|
-
this.bumpGeneration(host.serverId);
|
|
199
|
-
this.disposeClient(host.serverId);
|
|
200
|
-
existing.connectionStatus = host.status;
|
|
201
|
-
existing.status = fleetStatusFor(host.status);
|
|
202
|
-
if (host.status !== "online") {
|
|
203
|
-
// Disconnected hosts keep no counts; only a `stale` probe retains
|
|
204
|
-
// its last-known snapshot as a deliberate "unresponsive" state.
|
|
205
|
-
existing.staleAt = null;
|
|
206
|
-
existing.counts = null;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
existing.label = host.label;
|
|
210
|
-
}
|
|
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
|
+
}
|
|
211
147
|
|
|
212
|
-
|
|
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>();
|
|
165
|
+
/** Bumped on any connection transition so in-flight results are discarded. */
|
|
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;
|
|
213
174
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
start(): void {
|
|
217
|
-
if (this.disposed || this.running) return;
|
|
218
|
-
this.running = true;
|
|
219
|
-
void this.pollOnce();
|
|
220
|
-
this.timer = setInterval(() => void this.pollOnce(), this.options.intervalMs);
|
|
175
|
+
function bumpGeneration(serverId: string): void {
|
|
176
|
+
generation.set(serverId, currentGeneration(serverId) + 1);
|
|
221
177
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
this.running = false;
|
|
225
|
-
if (this.timer !== null) {
|
|
226
|
-
clearInterval(this.timer);
|
|
227
|
-
this.timer = null;
|
|
228
|
-
}
|
|
178
|
+
function getSnapshots(): FleetHostSnapshot[] {
|
|
179
|
+
return sortFleetSnapshots([...snapshots.values()]);
|
|
229
180
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
async pollOnce(): Promise<void> {
|
|
233
|
-
if (this.disposed || this.tickInFlight) return;
|
|
234
|
-
this.tickInFlight = true;
|
|
235
|
-
try {
|
|
236
|
-
await Promise.all(
|
|
237
|
-
[...this.snapshots.keys()].map((serverId) => this.probeHost(serverId)),
|
|
238
|
-
);
|
|
239
|
-
} finally {
|
|
240
|
-
this.tickInFlight = false;
|
|
241
|
-
this.emit();
|
|
242
|
-
}
|
|
181
|
+
function emit(): void {
|
|
182
|
+
options.onUpdate?.(getSnapshots());
|
|
243
183
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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;
|
|
247
190
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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.
|
|
255
199
|
}
|
|
256
|
-
this.snapshots.clear();
|
|
257
|
-
this.inFlight.clear();
|
|
258
200
|
}
|
|
259
201
|
|
|
260
|
-
|
|
261
|
-
const snapshot =
|
|
202
|
+
async function probeHost(serverId: string): Promise<void> {
|
|
203
|
+
const snapshot = snapshots.get(serverId);
|
|
262
204
|
if (!snapshot) return;
|
|
263
205
|
|
|
264
206
|
if (snapshot.connectionStatus !== "online") {
|
|
@@ -267,75 +209,134 @@ export class MultiHostPoller {
|
|
|
267
209
|
snapshot.connectionStatus === "error" ? "Host connection error" : null;
|
|
268
210
|
return;
|
|
269
211
|
}
|
|
270
|
-
if (
|
|
212
|
+
if (inFlight.has(serverId)) return;
|
|
271
213
|
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
const startedAt =
|
|
275
|
-
let timedOut = false;
|
|
214
|
+
inFlight.add(serverId);
|
|
215
|
+
const startGeneration = currentGeneration(serverId);
|
|
216
|
+
const startedAt = options.now();
|
|
276
217
|
try {
|
|
277
|
-
const client =
|
|
218
|
+
const client = acquireClient(serverId);
|
|
278
219
|
const counts = await withTimeout(
|
|
279
220
|
client.readCounts(),
|
|
280
|
-
|
|
221
|
+
options.timeoutMs,
|
|
281
222
|
`fleet probe ${snapshot.label}`,
|
|
282
223
|
);
|
|
283
|
-
if (
|
|
224
|
+
if (currentGeneration(serverId) !== startGeneration) return;
|
|
284
225
|
snapshot.status = "online";
|
|
285
226
|
snapshot.counts = counts;
|
|
286
|
-
snapshot.latencyMs =
|
|
287
|
-
snapshot.lastUpdatedAt =
|
|
227
|
+
snapshot.latencyMs = options.now() - startedAt;
|
|
228
|
+
snapshot.lastUpdatedAt = options.now();
|
|
288
229
|
snapshot.staleAt = null;
|
|
289
230
|
snapshot.error = null;
|
|
290
231
|
} catch (cause) {
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
if (timedOut) {
|
|
232
|
+
if (currentGeneration(serverId) !== startGeneration) return;
|
|
233
|
+
if (isTimeoutError(cause)) {
|
|
294
234
|
// Unresponsive: mark stale with a timestamp and keep the borrowed
|
|
295
235
|
// client. The next 15s tick is the only retry.
|
|
296
236
|
snapshot.status = "stale";
|
|
297
|
-
snapshot.staleAt =
|
|
237
|
+
snapshot.staleAt = options.now();
|
|
298
238
|
snapshot.error = "Timed out";
|
|
299
239
|
} else {
|
|
300
240
|
// A released/replaced connection surfaces here; drop the client so the
|
|
301
241
|
// next probe reacquires a fresh one.
|
|
302
242
|
snapshot.status = "error";
|
|
303
243
|
snapshot.error = errorMessage(cause);
|
|
304
|
-
|
|
244
|
+
disposeClient(serverId);
|
|
305
245
|
}
|
|
306
246
|
} finally {
|
|
307
|
-
|
|
247
|
+
inFlight.delete(serverId);
|
|
308
248
|
}
|
|
309
249
|
}
|
|
310
250
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const client = this.options.acquire(serverId);
|
|
315
|
-
this.clients.set(serverId, client);
|
|
316
|
-
return client;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
private disposeClient(serverId: string): void {
|
|
320
|
-
const client = this.clients.get(serverId);
|
|
321
|
-
if (!client) return;
|
|
322
|
-
this.clients.delete(serverId);
|
|
251
|
+
async function pollOnce(): Promise<void> {
|
|
252
|
+
if (disposed || tickInFlight) return;
|
|
253
|
+
tickInFlight = true;
|
|
323
254
|
try {
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
255
|
+
await Promise.all([...snapshots.keys()].map((serverId) => probeHost(serverId)));
|
|
256
|
+
} finally {
|
|
257
|
+
tickInFlight = false;
|
|
258
|
+
emit();
|
|
327
259
|
}
|
|
328
260
|
}
|
|
329
261
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
+
}
|
|
274
|
+
|
|
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
|
+
}
|
|
333
308
|
|
|
334
|
-
|
|
335
|
-
this.generation.set(serverId, this.currentGeneration(serverId) + 1);
|
|
309
|
+
emit();
|
|
336
310
|
}
|
|
337
311
|
|
|
338
|
-
|
|
339
|
-
|
|
312
|
+
function stop(): void {
|
|
313
|
+
running = false;
|
|
314
|
+
if (timer !== null) {
|
|
315
|
+
clearInterval(timer);
|
|
316
|
+
timer = null;
|
|
317
|
+
}
|
|
340
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
|
+
};
|
|
341
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 };
|
|
@@ -654,7 +799,7 @@ export const TopSettingsSchema = z.preprocess(
|
|
|
654
799
|
recordTurnTelemetry: z.boolean().optional(),
|
|
655
800
|
timelineCadence: z.number().min(0).max(10).default(1),
|
|
656
801
|
intervalSeconds: z.number().min(1).max(60).default(3),
|
|
657
|
-
defaultTab: z.enum(["system", "context", "settings", "about"]).default("system"),
|
|
802
|
+
defaultTab: z.enum(["system", "context", "permissions", "settings", "about"]).default("system"),
|
|
658
803
|
})
|
|
659
804
|
);
|
|
660
805
|
|
|
@@ -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
|
|