@mirrowel/opencode-subagent-explorer 0.1.0 → 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 +42 -10
- package/dist/sessions.js +147 -20
- package/dist/tui.js +328 -144
- package/dist/wizard.d.ts +13 -2
- package/dist/wizard.js +334 -149
- package/package.json +1 -1
- package/src/palette-category.ts +83 -83
- package/src/sessions.ts +164 -18
- package/src/wizard.tsx +110 -16
package/dist/sessions.d.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core subagent-session data layer. Pure tree/classification logic plus
|
|
3
|
-
* fail-soft client seams. Every client call is attached to its namespace
|
|
4
|
-
* object (v2-gen class methods throw when detached), feature-detected,
|
|
5
|
-
* timeout-bounded, and envelope-unwrapping ({data} vs raw) so one code path
|
|
6
|
-
* serves the v1 and v2 hosts and the studio embed alike.
|
|
7
|
-
*/
|
|
8
1
|
export type SessionLite = {
|
|
9
2
|
id: string;
|
|
10
3
|
parentID?: string;
|
|
@@ -31,13 +24,40 @@ export type CleanupPlan = {
|
|
|
31
24
|
export declare const BUILTIN_AGENTS: Set<string>;
|
|
32
25
|
export declare function withTimeout<T>(fn: () => Promise<T>, ms?: number): Promise<T | undefined>;
|
|
33
26
|
export declare function normalizeSession(raw: unknown): SessionLite | undefined;
|
|
34
|
-
/** Lists sessions for a directory. Tries the v2 client's flat params first,
|
|
35
|
-
* then the v1 `{query}` shape; unwraps both envelope styles. Fail-soft. */
|
|
36
27
|
export declare function fetchSessions(client: unknown, directory: string): Promise<SessionLite[] | undefined>;
|
|
37
28
|
/** Running session ids via `session.active()` with the `session.status()`
|
|
38
29
|
* fallback (busy/retry = running). `undefined` when neither exists or both
|
|
39
30
|
* fail — callers must treat that as "unknown", never "idle". */
|
|
31
|
+
/**
|
|
32
|
+
* Running session ids via `session.active()` with the `session.status()`
|
|
33
|
+
* fallback (busy/retry/unknown-type = running). Mirrors the Config Studio
|
|
34
|
+
* reload coordinator's proven probing: PRESENCE in the active map means
|
|
35
|
+
* running (values may be falsy and must not be filtered), the status map
|
|
36
|
+
* keys carry `.type` (not `.status`), and all calls stay attached to the
|
|
37
|
+
* namespace object (SDK methods dereference `this`). `undefined` when
|
|
38
|
+
* neither exists or both fail - callers must treat that as "unknown",
|
|
39
|
+
* never "idle".
|
|
40
|
+
*/
|
|
40
41
|
export declare function fetchActiveSessionIDs(client: unknown): Promise<Set<string> | undefined>;
|
|
42
|
+
/** Minimal agent-variants sidecar view: which parents exist and which have
|
|
43
|
+
* the base disabled. Absent file / parse errors -> empty (fail-soft). */
|
|
44
|
+
export type AvSidecar = {
|
|
45
|
+
parents: Map<string, {
|
|
46
|
+
disableBase: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
export declare function loadAvSidecar(globalConfigDir: string | undefined): AvSidecar;
|
|
50
|
+
/** child session id -> the AV variant that created it (from the parent's
|
|
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). */
|
|
56
|
+
export type VariantAttribution = Map<string, {
|
|
57
|
+
alias: string;
|
|
58
|
+
parent: string;
|
|
59
|
+
}>;
|
|
60
|
+
export declare function fetchVariantAttribution(client: unknown, parentSessionID: string | undefined, directory: string | undefined, avParents: Set<string>): Promise<VariantAttribution>;
|
|
41
61
|
/** Builds the recursive subagent tree for `rootID`. Unknown-parent sessions
|
|
42
62
|
* are ignored (not part of this session's tree). Guarded against cycles. */
|
|
43
63
|
export declare function buildSessionTree(sessions: SessionLite[], rootID: string): SessionTreeNode[];
|
|
@@ -60,9 +80,21 @@ export declare function configAgentIDs(config: unknown): Set<string>;
|
|
|
60
80
|
* in the config, or the agent was registered at runtime (not config-known,
|
|
61
81
|
* not built-in, not an agent-variants clone). */
|
|
62
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;
|
|
63
92
|
/** Deletes one session through whichever delete method the host client
|
|
64
93
|
* exposes (`session.delete`, `session.remove`). Returns false when no
|
|
65
|
-
* 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. */
|
|
66
98
|
export declare function deleteSession(client: unknown, sessionID: string): Promise<boolean>;
|
|
67
99
|
export type SessionDetails = {
|
|
68
100
|
messageCount: number;
|
package/dist/sessions.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* timeout-bounded, and envelope-unwrapping ({data} vs raw) so one code path
|
|
6
6
|
* serves the v1 and v2 hosts and the studio embed alike.
|
|
7
7
|
*/
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
9
|
const CALL_TIMEOUT_MS = 5000;
|
|
9
10
|
/** OpenCode's built-in agent ids (v1): anything else that is not in the
|
|
10
11
|
* user's config was registered at runtime (plugins) and is treated as
|
|
@@ -54,17 +55,26 @@ export function normalizeSession(raw) {
|
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
/** Lists sessions for a directory. Tries the v2 client's flat params first,
|
|
57
|
-
* 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;
|
|
58
68
|
export async function fetchSessions(client, directory) {
|
|
59
69
|
const namespace = client?.session;
|
|
60
70
|
if (!namespace?.list)
|
|
61
71
|
return undefined;
|
|
62
72
|
const calls = [
|
|
63
|
-
() => namespace.list({ directory }),
|
|
64
|
-
() => namespace.list({ query: { directory } }),
|
|
73
|
+
() => namespace.list({ directory, limit: SESSION_LIST_LIMIT }),
|
|
74
|
+
() => namespace.list({ query: { directory, limit: SESSION_LIST_LIMIT } }),
|
|
65
75
|
];
|
|
66
76
|
for (const call of calls) {
|
|
67
|
-
const result = unwrap(await withTimeout(call));
|
|
77
|
+
const result = unwrap(await withTimeout(call, 8000));
|
|
68
78
|
if (Array.isArray(result))
|
|
69
79
|
return result.map(normalizeSession).filter((s) => !!s);
|
|
70
80
|
}
|
|
@@ -73,28 +83,35 @@ export async function fetchSessions(client, directory) {
|
|
|
73
83
|
/** Running session ids via `session.active()` with the `session.status()`
|
|
74
84
|
* fallback (busy/retry = running). `undefined` when neither exists or both
|
|
75
85
|
* fail — callers must treat that as "unknown", never "idle". */
|
|
86
|
+
/**
|
|
87
|
+
* Running session ids via `session.active()` with the `session.status()`
|
|
88
|
+
* fallback (busy/retry/unknown-type = running). Mirrors the Config Studio
|
|
89
|
+
* reload coordinator's proven probing: PRESENCE in the active map means
|
|
90
|
+
* running (values may be falsy and must not be filtered), the status map
|
|
91
|
+
* keys carry `.type` (not `.status`), and all calls stay attached to the
|
|
92
|
+
* namespace object (SDK methods dereference `this`). `undefined` when
|
|
93
|
+
* neither exists or both fail - callers must treat that as "unknown",
|
|
94
|
+
* never "idle".
|
|
95
|
+
*/
|
|
76
96
|
export async function fetchActiveSessionIDs(client) {
|
|
77
97
|
const namespace = client?.session;
|
|
78
98
|
if (!namespace)
|
|
79
99
|
return undefined;
|
|
80
100
|
if (typeof namespace.active === "function") {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (value)
|
|
86
|
-
ids.add(id);
|
|
87
|
-
}
|
|
88
|
-
return ids;
|
|
101
|
+
const result = await withTimeout(() => namespace.active());
|
|
102
|
+
const data = result && typeof result === "object" ? result.data : undefined;
|
|
103
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
104
|
+
return new Set(Object.keys(data));
|
|
89
105
|
}
|
|
90
106
|
}
|
|
91
107
|
if (typeof namespace.status === "function") {
|
|
92
|
-
const
|
|
93
|
-
|
|
108
|
+
const result = await withTimeout(() => namespace.status());
|
|
109
|
+
const data = result && typeof result === "object" ? result.data : undefined;
|
|
110
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
94
111
|
const ids = new Set();
|
|
95
|
-
for (const [id, value] of Object.entries(
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
112
|
+
for (const [id, value] of Object.entries(data)) {
|
|
113
|
+
const type = value && typeof value === "object" ? (value.type ?? value.status) : value;
|
|
114
|
+
if (type === undefined || type === "busy" || type === "retry")
|
|
98
115
|
ids.add(id);
|
|
99
116
|
}
|
|
100
117
|
return ids;
|
|
@@ -102,6 +119,96 @@ export async function fetchActiveSessionIDs(client) {
|
|
|
102
119
|
}
|
|
103
120
|
return undefined;
|
|
104
121
|
}
|
|
122
|
+
export function loadAvSidecar(globalConfigDir) {
|
|
123
|
+
const parents = new Map();
|
|
124
|
+
if (!globalConfigDir)
|
|
125
|
+
return { parents };
|
|
126
|
+
try {
|
|
127
|
+
const path = `${globalConfigDir.replace(/[\\/]+$/, "")}/agent-variants.jsonc`;
|
|
128
|
+
if (!existsSync(path))
|
|
129
|
+
return { parents };
|
|
130
|
+
const raw = readFileSync(path, "utf8");
|
|
131
|
+
const json = JSON.parse(stripJsoncComments(raw));
|
|
132
|
+
for (const [parent, entry] of Object.entries(json.agents ?? {})) {
|
|
133
|
+
if (!entry || typeof entry !== "object")
|
|
134
|
+
continue;
|
|
135
|
+
parents.set(parent, { disableBase: entry.disable_base === true && entry.disable !== true });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Not our file to complain about - AV may simply not be installed.
|
|
140
|
+
}
|
|
141
|
+
return { parents };
|
|
142
|
+
}
|
|
143
|
+
function stripJsoncComments(text) {
|
|
144
|
+
// Block comments, line comments, and trailing commas before } or ].
|
|
145
|
+
return text
|
|
146
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
147
|
+
.replace(/(^|[^:"'\\])\/\/.*$/gm, "$1")
|
|
148
|
+
.replace(/,(\s*[}\]])/g, "$1");
|
|
149
|
+
}
|
|
150
|
+
const ATTRIBUTION_PAGE_SIZE = 100;
|
|
151
|
+
const ATTRIBUTION_PAGES = 5;
|
|
152
|
+
export async function fetchVariantAttribution(client, parentSessionID, directory, avParents) {
|
|
153
|
+
const map = new Map();
|
|
154
|
+
if (avParents.size === 0 || !parentSessionID)
|
|
155
|
+
return map;
|
|
156
|
+
const namespace = client?.session;
|
|
157
|
+
if (!namespace || typeof namespace.messages !== "function")
|
|
158
|
+
return map;
|
|
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
|
+
}
|
|
199
|
+
}
|
|
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;
|
|
209
|
+
}
|
|
210
|
+
return map;
|
|
211
|
+
}
|
|
105
212
|
/** Builds the recursive subagent tree for `rootID`. Unknown-parent sessions
|
|
106
213
|
* are ignored (not part of this session's tree). Guarded against cycles. */
|
|
107
214
|
export function buildSessionTree(sessions, rootID) {
|
|
@@ -200,13 +307,30 @@ export function isHiddenAgent(agent, hidden, configAgents) {
|
|
|
200
307
|
return true;
|
|
201
308
|
return !configAgents.has(agent) && !BUILTIN_AGENTS.has(agent) && !agent.startsWith("av:");
|
|
202
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
|
+
}
|
|
203
323
|
/** Deletes one session through whichever delete method the host client
|
|
204
324
|
* exposes (`session.delete`, `session.remove`). Returns false when no
|
|
205
|
-
* 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. */
|
|
206
329
|
export async function deleteSession(client, sessionID) {
|
|
207
330
|
const namespace = client?.session;
|
|
208
331
|
if (!namespace)
|
|
209
332
|
return false;
|
|
333
|
+
const isErrorEnvelope = (value) => !!value && typeof value === "object" && "error" in value && !("data" in value);
|
|
210
334
|
const shapes = [];
|
|
211
335
|
if (typeof namespace.delete === "function") {
|
|
212
336
|
shapes.push(() => namespace.delete({ sessionID }), () => namespace.delete({ path: { sessionID } }), () => namespace.delete({ path: { id: sessionID } }), () => namespace.delete({ id: sessionID }));
|
|
@@ -216,8 +340,11 @@ export async function deleteSession(client, sessionID) {
|
|
|
216
340
|
}
|
|
217
341
|
for (const call of shapes) {
|
|
218
342
|
const result = await withTimeout(call);
|
|
219
|
-
if (result
|
|
220
|
-
|
|
343
|
+
if (result === undefined)
|
|
344
|
+
continue;
|
|
345
|
+
if (isErrorEnvelope(result))
|
|
346
|
+
continue;
|
|
347
|
+
return true;
|
|
221
348
|
}
|
|
222
349
|
return false;
|
|
223
350
|
}
|