@hueyexe/opencode-ensemble 0.15.2 → 0.16.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/README.md +4 -4
- package/dist/index.js +1648 -335
- package/package.json +3 -3
- package/src/activity.ts +215 -0
- package/src/client.ts +3 -1
- package/src/dashboard-html.ts +1 -0
- package/src/dashboard-js-core.ts +1 -1
- package/src/dashboard-js-events.ts +26 -0
- package/src/dashboard-js-render.ts +94 -0
- package/src/dashboard.ts +123 -5
- package/src/index.ts +29 -2
- package/src/tools/team-claim.ts +33 -18
- package/src/tools/team-spawn.ts +41 -10
- package/src/tools/team-tasks-complete.ts +27 -3
- package/src/types.ts +2 -0
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { log, initLog } from "./log"
|
|
|
16
16
|
import { findTeamBySession } from "./types"
|
|
17
17
|
import { loadConfig } from "./config"
|
|
18
18
|
import { ProgressTracker } from "./progress"
|
|
19
|
+
import { ActivityBuffer, recordFromV2Event, recordFromToolBefore, recordFromToolAfter } from "./activity"
|
|
19
20
|
import { startDashboard } from "./dashboard"
|
|
20
21
|
import { executeTeamCreate } from "./tools/team-create"
|
|
21
22
|
import { executeTeamSpawn } from "./tools/team-spawn"
|
|
@@ -59,6 +60,7 @@ const plugin: Plugin = async (input) => {
|
|
|
59
60
|
const purgeApprovals = new PendingPurgeApprovals()
|
|
60
61
|
const nudgedMembers = new Set<string>()
|
|
61
62
|
const progressTracker = new ProgressTracker()
|
|
63
|
+
const activityBuffer = new ActivityBuffer()
|
|
62
64
|
const wakeLeadTimestamps = new Map<string, number>()
|
|
63
65
|
const WAKE_LEAD_COOLDOWN_MS = 5000
|
|
64
66
|
|
|
@@ -104,7 +106,7 @@ const plugin: Plugin = async (input) => {
|
|
|
104
106
|
|
|
105
107
|
// Start dashboard server (main instance only, not worktree instances)
|
|
106
108
|
if (config.dashboardPort !== 0) {
|
|
107
|
-
startDashboard(db, config.dashboardPort).catch((err) => {
|
|
109
|
+
startDashboard(db, config.dashboardPort, { activityBuffer, client }).catch((err) => {
|
|
108
110
|
log(`init:dashboard:failed err=${err instanceof Error ? err.message : String(err)}`)
|
|
109
111
|
})
|
|
110
112
|
}
|
|
@@ -304,6 +306,15 @@ const plugin: Plugin = async (input) => {
|
|
|
304
306
|
}
|
|
305
307
|
}
|
|
306
308
|
}
|
|
309
|
+
|
|
310
|
+
// Capture activity for dashboard verbose view (best-effort, v2 event types)
|
|
311
|
+
// Only shell and step events are recorded here — tool calls/results are
|
|
312
|
+
// recorded via tool.execute.before/after to avoid duplicate entries.
|
|
313
|
+
recordFromV2Event(
|
|
314
|
+
event as unknown as { type: string; properties: { sessionID?: string; tool?: string; input?: string; content?: string; title?: string; error?: string; command?: string; exitCode?: number; cost?: number; tokens?: { input?: number; output?: number } } },
|
|
315
|
+
registry,
|
|
316
|
+
activityBuffer,
|
|
317
|
+
)
|
|
307
318
|
},
|
|
308
319
|
|
|
309
320
|
// Sub-agent isolation + rate limiting hook
|
|
@@ -315,12 +326,16 @@ const plugin: Plugin = async (input) => {
|
|
|
315
326
|
await rateLimiter.waitForToken()
|
|
316
327
|
}
|
|
317
328
|
}
|
|
329
|
+
// Record tool call activity for team member sessions
|
|
330
|
+
recordFromToolBefore(input, registry, activityBuffer)
|
|
318
331
|
},
|
|
319
332
|
|
|
320
333
|
"tool.execute.after": async (input, output) => {
|
|
321
334
|
if (input.tool === "question") {
|
|
322
335
|
purgeApprovals.recordQuestionAnswer(input.sessionID, output.output, input.args)
|
|
323
336
|
}
|
|
337
|
+
// Record tool result activity for team member sessions
|
|
338
|
+
recordFromToolAfter(input, output, registry, activityBuffer)
|
|
324
339
|
},
|
|
325
340
|
|
|
326
341
|
// System prompt injection — keeps lead aware of team state, reminds teammates of role
|
|
@@ -508,7 +523,10 @@ const plugin: Plugin = async (input) => {
|
|
|
508
523
|
const result = await executeTeamShutdown(deps, args, ctx.sessionID)
|
|
509
524
|
// Clean up progress tracking for the shut-down member
|
|
510
525
|
const member = deps.db.query("SELECT session_id FROM team_member WHERE name = ? AND status IN ('shutdown', 'shutdown_requested')").get(args.member) as { session_id: string } | null
|
|
511
|
-
if (member)
|
|
526
|
+
if (member) {
|
|
527
|
+
progressTracker.remove(member.session_id)
|
|
528
|
+
activityBuffer.remove(member.session_id)
|
|
529
|
+
}
|
|
512
530
|
const hasWarning = result.includes("uncommitted")
|
|
513
531
|
ctx.metadata({ title: hasWarning ? `${args.member} shut down — uncommitted changes` : `${args.member} shut down` })
|
|
514
532
|
return result
|
|
@@ -542,7 +560,16 @@ const plugin: Plugin = async (input) => {
|
|
|
542
560
|
})
|
|
543
561
|
}
|
|
544
562
|
: undefined
|
|
563
|
+
// Collect member session IDs before cleanup so we can clean up activity buffers after
|
|
564
|
+
const teamInfoForCleanup = findTeamBySession(db, registry, ctx.sessionID)
|
|
565
|
+
const memberSessions = teamInfoForCleanup
|
|
566
|
+
? (db.query("SELECT session_id FROM team_member WHERE team_id = ?").all(teamInfoForCleanup.teamId) as Array<{ session_id: string }>).map(m => m.session_id)
|
|
567
|
+
: []
|
|
545
568
|
const result = await executeTeamCleanup(deps, args, ctx.sessionID, undefined, undefined, undefined, config.mergeOnCleanup, undefined, approvePurge)
|
|
569
|
+
// Clean up activity buffers for team members after successful cleanup
|
|
570
|
+
if (!result.includes("uncommitted") && !args.purge) {
|
|
571
|
+
for (const sid of memberSessions) activityBuffer.remove(sid)
|
|
572
|
+
}
|
|
546
573
|
const blocked = result.includes("uncommitted")
|
|
547
574
|
const title = args.purge
|
|
548
575
|
? result.startsWith("No archived teams") ? "No archived teams to purge" : result.startsWith("Purge preview") ? "Purge confirmation required" : "Archived teams purged"
|
package/src/tools/team-claim.ts
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
|
+
import type { Database } from "../db"
|
|
1
2
|
import type { ToolDeps } from "../types"
|
|
2
3
|
import { requireTeamMember } from "./shared"
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Atomically claim a pending task for an assignee. Sets status to
|
|
7
|
+
* 'in_progress' and records the assignee. Throws if the task is missing,
|
|
8
|
+
* blocked, already claimed, or no longer pending. Returns the task content
|
|
9
|
+
* on success.
|
|
10
|
+
*
|
|
11
|
+
* Shared by team_claim and team_spawn's claim_task auto-claim so both paths
|
|
12
|
+
* enforce the same atomic claim invariant.
|
|
13
|
+
*/
|
|
14
|
+
export function claimTask(db: Database, teamId: string, taskId: string, assignee: string): string {
|
|
15
|
+
const task = db.query("SELECT content, status, assignee FROM team_task WHERE id = ? AND team_id = ?")
|
|
16
|
+
.get(taskId, teamId) as { content: string; status: string; assignee: string | null } | null
|
|
17
|
+
if (!task) throw new Error(`Task "${taskId}" not found`)
|
|
18
|
+
if (task.status === "blocked") throw new Error(`Task "${taskId}" is blocked by unresolved dependencies`)
|
|
19
|
+
if (task.status !== "pending") throw new Error(`Task "${taskId}" is not pending (status: ${task.status})`)
|
|
20
|
+
if (task.assignee) throw new Error(`Task "${taskId}" is already claimed by ${task.assignee}`)
|
|
21
|
+
|
|
22
|
+
// Atomic claim: UPDATE only if still pending and unassigned
|
|
23
|
+
const result = db.run(
|
|
24
|
+
"UPDATE team_task SET status = 'in_progress', assignee = ?, time_updated = ? WHERE id = ? AND status = 'pending' AND assignee IS NULL",
|
|
25
|
+
[assignee, Date.now(), taskId]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if (result.changes === 0) {
|
|
29
|
+
throw new Error(`Task "${taskId}" is already claimed (race condition)`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return task.content
|
|
33
|
+
}
|
|
34
|
+
|
|
4
35
|
/**
|
|
5
36
|
* Execute the team_claim tool. Atomically claims a pending task.
|
|
6
37
|
* Rejects if the task is already claimed, blocked, or not pending.
|
|
@@ -13,23 +44,7 @@ export async function executeTeamClaim(
|
|
|
13
44
|
const teamInfo = requireTeamMember(deps, sessionId)
|
|
14
45
|
|
|
15
46
|
const claimerName = teamInfo.role === "lead" ? "lead" : (teamInfo.memberName ?? "unknown")
|
|
47
|
+
const content = claimTask(deps.db, teamInfo.teamId, args.task_id, claimerName)
|
|
16
48
|
|
|
17
|
-
|
|
18
|
-
.get(args.task_id, teamInfo.teamId) as Record<string, unknown> | null
|
|
19
|
-
if (!task) throw new Error(`Task "${args.task_id}" not found`)
|
|
20
|
-
if (task.status === "blocked") throw new Error(`Task "${args.task_id}" is blocked by unresolved dependencies`)
|
|
21
|
-
if (task.status !== "pending") throw new Error(`Task "${args.task_id}" is not pending (status: ${task.status})`)
|
|
22
|
-
if (task.assignee) throw new Error(`Task "${args.task_id}" is already claimed by ${task.assignee}`)
|
|
23
|
-
|
|
24
|
-
// Atomic claim: UPDATE only if still pending and unassigned
|
|
25
|
-
const result = deps.db.run(
|
|
26
|
-
"UPDATE team_task SET status = 'in_progress', assignee = ?, time_updated = ? WHERE id = ? AND status = 'pending' AND assignee IS NULL",
|
|
27
|
-
[claimerName, Date.now(), args.task_id]
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
if (result.changes === 0) {
|
|
31
|
-
throw new Error(`Task "${args.task_id}" is already claimed (race condition)`)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return `Claimed task: ${task.content}`
|
|
49
|
+
return `Claimed task: ${content}`
|
|
35
50
|
}
|
package/src/tools/team-spawn.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ToolDeps, PermissionRule } from "../types"
|
|
2
2
|
import { validateMemberName } from "../util"
|
|
3
3
|
import { requireLead } from "./shared"
|
|
4
|
+
import { claimTask } from "./team-claim"
|
|
4
5
|
import { sendMessage } from "../messaging"
|
|
5
6
|
import { log } from "../log"
|
|
6
7
|
import type { EnsembleConfig } from "../config"
|
|
@@ -67,9 +68,14 @@ function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise
|
|
|
67
68
|
*/
|
|
68
69
|
export async function executeTeamSpawn(
|
|
69
70
|
deps: ToolDeps,
|
|
70
|
-
args: { name: string; agent
|
|
71
|
+
args: { name: string; agent?: string | null; prompt: string; model?: string; claim_task?: string; worktree?: boolean; plan_approval?: boolean },
|
|
71
72
|
sessionId: string,
|
|
72
73
|
): Promise<string> {
|
|
74
|
+
// Normalize agent: the tool schema defaults to "build", but Zod's .default()
|
|
75
|
+
// only fires on undefined — an explicit null slips through and would violate
|
|
76
|
+
// the team_member.agent NOT NULL constraint (issue #28).
|
|
77
|
+
const agent = args.agent ?? "build"
|
|
78
|
+
|
|
73
79
|
const nameError = validateMemberName(args.name)
|
|
74
80
|
if (nameError) throw new Error(nameError)
|
|
75
81
|
|
|
@@ -86,11 +92,11 @@ export async function executeTeamSpawn(
|
|
|
86
92
|
.get(teamInfo.teamId, args.name)
|
|
87
93
|
if (existing) throw new Error(`Teammate "${args.name}" already exists in team "${teamInfo.teamName}"`)
|
|
88
94
|
|
|
89
|
-
const isReadOnly =
|
|
95
|
+
const isReadOnly = agent === "plan" || agent === "explore"
|
|
90
96
|
const useWorktree = args.worktree !== false && !isReadOnly && !isWorktreeDirectory(deps.directory)
|
|
91
97
|
const usePlanApproval = args.plan_approval === true
|
|
92
98
|
|
|
93
|
-
log(`spawn:start name=${args.name} agent=${
|
|
99
|
+
log(`spawn:start name=${args.name} agent=${agent} worktree=${useWorktree}`)
|
|
94
100
|
|
|
95
101
|
// Create worktree if enabled
|
|
96
102
|
let worktreeDir: string | null = null
|
|
@@ -177,7 +183,7 @@ export async function executeTeamSpawn(
|
|
|
177
183
|
const createResult = await withTimeout(
|
|
178
184
|
deps.client.session.create({
|
|
179
185
|
parentID: sessionId,
|
|
180
|
-
title: `${args.name} (@${
|
|
186
|
+
title: `${args.name} (@${agent} teammate)`,
|
|
181
187
|
permission,
|
|
182
188
|
...(workspaceId ? { workspaceID: workspaceId } : {}),
|
|
183
189
|
}),
|
|
@@ -216,22 +222,37 @@ export async function executeTeamSpawn(
|
|
|
216
222
|
const now = Date.now()
|
|
217
223
|
// Resolve model before DB insert so the stored value matches what promptAsync uses
|
|
218
224
|
const memberCount = (deps.db.query("SELECT COUNT(*) as c FROM team_member WHERE team_id = ?").get(teamInfo.teamId) as { c: number }).c
|
|
219
|
-
const resolvedModel = resolveModel(args.model,
|
|
225
|
+
const resolvedModel = resolveModel(args.model, agent, memberCount, deps.config)
|
|
220
226
|
if (resolvedModel) log(`spawn:model name=${args.name} model=${resolvedModel}`)
|
|
221
227
|
|
|
222
228
|
deps.db.run(
|
|
223
229
|
`INSERT INTO team_member (team_id, name, session_id, agent, status, execution_status, model, prompt, worktree_dir, worktree_branch, workspace_id, plan_approval, time_created, time_updated)
|
|
224
230
|
VALUES (?, ?, ?, ?, 'busy', 'starting', ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
225
|
-
[teamInfo.teamId, args.name, childSessionId,
|
|
231
|
+
[teamInfo.teamId, args.name, childSessionId, agent, resolvedModel ?? null, args.prompt, worktreeDir, worktreeBranch, workspaceId, planApproval, now, now]
|
|
226
232
|
)
|
|
227
233
|
|
|
228
234
|
// Register in memory
|
|
229
235
|
deps.registry.register(teamInfo.teamId, args.name, childSessionId)
|
|
230
236
|
|
|
237
|
+
// Auto-claim a task for this teammate when claim_task is provided (issue #27).
|
|
238
|
+
// Claims atomically so a task is never double-assigned when the lead hands out
|
|
239
|
+
// more tasks than there are teammates. Read-only agents cannot claim tasks.
|
|
240
|
+
let claimedTaskContent: string | null = null
|
|
241
|
+
let claimWarning: string | null = null
|
|
242
|
+
if (args.claim_task && !isReadOnly) {
|
|
243
|
+
try {
|
|
244
|
+
claimedTaskContent = claimTask(deps.db, teamInfo.teamId, args.claim_task, args.name)
|
|
245
|
+
log(`spawn:claim:ok name=${args.name} task=${args.claim_task}`)
|
|
246
|
+
} catch (err) {
|
|
247
|
+
claimWarning = err instanceof Error ? err.message : String(err)
|
|
248
|
+
log(`spawn:claim:failed name=${args.name} task=${args.claim_task} err=${claimWarning}`)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
231
252
|
// Build teammate context message
|
|
232
253
|
const context = [
|
|
233
254
|
`You are "${args.name}", a teammate in team "${teamInfo.teamName}".`,
|
|
234
|
-
`Your agent type is "${
|
|
255
|
+
`Your agent type is "${agent}".`,
|
|
235
256
|
]
|
|
236
257
|
|
|
237
258
|
// Show other teammates so this agent knows who to message
|
|
@@ -347,7 +368,7 @@ export async function executeTeamSpawn(
|
|
|
347
368
|
args.prompt,
|
|
348
369
|
)
|
|
349
370
|
|
|
350
|
-
if (
|
|
371
|
+
if (claimedTaskContent) {
|
|
351
372
|
context.push("", `You have been assigned task ${args.claim_task}. Mark it complete when done.`)
|
|
352
373
|
}
|
|
353
374
|
|
|
@@ -364,7 +385,7 @@ export async function executeTeamSpawn(
|
|
|
364
385
|
deps.client.session.promptAsync({
|
|
365
386
|
sessionID: childSessionId,
|
|
366
387
|
parts: [{ type: "text", text: contextStr }],
|
|
367
|
-
agent
|
|
388
|
+
agent,
|
|
368
389
|
...(modelParam ? { model: modelParam } : {}),
|
|
369
390
|
}).catch((err) => {
|
|
370
391
|
const errMsg = err instanceof Error ? err.message : String(err)
|
|
@@ -372,6 +393,13 @@ export async function executeTeamSpawn(
|
|
|
372
393
|
try {
|
|
373
394
|
deps.db.run("DELETE FROM team_member WHERE team_id = ? AND session_id = ?", [teamInfo.teamId, childSessionId])
|
|
374
395
|
deps.registry.unregister(childSessionId)
|
|
396
|
+
// Release any task auto-claimed for this teammate so it returns to the pool.
|
|
397
|
+
if (claimedTaskContent) {
|
|
398
|
+
deps.db.run(
|
|
399
|
+
"UPDATE team_task SET status = 'pending', assignee = NULL, time_updated = ? WHERE id = ? AND assignee = ?",
|
|
400
|
+
[Date.now(), args.claim_task, args.name]
|
|
401
|
+
)
|
|
402
|
+
}
|
|
375
403
|
deps.client.session.abort({ sessionID: childSessionId }).catch(() => { /* best effort */ })
|
|
376
404
|
if (workspaceId) {
|
|
377
405
|
deps.client.workspace.remove({ id: workspaceId }).catch(() => { /* best effort */ })
|
|
@@ -397,8 +425,11 @@ export async function executeTeamSpawn(
|
|
|
397
425
|
|
|
398
426
|
const branchInfo = worktreeBranch ? ` (branch: ${worktreeBranch})` : ""
|
|
399
427
|
const planInfo = usePlanApproval ? " [plan mode — will send plan for approval]" : ""
|
|
428
|
+
const claimInfo = claimedTaskContent
|
|
429
|
+
? ` (claimed task: ${args.claim_task})`
|
|
430
|
+
: claimWarning ? ` (could not claim task: ${claimWarning})` : ""
|
|
400
431
|
// Reset circuit breaker on success
|
|
401
432
|
spawnFailures.delete(teamInfo.teamId)
|
|
402
433
|
log(`spawn:done name=${args.name} sessionId=${childSessionId}`)
|
|
403
|
-
return `Teammate "${args.name}" spawned (agent: ${
|
|
434
|
+
return `Teammate "${args.name}" spawned (agent: ${agent})${branchInfo}${planInfo}${claimInfo}. They are working on: ${args.prompt.slice(0, 120)}${args.prompt.length > 120 ? "..." : ""}`
|
|
404
435
|
}
|
|
@@ -5,6 +5,12 @@ import { log } from "../log"
|
|
|
5
5
|
/**
|
|
6
6
|
* Execute the team_tasks_complete tool. Marks a task as completed
|
|
7
7
|
* and unblocks any dependent tasks.
|
|
8
|
+
*
|
|
9
|
+
* Enforces task-board accuracy (issue #27):
|
|
10
|
+
* - Rejects completing an already-completed or cancelled task.
|
|
11
|
+
* - Rejects completing a blocked task (dependencies must resolve first).
|
|
12
|
+
* - Only the assignee (or the lead) may complete a claimed task.
|
|
13
|
+
* - Completing an unclaimed task attributes it to the caller atomically.
|
|
8
14
|
*/
|
|
9
15
|
export async function executeTeamTasksComplete(
|
|
10
16
|
deps: ToolDeps,
|
|
@@ -12,13 +18,31 @@ export async function executeTeamTasksComplete(
|
|
|
12
18
|
sessionId: string,
|
|
13
19
|
): Promise<string> {
|
|
14
20
|
const teamInfo = requireTeamMember(deps, sessionId)
|
|
21
|
+
const caller = teamInfo.role === "lead" ? "lead" : (teamInfo.memberName ?? "unknown")
|
|
15
22
|
|
|
16
|
-
const task = deps.db.query("SELECT
|
|
17
|
-
.get(args.task_id, teamInfo.teamId) as
|
|
23
|
+
const task = deps.db.query("SELECT id, content, status, assignee FROM team_task WHERE id = ? AND team_id = ?")
|
|
24
|
+
.get(args.task_id, teamInfo.teamId) as { id: string; content: string; status: string; assignee: string | null } | null
|
|
18
25
|
if (!task) throw new Error(`Task "${args.task_id}" not found`)
|
|
26
|
+
if (task.status === "completed") throw new Error(`Task "${args.task_id}" is already completed`)
|
|
27
|
+
if (task.status === "cancelled") throw new Error(`Task "${args.task_id}" was cancelled`)
|
|
28
|
+
if (task.status === "blocked") throw new Error(`Task "${args.task_id}" is blocked by unresolved dependencies`)
|
|
29
|
+
if (task.status === "in_progress" && task.assignee && teamInfo.role !== "lead" && caller !== task.assignee) {
|
|
30
|
+
throw new Error(`Task "${args.task_id}" is claimed by ${task.assignee}`)
|
|
31
|
+
}
|
|
19
32
|
|
|
20
33
|
const now = Date.now()
|
|
21
|
-
|
|
34
|
+
if (task.assignee) {
|
|
35
|
+
deps.db.run("UPDATE team_task SET status = 'completed', time_updated = ? WHERE id = ?", [now, args.task_id])
|
|
36
|
+
} else {
|
|
37
|
+
// Attribute an unclaimed task to the caller so the board reflects who did it.
|
|
38
|
+
const result = deps.db.run(
|
|
39
|
+
"UPDATE team_task SET status = 'completed', assignee = ?, time_updated = ? WHERE id = ? AND assignee IS NULL",
|
|
40
|
+
[caller, now, args.task_id]
|
|
41
|
+
)
|
|
42
|
+
if (result.changes === 0) {
|
|
43
|
+
throw new Error(`Task "${args.task_id}" was just completed by another teammate`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
22
46
|
|
|
23
47
|
// Unblock dependent tasks
|
|
24
48
|
const allTasks = deps.db.query("SELECT id, depends_on, status FROM team_task WHERE team_id = ?")
|
package/src/types.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface PluginClient {
|
|
|
50
50
|
}): Promise<unknown>
|
|
51
51
|
abort(options: { sessionID: string }): Promise<unknown>
|
|
52
52
|
status(): Promise<{ data?: Record<string, { type: string }> }>
|
|
53
|
+
messages(options: { sessionID: string; limit?: number }): Promise<{ data?: Array<{ info: unknown; parts: unknown[] }> }>
|
|
54
|
+
get(options: { sessionID: string }): Promise<{ data?: unknown }>
|
|
53
55
|
}
|
|
54
56
|
tui: {
|
|
55
57
|
showToast(options: {
|