@cortexkit/aft-opencode 0.58.2 → 0.60.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.
Files changed (93) hide show
  1. package/dist/bash-wait-detach.d.ts.map +1 -1
  2. package/dist/bg-notifications.d.ts +13 -0
  3. package/dist/bg-notifications.d.ts.map +1 -1
  4. package/dist/bridge-bootstrap.d.ts +6 -6
  5. package/dist/bridge-bootstrap.d.ts.map +1 -1
  6. package/dist/config-live-reload.d.ts +36 -8
  7. package/dist/config-live-reload.d.ts.map +1 -1
  8. package/dist/config.d.ts +72 -17
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/configure-warnings.d.ts.map +1 -1
  11. package/dist/entry/server.js +5079 -2255
  12. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  13. package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +4674 -1999
  16. package/dist/lsp-auto-install.d.ts.map +1 -1
  17. package/dist/lsp-github-install.d.ts +11 -2
  18. package/dist/lsp-github-install.d.ts.map +1 -1
  19. package/dist/notifications.d.ts +1 -1
  20. package/dist/notifications.d.ts.map +1 -1
  21. package/dist/rpc/contract.d.ts +1 -1
  22. package/dist/rpc/contract.d.ts.map +1 -1
  23. package/dist/rpc/register.d.ts +3 -1
  24. package/dist/rpc/register.d.ts.map +1 -1
  25. package/dist/shared/ignored-message.d.ts.map +1 -1
  26. package/dist/shared/read-vision.d.ts.map +1 -1
  27. package/dist/shared/rpc-client.d.ts +8 -0
  28. package/dist/shared/rpc-client.d.ts.map +1 -1
  29. package/dist/shared/rpc-server.d.ts.map +1 -1
  30. package/dist/shared/session-directory.d.ts +7 -5
  31. package/dist/shared/session-directory.d.ts.map +1 -1
  32. package/dist/shared/status.d.ts +10 -4
  33. package/dist/shared/status.d.ts.map +1 -1
  34. package/dist/shared/v2-context.d.ts +3 -0
  35. package/dist/shared/v2-context.d.ts.map +1 -0
  36. package/dist/subc-tool-schemas.d.ts +23 -1
  37. package/dist/subc-tool-schemas.d.ts.map +1 -1
  38. package/dist/tool-registration.d.ts +6 -3
  39. package/dist/tool-registration.d.ts.map +1 -1
  40. package/dist/tools/_shared.d.ts +2 -5
  41. package/dist/tools/_shared.d.ts.map +1 -1
  42. package/dist/tools/bash.d.ts +17 -0
  43. package/dist/tools/bash.d.ts.map +1 -1
  44. package/dist/tools/bash_watch.d.ts +11 -1
  45. package/dist/tools/bash_watch.d.ts.map +1 -1
  46. package/dist/tools/definitions/v2.d.ts +11 -1
  47. package/dist/tools/definitions/v2.d.ts.map +1 -1
  48. package/dist/tools/hoisted.d.ts +18 -0
  49. package/dist/tools/hoisted.d.ts.map +1 -1
  50. package/dist/tools/inspect.d.ts +10 -1
  51. package/dist/tools/inspect.d.ts.map +1 -1
  52. package/dist/tools/permissions.d.ts +2 -1
  53. package/dist/tools/permissions.d.ts.map +1 -1
  54. package/dist/tools/search.d.ts.map +1 -1
  55. package/dist/tools/semantic.d.ts.map +1 -1
  56. package/dist/tui/aft-rpc.d.ts +14 -0
  57. package/dist/tui/aft-rpc.d.ts.map +1 -1
  58. package/dist/tui/v2-status.d.ts.map +1 -1
  59. package/dist/types.d.ts +7 -1
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/v2-host-tool-names.d.ts +43 -0
  62. package/dist/v2-host-tool-names.d.ts.map +1 -0
  63. package/dist/v2-prompt-detach.d.ts +1 -0
  64. package/dist/v2-prompt-detach.d.ts.map +1 -1
  65. package/dist/v2-session-lifecycle.d.ts +22 -0
  66. package/dist/v2-session-lifecycle.d.ts.map +1 -0
  67. package/dist/v2-tool-hooks.d.ts +18 -0
  68. package/dist/v2-tool-hooks.d.ts.map +1 -0
  69. package/dist/v2-workflow-hints.d.ts +4 -0
  70. package/dist/v2-workflow-hints.d.ts.map +1 -0
  71. package/dist/wakes/runtime-consumer.d.ts +6 -1
  72. package/dist/wakes/runtime-consumer.d.ts.map +1 -1
  73. package/dist/wakes/session-delivery.d.ts +12 -7
  74. package/dist/wakes/session-delivery.d.ts.map +1 -1
  75. package/package.json +9 -9
  76. package/src/entry/server-runtime.mjs +36 -4
  77. package/src/shared/ignored-message.ts +18 -0
  78. package/src/shared/read-vision.ts +14 -4
  79. package/src/shared/rpc-client.ts +46 -5
  80. package/src/shared/rpc-server.ts +10 -0
  81. package/src/shared/session-directory.ts +67 -19
  82. package/src/shared/status.ts +40 -9
  83. package/src/shared/v2-context.ts +7 -0
  84. package/src/tui/aft-rpc.ts +11 -0
  85. package/src/tui/index.tsx +20 -16
  86. package/src/tui/sidebar-view.tsx +9 -1
  87. package/src/tui/v2-status.ts +8 -5
  88. package/src/tui/v2.tsx +50 -0
  89. package/src/tui-compiled/aft-rpc.ts +11 -0
  90. package/src/tui-compiled/index.tsx +27 -16
  91. package/src/tui-compiled/sidebar-view.tsx +11 -2
  92. package/src/tui-compiled/v2-status.ts +8 -5
  93. package/src/tui-compiled/v2.tsx +29 -0
