GenerateSaaS

Terminal and side panel

The side panel's two built-in tabs - Chat and Terminal - how to choose which ship and which opens first, and what a Terminal session gives the user's own CLI.

The right side panel hosts two built-in surfaces. Chat is the conversational assistant (Chat). Terminal launches the user's OWN coding CLI (Claude Code, Codex, Grok or OpenCode) in an embedded terminal, in the app's work folder.

Terminal runs on the app's own agent runtime. The CLI runs on the user's machine in a real pty, driven by the runtime the app forks - nothing to install and nothing to pair, and the tab appears whenever config.desktop.agents.enabled is on. A session sees the same connected CLIs and audit log as Chat and Automations, and updating the app updates the runtime with it, so a session can never meet an older one.

Choose which tabs ship

Set config.desktop.agents.sidePanel to pick which tabs ship and which opens first. Both ship by default, with Chat first.

// packages/config/src/desktop.mjs
agents: {
  // ...
  sidePanel: { chat: true, terminal: true, default: "chat" }
}
FieldTypeDefaultEffect
chatbooleantrueWhether the Chat tab ships.
terminalbooleantrueWhether the Terminal ships - the panel tab and its sidebar entry.
default"chat" | "terminal""chat"Which tab opens the first time the panel is shown.
  • The sidebar's Terminal entry opens the panel on its Terminal tab (there is no separate page - one host per session). Sessions live outside the view, so closing or reopening the panel never stops a running CLI.
  • One tab only: set the other false - the panel renders as a single surface ({ chat: false, terminal: true, default: "terminal" } for a terminal-only app).
  • No panel: set both false; the panel and its trigger disappear.
  • The user's choice wins: default picks the tab on first open only. After the user switches, their choice is remembered across restarts. A default naming a disabled tab falls back to the first available one.
  • The whole panel is gated on config.desktop.agents.enabled: with AI off, neither tab nor the trigger renders, and the source still ships.

What the Terminal wires in

The app names only the CLI to run, the terminal's size, the starting model when your config pins one, and (optionally) a folder the user picked. The rest of the session is composed by the runtime on the user's machine:

  • Any terminal-capable CLI: the picker offers the whole allowlist and launches any of them. Connecting a CLI on the Models screen is what puts it in the runtime's model catalog for Chat and Automations; a terminal session needs only the binary on the user's PATH, so an unconnected CLI is offered a one-click connect beside it rather than being withheld.
  • A working folder: the project's work folder by default, its connected folder when one is granted, or whatever the user picks for the session.
  • MCP servers: the ones the user added on this machine, plus your product's own MCP server when config.mcpServer is enabled - so a CLI opened here reaches your product's tools, not only this device's. Claude Code and Codex sessions only: Grok and OpenCode take MCP servers through their config files, not their interactive commands, so those sessions run with the user's own CLI setup unchanged.
  • An audit entry: the session is recorded in the app's local audit log before the CLI starts.

Nothing extra is asked of the user: the CLI resolves its own login and configuration exactly as it does in a plain terminal.

Your MCP server is wired in as an endpoint, never a credential. The app hands the CLI the URL only; the CLI runs its own browser sign-in against it on first connect, exactly as it would for a server the user added by hand. Terminal sessions with Claude Code or Codex are the only place it is wired in - a chat turn or an automation runs with nobody present to complete that sign-in.

Starting model

config.ai.terminals.cli pins the model a session starts on, per CLI. Empty by default - each CLI picks its own default, on the user's own subscription.

// packages/config/src/index.ts
ai: {
  terminals: {
    cli: {
      "claude-code": { model: "haiku" },
      codex: { model: "gpt-5.6-luna" }
    }
  }
}
EntryEffect
[cliId]: { model } - that CLI's own model idThe session launches on that model (--model, or -m for Codex).
No entryThe CLI starts on its own default, exactly as in a plain terminal.
  • It seeds the launch only. The user switches model inside the TUI whenever they want.
  • Separate from chat titles (config.ai.titles.cli): naming is one throwaway call that should always be the cheapest model going, a session is the user's real work.

Working folder

A session runs in the active project's work folder - the same one that project's chat turns and automations use, so switching project switches the folder too. Only the folder follows the workspace: the CLI's trust posture and MCP servers stay machine-global. To work on a real project, the user picks one with Choose a folder..., which opens the OS's native dialog.

Where a session startsWhen
The project's managed work folderThe normal case
The project's connected folderThe user granted one for this project
The folder they just pickedAn explicit Choose a folder..., which wins over both

Picking the folder IS the consent, for this session. It holds until they pick another, and no backend can name it. A connected folder is the other kind of consent - durable, granted once on the Project screen - and a session refuses to open when that grant no longer resolves.

Approval prompts

A Terminal session always starts with the CLI's own approval prompts bypassed - it edits and runs without asking, and nothing keeps it in the folder it started in. The tab states this rather than offering a switch.

The containment guarantee elsewhere in these docs covers unattended runs - chat turns and automations the runtime drives with nobody watching. A Terminal session is the user at their own keyboard, and is deliberately not confined.

On this page