@cortexkit/aft-opencode 0.26.3 → 0.27.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.
@@ -11,7 +11,7 @@ import type { TuiPluginApi, TuiSlotPlugin, TuiThemeCurrent } from "@opencode-ai/
11
11
  import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js";
12
12
 
13
13
  import { AftRpcClient } from "../shared/rpc-client";
14
- import { type AftStatusSnapshot, coerceAftStatus } from "../shared/status";
14
+ import { type AftStatusSnapshot, coerceAftStatus, type StatusCompression } from "../shared/status";
15
15
 
16
16
  const SINGLE_BORDER = { type: "single" } as any;
17
17
  const REFRESH_DEBOUNCE_MS = 200;
@@ -35,6 +35,46 @@ function formatCount(n: number | null | undefined): string {
35
35
  return String(n);
36
36
  }
37
37
 
38
+ /** Tagged rows for the Compression section. Each scope (Session / Project)
39
+ * emits a "scope" header followed by two "stat" rows — Tokens Saved and
40
+ * Compression Ratio — so the renderer can use the same StatRow layout as
41
+ * Search Index / Semantic Index above. Pi's monospace overlay and the
42
+ * OpenCode TUI dialog/sidebar all consume this same shape. */
43
+ export type CompressionRow =
44
+ | { kind: "scope"; label: string }
45
+ | { kind: "stat"; label: string; value: string };
46
+
47
+ function appendScope(
48
+ rows: CompressionRow[],
49
+ label: string,
50
+ scope: {
51
+ events: number;
52
+ original_tokens: number;
53
+ compressed_tokens: number;
54
+ savings_tokens: number;
55
+ },
56
+ ): void {
57
+ const savings = scope.savings_tokens;
58
+ const pct = scope.original_tokens > 0 ? Math.round((savings / scope.original_tokens) * 100) : 0;
59
+ rows.push({ kind: "scope", label });
60
+ rows.push({ kind: "stat", label: "Tokens Saved", value: savings.toLocaleString("en-US") });
61
+ rows.push({ kind: "stat", label: "Compression Ratio", value: `${pct}%` });
62
+ }
63
+
64
+ export function formatCompressionSidebarRows(
65
+ compression: StatusCompression | undefined,
66
+ ): CompressionRow[] {
67
+ if (!compression || compression.project.events <= 0) return [];
68
+
69
+ const rows: CompressionRow[] = [];
70
+ if (compression.session.events > 0) {
71
+ appendScope(rows, "Session", compression.session);
72
+ }
73
+ appendScope(rows, "Project", compression.project);
74
+
75
+ return rows;
76
+ }
77
+
38
78
  // Map index status → (label, theme color name). The label is what we want
39
79
  // the user to see; the color encodes severity so the eye lands on warnings.
40
80
  function statusDisplay(status: string): { label: string; tone: "ok" | "warn" | "err" | "muted" } {
@@ -104,7 +144,10 @@ function getClient(directory: string): AftRpcClient {
104
144
  if (client) return client;
105
145
  const home = process.env.HOME || process.env.USERPROFILE || "";
106
146
  const dataHome = process.env.XDG_DATA_HOME || `${home}/.local/share`;
107
- const storageDir = `${dataHome}/opencode/storage/plugin/aft`;
147
+ // v0.27 moved AFT storage to the CortexKit root. Must match the server
148
+ // plugin's `resolveCortexKitStorageRoot()` or the sidebar will poll a
149
+ // stale legacy port file and never connect to the live RPC server.
150
+ const storageDir = `${dataHome}/cortexkit/aft`;
108
151
  client = new AftRpcClient(storageDir, directory);
109
152
  sidebarClients.set(directory, client);
110
153
  return client;
@@ -225,6 +268,7 @@ const SidebarContent = (props: {
225
268
  const semanticStatus = () => statusDisplay(s()?.semantic_index?.status ?? "disabled");
226
269
  const trigramBytes = () => s()?.disk?.trigram_disk_bytes ?? 0;
227
270
  const semanticBytes = () => s()?.disk?.semantic_disk_bytes ?? 0;
271
+ const compressionRows = () => formatCompressionSidebarRows(s()?.compression);
228
272
 
229
273
  // Degraded-mode reason → human-readable hint. Distinct strings per reason
230
274
  // because the UX direction is different: "home_root" tells the user to
@@ -292,6 +336,21 @@ const SidebarContent = (props: {
292
336
  </box>
293
337
  )}
294
338
 
339
+ {/* Lazy-bridge placeholder. AFT skips spawning the `aft` binary at
340
+ plugin init to keep memory/CPU low on OpenCode Desktop sessions
341
+ that have many projects pinned in the sidebar. The RPC server
342
+ returns a synthetic `cache_role === "not_initialized"` snapshot
343
+ until the first tool call routes through `callBridge()` and warms
344
+ the bridge. Show the explanatory message instead of empty status
345
+ rows so users understand why metrics are blank. */}
346
+ {s()?.cache_role === "not_initialized" && (
347
+ <box marginTop={1} width="100%">
348
+ <text fg={props.theme.textMuted}>
349
+ {s()!.message || "Waiting for first tool call to populate"}
350
+ </text>
351
+ </box>
352
+ )}
353
+
295
354
  {/* Search index */}
296
355
  <SectionHeader theme={props.theme} title="Search Index" />
297
356
  <StatRow
@@ -342,6 +401,26 @@ const SidebarContent = (props: {
342
401
  )}
343
402
  <StatRow theme={props.theme} label="Disk" value={formatBytes(semanticBytes())} tone="muted" />
344
403
 
404
+ {/* Compression aggregates. Tabular layout matching Search/Semantic
405
+ Index above: each scope ("Session", "Project") renders as a
406
+ subheader followed by two StatRows (Tokens Saved, Compression
407
+ Ratio). Keeps numbers right-aligned in the value column instead
408
+ of jamming them after the label on the same line. */}
409
+ {compressionRows().length > 0 && (
410
+ <>
411
+ <SectionHeader theme={props.theme} title="Compression" />
412
+ {compressionRows().map((row) =>
413
+ row.kind === "scope" ? (
414
+ <box width="100%">
415
+ <text fg={props.theme.text}>{row.label}</text>
416
+ </box>
417
+ ) : (
418
+ <StatRow theme={props.theme} label={row.label} value={row.value} tone="muted" />
419
+ ),
420
+ )}
421
+ </>
422
+ )}
423
+
345
424
  {/* Surface failures clearly so users know to act (install ONNX,
346
425
  fix config, etc.) rather than silently leaving the panel "off". */}
347
426
  {s()?.semantic_index?.status === "failed" && s()?.semantic_index?.error && (