@@ -68,12 +68,14 @@ function isV2PluginContext(client: unknown): client is V2PluginContextShape {
68
68
  }
69
69
 
70
70
  /**
71
- * How long an OpenCode 2 session lookup may run. The lookup is awaited before
72
- * the first tool call of a session is routed, so a host that never answers
73
- * must not hold that call; after this long the call proceeds with the
74
- * Location's directory instead.
71
+ * How long a session lookup may run on either host version. Tool routing
72
+ * awaits this lookup, so an unresponsive host must not hold the call forever.
73
+ * After this budget the call proceeds with its own directory instead.
75
74
  */
76
- export const V2_SESSION_LOOKUP_TIMEOUT_MS = 3_000;
75
+ export const LOOKUP_TIMEOUT_MS = 1_000;
76
+ export const V2_SESSION_LOOKUP_TIMEOUT_MS = LOOKUP_TIMEOUT_MS;
77
+ /** A failed lookup is provisional; retry later without querying on every call. */
78
+ export const FAILED_LOOKUP_RETRY_MS = 30_000;
77
79
 
78
80
  interface OpenCodeClientShape {
79
81
  session?: {
@@ -86,14 +88,15 @@ interface OpenCodeClientShape {
86
88
  }
87
89
 
88
90
  interface CacheEntry {
89
- /** Resolved directory, or `null` if lookup failed and we should not retry. */
91
+ /** Resolved directory, or `null` while a failed lookup is cooling down. */
90
92
  directory: string | null;
91
- /** Wall-clock timestamp of the cache entry, used only for LRU eviction. */
93
+ /** When the answer was recorded; failed lookups become retryable after 30 seconds. */
92
94
  recordedAt: number;
93
95
  }
94
96
 
95
97
  const CACHE_MAX_ENTRIES = 200;
96
98
  const cache = new Map<string, CacheEntry>();
99
+ const inflight = new Map<string, Promise<string | null>>();
97
100
 
98
101
  /**
99
102
  * Resolve the project directory the session was created with from the SDK's
@@ -114,19 +117,63 @@ export async function getSessionDirectory(
114
117
  if (!sessionId) return null;
115
118
 
116
119
  const cached = cache.get(sessionId);
117
- if (cached) {
120
+ if (
121
+ cached &&
122
+ (cached.directory !== null || Date.now() - cached.recordedAt < FAILED_LOOKUP_RETRY_MS)
123
+ ) {
118
124
  // Refresh LRU position
119
125
  cache.delete(sessionId);
120
126
  cache.set(sessionId, cached);
121
127
  return cached.directory;
122
128
  }
123
129
 
130
+ const pending = inflight.get(sessionId);
131
+ if (pending) return pending;
132
+ const lookup = boundedLookup(client, sessionId, fallbackDirectory).finally(() => {
133
+ inflight.delete(sessionId);
134
+ });
135
+ inflight.set(sessionId, lookup);
136
+ return lookup;
137
+ }
138
+
139
+ /** Ignore late answers: a timed-out lookup must not overwrite a later retry. */
140
+ async function boundedLookup(
141
+ client: unknown,
142
+ sessionId: string,
143
+ fallbackDirectory: string,
144
+ ): Promise<string | null> {
145
+ let timer: ReturnType<typeof setTimeout> | undefined;
146
+ try {
147
+ const timeout = new Promise<null>((resolve) => {
148
+ timer = setTimeout(() => {
149
+ sessionWarn(
150
+ sessionId,
151
+ `[aft-plugin] session lookup did not answer within ${LOOKUP_TIMEOUT_MS}ms; using the call's directory for now`,
152
+ );
153
+ resolve(null);
154
+ }, LOOKUP_TIMEOUT_MS);
155
+ });
156
+ const directory = await Promise.race([
157
+ lookupSessionDirectory(client, sessionId, fallbackDirectory),
158
+ timeout,
159
+ ]);
160
+ setCache(sessionId, directory);
161
+ return directory;
162
+ } finally {
163
+ clearTimeout(timer);
164
+ }
165
+ }
166
+
167
+ async function lookupSessionDirectory(
168
+ client: unknown,
169
+ sessionId: string,
170
+ fallbackDirectory: string,
171
+ ): Promise<string | null> {
124
172
  if (isV2PluginContext(client)) return lookupV2SessionDirectory(client, sessionId);
125
173
 
126
174
  const c = client as OpenCodeClientShape;
127
175
  const sessionApi = c?.session;
128
176
  if (!sessionApi || typeof sessionApi.get !== "function") {
129
- setCache(sessionId, null);
130
177
  return null;
131
178
  }
132
179
 
@@ -148,7 +195,7 @@ export async function getSessionDirectory(
148
195
  dir = session.directory;
149
196
  }
150
197
  } catch (err) {
151
- // Don't poison the cache on transient errors — but do log once.
198
+ // The bounded caller caches failures briefly so a struggling host is not polled per call.
152
199
  sessionWarn(
153
200
  sessionId,
154
201
  `[aft-plugin] session.get lookup failed: ${err instanceof Error ? err.message : String(err)}`,
@@ -156,7 +203,6 @@ export async function getSessionDirectory(
156
203
  return null;
157
204
  }
158
205
 
159
- setCache(sessionId, dir);
160
206
  return dir;
161
207
  }
162
208
 
@@ -164,12 +210,10 @@ export async function getSessionDirectory(
164
210
  * OpenCode 2 lookup: run the context's `session.get` Effect under a timeout and
165
211
  * read `location.directory`.
166
212
  *
167
- * Unlike the OpenCode 1 path, a failure or timeout is cached (as `null`), so it
168
- * is logged once per session and never retried. The fallback is already right:
169
- * with no cached directory, `projectRootFor` uses the tool call's Location
170
- * directory for OpenCode 2 runtimes, and OpenCode 2 runs every session inside
171
- * the Location created for that session's own directory. Retrying would only
172
- * add up to the timeout to every later tool call while the host misbehaves.
213
+ * The outer bounded lookup applies the same retry policy as OpenCode 1.
214
+ * The Effect timeout also interrupts the abandoned V2 lookup; fallback remains
215
+ * the tool call's runtime.directory (the host's Location directory), never
216
+ * the main checkout.
173
217
  */
174
218
  async function lookupV2SessionDirectory(
175
219
  context: V2PluginContextShape,
@@ -196,7 +240,6 @@ async function lookupV2SessionDirectory(
196
240
  `[aft-plugin] OpenCode 2 session lookup failed (${failure}); tool paths and bash use the Location directory ${context.location.directory} for this session`,
197
241
  );
198
242
  }
199
- setCache(sessionId, dir);
200
243
  return dir;
201
244
  }
202
245
 
@@ -226,6 +269,10 @@ export function getSessionDirectoryCached(
226
269
  if (!sessionId) return undefined;
227
270
  const cached = cache.get(sessionId);
228
271
  if (!cached) return undefined;
272
+ if (cached.directory === null && Date.now() - cached.recordedAt >= FAILED_LOOKUP_RETRY_MS) {
273
+ cache.delete(sessionId);
274
+ return undefined;
275
+ }
229
276
  return cached.directory;
230
277
  }
231
278
 
@@ -240,7 +287,7 @@ export function warmSessionDirectory(
240
287
  fallbackDirectory: string,
241
288
  ): void {
242
289
  if (!sessionId) return;
243
- if (cache.has(sessionId)) return;
290
+ if (getSessionDirectoryCached(sessionId) !== undefined) return;
244
291
  void getSessionDirectory(client, sessionId, fallbackDirectory);
245
292
  }
246
293
 
@@ -302,5 +349,6 @@ export async function verifySessionDirectory(
302
349
  /** Test-only: clear the cache between unit tests. */
303
350
  export function _resetSessionDirectoryCacheForTest(): void {
304
351
  cache.clear();
352
+ inflight.clear();
305
353
  verifyCache.clear();
306
354
  }
@@ -55,6 +55,7 @@ export interface StatusCompression {
55
55
  * absence as a clean zero.
56
56
  */
57
57
  export interface StatusBar {
58
+ disabled_categories?: string[];
58
59
  errors?: number;
59
60
  warnings?: number;
60
61
  dead_code?: number;
@@ -63,11 +64,12 @@ export interface StatusBar {
63
64
  todos?: number;
64
65
  tier2_stale?: boolean;
65
66
  /**
66
- * `"no_language_server"` when no language server is running, so errors and
67
- * warnings will not arrive until one starts. Absent while the counts are
68
- * present or a running server has not reported yet.
67
+ * `no_language_server` means no server is running. `producer_missing`
68
+ * masks both counts when an expected project producer has failed, even if
69
+ * another server supplied counts; diagnostics_gaps identifies the failures.
69
70
  */
70
- diagnostics?: "no_language_server";
71
+ diagnostics?: "no_language_server" | "producer_missing";
72
+ diagnostics_gaps?: Array<{ language: string; producer: string; root: string }>;
71
73
  }
72
74
 
73
75
  export interface AftStatusSnapshot {
@@ -214,6 +216,9 @@ function readCompression(value: unknown): StatusCompression | undefined {
214
216
  function readStatusBar(value: unknown): StatusBar | undefined {
215
217
  if (typeof value !== "object" || value === null) return undefined;
216
218
  const bar = asRecord(value);
219
+ const disabled = Array.isArray(bar.disabled_categories)
220
+ ? bar.disabled_categories.filter((key): key is string => typeof key === "string")
221
+ : [];
217
222
  const errors = readOptionalNumber(bar.errors);
218
223
  const warnings = readOptionalNumber(bar.warnings);
219
224
  const deadCode = readOptionalNumber(bar.dead_code);
@@ -221,6 +226,22 @@ function readStatusBar(value: unknown): StatusBar | undefined {
221
226
  const duplicates = readOptionalNumber(bar.duplicates);
222
227
  const todos = readOptionalNumber(bar.todos);
223
228
  const noLanguageServer = bar.diagnostics === "no_language_server";
229
+ const producerMissing = bar.diagnostics === "producer_missing";
230
+ const diagnosticsGaps = Array.isArray(bar.diagnostics_gaps)
231
+ ? bar.diagnostics_gaps
232
+ .map(asRecord)
233
+ .filter(
234
+ (gap) =>
235
+ typeof gap.language === "string" &&
236
+ typeof gap.producer === "string" &&
237
+ typeof gap.root === "string",
238
+ )
239
+ .map((gap) => ({
240
+ language: gap.language as string,
241
+ producer: gap.producer as string,
242
+ root: gap.root as string,
243
+ }))
244
+ : [];
224
245
  if (
225
246
  errors === null &&
226
247
  warnings === null &&
@@ -228,11 +249,14 @@ function readStatusBar(value: unknown): StatusBar | undefined {
228
249
  unusedExports === null &&
229
250
  duplicates === null &&
230
251
  todos === null &&
231
- !noLanguageServer
252
+ !noLanguageServer &&
253
+ !producerMissing &&
254
+ disabled.length === 0
232
255
  ) {
233
256
  return undefined;
234
257
  }
235
258
  return {
259
+ ...(disabled.length > 0 ? { disabled_categories: disabled } : {}),
236
260
  ...(errors !== null ? { errors } : {}),
237
261
  ...(warnings !== null ? { warnings } : {}),
238
262
  ...(deadCode !== null ? { dead_code: deadCode } : {}),
@@ -241,6 +265,9 @@ function readStatusBar(value: unknown): StatusBar | undefined {
241
265
  ...(todos !== null ? { todos } : {}),
242
266
  ...(bar.tier2_stale === true ? { tier2_stale: true } : {}),
243
267
  ...(noLanguageServer ? { diagnostics: "no_language_server" as const } : {}),
268
+ ...(producerMissing
269
+ ? { diagnostics: "producer_missing" as const, diagnostics_gaps: diagnosticsGaps }
270
+ : {}),
244
271
  };
245
272
  }
246
273
 
@@ -437,8 +464,10 @@ export function formatStatusDialogMessage(status: AftStatusSnapshot): string {
437
464
  lines.push(`- storage dir: ${status.storage_dir ?? status.disk.storage_dir}`);
438
465
  }
439
466
 
440
- if (status.status_bar) {
441
- const sb = status.status_bar;
467
+ const sb = status.status_bar ?? status.status_bar_values;
468
+ if (sb) {
469
+ for (const key of sb.disabled_categories ?? [])
470
+ lines.push(`- ${key.replaceAll("_", " ")}: ○ off`);
442
471
  const rows = [
443
472
  ["errors", sb.errors],
444
473
  ["warnings", sb.warnings],
@@ -594,8 +623,10 @@ export function formatStatusMarkdown(status: AftStatusSnapshot): string {
594
623
  lines.push(`- **Storage dir:** \`${status.storage_dir ?? status.disk.storage_dir}\``);
595
624
  }
596
625
 
597
- if (status.status_bar) {
598
- const sb = status.status_bar;
626
+ const sb = status.status_bar ?? status.status_bar_values;
627
+ if (sb) {
628
+ for (const key of sb.disabled_categories ?? [])
629
+ lines.push(`- **${key.replaceAll("_", " ")}:** ○ off`);
599
630
  const rows = [
600
631
  ["Errors", sb.errors],
601
632
  ["Warnings", sb.warnings],
@@ -0,0 +1,7 @@
1
+ /** V2 passes a Location-scoped Effect context where V1 passes an SDK client. */
2
+ export function isV2PluginContext(client: unknown): boolean {
3
+ return (
4
+ typeof (client as { location?: { directory?: unknown } } | null)?.location?.directory ===
5
+ "string"
6
+ );
7
+ }
@@ -24,8 +24,18 @@ export const IndexProgress = Schema.Struct({
24
24
  total: Schema.optional(Schema.Number),
25
25
  });
26
26
 
27
+ /**
28
+ * A one-off notice for the user, shown by the OpenCode 2 TUI as a toast. It
29
+ * travels over RPC rather than into the session because anything written into
30
+ * a session (a synthetic record included) is sent to the model on later turns.
31
+ */
32
+ export const HostToolOverlapNotice = Schema.Struct({
33
+ message: Schema.String,
34
+ });
35
+
27
36
  export type AftRpcSession = typeof RpcSession.Type;
28
37
  export type AftIndexProgress = typeof IndexProgress.Type;
38
+ export type AftHostToolOverlapNotice = typeof HostToolOverlapNotice.Type;
29
39
 
30
40
  export const AftRpc = Rpc.define({
31
41
  id: "aft",
@@ -39,5 +49,6 @@ export const AftRpc = Rpc.define({
39
49
  statusInvalidated: { schema: RpcSession },
40
50
  showStatusDialog: { schema: RpcSession },
41
51
  indexProgress: { schema: IndexProgress },
52
+ hostToolOverlap: { schema: HostToolOverlapNotice },
42
53
  },
43
54
  });
package/src/tui/index.tsx CHANGED
@@ -230,6 +230,7 @@ const StatusDialog = (props: StatusDialogProps) => {
230
230
  // The earlier `formatCompressionDialogRows` returned padded strings that
231
231
  // looked offset against neighboring sections.
232
232
  const compressionAggregateRows = () => formatCompressionSidebarRows(status()?.compression);
233
+ const statusBar = () => status()?.status_bar ?? status()?.status_bar_values;
233
234
 
234
235
  return (
235
236
  <box
@@ -492,56 +493,59 @@ const StatusDialog = (props: StatusDialogProps) => {
492
493
 
493
494
  {/* Human health values are optional. Omit a category until the server
494
495
  proves it rather than displaying an unproven zero as clean. */}
495
- {status()?.status_bar ? (
496
+ {statusBar() ? (
496
497
  <box flexDirection="column" width="100%" marginTop={1}>
497
498
  <text fg={t().text}>
498
- <b>{status()!.status_bar!.tier2_stale ? "Code Health ~" : "Code Health"}</b>
499
+ <b>{statusBar()!.tier2_stale ? "Code Health ~" : "Code Health"}</b>
499
500
  </text>
500
- {status()!.status_bar!.errors !== undefined ? (
501
+ {(statusBar()!.disabled_categories ?? []).map((key) => (
502
+ <R theme={t()} label={key.replaceAll("_", " ")} value="○ off" tone="muted" />
503
+ ))}
504
+ {statusBar()!.errors !== undefined ? (
501
505
  <R
502
506
  theme={t()}
503
507
  label="Errors"
504
- value={formatCountShort(status()!.status_bar!.errors)}
505
- tone={status()!.status_bar!.errors! > 0 ? "err" : "muted"}
508
+ value={formatCountShort(statusBar()!.errors)}
509
+ tone={statusBar()!.errors! > 0 ? "err" : "muted"}
506
510
  />
507
511
  ) : null}
508
- {status()!.status_bar!.warnings !== undefined ? (
512
+ {statusBar()!.warnings !== undefined ? (
509
513
  <R
510
514
  theme={t()}
511
515
  label="Warnings"
512
- value={formatCountShort(status()!.status_bar!.warnings)}
513
- tone={status()!.status_bar!.warnings! > 0 ? "warn" : "muted"}
516
+ value={formatCountShort(statusBar()!.warnings)}
517
+ tone={statusBar()!.warnings! > 0 ? "warn" : "muted"}
514
518
  />
515
519
  ) : null}
516
- {status()!.status_bar!.dead_code !== undefined ? (
520
+ {statusBar()!.dead_code !== undefined ? (
517
521
  <R
518
522
  theme={t()}
519
523
  label="Dead Code"
520
- value={formatCountShort(status()!.status_bar!.dead_code)}
524
+ value={formatCountShort(statusBar()!.dead_code)}
521
525
  tone="muted"
522
526
  />
523
527
  ) : null}
524
- {status()!.status_bar!.unused_exports !== undefined ? (
528
+ {statusBar()!.unused_exports !== undefined ? (
525
529
  <R
526
530
  theme={t()}
527
531
  label="Unused Exports"
528
- value={formatCountShort(status()!.status_bar!.unused_exports)}
532
+ value={formatCountShort(statusBar()!.unused_exports)}
529
533
  tone="muted"
530
534
  />
531
535
  ) : null}
532
- {status()!.status_bar!.duplicates !== undefined ? (
536
+ {statusBar()!.duplicates !== undefined ? (
533
537
  <R
534
538
  theme={t()}
535
539
  label="Duplicates"
536
- value={formatCountShort(status()!.status_bar!.duplicates)}
540
+ value={formatCountShort(statusBar()!.duplicates)}
537
541
  tone="muted"
538
542
  />
539
543
  ) : null}
540
- {status()!.status_bar!.todos !== undefined ? (
544
+ {statusBar()!.todos !== undefined ? (
541
545
  <R
542
546
  theme={t()}
543
547
  label="TODOs"
544
- value={formatCountShort(status()!.status_bar!.todos)}
548
+ value={formatCountShort(statusBar()!.todos)}
545
549
  tone="muted"
546
550
  />
547
551
  ) : null}
@@ -421,7 +421,7 @@ export const AftSidebarPanel = (props: AftSidebarPanelProps) => {
421
421
  const trigramBytes = () => s()?.disk?.trigram_disk_bytes ?? 0;
422
422
  const semanticBytes = () => s()?.disk?.semantic_disk_bytes ?? 0;
423
423
  const compressionRows = () => formatCompressionSidebarRows(s()?.compression);
424
- const statusBar = () => s()?.status_bar;
424
+ const statusBar = () => s()?.status_bar ?? s()?.status_bar_values;
425
425
 
426
426
  const degradedSummary = () => {
427
427
  const snap = s();
@@ -653,6 +653,14 @@ export const AftSidebarPanel = (props: AftSidebarPanelProps) => {
653
653
  palette={props.palette}
654
654
  title={statusBar()!.tier2_stale ? "Code Health ~" : "Code Health"}
655
655
  />
656
+ {(statusBar()!.disabled_categories ?? []).map((key) => (
657
+ <StatRow
658
+ palette={props.palette}
659
+ label={key.replaceAll("_", " ")}
660
+ value="○ off"
661
+ tone="muted"
662
+ />
663
+ ))}
656
664
  {statusBar()!.errors !== undefined && (
657
665
  <StatRow
658
666
  palette={props.palette}
@@ -1,6 +1,7 @@
1
1
  import { type AftStatusSnapshot, degradedHeaderMarker, type StatusBar } from "../shared/status";
2
2
 
3
- function count(value: number | undefined): string {
3
+ function count(value: number | undefined, off = false): string {
4
+ if (off) return "○";
4
5
  return value === undefined ? "?" : String(value);
5
6
  }
6
7
 
@@ -10,6 +11,8 @@ function count(value: number | undefined): string {
10
11
  * indefinitely.
11
12
  */
12
13
  function diagnosticsPart(bar: StatusBar): string {
14
+ if (bar.disabled_categories?.includes("diagnostics")) return "E○ W○";
15
+ if (bar.diagnostics === "producer_missing") return "E? W?";
13
16
  if (
14
17
  bar.errors === undefined &&
15
18
  bar.warnings === undefined &&
@@ -36,8 +39,8 @@ export function formatAftStatusSegment(status: AftStatusSnapshot | null): string
36
39
  const stale = bar.tier2_stale ? "~" : "";
37
40
  return (
38
41
  `AFT ${diagnosticsPart(bar)} | ` +
39
- `${stale}D${count(bar.dead_code)} U${count(bar.unused_exports)} ` +
40
- `C${count(bar.duplicates)} | T${count(bar.todos)}`
42
+ `${stale}D${count(bar.dead_code, bar.disabled_categories?.includes("dead_code"))} U${count(bar.unused_exports, bar.disabled_categories?.includes("unused_exports"))} ` +
43
+ `C${count(bar.duplicates, bar.disabled_categories?.includes("duplicates"))} | T${count(bar.todos, bar.disabled_categories?.includes("todos"))}`
41
44
  );
42
45
  }
43
46
 
@@ -53,8 +56,8 @@ function healthSummary(bar: StatusBar | undefined): string {
53
56
  const stale = bar.tier2_stale ? "~" : "";
54
57
  return (
55
58
  `${diagnosticsPart(bar)} ` +
56
- `${stale}D${count(bar.dead_code)} U${count(bar.unused_exports)} ` +
57
- `C${count(bar.duplicates)} T${count(bar.todos)}`
59
+ `${stale}D${count(bar.dead_code, bar.disabled_categories?.includes("dead_code"))} U${count(bar.unused_exports, bar.disabled_categories?.includes("unused_exports"))} ` +
60
+ `C${count(bar.duplicates, bar.disabled_categories?.includes("duplicates"))} T${count(bar.todos, bar.disabled_categories?.includes("todos"))}`
58
61
  );
59
62
  }
60
63
 
package/src/tui/v2.tsx CHANGED
@@ -28,6 +28,11 @@ type AftRpcClient = {
28
28
  handler: (event: { data: { sessionID?: string } }) => void | Promise<void>,
29
29
  options?: { signal?: AbortSignal },
30
30
  ): () => void;
31
+ on(
32
+ name: "hostToolOverlap",
33
+ handler: (event: { data: { message: string } }) => void | Promise<void>,
34
+ options?: { signal?: AbortSignal },
35
+ ): () => void;
31
36
  };
32
37
  };
33
38
 
@@ -87,6 +92,14 @@ type V2TuiContext = {
87
92
  };
88
93
  ui: {
89
94
  dialog: { alert(input: { title: string; message: string }): Promise<void> };
95
+ toast?: {
96
+ show(input: {
97
+ title?: string;
98
+ message: string;
99
+ variant?: "info" | "success" | "warning" | "error";
100
+ duration?: number;
101
+ }): void;
102
+ };
90
103
  router: { current(): { type: string; sessionID?: string } };
91
104
  slot(claim: {
92
105
  append: "app" | "prompt.footer.status" | "sidebar.content";
@@ -316,6 +329,39 @@ function AftStatusCommands(props: { context: V2TuiContext; rpc: AftRpcClient })
316
329
  return null;
317
330
  }
318
331
 
332
+ /** How long the built-in tool overlap toast stays up: long enough to read the command. */
333
+ const HOST_TOOL_OVERLAP_TOAST_MS = 15_000;
334
+
335
+ /**
336
+ * Show a server's one-off built-in tool overlap notice as a toast. A toast
337
+ * reaches the person at the terminal and nothing else. The current server no
338
+ * longer sends this notice: on OpenCode 2 AFT registers its command and patch
339
+ * tools as `shell` and `patch`, replacing the host's built-ins instead of
340
+ * sitting beside them. The subscription stays so a TUI still shows the notice
341
+ * from an older AFT server running in the same host during an upgrade. A host
342
+ * without a toast surface shows nothing.
343
+ */
344
+ export function subscribeV2HostToolOverlapToast(
345
+ context: Pick<V2TuiContext, "ui">,
346
+ rpc: AftRpcClient,
347
+ options?: { signal?: AbortSignal },
348
+ ): () => void {
349
+ return rpc.events.on(
350
+ "hostToolOverlap",
351
+ (event) => {
352
+ const toast = context.ui.toast;
353
+ if (typeof toast?.show !== "function") return;
354
+ toast.show({
355
+ title: "AFT",
356
+ message: event.data.message,
357
+ variant: "warning",
358
+ duration: HOST_TOOL_OVERLAP_TOAST_MS,
359
+ });
360
+ },
361
+ options,
362
+ );
363
+ }
364
+
319
365
  export async function setupV2Tui(context: V2TuiContext): Promise<() => void> {
320
366
  const rpc = context.client.rpc(AftRpc);
321
367
  const controller = new AbortController();
@@ -343,10 +389,14 @@ export async function setupV2Tui(context: V2TuiContext): Promise<() => void> {
343
389
  (event) => showStatusDialog(context, rpc, event.data.sessionID ?? activeSessionID(context)),
344
390
  { signal: controller.signal },
345
391
  );
392
+ const stopOverlapToasts = subscribeV2HostToolOverlapToast(context, rpc, {
393
+ signal: controller.signal,
394
+ });
346
395
 
347
396
  return () => {
348
397
  controller.abort();
349
398
  stopDialogEvents();
399
+ stopOverlapToasts();
350
400
  for (const cleanup of slotCleanups) cleanup();
351
401
  };
352
402
  }
@@ -24,8 +24,18 @@ export const IndexProgress = Schema.Struct({
24
24
  total: Schema.optional(Schema.Number),
25
25
  });
26
26
 
27
+ /**
28
+ * A one-off notice for the user, shown by the OpenCode 2 TUI as a toast. It
29
+ * travels over RPC rather than into the session because anything written into
30
+ * a session (a synthetic record included) is sent to the model on later turns.
31
+ */
32
+ export const HostToolOverlapNotice = Schema.Struct({
33
+ message: Schema.String,
34
+ });
35
+
27
36
  export type AftRpcSession = typeof RpcSession.Type;
28
37
  export type AftIndexProgress = typeof IndexProgress.Type;
38
+ export type AftHostToolOverlapNotice = typeof HostToolOverlapNotice.Type;
29
39
 
30
40
  export const AftRpc = Rpc.define({
31
41
  id: "aft",
@@ -39,5 +49,6 @@ export const AftRpc = Rpc.define({
39
49
  statusInvalidated: { schema: RpcSession },
40
50
  showStatusDialog: { schema: RpcSession },
41
51
  indexProgress: { schema: IndexProgress },
52
+ hostToolOverlap: { schema: HostToolOverlapNotice },
42
53
  },
43
54
  });