@mirrowel/opencode-subagent-explorer 0.2.0-dev.1 → 0.2.0-dev.3
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 +18 -5
- package/dist/sessions.js +92 -33
- package/dist/tui.js +251 -143
- package/dist/wizard.js +251 -143
- package/package.json +1 -1
- package/src/sessions.ts +96 -32
- package/src/wizard.tsx +60 -10
package/dist/sessions.d.ts
CHANGED
|
@@ -24,8 +24,6 @@ export type CleanupPlan = {
|
|
|
24
24
|
export declare const BUILTIN_AGENTS: Set<string>;
|
|
25
25
|
export declare function withTimeout<T>(fn: () => Promise<T>, ms?: number): Promise<T | undefined>;
|
|
26
26
|
export declare function normalizeSession(raw: unknown): SessionLite | undefined;
|
|
27
|
-
/** Lists sessions for a directory. Tries the v2 client's flat params first,
|
|
28
|
-
* then the v1 `{query}` shape; unwraps both envelope styles. Fail-soft. */
|
|
29
27
|
export declare function fetchSessions(client: unknown, directory: string): Promise<SessionLite[] | undefined>;
|
|
30
28
|
/** Running session ids via `session.active()` with the `session.status()`
|
|
31
29
|
* fallback (busy/retry = running). `undefined` when neither exists or both
|
|
@@ -50,8 +48,11 @@ export type AvSidecar = {
|
|
|
50
48
|
};
|
|
51
49
|
export declare function loadAvSidecar(globalConfigDir: string | undefined): AvSidecar;
|
|
52
50
|
/** child session id -> the AV variant that created it (from the parent's
|
|
53
|
-
* task-part metadata).
|
|
54
|
-
*
|
|
51
|
+
* task-part metadata). Paged backward through the parent's history via the
|
|
52
|
+
* `before` message cursor (up to ATTRIBUTION_PAGES pages), so large trees
|
|
53
|
+
* attribute most children instead of only the newest ~15 tasks; very deep
|
|
54
|
+
* history simply stays unattributed (the row then shows the parent agent,
|
|
55
|
+
* never a hidden badge - see explorerRows). */
|
|
55
56
|
export type VariantAttribution = Map<string, {
|
|
56
57
|
alias: string;
|
|
57
58
|
parent: string;
|
|
@@ -79,9 +80,21 @@ export declare function configAgentIDs(config: unknown): Set<string>;
|
|
|
79
80
|
* in the config, or the agent was registered at runtime (not config-known,
|
|
80
81
|
* not built-in, not an agent-variants clone). */
|
|
81
82
|
export declare function isHiddenAgent(agent: string | undefined, hidden: Set<string>, configAgents: Set<string>): boolean;
|
|
83
|
+
/** Badge decision for explorer rows: variant children and children of
|
|
84
|
+
* AV-managed parents never carry the hidden badge - a hidden AV parent is
|
|
85
|
+
* agent-variants' base-disable ("must use a variant"), not a background
|
|
86
|
+
* agent, and the decision must not depend on attribution windows. Only
|
|
87
|
+
* genuinely hidden non-AV agents (plugin background agents, hidden custom
|
|
88
|
+
* agents without variants) keep the badge. */
|
|
89
|
+
export declare function rowHidden(variant: unknown, agent: string | undefined, avParents: {
|
|
90
|
+
has: (id: string) => boolean;
|
|
91
|
+
}, hidden: Set<string>, configAgents: Set<string>): boolean;
|
|
82
92
|
/** Deletes one session through whichever delete method the host client
|
|
83
93
|
* exposes (`session.delete`, `session.remove`). Returns false when no
|
|
84
|
-
* method exists or the call failed.
|
|
94
|
+
* method exists or the call failed. An `{error}` envelope without `data`
|
|
95
|
+
* (the runtime's non-throwing failure shape) counts as a failure, not a
|
|
96
|
+
* success - the server's remove handler swallows internal errors and can
|
|
97
|
+
* still answer, so callers verify after batch deletes. */
|
|
85
98
|
export declare function deleteSession(client: unknown, sessionID: string): Promise<boolean>;
|
|
86
99
|
export type SessionDetails = {
|
|
87
100
|
messageCount: number;
|
package/dist/sessions.js
CHANGED
|
@@ -55,17 +55,26 @@ export function normalizeSession(raw) {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
/** Lists sessions for a directory. Tries the v2 client's flat params first,
|
|
58
|
-
* then the v1 `{query}` shape; unwraps both envelope styles. Fail-soft.
|
|
58
|
+
* then the v1 `{query}` shape; unwraps both envelope styles. Fail-soft.
|
|
59
|
+
*
|
|
60
|
+
* EXPLICIT LARGE LIMIT: OpenCode's Session.list defaults to a 100-row page
|
|
61
|
+
* (ordered by time_updated DESC, session.ts `limit ?? 100`) - a plain call
|
|
62
|
+
* silently hides older sessions, which truncated large trees (100+ subagents
|
|
63
|
+
* showed ~85) and made cleanups miss everything past the first page. The
|
|
64
|
+
* server imposes no max on limit (NonNegativeInt), and its `start` cursor
|
|
65
|
+
* filters to NEWER sessions (gte), so downward paging is impossible by
|
|
66
|
+
* design - one large bounded page is the honest shape. */
|
|
67
|
+
const SESSION_LIST_LIMIT = 5000;
|
|
59
68
|
export async function fetchSessions(client, directory) {
|
|
60
69
|
const namespace = client?.session;
|
|
61
70
|
if (!namespace?.list)
|
|
62
71
|
return undefined;
|
|
63
72
|
const calls = [
|
|
64
|
-
() => namespace.list({ directory }),
|
|
65
|
-
() => namespace.list({ query: { directory } }),
|
|
73
|
+
() => namespace.list({ directory, limit: SESSION_LIST_LIMIT }),
|
|
74
|
+
() => namespace.list({ query: { directory, limit: SESSION_LIST_LIMIT } }),
|
|
66
75
|
];
|
|
67
76
|
for (const call of calls) {
|
|
68
|
-
const result = unwrap(await withTimeout(call));
|
|
77
|
+
const result = unwrap(await withTimeout(call, 8000));
|
|
69
78
|
if (Array.isArray(result))
|
|
70
79
|
return result.map(normalizeSession).filter((s) => !!s);
|
|
71
80
|
}
|
|
@@ -138,6 +147,8 @@ function stripJsoncComments(text) {
|
|
|
138
147
|
.replace(/(^|[^:"'\\])\/\/.*$/gm, "$1")
|
|
139
148
|
.replace(/,(\s*[}\]])/g, "$1");
|
|
140
149
|
}
|
|
150
|
+
const ATTRIBUTION_PAGE_SIZE = 100;
|
|
151
|
+
const ATTRIBUTION_PAGES = 5;
|
|
141
152
|
export async function fetchVariantAttribution(client, parentSessionID, directory, avParents) {
|
|
142
153
|
const map = new Map();
|
|
143
154
|
if (avParents.size === 0 || !parentSessionID)
|
|
@@ -145,34 +156,62 @@ export async function fetchVariantAttribution(client, parentSessionID, directory
|
|
|
145
156
|
const namespace = client?.session;
|
|
146
157
|
if (!namespace || typeof namespace.messages !== "function")
|
|
147
158
|
return map;
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
? messages
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
159
|
+
const fetchPage = (before) => {
|
|
160
|
+
const query = { directory, limit: ATTRIBUTION_PAGE_SIZE };
|
|
161
|
+
if (before)
|
|
162
|
+
query.before = before;
|
|
163
|
+
// Try the flat (v2-gen) shape first, then the legacy {path, query} wrapper.
|
|
164
|
+
const flat = namespace.messages({ sessionID: parentSessionID, ...query });
|
|
165
|
+
return flat instanceof Promise ? flat : namespace.messages({ path: { id: parentSessionID }, query });
|
|
166
|
+
};
|
|
167
|
+
const asList = (value) => {
|
|
168
|
+
if (Array.isArray(value))
|
|
169
|
+
return value;
|
|
170
|
+
if (value && typeof value === "object") {
|
|
171
|
+
const unwrapped = value.data;
|
|
172
|
+
if (Array.isArray(unwrapped))
|
|
173
|
+
return unwrapped;
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
};
|
|
177
|
+
let before;
|
|
178
|
+
const seenCursors = new Set();
|
|
179
|
+
for (let page = 0; page < ATTRIBUTION_PAGES; page++) {
|
|
180
|
+
const messages = await withTimeout(() => fetchPage(before), 8000);
|
|
181
|
+
const list = asList(messages);
|
|
182
|
+
if (!list || list.length === 0)
|
|
183
|
+
break;
|
|
184
|
+
for (const message of list) {
|
|
185
|
+
for (const part of message.parts ?? []) {
|
|
186
|
+
if (part?.type !== "tool" || part.tool !== "task")
|
|
187
|
+
continue;
|
|
188
|
+
const metadata = part.state?.metadata;
|
|
189
|
+
const child = metadata?.sessionId;
|
|
190
|
+
const variantMeta = metadata?.agentVariants;
|
|
191
|
+
// Two alias sources: the after-hook metadata stamp (terminal
|
|
192
|
+
// completed parts) and the before-hook description annotation -
|
|
193
|
+
// aborted/error parts never receive the metadata stamp, but the
|
|
194
|
+
// annotation ("(@alias variant)" appended at call time) persists.
|
|
195
|
+
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];
|
|
198
|
+
const routedAgent = variantMeta?.routedAgent;
|
|
199
|
+
if (typeof child === "string" && typeof alias === "string" && !map.has(child)) {
|
|
200
|
+
map.set(child, {
|
|
201
|
+
alias,
|
|
202
|
+
parent: typeof routedAgent === "string" ? routedAgent : alias.replace(/-[a-z0-9-]+$/, ""),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
174
205
|
}
|
|
175
206
|
}
|
|
207
|
+
if (list.length < ATTRIBUTION_PAGE_SIZE)
|
|
208
|
+
break;
|
|
209
|
+
const last = list[list.length - 1];
|
|
210
|
+
const next = last?.info?.id ?? last?.id;
|
|
211
|
+
if (!next || seenCursors.has(next))
|
|
212
|
+
break;
|
|
213
|
+
seenCursors.add(next);
|
|
214
|
+
before = next;
|
|
176
215
|
}
|
|
177
216
|
return map;
|
|
178
217
|
}
|
|
@@ -274,13 +313,30 @@ export function isHiddenAgent(agent, hidden, configAgents) {
|
|
|
274
313
|
return true;
|
|
275
314
|
return !configAgents.has(agent) && !BUILTIN_AGENTS.has(agent) && !agent.startsWith("av:");
|
|
276
315
|
}
|
|
316
|
+
/** Badge decision for explorer rows: variant children and children of
|
|
317
|
+
* AV-managed parents never carry the hidden badge - a hidden AV parent is
|
|
318
|
+
* agent-variants' base-disable ("must use a variant"), not a background
|
|
319
|
+
* agent, and the decision must not depend on attribution windows. Only
|
|
320
|
+
* genuinely hidden non-AV agents (plugin background agents, hidden custom
|
|
321
|
+
* agents without variants) keep the badge. */
|
|
322
|
+
export function rowHidden(variant, agent, avParents, hidden, configAgents) {
|
|
323
|
+
if (variant)
|
|
324
|
+
return false;
|
|
325
|
+
if (agent && avParents.has(agent))
|
|
326
|
+
return false;
|
|
327
|
+
return isHiddenAgent(agent, hidden, configAgents);
|
|
328
|
+
}
|
|
277
329
|
/** Deletes one session through whichever delete method the host client
|
|
278
330
|
* exposes (`session.delete`, `session.remove`). Returns false when no
|
|
279
|
-
* method exists or the call failed.
|
|
331
|
+
* method exists or the call failed. An `{error}` envelope without `data`
|
|
332
|
+
* (the runtime's non-throwing failure shape) counts as a failure, not a
|
|
333
|
+
* success - the server's remove handler swallows internal errors and can
|
|
334
|
+
* still answer, so callers verify after batch deletes. */
|
|
280
335
|
export async function deleteSession(client, sessionID) {
|
|
281
336
|
const namespace = client?.session;
|
|
282
337
|
if (!namespace)
|
|
283
338
|
return false;
|
|
339
|
+
const isErrorEnvelope = (value) => !!value && typeof value === "object" && "error" in value && !("data" in value);
|
|
284
340
|
const shapes = [];
|
|
285
341
|
if (typeof namespace.delete === "function") {
|
|
286
342
|
shapes.push(() => namespace.delete({ sessionID }), () => namespace.delete({ path: { sessionID } }), () => namespace.delete({ path: { id: sessionID } }), () => namespace.delete({ id: sessionID }));
|
|
@@ -290,8 +346,11 @@ export async function deleteSession(client, sessionID) {
|
|
|
290
346
|
}
|
|
291
347
|
for (const call of shapes) {
|
|
292
348
|
const result = await withTimeout(call);
|
|
293
|
-
if (result
|
|
294
|
-
|
|
349
|
+
if (result === undefined)
|
|
350
|
+
continue;
|
|
351
|
+
if (isErrorEnvelope(result))
|
|
352
|
+
continue;
|
|
353
|
+
return true;
|
|
295
354
|
}
|
|
296
355
|
return false;
|
|
297
356
|
}
|