@mirrowel/opencode-subagent-explorer 0.1.0-dev.1
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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +20 -0
- package/dist/palette-category.d.ts +30 -0
- package/dist/palette-category.js +72 -0
- package/dist/selfwire.d.ts +57 -0
- package/dist/selfwire.js +321 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.js +16 -0
- package/dist/sessions.d.ts +78 -0
- package/dist/sessions.js +299 -0
- package/dist/tui-host.d.ts +161 -0
- package/dist/tui-host.js +9 -0
- package/dist/tui.d.ts +24 -0
- package/dist/tui.js +1962 -0
- package/dist/v2-server.d.ts +10 -0
- package/dist/v2-server.js +14 -0
- package/dist/v2-tui.d.ts +24 -0
- package/dist/v2-tui.js +357 -0
- package/dist/v2-types.d.ts +270 -0
- package/dist/v2-types.js +10 -0
- package/dist/wizard.d.ts +67 -0
- package/dist/wizard.js +1580 -0
- package/package.json +97 -0
- package/src/index.ts +23 -0
- package/src/palette-category.ts +83 -0
- package/src/selfwire.ts +311 -0
- package/src/server.ts +17 -0
- package/src/sessions.ts +318 -0
- package/src/tui-host.ts +154 -0
- package/src/tui.tsx +75 -0
- package/src/v2-server.ts +17 -0
- package/src/v2-tui.ts +360 -0
- package/src/v2-types.ts +243 -0
- package/src/wizard.tsx +844 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
export type SessionLite = {
|
|
9
|
+
id: string;
|
|
10
|
+
parentID?: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
agent?: string;
|
|
13
|
+
directory?: string;
|
|
14
|
+
created?: number;
|
|
15
|
+
updated?: number;
|
|
16
|
+
};
|
|
17
|
+
export type SessionTreeNode = {
|
|
18
|
+
session: SessionLite;
|
|
19
|
+
depth: number;
|
|
20
|
+
children: SessionTreeNode[];
|
|
21
|
+
};
|
|
22
|
+
export type CleanupPlan = {
|
|
23
|
+
/** Idle sessions, ordered leaf-first so children die before parents. */
|
|
24
|
+
deletable: SessionLite[];
|
|
25
|
+
/** Sessions spared because they are running right now. */
|
|
26
|
+
sparedActive: SessionLite[];
|
|
27
|
+
};
|
|
28
|
+
/** OpenCode's built-in agent ids (v1): anything else that is not in the
|
|
29
|
+
* user's config was registered at runtime (plugins) and is treated as
|
|
30
|
+
* hidden/background for display purposes. */
|
|
31
|
+
export declare const BUILTIN_AGENTS: Set<string>;
|
|
32
|
+
export declare function withTimeout<T>(fn: () => Promise<T>, ms?: number): Promise<T | undefined>;
|
|
33
|
+
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
|
+
export declare function fetchSessions(client: unknown, directory: string): Promise<SessionLite[] | undefined>;
|
|
37
|
+
/** Running session ids via `session.active()` with the `session.status()`
|
|
38
|
+
* fallback (busy/retry = running). `undefined` when neither exists or both
|
|
39
|
+
* fail — callers must treat that as "unknown", never "idle". */
|
|
40
|
+
export declare function fetchActiveSessionIDs(client: unknown): Promise<Set<string> | undefined>;
|
|
41
|
+
/** Builds the recursive subagent tree for `rootID`. Unknown-parent sessions
|
|
42
|
+
* are ignored (not part of this session's tree). Guarded against cycles. */
|
|
43
|
+
export declare function buildSessionTree(sessions: SessionLite[], rootID: string): SessionTreeNode[];
|
|
44
|
+
/** Flattens a tree depth-first, parents before their children. */
|
|
45
|
+
export declare function collectSubtree(nodes: SessionTreeNode[]): SessionLite[];
|
|
46
|
+
/** Splits a tree into deletable (idle) and spared (running) sessions.
|
|
47
|
+
* Deletable order is leaf-first: children are removed before the parents
|
|
48
|
+
* they hang off, so no cascade can leave dangling rows. */
|
|
49
|
+
export declare function cleanupTargets(nodes: SessionTreeNode[], activeIDs: Set<string> | undefined): CleanupPlan;
|
|
50
|
+
/** The current session: the router's session id when the host provides one,
|
|
51
|
+
* else the most recently updated session in the directory. */
|
|
52
|
+
export declare function currentSessionID(explicit: string | undefined, sessions: SessionLite[]): string | undefined;
|
|
53
|
+
/** Hidden/background classification: config agents flagged hidden, plus
|
|
54
|
+
* runtime-registered agents OpenCode does not expose in the config. */
|
|
55
|
+
export declare function hiddenAgentIDs(config: unknown): Set<string>;
|
|
56
|
+
/** All agent ids the host knows about (config-declared). Anything absent
|
|
57
|
+
* from this set and from the built-ins was registered at runtime. */
|
|
58
|
+
export declare function configAgentIDs(config: unknown): Set<string>;
|
|
59
|
+
/** A session counts as hidden/background when its agent is flagged hidden
|
|
60
|
+
* in the config, or the agent was registered at runtime (not config-known,
|
|
61
|
+
* not built-in, not an agent-variants clone). */
|
|
62
|
+
export declare function isHiddenAgent(agent: string | undefined, hidden: Set<string>, configAgents: Set<string>): boolean;
|
|
63
|
+
/** Deletes one session through whichever delete method the host client
|
|
64
|
+
* exposes (`session.delete`, `session.remove`). Returns false when no
|
|
65
|
+
* method exists or the call failed. */
|
|
66
|
+
export declare function deleteSession(client: unknown, sessionID: string): Promise<boolean>;
|
|
67
|
+
export type SessionDetails = {
|
|
68
|
+
messageCount: number;
|
|
69
|
+
model?: string;
|
|
70
|
+
firstPrompt?: string;
|
|
71
|
+
};
|
|
72
|
+
/** Cheap detail read for the [i] view: last assistant model + first user
|
|
73
|
+
* prompt, from a bounded tail of the session's messages. */
|
|
74
|
+
export declare function fetchSessionDetails(client: unknown, sessionID: string): Promise<SessionDetails | undefined>;
|
|
75
|
+
export declare function formatAge(timestamp: number | undefined, now?: number): string;
|
|
76
|
+
/** Groups sessions for the session picker: every session in the directory
|
|
77
|
+
* that has children is a potential explorer root. */
|
|
78
|
+
export declare function parentCandidates(sessions: SessionLite[]): SessionLite[];
|
package/dist/sessions.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
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
|
+
const CALL_TIMEOUT_MS = 5000;
|
|
9
|
+
/** OpenCode's built-in agent ids (v1): anything else that is not in the
|
|
10
|
+
* user's config was registered at runtime (plugins) and is treated as
|
|
11
|
+
* hidden/background for display purposes. */
|
|
12
|
+
export const BUILTIN_AGENTS = new Set(["build", "plan"]);
|
|
13
|
+
export function withTimeout(fn, ms = CALL_TIMEOUT_MS) {
|
|
14
|
+
return Promise.race([Promise.resolve().then(fn).catch(() => undefined), new Promise((resolve) => setTimeout(() => resolve(undefined), ms))]);
|
|
15
|
+
}
|
|
16
|
+
function unwrap(value) {
|
|
17
|
+
if (value && typeof value === "object" && "data" in value)
|
|
18
|
+
return value.data;
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function toMillis(value) {
|
|
22
|
+
if (typeof value === "number")
|
|
23
|
+
return value;
|
|
24
|
+
if (typeof value === "string") {
|
|
25
|
+
const parsed = Date.parse(value);
|
|
26
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
export function normalizeSession(raw) {
|
|
31
|
+
if (!raw || typeof raw !== "object")
|
|
32
|
+
return undefined;
|
|
33
|
+
const record = raw;
|
|
34
|
+
const id = typeof record.id === "string" ? record.id : undefined;
|
|
35
|
+
if (!id)
|
|
36
|
+
return undefined;
|
|
37
|
+
const agentRaw = record.agent;
|
|
38
|
+
const agent = typeof agentRaw === "string"
|
|
39
|
+
? agentRaw
|
|
40
|
+
: typeof agentRaw?.id === "string"
|
|
41
|
+
? agentRaw.id
|
|
42
|
+
: typeof agentRaw?.agentID === "string"
|
|
43
|
+
? agentRaw.agentID
|
|
44
|
+
: undefined;
|
|
45
|
+
const time = record.time && typeof record.time === "object" ? record.time : {};
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
parentID: typeof record.parentID === "string" ? record.parentID : undefined,
|
|
49
|
+
title: typeof record.title === "string" && record.title !== "" ? record.title : undefined,
|
|
50
|
+
agent,
|
|
51
|
+
directory: typeof record.directory === "string" ? record.directory : undefined,
|
|
52
|
+
created: toMillis(time.created ?? record.time_created),
|
|
53
|
+
updated: toMillis(time.updated ?? record.time_updated),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** 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
|
+
export async function fetchSessions(client, directory) {
|
|
59
|
+
const namespace = client?.session;
|
|
60
|
+
if (!namespace?.list)
|
|
61
|
+
return undefined;
|
|
62
|
+
const calls = [
|
|
63
|
+
() => namespace.list({ directory }),
|
|
64
|
+
() => namespace.list({ query: { directory } }),
|
|
65
|
+
];
|
|
66
|
+
for (const call of calls) {
|
|
67
|
+
const result = unwrap(await withTimeout(call));
|
|
68
|
+
if (Array.isArray(result))
|
|
69
|
+
return result.map(normalizeSession).filter((s) => !!s);
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
/** Running session ids via `session.active()` with the `session.status()`
|
|
74
|
+
* fallback (busy/retry = running). `undefined` when neither exists or both
|
|
75
|
+
* fail — callers must treat that as "unknown", never "idle". */
|
|
76
|
+
export async function fetchActiveSessionIDs(client) {
|
|
77
|
+
const namespace = client?.session;
|
|
78
|
+
if (!namespace)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (typeof namespace.active === "function") {
|
|
81
|
+
const active = unwrap(await withTimeout(() => namespace.active()));
|
|
82
|
+
if (active && typeof active === "object") {
|
|
83
|
+
const ids = new Set();
|
|
84
|
+
for (const [id, value] of Object.entries(active)) {
|
|
85
|
+
if (value)
|
|
86
|
+
ids.add(id);
|
|
87
|
+
}
|
|
88
|
+
return ids;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (typeof namespace.status === "function") {
|
|
92
|
+
const status = unwrap(await withTimeout(() => namespace.status()));
|
|
93
|
+
if (status && typeof status === "object") {
|
|
94
|
+
const ids = new Set();
|
|
95
|
+
for (const [id, value] of Object.entries(status)) {
|
|
96
|
+
const state = value?.status ?? value;
|
|
97
|
+
if (state === "busy" || state === "retry")
|
|
98
|
+
ids.add(id);
|
|
99
|
+
}
|
|
100
|
+
return ids;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
/** Builds the recursive subagent tree for `rootID`. Unknown-parent sessions
|
|
106
|
+
* are ignored (not part of this session's tree). Guarded against cycles. */
|
|
107
|
+
export function buildSessionTree(sessions, rootID) {
|
|
108
|
+
const byParent = new Map();
|
|
109
|
+
for (const session of sessions) {
|
|
110
|
+
if (!session.parentID)
|
|
111
|
+
continue;
|
|
112
|
+
const list = byParent.get(session.parentID) ?? [];
|
|
113
|
+
list.push(session);
|
|
114
|
+
byParent.set(session.parentID, list);
|
|
115
|
+
}
|
|
116
|
+
const createdOf = (session) => session.created ?? 0;
|
|
117
|
+
const sortSiblings = (list) => [...list].sort((a, b) => createdOf(a) - createdOf(b));
|
|
118
|
+
const visited = new Set([rootID]);
|
|
119
|
+
const build = (list, depth) => sortSiblings(list).map((session) => {
|
|
120
|
+
if (visited.has(session.id))
|
|
121
|
+
return { session, depth, children: [] };
|
|
122
|
+
visited.add(session.id);
|
|
123
|
+
const children = build(byParent.get(session.id) ?? [], depth + 1);
|
|
124
|
+
return { session, depth, children };
|
|
125
|
+
});
|
|
126
|
+
return build(byParent.get(rootID) ?? [], 0);
|
|
127
|
+
}
|
|
128
|
+
/** Flattens a tree depth-first, parents before their children. */
|
|
129
|
+
export function collectSubtree(nodes) {
|
|
130
|
+
const out = [];
|
|
131
|
+
const walk = (list) => {
|
|
132
|
+
for (const node of list) {
|
|
133
|
+
out.push(node.session);
|
|
134
|
+
walk(node.children);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
walk(nodes);
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
/** Splits a tree into deletable (idle) and spared (running) sessions.
|
|
141
|
+
* Deletable order is leaf-first: children are removed before the parents
|
|
142
|
+
* they hang off, so no cascade can leave dangling rows. */
|
|
143
|
+
export function cleanupTargets(nodes, activeIDs) {
|
|
144
|
+
const deletable = [];
|
|
145
|
+
const sparedActive = [];
|
|
146
|
+
const walk = (list) => {
|
|
147
|
+
for (const node of list) {
|
|
148
|
+
walk(node.children);
|
|
149
|
+
if (activeIDs?.has(node.session.id))
|
|
150
|
+
sparedActive.push(node.session);
|
|
151
|
+
else
|
|
152
|
+
deletable.push(node.session);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
walk(nodes);
|
|
156
|
+
return { deletable, sparedActive };
|
|
157
|
+
}
|
|
158
|
+
/** The current session: the router's session id when the host provides one,
|
|
159
|
+
* else the most recently updated session in the directory. */
|
|
160
|
+
export function currentSessionID(explicit, sessions) {
|
|
161
|
+
if (explicit)
|
|
162
|
+
return explicit;
|
|
163
|
+
let newest;
|
|
164
|
+
for (const session of sessions) {
|
|
165
|
+
const stamp = session.updated ?? session.created ?? 0;
|
|
166
|
+
const newestStamp = newest?.updated ?? newest?.created ?? 0;
|
|
167
|
+
if (!newest || stamp > newestStamp)
|
|
168
|
+
newest = session;
|
|
169
|
+
}
|
|
170
|
+
return newest?.id;
|
|
171
|
+
}
|
|
172
|
+
/** Hidden/background classification: config agents flagged hidden, plus
|
|
173
|
+
* runtime-registered agents OpenCode does not expose in the config. */
|
|
174
|
+
export function hiddenAgentIDs(config) {
|
|
175
|
+
const agents = config?.agent;
|
|
176
|
+
const hidden = new Set();
|
|
177
|
+
if (!agents || typeof agents !== "object")
|
|
178
|
+
return hidden;
|
|
179
|
+
for (const [id, entry] of Object.entries(agents)) {
|
|
180
|
+
if (entry && typeof entry === "object" && entry.hidden === true)
|
|
181
|
+
hidden.add(id);
|
|
182
|
+
}
|
|
183
|
+
return hidden;
|
|
184
|
+
}
|
|
185
|
+
/** All agent ids the host knows about (config-declared). Anything absent
|
|
186
|
+
* from this set and from the built-ins was registered at runtime. */
|
|
187
|
+
export function configAgentIDs(config) {
|
|
188
|
+
const agents = config?.agent;
|
|
189
|
+
if (!agents || typeof agents !== "object")
|
|
190
|
+
return new Set();
|
|
191
|
+
return new Set(Object.keys(agents));
|
|
192
|
+
}
|
|
193
|
+
/** A session counts as hidden/background when its agent is flagged hidden
|
|
194
|
+
* in the config, or the agent was registered at runtime (not config-known,
|
|
195
|
+
* not built-in, not an agent-variants clone). */
|
|
196
|
+
export function isHiddenAgent(agent, hidden, configAgents) {
|
|
197
|
+
if (!agent)
|
|
198
|
+
return false;
|
|
199
|
+
if (hidden.has(agent))
|
|
200
|
+
return true;
|
|
201
|
+
return !configAgents.has(agent) && !BUILTIN_AGENTS.has(agent) && !agent.startsWith("av:");
|
|
202
|
+
}
|
|
203
|
+
/** Deletes one session through whichever delete method the host client
|
|
204
|
+
* exposes (`session.delete`, `session.remove`). Returns false when no
|
|
205
|
+
* method exists or the call failed. */
|
|
206
|
+
export async function deleteSession(client, sessionID) {
|
|
207
|
+
const namespace = client?.session;
|
|
208
|
+
if (!namespace)
|
|
209
|
+
return false;
|
|
210
|
+
const shapes = [];
|
|
211
|
+
if (typeof namespace.delete === "function") {
|
|
212
|
+
shapes.push(() => namespace.delete({ sessionID }), () => namespace.delete({ path: { sessionID } }), () => namespace.delete({ path: { id: sessionID } }), () => namespace.delete({ id: sessionID }));
|
|
213
|
+
}
|
|
214
|
+
if (typeof namespace.remove === "function") {
|
|
215
|
+
shapes.push(() => namespace.remove({ sessionID }), () => namespace.remove({ path: { sessionID } }));
|
|
216
|
+
}
|
|
217
|
+
for (const call of shapes) {
|
|
218
|
+
const result = await withTimeout(call);
|
|
219
|
+
if (result !== undefined)
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
/** Cheap detail read for the [i] view: last assistant model + first user
|
|
225
|
+
* prompt, from a bounded tail of the session's messages. */
|
|
226
|
+
export async function fetchSessionDetails(client, sessionID) {
|
|
227
|
+
const namespace = client?.session;
|
|
228
|
+
if (!namespace?.messages)
|
|
229
|
+
return undefined;
|
|
230
|
+
const calls = [
|
|
231
|
+
() => namespace.messages({ sessionID, limit: 50 }),
|
|
232
|
+
() => namespace.messages({ path: { id: sessionID }, query: { limit: 50 } }),
|
|
233
|
+
];
|
|
234
|
+
for (const call of calls) {
|
|
235
|
+
const result = unwrap(await withTimeout(call, 4000));
|
|
236
|
+
const messages = Array.isArray(result) ? result : undefined;
|
|
237
|
+
if (!messages)
|
|
238
|
+
continue;
|
|
239
|
+
let model;
|
|
240
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
241
|
+
const info = messages[i]?.info ?? messages[i];
|
|
242
|
+
const modelRef = info?.model;
|
|
243
|
+
if (modelRef?.providerID && modelRef?.modelID) {
|
|
244
|
+
model = `${modelRef.providerID}/${modelRef.modelID}${modelRef.variantID ? ` @${modelRef.variantID}` : ""}`;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
let firstPrompt;
|
|
249
|
+
for (const message of messages) {
|
|
250
|
+
const info = message?.info ?? message;
|
|
251
|
+
if (info?.role !== "user")
|
|
252
|
+
continue;
|
|
253
|
+
const text = firstTextOf(message.parts);
|
|
254
|
+
if (text) {
|
|
255
|
+
firstPrompt = text.length > 400 ? `${text.slice(0, 400)}…` : text;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { messageCount: messages.length, model, firstPrompt };
|
|
260
|
+
}
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
function firstTextOf(parts) {
|
|
264
|
+
if (!Array.isArray(parts))
|
|
265
|
+
return undefined;
|
|
266
|
+
for (const part of parts) {
|
|
267
|
+
const text = part?.text ?? part?.part?.text;
|
|
268
|
+
if (typeof text === "string" && text.trim() !== "")
|
|
269
|
+
return text.trim();
|
|
270
|
+
}
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
export function formatAge(timestamp, now = Date.now()) {
|
|
274
|
+
if (!timestamp)
|
|
275
|
+
return "—";
|
|
276
|
+
const delta = Math.max(0, now - timestamp);
|
|
277
|
+
const minutes = Math.floor(delta / 60_000);
|
|
278
|
+
if (minutes < 1)
|
|
279
|
+
return "just now";
|
|
280
|
+
if (minutes < 60)
|
|
281
|
+
return `${minutes}m ago`;
|
|
282
|
+
const hours = Math.floor(minutes / 60);
|
|
283
|
+
if (hours < 24)
|
|
284
|
+
return `${hours}h ago`;
|
|
285
|
+
const days = Math.floor(hours / 24);
|
|
286
|
+
return `${days}d ago`;
|
|
287
|
+
}
|
|
288
|
+
/** Groups sessions for the session picker: every session in the directory
|
|
289
|
+
* that has children is a potential explorer root. */
|
|
290
|
+
export function parentCandidates(sessions) {
|
|
291
|
+
const parents = new Set();
|
|
292
|
+
for (const session of sessions) {
|
|
293
|
+
if (session.parentID)
|
|
294
|
+
parents.add(session.parentID);
|
|
295
|
+
}
|
|
296
|
+
return sessions
|
|
297
|
+
.filter((session) => parents.has(session.id))
|
|
298
|
+
.sort((a, b) => (b.updated ?? b.created ?? 0) - (a.updated ?? a.created ?? 0));
|
|
299
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-agnostic TUI API surface consumed by the wizard (wizard.tsx) and by
|
|
3
|
+
* embedding hosts (Config Studio). This is the exact bounded subset of the
|
|
4
|
+
* OpenCode v1 `TuiPluginApi` the wizard uses; a v1 api instance satisfies it
|
|
5
|
+
* structurally, and `v2-tui.ts` implements it over the OpenCode v2 TUI
|
|
6
|
+
* plugin context. The wizard never touches anything outside this interface,
|
|
7
|
+
* which is what lets one compiled wizard bundle run on both hosts.
|
|
8
|
+
*/
|
|
9
|
+
export type TuiDialogSelectOption<Value = string> = {
|
|
10
|
+
title: string;
|
|
11
|
+
value: Value;
|
|
12
|
+
description?: string;
|
|
13
|
+
category?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type TuiDialogSelectProps<Value = string> = {
|
|
17
|
+
title: string;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
options: TuiDialogSelectOption<Value>[];
|
|
20
|
+
current?: Value;
|
|
21
|
+
flat?: boolean;
|
|
22
|
+
onSelect: (option: TuiDialogSelectOption<Value>) => void;
|
|
23
|
+
};
|
|
24
|
+
export type TuiDialogPromptProps = {
|
|
25
|
+
title: string;
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
value?: string;
|
|
28
|
+
onConfirm: (value: string) => void;
|
|
29
|
+
onCancel?: () => void;
|
|
30
|
+
};
|
|
31
|
+
export type TuiDialogConfirmProps = {
|
|
32
|
+
title: string;
|
|
33
|
+
message: string;
|
|
34
|
+
confirmLabel?: string;
|
|
35
|
+
onConfirm: () => void;
|
|
36
|
+
onCancel?: () => void;
|
|
37
|
+
};
|
|
38
|
+
export type TuiDialogAlertProps = {
|
|
39
|
+
title: string;
|
|
40
|
+
message: string;
|
|
41
|
+
onConfirm?: () => void;
|
|
42
|
+
};
|
|
43
|
+
export type TuiHostTheme = {
|
|
44
|
+
text: any;
|
|
45
|
+
textMuted: any;
|
|
46
|
+
background: any;
|
|
47
|
+
backgroundPanel: any;
|
|
48
|
+
primary: any;
|
|
49
|
+
secondary: any;
|
|
50
|
+
accent: any;
|
|
51
|
+
success: any;
|
|
52
|
+
warning: any;
|
|
53
|
+
error: any;
|
|
54
|
+
info: any;
|
|
55
|
+
};
|
|
56
|
+
export type TuiHostKeyContext = {
|
|
57
|
+
event?: {
|
|
58
|
+
preventDefault?: () => void;
|
|
59
|
+
stopPropagation?: () => void;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export type TuiHostLayerCommand = {
|
|
63
|
+
name: string;
|
|
64
|
+
title?: string;
|
|
65
|
+
desc?: string;
|
|
66
|
+
run: (ctx: TuiHostKeyContext) => void;
|
|
67
|
+
};
|
|
68
|
+
export type TuiHostKeymapLayer = {
|
|
69
|
+
mode?: string;
|
|
70
|
+
priority?: number;
|
|
71
|
+
commands: readonly TuiHostLayerCommand[];
|
|
72
|
+
bindings: readonly {
|
|
73
|
+
key: string;
|
|
74
|
+
cmd: string;
|
|
75
|
+
desc?: string;
|
|
76
|
+
}[];
|
|
77
|
+
};
|
|
78
|
+
/** Command descriptor shared with the palette-category registry. Carries both the v1 (`category`) and v2 (`group`) grouping fields. */
|
|
79
|
+
export type TuiHostPaletteCommand = {
|
|
80
|
+
name: string;
|
|
81
|
+
title: string;
|
|
82
|
+
desc?: string;
|
|
83
|
+
category?: string;
|
|
84
|
+
group?: string;
|
|
85
|
+
run: () => void | Promise<void>;
|
|
86
|
+
};
|
|
87
|
+
/** Minimal provider/model shape the wizard reads; the v1 SDK's richer Provider remains structurally assignable. */
|
|
88
|
+
export type TuiHostProvider = {
|
|
89
|
+
id: string;
|
|
90
|
+
name: string;
|
|
91
|
+
models: Record<string, {
|
|
92
|
+
id: string;
|
|
93
|
+
name: string;
|
|
94
|
+
variants?: Record<string, unknown>;
|
|
95
|
+
}>;
|
|
96
|
+
};
|
|
97
|
+
export type TuiHostApi = {
|
|
98
|
+
/** Host version marker (v2 adapter sets 2; v1 leaves undefined). */
|
|
99
|
+
hostVersion?: 1 | 2;
|
|
100
|
+
app?: {
|
|
101
|
+
version?: string;
|
|
102
|
+
};
|
|
103
|
+
/** Raw host client (v1 SDK / v2 OpenCodeClient). Embedders feature-detect. */
|
|
104
|
+
client?: unknown;
|
|
105
|
+
kv: {
|
|
106
|
+
get<Value = unknown>(key: string, fallback?: Value): Value;
|
|
107
|
+
set(key: string, value: unknown): void;
|
|
108
|
+
readonly ready: boolean;
|
|
109
|
+
};
|
|
110
|
+
lifecycle: {
|
|
111
|
+
onDispose(fn: () => void): void;
|
|
112
|
+
};
|
|
113
|
+
mode: {
|
|
114
|
+
push(mode: string): () => void;
|
|
115
|
+
};
|
|
116
|
+
renderer: {
|
|
117
|
+
root?: any;
|
|
118
|
+
};
|
|
119
|
+
state: {
|
|
120
|
+
config: any;
|
|
121
|
+
provider: readonly TuiHostProvider[];
|
|
122
|
+
path?: {
|
|
123
|
+
config: string;
|
|
124
|
+
directory: string;
|
|
125
|
+
worktree: string;
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
/** Router's active session id (v1: route.current walk; v2 adapter maps its
|
|
129
|
+
* own notion). Optional — the explorer falls back to the newest session. */
|
|
130
|
+
currentSessionID?: () => string | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* Dialog sizing scope. "standalone" (default): the wizard uses its OWN
|
|
133
|
+
* size kv keys and offers the size picker. "embedded": the wizard reads
|
|
134
|
+
* the embedding host's keys (Config Studio's ui-width/ui-height-percent)
|
|
135
|
+
* and hides the picker — the host's dialog settings rule.
|
|
136
|
+
*/
|
|
137
|
+
dialogScope?: "standalone" | "embedded";
|
|
138
|
+
theme: {
|
|
139
|
+
readonly current: TuiHostTheme;
|
|
140
|
+
};
|
|
141
|
+
ui: {
|
|
142
|
+
toast(input: {
|
|
143
|
+
variant?: string;
|
|
144
|
+
title?: string;
|
|
145
|
+
message: string;
|
|
146
|
+
duration?: number;
|
|
147
|
+
}): void;
|
|
148
|
+
dialog: {
|
|
149
|
+
replace(renderer: unknown, onClose?: () => void): void;
|
|
150
|
+
clear(): void;
|
|
151
|
+
setSize(size: string): void;
|
|
152
|
+
};
|
|
153
|
+
DialogSelect<Value = string>(props: TuiDialogSelectProps<Value>): void;
|
|
154
|
+
DialogPrompt(props: TuiDialogPromptProps): void;
|
|
155
|
+
DialogConfirm(props: TuiDialogConfirmProps): void;
|
|
156
|
+
DialogAlert(props: TuiDialogAlertProps): void;
|
|
157
|
+
};
|
|
158
|
+
keymap: {
|
|
159
|
+
registerLayer(layer: TuiHostKeymapLayer): () => void;
|
|
160
|
+
};
|
|
161
|
+
};
|
package/dist/tui-host.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-agnostic TUI API surface consumed by the wizard (wizard.tsx) and by
|
|
3
|
+
* embedding hosts (Config Studio). This is the exact bounded subset of the
|
|
4
|
+
* OpenCode v1 `TuiPluginApi` the wizard uses; a v1 api instance satisfies it
|
|
5
|
+
* structurally, and `v2-tui.ts` implements it over the OpenCode v2 TUI
|
|
6
|
+
* plugin context. The wizard never touches anything outside this interface,
|
|
7
|
+
* which is what lets one compiled wizard bundle run on both hosts.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/dist/tui.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
/**
|
|
3
|
+
* Subagent Explorer TUI plugin entry.
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper: registers the palette/slash command and bootstraps the
|
|
6
|
+
* explorer wizard library (wizard.tsx). Config Studio imports the wizard
|
|
7
|
+
* library directly when the module is embedded.
|
|
8
|
+
*/
|
|
9
|
+
import type { TuiPlugin } from "@opencode-ai/plugin/tui";
|
|
10
|
+
/**
|
|
11
|
+
* Dual-target TUI entry.
|
|
12
|
+
*
|
|
13
|
+
* - OpenCode v1 (strict loader) requires `default.tui` to be a function;
|
|
14
|
+
* excess keys are ignored.
|
|
15
|
+
* - OpenCode v2 requires `default` to be `{ id, setup }`; its hand-written
|
|
16
|
+
* plugin check only inspects `id` + `setup`, so the legacy `tui` factory
|
|
17
|
+
* rides along harmlessly.
|
|
18
|
+
*/
|
|
19
|
+
declare const _default: {
|
|
20
|
+
id: string;
|
|
21
|
+
tui: TuiPlugin;
|
|
22
|
+
setup: import("./v2-types.js").V2TuiSetup;
|
|
23
|
+
};
|
|
24
|
+
export default _default;
|