@cortexkit/aft-opencode 0.34.0 → 0.35.1

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/config.d.ts CHANGED
@@ -7,8 +7,8 @@ export declare const ConfigureWarningsDeliveryEnum: z.ZodEnum<{
7
7
  }>;
8
8
  export type ConfigureWarningsDelivery = z.infer<typeof ConfigureWarningsDeliveryEnum>;
9
9
  export declare const LspServerSchema: z.ZodObject<{
10
- extensions: z.ZodArray<z.ZodString>;
11
- binary: z.ZodString;
10
+ extensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
+ binary: z.ZodOptional<z.ZodString>;
12
12
  args: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
13
13
  root_markers: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
14
14
  disabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
@@ -101,8 +101,8 @@ export declare const AftConfigSchema: z.ZodObject<{
101
101
  }, z.core.$strip>>;
102
102
  lsp: z.ZodOptional<z.ZodObject<{
103
103
  servers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
104
- extensions: z.ZodArray<z.ZodString>;
105
- binary: z.ZodString;
104
+ extensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
105
+ binary: z.ZodOptional<z.ZodString>;
106
106
  args: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
107
107
  root_markers: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
108
108
  disabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
@@ -141,8 +141,10 @@ export type AftConfig = z.infer<typeof AftConfigSchema>;
141
141
  export type LspServerConfig = z.infer<typeof LspServerSchema>;
142
142
  export interface ConfigureLspServer {
143
143
  id: string;
144
- extensions: string[];
145
- binary: string;
144
+ /** Omitted when overriding a built-in server to inherit its extensions. */
145
+ extensions?: string[];
146
+ /** Omitted when overriding a built-in server to inherit its binary. */
147
+ binary?: string;
146
148
  args: string[];
147
149
  root_markers: string[];
148
150
  disabled: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -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;AA2MlD;;;;;;;;;;;;;;;;;GAiBG;AAQH,QAAA,MAAM,MAAM,EAAE,MAA6D,CAAC;AAs0B5E,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;AA6MlD;;;;;;;;;;;;;;;;;GAiBG;AAQH,QAAA,MAAM,MAAM,EAAE,MAA6D,CAAC;AA80B5E,eAAe,MAAM,CAAC"}
package/dist/index.js CHANGED
@@ -11593,11 +11593,81 @@ function error(message, meta) {
11593
11593
  console.error(`[aft-bridge] ERROR: ${message}`);
11594
11594
  }
11595
11595
  }
11596
+ // ../aft-bridge/dist/bash-hints.js
11597
+ var CONFLICT_HINT = `
11598
+
11599
+ [Hint] Use aft_conflicts to see all conflict regions across files in a single call.`;
11600
+ var GREP_HINT = `
11601
+
11602
+ [Hint] Use the grep tool instead of bash for faster indexed search.`;
11603
+ function maybeAppendConflictsHint(output) {
11604
+ if (!output.includes("Automatic merge failed; fix conflicts"))
11605
+ return output;
11606
+ if (!/^CONFLICT \(|^error: could not apply /m.test(output))
11607
+ return output;
11608
+ return output + CONFLICT_HINT;
11609
+ }
11610
+ function maybeAppendGrepHint(output, command) {
11611
+ const probe = command !== undefined ? command : output.slice(0, 300).split(`
11612
+ `)[0] ?? "";
11613
+ if (!/\b(rg|grep)\s/.test(probe))
11614
+ return output;
11615
+ return output + GREP_HINT;
11616
+ }
11596
11617
  // ../aft-bridge/dist/bridge.js
11597
11618
  import { spawn } from "node:child_process";
11598
11619
  import { homedir } from "node:os";
11599
11620
  import { join } from "node:path";
11600
11621
  import { StringDecoder } from "node:string_decoder";
11622
+
11623
+ // ../aft-bridge/dist/status-bar.js
11624
+ var STATUS_BAR_HEARTBEAT_CALLS = 15;
11625
+ function createStatusBarEmitState() {
11626
+ return { callsSinceEmit: 0 };
11627
+ }
11628
+ function parseStatusBarCounts(value) {
11629
+ if (!value || typeof value !== "object")
11630
+ return;
11631
+ const record = value;
11632
+ const num = (key) => {
11633
+ const raw = record[key];
11634
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
11635
+ };
11636
+ return {
11637
+ errors: num("errors"),
11638
+ warnings: num("warnings"),
11639
+ dead_code: num("dead_code"),
11640
+ unused_exports: num("unused_exports"),
11641
+ duplicates: num("duplicates"),
11642
+ todos: num("todos"),
11643
+ tier2_stale: record.tier2_stale === true
11644
+ };
11645
+ }
11646
+ function countsEqual(a, b) {
11647
+ return a.errors === b.errors && a.warnings === b.warnings && a.dead_code === b.dead_code && a.unused_exports === b.unused_exports && a.duplicates === b.duplicates && a.todos === b.todos && a.tier2_stale === b.tier2_stale;
11648
+ }
11649
+ function shouldEmitStatusBar(state, next) {
11650
+ const changed = state.last === undefined || !countsEqual(state.last, next);
11651
+ state.callsSinceEmit += 1;
11652
+ const heartbeat = state.callsSinceEmit >= STATUS_BAR_HEARTBEAT_CALLS;
11653
+ if (changed || heartbeat) {
11654
+ state.last = next;
11655
+ state.callsSinceEmit = 0;
11656
+ return true;
11657
+ }
11658
+ return false;
11659
+ }
11660
+ function formatStatusBar(counts) {
11661
+ const staleMark = counts.tier2_stale ? "~" : "";
11662
+ return `[AFT E${counts.errors} W${counts.warnings} | ` + `${staleMark}D${counts.dead_code} U${counts.unused_exports} C${counts.duplicates} | ` + `T${counts.todos}]`;
11663
+ }
11664
+ function statusBarLine(counts) {
11665
+ return `
11666
+
11667
+ ${formatStatusBar(counts)}`;
11668
+ }
11669
+
11670
+ // ../aft-bridge/dist/bridge.js
11601
11671
  var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
11602
11672
  var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
11603
11673
  var SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS = 5000;
@@ -11708,6 +11778,7 @@ class BinaryBridge {
11708
11778
  configureWarningClients = new Map;
11709
11779
  restartResetTimer = null;
11710
11780
  lastChildActivityAt = 0;
11781
+ lastStatusBar;
11711
11782
  consecutiveRequestTimeouts = 0;
11712
11783
  errorPrefix;
11713
11784
  logger;
@@ -12085,6 +12156,7 @@ class BinaryBridge {
12085
12156
  this.spawnProcess(triggeringSessionId);
12086
12157
  }
12087
12158
  spawnProcess(triggeringSessionId) {
12159
+ this.lastStatusBar = undefined;
12088
12160
  if (triggeringSessionId) {
12089
12161
  this.sessionLogVia(triggeringSessionId, `Spawning binary: ${this.binaryPath} (cwd: ${this.cwd})`);
12090
12162
  } else {
@@ -12286,6 +12358,7 @@ class BinaryBridge {
12286
12358
  clearTimeout(entry.timer);
12287
12359
  this.consecutiveRequestTimeouts = 0;
12288
12360
  this.scheduleRestartCountReset();
12361
+ this.captureStatusBar(response);
12289
12362
  entry.resolve(response);
12290
12363
  } else if (typeof response.type === "string") {
12291
12364
  this.logVia(`Ignoring unknown stdout push frame type: ${response.type}`);
@@ -12295,6 +12368,14 @@ class BinaryBridge {
12295
12368
  }
12296
12369
  }
12297
12370
  }
12371
+ captureStatusBar(response) {
12372
+ const parsed = parseStatusBarCounts(response.status_bar);
12373
+ if (parsed)
12374
+ this.lastStatusBar = parsed;
12375
+ }
12376
+ getStatusBar() {
12377
+ return this.lastStatusBar;
12378
+ }
12298
12379
  handleTimeout(triggeringSessionId) {
12299
12380
  this.consecutiveRequestTimeouts = 0;
12300
12381
  this.rejectAllPending(new Error(`${this.errorPrefix} bridge killed during sibling timeout — request aborted`));
@@ -12659,6 +12740,20 @@ async function fetchLatestTag() {
12659
12740
  clearTimeout(timeout);
12660
12741
  }
12661
12742
  }
12743
+ // ../aft-bridge/dist/jsonc.js
12744
+ function stripJsoncSymbols(value) {
12745
+ if (Array.isArray(value)) {
12746
+ return value.map((item) => stripJsoncSymbols(item));
12747
+ }
12748
+ if (value !== null && typeof value === "object") {
12749
+ const out = {};
12750
+ for (const [key, val] of Object.entries(value)) {
12751
+ out[key] = stripJsoncSymbols(val);
12752
+ }
12753
+ return out;
12754
+ }
12755
+ return value;
12756
+ }
12662
12757
  // ../aft-bridge/dist/migration.js
12663
12758
  import { spawnSync as spawnSync2 } from "node:child_process";
12664
12759
  import { existsSync as existsSync4, mkdirSync as mkdirSync4 } from "node:fs";
@@ -12717,7 +12812,7 @@ function markAnnouncementSeen(storageRoot, harness, currentVersion) {
12717
12812
 
12718
12813
  // ../aft-bridge/dist/resolver.js
12719
12814
  import { execSync } from "node:child_process";
12720
- import { chmodSync as chmodSync2, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, renameSync as renameSync3, unlinkSync as unlinkSync2 } from "node:fs";
12815
+ import { chmodSync as chmodSync2, closeSync as closeSync2, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync as openSync2, readSync, renameSync as renameSync3, unlinkSync as unlinkSync2 } from "node:fs";
12721
12816
  import { createRequire as createRequire2 } from "node:module";
12722
12817
  import { homedir as homedir3 } from "node:os";
12723
12818
  import { join as join4 } from "node:path";
@@ -12798,6 +12893,37 @@ function probeBinaryCandidate(binaryPath, source, expectedVersion) {
12798
12893
  function parsePathLookupOutput(output) {
12799
12894
  return output.split(/\r?\n/).map((candidate) => candidate.trim()).filter(Boolean);
12800
12895
  }
12896
+ function isNativeExecutable(binaryPath) {
12897
+ let fd = null;
12898
+ try {
12899
+ fd = openSync2(binaryPath, "r");
12900
+ const buf = Buffer.alloc(4);
12901
+ const read = readSync(fd, buf, 0, 4, 0);
12902
+ if (read < 2)
12903
+ return false;
12904
+ const b0 = buf[0];
12905
+ const b1 = buf[1];
12906
+ if (b0 === 35 && b1 === 33)
12907
+ return false;
12908
+ const m32 = buf.readUInt32BE(0);
12909
+ const machO = new Set([4277009102, 4277009103, 3472551422, 3489328638, 3405691582]);
12910
+ if (read >= 4 && machO.has(m32))
12911
+ return true;
12912
+ if (read >= 4 && m32 === 2135247942)
12913
+ return true;
12914
+ if (b0 === 77 && b1 === 90)
12915
+ return true;
12916
+ return false;
12917
+ } catch {
12918
+ return false;
12919
+ } finally {
12920
+ if (fd !== null) {
12921
+ try {
12922
+ closeSync2(fd);
12923
+ } catch {}
12924
+ }
12925
+ }
12926
+ }
12801
12927
  function platformKey(platform = process.platform, arch = process.arch) {
12802
12928
  const archMap = PLATFORM_ARCH_MAP[platform];
12803
12929
  if (!archMap) {
@@ -12851,6 +12977,10 @@ function findBinarySync(expectedVersion) {
12851
12977
  stdio: ["pipe", "pipe", "pipe"]
12852
12978
  }).trim();
12853
12979
  for (const candidate of parsePathLookupOutput(result)) {
12980
+ if (!isNativeExecutable(candidate)) {
12981
+ warn(`PATH binary at ${candidate} is not a native executable (script shim?); skipping`);
12982
+ continue;
12983
+ }
12854
12984
  const usable = probeBinaryCandidate(candidate, "PATH", expectedVersion);
12855
12985
  if (usable)
12856
12986
  return usable;
@@ -13000,7 +13130,7 @@ async function ensureStorageMigrated(opts) {
13000
13130
  // ../aft-bridge/dist/onnx-runtime.js
13001
13131
  import { execFileSync } from "node:child_process";
13002
13132
  import { createHash as createHash2 } from "node:crypto";
13003
- import { chmodSync as chmodSync3, closeSync as closeSync2, copyFileSync as copyFileSync3, createWriteStream as createWriteStream2, existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync5, openSync as openSync2, readdirSync, readFileSync as readFileSync3, readlinkSync, realpathSync, rmSync as rmSync2, statSync as statSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
13133
+ import { chmodSync as chmodSync3, closeSync as closeSync3, copyFileSync as copyFileSync3, createWriteStream as createWriteStream2, existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync5, openSync as openSync3, readdirSync, readFileSync as readFileSync3, readlinkSync, realpathSync, rmSync as rmSync2, statSync as statSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
13004
13134
  import { basename, dirname as dirname3, isAbsolute, join as join6, relative, resolve, win32 } from "node:path";
13005
13135
  import { Readable as Readable2 } from "node:stream";
13006
13136
  import { pipeline as pipeline2 } from "node:stream/promises";
@@ -13575,13 +13705,13 @@ function sha256File(path) {
13575
13705
  function acquireLock(lockPath) {
13576
13706
  const tryClaim = () => {
13577
13707
  try {
13578
- const fd = openSync2(lockPath, "wx");
13708
+ const fd = openSync3(lockPath, "wx");
13579
13709
  try {
13580
13710
  writeFileSync2(fd, `${process.pid}
13581
13711
  ${new Date().toISOString()}
13582
13712
  `);
13583
13713
  } finally {
13584
- closeSync2(fd);
13714
+ closeSync3(fd);
13585
13715
  }
13586
13716
  return true;
13587
13717
  } catch (err) {
@@ -13717,7 +13847,8 @@ class BridgePool {
13717
13847
  onBashLongRunning: options.onBashLongRunning,
13718
13848
  onBashPatternMatch: options.onBashPatternMatch,
13719
13849
  errorPrefix: options.errorPrefix,
13720
- logger: options.logger
13850
+ logger: options.logger,
13851
+ childEnv: options.childEnv
13721
13852
  };
13722
13853
  this.configOverrides = configOverrides;
13723
13854
  if (Number.isFinite(this.idleTimeoutMs)) {
@@ -13850,6 +13981,9 @@ class BridgePool {
13850
13981
  _testGetConfigOverrides() {
13851
13982
  return { ...this.configOverrides };
13852
13983
  }
13984
+ _testGetBridgeOptions() {
13985
+ return { ...this.bridgeOptions };
13986
+ }
13853
13987
  }
13854
13988
  function normalizeKey(projectRoot) {
13855
13989
  const stripped = projectRoot.replace(/[/\\]+$/, "");
@@ -14897,10 +15031,10 @@ function formatDuration(completion) {
14897
15031
  }
14898
15032
 
14899
15033
  // src/config.ts
14900
- var import_comment_json = __toESM(require_src2(), 1);
14901
15034
  import { existsSync as existsSync6, readFileSync as readFileSync4, renameSync as renameSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "node:fs";
14902
15035
  import { homedir as homedir5 } from "node:os";
14903
15036
  import { join as join8 } from "node:path";
15037
+ var import_comment_json = __toESM(require_src2(), 1);
14904
15038
 
14905
15039
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
14906
15040
  var exports_external = {};
@@ -28473,8 +28607,8 @@ var LspExtensionSchema = exports_external.string().trim().min(1).refine((value)
28473
28607
  message: "Extension must include characters other than leading dots"
28474
28608
  });
28475
28609
  var LspServerEntrySchema = exports_external.object({
28476
- extensions: exports_external.array(LspExtensionSchema).min(1),
28477
- binary: exports_external.string().trim().min(1),
28610
+ extensions: exports_external.array(LspExtensionSchema).min(1).optional(),
28611
+ binary: exports_external.string().trim().min(1).optional(),
28478
28612
  args: exports_external.array(exports_external.string()).optional().default([]),
28479
28613
  root_markers: exports_external.array(exports_external.string().trim().min(1)).optional().default([".git"]),
28480
28614
  disabled: exports_external.boolean().optional().default(false),
@@ -28579,12 +28713,16 @@ function resolveLspConfigForConfigure(config2) {
28579
28713
  const servers = Object.entries(config2.lsp?.servers ?? {}).map(([id, server]) => {
28580
28714
  const entry = {
28581
28715
  id,
28582
- extensions: server.extensions.map(normalizeLspExtension),
28583
- binary: server.binary,
28584
28716
  args: server.args,
28585
28717
  root_markers: server.root_markers,
28586
28718
  disabled: server.disabled
28587
28719
  };
28720
+ if (server.extensions && server.extensions.length > 0) {
28721
+ entry.extensions = server.extensions.map(normalizeLspExtension);
28722
+ }
28723
+ if (server.binary) {
28724
+ entry.binary = server.binary;
28725
+ }
28588
28726
  if (server.env && Object.keys(server.env).length > 0) {
28589
28727
  entry.env = server.env;
28590
28728
  }
@@ -28900,14 +29038,15 @@ function loadConfigFromPath(configPath) {
28900
29038
  const content = readFileSync4(configPath, "utf-8");
28901
29039
  const rawConfig = import_comment_json.parse(content);
28902
29040
  migrateRawConfig(rawConfig, configPath, { log: log2, warn: warn2 });
28903
- const result = AftConfigSchema.safeParse(rawConfig);
29041
+ const cleanConfig = stripJsoncSymbols(rawConfig);
29042
+ const result = AftConfigSchema.safeParse(cleanConfig);
28904
29043
  if (result.success) {
28905
29044
  log2(`Config loaded from ${configPath}`);
28906
29045
  return result.data;
28907
29046
  }
28908
29047
  const errorMsg = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
28909
29048
  warn2(`Config validation error in ${configPath}: ${errorMsg}`);
28910
- return parseConfigPartially(rawConfig);
29049
+ return parseConfigPartially(cleanConfig);
28911
29050
  } catch (err) {
28912
29051
  const errorMsg = err instanceof Error ? err.message : String(err);
28913
29052
  error2(`Error loading config from ${configPath}: ${errorMsg}`);
@@ -29612,10 +29751,10 @@ async function flushConfigureWarningsOnIdle(sessionId) {
29612
29751
 
29613
29752
  // src/hooks/auto-update-checker/index.ts
29614
29753
  import {
29615
- closeSync as closeSync3,
29754
+ closeSync as closeSync4,
29616
29755
  existsSync as existsSync10,
29617
29756
  mkdirSync as mkdirSync6,
29618
- openSync as openSync3,
29757
+ openSync as openSync4,
29619
29758
  readFileSync as readFileSync8,
29620
29759
  renameSync as renameSync5,
29621
29760
  rmSync as rmSync4,
@@ -30166,7 +30305,7 @@ function claimCheckSlot(storageDir, intervalMs) {
30166
30305
  const lockPath = `${file2}.lock`;
30167
30306
  let lockFd;
30168
30307
  try {
30169
- lockFd = openSync3(lockPath, "wx");
30308
+ lockFd = openSync4(lockPath, "wx");
30170
30309
  writeFileSync6(lockFd, JSON.stringify({ pid: process.pid, startedMs: Date.now() }));
30171
30310
  } catch (err) {
30172
30311
  if (err.code !== "EEXIST") {
@@ -30177,7 +30316,7 @@ function claimCheckSlot(storageDir, intervalMs) {
30177
30316
  const lock = {
30178
30317
  release: () => {
30179
30318
  try {
30180
- closeSync3(lockFd);
30319
+ closeSync4(lockFd);
30181
30320
  } catch {}
30182
30321
  rmSync4(lockPath, { force: true });
30183
30322
  }
@@ -30307,9 +30446,9 @@ import { join as join15 } from "node:path";
30307
30446
 
30308
30447
  // src/lsp-cache.ts
30309
30448
  import {
30310
- closeSync as closeSync4,
30449
+ closeSync as closeSync5,
30311
30450
  mkdirSync as mkdirSync7,
30312
- openSync as openSync4,
30451
+ openSync as openSync5,
30313
30452
  readFileSync as readFileSync9,
30314
30453
  statSync as statSync4,
30315
30454
  unlinkSync as unlinkSync5,
@@ -30402,13 +30541,13 @@ function acquireInstallLock(lockKey) {
30402
30541
  const lock = lockPath(lockKey);
30403
30542
  const tryClaim = () => {
30404
30543
  try {
30405
- const fd = openSync4(lock, "wx");
30544
+ const fd = openSync5(lock, "wx");
30406
30545
  try {
30407
30546
  writeFileSync7(fd, `${process.pid}
30408
30547
  ${new Date().toISOString()}
30409
30548
  `);
30410
30549
  } finally {
30411
- closeSync4(fd);
30550
+ closeSync5(fd);
30412
30551
  }
30413
30552
  return true;
30414
30553
  } catch (err) {
@@ -32015,29 +32154,6 @@ function normalizeToolMap(tools) {
32015
32154
  }
32016
32155
  return tools;
32017
32156
  }
32018
-
32019
- // src/shared/bash-hints.ts
32020
- var CONFLICT_HINT = `
32021
-
32022
- [Hint] Use aft_conflicts to see all conflict regions across files in a single call.`;
32023
- var GREP_HINT = `
32024
-
32025
- [Hint] Use the grep tool instead of bash for faster indexed search.`;
32026
- function maybeAppendConflictsHint(output) {
32027
- if (!output.includes("Automatic merge failed; fix conflicts"))
32028
- return output;
32029
- if (!/^CONFLICT \(|^error: could not apply /m.test(output))
32030
- return output;
32031
- return output + CONFLICT_HINT;
32032
- }
32033
- function maybeAppendGrepHint(output) {
32034
- const firstLine = output.slice(0, 300).split(`
32035
- `)[0] ?? "";
32036
- if (!/\b(rg|grep)\s/.test(firstLine))
32037
- return output;
32038
- return output + GREP_HINT;
32039
- }
32040
-
32041
32157
  // src/shared/pty-cache.ts
32042
32158
  var import_headless = __toESM(require_xterm_headless(), 1);
32043
32159
  import * as fs2 from "node:fs/promises";
@@ -32631,6 +32747,22 @@ function registerShutdownCleanup(fn) {
32631
32747
  };
32632
32748
  }
32633
32749
 
32750
+ // src/status-bar-inject.ts
32751
+ var emitStateBySession = new Map;
32752
+ function statusBarSuffixForSession(sessionID, counts) {
32753
+ if (!counts)
32754
+ return "";
32755
+ let state = emitStateBySession.get(sessionID);
32756
+ if (!state) {
32757
+ state = createStatusBarEmitState();
32758
+ emitStateBySession.set(sessionID, state);
32759
+ }
32760
+ return shouldEmitStatusBar(state, counts) ? statusBarLine(counts) : "";
32761
+ }
32762
+ function clearStatusBarSession(sessionID) {
32763
+ emitStateBySession.delete(sessionID);
32764
+ }
32765
+
32634
32766
  // src/tools/ast.ts
32635
32767
  import { tool as tool3 } from "@opencode-ai/plugin";
32636
32768
 
@@ -36905,6 +37037,7 @@ function buildWorkflowHints(opts) {
36905
37037
  }
36906
37038
  if (hasInspect) {
36907
37039
  sections.push("**Codebase health & diagnostics**: AFT does not surface compile/type errors automatically after edits — pull them with `aft_inspect`. Run it after a batch of edits and before you run tests or commit, when starting in unfamiliar code, or before a refactor/review. One call summarizes diagnostics (compile/type errors), TODOs, metrics, dead code, unused exports, and duplicates; pass `sections` for focused drill-down and `scope` to actively pull diagnostics for a specific file or directory. Its diagnostics are a fast checkpoint, not the authority — a clean `tsc` / `cargo check` / `pyright` run is the real gate. Treat `stale_categories` as a genuine stale-cache signal while an async Tier 2 refresh catches up.");
37040
+ sections.push("**AFT status bar**: tool results may end with a one-line health bar `[AFT E<errors> W<warnings> | D<dead-code> U<unused-exports> C<clone/dup-groups> | T<todos>]` — an IDE-style glance that appears when a count changes. `E`/`W` are live LSP diagnostics for files touched this session (your universal compile-error signal across every language with an LSP). A `~` before `D` means the dead-code/unused/dup counts predate your latest edit — run `aft_inspect` for current numbers and detail. When `E>0`, you likely just introduced errors; investigate before moving on.");
36908
37041
  }
36909
37042
  if (hasNavigate) {
36910
37043
  sections.push([
@@ -36984,13 +37117,14 @@ var PLUGIN_VERSION = (() => {
36984
37117
  return "0.0.0";
36985
37118
  }
36986
37119
  })();
36987
- var ANNOUNCEMENT_VERSION = "0.34.0";
37120
+ var ANNOUNCEMENT_VERSION = "0.35.0";
36988
37121
  var ANNOUNCEMENT_FEATURES = [
36989
- "`aft_import` now supports 12 more languages (Solidity, Java, C#, PHP, Kotlin, Scala, Swift, Ruby, Lua, Perl, C/C++, Vue) 17 total.",
36990
- "New YAML + Kubernetes/CRD support across outline, zoom, search, and ast_grep.",
36991
- "`aft_conflicts` takes an optional `path` to inspect any repo or git worktree, and names the repository it checked.",
36992
- "`.aftignore` (layered on `.gitignore`) plus ripgrep-style `grep` that searches an explicitly-named file even when ignored.",
36993
- "`aft_zoom` call-graph annotations are now opt-in (`callgraph: true`); `semantic.max_files` and `bash.foreground_wait_window_ms` are configurable."
37122
+ "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.",
37123
+ "New IDE-style status bar on tool results — live LSP errors/warnings plus dead-code, unused-export, duplicate, and TODO counts at a glance.",
37124
+ "`aft_inspect` is more accurate (dead-code reachability, duplicate collapse, ranked findings) and a background scheduler keeps its counts fresh during long sessions.",
37125
+ "The TUI sidebar is now collapsible — click the `AFT` header to toggle a condensed view.",
37126
+ "`npx @cortexkit/aft --version` reports CLI, binary, and per-harness versions; `doctor --issue` can scope its report to a single session.",
37127
+ "New SCSS support and `.inc` files parsed as PHP."
36994
37128
  ];
36995
37129
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
36996
37130
  var plugin = async (input) => initializePluginForDirectory(input);
@@ -37398,6 +37532,7 @@ Install: ${getManualInstallHint()}`).catch(() => {});
37398
37532
  const sessionID = extractSessionID(eventInput.event.properties);
37399
37533
  if ((eventType === "session.deleted" || eventType === "session.shutdown") && sessionID) {
37400
37534
  inspectTier2Idle.clear(sessionID);
37535
+ clearStatusBarSession(sessionID);
37401
37536
  return;
37402
37537
  }
37403
37538
  if (eventType !== "session.idle")
@@ -37460,6 +37595,12 @@ Install: ${getManualInstallHint()}`).catch(() => {});
37460
37595
  }
37461
37596
  const sessionDir = getSessionDirectoryCached(toolInput.sessionID) ?? input.directory;
37462
37597
  await appendInTurnBgCompletions({ ctx, directory: sessionDir, sessionID: toolInput.sessionID }, output);
37598
+ if (output.output !== undefined) {
37599
+ const activeBridge = ctx.pool.getActiveBridgeForRoot(sessionDir);
37600
+ const suffix = statusBarSuffixForSession(toolInput.sessionID, activeBridge?.getStatusBar());
37601
+ if (suffix)
37602
+ output.output += suffix;
37603
+ }
37463
37604
  },
37464
37605
  config: async (config2) => {
37465
37606
  if (!config2 || typeof config2 !== "object")
@@ -1,24 +1,2 @@
1
- /**
2
- * Append the `aft_conflicts` hint when the output indicates a real git merge
3
- * or rebase produced conflicts.
4
- *
5
- * Gated on BOTH:
6
- * - the "Automatic merge failed; fix conflicts" marker, AND
7
- * - a git-conflict signal (`CONFLICT (...)` line or `error: could not apply`)
8
- *
9
- * Both conditions are required because `aft_conflicts` calls `git ls-files -u`,
10
- * which fails with "not a git repository" outside a git working tree. The
11
- * marker string can legitimately appear in docs, READMEs, test fixtures, and
12
- * grep output, so we cannot key off it alone — a false-positive hint sends
13
- * agents into a confusing error.
14
- */
15
- export declare function maybeAppendConflictsHint(output: string): string;
16
- /**
17
- * Append the grep-tool hint when the bash command was a grep/rg invocation.
18
- *
19
- * Detected by examining the FIRST LINE of output (which is the echoed command
20
- * line on most shells in foreground mode). We cap the slice at 300 chars so
21
- * a single huge first line doesn't slow this hook.
22
- */
23
- export declare function maybeAppendGrepHint(output: string): string;
1
+ export { maybeAppendConflictsHint, maybeAppendGrepHint } from "@cortexkit/aft-bridge";
24
2
  //# sourceMappingURL=bash-hints.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bash-hints.d.ts","sourceRoot":"","sources":["../../src/shared/bash-hints.ts"],"names":[],"mappings":"AAWA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAM/D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAI1D"}
1
+ {"version":3,"file":"bash-hints.d.ts","sourceRoot":"","sources":["../../src/shared/bash-hints.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { type StatusBarCounts } from "@cortexkit/aft-bridge";
2
+ /**
3
+ * Decide whether to surface the bar for this session+counts and, if so, return
4
+ * the line to append to the tool output. Returns `""` when suppressed
5
+ * (unchanged since last emit and heartbeat not elapsed) or when counts are
6
+ * absent (no inspect scan has populated Tier-2 yet).
7
+ */
8
+ export declare function statusBarSuffixForSession(sessionID: string, counts: StatusBarCounts | undefined): string;
9
+ /** Drop a session's emit state (on session delete/shutdown). */
10
+ export declare function clearStatusBarSession(sessionID: string): void;
11
+ //# sourceMappingURL=status-bar-inject.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status-bar-inject.d.ts","sourceRoot":"","sources":["../src/status-bar-inject.ts"],"names":[],"mappings":"AAOA,OAAO,EAEL,KAAK,eAAe,EAIrB,MAAM,uBAAuB,CAAC;AAI/B;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,GAAG,SAAS,GAClC,MAAM,CAQR;AAED,gEAAgE;AAChE,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE7D"}
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.34.0";
4
+ var version = "0.35.1";
5
5
 
6
6
  // src/shared/rpc-client.ts
7
7
  import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
@@ -560,6 +560,37 @@ var SectionHeader = (props) => /* @__PURE__ */ jsxDEV("box", {
560
560
  }, undefined, false, undefined, this)
561
561
  }, undefined, false, undefined, this)
562
562
  }, undefined, false, undefined, this);
563
+ function toneColor(theme, tone) {
564
+ switch (tone) {
565
+ case "ok":
566
+ return theme.success ?? theme.accent;
567
+ case "warn":
568
+ return theme.warning;
569
+ case "err":
570
+ return theme.error;
571
+ default:
572
+ return theme.textMuted;
573
+ }
574
+ }
575
+ var CollapsedRow = (props) => /* @__PURE__ */ jsxDEV("box", {
576
+ width: "100%",
577
+ flexDirection: "row",
578
+ justifyContent: "space-between",
579
+ children: [
580
+ /* @__PURE__ */ jsxDEV("text", {
581
+ fg: props.theme.textMuted,
582
+ children: props.label
583
+ }, undefined, false, undefined, this),
584
+ props.children
585
+ ]
586
+ }, undefined, true, undefined, this);
587
+ function collapsedCompressionValue(compression) {
588
+ if (!compression || compression.project.events <= 0)
589
+ return null;
590
+ const { savings_tokens, original_tokens } = compression.project;
591
+ const pct = original_tokens > 0 ? Math.round(savings_tokens / original_tokens * 100) : 0;
592
+ return `${formatCount(savings_tokens)} / ${pct}%`;
593
+ }
563
594
  function resolveTuiStorageDir() {
564
595
  return resolveCortexKitStorageRoot();
565
596
  }
@@ -579,8 +610,12 @@ function scopedSidebarSnapshot(scoped, directory, sessionID) {
579
610
  return null;
580
611
  return scoped.snapshot;
581
612
  }
613
+ function shouldSuppressUninitializedDowngrade(incomingCacheRole, haveInitializedForContext) {
614
+ return incomingCacheRole === "not_initialized" && haveInitializedForContext;
615
+ }
582
616
  var SidebarContent = (props) => {
583
617
  const [status, setStatus] = createSignal(null);
618
+ const [collapsed, setCollapsed] = createSignal(false);
584
619
  let inflight = null;
585
620
  let generation = 0;
586
621
  let debounceTimer;
@@ -637,6 +672,10 @@ var SidebarContent = (props) => {
637
672
  return;
638
673
  if (response && response.success !== false) {
639
674
  const snapshot = coerceAftStatus(response);
675
+ const current = status();
676
+ const haveGoodForContext = current !== null && current.directory === directory && current.sessionID === sid && current.snapshot.cache_role !== "not_initialized";
677
+ if (shouldSuppressUninitializedDowngrade(snapshot.cache_role, haveGoodForContext))
678
+ return;
640
679
  setStatus({ directory, sessionID: sid, snapshot });
641
680
  requestRender();
642
681
  }
@@ -749,6 +788,10 @@ var SidebarContent = (props) => {
749
788
  flexDirection: "row",
750
789
  justifyContent: "space-between",
751
790
  alignItems: "center",
791
+ onMouseDown: () => {
792
+ if (!notInitialized())
793
+ setCollapsed((x) => !x);
794
+ },
752
795
  children: [
753
796
  /* @__PURE__ */ jsxDEV("box", {
754
797
  flexDirection: "row",
@@ -761,8 +804,11 @@ var SidebarContent = (props) => {
761
804
  children: /* @__PURE__ */ jsxDEV("text", {
762
805
  fg: props.theme.background,
763
806
  children: /* @__PURE__ */ jsxDEV("b", {
764
- children: "AFT"
765
- }, undefined, false, undefined, this)
807
+ children: [
808
+ notInitialized() ? "" : collapsed() ? "▶ " : "▼ ",
809
+ "AFT"
810
+ ]
811
+ }, undefined, true, undefined, this)
766
812
  }, undefined, false, undefined, this)
767
813
  }, undefined, false, undefined, this),
768
814
  s()?.degraded && /* @__PURE__ */ jsxDEV("box", {
@@ -807,7 +853,39 @@ var SidebarContent = (props) => {
807
853
  children: s().message || "AFT bridge is now spawned lazily, information here will be populated after first tool call."
808
854
  }, undefined, false, undefined, this)
809
855
  }, undefined, false, undefined, this),
810
- !notInitialized() && /* @__PURE__ */ jsxDEV(Fragment, {
856
+ !notInitialized() && collapsed() && /* @__PURE__ */ jsxDEV("box", {
857
+ width: "100%",
858
+ flexDirection: "column",
859
+ children: [
860
+ /* @__PURE__ */ jsxDEV(CollapsedRow, {
861
+ theme: props.theme,
862
+ label: "Search Index",
863
+ children: /* @__PURE__ */ jsxDEV("text", {
864
+ fg: toneColor(props.theme, searchStatus().tone),
865
+ children: "●"
866
+ }, undefined, false, undefined, this)
867
+ }, undefined, false, undefined, this),
868
+ /* @__PURE__ */ jsxDEV(CollapsedRow, {
869
+ theme: props.theme,
870
+ label: "Semantic Index",
871
+ children: /* @__PURE__ */ jsxDEV("text", {
872
+ fg: toneColor(props.theme, semanticStatus().tone),
873
+ children: "●"
874
+ }, undefined, false, undefined, this)
875
+ }, undefined, false, undefined, this),
876
+ collapsedCompressionValue(s()?.compression) && /* @__PURE__ */ jsxDEV(CollapsedRow, {
877
+ theme: props.theme,
878
+ label: "Compression",
879
+ children: /* @__PURE__ */ jsxDEV("text", {
880
+ fg: props.theme.textMuted,
881
+ children: /* @__PURE__ */ jsxDEV("b", {
882
+ children: collapsedCompressionValue(s()?.compression)
883
+ }, undefined, false, undefined, this)
884
+ }, undefined, false, undefined, this)
885
+ }, undefined, false, undefined, this)
886
+ ]
887
+ }, undefined, true, undefined, this),
888
+ !notInitialized() && !collapsed() && /* @__PURE__ */ jsxDEV(Fragment, {
811
889
  children: [
812
890
  /* @__PURE__ */ jsxDEV(SectionHeader, {
813
891
  theme: props.theme,
@@ -1018,7 +1096,12 @@ var StatusDialog = (props) => {
1018
1096
  if (controller.signal.aborted || requestGeneration !== pollGeneration)
1019
1097
  return;
1020
1098
  if (response.success !== false) {
1021
- setStatus(coerceAftStatus(response));
1099
+ const snapshot = coerceAftStatus(response);
1100
+ const current = status();
1101
+ if (shouldSuppressUninitializedDowngrade(snapshot.cache_role, current !== null && current.cache_role !== "not_initialized")) {
1102
+ return;
1103
+ }
1104
+ setStatus(snapshot);
1022
1105
  setError(null);
1023
1106
  }
1024
1107
  } catch {
@@ -1 +1 @@
1
- {"version":3,"file":"workflow-hints.d.ts","sourceRoot":"","sources":["../src/workflow-hints.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,SAAS,EAAqB,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,WAAW,EAAE,SAAS,GAAG,aAAa,GAAG,KAAK,CAAC;IAC/C,4EAA4E;IAC5E,aAAa,EAAE,OAAO,CAAC;IACvB,mEAAmE;IACnE,eAAe,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,qBAAqB,EAAE,OAAO,CAAC;IAC/B,4DAA4D;IAC5D,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC5B;AAID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CA0FzE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAQjG"}
1
+ {"version":3,"file":"workflow-hints.d.ts","sourceRoot":"","sources":["../src/workflow-hints.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,SAAS,EAAqB,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,WAAW,EAAE,SAAS,GAAG,aAAa,GAAG,KAAK,CAAC;IAC/C,4EAA4E;IAC5E,aAAa,EAAE,OAAO,CAAC;IACvB,mEAAmE;IACnE,eAAe,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,qBAAqB,EAAE,OAAO,CAAC;IAC/B,4DAA4D;IAC5D,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC5B;AAID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CA+FzE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAQjG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.34.0",
3
+ "version": "0.35.1",
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.34.0",
33
+ "@cortexkit/aft-bridge": "0.35.1",
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.34.0",
41
- "@cortexkit/aft-darwin-x64": "0.34.0",
42
- "@cortexkit/aft-linux-arm64": "0.34.0",
43
- "@cortexkit/aft-linux-x64": "0.34.0",
44
- "@cortexkit/aft-win32-arm64": "0.34.0",
45
- "@cortexkit/aft-win32-x64": "0.34.0"
40
+ "@cortexkit/aft-darwin-arm64": "0.35.1",
41
+ "@cortexkit/aft-darwin-x64": "0.35.1",
42
+ "@cortexkit/aft-linux-arm64": "0.35.1",
43
+ "@cortexkit/aft-linux-x64": "0.35.1",
44
+ "@cortexkit/aft-win32-arm64": "0.35.1",
45
+ "@cortexkit/aft-win32-x64": "0.35.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@opencode-ai/plugin": "^1.15.11",
@@ -1,45 +1,4 @@
1
- // Pure helpers for the bash-output hint nudges fired from `tool.execute.after`.
2
- //
3
- // These run on every bash result and append agent-visible "[Hint] ..." lines
4
- // that steer toward AFT tools. The functions return the new output string
5
- // (or the original string when no hint should fire) so the hook can compose
6
- // them without managing state.
7
-
8
- const CONFLICT_HINT =
9
- "\n\n[Hint] Use aft_conflicts to see all conflict regions across files in a single call.";
10
- const GREP_HINT = "\n\n[Hint] Use the grep tool instead of bash for faster indexed search.";
11
-
12
- /**
13
- * Append the `aft_conflicts` hint when the output indicates a real git merge
14
- * or rebase produced conflicts.
15
- *
16
- * Gated on BOTH:
17
- * - the "Automatic merge failed; fix conflicts" marker, AND
18
- * - a git-conflict signal (`CONFLICT (...)` line or `error: could not apply`)
19
- *
20
- * Both conditions are required because `aft_conflicts` calls `git ls-files -u`,
21
- * which fails with "not a git repository" outside a git working tree. The
22
- * marker string can legitimately appear in docs, READMEs, test fixtures, and
23
- * grep output, so we cannot key off it alone — a false-positive hint sends
24
- * agents into a confusing error.
25
- */
26
- export function maybeAppendConflictsHint(output: string): string {
27
- if (!output.includes("Automatic merge failed; fix conflicts")) return output;
28
- // git merge prints "CONFLICT (content|file|...): ..." per file.
29
- // git rebase / git am print "error: could not apply <sha>" per failed pick.
30
- if (!/^CONFLICT \(|^error: could not apply /m.test(output)) return output;
31
- return output + CONFLICT_HINT;
32
- }
33
-
34
- /**
35
- * Append the grep-tool hint when the bash command was a grep/rg invocation.
36
- *
37
- * Detected by examining the FIRST LINE of output (which is the echoed command
38
- * line on most shells in foreground mode). We cap the slice at 300 chars so
39
- * a single huge first line doesn't slow this hook.
40
- */
41
- export function maybeAppendGrepHint(output: string): string {
42
- const firstLine = output.slice(0, 300).split("\n")[0] ?? "";
43
- if (!/\b(rg|grep)\s/.test(firstLine)) return output;
44
- return output + GREP_HINT;
45
- }
1
+ // Bash-output hint nudges now live in the shared bridge package so both plugin
2
+ // hosts use one implementation. Re-exported here to keep the existing OpenCode
3
+ // import path stable.
4
+ export { maybeAppendConflictsHint, maybeAppendGrepHint } from "@cortexkit/aft-bridge";
package/src/tui/index.tsx CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  createAftSidebarSlot,
18
18
  formatCompressionSidebarRows,
19
19
  resolveTuiStorageDir,
20
+ shouldSuppressUninitializedDowngrade,
20
21
  } from "./sidebar";
21
22
 
22
23
  // The TUI talks to the server plugin via AftRpcClient. The client reads the
@@ -160,7 +161,20 @@ const StatusDialog = (props: StatusDialogProps) => {
160
161
  );
161
162
  if (controller.signal.aborted || requestGeneration !== pollGeneration) return;
162
163
  if ((response as Record<string, unknown>).success !== false) {
163
- setStatus(coerceAftStatus(response as Record<string, unknown>));
164
+ const snapshot = coerceAftStatus(response as Record<string, unknown>);
165
+ // Stale-while-revalidate: don't downgrade a good snapshot to a transient
166
+ // `not_initialized` (bridge mid-respawn / session-dir key miss) — it
167
+ // arrives as success:true and would blank the dialog until the next poll.
168
+ const current = status();
169
+ if (
170
+ shouldSuppressUninitializedDowngrade(
171
+ snapshot.cache_role,
172
+ current !== null && current.cache_role !== "not_initialized",
173
+ )
174
+ ) {
175
+ return;
176
+ }
177
+ setStatus(snapshot);
164
178
  setError(null);
165
179
  }
166
180
  } catch {
@@ -142,6 +142,43 @@ const SectionHeader = (props: { theme: TuiThemeCurrent; title: string; marginTop
142
142
  </box>
143
143
  );
144
144
 
145
+ // Map a status tone to a theme color — used for the collapsed-view status dots.
146
+ function toneColor(theme: TuiThemeCurrent, tone: "ok" | "warn" | "err" | "muted"): string {
147
+ switch (tone) {
148
+ case "ok":
149
+ return theme.success ?? theme.accent;
150
+ case "warn":
151
+ return theme.warning;
152
+ case "err":
153
+ return theme.error;
154
+ default:
155
+ return theme.textMuted;
156
+ }
157
+ }
158
+
159
+ // Collapsed-view row: label on the left, a status dot (or compact value) on the
160
+ // right. Mirrors the expanded StatRow layout so the columns line up.
161
+ const CollapsedRow = (props: { theme: TuiThemeCurrent; label: string; children: JSX.Element }) => (
162
+ <box width="100%" flexDirection="row" justifyContent="space-between">
163
+ <text fg={props.theme.textMuted}>{props.label}</text>
164
+ {props.children}
165
+ </box>
166
+ );
167
+
168
+ // Compact "saved / ratio" string for the collapsed Compression row — e.g.
169
+ // "7.6M / 64%". Uses the local `formatCount` (not the aft-bridge token
170
+ // formatter) so the TUI bundle doesn't pull the bridge barrel, which exports
171
+ // URL-fetch helpers unsuitable for Bun's TUI runtime. Returns null when no
172
+ // compression has been recorded yet.
173
+ export function collapsedCompressionValue(
174
+ compression: StatusCompression | undefined,
175
+ ): string | null {
176
+ if (!compression || compression.project.events <= 0) return null;
177
+ const { savings_tokens, original_tokens } = compression.project;
178
+ const pct = original_tokens > 0 ? Math.round((savings_tokens / original_tokens) * 100) : 0;
179
+ return `${formatCount(savings_tokens)} / ${pct}%`;
180
+ }
181
+
145
182
  // v0.27 moved AFT storage to the CortexKit root. TUI code must use a
146
183
  // lightweight local path helper rather than the shared bridge barrel, which
147
184
  // also exports URL-fetch helpers unsuitable for Bun's TUI runtime.
@@ -177,6 +214,22 @@ export function scopedSidebarSnapshot(
177
214
  return scoped.snapshot;
178
215
  }
179
216
 
217
+ /**
218
+ * Stale-while-revalidate guard. A transient `not_initialized` snapshot (bridge
219
+ * mid-respawn after a binary swap, or a momentary session-dir key miss) arrives
220
+ * over RPC as `success: true`, so a naive `setStatus` would overwrite a good
221
+ * snapshot and collapse the panel to the lazy-bridge placeholder — the blank
222
+ * flicker that recovers on the next poll. Suppress the downgrade only when we
223
+ * already hold initialized data for the same context; never blocks the first
224
+ * real snapshot, and a genuine context switch clears separately.
225
+ */
226
+ export function shouldSuppressUninitializedDowngrade(
227
+ incomingCacheRole: string | undefined,
228
+ haveInitializedForContext: boolean,
229
+ ): boolean {
230
+ return incomingCacheRole === "not_initialized" && haveInitializedForContext;
231
+ }
232
+
180
233
  const SidebarContent = (props: {
181
234
  api: TuiPluginApi;
182
235
  sessionID: () => string;
@@ -184,6 +237,9 @@ const SidebarContent = (props: {
184
237
  pluginVersion: string;
185
238
  }) => {
186
239
  const [status, setStatus] = createSignal<ScopedSidebarStatus | null>(null);
240
+ // Collapsed/expanded toggle — local UI state (not persisted), mirroring
241
+ // OpenCode's native MCP sidebar section. Default expanded.
242
+ const [collapsed, setCollapsed] = createSignal(false);
187
243
  let inflight: {
188
244
  controller: AbortController;
189
245
  generation: number;
@@ -251,6 +307,16 @@ const SidebarContent = (props: {
251
307
  if (currentDirectory() !== directory || props.sessionID() !== sid) return;
252
308
  if (response && (response as Record<string, unknown>).success !== false) {
253
309
  const snapshot = coerceAftStatus(response as Record<string, unknown>);
310
+ // Stale-while-revalidate: keep the last-good snapshot instead of
311
+ // flickering to the lazy-bridge placeholder on a transient
312
+ // not_initialized. See shouldSuppressUninitializedDowngrade.
313
+ const current = status();
314
+ const haveGoodForContext =
315
+ current !== null &&
316
+ current.directory === directory &&
317
+ current.sessionID === sid &&
318
+ current.snapshot.cache_role !== "not_initialized";
319
+ if (shouldSuppressUninitializedDowngrade(snapshot.cache_role, haveGoodForContext)) return;
254
320
  setStatus({ directory, sessionID: sid, snapshot });
255
321
  requestRender();
256
322
  }
@@ -394,12 +460,27 @@ const SidebarContent = (props: {
394
460
  paddingLeft={1}
395
461
  paddingRight={1}
396
462
  >
397
- {/* Header: AFT badge + binary version + degraded badge (when active) */}
398
- <box flexDirection="row" justifyContent="space-between" alignItems="center">
463
+ {/* Header: triangle toggle + AFT badge + binary version + degraded badge.
464
+ Clicking the header row collapses/expands the panel (mirrors OpenCode's
465
+ native MCP sidebar section). Only interactive once initialized — the
466
+ lazy-bridge placeholder has nothing to collapse. */}
467
+ <box
468
+ flexDirection="row"
469
+ justifyContent="space-between"
470
+ alignItems="center"
471
+ onMouseDown={() => {
472
+ if (!notInitialized()) setCollapsed((x) => !x);
473
+ }}
474
+ >
399
475
  <box flexDirection="row" alignItems="center">
476
+ {/* Triangle lives inside the accent badge so the toggle reads as one
477
+ unit: "▶ AFT" / "▼ AFT". Hidden pre-init (nothing to collapse). */}
400
478
  <box paddingLeft={1} paddingRight={1} backgroundColor={props.theme.accent}>
401
479
  <text fg={props.theme.background}>
402
- <b>AFT</b>
480
+ <b>
481
+ {notInitialized() ? "" : collapsed() ? "▶ " : "▼ "}
482
+ AFT
483
+ </b>
403
484
  </text>
404
485
  </box>
405
486
  {s()?.degraded && (
@@ -445,8 +526,29 @@ const SidebarContent = (props: {
445
526
  </box>
446
527
  )}
447
528
 
529
+ {/* Collapsed view — condensed status dots + compact compression. Shown
530
+ only when initialized AND collapsed. Three rows mirroring the section
531
+ order of the expanded grid. */}
532
+ {!notInitialized() && collapsed() && (
533
+ <box width="100%" flexDirection="column">
534
+ <CollapsedRow theme={props.theme} label="Search Index">
535
+ <text fg={toneColor(props.theme, searchStatus().tone)}>●</text>
536
+ </CollapsedRow>
537
+ <CollapsedRow theme={props.theme} label="Semantic Index">
538
+ <text fg={toneColor(props.theme, semanticStatus().tone)}>●</text>
539
+ </CollapsedRow>
540
+ {collapsedCompressionValue(s()?.compression) && (
541
+ <CollapsedRow theme={props.theme} label="Compression">
542
+ <text fg={props.theme.textMuted}>
543
+ <b>{collapsedCompressionValue(s()?.compression)}</b>
544
+ </text>
545
+ </CollapsedRow>
546
+ )}
547
+ </box>
548
+ )}
549
+
448
550
  {/* Search index */}
449
- {!notInitialized() && (
551
+ {!notInitialized() && !collapsed() && (
450
552
  <>
451
553
  <SectionHeader theme={props.theme} title="Search Index" />
452
554
  <StatRow