@cortexkit/aft-opencode 0.57.1 → 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 +9984 -7887
- 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 +9802 -7770
- package/dist/logger.d.ts +6 -0
- 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 +53 -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/permissions.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 +8 -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 +18 -6
- package/src/shared/status.ts +92 -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 +34 -6
- package/src/tui-compiled/index.tsx +3 -3
- package/src/tui-compiled/sidebar-view.tsx +168 -126
- package/src/tui-compiled/v2-status.ts +34 -6
|
@@ -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
|
/**
|
|
@@ -110,6 +109,19 @@ export function getLogFilePath(): string {
|
|
|
110
109
|
return logFile;
|
|
111
110
|
}
|
|
112
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Write buffered log lines now instead of on the next flush tick. For lines
|
|
114
|
+
* logged while the host may be about to exit, which would drop anything still
|
|
115
|
+
* buffered.
|
|
116
|
+
*/
|
|
117
|
+
export function flushLog(): void {
|
|
118
|
+
if (flushTimer) {
|
|
119
|
+
clearTimeout(flushTimer);
|
|
120
|
+
flushTimer = null;
|
|
121
|
+
}
|
|
122
|
+
flush();
|
|
123
|
+
}
|
|
124
|
+
|
|
113
125
|
/**
|
|
114
126
|
* Adapter that exposes this logger as a {@link import("@cortexkit/aft-bridge").Logger}
|
|
115
127
|
* for the shared bridge package. The bridge package never knows about this log
|
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;
|
|
@@ -109,6 +144,14 @@ export interface AftStatusSnapshot {
|
|
|
109
144
|
compression?: StatusCompression;
|
|
110
145
|
/** Human health counts. Each unproven category stays absent rather than zero. */
|
|
111
146
|
status_bar?: StatusBar;
|
|
147
|
+
/**
|
|
148
|
+
* Per-category health values from the same snapshot. The bridge sends
|
|
149
|
+
* `status_bar` only once every category has a value; this field carries the
|
|
150
|
+
* categories that do, so a renderer can show them while one producer (for
|
|
151
|
+
* example dead-code without a callgraph) is unavailable. Absent when the
|
|
152
|
+
* bridge predates the field or no category has a value yet.
|
|
153
|
+
*/
|
|
154
|
+
status_bar_values?: StatusBar;
|
|
112
155
|
/**
|
|
113
156
|
* Human-readable explanation for a synthetic snapshot (e.g.
|
|
114
157
|
* `cache_role === "not_initialized"`). When the plugin returns a placeholder
|
|
@@ -117,6 +160,12 @@ export interface AftStatusSnapshot {
|
|
|
117
160
|
* Empty string when the snapshot is real bridge data.
|
|
118
161
|
*/
|
|
119
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;
|
|
120
169
|
}
|
|
121
170
|
|
|
122
171
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
@@ -171,13 +220,15 @@ function readStatusBar(value: unknown): StatusBar | undefined {
|
|
|
171
220
|
const unusedExports = readOptionalNumber(bar.unused_exports);
|
|
172
221
|
const duplicates = readOptionalNumber(bar.duplicates);
|
|
173
222
|
const todos = readOptionalNumber(bar.todos);
|
|
223
|
+
const noLanguageServer = bar.diagnostics === "no_language_server";
|
|
174
224
|
if (
|
|
175
225
|
errors === null &&
|
|
176
226
|
warnings === null &&
|
|
177
227
|
deadCode === null &&
|
|
178
228
|
unusedExports === null &&
|
|
179
229
|
duplicates === null &&
|
|
180
|
-
todos === null
|
|
230
|
+
todos === null &&
|
|
231
|
+
!noLanguageServer
|
|
181
232
|
) {
|
|
182
233
|
return undefined;
|
|
183
234
|
}
|
|
@@ -189,6 +240,7 @@ function readStatusBar(value: unknown): StatusBar | undefined {
|
|
|
189
240
|
...(duplicates !== null ? { duplicates } : {}),
|
|
190
241
|
...(todos !== null ? { todos } : {}),
|
|
191
242
|
...(bar.tier2_stale === true ? { tier2_stale: true } : {}),
|
|
243
|
+
...(noLanguageServer ? { diagnostics: "no_language_server" as const } : {}),
|
|
192
244
|
};
|
|
193
245
|
}
|
|
194
246
|
|
|
@@ -289,6 +341,8 @@ export function coerceAftStatus(response: Record<string, unknown>): AftStatusSna
|
|
|
289
341
|
entries: readOptionalNumber(semanticIndex.entries),
|
|
290
342
|
dimension: readOptionalNumber(semanticIndex.dimension),
|
|
291
343
|
error: readNullableString(semanticIndex.error),
|
|
344
|
+
reason: readNullableString(semanticIndex.reason),
|
|
345
|
+
backend_url: readNullableString(semanticIndex.backend_url),
|
|
292
346
|
},
|
|
293
347
|
disk: {
|
|
294
348
|
storage_dir: readNullableString(disk.storage_dir),
|
|
@@ -314,7 +368,11 @@ export function coerceAftStatus(response: Record<string, unknown>): AftStatusSna
|
|
|
314
368
|
},
|
|
315
369
|
compression: readCompression(response.compression),
|
|
316
370
|
status_bar: readStatusBar(response.status_bar),
|
|
371
|
+
status_bar_values: readStatusBar(response.status_bar_values),
|
|
317
372
|
message: readString(response.message, ""),
|
|
373
|
+
...(typeof response.config_error === "string" && response.config_error.length > 0
|
|
374
|
+
? { config_error: response.config_error }
|
|
375
|
+
: {}),
|
|
318
376
|
};
|
|
319
377
|
}
|
|
320
378
|
|
|
@@ -347,7 +405,7 @@ export function formatStatusDialogMessage(status: AftStatusSnapshot): string {
|
|
|
347
405
|
`- trigrams: ${formatCount(status.search_index.trigrams)}`,
|
|
348
406
|
"",
|
|
349
407
|
"Semantic index",
|
|
350
|
-
`- status: ${
|
|
408
|
+
`- status: ${formatSemanticIndexLabel(status.semantic_index)}`,
|
|
351
409
|
);
|
|
352
410
|
const refreshing = formatSemanticRefreshing(status.semantic_index.refreshing_count);
|
|
353
411
|
if (refreshing) {
|
|
@@ -422,9 +480,39 @@ export function formatStatusDialogMessage(status: AftStatusSnapshot): string {
|
|
|
422
480
|
return lines.join("\n");
|
|
423
481
|
}
|
|
424
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
|
+
|
|
425
512
|
function appendDegradedStatus(lines: string[], status: AftStatusSnapshot, markdown: boolean): void {
|
|
426
513
|
if (!status.degraded || status.degraded_reasons.length === 0) return;
|
|
427
|
-
|
|
514
|
+
const { heading } = degradedHeaderMarker(status.degraded_reasons);
|
|
515
|
+
lines.push("", markdown ? `### ${heading}` : heading);
|
|
428
516
|
for (const reason of status.degraded_reasons) {
|
|
429
517
|
const detail =
|
|
430
518
|
reason === "home_root"
|
|
@@ -458,7 +546,7 @@ export function formatStatusMarkdown(status: AftStatusSnapshot): string {
|
|
|
458
546
|
`- **Trigrams:** ${formatCount(status.search_index.trigrams)}`,
|
|
459
547
|
"",
|
|
460
548
|
"### Semantic index",
|
|
461
|
-
`- **Status:** \`${
|
|
549
|
+
`- **Status:** \`${formatSemanticIndexLabel(status.semantic_index)}\``,
|
|
462
550
|
);
|
|
463
551
|
const refreshing = formatSemanticRefreshing(status.semantic_index.refreshing_count);
|
|
464
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
|
|