From d908a712b999d7fe199e455a5233d232203b79f9 Mon Sep 17 00:00:00 2001 From: ismeth Date: Tue, 17 Feb 2026 14:38:56 +0100 Subject: [PATCH] feat(athena): make council member background tasks visible in UI Council member tasks were launched via BackgroundManager but lacked the ctx.metadata() call that links background sessions to the tool call in the OpenCode TUI. Users couldn't click to inspect individual member outputs. - Add session-waiter.ts to poll for session creation on launched tasks - Call ctx.metadata() for each council member with sessionId linkage - Matches the pattern used by delegate-task/background-task.ts Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- src/tools/athena-council/session-waiter.ts | 52 ++++++++++++++++++++++ src/tools/athena-council/tools.ts | 26 +++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/tools/athena-council/session-waiter.ts 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) => ({