@cortexkit/aft-opencode 0.57.2 → 0.58.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 +14 -5
- package/dist/bash-wait-detach.d.ts.map +1 -1
- package/dist/bridge-bootstrap.d.ts +191 -0
- package/dist/bridge-bootstrap.d.ts.map +1 -0
- package/dist/config-error-surface.d.ts +23 -0
- package/dist/config-error-surface.d.ts.map +1 -0
- package/dist/config.d.ts +66 -54
- package/dist/config.d.ts.map +1 -1
- package/dist/entry/server.js +9729 -7942
- package/dist/hooks/auto-update-checker/index.d.ts +6 -0
- package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9515 -7793
- package/dist/logger.d.ts.map +1 -1
- package/dist/lsp-auto-install.d.ts.map +1 -1
- package/dist/rpc/register.d.ts +7 -1
- package/dist/rpc/register.d.ts.map +1 -1
- package/dist/shared/status.d.ts +45 -0
- package/dist/shared/status.d.ts.map +1 -1
- package/dist/shared/subagent-detect.d.ts.map +1 -1
- package/dist/shutdown-hooks.d.ts +3 -9
- package/dist/shutdown-hooks.d.ts.map +1 -1
- package/dist/subc-tool-schemas.d.ts +9 -1
- package/dist/subc-tool-schemas.d.ts.map +1 -1
- package/dist/tool-registration.d.ts +19 -12
- package/dist/tool-registration.d.ts.map +1 -1
- package/dist/tools/ast.d.ts.map +1 -1
- package/dist/tools/bash.d.ts +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash_watch.d.ts.map +1 -1
- package/dist/tools/definitions/v2.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts +9 -12
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/inspect.d.ts +5 -7
- package/dist/tools/inspect.d.ts.map +1 -1
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tui/v2-status.d.ts +1 -1
- package/dist/tui/v2-status.d.ts.map +1 -1
- package/dist/workflow-hints.d.ts +1 -7
- package/dist/workflow-hints.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/entry/server-runtime.mjs +117 -33
- package/src/logger.ts +5 -6
- package/src/shared/status.ts +83 -4
- package/src/shared/subagent-detect.ts +56 -1
- package/src/tui/index.tsx +4 -10
- package/src/tui/sidebar-view.tsx +36 -15
- package/src/tui/v2-status.ts +25 -4
- package/src/tui-compiled/index.tsx +3 -3
- package/src/tui-compiled/sidebar-view.tsx +168 -126
- package/src/tui-compiled/v2-status.ts +25 -4
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
acquireBridge,
|
|
3
|
-
findBinary,
|
|
4
|
-
releaseBridge,
|
|
5
|
-
resolveCortexKitStorageRoot,
|
|
6
|
-
} from "@cortexkit/aft-bridge";
|
|
1
|
+
import { acquireBridge, releaseBridge } from "@cortexkit/aft-bridge";
|
|
7
2
|
import { Effect } from "effect";
|
|
8
3
|
|
|
9
4
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
applyToolSurfaceOverrides,
|
|
6
|
+
createProjectAcceptance,
|
|
7
|
+
createSharedPoolOptions,
|
|
8
|
+
defaultBridgeBootstrapDependencies,
|
|
9
|
+
prepareBridgeEnvironment,
|
|
10
|
+
reportHashlineDowngrade,
|
|
11
|
+
resolveBootstrapConfig,
|
|
12
|
+
unknownDisabledToolsReporter,
|
|
13
|
+
} from "../bridge-bootstrap.js";
|
|
14
|
+
import { resolveBridgePoolTransportOptions } from "../config.js";
|
|
15
|
+
import { buildConfigErrorToolMap } from "../config-error-surface.js";
|
|
16
|
+
import { debug, log, warn } from "../logger.js";
|
|
15
17
|
import { resolvePluginVersion } from "../plugin-version.js";
|
|
16
|
-
import { registerAftRpc } from "../rpc/register.js";
|
|
18
|
+
import { registerAftConfigErrorRpc, registerAftRpc } from "../rpc/register.js";
|
|
17
19
|
import { hoistedV2ToolConsumers } from "../tools/hoisted/v2.js";
|
|
18
20
|
import { createV2RuntimeConsumer } from "../wakes/runtime-consumer.js";
|
|
19
21
|
import {
|
|
@@ -22,61 +24,134 @@ import {
|
|
|
22
24
|
registerAftTools,
|
|
23
25
|
} from "../tool-registration.js";
|
|
24
26
|
|
|
27
|
+
// The bridge environment (binary, storage migration, configure overrides, ONNX
|
|
28
|
+
// Runtime, LSP installs) comes from the bootstrap shared with the OpenCode 1
|
|
29
|
+
// entry; every one of those steps can be replaced here through overrides.
|
|
25
30
|
const defaults = {
|
|
26
|
-
|
|
31
|
+
...defaultBridgeBootstrapDependencies,
|
|
27
32
|
buildToolMap: buildAftToolDefinitions,
|
|
28
33
|
registerTools: registerAftTools,
|
|
29
34
|
registerRpc: registerAftRpc,
|
|
35
|
+
registerConfigErrorRpc: registerAftConfigErrorRpc,
|
|
30
36
|
toolConsumers: (context) => ({
|
|
31
37
|
...hoistedV2ToolConsumers(context),
|
|
32
38
|
...createV2RuntimeConsumer(context),
|
|
33
39
|
}),
|
|
34
40
|
acquireBridge,
|
|
35
|
-
loadConfig: loadAftConfig,
|
|
36
41
|
releaseBridge,
|
|
37
|
-
resolveBinary: findBinary,
|
|
38
42
|
resolvePoolOptions: resolveBridgePoolTransportOptions,
|
|
39
|
-
resolveStorageRoot: resolveCortexKitStorageRoot,
|
|
40
43
|
resolveVersion: () => resolvePluginVersion(import.meta.url),
|
|
41
44
|
};
|
|
42
45
|
|
|
43
46
|
async function bootLocation(context, location, dependencies) {
|
|
44
47
|
const directory = location.directory;
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
// The V2 host has no session UI to deliver startup warnings into, so they
|
|
49
|
+
// go to the plugin log.
|
|
50
|
+
const notify = (message) => warn(message);
|
|
51
|
+
const bootstrap = await resolveBootstrapConfig(directory, notify, dependencies);
|
|
52
|
+
if (!bootstrap.ok) {
|
|
53
|
+
// The config error state: register the tool surface with every call
|
|
54
|
+
// failing, and acquire no bridge.
|
|
55
|
+
log(`AFT is in the config error state for ${directory}; every tool call will fail`);
|
|
56
|
+
return {
|
|
57
|
+
configError: bootstrap.message,
|
|
58
|
+
consumers: {},
|
|
59
|
+
pool: null,
|
|
60
|
+
tools: buildConfigErrorToolMap(
|
|
61
|
+
bootstrap.config,
|
|
62
|
+
bootstrap.message,
|
|
63
|
+
context,
|
|
64
|
+
dependencies.buildToolMap,
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const config = bootstrap.config;
|
|
47
69
|
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
const binaryPath = await dependencies.resolveBinary(dependencies.resolveVersion());
|
|
70
|
+
const pluginVersion = dependencies.resolveVersion();
|
|
71
|
+
const environment = await prepareBridgeEnvironment(
|
|
72
|
+
{ configRoot: directory, lspDirectory: directory, config, pluginVersion, notify },
|
|
73
|
+
dependencies,
|
|
74
|
+
);
|
|
55
75
|
const canonicalDirectory = location.project?.canonical ?? directory;
|
|
56
76
|
const consumers = dependencies.toolConsumers(context);
|
|
77
|
+
// Only a rejected configuration keeps AFT out of a project; there is no
|
|
78
|
+
// config switch that turns it off.
|
|
79
|
+
const isProjectEnabled = createProjectAcceptance(directory, dependencies);
|
|
80
|
+
// getPool is only called on a version mismatch, after the pool exists.
|
|
57
81
|
const pool = await dependencies.acquireBridge(canonicalDirectory, {
|
|
58
82
|
harness: "opencode",
|
|
59
|
-
binaryPath,
|
|
83
|
+
binaryPath: environment.binaryPath,
|
|
60
84
|
poolOptions: {
|
|
61
85
|
...dependencies.resolvePoolOptions(config),
|
|
86
|
+
...createSharedPoolOptions({
|
|
87
|
+
pluginVersion,
|
|
88
|
+
getPool: () => pool,
|
|
89
|
+
isProjectEnabled,
|
|
90
|
+
notifyForRoot: (_projectRoot, message) => notify(message),
|
|
91
|
+
dependencies,
|
|
92
|
+
}),
|
|
62
93
|
...consumers.bridgeOptions,
|
|
63
94
|
},
|
|
64
|
-
configOverrides,
|
|
95
|
+
configOverrides: environment.configOverrides,
|
|
65
96
|
subcConnectionFile: config.subc?.connection_file,
|
|
66
97
|
});
|
|
98
|
+
environment.attach(pool);
|
|
67
99
|
const toolContext = {
|
|
68
100
|
pool,
|
|
69
101
|
client: context,
|
|
70
102
|
config,
|
|
71
103
|
hashlineEffective: openCodeHashlineEffective(config),
|
|
72
|
-
storageDir,
|
|
73
|
-
isProjectEnabled
|
|
74
|
-
projectRoot === directory ? true : dependencies.loadConfig(projectRoot).enabled !== false,
|
|
104
|
+
storageDir: environment.storageDir,
|
|
105
|
+
isProjectEnabled,
|
|
75
106
|
};
|
|
76
|
-
const tools = dependencies.buildToolMap(
|
|
107
|
+
const tools = dependencies.buildToolMap(
|
|
108
|
+
toolContext,
|
|
109
|
+
config,
|
|
110
|
+
unknownDisabledToolsReporter(notify),
|
|
111
|
+
);
|
|
112
|
+
const registeredTools = new Set(Object.keys(tools));
|
|
113
|
+
const { hashlineEditRegistered } = applyToolSurfaceOverrides(pool, config, registeredTools);
|
|
114
|
+
reportHashlineDowngrade(config, registeredTools, notify);
|
|
115
|
+
toolContext.hashlineEffective = hashlineEditRegistered;
|
|
77
116
|
return { consumers, pool, tools };
|
|
78
117
|
}
|
|
79
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Locations whose runtime is running in this process, by directory, so the
|
|
121
|
+
* start line can say when the host starts a second Location for a directory
|
|
122
|
+
* that already has one.
|
|
123
|
+
*/
|
|
124
|
+
const runningByDirectory = new Map();
|
|
125
|
+
let startsInProcess = 0;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The start line names the entry, the Location and how many starts this
|
|
129
|
+
* process has seen. The V2 host calls the server effect once per Location and
|
|
130
|
+
* again when it reloads one, so two start lines on one host launch are two
|
|
131
|
+
* Locations or a reload; with the Location and the matching "stopped" line in
|
|
132
|
+
* the log, a reader can tell which instead of seeing the same line twice.
|
|
133
|
+
*/
|
|
134
|
+
function logRuntimeStart(location) {
|
|
135
|
+
startsInProcess += 1;
|
|
136
|
+
const directory = location.directory;
|
|
137
|
+
const running = runningByDirectory.get(directory) ?? 0;
|
|
138
|
+
runningByDirectory.set(directory, running + 1);
|
|
139
|
+
const id = typeof location.id === "string" ? location.id : "(no id)";
|
|
140
|
+
const overlap =
|
|
141
|
+
running > 0
|
|
142
|
+
? `; ${running} other Location(s) for this directory still running`
|
|
143
|
+
: "";
|
|
144
|
+
log(
|
|
145
|
+
`AFT V2 runtime starting (server entry, Location ${id} at ${directory}; start ${startsInProcess} in this process${overlap})`,
|
|
146
|
+
);
|
|
147
|
+
return () => {
|
|
148
|
+
const left = (runningByDirectory.get(directory) ?? 1) - 1;
|
|
149
|
+
if (left > 0) runningByDirectory.set(directory, left);
|
|
150
|
+
else runningByDirectory.delete(directory);
|
|
151
|
+
log(`AFT V2 runtime stopped (server entry, Location ${id} at ${directory})`);
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
80
155
|
export function makeServerEffect(overrides = {}) {
|
|
81
156
|
const dependencies = { ...defaults, ...overrides };
|
|
82
157
|
|
|
@@ -93,12 +168,21 @@ export function makeServerEffect(overrides = {}) {
|
|
|
93
168
|
return Effect.void;
|
|
94
169
|
}
|
|
95
170
|
|
|
96
|
-
log("AFT V2 runtime starting");
|
|
97
|
-
|
|
98
171
|
return Effect.gen(function* () {
|
|
172
|
+
// Logged when the program runs, so every start pairs with the stop its
|
|
173
|
+
// finalizer logs when the host disposes the Location.
|
|
174
|
+
const logRuntimeStop = logRuntimeStart(location);
|
|
175
|
+
yield* Effect.addFinalizer(() => Effect.sync(logRuntimeStop));
|
|
99
176
|
const runtime = yield* Effect.promise(() => bootLocation(context, location, dependencies));
|
|
100
177
|
if (!runtime) return;
|
|
101
178
|
|
|
179
|
+
if (runtime.configError !== undefined) {
|
|
180
|
+
const rpc = yield* dependencies.registerConfigErrorRpc(context, runtime.configError);
|
|
181
|
+
yield* Effect.addFinalizer(() => Effect.promise(() => rpc.dispose()));
|
|
182
|
+
yield* dependencies.registerTools(context, location, runtime.tools, runtime.consumers);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
102
186
|
yield* Effect.addFinalizer(() =>
|
|
103
187
|
Effect.promise(async () => {
|
|
104
188
|
runtime.consumers.dispose?.();
|
package/src/logger.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { RotatingLogSink,
|
|
1
|
+
import { RotatingLogSink, resolvePluginLogPath } from "@cortexkit/aft-bridge";
|
|
2
2
|
|
|
3
3
|
const TAG = "[aft-plugin]";
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
// live
|
|
7
|
-
//
|
|
8
|
-
const
|
|
9
|
-
const logFile = resolveAftLogPath(isTestEnv ? "aft-plugin-test.log" : "aft-plugin.log");
|
|
5
|
+
// Test runs log to aft-plugin-test.log under the system temp directory, so
|
|
6
|
+
// `bun test` never writes into the live log directory users read to diagnose
|
|
7
|
+
// problems.
|
|
8
|
+
const logFile = resolvePluginLogPath();
|
|
10
9
|
const fileSink = new RotatingLogSink(logFile);
|
|
11
10
|
|
|
12
11
|
/**
|
package/src/shared/status.ts
CHANGED
|
@@ -12,6 +12,31 @@ import {
|
|
|
12
12
|
*/
|
|
13
13
|
export { formatSemanticIndexStatus, type SemanticIndexStatusKind, semanticIndexStatusKind };
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* What the sidebar and status dialog show before AFT has started for this
|
|
17
|
+
* project. AFT starts on the first tool call, so until then there is nothing
|
|
18
|
+
* to report beyond that fact.
|
|
19
|
+
*/
|
|
20
|
+
export const NOT_STARTED_STATUS_TEXT = "Starts on the first tool call.";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The status label for a snapshot's semantic index, including the backend URL
|
|
24
|
+
* and reason when the embedding backend is unavailable. Every surface that
|
|
25
|
+
* shows the semantic status goes through this so none of them can drop the
|
|
26
|
+
* reason and fall back to a bare word.
|
|
27
|
+
*/
|
|
28
|
+
export function formatSemanticIndexLabel(
|
|
29
|
+
semantic: Pick<
|
|
30
|
+
AftStatusSnapshot["semantic_index"],
|
|
31
|
+
"status" | "stage" | "error" | "reason" | "backend_url"
|
|
32
|
+
>,
|
|
33
|
+
): string {
|
|
34
|
+
return formatSemanticIndexStatus(semantic.status, semantic.stage, semantic.error, {
|
|
35
|
+
reason: semantic.reason,
|
|
36
|
+
backendUrl: semantic.backend_url,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
15
40
|
export interface StatusCompressionAggregate {
|
|
16
41
|
events: number;
|
|
17
42
|
original_tokens: number;
|
|
@@ -37,6 +62,12 @@ export interface StatusBar {
|
|
|
37
62
|
duplicates?: number;
|
|
38
63
|
todos?: number;
|
|
39
64
|
tier2_stale?: boolean;
|
|
65
|
+
/**
|
|
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.
|
|
69
|
+
*/
|
|
70
|
+
diagnostics?: "no_language_server";
|
|
40
71
|
}
|
|
41
72
|
|
|
42
73
|
export interface AftStatusSnapshot {
|
|
@@ -80,6 +111,10 @@ export interface AftStatusSnapshot {
|
|
|
80
111
|
entries: number | null;
|
|
81
112
|
dimension: number | null;
|
|
82
113
|
error?: string | null;
|
|
114
|
+
/** Engine's reason when `status === "backend_unavailable"`. */
|
|
115
|
+
reason?: string | null;
|
|
116
|
+
/** Configured remote embedding backend URL, when there is one. */
|
|
117
|
+
backend_url?: string | null;
|
|
83
118
|
};
|
|
84
119
|
disk: {
|
|
85
120
|
storage_dir: string | null;
|
|
@@ -125,6 +160,12 @@ export interface AftStatusSnapshot {
|
|
|
125
160
|
* Empty string when the snapshot is real bridge data.
|
|
126
161
|
*/
|
|
127
162
|
message: string;
|
|
163
|
+
/**
|
|
164
|
+
* Present only when the plugin is in the config error state: its
|
|
165
|
+
* configuration could not be used, so no bridge runs and every tool call
|
|
166
|
+
* fails with this text. `message` then carries its one-line form.
|
|
167
|
+
*/
|
|
168
|
+
config_error?: string;
|
|
128
169
|
}
|
|
129
170
|
|
|
130
171
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
@@ -179,13 +220,15 @@ function readStatusBar(value: unknown): StatusBar | undefined {
|
|
|
179
220
|
const unusedExports = readOptionalNumber(bar.unused_exports);
|
|
180
221
|
const duplicates = readOptionalNumber(bar.duplicates);
|
|
181
222
|
const todos = readOptionalNumber(bar.todos);
|
|
223
|
+
const noLanguageServer = bar.diagnostics === "no_language_server";
|
|
182
224
|
if (
|
|
183
225
|
errors === null &&
|
|
184
226
|
warnings === null &&
|
|
185
227
|
deadCode === null &&
|
|
186
228
|
unusedExports === null &&
|
|
187
229
|
duplicates === null &&
|
|
188
|
-
todos === null
|
|
230
|
+
todos === null &&
|
|
231
|
+
!noLanguageServer
|
|
189
232
|
) {
|
|
190
233
|
return undefined;
|
|
191
234
|
}
|
|
@@ -197,6 +240,7 @@ function readStatusBar(value: unknown): StatusBar | undefined {
|
|
|
197
240
|
...(duplicates !== null ? { duplicates } : {}),
|
|
198
241
|
...(todos !== null ? { todos } : {}),
|
|
199
242
|
...(bar.tier2_stale === true ? { tier2_stale: true } : {}),
|
|
243
|
+
...(noLanguageServer ? { diagnostics: "no_language_server" as const } : {}),
|
|
200
244
|
};
|
|
201
245
|
}
|
|
202
246
|
|
|
@@ -297,6 +341,8 @@ export function coerceAftStatus(response: Record<string, unknown>): AftStatusSna
|
|
|
297
341
|
entries: readOptionalNumber(semanticIndex.entries),
|
|
298
342
|
dimension: readOptionalNumber(semanticIndex.dimension),
|
|
299
343
|
error: readNullableString(semanticIndex.error),
|
|
344
|
+
reason: readNullableString(semanticIndex.reason),
|
|
345
|
+
backend_url: readNullableString(semanticIndex.backend_url),
|
|
300
346
|
},
|
|
301
347
|
disk: {
|
|
302
348
|
storage_dir: readNullableString(disk.storage_dir),
|
|
@@ -324,6 +370,9 @@ export function coerceAftStatus(response: Record<string, unknown>): AftStatusSna
|
|
|
324
370
|
status_bar: readStatusBar(response.status_bar),
|
|
325
371
|
status_bar_values: readStatusBar(response.status_bar_values),
|
|
326
372
|
message: readString(response.message, ""),
|
|
373
|
+
...(typeof response.config_error === "string" && response.config_error.length > 0
|
|
374
|
+
? { config_error: response.config_error }
|
|
375
|
+
: {}),
|
|
327
376
|
};
|
|
328
377
|
}
|
|
329
378
|
|
|
@@ -356,7 +405,7 @@ export function formatStatusDialogMessage(status: AftStatusSnapshot): string {
|
|
|
356
405
|
`- trigrams: ${formatCount(status.search_index.trigrams)}`,
|
|
357
406
|
"",
|
|
358
407
|
"Semantic index",
|
|
359
|
-
`- status: ${
|
|
408
|
+
`- status: ${formatSemanticIndexLabel(status.semantic_index)}`,
|
|
360
409
|
);
|
|
361
410
|
const refreshing = formatSemanticRefreshing(status.semantic_index.refreshing_count);
|
|
362
411
|
if (refreshing) {
|
|
@@ -431,9 +480,39 @@ export function formatStatusDialogMessage(status: AftStatusSnapshot): string {
|
|
|
431
480
|
return lines.join("\n");
|
|
432
481
|
}
|
|
433
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Opening words of the reason the engine reports when it has turned git off
|
|
485
|
+
* because Apple's developer tools are missing (MISSING_DEVELOPER_TOOLS_REASON
|
|
486
|
+
* in crates/aft/src/developer_tools.rs).
|
|
487
|
+
*/
|
|
488
|
+
const GIT_OFF_REASON_PREFIX = "git features are off";
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* How a surface should head the degraded state. A Mac without developer tools
|
|
492
|
+
* is the normal state of a fresh machine and everything except git-backed
|
|
493
|
+
* features works, so that case gets a calm warning mark naming what is off
|
|
494
|
+
* instead of a capitalised DEGRADED next to the product name, which read as
|
|
495
|
+
* broken. Any other reason (home directory as project root, too many files)
|
|
496
|
+
* switches off heavy features and keeps the stronger wording.
|
|
497
|
+
*/
|
|
498
|
+
export function degradedHeaderMarker(reasons: readonly string[]): {
|
|
499
|
+
/** Short header text: "⚠ git features off" or "DEGRADED". */
|
|
500
|
+
text: string;
|
|
501
|
+
/** Section heading for the status dialog. */
|
|
502
|
+
heading: string;
|
|
503
|
+
/** True for the git-only case, drawn as plain warning text rather than a badge. */
|
|
504
|
+
calm: boolean;
|
|
505
|
+
} {
|
|
506
|
+
if (reasons.length > 0 && reasons.every((reason) => reason.startsWith(GIT_OFF_REASON_PREFIX))) {
|
|
507
|
+
return { text: "⚠ git features off", heading: "Git features off", calm: true };
|
|
508
|
+
}
|
|
509
|
+
return { text: "DEGRADED", heading: "Degraded mode", calm: false };
|
|
510
|
+
}
|
|
511
|
+
|
|
434
512
|
function appendDegradedStatus(lines: string[], status: AftStatusSnapshot, markdown: boolean): void {
|
|
435
513
|
if (!status.degraded || status.degraded_reasons.length === 0) return;
|
|
436
|
-
|
|
514
|
+
const { heading } = degradedHeaderMarker(status.degraded_reasons);
|
|
515
|
+
lines.push("", markdown ? `### ${heading}` : heading);
|
|
437
516
|
for (const reason of status.degraded_reasons) {
|
|
438
517
|
const detail =
|
|
439
518
|
reason === "home_root"
|
|
@@ -467,7 +546,7 @@ export function formatStatusMarkdown(status: AftStatusSnapshot): string {
|
|
|
467
546
|
`- **Trigrams:** ${formatCount(status.search_index.trigrams)}`,
|
|
468
547
|
"",
|
|
469
548
|
"### Semantic index",
|
|
470
|
-
`- **Status:** \`${
|
|
549
|
+
`- **Status:** \`${formatSemanticIndexLabel(status.semantic_index)}\``,
|
|
471
550
|
);
|
|
472
551
|
const refreshing = formatSemanticRefreshing(status.semantic_index.refreshing_count);
|
|
473
552
|
if (refreshing) {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* 2. Disable auto-promotion: the foreground poll window extends to the
|
|
16
16
|
* task's full hard-kill timeout instead of the default 5s.
|
|
17
17
|
*
|
|
18
|
-
* Detection is via the OpenCode SDK `client.session.get
|
|
18
|
+
* Detection is via the OpenCode SDK `client.session.get` on OpenCode 1 and
|
|
19
|
+
* via the plugin context's `session.get` Effect on OpenCode 2. The result is
|
|
19
20
|
* cached per sessionID for the lifetime of the plugin process — subagent
|
|
20
21
|
* identity is sticky (parentID never changes after session creation), so
|
|
21
22
|
* the cache never needs invalidation.
|
|
@@ -27,6 +28,8 @@
|
|
|
27
28
|
*
|
|
28
29
|
* Mirrors the pattern in `session-directory.ts`.
|
|
29
30
|
*/
|
|
31
|
+
import { Effect } from "effect";
|
|
32
|
+
|
|
30
33
|
import { sessionLog, sessionWarn } from "../logger.js";
|
|
31
34
|
|
|
32
35
|
interface SessionInfo {
|
|
@@ -79,6 +82,8 @@ export async function resolveIsSubagent(
|
|
|
79
82
|
return cached.isSubagent;
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
if (isV2PluginContext(client)) return resolveV2(client, sessionId);
|
|
86
|
+
|
|
82
87
|
const c = client as OpenCodeClientShape;
|
|
83
88
|
const sessionApi = c?.session;
|
|
84
89
|
if (!sessionApi || typeof sessionApi.get !== "function") {
|
|
@@ -135,6 +140,56 @@ export async function resolveIsSubagent(
|
|
|
135
140
|
return isSubagent;
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The OpenCode 2 runtime passes its plugin context where the V1 plugin passes
|
|
145
|
+
* the SDK client (see `entry/server-runtime.mjs`). That context has no SDK
|
|
146
|
+
* client at all; its `session.get` takes `{ sessionID }` and returns an Effect
|
|
147
|
+
* that resolves to the bare session record. Calling it the V1 way builds an
|
|
148
|
+
* Effect that never runs, and awaiting that Effect yields the Effect itself, so
|
|
149
|
+
* no `parentID` is ever read and every OpenCode 2 session would be cached as
|
|
150
|
+
* primary. The context is recognised by its `location`, the same capability
|
|
151
|
+
* the runtime checks before booting.
|
|
152
|
+
*/
|
|
153
|
+
interface V2PluginContextShape {
|
|
154
|
+
location: { directory: string };
|
|
155
|
+
session: {
|
|
156
|
+
get(input: { sessionID: string }): Effect.Effect<SessionInfo | undefined, unknown>;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isV2PluginContext(client: unknown): client is V2PluginContextShape {
|
|
161
|
+
if (!client || typeof client !== "object") return false;
|
|
162
|
+
const candidate = client as { location?: { directory?: unknown }; session?: { get?: unknown } };
|
|
163
|
+
return (
|
|
164
|
+
typeof candidate.location?.directory === "string" &&
|
|
165
|
+
typeof candidate.session?.get === "function"
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function resolveV2(context: V2PluginContextShape, sessionId: string): Promise<boolean> {
|
|
170
|
+
let session: SessionInfo | undefined;
|
|
171
|
+
try {
|
|
172
|
+
session = await Effect.runPromise(
|
|
173
|
+
context.session.get({ sessionID: sessionId }) as Effect.Effect<SessionInfo | undefined>,
|
|
174
|
+
);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
// Same policy as the V1 path: a failed lookup is not cached, and the call
|
|
177
|
+
// is treated as primary for now so the next tool call can retry.
|
|
178
|
+
sessionWarn(
|
|
179
|
+
sessionId,
|
|
180
|
+
`[subagent-detect] OpenCode 2 session lookup failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
181
|
+
);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const isSubagent = typeof session?.parentID === "string" && session.parentID.length > 0;
|
|
185
|
+
sessionLog(
|
|
186
|
+
sessionId,
|
|
187
|
+
`[subagent-detect] OpenCode 2 session parentID=${JSON.stringify(session?.parentID)} → isSubagent=${isSubagent}`,
|
|
188
|
+
);
|
|
189
|
+
setCache(sessionId, isSubagent);
|
|
190
|
+
return isSubagent;
|
|
191
|
+
}
|
|
192
|
+
|
|
138
193
|
function setCache(sessionId: string, isSubagent: boolean): void {
|
|
139
194
|
if (cache.has(sessionId)) cache.delete(sessionId);
|
|
140
195
|
cache.set(sessionId, { isSubagent, recordedAt: Date.now() });
|
package/src/tui/index.tsx
CHANGED
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
coerceAftStatus,
|
|
12
12
|
formatBytes,
|
|
13
13
|
formatCacheRoleLabel,
|
|
14
|
-
|
|
14
|
+
formatSemanticIndexLabel,
|
|
15
15
|
formatSemanticRefreshing,
|
|
16
|
+
NOT_STARTED_STATUS_TEXT,
|
|
16
17
|
} from "../shared/status";
|
|
17
18
|
import {
|
|
18
19
|
createDebouncedStatusRefresh,
|
|
@@ -265,10 +266,7 @@ const StatusDialog = (props: StatusDialogProps) => {
|
|
|
265
266
|
instead of an empty grid of zeros and "unknown" rows. */}
|
|
266
267
|
{status()?.cache_role === "not_initialized" ? (
|
|
267
268
|
<box width="100%" marginTop={1} justifyContent="center">
|
|
268
|
-
<text fg={t().textMuted}>
|
|
269
|
-
{status()!.message ||
|
|
270
|
-
"AFT bridge is now spawned lazily, information here will be populated after first tool call."}
|
|
271
|
-
</text>
|
|
269
|
+
<text fg={t().textMuted}>{status()!.message || NOT_STARTED_STATUS_TEXT}</text>
|
|
272
270
|
</box>
|
|
273
271
|
) : null}
|
|
274
272
|
|
|
@@ -380,11 +378,7 @@ const StatusDialog = (props: StatusDialogProps) => {
|
|
|
380
378
|
<R
|
|
381
379
|
theme={t()}
|
|
382
380
|
label="Status"
|
|
383
|
-
value={
|
|
384
|
-
status()!.semantic_index.status,
|
|
385
|
-
status()!.semantic_index.stage,
|
|
386
|
-
status()!.semantic_index.error,
|
|
387
|
-
)}
|
|
381
|
+
value={formatSemanticIndexLabel(status()!.semantic_index)}
|
|
388
382
|
tone={statusTone(status()!.semantic_index.status)}
|
|
389
383
|
/>
|
|
390
384
|
{formatSemanticRefreshing(status()!.semantic_index.refreshing_count) ? (
|
package/src/tui/sidebar-view.tsx
CHANGED
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
import { createMemo, createSignal, onCleanup } from "solid-js";
|
|
15
15
|
import {
|
|
16
16
|
type AftStatusSnapshot,
|
|
17
|
-
|
|
17
|
+
degradedHeaderMarker,
|
|
18
|
+
formatSemanticIndexLabel,
|
|
18
19
|
formatSemanticRefreshing,
|
|
20
|
+
NOT_STARTED_STATUS_TEXT,
|
|
19
21
|
type StatusBar,
|
|
20
22
|
type StatusCompression,
|
|
21
23
|
semanticIndexStatusKind,
|
|
@@ -215,10 +217,16 @@ const StatRow = (props: {
|
|
|
215
217
|
}
|
|
216
218
|
});
|
|
217
219
|
|
|
220
|
+
// The label never shrinks and the value wraps in the space left beside it:
|
|
221
|
+
// left to flexbox, a long value (a semantic index that is unavailable, with
|
|
222
|
+
// its reason) squeezed the label and drew over it, so "Status" came out as
|
|
223
|
+
// "Statu" followed by the value.
|
|
218
224
|
return (
|
|
219
225
|
<box width="100%" flexDirection="row" justifyContent="space-between">
|
|
220
|
-
<text fg={props.palette.textMuted}
|
|
221
|
-
|
|
226
|
+
<text fg={props.palette.textMuted} flexShrink={0}>
|
|
227
|
+
{props.label}
|
|
228
|
+
</text>
|
|
229
|
+
<text fg={fg()} flexShrink={1} marginLeft={1} wrapMode="word">
|
|
222
230
|
<b>{props.value}</b>
|
|
223
231
|
</text>
|
|
224
232
|
</box>
|
|
@@ -397,12 +405,15 @@ export const AftSidebarPanel = (props: AftSidebarPanelProps) => {
|
|
|
397
405
|
return {
|
|
398
406
|
...display,
|
|
399
407
|
// The error is passed through so a capability failure (a missing ONNX
|
|
400
|
-
// Runtime) is named here instead of arriving as a bare status word
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
s()?.semantic_index?.
|
|
405
|
-
|
|
408
|
+
// Runtime) is named here instead of arriving as a bare status word, and
|
|
409
|
+
// an unreachable backend carries its URL and reason.
|
|
410
|
+
label: formatSemanticIndexLabel({
|
|
411
|
+
status: rawStatus,
|
|
412
|
+
stage: s()?.semantic_index?.stage,
|
|
413
|
+
error: s()?.semantic_index?.error,
|
|
414
|
+
reason: s()?.semantic_index?.reason,
|
|
415
|
+
backend_url: s()?.semantic_index?.backend_url,
|
|
416
|
+
}),
|
|
406
417
|
};
|
|
407
418
|
};
|
|
408
419
|
const semanticRefreshing = () =>
|
|
@@ -459,7 +470,14 @@ export const AftSidebarPanel = (props: AftSidebarPanelProps) => {
|
|
|
459
470
|
</b>
|
|
460
471
|
</text>
|
|
461
472
|
</box>
|
|
462
|
-
{s()?.degraded && (
|
|
473
|
+
{s()?.degraded && degradedHeaderMarker(s()?.degraded_reasons ?? []).calm && (
|
|
474
|
+
<box marginLeft={1}>
|
|
475
|
+
<text fg={props.palette.warning}>
|
|
476
|
+
{degradedHeaderMarker(s()?.degraded_reasons ?? []).text}
|
|
477
|
+
</text>
|
|
478
|
+
</box>
|
|
479
|
+
)}
|
|
480
|
+
{s()?.degraded && !degradedHeaderMarker(s()?.degraded_reasons ?? []).calm && (
|
|
463
481
|
<box
|
|
464
482
|
paddingLeft={1}
|
|
465
483
|
paddingRight={1}
|
|
@@ -467,7 +485,7 @@ export const AftSidebarPanel = (props: AftSidebarPanelProps) => {
|
|
|
467
485
|
backgroundColor={props.palette.warning}
|
|
468
486
|
>
|
|
469
487
|
<text fg={badgeForeground(props.palette.warning, props.palette.background)}>
|
|
470
|
-
<b>
|
|
488
|
+
<b>{degradedHeaderMarker(s()?.degraded_reasons ?? []).text}</b>
|
|
471
489
|
</text>
|
|
472
490
|
</box>
|
|
473
491
|
)}
|
|
@@ -501,10 +519,13 @@ export const AftSidebarPanel = (props: AftSidebarPanelProps) => {
|
|
|
501
519
|
rows so users understand why metrics are blank. */}
|
|
502
520
|
{notInitialized() && (
|
|
503
521
|
<box marginTop={1} width="100%">
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
522
|
+
{s()!.config_error ? (
|
|
523
|
+
// Config error state: no bridge will ever start in this process,
|
|
524
|
+
// so the placeholder text would mislead; show the error instead.
|
|
525
|
+
<text fg={props.palette.error}>✖ {s()!.message}</text>
|
|
526
|
+
) : (
|
|
527
|
+
<text fg={props.palette.textMuted}>{s()!.message || NOT_STARTED_STATUS_TEXT}</text>
|
|
528
|
+
)}
|
|
508
529
|
</box>
|
|
509
530
|
)}
|
|
510
531
|
|
package/src/tui/v2-status.ts
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AftStatusSnapshot, degradedHeaderMarker, type StatusBar } from "../shared/status";
|
|
2
2
|
|
|
3
3
|
function count(value: number | undefined): string {
|
|
4
4
|
return value === undefined ? "?" : String(value);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The errors/warnings part of the footer. With no language server running the
|
|
9
|
+
* counts will never arrive, so the footer says that instead of showing `E? W?`
|
|
10
|
+
* indefinitely.
|
|
11
|
+
*/
|
|
12
|
+
function diagnosticsPart(bar: StatusBar): string {
|
|
13
|
+
if (
|
|
14
|
+
bar.errors === undefined &&
|
|
15
|
+
bar.warnings === undefined &&
|
|
16
|
+
bar.diagnostics === "no_language_server"
|
|
17
|
+
) {
|
|
18
|
+
return "no LSP";
|
|
19
|
+
}
|
|
20
|
+
return `E${count(bar.errors)} W${count(bar.warnings)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
/**
|
|
8
24
|
* The V2 footer segment. "AFT starting…" is only true before the bridge has
|
|
9
25
|
* delivered a real snapshot. After that the footer shows every health category
|
|
@@ -12,11 +28,14 @@ function count(value: number | undefined): string {
|
|
|
12
28
|
* can leave the complete `status_bar` missing for the whole session.
|
|
13
29
|
*/
|
|
14
30
|
export function formatAftStatusSegment(status: AftStatusSnapshot | null): string {
|
|
31
|
+
// The config error state never produces a real snapshot; say so instead of
|
|
32
|
+
// "starting", which would suggest the bridge is on its way.
|
|
33
|
+
if (status?.config_error) return "AFT config error";
|
|
15
34
|
if (!status || status.cache_role === "not_initialized") return "AFT starting…";
|
|
16
35
|
const bar: StatusBar = status.status_bar ?? status.status_bar_values ?? {};
|
|
17
36
|
const stale = bar.tier2_stale ? "~" : "";
|
|
18
37
|
return (
|
|
19
|
-
`AFT
|
|
38
|
+
`AFT ${diagnosticsPart(bar)} | ` +
|
|
20
39
|
`${stale}D${count(bar.dead_code)} U${count(bar.unused_exports)} ` +
|
|
21
40
|
`C${count(bar.duplicates)} | T${count(bar.todos)}`
|
|
22
41
|
);
|
|
@@ -33,7 +52,7 @@ function healthSummary(bar: StatusBar | undefined): string {
|
|
|
33
52
|
if (!bar) return "health pending";
|
|
34
53
|
const stale = bar.tier2_stale ? "~" : "";
|
|
35
54
|
return (
|
|
36
|
-
|
|
55
|
+
`${diagnosticsPart(bar)} ` +
|
|
37
56
|
`${stale}D${count(bar.dead_code)} U${count(bar.unused_exports)} ` +
|
|
38
57
|
`C${count(bar.duplicates)} T${count(bar.todos)}`
|
|
39
58
|
);
|
|
@@ -61,7 +80,9 @@ export function summarizeAftSidebar(status: AftStatusSnapshot | null): AftSideba
|
|
|
61
80
|
? `${status.semantic_index.status} (${status.semantic_index.stage})`
|
|
62
81
|
: status.semantic_index.status;
|
|
63
82
|
return {
|
|
64
|
-
title: status.degraded
|
|
83
|
+
title: status.degraded
|
|
84
|
+
? `AFT · ${degradedHeaderMarker(status.degraded_reasons ?? []).text}`
|
|
85
|
+
: "AFT",
|
|
65
86
|
version: status.version || undefined,
|
|
66
87
|
search: status.search_index.status,
|
|
67
88
|
semantic,
|