CLI & TUI Architecture
Source Specs:cli-tui-beautify-plan.md,cli-tui-production-readiness.md,docs/10-tui.md,docs/architecture.md,docs/TUI_TERMINAL_EQUIVALENCE.md
The Picasso CLI (apps/cli) and interactive TUI (packages/tui) run on the shared engine (pinned to 0.2.2-rc.1, kept current by the background engine updater) over a line-delimited JSON-RPC stdio transport managed by packages/core/src/dsh/runtime-session.ts and packages/picasso-sdk-server.
---
1. End-to-End Execution Path
- CLI Entry (`apps/cli/src/index.ts`): Startup runs in a fixed order (H-C14): set the runtime asset root (
<pkg>/runtimein the npm package, else the repo resolver;PICASSO_RUNTIME_ASSETS_DIRoverrides), load the signed-in session from the shared Keychain item (primeLoginSession), readGET /v1/client-configand enforce the minimum CLI version (skipped forupdate,auth,help,--help,--version), run the first-run engine install when the packaged CLI has no pinned engine (ensureFirstRunRuntime), then parse the subcommand (picasso,picasso run/-p,attach,auth,mcp,memory,settings,cache,skills,update, …). Every error goes through one top-level handler that printspicasso: <message>and exits with theCliErrorcode (1 failure, 2 usage, 3 auth/no provider); a stack prints only withPICASSO_DEBUG=1.--permission-mode safe|auto|yolo|paranoidapplies torun/-pas well as the TUI; without it the mode comes from the shared settings file (defaultauto). - Runtime Session (`packages/core/src/dsh/runtime-session.ts`):
- Resolves the pinned engine binary:
PICASSO_DSH_RUNTIME_BIN, else~/.picasso/dsh/bin/dshwhen it is the pin, else the first engine binary onPATHthat is the pin (packages/core/src/dsh/updater.tsresolvePinnedDshBin). It refuses to launch if the version differs fromDSH_PINNED_VERSION(verifyDshRuntimeIdentityin the engine adapter). The overlays come from the runtime asset root (picassoRuntimeAsset;assertRuntimeAssetsfails withPicasso runtime files are missing (<file>)when a required patch is absent). It then launches the engine'ssdkprofile (its built-in base + SDK-app composition) with three static patches —dsh/picasso-sdk.patch.yml,dsh/picasso-ecosystem.cordis.patch.yml,dsh/picasso-ecosystem.community.patch.yml— and one generated runtime patch (@picasso/sdk-serverin place of the stocksdk-jsonrpc-server, lean-context, memory, commands, cache-probe, per-server MCP entries).dsh/picasso-runtime.cordis.ymlis not in this chain: it is read only for the provider-route menu (provider-commands.ts,tui-backend.ts) and by legacy/dev scripts viaPICASSO_DSH_CONFIG. - The engine home
$DSH_HOME(default~/.picasso/dsh-home, Picasso's own and shared with the Mac relay, not with the engine's standalone desktop app; underPICASSO_HOMEwhen set) is also part of the composition: itscordis.patch.ymlandprofiles/sdk/cordis.patch.ymllayers apply underneath the repo patches (the repo's sandbox/approval/permission rows win; anything else, such asagent-default-model, leaks through). - Maps
--permission-modetoDSH_PERMISSION_MODE(paranoidor active plan ->read-only;safe/auto->workspace-write;yolo->danger-full-access). - Communicates over stdio via
DshJsonRpcClient(packages/core/src/dsh/jsonrpc-client.ts).
- SDK Server Replacement (`packages/picasso-sdk-server/index.js`):
- Replaces the engine's stock SDK JSON-RPC server while delegating standard SDK methods (
initialize,session/prompt,session/resume,session/wait,shutdown). - Adds the Picasso methods in
PICASSO_SDK_METHODS(packages/picasso-sdk-server/index.js):picasso/capabilities,session/cancel,session/fork,approval/respond,approval/grants/list|revoke,question/respond,command/run,command/list,workspace/changes/get|diff,workspace/restore,session/queue,session/queue/list|update,session/rewind,session/model/set|get|catalog,session/aside/open|close,session/picasso/record(plugin:picasso/<kind>durable log events),job/list|kill,schedule/list|create|update|delete|catalog|history,team/task/lists|list|create|claim|complete|delete,goal/get|update,sandbox/network/set|get|clear, andattachment/admit-file. Notifications beyond the stock four addsession.deltaandsession.queue; the only server-to-client requests areapproval/requestandquestion/request. - Hooks
approval/requestanduser-questions/requestwaterfalls so interactive TUI prompts write durableapproval/asked+approval/decidedaudit pairs.
- Event Projection (`packages/core/src/dsh/project.ts` & `contract.ts`):
DshEventProjectornormalizes raw engine session events, streaming deltas (session.delta), token-meter usage (cache-usage), andplugin:picasso/<kind>records into strongly typedLoopEventstreams that match the MacThreadEventStoreturn/tool/approval counts 1:1.
- TUI Surface (`packages/tui`):
- Renders the interactive terminal workbench, slash-command palette (
packages/picasso-commands), diff viewer, approval modals,/skills(Indexed vs. Loaded state + token cost),/mcp(Cataloged / Running / Error state + token cost), and/memory(inline-always vs. on-demand entries).
- Goals (one system, engine-native):
/goalin the TUI drives the engine's same-session goal (its goal, goal-round driver and goal command plugins), the same one the Mac app uses./goal [<objective>|edit <objective>|pause|resume|clear]runs the runtime's own command;/goal continue [n](raise the round cap by n, default by the current cap, then resume — the way out of around-limitblock),/goal rounds <n>and/goal completego throughgoal/update(packages/picasso-sdk-server/goal.js, compare-and-set on the goal revision). A restarted session leaves an active goal disarmed until/goal resume.apps/cli/src/goal-runtime.tsremains only behind the scriptablepicasso goal …contract subcommands. - Session follow: a prompt stream sees only its own turns and the goal rounds that follow it. Turns the engine starts by itself while no stream is open (goal rounds after
/goal resumeor/goal continue, subagent completion notices, scheduled turns) reach the transcript throughwatchEngineTurns(tui-backend.tsengineFollow): the backend holds the turn's notifications from its firstsession.status running/turn/start, the TUI opens a card and attaches withexecuteCommandStream("canvas", …, { follow: true }), which replays them and streams live through the same event path as a prompt. An open prompt stream owns its turns, so nothing is shown twice.
---
2. Key Invariants
- The engine is the sole runtime: The legacy local helper (
run-local-helper.mjs/packages/desktop-agent) is retired from the CLI/TUI; all one-shot (picasso run) and interactive sessions execute throughDshRuntimeSession. - Pinned engine updates:
picasso update runtimereportsengine pinned <pin> · in use <label> <version> · status ok|mismatch|missing;picasso update runtime --installinstalls the pin into~/.picasso/dshfrom the packaged lockfile, and the packaged CLI does the same automatically on first run (npm 12 skips global postinstall scripts). The pin only changes with a CLI release.picasso updateitself checks the npm dist-tags (docs/14-distribution.md). - No synthesized turns (C1): a missing engine, a failed launch or a dropped Mac relay stream is an error, never a stub reply.
- Isolated test homes: All CLI/core integration tests must set
DSH_HOMEto a temporarymkdtempdirectory so test sessions never leak into~/.picasso/dsh-home. - Visual & terminal equivalence: TUI theme tokens (
design/styles.csssage accent#4F7D68/#79A591, warm dark neutrals#1f201e..#ecece7, paper/dark/colorblind palettes) are verified viapnpm design:tokens:checkand remote Playwright/TUI verification suites.
---
3. Account, sync and attach surfaces
| Surface | Code | Behaviour | |||
|---|---|---|---|---|---|
picasso auth login [--accept-terms] / logout / status [--json] | auth-provider-commands.ts, login-store.ts | Device-code sign-in. The token lives in the Keychain item com.picassohq.account-session (account = API origin) shared with Picasso for Mac, written through security -i on stdin. Terms are accepted at a [y/N] prompt or with --accept-terms; a non-interactive login without it exits 2. Signed out, expired, revoked or suspended exit 3. See docs/11-auth-and-accounts.md. | |||
| 401 / 403 mapping | api-client.ts | 401 deletes the local token and fails Signed out — run \picasso auth login\`; 403 account_suspended keeps it and fails Your Picasso account is suspended. Contact [email protected].` (both exit 3). The TUI shows the same strings. | |||
picasso attach --mac | relay-attach.ts (RelayAttachSession) | Reads the discovery file ~/.picasso/run/mac-helper.json (0600, in a 0700 dir, owner-checked; {version, pid, url, token, startedAt}) written by the running Mac helper, and talks to its loopback relay with the scoped local-attach token in the authorization header. No flags: list the Mac threads. --session <id> --prompt <text>: send a prompt and stream the reply until that turn ends. --session <id> --cancel: stop the current turn. `--interaction <id> --decision allow\ | allow-session\ | deny: answer a pending approval. --json for machine output. A bare picasso attach with no PICASSO_REMOTE_HOST also uses the Mac. When the app is not running it prints Picasso for Mac isn't running. This is a non-interactive surface: the interactive TUI attach to a Mac thread is not built yet (H-C22); picasso attach <host>` still opens the TUI against a remote Host. | |
picasso host …, picasso attach --pair <CODE@host:port#fp> / --host <name> | host-commands.ts, host-pairing-client.ts, host-attach.ts | Run this machine as a headless host the Mac app and other CLIs pair with directly (no cloud), and pair with one. Security model and commands: docs/mac-relay.md § Host mode. /doctor in the TUI lists paired hosts (online, version, folders). | |||
| `picasso settings set sync.cloudSessions on\ | off, sync.cloudMemory on\ | off` | settings-commands.ts | Writes ~/.picasso/settings/settings.json (shared with the Mac, locked writes). Both default off. With sync.cloudSessions on and a signed-in account, each finished run uploads the session (CloudSessionSyncClient.upload); a failure is recorded as a cloud-session-sync-failed event and never fails the turn. sync.cloudMemory is read by the Mac helper; the CLI syncs memory only through picasso memory sync. | |
| `picasso memory key status\ | export\ | import\ | link` | memory-key-commands.ts | The account memory key that encrypts synced memory, in the Keychain item com.picassohq.memory-key. export prints the recovery string (a secret) with a warning; import reads it from stdin or a hidden prompt (never argv); link --request on a new device shows six words and waits, link --approve on a device with the key seals it to the requester after the words match. |
| Cloud session list | — | picasso list lists local sessions. picasso session list --cloud lists the sessions synced to your account (GET /v1/sessions, needs settings sync.cloudSessions on); picasso session cloud show <id> prints one read-only and picasso session cloud continue <id> starts a local session from it (U3-2). |
4. PR lifecycle watch
apps/cli/src/pr-watch.ts runs packages/pr-watch inside the TUI for the sessions live in this process: it follows each session's turns (session.status / turn/start / turn/end notifications), scans the finished turn's engine log for gh pr create, and /pr confirm registers the PR it opened. Wakes go through the session's own queue (session/queue: followup while "Wake threads on PR updates" is on, steer into the next turn while it is off). The header and sidebar show the watched PR's status (backend.loadPrStatus, polled every 15 s). /pr status, /pr watch <n> and /pr unwatch <n> work in the TUI; picasso pr watch <n> --session <id>|--last [--once] and picasso pr status work from a shell. The watch list is the shared <picassoHome>/pr-watch.json, so a watch outlives the process that started it (the next TUI, picasso pr watch, or the Mac relay resumes it). picasso settings set wakeOnPrUpdates on|off toggles waking. Tests stub gh on PATH.
5. TUI surfaces added since 2026-10-28
| Surface | Code | Behaviour | |||
|---|---|---|---|---|---|
| Queue, steer and interrupt | packages/tui/src/queue.tsx, composer.tsx | With text, Enter queues while a turn runs (or steers, per the shared enterWhileRunning setting, `picasso settings set enterWhileRunning queue\ | steer`). In an empty composer Enter sends the selected, else the oldest, queued message into the running turn now; the row reads "queued · Enter sends now". ⌃Q interrupts. A steered message renders as "› steered …" in the user's style; a shell command that exited non-zero shows ✕. | ||
| Decisions and options | packages/tui/src/decisions.ts | Two or more asks in one turn become one "Needs you · N decisions" block answered by number; approvals take 1/2/3 (a/s/d still work). The deferred present_options tool shows 2–4 options as a numbered list (←/→ or n/p, o opens one). | |||
| Resume picker | packages/tui/src/screens/sessions.tsx, project-identity.ts | Groups Needs you / Working / Recent / Settled; e archives, u undoes. Each project shows the same initials tile as the Mac app (AuraOne → AO, my-web-app → MW) in a distinct palette colour; nothing is read from project folders. | |||
| Start page | packages/tui/src/start-page.ts, activity-heatmap.ts | /activity [github] [year] (threads heatmap, or GitHub/GitLab contributions from the connected repo host), /customize (show, hide and reorder sections, shared with the Mac app in settings.startPage), Recent with a "This month" footer line. | |||
| Background work | packages/tui/src/background-jobs.ts, next-recent.tsx | /tasks [kill <jobId>] lists the engine's background jobs; `/librarian [pause\ | resume\ | look-all\ | budget <tokens>] and /updates` show the librarian and background engine updates as Next / Recent lists. |
| Resident runner and schedules | apps/cli/src/service-commands.ts, runner-schedules.ts | `picasso service install\ | uninstall\ | status installs a LaunchAgent runner (~/Library/LaunchAgents/com.picassohq.cli.runner.plist) so threads and schedules keep going after the terminal closes; it installs nothing when the Mac app's runner is up. /schedules (alias /schedule) needs a runner and says how to turn one on; /settings` shows "Keep threads running when Picasso quits: on/off". | |
| Context timeline, My Work, X | apps/cli/src/tui-backend.ts | /context timeline (per-turn context additions, compaction, cache hit/miss, subagent lanes; also picasso context timeline [thread] [--last] [--json], which needs no engine or model call); /work lists the files sessions made (`open\ | copy\ | similar <n>); /x` reads public X through the third-party x.md service. | |
| Themes and motion | packages/tui/src/color-themes.ts, ui-settings.ts | /theme picks one of 13 presets shared with the Mac app (or a custom theme); --no-animation / PICASSO_REDUCED_MOTION=1 or Settings › Appearance › motion stop animation, and --accessible / PICASSO_A11Y=1 gives plain output. | |||
| Tour and footprint | packages/tui/src/tour.ts | /tour is a short paged tour of the main keys; /about shows what Picasso puts on this computer and /about prompt prints the install-by-agent prompt. |