/** * liveProgressUi.ts — host-side relay of TurnProgress snapshots to VS Code UI * (#60, spec coauthoring-live-progress.md §3.4). The ONLY surfaces are the * existing withProgress notification (an activity line) and a dedicated * OutputChannel streaming the full assistant text — no new network/webview * surface (INV-45). vscode-only; all pure logic lives in turnProgress.ts. */ import * as vscode from "vscode"; import { formatProgressLine, type TurnProgressSnapshot } from "./turnProgress"; const CHANNEL_NAME = "Cowriting: Claude"; export interface TurnUi { /** Pass to runEditTurn's opts.onProgress. */ onProgress: (snapshot: TurnProgressSnapshot) => void; /** Pass to runEditTurn's opts.signal — fired when the user cancels the notification. */ signal: AbortSignal; } export class LiveProgressUi { readonly channel: vscode.OutputChannel; constructor() { this.channel = vscode.window.createOutputChannel(CHANNEL_NAME); } /** * Begin one turn's UI. Writes the per-turn header to the channel and returns * the onProgress relay + an AbortSignal linked to the notification's cancel * token. The channel APPENDS (it doubles as a debug log of recent turns, spec * §3.5); it auto-reveals (without stealing focus) on the first streamed text, * gated by `cowriting.liveProgress.revealOutput`. */ begin( instruction: string, progress: vscode.Progress<{ message?: string }>, token: vscode.CancellationToken, ): TurnUi { const controller = new AbortController(); token.onCancellationRequested(() => controller.abort()); this.channel.appendLine(`── asking: ${instruction} ──`); let revealed = false; const reveal = (): void => { if (revealed) return; revealed = true; const cfg = vscode.workspace.getConfiguration("cowriting"); if (cfg.get("liveProgress.revealOutput", true)) this.channel.show(true); }; const onProgress = (s: TurnProgressSnapshot): void => { progress.report({ message: formatProgressLine(s) }); if (s.textDelta) { this.channel.append(s.textDelta); reveal(); } }; return { onProgress, signal: controller.signal }; } dispose(): void { this.channel.dispose(); } }