@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/dist/rpc.js CHANGED
@@ -43,6 +43,12 @@ function isHistoryRead(value) {
43
43
  function isHistoryResult(value) {
44
44
  return valid(value, historyResultSchema);
45
45
  }
46
+ function isAnalyticsRead(value) {
47
+ 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");
48
+ }
49
+ function isAnalyticsResult(value) {
50
+ return valid(value, analyticsResultSchema);
51
+ }
46
52
  function record(x) {
47
53
  return typeof x === "object" && x !== null && !Array.isArray(x);
48
54
  }
@@ -100,7 +106,7 @@ var count, text, enumeration = (...values) => ({ type: "string", enum: values })
100
106
  type: "array",
101
107
  items,
102
108
  maxItems
103
- }), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc;
109
+ }), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc, modelSummarySchema, analyticsReadSchema, analyticsResultSchema, analyticsRpc;
104
110
  var init_rpc = __esm(() => {
105
111
  count = {
106
112
  type: "integer",
@@ -336,6 +342,123 @@ var init_rpc = __esm(() => {
336
342
  methods: { read: { input: historyReadSchema, output: historyResultSchema } },
337
343
  events: {}
338
344
  });
345
+ modelSummarySchema = object({
346
+ model: text,
347
+ sessions: count,
348
+ completedRequests: count,
349
+ partialRequests: count,
350
+ legacyAttributionRequests: count,
351
+ tpsRequests: count,
352
+ ttftRequests: count,
353
+ excludedTpsRequests: count,
354
+ excludedTtftRequests: count,
355
+ outputTokens: nonnegative,
356
+ durationMs: nonnegative,
357
+ averageTps: nonnegative,
358
+ meanTtftMs: nonnegative,
359
+ medianTtftMs: nonnegative,
360
+ p95TtftMs: nonnegative
361
+ }, [
362
+ "sessions",
363
+ "completedRequests",
364
+ "partialRequests",
365
+ "legacyAttributionRequests",
366
+ "tpsRequests",
367
+ "ttftRequests",
368
+ "excludedTpsRequests",
369
+ "excludedTtftRequests",
370
+ "outputTokens",
371
+ "durationMs"
372
+ ]);
373
+ analyticsReadSchema = object({
374
+ schemaVersion: { const: 1 },
375
+ from: count,
376
+ to: count,
377
+ buckets: { type: "integer", minimum: 1, maximum: 240 },
378
+ agent: text,
379
+ model: text,
380
+ unknownModel: { type: "boolean" },
381
+ sort: enumeration("requests", "tps", "ttft", "identity"),
382
+ snapshotId: text,
383
+ modelCursor: text,
384
+ requestCursor: text
385
+ }, ["schemaVersion", "from", "to", "buckets"]);
386
+ analyticsResultSchema = object({
387
+ schemaVersion: { const: 1 },
388
+ generatedAt: count,
389
+ expiresAt: count,
390
+ snapshotId: text,
391
+ enabled: { type: "boolean" },
392
+ from: count,
393
+ to: count,
394
+ agent: text,
395
+ models: array(modelSummarySchema, 50),
396
+ totalModels: count,
397
+ nextModelsCursor: text,
398
+ selected: modelSummarySchema,
399
+ buckets: array(object({ from: nonnegative, to: nonnegative, summary: modelSummarySchema }, ["from", "to"]), 240),
400
+ requests: array(object({
401
+ location: text,
402
+ sessionId: text,
403
+ requestId: text,
404
+ startedAt: count,
405
+ completedAt: count,
406
+ model: text,
407
+ agent: text,
408
+ attribution: enumeration("request", "legacy-record", "unknown"),
409
+ partial: { type: "boolean" },
410
+ outputTokens: nonnegative,
411
+ ttftMs: nonnegative
412
+ }, [
413
+ "location",
414
+ "sessionId",
415
+ "requestId",
416
+ "startedAt",
417
+ "completedAt",
418
+ "attribution",
419
+ "partial"
420
+ ]), 50),
421
+ nextRequestsCursor: text,
422
+ coverage: object({
423
+ source: enumeration("retained-request-records"),
424
+ scannedRecords: count,
425
+ unscannedRecords: count,
426
+ duplicateRecords: count,
427
+ conflictingRequests: count,
428
+ missingIdentityRecords: count,
429
+ incompleteRequests: count,
430
+ invalidTimingRequests: count,
431
+ gapRecords: count,
432
+ maxRequests: count,
433
+ partial: { type: "boolean" }
434
+ }),
435
+ metadata: historyResultSchema.properties.metadata ?? {},
436
+ gaps: array(count, 128),
437
+ omittedGaps: count
438
+ }, [
439
+ "schemaVersion",
440
+ "generatedAt",
441
+ "expiresAt",
442
+ "snapshotId",
443
+ "enabled",
444
+ "from",
445
+ "to",
446
+ "models",
447
+ "totalModels",
448
+ "buckets",
449
+ "requests",
450
+ "coverage",
451
+ "metadata",
452
+ "gaps",
453
+ "omittedGaps"
454
+ ]);
455
+ analyticsRpc = Rpc.define({
456
+ id: "context-guard.analytics.v1",
457
+ methods: {
458
+ read: { input: analyticsReadSchema, output: analyticsResultSchema }
459
+ },
460
+ events: {}
461
+ });
339
462
  });
340
463
  init_rpc();
341
464
 
@@ -355,7 +478,11 @@ export {
355
478
  isHistoryResult,
356
479
  isHistoryRead,
357
480
  isBatch,
481
+ isAnalyticsResult,
482
+ isAnalyticsRead,
358
483
  historyRpc,
359
484
  historyReadSchema,
360
- batchSchema
485
+ batchSchema,
486
+ analyticsRpc,
487
+ analyticsReadSchema
361
488
  };
package/dist/tui.js CHANGED
@@ -43,6 +43,12 @@ function isHistoryRead(value) {
43
43
  function isHistoryResult(value) {
44
44
  return valid(value, historyResultSchema);
45
45
  }
46
+ function isAnalyticsRead(value) {
47
+ 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");
48
+ }
49
+ function isAnalyticsResult(value) {
50
+ return valid(value, analyticsResultSchema);
51
+ }
46
52
  function record(x) {
47
53
  return typeof x === "object" && x !== null && !Array.isArray(x);
48
54
  }
@@ -100,7 +106,7 @@ var count, text, enumeration = (...values) => ({ type: "string", enum: values })
100
106
  type: "array",
101
107
  items,
102
108
  maxItems
103
- }), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc;
109
+ }), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc, modelSummarySchema, analyticsReadSchema, analyticsResultSchema, analyticsRpc;
104
110
  var init_rpc = __esm(() => {
105
111
  count = {
106
112
  type: "integer",
@@ -336,6 +342,123 @@ var init_rpc = __esm(() => {
336
342
  methods: { read: { input: historyReadSchema, output: historyResultSchema } },
337
343
  events: {}
338
344
  });
345
+ modelSummarySchema = object({
346
+ model: text,
347
+ sessions: count,
348
+ completedRequests: count,
349
+ partialRequests: count,
350
+ legacyAttributionRequests: count,
351
+ tpsRequests: count,
352
+ ttftRequests: count,
353
+ excludedTpsRequests: count,
354
+ excludedTtftRequests: count,
355
+ outputTokens: nonnegative,
356
+ durationMs: nonnegative,
357
+ averageTps: nonnegative,
358
+ meanTtftMs: nonnegative,
359
+ medianTtftMs: nonnegative,
360
+ p95TtftMs: nonnegative
361
+ }, [
362
+ "sessions",
363
+ "completedRequests",
364
+ "partialRequests",
365
+ "legacyAttributionRequests",
366
+ "tpsRequests",
367
+ "ttftRequests",
368
+ "excludedTpsRequests",
369
+ "excludedTtftRequests",
370
+ "outputTokens",
371
+ "durationMs"
372
+ ]);
373
+ analyticsReadSchema = object({
374
+ schemaVersion: { const: 1 },
375
+ from: count,
376
+ to: count,
377
+ buckets: { type: "integer", minimum: 1, maximum: 240 },
378
+ agent: text,
379
+ model: text,
380
+ unknownModel: { type: "boolean" },
381
+ sort: enumeration("requests", "tps", "ttft", "identity"),
382
+ snapshotId: text,
383
+ modelCursor: text,
384
+ requestCursor: text
385
+ }, ["schemaVersion", "from", "to", "buckets"]);
386
+ analyticsResultSchema = object({
387
+ schemaVersion: { const: 1 },
388
+ generatedAt: count,
389
+ expiresAt: count,
390
+ snapshotId: text,
391
+ enabled: { type: "boolean" },
392
+ from: count,
393
+ to: count,
394
+ agent: text,
395
+ models: array(modelSummarySchema, 50),
396
+ totalModels: count,
397
+ nextModelsCursor: text,
398
+ selected: modelSummarySchema,
399
+ buckets: array(object({ from: nonnegative, to: nonnegative, summary: modelSummarySchema }, ["from", "to"]), 240),
400
+ requests: array(object({
401
+ location: text,
402
+ sessionId: text,
403
+ requestId: text,
404
+ startedAt: count,
405
+ completedAt: count,
406
+ model: text,
407
+ agent: text,
408
+ attribution: enumeration("request", "legacy-record", "unknown"),
409
+ partial: { type: "boolean" },
410
+ outputTokens: nonnegative,
411
+ ttftMs: nonnegative
412
+ }, [
413
+ "location",
414
+ "sessionId",
415
+ "requestId",
416
+ "startedAt",
417
+ "completedAt",
418
+ "attribution",
419
+ "partial"
420
+ ]), 50),
421
+ nextRequestsCursor: text,
422
+ coverage: object({
423
+ source: enumeration("retained-request-records"),
424
+ scannedRecords: count,
425
+ unscannedRecords: count,
426
+ duplicateRecords: count,
427
+ conflictingRequests: count,
428
+ missingIdentityRecords: count,
429
+ incompleteRequests: count,
430
+ invalidTimingRequests: count,
431
+ gapRecords: count,
432
+ maxRequests: count,
433
+ partial: { type: "boolean" }
434
+ }),
435
+ metadata: historyResultSchema.properties.metadata ?? {},
436
+ gaps: array(count, 128),
437
+ omittedGaps: count
438
+ }, [
439
+ "schemaVersion",
440
+ "generatedAt",
441
+ "expiresAt",
442
+ "snapshotId",
443
+ "enabled",
444
+ "from",
445
+ "to",
446
+ "models",
447
+ "totalModels",
448
+ "buckets",
449
+ "requests",
450
+ "coverage",
451
+ "metadata",
452
+ "gaps",
453
+ "omittedGaps"
454
+ ]);
455
+ analyticsRpc = Rpc.define({
456
+ id: "context-guard.analytics.v1",
457
+ methods: {
458
+ read: { input: analyticsReadSchema, output: analyticsResultSchema }
459
+ },
460
+ events: {}
461
+ });
339
462
  });
340
463
 
341
464
  // src/family.ts
@@ -834,9 +957,9 @@ var init_format = __esm(() => {
834
957
  });
835
958
 
836
959
  // src/tui/chartformat.ts
837
- function columns2(points, from, to, width, gaps = []) {
960
+ function columns2(points, from, to, width, gaps = [], halfOpen = false) {
838
961
  const size = Math.max(1, Math.min(120, Math.floor(width))), values = Array.from({ length: size });
839
- const column = (time) => Math.max(0, Math.min(size - 1, Math.floor((time - from) / Math.max(1, to - from) * (size - 1))));
962
+ const column = (time) => Math.max(0, Math.min(size - 1, Math.floor((time - from) / Math.max(1, to - from) * (halfOpen ? size : size - 1))));
840
963
  let min = Infinity, max = -Infinity;
841
964
  for (const point of points) {
842
965
  if (point.time < from || point.time > to || point.value === undefined || !Number.isFinite(point.value) || point.value < 0)
@@ -860,12 +983,12 @@ function sparkline(points, from, to, width, gaps = []) {
860
983
  chars[column(time)] = "\u2506";
861
984
  return { line: chars.join(""), ...min === Infinity ? {} : { min, max } };
862
985
  }
863
- function metricPlot(points, from, to, width, gaps = []) {
986
+ function metricPlot(points, from, to, width, gaps = [], halfOpen = false) {
864
987
  const label = (value) => value >= 1e6 ? `${(value / 1e6).toFixed(1)}M` : value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value.toFixed(1);
865
988
  const peak = Math.max(0, ...points.filter((p) => p.time >= from && p.time <= to && Number.isFinite(p.value)).map((p) => p.value ?? 0));
866
989
  const scale = Math.max(1, peak);
867
990
  const gutter = width >= 20 ? Math.max(label(scale).length, 3) : 0;
868
- const { values, column } = columns2(points, from, to, Math.max(1, width - gutter - 2), gaps);
991
+ const { values, column } = columns2(points, from, to, Math.max(1, halfOpen ? Math.min(points.length, width - gutter - 2) : width - gutter - 2), gaps, halfOpen);
869
992
  const gapColumns = new Set(gaps.filter((time) => time >= from && time <= to).map(column));
870
993
  const height = 6;
871
994
  const blocks = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
@@ -1,10 +1,12 @@
1
1
  import { type HistoryRpcPort } from "./historyserver.js";
2
+ import { type AnalyticsRpcPort } from "./modelanalyticsserver.js";
2
3
  import { type SnapshotRpcPort } from "./snapshotserver.js";
3
4
  import type { Plugin } from "@opencode/plugin";
4
5
  import type { TelemetrySink } from "./types.js";
5
6
  export interface Host {
6
7
  rpc?: SnapshotRpcPort;
7
8
  historyRpc?: HistoryRpcPort;
9
+ analyticsRpc?: AnalyticsRpcPort;
8
10
  app: {
9
11
  version: string;
10
12
  };
@@ -9,3 +9,4 @@ export * from "./family.js";
9
9
  export * from "./performance.js";
10
10
  export * from "./charts.js";
11
11
  export * from "./modelanalytics.js";
12
+ export * from "./modelseries.js";
@@ -1,5 +1,5 @@
1
1
  import type { DashboardConnectionOptions } from "./connection.js";
2
- export declare const dashboardHelp = "Usage: context-guard dashboard tui [options]\nOpen a read-only live terminal dashboard for an existing OpenCode server.\n\nStart here\n 1. Keep an OpenCode server running with Context Guard registered.\n 2. Close OpenCode's native TUI before opening the standalone dashboard.\n 3. In the owning project directory, run: context-guard dashboard tui\n\nOptions\n --directory PATH Owning project directory (local default: current directory)\n --server URL Connect to an explicit server; requires --directory with\n an absolute server-side project path. Use HTTPS except\n on loopback. Credentials in URLs are rejected.\n -h, --help Show this help\n\nExamples\n context-guard dashboard tui\n context-guard dashboard tui --directory /absolute/project\n context-guard dashboard tui --server https://server.example --directory /server/project\n\nRequirements\n Interactive terminal and Bun >=1.3.0. The OpenTUI renderer is installed\n automatically with CGP. This command never starts a server.\n\nAuthentication (explicit servers)\n CONTEXT_GUARD_PASSWORD Server password\n CONTEXT_GUARD_USERNAME Server username (default: opencode)\n\nKeys\n 1/2/3 overview/detail/history; arrows or j/k move; / search; f filters;\n l location; w history window; r refresh; ? help; q or Ctrl+C quit.";
2
+ export declare const dashboardHelp = "Usage: context-guard dashboard tui [options]\nOpen a read-only live terminal dashboard for an existing OpenCode server.\n\nStart here\n 1. Keep an OpenCode server running with Context Guard registered.\n 2. Open a separate terminal tab, window or pane for the dashboard.\n 3. In the same project directory, run: context-guard dashboard tui\n Keep working in OpenCode; the dashboard reads live updates from its server.\n\nOptions\n --directory PATH Owning project directory (local default: current directory)\n --server URL Connect to an explicit server; requires --directory with\n an absolute server-side project path. Use HTTPS except\n on loopback. Credentials in URLs are rejected.\n -h, --help Show this help\n\nExamples\n context-guard dashboard tui\n context-guard dashboard tui --directory /absolute/project\n context-guard dashboard tui --server https://server.example --directory /server/project\n\nRequirements\n Interactive terminal and Bun >=1.3.0. The OpenTUI renderer is installed\n automatically with CGP. This command never starts a server.\n\nAuthentication (explicit servers)\n CONTEXT_GUARD_PASSWORD Server password\n CONTEXT_GUARD_USERNAME Server username (default: opencode)\n\nKeys\n 1/2/3/4 overview/detail/history/models; arrows or j/k move; Enter detail/chart;\n Models: Left/Right metric, [/] bucket, d requests, n/p pages, s sort;\n / session search; f filters (Models: agent); l location; w time window;\n r refresh; ? help; q or Ctrl+C quit.";
3
3
  export declare function parseDashboardArgs(args: readonly string[], cwd: string, env: Readonly<Record<string, string | undefined>>): DashboardConnectionOptions | "help";
4
4
  export declare function checkTerminalRuntime(version: string, interactive: boolean): void;
5
5
  export declare function loadTerminalRenderer(): Promise<typeof import("@opentui/core")>;
@@ -2,6 +2,7 @@ import { Service } from "@opencode/client/service";
2
2
  import type { HistoryChartRead } from "../historycharts.js";
3
3
  import type { SnapshotTransport } from "../tui/transport.js";
4
4
  import type { DashboardPort, SessionPageInput } from "./port.js";
5
+ import type { AnalyticsRead } from "../modelseries.js";
5
6
  export interface DashboardConnectionOptions {
6
7
  readonly directory: string;
7
8
  readonly server?: string;
@@ -32,5 +33,6 @@ export declare function connectDashboard(options: DashboardConnectionOptions, di
32
33
  readonly transport: SnapshotTransport;
33
34
  }>;
34
35
  history(input: HistoryChartRead, signal: AbortSignal): Promise<import("../historycharts.js").HistoryChartResult>;
36
+ analytics?(input: AnalyticsRead, signal: AbortSignal): Promise<import("../modelseries.js").AnalyticsResult>;
35
37
  }>;
36
38
  export type DashboardConnection = Awaited<ReturnType<typeof connectDashboard>>;
@@ -1,5 +1,6 @@
1
1
  import { type InventoryLimits } from "./inventory.js";
2
2
  import type { DashboardPort } from "./port.js";
3
+ import { ModelsController } from "./models.js";
3
4
  import { type DashboardView, type DashboardScreen, type DashboardFilters, type HistoryWindow } from "./view.js";
4
5
  export interface DashboardControllerOptions {
5
6
  readonly limits?: InventoryLimits;
@@ -32,6 +33,7 @@ export declare class DashboardController {
32
33
  private refreshTimer;
33
34
  private ageTimer;
34
35
  private view;
36
+ readonly models: ModelsController;
35
37
  constructor(port: DashboardPort, options?: DashboardControllerOptions);
36
38
  private createLocation;
37
39
  getView(): DashboardView;
@@ -44,6 +46,7 @@ export declare class DashboardController {
44
46
  setScreen(screen: DashboardScreen): void;
45
47
  setWindow(windowMs: HistoryWindow): void;
46
48
  setFilters(filters: Partial<DashboardFilters>): void;
49
+ private configureModels;
47
50
  refresh(): Promise<void>;
48
51
  private current;
49
52
  private scan;
@@ -0,0 +1,42 @@
1
+ import type { AnalyticsRead, AnalyticsResult } from "../modelseries.js";
2
+ import { type DashboardPort } from "./port.js";
3
+ import type { HistoryWindow } from "./view.js";
4
+ export interface DashboardModels {
5
+ status: "idle" | "loading" | "ready" | "disabled" | "unsupported" | "unavailable";
6
+ stale: boolean;
7
+ sort: NonNullable<AnalyticsRead["sort"]>;
8
+ windowMs: HistoryWindow;
9
+ modelPage: number;
10
+ requestPage: number;
11
+ result?: AnalyticsResult;
12
+ }
13
+ /** Location-scoped request state; it never opens a monitor/event subscription. */
14
+ export declare class ModelsController {
15
+ private readonly changed;
16
+ private port;
17
+ private active;
18
+ private key;
19
+ private agent;
20
+ private seedModel;
21
+ private selection;
22
+ private resolution;
23
+ private from;
24
+ private to;
25
+ private generation;
26
+ private abort;
27
+ private pending;
28
+ private modelPages;
29
+ private requestPages;
30
+ private state;
31
+ constructor(changed: () => void);
32
+ getView(): DashboardModels;
33
+ configure(port: DashboardPort, active: boolean, windowMs: HistoryWindow, agent: string, seedModel: string, now: number): void;
34
+ cancel(): void;
35
+ stop(): void;
36
+ refresh(now: number): Promise<void>;
37
+ setWidth(width: number): void;
38
+ select(model: string): void;
39
+ sort(): void;
40
+ page(kind: "models" | "requests", direction: -1 | 1): void;
41
+ private query;
42
+ }
@@ -0,0 +1,30 @@
1
+ import type { ModelSummary } from "../modelanalytics.js";
2
+ import type { DashboardView } from "./view.js";
3
+ export declare const modelMetrics: readonly [{
4
+ readonly key: "averageTps";
5
+ readonly label: "Weighted request TPS";
6
+ readonly unit: "t/s";
7
+ readonly divisor: 1;
8
+ }, {
9
+ readonly key: "medianTtftMs";
10
+ readonly label: "TTFT median ~";
11
+ readonly unit: "s";
12
+ readonly divisor: 1000;
13
+ }, {
14
+ readonly key: "p95TtftMs";
15
+ readonly label: "TTFT p95 ~";
16
+ readonly unit: "s";
17
+ readonly divisor: 1000;
18
+ }, {
19
+ readonly key: "completedRequests";
20
+ readonly label: "Completed requests";
21
+ readonly unit: "requests";
22
+ readonly divisor: 1;
23
+ }, {
24
+ readonly key: "outputTokens";
25
+ readonly label: "Reported output";
26
+ readonly unit: "tokens";
27
+ readonly divisor: 1;
28
+ }];
29
+ export declare function terminalModelRow(model: Readonly<ModelSummary>, focused: boolean): string[];
30
+ export declare function terminalModels(view: DashboardView, width: number, detail: boolean, metricIndex: number, sample: number, focus: number, requests: boolean): string[];
@@ -1,6 +1,11 @@
1
1
  import type { ExecutionStatus } from "../family.js";
2
2
  import type { HistoryChartRead, HistoryChartResult } from "../historycharts.js";
3
3
  import type { SnapshotTransport } from "../tui/transport.js";
4
+ import type { AnalyticsRead, AnalyticsResult } from "../modelseries.js";
5
+ export declare class AnalyticsUnavailable extends Error {
6
+ readonly reason: "unsupported" | "unavailable";
7
+ constructor(reason: "unsupported" | "unavailable");
8
+ }
4
9
  /** The inventory boundary deliberately excludes titles, metadata, and content. */
5
10
  export interface DashboardSession {
6
11
  readonly sessionId: string;
@@ -34,6 +39,7 @@ export interface DashboardPort {
34
39
  readonly transport: SnapshotTransport;
35
40
  }>;
36
41
  history(input: HistoryChartRead, signal: AbortSignal): Promise<HistoryChartResult>;
42
+ analytics?(input: AnalyticsRead, signal: AbortSignal): Promise<AnalyticsResult>;
37
43
  }
38
44
  export interface InventoryNode extends DashboardSession {
39
45
  readonly status: ExecutionStatus;
@@ -15,6 +15,11 @@ export declare class TerminalShell {
15
15
  scroll: number;
16
16
  metric: number;
17
17
  sample: number;
18
+ modelDetail: boolean;
19
+ modelRequests: boolean;
20
+ modelFocus: number;
21
+ private modelFocusId;
22
+ private modelViewport;
18
23
  help: boolean;
19
24
  editor: Editor | undefined;
20
25
  input: string;
@@ -3,12 +3,13 @@ import type { MonitorHealth } from "../snapshot.js";
3
3
  import type { HistoryChartResult } from "../historycharts.js";
4
4
  import type { Connection } from "../tui/transport.js";
5
5
  import type { InventoryNotice } from "./inventory.js";
6
+ import type { DashboardModels } from "./models.js";
6
7
  export type Immutable<T> = T extends object ? {
7
8
  readonly [K in keyof T]: Immutable<T[K]>;
8
9
  } : T;
9
10
  export declare const historyWindows: readonly [300000, 3600000, 86400000, 604800000];
10
11
  export type HistoryWindow = (typeof historyWindows)[number];
11
- export type DashboardScreen = "overview" | "detail" | "history";
12
+ export type DashboardScreen = "overview" | "detail" | "history" | "models";
12
13
  export interface DashboardFilters {
13
14
  readonly search: string;
14
15
  readonly agent: string;
@@ -72,6 +73,7 @@ export type DashboardView = Immutable<{
72
73
  selectedSessionId?: string;
73
74
  selected?: DashboardRow;
74
75
  history: DashboardHistory;
76
+ models: DashboardModels;
75
77
  notices: readonly string[];
76
78
  }>;
77
79
  /** Clone before freezing so transport-owned objects never become UI-owned state. */
@@ -38,6 +38,7 @@ export declare class PerformanceHistory {
38
38
  }
39
39
  export declare function scalarPerformance(p: PerformanceSnapshot): PerformanceSnapshot;
40
40
  export interface HistoryFilter {
41
+ signal?: AbortSignal;
41
42
  sessionId?: string;
42
43
  agent?: string;
43
44
  model?: string;
@@ -57,4 +57,12 @@ export interface ModelAnalyticsResult {
57
57
  };
58
58
  }
59
59
  /** Aggregate raw request records before any chart downsampling or session limits. */
60
+ export declare function analyzeModelRequests(records: readonly HistoryRecord[], options: ModelAnalyticsOptions): ModelAnalyticsResult & {
61
+ requests: CompletedAnalyticsRequest[];
62
+ };
63
+ export interface CompletedAnalyticsRequest extends AnalyticsRequest {
64
+ completedAt: number;
65
+ }
66
+ /** Summarize already reconciled requests, including independent metric eligibility. */
67
+ export declare function summarizeModelRequests(requests: readonly CompletedAnalyticsRequest[]): ModelSummary[];
60
68
  export declare function aggregateModels(records: readonly HistoryRecord[], options: ModelAnalyticsOptions): ModelAnalyticsResult;
@@ -0,0 +1,16 @@
1
+ import type { RpcHandlers, RpcRegistration } from "@opencode/plugin/promise/rpc";
2
+ import { readPerformanceHistory } from "./history.js";
3
+ import { type AnalyticsRead, type AnalyticsResult } from "./modelseries.js";
4
+ import { analyticsRpc } from "./rpc.js";
5
+ export interface AnalyticsRpcPort {
6
+ register(definition: typeof analyticsRpc, handlers: RpcHandlers<typeof analyticsRpc>): Promise<Pick<RpcRegistration<typeof analyticsRpc>, "dispose">>;
7
+ }
8
+ /** One instance owns exactly one server-selected location/archive; no client paths. */
9
+ export declare function createAnalyticsService(path: string, location: string, enabled: boolean, options?: {
10
+ now?: () => number;
11
+ read?: typeof readPerformanceHistory;
12
+ }): {
13
+ read(input: AnalyticsRead, signal: AbortSignal): Promise<AnalyticsResult>;
14
+ dispose(): void;
15
+ };
16
+ export declare function serveModelAnalytics(port: AnalyticsRpcPort | undefined, path: string, location: string, enabled: boolean): Promise<() => void>;
@@ -0,0 +1,43 @@
1
+ import { type CompletedAnalyticsRequest, type ModelAnalyticsResult, type ModelSummary } from "./modelanalytics.js";
2
+ import type { HistoryMetadata } from "./history.js";
3
+ export interface ModelBucket {
4
+ from: number;
5
+ to: number;
6
+ summary?: ModelSummary;
7
+ }
8
+ export interface AnalyticsRead {
9
+ schemaVersion: 1;
10
+ from: number;
11
+ to: number;
12
+ buckets: number;
13
+ agent?: string;
14
+ model?: string;
15
+ unknownModel?: boolean;
16
+ sort?: "requests" | "tps" | "ttft" | "identity";
17
+ snapshotId?: string;
18
+ modelCursor?: string;
19
+ requestCursor?: string;
20
+ }
21
+ export interface AnalyticsResult {
22
+ schemaVersion: 1;
23
+ generatedAt: number;
24
+ expiresAt: number;
25
+ snapshotId: string;
26
+ enabled: boolean;
27
+ from: number;
28
+ to: number;
29
+ agent?: string;
30
+ models: ModelSummary[];
31
+ totalModels: number;
32
+ nextModelsCursor?: string;
33
+ selected?: ModelSummary;
34
+ buckets: ModelBucket[];
35
+ requests: CompletedAnalyticsRequest[];
36
+ nextRequestsCursor?: string;
37
+ coverage: ModelAnalyticsResult["coverage"];
38
+ metadata: HistoryMetadata;
39
+ gaps: number[];
40
+ omittedGaps: number;
41
+ }
42
+ /** Requests belong to half-open completion buckets; rates never average rates. */
43
+ export declare function bucketModelRequests(requests: readonly CompletedAnalyticsRequest[], from: number, to: number, count: number): ModelBucket[];
@@ -124,6 +124,34 @@ export declare const historyRpc: {
124
124
  };
125
125
  export declare function isHistoryRead(value: unknown): value is import("./historycharts.js").HistoryChartRead;
126
126
  export declare function isHistoryResult(value: unknown): value is import("./historycharts.js").HistoryChartResult;
127
+ export declare const analyticsReadSchema: {
128
+ type: "object";
129
+ properties: Record<string, object>;
130
+ required: string[];
131
+ additionalProperties: boolean;
132
+ };
133
+ export declare const analyticsRpc: {
134
+ readonly id: "context-guard.analytics.v1";
135
+ readonly methods: {
136
+ readonly read: {
137
+ readonly input: {
138
+ type: "object";
139
+ properties: Record<string, object>;
140
+ required: string[];
141
+ additionalProperties: boolean;
142
+ };
143
+ readonly output: {
144
+ type: "object";
145
+ properties: Record<string, object>;
146
+ required: string[];
147
+ additionalProperties: boolean;
148
+ };
149
+ };
150
+ };
151
+ readonly events: {};
152
+ };
153
+ export declare function isAnalyticsRead(value: unknown): value is import("./modelseries.js").AnalyticsRead;
154
+ export declare function isAnalyticsResult(value: unknown): value is import("./modelseries.js").AnalyticsResult;
127
155
  export declare function isRead(value: unknown): value is SnapshotRead;
128
156
  export declare function isBatch(value: unknown): value is SnapshotBatch;
129
157
  export declare function isResult(value: unknown): value is SnapshotReadResult;
@@ -42,5 +42,5 @@ export declare const chartMetrics: readonly [{
42
42
  export declare function metricPlot(points: readonly {
43
43
  time: number;
44
44
  value?: number;
45
- }[], from: number, to: number, width: number, gaps?: readonly number[]): string[];
45
+ }[], from: number, to: number, width: number, gaps?: readonly number[], halfOpen?: boolean): string[];
46
46
  export declare function historyChartLines(result: HistoryChartResult, width: number, from: number, to: number, metricIndex?: number): string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cesarus777/opencode-cgp",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {