@cortexkit/aft-opencode 0.35.2 → 0.35.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AA6MlD;;;;;;;;;;;;;;;;;GAiBG;AAQH,QAAA,MAAM,MAAM,EAAE,MAA6D,CAAC;AA80B5E,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AA2MlD;;;;;;;;;;;;;;;;;GAiBG;AAQH,QAAA,MAAM,MAAM,EAAE,MAA6D,CAAC;AA80B5E,eAAe,MAAM,CAAC"}
package/dist/index.js CHANGED
@@ -12064,7 +12064,6 @@ class BinaryBridge {
12064
12064
  if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
12065
12065
  return;
12066
12066
  this.cachedStatus = snapshot;
12067
- this.logVia("Received status_changed push frame; cached AFT status snapshot");
12068
12067
  for (const listener of this.statusListeners) {
12069
12068
  this.deliverStatusSnapshot(listener, this.cachedStatus);
12070
12069
  }
@@ -30598,7 +30597,16 @@ function showToast(ctx, title, message, variant = "info", duration3 = 3000) {
30598
30597
  // src/lsp-auto-install.ts
30599
30598
  import { spawn as spawn3 } from "node:child_process";
30600
30599
  import { createHash as createHash4 } from "node:crypto";
30601
- import { createReadStream, mkdirSync as mkdirSync8, readFileSync as readFileSync11, renameSync as renameSync6, rmSync as rmSync5, statSync as statSync6 } from "node:fs";
30600
+ import {
30601
+ createReadStream,
30602
+ existsSync as existsSync12,
30603
+ mkdirSync as mkdirSync8,
30604
+ readFileSync as readFileSync11,
30605
+ renameSync as renameSync6,
30606
+ rmSync as rmSync5,
30607
+ statSync as statSync6,
30608
+ writeFileSync as writeFileSync8
30609
+ } from "node:fs";
30602
30610
  import { join as join16 } from "node:path";
30603
30611
 
30604
30612
  // src/lsp-cache.ts
@@ -31173,6 +31181,17 @@ async function resolveTargetVersion(spec, config2, fetchImpl = fetch) {
31173
31181
  writeVersionCheck(spec.npm, probe.version);
31174
31182
  return { version: probe.version, pinned: false, probe };
31175
31183
  }
31184
+ function ensureInstallAnchor(cwd) {
31185
+ try {
31186
+ const stub = join16(cwd, "package.json");
31187
+ if (!existsSync12(stub)) {
31188
+ writeFileSync8(stub, `${JSON.stringify({ name: "aft-lsp-cache", version: "0.0.0", private: true })}
31189
+ `);
31190
+ }
31191
+ } catch (err) {
31192
+ warn2(`[lsp] could not write package.json stub in ${cwd}: ${err}`);
31193
+ }
31194
+ }
31176
31195
  function runInstall(spec, version2, cwd, signal) {
31177
31196
  return new Promise((resolve3) => {
31178
31197
  const target = `${spec.npm}@${version2}`;
@@ -31188,6 +31207,7 @@ function runInstall(spec, version2, cwd, signal) {
31188
31207
  resolve3(false);
31189
31208
  return;
31190
31209
  }
31210
+ ensureInstallAnchor(cwd);
31191
31211
  const child = spawn3(npm.command, ["install", "--no-save", "--ignore-scripts", "--silent", target], {
31192
31212
  stdio: ["ignore", "pipe", "pipe"],
31193
31213
  cwd,
@@ -31441,7 +31461,7 @@ import {
31441
31461
  copyFileSync as copyFileSync4,
31442
31462
  createReadStream as createReadStream2,
31443
31463
  createWriteStream as createWriteStream3,
31444
- existsSync as existsSync12,
31464
+ existsSync as existsSync13,
31445
31465
  lstatSync as lstatSync2,
31446
31466
  mkdirSync as mkdirSync9,
31447
31467
  readdirSync as readdirSync4,
@@ -31452,7 +31472,7 @@ import {
31452
31472
  rmSync as rmSync6,
31453
31473
  statSync as statSync7,
31454
31474
  unlinkSync as unlinkSync6,
31455
- writeFileSync as writeFileSync8
31475
+ writeFileSync as writeFileSync9
31456
31476
  } from "node:fs";
31457
31477
  import { dirname as dirname8, join as join17, relative as relative2, resolve as resolve3 } from "node:path";
31458
31478
  import { Readable as Readable3 } from "node:stream";
@@ -31603,7 +31623,7 @@ function writeGithubInstalledMetaIn(installDir, version2, binarySha256, archiveS
31603
31623
  binarySha256,
31604
31624
  ...archiveSha256 ? { archiveSha256 } : {}
31605
31625
  };
31606
- writeFileSync8(join17(installDir, INSTALLED_META_FILE2), JSON.stringify(meta3), "utf8");
31626
+ writeFileSync9(join17(installDir, INSTALLED_META_FILE2), JSON.stringify(meta3), "utf8");
31607
31627
  } catch (err) {
31608
31628
  warn2(`[lsp] failed to write github installed metadata in ${installDir}: ${err}`);
31609
31629
  }
@@ -32072,7 +32092,7 @@ async function downloadAndInstall(spec, tag, assets, platform3, arch, fetchImpl,
32072
32092
  } catch {}
32073
32093
  }
32074
32094
  const innerBinaryPath = join17(extractDir, spec.binaryPathInArchive(platform3, arch, version2));
32075
- if (!existsSync12(innerBinaryPath)) {
32095
+ if (!existsSync13(innerBinaryPath)) {
32076
32096
  error2(`[lsp] ${spec.id}: extracted binary not found at ${innerBinaryPath}`);
32077
32097
  return null;
32078
32098
  }
@@ -32156,7 +32176,7 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
32156
32176
  if (!host) {
32157
32177
  for (const spec of GITHUB_LSP_TABLE) {
32158
32178
  try {
32159
- if (existsSync12(ghBinDir(spec))) {
32179
+ if (existsSync13(ghBinDir(spec))) {
32160
32180
  cachedBinDirs.push(ghBinDir(spec));
32161
32181
  }
32162
32182
  } catch {}
@@ -32403,7 +32423,7 @@ function writeTerminal(terminal, data) {
32403
32423
 
32404
32424
  // src/shared/rpc-server.ts
32405
32425
  import { randomBytes as randomBytes2 } from "node:crypto";
32406
- import { mkdirSync as mkdirSync10, renameSync as renameSync8, unlinkSync as unlinkSync7, writeFileSync as writeFileSync9 } from "node:fs";
32426
+ import { mkdirSync as mkdirSync10, renameSync as renameSync8, unlinkSync as unlinkSync7, writeFileSync as writeFileSync10 } from "node:fs";
32407
32427
  import { createServer } from "node:http";
32408
32428
  import { dirname as dirname9, join as join19 } from "node:path";
32409
32429
 
@@ -32456,7 +32476,7 @@ class AftRpcServer {
32456
32476
  const dir = dirname9(this.portFilePath);
32457
32477
  mkdirSync10(dir, { recursive: true, mode: 448 });
32458
32478
  const tmpPath = `${this.portFilePath}.tmp`;
32459
- writeFileSync9(tmpPath, JSON.stringify({ port: this.port, token: this.token }), {
32479
+ writeFileSync10(tmpPath, JSON.stringify({ port: this.port, token: this.token }), {
32460
32480
  encoding: "utf-8",
32461
32481
  mode: 384
32462
32482
  });
@@ -32632,6 +32652,20 @@ function readCompression(value) {
32632
32652
  session: readCompressionAggregate(compression.session)
32633
32653
  };
32634
32654
  }
32655
+ function readStatusBar(value) {
32656
+ if (typeof value !== "object" || value === null)
32657
+ return;
32658
+ const bar = asRecord(value);
32659
+ return {
32660
+ errors: readNumber(bar.errors),
32661
+ warnings: readNumber(bar.warnings),
32662
+ dead_code: readNumber(bar.dead_code),
32663
+ unused_exports: readNumber(bar.unused_exports),
32664
+ duplicates: readNumber(bar.duplicates),
32665
+ todos: readNumber(bar.todos),
32666
+ tier2_stale: readBoolean(bar.tier2_stale)
32667
+ };
32668
+ }
32635
32669
  function formatFlag(enabled) {
32636
32670
  return enabled ? "enabled" : "disabled";
32637
32671
  }
@@ -32725,6 +32759,7 @@ function coerceAftStatus(response) {
32725
32759
  checkpoints: readNumber(session.checkpoints)
32726
32760
  },
32727
32761
  compression: readCompression(response.compression),
32762
+ status_bar: readStatusBar(response.status_bar),
32728
32763
  message: readString(response.message, "")
32729
32764
  };
32730
32765
  }
@@ -32780,6 +32815,10 @@ function formatStatusMarkdown(status) {
32780
32815
  if (status.storage_dir ?? status.disk.storage_dir) {
32781
32816
  lines.push(`- **Storage dir:** \`${status.storage_dir ?? status.disk.storage_dir}\``);
32782
32817
  }
32818
+ if (status.status_bar) {
32819
+ const sb = status.status_bar;
32820
+ lines.push("", `### Code Health${sb.tier2_stale ? " (~ stale)" : ""}`, `- **Errors:** ${formatCount(sb.errors)}`, `- **Warnings:** ${formatCount(sb.warnings)}`, `- **Duplicates:** ${formatCount(sb.duplicates)}`, `- **TODOs:** ${formatCount(sb.todos)}`);
32821
+ }
32783
32822
  lines.push("", "### Current session", `- **ID:** \`${status.session.id}\``, `- **Tracked files:** ${formatCount(status.session.tracked_files)}`, `- **Checkpoints:** ${formatCount(status.session.checkpoints)}`, `- **Project checkpoints (all sessions):** ${formatCount(status.checkpoints_total)}`);
32784
32823
  return lines.join(`
32785
32824
  `);
@@ -32787,7 +32826,7 @@ function formatStatusMarkdown(status) {
32787
32826
 
32788
32827
  // src/shared/tui-config.ts
32789
32828
  var import_comment_json4 = __toESM(require_src2(), 1);
32790
- import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "node:fs";
32829
+ import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "node:fs";
32791
32830
  import { dirname as dirname10, join as join21 } from "node:path";
32792
32831
 
32793
32832
  // src/shared/opencode-config-dir.ts
@@ -32824,9 +32863,9 @@ function resolveTuiConfigPath() {
32824
32863
  const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
32825
32864
  const jsoncPath = join21(configDir, "tui.jsonc");
32826
32865
  const jsonPath = join21(configDir, "tui.json");
32827
- if (existsSync13(jsoncPath))
32866
+ if (existsSync14(jsoncPath))
32828
32867
  return jsoncPath;
32829
- if (existsSync13(jsonPath))
32868
+ if (existsSync14(jsonPath))
32830
32869
  return jsonPath;
32831
32870
  return jsonPath;
32832
32871
  }
@@ -32834,7 +32873,7 @@ function ensureTuiPluginEntry() {
32834
32873
  try {
32835
32874
  const configPath = resolveTuiConfigPath();
32836
32875
  let config2 = {};
32837
- if (existsSync13(configPath)) {
32876
+ if (existsSync14(configPath)) {
32838
32877
  config2 = import_comment_json4.parse(readFileSync13(configPath, "utf-8")) ?? {};
32839
32878
  }
32840
32879
  const plugins = Array.isArray(config2.plugin) ? config2.plugin.filter((value) => typeof value === "string") : [];
@@ -32844,7 +32883,7 @@ function ensureTuiPluginEntry() {
32844
32883
  plugins.push(PLUGIN_ENTRY);
32845
32884
  config2.plugin = plugins;
32846
32885
  mkdirSync11(dirname10(configPath), { recursive: true });
32847
- writeFileSync10(configPath, `${import_comment_json4.stringify(config2, null, 2)}
32886
+ writeFileSync11(configPath, `${import_comment_json4.stringify(config2, null, 2)}
32848
32887
  `);
32849
32888
  log2(`[aft-plugin] added TUI plugin entry to ${configPath}`);
32850
32889
  return true;
@@ -37272,14 +37311,12 @@ var PLUGIN_VERSION = (() => {
37272
37311
  return "0.0.0";
37273
37312
  }
37274
37313
  })();
37275
- var ANNOUNCEMENT_VERSION = "0.35.0";
37314
+ var ANNOUNCEMENT_VERSION = "0.35.3";
37276
37315
  var ANNOUNCEMENT_FEATURES = [
37277
- "Leaner semantic indexing local embedding now uses about half the CPU and a fraction of the peak memory, with no change to results and no re-index needed.",
37278
- "New IDE-style status bar on tool results live LSP errors/warnings plus dead-code, unused-export, duplicate, and TODO counts at a glance.",
37279
- "`aft_inspect` is more accurate (dead-code reachability, duplicate collapse, ranked findings) and a background scheduler keeps its counts fresh during long sessions.",
37280
- "The TUI sidebar is now collapsible click the `AFT` header to toggle a condensed view.",
37281
- "`npx @cortexkit/aft --version` reports CLI, binary, and per-harness versions; `doctor --issue` can scope its report to a single session.",
37282
- "New SCSS support and `.inc` files parsed as PHP."
37316
+ "Code Health in the TUI sidebar and `/aft-status`: live LSP errors and warnings plus duplicate and TODO counts, shown as at-a-glance traffic lights when the sidebar is collapsed.",
37317
+ "The semantic index now recovers on its own from a transient embedding-backend blip (a restarted local server, or a model still loading) instead of getting stuck on `failed`.",
37318
+ "Fixed a background codebase-scan crash on very deep or minified files.",
37319
+ "More reliable LSP auto-install when a parent directory has its own `package.json`."
37283
37320
  ];
37284
37321
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
37285
37322
  var plugin = async (input) => initializePluginForDirectory(input);
@@ -71,6 +71,36 @@ export interface AutoInstallResult {
71
71
  installsComplete: Promise<void>;
72
72
  }
73
73
  export declare function abortInFlightAutoInstalls(): Promise<void>;
74
+ /**
75
+ * Spawn `npm install --no-save <pkg>@<version>` in the cache dir.
76
+ *
77
+ * Uses `--ignore-scripts` to neutralize lifecycle hooks (the v0.16 audit
78
+ * hardening). Output goes to plugin log.
79
+ *
80
+ * Previously this used `bun add`, but OpenCode runs under whichever runtime
81
+ * the user has installed (Node 22 is common), and bun is not guaranteed to
82
+ * be on PATH. Every install would fail silently with ENOENT, so users
83
+ * without bun saw recurring `lsp_binary_missing` warnings for newer servers
84
+ * like `@vue/language-server` even though `lsp.auto_install` was true.
85
+ * GitHub #46 reported this exact symptom against v0.27.0.
86
+ *
87
+ * npm is guaranteed to be present whenever the plugin runs through OpenCode's
88
+ * normal CLI distribution path, matches Pi's auto-install behavior, and is
89
+ * what OpenCode itself uses for its built-in LSP auto-install.
90
+ */
91
+ /**
92
+ * Anchor an `npm install --no-save` to `cwd` by writing a minimal package.json.
93
+ *
94
+ * Without a package.json in `cwd`, npm walks UP the directory tree; if any
95
+ * ancestor (e.g. ~/package.json) has one, npm installs into THAT package's
96
+ * node_modules instead, leaving our cache dir's node_modules/<server> missing
97
+ * while still exiting 0. The result is a silent install failure and a recurring
98
+ * lsp_binary_missing warning. The old `bun add` flow created this package.json
99
+ * implicitly; npm needs it written explicitly. GitHub #92.
100
+ *
101
+ * Idempotent: only writes when absent. Failures are non-fatal (logged).
102
+ */
103
+ export declare function ensureInstallAnchor(cwd: string): void;
74
104
  /**
75
105
  * Top-level entry point. Returns the list of bin directories that already
76
106
  * have an installed binary AND kicks off background installs for missing
@@ -1 +1 @@
1
- {"version":3,"file":"lsp-auto-install.d.ts","sourceRoot":"","sources":["../src/lsp-auto-install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA6BH,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,oCAAoC;IACpC,WAAW,EAAE,OAAO,CAAC;IACrB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3C,kGAAkG;IAClG,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED,qCAAqC;AACrC,MAAM,WAAW,iBAAiB;IAChC,mFAAmF;IACnF,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gDAAgD;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B;;;;;;;OAOG;IACH,OAAO,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C;;;;;;;;;OASG;IACH,gBAAgB,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AA8DD,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAM/D;AAsYD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,iBAAiB,EACzB,SAAS,GAAE,OAAO,KAAa,GAC9B,iBAAiB,CAwEnB"}
1
+ {"version":3,"file":"lsp-auto-install.d.ts","sourceRoot":"","sources":["../src/lsp-auto-install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAsCH,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,oCAAoC;IACpC,WAAW,EAAE,OAAO,CAAC;IACrB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3C,kGAAkG;IAClG,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED,qCAAqC;AACrC,MAAM,WAAW,iBAAiB;IAChC,mFAAmF;IACnF,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gDAAgD;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B;;;;;;;OAOG;IACH,OAAO,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C;;;;;;;;;OASG;IACH,gBAAgB,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AA8DD,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAM/D;AA4DD;;;;;;;;;;;;;;;;GAgBG;AACH;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAYrD;AA8TD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,iBAAiB,EACzB,SAAS,GAAE,OAAO,KAAa,GAC9B,iBAAiB,CAwEnB"}
@@ -8,6 +8,23 @@ export interface StatusCompression {
8
8
  project: StatusCompressionAggregate;
9
9
  session: StatusCompressionAggregate;
10
10
  }
11
+ /**
12
+ * The agent status-bar glance — `[AFT E· W· | D· U· C· | T·]`. `errors`/
13
+ * `warnings` are live LSP diagnostics for files touched this session; the
14
+ * Tier-2 trio (`dead_code`/`unused_exports`/`duplicates`) plus `todos` come
15
+ * from the last completed background scan. `tier2_stale` marks those Tier-2
16
+ * counts as predating the latest edit. Null in the snapshot until the Tier-2
17
+ * cache is populated at least once (no fabricated zeros).
18
+ */
19
+ export interface StatusBar {
20
+ errors: number;
21
+ warnings: number;
22
+ dead_code: number;
23
+ unused_exports: number;
24
+ duplicates: number;
25
+ todos: number;
26
+ tier2_stale: boolean;
27
+ }
11
28
  export interface AftStatusSnapshot {
12
29
  version: string;
13
30
  project_root: string | null;
@@ -70,6 +87,12 @@ export interface AftStatusSnapshot {
70
87
  };
71
88
  /** Compression aggregate passthrough; rendering is added separately. */
72
89
  compression?: StatusCompression;
90
+ /**
91
+ * Agent status-bar counts (E/W/D/U/C/T). Undefined until the Tier-2 cache
92
+ * is populated at least once — the sidebar hides the Code Health section
93
+ * until then so it never shows fabricated zeros.
94
+ */
95
+ status_bar?: StatusBar;
73
96
  /**
74
97
  * Human-readable explanation for a synthetic snapshot (e.g.
75
98
  * `cache_role === "not_initialized"`). When the plugin returns a placeholder
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/shared/status.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/shared/status.ts"],"names":[],"mappings":"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"}
package/dist/tui.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/tui/index.tsx
2
2
  import { createMemo as createMemo2, createSignal as createSignal2, onCleanup as onCleanup2 } from "solid-js";
3
3
  // package.json
4
- var version = "0.35.2";
4
+ var version = "0.35.3";
5
5
 
6
6
  // src/shared/rpc-client.ts
7
7
  import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
@@ -343,6 +343,20 @@ function readCompression(value) {
343
343
  session: readCompressionAggregate(compression.session)
344
344
  };
345
345
  }
346
+ function readStatusBar(value) {
347
+ if (typeof value !== "object" || value === null)
348
+ return;
349
+ const bar = asRecord(value);
350
+ return {
351
+ errors: readNumber(bar.errors),
352
+ warnings: readNumber(bar.warnings),
353
+ dead_code: readNumber(bar.dead_code),
354
+ unused_exports: readNumber(bar.unused_exports),
355
+ duplicates: readNumber(bar.duplicates),
356
+ todos: readNumber(bar.todos),
357
+ tier2_stale: readBoolean(bar.tier2_stale)
358
+ };
359
+ }
346
360
  function formatSemanticIndexStatus(status, stage) {
347
361
  if ((status === "loading" || status === "building") && stage === "fingerprint_change") {
348
362
  return "Rebuilding (model changed)";
@@ -430,6 +444,7 @@ function coerceAftStatus(response) {
430
444
  checkpoints: readNumber(session.checkpoints)
431
445
  },
432
446
  compression: readCompression(response.compression),
447
+ status_bar: readStatusBar(response.status_bar),
433
448
  message: readString(response.message, "")
434
449
  };
435
450
  }
@@ -591,6 +606,14 @@ function collapsedCompressionValue(compression) {
591
606
  const pct = original_tokens > 0 ? Math.round(savings_tokens / original_tokens * 100) : 0;
592
607
  return `${formatCount(savings_tokens)} / ${pct}%`;
593
608
  }
609
+ function collapsedHealthLights(statusBar) {
610
+ if (!statusBar)
611
+ return null;
612
+ const diagnostics = statusBar.errors > 0 ? "err" : statusBar.warnings > 0 ? "warn" : "ok";
613
+ const code = statusBar.duplicates > 0 ? "warn" : "ok";
614
+ const todos = statusBar.todos > 0 ? "warn" : "ok";
615
+ return { diagnostics, code, todos };
616
+ }
594
617
  function resolveTuiStorageDir() {
595
618
  return resolveCortexKitStorageRoot();
596
619
  }
@@ -755,6 +778,7 @@ var SidebarContent = (props) => {
755
778
  const trigramBytes = () => s()?.disk?.trigram_disk_bytes ?? 0;
756
779
  const semanticBytes = () => s()?.disk?.semantic_disk_bytes ?? 0;
757
780
  const compressionRows = () => formatCompressionSidebarRows(s()?.compression);
781
+ const statusBar = () => s()?.status_bar;
758
782
  const degradedReasonLabel = (reason) => {
759
783
  if (reason === "home_root") {
760
784
  return "project root is your home directory";
@@ -873,6 +897,28 @@ var SidebarContent = (props) => {
873
897
  children: "●"
874
898
  }, undefined, false, undefined, this)
875
899
  }, undefined, false, undefined, this),
900
+ collapsedHealthLights(statusBar()) && /* @__PURE__ */ jsxDEV(CollapsedRow, {
901
+ theme: props.theme,
902
+ label: "Code Health",
903
+ children: /* @__PURE__ */ jsxDEV("box", {
904
+ flexDirection: "row",
905
+ gap: 1,
906
+ children: [
907
+ /* @__PURE__ */ jsxDEV("text", {
908
+ fg: toneColor(props.theme, collapsedHealthLights(statusBar()).diagnostics),
909
+ children: "●"
910
+ }, undefined, false, undefined, this),
911
+ /* @__PURE__ */ jsxDEV("text", {
912
+ fg: toneColor(props.theme, collapsedHealthLights(statusBar()).code),
913
+ children: "●"
914
+ }, undefined, false, undefined, this),
915
+ /* @__PURE__ */ jsxDEV("text", {
916
+ fg: toneColor(props.theme, collapsedHealthLights(statusBar()).todos),
917
+ children: "●"
918
+ }, undefined, false, undefined, this)
919
+ ]
920
+ }, undefined, true, undefined, this)
921
+ }, undefined, false, undefined, this),
876
922
  collapsedCompressionValue(s()?.compression) && /* @__PURE__ */ jsxDEV(CollapsedRow, {
877
923
  theme: props.theme,
878
924
  label: "Compression",
@@ -944,6 +990,38 @@ var SidebarContent = (props) => {
944
990
  value: formatBytes2(semanticBytes()),
945
991
  tone: "muted"
946
992
  }, undefined, false, undefined, this),
993
+ statusBar() && /* @__PURE__ */ jsxDEV(Fragment, {
994
+ children: [
995
+ /* @__PURE__ */ jsxDEV(SectionHeader, {
996
+ theme: props.theme,
997
+ title: statusBar().tier2_stale ? "Code Health ~" : "Code Health"
998
+ }, undefined, false, undefined, this),
999
+ /* @__PURE__ */ jsxDEV(StatRow, {
1000
+ theme: props.theme,
1001
+ label: "Errors",
1002
+ value: formatCount(statusBar().errors),
1003
+ tone: statusBar().errors > 0 ? "err" : "muted"
1004
+ }, undefined, false, undefined, this),
1005
+ /* @__PURE__ */ jsxDEV(StatRow, {
1006
+ theme: props.theme,
1007
+ label: "Warnings",
1008
+ value: formatCount(statusBar().warnings),
1009
+ tone: statusBar().warnings > 0 ? "warn" : "muted"
1010
+ }, undefined, false, undefined, this),
1011
+ /* @__PURE__ */ jsxDEV(StatRow, {
1012
+ theme: props.theme,
1013
+ label: "Duplicates",
1014
+ value: formatCount(statusBar().duplicates),
1015
+ tone: "muted"
1016
+ }, undefined, false, undefined, this),
1017
+ /* @__PURE__ */ jsxDEV(StatRow, {
1018
+ theme: props.theme,
1019
+ label: "TODOs",
1020
+ value: formatCount(statusBar().todos),
1021
+ tone: "muted"
1022
+ }, undefined, false, undefined, this)
1023
+ ]
1024
+ }, undefined, true, undefined, this),
947
1025
  compressionRows().length > 0 && /* @__PURE__ */ jsxDEV(Fragment, {
948
1026
  children: [
949
1027
  /* @__PURE__ */ jsxDEV(SectionHeader, {
@@ -1433,6 +1511,43 @@ var StatusDialog = (props) => {
1433
1511
  }, undefined, false, undefined, this))
1434
1512
  ]
1435
1513
  }, undefined, true, undefined, this) : null,
1514
+ status()?.status_bar ? /* @__PURE__ */ jsxDEV2("box", {
1515
+ flexDirection: "column",
1516
+ width: "100%",
1517
+ marginTop: 1,
1518
+ children: [
1519
+ /* @__PURE__ */ jsxDEV2("text", {
1520
+ fg: t().text,
1521
+ children: /* @__PURE__ */ jsxDEV2("b", {
1522
+ children: status().status_bar.tier2_stale ? "Code Health ~" : "Code Health"
1523
+ }, undefined, false, undefined, this)
1524
+ }, undefined, false, undefined, this),
1525
+ /* @__PURE__ */ jsxDEV2(R, {
1526
+ theme: t(),
1527
+ label: "Errors",
1528
+ value: formatCountShort(status().status_bar.errors),
1529
+ tone: status().status_bar.errors > 0 ? "err" : "muted"
1530
+ }, undefined, false, undefined, this),
1531
+ /* @__PURE__ */ jsxDEV2(R, {
1532
+ theme: t(),
1533
+ label: "Warnings",
1534
+ value: formatCountShort(status().status_bar.warnings),
1535
+ tone: status().status_bar.warnings > 0 ? "warn" : "muted"
1536
+ }, undefined, false, undefined, this),
1537
+ /* @__PURE__ */ jsxDEV2(R, {
1538
+ theme: t(),
1539
+ label: "Duplicates",
1540
+ value: formatCountShort(status().status_bar.duplicates),
1541
+ tone: "muted"
1542
+ }, undefined, false, undefined, this),
1543
+ /* @__PURE__ */ jsxDEV2(R, {
1544
+ theme: t(),
1545
+ label: "TODOs",
1546
+ value: formatCountShort(status().status_bar.todos),
1547
+ tone: "muted"
1548
+ }, undefined, false, undefined, this)
1549
+ ]
1550
+ }, undefined, true, undefined, this) : null,
1436
1551
  /* @__PURE__ */ jsxDEV2("box", {
1437
1552
  marginTop: 1,
1438
1553
  justifyContent: "flex-end",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.35.2",
3
+ "version": "0.35.3",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
6
6
  "main": "dist/index.js",
@@ -30,19 +30,19 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@clack/prompts": "^1.2.0",
33
- "@cortexkit/aft-bridge": "0.35.2",
33
+ "@cortexkit/aft-bridge": "0.35.3",
34
34
  "@xterm/headless": "^5.5.0",
35
35
  "comment-json": "^4.6.2",
36
36
  "undici": "^7.25.0",
37
37
  "zod": "^4.1.8"
38
38
  },
39
39
  "optionalDependencies": {
40
- "@cortexkit/aft-darwin-arm64": "0.35.2",
41
- "@cortexkit/aft-darwin-x64": "0.35.2",
42
- "@cortexkit/aft-linux-arm64": "0.35.2",
43
- "@cortexkit/aft-linux-x64": "0.35.2",
44
- "@cortexkit/aft-win32-arm64": "0.35.2",
45
- "@cortexkit/aft-win32-x64": "0.35.2"
40
+ "@cortexkit/aft-darwin-arm64": "0.35.3",
41
+ "@cortexkit/aft-darwin-x64": "0.35.3",
42
+ "@cortexkit/aft-linux-arm64": "0.35.3",
43
+ "@cortexkit/aft-linux-x64": "0.35.3",
44
+ "@cortexkit/aft-win32-arm64": "0.35.3",
45
+ "@cortexkit/aft-win32-x64": "0.35.3"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@opencode-ai/plugin": "^1.15.11",
@@ -10,6 +10,24 @@ export interface StatusCompression {
10
10
  session: StatusCompressionAggregate;
11
11
  }
12
12
 
13
+ /**
14
+ * The agent status-bar glance — `[AFT E· W· | D· U· C· | T·]`. `errors`/
15
+ * `warnings` are live LSP diagnostics for files touched this session; the
16
+ * Tier-2 trio (`dead_code`/`unused_exports`/`duplicates`) plus `todos` come
17
+ * from the last completed background scan. `tier2_stale` marks those Tier-2
18
+ * counts as predating the latest edit. Null in the snapshot until the Tier-2
19
+ * cache is populated at least once (no fabricated zeros).
20
+ */
21
+ export interface StatusBar {
22
+ errors: number;
23
+ warnings: number;
24
+ dead_code: number;
25
+ unused_exports: number;
26
+ duplicates: number;
27
+ todos: number;
28
+ tier2_stale: boolean;
29
+ }
30
+
13
31
  export interface AftStatusSnapshot {
14
32
  version: string;
15
33
  project_root: string | null;
@@ -72,6 +90,12 @@ export interface AftStatusSnapshot {
72
90
  };
73
91
  /** Compression aggregate passthrough; rendering is added separately. */
74
92
  compression?: StatusCompression;
93
+ /**
94
+ * Agent status-bar counts (E/W/D/U/C/T). Undefined until the Tier-2 cache
95
+ * is populated at least once — the sidebar hides the Code Health section
96
+ * until then so it never shows fabricated zeros.
97
+ */
98
+ status_bar?: StatusBar;
75
99
  /**
76
100
  * Human-readable explanation for a synthetic snapshot (e.g.
77
101
  * `cache_role === "not_initialized"`). When the plugin returns a placeholder
@@ -125,6 +149,23 @@ function readCompression(value: unknown): StatusCompression | undefined {
125
149
  };
126
150
  }
127
151
 
152
+ function readStatusBar(value: unknown): StatusBar | undefined {
153
+ // Null until Tier-2 is populated — the Rust snapshot emits JSON null, which
154
+ // is not an object, so this returns undefined and the sidebar hides the
155
+ // Code Health section (no fabricated zeros).
156
+ if (typeof value !== "object" || value === null) return undefined;
157
+ const bar = asRecord(value);
158
+ return {
159
+ errors: readNumber(bar.errors),
160
+ warnings: readNumber(bar.warnings),
161
+ dead_code: readNumber(bar.dead_code),
162
+ unused_exports: readNumber(bar.unused_exports),
163
+ duplicates: readNumber(bar.duplicates),
164
+ todos: readNumber(bar.todos),
165
+ tier2_stale: readBoolean(bar.tier2_stale),
166
+ };
167
+ }
168
+
128
169
  function formatFlag(enabled: boolean): string {
129
170
  return enabled ? "enabled" : "disabled";
130
171
  }
@@ -227,6 +268,7 @@ export function coerceAftStatus(response: Record<string, unknown>): AftStatusSna
227
268
  checkpoints: readNumber(session.checkpoints),
228
269
  },
229
270
  compression: readCompression(response.compression),
271
+ status_bar: readStatusBar(response.status_bar),
230
272
  message: readString(response.message, ""),
231
273
  };
232
274
  }
@@ -288,6 +330,21 @@ export function formatStatusDialogMessage(status: AftStatusSnapshot): string {
288
330
  lines.push(`- storage dir: ${status.storage_dir ?? status.disk.storage_dir}`);
289
331
  }
290
332
 
333
+ if (status.status_bar) {
334
+ const sb = status.status_bar;
335
+ lines.push(
336
+ "",
337
+ `Code Health${sb.tier2_stale ? " (~ stale)" : ""}`,
338
+ `- errors: ${formatCount(sb.errors)}`,
339
+ `- warnings: ${formatCount(sb.warnings)}`,
340
+ // dead code / unused exports hidden until oxc resolver lands (restore both)
341
+ // `- dead code: ${formatCount(sb.dead_code)}`,
342
+ // `- unused exports: ${formatCount(sb.unused_exports)}`,
343
+ `- duplicates: ${formatCount(sb.duplicates)}`,
344
+ `- todos: ${formatCount(sb.todos)}`,
345
+ );
346
+ }
347
+
291
348
  lines.push(
292
349
  "",
293
350
  "Current session",
@@ -383,6 +440,21 @@ export function formatStatusMarkdown(status: AftStatusSnapshot): string {
383
440
  lines.push(`- **Storage dir:** \`${status.storage_dir ?? status.disk.storage_dir}\``);
384
441
  }
385
442
 
443
+ if (status.status_bar) {
444
+ const sb = status.status_bar;
445
+ lines.push(
446
+ "",
447
+ `### Code Health${sb.tier2_stale ? " (~ stale)" : ""}`,
448
+ `- **Errors:** ${formatCount(sb.errors)}`,
449
+ `- **Warnings:** ${formatCount(sb.warnings)}`,
450
+ // dead code / unused exports hidden until oxc resolver lands (restore both)
451
+ // `- **Dead code:** ${formatCount(sb.dead_code)}`,
452
+ // `- **Unused exports:** ${formatCount(sb.unused_exports)}`,
453
+ `- **Duplicates:** ${formatCount(sb.duplicates)}`,
454
+ `- **TODOs:** ${formatCount(sb.todos)}`,
455
+ );
456
+ }
457
+
386
458
  lines.push(
387
459
  "",
388
460
  "### Current session",
package/src/tui/index.tsx CHANGED
@@ -466,6 +466,57 @@ const StatusDialog = (props: StatusDialogProps) => {
466
466
  </box>
467
467
  ) : null}
468
468
 
469
+ {/* Code Health — the agent status-bar glance (E/W/D/U/C/T), surfaced so
470
+ users see the same view agents get. Hidden until the Tier-2 cache is
471
+ populated (status_bar undefined) so it never shows fabricated zeros.
472
+ A `~` on the header flags the Tier-2 counts as predating the latest
473
+ edit. */}
474
+ {status()?.status_bar ? (
475
+ <box flexDirection="column" width="100%" marginTop={1}>
476
+ <text fg={t().text}>
477
+ <b>{status()!.status_bar!.tier2_stale ? "Code Health ~" : "Code Health"}</b>
478
+ </text>
479
+ <R
480
+ theme={t()}
481
+ label="Errors"
482
+ value={formatCountShort(status()!.status_bar!.errors)}
483
+ tone={status()!.status_bar!.errors > 0 ? "err" : "muted"}
484
+ />
485
+ <R
486
+ theme={t()}
487
+ label="Warnings"
488
+ value={formatCountShort(status()!.status_bar!.warnings)}
489
+ tone={status()!.status_bar!.warnings > 0 ? "warn" : "muted"}
490
+ />
491
+ {/* Dead Code / Unused Exports hidden until the oxc resolver lands
492
+ (current scanner over-reports on barrel re-exports). Restore both. */}
493
+ {/* <R
494
+ theme={t()}
495
+ label="Dead Code"
496
+ value={formatCountShort(status()!.status_bar!.dead_code)}
497
+ tone="muted"
498
+ />
499
+ <R
500
+ theme={t()}
501
+ label="Unused Exports"
502
+ value={formatCountShort(status()!.status_bar!.unused_exports)}
503
+ tone="muted"
504
+ /> */}
505
+ <R
506
+ theme={t()}
507
+ label="Duplicates"
508
+ value={formatCountShort(status()!.status_bar!.duplicates)}
509
+ tone="muted"
510
+ />
511
+ <R
512
+ theme={t()}
513
+ label="TODOs"
514
+ value={formatCountShort(status()!.status_bar!.todos)}
515
+ tone="muted"
516
+ />
517
+ </box>
518
+ ) : null}
519
+
469
520
  {/* Footer */}
470
521
  <box marginTop={1} justifyContent="flex-end" width="100%">
471
522
  <text fg={t().textMuted}>Enter or Esc to close</text>
@@ -16,6 +16,7 @@ import {
16
16
  coerceAftStatus,
17
17
  formatSemanticIndexStatus,
18
18
  formatSemanticRefreshing,
19
+ type StatusBar,
19
20
  type StatusCompression,
20
21
  } from "../shared/status";
21
22
  import { resolveCortexKitStorageRoot } from "../shared/storage-paths";
@@ -179,6 +180,34 @@ export function collapsedCompressionValue(
179
180
  return `${formatCount(savings_tokens)} / ${pct}%`;
180
181
  }
181
182
 
183
+ export type HealthLightTone = "ok" | "warn" | "err";
184
+
185
+ export interface HealthLights {
186
+ // Diagnostics: red if any errors, yellow if any warnings, else green.
187
+ diagnostics: HealthLightTone;
188
+ // Code cruft: yellow if there is any duplicate, green when zero. Never red —
189
+ // cruft is not a build failure. NOTE: dead_code / unused_exports are
190
+ // temporarily excluded from this signal (and the expanded rows below) until
191
+ // the oxc-based resolver lands and makes those counts trustworthy on real
192
+ // TS/JS codebases. Restore them here when that engine ships.
193
+ code: HealthLightTone;
194
+ // TODOs: yellow if any, else green.
195
+ todos: HealthLightTone;
196
+ }
197
+
198
+ // Three traffic lights for the collapsed view, replacing the cramped
199
+ // `E· W· | D· U· C· | T·` string. Returns null until populated.
200
+ export function collapsedHealthLights(statusBar: StatusBar | undefined): HealthLights | null {
201
+ if (!statusBar) return null;
202
+ const diagnostics: HealthLightTone =
203
+ statusBar.errors > 0 ? "err" : statusBar.warnings > 0 ? "warn" : "ok";
204
+ const code: HealthLightTone =
205
+ // statusBar.dead_code > 0 || statusBar.unused_exports > 0 || // restore with oxc engine
206
+ statusBar.duplicates > 0 ? "warn" : "ok";
207
+ const todos: HealthLightTone = statusBar.todos > 0 ? "warn" : "ok";
208
+ return { diagnostics, code, todos };
209
+ }
210
+
182
211
  // v0.27 moved AFT storage to the CortexKit root. TUI code must use a
183
212
  // lightweight local path helper rather than the shared bridge barrel, which
184
213
  // also exports URL-fetch helpers unsuitable for Bun's TUI runtime.
@@ -426,6 +455,7 @@ const SidebarContent = (props: {
426
455
  const trigramBytes = () => s()?.disk?.trigram_disk_bytes ?? 0;
427
456
  const semanticBytes = () => s()?.disk?.semantic_disk_bytes ?? 0;
428
457
  const compressionRows = () => formatCompressionSidebarRows(s()?.compression);
458
+ const statusBar = () => s()?.status_bar;
429
459
 
430
460
  // Degraded-mode reason → human-readable hint. Distinct strings per reason
431
461
  // because the UX direction is different: "home_root" tells the user to
@@ -537,6 +567,19 @@ const SidebarContent = (props: {
537
567
  <CollapsedRow theme={props.theme} label="Semantic Index">
538
568
  <text fg={toneColor(props.theme, semanticStatus().tone)}>●</text>
539
569
  </CollapsedRow>
570
+ {collapsedHealthLights(statusBar()) && (
571
+ <CollapsedRow theme={props.theme} label="Code Health">
572
+ <box flexDirection="row" gap={1}>
573
+ <text fg={toneColor(props.theme, collapsedHealthLights(statusBar())!.diagnostics)}>
574
+
575
+ </text>
576
+ <text fg={toneColor(props.theme, collapsedHealthLights(statusBar())!.code)}>●</text>
577
+ <text fg={toneColor(props.theme, collapsedHealthLights(statusBar())!.todos)}>
578
+
579
+ </text>
580
+ </box>
581
+ </CollapsedRow>
582
+ )}
540
583
  {collapsedCompressionValue(s()?.compression) && (
541
584
  <CollapsedRow theme={props.theme} label="Compression">
542
585
  <text fg={props.theme.textMuted}>
@@ -614,6 +657,61 @@ const SidebarContent = (props: {
614
657
  tone="muted"
615
658
  />
616
659
 
660
+ {/* Code Health — the agent status-bar glance, surfaced for users.
661
+ Hidden until the Tier-2 cache is populated (status_bar undefined),
662
+ so it never shows fabricated zeros. Errors/warnings are live LSP
663
+ diagnostics; D/U/C/T come from the last background scan. A `~` on
664
+ the section header flags the Tier-2 counts as predating the latest
665
+ edit. */}
666
+ {statusBar() && (
667
+ <>
668
+ <SectionHeader
669
+ theme={props.theme}
670
+ title={statusBar()!.tier2_stale ? "Code Health ~" : "Code Health"}
671
+ />
672
+ <StatRow
673
+ theme={props.theme}
674
+ label="Errors"
675
+ value={formatCount(statusBar()!.errors)}
676
+ tone={statusBar()!.errors > 0 ? "err" : "muted"}
677
+ />
678
+ <StatRow
679
+ theme={props.theme}
680
+ label="Warnings"
681
+ value={formatCount(statusBar()!.warnings)}
682
+ tone={statusBar()!.warnings > 0 ? "warn" : "muted"}
683
+ />
684
+ {/* Dead Code / Unused Exports temporarily hidden until the
685
+ oxc-based resolver lands and makes these trustworthy on real
686
+ TS/JS codebases (current tree-sitter scanner over-reports via
687
+ barrel re-export gaps). Restore both rows when that ships. */}
688
+ {/* <StatRow
689
+ theme={props.theme}
690
+ label="Dead Code"
691
+ value={formatCount(statusBar()!.dead_code)}
692
+ tone="muted"
693
+ />
694
+ <StatRow
695
+ theme={props.theme}
696
+ label="Unused Exports"
697
+ value={formatCount(statusBar()!.unused_exports)}
698
+ tone="muted"
699
+ /> */}
700
+ <StatRow
701
+ theme={props.theme}
702
+ label="Duplicates"
703
+ value={formatCount(statusBar()!.duplicates)}
704
+ tone="muted"
705
+ />
706
+ <StatRow
707
+ theme={props.theme}
708
+ label="TODOs"
709
+ value={formatCount(statusBar()!.todos)}
710
+ tone="muted"
711
+ />
712
+ </>
713
+ )}
714
+
617
715
  {/* Compression aggregates. Tabular layout matching Search/Semantic
618
716
  Index above: each scope ("Session", "Project") renders as a
619
717
  subheader followed by two StatRows (Tokens Saved, Compression