import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; import { randomUUID } from "crypto"; import { Type } from "typebox"; import { Text } from "@mariozechner/pi-tui"; import { IntercomClient } from "./broker/client.ts"; import { spawnBrokerIfNeeded } from "./broker/spawn.ts"; import { SessionListOverlay } from "./ui/session-list.ts"; import { ComposeOverlay, type ComposeResult } from "./ui/compose.ts"; import { InlineMessageComponent } from "./ui/inline-message.ts"; import { loadConfig, type IntercomConfig } from "./config.ts"; import type { SessionInfo, Message, Attachment } from "./types.ts"; import { ReplyTracker } from "./reply-tracker.ts"; const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom"; const SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom"; const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery"; const INBOUND_FLUSH_DELAY_MS = 200; const INBOUND_IDLE_RETRY_MS = 500; const DEFAULT_UNNAMED_SESSION_ALIAS_PREFIX = "subagent-chat"; const SUBAGENT_ORCHESTRATOR_TARGET_ENV = "PI_SUBAGENT_ORCHESTRATOR_TARGET"; const SUBAGENT_RUN_ID_ENV = "PI_SUBAGENT_RUN_ID"; const SUBAGENT_CHILD_AGENT_ENV = "PI_SUBAGENT_CHILD_AGENT"; const SUBAGENT_CHILD_INDEX_ENV = "PI_SUBAGENT_CHILD_INDEX"; const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME"; interface ChildOrchestratorMetadata { orchestratorTarget: string; runId: string; agent: string; index: string; sessionName?: string; } interface InboundMessageEntry { from: SessionInfo; message: Message; replyCommand?: string; bodyText: string; } type ContactSupervisorReason = "need_decision" | "progress_update" | "interview_request"; interface SupervisorInterviewQuestion extends Record { id: string; type: "single" | "multi" | "text" | "image" | "info"; question: string; options?: unknown[]; } interface SupervisorInterviewRequest extends Record { title?: string; description?: string; questions: SupervisorInterviewQuestion[]; } interface SupervisorInterviewReply { responses: Array<{ id: string; value: unknown }>; } function getErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } function toError(error: unknown): Error { return error instanceof Error ? error : new Error(String(error)); } function formatAttachments(attachments: Attachment[]): string { let text = ""; for (const att of attachments) { if (att.language) { text += `\n\n---\n๐Ÿ“Ž ${att.name}\n~~~${att.language}\n${att.content}\n~~~`; } else { text += `\n\n---\n๐Ÿ“Ž ${att.name}\n${att.content}`; } } return text; } function readChildOrchestratorMetadata(): ChildOrchestratorMetadata | null { const orchestratorTarget = process.env[SUBAGENT_ORCHESTRATOR_TARGET_ENV]?.trim(); const runId = process.env[SUBAGENT_RUN_ID_ENV]?.trim(); const agent = process.env[SUBAGENT_CHILD_AGENT_ENV]?.trim(); const index = process.env[SUBAGENT_CHILD_INDEX_ENV]?.trim(); if (!orchestratorTarget || !runId || !agent || !index) { return null; } const sessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim(); return { orchestratorTarget, runId, agent, index, ...(sessionName ? { sessionName } : {}), }; } function formatChildOrchestratorMessage(kind: "ask" | "update" | "interview", metadata: ChildOrchestratorMetadata, message: string): string { const heading = kind === "ask" ? "Subagent needs a supervisor decision." : kind === "interview" ? "Subagent requests a structured supervisor interview." : "Subagent progress update."; return [ heading, `Run: ${metadata.runId}`, `Agent: ${metadata.agent}`, `Child index: ${metadata.index}`, metadata.sessionName ? `Child intercom target: ${metadata.sessionName}` : undefined, "", message, ].filter((line): line is string => line !== undefined).join("\n"); } function validateSupervisorInterviewRequest(input: unknown): { ok: true; interview: SupervisorInterviewRequest } | { ok: false; error: string } { if (!input || typeof input !== "object" || Array.isArray(input)) { return { ok: false, error: "interview must be an object with a questions array" }; } const raw = input as Record; if (raw.title !== undefined && typeof raw.title !== "string") { return { ok: false, error: "interview.title must be a string when provided" }; } if (raw.description !== undefined && typeof raw.description !== "string") { return { ok: false, error: "interview.description must be a string when provided" }; } if (!Array.isArray(raw.questions) || raw.questions.length === 0) { return { ok: false, error: "interview.questions must be a non-empty array" }; } const validTypes = new Set(["single", "multi", "text", "image", "info"]); const ids = new Set(); const questions: SupervisorInterviewQuestion[] = []; for (let index = 0; index < raw.questions.length; index++) { const questionInput = raw.questions[index]; if (!questionInput || typeof questionInput !== "object" || Array.isArray(questionInput)) { return { ok: false, error: `interview.questions[${index}] must be an object` }; } const question = questionInput as Record; if (typeof question.id !== "string" || question.id.trim() === "") { return { ok: false, error: `interview.questions[${index}].id must be a non-empty string` }; } const id = question.id.trim(); if (ids.has(id)) { return { ok: false, error: `interview question id must be unique: ${id}` }; } ids.add(id); if (typeof question.type !== "string" || !validTypes.has(question.type)) { return { ok: false, error: `interview.questions[${index}].type must be one of: single, multi, text, image, info` }; } if (typeof question.question !== "string" || question.question.trim() === "") { return { ok: false, error: `interview.questions[${index}].question must be a non-empty string` }; } if (question.context !== undefined && typeof question.context !== "string") { return { ok: false, error: `interview.questions[${index}].context must be a string when provided` }; } let options: unknown[] | undefined; if (question.options !== undefined) { if (!Array.isArray(question.options)) { return { ok: false, error: `interview.questions[${index}].options must be an array when provided` }; } options = []; for (let optionIndex = 0; optionIndex < question.options.length; optionIndex++) { const option = question.options[optionIndex]; if (typeof option === "string") { const label = option.trim(); if (!label) { return { ok: false, error: `interview.questions[${index}].options[${optionIndex}] must not be empty` }; } options.push(label); } else if (!option || typeof option !== "object" || Array.isArray(option) || typeof (option as { label?: unknown }).label !== "string" || (option as { label: string }).label.trim() === "") { return { ok: false, error: `interview.questions[${index}].options[${optionIndex}] must be a non-empty string or an object with a non-empty label` }; } else { options.push({ ...option, label: (option as { label: string }).label.trim() }); } } } if ((question.type === "single" || question.type === "multi") && (!options || options.length === 0)) { return { ok: false, error: `interview.questions[${index}].options must be a non-empty array for ${question.type} questions` }; } if (question.type !== "single" && question.type !== "multi" && options) { return { ok: false, error: `interview.questions[${index}].options is only valid for single and multi questions` }; } questions.push({ ...question, id, type: question.type as SupervisorInterviewQuestion["type"], question: question.question.trim(), ...(options ? { options } : {}), }); } return { ok: true, interview: { ...raw, ...(typeof raw.title === "string" ? { title: raw.title.trim() } : {}), ...(typeof raw.description === "string" ? { description: raw.description.trim() } : {}), questions, }, }; } function interviewOptionLabel(option: unknown): string { return typeof option === "string" ? option : (option as { label: string }).label; } function interviewExampleValue(question: SupervisorInterviewQuestion): unknown { if (question.type === "multi") { return question.options?.slice(0, 2).map(interviewOptionLabel) ?? []; } if (question.type === "single") { return question.options?.[0] !== undefined ? interviewOptionLabel(question.options[0]) : "option label"; } if (question.type === "image") { return "image/file reference or description"; } return "answer text"; } function formatSupervisorInterviewRequest(interview: SupervisorInterviewRequest, message?: string): string { const lines: string[] = []; const title = interview.title?.trim(); if (title) lines.push(`Interview: ${title}`); const description = interview.description?.trim(); if (description) lines.push(description); const note = message?.trim(); if (note) lines.push(`Child note: ${note}`); if (lines.length > 0) lines.push(""); lines.push("Questions:"); interview.questions.forEach((question, index) => { lines.push(`${index + 1}. [${question.id}] (${question.type}) ${question.question}`); if (typeof question.context === "string" && question.context.trim()) { lines.push(` Context: ${question.context.trim()}`); } if (question.options?.length) { lines.push(" Options:"); for (const option of question.options) { lines.push(` - ${interviewOptionLabel(option)}`); } } }); const responseExample = { responses: interview.questions .filter((question) => question.type !== "info") .map((question) => ({ id: question.id, value: interviewExampleValue(question), })), }; lines.push( "", "Supervisor reply instructions:", "Reply with plain JSON or a fenced ```json block using this stable shape. Use the question ids exactly. Info questions are context-only and do not need responses. For single questions, value is one option label. For multi questions, value is an array of option labels. For text/image questions, value is a string unless the question asks otherwise.", "", "```json", JSON.stringify(responseExample, null, 2), "```", ); return lines.join("\n"); } function validateSupervisorInterviewReply(value: unknown, interview: SupervisorInterviewRequest): SupervisorInterviewReply { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error("reply JSON must be an object with a responses array"); } const responsesInput = (value as Record).responses; if (!Array.isArray(responsesInput)) { throw new Error("reply JSON must include a responses array"); } const questionById = new Map(interview.questions .filter((question) => question.type !== "info") .map((question) => [question.id, question])); const seenIds = new Set(); const responses: SupervisorInterviewReply["responses"] = []; for (let index = 0; index < responsesInput.length; index++) { const response = responsesInput[index]; if (!response || typeof response !== "object" || Array.isArray(response)) { throw new Error(`responses[${index}] must be an object`); } const raw = response as Record; if (typeof raw.id !== "string" || raw.id.trim() === "") { throw new Error(`responses[${index}].id must be a non-empty string`); } const id = raw.id.trim(); const question = questionById.get(id); if (!question) { throw new Error(`responses[${index}].id must match a non-info interview question id`); } if (seenIds.has(id)) { throw new Error(`responses[${index}].id is duplicated: ${id}`); } seenIds.add(id); if (!Object.hasOwn(raw, "value")) { throw new Error(`responses[${index}].value is required`); } const value = raw.value; if (question.type === "single") { if (typeof value !== "string") throw new Error(`responses[${index}].value must be a string for single questions`); const optionLabels = new Set(question.options?.map(interviewOptionLabel)); if (!optionLabels.has(value.trim())) throw new Error(`responses[${index}].value must match one of the question options`); responses.push({ id, value: value.trim() }); continue; } if (question.type === "multi") { if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) { throw new Error(`responses[${index}].value must be an array of strings for multi questions`); } const optionLabels = new Set(question.options?.map(interviewOptionLabel)); const selected = value.map((item) => item.trim()); const invalid = selected.find((item) => !optionLabels.has(item)); if (invalid) throw new Error(`responses[${index}].value contains an option that is not in the question options: ${invalid}`); responses.push({ id, value: selected }); continue; } if (typeof value !== "string") { throw new Error(`responses[${index}].value must be a string for ${question.type} questions`); } responses.push({ id, value }); } return { responses }; } function parseStructuredSupervisorReply(text: string, interview: SupervisorInterviewRequest): { value?: SupervisorInterviewReply; error?: string } | undefined { const fencedMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/i); const candidate = (fencedMatch?.[1] ?? text).trim(); if (!candidate.startsWith("{") && !candidate.startsWith("[")) { return undefined; } try { return { value: validateSupervisorInterviewReply(JSON.parse(candidate), interview) }; } catch (error) { return { error: getErrorMessage(error) }; } } function duplicateSessionNames(sessions: SessionInfo[]): Set { return new Set( sessions .map(s => s.name?.toLowerCase()) .filter((name): name is string => Boolean(name)) .filter((name, index, names) => names.indexOf(name) !== index) ); } function shortSessionId(sessionId: string): string { return sessionId.slice(0, 8); } function parseSubagentIntercomPayload(payload: unknown): { to: string; message: string; requestId?: string } | null { if (typeof payload !== "object" || payload === null) { return null; } const record = payload as Record; if (typeof record.to !== "string" || typeof record.message !== "string") { return null; } const requestId = typeof record.requestId === "string" ? record.requestId : undefined; return { to: record.to, message: record.message, ...(requestId ? { requestId } : {}) }; } function resolveIntercomPresenceName(sessionName: string | undefined, sessionId: string): string { const trimmedName = sessionName?.trim(); if (trimmedName) { return trimmedName; } const normalizedSessionId = sessionId.startsWith("session-") ? sessionId.slice("session-".length) : sessionId; return `${DEFAULT_UNNAMED_SESSION_ALIAS_PREFIX}-${normalizedSessionId.slice(0, 8)}`; } function buildPresenceIdentity(pi: ExtensionAPI, sessionId: string): { name: string } { return { name: resolveIntercomPresenceName(pi.getSessionName(), sessionId), }; } function formatSessionLabel(session: SessionInfo, duplicates: Set): string { if (!session.name) { return session.id; } return duplicates.has(session.name.toLowerCase()) ? `${session.name} (${shortSessionId(session.id)})` : session.name; } function formatSessionListRow(session: SessionInfo, currentCwd: string, isSelf: boolean): string { const name = session.name || "Unnamed session"; const tags = [isSelf ? "self" : session.cwd === currentCwd ? "same cwd" : undefined, session.status] .filter((tag): tag is string => Boolean(tag)); const suffix = tags.length ? ` [${tags.join(", ")}]` : ""; return `โ€ข ${name} (${shortSessionId(session.id)}) โ€” ${session.cwd} (${session.model})${suffix}`; } function previewText(value: unknown, maxLength = 72): string | undefined { if (typeof value !== "string") { return undefined; } const normalized = value.replace(/\s+/g, " ").trim(); if (!normalized) { return undefined; } return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 1)}โ€ฆ` : normalized; } function firstTextContent(result: { content?: Array<{ type: string; text?: string }> }): string { return result.content?.find((item) => item.type === "text" && typeof item.text === "string")?.text?.replace(/\*\*/g, "") ?? ""; } export default function piIntercomExtension(pi: ExtensionAPI) { let client: IntercomClient | null = null; const config: IntercomConfig = loadConfig(); let runtimeContext: ExtensionContext | null = null; let currentSessionId: string | null = null; let currentModel = "unknown"; let sessionStartedAt: number | null = null; let reconnectTimer: NodeJS.Timeout | null = null; let reconnectPromise: Promise | null = null; let reconnectPromiseGeneration: number | null = null; let startupConnectTimer: NodeJS.Timeout | null = null; let reconnectAttempt = 0; let shuttingDown = false; let disposed = true; let runtimeStarted = false; let runtimeGeneration = 0; let agentRunning = false; const activeTools = new Map(); const replyTracker = new ReplyTracker(); const pendingIdleMessages: InboundMessageEntry[] = []; let inboundFlushTimer: NodeJS.Timeout | null = null; let replyWaiter: { from: string; replyTo: string; resolve: (message: Message) => void; reject: (error: Error) => void; } | null = null; function waitForReply(from: string, replyTo: string, signal?: AbortSignal): Promise { if (replyWaiter) { return Promise.reject(new Error("Already waiting for a reply")); } if (signal?.aborted) { return Promise.reject(new Error("Cancelled")); } return new Promise((resolve, reject) => { const timeout = setTimeout(() => { rejectReplyWaiter(new Error(`No reply from "${from}" within 10 minutes`)); }, 10 * 60 * 1000); const cleanup = () => { clearTimeout(timeout); signal?.removeEventListener("abort", onAbort); if (replyWaiter?.replyTo === replyTo) { replyWaiter = null; } }; const onAbort = () => { cleanup(); reject(new Error("Cancelled")); }; signal?.addEventListener("abort", onAbort, { once: true }); replyWaiter = { from, replyTo, resolve: (message) => { cleanup(); resolve(message); }, reject: (error) => { cleanup(); reject(error); }, }; }); } function rejectReplyWaiter(error: Error): void { replyWaiter?.reject(error); } function clearReconnectTimer(): void { if (!reconnectTimer) { return; } clearTimeout(reconnectTimer); reconnectTimer = null; } function clearStartupConnectTimer(): void { if (!startupConnectTimer) { return; } clearTimeout(startupConnectTimer); startupConnectTimer = null; } function clearInboundFlushTimer(): void { if (!inboundFlushTimer) { return; } clearTimeout(inboundFlushTimer); inboundFlushTimer = null; } function getLiveContext(ctx: ExtensionContext | null = runtimeContext, generation = runtimeGeneration): ExtensionContext | null { if (disposed || shuttingDown || generation !== runtimeGeneration || !ctx) { return null; } try { if (currentSessionId && ctx.sessionManager.getSessionId() !== currentSessionId) { return null; } void ctx.hasUI; return ctx; } catch { // A context that throws while reading session/UI state is no longer usable. return null; } } function notifyIfLive(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error", generation = runtimeGeneration): void { const liveContext = getLiveContext(ctx, generation); if (!liveContext?.hasUI) { return; } try { liveContext.ui.notify(message, level); } catch { // The UI can disappear during session shutdown/reload while async overlay work is settling. } } function getReconnectDelayMs(): number { const backoffMs = [1000, 2000, 5000, 10000, 30000]; return backoffMs[Math.min(reconnectAttempt, backoffMs.length - 1)]!; } function currentStatus(): string { const activeToolName = activeTools.values().next().value; const lifecycleStatus = activeToolName ? `tool:${activeToolName}` : agentRunning ? "thinking" : "idle"; return config.status ? `${lifecycleStatus} ยท ${config.status}` : lifecycleStatus; } function buildRegistration(): Omit { const liveContext = getLiveContext(); if (!liveContext || !currentSessionId || sessionStartedAt === null) { throw new Error("Intercom runtime not initialized"); } const identity = buildPresenceIdentity(pi, currentSessionId); return { name: identity.name, cwd: liveContext.cwd ?? process.cwd(), model: currentModel, pid: process.pid, startedAt: sessionStartedAt, lastActivity: Date.now(), status: currentStatus(), }; } function syncPresenceIdentity(sessionId: string): void { if (!client || !getLiveContext()) { return; } client.updatePresence({ ...buildPresenceIdentity(pi, sessionId), status: currentStatus() }); } function syncPresenceStatus(): void { if (!client || !currentSessionId || !getLiveContext()) { return; } client.updatePresence({ status: currentStatus() }); } function currentSessionTargetMatches(to: string, resolvedTo?: string | null, activeClient?: IntercomClient): boolean { const targets = new Set(); const addTarget = (target: string | undefined | null) => { const trimmed = target?.trim(); if (trimmed) targets.add(trimmed.toLowerCase()); }; addTarget(currentSessionId); addTarget(activeClient?.sessionId); addTarget(pi.getSessionName()); if (currentSessionId) addTarget(buildPresenceIdentity(pi, currentSessionId).name); return Boolean(resolvedTo && activeClient?.sessionId && resolvedTo === activeClient.sessionId) || targets.has(to.trim().toLowerCase()); } function sendIncomingMessage(entry: InboundMessageEntry, delivery: "trigger" | "followUp", generation = runtimeGeneration): void { if (runtimeStarted && !getLiveContext(runtimeContext, generation)) { return; } if (delivery !== "followUp") { replyTracker.queueTurnContext({ from: entry.from, message: entry.message, receivedAt: Date.now() }); } const senderDisplay = entry.from.name || entry.from.id.slice(0, 8); const replyInstruction = entry.replyCommand ? `\n\nTo reply, use the intercom tool: ${entry.replyCommand}` : ""; pi.sendMessage( { customType: "intercom_message", content: `**๐Ÿ“จ From ${senderDisplay}** (${entry.from.cwd})${replyInstruction}\n\n${entry.bodyText}`, display: true, details: entry, }, delivery === "trigger" ? { triggerTurn: true } : { deliverAs: "followUp" } ); } function scheduleInboundFlush(delayMs = INBOUND_FLUSH_DELAY_MS): void { if (!getLiveContext()) { return; } const scheduledGeneration = runtimeGeneration; clearInboundFlushTimer(); inboundFlushTimer = setTimeout(() => { inboundFlushTimer = null; flushIdleMessages(scheduledGeneration); }, delayMs); } function flushIdleMessages(generation = runtimeGeneration): void { if (pendingIdleMessages.length === 0) { return; } const ctx = getLiveContext(runtimeContext, generation); if (!ctx) { return; } let isIdle: boolean; try { isIdle = ctx.isIdle(); } catch { // Stale contexts are cleaned up by shutdown/reload; do not deliver queued messages through them. return; } if (!isIdle) { scheduleInboundFlush(INBOUND_IDLE_RETRY_MS); return; } const entries = pendingIdleMessages.splice(0, pendingIdleMessages.length); entries.forEach((entry, index) => { sendIncomingMessage(entry, index === 0 ? "trigger" : "followUp"); }); } function queueIdleMessage(entry: InboundMessageEntry): void { pendingIdleMessages.push(entry); scheduleInboundFlush(); } function handleIncomingMessage(ctx: ExtensionContext, from: SessionInfo, message: Message): void { const messageGeneration = runtimeGeneration; const liveContext = getLiveContext(ctx, messageGeneration); if (!liveContext) { return; } if (replyWaiter) { const senderTarget = from.name || from.id; const fromMatches = senderTarget.toLowerCase() === replyWaiter.from.toLowerCase() || from.id === replyWaiter.from; const replyMatches = message.replyTo === replyWaiter.replyTo; if (fromMatches && replyMatches) { replyWaiter.resolve(message); return; } } const attachmentText = message.content.attachments?.length ? formatAttachments(message.content.attachments) : ""; const bodyText = `${message.content.text}${attachmentText}`; const replyCommand = config.replyHint && message.expectsReply ? `intercom({ action: "reply", message: "..." })` : undefined; replyTracker.recordIncomingMessage(from, message); const entry = { from, message, replyCommand, bodyText }; void (async () => { const activeContext = getLiveContext(liveContext, messageGeneration); if (!activeContext) { return; } if (!activeContext.isIdle()) { if (!activeContext.hasUI) { const activeClient = client; if (!message.replyTo && activeClient?.isConnected()) { try { const result = await activeClient.send(from.id, { text: "This agent is running in non-interactive mode and cannot respond to intercom messages while it is working. It will continue its current task and exit when done.", replyTo: message.id, }); if (result.delivered && getLiveContext(liveContext, messageGeneration)) { replyTracker.markReplied(message.id); } } catch { // Best-effort reply; keep the busy non-interactive session running either way. } } return; } queueIdleMessage(entry); return; } if (getLiveContext(liveContext, messageGeneration)) { sendIncomingMessage(entry, "trigger", messageGeneration); } })(); } function attachClientHandlers(nextClient: IntercomClient): void { nextClient.on("message", (from, message) => { const liveContext = getLiveContext(); if (client !== nextClient || !liveContext) { return; } handleIncomingMessage(liveContext, from, message); }); nextClient.on("disconnected", (error: Error) => { if (client !== nextClient) { return; } rejectReplyWaiter(new Error(`Disconnected while waiting for reply: ${error.message}`, { cause: error })); client = null; if (!shuttingDown && !disposed) { clearReconnectTimer(); scheduleReconnect(); } }); nextClient.on("error", () => { // Keep broker/socket noise out of the TUI. Reconnect logic runs from the disconnect path. }); } function scheduleReconnect(): void { if (disposed || shuttingDown || reconnectTimer || reconnectPromise || !getLiveContext()) { return; } const scheduledGeneration = runtimeGeneration; reconnectTimer = setTimeout(() => { reconnectTimer = null; if (scheduledGeneration !== runtimeGeneration || !getLiveContext()) { return; } reconnectAttempt += 1; void ensureConnected("background").catch(() => { // ensureConnected("background") already queued the next retry. }); }, getReconnectDelayMs()); } async function ensureConnected(reason: "startup" | "background" | "tool" | "overlay"): Promise { if (!config.enabled) { throw new Error("Intercom disabled"); } if (disposed || shuttingDown) { throw new Error("Intercom shutting down"); } if (client && client.isConnected()) { return client; } const contextAtStart = getLiveContext(); const generationAtStart = runtimeGeneration; if (!contextAtStart || !currentSessionId || sessionStartedAt === null) { throw new Error("Intercom runtime not initialized"); } clearReconnectTimer(); if (reconnectPromise && reconnectPromiseGeneration === generationAtStart) { return reconnectPromise; } const nextReconnectPromise = (async () => { const nextClient = new IntercomClient(); client = nextClient; attachClientHandlers(nextClient); try { await spawnBrokerIfNeeded(config.brokerCommand, config.brokerArgs); await nextClient.connect(buildRegistration()); if (!getLiveContext(contextAtStart, generationAtStart)) { await nextClient.disconnect(); throw new Error("Intercom runtime no longer active"); } client = nextClient; reconnectAttempt = 0; return nextClient; } catch (error) { if (client === nextClient) { client = null; } if (reason === "background" && getLiveContext(contextAtStart, generationAtStart)) { scheduleReconnect(); } throw toError(error); } finally { if (reconnectPromise === nextReconnectPromise) { reconnectPromise = null; reconnectPromiseGeneration = null; } } })(); reconnectPromise = nextReconnectPromise; reconnectPromiseGeneration = generationAtStart; return nextReconnectPromise; } async function resolveSessionTarget(activeClient: IntercomClient, nameOrId: string): Promise { const sessions = await activeClient.listSessions(); const byId = sessions.find(s => s.id === nameOrId); if (byId) { return byId.id; } const lowerName = nameOrId.toLowerCase(); const byName = sessions.filter(s => s.name?.toLowerCase() === lowerName); if (byName.length > 1) { throw new Error(`Multiple sessions named "${nameOrId}" are connected. Use the session ID instead.`); } return byName[0]?.id ?? null; } function deliverLocalSubagentRelayMessage(sender: "subagent-control" | "subagent-result", status: string, messageText: string): void { const now = Date.now(); sendIncomingMessage({ from: { id: sender, name: sender, cwd: runtimeContext?.cwd ?? process.cwd(), model: sender, pid: process.pid, startedAt: now, lastActivity: now, status, }, message: { id: randomUUID(), timestamp: now, content: { text: messageText }, }, bodyText: messageText, }, "trigger"); } function recordSubagentDeliveryError(entryType: string, to: string, message: string, error: unknown): void { pi.appendEntry(entryType, { to, message, error: getErrorMessage(error), timestamp: Date.now(), }); } function emitResultDelivery(requestId: string | undefined, delivered: boolean, error?: unknown): void { if (!requestId) return; pi.events.emit(SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT, { requestId, delivered, ...(error ? { error: getErrorMessage(error) } : {}), }); } function relaySubagentIntercomPayload(payload: unknown, options: { sender: "subagent-control" | "subagent-result"; status: string; errorEntryType: string; acknowledge?: boolean; }): void { const parsed = parseSubagentIntercomPayload(payload); if (!parsed) return; const relayGeneration = runtimeGeneration; void (async () => { const relayStillLive = () => !runtimeStarted || Boolean(getLiveContext(runtimeContext, relayGeneration)); if (!relayStillLive()) { return; } if (currentSessionTargetMatches(parsed.to)) { deliverLocalSubagentRelayMessage(options.sender, options.status, parsed.message); if (options.acknowledge) emitResultDelivery(parsed.requestId, true); return; } let activeClient: IntercomClient; let target: string; try { activeClient = await ensureConnected("background"); target = await resolveSessionTarget(activeClient, parsed.to) ?? parsed.to; } catch (error) { if (!relayStillLive()) return; recordSubagentDeliveryError(options.errorEntryType, parsed.to, parsed.message, error); if (options.acknowledge) emitResultDelivery(parsed.requestId, false, error); return; } if (!relayStillLive()) { return; } if (currentSessionTargetMatches(parsed.to, target, activeClient)) { deliverLocalSubagentRelayMessage(options.sender, options.status, parsed.message); if (options.acknowledge) emitResultDelivery(parsed.requestId, true); return; } try { const result = await activeClient.send(target, { text: parsed.message }); if (!relayStillLive()) return; if (!result.delivered) { const error = new Error(result.reason ?? "Session may not exist or has disconnected."); recordSubagentDeliveryError(options.errorEntryType, parsed.to, parsed.message, error); if (options.acknowledge) emitResultDelivery(parsed.requestId, false, error); return; } if (options.acknowledge) emitResultDelivery(parsed.requestId, true); } catch (error) { if (!relayStillLive()) return; recordSubagentDeliveryError(options.errorEntryType, parsed.to, parsed.message, error); if (options.acknowledge) emitResultDelivery(parsed.requestId, false, error); } })(); } pi.events.on(SUBAGENT_CONTROL_INTERCOM_EVENT, (payload) => { relaySubagentIntercomPayload(payload, { sender: "subagent-control", status: "needs_attention", errorEntryType: "intercom_control_error", }); }); pi.events.on(SUBAGENT_RESULT_INTERCOM_EVENT, (payload) => { relaySubagentIntercomPayload(payload, { sender: "subagent-result", status: "result", errorEntryType: "intercom_result_error", acknowledge: true, }); }); pi.on("session_start", (_event, ctx) => { if (!config.enabled) { return; } shuttingDown = false; disposed = false; runtimeStarted = true; runtimeGeneration += 1; reconnectAttempt = 0; clearReconnectTimer(); clearStartupConnectTimer(); runtimeContext = ctx; currentSessionId = ctx.sessionManager.getSessionId(); currentModel = ctx.model?.id ?? "unknown"; sessionStartedAt = Date.now(); agentRunning = false; activeTools.clear(); const startupGeneration = runtimeGeneration; startupConnectTimer = setTimeout(() => { startupConnectTimer = null; if (!getLiveContext(ctx, startupGeneration)) { return; } void ensureConnected("startup").catch(() => { if (!getLiveContext(ctx, startupGeneration)) { return; } client = null; scheduleReconnect(); }); }, 0); }); pi.on("session_shutdown", async () => { shuttingDown = true; disposed = true; runtimeGeneration += 1; clearStartupConnectTimer(); clearReconnectTimer(); rejectReplyWaiter(new Error("Session shutting down")); replyTracker.reset(); pendingIdleMessages.length = 0; clearInboundFlushTimer(); agentRunning = false; activeTools.clear(); if (client) { await client.disconnect(); client = null; } runtimeContext = null; currentSessionId = null; sessionStartedAt = null; }); pi.on("turn_end", () => { if (!getLiveContext()) { return; } replyTracker.endTurn(); scheduleInboundFlush(0); }); pi.on("agent_start", () => { if (!getLiveContext()) { return; } agentRunning = true; activeTools.clear(); syncPresenceStatus(); }); pi.on("tool_execution_start", (event) => { if (!getLiveContext()) { return; } activeTools.set(event.toolCallId, event.toolName); syncPresenceStatus(); }); pi.on("tool_execution_end", (event) => { if (!getLiveContext()) { return; } activeTools.delete(event.toolCallId); syncPresenceStatus(); }); pi.on("agent_end", () => { if (!getLiveContext()) { return; } agentRunning = false; activeTools.clear(); syncPresenceStatus(); scheduleInboundFlush(0); }); pi.on("turn_start", (_event, ctx) => { if (!getLiveContext(ctx)) { return; } currentSessionId = ctx.sessionManager.getSessionId(); syncPresenceIdentity(ctx.sessionManager.getSessionId()); replyTracker.beginTurn(); }); pi.on("model_select", (event, ctx) => { if (!getLiveContext(ctx)) { return; } currentModel = event.model.id; if (client) { client.updatePresence({ ...buildPresenceIdentity(pi, ctx.sessionManager.getSessionId()), model: event.model.id, status: currentStatus(), }); } }); pi.registerMessageRenderer("intercom_message", (message, _options, theme) => { const details = message.details as { from: SessionInfo; message: Message; replyCommand?: string; bodyText?: string } | undefined; if (!details) return undefined; return new InlineMessageComponent(details.from, details.message, theme, details.replyCommand, details.bodyText); }); const childOrchestratorMetadata = readChildOrchestratorMetadata(); if (childOrchestratorMetadata) { pi.registerTool({ name: "contact_supervisor", label: "Contact Supervisor", description: "Subagent-only tool for contacting the supervisor agent that delegated this task. Use need_decision when blocked, uncertain, needing approval, or facing a product/API/scope decision before continuing; this waits for the supervisor's reply. Use interview_request when multiple structured questions need supervisor answers; this also waits for a reply. Use progress_update only for meaningful progress or unexpected discoveries that change the plan; this does not wait for a reply. Do not use for routine completion handoffs.", promptSnippet: "Subagent-only: contact the supervisor for decisions, structured interviews, or meaningful plan-changing updates. Do not use for routine completion handoffs.", promptGuidelines: [ "Use contact_supervisor with reason='need_decision' when a subagent is blocked, uncertain, needs approval, or faces a product/API/scope decision before continuing.", "Use contact_supervisor with reason='interview_request' when the child needs multiple structured answers from the supervisor in one blocking exchange.", "Use contact_supervisor with reason='progress_update' only for meaningful progress or unexpected discoveries that change the plan.", "Do not use contact_supervisor for routine completion handoffs; return the final subagent result normally.", ], parameters: Type.Object({ reason: Type.String({ enum: ["need_decision", "progress_update", "interview_request"], description: "Contact reason: 'need_decision' waits for a reply; 'interview_request' sends structured questions and waits for a reply; 'progress_update' sends a non-blocking update", }), message: Type.Optional(Type.String({ description: "Decision request, optional interview note, or meaningful progress update for the supervisor", })), interview: Type.Optional(Type.Object({ title: Type.Optional(Type.String()), description: Type.Optional(Type.String()), questions: Type.Array(Type.Object({ id: Type.String(), type: Type.String({ description: "Question type: single, multi, text, image, or info" }), question: Type.String(), options: Type.Optional(Type.Array(Type.Any())), context: Type.Optional(Type.String()), })), }, { description: "Structured interview request for reason='interview_request'" })), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { const reason = params.reason as ContactSupervisorReason; if (reason !== "need_decision" && reason !== "progress_update" && reason !== "interview_request") { return { content: [{ type: "text", text: "Invalid reason. Use 'need_decision', 'interview_request', or 'progress_update'." }], isError: true, details: { error: true }, }; } if ((reason === "need_decision" || reason === "progress_update") && typeof params.message !== "string") { return { content: [{ type: "text", text: `Missing 'message' parameter for reason '${reason}'.` }], isError: true, details: { error: true }, }; } const interviewValidation = reason === "interview_request" ? validateSupervisorInterviewRequest(params.interview) : undefined; if (interviewValidation?.ok === false) { return { content: [{ type: "text", text: `Invalid interview request: ${interviewValidation.error}` }], isError: true, details: { error: true }, }; } const supervisorInterview = interviewValidation?.ok === true ? interviewValidation.interview : undefined; let connectedClient: IntercomClient; try { connectedClient = await ensureConnected("tool"); } catch (error) { return { content: [{ type: "text", text: `Intercom not connected: ${getErrorMessage(error)}` }], isError: true, details: { error: true }, }; } syncPresenceIdentity(ctx.sessionManager.getSessionId()); if (signal?.aborted) { return { content: [{ type: "text", text: "Cancelled" }], isError: true, details: { error: true }, }; } const metadata = childOrchestratorMetadata; let sendTo: string; try { sendTo = await resolveSessionTarget(connectedClient, metadata.orchestratorTarget) ?? metadata.orchestratorTarget; } catch (error) { return { content: [{ type: "text", text: `Failed to resolve supervisor target: ${getErrorMessage(error)}` }], isError: true, details: { error: true }, }; } if (signal?.aborted) { return { content: [{ type: "text", text: "Cancelled" }], isError: true, details: { error: true }, }; } if (sendTo === connectedClient.sessionId) { return { content: [{ type: "text", text: "Cannot message the current session" }], isError: true, details: { error: true }, }; } if (reason === "progress_update") { const message = params.message as string; try { const result = await connectedClient.send(sendTo, { text: formatChildOrchestratorMessage("update", metadata, message), }); if (!result.delivered) { const errorText = result.reason ?? "Session may not exist or has disconnected."; return { content: [{ type: "text", text: `Message to "${metadata.orchestratorTarget}" was not delivered: ${errorText}` }], isError: true, details: { messageId: result.id, delivered: false, reason: result.reason }, }; } pi.appendEntry("intercom_sent", { to: metadata.orchestratorTarget, message: { text: message, reason }, messageId: result.id, timestamp: Date.now(), subagent: { runId: metadata.runId, agent: metadata.agent, index: metadata.index }, }); return { content: [{ type: "text", text: `Progress update sent to supervisor ${metadata.orchestratorTarget}` }], isError: false, details: { messageId: result.id, delivered: true }, }; } catch (error) { return { content: [{ type: "text", text: `Failed to send progress update: ${getErrorMessage(error)}` }], isError: true, details: { error: true }, }; } } if (replyWaiter) { return { content: [{ type: "text", text: "Already waiting for a reply" }], isError: true, details: { error: true }, }; } let replyPromise: Promise | null = null; try { const questionId = randomUUID(); replyPromise = waitForReply(sendTo, questionId, signal); replyPromise.catch(() => undefined); if (signal?.aborted) { rejectReplyWaiter(new Error("Cancelled")); try { await replyPromise; } catch { // The waiter was intentionally rejected above; the tool result reports cancellation. } return { content: [{ type: "text", text: "Cancelled" }], isError: true, details: { error: true }, }; } const requestText = reason === "interview_request" ? formatChildOrchestratorMessage("interview", metadata, formatSupervisorInterviewRequest(supervisorInterview!, typeof params.message === "string" ? params.message : undefined)) : formatChildOrchestratorMessage("ask", metadata, params.message as string); const sendResult = await connectedClient.send(sendTo, { messageId: questionId, text: requestText, expectsReply: true, }); if (!sendResult.delivered) { const errorText = sendResult.reason ?? "Session may not exist or has disconnected."; rejectReplyWaiter(new Error(`Message to "${metadata.orchestratorTarget}" was not delivered: ${errorText}`)); if (replyPromise) { try { await replyPromise; } catch { // The waiter was already rejected above. Keep the delivery failure as the only error here. } } return { content: [{ type: "text", text: `Message to "${metadata.orchestratorTarget}" was not delivered: ${errorText}` }], isError: true, details: { error: true }, }; } pi.appendEntry("intercom_sent", { to: metadata.orchestratorTarget, message: { text: reason === "interview_request" ? requestText : params.message, reason, ...(reason === "interview_request" ? { interview: supervisorInterview } : {}), }, messageId: sendResult.id, timestamp: Date.now(), subagent: { runId: metadata.runId, agent: metadata.agent, index: metadata.index }, }); const replyMessage = await replyPromise; const replyText = replyMessage.content.text; const replyAttachments = replyMessage.content.attachments?.length ? formatAttachments(replyMessage.content.attachments) : ""; const structuredReply = reason === "interview_request" ? parseStructuredSupervisorReply(replyText, supervisorInterview!) : undefined; pi.appendEntry("intercom_received", { from: metadata.orchestratorTarget, message: { text: replyText, attachments: replyMessage.content.attachments }, messageId: replyMessage.id, timestamp: replyMessage.timestamp, subagent: { runId: metadata.runId, agent: metadata.agent, index: metadata.index }, }); return { content: [{ type: "text", text: `**Reply from supervisor:**\n${replyText}${replyAttachments}` }], isError: false, ...(structuredReply ? { details: structuredReply.value !== undefined ? { structuredReply: structuredReply.value } : { structuredReplyParseError: structuredReply.error } } : {}), }; } catch (error) { rejectReplyWaiter(toError(error)); if (replyPromise) { try { await replyPromise; } catch { // The waiter is cleanup-only on this path. The real failure is the one from the outer catch. } } return { content: [{ type: "text", text: `Failed: ${getErrorMessage(error)}` }], isError: true, details: { error: true }, }; } }, renderCall(args, theme) { const reason = typeof args.reason === "string" ? args.reason : "contact"; const messagePreview = previewText(args.message, 96); const interview = args.interview && typeof args.interview === "object" ? args.interview as { title?: unknown } : undefined; let text = theme.fg("toolTitle", theme.bold("contact_supervisor ")); text += theme.fg(reason === "need_decision" ? "warning" : reason === "progress_update" ? "muted" : "accent", reason); if (typeof interview?.title === "string" && interview.title.trim()) { text += " " + theme.fg("accent", interview.title.trim()); } if (messagePreview) { text += "\n " + theme.fg("dim", messagePreview); } return new Text(text, 0, 0); }, renderResult(result, { isPartial }, theme, context) { if (isPartial) { return new Text(theme.fg("warning", "Waiting for supervisor..."), 0, 0); } const details = result.details as { delivered?: boolean; error?: boolean; messageId?: string; reason?: string; structuredReplyParseError?: string } | undefined; const textContent = firstTextContent(result); const failed = Boolean(context.isError || details?.error === true || details?.delivered === false); const parseWarning = typeof details?.structuredReplyParseError === "string"; let text = failed ? theme.fg("error", "โœ— ") : parseWarning ? theme.fg("warning", "โš  ") : theme.fg("success", "โœ“ "); text += theme.fg(failed ? "error" : "text", textContent); if (parseWarning) { text += "\n" + theme.fg("warning", `Structured reply parse issue: ${details.structuredReplyParseError}`); } return new Text(text, 0, 0); }, }); } pi.registerTool({ name: "intercom", label: "Intercom", description: `Send a message to another pi session running on this machine. Use this to communicate findings, request help, or coordinate work with other sessions. Usage: intercom({ action: "list" }) โ†’ List active sessions intercom({ action: "send", to: "session-name", message: "..." }) โ†’ Send message intercom({ action: "ask", to: "session-name", message: "..." }) โ†’ Ask and wait for reply intercom({ action: "reply", message: "..." }) โ†’ Reply to the active/single pending ask intercom({ action: "pending" }) โ†’ List unresolved inbound asks intercom({ action: "status" }) โ†’ Show connection status`, promptSnippet: "Use to coordinate with other local pi sessions: list peers, send updates, ask for help, or check intercom connectivity.", parameters: Type.Object({ action: Type.String({ description: "Action: 'list', 'send', 'ask', 'reply', 'pending', or 'status'", }), to: Type.Optional(Type.String({ description: "Target session name or ID (for 'send', 'ask', or disambiguating 'reply')", })), message: Type.Optional(Type.String({ description: "Message to send (for 'send', 'ask', or 'reply' action)", })), attachments: Type.Optional(Type.Array(Type.Object({ type: Type.Union([Type.Literal("file"), Type.Literal("snippet"), Type.Literal("context")]), name: Type.String(), content: Type.String(), language: Type.Optional(Type.String()), }))), replyTo: Type.Optional(Type.String({ description: "Message ID to reply to (for threading or responding to an 'ask')", })), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { let connectedClient: IntercomClient; try { connectedClient = await ensureConnected("tool"); } catch (error) { return { content: [{ type: "text", text: `Intercom not connected: ${getErrorMessage(error)}` }], isError: true, details: { error: true }, }; } syncPresenceIdentity(ctx.sessionManager.getSessionId()); const { action, to, message, attachments, replyTo } = params; switch (action) { case "list": { try { const mySessionId = connectedClient.sessionId; const sessions = await connectedClient.listSessions(); const currentSession = sessions.find(s => s.id === mySessionId); const otherSessions = sessions.filter(s => s.id !== mySessionId); if (!currentSession) { return { content: [{ type: "text", text: "Current session is missing from intercom session list." }], isError: true, details: { error: true }, }; } const currentSection = `**Current session:**\n${formatSessionListRow(currentSession, currentSession.cwd, true)}`; const otherSection = otherSessions.length === 0 ? "**Other sessions:**\nNo other sessions connected." : `**Other sessions:**\n${otherSessions.map(s => formatSessionListRow(s, currentSession.cwd, false)).join("\n")}`; return { content: [{ type: "text", text: `${currentSection}\n\n${otherSection}` }], isError: false, }; } catch (error) { return { content: [{ type: "text", text: `Failed to list sessions: ${getErrorMessage(error)}` }], isError: true, details: { error: true }, }; } } case "send": { if (!to || !message) { return { content: [{ type: "text", text: "Missing 'to' or 'message' parameter" }], isError: true, details: { error: true }, }; } try { const sendTo = await resolveSessionTarget(connectedClient, to) ?? to; if (sendTo === connectedClient.sessionId) { return { content: [{ type: "text", text: "Cannot message the current session" }], isError: true, details: { error: true }, }; } if (!replyTo && config.confirmSend && ctx.hasUI) { const attachmentText = attachments?.length ? formatAttachments(attachments) : ""; const confirmed = await ctx.ui.confirm( "Send Message", `Send to "${to}":\n\n${message}${attachmentText}`, ); if (!confirmed) { return { content: [{ type: "text", text: "Message cancelled by user" }], isError: false, }; } } const result = await connectedClient.send(sendTo, { text: message, attachments, replyTo, }); if (!result.delivered) { const errorText = result.reason ?? "Session may not exist or has disconnected."; return { content: [{ type: "text", text: `Message to "${to}" was not delivered: ${errorText}` }], isError: true, details: { messageId: result.id, delivered: false, reason: result.reason }, }; } pi.appendEntry("intercom_sent", { to, message: { text: message, attachments, replyTo }, messageId: result.id, timestamp: Date.now(), }); if (replyTo) { replyTracker.markReplied(replyTo); } return { content: [{ type: "text", text: `Message sent to ${to}` }], isError: false, details: { messageId: result.id, delivered: true }, }; } catch (error) { return { content: [{ type: "text", text: `Failed to send: ${getErrorMessage(error)}` }], isError: true, details: { error: true }, }; } } case "ask": { if (!to || !message) { return { content: [{ type: "text", text: "Missing 'to' or 'message' parameter" }], isError: true, details: { error: true }, }; } if (replyWaiter) { return { content: [{ type: "text", text: "Already waiting for a reply" }], isError: true, details: { error: true }, }; } if (_signal?.aborted) { return { content: [{ type: "text", text: "Cancelled" }], isError: true, details: { error: true }, }; } let replyPromise: Promise | null = null; try { const sendTo = await resolveSessionTarget(connectedClient, to) ?? to; if (_signal?.aborted) { return { content: [{ type: "text", text: "Cancelled" }], isError: true, details: { error: true }, }; } if (sendTo === connectedClient.sessionId) { return { content: [{ type: "text", text: "Cannot message the current session" }], isError: true, details: { error: true }, }; } const questionId = randomUUID(); replyPromise = waitForReply(sendTo, questionId, _signal); const sendResult = await connectedClient.send(sendTo, { messageId: questionId, text: message, attachments, replyTo, expectsReply: true, }); if (!sendResult.delivered) { const errorText = sendResult.reason ?? "Session may not exist or has disconnected."; rejectReplyWaiter(new Error(`Message to "${to}" was not delivered: ${errorText}`)); if (replyPromise) { try { await replyPromise; } catch { // The waiter was already rejected above. Keep the delivery failure as the only error here. } } return { content: [{ type: "text", text: `Message to "${to}" was not delivered: ${errorText}` }], isError: true, details: { error: true }, }; } pi.appendEntry("intercom_sent", { to, message: { text: message, attachments, replyTo }, messageId: sendResult.id, timestamp: Date.now(), }); const replyMessage = await replyPromise; const replyText = replyMessage.content.text; const replyAttachments = replyMessage.content.attachments?.length ? formatAttachments(replyMessage.content.attachments) : ""; pi.appendEntry("intercom_received", { from: to, message: { text: replyText, attachments: replyMessage.content.attachments }, messageId: replyMessage.id, timestamp: replyMessage.timestamp, }); return { content: [{ type: "text", text: `**Reply from ${to}:**\n${replyText}${replyAttachments}` }], isError: false, }; } catch (error) { rejectReplyWaiter(toError(error)); if (replyPromise) { try { await replyPromise; } catch { // The waiter is cleanup-only on this path. The real failure is the one from the outer catch. } } return { content: [{ type: "text", text: `Failed: ${getErrorMessage(error)}` }], isError: true, details: { error: true }, }; } } case "reply": { if (!message) { return { content: [{ type: "text", text: "Missing 'message' parameter" }], isError: true, details: { error: true }, }; } try { const target = replyTracker.resolveReplyTarget({ to }); if (target.from.id === connectedClient.sessionId) { return { content: [{ type: "text", text: "Cannot message the current session" }], isError: true, details: { error: true }, }; } const result = await connectedClient.send(target.from.id, { text: message, replyTo: target.message.id, }); if (!result.delivered) { const errorText = result.reason ?? "Session may not exist or has disconnected."; return { content: [{ type: "text", text: `Reply to "${target.from.name || target.from.id}" was not delivered: ${errorText}` }], isError: true, details: { messageId: result.id, delivered: false, reason: result.reason }, }; } replyTracker.markReplied(target.message.id); pi.appendEntry("intercom_sent", { to: target.from.name || target.from.id, message: { text: message, replyTo: target.message.id }, messageId: result.id, timestamp: Date.now(), }); return { content: [{ type: "text", text: `Reply sent to ${target.from.name || target.from.id}` }], isError: false, details: { messageId: result.id, delivered: true, replyTo: target.message.id }, }; } catch (error) { return { content: [{ type: "text", text: `Failed to reply: ${getErrorMessage(error)}` }], isError: true, details: { error: true }, }; } } case "pending": { const pendingAsks = replyTracker.listPending(); if (pendingAsks.length === 0) { return { content: [{ type: "text", text: "No unresolved inbound asks." }], isError: false, }; } const now = Date.now(); const lines = pendingAsks.map(({ from, message, receivedAt }) => { const preview = message.content.text.replace(/\s+/g, " ").slice(0, 80); const elapsedSeconds = Math.max(0, Math.floor((now - receivedAt) / 1000)); return `- ${from.name || from.id} ยท ${message.id} ยท ${elapsedSeconds}s ago ยท ${preview}`; }); return { content: [{ type: "text", text: `**Pending asks:**\n${lines.join("\n")}` }], isError: false, }; } case "status": { try { const mySessionId = connectedClient.sessionId; const sessions = await connectedClient.listSessions(); return { content: [{ type: "text", text: `**Intercom Status:**\nConnected: Yes\nSession ID: ${mySessionId}\nActive sessions: ${sessions.length}`, }], isError: false, }; } catch (error) { return { content: [{ type: "text", text: `Failed to get status: ${getErrorMessage(error)}` }], isError: true, details: { error: true }, }; } } default: return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true, details: { error: true }, }; } }, renderCall(args, theme) { const action = typeof args.action === "string" ? args.action : "intercom"; const target = typeof args.to === "string" && args.to.trim() ? args.to.trim() : undefined; const messagePreview = previewText(args.message, 96); const attachmentCount = Array.isArray(args.attachments) ? args.attachments.length : 0; let text = theme.fg("toolTitle", theme.bold("intercom ")); text += theme.fg(action === "ask" ? "warning" : action === "reply" ? "success" : "accent", action); if (target) { text += " " + theme.fg("muted", "โ†’") + " " + theme.fg("accent", target); } if (attachmentCount > 0) { text += " " + theme.fg("dim", `(${attachmentCount} attachment${attachmentCount === 1 ? "" : "s"})`); } if (messagePreview) { text += "\n " + theme.fg("dim", messagePreview); } return new Text(text, 0, 0); }, renderResult(result, { isPartial }, theme, context) { if (isPartial) { return new Text(theme.fg("warning", "Intercom working..."), 0, 0); } const details = result.details as { delivered?: boolean; error?: boolean; messageId?: string; reason?: string } | undefined; const failed = Boolean(context.isError || details?.error === true || details?.delivered === false); let text = failed ? theme.fg("error", "โœ— ") : theme.fg("success", "โœ“ "); text += theme.fg(failed ? "error" : "text", firstTextContent(result)); if (details?.messageId && !context.expanded) { text += theme.fg("dim", ` (${details.messageId.slice(0, 8)})`); } if (details?.reason && context.expanded) { text += "\n" + theme.fg("dim", `Reason: ${details.reason}`); } return new Text(text, 0, 0); }, }); async function openIntercomOverlay(ctx: ExtensionContext): Promise { const overlayGeneration = runtimeGeneration; const liveContext = getLiveContext(ctx, overlayGeneration); if (!liveContext?.hasUI) return; let overlayClient: IntercomClient; try { overlayClient = await ensureConnected("overlay"); } catch (error) { notifyIfLive(ctx, `Intercom unavailable: ${getErrorMessage(error)}`, "error", overlayGeneration); return; } if (!getLiveContext(ctx, overlayGeneration)) return; syncPresenceIdentity(ctx.sessionManager.getSessionId()); let currentSession: SessionInfo; let sessions: SessionInfo[]; let duplicates: Set; try { const mySessionId = overlayClient.sessionId; const allSessions = await overlayClient.listSessions(); if (!getLiveContext(ctx, overlayGeneration)) return; const foundCurrentSession = allSessions.find(s => s.id === mySessionId); if (!foundCurrentSession) { notifyIfLive(ctx, "Current session is missing from intercom session list", "error", overlayGeneration); return; } currentSession = foundCurrentSession; duplicates = duplicateSessionNames(allSessions); sessions = allSessions.filter(s => s.id !== mySessionId); } catch (error) { notifyIfLive(ctx, `Failed to list sessions: ${getErrorMessage(error)}`, "error", overlayGeneration); return; } const selectedSession = await ctx.ui.custom( (_tui, theme, keybindings, done) => new SessionListOverlay(theme, keybindings, currentSession, sessions, done), { overlay: true } ).catch(() => undefined); if (!selectedSession || !getLiveContext(ctx, overlayGeneration)) return; try { overlayClient = await ensureConnected("overlay"); } catch (error) { notifyIfLive(ctx, `Intercom unavailable: ${getErrorMessage(error)}`, "error", overlayGeneration); return; } if (!getLiveContext(ctx, overlayGeneration)) return; const targetLabel = formatSessionLabel(selectedSession, duplicates); const result = await ctx.ui.custom( (tui, theme, keybindings, done) => new ComposeOverlay(tui, theme, keybindings, selectedSession, targetLabel, overlayClient, done), { overlay: true } ).catch(() => undefined); if (result?.sent && result.messageId && result.text && getLiveContext(ctx, overlayGeneration)) { pi.appendEntry("intercom_sent", { to: selectedSession.name || selectedSession.id, message: { text: result.text }, messageId: result.messageId, timestamp: Date.now(), }); notifyIfLive(ctx, `Message sent to ${targetLabel}`, "info", overlayGeneration); } } pi.registerCommand("intercom", { description: "Open session intercom overlay", handler: async (_args, ctx) => openIntercomOverlay(ctx), }); pi.registerShortcut("alt+m", { description: "Open session intercom", handler: async (ctx) => openIntercomOverlay(ctx), }); }