@cortexkit/aft-opencode 0.23.0 → 0.24.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.
@@ -39,7 +39,6 @@ interface DrainContext {
39
39
  ctx: PluginContext;
40
40
  directory: string;
41
41
  sessionID: string;
42
- isActive?: () => boolean;
43
42
  }
44
43
  export declare function trackBgTask(sessionID: string | undefined, taskId: string): void;
45
44
  export declare function ingestBgCompletions(sessionID: string | undefined, completions: unknown): BgCompletion[];
@@ -1 +1 @@
1
- {"version":3,"file":"bg-notifications.d.ts","sourceRoot":"","sources":["../src/bg-notifications.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,KAAK,cAAc,GAAG;IACpB,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAChC,kBAAkB,EAAE,YAAY,EAAE,CAAC;IACnC,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;IAC5C,aAAa,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IACrC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,wBAAwB,EAAE,MAAM,CAAC;IACjC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kBAAkB,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,YAAY,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAa,CAAC;AAGtE,eAAO,MAAM,4BAA4B,QAAiB,CAAC;AAQ3D,UAAU,YAAY;IACpB,GAAG,EAAE,aAAa,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC;CAC1B;AASD,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAgB/E;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,WAAW,EAAE,OAAO,GACnB,YAAY,EAAE,CAoBhB;AAED,wBAAsB,wBAAwB,CAC5C,YAAY,EAAE,YAAY,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,EAChD,UAAU,EAAE,OAAO,GAClB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,YAAY,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,EAChD,QAAQ,EAAE,qBAAqB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GACtC,OAAO,CAAC,IAAI,CAAC,CAgCf;AAED,wBAAsB,uBAAuB,CAC3C,YAAY,EAAE,YAAY,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GAC/C,OAAO,CAAC,IAAI,CAAC,CAEf;AAwDD,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CASjF;AAED,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,SAAS,qBAAqB,EAAE,GAAG,MAAM,CAQ7F;AAWD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAcnE;AAED,wBAAgB,kCAAkC,IAAI,IAAI,CAKzD"}
1
+ {"version":3,"file":"bg-notifications.d.ts","sourceRoot":"","sources":["../src/bg-notifications.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AA8JlD;;;;;;;;;;;;;;;;;;GAkBG;AAQH,QAAA,MAAM,MAAM,EAAE,MAA6D,CAAC;AAwuB5E,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AA8JlD;;;;;;;;;;;;;;;;;;GAkBG;AAQH,QAAA,MAAM,MAAM,EAAE,MAA6D,CAAC;AAovB5E,eAAe,MAAM,CAAC"}
package/dist/index.js CHANGED
@@ -7953,6 +7953,8 @@ class BinaryBridge {
7953
7953
  onConfigureWarnings;
7954
7954
  onBashCompletion;
7955
7955
  onBashLongRunning;
7956
+ cachedStatus = null;
7957
+ statusListeners = new Set;
7956
7958
  configureWarningClients = new Map;
7957
7959
  restartResetTimer = null;
7958
7960
  errorPrefix;
@@ -8001,6 +8003,21 @@ class BinaryBridge {
8001
8003
  hasPendingRequests() {
8002
8004
  return this.pending.size > 0;
8003
8005
  }
8006
+ getCachedStatus() {
8007
+ return this.cachedStatus;
8008
+ }
8009
+ subscribeStatus(listener) {
8010
+ this.statusListeners.add(listener);
8011
+ if (this.cachedStatus !== null) {
8012
+ this.deliverStatusSnapshot(listener, this.cachedStatus);
8013
+ }
8014
+ return () => {
8015
+ this.statusListeners.delete(listener);
8016
+ };
8017
+ }
8018
+ cacheStatusSnapshot(snapshot) {
8019
+ this.cachedStatus = snapshot;
8020
+ }
8004
8021
  async send(command, params = {}, options) {
8005
8022
  if (this._shuttingDown) {
8006
8023
  throw new Error(`${this.errorPrefix} Bridge is shutting down, cannot send "${command}"`);
@@ -8137,6 +8154,23 @@ class BinaryBridge {
8137
8154
  }
8138
8155
  }
8139
8156
  }
8157
+ handleStatusChangedFrame(frame) {
8158
+ const snapshot = frame.snapshot;
8159
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
8160
+ return;
8161
+ this.cachedStatus = snapshot;
8162
+ log("Received status_changed push frame; cached AFT status snapshot");
8163
+ for (const listener of this.statusListeners) {
8164
+ this.deliverStatusSnapshot(listener, this.cachedStatus);
8165
+ }
8166
+ }
8167
+ deliverStatusSnapshot(listener, snapshot) {
8168
+ try {
8169
+ listener(snapshot);
8170
+ } catch (err) {
8171
+ warn(`status listener threw: ${err instanceof Error ? err.message : String(err)}`);
8172
+ }
8173
+ }
8140
8174
  async shutdown() {
8141
8175
  this._shuttingDown = true;
8142
8176
  this.clearRestartResetTimer();
@@ -8341,6 +8375,10 @@ class BinaryBridge {
8341
8375
  });
8342
8376
  continue;
8343
8377
  }
8378
+ if (response.type === "status_changed") {
8379
+ this.handleStatusChangedFrame(response);
8380
+ continue;
8381
+ }
8344
8382
  const id = response.id;
8345
8383
  if (id && this.pending.has(id)) {
8346
8384
  const entry = this.pending.get(id);
@@ -10055,8 +10093,6 @@ async function handleIdleBgCompletions(drainContext) {
10055
10093
  }
10056
10094
  async function triggerWakeIfPending(drainContext, skipDrain) {
10057
10095
  const state = stateFor(drainContext.sessionID);
10058
- if (drainContext.isActive?.())
10059
- return;
10060
10096
  if (!skipDrain && state.outstandingTaskIds.size > 0) {
10061
10097
  await drainCompletions(drainContext);
10062
10098
  }
@@ -27096,6 +27132,8 @@ function coerceAftStatus(response) {
27096
27132
  return {
27097
27133
  version: readString(response.version, "unknown"),
27098
27134
  project_root: readNullableString(response.project_root),
27135
+ canonical_root: readNullableString(response.canonical_root),
27136
+ cache_role: readString(response.cache_role, "not_initialized"),
27099
27137
  features: {
27100
27138
  format_on_edit: readBoolean(features.format_on_edit),
27101
27139
  validate_on_edit: readString(features.validate_on_edit, "off"),
@@ -27145,6 +27183,8 @@ function formatStatusMarkdown(status) {
27145
27183
  "",
27146
27184
  `- **Version:** \`${status.version}\``,
27147
27185
  `- **Project root:** \`${status.project_root ?? "(not configured)"}\``,
27186
+ `- **Canonical root:** \`${status.canonical_root ?? "(not configured)"}\``,
27187
+ `- **Cache role:** \`${status.cache_role}\``,
27148
27188
  "",
27149
27189
  "### Enabled features",
27150
27190
  `- \`format_on_edit\`: ${formatFlag(status.features.format_on_edit)}`,
@@ -30810,24 +30850,22 @@ ${lines}
30810
30850
  pluginVersion: PLUGIN_VERSION
30811
30851
  });
30812
30852
  },
30813
- onBashCompletion: (completion, bridge) => {
30853
+ onBashCompletion: (completion) => {
30814
30854
  const sessionDir = getSessionDirectoryCached(completion.session_id) ?? input.directory;
30815
30855
  handlePushedBgCompletion({
30816
30856
  ctx,
30817
30857
  directory: sessionDir,
30818
30858
  sessionID: completion.session_id,
30819
- client: input.client,
30820
- isActive: () => bridge.hasPendingRequests()
30859
+ client: input.client
30821
30860
  }, completion);
30822
30861
  },
30823
- onBashLongRunning: (reminder, bridge) => {
30862
+ onBashLongRunning: (reminder) => {
30824
30863
  const sessionDir = getSessionDirectoryCached(reminder.session_id) ?? input.directory;
30825
30864
  handlePushedBgLongRunning({
30826
30865
  ctx,
30827
30866
  directory: sessionDir,
30828
30867
  sessionID: reminder.session_id,
30829
- client: input.client,
30830
- isActive: () => bridge.hasPendingRequests()
30868
+ client: input.client
30831
30869
  }, reminder);
30832
30870
  }
30833
30871
  };
@@ -30898,7 +30936,15 @@ ${lines}
30898
30936
  message: "AFT bridge has not been initialized for this project yet. Status will populate after the first tool call."
30899
30937
  };
30900
30938
  }
30901
- return await bridge.send("status", { session_id: sessionID });
30939
+ const cached2 = bridge.getCachedStatus();
30940
+ if (cached2 !== null) {
30941
+ return { success: true, ...cached2 };
30942
+ }
30943
+ const response = await bridge.send("status", { session_id: sessionID });
30944
+ if (response.success !== false) {
30945
+ bridge.cacheStatusSnapshot(response);
30946
+ }
30947
+ return response;
30902
30948
  });
30903
30949
  const storageDir = configOverrides.storage_dir;
30904
30950
  rpcServer.handle("get-announcement", async () => {
@@ -31061,7 +31107,11 @@ Install: ${getManualInstallHint()}`).catch(() => {});
31061
31107
  }
31062
31108
  const sessionDir = await getSessionDirectory(input.client, commandInput.sessionID, input.directory) ?? input.directory;
31063
31109
  const bridge = ctx.pool.getActiveBridgeForRoot(sessionDir) ?? ctx.pool.getBridge(sessionDir);
31064
- const response = await bridge.send("status", { session_id: commandInput.sessionID });
31110
+ const cached2 = bridge.getCachedStatus();
31111
+ const response = cached2 ? { success: true, ...cached2 } : await bridge.send("status", { session_id: commandInput.sessionID });
31112
+ if (!cached2 && response.success !== false) {
31113
+ bridge.cacheStatusSnapshot(response);
31114
+ }
31065
31115
  if (response.success === false) {
31066
31116
  throw new Error(response.message || "status failed");
31067
31117
  }
@@ -1,6 +1,8 @@
1
1
  export interface AftStatusSnapshot {
2
2
  version: string;
3
3
  project_root: string | null;
4
+ canonical_root: string | null;
5
+ cache_role: string;
4
6
  features: {
5
7
  format_on_edit: boolean;
6
8
  validate_on_edit: string;
@@ -47,6 +49,13 @@ export interface AftStatusSnapshot {
47
49
  }
48
50
  export declare function formatBytes(bytes: number): string;
49
51
  export declare function coerceAftStatus(response: Record<string, unknown>): AftStatusSnapshot;
52
+ /**
53
+ * Plain-text status renderer used by the Desktop `sendIgnoredMessage` path,
54
+ * which can only show a plain string. The TUI dialog uses a custom JSX
55
+ * component in `tui/index.tsx` (see `StatusDialog`) so it can render with
56
+ * themed colors, proper flex columns, and right-aligned values instead of
57
+ * monospace padding.
58
+ */
50
59
  export declare function formatStatusDialogMessage(status: AftStatusSnapshot): string;
51
60
  export declare function formatStatusMarkdown(status: AftStatusSnapshot): string;
52
61
  //# sourceMappingURL=status.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/shared/status.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,EAAE;QACR,cAAc,EAAE,OAAO,CAAC;QACxB,gBAAgB,EAAE,MAAM,CAAC;QACzB,wBAAwB,EAAE,OAAO,CAAC;QAClC,YAAY,EAAE,OAAO,CAAC;QACtB,eAAe,EAAE,OAAO,CAAC;KAC1B,CAAC;IACF,YAAY,EAAE;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;KACzB,CAAC;IACF,cAAc,EAAE;QACd,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,CAAC;IACF,IAAI,EAAE;QACJ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,mBAAmB,EAAE,MAAM,CAAC;KAC7B,CAAC;IACF,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE;QACZ,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iEAAiE;IACjE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,aAAa,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAkCD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAajD;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,iBAAiB,CA2DpF;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAsE3E;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAwEtE"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/shared/status.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE;QACR,cAAc,EAAE,OAAO,CAAC;QACxB,gBAAgB,EAAE,MAAM,CAAC;QACzB,wBAAwB,EAAE,OAAO,CAAC;QAClC,YAAY,EAAE,OAAO,CAAC;QACtB,eAAe,EAAE,OAAO,CAAC;KAC1B,CAAC;IACF,YAAY,EAAE;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;KACzB,CAAC;IACF,cAAc,EAAE;QACd,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,CAAC;IACF,IAAI,EAAE;QACJ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,mBAAmB,EAAE,MAAM,CAAC;KAC7B,CAAC;IACF,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE;QACZ,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iEAAiE;IACjE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,aAAa,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAkCD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAajD;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,iBAAiB,CA6DpF;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAuE3E;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CA0EtE"}
package/dist/tui.js CHANGED
@@ -1,8 +1,10 @@
1
1
  // @bun
2
+ // src/tui/index.tsx
3
+ import { createMemo as createMemo2, createSignal as createSignal2, onCleanup as onCleanup2 } from "solid-js";
2
4
  // package.json
3
5
  var package_default = {
4
6
  name: "@cortexkit/aft-opencode",
5
- version: "0.23.0",
7
+ version: "0.24.0",
6
8
  type: "module",
7
9
  description: "OpenCode plugin for Agent File Tools (AFT) \u2014 tree-sitter and lsp powered code analysis",
8
10
  main: "dist/index.js",
@@ -30,7 +32,7 @@ var package_default = {
30
32
  },
31
33
  dependencies: {
32
34
  "@clack/prompts": "^1.2.0",
33
- "@cortexkit/aft-bridge": "0.23.0",
35
+ "@cortexkit/aft-bridge": "0.24.0",
34
36
  "@opencode-ai/plugin": "^1.14.39",
35
37
  "@opencode-ai/sdk": "^1.14.39",
36
38
  "comment-json": "^4.6.2",
@@ -38,11 +40,11 @@ var package_default = {
38
40
  zod: "^4.1.8"
39
41
  },
40
42
  optionalDependencies: {
41
- "@cortexkit/aft-darwin-arm64": "0.23.0",
42
- "@cortexkit/aft-darwin-x64": "0.23.0",
43
- "@cortexkit/aft-linux-arm64": "0.23.0",
44
- "@cortexkit/aft-linux-x64": "0.23.0",
45
- "@cortexkit/aft-win32-x64": "0.23.0"
43
+ "@cortexkit/aft-darwin-arm64": "0.24.0",
44
+ "@cortexkit/aft-darwin-x64": "0.24.0",
45
+ "@cortexkit/aft-linux-arm64": "0.24.0",
46
+ "@cortexkit/aft-linux-x64": "0.24.0",
47
+ "@cortexkit/aft-win32-x64": "0.24.0"
46
48
  },
47
49
  devDependencies: {
48
50
  "@types/node": "^22.0.0",
@@ -303,12 +305,6 @@ function readNumber(value, fallback = 0) {
303
305
  function readOptionalNumber(value) {
304
306
  return typeof value === "number" && Number.isFinite(value) ? value : null;
305
307
  }
306
- function formatFlag(enabled) {
307
- return enabled ? "enabled" : "disabled";
308
- }
309
- function formatCount(value) {
310
- return value == null ? "\u2014" : value.toLocaleString("en-US");
311
- }
312
308
  function formatBytes(bytes) {
313
309
  if (!Number.isFinite(bytes) || bytes <= 0)
314
310
  return "0 B";
@@ -336,6 +332,8 @@ function coerceAftStatus(response) {
336
332
  return {
337
333
  version: readString(response.version, "unknown"),
338
334
  project_root: readNullableString(response.project_root),
335
+ canonical_root: readNullableString(response.canonical_root),
336
+ cache_role: readString(response.cache_role, "not_initialized"),
339
337
  features: {
340
338
  format_on_edit: readBoolean(features.format_on_edit),
341
339
  validate_on_edit: readString(features.validate_on_edit, "off"),
@@ -379,54 +377,6 @@ function coerceAftStatus(response) {
379
377
  }
380
378
  };
381
379
  }
382
- function formatStatusDialogMessage(status) {
383
- const lines = [
384
- `AFT version: ${status.version}`,
385
- `Project root: ${status.project_root ?? "(not configured)"}`,
386
- "",
387
- "Enabled features",
388
- `- format_on_edit: ${formatFlag(status.features.format_on_edit)}`,
389
- `- search_index: ${formatFlag(status.features.search_index)}`,
390
- `- semantic_search: ${formatFlag(status.features.semantic_search)}`,
391
- "",
392
- "Search index",
393
- `- status: ${status.search_index.status}`,
394
- `- files: ${formatCount(status.search_index.files)}`,
395
- `- trigrams: ${formatCount(status.search_index.trigrams)}`,
396
- "",
397
- "Semantic index",
398
- `- status: ${status.semantic_index.status}`,
399
- `- entries: ${formatCount(status.semantic_index.entries)}`
400
- ];
401
- if (status.semantic_index.backend) {
402
- lines.push(`- backend: ${status.semantic_index.backend}`);
403
- }
404
- if (status.semantic_index.model) {
405
- lines.push(`- model: ${status.semantic_index.model}`);
406
- }
407
- if (status.semantic_index.dimension != null) {
408
- lines.push(`- dimension: ${formatCount(status.semantic_index.dimension)}`);
409
- }
410
- lines.push("", "Disk usage", `- trigram index: ${formatBytes(status.disk.trigram_disk_bytes)}`, `- semantic index: ${formatBytes(status.disk.semantic_disk_bytes)}`, "", "Runtime", `- LSP servers: ${formatCount(status.lsp_servers)}`, `- symbol cache: ${formatCount(status.symbol_cache.local_entries)} local / ${formatCount(status.symbol_cache.warm_entries)} warm`);
411
- if (status.storage_dir ?? status.disk.storage_dir) {
412
- lines.push(`- storage dir: ${status.storage_dir ?? status.disk.storage_dir}`);
413
- }
414
- 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)}`);
415
- if (status.semantic_index.stage) {
416
- lines.push("", "Semantic stage", status.semantic_index.stage);
417
- }
418
- if (status.semantic_index.files != null) {
419
- lines.push(`- semantic files: ${formatCount(status.semantic_index.files)}`);
420
- }
421
- if (status.semantic_index.entries_done != null || status.semantic_index.entries_total != null) {
422
- lines.push(`- semantic progress: ${formatCount(status.semantic_index.entries_done ?? null)} / ${formatCount(status.semantic_index.entries_total ?? null)}`);
423
- }
424
- if (status.semantic_index.error) {
425
- lines.push("", "Semantic error", status.semantic_index.error);
426
- }
427
- return lines.join(`
428
- `);
429
- }
430
380
 
431
381
  // src/tui/sidebar.tsx
432
382
  import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js";
@@ -445,7 +395,7 @@ function formatBytes2(n) {
445
395
  return `${Math.round(n / 1024)} KB`;
446
396
  return `${n} B`;
447
397
  }
448
- function formatCount2(n) {
398
+ function formatCount(n) {
449
399
  if (n == null || !Number.isFinite(n))
450
400
  return "\u2014";
451
401
  if (n >= 1e6)
@@ -658,7 +608,7 @@ var SidebarContent = (props) => {
658
608
  (s()?.search_index?.files ?? null) != null && /* @__PURE__ */ jsxDEV(StatRow, {
659
609
  theme: props.theme,
660
610
  label: "Files",
661
- value: formatCount2(s().search_index.files),
611
+ value: formatCount(s().search_index.files),
662
612
  tone: "muted"
663
613
  }, undefined, false, undefined, this),
664
614
  /* @__PURE__ */ jsxDEV(StatRow, {
@@ -680,13 +630,13 @@ var SidebarContent = (props) => {
680
630
  s()?.semantic_index?.status === "loading" && s()?.semantic_index?.entries_total != null && s().semantic_index.entries_total > 0 && /* @__PURE__ */ jsxDEV(StatRow, {
681
631
  theme: props.theme,
682
632
  label: "Progress",
683
- value: `${formatCount2(s().semantic_index.entries_done)} / ${formatCount2(s().semantic_index.entries_total)}`,
633
+ value: `${formatCount(s().semantic_index.entries_done)} / ${formatCount(s().semantic_index.entries_total)}`,
684
634
  tone: "warn"
685
635
  }, undefined, false, undefined, this),
686
636
  (s()?.semantic_index?.entries ?? null) != null && /* @__PURE__ */ jsxDEV(StatRow, {
687
637
  theme: props.theme,
688
638
  label: "Entries",
689
- value: formatCount2(s().semantic_index.entries),
639
+ value: formatCount(s().semantic_index.entries),
690
640
  tone: "muted"
691
641
  }, undefined, false, undefined, this),
692
642
  /* @__PURE__ */ jsxDEV(StatRow, {
@@ -750,6 +700,361 @@ function getSessionId(api) {
750
700
  } catch {}
751
701
  return null;
752
702
  }
703
+ var POLL_INTERVAL_MS2 = 1500;
704
+ function formatCountShort(value) {
705
+ if (value == null || !Number.isFinite(value))
706
+ return "\u2014";
707
+ if (value >= 1e6)
708
+ return `${(value / 1e6).toFixed(1)}M`;
709
+ if (value >= 1000)
710
+ return `${Math.round(value / 1000)}K`;
711
+ return String(value);
712
+ }
713
+ function statusTone(status) {
714
+ switch (status) {
715
+ case "ready":
716
+ return "ok";
717
+ case "loading":
718
+ case "building":
719
+ return "warn";
720
+ case "failed":
721
+ case "error":
722
+ return "err";
723
+ default:
724
+ return "muted";
725
+ }
726
+ }
727
+ function pickToneColor(theme, tone) {
728
+ switch (tone) {
729
+ case "ok":
730
+ return theme.success ?? theme.accent;
731
+ case "warn":
732
+ return theme.warning;
733
+ case "err":
734
+ return theme.error;
735
+ case "muted":
736
+ return theme.textMuted;
737
+ }
738
+ }
739
+ var R = (props) => {
740
+ const fg = createMemo2(() => {
741
+ if (!props.tone)
742
+ return props.theme.text;
743
+ if (props.tone === "accent")
744
+ return props.theme.accent;
745
+ return pickToneColor(props.theme, props.tone);
746
+ });
747
+ return /* @__PURE__ */ jsxDEV2("box", {
748
+ flexDirection: "row",
749
+ width: "100%",
750
+ justifyContent: "space-between",
751
+ children: [
752
+ /* @__PURE__ */ jsxDEV2("text", {
753
+ fg: props.theme.textMuted,
754
+ children: props.label
755
+ }, undefined, false, undefined, this),
756
+ /* @__PURE__ */ jsxDEV2("text", {
757
+ fg: fg(),
758
+ children: props.value
759
+ }, undefined, false, undefined, this)
760
+ ]
761
+ }, undefined, true, undefined, this);
762
+ };
763
+ var StatusDialog = (props) => {
764
+ const theme = createMemo2(() => props.api.theme.current);
765
+ const t = () => theme();
766
+ const [status, setStatus] = createSignal2(props.initial);
767
+ const [error, setError] = createSignal2(props.initialError);
768
+ const timer = setInterval(async () => {
769
+ try {
770
+ const response = await props.client.call("status", { sessionID: props.sessionID });
771
+ if (response.success !== false) {
772
+ setStatus(coerceAftStatus(response));
773
+ setError(null);
774
+ }
775
+ } catch {}
776
+ }, POLL_INTERVAL_MS2);
777
+ onCleanup2(() => clearInterval(timer));
778
+ const cacheRoleTone = (role) => role === "main" ? "accent" : role === "worktree" ? "warn" : "muted";
779
+ return /* @__PURE__ */ jsxDEV2("box", {
780
+ flexDirection: "column",
781
+ width: "100%",
782
+ paddingLeft: 2,
783
+ paddingRight: 2,
784
+ paddingTop: 1,
785
+ paddingBottom: 1,
786
+ children: [
787
+ /* @__PURE__ */ jsxDEV2("box", {
788
+ justifyContent: "center",
789
+ width: "100%",
790
+ marginBottom: 1,
791
+ flexDirection: "row",
792
+ gap: 2,
793
+ children: [
794
+ /* @__PURE__ */ jsxDEV2("text", {
795
+ fg: t().accent,
796
+ children: /* @__PURE__ */ jsxDEV2("b", {
797
+ children: "\u26A1 AFT Status"
798
+ }, undefined, false, undefined, this)
799
+ }, undefined, false, undefined, this),
800
+ /* @__PURE__ */ jsxDEV2("text", {
801
+ fg: t().textMuted,
802
+ children: [
803
+ "v",
804
+ status()?.version ?? package_default.version
805
+ ]
806
+ }, undefined, true, undefined, this)
807
+ ]
808
+ }, undefined, true, undefined, this),
809
+ error() ? /* @__PURE__ */ jsxDEV2("box", {
810
+ width: "100%",
811
+ marginBottom: 1,
812
+ children: /* @__PURE__ */ jsxDEV2("text", {
813
+ fg: t().warning,
814
+ children: error()
815
+ }, undefined, false, undefined, this)
816
+ }, undefined, false, undefined, this) : null,
817
+ status() ? /* @__PURE__ */ jsxDEV2("box", {
818
+ flexDirection: "column",
819
+ width: "100%",
820
+ marginBottom: 1,
821
+ children: [
822
+ /* @__PURE__ */ jsxDEV2(R, {
823
+ theme: t(),
824
+ label: "Project root",
825
+ value: status().project_root ?? "(not configured)"
826
+ }, undefined, false, undefined, this),
827
+ /* @__PURE__ */ jsxDEV2(R, {
828
+ theme: t(),
829
+ label: "Canonical root",
830
+ value: status().canonical_root ?? "(not configured)"
831
+ }, undefined, false, undefined, this),
832
+ /* @__PURE__ */ jsxDEV2(R, {
833
+ theme: t(),
834
+ label: "Cache role",
835
+ value: status().cache_role,
836
+ tone: cacheRoleTone(status().cache_role)
837
+ }, undefined, false, undefined, this)
838
+ ]
839
+ }, undefined, true, undefined, this) : null,
840
+ status() ? /* @__PURE__ */ jsxDEV2("box", {
841
+ flexDirection: "row",
842
+ width: "100%",
843
+ gap: 4,
844
+ children: [
845
+ /* @__PURE__ */ jsxDEV2("box", {
846
+ flexDirection: "column",
847
+ flexGrow: 1,
848
+ flexBasis: 0,
849
+ children: [
850
+ /* @__PURE__ */ jsxDEV2("text", {
851
+ fg: t().text,
852
+ children: /* @__PURE__ */ jsxDEV2("b", {
853
+ children: "Search index"
854
+ }, undefined, false, undefined, this)
855
+ }, undefined, false, undefined, this),
856
+ /* @__PURE__ */ jsxDEV2(R, {
857
+ theme: t(),
858
+ label: "Status",
859
+ value: status().search_index.status,
860
+ tone: statusTone(status().search_index.status)
861
+ }, undefined, false, undefined, this),
862
+ /* @__PURE__ */ jsxDEV2(R, {
863
+ theme: t(),
864
+ label: "Files",
865
+ value: formatCountShort(status().search_index.files)
866
+ }, undefined, false, undefined, this),
867
+ /* @__PURE__ */ jsxDEV2(R, {
868
+ theme: t(),
869
+ label: "Trigrams",
870
+ value: formatCountShort(status().search_index.trigrams)
871
+ }, undefined, false, undefined, this),
872
+ /* @__PURE__ */ jsxDEV2(R, {
873
+ theme: t(),
874
+ label: "Disk",
875
+ value: formatBytes(status().disk.trigram_disk_bytes),
876
+ tone: "muted"
877
+ }, undefined, false, undefined, this),
878
+ /* @__PURE__ */ jsxDEV2("box", {
879
+ marginTop: 1,
880
+ children: /* @__PURE__ */ jsxDEV2("text", {
881
+ fg: t().text,
882
+ children: /* @__PURE__ */ jsxDEV2("b", {
883
+ children: "Runtime"
884
+ }, undefined, false, undefined, this)
885
+ }, undefined, false, undefined, this)
886
+ }, undefined, false, undefined, this),
887
+ /* @__PURE__ */ jsxDEV2(R, {
888
+ theme: t(),
889
+ label: "LSP servers",
890
+ value: String(status().lsp_servers)
891
+ }, undefined, false, undefined, this),
892
+ /* @__PURE__ */ jsxDEV2(R, {
893
+ theme: t(),
894
+ label: "Symbol cache (local)",
895
+ value: formatCountShort(status().symbol_cache.local_entries)
896
+ }, undefined, false, undefined, this),
897
+ /* @__PURE__ */ jsxDEV2(R, {
898
+ theme: t(),
899
+ label: "Symbol cache (warm)",
900
+ value: formatCountShort(status().symbol_cache.warm_entries),
901
+ tone: "muted"
902
+ }, undefined, false, undefined, this),
903
+ /* @__PURE__ */ jsxDEV2("box", {
904
+ marginTop: 1,
905
+ children: /* @__PURE__ */ jsxDEV2("text", {
906
+ fg: t().text,
907
+ children: /* @__PURE__ */ jsxDEV2("b", {
908
+ children: "Features"
909
+ }, undefined, false, undefined, this)
910
+ }, undefined, false, undefined, this)
911
+ }, undefined, false, undefined, this),
912
+ /* @__PURE__ */ jsxDEV2(R, {
913
+ theme: t(),
914
+ label: "format_on_edit",
915
+ value: status().features.format_on_edit ? "on" : "off",
916
+ tone: status().features.format_on_edit ? "ok" : "muted"
917
+ }, undefined, false, undefined, this),
918
+ /* @__PURE__ */ jsxDEV2(R, {
919
+ theme: t(),
920
+ label: "search_index",
921
+ value: status().features.search_index ? "on" : "off",
922
+ tone: status().features.search_index ? "ok" : "muted"
923
+ }, undefined, false, undefined, this),
924
+ /* @__PURE__ */ jsxDEV2(R, {
925
+ theme: t(),
926
+ label: "semantic_search",
927
+ value: status().features.semantic_search ? "on" : "off",
928
+ tone: status().features.semantic_search ? "ok" : "muted"
929
+ }, undefined, false, undefined, this)
930
+ ]
931
+ }, undefined, true, undefined, this),
932
+ /* @__PURE__ */ jsxDEV2("box", {
933
+ flexDirection: "column",
934
+ flexGrow: 1,
935
+ flexBasis: 0,
936
+ children: [
937
+ /* @__PURE__ */ jsxDEV2("text", {
938
+ fg: t().text,
939
+ children: /* @__PURE__ */ jsxDEV2("b", {
940
+ children: "Semantic index"
941
+ }, undefined, false, undefined, this)
942
+ }, undefined, false, undefined, this),
943
+ /* @__PURE__ */ jsxDEV2(R, {
944
+ theme: t(),
945
+ label: "Status",
946
+ value: status().semantic_index.status,
947
+ tone: statusTone(status().semantic_index.status)
948
+ }, undefined, false, undefined, this),
949
+ /* @__PURE__ */ jsxDEV2(R, {
950
+ theme: t(),
951
+ label: "Entries",
952
+ value: formatCountShort(status().semantic_index.entries)
953
+ }, undefined, false, undefined, this),
954
+ status().semantic_index.backend ? /* @__PURE__ */ jsxDEV2(R, {
955
+ theme: t(),
956
+ label: "Backend",
957
+ value: status().semantic_index.backend,
958
+ tone: "muted"
959
+ }, undefined, false, undefined, this) : null,
960
+ status().semantic_index.model ? /* @__PURE__ */ jsxDEV2(R, {
961
+ theme: t(),
962
+ label: "Model",
963
+ value: status().semantic_index.model,
964
+ tone: "muted"
965
+ }, undefined, false, undefined, this) : null,
966
+ status().semantic_index.dimension != null ? /* @__PURE__ */ jsxDEV2(R, {
967
+ theme: t(),
968
+ label: "Dimension",
969
+ value: String(status().semantic_index.dimension),
970
+ tone: "muted"
971
+ }, undefined, false, undefined, this) : null,
972
+ /* @__PURE__ */ jsxDEV2(R, {
973
+ theme: t(),
974
+ label: "Disk",
975
+ value: formatBytes(status().disk.semantic_disk_bytes),
976
+ tone: "muted"
977
+ }, undefined, false, undefined, this),
978
+ /* @__PURE__ */ jsxDEV2("box", {
979
+ marginTop: 1,
980
+ children: /* @__PURE__ */ jsxDEV2("text", {
981
+ fg: t().text,
982
+ children: /* @__PURE__ */ jsxDEV2("b", {
983
+ children: "Current session"
984
+ }, undefined, false, undefined, this)
985
+ }, undefined, false, undefined, this)
986
+ }, undefined, false, undefined, this),
987
+ /* @__PURE__ */ jsxDEV2(R, {
988
+ theme: t(),
989
+ label: "Tracked files",
990
+ value: String(status().session.tracked_files)
991
+ }, undefined, false, undefined, this),
992
+ /* @__PURE__ */ jsxDEV2(R, {
993
+ theme: t(),
994
+ label: "Checkpoints",
995
+ value: String(status().session.checkpoints)
996
+ }, undefined, false, undefined, this),
997
+ /* @__PURE__ */ jsxDEV2(R, {
998
+ theme: t(),
999
+ label: "All-session checkpoints",
1000
+ value: String(status().checkpoints_total),
1001
+ tone: "muted"
1002
+ }, undefined, false, undefined, this)
1003
+ ]
1004
+ }, undefined, true, undefined, this)
1005
+ ]
1006
+ }, undefined, true, undefined, this) : null,
1007
+ status()?.semantic_index.stage ? /* @__PURE__ */ jsxDEV2("box", {
1008
+ flexDirection: "column",
1009
+ width: "100%",
1010
+ marginTop: 1,
1011
+ children: [
1012
+ /* @__PURE__ */ jsxDEV2("text", {
1013
+ fg: t().text,
1014
+ children: /* @__PURE__ */ jsxDEV2("b", {
1015
+ children: "Semantic build progress"
1016
+ }, undefined, false, undefined, this)
1017
+ }, undefined, false, undefined, this),
1018
+ /* @__PURE__ */ jsxDEV2(R, {
1019
+ theme: t(),
1020
+ label: "Stage",
1021
+ value: status().semantic_index.stage
1022
+ }, undefined, false, undefined, this),
1023
+ status().semantic_index.files != null ? /* @__PURE__ */ jsxDEV2(R, {
1024
+ theme: t(),
1025
+ label: "Files seen",
1026
+ value: formatCountShort(status().semantic_index.files)
1027
+ }, undefined, false, undefined, this) : null,
1028
+ status().semantic_index.entries_done != null || status().semantic_index.entries_total != null ? /* @__PURE__ */ jsxDEV2(R, {
1029
+ theme: t(),
1030
+ label: "Progress",
1031
+ value: `${formatCountShort(status().semantic_index.entries_done ?? null)} / ${formatCountShort(status().semantic_index.entries_total ?? null)}`
1032
+ }, undefined, false, undefined, this) : null
1033
+ ]
1034
+ }, undefined, true, undefined, this) : null,
1035
+ status()?.semantic_index.error ? /* @__PURE__ */ jsxDEV2("box", {
1036
+ marginTop: 1,
1037
+ width: "100%",
1038
+ children: /* @__PURE__ */ jsxDEV2("text", {
1039
+ fg: t().error,
1040
+ children: [
1041
+ "\u26A0 ",
1042
+ status().semantic_index.error
1043
+ ]
1044
+ }, undefined, true, undefined, this)
1045
+ }, undefined, false, undefined, this) : null,
1046
+ /* @__PURE__ */ jsxDEV2("box", {
1047
+ marginTop: 1,
1048
+ justifyContent: "flex-end",
1049
+ width: "100%",
1050
+ children: /* @__PURE__ */ jsxDEV2("text", {
1051
+ fg: t().textMuted,
1052
+ children: "Enter or Esc to close"
1053
+ }, undefined, false, undefined, this)
1054
+ }, undefined, false, undefined, this)
1055
+ ]
1056
+ }, undefined, true, undefined, this);
1057
+ };
753
1058
  async function showStatusDialog(api) {
754
1059
  const sessionID = getSessionId(api);
755
1060
  if (!sessionID) {
@@ -762,68 +1067,29 @@ async function showStatusDialog(api) {
762
1067
  return;
763
1068
  }
764
1069
  const client = getRpcClient(directory);
765
- let currentMessage = "Connecting to AFT...";
1070
+ let initial = null;
1071
+ let initialError = null;
766
1072
  try {
767
1073
  const response = await client.call("status", { sessionID });
768
1074
  if (response.success !== false) {
769
- const status = coerceAftStatus(response);
770
- currentMessage = formatStatusDialogMessage(status);
1075
+ initial = coerceAftStatus(response);
1076
+ } else {
1077
+ initialError = "AFT bridge returned an error response.";
771
1078
  }
772
1079
  } catch {
773
- currentMessage = "AFT is starting up. Status will refresh automatically...";
1080
+ initialError = "AFT is starting up. Status will refresh automatically...";
774
1081
  }
775
- let dialogOpen = true;
776
- let pollTimer = null;
777
1082
  api.ui.dialog.setSize("large");
778
- api.ui.dialog.replace(() => {
779
- if (!pollTimer) {
780
- pollTimer = setInterval(async () => {
781
- if (!dialogOpen) {
782
- if (pollTimer)
783
- clearInterval(pollTimer);
784
- return;
785
- }
786
- try {
787
- const response = await client.call("status", { sessionID });
788
- if (response.success !== false) {
789
- const status = coerceAftStatus(response);
790
- const newMessage = formatStatusDialogMessage(status);
791
- if (newMessage !== currentMessage) {
792
- currentMessage = newMessage;
793
- api.ui.dialog.replace(() => /* @__PURE__ */ jsxDEV2(api.ui.DialogAlert, {
794
- title: "AFT Status",
795
- message: currentMessage,
796
- onConfirm: () => {
797
- dialogOpen = false;
798
- if (pollTimer)
799
- clearInterval(pollTimer);
800
- api.ui.dialog.setSize("medium");
801
- }
802
- }, undefined, false, undefined, this), () => {
803
- dialogOpen = false;
804
- if (pollTimer)
805
- clearInterval(pollTimer);
806
- api.ui.dialog.setSize("medium");
807
- });
808
- }
809
- }
810
- } catch {}
811
- }, 1500);
1083
+ api.ui.dialog.replace(() => /* @__PURE__ */ jsxDEV2(StatusDialog, {
1084
+ api,
1085
+ client,
1086
+ sessionID,
1087
+ initial,
1088
+ initialError,
1089
+ onClose: () => {
1090
+ api.ui.dialog.setSize("medium");
812
1091
  }
813
- return /* @__PURE__ */ jsxDEV2(api.ui.DialogAlert, {
814
- title: "AFT Status",
815
- message: currentMessage,
816
- onConfirm: () => {
817
- dialogOpen = false;
818
- if (pollTimer)
819
- clearInterval(pollTimer);
820
- api.ui.dialog.setSize("medium");
821
- }
822
- }, undefined, false, undefined, this);
823
- }, () => {
824
- dialogOpen = false;
825
- if (pollTimer)
826
- clearInterval(pollTimer);
1092
+ }, undefined, false, undefined, this), () => {
827
1093
  api.ui.dialog.setSize("medium");
828
1094
  });
829
1095
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.23.0",
3
+ "version": "0.24.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",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@clack/prompts": "^1.2.0",
31
- "@cortexkit/aft-bridge": "0.23.0",
31
+ "@cortexkit/aft-bridge": "0.24.0",
32
32
  "@opencode-ai/plugin": "^1.14.39",
33
33
  "@opencode-ai/sdk": "^1.14.39",
34
34
  "comment-json": "^4.6.2",
@@ -36,11 +36,11 @@
36
36
  "zod": "^4.1.8"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@cortexkit/aft-darwin-arm64": "0.23.0",
40
- "@cortexkit/aft-darwin-x64": "0.23.0",
41
- "@cortexkit/aft-linux-arm64": "0.23.0",
42
- "@cortexkit/aft-linux-x64": "0.23.0",
43
- "@cortexkit/aft-win32-x64": "0.23.0"
39
+ "@cortexkit/aft-darwin-arm64": "0.24.0",
40
+ "@cortexkit/aft-darwin-x64": "0.24.0",
41
+ "@cortexkit/aft-linux-arm64": "0.24.0",
42
+ "@cortexkit/aft-linux-x64": "0.24.0",
43
+ "@cortexkit/aft-win32-x64": "0.24.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^22.0.0",
@@ -1,6 +1,8 @@
1
1
  export interface AftStatusSnapshot {
2
2
  version: string;
3
3
  project_root: string | null;
4
+ canonical_root: string | null;
5
+ cache_role: string;
4
6
  features: {
5
7
  format_on_edit: boolean;
6
8
  validate_on_edit: string;
@@ -108,6 +110,8 @@ export function coerceAftStatus(response: Record<string, unknown>): AftStatusSna
108
110
  return {
109
111
  version: readString(response.version, "unknown"),
110
112
  project_root: readNullableString(response.project_root),
113
+ canonical_root: readNullableString(response.canonical_root),
114
+ cache_role: readString(response.cache_role, "not_initialized"),
111
115
  features: {
112
116
  format_on_edit: readBoolean(features.format_on_edit),
113
117
  validate_on_edit: readString(features.validate_on_edit, "off"),
@@ -154,10 +158,19 @@ export function coerceAftStatus(response: Record<string, unknown>): AftStatusSna
154
158
  };
155
159
  }
156
160
 
161
+ /**
162
+ * Plain-text status renderer used by the Desktop `sendIgnoredMessage` path,
163
+ * which can only show a plain string. The TUI dialog uses a custom JSX
164
+ * component in `tui/index.tsx` (see `StatusDialog`) so it can render with
165
+ * themed colors, proper flex columns, and right-aligned values instead of
166
+ * monospace padding.
167
+ */
157
168
  export function formatStatusDialogMessage(status: AftStatusSnapshot): string {
158
169
  const lines = [
159
170
  `AFT version: ${status.version}`,
160
171
  `Project root: ${status.project_root ?? "(not configured)"}`,
172
+ `Canonical root: ${status.canonical_root ?? "(not configured)"}`,
173
+ `Cache role: ${status.cache_role}`,
161
174
  "",
162
175
  "Enabled features",
163
176
  `- format_on_edit: ${formatFlag(status.features.format_on_edit)}`,
@@ -179,7 +192,6 @@ export function formatStatusDialogMessage(status: AftStatusSnapshot): string {
179
192
  if (status.semantic_index.model) {
180
193
  lines.push(`- model: ${status.semantic_index.model}`);
181
194
  }
182
-
183
195
  if (status.semantic_index.dimension != null) {
184
196
  lines.push(`- dimension: ${formatCount(status.semantic_index.dimension)}`);
185
197
  }
@@ -232,6 +244,8 @@ export function formatStatusMarkdown(status: AftStatusSnapshot): string {
232
244
  "",
233
245
  `- **Version:** \`${status.version}\``,
234
246
  `- **Project root:** \`${status.project_root ?? "(not configured)"}\``,
247
+ `- **Canonical root:** \`${status.canonical_root ?? "(not configured)"}\``,
248
+ `- **Cache role:** \`${status.cache_role}\``,
235
249
  "",
236
250
  "### Enabled features",
237
251
  `- \`format_on_edit\`: ${formatFlag(status.features.format_on_edit)}`,
package/src/tui/index.tsx CHANGED
@@ -1,10 +1,11 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  // @ts-nocheck
3
3
 
4
- import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui";
4
+ import type { TuiPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui";
5
+ import { createMemo, createSignal, onCleanup } from "solid-js";
5
6
  import packageJson from "../../package.json";
6
7
  import { AftRpcClient } from "../shared/rpc-client";
7
- import { coerceAftStatus, formatStatusDialogMessage } from "../shared/status";
8
+ import { type AftStatusSnapshot, coerceAftStatus, formatBytes } from "../shared/status";
8
9
  import { createAftSidebarSlot } from "./sidebar";
9
10
 
10
11
  // The TUI talks to the server plugin via AftRpcClient. The client reads the
@@ -42,6 +43,332 @@ function getSessionId(api: TuiPluginApi): string | null {
42
43
  return null;
43
44
  }
44
45
 
46
+ // ---------------------------------------------------------------------------
47
+ // StatusDialog — themed, two-column JSX dialog. Modeled on the magic-context
48
+ // /ctx-status pattern (packages/plugin/src/tui/index.tsx in that repo):
49
+ // custom JSX rendered via `api.ui.dialog.replace(() => <StatusDialog .../>)`
50
+ // instead of feeding a padded monospace string into DialogAlert. The
51
+ // difference matters because OpenCode renders DialogAlert text in a
52
+ // proportional font with no column alignment; only TUI flex primitives
53
+ // (<box flexDirection="row" flexBasis={0}>) actually produce visible
54
+ // columns. This component owns its own RPC polling so it can re-render
55
+ // reactively as the status snapshot changes, with no parent re-mount.
56
+ // ---------------------------------------------------------------------------
57
+
58
+ const POLL_INTERVAL_MS = 1500;
59
+
60
+ function formatCountShort(value: number | null | undefined): string {
61
+ if (value == null || !Number.isFinite(value)) return "—";
62
+ if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
63
+ if (value >= 1_000) return `${Math.round(value / 1_000)}K`;
64
+ return String(value);
65
+ }
66
+
67
+ function statusTone(status: string): "ok" | "warn" | "err" | "muted" {
68
+ switch (status) {
69
+ case "ready":
70
+ return "ok";
71
+ case "loading":
72
+ case "building":
73
+ return "warn";
74
+ case "failed":
75
+ case "error":
76
+ return "err";
77
+ default:
78
+ return "muted";
79
+ }
80
+ }
81
+
82
+ function pickToneColor(theme: TuiThemeCurrent, tone: "ok" | "warn" | "err" | "muted"): string {
83
+ switch (tone) {
84
+ case "ok":
85
+ return (theme as any).success ?? theme.accent;
86
+ case "warn":
87
+ return theme.warning;
88
+ case "err":
89
+ return theme.error;
90
+ case "muted":
91
+ return theme.textMuted;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Label/value row. Label is left-aligned and muted; value is right-aligned
97
+ * and themed. flexDirection="row" + justifyContent="space-between" replaces
98
+ * the monospace `padEnd(40)` hack from the previous string formatter.
99
+ */
100
+ const R = (props: {
101
+ theme: TuiThemeCurrent;
102
+ label: string;
103
+ value: string;
104
+ tone?: "ok" | "warn" | "err" | "muted" | "accent";
105
+ }) => {
106
+ const fg = createMemo(() => {
107
+ if (!props.tone) return props.theme.text;
108
+ if (props.tone === "accent") return props.theme.accent;
109
+ return pickToneColor(props.theme, props.tone);
110
+ });
111
+
112
+ return (
113
+ <box flexDirection="row" width="100%" justifyContent="space-between">
114
+ <text fg={props.theme.textMuted}>{props.label}</text>
115
+ <text fg={fg()}>{props.value}</text>
116
+ </box>
117
+ );
118
+ };
119
+
120
+ interface StatusDialogProps {
121
+ api: TuiPluginApi;
122
+ client: AftRpcClient;
123
+ sessionID: string;
124
+ initial: AftStatusSnapshot | null;
125
+ initialError: string | null;
126
+ onClose: () => void;
127
+ }
128
+
129
+ const StatusDialog = (props: StatusDialogProps) => {
130
+ const theme = createMemo(() => (props.api as any).theme.current as TuiThemeCurrent);
131
+ const t = () => theme();
132
+
133
+ // Reactive status signal — the dialog re-renders on every status
134
+ // transition without remounting. The RPC polling is local to the dialog
135
+ // and stops when it unmounts.
136
+ const [status, setStatus] = createSignal<AftStatusSnapshot | null>(props.initial);
137
+ const [error, setError] = createSignal<string | null>(props.initialError);
138
+
139
+ const timer = setInterval(async () => {
140
+ try {
141
+ const response = await props.client.call("status", { sessionID: props.sessionID });
142
+ if ((response as Record<string, unknown>).success !== false) {
143
+ setStatus(coerceAftStatus(response as Record<string, unknown>));
144
+ setError(null);
145
+ }
146
+ } catch {
147
+ // transient — keep showing last good snapshot
148
+ }
149
+ }, POLL_INTERVAL_MS);
150
+ onCleanup(() => clearInterval(timer));
151
+
152
+ // Visual cache-role badge: main is accent, worktree is warning,
153
+ // not_initialized is muted. Matches the sidebar convention.
154
+ const cacheRoleTone = (role: string): "accent" | "warn" | "muted" =>
155
+ role === "main" ? "accent" : role === "worktree" ? "warn" : "muted";
156
+
157
+ return (
158
+ <box
159
+ flexDirection="column"
160
+ width="100%"
161
+ paddingLeft={2}
162
+ paddingRight={2}
163
+ paddingTop={1}
164
+ paddingBottom={1}
165
+ >
166
+ {/* Title */}
167
+ <box justifyContent="center" width="100%" marginBottom={1} flexDirection="row" gap={2}>
168
+ <text fg={t().accent}>
169
+ <b>⚡ AFT Status</b>
170
+ </text>
171
+ <text fg={t().textMuted}>v{status()?.version ?? packageJson.version}</text>
172
+ </box>
173
+
174
+ {/* Error / not-yet-ready state */}
175
+ {error() ? (
176
+ <box width="100%" marginBottom={1}>
177
+ <text fg={t().warning}>{error()}</text>
178
+ </box>
179
+ ) : null}
180
+
181
+ {/* Header rows — paths span full width since they can be long */}
182
+ {status() ? (
183
+ <box flexDirection="column" width="100%" marginBottom={1}>
184
+ <R
185
+ theme={t()}
186
+ label="Project root"
187
+ value={status()!.project_root ?? "(not configured)"}
188
+ />
189
+ <R
190
+ theme={t()}
191
+ label="Canonical root"
192
+ value={status()!.canonical_root ?? "(not configured)"}
193
+ />
194
+ <R
195
+ theme={t()}
196
+ label="Cache role"
197
+ value={status()!.cache_role}
198
+ tone={cacheRoleTone(status()!.cache_role)}
199
+ />
200
+ </box>
201
+ ) : null}
202
+
203
+ {/* 2-column body */}
204
+ {status() ? (
205
+ <box flexDirection="row" width="100%" gap={4}>
206
+ {/* Left column */}
207
+ <box flexDirection="column" flexGrow={1} flexBasis={0}>
208
+ <text fg={t().text}>
209
+ <b>Search index</b>
210
+ </text>
211
+ <R
212
+ theme={t()}
213
+ label="Status"
214
+ value={status()!.search_index.status}
215
+ tone={statusTone(status()!.search_index.status)}
216
+ />
217
+ <R theme={t()} label="Files" value={formatCountShort(status()!.search_index.files)} />
218
+ <R
219
+ theme={t()}
220
+ label="Trigrams"
221
+ value={formatCountShort(status()!.search_index.trigrams)}
222
+ />
223
+ <R
224
+ theme={t()}
225
+ label="Disk"
226
+ value={formatBytes(status()!.disk.trigram_disk_bytes)}
227
+ tone="muted"
228
+ />
229
+
230
+ <box marginTop={1}>
231
+ <text fg={t().text}>
232
+ <b>Runtime</b>
233
+ </text>
234
+ </box>
235
+ <R theme={t()} label="LSP servers" value={String(status()!.lsp_servers)} />
236
+ <R
237
+ theme={t()}
238
+ label="Symbol cache (local)"
239
+ value={formatCountShort(status()!.symbol_cache.local_entries)}
240
+ />
241
+ <R
242
+ theme={t()}
243
+ label="Symbol cache (warm)"
244
+ value={formatCountShort(status()!.symbol_cache.warm_entries)}
245
+ tone="muted"
246
+ />
247
+
248
+ <box marginTop={1}>
249
+ <text fg={t().text}>
250
+ <b>Features</b>
251
+ </text>
252
+ </box>
253
+ <R
254
+ theme={t()}
255
+ label="format_on_edit"
256
+ value={status()!.features.format_on_edit ? "on" : "off"}
257
+ tone={status()!.features.format_on_edit ? "ok" : "muted"}
258
+ />
259
+ <R
260
+ theme={t()}
261
+ label="search_index"
262
+ value={status()!.features.search_index ? "on" : "off"}
263
+ tone={status()!.features.search_index ? "ok" : "muted"}
264
+ />
265
+ <R
266
+ theme={t()}
267
+ label="semantic_search"
268
+ value={status()!.features.semantic_search ? "on" : "off"}
269
+ tone={status()!.features.semantic_search ? "ok" : "muted"}
270
+ />
271
+ </box>
272
+
273
+ {/* Right column */}
274
+ <box flexDirection="column" flexGrow={1} flexBasis={0}>
275
+ <text fg={t().text}>
276
+ <b>Semantic index</b>
277
+ </text>
278
+ <R
279
+ theme={t()}
280
+ label="Status"
281
+ value={status()!.semantic_index.status}
282
+ tone={statusTone(status()!.semantic_index.status)}
283
+ />
284
+ <R
285
+ theme={t()}
286
+ label="Entries"
287
+ value={formatCountShort(status()!.semantic_index.entries)}
288
+ />
289
+ {status()!.semantic_index.backend ? (
290
+ <R
291
+ theme={t()}
292
+ label="Backend"
293
+ value={status()!.semantic_index.backend!}
294
+ tone="muted"
295
+ />
296
+ ) : null}
297
+ {status()!.semantic_index.model ? (
298
+ <R theme={t()} label="Model" value={status()!.semantic_index.model!} tone="muted" />
299
+ ) : null}
300
+ {status()!.semantic_index.dimension != null ? (
301
+ <R
302
+ theme={t()}
303
+ label="Dimension"
304
+ value={String(status()!.semantic_index.dimension)}
305
+ tone="muted"
306
+ />
307
+ ) : null}
308
+ <R
309
+ theme={t()}
310
+ label="Disk"
311
+ value={formatBytes(status()!.disk.semantic_disk_bytes)}
312
+ tone="muted"
313
+ />
314
+
315
+ <box marginTop={1}>
316
+ <text fg={t().text}>
317
+ <b>Current session</b>
318
+ </text>
319
+ </box>
320
+ <R theme={t()} label="Tracked files" value={String(status()!.session.tracked_files)} />
321
+ <R theme={t()} label="Checkpoints" value={String(status()!.session.checkpoints)} />
322
+ <R
323
+ theme={t()}
324
+ label="All-session checkpoints"
325
+ value={String(status()!.checkpoints_total)}
326
+ tone="muted"
327
+ />
328
+ </box>
329
+ </box>
330
+ ) : null}
331
+
332
+ {/* Optional semantic build progress — full-width below the columns */}
333
+ {status()?.semantic_index.stage ? (
334
+ <box flexDirection="column" width="100%" marginTop={1}>
335
+ <text fg={t().text}>
336
+ <b>Semantic build progress</b>
337
+ </text>
338
+ <R theme={t()} label="Stage" value={status()!.semantic_index.stage!} />
339
+ {status()!.semantic_index.files != null ? (
340
+ <R
341
+ theme={t()}
342
+ label="Files seen"
343
+ value={formatCountShort(status()!.semantic_index.files)}
344
+ />
345
+ ) : null}
346
+ {status()!.semantic_index.entries_done != null ||
347
+ status()!.semantic_index.entries_total != null ? (
348
+ <R
349
+ theme={t()}
350
+ label="Progress"
351
+ value={`${formatCountShort(status()!.semantic_index.entries_done ?? null)} / ${formatCountShort(status()!.semantic_index.entries_total ?? null)}`}
352
+ />
353
+ ) : null}
354
+ </box>
355
+ ) : null}
356
+
357
+ {/* Semantic error — full-width, themed error color */}
358
+ {status()?.semantic_index.error ? (
359
+ <box marginTop={1} width="100%">
360
+ <text fg={t().error}>⚠ {status()!.semantic_index.error}</text>
361
+ </box>
362
+ ) : null}
363
+
364
+ {/* Footer */}
365
+ <box marginTop={1} justifyContent="flex-end" width="100%">
366
+ <text fg={t().textMuted}>Enter or Esc to close</text>
367
+ </box>
368
+ </box>
369
+ );
370
+ };
371
+
45
372
  async function showStatusDialog(api: TuiPluginApi): Promise<void> {
46
373
  const sessionID = getSessionId(api);
47
374
  if (!sessionID) {
@@ -57,82 +384,36 @@ async function showStatusDialog(api: TuiPluginApi): Promise<void> {
57
384
 
58
385
  const client = getRpcClient(directory);
59
386
 
60
- // Fetch status immediately and show the dialog
61
- let currentMessage = "Connecting to AFT...";
387
+ // Prime the dialog with one initial fetch so we don't show a blank
388
+ // skeleton the component then takes over polling.
389
+ let initial: AftStatusSnapshot | null = null;
390
+ let initialError: string | null = null;
62
391
  try {
63
392
  const response = await client.call("status", { sessionID });
64
393
  if ((response as Record<string, unknown>).success !== false) {
65
- const status = coerceAftStatus(response as Record<string, unknown>);
66
- currentMessage = formatStatusDialogMessage(status);
394
+ initial = coerceAftStatus(response as Record<string, unknown>);
395
+ } else {
396
+ initialError = "AFT bridge returned an error response.";
67
397
  }
68
398
  } catch {
69
- currentMessage = "AFT is starting up. Status will refresh automatically...";
399
+ initialError = "AFT is starting up. Status will refresh automatically...";
70
400
  }
71
401
 
72
- // Track whether dialog is still open for polling cleanup
73
- let dialogOpen = true;
74
- let pollTimer: ReturnType<typeof setInterval> | null = null;
75
-
76
- // Show dialog with initial data
77
402
  api.ui.dialog.setSize("large");
78
403
  api.ui.dialog.replace(
404
+ () => (
405
+ <StatusDialog
406
+ api={api}
407
+ client={client}
408
+ sessionID={sessionID}
409
+ initial={initial}
410
+ initialError={initialError}
411
+ onClose={() => {
412
+ api.ui.dialog.setSize("medium");
413
+ }}
414
+ />
415
+ ),
79
416
  () => {
80
- // Start polling after dialog renders
81
- if (!pollTimer) {
82
- pollTimer = setInterval(async () => {
83
- if (!dialogOpen) {
84
- if (pollTimer) clearInterval(pollTimer);
85
- return;
86
- }
87
- try {
88
- const response = await client.call("status", { sessionID });
89
- if ((response as Record<string, unknown>).success !== false) {
90
- const status = coerceAftStatus(response as Record<string, unknown>);
91
- const newMessage = formatStatusDialogMessage(status);
92
- if (newMessage !== currentMessage) {
93
- currentMessage = newMessage;
94
- // Re-render dialog with updated status
95
- api.ui.dialog.replace(
96
- () => (
97
- <api.ui.DialogAlert
98
- title="AFT Status"
99
- message={currentMessage}
100
- onConfirm={() => {
101
- dialogOpen = false;
102
- if (pollTimer) clearInterval(pollTimer);
103
- api.ui.dialog.setSize("medium");
104
- }}
105
- />
106
- ),
107
- () => {
108
- dialogOpen = false;
109
- if (pollTimer) clearInterval(pollTimer);
110
- api.ui.dialog.setSize("medium");
111
- },
112
- );
113
- }
114
- }
115
- } catch {
116
- // Polling failure is non-fatal — just skip this tick
117
- }
118
- }, 1500);
119
- }
120
-
121
- return (
122
- <api.ui.DialogAlert
123
- title="AFT Status"
124
- message={currentMessage}
125
- onConfirm={() => {
126
- dialogOpen = false;
127
- if (pollTimer) clearInterval(pollTimer);
128
- api.ui.dialog.setSize("medium");
129
- }}
130
- />
131
- );
132
- },
133
- () => {
134
- dialogOpen = false;
135
- if (pollTimer) clearInterval(pollTimer);
136
417
  api.ui.dialog.setSize("medium");
137
418
  },
138
419
  );