@cesarus777/opencode-cgp 0.3.1 → 0.4.0
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/CHANGELOG.md +9 -0
- package/README.md +9 -3
- package/dist/cli.js +674 -21
- package/dist/core.js +49 -4
- package/dist/index.js +570 -9
- package/dist/rpc.js +129 -2
- package/dist/tui.js +128 -5
- package/dist/types/adapter.d.ts +2 -0
- package/dist/types/core.d.ts +1 -0
- package/dist/types/dashboard/command.d.ts +1 -1
- package/dist/types/dashboard/connection.d.ts +2 -0
- package/dist/types/dashboard/controller.d.ts +3 -0
- package/dist/types/dashboard/models.d.ts +42 -0
- package/dist/types/dashboard/modelview.d.ts +30 -0
- package/dist/types/dashboard/port.d.ts +6 -0
- package/dist/types/dashboard/terminal.d.ts +5 -0
- package/dist/types/dashboard/view.d.ts +3 -1
- package/dist/types/history.d.ts +1 -0
- package/dist/types/modelanalytics.d.ts +8 -0
- package/dist/types/modelanalyticsserver.d.ts +16 -0
- package/dist/types/modelseries.d.ts +43 -0
- package/dist/types/rpc.d.ts +28 -0
- package/dist/types/tui/chartformat.d.ts +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -44,6 +44,12 @@ function isHistoryRead(value) {
|
|
|
44
44
|
function isHistoryResult(value) {
|
|
45
45
|
return valid(value, historyResultSchema);
|
|
46
46
|
}
|
|
47
|
+
function isAnalyticsRead(value) {
|
|
48
|
+
return valid(value, analyticsReadSchema) && record(value) && typeof value.from === "number" && typeof value.to === "number" && value.from < value.to && value.to - value.from <= 604800000 && !(value.model !== undefined && value.unknownModel === true) && (!(value.modelCursor || value.requestCursor) || typeof value.snapshotId === "string");
|
|
49
|
+
}
|
|
50
|
+
function isAnalyticsResult(value) {
|
|
51
|
+
return valid(value, analyticsResultSchema);
|
|
52
|
+
}
|
|
47
53
|
function record(x) {
|
|
48
54
|
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
49
55
|
}
|
|
@@ -101,7 +107,7 @@ var count, text, enumeration = (...values) => ({ type: "string", enum: values })
|
|
|
101
107
|
type: "array",
|
|
102
108
|
items,
|
|
103
109
|
maxItems
|
|
104
|
-
}), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc;
|
|
110
|
+
}), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc, modelSummarySchema, analyticsReadSchema, analyticsResultSchema, analyticsRpc;
|
|
105
111
|
var init_rpc = __esm(() => {
|
|
106
112
|
count = {
|
|
107
113
|
type: "integer",
|
|
@@ -337,6 +343,123 @@ var init_rpc = __esm(() => {
|
|
|
337
343
|
methods: { read: { input: historyReadSchema, output: historyResultSchema } },
|
|
338
344
|
events: {}
|
|
339
345
|
});
|
|
346
|
+
modelSummarySchema = object({
|
|
347
|
+
model: text,
|
|
348
|
+
sessions: count,
|
|
349
|
+
completedRequests: count,
|
|
350
|
+
partialRequests: count,
|
|
351
|
+
legacyAttributionRequests: count,
|
|
352
|
+
tpsRequests: count,
|
|
353
|
+
ttftRequests: count,
|
|
354
|
+
excludedTpsRequests: count,
|
|
355
|
+
excludedTtftRequests: count,
|
|
356
|
+
outputTokens: nonnegative,
|
|
357
|
+
durationMs: nonnegative,
|
|
358
|
+
averageTps: nonnegative,
|
|
359
|
+
meanTtftMs: nonnegative,
|
|
360
|
+
medianTtftMs: nonnegative,
|
|
361
|
+
p95TtftMs: nonnegative
|
|
362
|
+
}, [
|
|
363
|
+
"sessions",
|
|
364
|
+
"completedRequests",
|
|
365
|
+
"partialRequests",
|
|
366
|
+
"legacyAttributionRequests",
|
|
367
|
+
"tpsRequests",
|
|
368
|
+
"ttftRequests",
|
|
369
|
+
"excludedTpsRequests",
|
|
370
|
+
"excludedTtftRequests",
|
|
371
|
+
"outputTokens",
|
|
372
|
+
"durationMs"
|
|
373
|
+
]);
|
|
374
|
+
analyticsReadSchema = object({
|
|
375
|
+
schemaVersion: { const: 1 },
|
|
376
|
+
from: count,
|
|
377
|
+
to: count,
|
|
378
|
+
buckets: { type: "integer", minimum: 1, maximum: 240 },
|
|
379
|
+
agent: text,
|
|
380
|
+
model: text,
|
|
381
|
+
unknownModel: { type: "boolean" },
|
|
382
|
+
sort: enumeration("requests", "tps", "ttft", "identity"),
|
|
383
|
+
snapshotId: text,
|
|
384
|
+
modelCursor: text,
|
|
385
|
+
requestCursor: text
|
|
386
|
+
}, ["schemaVersion", "from", "to", "buckets"]);
|
|
387
|
+
analyticsResultSchema = object({
|
|
388
|
+
schemaVersion: { const: 1 },
|
|
389
|
+
generatedAt: count,
|
|
390
|
+
expiresAt: count,
|
|
391
|
+
snapshotId: text,
|
|
392
|
+
enabled: { type: "boolean" },
|
|
393
|
+
from: count,
|
|
394
|
+
to: count,
|
|
395
|
+
agent: text,
|
|
396
|
+
models: array(modelSummarySchema, 50),
|
|
397
|
+
totalModels: count,
|
|
398
|
+
nextModelsCursor: text,
|
|
399
|
+
selected: modelSummarySchema,
|
|
400
|
+
buckets: array(object({ from: nonnegative, to: nonnegative, summary: modelSummarySchema }, ["from", "to"]), 240),
|
|
401
|
+
requests: array(object({
|
|
402
|
+
location: text,
|
|
403
|
+
sessionId: text,
|
|
404
|
+
requestId: text,
|
|
405
|
+
startedAt: count,
|
|
406
|
+
completedAt: count,
|
|
407
|
+
model: text,
|
|
408
|
+
agent: text,
|
|
409
|
+
attribution: enumeration("request", "legacy-record", "unknown"),
|
|
410
|
+
partial: { type: "boolean" },
|
|
411
|
+
outputTokens: nonnegative,
|
|
412
|
+
ttftMs: nonnegative
|
|
413
|
+
}, [
|
|
414
|
+
"location",
|
|
415
|
+
"sessionId",
|
|
416
|
+
"requestId",
|
|
417
|
+
"startedAt",
|
|
418
|
+
"completedAt",
|
|
419
|
+
"attribution",
|
|
420
|
+
"partial"
|
|
421
|
+
]), 50),
|
|
422
|
+
nextRequestsCursor: text,
|
|
423
|
+
coverage: object({
|
|
424
|
+
source: enumeration("retained-request-records"),
|
|
425
|
+
scannedRecords: count,
|
|
426
|
+
unscannedRecords: count,
|
|
427
|
+
duplicateRecords: count,
|
|
428
|
+
conflictingRequests: count,
|
|
429
|
+
missingIdentityRecords: count,
|
|
430
|
+
incompleteRequests: count,
|
|
431
|
+
invalidTimingRequests: count,
|
|
432
|
+
gapRecords: count,
|
|
433
|
+
maxRequests: count,
|
|
434
|
+
partial: { type: "boolean" }
|
|
435
|
+
}),
|
|
436
|
+
metadata: historyResultSchema.properties.metadata ?? {},
|
|
437
|
+
gaps: array(count, 128),
|
|
438
|
+
omittedGaps: count
|
|
439
|
+
}, [
|
|
440
|
+
"schemaVersion",
|
|
441
|
+
"generatedAt",
|
|
442
|
+
"expiresAt",
|
|
443
|
+
"snapshotId",
|
|
444
|
+
"enabled",
|
|
445
|
+
"from",
|
|
446
|
+
"to",
|
|
447
|
+
"models",
|
|
448
|
+
"totalModels",
|
|
449
|
+
"buckets",
|
|
450
|
+
"requests",
|
|
451
|
+
"coverage",
|
|
452
|
+
"metadata",
|
|
453
|
+
"gaps",
|
|
454
|
+
"omittedGaps"
|
|
455
|
+
]);
|
|
456
|
+
analyticsRpc = Rpc.define({
|
|
457
|
+
id: "context-guard.analytics.v1",
|
|
458
|
+
methods: {
|
|
459
|
+
read: { input: analyticsReadSchema, output: analyticsResultSchema }
|
|
460
|
+
},
|
|
461
|
+
events: {}
|
|
462
|
+
});
|
|
340
463
|
});
|
|
341
464
|
|
|
342
465
|
// src/history.ts
|
|
@@ -529,6 +652,7 @@ function scalarPerformance(p) {
|
|
|
529
652
|
async function readPerformanceHistory(directory, filter = {}) {
|
|
530
653
|
const records = [];
|
|
531
654
|
const files = await segments(directory);
|
|
655
|
+
filter.signal?.throwIfAborted();
|
|
532
656
|
const maxBytes = filter.maxBytes ?? 64 * 1024 * 1024;
|
|
533
657
|
let bytes = 0, skippedBytes = 0, malformedRecords = 0;
|
|
534
658
|
const selected = [];
|
|
@@ -541,6 +665,7 @@ async function readPerformanceHistory(directory, filter = {}) {
|
|
|
541
665
|
selected.unshift(file);
|
|
542
666
|
}
|
|
543
667
|
for (const file of selected) {
|
|
668
|
+
filter.signal?.throwIfAborted();
|
|
544
669
|
const handle = await open(join(directory, file.name), "r");
|
|
545
670
|
let source;
|
|
546
671
|
try {
|
|
@@ -550,7 +675,12 @@ async function readPerformanceHistory(directory, filter = {}) {
|
|
|
550
675
|
} finally {
|
|
551
676
|
await handle.close();
|
|
552
677
|
}
|
|
678
|
+
let scannedLines = 0;
|
|
553
679
|
for (const line of source.split("\n")) {
|
|
680
|
+
if (filter.signal && ++scannedLines % 256 === 0) {
|
|
681
|
+
await new Promise((done) => setTimeout(done, 0));
|
|
682
|
+
filter.signal.throwIfAborted();
|
|
683
|
+
}
|
|
554
684
|
if (!line)
|
|
555
685
|
continue;
|
|
556
686
|
try {
|
|
@@ -671,8 +801,9 @@ Open a read-only live terminal dashboard for an existing OpenCode server.
|
|
|
671
801
|
|
|
672
802
|
Start here
|
|
673
803
|
1. Keep an OpenCode server running with Context Guard registered.
|
|
674
|
-
2.
|
|
675
|
-
3. In the
|
|
804
|
+
2. Open a separate terminal tab, window or pane for the dashboard.
|
|
805
|
+
3. In the same project directory, run: context-guard dashboard tui
|
|
806
|
+
Keep working in OpenCode; the dashboard reads live updates from its server.
|
|
676
807
|
|
|
677
808
|
Options
|
|
678
809
|
--directory PATH Owning project directory (local default: current directory)
|
|
@@ -695,8 +826,10 @@ Authentication (explicit servers)
|
|
|
695
826
|
CONTEXT_GUARD_USERNAME Server username (default: opencode)
|
|
696
827
|
|
|
697
828
|
Keys
|
|
698
|
-
1/2/3 overview/detail/history; arrows or j/k move; /
|
|
699
|
-
|
|
829
|
+
1/2/3/4 overview/detail/history/models; arrows or j/k move; Enter detail/chart;
|
|
830
|
+
Models: Left/Right metric, [/] bucket, d requests, n/p pages, s sort;
|
|
831
|
+
/ session search; f filters (Models: agent); l location; w time window;
|
|
832
|
+
r refresh; ? help; q or Ctrl+C quit.`;
|
|
700
833
|
var init_command = () => {
|
|
701
834
|
};
|
|
702
835
|
|
|
@@ -1164,6 +1297,249 @@ var init_inventory = __esm(() => {
|
|
|
1164
1297
|
});
|
|
1165
1298
|
});
|
|
1166
1299
|
|
|
1300
|
+
// src/dashboard/port.ts
|
|
1301
|
+
var AnalyticsUnavailable;
|
|
1302
|
+
var init_port = __esm(() => {
|
|
1303
|
+
AnalyticsUnavailable = class AnalyticsUnavailable extends Error {
|
|
1304
|
+
reason;
|
|
1305
|
+
constructor(reason) {
|
|
1306
|
+
super(`Model analytics ${reason}`);
|
|
1307
|
+
this.reason = reason;
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
// src/dashboard/models.ts
|
|
1313
|
+
class ModelsController {
|
|
1314
|
+
changed;
|
|
1315
|
+
port;
|
|
1316
|
+
active = false;
|
|
1317
|
+
key = "";
|
|
1318
|
+
agent = "";
|
|
1319
|
+
seedModel = "";
|
|
1320
|
+
selection;
|
|
1321
|
+
resolution = 120;
|
|
1322
|
+
from = 0;
|
|
1323
|
+
to = 1;
|
|
1324
|
+
generation = 0;
|
|
1325
|
+
abort;
|
|
1326
|
+
pending;
|
|
1327
|
+
modelPages = [undefined];
|
|
1328
|
+
requestPages = [undefined];
|
|
1329
|
+
state = {
|
|
1330
|
+
status: "idle",
|
|
1331
|
+
stale: false,
|
|
1332
|
+
sort: "requests",
|
|
1333
|
+
windowMs: 300000,
|
|
1334
|
+
modelPage: 0,
|
|
1335
|
+
requestPage: 0
|
|
1336
|
+
};
|
|
1337
|
+
constructor(changed) {
|
|
1338
|
+
this.changed = changed;
|
|
1339
|
+
}
|
|
1340
|
+
getView() {
|
|
1341
|
+
return this.state;
|
|
1342
|
+
}
|
|
1343
|
+
configure(port, active, windowMs, agent, seedModel, now) {
|
|
1344
|
+
const key = JSON.stringify([
|
|
1345
|
+
port.location.directory,
|
|
1346
|
+
windowMs,
|
|
1347
|
+
agent,
|
|
1348
|
+
seedModel
|
|
1349
|
+
]);
|
|
1350
|
+
const different = key !== this.key;
|
|
1351
|
+
const preserveSelection = this.port?.location.directory === port.location.directory && this.agent === agent && this.seedModel === seedModel;
|
|
1352
|
+
const entering = active && !this.active;
|
|
1353
|
+
if (different || !active)
|
|
1354
|
+
this.cancel();
|
|
1355
|
+
this.port = port;
|
|
1356
|
+
this.active = active;
|
|
1357
|
+
if (different) {
|
|
1358
|
+
this.key = key;
|
|
1359
|
+
this.agent = agent;
|
|
1360
|
+
this.seedModel = seedModel;
|
|
1361
|
+
if (!preserveSelection)
|
|
1362
|
+
this.selection = seedModel || undefined;
|
|
1363
|
+
this.modelPages = [undefined];
|
|
1364
|
+
this.requestPages = [undefined];
|
|
1365
|
+
this.state = {
|
|
1366
|
+
status: "idle",
|
|
1367
|
+
stale: false,
|
|
1368
|
+
sort: this.state.sort,
|
|
1369
|
+
windowMs,
|
|
1370
|
+
modelPage: 0,
|
|
1371
|
+
requestPage: 0
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
if (active && (different || entering))
|
|
1375
|
+
this.refresh(now);
|
|
1376
|
+
}
|
|
1377
|
+
cancel() {
|
|
1378
|
+
this.generation++;
|
|
1379
|
+
this.abort?.abort();
|
|
1380
|
+
this.abort = undefined;
|
|
1381
|
+
this.pending = undefined;
|
|
1382
|
+
}
|
|
1383
|
+
stop() {
|
|
1384
|
+
this.cancel();
|
|
1385
|
+
this.active = false;
|
|
1386
|
+
this.state = { ...this.state, stale: Boolean(this.state.result) };
|
|
1387
|
+
}
|
|
1388
|
+
async refresh(now) {
|
|
1389
|
+
if (!this.active || !this.port)
|
|
1390
|
+
return;
|
|
1391
|
+
if (this.pending)
|
|
1392
|
+
return this.pending;
|
|
1393
|
+
this.to = Math.max(1, Math.floor(now));
|
|
1394
|
+
this.from = Math.max(0, this.to - this.state.windowMs);
|
|
1395
|
+
this.modelPages = [undefined];
|
|
1396
|
+
this.requestPages = [undefined];
|
|
1397
|
+
this.state = { ...this.state, modelPage: 0, requestPage: 0 };
|
|
1398
|
+
return this.query(false);
|
|
1399
|
+
}
|
|
1400
|
+
setWidth(width) {
|
|
1401
|
+
const next = Math.max(1, Math.min(120, Math.floor(width) - 10));
|
|
1402
|
+
if (next === this.resolution)
|
|
1403
|
+
return;
|
|
1404
|
+
this.resolution = next;
|
|
1405
|
+
if (this.active && this.state.result)
|
|
1406
|
+
this.query(true);
|
|
1407
|
+
}
|
|
1408
|
+
select(model) {
|
|
1409
|
+
if (!this.active || model === this.selection)
|
|
1410
|
+
return;
|
|
1411
|
+
this.selection = model;
|
|
1412
|
+
this.requestPages = [undefined];
|
|
1413
|
+
this.state = { ...this.state, requestPage: 0 };
|
|
1414
|
+
this.query(true);
|
|
1415
|
+
}
|
|
1416
|
+
sort() {
|
|
1417
|
+
const sorts = ["requests", "tps", "ttft", "identity"];
|
|
1418
|
+
this.modelPages = [undefined];
|
|
1419
|
+
this.state = {
|
|
1420
|
+
...this.state,
|
|
1421
|
+
sort: sorts[(sorts.indexOf(this.state.sort) + 1) % sorts.length] ?? "requests",
|
|
1422
|
+
modelPage: 0
|
|
1423
|
+
};
|
|
1424
|
+
this.query(true);
|
|
1425
|
+
}
|
|
1426
|
+
page(kind, direction) {
|
|
1427
|
+
if (this.state.status !== "ready")
|
|
1428
|
+
return;
|
|
1429
|
+
const pages = kind === "models" ? this.modelPages : this.requestPages;
|
|
1430
|
+
const index = kind === "models" ? this.state.modelPage : this.state.requestPage;
|
|
1431
|
+
const next = kind === "models" ? this.state.result?.nextModelsCursor : this.state.result?.nextRequestsCursor;
|
|
1432
|
+
if (direction < 0 && index === 0 || direction > 0 && !next)
|
|
1433
|
+
return;
|
|
1434
|
+
if (direction > 0)
|
|
1435
|
+
pages[index + 1] = next;
|
|
1436
|
+
this.state = {
|
|
1437
|
+
...this.state,
|
|
1438
|
+
...kind === "models" ? { modelPage: index + direction, requestPage: 0 } : { requestPage: index + direction }
|
|
1439
|
+
};
|
|
1440
|
+
if (kind === "models") {
|
|
1441
|
+
this.selection = undefined;
|
|
1442
|
+
this.requestPages = [undefined];
|
|
1443
|
+
}
|
|
1444
|
+
this.query(true);
|
|
1445
|
+
}
|
|
1446
|
+
query(reuse) {
|
|
1447
|
+
this.cancel();
|
|
1448
|
+
const port = this.port;
|
|
1449
|
+
if (!this.active || !port)
|
|
1450
|
+
return Promise.resolve();
|
|
1451
|
+
if (!port.analytics) {
|
|
1452
|
+
this.state = { ...this.state, status: "unsupported", stale: false };
|
|
1453
|
+
this.changed();
|
|
1454
|
+
return Promise.resolve();
|
|
1455
|
+
}
|
|
1456
|
+
const read = port.analytics.bind(port);
|
|
1457
|
+
const controller = new AbortController;
|
|
1458
|
+
this.abort = controller;
|
|
1459
|
+
const generation = this.generation;
|
|
1460
|
+
const signal = AbortSignal.any([
|
|
1461
|
+
controller.signal,
|
|
1462
|
+
AbortSignal.timeout(15000)
|
|
1463
|
+
]);
|
|
1464
|
+
const modelCursor = this.modelPages[this.state.modelPage];
|
|
1465
|
+
const requestCursor = this.requestPages[this.state.requestPage];
|
|
1466
|
+
const input = {
|
|
1467
|
+
schemaVersion: 1,
|
|
1468
|
+
from: this.from,
|
|
1469
|
+
to: this.to,
|
|
1470
|
+
buckets: this.resolution,
|
|
1471
|
+
sort: this.state.sort,
|
|
1472
|
+
...this.agent ? { agent: this.agent } : {},
|
|
1473
|
+
...this.selection === "" ? { unknownModel: true } : this.selection ? { model: this.selection } : {},
|
|
1474
|
+
...reuse && this.state.result ? { snapshotId: this.state.result.snapshotId } : {},
|
|
1475
|
+
...reuse && modelCursor ? { modelCursor } : {},
|
|
1476
|
+
...reuse && requestCursor ? { requestCursor } : {}
|
|
1477
|
+
};
|
|
1478
|
+
this.state = {
|
|
1479
|
+
...this.state,
|
|
1480
|
+
status: "loading",
|
|
1481
|
+
stale: Boolean(this.state.result)
|
|
1482
|
+
};
|
|
1483
|
+
this.changed();
|
|
1484
|
+
const task = Promise.resolve().then(async () => {
|
|
1485
|
+
try {
|
|
1486
|
+
let result;
|
|
1487
|
+
try {
|
|
1488
|
+
result = await read(input, signal);
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
if (!input.snapshotId || signal.aborted || error instanceof AnalyticsUnavailable && error.reason === "unsupported")
|
|
1491
|
+
throw error;
|
|
1492
|
+
const {
|
|
1493
|
+
snapshotId: _id,
|
|
1494
|
+
modelCursor: _m,
|
|
1495
|
+
requestCursor: _r,
|
|
1496
|
+
...fresh
|
|
1497
|
+
} = input;
|
|
1498
|
+
result = await read(fresh, signal);
|
|
1499
|
+
if (generation !== this.generation)
|
|
1500
|
+
return;
|
|
1501
|
+
signal.throwIfAborted();
|
|
1502
|
+
this.modelPages = [undefined];
|
|
1503
|
+
this.requestPages = [undefined];
|
|
1504
|
+
this.state = { ...this.state, modelPage: 0, requestPage: 0 };
|
|
1505
|
+
}
|
|
1506
|
+
if (generation !== this.generation)
|
|
1507
|
+
return;
|
|
1508
|
+
signal.throwIfAborted();
|
|
1509
|
+
if (!isAnalyticsResult(result) || result.from !== input.from || result.to !== input.to || result.agent !== input.agent || result.requests.some((r) => r.location !== port.location.directory))
|
|
1510
|
+
throw new Error("Invalid analytics response");
|
|
1511
|
+
if (result.selected)
|
|
1512
|
+
this.selection = result.selected.model ?? "";
|
|
1513
|
+
this.state = {
|
|
1514
|
+
...this.state,
|
|
1515
|
+
result: structuredClone(result),
|
|
1516
|
+
status: result.enabled ? "ready" : "disabled",
|
|
1517
|
+
stale: false
|
|
1518
|
+
};
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
if (generation !== this.generation || controller.signal.aborted)
|
|
1521
|
+
return;
|
|
1522
|
+
this.state = {
|
|
1523
|
+
...this.state,
|
|
1524
|
+
status: error instanceof AnalyticsUnavailable && error.reason === "unsupported" && !this.state.result ? "unsupported" : "unavailable",
|
|
1525
|
+
stale: Boolean(this.state.result)
|
|
1526
|
+
};
|
|
1527
|
+
} finally {
|
|
1528
|
+
if (generation === this.generation) {
|
|
1529
|
+
this.pending = undefined;
|
|
1530
|
+
this.changed();
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
this.pending = task;
|
|
1535
|
+
return task;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
var init_models = __esm(() => {
|
|
1539
|
+
init_rpc();
|
|
1540
|
+
init_port();
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1167
1543
|
// src/dashboard/view.ts
|
|
1168
1544
|
function immutableView(value) {
|
|
1169
1545
|
const copy = structuredClone(value);
|
|
@@ -1256,6 +1632,7 @@ class DashboardController {
|
|
|
1256
1632
|
refreshTimer;
|
|
1257
1633
|
ageTimer;
|
|
1258
1634
|
view;
|
|
1635
|
+
models;
|
|
1259
1636
|
constructor(port, options = {}) {
|
|
1260
1637
|
this.limits = { ...options.limits ?? inventoryLimits };
|
|
1261
1638
|
validateInventoryLimits(this.limits);
|
|
@@ -1266,6 +1643,7 @@ class DashboardController {
|
|
|
1266
1643
|
throw new Error("Invalid dashboard refresh interval");
|
|
1267
1644
|
this.state = this.createLocation(port);
|
|
1268
1645
|
this.locations.set(port.location.directory, this.state);
|
|
1646
|
+
this.models = new ModelsController(() => this.publish());
|
|
1269
1647
|
this.view = this.buildView();
|
|
1270
1648
|
}
|
|
1271
1649
|
createLocation(port) {
|
|
@@ -1315,6 +1693,7 @@ class DashboardController {
|
|
|
1315
1693
|
await this.refresh();
|
|
1316
1694
|
}
|
|
1317
1695
|
stop() {
|
|
1696
|
+
this.models.stop();
|
|
1318
1697
|
this.running = false;
|
|
1319
1698
|
this.generation++;
|
|
1320
1699
|
this.scope?.abort();
|
|
@@ -1349,6 +1728,7 @@ class DashboardController {
|
|
|
1349
1728
|
this.locations.delete(directory);
|
|
1350
1729
|
this.locations.set(directory, next);
|
|
1351
1730
|
this.state = next;
|
|
1731
|
+
this.configureModels();
|
|
1352
1732
|
if (this.locations.size > 4) {
|
|
1353
1733
|
const oldest = this.locations.keys().next().value;
|
|
1354
1734
|
if (oldest !== undefined)
|
|
@@ -1370,11 +1750,12 @@ class DashboardController {
|
|
|
1370
1750
|
this.publish();
|
|
1371
1751
|
}
|
|
1372
1752
|
setScreen(screen) {
|
|
1373
|
-
if (!["overview", "detail", "history"].includes(screen))
|
|
1753
|
+
if (!["overview", "detail", "history", "models"].includes(screen))
|
|
1374
1754
|
throw new Error("Invalid dashboard screen");
|
|
1375
1755
|
if (screen === this.screen)
|
|
1376
1756
|
return;
|
|
1377
1757
|
this.screen = screen;
|
|
1758
|
+
this.configureModels();
|
|
1378
1759
|
this.queueHistory();
|
|
1379
1760
|
this.publish();
|
|
1380
1761
|
}
|
|
@@ -1384,6 +1765,7 @@ class DashboardController {
|
|
|
1384
1765
|
if (windowMs === this.windowMs)
|
|
1385
1766
|
return;
|
|
1386
1767
|
this.windowMs = windowMs;
|
|
1768
|
+
this.configureModels();
|
|
1387
1769
|
this.queueHistory();
|
|
1388
1770
|
this.publish();
|
|
1389
1771
|
}
|
|
@@ -1396,11 +1778,18 @@ class DashboardController {
|
|
|
1396
1778
|
agent: filters.agent ?? this.filters.agent,
|
|
1397
1779
|
model: filters.model ?? this.filters.model
|
|
1398
1780
|
};
|
|
1781
|
+
this.configureModels();
|
|
1399
1782
|
this.publish();
|
|
1400
1783
|
}
|
|
1784
|
+
configureModels() {
|
|
1785
|
+
this.models.configure(this.state.port, this.running && this.screen === "models", this.windowMs, this.filters.agent, this.filters.model, this.serverNow());
|
|
1786
|
+
}
|
|
1401
1787
|
async refresh() {
|
|
1402
1788
|
if (!this.running || !this.scope)
|
|
1403
1789
|
return;
|
|
1790
|
+
this.configureModels();
|
|
1791
|
+
if (this.screen === "models")
|
|
1792
|
+
this.models.refresh(this.serverNow());
|
|
1404
1793
|
for (const cached of this.cache.values())
|
|
1405
1794
|
if (cached.directory === this.state.port.location.directory)
|
|
1406
1795
|
cached.valid = false;
|
|
@@ -1701,6 +2090,7 @@ class DashboardController {
|
|
|
1701
2090
|
"maximum-500-points-and-requests"
|
|
1702
2091
|
]
|
|
1703
2092
|
},
|
|
2093
|
+
models: this.models.getView(),
|
|
1704
2094
|
notices: [
|
|
1705
2095
|
...!state.complete ? ["partial-inventory"] : [],
|
|
1706
2096
|
...state.connection !== "live" ? ["live-data-stale"] : [],
|
|
@@ -1713,6 +2103,7 @@ var init_controller = __esm(() => {
|
|
|
1713
2103
|
init_rpc();
|
|
1714
2104
|
init_transport();
|
|
1715
2105
|
init_inventory();
|
|
2106
|
+
init_models();
|
|
1716
2107
|
init_view();
|
|
1717
2108
|
});
|
|
1718
2109
|
|
|
@@ -1865,6 +2256,19 @@ async function connectDashboard(options, discovery = Service) {
|
|
|
1865
2256
|
}
|
|
1866
2257
|
};
|
|
1867
2258
|
},
|
|
2259
|
+
async analytics(input, signal) {
|
|
2260
|
+
if (!isAnalyticsRead(input))
|
|
2261
|
+
throw new Error("Invalid analytics request");
|
|
2262
|
+
try {
|
|
2263
|
+
const result = await client.rpc(analyticsRpc).read(input, { location, signal: deadline(signal) });
|
|
2264
|
+
if (!isAnalyticsResult(result))
|
|
2265
|
+
throw new Error("Invalid analytics response");
|
|
2266
|
+
return result;
|
|
2267
|
+
} catch (error) {
|
|
2268
|
+
const missing = typeof error === "object" && error !== null && "type" in error && (error.type === "rpc.unavailable" || error.type === "rpc.method_not_found");
|
|
2269
|
+
throw new AnalyticsUnavailable(missing ? "unsupported" : "unavailable");
|
|
2270
|
+
}
|
|
2271
|
+
},
|
|
1868
2272
|
async history(input, signal) {
|
|
1869
2273
|
if (!isHistoryRead(input))
|
|
1870
2274
|
throw new Error("History requires one session, at most seven days and 16\u2013500 points");
|
|
@@ -1892,6 +2296,7 @@ async function connectDashboard(options, discovery = Service) {
|
|
|
1892
2296
|
}
|
|
1893
2297
|
var init_connection = __esm(() => {
|
|
1894
2298
|
init_rpc();
|
|
2299
|
+
init_port();
|
|
1895
2300
|
});
|
|
1896
2301
|
|
|
1897
2302
|
// src/tui/format.ts
|
|
@@ -2012,9 +2417,9 @@ var init_format = __esm(() => {
|
|
|
2012
2417
|
});
|
|
2013
2418
|
|
|
2014
2419
|
// src/tui/chartformat.ts
|
|
2015
|
-
function columns(points, from, to, width, gaps = []) {
|
|
2420
|
+
function columns(points, from, to, width, gaps = [], halfOpen = false) {
|
|
2016
2421
|
const size = Math.max(1, Math.min(120, Math.floor(width))), values = Array.from({ length: size });
|
|
2017
|
-
const column = (time) => Math.max(0, Math.min(size - 1, Math.floor((time - from) / Math.max(1, to - from) * (size - 1))));
|
|
2422
|
+
const column = (time) => Math.max(0, Math.min(size - 1, Math.floor((time - from) / Math.max(1, to - from) * (halfOpen ? size : size - 1))));
|
|
2018
2423
|
let min = Infinity, max = -Infinity;
|
|
2019
2424
|
for (const point of points) {
|
|
2020
2425
|
if (point.time < from || point.time > to || point.value === undefined || !Number.isFinite(point.value) || point.value < 0)
|
|
@@ -2038,12 +2443,12 @@ function sparkline(points, from, to, width, gaps = []) {
|
|
|
2038
2443
|
chars[column(time)] = "\u2506";
|
|
2039
2444
|
return { line: chars.join(""), ...min === Infinity ? {} : { min, max } };
|
|
2040
2445
|
}
|
|
2041
|
-
function metricPlot(points, from, to, width, gaps = []) {
|
|
2446
|
+
function metricPlot(points, from, to, width, gaps = [], halfOpen = false) {
|
|
2042
2447
|
const label = (value) => value >= 1e6 ? `${(value / 1e6).toFixed(1)}M` : value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value.toFixed(1);
|
|
2043
2448
|
const peak = Math.max(0, ...points.filter((p) => p.time >= from && p.time <= to && Number.isFinite(p.value)).map((p) => p.value ?? 0));
|
|
2044
2449
|
const scale = Math.max(1, peak);
|
|
2045
2450
|
const gutter = width >= 20 ? Math.max(label(scale).length, 3) : 0;
|
|
2046
|
-
const { values, column } = columns(points, from, to, Math.max(1, width - gutter - 2), gaps);
|
|
2451
|
+
const { values, column } = columns(points, from, to, Math.max(1, halfOpen ? Math.min(points.length, width - gutter - 2) : width - gutter - 2), gaps, halfOpen);
|
|
2047
2452
|
const gapColumns = new Set(gaps.filter((time) => time >= from && time <= to).map(column));
|
|
2048
2453
|
const height = 6;
|
|
2049
2454
|
const blocks = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
@@ -2120,6 +2525,108 @@ var init_chartformat = __esm(() => {
|
|
|
2120
2525
|
];
|
|
2121
2526
|
});
|
|
2122
2527
|
|
|
2528
|
+
// src/dashboard/modelview.ts
|
|
2529
|
+
function metricValue(summary, metric2) {
|
|
2530
|
+
if (metric2.key === "outputTokens" && !summary?.tpsRequests)
|
|
2531
|
+
return;
|
|
2532
|
+
const value = summary?.[metric2.key];
|
|
2533
|
+
return value === undefined ? undefined : value / metric2.divisor;
|
|
2534
|
+
}
|
|
2535
|
+
function terminalModelRow(model, focused) {
|
|
2536
|
+
return [
|
|
2537
|
+
`${focused ? "> " : " "}${model.model ?? "unknown model attribution"}`,
|
|
2538
|
+
` ${stats(model)}`,
|
|
2539
|
+
` TPS eligible ${model.tpsRequests}; TTFT eligible ${model.ttftRequests}; partial ${model.partialRequests}; legacy attribution ${model.legacyAttributionRequests}`
|
|
2540
|
+
];
|
|
2541
|
+
}
|
|
2542
|
+
function terminalModels(view, width, detail, metricIndex, sample, focus, requests) {
|
|
2543
|
+
const state = view.models;
|
|
2544
|
+
const result = state.result;
|
|
2545
|
+
const lines = [
|
|
2546
|
+
`Models \xB7 ${state.status}${state.stale ? " \xB7 STALE cached results" : ""} \xB7 ${state.windowMs / 60000} minutes \xB7 retained history`,
|
|
2547
|
+
`Agent: ${(result?.agent ?? view.filters.agent) || "all"} \xB7 sort ${state.sort} \xB7 location ${view.location}`,
|
|
2548
|
+
"4 Models \xB7 Enter chart \xB7 Esc model list/overview \xB7 w window \xB7 f agent \xB7 r refresh"
|
|
2549
|
+
];
|
|
2550
|
+
if (state.status === "unsupported")
|
|
2551
|
+
return [
|
|
2552
|
+
...lines,
|
|
2553
|
+
"Models requires a server with context-guard.analytics.v1; update the server plugin. Overview and session history remain available."
|
|
2554
|
+
];
|
|
2555
|
+
if (state.status === "disabled")
|
|
2556
|
+
return [
|
|
2557
|
+
...lines,
|
|
2558
|
+
"Retained performance recording is disabled on this server."
|
|
2559
|
+
];
|
|
2560
|
+
if (!result)
|
|
2561
|
+
return [
|
|
2562
|
+
...lines,
|
|
2563
|
+
state.status === "loading" ? "Loading model aggregates\u2026" : "Model analytics unavailable; press r to retry."
|
|
2564
|
+
];
|
|
2565
|
+
lines.push(`${new Date(result.from).toISOString()} \u2192 ${new Date(result.to).toISOString()} (UTC; completion time)`);
|
|
2566
|
+
const partial = result.coverage.partial || result.metadata.skippedBytes > 0 || result.metadata.malformedRecords > 0 || result.gaps.length > 0 || result.omittedGaps > 0;
|
|
2567
|
+
lines.push(`Coverage: ${partial ? "PARTIAL" : "bounded observations"}; 64 MiB scan limit; ${result.coverage.maxRequests} request identities maximum; no pre-observation reconstruction.`);
|
|
2568
|
+
lines.push(`Archive retained since ${result.metadata.retainedFrom === undefined ? "unknown" : new Date(result.metadata.retainedFrom).toISOString()}; snapshot observed at ${new Date(result.generatedAt).toISOString()}.`);
|
|
2569
|
+
lines.push(`Scan: ${result.coverage.scannedRecords} records; ${result.metadata.skippedBytes} bytes skipped; ${result.metadata.malformedRecords} malformed; ${result.coverage.conflictingRequests} conflicting; ${result.coverage.incompleteRequests} incomplete; ${result.coverage.invalidTimingRequests} invalid timing; gaps ${result.gaps.length}${result.omittedGaps ? ` (+${result.omittedGaps} omitted)` : ""}.`);
|
|
2570
|
+
if (!detail) {
|
|
2571
|
+
lines.push(`Model page ${state.modelPage + 1} \xB7 ${result.totalModels} models \xB7 Up/Down scroll/select \xB7 n/p pages \xB7 s sort`);
|
|
2572
|
+
if (!result.models.length)
|
|
2573
|
+
lines.push("No completed requests match this range and agent filter.");
|
|
2574
|
+
result.models.forEach((model, index) => {
|
|
2575
|
+
lines.push(...terminalModelRow(model, index === focus));
|
|
2576
|
+
});
|
|
2577
|
+
return lines;
|
|
2578
|
+
}
|
|
2579
|
+
const selected = result.selected;
|
|
2580
|
+
if (!selected)
|
|
2581
|
+
return [
|
|
2582
|
+
...lines,
|
|
2583
|
+
"No completed requests for this model in the selected range."
|
|
2584
|
+
];
|
|
2585
|
+
lines.push(`Model: ${selected.model ?? "unknown model attribution"}`, stats(selected));
|
|
2586
|
+
lines.push(`Reported output ${number(selected.tpsRequests ? selected.outputTokens : undefined)} tokens; matched duration ${number(selected.tpsRequests ? selected.durationMs / 1000 : undefined)}s; TPS eligible/excluded ${selected.tpsRequests}/${selected.excludedTpsRequests}; TTFT eligible/excluded ${selected.ttftRequests}/${selected.excludedTtftRequests}; partial ${selected.partialRequests}; legacy attribution ${selected.legacyAttributionRequests}`);
|
|
2587
|
+
if (requests) {
|
|
2588
|
+
lines.push(`Request page ${state.requestPage + 1} \xB7 n/p pages \xB7 d chart \xB7 completed stream \u2260 task success`);
|
|
2589
|
+
result.requests.forEach((r) => lines.push(`Request ${r.requestId} \xB7 session ${r.sessionId}`, `Completed ${new Date(r.completedAt).toISOString()} \xB7 ${r.agent ?? "unknown agent"} \xB7 output ${number(r.outputTokens)} \xB7 duration ${number((r.completedAt - r.startedAt) / 1000)}s \xB7 TTFT ${number(r.ttftMs === undefined ? undefined : r.ttftMs / 1000)}s ~ \xB7 ${r.attribution}${r.partial ? " \xB7 partial" : ""}`));
|
|
2590
|
+
return lines;
|
|
2591
|
+
}
|
|
2592
|
+
const metric2 = modelMetrics[metricIndex] ?? modelMetrics[0];
|
|
2593
|
+
lines.push(`\u2039 ${metric2.label} (${metric2.unit}) \u203A \xB7 Left/Right metric \xB7 [/] bucket \xB7 d requests`);
|
|
2594
|
+
lines.push(...metricPlot(result.buckets.map((bucket2) => {
|
|
2595
|
+
const value = metricValue(bucket2.summary, metric2);
|
|
2596
|
+
return {
|
|
2597
|
+
time: (bucket2.from + bucket2.to) / 2,
|
|
2598
|
+
...value === undefined ? {} : { value }
|
|
2599
|
+
};
|
|
2600
|
+
}), result.from, result.to, width, result.gaps, true));
|
|
2601
|
+
lines.push("UTC buckets \xB7 blank = no eligible observations \xB7 \u2506 = retained gap; partial coverage may hide other gaps.");
|
|
2602
|
+
lines.push("TPS = reported output / summed matched request durations (includes TTFT); concurrent durations add. In-flight requests enter after stream completion.");
|
|
2603
|
+
const bucket = result.buckets[Math.min(sample, Math.max(0, result.buckets.length - 1))];
|
|
2604
|
+
if (bucket) {
|
|
2605
|
+
lines.push(`Bucket ${Math.min(sample + 1, result.buckets.length)}/${result.buckets.length}: ${new Date(bucket.from).toISOString()} \u2192 ${new Date(bucket.to).toISOString()}`);
|
|
2606
|
+
lines.push(bucket.summary ? stats(bucket.summary) : "No eligible observations; measurements unknown.");
|
|
2607
|
+
lines.push(`Selected ${metric2.label}: ${number(metricValue(bucket.summary, metric2))} ${metric2.unit}`);
|
|
2608
|
+
if (bucket.summary)
|
|
2609
|
+
lines.push(`Bucket eligibility: TPS ${bucket.summary.tpsRequests}/${bucket.summary.completedRequests}; TTFT ${bucket.summary.ttftRequests}/${bucket.summary.completedRequests}; partial ${bucket.summary.partialRequests}`);
|
|
2610
|
+
}
|
|
2611
|
+
return lines;
|
|
2612
|
+
}
|
|
2613
|
+
var modelMetrics, number = (n) => n === undefined ? "?" : Number.isInteger(n) ? String(n) : n.toFixed(2), stats = (m) => `${m.sessions} sessions \xB7 ${m.completedRequests} completed \xB7 weighted TPS ${number(m.averageTps)} \xB7 TTFT median/p95 ${number(m.medianTtftMs === undefined ? undefined : m.medianTtftMs / 1000)}/${number(m.p95TtftMs === undefined ? undefined : m.p95TtftMs / 1000)}s ~`;
|
|
2614
|
+
var init_modelview = __esm(() => {
|
|
2615
|
+
init_chartformat();
|
|
2616
|
+
modelMetrics = [
|
|
2617
|
+
{ key: "averageTps", label: "Weighted request TPS", unit: "t/s", divisor: 1 },
|
|
2618
|
+
{ key: "medianTtftMs", label: "TTFT median ~", unit: "s", divisor: 1000 },
|
|
2619
|
+
{ key: "p95TtftMs", label: "TTFT p95 ~", unit: "s", divisor: 1000 },
|
|
2620
|
+
{
|
|
2621
|
+
key: "completedRequests",
|
|
2622
|
+
label: "Completed requests",
|
|
2623
|
+
unit: "requests",
|
|
2624
|
+
divisor: 1
|
|
2625
|
+
},
|
|
2626
|
+
{ key: "outputTokens", label: "Reported output", unit: "tokens", divisor: 1 }
|
|
2627
|
+
];
|
|
2628
|
+
});
|
|
2629
|
+
|
|
2123
2630
|
// src/dashboard/terminalview.ts
|
|
2124
2631
|
function overviewRow(row2) {
|
|
2125
2632
|
const s = row2.snapshot;
|
|
@@ -2202,7 +2709,11 @@ function terminalHistory(view, width, metricIndex, sample) {
|
|
|
2202
2709
|
];
|
|
2203
2710
|
}
|
|
2204
2711
|
function wrapped(lines, width) {
|
|
2205
|
-
|
|
2712
|
+
const available = Math.max(1, width);
|
|
2713
|
+
return lines.flatMap((line) => {
|
|
2714
|
+
const clean = clip(line, Math.max(available, cells(line)));
|
|
2715
|
+
return cells(clean) <= available ? [clean] : wrap(clean, available);
|
|
2716
|
+
});
|
|
2206
2717
|
}
|
|
2207
2718
|
function headerLines(view, width) {
|
|
2208
2719
|
return [
|
|
@@ -2282,7 +2793,10 @@ var init_terminalview = __esm(() => {
|
|
|
2282
2793
|
];
|
|
2283
2794
|
terminalHelp = [
|
|
2284
2795
|
"Context Guard standalone dashboard \u2014 keyboard help",
|
|
2285
|
-
"1 overview \xB7 2 detail \xB7 3 history \xB7 Enter detail",
|
|
2796
|
+
"1 overview \xB7 2 detail \xB7 3 history \xB7 4 models \xB7 Enter detail/chart",
|
|
2797
|
+
"Models: Up/Down scroll tall rows, then select \xB7 Enter aggregate chart \xB7 Esc model list \xB7 s sort",
|
|
2798
|
+
"Models charts: Left/Right metric \xB7 w window \xB7 [/] bucket \xB7 d request details",
|
|
2799
|
+
"Models list/request details: n/p pages \xB7 f exact agent filter \xB7 r refresh",
|
|
2286
2800
|
"Up/Down or j/k: select session in overview; scroll other screens",
|
|
2287
2801
|
"PageUp/PageDown: move by a page \xB7 Home/End: first/last",
|
|
2288
2802
|
"Left/Right: history metric (six metrics) \xB7 w: cycle 5m/1h/24h/7d",
|
|
@@ -2312,6 +2826,11 @@ class TerminalShell {
|
|
|
2312
2826
|
scroll = 0;
|
|
2313
2827
|
metric = 0;
|
|
2314
2828
|
sample = 0;
|
|
2829
|
+
modelDetail = false;
|
|
2830
|
+
modelRequests = false;
|
|
2831
|
+
modelFocus = 0;
|
|
2832
|
+
modelFocusId;
|
|
2833
|
+
modelViewport;
|
|
2315
2834
|
help = false;
|
|
2316
2835
|
editor;
|
|
2317
2836
|
input = "";
|
|
@@ -2357,7 +2876,7 @@ class TerminalShell {
|
|
|
2357
2876
|
this.task(this.controller.selectLocation(this.input));
|
|
2358
2877
|
} else
|
|
2359
2878
|
this.controller.setFilters({ [editor]: this.input });
|
|
2360
|
-
this.editor = editor === "agent" ? "model" : undefined;
|
|
2879
|
+
this.editor = editor === "agent" && view.screen !== "models" ? "model" : undefined;
|
|
2361
2880
|
this.input = this.editor === "model" ? view.filters.model : "";
|
|
2362
2881
|
this.scroll = 0;
|
|
2363
2882
|
this.notice = "";
|
|
@@ -2377,6 +2896,11 @@ class TerminalShell {
|
|
|
2377
2896
|
if (name === "escape") {
|
|
2378
2897
|
this.help = false;
|
|
2379
2898
|
this.scroll = 0;
|
|
2899
|
+
if (view.screen === "models" && this.modelDetail) {
|
|
2900
|
+
this.modelDetail = false;
|
|
2901
|
+
this.modelRequests = false;
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2380
2904
|
this.controller.setScreen("overview");
|
|
2381
2905
|
return;
|
|
2382
2906
|
}
|
|
@@ -2399,6 +2923,81 @@ class TerminalShell {
|
|
|
2399
2923
|
this.task(this.controller.refresh());
|
|
2400
2924
|
return;
|
|
2401
2925
|
}
|
|
2926
|
+
if (name === "4") {
|
|
2927
|
+
this.help = false;
|
|
2928
|
+
this.scroll = 0;
|
|
2929
|
+
this.modelDetail = false;
|
|
2930
|
+
this.modelRequests = false;
|
|
2931
|
+
this.controller.setScreen("models");
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
if (view.screen === "models" && !this.help) {
|
|
2935
|
+
if (name === "enter" || name === "return") {
|
|
2936
|
+
const model = view.models.result?.models[this.modelFocus];
|
|
2937
|
+
if (model) {
|
|
2938
|
+
this.controller.models.select(model.model ?? "");
|
|
2939
|
+
this.modelDetail = true;
|
|
2940
|
+
this.modelRequests = false;
|
|
2941
|
+
this.sample = 0;
|
|
2942
|
+
this.scroll = 0;
|
|
2943
|
+
}
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
if (name === "w") {
|
|
2947
|
+
this.controller.setWindow(historyWindows[(historyWindows.indexOf(view.models.windowMs) + 1) % historyWindows.length] ?? historyWindows[0]);
|
|
2948
|
+
this.sample = 0;
|
|
2949
|
+
this.scroll = 0;
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
if (name === "s" && !this.modelDetail) {
|
|
2953
|
+
this.controller.models.sort();
|
|
2954
|
+
this.scroll = 0;
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
if (name === "n" || name === "p") {
|
|
2958
|
+
if (!this.modelDetail || this.modelRequests) {
|
|
2959
|
+
this.controller.models.page(this.modelDetail ? "requests" : "models", name === "n" ? 1 : -1);
|
|
2960
|
+
if (!this.modelDetail) {
|
|
2961
|
+
this.modelFocus = 0;
|
|
2962
|
+
this.modelFocusId = undefined;
|
|
2963
|
+
}
|
|
2964
|
+
this.scroll = 0;
|
|
2965
|
+
}
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
if (this.modelDetail) {
|
|
2969
|
+
if (name === "d") {
|
|
2970
|
+
this.modelRequests = !this.modelRequests;
|
|
2971
|
+
this.scroll = 0;
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
if (name === "left" || name === "right") {
|
|
2975
|
+
this.metric = (this.metric + modelMetrics.length + (name === "left" ? -1 : 1)) % modelMetrics.length;
|
|
2976
|
+
this.sample = 0;
|
|
2977
|
+
this.scroll = 0;
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
if (name === "[" || name === "]") {
|
|
2981
|
+
this.sample = Math.max(0, Math.min((view.models.result?.buckets.length ?? 1) - 1, this.sample + (name === "[" ? -1 : 1)));
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
} else if (["up", "down", "j", "k", "home", "end", "pageup", "pagedown"].includes(name)) {
|
|
2985
|
+
const models = view.models.result?.models ?? [];
|
|
2986
|
+
const delta2 = name === "down" || name === "j" ? 1 : name === "up" || name === "k" ? -1 : name === "pagedown" ? page : -page;
|
|
2987
|
+
const viewport = this.modelViewport;
|
|
2988
|
+
if (viewport?.focus === this.modelFocus && viewport.end - viewport.start > viewport.height && name !== "home" && name !== "end" && (delta2 > 0 ? this.scroll + viewport.height < viewport.end : this.scroll > viewport.start)) {
|
|
2989
|
+
this.scroll = Math.max(viewport.start, Math.min(viewport.end - viewport.height, this.scroll + delta2));
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
if (name === "home")
|
|
2993
|
+
this.scroll = 0;
|
|
2994
|
+
if (name === "end")
|
|
2995
|
+
this.scroll = Number.MAX_SAFE_INTEGER;
|
|
2996
|
+
this.modelFocus = Math.max(0, Math.min(models.length - 1, name === "home" ? 0 : name === "end" ? models.length - 1 : this.modelFocus + delta2));
|
|
2997
|
+
this.modelFocusId = models[this.modelFocus]?.model ?? "";
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
2402
3001
|
if (["1", "2", "3", "return", "enter"].includes(name)) {
|
|
2403
3002
|
this.help = false;
|
|
2404
3003
|
this.scroll = 0;
|
|
@@ -2435,14 +3034,28 @@ class TerminalShell {
|
|
|
2435
3034
|
frame(width, height) {
|
|
2436
3035
|
width = Math.max(1, width);
|
|
2437
3036
|
height = Math.max(1, height);
|
|
3037
|
+
this.controller.models.setWidth(width);
|
|
2438
3038
|
const view = this.controller.getView();
|
|
2439
|
-
|
|
3039
|
+
if (view.screen === "models") {
|
|
3040
|
+
const models = view.models.result?.models ?? [];
|
|
3041
|
+
const identity2 = this.modelFocusId ?? view.models.result?.selected?.model ?? "";
|
|
3042
|
+
const focused = models.findIndex((m) => (m.model ?? "") === identity2);
|
|
3043
|
+
this.modelFocus = Math.max(0, focused < 0 ? Math.min(this.modelFocus, models.length - 1) : focused);
|
|
3044
|
+
if (view.models.status === "ready" && models.length)
|
|
3045
|
+
this.modelFocusId = models[this.modelFocus]?.model ?? "";
|
|
3046
|
+
}
|
|
3047
|
+
const identity = view.screen === "models" ? JSON.stringify([
|
|
3048
|
+
view.location,
|
|
3049
|
+
view.models.windowMs,
|
|
3050
|
+
view.filters.agent,
|
|
3051
|
+
view.models.result?.selected?.model
|
|
3052
|
+
]) : `${view.location}\0${view.selectedSessionId}\0${view.history.windowMs}`;
|
|
2440
3053
|
if (identity !== this.historyIdentity) {
|
|
2441
3054
|
this.sample = 0;
|
|
2442
3055
|
this.scroll = 0;
|
|
2443
3056
|
this.historyIdentity = identity;
|
|
2444
3057
|
}
|
|
2445
|
-
const footer = this.editor ? `${this.editor}> ${this.input} [Enter apply; Esc cancel${this.editor === "location" ? "; Tab remembered" : ""}]` : "? help q quit \xB7 1 overview 2 detail 3 history \xB7 / search f filters l location r refresh";
|
|
3058
|
+
const footer = this.editor ? `${this.editor}> ${this.input} [Enter apply; Esc cancel${this.editor === "location" ? "; Tab remembered" : ""}]` : "? help q quit \xB7 1 overview 2 detail 3 history 4 models \xB7 / search f filters l location r refresh";
|
|
2446
3059
|
if (height < 5)
|
|
2447
3060
|
return [
|
|
2448
3061
|
clip("Context Guard \xB7 ? help \xB7 q quit", width),
|
|
@@ -2455,7 +3068,7 @@ class TerminalShell {
|
|
|
2455
3068
|
...view.inventory.notices,
|
|
2456
3069
|
...view.health?.reasons ?? [],
|
|
2457
3070
|
...this.notice ? [this.notice] : [],
|
|
2458
|
-
...Object.values(view.filters).some(Boolean) ? [
|
|
3071
|
+
...view.screen !== "models" && Object.values(view.filters).some(Boolean) ? [
|
|
2459
3072
|
`Filters: search=${view.filters.search} agent=${view.filters.agent} model=${view.filters.model}`
|
|
2460
3073
|
] : []
|
|
2461
3074
|
];
|
|
@@ -2490,8 +3103,28 @@ class TerminalShell {
|
|
|
2490
3103
|
} else
|
|
2491
3104
|
body = wrapped([
|
|
2492
3105
|
...notices,
|
|
2493
|
-
...view.screen === "detail" ? terminalDetail(view.selected) : terminalHistory(view, width, this.metric, this.sample)
|
|
3106
|
+
...view.screen === "detail" ? terminalDetail(view.selected) : view.screen === "models" ? terminalModels(view, width, this.modelDetail, this.metric, this.sample, this.modelFocus, this.modelRequests) : terminalHistory(view, width, this.metric, this.sample)
|
|
2494
3107
|
], width);
|
|
3108
|
+
this.modelViewport = undefined;
|
|
3109
|
+
if (view.screen === "models" && !this.modelDetail && !this.help && view.models.result?.models.length) {
|
|
3110
|
+
const heights = (view.models.result?.models ?? []).map((model, index) => wrapped(terminalModelRow(model, index === this.modelFocus), width).length);
|
|
3111
|
+
const start = body.length - heights.slice(this.modelFocus).reduce((a, b) => a + b, 0);
|
|
3112
|
+
const end = start + (heights[this.modelFocus] ?? 0);
|
|
3113
|
+
this.modelViewport = {
|
|
3114
|
+
focus: this.modelFocus,
|
|
3115
|
+
start,
|
|
3116
|
+
end,
|
|
3117
|
+
height: available
|
|
3118
|
+
};
|
|
3119
|
+
if (end - start > available) {
|
|
3120
|
+
this.scroll = Math.max(start, Math.min(this.scroll, end - available));
|
|
3121
|
+
} else {
|
|
3122
|
+
if (start < this.scroll)
|
|
3123
|
+
this.scroll = start;
|
|
3124
|
+
if (end > this.scroll + available)
|
|
3125
|
+
this.scroll = end - available;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
2495
3128
|
this.scroll = Math.max(0, Math.min(this.scroll, Math.max(0, body.length - available)));
|
|
2496
3129
|
return [
|
|
2497
3130
|
...header,
|
|
@@ -2621,6 +3254,7 @@ var init_terminal = __esm(() => {
|
|
|
2621
3254
|
init_connection();
|
|
2622
3255
|
init_command();
|
|
2623
3256
|
init_view();
|
|
3257
|
+
init_modelview();
|
|
2624
3258
|
init_format();
|
|
2625
3259
|
init_terminalview();
|
|
2626
3260
|
});
|
|
@@ -2668,7 +3302,7 @@ function reconcile(a, b) {
|
|
|
2668
3302
|
partial: a.partial || b.partial
|
|
2669
3303
|
};
|
|
2670
3304
|
}
|
|
2671
|
-
function
|
|
3305
|
+
function analyzeModelRequests(records, options) {
|
|
2672
3306
|
const maxRequests = options.maxRequests ?? 1e5;
|
|
2673
3307
|
if (!Number.isSafeInteger(maxRequests) || maxRequests < 1 || maxRequests > 1e6)
|
|
2674
3308
|
throw new Error("maxRequests must be an integer from 1 to 1000000");
|
|
@@ -2729,7 +3363,7 @@ function aggregateModels(records, options) {
|
|
|
2729
3363
|
requests.set(key, { fact, conflict: false });
|
|
2730
3364
|
}
|
|
2731
3365
|
}
|
|
2732
|
-
const
|
|
3366
|
+
const accepted = [];
|
|
2733
3367
|
for (const { fact: r, conflict } of requests.values()) {
|
|
2734
3368
|
if (conflict) {
|
|
2735
3369
|
coverage.conflictingRequests++;
|
|
@@ -2746,6 +3380,22 @@ function aggregateModels(records, options) {
|
|
|
2746
3380
|
}
|
|
2747
3381
|
if (options.from !== undefined && r.completedAt < options.from || options.to !== undefined && r.completedAt >= options.to || options.model !== undefined && r.model !== options.model || options.agent !== undefined && r.agent !== options.agent)
|
|
2748
3382
|
continue;
|
|
3383
|
+
accepted.push({ ...r, completedAt: r.completedAt });
|
|
3384
|
+
}
|
|
3385
|
+
const models = summarizeModelRequests(accepted);
|
|
3386
|
+
coverage.partial = coverage.unscannedRecords > 0 || coverage.conflictingRequests > 0 || coverage.missingIdentityRecords > 0 || coverage.incompleteRequests > 0 || coverage.invalidTimingRequests > 0 || coverage.gapRecords > 0 || models.some((s) => s.partialRequests > 0 || s.legacyAttributionRequests > 0 || s.excludedTpsRequests > 0 || s.excludedTtftRequests > 0 || !s.model);
|
|
3387
|
+
return {
|
|
3388
|
+
schemaVersion: 1,
|
|
3389
|
+
location: options.location,
|
|
3390
|
+
models,
|
|
3391
|
+
coverage,
|
|
3392
|
+
requests: accepted
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
function summarizeModelRequests(requests) {
|
|
3396
|
+
const groups = new Map;
|
|
3397
|
+
for (const r of requests) {
|
|
3398
|
+
const duration = r.completedAt - r.startedAt;
|
|
2749
3399
|
let group = groups.get(r.model);
|
|
2750
3400
|
if (!group) {
|
|
2751
3401
|
group = {
|
|
@@ -2800,8 +3450,11 @@ function aggregateModels(records, options) {
|
|
|
2800
3450
|
}
|
|
2801
3451
|
return s;
|
|
2802
3452
|
}).sort((a, b) => (a.model ?? "").localeCompare(b.model ?? ""));
|
|
2803
|
-
|
|
2804
|
-
|
|
3453
|
+
return models;
|
|
3454
|
+
}
|
|
3455
|
+
function aggregateModels(records, options) {
|
|
3456
|
+
const { requests: _requests, ...result } = analyzeModelRequests(records, options);
|
|
3457
|
+
return result;
|
|
2805
3458
|
}
|
|
2806
3459
|
var nonnegative2 = (n) => n !== undefined && Number.isFinite(n) && n >= 0;
|
|
2807
3460
|
|