@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.
- package/dist/bash-wait-detach.d.ts.map +1 -1
- package/dist/bg-notifications.d.ts +13 -0
- package/dist/bg-notifications.d.ts.map +1 -1
- package/dist/bridge-bootstrap.d.ts +6 -6
- package/dist/bridge-bootstrap.d.ts.map +1 -1
- package/dist/config-live-reload.d.ts +36 -8
- package/dist/config-live-reload.d.ts.map +1 -1
- package/dist/config.d.ts +72 -17
- package/dist/config.d.ts.map +1 -1
- package/dist/configure-warnings.d.ts.map +1 -1
- package/dist/entry/server.js +5079 -2255
- package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4674 -1999
- package/dist/lsp-auto-install.d.ts.map +1 -1
- package/dist/lsp-github-install.d.ts +11 -2
- package/dist/lsp-github-install.d.ts.map +1 -1
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.d.ts.map +1 -1
- package/dist/rpc/contract.d.ts +1 -1
- package/dist/rpc/contract.d.ts.map +1 -1
- package/dist/rpc/register.d.ts +3 -1
- package/dist/rpc/register.d.ts.map +1 -1
- package/dist/shared/ignored-message.d.ts.map +1 -1
- package/dist/shared/read-vision.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +8 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/session-directory.d.ts +7 -5
- package/dist/shared/session-directory.d.ts.map +1 -1
- package/dist/shared/status.d.ts +10 -4
- package/dist/shared/status.d.ts.map +1 -1
- package/dist/shared/v2-context.d.ts +3 -0
- package/dist/shared/v2-context.d.ts.map +1 -0
- package/dist/subc-tool-schemas.d.ts +23 -1
- package/dist/subc-tool-schemas.d.ts.map +1 -1
- package/dist/tool-registration.d.ts +6 -3
- package/dist/tool-registration.d.ts.map +1 -1
- package/dist/tools/_shared.d.ts +2 -5
- package/dist/tools/_shared.d.ts.map +1 -1
- package/dist/tools/bash.d.ts +17 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash_watch.d.ts +11 -1
- package/dist/tools/bash_watch.d.ts.map +1 -1
- package/dist/tools/definitions/v2.d.ts +11 -1
- package/dist/tools/definitions/v2.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts +18 -0
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/inspect.d.ts +10 -1
- package/dist/tools/inspect.d.ts.map +1 -1
- package/dist/tools/permissions.d.ts +2 -1
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/semantic.d.ts.map +1 -1
- package/dist/tui/aft-rpc.d.ts +14 -0
- package/dist/tui/aft-rpc.d.ts.map +1 -1
- package/dist/tui/v2-status.d.ts.map +1 -1
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/v2-host-tool-names.d.ts +43 -0
- package/dist/v2-host-tool-names.d.ts.map +1 -0
- package/dist/v2-prompt-detach.d.ts +1 -0
- package/dist/v2-prompt-detach.d.ts.map +1 -1
- package/dist/v2-session-lifecycle.d.ts +22 -0
- package/dist/v2-session-lifecycle.d.ts.map +1 -0
- package/dist/v2-tool-hooks.d.ts +18 -0
- package/dist/v2-tool-hooks.d.ts.map +1 -0
- package/dist/v2-workflow-hints.d.ts +4 -0
- package/dist/v2-workflow-hints.d.ts.map +1 -0
- package/dist/wakes/runtime-consumer.d.ts +6 -1
- package/dist/wakes/runtime-consumer.d.ts.map +1 -1
- package/dist/wakes/session-delivery.d.ts +12 -7
- package/dist/wakes/session-delivery.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/entry/server-runtime.mjs +36 -4
- package/src/shared/ignored-message.ts +18 -0
- package/src/shared/read-vision.ts +14 -4
- package/src/shared/rpc-client.ts +46 -5
- package/src/shared/rpc-server.ts +10 -0
- package/src/shared/session-directory.ts +67 -19
- package/src/shared/status.ts +40 -9
- package/src/shared/v2-context.ts +7 -0
- package/src/tui/aft-rpc.ts +11 -0
- package/src/tui/index.tsx +20 -16
- package/src/tui/sidebar-view.tsx +9 -1
- package/src/tui/v2-status.ts +8 -5
- package/src/tui/v2.tsx +50 -0
- package/src/tui-compiled/aft-rpc.ts +11 -0
- package/src/tui-compiled/index.tsx +27 -16
- package/src/tui-compiled/sidebar-view.tsx +11 -2
- package/src/tui-compiled/v2-status.ts +8 -5
- 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
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
|
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`
|
|
91
|
+
/** Resolved directory, or `null` while a failed lookup is cooling down. */
|
|
90
92
|
directory: string | null;
|
|
91
|
-
/**
|
|
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 (
|
|
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
|
-
//
|
|
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
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
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 (
|
|
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
|
}
|
package/src/shared/status.ts
CHANGED
|
@@ -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
|
-
* `
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
|
|
598
|
-
|
|
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],
|
package/src/tui/aft-rpc.ts
CHANGED
|
@@ -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
|
-
{
|
|
496
|
+
{statusBar() ? (
|
|
496
497
|
<box flexDirection="column" width="100%" marginTop={1}>
|
|
497
498
|
<text fg={t().text}>
|
|
498
|
-
<b>{
|
|
499
|
+
<b>{statusBar()!.tier2_stale ? "Code Health ~" : "Code Health"}</b>
|
|
499
500
|
</text>
|
|
500
|
-
{
|
|
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(
|
|
505
|
-
tone={
|
|
508
|
+
value={formatCountShort(statusBar()!.errors)}
|
|
509
|
+
tone={statusBar()!.errors! > 0 ? "err" : "muted"}
|
|
506
510
|
/>
|
|
507
511
|
) : null}
|
|
508
|
-
{
|
|
512
|
+
{statusBar()!.warnings !== undefined ? (
|
|
509
513
|
<R
|
|
510
514
|
theme={t()}
|
|
511
515
|
label="Warnings"
|
|
512
|
-
value={formatCountShort(
|
|
513
|
-
tone={
|
|
516
|
+
value={formatCountShort(statusBar()!.warnings)}
|
|
517
|
+
tone={statusBar()!.warnings! > 0 ? "warn" : "muted"}
|
|
514
518
|
/>
|
|
515
519
|
) : null}
|
|
516
|
-
{
|
|
520
|
+
{statusBar()!.dead_code !== undefined ? (
|
|
517
521
|
<R
|
|
518
522
|
theme={t()}
|
|
519
523
|
label="Dead Code"
|
|
520
|
-
value={formatCountShort(
|
|
524
|
+
value={formatCountShort(statusBar()!.dead_code)}
|
|
521
525
|
tone="muted"
|
|
522
526
|
/>
|
|
523
527
|
) : null}
|
|
524
|
-
{
|
|
528
|
+
{statusBar()!.unused_exports !== undefined ? (
|
|
525
529
|
<R
|
|
526
530
|
theme={t()}
|
|
527
531
|
label="Unused Exports"
|
|
528
|
-
value={formatCountShort(
|
|
532
|
+
value={formatCountShort(statusBar()!.unused_exports)}
|
|
529
533
|
tone="muted"
|
|
530
534
|
/>
|
|
531
535
|
) : null}
|
|
532
|
-
{
|
|
536
|
+
{statusBar()!.duplicates !== undefined ? (
|
|
533
537
|
<R
|
|
534
538
|
theme={t()}
|
|
535
539
|
label="Duplicates"
|
|
536
|
-
value={formatCountShort(
|
|
540
|
+
value={formatCountShort(statusBar()!.duplicates)}
|
|
537
541
|
tone="muted"
|
|
538
542
|
/>
|
|
539
543
|
) : null}
|
|
540
|
-
{
|
|
544
|
+
{statusBar()!.todos !== undefined ? (
|
|
541
545
|
<R
|
|
542
546
|
theme={t()}
|
|
543
547
|
label="TODOs"
|
|
544
|
-
value={formatCountShort(
|
|
548
|
+
value={formatCountShort(statusBar()!.todos)}
|
|
545
549
|
tone="muted"
|
|
546
550
|
/>
|
|
547
551
|
) : null}
|
package/src/tui/sidebar-view.tsx
CHANGED
|
@@ -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}
|
package/src/tui/v2-status.ts
CHANGED
|
@@ -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
|
});
|