@cortexkit/aft-opencode 0.37.2 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11607,25 +11607,26 @@ function maybeAppendConflictsHint(output) {
11607
11607
  return output;
11608
11608
  return output + CONFLICT_HINT;
11609
11609
  }
11610
- function commandLeadsWithCodeSearch(command) {
11611
- const trimmed = command.trim();
11612
- if (!trimmed)
11610
+ function commandInvokesCodeSearch(command) {
11611
+ const statements = splitTopLevelStatements(command);
11612
+ if (statements === null)
11613
11613
  return false;
11614
- const afterCd = peelLeadingCdAnd(trimmed);
11615
- if (afterCd === null)
11616
- return false;
11617
- const firstStage = firstPipelineStage(afterCd);
11618
- if (firstStage === null)
11619
- return false;
11620
- const firstToken = readShellToken(firstStage, skipSpaces(firstStage, 0));
11621
- if (firstToken === null)
11622
- return false;
11623
- return firstToken.token === "grep" || firstToken.token === "rg";
11614
+ for (const statement of statements) {
11615
+ const firstStage = firstPipelineStage(statement);
11616
+ if (firstStage === null)
11617
+ continue;
11618
+ const firstToken = readShellToken(firstStage, skipSpaces(firstStage, 0));
11619
+ if (firstToken === null)
11620
+ continue;
11621
+ if (firstToken.token === "grep" || firstToken.token === "rg")
11622
+ return true;
11623
+ }
11624
+ return false;
11624
11625
  }
11625
11626
  function maybeAppendGrepSearchHint(output, command, aftSearchRegistered) {
11626
11627
  if (output === "")
11627
11628
  return output;
11628
- if (!commandLeadsWithCodeSearch(command))
11629
+ if (!commandInvokesCodeSearch(command))
11629
11630
  return output;
11630
11631
  if (output.includes(GREP_SEARCH_HINT_PREFIX))
11631
11632
  return output;
@@ -11634,21 +11635,78 @@ function maybeAppendGrepSearchHint(output, command, aftSearchRegistered) {
11634
11635
 
11635
11636
  ${hint}`;
11636
11637
  }
11637
- function peelLeadingCdAnd(command) {
11638
- const first = readShellToken(command, skipSpaces(command, 0));
11639
- if (first === null)
11640
- return null;
11641
- if (first.token !== "cd")
11642
- return command;
11643
- const dir = readShellToken(command, skipSpaces(command, first.end));
11644
- if (dir === null)
11638
+ function splitTopLevelStatements(command) {
11639
+ const statements = [];
11640
+ let start = 0;
11641
+ let quote = "none";
11642
+ let escaped = false;
11643
+ let inBacktick = false;
11644
+ let parenDepth = 0;
11645
+ for (let index = 0;index < command.length; index++) {
11646
+ const ch = command[index];
11647
+ if (escaped) {
11648
+ escaped = false;
11649
+ continue;
11650
+ }
11651
+ if (quote === "single") {
11652
+ if (ch === "'")
11653
+ quote = "none";
11654
+ continue;
11655
+ }
11656
+ if (quote === "double") {
11657
+ if (ch === "\\")
11658
+ escaped = true;
11659
+ else if (ch === '"')
11660
+ quote = "none";
11661
+ continue;
11662
+ }
11663
+ if (inBacktick) {
11664
+ if (ch === "`")
11665
+ inBacktick = false;
11666
+ continue;
11667
+ }
11668
+ if (ch === "\\") {
11669
+ escaped = true;
11670
+ continue;
11671
+ }
11672
+ if (ch === "'") {
11673
+ quote = "single";
11674
+ continue;
11675
+ }
11676
+ if (ch === '"') {
11677
+ quote = "double";
11678
+ continue;
11679
+ }
11680
+ if (ch === "`") {
11681
+ inBacktick = true;
11682
+ continue;
11683
+ }
11684
+ if (ch === "(") {
11685
+ parenDepth++;
11686
+ continue;
11687
+ }
11688
+ if (ch === ")") {
11689
+ if (parenDepth > 0)
11690
+ parenDepth--;
11691
+ continue;
11692
+ }
11693
+ if (parenDepth > 0)
11694
+ continue;
11695
+ const next = command[index + 1];
11696
+ if (ch === "&" && next === "&" || ch === "|" && next === "|") {
11697
+ statements.push(command.slice(start, index));
11698
+ index++;
11699
+ start = index + 1;
11700
+ } else if (ch === ";" || ch === `
11701
+ ` || ch === "&") {
11702
+ statements.push(command.slice(start, index));
11703
+ start = index + 1;
11704
+ }
11705
+ }
11706
+ if (quote !== "none" || inBacktick || escaped)
11645
11707
  return null;
11646
- if (!dir.token)
11647
- return command;
11648
- const afterDir = skipSpaces(command, dir.end);
11649
- if (!command.startsWith("&&", afterDir))
11650
- return command;
11651
- return command.slice(afterDir + 2).trim();
11708
+ statements.push(command.slice(start));
11709
+ return statements;
11652
11710
  }
