@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.
@@ -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. Absent file / parse errors -> empty (fail-soft). */
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 ("(@alias variant)" appended at call time) persists.
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 descriptionMatch = typeof description === "string" ? description.match(/\s+\(@(\S+) variant\)$/) : undefined;
197
- const alias = (typeof variantMeta?.alias === "string" ? variantMeta.alias : undefined) ?? descriptionMatch?.[1];
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 descriptionMatch = typeof description === "string" ? description.match(/\s+\(@(\S+) variant\)$/) : undefined;
216
- const alias = (typeof variantMeta?.alias === "string" ? variantMeta.alias : undefined) ?? descriptionMatch?.[1];
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 descriptionMatch = typeof description === "string" ? description.match(/\s+\(@(\S+) variant\)$/) : undefined;
163
- const alias = (typeof variantMeta?.alias === "string" ? variantMeta.alias : undefined) ?? descriptionMatch?.[1];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mirrowel/opencode-subagent-explorer",
3
- "version": "0.2.0-dev.3",
3
+ "version": "0.2.0-dev.5",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for exploring and cleaning up subagent sessions of the current session",
6
6
  "license": "MIT",
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. Absent file / parse errors -> empty (fail-soft). */
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
- if (!globalConfigDir) return { parents }
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 ("(@alias variant)" appended at call time) persists.
234
- const description = part.state?.input?.description
235
- const descriptionMatch = typeof description === "string" ? description.match(/\s+\(@(\S+) variant\)$/) : undefined
236
- const alias = (typeof variantMeta?.alias === "string" ? variantMeta.alias : undefined) ?? descriptionMatch?.[1]
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
- const variant = data.variantOf.get(node.session.id)
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
  }