A professional workspace for AI coding agents with: - Real-time streaming chat with SSE - Session management (fork, clone, tree navigation) - MCP tools browser & metadata panel - Skills browser (global, project, package sources) - Voice input & STT provider configuration - Integrated terminal & file explorer - Model selector with capability metadata - Keyboard-first UI with command palette - 8 languages (en, pt-BR, es, ja, zh-CN, ar, bn, hi) - Tauri 2 desktop app - React 19 + TypeScript + Vite 8 + Tailwind CSS 4 - Pi Agent extension runtime Originally forked from OpenCodeUI, now an independent project. License: GPL-3.0-only
6.7 KiB
PiAgentUI Active Session Switch Design
Goal
When the user opens an existing session in PiAgentUI and sends a message, the message must be delivered to that selected Pi session, not to whichever session was active when the Pi TUI launched.
Current finding
- The frontend route/store correctly loads selected sessions and their messages.
src/hooks/useChatSession.tssends the selectedsessionIdtosendMessageAsync().extensions/piagentui-server.tsreceivessessionId, but still callspi.sendUserMessage(parsed.text).pi.sendUserMessage()always targets the current active Pi runtime session.- Pi exposes
switchSession()only onExtensionCommandContext, not on normal eventExtensionContextused bysession_startand the HTTP server. AgentSessionRuntime.switchSession()intentionally emitssession_shutdown, disposes the old session, creates the replacement session, rebinds extensions, then runswithSession(replacedCtx).- The existing PiAgentUI extension currently stops the web server on every
session_shutdown, so a normal Pi session switch can close the HTTP/SSE server and force reconnects.
Selected approach: activate and send on the backend
The backend should become authoritative for message routing:
- Resolve requested
sessionIdto the JSONL session file in the current session directory. - If the requested session differs from the active Pi session, switch the real Pi runtime to that session.
- Send the user message inside the replacement session callback so the message cannot land in the old session.
- Keep the existing frontend model: route/session selection still works as today;
sendMessageAsync()keeps passingsessionId.
This preserves the current UI while making the server guarantee correctness.
Required changes
Pi core
Expose a safe session-switch capability to extension event contexts, or a narrowly-scoped equivalent that can be used by long-lived extension services.
Preferred API shape:
interface ExtensionContext {
switchSession(
sessionPath: string,
options?: { withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
): Promise<{ cancelled: boolean }>
}
Implementation should reuse the existing switchSessionHandler already used by createCommandContext(). The important behavior is not a new session implementation; it is making the already-existing runtime switch available to the PiAgentUI HTTP server context.
PiAgentUI extension server
Add helpers in extensions/piagentui-server.ts:
getSessionFileForId(sessionId)scanslatestCtx.sessionManager.getSessionDir()for*_${sessionId}.jsonl.sendUserMessageToSession(parsed)validatessessionId, resolves the session file, switches if needed, and sends in the correct context.session_shutdownhandling must not tear down the HTTP server onreason === 'resume'if the server is intentionally switching sessions for the web UI. The server should keep the port stable and updatelatestCtxon the followingsession_start.
Target send flow:
if (requestedSessionId && requestedSessionId !== activeSessionId) {
const sessionFile = getSessionFileForId(requestedSessionId)
const result = await latestCtx.switchSession(sessionFile, {
withSession: async replacedCtx => {
await maybeSetModel(replacedCtx, parsed.model)
await replacedCtx.sendUserMessage(parsed.text)
},
})
if (result.cancelled) return HTTP 409
return HTTP 202
}
await maybeSetModel(latestCtx, parsed.model)
pi.sendUserMessage(parsed.text)
If sessionId is missing or already active, keep the existing path.
Frontend
Keep frontend mostly unchanged.
Small additions only if needed:
sendMessageAsync()continues sendingsessionId.- Optional: handle HTTP
409/404with a clear toast if target session cannot be activated. - Optional: add
activeSessionIdfromserver.connected/session.statuslater, but not required for the core fix.
Alternatives considered
Activate on session selection
Frontend would call /api/sessions/:id/activate when a user clicks a session. This matches the user's mental model, but it introduces a race: if the user sends before activation completes, the message can still hit the old session unless send also validates activation. It is useful as a UX enhancement after backend routing is correct, not as the primary correctness mechanism.
Frontend guard only
Frontend could block sending if selected session differs from active Pi session. This avoids Pi core changes but makes UX worse and still requires a trustworthy active-session signal from the backend. It detects the problem instead of solving it.
Tests
Extension/unit tests
Add or update extension server tests to cover:
- Sending with active
sessionIdcallspi.sendUserMessage()without switching. - Sending with different
sessionIdresolves the matching JSONL file and callsctx.switchSession(sessionFile, { withSession }). - The
withSessioncallback callsreplacedCtx.sendUserMessage(text). - Unknown
sessionIdreturns 404 and does not send to the active session. - Cancelled switch returns 409 and does not send.
session_shutdownwithreason: 'resume'does not close the web server during web-initiated session switching.
Frontend tests
Add or update API/hook tests to cover:
sendMessageAsync()includessessionIdin the payload.- Send error responses surface through the existing error handling path.
- Existing route/session selection behavior remains unchanged.
Manual verification
- Start Pi in session A.
- Open PiAgentUI.
- Select existing session B in the sidebar.
- Send a message.
- Verify new user and assistant messages appear in session B.
- Verify session A JSONL is unchanged.
- Verify
/global/eventremains connected and streaming works without F5.
Risks and mitigations
- Switching sessions invalidates old extension contexts. Mitigation: send inside
withSession(replacedCtx)and avoid captured stalepi/ctx after switch. - Current extension closes the HTTP server on
session_shutdown. Mitigation: preserve server acrossresumeswitches and updatelatestCtxonsession_start. - Pi core API change can be too broad. Mitigation: expose only the existing switch handler; do not create parallel session logic.
- Session file lookup by suffix must be strict. Mitigation: only match
_${sessionId}.jsonlinside the current session dir.
Acceptance criteria
- Sending from selected session B never writes to active-at-launch session A.
- Frontend routing and message loading stay visually the same.
- The web server port remains stable during a session switch initiated by PiAgentUI.
- Streaming still updates live after switch.
- Typecheck, extension typecheck, relevant tests, and build pass.