Repository navigation
Description
Description
When a pi extension calls ctx.ui.setTitle(undefined) or ctx.ui.setTitle(null), the whole page is replaced by "The app panel hit an error", not only the chat panel. Only a page reload brings the app back.
pi types the argument as string, but pi loads extensions without a type check: my .ts test extension passes undefined and loads without errors. One wrong call from any extension breaks the whole UI.
How it happens (line numbers are from main at 52ae5478):
setTitlepublishes the value as it is (packages/server/src/agent/webUiContext.ts:214-216). The host sends the frame withJSON.stringify(packages/server/src/host/server.ts:1160), so forundefinedthe frame has notitlefield at all, and fornullit has"title": null. The contract saystitle: string(packages/contracts/src/piProtocol.ts:309).- The web takes the frame without checks (
apps/web/src/transport/wireTransport.ts:393), andapplyExtUipassesrequest.titlestraight torenameChat(apps/web/src/store/appStore.ts:4724-4725). The chat tab getsname: undefined. Thesession_info_changedpath uses the samerenameChat, but it normalizes the name first:event.name?.trim() ?? ""(appStore.ts:4593). The store spec says this shared rename should "normalize an absent name to the empty untitled name" (apps/web/src/store/SPEC.md:253-255). But the normalization is in thesession_info_changedcaller, not inrenameChat, so thesetTitlepath skips it. selectUnnamedChatFirstPromptscallstab.name.trim()(apps/web/src/store/selectors.ts:1276). It runs inWorkspaceWorkbench(apps/web/src/shell/WorkspaceWorkbench.tsx:612). No closer error boundary catches theTypeError, so the app-level one (apps/web/src/main.tsx:51) does and replaces the whole page.
An idea for the fix: the host could always send a string ("" for a missing title), so the frame matches the contract. And applyExtUi could normalize the title the same way as session_info_changed.
Found while working on #1353. That PR keeps this behavior: its stripAnsi returns non-strings unchanged, and its test expects a setTitle frame with title: undefined.
Steps to reproduce
-
Add a global extension, for example
~/.pi/agent/extensions/title-undefined.ts:export default function (pi) { pi.registerCommand("title-undefined", { description: "calls ctx.ui.setTitle(undefined)", handler: async (_args, ctx) => { ctx.ui.setTitle(undefined); }, }); }
-
Start ThinkRail with
bun run dev, open a project and start a chat. No provider is needed: an extension command doesn't call the model. -
Send
/title-undefinedin the chat.
Expected behavior
The app keeps working. The chat tab falls back to its untitled name, the same as when pi reports a session without a name.
Actual behavior
The whole page shows "The app panel hit an error" with Cannot read properties of undefined (reading 'trim'). "Try again" shows the same error. A page reload brings the app back.
The console has the TypeError from selectUnnamedChatFirstPrompts, called from WorkspaceWorkbench, and [ErrorBoundary: app] reports it.
ctx.ui.setTitle(null) does the same, with Cannot read properties of null (reading 'trim'). ctx.ui.setTitle("Title from extension") renames the tab as expected.
Environment
- OS: macOS 27.0.1
- Bun version (
bun --version): 1.4.4 - Node version (
node --version): v24.10.0 - ThinkRail version / commit: main
52ae5478, pi 1.5.2, web UI frombun run devin a Chromium-based browser
Activity