import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { StringEnum } from "@earendil-works/pi-ai"; import { randomUUID } from "crypto"; import { spawn, spawnSync } from "child_process"; import { Type } from "typebox"; import { Text } from "@earendil-works/pi-tui"; import { IntercomClient, type SendResult } 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 { sanitizeDisplayText, shortestUniqueIdPrefixes } from "./ui/session-identity.ts"; import { getAskTimeoutMs, getAskWaitMs, loadConfig, type IntercomConfig } from "./config.ts"; import type { SessionInfo, Message, Attachment } from "./types.ts"; import { ReplyTracker } from "./reply-tracker.ts"; import { InboundMessageConflictError, PersistentInboundInbox, type StoredInboundMessage } from "./inbound-inbox.ts"; import { formatIntercomTeam, resolveIntercomTeam } from "./team.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_BATCH_QUIET_MS = 300; const INBOUND_BATCH_MAX_LATENCY_MS = 1000; 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_ORCHESTRATOR_SESSION_ID_ENV = "PI_SUBAGENT_ORCHESTRATOR_SESSION_ID"; const INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID"; const NAME_POLL_MS_ENV = "PI_INTERCOM_NAME_POLL_MS"; 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; orchestratorSessionId?: string; runId: string; agent: string; index: string; sessionName?: string; } interface InboundMessageEntry { key?: string; from: SessionInfo; message: Message; receivedAt: number; replyCommand?: string; bodyText: string; } interface InboundMessageBatchDetails { entries: InboundMessageEntry[]; } 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)); } class AskWaitElapsedError extends Error { constructor( readonly target: string, readonly replyTo: string, readonly waitMs: number, ) { super(`No reply from "${target}" within ${formatDuration(waitMs)}`); this.name = "AskWaitElapsedError"; } } function formatDuration(durationMs: number): string { if (durationMs % 60000 === 0) { const minutes = durationMs / 60000; return `${minutes} minute${minutes === 1 ? "" : "s"}`; } if (durationMs % 1000 === 0) { const seconds = durationMs / 1000; return `${seconds} second${seconds === 1 ? "" : "s"}`; } return `${durationMs}ms`; } function deferredAskResult(target: string, error: AskWaitElapsedError, deferred: boolean) { return { content: [{ type: "text" as const, text: deferred ? `Ask delivered to ${target}, but no reply arrived within ${formatDuration(error.waitMs)}. Deferral requested. Continuing asynchronously; a late reply will arrive as a new intercom message.` : `Ask delivered to ${target}, but no reply arrived within ${formatDuration(error.waitMs)}. Continuing without waiting; the connection closed before asynchronous deferral could be confirmed.`, }], details: { delivered: true, pending: true, deferred, deferRequested: deferred, waitTimedOut: true, messageId: error.replyTo, }, }; } function deliveryResultDetails(result: SendResult, extra: Record = {}): Record { return { messageId: result.id, accepted: result.accepted, delivered: result.delivered, ...(result.deliveryId ? { deliveryId: result.deliveryId } : {}), ...(result.code ? { code: result.code } : {}), ...(result.reason ? { reason: result.reason } : {}), ...extra, }; } 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 orchestratorSessionId = process.env[SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV]?.trim() || process.env[INTERCOM_SESSION_ID_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, ...(orchestratorSessionId ? { orchestratorSessionId } : {}), 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 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, idPrefix: string): string { if (!session.name) { return sanitizeDisplayText(idPrefix, session.id); } const name = sanitizeDisplayText(session.name, "Unnamed session"); return duplicates.has(session.name.toLowerCase()) ? `${name} (${sanitizeDisplayText(idPrefix, session.id)})` : name; } function formatSessionListRow(session: SessionInfo, currentCwd: string, isSelf: boolean, idPrefix: string): string { const name = sanitizeDisplayText(session.name, "Unnamed session"); const cwd = sanitizeDisplayText(session.cwd, "Unknown path"); const model = sanitizeDisplayText(session.model, "Unknown model"); const status = sanitizeDisplayText(session.status); const tags = [isSelf ? "self" : session.cwd === currentCwd ? "same cwd" : undefined, status || undefined] .filter((tag): tag is string => Boolean(tag)); const suffix = tags.length ? ` [${tags.join(", ")}]` : ""; return `โ€ข ${name} (${sanitizeDisplayText(idPrefix, session.id)}) โ€” ${cwd} (${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 function chooseContactTarget(currentSession: SessionInfo, sessions: SessionInfo[]): { target: string; name?: string; id: string; duplicateName: boolean } { const duplicates = duplicateSessionNames(sessions); const name = currentSession.name?.trim() || undefined; const duplicateName = Boolean(name && duplicates.has(name.toLowerCase())); return { target: name && !duplicateName ? name : currentSession.id, ...(name ? { name } : {}), id: currentSession.id, duplicateName, }; } export function formatContactInstruction(contact: { target: string; id: string; name?: string; duplicateName?: boolean }): string { return contact.target === contact.id ? `Intercom target: ${contact.id}` : `Intercom target: ${contact.target}\nStable session ID: ${contact.id}`; } interface ClipboardCopyResult { ok: boolean; method?: string; error?: string; } function runDetachedClipboardCommand(command: string, args: string[], text: string): ClipboardCopyResult { const found = spawnSync("which", [command], { stdio: "ignore", timeout: 1000 }); if (found.status !== 0) return { ok: false, error: `${command} not found` }; try { const proc = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"] }); proc.stdin.on("error", () => { // Ignore EPIPE if the clipboard helper exits early. }); proc.stdin.write(text); proc.stdin.end(); proc.unref(); return { ok: true, method: command }; } catch (error) { return { ok: false, error: getErrorMessage(error) }; } } function runClipboardCommand(command: string, args: string[], text: string): ClipboardCopyResult { if (command === "wl-copy") return runDetachedClipboardCommand(command, args, text); const result = spawnSync(command, args, { input: text, encoding: "utf8", timeout: 2000, stdio: ["pipe", "ignore", "pipe"], }); if (result.status === 0) return { ok: true, method: command }; if (result.error) return { ok: false, error: result.error.message }; return { ok: false, error: result.stderr?.toString().trim() || `${command} exited ${result.status}` }; } export function copyTextToClipboard(text: string): ClipboardCopyResult { const candidates: Array<[string, string[]]> = []; if (process.platform === "darwin") candidates.push(["pbcopy", []]); else if (process.platform === "win32") candidates.push(["clip.exe", []]); else { if (process.env.WAYLAND_DISPLAY) candidates.push(["wl-copy", []]); if (process.env.DISPLAY) { candidates.push(["xclip", ["-selection", "clipboard"]]); candidates.push(["xsel", ["--clipboard", "--input"]]); } candidates.push(["clip.exe", []]); } let lastError = "No clipboard command available"; for (const [command, args] of candidates) { const result = runClipboardCommand(command, args, text); if (result.ok) return result; lastError = result.error ?? lastError; } return { ok: false, error: lastError }; } function getNamePollMs(): number { const configured = process.env[NAME_POLL_MS_ENV]; if (configured !== undefined) { const value = Number(configured); if (Number.isFinite(value) && value > 0) { return value; } } return 1000; } export default function piIntercomExtension(pi: ExtensionAPI) { let client: IntercomClient | null = null; const config: IntercomConfig = loadConfig(); const askWaitMs = getAskWaitMs(); 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 namePollTimer: NodeJS.Timeout | null = null; let lastPresenceName: string | null = null; const previousIntercomSessionId = process.env[INTERCOM_SESSION_ID_ENV]; 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(); let inboundInbox: PersistentInboundInbox | null = null; let inboundFirstQueuedAt: number | null = null; let inboundLastQueuedAt: number | null = null; 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, onCancel?: () => void): 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 AskWaitElapsedError(from, replyTo, askWaitMs)); }, askWaitMs); const cleanup = () => { clearTimeout(timeout); signal?.removeEventListener("abort", onAbort); if (replyWaiter?.replyTo === replyTo) { replyWaiter = null; } }; const onAbort = () => { onCancel?.(); 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 clearNamePollTimer(): void { if (!namePollTimer) { return; } clearInterval(namePollTimer); namePollTimer = 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"; const queueStatus = inboundInbox?.size ? ` ยท inbox:${inboundInbox.size}` : ""; const outboxStatus = client?.outboxSize ? ` ยท outbox:${client.outboxSize}` : ""; return config.status ? `${lifecycleStatus}${queueStatus}${outboxStatus} ยท ${config.status}` : `${lifecycleStatus}${queueStatus}${outboxStatus}`; } 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, model: currentModel, pid: process.pid, startedAt: sessionStartedAt, lastActivity: Date.now(), status: currentStatus(), }; } function syncPresenceIdentity(sessionId: string): void { if (!client || !getLiveContext()) { return; } const identity = buildPresenceIdentity(pi, sessionId); lastPresenceName = identity.name; client.updatePresence({ ...identity, status: currentStatus() }); } function startNamePoll(): void { clearNamePollTimer(); lastPresenceName = currentSessionId ? buildPresenceIdentity(pi, currentSessionId).name : null; namePollTimer = setInterval(() => { if (!currentSessionId || !getLiveContext()) { return; } const identity = buildPresenceIdentity(pi, currentSessionId); if (identity.name !== lastPresenceName) { syncPresenceIdentity(currentSessionId); } }, getNamePollMs()); namePollTimer.unref?.(); } function publishIntercomSessionId(sessionId: string): void { process.env[INTERCOM_SESSION_ID_ENV] = sessionId; } function restoreIntercomSessionId(): void { if (previousIntercomSessionId === undefined) { delete process.env[INTERCOM_SESSION_ID_ENV]; return; } process.env[INTERCOM_SESSION_ID_ENV] = previousIntercomSessionId; } 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 shouldTriggerInboundMessage(entry: InboundMessageEntry, forceTrigger = false): boolean { if (forceTrigger) { return true; } if (config.inboundTrigger === "always") { return true; } if (config.inboundTrigger === "replies") { return Boolean(entry.message.replyTo); } return false; } function entryFromStoredMessage(stored: StoredInboundMessage, batchSize: number): InboundMessageEntry { const attachmentText = stored.message.content.attachments?.length ? formatAttachments(stored.message.content.attachments) : ""; const replyCommand = config.replyHint && stored.message.expectsReply ? batchSize === 1 ? `intercom_reply({ message: "..." })` : `intercom_reply({ to: "${stored.from.id}", message: "..." })` : undefined; return { key: stored.key, from: stored.from, message: stored.message, receivedAt: stored.receivedAt, replyCommand, bodyText: `${stored.message.content.text}${attachmentText}`, }; } function formatIncomingEntry(entry: InboundMessageEntry, position?: { index: number; total: number }): string { const senderDisplay = entry.from.name || entry.from.id.slice(0, 8); const prefix = position ? `[${position.index}/${position.total}] ` : ""; const replyInstruction = entry.replyCommand ? `\n\nTo reply, use: ${entry.replyCommand}` : ""; return `**${prefix}๐Ÿ“จ From ${senderDisplay}** (${entry.from.cwd})${replyInstruction}\n\n${entry.bodyText}`; } function sendIncomingBatch(entries: InboundMessageEntry[], generation = runtimeGeneration, forceTrigger = false): void { if (entries.length === 0) return; if (runtimeStarted && !getLiveContext(runtimeContext, generation)) { return; } const contexts = entries.map((entry) => replyTracker.recordIncomingMessage(entry.from, entry.message, entry.receivedAt)); replyTracker.queueTurnContexts(contexts); const content = entries.length === 1 ? formatIncomingEntry(entries[0]!) : `**๐Ÿ“จ Intercom batch (${entries.length} messages)**\n\n${entries.map((entry, index) => formatIncomingEntry(entry, { index: index + 1, total: entries.length })).join("\n\n---\n\n")}`; const triggerTurn = forceTrigger || entries.some((entry) => shouldTriggerInboundMessage(entry)); pi.sendMessage( { customType: "intercom_message", content, display: true, details: { entries } satisfies InboundMessageBatchDetails, }, triggerTurn ? { triggerTurn: true } : { deliverAs: "followUp" } ); } function refreshInboundBatchWindow(): void { const pending = inboundInbox?.list() ?? []; inboundFirstQueuedAt = pending.length > 0 ? pending[0]!.receivedAt : null; inboundLastQueuedAt = pending.length > 0 ? pending[pending.length - 1]!.receivedAt : null; } function scheduleInboundFlush(delayMs?: number): void { if (!getLiveContext()) { return; } if (!inboundInbox || inboundInbox.size === 0) return; const scheduledGeneration = runtimeGeneration; const now = Date.now(); const computedDelay = delayMs ?? Math.max(0, Math.min( (inboundLastQueuedAt ?? now) + INBOUND_BATCH_QUIET_MS, (inboundFirstQueuedAt ?? now) + INBOUND_BATCH_MAX_LATENCY_MS, ) - now); clearInboundFlushTimer(); inboundFlushTimer = setTimeout(() => { inboundFlushTimer = null; flushIdleMessages(scheduledGeneration); }, computedDelay); inboundFlushTimer.unref?.(); } function flushIdleMessages(generation = runtimeGeneration): void { if (!inboundInbox || inboundInbox.size === 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 stored = inboundInbox.list(); const entries = stored.map((entry) => entryFromStoredMessage(entry, stored.length)); try { sendIncomingBatch(entries, generation); inboundInbox.consume(stored.map((entry) => entry.key)); refreshInboundBatchWindow(); syncPresenceStatus(); } catch (error) { pi.appendEntry("intercom_inbox_delivery_error", { messageIds: stored.map((entry) => entry.message.id), error: getErrorMessage(error), timestamp: Date.now(), }); scheduleInboundFlush(INBOUND_IDLE_RETRY_MS); } } async function handleIncomingMessage(ctx: ExtensionContext, from: SessionInfo, message: Message, deliveryId: string, receivingClient: IntercomClient): Promise { const messageGeneration = runtimeGeneration; const liveContext = getLiveContext(ctx, messageGeneration); const inboxAtReceive = inboundInbox; if (!liveContext || !inboxAtReceive) { return; } let enqueued; try { enqueued = inboxAtReceive.enqueue(from, message); } catch (error) { if (error instanceof InboundMessageConflictError) { receivingClient.rejectMessage(deliveryId, error.message); } pi.appendEntry("intercom_inbox_persist_error", { from: from.id, messageId: message.id, error: getErrorMessage(error), timestamp: Date.now(), }); return; } receivingClient.acknowledgeMessage(deliveryId); if (enqueued.duplicate) 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); inboxAtReceive.consume([enqueued.entry.key]); refreshInboundBatchWindow(); syncPresenceStatus(); return; } } replyTracker.recordIncomingMessage(from, message, enqueued.entry.receivedAt); if (inboundFirstQueuedAt === null) inboundFirstQueuedAt = enqueued.entry.receivedAt; inboundLastQueuedAt = enqueued.entry.receivedAt; syncPresenceStatus(); scheduleInboundFlush(); } function attachClientHandlers(nextClient: IntercomClient): void { nextClient.on("message", (from: SessionInfo, message: Message, deliveryId: string) => { const liveContext = getLiveContext(); if (client !== nextClient || !liveContext) { return; } void handleIncomingMessage(liveContext, from, message, deliveryId, nextClient).catch((error) => { pi.appendEntry("intercom_inbox_error", { from: from.id, messageId: message.id, error: getErrorMessage(error), timestamp: Date.now(), }); }); }); nextClient.on("ask_deferred", (messageId: string, fromSessionId: string) => { replyTracker.markDeferred(messageId, fromSessionId); }); nextClient.on("ask_cancelled", (messageId: string, fromSessionId: string) => { replyTracker.dismissPendingAsk(messageId, fromSessionId); inboundInbox?.dismissPendingAsk(messageId, fromSessionId); const cancelledKeys = (inboundInbox?.list() ?? []) .filter((entry) => entry.from.id === fromSessionId && entry.message.id === messageId) .map((entry) => entry.key); inboundInbox?.consume(cancelledKeys); refreshInboundBatchWindow(); syncPresenceStatus(); }); nextClient.on("outbox_delivered", (messageId: string, deliveryId: string) => { pi.appendEntry("intercom_outbox_delivered", { messageId, deliveryId, timestamp: Date.now() }); syncPresenceStatus(); }); nextClient.on("outbox_failed", (messageId: string, code: string, reason: string) => { pi.appendEntry("intercom_outbox_failed", { messageId, code, reason, timestamp: Date.now() }); syncPresenceStatus(); }); 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(), currentSessionId); if (!getLiveContext(contextAtStart, generationAtStart)) { await nextClient.disconnect(true); 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.`); } if (byName.length === 1) { return byName[0]!.id; } const byIdPrefix = sessions.filter(s => s.id.startsWith(nameOrId)); if (byIdPrefix.length === 1) { return byIdPrefix[0]!.id; } if (byIdPrefix.length > 1) { throw new Error(`Multiple sessions match ID prefix "${nameOrId}". Use a longer session ID prefix.`); } return null; } async function resolveSupervisorTarget(activeClient: IntercomClient, metadata: ChildOrchestratorMetadata): Promise { if (metadata.orchestratorSessionId) { const bySessionId = await resolveSessionTarget(activeClient, metadata.orchestratorSessionId); if (bySessionId) { return bySessionId; } } return await resolveSessionTarget(activeClient, metadata.orchestratorTarget) ?? metadata.orchestratorTarget; } function deliverLocalSubagentRelayMessage(sender: "subagent-control" | "subagent-result", status: string, messageText: string): void { const liveContext = getLiveContext(); const now = Date.now(); sendIncomingBatch([{ from: { id: sender, name: sender, cwd: liveContext?.cwd ?? "", model: sender, pid: process.pid, startedAt: now, lastActivity: now, status, }, message: { id: randomUUID(), timestamp: now, content: { text: messageText }, }, receivedAt: now, bodyText: messageText, }], runtimeGeneration, true); } function recordSubagentDeliveryError(entryType: string, to: string, message: string, error: unknown): void { pi.appendEntry(entryType, { to, message, error: getErrorMessage(error), timestamp: Date.now(), }); } function startSessionRuntime(ctx: ExtensionContext): void { const previousClient = client; if (previousClient) { client = null; void previousClient.disconnect(true).catch(() => undefined); } shuttingDown = false; disposed = false; runtimeStarted = true; runtimeGeneration += 1; reconnectAttempt = 0; clearReconnectTimer(); clearStartupConnectTimer(); clearNamePollTimer(); clearInboundFlushTimer(); rejectReplyWaiter(new Error("Session replaced")); replyTracker.reset(); runtimeContext = ctx; currentSessionId = ctx.sessionManager.getSessionId(); inboundInbox = new PersistentInboundInbox(currentSessionId); inboundInbox.prunePendingAsks(getAskTimeoutMs()); const recoveredMessages = inboundInbox.list(); for (const recovered of recoveredMessages) { replyTracker.recordIncomingMessage(recovered.from, recovered.message, recovered.receivedAt); } for (const pendingAsk of inboundInbox.listPendingAsks()) { replyTracker.recordIncomingMessage(pendingAsk.from, pendingAsk.message, pendingAsk.receivedAt); } refreshInboundBatchWindow(); publishIntercomSessionId(currentSessionId); currentModel = ctx.model?.id ?? "unknown"; sessionStartedAt = Date.now(); lastPresenceName = buildPresenceIdentity(pi, currentSessionId).name; agentRunning = false; activeTools.clear(); startNamePoll(); if (recoveredMessages.length > 0) scheduleInboundFlush(0); 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); } 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); } })(); } const unsubscribeSubagentControlIntercom = pi.events.on(SUBAGENT_CONTROL_INTERCOM_EVENT, (payload) => { relaySubagentIntercomPayload(payload, { sender: "subagent-control", status: "needs_attention", errorEntryType: "intercom_control_error", }); }); const unsubscribeSubagentResultIntercom = 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; } startSessionRuntime(ctx); }); pi.on("session_shutdown", async () => { unsubscribeSubagentControlIntercom(); unsubscribeSubagentResultIntercom(); shuttingDown = true; disposed = true; runtimeGeneration += 1; clearStartupConnectTimer(); clearReconnectTimer(); clearNamePollTimer(); restoreIntercomSessionId(); rejectReplyWaiter(new Error("Session shutting down")); replyTracker.reset(); clearInboundFlushTimer(); inboundInbox = null; inboundFirstQueuedAt = null; inboundLastQueuedAt = null; agentRunning = false; activeTools.clear(); if (client) { await client.disconnect(true); 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) => { const sessionId = ctx.sessionManager.getSessionId(); if (!currentSessionId || sessionId !== currentSessionId) { if (!config.enabled) { return; } startSessionRuntime(ctx); replyTracker.beginTurn(); return; } if (!getLiveContext(ctx)) { return; } syncPresenceIdentity(sessionId); 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 InboundMessageBatchDetails | InboundMessageEntry | undefined; if (!details) return undefined; const entries = "entries" in details ? details.entries : [details]; if (entries.length !== 1) { return new Text(typeof message.content === "string" ? message.content : "Intercom message batch", 0, 0); } const entry = entries[0]!; return new InlineMessageComponent(entry.from, entry.message, theme, entry.replyCommand, entry.bodyText, !options.expanded); }); const intercomResultToolNames = new Set([ "intercom", "intercom_send", "intercom_ask", "intercom_reply", "intercom_list", "intercom_pending", "intercom_status", "intercom_team", "contact_supervisor", ]); pi.on("tool_result", (event) => { if (!intercomResultToolNames.has(event.toolName)) { return; } if (!event.details || typeof event.details !== "object") { return; } const details = event.details as { error?: unknown; delivered?: unknown }; if (details.error === true || details.delivered === false) { return { isError: true }; } }); 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 up to 30 seconds, then continues asynchronously if unanswered. Use interview_request when multiple structured questions need supervisor answers; it has the same soft wait. 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; after the 30-second soft wait, continue only with work that does not depend on the answer.", "Use contact_supervisor with reason='interview_request' when the child needs multiple structured answers from the supervisor; after the 30-second soft wait, the reply may arrive asynchronously.", "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: StringEnum(["need_decision", "progress_update", "interview_request"] as const, { description: "Contact reason: 'need_decision' and 'interview_request' wait up to 30 seconds before continuing asynchronously; '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: StringEnum(["single", "multi", "text", "image", "info"] as const, { 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'." }], 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}'.` }], 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}` }], 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)}` }], details: { error: true }, }; } syncPresenceIdentity(ctx.sessionManager.getSessionId()); if (signal?.aborted) { return { content: [{ type: "text", text: "Cancelled" }], details: { error: true }, }; } const metadata = childOrchestratorMetadata; let sendTo: string; try { sendTo = await resolveSupervisorTarget(connectedClient, metadata); } catch (error) { return { content: [{ type: "text", text: `Failed to resolve supervisor target: ${getErrorMessage(error)}` }], details: { error: true }, }; } if (signal?.aborted) { return { content: [{ type: "text", text: "Cancelled" }], details: { error: true }, }; } if (sendTo === connectedClient.sessionId) { return { content: [{ type: "text", text: "Cannot message the current session" }], 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}` }], details: deliveryResultDetails(result), }; } 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}` }], details: deliveryResultDetails(result), }; } catch (error) { return { content: [{ type: "text", text: `Failed to send progress update: ${getErrorMessage(error)}` }], details: { error: true }, }; } } if (replyWaiter) { return { content: [{ type: "text", text: "Already waiting for a reply" }], details: { error: true }, }; } let replyPromise: Promise | null = null; try { const questionId = randomUUID(); replyPromise = waitForReply(sendTo, questionId, signal, () => { void connectedClient.cancelAsk(questionId); }); 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" }], 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}` }], details: { ...deliveryResultDetails(sendResult), 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}` }], details: structuredReply ? structuredReply.value !== undefined ? { structuredReply: structuredReply.value } : { structuredReplyParseError: structuredReply.error } : {}, }; } catch (error) { if (error instanceof AskWaitElapsedError) { return deferredAskResult(metadata.orchestratorTarget, error, await connectedClient.deferAsk(error.replyTo)); } 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)}` }], 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); }, } as any); } const legacyIntercomTool = { name: "intercom", label: "Intercom (Legacy)", 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, waiting up to 30 seconds before continuing asynchronously 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: StringEnum(["list", "send", "ask", "reply", "pending", "status"] as const, { 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: StringEnum(["file", "snippet", "context"] as const), 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)}` }], 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); const idPrefixes = shortestUniqueIdPrefixes(sessions.map((session) => session.id), 8); if (!currentSession) { return { content: [{ type: "text", text: "Current session is missing from intercom session list." }], details: { error: true }, }; } const currentSection = `**Current session:**\n${formatSessionListRow(currentSession, currentSession.cwd, true, idPrefixes.get(currentSession.id) ?? currentSession.id)}`; const otherSection = otherSessions.length === 0 ? "**Other sessions:**\nNo other sessions connected." : `**Other sessions:**\n${otherSessions.map(s => formatSessionListRow(s, currentSession.cwd, false, idPrefixes.get(s.id) ?? s.id)).join("\n")}`; return { content: [{ type: "text", text: `${currentSection}\n\n${otherSection}` }], details: {}, }; } catch (error) { return { content: [{ type: "text", text: `Failed to list sessions: ${getErrorMessage(error)}` }], details: { error: true }, }; } } case "send": { if (!to && replyTo) { return { content: [{ type: "text", text: `Missing required 'to' for action 'send'. 'replyTo' is a message ID, not a recipient. Retry with { action: "send", to: "${replyTo}", message: "..." } if that value is the intended session target.` }], details: { error: true, missing: ["to"], invalidRecipientField: "replyTo" }, }; } if (!to || !message) { return { content: [{ type: "text", text: "Missing 'to' or 'message' parameter" }], 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" }], 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" }], details: {}, }; } } 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}` }], details: deliveryResultDetails(result), }; } pi.appendEntry("intercom_sent", { to, message: { text: message, attachments, replyTo }, messageId: result.id, timestamp: Date.now(), }); if (replyTo) { replyTracker.markReplied(replyTo, sendTo); inboundInbox?.dismissPendingAsk(replyTo, sendTo); } return { content: [{ type: "text", text: `Message sent to ${to}` }], details: deliveryResultDetails(result), }; } catch (error) { return { content: [{ type: "text", text: `Failed to send: ${getErrorMessage(error)}` }], details: { error: true }, }; } } case "ask": { if (!to && replyTo) { return { content: [{ type: "text", text: `Missing required 'to' for action 'ask'. 'replyTo' is a message ID, not a recipient. Retry with { action: "ask", to: "${replyTo}", message: "..." } if that value is the intended session target.` }], details: { error: true, missing: ["to"], invalidRecipientField: "replyTo" }, }; } if (!to || !message) { return { content: [{ type: "text", text: "Missing 'to' or 'message' parameter" }], details: { error: true }, }; } if (replyWaiter) { return { content: [{ type: "text", text: "Already waiting for a reply" }], details: { error: true }, }; } if (_signal?.aborted) { return { content: [{ type: "text", text: "Cancelled" }], 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" }], details: { error: true }, }; } if (sendTo === connectedClient.sessionId) { return { content: [{ type: "text", text: "Cannot message the current session" }], details: { error: true }, }; } if (replyWaiter) { return { content: [{ type: "text", text: "Already waiting for a reply" }], details: { error: true }, }; } const questionId = randomUUID(); replyPromise = waitForReply(sendTo, questionId, _signal, () => { void connectedClient.cancelAsk(questionId); }); replyPromise.catch(() => undefined); 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}` }], details: { ...deliveryResultDetails(sendResult), 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}` }], details: {}, }; } catch (error) { if (error instanceof AskWaitElapsedError) { return deferredAskResult(to, error, await connectedClient.deferAsk(error.replyTo)); } 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)}` }], details: { error: true }, }; } } case "reply": { if (!message) { return { content: [{ type: "text", text: "Missing 'message' parameter" }], details: { error: true }, }; } try { const target = replyTracker.resolveReplyTarget({ to, replyTo }); if (target.from.id === connectedClient.sessionId) { return { content: [{ type: "text", text: "Cannot message the current session" }], details: { error: true }, }; } const threadedReplyTo = target.message.expectsReply ? target.message.id : undefined; const result = await connectedClient.send(target.from.id, { text: message, ...(threadedReplyTo ? { replyTo: threadedReplyTo } : {}), }); if (!result.delivered) { const errorText = result.reason ?? "Session may not exist or has disconnected."; if (threadedReplyTo && result.code === "INVALID_REPLY_TARGET") { replyTracker.dismissPendingAsk(target.message.id, target.from.id); inboundInbox?.dismissPendingAsk(target.message.id, target.from.id); } return { content: [{ type: "text", text: `Reply to "${target.from.name || target.from.id}" was not delivered: ${errorText}` }], details: deliveryResultDetails(result, threadedReplyTo ? { replyTo: threadedReplyTo } : {}), }; } if (threadedReplyTo) { replyTracker.markReplied(threadedReplyTo, target.from.id); inboundInbox?.dismissPendingAsk(threadedReplyTo, target.from.id); } pi.appendEntry("intercom_sent", { to: target.from.name || target.from.id, message: { text: message, ...(threadedReplyTo ? { replyTo: threadedReplyTo } : {}) }, messageId: result.id, timestamp: Date.now(), }); return { content: [{ type: "text", text: `Reply sent to ${target.from.name || target.from.id}` }], details: deliveryResultDetails(result, threadedReplyTo ? { replyTo: threadedReplyTo } : {}), }; } catch (error) { return { content: [{ type: "text", text: `Failed to reply: ${getErrorMessage(error)}` }], details: { error: true }, }; } } case "pending": { inboundInbox?.prunePendingAsks(getAskTimeoutMs()); const pendingAsks = replyTracker.listPending(); if (pendingAsks.length === 0) { return { content: [{ type: "text", text: "No unresolved inbound asks." }], details: {}, }; } const now = Date.now(); const lines = pendingAsks.map(({ from, message, receivedAt, deferredAt }) => { const preview = message.content.text.replace(/\s+/g, " ").slice(0, 80); const elapsedSeconds = Math.max(0, Math.floor((now - receivedAt) / 1000)); const state = deferredAt ? " ยท async" : ""; return `- ${from.name || from.id} ยท ${message.id} ยท ${elapsedSeconds}s ago${state} ยท ${preview}`; }); return { content: [{ type: "text", text: `**Pending asks:**\n${lines.join("\n")}` }], details: {}, }; } 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}\nQueued inbound messages: ${inboundInbox?.size ?? 0}\nQueued outbound messages: ${connectedClient.outboxSize}\nPending inbound asks: ${replyTracker.listPending().length}`, }], details: {}, }; } catch (error) { return { content: [{ type: "text", text: `Failed to get status: ${getErrorMessage(error)}` }], details: { error: true }, }; } } default: return { content: [{ type: "text", text: `Unknown action: ${action}` }], 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); }, } as any; const attachmentParameters = Type.Optional(Type.Array(Type.Object({ type: StringEnum(["file", "snippet", "context"] as const), name: Type.String(), content: Type.String(), language: Type.Optional(Type.String()), }))); type IntercomAction = "list" | "send" | "ask" | "reply" | "pending" | "status"; const executeSplitAction = (action: IntercomAction) => async (toolCallId: string, params: Record, signal: AbortSignal | undefined, onUpdate: unknown, ctx: ExtensionContext) => legacyIntercomTool.execute(toolCallId, { ...params, action }, signal, onUpdate, ctx); const renderSplitCall = (action: IntercomAction) => (args: Record, theme: unknown, context: unknown) => legacyIntercomTool.renderCall({ ...args, action }, theme, context); const renderSplitResult = legacyIntercomTool.renderResult; pi.registerTool({ name: "intercom_send", label: "Intercom Send", description: "Send a message to another local Pi session. Both the recipient and message are required.", promptSnippet: "Send a fire-and-forget message to another local Pi session.", promptGuidelines: ["Use intercom_send for progress updates, notifications, and other messages that do not require a conversational reply."], parameters: Type.Object({ to: Type.String({ description: "Required recipient session name or stable session ID" }), message: Type.String({ description: "Required message to send" }), attachments: attachmentParameters, }), execute: executeSplitAction("send"), renderCall: renderSplitCall("send"), renderResult: renderSplitResult, } as any); pi.registerTool({ name: "intercom_ask", label: "Intercom Ask", description: "Ask another local Pi session a question, waiting briefly before continuing asynchronously.", promptSnippet: "Ask another local Pi session a question when the next step depends on its answer.", promptGuidelines: ["Use intercom_ask only when work genuinely depends on another session's answer; use intercom_send for notifications."], parameters: Type.Object({ to: Type.String({ description: "Required recipient session name or stable session ID" }), message: Type.String({ description: "Required question to ask" }), attachments: attachmentParameters, }), execute: executeSplitAction("ask"), renderCall: renderSplitCall("ask"), renderResult: renderSplitResult, } as any); pi.registerTool({ name: "intercom_reply", label: "Intercom Reply", description: "Reply to an inbound intercom message or ask. Exact protocol threading is resolved internally.", promptSnippet: "Reply to the active or pending inbound intercom message.", promptGuidelines: ["Use intercom_reply to answer an inbound intercom message. Its optional `to` field selects a sender when multiple asks are pending; it is never a message or thread ID."], parameters: Type.Object({ message: Type.String({ description: "Required reply text" }), to: Type.Optional(Type.String({ description: "Optional sender/session selector only when multiple pending asks need disambiguation; never a message or thread ID" })), }), execute: executeSplitAction("reply"), renderCall: renderSplitCall("reply"), renderResult: renderSplitResult, } as any); pi.registerTool({ name: "intercom_team", label: "Intercom Team", description: "Show your current manager and the live coworkers owned by that manager. No arguments are required.", promptSnippet: "Find your manager and managed coworkers without searching the global peer list.", promptGuidelines: ["Use intercom_team whenever you need your manager's target or the other coworkers in your managed group."], parameters: Type.Object({}), async execute(_toolCallId, _params, _signal, _onUpdate, ctx) { try { const connectedClient = await ensureConnected("tool"); syncPresenceIdentity(ctx.sessionManager.getSessionId()); const sessions = await connectedClient.listSessions(); const team = await resolveIntercomTeam({ selfId: connectedClient.sessionId, sessions }); return { content: [{ type: "text", text: formatIntercomTeam(team) }], details: team }; } catch (error) { return { content: [{ type: "text", text: `Failed to resolve intercom team: ${getErrorMessage(error)}` }], details: { error: true } }; } }, renderCall(_args, theme) { return new Text(theme.fg("toolTitle", theme.bold("intercom_team")), 0, 0); }, renderResult(result, { isPartial }, theme, context) { if (isPartial) return new Text(theme.fg("warning", "Resolving team..."), 0, 0); const details = result.details as { error?: boolean } | undefined; const failed = Boolean(context.isError || details?.error === true); return new Text(`${failed ? theme.fg("error", "โœ— ") : theme.fg("success", "โœ“ ")}${theme.fg(failed ? "error" : "text", firstTextContent(result))}`, 0, 0); }, } as any); for (const definition of [ { name: "intercom_list", label: "Intercom List", action: "list", description: "List active local intercom sessions.", promptSnippet: "List active local intercom sessions." }, { name: "intercom_pending", label: "Intercom Pending", action: "pending", description: "List unresolved inbound intercom asks.", promptSnippet: "List unresolved inbound intercom asks." }, { name: "intercom_status", label: "Intercom Status", action: "status", description: "Show this session's intercom connection status.", promptSnippet: "Show intercom connection status." }, ] as const) { pi.registerTool({ name: definition.name, label: definition.label, description: definition.description, promptSnippet: definition.promptSnippet, parameters: Type.Object({}), execute: executeSplitAction(definition.action), renderCall: renderSplitCall(definition.action), renderResult: renderSplitResult, } as any); } if (config.legacyTool) { pi.registerTool(legacyIntercomTool); } async function resolveCurrentContact(ctx: ExtensionContext, generation = runtimeGeneration): Promise<{ target: string; name?: string; id: string; duplicateName: boolean } | undefined> { let contactClient: IntercomClient; try { contactClient = await ensureConnected("overlay"); } catch (error) { notifyIfLive(ctx, `Intercom unavailable: ${getErrorMessage(error)}`, "error", generation); return undefined; } if (!getLiveContext(ctx, generation)) return undefined; syncPresenceIdentity(ctx.sessionManager.getSessionId()); try { const sessions = await contactClient.listSessions(); const currentSession = sessions.find(s => s.id === contactClient.sessionId); if (!currentSession) { return { target: contactClient.sessionId, id: contactClient.sessionId, duplicateName: false }; } return chooseContactTarget(currentSession, sessions); } catch (error) { notifyIfLive(ctx, `Failed to read intercom id: ${getErrorMessage(error)}`, "error", generation); return undefined; } } function insertIntoEditor(ctx: ExtensionContext, text: string): void { const existing = ctx.ui.getEditorText?.() ?? ""; const next = existing.trim() ? `${existing.trimEnd()}\n\n${text}` : text; ctx.ui.setEditorText(next); } async function showIntercomId(ctx: ExtensionContext, mode: "copy" | "insert" = "copy"): Promise { const generation = runtimeGeneration; const liveContext = getLiveContext(ctx, generation); if (!liveContext) return; const contact = await resolveCurrentContact(liveContext, generation); if (!contact || !getLiveContext(liveContext, generation)) return; const instruction = formatContactInstruction(contact); if (mode === "insert") { if (!liveContext.hasUI) { notifyIfLive(liveContext, `Intercom target: ${contact.target}`, "info", generation); return; } insertIntoEditor(liveContext, instruction); notifyIfLive(liveContext, `Inserted intercom contact target for another agent: ${contact.target}`, "info", generation); return; } const copied = copyTextToClipboard(instruction); if (copied.ok) { notifyIfLive(liveContext, `Copied intercom contact target for another agent: ${contact.target}`, "info", generation); return; } if (liveContext.hasUI) { insertIntoEditor(liveContext, instruction); notifyIfLive(liveContext, `Clipboard unavailable; inserted intercom contact target for another agent: ${contact.target}`, "warning", generation); return; } notifyIfLive(liveContext, `Intercom target: ${contact.target}`, "info", generation); } async function openIntercomOverlay(ctx: ExtensionContext): Promise { const overlayGeneration = runtimeGeneration; const liveContext = getLiveContext(ctx, overlayGeneration); if (!liveContext?.hasUI || (liveContext as ExtensionContext & { mode?: string }).mode !== "tui") 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, overlayOptions: { width: 88 } } ).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 idPrefixes = shortestUniqueIdPrefixes([currentSession.id, ...sessions.map((session) => session.id)], 8); const targetLabel = formatSessionLabel(selectedSession, duplicates, idPrefixes.get(selectedSession.id) ?? selectedSession.id); const result = await ctx.ui.custom( (tui, theme, keybindings, done) => new ComposeOverlay(tui, theme, keybindings, selectedSession, targetLabel, overlayClient, done), { overlay: true, overlayOptions: { width: 72 } } ).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.registerCommand("intercom-id", { description: "Copy this session's intercom contact target for another agent. Use /intercom-id insert to put the handoff snippet in the editor.", handler: async (args, ctx) => { const mode = args.trim().toLowerCase(); await showIntercomId(ctx, mode === "insert" || mode === "editor" ? "insert" : "copy"); }, }); pi.registerShortcut("alt+m", { description: "Open session intercom", handler: async (ctx) => openIntercomOverlay(ctx), }); pi.registerShortcut("alt+i", { description: "Copy this session's intercom contact target for another agent, falling back to editor insert", handler: async (ctx) => showIntercomId(ctx, "copy"), }); }