@cortexkit/aft-opencode 0.27.1 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui.js CHANGED
@@ -1,12 +1,11 @@
1
- // @bun
2
1
  // src/tui/index.tsx
3
2
  import { createMemo as createMemo2, createSignal as createSignal2, onCleanup as onCleanup2 } from "solid-js";
4
3
  // package.json
5
4
  var package_default = {
6
5
  name: "@cortexkit/aft-opencode",
7
- version: "0.27.1",
6
+ version: "0.28.1",
8
7
  type: "module",
9
- description: "OpenCode plugin for Agent File Tools (AFT) \u2014 tree-sitter and lsp powered code analysis",
8
+ description: "OpenCode plugin for Agent File Tools (AFT) tree-sitter and lsp powered code analysis",
10
9
  main: "dist/index.js",
11
10
  types: "dist/index.d.ts",
12
11
  license: "MIT",
@@ -22,7 +21,7 @@ var package_default = {
22
21
  "README.md"
23
22
  ],
24
23
  scripts: {
25
- build: "bun build src/index.ts --outdir dist --target bun --format esm --external @opencode-ai/plugin && bun build src/tui/index.tsx --outfile dist/tui.js --target bun --format esm --external @opencode-ai/plugin --external @opencode-ai/plugin/tui --external @opentui/solid --external solid-js && tsc --emitDeclarationOnly",
24
+ build: "bun build src/index.ts --outdir dist --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/sdk && bun build src/tui/index.tsx --outfile dist/tui.js --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/plugin/tui --external @opencode-ai/sdk --external @opentui/solid --external solid-js && tsc --emitDeclarationOnly",
26
25
  typecheck: "tsc --noEmit",
27
26
  pretest: "cd ../.. && cargo build",
28
27
  test: "bun test",
@@ -33,7 +32,7 @@ var package_default = {
33
32
  },
34
33
  dependencies: {
35
34
  "@clack/prompts": "^1.2.0",
36
- "@cortexkit/aft-bridge": "0.27.1",
35
+ "@cortexkit/aft-bridge": "0.28.1",
37
36
  "@opencode-ai/plugin": "^1.15.5",
38
37
  "@opencode-ai/sdk": "^1.15.5",
39
38
  "comment-json": "^4.6.2",
@@ -41,11 +40,12 @@ var package_default = {
41
40
  zod: "^4.1.8"
42
41
  },
43
42
  optionalDependencies: {
44
- "@cortexkit/aft-darwin-arm64": "0.27.1",
45
- "@cortexkit/aft-darwin-x64": "0.27.1",
46
- "@cortexkit/aft-linux-arm64": "0.27.1",
47
- "@cortexkit/aft-linux-x64": "0.27.1",
48
- "@cortexkit/aft-win32-x64": "0.27.1"
43
+ "@cortexkit/aft-darwin-arm64": "0.28.1",
44
+ "@cortexkit/aft-darwin-x64": "0.28.1",
45
+ "@cortexkit/aft-linux-arm64": "0.28.1",
46
+ "@cortexkit/aft-linux-x64": "0.28.1",
47
+ "@cortexkit/aft-win32-arm64": "0.28.1",
48
+ "@cortexkit/aft-win32-x64": "0.28.1"
49
49
  },
50
50
  devDependencies: {
51
51
  "@types/node": "^22.0.0",
@@ -71,12 +71,13 @@ var package_default = {
71
71
  };
72
72
 
73
73
  // src/shared/rpc-client.ts
74
- import { readFileSync } from "fs";
74
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
75
+ import { join as join3 } from "node:path";
75
76
 
76
77
  // src/logger.ts
77
- import * as fs from "fs";
78
- import * as os from "os";
79
- import * as path from "path";
78
+ import * as fs from "node:fs";
79
+ import * as os from "node:os";
80
+ import * as path from "node:path";
80
81
  var TAG = "[aft-plugin]";
81
82
  var isTestEnv = process.env.BUN_TEST === "1" || false;
82
83
  var logFile = path.join(os.tmpdir(), isTestEnv ? "aft-plugin-test.log" : "aft-plugin.log");
@@ -133,8 +134,8 @@ function warn(message, data) {
133
134
  }
134
135
 
135
136
  // src/shared/rpc-utils.ts
136
- import { createHash } from "crypto";
137
- import { join as join2 } from "path";
137
+ import { createHash } from "node:crypto";
138
+ import { join as join2 } from "node:path";
138
139
  function projectHash(directory) {
139
140
  const normalized = directory.replace(/\/+$/, "");
140
141
  return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
@@ -143,6 +144,10 @@ function rpcPortFilePath(storageDir, directory) {
143
144
  const hash = projectHash(directory);
144
145
  return join2(storageDir, "rpc", hash, "port");
145
146
  }
147
+ function rpcPortFileDir(storageDir, directory) {
148
+ const hash = projectHash(directory);
149
+ return join2(storageDir, "rpc", hash, "ports");
150
+ }
146
151
 
147
152
  // src/shared/rpc-client.ts
148
153
  var MAX_RETRIES = 10;
@@ -152,95 +157,106 @@ var REQUEST_TIMEOUT_MS = 5000;
152
157
  class AftRpcClient {
153
158
  port = null;
154
159
  token = null;
155
- portFilePath;
156
- healthChecked = false;
160
+ portsDir;
161
+ legacyPortFile;
157
162
  constructor(storageDir, directory) {
158
- this.portFilePath = rpcPortFilePath(storageDir, directory);
163
+ this.portsDir = rpcPortFileDir(storageDir, directory);
164
+ this.legacyPortFile = rpcPortFilePath(storageDir, directory);
159
165
  }
160
166
  async call(method, params = {}) {
161
- const info = await this.resolvePortInfo();
162
- if (!info) {
167
+ const infos = await this.resolvePortInfos();
168
+ if (infos.length === 0) {
163
169
  throw new Error("AFT RPC server not available");
164
170
  }
165
- return this.callResolved(method, params, info, true);
166
- }
167
- async callResolved(method, params, info, retryOnConnectionFailure) {
168
- let response;
169
- try {
170
- response = await this.fetchWithTimeout(`http://127.0.0.1:${info.port}/rpc/${method}`, {
171
- method: "POST",
172
- headers: { "Content-Type": "application/json" },
173
- body: JSON.stringify({ ...params, token: info.token })
174
- });
175
- } catch (err) {
176
- if (!retryOnConnectionFailure)
177
- throw err;
178
- return this.retryAfterReset(method, params, err);
171
+ let placeholder = null;
172
+ let lastError = null;
173
+ for (const info of infos) {
174
+ try {
175
+ const result = await this.callOne(method, params, info);
176
+ if (this.looksLikePlaceholder(result)) {
177
+ placeholder = result;
178
+ continue;
179
+ }
180
+ this.port = info.port;
181
+ this.token = info.token;
182
+ return result;
183
+ } catch (err) {
184
+ lastError = err;
185
+ }
179
186
  }
187
+ if (placeholder !== null)
188
+ return placeholder;
189
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
190
+ }
191
+ async callOne(method, params, info) {
192
+ const response = await this.fetchWithTimeout(`http://127.0.0.1:${info.port}/rpc/${method}`, {
193
+ method: "POST",
194
+ headers: { "Content-Type": "application/json" },
195
+ body: JSON.stringify({ ...params, token: info.token })
196
+ });
180
197
  if (!response.ok) {
181
198
  const text = await response.text();
182
- if (response.status >= 500 && retryOnConnectionFailure) {
183
- return this.retryAfterReset(method, params, `HTTP ${response.status}: ${text}`);
184
- }
185
199
  throw new Error(`RPC ${method} failed (${response.status}): ${text}`);
186
200
  }
187
201
  return await response.json();
188
202
  }
189
- async retryAfterReset(method, params, reason) {
190
- const message = reason instanceof Error ? reason.message : String(reason);
191
- warn(`RPC ${method} failed on cached port; retrying after port refresh (${message})`);
192
- this.reset();
193
- const refreshed = await this.resolvePortInfo();
194
- if (!refreshed) {
195
- throw new Error("AFT RPC server not available after port refresh");
196
- }
197
- return this.callResolved(method, params, refreshed, false);
203
+ looksLikePlaceholder(result) {
204
+ if (!result || typeof result !== "object")
205
+ return false;
206
+ const status = result.status;
207
+ return status === "not_initialized";
198
208
  }
199
209
  async isAvailable() {
200
210
  try {
201
- const port = await this.resolvePort();
202
- return port !== null;
211
+ const infos = await this.resolvePortInfos();
212
+ return infos.length > 0;
203
213
  } catch {
204
214
  return false;
205
215
  }
206
216
  }
207
- async resolvePort() {
208
- return (await this.resolvePortInfo())?.port ?? null;
209
- }
210
- async resolvePortInfo() {
211
- if (this.port && this.healthChecked) {
212
- return { port: this.port, token: this.token };
213
- }
214
- if (this.port) {
215
- const alive = await this.healthCheck(this.port);
216
- if (alive) {
217
- this.healthChecked = true;
218
- return { port: this.port, token: this.token };
219
- }
220
- this.port = null;
221
- this.token = null;
222
- this.healthChecked = false;
223
- }
217
+ async resolvePortInfos() {
224
218
  for (let attempt = 0;attempt < MAX_RETRIES; attempt++) {
225
- const info = this.readPortFile();
226
- if (info) {
227
- const alive = await this.healthCheck(info.port);
228
- if (alive) {
229
- this.port = info.port;
230
- this.token = info.token;
231
- this.healthChecked = true;
232
- return info;
219
+ const infos = this.readAllPortFiles();
220
+ if (infos.length > 0) {
221
+ const alive = [];
222
+ for (const info of infos) {
223
+ if (await this.healthCheck(info.port)) {
224
+ alive.push(info);
225
+ }
233
226
  }
227
+ if (alive.length > 0)
228
+ return alive;
234
229
  }
235
230
  if (attempt < MAX_RETRIES - 1) {
236
231
  await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
237
232
  }
238
233
  }
239
- return null;
234
+ return [];
240
235
  }
241
- readPortFile() {
236
+ readAllPortFiles() {
237
+ const collected = [];
238
+ if (existsSync(this.portsDir)) {
239
+ try {
240
+ const entries = readdirSync(this.portsDir);
241
+ for (const entry of entries) {
242
+ if (!entry.endsWith(".json"))
243
+ continue;
244
+ const info = this.parsePortFile(join3(this.portsDir, entry));
245
+ if (info)
246
+ collected.push(info);
247
+ }
248
+ } catch {}
249
+ }
250
+ if (collected.length === 0) {
251
+ const info = this.parsePortFile(this.legacyPortFile);
252
+ if (info)
253
+ collected.push(info);
254
+ }
255
+ return collected;
256
+ }
257
+ parsePortFile(path2) {
242
258
  try {
243
- const content = readFileSync(this.portFilePath, "utf-8").trim();
259
+ const content = readFileSync(path2, "utf-8").trim();
244
260
  let port;
245
261
  let token;
246
262
  if (content.startsWith("{")) {
@@ -282,7 +298,6 @@ class AftRpcClient {
282
298
  reset() {
283
299
  this.port = null;
284
300
  this.token = null;
285
- this.healthChecked = false;
286
301
  }
287
302
  }
288
303
 
@@ -408,7 +423,7 @@ var REFRESH_DEBOUNCE_MS = 200;
408
423
  var POLL_INTERVAL_MS = 1500;
409
424
  function formatBytes2(n) {
410
425
  if (!Number.isFinite(n) || n <= 0)
411
- return "\u2014";
426
+ return "";
412
427
  if (n >= 1073741824)
413
428
  return `${(n / 1073741824).toFixed(1)} GB`;
414
429
  if (n >= 1048576)
@@ -419,7 +434,7 @@ function formatBytes2(n) {
419
434
  }
420
435
  function formatCount(n) {
421
436
  if (n == null || !Number.isFinite(n))
422
- return "\u2014";
437
+ return "";
423
438
  if (n >= 1e6)
424
439
  return `${(n / 1e6).toFixed(1)}M`;
425
440
  if (n >= 1000)
@@ -595,6 +610,7 @@ var SidebarContent = (props) => {
595
610
  });
596
611
  }, { defer: false }));
597
612
  const s = createMemo(() => status());
613
+ const notInitialized = () => s()?.cache_role === "not_initialized";
598
614
  const searchStatus = () => statusDisplay(s()?.search_index?.status ?? "disabled");
599
615
  const semanticStatus = () => statusDisplay(s()?.semantic_index?.status ?? "disabled");
600
616
  const trigramBytes = () => s()?.disk?.trigram_disk_bytes ?? 0;
@@ -663,7 +679,7 @@ var SidebarContent = (props) => {
663
679
  }, undefined, false, undefined, this)
664
680
  ]
665
681
  }, undefined, true, undefined, this),
666
- /* @__PURE__ */ jsxDEV("text", {
682
+ !notInitialized() && /* @__PURE__ */ jsxDEV("text", {
667
683
  fg: props.theme.textMuted,
668
684
  children: [
669
685
  "v",
@@ -678,100 +694,104 @@ var SidebarContent = (props) => {
678
694
  children: /* @__PURE__ */ jsxDEV("text", {
679
695
  fg: props.theme.warning,
680
696
  children: [
681
- "\u26A0 ",
697
+ " ",
682
698
  degradedSummary()
683
699
  ]
684
700
  }, undefined, true, undefined, this)
685
701
  }, undefined, false, undefined, this),
686
- s()?.cache_role === "not_initialized" && /* @__PURE__ */ jsxDEV("box", {
702
+ notInitialized() && /* @__PURE__ */ jsxDEV("box", {
687
703
  marginTop: 1,
688
704
  width: "100%",
689
705
  children: /* @__PURE__ */ jsxDEV("text", {
690
706
  fg: props.theme.textMuted,
691
- children: s().message || "Waiting for first tool call to populate"
707
+ children: s().message || "AFT bridge is now spawned lazily, information here will be populated after first tool call."
692
708
  }, undefined, false, undefined, this)
693
709
  }, undefined, false, undefined, this),
694
- /* @__PURE__ */ jsxDEV(SectionHeader, {
695
- theme: props.theme,
696
- title: "Search Index"
697
- }, undefined, false, undefined, this),
698
- /* @__PURE__ */ jsxDEV(StatRow, {
699
- theme: props.theme,
700
- label: "Status",
701
- value: searchStatus().label,
702
- tone: searchStatus().tone
703
- }, undefined, false, undefined, this),
704
- (s()?.search_index?.files ?? null) != null && /* @__PURE__ */ jsxDEV(StatRow, {
705
- theme: props.theme,
706
- label: "Files",
707
- value: formatCount(s().search_index.files),
708
- tone: "muted"
709
- }, undefined, false, undefined, this),
710
- /* @__PURE__ */ jsxDEV(StatRow, {
711
- theme: props.theme,
712
- label: "Disk",
713
- value: formatBytes2(trigramBytes()),
714
- tone: "muted"
715
- }, undefined, false, undefined, this),
716
- /* @__PURE__ */ jsxDEV(SectionHeader, {
717
- theme: props.theme,
718
- title: "Semantic Index"
719
- }, undefined, false, undefined, this),
720
- /* @__PURE__ */ jsxDEV(StatRow, {
721
- theme: props.theme,
722
- label: "Status",
723
- value: semanticStatus().label,
724
- tone: semanticStatus().tone
725
- }, undefined, false, undefined, this),
726
- s()?.semantic_index?.status === "loading" && s()?.semantic_index?.entries_total != null && s().semantic_index.entries_total > 0 && /* @__PURE__ */ jsxDEV(StatRow, {
727
- theme: props.theme,
728
- label: "Progress",
729
- value: `${formatCount(s().semantic_index.entries_done)} / ${formatCount(s().semantic_index.entries_total)}`,
730
- tone: "warn"
731
- }, undefined, false, undefined, this),
732
- (s()?.semantic_index?.entries ?? null) != null && /* @__PURE__ */ jsxDEV(StatRow, {
733
- theme: props.theme,
734
- label: "Entries",
735
- value: formatCount(s().semantic_index.entries),
736
- tone: "muted"
737
- }, undefined, false, undefined, this),
738
- /* @__PURE__ */ jsxDEV(StatRow, {
739
- theme: props.theme,
740
- label: "Disk",
741
- value: formatBytes2(semanticBytes()),
742
- tone: "muted"
743
- }, undefined, false, undefined, this),
744
- compressionRows().length > 0 && /* @__PURE__ */ jsxDEV(Fragment, {
710
+ !notInitialized() && /* @__PURE__ */ jsxDEV(Fragment, {
745
711
  children: [
746
712
  /* @__PURE__ */ jsxDEV(SectionHeader, {
747
713
  theme: props.theme,
748
- title: "Compression"
714
+ title: "Search Index"
749
715
  }, undefined, false, undefined, this),
750
- compressionRows().map((row) => row.kind === "scope" ? /* @__PURE__ */ jsxDEV("box", {
751
- width: "100%",
752
- children: /* @__PURE__ */ jsxDEV("text", {
753
- fg: props.theme.text,
754
- children: row.label
755
- }, undefined, false, undefined, this)
756
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV(StatRow, {
716
+ /* @__PURE__ */ jsxDEV(StatRow, {
757
717
  theme: props.theme,
758
- label: row.label,
759
- value: row.value,
718
+ label: "Status",
719
+ value: searchStatus().label,
720
+ tone: searchStatus().tone
721
+ }, undefined, false, undefined, this),
722
+ (s()?.search_index?.files ?? null) != null && /* @__PURE__ */ jsxDEV(StatRow, {
723
+ theme: props.theme,
724
+ label: "Files",
725
+ value: formatCount(s().search_index.files),
760
726
  tone: "muted"
761
- }, undefined, false, undefined, this))
727
+ }, undefined, false, undefined, this),
728
+ /* @__PURE__ */ jsxDEV(StatRow, {
729
+ theme: props.theme,
730
+ label: "Disk",
731
+ value: formatBytes2(trigramBytes()),
732
+ tone: "muted"
733
+ }, undefined, false, undefined, this),
734
+ /* @__PURE__ */ jsxDEV(SectionHeader, {
735
+ theme: props.theme,
736
+ title: "Semantic Index"
737
+ }, undefined, false, undefined, this),
738
+ /* @__PURE__ */ jsxDEV(StatRow, {
739
+ theme: props.theme,
740
+ label: "Status",
741
+ value: semanticStatus().label,
742
+ tone: semanticStatus().tone
743
+ }, undefined, false, undefined, this),
744
+ s()?.semantic_index?.status === "loading" && s()?.semantic_index?.entries_total != null && s().semantic_index.entries_total > 0 && /* @__PURE__ */ jsxDEV(StatRow, {
745
+ theme: props.theme,
746
+ label: "Progress",
747
+ value: `${formatCount(s().semantic_index.entries_done)} / ${formatCount(s().semantic_index.entries_total)}`,
748
+ tone: "warn"
749
+ }, undefined, false, undefined, this),
750
+ (s()?.semantic_index?.entries ?? null) != null && /* @__PURE__ */ jsxDEV(StatRow, {
751
+ theme: props.theme,
752
+ label: "Entries",
753
+ value: formatCount(s().semantic_index.entries),
754
+ tone: "muted"
755
+ }, undefined, false, undefined, this),
756
+ /* @__PURE__ */ jsxDEV(StatRow, {
757
+ theme: props.theme,
758
+ label: "Disk",
759
+ value: formatBytes2(semanticBytes()),
760
+ tone: "muted"
761
+ }, undefined, false, undefined, this),
762
+ compressionRows().length > 0 && /* @__PURE__ */ jsxDEV(Fragment, {
763
+ children: [
764
+ /* @__PURE__ */ jsxDEV(SectionHeader, {
765
+ theme: props.theme,
766
+ title: "Compression"
767
+ }, undefined, false, undefined, this),
768
+ compressionRows().map((row) => row.kind === "scope" ? /* @__PURE__ */ jsxDEV("box", {
769
+ width: "100%",
770
+ children: /* @__PURE__ */ jsxDEV("text", {
771
+ fg: props.theme.text,
772
+ children: row.label
773
+ }, undefined, false, undefined, this)
774
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV(StatRow, {
775
+ theme: props.theme,
776
+ label: row.label,
777
+ value: row.value,
778
+ tone: "muted"
779
+ }, undefined, false, undefined, this))
780
+ ]
781
+ }, undefined, true, undefined, this),
782
+ s()?.semantic_index?.status === "failed" && s()?.semantic_index?.error && /* @__PURE__ */ jsxDEV("box", {
783
+ marginTop: 1,
784
+ width: "100%",
785
+ children: /* @__PURE__ */ jsxDEV("text", {
786
+ fg: props.theme.error,
787
+ children: [
788
+ "⚠ ",
789
+ s().semantic_index.error
790
+ ]
791
+ }, undefined, true, undefined, this)
792
+ }, undefined, false, undefined, this)
762
793
  ]
763
- }, undefined, true, undefined, this),
764
- s()?.semantic_index?.status === "failed" && s()?.semantic_index?.error && /* @__PURE__ */ jsxDEV("box", {
765
- marginTop: 1,
766
- width: "100%",
767
- children: /* @__PURE__ */ jsxDEV("text", {
768
- fg: props.theme.error,
769
- children: [
770
- "\u26A0 ",
771
- s().semantic_index.error
772
- ]
773
- }, undefined, true, undefined, this)
774
- }, undefined, false, undefined, this)
794
+ }, undefined, true, undefined, this)
775
795
  ]
776
796
  }, undefined, true, undefined, this);
777
797
  };
@@ -819,7 +839,7 @@ function getSessionId(api) {
819
839
  var POLL_INTERVAL_MS2 = 1500;
820
840
  function formatCountShort(value) {
821
841
  if (value == null || !Number.isFinite(value))
822
- return "\u2014";
842
+ return "";
823
843
  if (value >= 1e6)
824
844
  return `${(value / 1e6).toFixed(1)}M`;
825
845
  if (value >= 1000)
@@ -911,10 +931,10 @@ var StatusDialog = (props) => {
911
931
  /* @__PURE__ */ jsxDEV2("text", {
912
932
  fg: t().accent,
913
933
  children: /* @__PURE__ */ jsxDEV2("b", {
914
- children: "\u26A1 AFT Status"
934
+ children: " AFT Status"
915
935
  }, undefined, false, undefined, this)
916
936
  }, undefined, false, undefined, this),
917
- /* @__PURE__ */ jsxDEV2("text", {
937
+ status()?.cache_role !== "not_initialized" && /* @__PURE__ */ jsxDEV2("text", {
918
938
  fg: t().textMuted,
919
939
  children: [
920
940
  "v",
@@ -937,7 +957,7 @@ var StatusDialog = (props) => {
937
957
  justifyContent: "center",
938
958
  children: /* @__PURE__ */ jsxDEV2("text", {
939
959
  fg: t().textMuted,
940
- children: status().message || "Waiting for first tool call to populate"
960
+ children: status().message || "AFT bridge is now spawned lazily, information here will be populated after first tool call."
941
961
  }, undefined, false, undefined, this)
942
962
  }, undefined, false, undefined, this) : null,
943
963
  status() && status().cache_role !== "not_initialized" ? /* @__PURE__ */ jsxDEV2("box", {
@@ -963,7 +983,7 @@ var StatusDialog = (props) => {
963
983
  }, undefined, false, undefined, this)
964
984
  ]
965
985
  }, undefined, true, undefined, this) : null,
966
- status() ? /* @__PURE__ */ jsxDEV2("box", {
986
+ status() && status().cache_role !== "not_initialized" ? /* @__PURE__ */ jsxDEV2("box", {
967
987
  flexDirection: "row",
968
988
  width: "100%",
969
989
  gap: 4,
@@ -1164,7 +1184,7 @@ var StatusDialog = (props) => {
1164
1184
  children: /* @__PURE__ */ jsxDEV2("text", {
1165
1185
  fg: t().error,
1166
1186
  children: [
1167
- "\u26A0 ",
1187
+ " ",
1168
1188
  status().semantic_index.error
1169
1189
  ]
1170
1190
  }, undefined, true, undefined, this)
@@ -1287,13 +1307,19 @@ async function showStartupNotifications(api) {
1287
1307
  try {
1288
1308
  const announcement = await client.call("get-announcement", {});
1289
1309
  if (announcement.show && announcement.version && announcement.features?.length) {
1290
- const featureText = announcement.features.map((f) => ` \u2022 ${f}`).join(`
1310
+ const featureText = announcement.features.map((f) => ` ${f}`).join(`
1291
1311
  `);
1312
+ const hasFooter = typeof announcement.footer === "string" && announcement.footer.trim().length > 0;
1313
+ const message = hasFooter ? `What's new:
1314
+
1315
+ ${featureText}
1316
+
1317
+ ${announcement.footer}` : `What's new:
1318
+
1319
+ ${featureText}`;
1292
1320
  api.ui.dialog.replace(() => /* @__PURE__ */ jsxDEV2(api.ui.DialogAlert, {
1293
1321
  title: `AFT v${announcement.version}`,
1294
- message: `What's new:
1295
-
1296
- ${featureText}`,
1322
+ message,
1297
1323
  onConfirm: () => {
1298
1324
  client.call("mark-announced", {});
1299
1325
  }
@@ -1,4 +1,4 @@
1
- import type { AftConfig } from "./config.js";
1
+ import { type AftConfig } from "./config.js";
2
2
  export interface WorkflowHintsOpts {
3
3
  /** `tool_surface` setting — controls which tools are registered. */
4
4
  toolSurface: "minimal" | "recommended" | "all";
@@ -20,6 +20,10 @@ export declare function buildWorkflowHints(opts: WorkflowHintsOpts): string | nu
20
20
  /**
21
21
  * Resolve workflow-hints opts from a loaded AftConfig and the active
22
22
  * disabled-tools set computed at registration time.
23
+ *
24
+ * Background-bash gating reads the resolved bash config so the new
25
+ * graduated `bash: true` / `bash: { background: true }` shapes enable the
26
+ * hint, not just the legacy `experimental.bash.background: true` path.
23
27
  */
24
28
  export declare function buildHintsFromConfig(config: AftConfig, disabledTools: Set<string>): string | null;
25
29
  //# sourceMappingURL=workflow-hints.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workflow-hints.d.ts","sourceRoot":"","sources":["../src/workflow-hints.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,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,CAmEzE;AAED;;;GAGG;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,CAmEzE;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.27.1",
3
+ "version": "0.28.1",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
6
6
  "main": "dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "README.md"
19
19
  ],
20
20
  "scripts": {
21
- "build": "bun build src/index.ts --outdir dist --target bun --format esm --external @opencode-ai/plugin && bun build src/tui/index.tsx --outfile dist/tui.js --target bun --format esm --external @opencode-ai/plugin --external @opencode-ai/plugin/tui --external @opentui/solid --external solid-js && tsc --emitDeclarationOnly",
21
+ "build": "bun build src/index.ts --outdir dist --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/sdk && bun build src/tui/index.tsx --outfile dist/tui.js --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/plugin/tui --external @opencode-ai/sdk --external @opentui/solid --external solid-js && tsc --emitDeclarationOnly",
22
22
  "typecheck": "tsc --noEmit",
23
23
  "pretest": "cd ../.. && cargo build",
24
24
  "test": "bun test",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@clack/prompts": "^1.2.0",
32
- "@cortexkit/aft-bridge": "0.27.1",
32
+ "@cortexkit/aft-bridge": "0.28.1",
33
33
  "@opencode-ai/plugin": "^1.15.5",
34
34
  "@opencode-ai/sdk": "^1.15.5",
35
35
  "comment-json": "^4.6.2",
@@ -37,11 +37,12 @@
37
37
  "zod": "^4.1.8"
38
38
  },
39
39
  "optionalDependencies": {
40
- "@cortexkit/aft-darwin-arm64": "0.27.1",
41
- "@cortexkit/aft-darwin-x64": "0.27.1",
42
- "@cortexkit/aft-linux-arm64": "0.27.1",
43
- "@cortexkit/aft-linux-x64": "0.27.1",
44
- "@cortexkit/aft-win32-x64": "0.27.1"
40
+ "@cortexkit/aft-darwin-arm64": "0.28.1",
41
+ "@cortexkit/aft-darwin-x64": "0.28.1",
42
+ "@cortexkit/aft-linux-arm64": "0.28.1",
43
+ "@cortexkit/aft-linux-x64": "0.28.1",
44
+ "@cortexkit/aft-win32-arm64": "0.28.1",
45
+ "@cortexkit/aft-win32-x64": "0.28.1"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@types/node": "^22.0.0",