diff --git a/src/tools/athena-council/session-waiter.ts b/src/tools/athena-council/session-waiter.ts new file mode 100644 index 000000000..071b27c81 --- /dev/null +++ b/src/tools/athena-council/session-waiter.ts @@ -0,0 +1,52 @@ +import type { BackgroundManager } from "../../features/background-agent" +import type { CouncilLaunchedMember } from "../../agents/athena/types" + +const WAIT_INTERVAL_MS = 100 +const WAIT_TIMEOUT_MS = 30_000 + +interface CouncilSessionInfo { + taskId: string + memberName: string + model: string + sessionId: string +} + +/** + * Waits for background sessions to be created for launched council members. + * Returns session info for each member whose session became available within the timeout. + */ +export async function waitForCouncilSessions( + launched: CouncilLaunchedMember[], + manager: BackgroundManager, + abort?: AbortSignal +): Promise { + const results: CouncilSessionInfo[] = [] + const pending = new Map( + launched.map((entry) => [entry.taskId, entry]) + ) + + const deadline = Date.now() + WAIT_TIMEOUT_MS + + while (pending.size > 0 && Date.now() < deadline) { + if (abort?.aborted) break + + for (const [taskId, entry] of pending) { + const task = manager.getTask(taskId) + if (task?.sessionID) { + results.push({ + taskId, + memberName: entry.member.name ?? entry.member.model, + model: entry.member.model, + sessionId: task.sessionID, + }) + pending.delete(taskId) + } + } + + if (pending.size > 0) { + await new Promise((resolve) => setTimeout(resolve, WAIT_INTERVAL_MS)) + } + } + + return results +} diff --git a/src/tools/athena-council/tools.ts b/src/tools/athena-council/tools.ts index b4a6c2186..d18389d73 100644 --- a/src/tools/athena-council/tools.ts +++ b/src/tools/athena-council/tools.ts @@ -5,6 +5,7 @@ import type { BackgroundManager } from "../../features/background-agent" import { ATHENA_COUNCIL_TOOL_DESCRIPTION_TEMPLATE } from "./constants" import { createCouncilLauncher } from "./council-launcher" import { isCouncilRunning, markCouncilDone, markCouncilRunning } from "./session-guard" +import { waitForCouncilSessions } from "./session-waiter" import type { AthenaCouncilLaunchResult, AthenaCouncilToolArgs } from "./types" function isCouncilConfigured(councilConfig: CouncilConfig | undefined): councilConfig is CouncilConfig { @@ -112,6 +113,31 @@ export function createAthenaCouncilTool(args: { parentAgent: toolContext.agent, }) + // Wait for sessions to be created so we can register metadata for UI visibility. + // This makes council member tasks clickable in the OpenCode TUI, matching the + // behavior of the task tool (delegate-task/background-task.ts). + const metadataFn = (toolContext as Record).metadata as + | ((input: { title?: string; metadata?: Record }) => Promise) + | undefined + if (metadataFn && execution.launched.length > 0) { + const sessions = await waitForCouncilSessions( + execution.launched, + backgroundManager, + toolContext.abort + ) + for (const session of sessions) { + await metadataFn({ + title: `Council: ${session.memberName}`, + metadata: { + sessionId: session.sessionId, + agent: "council-member", + model: session.model, + description: `Council member: ${session.memberName}`, + }, + }) + } + } + const launchResult: AthenaCouncilLaunchResult = { launched: execution.launched.length, members: execution.launched.map((entry) => ({