diff --git a/app/src/client/pages/ChatPage.tsx b/app/src/client/pages/ChatPage.tsx index edf9a37..b4dcc40 100644 --- a/app/src/client/pages/ChatPage.tsx +++ b/app/src/client/pages/ChatPage.tsx @@ -18,6 +18,25 @@ const SUGGESTIONS = [ "Does anything in my schedule conflict?", ]; +/** + * A stream interruption + recovery can split one utterance across several + * consecutive text parts (often mid-sentence). Rendering each part as its + * own markdown block tears sentences and lists apart, so stitch runs of + * text parts back together before rendering. + */ +function coalesceParts(parts: UIMessage["parts"]): UIMessage["parts"] { + const out: UIMessage["parts"] = []; + for (const part of parts) { + const last = out[out.length - 1]; + if (part.type === "text" && last?.type === "text") { + out[out.length - 1] = { ...last, text: last.text + part.text }; + } else { + out.push(part); + } + } + return out; +} + function messageText(m: UIMessage): string { return m.parts .map((p) => (p.type === "text" ? p.text : "")) @@ -328,7 +347,7 @@ function Turn({ return (
- {message.parts.map((part, i) => { + {coalesceParts(message.parts).map((part, i) => { if (part.type === "text") { return part.text.trim() ? : null; } diff --git a/app/src/server/pi.ts b/app/src/server/pi.ts index 9d2e8dd..ccd2388 100644 --- a/app/src/server/pi.ts +++ b/app/src/server/pi.ts @@ -32,6 +32,12 @@ export class Pi extends Think { workspaceBash = false; waitForMcpConnections = { timeout: 15_000 }; maxSteps = 12; + /** + * Abort-and-recover a model stream that parks without erroring — the + * cause of "spinning forever" turns. Set well above the slowest + * time-to-first-token plus the slowest MCP tool call. + */ + chatStreamStallTimeoutMs = 120_000; getDefaultTimezone() { return "America/New_York";