Skip to content

ctx.ui.setTitle(undefined) from an extension crashes the whole page #1356

Closed

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):

  1. setTitle publishes the value as it is (packages/server/src/agent/webUiContext.ts:214-216). The host sends the frame with JSON.stringify (packages/server/src/host/server.ts:1160), so for undefined the frame has no title field at all, and for null it has "title": null. The contract says title: string (packages/contracts/src/piProtocol.ts:309).
  2. The web takes the frame without checks (apps/web/src/transport/wireTransport.ts:393), and applyExtUi passes request.title straight to renameChat (apps/web/src/store/appStore.ts:4724-4725). The chat tab gets name: undefined. The session_info_changed path uses the same renameChat, 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 the session_info_changed caller, not in renameChat, so the setTitle path skips it.
  3. selectUnnamedChatFirstPrompts calls tab.name.trim() (apps/web/src/store/selectors.ts:1276). It runs in WorkspaceWorkbench (apps/web/src/shell/WorkspaceWorkbench.tsx:612). No closer error boundary catches the TypeError, 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

  1. 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);
    		},
    	});
    }
  2. 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.

  3. Send /title-undefined in 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 from bun run dev in a Chromium-based browser
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions