{"version":3,"file":"chat-message.mjs","names":[],"sources":["../../src/compaction/chat-message.ts"],"sourcesContent":["/**\n * Live-streaming custom message in Pi's chat flow during compaction.\n *\n * Pattern (all documented Pi APIs — see docs/tui-streaming-patterns.md):\n * 1. Capture TUI ref from widget factory\n * 2. Register custom message renderer (toolPendingBg → customMessageBg)\n * 3. Filter from LLM context via pi.on('context')\n * 4. Send volatile message (renders in chat, never persisted)\n * 5. Dual mutation (msgObj.content + component refs) + TUI.requestRender()\n */\n\nimport {\n\ttype ExtensionAPI,\n\tgetMarkdownTheme,\n\ttype ThemeColor,\n} from '@earendil-works/pi-coding-agent';\nimport type { TUI } from '@earendil-works/pi-tui';\nimport { Box, Markdown, Spacer, Text } from '@earendil-works/pi-tui';\nimport {\n\tinstallVolatileCompactionMessagePatch,\n\tLIVE_COMPACTION_STREAM_CUSTOM_TYPE,\n} from '@live-compaction/compaction/volatile-message';\nimport type { HookContext, SummaryProgress } from '@live-compaction/types';\nimport { safeUI } from '@live-compaction/types';\n\nconst CUSTOM_TYPE = LIVE_COMPACTION_STREAM_CUSTOM_TYPE;\nconst THROTTLE_MS = 150;\n\n/**\n * Shared mutable state between the registered renderer and the progress\n * factory returned by {@link registerCompactionChatMessage}. Both closures\n * close over the same object so dual mutation is automatic.\n */\ninterface ChatState {\n\tphase: 'idle' | 'streaming' | 'done';\n\tmsgObj: { content: unknown } | null;\n\tmdRef: InstanceType<typeof Markdown> | null;\n\theaderRef: InstanceType<typeof Text> | null;\n\ttuiRef: TUI | null;\n\tthemeRef: { fg: (key: ThemeColor, text: string) => string } | null;\n}\n\nfunction buildHeaderText(\n\tphase: ChatState['phase'],\n\tlineCount: number,\n\ttheme: { fg: (key: ThemeColor, text: string) => string },\n): string {\n\tconst label = phase === 'done' ? 'done' : 'streaming';\n\treturn (\n\t\ttheme.fg('accent', `\\x1b[1m[compaction · ${label}]\\x1b[22m`) +\n\t\t' ' +\n\t\ttheme.fg('muted', `${lineCount} lines`)\n\t);\n}\n\n/**\n * One-time setup: register renderer, context filter, and compaction cleanup.\n * Call once during extension factory. Pi clears renderer/handler registrations\n * on reload, so this is idempotent across reloads.\n *\n * Returns a factory that creates a chat-flow {@link SummaryProgress} for each\n * compaction run — sharing renderer state via closure.\n */\nexport async function registerCompactionChatMessage(\n\tpi: ExtensionAPI,\n): Promise<(ctx: HookContext) => SummaryProgress | undefined> {\n\tawait installVolatileCompactionMessagePatch();\n\n\tconst state: ChatState = {\n\t\tphase: 'idle',\n\t\tmsgObj: null,\n\t\tmdRef: null,\n\t\theaderRef: null,\n\t\ttuiRef: null,\n\t\tthemeRef: null,\n\t};\n\n\t// ---- Renderer (function form — required, object form silently breaks) ----\n\tpi.registerMessageRenderer(CUSTOM_TYPE, (message, options, theme) => {\n\t\tstate.msgObj = message;\n\t\tstate.themeRef = theme;\n\t\tconst bgKey = state.phase === 'done' ? 'customMessageBg' : 'toolPendingBg';\n\t\tconst bgFn = (t: string) => theme.bg(bgKey, t);\n\t\tconst box = new Box(1, 1, bgFn);\n\n\t\tconst content = String(message.content);\n\t\tconst lines = content.split('\\n').length;\n\t\tconst header = new Text(buildHeaderText(state.phase, lines, theme), 0, 0);\n\t\tbox.addChild(header);\n\t\tstate.headerRef = header;\n\n\t\tif (options.expanded) {\n\t\t\tbox.addChild(new Spacer(1));\n\t\t\tconst md = new Markdown(content, 0, 0, getMarkdownTheme(), {\n\t\t\t\tcolor: (t: string) => theme.fg('customMessageText', t),\n\t\t\t});\n\t\t\tbox.addChild(md);\n\t\t\tstate.mdRef = md;\n\t\t} else {\n\t\t\tstate.mdRef = null;\n\t\t}\n\n\t\treturn box;\n\t});\n\n\t// ---- Filter from LLM context — zero cost to model ----\n\tpi.on('context', (event) => ({\n\t\tmessages: event.messages.filter(\n\t\t\t(m) => !(m.role === 'custom' && (m as { customType?: string }).customType === CUSTOM_TYPE),\n\t\t),\n\t}));\n\n\t// ---- Reset local renderer state before Pi rebuilds the compacted chat ----\n\tpi.on('session_compact', () => {\n\t\tstate.phase = 'idle';\n\t\tstate.msgObj = null;\n\t\tstate.mdRef = null;\n\t\tstate.headerRef = null;\n\t\tstate.themeRef = null;\n\t});\n\n\t// ---- Progress factory — shares `state` with renderer above ----\n\treturn function makeChatSummaryProgress(ctx: HookContext): SummaryProgress | undefined {\n\t\tif (!ctx.hasUI) return undefined;\n\n\t\tconst ui = safeUI(ctx);\n\t\tlet lastUpdate = 0;\n\t\tlet started = false;\n\n\t\t// Capture TUI ref (documented: tui.md Pattern 6)\n\t\tconst captureTui = () => {\n\t\t\tui.setWidget?.(\n\t\t\t\t'_tui-cap',\n\t\t\t\t(tui) => {\n\t\t\t\t\tstate.tuiRef = tui as TUI;\n\t\t\t\t\treturn new Spacer(0);\n\t\t\t\t},\n\t\t\t\t{ placement: 'belowEditor' },\n\t\t\t);\n\t\t\tsetTimeout(() => ui.setWidget?.('_tui-cap', undefined), 100);\n\t\t};\n\n\t\treturn {\n\t\t\tstart(modelLabel: string, _tokensBefore: number) {\n\t\t\t\tstarted = true;\n\t\t\t\tstate.phase = 'streaming';\n\t\t\t\tcaptureTui();\n\t\t\t\tui.setWorkingMessage?.(`Compacting with ${modelLabel}…`);\n\n\t\t\t\t// Render initial message in chat\n\t\t\t\tpi.sendMessage({\n\t\t\t\t\tcustomType: CUSTOM_TYPE,\n\t\t\t\t\tcontent: '_Waiting for model output…_',\n\t\t\t\t\tdisplay: true,\n\t\t\t\t});\n\t\t\t},\n\n\t\t\tupdate(text: string) {\n\t\t\t\tif (!started) return;\n\t\t\t\tconst now = Date.now();\n\t\t\t\tif (now - lastUpdate < THROTTLE_MS) return;\n\t\t\t\tlastUpdate = now;\n\n\t\t\t\tconst lineCount = text ? text.split('\\n').length : 0;\n\t\t\t\tui.setWorkingMessage?.(`Compacting · ${lineCount} lines`);\n\n\t\t\t\t// Dual mutation — model object + immediate visual\n\t\t\t\tif (state.msgObj) {\n\t\t\t\t\tstate.msgObj.content = text;\n\t\t\t\t}\n\t\t\t\tstate.mdRef?.setText(text);\n\t\t\t\tif (state.headerRef && state.themeRef) {\n\t\t\t\t\tstate.headerRef.setText(buildHeaderText(state.phase, lineCount, state.themeRef));\n\t\t\t\t}\n\t\t\t\tstate.tuiRef?.requestRender();\n\t\t\t},\n\n\t\t\tfinish() {\n\t\t\t\tstarted = false;\n\t\t\t\tstate.phase = 'done';\n\t\t\t\tui.setWorkingMessage?.();\n\t\t\t\t// Trigger rebuild to apply customMessageBg\n\t\t\t\tstate.tuiRef?.requestRender();\n\t\t\t},\n\n\t\t\tfail(message: string) {\n\t\t\t\tstarted = false;\n\t\t\t\tstate.phase = 'idle';\n\t\t\t\tui.setWorkingMessage?.();\n\t\t\t\tui.setWidget?.('live-compaction-error', [`Compaction failed: ${message}`], {\n\t\t\t\t\tplacement: 'aboveEditor',\n\t\t\t\t});\n\t\t\t},\n\t\t};\n\t};\n}\n"],"mappings":"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"}