@mirrowel/opencode-subagent-explorer 0.2.0-dev.1 → 0.2.0-dev.2
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 +86 -33
- package/dist/tui.js +249 -143
- package/dist/wizard.js +249 -143
- package/package.json +1 -1
- package/src/sessions.ts +90 -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,56 @@ 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
|
+
const alias = variantMeta?.alias;
|
|
192
|
+
const routedAgent = variantMeta?.routedAgent;
|
|
193
|
+
if (typeof child === "string" && typeof alias === "string" && !map.has(child)) {
|
|
194
|
+
map.set(child, {
|
|
195
|
+
alias,
|
|
196
|
+
parent: typeof routedAgent === "string" ? routedAgent : alias.replace(/-[a-z0-9-]+$/, ""),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
174
199
|
}
|
|
175
200
|
}
|
|
201
|
+
if (list.length < ATTRIBUTION_PAGE_SIZE)
|
|
202
|
+
break;
|
|
203
|
+
const last = list[list.length - 1];
|
|
204
|
+
const next = last?.info?.id ?? last?.id;
|
|
205
|
+
if (!next || seenCursors.has(next))
|
|
206
|
+
break;
|
|
207
|
+
seenCursors.add(next);
|
|
208
|
+
before = next;
|
|
176
209
|
}
|
|
177
210
|
return map;
|
|
178
211
|
}
|
|
@@ -274,13 +307,30 @@ export function isHiddenAgent(agent, hidden, configAgents) {
|
|
|
274
307
|
return true;
|
|
275
308
|
return !configAgents.has(agent) && !BUILTIN_AGENTS.has(agent) && !agent.startsWith("av:");
|
|
276
309
|
}
|
|
310
|
+
/** Badge decision for explorer rows: variant children and children of
|
|
311
|
+
* AV-managed parents never carry the hidden badge - a hidden AV parent is
|
|
312
|
+
* agent-variants' base-disable ("must use a variant"), not a background
|
|
313
|
+
* agent, and the decision must not depend on attribution windows. Only
|
|
314
|
+
* genuinely hidden non-AV agents (plugin background agents, hidden custom
|
|
315
|
+
* agents without variants) keep the badge. */
|
|
316
|
+
export function rowHidden(variant, agent, avParents, hidden, configAgents) {
|
|
317
|
+
if (variant)
|
|
318
|
+
return false;
|
|
319
|
+
if (agent && avParents.has(agent))
|
|
320
|
+
return false;
|
|
321
|
+
return isHiddenAgent(agent, hidden, configAgents);
|
|
322
|
+
}
|
|
277
323
|
/** Deletes one session through whichever delete method the host client
|
|
278
324
|
* exposes (`session.delete`, `session.remove`). Returns false when no
|
|
279
|
-
* method exists or the call failed.
|
|
325
|
+
* method exists or the call failed. An `{error}` envelope without `data`
|
|
326
|
+
* (the runtime's non-throwing failure shape) counts as a failure, not a
|
|
327
|
+
* success - the server's remove handler swallows internal errors and can
|
|
328
|
+
* still answer, so callers verify after batch deletes. */
|
|
280
329
|
export async function deleteSession(client, sessionID) {
|
|
281
330
|
const namespace = client?.session;
|
|
282
331
|
if (!namespace)
|
|
283
332
|
return false;
|
|
333
|
+
const isErrorEnvelope = (value) => !!value && typeof value === "object" && "error" in value && !("data" in value);
|
|
284
334
|
const shapes = [];
|
|
285
335
|
if (typeof namespace.delete === "function") {
|
|
286
336
|
shapes.push(() => namespace.delete({ sessionID }), () => namespace.delete({ path: { sessionID } }), () => namespace.delete({ path: { id: sessionID } }), () => namespace.delete({ id: sessionID }));
|
|
@@ -290,8 +340,11 @@ export async function deleteSession(client, sessionID) {
|
|
|
290
340
|
}
|
|
291
341
|
for (const call of shapes) {
|
|
292
342
|
const result = await withTimeout(call);
|
|
293
|
-
if (result
|
|
294
|
-
|
|
343
|
+
if (result === undefined)
|
|
344
|
+
continue;
|
|
345
|
+
if (isErrorEnvelope(result))
|
|
346
|
+
continue;
|
|
347
|
+
return true;
|
|
295
348
|
}
|
|
296
349
|
return false;
|
|
297
350
|
}
|