@cortexkit/aft-opencode 0.34.0 → 0.35.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/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`));
@@ -13717,7 +13798,8 @@ class BridgePool {
13717
13798
  onBashLongRunning: options.onBashLongRunning,
13718
13799
  onBashPatternMatch: options.onBashPatternMatch,
13719
13800
  errorPrefix: options.errorPrefix,
13720
- logger: options.logger
13801
+ logger: options.logger,
13802
+ childEnv: options.childEnv
13721
13803
  };
13722
13804
  this.configOverrides = configOverrides;
13723
13805
  if (Number.isFinite(this.idleTimeoutMs)) {
@@ -13850,6 +13932,9 @@ class BridgePool {
13850
13932
  _testGetConfigOverrides() {
13851
13933
  return { ...this.configOverrides };
13852
13934
  }
13935
+ _testGetBridgeOptions() {
13936
+ return { ...this.bridgeOptions };
13937
+ }
13853
13938
  }
13854
13939
  function normalizeKey(projectRoot) {
13855
13940
  const stripped = projectRoot.replace(/[/\\]+$/, "");
@@ -28473,8 +28558,8 @@ var LspExtensionSchema = exports_external.string().trim().min(1).refine((value)
28473
28558
  message: "Extension must include characters other than leading dots"
28474
28559
  });
28475
28560
  var LspServerEntrySchema = exports_external.object({
28476
- extensions: exports_external.array(LspExtensionSchema).min(1),
28477
- binary: exports_external.string().trim().min(1),
28561
+ extensions: exports_external.array(LspExtensionSchema).min(1).optional(),
28562
+ binary: exports_external.string().trim().min(1).optional(),
28478
28563
  args: exports_external.array(exports_external.string()).optional().default([]),
28479
28564
  root_markers: exports_external.array(exports_external.string().trim().min(1)).optional().default([".git"]),
28480
28565
  disabled: exports_external.boolean().optional().default(false),
@@ -28579,12 +28664,16 @@ function resolveLspConfigForConfigure(config2) {
28579
28664
  const servers = Object.entries(config2.lsp?.servers ?? {}).map(([id, server]) => {
28580
28665
  const entry = {
28581
28666
  id,
28582
- extensions: server.extensions.map(normalizeLspExtension),
28583
- binary: server.binary,
28584
28667
  args: server.args,
28585
28668
  root_markers: server.root_markers,
28586
28669
  disabled: server.disabled
28587
28670
  };
28671
+ if (server.extensions && server.extensions.length > 0) {
28672
+ entry.extensions = server.extensions.map(normalizeLspExtension);
28673
+ }
28674
+ if (server.binary) {
28675
+ entry.binary = server.binary;
28676
+ }
28588
28677
  if (server.env && Object.keys(server.env).length > 0) {
28589
28678
  entry.env = server.env;
28590
28679
  }
@@ -32015,29 +32104,6 @@ function normalizeToolMap(tools) {
32015
32104
  }
32016
32105
  return tools;
32017
32106
  }
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
32107
  // src/shared/pty-cache.ts
32042
32108
  var import_headless = __toESM(require_xterm_headless(), 1);
32043
32109
  import * as fs2 from "node:fs/promises";
@@ -32631,6 +32697,22 @@ function registerShutdownCleanup(fn) {
32631
32697
  };
32632
32698
  }
32633
32699
 
32700
+ // src/status-bar-inject.ts
32701
+ var emitStateBySession = new Map;
32702
+ function statusBarSuffixForSession(sessionID, counts) {
32703
+ if (!counts)
32704
+ return "";
32705
+ let state = emitStateBySession.get(sessionID);
32706
+ if (!state) {
32707
+ state = createStatusBarEmitState();
32708
+ emitStateBySession.set(sessionID, state);
32709
+ }
32710
+ return shouldEmitStatusBar(state, counts) ? statusBarLine(counts) : "";
32711
+ }
32712
+ function clearStatusBarSession(sessionID) {
32713
+ emitStateBySession.delete(sessionID);
32714
+ }
32715
+
32634
32716
  // src/tools/ast.ts
32635
32717
  import { tool as tool3 } from "@opencode-ai/plugin";
32636
32718
 
@@ -36905,6 +36987,7 @@ function buildWorkflowHints(opts) {
36905
36987
  }
36906
36988
  if (hasInspect) {
36907
36989
  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.");
36990
+ 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
36991
  }
36909
36992
  if (hasNavigate) {
36910
36993
  sections.push([
@@ -36984,13 +37067,14 @@ var PLUGIN_VERSION = (() => {
36984
37067
  return "0.0.0";
36985
37068
  }
36986
37069
  })();
36987
- var ANNOUNCEMENT_VERSION = "0.34.0";
37070
+ var ANNOUNCEMENT_VERSION = "0.35.0";
36988
37071
  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."
37072
+ "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.",
37073
+ "New IDE-style status bar on tool results — live LSP errors/warnings plus dead-code, unused-export, duplicate, and TODO counts at a glance.",
37074
+ "`aft_inspect` is more accurate (dead-code reachability, duplicate collapse, ranked findings) and a background scheduler keeps its counts fresh during long sessions.",
37075
+ "The TUI sidebar is now collapsible — click the `AFT` header to toggle a condensed view.",
37076
+ "`npx @cortexkit/aft --version` reports CLI, binary, and per-harness versions; `doctor --issue` can scope its report to a single session.",
37077
+ "New SCSS support and `.inc` files parsed as PHP."
36994
37078
  ];
36995
37079
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
36996
37080
  var plugin = async (input) => initializePluginForDirectory(input);
@@ -37398,6 +37482,7 @@ Install: ${getManualInstallHint()}`).catch(() => {});
37398
37482
  const sessionID = extractSessionID(eventInput.event.properties);
37399
37483
  if ((eventType === "session.deleted" || eventType === "session.shutdown") && sessionID) {
37400
37484
  inspectTier2Idle.clear(sessionID);
37485
+ clearStatusBarSession(sessionID);
37401
37486
  return;
37402
37487
  }
37403
37488
  if (eventType !== "session.idle")
@@ -37460,6 +37545,12 @@ Install: ${getManualInstallHint()}`).catch(() => {});
37460
37545
  }
37461
37546
  const sessionDir = getSessionDirectoryCached(toolInput.sessionID) ?? input.directory;
37462
37547
  await appendInTurnBgCompletions({ ctx, directory: sessionDir, sessionID: toolInput.sessionID }, output);
37548
+ if (output.output !== undefined) {
37549
+ const activeBridge = ctx.pool.getActiveBridgeForRoot(sessionDir);
37550
+ const suffix = statusBarSuffixForSession(toolInput.sessionID, activeBridge?.getStatusBar());
37551
+ if (suffix)
37552
+ output.output += suffix;
37553
+ }
37463
37554
  },
37464
37555
  config: async (config2) => {
37465
37556
  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.0";
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.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.34.0",
33
+ "@cortexkit/aft-bridge": "0.35.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.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.0",
41
+ "@cortexkit/aft-darwin-x64": "0.35.0",
42
+ "@cortexkit/aft-linux-arm64": "0.35.0",
43
+ "@cortexkit/aft-linux-x64": "0.35.0",
44
+ "@cortexkit/aft-win32-arm64": "0.35.0",
45
+ "@cortexkit/aft-win32-x64": "0.35.0"
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