@mirrowel/opencode-subagent-explorer 0.2.0-dev.3 → 0.2.0-dev.5
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/sessions.d.ts +19 -1
- package/dist/sessions.js +51 -6
- package/dist/tui.js +42 -8
- package/dist/wizard.d.ts +3 -1
- package/dist/wizard.js +42 -8
- package/package.json +1 -1
- package/src/sessions.ts +57 -9
- package/src/wizard.tsx +10 -1
package/dist/sessions.d.ts
CHANGED
|
@@ -40,13 +40,31 @@ export declare function fetchSessions(client: unknown, directory: string): Promi
|
|
|
40
40
|
*/
|
|
41
41
|
export declare function fetchActiveSessionIDs(client: unknown): Promise<Set<string> | undefined>;
|
|
42
42
|
/** Minimal agent-variants sidecar view: which parents exist and which have
|
|
43
|
-
* the base disabled
|
|
43
|
+
* the base disabled, plus the full alias set (variant name -> parent) used
|
|
44
|
+
* to validate parsed annotations. Absent file / parse errors -> empty
|
|
45
|
+
* (fail-soft). */
|
|
44
46
|
export type AvSidecar = {
|
|
45
47
|
parents: Map<string, {
|
|
46
48
|
disableBase: boolean;
|
|
47
49
|
}>;
|
|
50
|
+
aliases: Map<string, string>;
|
|
48
51
|
};
|
|
49
52
|
export declare function loadAvSidecar(globalConfigDir: string | undefined): AvSidecar;
|
|
53
|
+
/** Reads the last VALID variant annotation from a text. Native suffixes are
|
|
54
|
+
* never evidence. */
|
|
55
|
+
export declare function parseVariantAnnotation(text: string | undefined): {
|
|
56
|
+
alias: string;
|
|
57
|
+
switchedFrom?: string;
|
|
58
|
+
} | undefined;
|
|
59
|
+
/** Title-annotation fallback for children beyond the attribution walk's
|
|
60
|
+
* depth: AV writes the annotation into the child's session title at call
|
|
61
|
+
* time and it survives forever, so it attributes what the windowed walk
|
|
62
|
+
* cannot reach. Validated against the sidecar's alias set - an unknown
|
|
63
|
+
* token never attributes. */
|
|
64
|
+
export declare function variantFromTitle(title: string | undefined, sidecar: AvSidecar): {
|
|
65
|
+
alias: string;
|
|
66
|
+
parent: string;
|
|
67
|
+
} | undefined;
|
|
50
68
|
/** child session id -> the AV variant that created it (from the parent's
|
|
51
69
|
* task-part metadata). Paged backward through the parent's history via the
|
|
52
70
|
* `before` message cursor (up to ATTRIBUTION_PAGES pages), so large trees
|
package/dist/sessions.js
CHANGED
|
@@ -121,24 +121,68 @@ export async function fetchActiveSessionIDs(client) {
|
|
|
121
121
|
}
|
|
122
122
|
export function loadAvSidecar(globalConfigDir) {
|
|
123
123
|
const parents = new Map();
|
|
124
|
+
const aliases = new Map();
|
|
124
125
|
if (!globalConfigDir)
|
|
125
|
-
return { parents };
|
|
126
|
+
return { parents, aliases };
|
|
126
127
|
try {
|
|
127
128
|
const path = `${globalConfigDir.replace(/[\\/]+$/, "")}/agent-variants.jsonc`;
|
|
128
129
|
if (!existsSync(path))
|
|
129
|
-
return { parents };
|
|
130
|
+
return { parents, aliases };
|
|
130
131
|
const raw = readFileSync(path, "utf8");
|
|
131
132
|
const json = JSON.parse(stripJsoncComments(raw));
|
|
132
133
|
for (const [parent, entry] of Object.entries(json.agents ?? {})) {
|
|
133
134
|
if (!entry || typeof entry !== "object")
|
|
134
135
|
continue;
|
|
135
136
|
parents.set(parent, { disableBase: entry.disable_base === true && entry.disable !== true });
|
|
137
|
+
// Alias = the variant's custom name when set, else parent-key (mirrors
|
|
138
|
+
// AV's variantName) - the validation set for annotation parsing.
|
|
139
|
+
for (const [key, variant] of Object.entries(entry.variants ?? {})) {
|
|
140
|
+
if (!variant || typeof variant !== "object" || variant.disable === true)
|
|
141
|
+
continue;
|
|
142
|
+
const alias = typeof variant.name === "string" && variant.name !== "" ? variant.name : `${parent}-${key}`;
|
|
143
|
+
aliases.set(alias, parent);
|
|
144
|
+
}
|
|
136
145
|
}
|
|
137
146
|
}
|
|
138
147
|
catch {
|
|
139
148
|
// Not our file to complain about - AV may simply not be installed.
|
|
140
149
|
}
|
|
141
|
-
return { parents };
|
|
150
|
+
return { parents, aliases };
|
|
151
|
+
}
|
|
152
|
+
/** One annotation token in any historical form: legacy "(@x variant)",
|
|
153
|
+
* terse "(@x)", switch "(@x ← y)". Mirrors AV's parseVariantAnnotation. */
|
|
154
|
+
const ANNOTATION_TOKEN = /\(@\s*([^()@←]*?)\s*(?:\s+variant)?\s*(?:\s*←\s*([^()@←]*?))?\s*\)/g;
|
|
155
|
+
/** OpenCode's own task-title suffix " (@agent subagent)" (task.ts appends it
|
|
156
|
+
* on agent-switch resumes, always AFTER our note) matches the terse form
|
|
157
|
+
* grammatically - excluded explicitly or it would override the true alias
|
|
158
|
+
* under last-wins. */
|
|
159
|
+
const NATIVE_TITLE_SUFFIX = /^[^\s]+ subagent$/;
|
|
160
|
+
/** Reads the last VALID variant annotation from a text. Native suffixes are
|
|
161
|
+
* never evidence. */
|
|
162
|
+
export function parseVariantAnnotation(text) {
|
|
163
|
+
if (typeof text !== "string")
|
|
164
|
+
return undefined;
|
|
165
|
+
let last;
|
|
166
|
+
for (const match of text.matchAll(ANNOTATION_TOKEN)) {
|
|
167
|
+
const alias = (match[1] ?? "").trim();
|
|
168
|
+
if (alias === "" || NATIVE_TITLE_SUFFIX.test(alias))
|
|
169
|
+
continue;
|
|
170
|
+
const switchedFrom = (match[2] ?? "").trim();
|
|
171
|
+
last = switchedFrom === "" ? { alias } : { alias, switchedFrom };
|
|
172
|
+
}
|
|
173
|
+
return last;
|
|
174
|
+
}
|
|
175
|
+
/** Title-annotation fallback for children beyond the attribution walk's
|
|
176
|
+
* depth: AV writes the annotation into the child's session title at call
|
|
177
|
+
* time and it survives forever, so it attributes what the windowed walk
|
|
178
|
+
* cannot reach. Validated against the sidecar's alias set - an unknown
|
|
179
|
+
* token never attributes. */
|
|
180
|
+
export function variantFromTitle(title, sidecar) {
|
|
181
|
+
const alias = parseVariantAnnotation(title)?.alias;
|
|
182
|
+
if (alias === undefined || sidecar.aliases.size === 0)
|
|
183
|
+
return undefined;
|
|
184
|
+
const parent = sidecar.aliases.get(alias);
|
|
185
|
+
return parent !== undefined ? { alias, parent } : undefined;
|
|
142
186
|
}
|
|
143
187
|
function stripJsoncComments(text) {
|
|
144
188
|
// Block comments, line comments, and trailing commas before } or ].
|
|
@@ -191,10 +235,11 @@ export async function fetchVariantAttribution(client, parentSessionID, directory
|
|
|
191
235
|
// Two alias sources: the after-hook metadata stamp (terminal
|
|
192
236
|
// completed parts) and the before-hook description annotation -
|
|
193
237
|
// aborted/error parts never receive the metadata stamp, but the
|
|
194
|
-
// annotation
|
|
238
|
+
// annotation persists. The shared parser excludes OpenCode's native
|
|
239
|
+
// "(@agent subagent)" suffix, which would otherwise win last-wins.
|
|
195
240
|
const description = part.state?.input?.description;
|
|
196
|
-
const
|
|
197
|
-
const alias = (typeof variantMeta?.alias === "string" ? variantMeta.alias : undefined) ??
|
|
241
|
+
const descriptionAlias = parseVariantAnnotation(typeof description === "string" ? description : undefined)?.alias;
|
|
242
|
+
const alias = (typeof variantMeta?.alias === "string" ? variantMeta.alias : undefined) ?? (descriptionAlias !== "" ? descriptionAlias : undefined);
|
|
198
243
|
const routedAgent = variantMeta?.routedAgent;
|
|
199
244
|
if (typeof child === "string" && typeof alias === "string" && !map.has(child)) {
|
|
200
245
|
map.set(child, {
|
package/dist/tui.js
CHANGED
|
@@ -152,21 +152,50 @@ async function fetchActiveSessionIDs(client) {
|
|
|
152
152
|
}
|
|
153
153
|
function loadAvSidecar(globalConfigDir) {
|
|
154
154
|
const parents = new Map;
|
|
155
|
+
const aliases = new Map;
|
|
155
156
|
if (!globalConfigDir)
|
|
156
|
-
return { parents };
|
|
157
|
+
return { parents, aliases };
|
|
157
158
|
try {
|
|
158
159
|
const path = `${globalConfigDir.replace(/[\\/]+$/, "")}/agent-variants.jsonc`;
|
|
159
160
|
if (!existsSync(path))
|
|
160
|
-
return { parents };
|
|
161
|
+
return { parents, aliases };
|
|
161
162
|
const raw = readFileSync(path, "utf8");
|
|
162
163
|
const json = JSON.parse(stripJsoncComments(raw));
|
|
163
164
|
for (const [parent, entry] of Object.entries(json.agents ?? {})) {
|
|
164
165
|
if (!entry || typeof entry !== "object")
|
|
165
166
|
continue;
|
|
166
167
|
parents.set(parent, { disableBase: entry.disable_base === true && entry.disable !== true });
|
|
168
|
+
for (const [key, variant] of Object.entries(entry.variants ?? {})) {
|
|
169
|
+
if (!variant || typeof variant !== "object" || variant.disable === true)
|
|
170
|
+
continue;
|
|
171
|
+
const alias = typeof variant.name === "string" && variant.name !== "" ? variant.name : `${parent}-${key}`;
|
|
172
|
+
aliases.set(alias, parent);
|
|
173
|
+
}
|
|
167
174
|
}
|
|
168
175
|
} catch {}
|
|
169
|
-
return { parents };
|
|
176
|
+
return { parents, aliases };
|
|
177
|
+
}
|
|
178
|
+
var ANNOTATION_TOKEN = /\(@\s*([^()@\u2190]*?)\s*(?:\s+variant)?\s*(?:\s*\u2190\s*([^()@\u2190]*?))?\s*\)/g;
|
|
179
|
+
var NATIVE_TITLE_SUFFIX = /^[^\s]+ subagent$/;
|
|
180
|
+
function parseVariantAnnotation(text) {
|
|
181
|
+
if (typeof text !== "string")
|
|
182
|
+
return;
|
|
183
|
+
let last;
|
|
184
|
+
for (const match of text.matchAll(ANNOTATION_TOKEN)) {
|
|
185
|
+
const alias = (match[1] ?? "").trim();
|
|
186
|
+
if (alias === "" || NATIVE_TITLE_SUFFIX.test(alias))
|
|
187
|
+
continue;
|
|
188
|
+
const switchedFrom = (match[2] ?? "").trim();
|
|
189
|
+
last = switchedFrom === "" ? { alias } : { alias, switchedFrom };
|
|
190
|
+
}
|
|
191
|
+
return last;
|
|
192
|
+
}
|
|
193
|
+
function variantFromTitle(title, sidecar) {
|
|
194
|
+
const alias = parseVariantAnnotation(title)?.alias;
|
|
195
|
+
if (alias === undefined || sidecar.aliases.size === 0)
|
|
196
|
+
return;
|
|
197
|
+
const parent = sidecar.aliases.get(alias);
|
|
198
|
+
return parent !== undefined ? { alias, parent } : undefined;
|
|
170
199
|
}
|
|
171
200
|
function stripJsoncComments(text) {
|
|
172
201
|
return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'\\])\/\/.*$/gm, "$1").replace(/,(\s*[}\]])/g, "$1");
|
|
@@ -212,8 +241,8 @@ async function fetchVariantAttribution(client, parentSessionID, directory, avPar
|
|
|
212
241
|
const child = metadata?.sessionId;
|
|
213
242
|
const variantMeta = metadata?.agentVariants;
|
|
214
243
|
const description = part.state?.input?.description;
|
|
215
|
-
const
|
|
216
|
-
const alias = (typeof variantMeta?.alias === "string" ? variantMeta.alias : undefined) ??
|
|
244
|
+
const descriptionAlias = parseVariantAnnotation(typeof description === "string" ? description : undefined)?.alias;
|
|
245
|
+
const alias = (typeof variantMeta?.alias === "string" ? variantMeta.alias : undefined) ?? (descriptionAlias !== "" ? descriptionAlias : undefined);
|
|
217
246
|
const routedAgent = variantMeta?.routedAgent;
|
|
218
247
|
if (typeof child === "string" && typeof alias === "string" && !map.has(child)) {
|
|
219
248
|
map.set(child, {
|
|
@@ -428,14 +457,15 @@ async function loadExplorerData(api, directory, rootOverride) {
|
|
|
428
457
|
hidden: hiddenAgentIDs(api.state.config),
|
|
429
458
|
configAgents: configAgentIDs(api.state.config),
|
|
430
459
|
variantOf,
|
|
431
|
-
avParents: avSidecar.parents
|
|
460
|
+
avParents: avSidecar.parents,
|
|
461
|
+
avSidecar
|
|
432
462
|
};
|
|
433
463
|
}
|
|
434
464
|
function explorerRows(tree, data) {
|
|
435
465
|
const rows = [];
|
|
436
466
|
const walk = (nodes, depth) => {
|
|
437
467
|
for (const node of nodes) {
|
|
438
|
-
const variant = data.variantOf.get(node.session.id);
|
|
468
|
+
const variant = data.variantOf.get(node.session.id) ?? variantFromTitle(node.session.title, data.avSidecar);
|
|
439
469
|
const agent = node.session.agent;
|
|
440
470
|
rows.push({
|
|
441
471
|
session: node.session,
|
|
@@ -1266,7 +1296,11 @@ async function remainingIdleCount(api, directory, rootID) {
|
|
|
1266
1296
|
hidden: new Set,
|
|
1267
1297
|
configAgents: new Set,
|
|
1268
1298
|
variantOf: new Map,
|
|
1269
|
-
avParents: new Map
|
|
1299
|
+
avParents: new Map,
|
|
1300
|
+
avSidecar: {
|
|
1301
|
+
parents: new Map,
|
|
1302
|
+
aliases: new Map
|
|
1303
|
+
}
|
|
1270
1304
|
});
|
|
1271
1305
|
return rows.filter((row) => !row.running).length;
|
|
1272
1306
|
}
|
package/dist/wizard.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* import mainMenu from here and drive it with their own TuiHostApi instance.
|
|
11
11
|
*/
|
|
12
12
|
import type { TuiHostApi as TuiPluginApi } from "./tui-host.js";
|
|
13
|
-
import { type SessionLite, type SessionTreeNode, type VariantAttribution } from "./sessions.js";
|
|
13
|
+
import { type AvSidecar, type SessionLite, type SessionTreeNode, type VariantAttribution } from "./sessions.js";
|
|
14
14
|
export type ExplorerRow = {
|
|
15
15
|
session: SessionLite;
|
|
16
16
|
depth: number;
|
|
@@ -33,6 +33,8 @@ export type ExplorerData = {
|
|
|
33
33
|
avParents: Map<string, {
|
|
34
34
|
disableBase: boolean;
|
|
35
35
|
}>;
|
|
36
|
+
/** The full sidecar view - the alias set validates title annotations. */
|
|
37
|
+
avSidecar: AvSidecar;
|
|
36
38
|
};
|
|
37
39
|
export type TreeAction = {
|
|
38
40
|
type: "delete";
|
package/dist/wizard.js
CHANGED
|
@@ -99,21 +99,50 @@ async function fetchActiveSessionIDs(client) {
|
|
|
99
99
|
}
|
|
100
100
|
function loadAvSidecar(globalConfigDir) {
|
|
101
101
|
const parents = new Map;
|
|
102
|
+
const aliases = new Map;
|
|
102
103
|
if (!globalConfigDir)
|
|
103
|
-
return { parents };
|
|
104
|
+
return { parents, aliases };
|
|
104
105
|
try {
|
|
105
106
|
const path = `${globalConfigDir.replace(/[\\/]+$/, "")}/agent-variants.jsonc`;
|
|
106
107
|
if (!existsSync(path))
|
|
107
|
-
return { parents };
|
|
108
|
+
return { parents, aliases };
|
|
108
109
|
const raw = readFileSync(path, "utf8");
|
|
109
110
|
const json = JSON.parse(stripJsoncComments(raw));
|
|
110
111
|
for (const [parent, entry] of Object.entries(json.agents ?? {})) {
|
|
111
112
|
if (!entry || typeof entry !== "object")
|
|
112
113
|
continue;
|
|
113
114
|
parents.set(parent, { disableBase: entry.disable_base === true && entry.disable !== true });
|
|
115
|
+
for (const [key, variant] of Object.entries(entry.variants ?? {})) {
|
|
116
|
+
if (!variant || typeof variant !== "object" || variant.disable === true)
|
|
117
|
+
continue;
|
|
118
|
+
const alias = typeof variant.name === "string" && variant.name !== "" ? variant.name : `${parent}-${key}`;
|
|
119
|
+
aliases.set(alias, parent);
|
|
120
|
+
}
|
|
114
121
|
}
|
|
115
122
|
} catch {}
|
|
116
|
-
return { parents };
|
|
123
|
+
return { parents, aliases };
|
|
124
|
+
}
|
|
125
|
+
var ANNOTATION_TOKEN = /\(@\s*([^()@\u2190]*?)\s*(?:\s+variant)?\s*(?:\s*\u2190\s*([^()@\u2190]*?))?\s*\)/g;
|
|
126
|
+
var NATIVE_TITLE_SUFFIX = /^[^\s]+ subagent$/;
|
|
127
|
+
function parseVariantAnnotation(text) {
|
|
128
|
+
if (typeof text !== "string")
|
|
129
|
+
return;
|
|
130
|
+
let last;
|
|
131
|
+
for (const match of text.matchAll(ANNOTATION_TOKEN)) {
|
|
132
|
+
const alias = (match[1] ?? "").trim();
|
|
133
|
+
if (alias === "" || NATIVE_TITLE_SUFFIX.test(alias))
|
|
134
|
+
continue;
|
|
135
|
+
const switchedFrom = (match[2] ?? "").trim();
|
|
136
|
+
last = switchedFrom === "" ? { alias } : { alias, switchedFrom };
|
|
137
|
+
}
|
|
138
|
+
return last;
|
|
139
|
+
}
|
|
140
|
+
function variantFromTitle(title, sidecar) {
|
|
141
|
+
const alias = parseVariantAnnotation(title)?.alias;
|
|
142
|
+
if (alias === undefined || sidecar.aliases.size === 0)
|
|
143
|
+
return;
|
|
144
|
+
const parent = sidecar.aliases.get(alias);
|
|
145
|
+
return parent !== undefined ? { alias, parent } : undefined;
|
|
117
146
|
}
|
|
118
147
|
function stripJsoncComments(text) {
|
|
119
148
|
return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'\\])\/\/.*$/gm, "$1").replace(/,(\s*[}\]])/g, "$1");
|
|
@@ -159,8 +188,8 @@ async function fetchVariantAttribution(client, parentSessionID, directory, avPar
|
|
|
159
188
|
const child = metadata?.sessionId;
|
|
160
189
|
const variantMeta = metadata?.agentVariants;
|
|
161
190
|
const description = part.state?.input?.description;
|
|
162
|
-
const
|
|
163
|
-
const alias = (typeof variantMeta?.alias === "string" ? variantMeta.alias : undefined) ??
|
|
191
|
+
const descriptionAlias = parseVariantAnnotation(typeof description === "string" ? description : undefined)?.alias;
|
|
192
|
+
const alias = (typeof variantMeta?.alias === "string" ? variantMeta.alias : undefined) ?? (descriptionAlias !== "" ? descriptionAlias : undefined);
|
|
164
193
|
const routedAgent = variantMeta?.routedAgent;
|
|
165
194
|
if (typeof child === "string" && typeof alias === "string" && !map.has(child)) {
|
|
166
195
|
map.set(child, {
|
|
@@ -375,14 +404,15 @@ async function loadExplorerData(api, directory, rootOverride) {
|
|
|
375
404
|
hidden: hiddenAgentIDs(api.state.config),
|
|
376
405
|
configAgents: configAgentIDs(api.state.config),
|
|
377
406
|
variantOf,
|
|
378
|
-
avParents: avSidecar.parents
|
|
407
|
+
avParents: avSidecar.parents,
|
|
408
|
+
avSidecar
|
|
379
409
|
};
|
|
380
410
|
}
|
|
381
411
|
function explorerRows(tree, data) {
|
|
382
412
|
const rows = [];
|
|
383
413
|
const walk = (nodes, depth) => {
|
|
384
414
|
for (const node of nodes) {
|
|
385
|
-
const variant = data.variantOf.get(node.session.id);
|
|
415
|
+
const variant = data.variantOf.get(node.session.id) ?? variantFromTitle(node.session.title, data.avSidecar);
|
|
386
416
|
const agent = node.session.agent;
|
|
387
417
|
rows.push({
|
|
388
418
|
session: node.session,
|
|
@@ -1213,7 +1243,11 @@ async function remainingIdleCount(api, directory, rootID) {
|
|
|
1213
1243
|
hidden: new Set,
|
|
1214
1244
|
configAgents: new Set,
|
|
1215
1245
|
variantOf: new Map,
|
|
1216
|
-
avParents: new Map
|
|
1246
|
+
avParents: new Map,
|
|
1247
|
+
avSidecar: {
|
|
1248
|
+
parents: new Map,
|
|
1249
|
+
aliases: new Map
|
|
1250
|
+
}
|
|
1217
1251
|
});
|
|
1218
1252
|
return rows.filter((row) => !row.running).length;
|
|
1219
1253
|
}
|
package/package.json
CHANGED
package/src/sessions.ts
CHANGED
|
@@ -146,29 +146,76 @@ export async function fetchActiveSessionIDs(client: unknown): Promise<Set<string
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/** Minimal agent-variants sidecar view: which parents exist and which have
|
|
149
|
-
* the base disabled
|
|
149
|
+
* the base disabled, plus the full alias set (variant name -> parent) used
|
|
150
|
+
* to validate parsed annotations. Absent file / parse errors -> empty
|
|
151
|
+
* (fail-soft). */
|
|
150
152
|
export type AvSidecar = {
|
|
151
153
|
parents: Map<string, { disableBase: boolean }>
|
|
154
|
+
aliases: Map<string, string>
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
export function loadAvSidecar(globalConfigDir: string | undefined): AvSidecar {
|
|
155
158
|
const parents = new Map<string, { disableBase: boolean }>()
|
|
156
|
-
|
|
159
|
+
const aliases = new Map<string, string>()
|
|
160
|
+
if (!globalConfigDir) return { parents, aliases }
|
|
157
161
|
try {
|
|
158
162
|
const path = `${globalConfigDir.replace(/[\\/]+$/, "")}/agent-variants.jsonc`
|
|
159
|
-
if (!existsSync(path)) return { parents }
|
|
163
|
+
if (!existsSync(path)) return { parents, aliases }
|
|
160
164
|
const raw = readFileSync(path, "utf8")
|
|
161
165
|
const json = JSON.parse(stripJsoncComments(raw)) as {
|
|
162
|
-
agents?: Record<string, { disable_base?: boolean; disable?: boolean }>
|
|
166
|
+
agents?: Record<string, { disable_base?: boolean; disable?: boolean; variants?: Record<string, { name?: string; disable?: boolean }> }>
|
|
163
167
|
}
|
|
164
168
|
for (const [parent, entry] of Object.entries(json.agents ?? {})) {
|
|
165
169
|
if (!entry || typeof entry !== "object") continue
|
|
166
170
|
parents.set(parent, { disableBase: entry.disable_base === true && entry.disable !== true })
|
|
171
|
+
// Alias = the variant's custom name when set, else parent-key (mirrors
|
|
172
|
+
// AV's variantName) - the validation set for annotation parsing.
|
|
173
|
+
for (const [key, variant] of Object.entries(entry.variants ?? {})) {
|
|
174
|
+
if (!variant || typeof variant !== "object" || variant.disable === true) continue
|
|
175
|
+
const alias = typeof variant.name === "string" && variant.name !== "" ? variant.name : `${parent}-${key}`
|
|
176
|
+
aliases.set(alias, parent)
|
|
177
|
+
}
|
|
167
178
|
}
|
|
168
179
|
} catch {
|
|
169
180
|
// Not our file to complain about - AV may simply not be installed.
|
|
170
181
|
}
|
|
171
|
-
return { parents }
|
|
182
|
+
return { parents, aliases }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** One annotation token in any historical form: legacy "(@x variant)",
|
|
186
|
+
* terse "(@x)", switch "(@x ← y)". Mirrors AV's parseVariantAnnotation. */
|
|
187
|
+
const ANNOTATION_TOKEN = /\(@\s*([^()@←]*?)\s*(?:\s+variant)?\s*(?:\s*←\s*([^()@←]*?))?\s*\)/g
|
|
188
|
+
|
|
189
|
+
/** OpenCode's own task-title suffix " (@agent subagent)" (task.ts appends it
|
|
190
|
+
* on agent-switch resumes, always AFTER our note) matches the terse form
|
|
191
|
+
* grammatically - excluded explicitly or it would override the true alias
|
|
192
|
+
* under last-wins. */
|
|
193
|
+
const NATIVE_TITLE_SUFFIX = /^[^\s]+ subagent$/
|
|
194
|
+
|
|
195
|
+
/** Reads the last VALID variant annotation from a text. Native suffixes are
|
|
196
|
+
* never evidence. */
|
|
197
|
+
export function parseVariantAnnotation(text: string | undefined): { alias: string; switchedFrom?: string } | undefined {
|
|
198
|
+
if (typeof text !== "string") return undefined
|
|
199
|
+
let last: { alias: string; switchedFrom?: string } | undefined
|
|
200
|
+
for (const match of text.matchAll(ANNOTATION_TOKEN)) {
|
|
201
|
+
const alias = (match[1] ?? "").trim()
|
|
202
|
+
if (alias === "" || NATIVE_TITLE_SUFFIX.test(alias)) continue
|
|
203
|
+
const switchedFrom = (match[2] ?? "").trim()
|
|
204
|
+
last = switchedFrom === "" ? { alias } : { alias, switchedFrom }
|
|
205
|
+
}
|
|
206
|
+
return last
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Title-annotation fallback for children beyond the attribution walk's
|
|
210
|
+
* depth: AV writes the annotation into the child's session title at call
|
|
211
|
+
* time and it survives forever, so it attributes what the windowed walk
|
|
212
|
+
* cannot reach. Validated against the sidecar's alias set - an unknown
|
|
213
|
+
* token never attributes. */
|
|
214
|
+
export function variantFromTitle(title: string | undefined, sidecar: AvSidecar): { alias: string; parent: string } | undefined {
|
|
215
|
+
const alias = parseVariantAnnotation(title)?.alias
|
|
216
|
+
if (alias === undefined || sidecar.aliases.size === 0) return undefined
|
|
217
|
+
const parent = sidecar.aliases.get(alias)
|
|
218
|
+
return parent !== undefined ? { alias, parent } : undefined
|
|
172
219
|
}
|
|
173
220
|
|
|
174
221
|
function stripJsoncComments(text: string): string {
|
|
@@ -230,10 +277,11 @@ export async function fetchVariantAttribution(
|
|
|
230
277
|
// Two alias sources: the after-hook metadata stamp (terminal
|
|
231
278
|
// completed parts) and the before-hook description annotation -
|
|
232
279
|
// aborted/error parts never receive the metadata stamp, but the
|
|
233
|
-
// annotation
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
const
|
|
280
|
+
// annotation persists. The shared parser excludes OpenCode's native
|
|
281
|
+
// "(@agent subagent)" suffix, which would otherwise win last-wins.
|
|
282
|
+
const description: unknown = part.state?.input?.description
|
|
283
|
+
const descriptionAlias = parseVariantAnnotation(typeof description === "string" ? description : undefined)?.alias
|
|
284
|
+
const alias = (typeof variantMeta?.alias === "string" ? variantMeta.alias : undefined) ?? (descriptionAlias !== "" ? descriptionAlias : undefined)
|
|
237
285
|
const routedAgent = variantMeta?.routedAgent
|
|
238
286
|
if (typeof child === "string" && typeof alias === "string" && !map.has(child)) {
|
|
239
287
|
map.set(child, {
|
package/src/wizard.tsx
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
loadAvSidecar,
|
|
31
31
|
parentCandidates,
|
|
32
32
|
rowHidden,
|
|
33
|
+
variantFromTitle,
|
|
34
|
+
type AvSidecar,
|
|
33
35
|
type CleanupPlan,
|
|
34
36
|
type SessionLite,
|
|
35
37
|
type SessionTreeNode,
|
|
@@ -54,6 +56,8 @@ export type ExplorerData = {
|
|
|
54
56
|
variantOf: VariantAttribution
|
|
55
57
|
/** AV sidecar parents (empty when agent-variants is not installed). */
|
|
56
58
|
avParents: Map<string, { disableBase: boolean }>
|
|
59
|
+
/** The full sidecar view - the alias set validates title annotations. */
|
|
60
|
+
avSidecar: AvSidecar
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
export type TreeAction =
|
|
@@ -89,6 +93,7 @@ export async function loadExplorerData(api: TuiPluginApi, directory: string, roo
|
|
|
89
93
|
configAgents: configAgentIDs(api.state.config),
|
|
90
94
|
variantOf,
|
|
91
95
|
avParents: avSidecar.parents,
|
|
96
|
+
avSidecar,
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
99
|
|
|
@@ -96,7 +101,10 @@ export function explorerRows(tree: SessionTreeNode[], data: ExplorerData): Explo
|
|
|
96
101
|
const rows: ExplorerRow[] = []
|
|
97
102
|
const walk = (nodes: SessionTreeNode[], depth: number) => {
|
|
98
103
|
for (const node of nodes) {
|
|
99
|
-
|
|
104
|
+
// Attribution: windowed metadata/description walk first, then the
|
|
105
|
+
// title annotation (AV's call-time writing, carried forever - reaches
|
|
106
|
+
// children the walk cannot).
|
|
107
|
+
const variant = data.variantOf.get(node.session.id) ?? variantFromTitle(node.session.title, data.avSidecar)
|
|
100
108
|
const agent = node.session.agent
|
|
101
109
|
rows.push({
|
|
102
110
|
session: node.session,
|
|
@@ -603,6 +611,7 @@ async function remainingIdleCount(api: TuiPluginApi, directory: string, rootID:
|
|
|
603
611
|
configAgents: new Set(),
|
|
604
612
|
variantOf: new Map(),
|
|
605
613
|
avParents: new Map(),
|
|
614
|
+
avSidecar: { parents: new Map(), aliases: new Map() },
|
|
606
615
|
})
|
|
607
616
|
return rows.filter((row) => !row.running).length
|
|
608
617
|
}
|