Pidash TUI Parity
Pidash is not a viewer — it is a peer surface. Every prompt the agent asks and every operation the TUI offers is answerable from either surface, and state converges: anything done in the TUI reflects in the browser and vice versa. This page documents the parity contract and the seamless-switch walkthrough (issue #968).
The parity contract
| Surface | TUI | Browser | Convergence |
|---|---|---|---|
| ask_user questions | overlay dialog | inline action card | first answer wins; other surface sees the prompt closed |
| Dangerous-command approvals | ui.select prompt |
same prompt rendered inline | first answer wins; enforcement semantics identical either way |
| Commit-trailer selection | ui.select prompt |
same prompt rendered inline | first answer wins |
| Project-agent confirmation | ui.confirm prompt |
same prompt rendered inline | first answer wins |
/tasks menus |
bridged select menus | Tasks panel (dedicated) | both operate the same live TaskStore |
| Coms peers | pool widget | Coms panel | same registry, same deliverPrompt path |
| New session / compact | /new, /compact |
sidebar +, InfoBar compact |
session lifecycle events broadcast to all browsers |
| Memory status | situation report (implicit) | Memory panel | read from the same scoring/topic modules |
| MCP servers | /mcp |
MCP panel | both drive pi mcp |
| Settings | /pi-config-settings |
Settings page | both write through the live settings module |
Two invariants hold everywhere:
-
Operations go through the live session's extension state. The browser sends a
pidash-command; the owning extension applies it via its own module (TaskStore, comsdeliverPrompt, project-settings writer); an event broadcasts the new state to every connected browser. The daemon never writes a store directly. -
First answer wins. Prompt bridging (
extensions/shared/ui-bridge.ts) renders the prompt in the TUI and the browser simultaneously. Whichever surface answers first resolves the prompt; the other sees it closed (ui-dismissfor the browser,done()for the TUI dialog). Enforcement semantics are never bypassed — the browser is just another transport for the same decision.
Seamless-switch walkthrough
A scripted session driven alternately from TUI and browser with zero dead ends. Run it yourself to verify parity after changes.
-
Start —
piin a project; openhttp://localhost:19190in two browser tabs (both watch the session). -
Chat from browser — type a prompt in tab A. Streaming appears in the TUI, tab A, and tab B simultaneously.
-
Ask from TUI side — prompt the agent with something that triggers
ask_user(e.g. "ask me a question with options"). The question appears as an action card in both tabs AND as a dialog in the TUI. Answer in tab B. The TUI dialog closes instantly; tab A's card shows "✓ Answered". -
Dangerous approval from browser — ask the agent to run a command that trips the dangerous-command gate. The approval prompt (allow once / always allow project / always allow everywhere / no) renders in both surfaces. Answer "No" from the TUI: both browser cards close. Re-run and answer "Yes — run once" from the browser: the command executes with identical enforcement semantics.
-
Tasks from browser — open the Tasks panel (InfoBar
☐icon). Create "Demo task". The TUI task widget updates live. Start it from the browser; the TUI spinner picks it up. Complete it from the TUI/tasksmenu — the panel's counter updates without refresh. Both tabs converge. -
Coms from browser — with a peer session running, open the Coms panel: the peer appears with live busy/queue/context state. Send a message from the browser; the peer receives it through the same path as
coms_send, and the reply surfaces in chat. -
Session lifecycle from browser — click
+in the sidebar: name the session, pick a model, create. The session switches everywhere (TUI shows the new session; browsers replay it). Triggercompactfrom the InfoBar — compaction runs and context usage drops in every surface. -
Memory + MCP + Settings — Memory panel shows the situation report, topics, and budget usage. MCP panel lists servers (
pi mcp list) with login/logout. The Settings page shows every key fromsettings-keys.jsonwith project/global scope tabs; toggle a boolean — it writes through the live settings module (atomic write, comments-lost semantics identical to/pi-config-settings) and both tabs refresh the stored value. -
Switch mid-flight — while the agent streams, switch sessions from the TUI (
/resume) — the browser follows. Switch back from the browser session menu — the TUI follows. No dead ends in either direction.
Architecture notes
-
Prompt bridging —
extensions/shared/ui-bridge.ts(createBridgedUi):select/confirm/inputbuilt onctx.ui.customso a browser answer can close the TUI dialog via thedonecallback. Degrades to plain prompts when the event bus orui.customis unavailable. -
Event flow — browser → daemon (
pidash-command) → pi session (handleWsMessage) → owning extension (pi.events) → result event → daemon → every browser watching the session. -
Settings writes —
extensions/pidash/browser-ops.tsreads and writes throughgetSettingsPath/parseSettingsFile/writeSettingsFileand clears the settings cache;settings-keys.jsonis the single source of truth for the key list, types, and defaults. -
Tests —
tests/node/shared/ui-bridge.test.ts(race semantics),tests/node/pitasks/pidash-bridge.test.ts(task ops + live broadcast),tests/node/pidash/browser-ops.test.ts(settings round-trip),tests/node/pidash/parity-projections.test.ts(coms/settings pure projections).