Files
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

184 lines
9.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# In-App Terminals
Nomi ships a real terminal inside the app. Each terminal is a backend-managed
PTY session you can drive interactively from your browser/desktop window — and
that AutoWork can drive on your behalf when you bind it to a tag.
> Need the automation guide? See [AutoWork & Requirements](./autowork-requirements.md).
> Need to run an agent on a schedule? See [Scheduled Tasks](./scheduled-tasks.md).
![Nomi in-app terminal](../images/terminal-01-session.png)
## What an in-app terminal is
When you create a terminal, the backend (`nomifun-terminal`) spawns a child
process attached to a real pseudo-terminal via [`portable-pty`]. The session
has three pieces:
- **Persistent metadata** — id, name, working directory, command + args, env,
preset/backend, permission mode, current size (cols × rows), pinned flag,
exit status. Stored in SQLite so the session entry survives restarts.
- **A live PTY** (only while the child is running) — the OS pseudo-terminal,
its byte-stream output, and a scrollback buffer the backend keeps for late
joiners.
- **Realtime events on the WebSocket bus** — every chunk of PTY output is
base64-encoded and broadcast as `terminal.output`. Lifecycle events
(`terminal.created`, `terminal.updated`, `terminal.exit`, `terminal.removed`)
ride the same bus. The xterm.js view in the renderer subscribes and renders
the stream.
A PTY child cannot be paused or moved between processes: when the child exits,
the row stays but the live PTY is gone. Re-launching is in-place — the same
session id keeps a fresh process attached, so you do not get a new sidebar
entry every time you restart a CLI.
[`portable-pty`]: https://crates.io/crates/portable-pty
## Creating a terminal
Open the Terminal create page (the **+** button in the terminal sidebar
section, or navigate to `/terminal-new`). You pick five things:
1. **Workspace** — the working directory the child process will be spawned in.
Recent workspaces are remembered.
2. **Preset**`Shell`, `Claude Code`, `Codex`, or `Gemini`. The shell preset
resolves to your platform's login shell at launch time (Windows:
PowerShell/`cmd`, macOS/Linux: `$SHELL`); the agent presets launch the
matching CLI binary that must already be installed and on `PATH`.
3. **Permission mode** (agent presets only) — `Default` (interactive
approvals) or `Full Auto` (the CLI's own non-interactive flag is added):
| Preset | Full-auto flag |
| ------------ | ------------------------------------------- |
| `claude` | `--dangerously-skip-permissions` |
| `codex` | `--dangerously-bypass-approvals-and-sandbox`|
| `gemini` | `--yolo` |
These bypass the CLI's interactive approval prompt — needed for AutoWork to
drive a turn end-to-end without a human pressing Enter, but the same flags
give the CLI broad capability on your machine. Treat full-auto terminals
like a logged-in shell.
4. **Launch command** — the dialog renders the resolved `command + args` into
an editable field. Tweak it freely (extra flags, alternative entry point,
etc.) before pressing **Launch**.
5. **Knowledge bases** (optional) — multi-select one or more knowledge bases
to bind to this session. Bound bases are mounted at
`{workspace}/.nomi/knowledge/` before the child spawns, together with a
generated `README.md` (retrieval protocol + per-base digests + TOC +
write-back rules); the `claude` preset additionally gets an
`--append-system-prompt` pointer to that README. Rebinding takes effect on
the next re-launch. (The gateway tool `nomi_create_terminal` accepts the
same binding via `knowledge_base_ids`.)
![Terminal create page](../images/terminal-02-create-page.png)
The backend persists the row and spawns the child. The page navigates to
`/terminal/<id>` and you start receiving live output.
## Driving a terminal
The session page is xterm.js wired to the realtime stream:
- **Type** to send keystrokes to the PTY. The send box also accepts paste with
bracketed-paste markers, so multi-line text becomes one paste rather than a
flurry of Enters.
- **Resize** the panel and the backend resizes the PTY accordingly (`SIGWINCH`
is delivered to the child). The new dimensions are persisted.
- **Re-launch** when the child has exited: a single button kills any leftover
PTY for the same id, spawns a fresh process with the stored command + cwd
+ env, clears the view, and the same `terminal.<id>` subscription picks up
the new output. You keep the same sidebar entry.
- **Rename / pin** from the session header (renames broadcast as
`terminal.updated`; pinned terminals float to the top of the sidebar).
- **Kill** stops the child but keeps the row (it transitions to `exited` and
becomes re-launchable). **Delete** kills the child and removes the row
entirely.
![Driving a terminal session](../images/terminal-03-driving-session.png)
## Streaming model
Output flows over a single WebSocket. While you are looking at a session, your
client receives `terminal.output` events for that id and renders them. The
backend keeps a scrollback buffer in memory while the PTY is live: when you
open a terminal that is already running, the GET response includes a
base64-encoded `scrollback_b64` snapshot, so xterm replays history before live
events stream in.
Client-to-server input goes the other direction over a small REST endpoint
(base64-encoded bytes). The backend writes those bytes straight to the PTY's
stdin.
## Terminals as automation targets
The same in-memory PTY map that powers the UI is shared with the **AutoWork
orchestrator** in `nomifun-requirement` via the `TerminalDriver` trait. That
trait lets AutoWork:
- Subscribe to a copy of the terminal's live output (it watches for completion
markers and detects quiescence — see the AutoWork guide for the contract).
- Write input bytes to the PTY (it injects the requirement prompt wrapped in
bracketed-paste so a multi-line instruction lands as a single paste).
- Check liveness, read the row's metadata (user, backend, mode), and read or
write a per-terminal `autowork` config blob.
In other words: **a terminal you create here is automatable by AutoWork**.
Bind a tag from the AutoWork toolbar in the session header, and the
orchestrator will start claiming requirements and feeding them to the CLI
running in this terminal. Only agent-CLI terminals (`claude`, `codex`,
`gemini`) are eligible — a plain shell can be driven manually but is not an
AutoWork target. The orchestrator also recommends Full Auto mode, because a
turn that hits an interactive approval prompt will block until it times out.
If the workspace has knowledge bases mounted (`{cwd}/.nomi/knowledge/`
exists), AutoWork- and cron-driven prompts are automatically prefixed with a
one-line hint pointing the CLI at the mounted `README.md` before it starts
working.
If the PTY exits while AutoWork is still bound, the loop does not stop — it
idles and waits for you to re-launch the terminal, then resumes claiming
where it left off. If you delete the row, the loop stops for good.
## IDMM (decision-stall supervision)
Long-running CLI sessions sometimes stall: the provider drops, the model
spins on a tool call, the CLI prints a confirmation prompt nobody answers.
The IDMM (Intelligent Decision-Making Mode) supervisor watches a session and
intervenes — first with rule-based nudges (no LLM), then by calling a sidecar
backup model — so the turn reaches a terminal state instead of hanging until
the AutoWork timeout fires.
You can enable IDMM per-terminal from the same session header (the **IDMM**
control next to AutoWork). It works whether or not AutoWork is also bound;
when both are on, AutoWork ensures IDMM is supervising for the duration of
each turn.
## Routes & API
| What | Where |
| -------------------------- | ------------------------------------------- |
| Create page | `/terminal-new` |
| Session page | `/terminal/:id` |
| List / create | `GET /api/terminals`, `POST /api/terminals` |
| Get / update / delete | `GET|PATCH|DELETE /api/terminals/:id` |
| Send input | `POST /api/terminals/:id/input` |
| Resize | `POST /api/terminals/:id/resize` |
| Kill child | `POST /api/terminals/:id/kill` |
| Re-launch in place | `POST /api/terminals/:id/relaunch` |
| Live output / lifecycle | WebSocket events `terminal.*` |
## Troubleshooting
- **The CLI is not found.** The agent presets call `claude`, `codex`, or
`gemini` directly — they must be on the `PATH` of whatever account is
running the backend. Either install the CLI globally or edit the launch
command to use an absolute path before launching.
- **AutoWork bind is greyed out.** Only `claude`/`codex` terminals are
AutoWork targets today. A plain shell preset cannot be bound, and Gemini
terminal AutoWork is not wired into the backend completion contract yet.
- **Re-launch keeps reusing the same env / cwd.** That is intentional — the
session row stores them. To change them, create a new terminal with the
desired settings.
- **The output is garbled after resize.** Some TUIs need a redraw on
`SIGWINCH`. Press `Ctrl-L` (or your CLI's redraw shortcut).