11653
11711
  function firstPipelineStage(command) {
11654
11712
  let quote = "none";
@@ -14250,37 +14308,67 @@ var GREP_GUARD_FLAGS = new Set([
14250
14308
  function maybeStripCompressorPipe(command, compressionEnabled) {
14251
14309
  if (!compressionEnabled)
14252
14310
  return { command, stripped: false };
14253
- if (containsUnsplittableConstruct(command))
14254
- return { command, stripped: false };
14255
- const chain = splitTopLevelAndChain(command);
14311
+ const chain = splitTopLevelCommandChain(command);
14256
14312
  if (chain === null)
14257
14313
  return { command, stripped: false };
14258
- const prefix = chain.slice(0, -1).map((segment) => segment.trim()).filter(Boolean);
14259
- const pipeline3 = chain[chain.length - 1] ?? "";
14260
- const stages = splitTopLevelPipeline(pipeline3);
14261
- if (stages.length < 2)
14314
+ let stripped = false;
14315
+ const droppedFilterChains = [];
14316
+ const rebuilt = chain.map(({ segment, separator }) => {
14317
+ const result = stripSinglePipelineSegment(segment);
14318
+ if (result.stripped) {
14319
+ stripped = true;
14320
+ droppedFilterChains.push(result.filters);
14321
+ }
14322
+ return `${result.segment}${separator}`;
14323
+ }).join("");
14324
+ if (!stripped)
14262
14325
  return { command, stripped: false };
14326
+ return {
14327
+ command: rebuilt,
14328
+ stripped: true,
14329
+ note: formatStripNote(droppedFilterChains)
14330
+ };
14331
+ }
14332
+ function stripSinglePipelineSegment(segment) {
14333
+ const leading = /^\s*/.exec(segment)?.[0] ?? "";
14334
+ const trailing = /\s*$/.exec(segment)?.[0] ?? "";
14335
+ const coreStart = leading.length;
14336
+ const coreEnd = segment.length - trailing.length;
14337
+ if (coreEnd <= coreStart)
14338
+ return { segment, stripped: false };
14339
+ const core = segment.slice(coreStart, coreEnd);
14340
+ if (containsUnsplittableConstruct(core))
14341
+ return { segment, stripped: false };
14342
+ if (hasUnquotedBackground(core))
14343
+ return { segment, stripped: false };
14344
+ const stages = splitTopLevelPipeline(core);
14345
+ if (stages.length < 2)
14346
+ return { segment, stripped: false };
14263
14347
  const firstStage = stages[0]?.trim() ?? "";
14264
14348
  if (!isCompressorHandledRunner(firstStage))
14265
- return { command, stripped: false };
14349
+ return { segment, stripped: false };
14266
14350
  const filterStages = stages.slice(1).map((stage) => stage.trim());
14267
14351
  for (const stage of filterStages) {
14268
14352
  if (!filterStageIsSafeToDrop(stage))
14269
- return { command, stripped: false };
14353
+ return { segment, stripped: false };
14270
14354
  }
14271
- const filters = filterStages.join(" | ");
14272
- const rebuilt = [...prefix, firstStage].join(" && ");
14273
14355
  return {
14274
- command: rebuilt,
14356
+ segment: `${leading}${firstStage}${trailing}`,
14275
14357
  stripped: true,
14276
- note: `[AFT dropped \`| ${filters}\` (compressed:false to keep)]`
14358
+ filters: filterStages.join(" | ")
14277
14359
  };
14278
14360
  }
14279
- function splitTopLevelAndChain(command) {
14361
+ function formatStripNote(droppedFilterChains) {
14362
+ const filters = droppedFilterChains.map((filter) => `\`| ${filter}\``).join(", ");
14363
+ return `[AFT dropped ${filters} (compressed:false to keep)]`;
14364
+ }
14365
+ function splitTopLevelCommandChain(command) {
14280
14366
  const segments = [];
14281
14367
  let start = 0;
14282
14368
  let quote = null;
14283
14369
  let escaped = false;
14370
+ let inBacktick = false;
14371
+ let parenDepth = 0;
14284
14372
  for (let index = 0;index < command.length; index++) {
14285
14373
  const char = command[index];
14286
14374
  const next = command[index + 1];
@@ -14288,6 +14376,13 @@ function splitTopLevelAndChain(command) {
14288
14376
  escaped = false;
14289
14377
  continue;
14290
14378
  }
14379
+ if (inBacktick) {
14380
+ if (char === "\\")
14381
+ escaped = true;
14382
+ else if (char === "`")
14383
+ inBacktick = false;
14384
+ continue;
14385
+ }
14291
14386
  if (char === "\\" && quote !== "'") {
14292
14387
  escaped = true;
14293
14388
  continue;
@@ -14301,26 +14396,47 @@ function splitTopLevelAndChain(command) {
14301
14396
  quote = char;
14302
14397
  continue;
14303
14398
  }
14304
- if (char === "&" && next === "&") {
14305
- segments.push(command.slice(start, index));
14306
- start = index + 2;
14307
- index++;
14399
+ if (char === "`") {
14400
+ inBacktick = true;
14308
14401
  continue;
14309
14402
  }
14310
- if (char === "|" && next === "|")
14311
- return null;
14312
- if (char === ";")
14313
- return null;
14403
+ if (char === "(") {
14404
+ parenDepth++;
14405
+ continue;
14406
+ }
14407
+ if (char === ")") {
14408
+ if (parenDepth === 0)
14409
+ return null;
14410
+ parenDepth--;
14411
+ continue;
14412
+ }
14413
+ if (parenDepth > 0)
14414
+ continue;
14314
14415
  if (char === `
14315
14416
  ` || char === "\r")
14316
14417
  return null;
14317
- if (char === "&") {
14318
- const prev = command[index - 1];
14319
- if (prev !== ">" && next !== ">")
14320
- return null;
14418
+ if (char === "#" && (index === 0 || command[index - 1] === " " || command[index - 1] === "\t"))
14419
+ return null;
14420
+ if (char === "&" && next === "&") {
14421
+ segments.push({ segment: command.slice(start, index), separator: "&&" });
14422
+ start = index + 2;
14423
+ index++;
14424
+ continue;
14425
+ }
14426
+ if (char === "|" && next === "|") {
14427
+ segments.push({ segment: command.slice(start, index), separator: "||" });
14428
+ start = index + 2;
14429
+ index++;
14430
+ continue;
14431
+ }
14432
+ if (char === ";") {
14433
+ segments.push({ segment: command.slice(start, index), separator: ";" });
14434
+ start = index + 1;
14321
14435
  }
14322
14436
  }
14323
- segments.push(command.slice(start));
14437
+ if (escaped || quote || inBacktick || parenDepth !== 0)
14438
+ return null;
14439
+ segments.push({ segment: command.slice(start), separator: "" });
14324
14440
  return segments;
14325
14441
  }
14326
14442
  function splitTopLevelPipeline(command) {
@@ -14383,13 +14499,13 @@ function isCompressorHandledRunner(stage) {
14383
14499
  args = args.slice(1);
14384
14500
  const sub = args[0];
14385
14501
  const subNext = args[1];
14386
- return sub === "test" || sub === "run" && startsWithTest(subNext);
14502
+ return sub === "test" || sub === "run" && isJsVerificationScript(subNext);
14387
14503
  }
14388
14504
  if (first === "npm" || first === "pnpm") {
14389
- return second === "test" || second === "run" && startsWithTest(third);
14505
+ return second === "test" || second === "run" && isJsVerificationScript(third);
14390
14506
  }
14391
14507
  if (first === "yarn") {
14392
- return startsWithTest(second) || second === "run" && startsWithTest(third);
14508
+ return isJsVerificationScript(second) || second === "run" && isJsVerificationScript(third);
14393
14509
  }
14394
14510
  if (first === "deno")
14395
14511
  return ["test", "lint", "check", "bench"].includes(second ?? "");
@@ -14467,8 +14583,10 @@ function hasBuildTask(args, tasks) {
14467
14583
  }
14468
14584
  return sawAllowed;
14469
14585
  }
14470
- function startsWithTest(token) {
14471
- return token?.startsWith("test") === true;
14586
+ function isJsVerificationScript(token) {
14587
+ if (!token)
14588
+ return false;
14589
+ return token.startsWith("test") || token === "typecheck" || token.startsWith("typecheck:");
14472
14590
  }
14473
14591
  var XCODEBUILD_VALUE_FLAGS = new Set([
14474
14592
  "-scheme",
@@ -33127,6 +33245,7 @@ function writeTerminal(terminal, data) {
33127
33245
  // src/shared/rpc-server.ts
33128
33246
  import { randomBytes as randomBytes2 } from "node:crypto";
33129
33247
  import {
33248
+ existsSync as existsSync14,
33130
33249
  mkdirSync as mkdirSync10,
33131
33250
  readdirSync as readdirSync5,
33132
33251
  readFileSync as readFileSync13,
@@ -33185,6 +33304,8 @@ function parseRpcPortRecord(content) {
33185
33304
  }
33186
33305
 
33187
33306
  // src/shared/rpc-server.ts
33307
+ var PORT_FILE_HEARTBEAT_MS = 15000;
33308
+
33188
33309
  class AftRpcServer {
33189
33310
  server = null;
33190
33311
  port = 0;
@@ -33192,6 +33313,7 @@ class AftRpcServer {
33192
33313
  handlers = new Map;
33193
33314
  portFilePath;
33194
33315
  portsDir;
33316
+ heartbeatTimer = null;
33195
33317
  instanceId;
33196
33318
  constructor(storageDir, directory) {
33197
33319
  this.portsDir = rpcPortFileDir(storageDir, directory);
@@ -33229,6 +33351,8 @@ class AftRpcServer {
33229
33351
  }), { encoding: "utf-8", mode: 384 });
33230
33352
  renameSync8(tmpPath, this.portFilePath);
33231
33353
  log2(`RPC server listening on 127.0.0.1:${this.port}`);
33354
+ this.heartbeatTimer = setInterval(() => this.ensurePortFile(), PORT_FILE_HEARTBEAT_MS);
33355
+ this.heartbeatTimer.unref?.();
33232
33356
  this.sweepDeadPortFiles();
33233
33357
  } catch (err) {
33234
33358
  warn2(`Failed to write RPC port file: ${err}`);
@@ -33263,7 +33387,28 @@ class AftRpcServer {
33263
33387
  } catch {}
33264
33388
  }
33265
33389
  }
33390
+ ensurePortFile() {
33391
+ if (!this.server || this.token === null)
33392
+ return;
33393
+ try {
33394
+ if (existsSync14(this.portFilePath))
33395
+ return;
33396
+ const tmpPath = `${this.portFilePath}.tmp`;
33397
+ writeFileSync10(tmpPath, JSON.stringify({
33398
+ port: this.port,
33399
+ token: this.token,
33400
+ pid: process.pid,
33401
+ started_at: Date.now()
33402
+ }), { encoding: "utf-8", mode: 384 });
33403
+ renameSync8(tmpPath, this.portFilePath);
33404
+ log2(`RPC port file was missing; rewrote ${this.portFilePath}`);
33405
+ } catch {}
33406
+ }
33266
33407
  stop() {
33408
+ if (this.heartbeatTimer) {
33409
+ clearInterval(this.heartbeatTimer);
33410
+ this.heartbeatTimer = null;
33411
+ }
33267
33412
  if (this.server) {
33268
33413
  this.server.close();
33269
33414
  this.server = null;
@@ -33622,7 +33767,7 @@ function formatStatusMarkdown(status) {
33622
33767
  }
33623
33768
  if (status.status_bar) {
33624
33769
  const sb = status.status_bar;
33625
- lines.push("", `### Code Health${sb.tier2_stale ? " (~ stale)" : ""}`, `- **Errors:** ${formatCount(sb.errors)}`, `- **Warnings:** ${formatCount(sb.warnings)}`, `- **Duplicates:** ${formatCount(sb.duplicates)}`, `- **TODOs:** ${formatCount(sb.todos)}`);
33770
+ lines.push("", `### Code Health${sb.tier2_stale ? " (~ stale)" : ""}`, `- **Errors:** ${formatCount(sb.errors)}`, `- **Warnings:** ${formatCount(sb.warnings)}`, `- **Dead code:** ${formatCount(sb.dead_code)}`, `- **Unused exports:** ${formatCount(sb.unused_exports)}`, `- **Duplicates:** ${formatCount(sb.duplicates)}`, `- **TODOs:** ${formatCount(sb.todos)}`);
33626
33771
  }
33627
33772
  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)}`);
33628
33773
  return lines.join(`
@@ -33631,7 +33776,7 @@ function formatStatusMarkdown(status) {
33631
33776
 
33632
33777
  // src/shared/tui-config.ts
33633
33778
  var import_comment_json4 = __toESM(require_src2(), 1);
33634
- import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync11 } from "node:fs";
33779
+ import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync11 } from "node:fs";
33635
33780
  import { dirname as dirname10, join as join21 } from "node:path";
33636
33781
 
33637
33782
  // src/shared/opencode-config-dir.ts
@@ -33668,9 +33813,9 @@ function resolveTuiConfigPath() {
33668
33813
  const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
33669
33814
  const jsoncPath = join21(configDir, "tui.jsonc");
33670
33815
  const jsonPath = join21(configDir, "tui.json");
33671
- if (existsSync14(jsoncPath))
33816
+ if (existsSync15(jsoncPath))
33672
33817
  return jsoncPath;
33673
- if (existsSync14(jsonPath))
33818
+ if (existsSync15(jsonPath))
33674
33819
  return jsonPath;
33675
33820
  return jsonPath;
33676
33821
  }
@@ -33678,7 +33823,7 @@ function ensureTuiPluginEntry() {
33678
33823
  try {
33679
33824
  const configPath = resolveTuiConfigPath();
33680
33825
  let config2 = {};
33681
- if (existsSync14(configPath)) {
33826
+ if (existsSync15(configPath)) {
33682
33827
  config2 = import_comment_json4.parse(readFileSync14(configPath, "utf-8")) ?? {};
33683
33828
  }
33684
33829
  const plugins = Array.isArray(config2.plugin) ? config2.plugin.filter((value) => typeof value === "string") : [];
@@ -33730,6 +33875,12 @@ async function runCleanups(reason) {
33730
33875
  runningCleanups = false;
33731
33876
  }
33732
33877
  }
33878
+ var SIGNAL_EXIT_CODES = { SIGINT: 130, SIGTERM: 143, SIGHUP: 129 };
33879
+ var SIGNAL_CLEANUP_TIMEOUT_MS = 5000;
33880
+ function shouldForceExit(otherListenerCount) {
33881
+ return otherListenerCount === 0;
33882
+ }
33883
+ var signalShutdownStarted = false;
33733
33884
  function installProcessHandlers() {
33734
33885
  const state = getState();
33735
33886
  if (state.installed)
@@ -33737,9 +33888,27 @@ function installProcessHandlers() {
33737
33888
  state.installed = true;
33738
33889
  const signals = ["SIGTERM", "SIGINT", "SIGHUP"];
33739
33890
  for (const sig of signals) {
33740
- process.on(sig, () => {
33741
- runCleanups(sig);
33742
- });
33891
+ const handler = () => {
33892
+ const others = process.listenerCount(sig) - 1;
33893
+ if (!shouldForceExit(others)) {
33894
+ const names = process.listeners(sig).filter((fn) => fn !== handler).map((fn) => fn.name || fn.toString().slice(0, 80).replace(/\s+/g, " ")).join(" | ");
33895
+ log2(`${sig}: deferring termination to ${others} other listener(s); cleanup only. Others: ${names}`);
33896
+ runCleanups(sig);
33897
+ return;
33898
+ }
33899
+ if (signalShutdownStarted) {
33900
+ process.exit(SIGNAL_EXIT_CODES[sig]);
33901
+ }
33902
+ signalShutdownStarted = true;
33903
+ process.exitCode = SIGNAL_EXIT_CODES[sig];
33904
+ const timeout = new Promise((resolve5) => {
33905
+ setTimeout(resolve5, SIGNAL_CLEANUP_TIMEOUT_MS);
33906
+ });
33907
+ Promise.race([runCleanups(sig), timeout]).finally(() => {
33908
+ process.exit(SIGNAL_EXIT_CODES[sig]);
33909
+ });
33910
+ };
33911
+ process.on(sig, handler);
33743
33912
  }
33744
33913
  process.on("beforeExit", () => {
33745
33914
  runCleanups("beforeExit");
@@ -33910,7 +34079,15 @@ ${candidates.map((candidate) => ` - ${candidate}`).join(`
33910
34079
  `);
33911
34080
  }
33912
34081
  function collectStructuredExtras(response) {
33913
- const reserved = new Set(["id", "success", "code", "message", "data"]);
34082
+ const reserved = new Set([
34083
+ "id",
34084
+ "success",
34085
+ "code",
34086
+ "message",
34087
+ "data",
34088
+ "status_bar",
34089
+ "bg_completions"
34090
+ ]);
33914
34091
  const extras = {};
33915
34092
  for (const [key, value] of Object.entries(response)) {
33916
34093
  if (reserved.has(key))
@@ -34190,16 +34367,24 @@ async function checkAstPathsPermission(context, paths) {
34190
34367
  }
34191
34368
  return;
34192
34369
  }
34193
- var SUPPORTED_LANGS = ["typescript", "tsx", "javascript", "python", "rust", "go"];
34370
+ var SUPPORTED_LANGS = [
34371
+ "typescript",
34372
+ "tsx",
34373
+ "javascript",
34374
+ "python",
34375
+ "rust",
34376
+ "go",
34377
+ "pascal"
34378
+ ];
34194
34379
  function astTools(ctx) {
34195
34380
  const searchTool = {
34196
- description: `Search code patterns across filesystem using AST-aware matching. Supports 6 languages.
34381
+ description: `Search code patterns across filesystem using AST-aware matching. Supports 7 languages.
34197
34382
 
34198
34383
  ` + `Use meta-variables: $VAR matches a single AST node, $$$ matches multiple nodes (variadic).
34199
34384
  ` + `IMPORTANT: Patterns must be complete AST nodes (valid code fragments).
34200
34385
  ` + `For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not just 'export async function $NAME'.
34201
34386
 
34202
- ` + "Examples: pattern='console.log($MSG)' lang='typescript', pattern='async function $NAME($$$) { $$$ }' lang='javascript', pattern='def $FUNC($$$): $$$' lang='python'",
34387
+ ` + "Examples: pattern='console.log($MSG)' lang='typescript', pattern='async function $NAME($$$) { $$$ }' lang='javascript', pattern='def $FUNC($$$): $$$' lang='python', pattern='Writeln($MSG);' lang='pascal'",
34203
34388
  args: {
34204
34389
  pattern: z3.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."),
34205
34390
  lang: z3.enum(SUPPORTED_LANGS).describe("Target language"),
@@ -35745,6 +35930,91 @@ var z8 = tool8.schema;
35745
35930
  function diagnosticsOnEditDefault(ctx) {
35746
35931
  return ctx.config.lsp?.diagnostics_on_edit ?? false;
35747
35932
  }
35933
+ async function readCurrentFileForPreview(filePath) {
35934
+ try {
35935
+ return await fs6.promises.readFile(filePath, "utf-8");
35936
+ } catch (error50) {
35937
+ if (error50 && typeof error50 === "object" && "code" in error50 && error50.code === "ENOENT") {
35938
+ return "";
35939
+ }
35940
+ throw error50;
35941
+ }
35942
+ }
35943
+ function previewDiffFromResponse(data, filePath) {
35944
+ const previewDiff = data.preview_diff;
35945
+ if (typeof previewDiff === "string")
35946
+ return previewDiff;
35947
+ const diff = data.diff;
35948
+ if (typeof diff?.before === "string" && typeof diff.after === "string") {
35949
+ return buildUnifiedDiff(filePath, diff.before, diff.after);
35950
+ }
35951
+ return "";
35952
+ }
35953
+ function virtualPatchContent(virtualFiles, filePath) {
35954
+ return virtualFiles.has(filePath) ? virtualFiles.get(filePath) ?? null : undefined;
35955
+ }
35956
+ async function readPatchPreviewContent(virtualFiles, filePath, action, patchPath) {
35957
+ const virtualContent = virtualPatchContent(virtualFiles, filePath);
35958
+ if (virtualContent !== undefined) {
35959
+ if (virtualContent === null) {
35960
+ throw new Error(`Failed to ${action} ${patchPath}: file not found: ${filePath}`);
35961
+ }
35962
+ return virtualContent;
35963
+ }
35964
+ try {
35965
+ return await fs6.promises.readFile(filePath, "utf-8");
35966
+ } catch (error50) {
35967
+ throw new Error(`Failed to ${action} ${patchPath}: ${formatError2(error50)}`);
35968
+ }
35969
+ }
35970
+ async function buildApplyPatchPreview(hunks, projectRoot) {
35971
+ const virtualFiles = new Map;
35972
+ const patches = [];
35973
+ let firstFilePath = "";
35974
+ for (const hunk of hunks) {
35975
+ const filePath = resolvePathFromProjectRoot(projectRoot, hunk.path);
35976
+ if (!firstFilePath)
35977
+ firstFilePath = filePath;
35978
+ switch (hunk.type) {
35979
+ case "add": {
35980
+ const virtualContent = virtualPatchContent(virtualFiles, filePath);
35981
+ const exists = virtualContent !== undefined ? virtualContent !== null : fs6.existsSync(filePath);
35982
+ if (exists) {
35983
+ throw new Error(`Failed to create ${hunk.path}: file already exists. Use *** Update File: to modify, or *** Delete File: first if you want to replace it entirely.`);
35984
+ }
35985
+ const after = hunk.contents.endsWith(`
35986
+ `) ? hunk.contents : `${hunk.contents}
35987
+ `;
35988
+ patches.push(buildUnifiedDiff(filePath, "", after));
35989
+ virtualFiles.set(filePath, after);
35990
+ break;
35991
+ }
35992
+ case "delete": {
35993
+ const before = await readPatchPreviewContent(virtualFiles, filePath, "delete", hunk.path);
35994
+ patches.push(buildUnifiedDiff(filePath, before, ""));
35995
+ virtualFiles.set(filePath, null);
35996
+ break;
35997
+ }
35998
+ case "update": {
35999
+ const before = await readPatchPreviewContent(virtualFiles, filePath, "update", hunk.path);
36000
+ let after;
36001
+ try {
36002
+ after = applyUpdateChunks(before, filePath, hunk.chunks);
36003
+ } catch (error50) {
36004
+ throw new Error(`Failed to update ${hunk.path}: ${formatError2(error50)}`);
36005
+ }
36006
+ const targetPath = hunk.move_path ? resolvePathFromProjectRoot(projectRoot, hunk.move_path) : filePath;
36007
+ patches.push(buildUnifiedDiff(targetPath, before, after));
36008
+ if (hunk.move_path)
36009
+ virtualFiles.set(filePath, null);
36010
+ virtualFiles.set(targetPath, after);
36011
+ break;
36012
+ }
36013
+ }
36014
+ }
36015
+ return { filepath: firstFilePath || projectRoot, diff: patches.join(`
36016
+ `) };
36017
+ }
35748
36018
  var READ_DESCRIPTION = `Read file contents or list directory entries.
35749
36019
 
35750
36020
  Use either startLine/endLine OR offset/limit to read a section of a file.
@@ -35889,16 +36159,18 @@ function createWriteTool(ctx, editToolName = "edit") {
35889
36159
  const filePath = resolvePathFromProjectRoot(projectRoot, file2);
35890
36160
  const relPath = path4.relative(projectRoot, filePath);
35891
36161
  {
35892
- const denial = await assertExternalDirectoryPermission(context, filePath);
35893
- if (denial)
35894
- return permissionDeniedResponse(denial);
35895
- }
35896
- await runAsk(context.ask({
35897
- permission: "edit",
35898
- patterns: [relPath],
35899
- always: ["*"],
35900
- metadata: { filepath: filePath }
35901
- }));
36162
+ const denial2 = await assertExternalDirectoryPermission(context, filePath);
36163
+ if (denial2)
36164
+ return permissionDeniedResponse(denial2);
36165
+ }
36166
+ const currentContent = await readCurrentFileForPreview(filePath);
36167
+ const previewDiff = buildUnifiedDiff(filePath, currentContent, content);
36168
+ const denial = await askEditPermission(context, [relPath], {
36169
+ filepath: filePath,
36170
+ diff: previewDiff
36171
+ });
36172
+ if (denial)
36173
+ return permissionDeniedResponse(denial);
35902
36174
  const data = await callBridge(ctx, context, "write", {
35903
36175
  file: filePath,
35904
36176
  content,
@@ -36038,16 +36310,10 @@ function createEditTool(ctx, writeToolName = "write") {
36038
36310
  const filePath = resolvePathFromProjectRoot(projectRoot, file2);
36039
36311
  const relPath = path4.relative(projectRoot, filePath);
36040
36312
  {
36041
- const denial = await assertExternalDirectoryPermission(context, filePath);
36042
- if (denial)
36043
- return permissionDeniedResponse(denial);
36313
+ const denial2 = await assertExternalDirectoryPermission(context, filePath);
36314
+ if (denial2)
36315
+ return permissionDeniedResponse(denial2);
36044
36316
  }
36045
- await runAsk(context.ask({
36046
- permission: "edit",
36047
- patterns: [relPath],
36048
- always: ["*"],
36049
- metadata: { filepath: filePath }
36050
- }));
36051
36317
  const params = { file: filePath };
36052
36318
  let command;
36053
36319
  if (typeof args.appendContent === "string") {
@@ -36090,6 +36356,21 @@ function createEditTool(ctx, writeToolName = "write") {
36090
36356
  const hint = typeof args.content === "string" ? ` To write the whole file, use the '${writeToolName}' tool. To edit existing content, provide 'oldString' (and optionally 'newString'), 'symbol' + 'content', or an 'edits' array.` : " Provide 'oldString' (+ optional 'newString'), 'symbol' + 'content', or an 'edits' array.";
36091
36357
  throw new Error(`edit: no edit mode resolved from arguments.${hint}`);
36092
36358
  }
36359
+ const previewData = await callBridge(ctx, context, command, {
36360
+ ...params,
36361
+ preview: true,
36362
+ include_diff_content: true
36363
+ });
36364
+ if (previewData.success === false) {
36365
+ throw new Error(previewData.message || "edit preview failed");
36366
+ }
36367
+ const previewDiff = previewDiffFromResponse(previewData, filePath);
36368
+ const denial = await askEditPermission(context, [relPath], {
36369
+ filepath: filePath,
36370
+ diff: previewDiff
36371
+ });
36372
+ if (denial)
36373
+ return permissionDeniedResponse(denial);
36093
36374
  params.diagnostics = diagnosticsOnEditDefault(ctx);
36094
36375
  params.include_diff_content = true;
36095
36376
  const data = await callBridge(ctx, context, command, params);
@@ -36253,17 +36534,18 @@ function createApplyPatchTool(ctx) {
36253
36534
  if (asked.has(filePath))
36254
36535
  continue;
36255
36536
  asked.add(filePath);
36256
- const denial = await assertExternalDirectoryPermission(context, filePath);
36257
- if (denial)
36258
- return permissionDeniedResponse(denial);
36537
+ const denial2 = await assertExternalDirectoryPermission(context, filePath);
36538
+ if (denial2)
36539
+ return permissionDeniedResponse(denial2);
36259
36540
  }
36260
36541
  }
36261
- await runAsk(context.ask({
36262
- permission: "edit",
36263
- patterns: relPaths,
36264
- always: ["*"],
36265
- metadata: {}
36266
- }));
36542
+ const preview2 = await buildApplyPatchPreview(hunks, projectRoot);
36543
+ const denial = await askEditPermission(context, relPaths, {
36544
+ filepath: preview2.filepath,
36545
+ diff: preview2.diff
36546
+ });
36547
+ if (denial)
36548
+ return permissionDeniedResponse(denial);
36267
36549
  const checkpointPaths = Array.from(affectedAbs).filter((abs) => !newlyCreatedAbs.has(abs));
36268
36550
  const checkpointName = `apply_patch_${Date.now()}`;
36269
36551
  let checkpointCreated = false;
@@ -36635,16 +36917,18 @@ function aftPrefixedTools(ctx) {
36635
36917
  const filePath = resolvePathFromProjectRoot(projectRoot, file2);
36636
36918
  const relPath = path4.relative(projectRoot, filePath);
36637
36919
  {
36638
- const denial = await assertExternalDirectoryPermission(context, filePath);
36639
- if (denial)
36640
- return permissionDeniedResponse(denial);
36920
+ const denial2 = await assertExternalDirectoryPermission(context, filePath);
36921
+ if (denial2)
36922
+ return permissionDeniedResponse(denial2);
36641
36923
  }
36642
- await runAsk(context.ask({
36643
- permission: "edit",
36644
- patterns: [relPath],
36645
- always: ["*"],
36646
- metadata: { filepath: filePath }
36647
- }));
36924
+ const currentContent = await readCurrentFileForPreview(filePath);
36925
+ const previewDiff = buildUnifiedDiff(filePath, currentContent, normalizedArgs.content);
36926
+ const denial = await askEditPermission(context, [relPath], {
36927
+ filepath: filePath,
36928
+ diff: previewDiff
36929
+ });
36930
+ if (denial)
36931
+ return permissionDeniedResponse(denial);
36648
36932
  const writeParams = {
36649
36933
  file: filePath,
36650
36934
  content: normalizedArgs.content,
@@ -38091,12 +38375,12 @@ var PLUGIN_VERSION = (() => {
38091
38375
  return "0.0.0";
38092
38376
  }
38093
38377
  })();
38094
- var ANNOUNCEMENT_VERSION = "0.37.0";
38378
+ var ANNOUNCEMENT_VERSION = "0.39.0";
38095
38379
  var ANNOUNCEMENT_FEATURES = [
38096
- "Much lower background CPU: idle bridges shut down after 30 minutes, deleted project roots stop the watcher instead of spinning it, and background scans reuse warm caches on a bounded thread pool.",
38097
- "`aft_transform` removed: usage data showed agents never called it`edit` covers everything it did, and every request now carries a smaller tool surface.",
38098
- "Leaner tools: `bash`, `write`, `edit`, `apply_patch`, `aft_search`, and `aft_refactor` descriptions trimmed and config-aware; system-prompt guidance rewritten around what to do instead of what to avoid.",
38099
- "Background bash reminders are right-sized: failures keep head + tail context, successes show a short tail, and compressors never print a success summary for a non-zero exit."
38380
+ "Accurate Rust dead code: constructors and associated functions reached through receiver-type inference are no longer mis-reported as dead.",
38381
+ "Code Health scans reparse only the file you changed instead of the whole project on every edit a single-file edit dropped from ~3s of scan work to ~130ms on this repo, byte-identical results.",
38382
+ "R language support in outline, zoom, and the AST tools.",
38383
+ "Edit approval prompts now show the pending diff instead of 'No diff provided', so you can review a change before approving it."
38100
38384
  ];
38101
38385
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
38102
38386
  var plugin = async (input) => initializePluginForDirectory(input);
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-client.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;CACvC;AA4BD,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,iBAAiB,CAA6B;gBAE1C,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAKjD,iFAAiF;IAC3E,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,CAAC,CAAC;YA8CC,OAAO;IAsBrB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAM5B,4CAA4C;IACtC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IASrC;;;;OAIG;YACW,gBAAgB;IAwB9B,OAAO,CAAC,gBAAgB;IAwDxB,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,iBAAiB;YAsBX,WAAW;YAkCX,gBAAgB;IAmB9B,KAAK,IAAI,IAAI;CAKd"}
1
+ {"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-client.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;CACvC;AA4BD,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,iBAAiB,CAA6B;gBAE1C,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAKjD,iFAAiF;IAC3E,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,CAAC,CAAC;YA8CC,OAAO;IAsBrB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAM5B,4CAA4C;IACtC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IASrC;;;;OAIG;YACW,gBAAgB;IAwB9B,OAAO,CAAC,gBAAgB;IAwDxB,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,iBAAiB;YA6BX,WAAW;YAkCX,gBAAgB;IAmB9B,KAAK,IAAI,IAAI;CAKd"}
@@ -6,6 +6,7 @@ export declare class AftRpcServer {
6
6
  private handlers;
7
7
  private portFilePath;
8
8
  private portsDir;
9
+ private heartbeatTimer;
9
10
  /** Unique per-instance ID — distinguishes our entry from duplicate plugin loads. */
10
11
  private instanceId;
11
12
  constructor(storageDir: string, directory: string);
@@ -20,6 +21,8 @@ export declare class AftRpcServer {
20
21
  * Never touches our own freshly written file.
21
22
  */
22
23
  private sweepDeadPortFiles;
24
+ /** Rewrite the port file if it disappeared (wrongful deletion recovery). */
25
+ private ensurePortFile;
23
26
  /** Stop the server and clean up port file. */
24
27
  stop(): void;
25
28
  private dispatch;
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-server.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-server.ts"],"names":[],"mappings":"AAcA,KAAK,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAExF,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAS;IACzB,oFAAoF;IACpF,OAAO,CAAC,UAAU,CAAS;gBAEf,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAMjD,sCAAsC;IACtC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI;IAIjD,6DAA6D;IACvD,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IA0D9B;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IA6B1B,8CAA8C;IAC9C,IAAI,IAAI,IAAI;IAaZ,OAAO,CAAC,QAAQ;CAoEjB"}
1
+ {"version":3,"file":"rpc-server.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-server.ts"],"names":[],"mappings":"AAeA,KAAK,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAIxF,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAA+C;IACrE,oFAAoF;IACpF,OAAO,CAAC,UAAU,CAAS;gBAEf,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAMjD,sCAAsC;IACtC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI;IAIjD,6DAA6D;IACvD,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAkE9B;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IA6B1B,4EAA4E;IAC5E,OAAO,CAAC,cAAc;IAsBtB,8CAA8C;IAC9C,IAAI,IAAI,IAAI;IAiBZ,OAAO,CAAC,QAAQ;CAoEjB"}
@@ -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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"}
@@ -16,6 +16,23 @@
16
16
  */
17
17
  type Cleanup = () => Promise<void> | void;
18
18
  export declare function runCleanups(reason: string): Promise<void>;
19
+ /** Conventional exit codes for fatal signals (128 + signal number). */
20
+ export declare const SIGNAL_EXIT_CODES: {
21
+ readonly SIGINT: 130;
22
+ readonly SIGTERM: 143;
23
+ readonly SIGHUP: 129;
24
+ };
25
+ /**
26
+ * Decide whether AFT's handler must terminate the process after cleanup.
27
+ *
28
+ * Registering ANY listener for SIGINT/SIGTERM/SIGHUP disables Node/Bun's
29
+ * default terminate-on-signal behavior. If AFT's listener is the only one,
30
+ * the default was suppressed solely by us, so we must exit or the host hangs
31
+ * forever (OpenCode serve + Desktop /event SSE hung on Ctrl-C until SIGKILL).
32
+ * If the host or another plugin also listens, terminating is THEIR call —
33
+ * forcing an exit here would race a host's graceful shutdown.
34
+ */
35
+ export declare function shouldForceExit(otherListenerCount: number): boolean;
19
36
  /**
20
37
  * Register a shutdown cleanup. Call from plugin initialization; returned
21
38
  * function unregisters (use in `dispose` so plugin reloads don't leak).
@@ -1 +1 @@
1
- {"version":3,"file":"shutdown-hooks.d.ts","sourceRoot":"","sources":["../src/shutdown-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,KAAK,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAoB1C,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/D;AA0BD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,OAAO,GAAG,MAAM,IAAI,CAO/D"}
1
+ {"version":3,"file":"shutdown-hooks.d.ts","sourceRoot":"","sources":["../src/shutdown-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,KAAK,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAoB1C,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/D;AAED,uEAAuE;AACvE,eAAO,MAAM,iBAAiB;;;;CAAsD,CAAC;AAKrF;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,kBAAkB,EAAE,MAAM,GAAG,OAAO,CAEnE;AA2DD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,OAAO,GAAG,MAAM,IAAI,CAO/D"}
@@ -1 +1 @@
1
- {"version":3,"file":"_shared.d.ts","sourceRoot":"","sources":["../../src/tools/_shared.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAUhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIjD;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,GACR,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,OAAO,EACV,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,SAAS,CAWpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAMpD;AAMD,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAIhD,OAAO,EAAE,+BAA+B,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AA2B3F,8EAA8E;AAC9E,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,MAAM,CAsDR;AAsBD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAW3D;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQjD;AAED;;2BAE2B;AAC3B,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGtF;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,GAAG,YAAY,CAEhF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAgClC;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAMlC;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAE9F"}
1
+ {"version":3,"file":"_shared.d.ts","sourceRoot":"","sources":["../../src/tools/_shared.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAUhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIjD;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,GACR,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,OAAO,EACV,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,SAAS,CAepB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAMpD;AAMD,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAIhD,OAAO,EAAE,+BAA+B,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AA2B3F,8EAA8E;AAC9E,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,MAAM,CAsDR;AAiCD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAW3D;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQjD;AAED;;2BAE2B;AAC3B,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGtF;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,GAAG,YAAY,CAEhF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAgClC;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAMlC;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAE9F"}
@@ -1 +1 @@
1
- {"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/tools/ast.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA6DjD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAwS3E"}
1
+ {"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/tools/ast.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAqEjD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAwS3E"}
@@ -1 +1 @@
1
- {"version":3,"file":"hoisted.d.ts","sourceRoot":"","sources":["../../src/tools/hoisted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,qBAAqB,CAAC;AAItE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAwEjD,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,GAAI,IAAI,MAAM,EAAE,QAAQ,MAAM,EAAE,OAAO,MAAM,KAAG,MAChD,CAAC;AA4RtC;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAwJjE;AA+mCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA0B/E;AAMD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA+FnF"}
1
+ {"version":3,"file":"hoisted.d.ts","sourceRoot":"","sources":["../../src/tools/hoisted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,qBAAqB,CAAC;AAItE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAyEjD,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,GAAI,IAAI,MAAM,EAAE,QAAQ,MAAM,EAAE,OAAO,MAAM,KAAG,MAChD,CAAC;AA8YtC;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAwJjE;AAmnCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA0B/E;AAMD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAkGnF"}
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.37.2";
4
+ var version = "0.39.0";
5
5
 
6
6
  // src/shared/rpc-client.ts
7
7
  import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
@@ -248,6 +248,8 @@ class AftRpcClient {
248
248
  return;
249
249
  }
250
250
  this.stalePortFailures.delete(key);
251
+ if (info.pid === undefined || isPidAlive(info.pid))
252
+ return;
251
253
  try {
252
254
  const current = this.parsePortFile(info.path);
253
255
  if (current?.port === info.port && current.token === info.token) {
@@ -599,7 +601,7 @@ function collapsedHealthLights(statusBar) {
599
601
  if (!statusBar)
600
602
  return null;
601
603
  const diagnostics = statusBar.errors > 0 ? "err" : statusBar.warnings > 0 ? "warn" : "ok";
602
- const code = statusBar.duplicates > 0 ? "warn" : "ok";
604
+ const code = statusBar.dead_code > 0 || statusBar.unused_exports > 0 || statusBar.duplicates > 0 ? "warn" : "ok";
603
605
  const todos = statusBar.todos > 0 ? "warn" : "ok";
604
606
  return { diagnostics, code, todos };
605
607
  }
@@ -1022,6 +1024,18 @@ var SidebarContent = (props) => {
1022
1024
  value: formatCount(statusBar().warnings),
1023
1025
  tone: statusBar().warnings > 0 ? "warn" : "muted"
1024
1026
  }, undefined, false, undefined, this),
1027
+ /* @__PURE__ */ jsxDEV(StatRow, {
1028
+ theme: props.theme,
1029
+ label: "Dead Code",
1030
+ value: formatCount(statusBar().dead_code),
1031
+ tone: "muted"
1032
+ }, undefined, false, undefined, this),
1033
+ /* @__PURE__ */ jsxDEV(StatRow, {
1034
+ theme: props.theme,
1035
+ label: "Unused Exports",
1036
+ value: formatCount(statusBar().unused_exports),
1037
+ tone: "muted"
1038
+ }, undefined, false, undefined, this),
1025
1039
  /* @__PURE__ */ jsxDEV(StatRow, {
1026
1040
  theme: props.theme,
1027
1041
  label: "Duplicates",
@@ -1556,6 +1570,18 @@ var StatusDialog = (props) => {
1556
1570
  value: formatCountShort(status().status_bar.warnings),
1557
1571
  tone: status().status_bar.warnings > 0 ? "warn" : "muted"
1558
1572
  }, undefined, false, undefined, this),
1573
+ /* @__PURE__ */ jsxDEV2(R, {
1574
+ theme: t(),
1575
+ label: "Dead Code",
1576
+ value: formatCountShort(status().status_bar.dead_code),
1577
+ tone: "muted"
1578
+ }, undefined, false, undefined, this),
1579
+ /* @__PURE__ */ jsxDEV2(R, {
1580
+ theme: t(),
1581
+ label: "Unused Exports",
1582
+ value: formatCountShort(status().status_bar.unused_exports),
1583
+ tone: "muted"
1584
+ }, undefined, false, undefined, this),
1559
1585
  /* @__PURE__ */ jsxDEV2(R, {
1560
1586
  theme: t(),
1561
1587
  label: "Duplicates",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.37.2",
3
+ "version": "0.39.0",
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.37.2",
33
+ "@cortexkit/aft-bridge": "0.39.0",
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.37.2",
41
- "@cortexkit/aft-darwin-x64": "0.37.2",
42
- "@cortexkit/aft-linux-arm64": "0.37.2",
43
- "@cortexkit/aft-linux-x64": "0.37.2",
44
- "@cortexkit/aft-win32-arm64": "0.37.2",
45
- "@cortexkit/aft-win32-x64": "0.37.2"
40
+ "@cortexkit/aft-darwin-arm64": "0.39.0",
41
+ "@cortexkit/aft-darwin-x64": "0.39.0",
42
+ "@cortexkit/aft-linux-arm64": "0.39.0",
43
+ "@cortexkit/aft-linux-x64": "0.39.0",
44
+ "@cortexkit/aft-win32-arm64": "0.39.0",
45
+ "@cortexkit/aft-win32-x64": "0.39.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@opencode-ai/plugin": "^1.15.11",
@@ -281,6 +281,13 @@ export class AftRpcClient {
281
281
  }
282
282
 
283
283
  this.stalePortFailures.delete(key);
284
+ // Deletion requires a provably-dead owner. Health checks time out under
285
+ // host load (blocked event loop during builds/streaming), and unlinking a
286
+ // LIVE server's port file is permanent: the server only wrote it at
287
+ // startup, so the sidebar could never reconnect until host restart
288
+ // (issue #110). Live-pid and pid-less files are skipped this round and
289
+ // retried on the next poll instead.
290
+ if (info.pid === undefined || isPidAlive(info.pid)) return;
284
291
  try {
285
292
  // Do not unlink a replacement written after the failed health checks.
286
293
  const current = this.parsePortFile(info.path);
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import {
3
+ existsSync,
3
4
  mkdirSync,
4
5
  readdirSync,
5
6
  readFileSync,
@@ -14,6 +15,8 @@ import { isPidAlive, parseRpcPortRecord, rpcPortFileDir } from "./rpc-utils";
14
15
 
15
16
  type RpcHandler = (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
16
17
 
18
+ const PORT_FILE_HEARTBEAT_MS = 15_000;
19
+
17
20
  export class AftRpcServer {
18
21
  private server: Server | null = null;
19
22
  private port = 0;
@@ -21,6 +24,7 @@ export class AftRpcServer {
21
24
  private handlers = new Map<string, RpcHandler>();
22
25
  private portFilePath: string;
23
26
  private portsDir: string;
27
+ private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
24
28
  /** Unique per-instance ID — distinguishes our entry from duplicate plugin loads. */
25
29
  private instanceId: string;
26
30
 
@@ -75,6 +79,14 @@ export class AftRpcServer {
75
79
  );
76
80
  renameSync(tmpPath, this.portFilePath);
77
81
  log(`RPC server listening on 127.0.0.1:${this.port}`);
82
+ // Self-heal: the port file is this server's only discoverability
83
+ // record, and historically it was written exactly once — anything
84
+ // that deleted it (a client misjudging a load-induced health-check
85
+ // timeout as staleness, cache cleanup, manual sweeps) silently
86
+ // orphaned the server until host restart (issue #110: sidebar stuck
87
+ // on the lazy placeholder forever). Recreate it if it goes missing.
88
+ this.heartbeatTimer = setInterval(() => this.ensurePortFile(), PORT_FILE_HEARTBEAT_MS);
89
+ this.heartbeatTimer.unref?.();
78
90
  // Hygiene: sweep dead siblings while we're here. The client only
79
91
  // reclaims dead-pid files on a cold port scan, and it caches the
80
92
  // first warm port afterwards — so crash/restart leftovers piled up
@@ -129,8 +141,35 @@ export class AftRpcServer {
129
141
  }
130
142
  }
131
143
 
144
+ /** Rewrite the port file if it disappeared (wrongful deletion recovery). */
145
+ private ensurePortFile(): void {
146
+ if (!this.server || this.token === null) return;
147
+ try {
148
+ if (existsSync(this.portFilePath)) return;
149
+ const tmpPath = `${this.portFilePath}.tmp`;
150
+ writeFileSync(
151
+ tmpPath,
152
+ JSON.stringify({
153
+ port: this.port,
154
+ token: this.token,
155
+ pid: process.pid,
156
+ started_at: Date.now(),
157
+ }),
158
+ { encoding: "utf-8", mode: 0o600 },
159
+ );
160
+ renameSync(tmpPath, this.portFilePath);
161
+ log(`RPC port file was missing; rewrote ${this.portFilePath}`);
162
+ } catch {
163
+ // best-effort; retried on the next heartbeat
164
+ }
165
+ }
166
+
132
167
  /** Stop the server and clean up port file. */
133
168
  stop(): void {
169
+ if (this.heartbeatTimer) {
170
+ clearInterval(this.heartbeatTimer);
171
+ this.heartbeatTimer = null;
172
+ }
134
173
  if (this.server) {
135
174
  this.server.close();
136
175
  this.server = null;
@@ -337,9 +337,8 @@ export function formatStatusDialogMessage(status: AftStatusSnapshot): string {
337
337
  `Code Health${sb.tier2_stale ? " (~ stale)" : ""}`,
338
338
  `- errors: ${formatCount(sb.errors)}`,
339
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)}`,
340
+ `- dead code: ${formatCount(sb.dead_code)}`,
341
+ `- unused exports: ${formatCount(sb.unused_exports)}`,
343
342
  `- duplicates: ${formatCount(sb.duplicates)}`,
344
343
  `- todos: ${formatCount(sb.todos)}`,
345
344
  );
@@ -447,9 +446,8 @@ export function formatStatusMarkdown(status: AftStatusSnapshot): string {
447
446
  `### Code Health${sb.tier2_stale ? " (~ stale)" : ""}`,
448
447
  `- **Errors:** ${formatCount(sb.errors)}`,
449
448
  `- **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)}`,
449
+ `- **Dead code:** ${formatCount(sb.dead_code)}`,
450
+ `- **Unused exports:** ${formatCount(sb.unused_exports)}`,
453
451
  `- **Duplicates:** ${formatCount(sb.duplicates)}`,
454
452
  `- **TODOs:** ${formatCount(sb.todos)}`,
455
453
  );
package/src/tui/index.tsx CHANGED
@@ -507,9 +507,7 @@ const StatusDialog = (props: StatusDialogProps) => {
507
507
  value={formatCountShort(status()!.status_bar!.warnings)}
508
508
  tone={status()!.status_bar!.warnings > 0 ? "warn" : "muted"}
509
509
  />
510
- {/* Dead Code / Unused Exports hidden until the oxc resolver lands
511
- (current scanner over-reports on barrel re-exports). Restore both. */}
512
- {/* <R
510
+ <R
513
511
  theme={t()}
514
512
  label="Dead Code"
515
513
  value={formatCountShort(status()!.status_bar!.dead_code)}
@@ -520,7 +518,7 @@ const StatusDialog = (props: StatusDialogProps) => {
520
518
  label="Unused Exports"
521
519
  value={formatCountShort(status()!.status_bar!.unused_exports)}
522
520
  tone="muted"
523
- /> */}
521
+ />
524
522
  <R
525
523
  theme={t()}
526
524
  label="Duplicates"
@@ -185,11 +185,8 @@ export type HealthLightTone = "ok" | "warn" | "err";
185
185
  export interface HealthLights {
186
186
  // Diagnostics: red if any errors, yellow if any warnings, else green.
187
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.
188
+ // Code cruft: yellow if there is any dead code, unused export, or duplicate,
189
+ // green when zero. Never red — cruft is not a build failure.
193
190
  code: HealthLightTone;
194
191
  // TODOs: yellow if any, else green.
195
192
  todos: HealthLightTone;
@@ -202,8 +199,9 @@ export function collapsedHealthLights(statusBar: StatusBar | undefined): HealthL
202
199
  const diagnostics: HealthLightTone =
203
200
  statusBar.errors > 0 ? "err" : statusBar.warnings > 0 ? "warn" : "ok";
204
201
  const code: HealthLightTone =
205
- // statusBar.dead_code > 0 || statusBar.unused_exports > 0 || // restore with oxc engine
206
- statusBar.duplicates > 0 ? "warn" : "ok";
202
+ statusBar.dead_code > 0 || statusBar.unused_exports > 0 || statusBar.duplicates > 0
203
+ ? "warn"
204
+ : "ok";
207
205
  const todos: HealthLightTone = statusBar.todos > 0 ? "warn" : "ok";
208
206
  return { diagnostics, code, todos };
209
207
  }
@@ -744,11 +742,7 @@ const SidebarContent = (props: {
744
742
  value={formatCount(statusBar()!.warnings)}
745
743
  tone={statusBar()!.warnings > 0 ? "warn" : "muted"}
746
744
  />
747
- {/* Dead Code / Unused Exports temporarily hidden until the
748
- oxc-based resolver lands and makes these trustworthy on real
749
- TS/JS codebases (current tree-sitter scanner over-reports via
750
- barrel re-export gaps). Restore both rows when that ships. */}
751
- {/* <StatRow
745
+ <StatRow
752
746
  theme={props.theme}
753
747
  label="Dead Code"
754
748
  value={formatCount(statusBar()!.dead_code)}
@@ -759,7 +753,7 @@ const SidebarContent = (props: {
759
753
  label="Unused Exports"
760
754
  value={formatCount(statusBar()!.unused_exports)}
761
755
  tone="muted"
762
- /> */}
756
+ />
763
757
  <StatRow
764
758
  theme={props.theme}
765
759
  label="Duplicates"