Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ Self-hosted autonomous AI agent platform for a single owner, with a sandboxed wo

AgentStudio provides a streaming chat interface where the assistant can call tools such as web search and sandboxed code execution. The filesystem toolset supports ranged file reads, full writes, unified-diff patch apply, deterministic string replace, recursive directory listing, search, move/rename, delete, and file metadata lookups. Chat supports editing and branching, interleaved tool and thinking blocks, per-message performance and cost metrics, model selection, and per-prompt reasoning effort selection.

Chats run on Claude models through the Claude subscription, at no per-token cost. The model pickers — the composer, the default model and each agent's model — offer only models that can actually run here: the current Claude models Claude Code knows, plus, when the operator configures an Anthropic-compatible gateway such as OpenRouter's, the gateway's models, labelled **Gateway · paid** and priced per token in the usage ledger. The gateway is off by default. See [docs/llm/llm.md](docs/llm/llm.md).
Chats run on Claude models through the Claude subscription, at no per-token cost. The model pickers — the composer (its model pill and the `/model` command), the default model and each agent's model — offer only models that can actually run here: the current Claude models Claude Code knows, plus, when the operator configures an Anthropic-compatible gateway such as OpenRouter's, the gateway's models, labelled **Gateway · paid** and priced per token in the usage ledger. The gateway is off by default. See [docs/llm/llm.md](docs/llm/llm.md).

The message box has two shortcuts. Typing `@` lists the files in the chat's workspace (a bound project's folder, or an agent's persistent workspace) and inserts the chosen file's relative path. Typing `/` at the start opens a command palette over the app's own actions: `/compact`, `/model`, `/agent`, `/research`, `/plan`, `/effort`, `/attach` and `/voice`. Both work from the keyboard and by tapping on a phone. See "Composer shortcuts" in [docs/chat/spec.md](docs/chat/spec.md).

Replies can be read aloud: a speaker button on each reply plays it through an OpenRouter text-to-speech model (code blocks are skipped), and an opt-in, per-device **Auto-read** switch above the message box reads each new reply when its turn finishes, for hands-free use. The model and voice are set in Settings → Model & AI; spend is recorded in the usage ledger under "Read Aloud" and counts toward budget limits. See [docs/speech/speech.md](docs/speech/speech.md).

Expand Down Expand Up @@ -210,6 +212,7 @@ Notes:
- Runtime spec: `docs/runtime/spec.md`
- Tools (what agents can call, approvals, code execution): `docs/tools/tools.md`
- Chat plan: `docs/chat/plan.md`
- Chat spec (composer shortcuts, attachments, safe rendering): `docs/chat/spec.md`
- Memory spec: `docs/memory/spec.md`
- Automations: `docs/automations/automations.md`
- Monitors: `docs/monitors/monitors.md`
Expand Down
8 changes: 8 additions & 0 deletions docs/chat-console/chat-console.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@ These are the constraints that matter, and why:
- **The URL is always visible** above the frame, so it is never ambiguous what is loaded.
- **Framed pages are sandboxed.** Scripts and forms are allowed; a page on the app's own
origin additionally loses `allow-same-origin` so it cannot script the app.
- **The composer's `@` file list follows the same rules.** It lists the workspace the chat's
next turn will run in (a bound project's folder, or an agent's persistent workspace; a chat
whose turns each start in a fresh folder has nothing to list), re-checks ownership on every
search, never follows or lists a symbolic link, skips `.git`, `node_modules` and build
output, and returns relative names only. It never runs `git` or any other program over the
workspace: the agent can write a repository's `.git/config`, and some of its settings make
git run a program on the server. See "Composer shortcuts" in the chat spec.
- **Previewed markdown is sanitized.** Raw HTML in the file is escaped rather than
executed, and links and images that are not `http(s)` are dropped. That includes text
that follows an inline `<code>`, `<kbd>` or `<pre>` tag, which the markdown library
Expand All @@ -201,6 +208,7 @@ These are the constraints that matter, and why:
## Integrations

- `src/lib/workspace/workspace.server.ts` — workspace resolution and path containment.
- `src/lib/chat-console/workspace-files.server.ts` and `mentions.server.ts` — the `@` file list.
- `src/lib/tools/sandbox.server.ts` — the same workspace the agent's file tools use.
- `/api/preview/raw` — image and PDF bytes for the rail.
- `src/lib/engine/tool-result-details.ts` — the `file_edit` details (path, change type,
Expand Down
69 changes: 69 additions & 0 deletions docs/chat/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,72 @@ Rules that keep this honest:
3. The same rules apply whether the conversation is on a Claude model or a third-party model routed through the gateway. A gateway model that cannot see images will simply not use them; nothing about the delivery path changes.
4. Files attached on the new-chat page (`/`) go with the first message, exactly as if they had been attached inside the conversation. The new-chat page used to upload them, show them, and then send only the text.

### Composer shortcuts: `@` file mentions and `/` commands

The message box has two shortcuts. Both work from the keyboard, and both work on a phone by tapping.

#### Mentioning a file with `@`

Typing `@` at the start of a word opens a list of the files and folders in the chat's workspace. As you keep typing, the list narrows to the closest matches. The letters only have to appear in order, so `pnt` finds `PinnedTodoPanel.svelte`, and a match in a file's name ranks above the same letters spread across folder names. Picking an entry writes its path into the message as inline code (for example `src/lib/app.ts`) followed by a space. Nothing is sent. The **@ Context** button does the same as typing `@`.

Which files are listed depends on where the chat's next turn will run:

| The chat is… | The list shows |
| --- | --- |
| Bound to a project | The project's working folder |
| Talking to an agent that keeps a persistent workspace | That workspace |
| Neither | Nothing. Each turn in such a chat starts in a fresh, empty folder, so a file the last turn wrote is not where the next turn will look. The list says so and suggests binding the chat to a project. |
| Not created yet (the new-chat page) | No `@` list and no **@ Context** button |

Rules:

1. A path is relative to the folder the next turn starts in, so the agent can open it exactly as written.
2. Folders that are never worth mentioning and are often huge are skipped: `.git`, `node_modules`, build output (`build`, `dist`, `.next`, `.svelte-kit`, `.turbo`, `target`), caches, coverage reports and Python virtual environments. Other dot-folders stay, so project knowledge (`.agentstudio/knowledge`), `.github` and `.claude` can be mentioned.
3. Symbolic links are never followed or listed. A link the agent made, or one a cloned repository contains, cannot be used to see files elsewhere on the server.
4. Only names reach the browser. File contents and full server paths never do.
5. A very large workspace is searched only up to 20,000 entries, 16 folders deep or 1.5 seconds of scanning, whichever comes first, and the list says when that happened. The list of files is remembered for 15 seconds, so a brand-new file can take that long to appear.
6. Only the conversation's owner can search its files. Asking about someone else's conversation gets the same answer as asking about one that does not exist.
7. The path goes in as inline code, not as `@path`, so what the model receives does not depend on how the Agent SDK happens to treat an `@`.

#### Running a command with `/`

Typing `/` as the very first character of the message opens the command palette. The **/ Commands** button does the same. Every command is an action the app already has a button for; the palette is a quicker way to reach it, and it calls exactly what that button calls. When that button is switched off, the command is too, and it says why instead of running.

| Command | What it does | Available |
| --- | --- | --- |
| `/compact` | Asks the agent to summarise the conversation so far, to free up context. The same as Compact on the context meter | In a chat, when no reply is running |
| `/model <model>` | Switches the model for the next message. It lists the same models as the model picker: only models that can run here, with gateway models marked "Gateway · paid" (see [docs/llm/llm.md](../llm/llm.md)) | Everywhere |
| `/agent <agent>` | Hands the conversation to another agent | Everywhere, when there are agents to pick |
| `/research <question>` | Starts a deep research run on the question and opens its page | In a chat |
| `/plan` | Turns plan mode on or off. Plan mode is read-only: the agent plans instead of making changes. Turning it off goes back to Ask. It never switches to Bypass, which still needs the confirmation on the mode chip | In a chat, when no reply is running |
| `/effort <level>` (also `/reasoning`, `/think`) | Sets how hard the model thinks before answering | Everywhere, except on a gateway model, which always runs with reasoning off |
| `/attach` | Opens the file picker | Everywhere |
| `/voice` | Starts or stops dictation | Where the browser supports it, and not while the last recording is still being turned into text |

How it behaves:

1. A command that needs a choice (model, agent, effort) opens a second list. The value in effect now is marked "current" and starts highlighted, so Enter keeps it. Typing after the command narrows the list (`/model sonnet`), and the best match is then highlighted instead.
2. A list shows 50 entries at a time; a deployment with a large gateway can offer more models than that. When the current model is not among those 50, it is shown first, so it is never missing from the list. A conversation whose model is stored in OpenRouter's spelling (`anthropic/claude-sonnet-4.5`) still finds its model marked current. A model that can no longer run here is not in the list at all; the model pill marks it "Unavailable". The first time the list opens on a page it may still be loading; the highlight lands on the current model when the list arrives, unless you have already moved it with the arrow keys or the pointer.
3. `/research` takes free text: type the question on the same line, after the command, and press Enter. Anything on the lines below is left in the message box.
4. A message that starts with a known command runs the command instead of being sent, whether it was picked from the list or typed out in full (`/effort high` works). Any other message that starts with a slash, such as `/usr/bin is missing`, is sent as typed.
5. The rest of the message box is kept. The **/ Commands** button puts the slash on its own line above an existing draft, and the draft is still there after the command runs.
6. A short confirmation, or the reason a command could not run, shows above the message box for a few seconds. The message box can be open while a reply is still running, when the agent has paused to ask you a question. `/compact` and `/plan` then say that a reply is running rather than acting: the Compact button does nothing until the reply ends, and the mode chip is switched off because a running reply keeps the mode it started with.
7. The palette is built so that commands the Agent SDK reports for a trusted project (its own `.claude/commands` and skills) can be listed beside these later without changing how it works.

#### Keyboard and touch

| Key | With a list open |
| --- | --- |
| Up / Down | Move through the list (it wraps around) |
| Enter or Tab | Take the highlighted entry |
| Escape | Close the list. It stays closed until the cursor leaves that word |
| Enter or Tab, while the file list is still searching | Nothing yet. Pick from the list once it arrives, or press Escape to close it and send the message as typed |
| Enter, when the list has finished and is empty | Sends the message as typed |

While a Japanese, Chinese or Korean input method is composing, Enter confirms the composition. It never sends the message or picks from a list; before this, Enter could send half-composed text.

On a phone the **Attach**, **@ Context** and **/ Commands** buttons sit in a row above the message box (before this they did nothing). Tapping an entry in a list picks it without closing the keyboard. The list opens above the message box, or below it when the box is near the top of the screen, as on the new-chat page.

### Starting a conversation from the new-chat page

The new-chat page (`/`) greets the owner by the display name they gave during first-run setup ("Good morning, Alex"). If no name was given, the greeting is just "Good morning".
Expand Down Expand Up @@ -296,13 +362,16 @@ The renderer checks itself when the app starts by running a set of known attack
- Messages keep their order when two writers add to a conversation at the same moment (a Stop saving what was written so far while the turn saves its final reply, or a background run). The one that loses the race takes the next position and is still saved.
- A staged attachment always lands in the same sandbox workspace the run's own tools resolve, so the path quoted to the agent is a path the agent can open.
- Nothing the model writes can run script in the app, and displaying a reply never loads any image except an uploaded attachment.
- A path offered by `@` is one the next turn can open as written: it is relative to the folder that turn starts in, and it never reaches through a symbolic link.
- A palette command calls the same handler as the button it stands in for. The palette has no copy of its own of any action.

## Roles & Permissions

| Action | Who can do it |
| -------------------------------- | ------------------ |
| View own workbench sessions | Authenticated user |
| Approve own plan or tool request | Owner user, admin |
| Search a conversation's files with `@` | The conversation's owner |
| Resolve another user's item | Admin only |
| View admin observability panes | Admin only |

Expand Down
4 changes: 2 additions & 2 deletions docs/llm/llm.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The app's smaller features — conversation titles, memory, monitors' yes/no che

Three pickers choose the model a chat runs on. All three list only engine models:

1. **The chat composer**, for the conversation in front of you.
1. **The chat composer**, for the conversation in front of you: its model pill, and the `/model` command in its `/` palette, which lists the same models (a gateway row there says "Gateway · paid" beside its id).
2. **Settings → Model & AI → Default Model**, for new conversations.
3. **An agent's configuration** (`/agents/[id]`), for conversations that agent starts — from a monitor, a pull-request fix, and so on.

Expand Down Expand Up @@ -102,7 +102,7 @@ Nothing else from the server's environment reaches it, the same as for any turn
- **Off by default.** No gateway settings means Claude only. Both `LLM_GATEWAY_URL` and `LLM_GATEWAY_TOKEN` are needed; an empty value counts as unset.
- **Only models the gateway serves are offered.** If the gateway's model list cannot be fetched, no gateway models are offered (the app retries a minute later) rather than guessing.
- **Only gateway models that can use tools are offered.** Claude Code sends its tools with every request and expects a text answer. A model OpenRouter's catalogue lists without tool support, or that answers only in images or audio, would fail on the first message, so it is left out. A model the catalogue does not list at all (a local model behind LiteLLM) is offered, since nothing says it cannot run.
- **Reasoning is off on gateway models.** Claude's adaptive thinking and effort levels are Anthropic features; whether a gateway passes them on to another model is unverified, so a gateway turn runs with thinking off and the composer's reasoning control is disabled.
- **Reasoning is off on gateway models.** Claude's adaptive thinking and effort levels are Anthropic features; whether a gateway passes them on to another model is unverified, so a gateway turn runs with thinking off and the composer's reasoning control is disabled, as is its `/effort` command, which says why instead of opening its list.
- **Cost.** A Claude turn records its tokens and $0. A gateway turn is priced from OpenRouter's catalogue over that turn's own tokens — input, output, and cached prompt tokens at the catalogue's cache prices where it lists them. The ledger row notes `backend: gateway` and where the price came from (`costBasis`): `catalogue`, or `cli-estimate` for a model the catalogue does not price (Claude Code's own estimate), or `unpriced` when there is neither. An unpriced turn is recorded like any other call the ledger cannot price: at $0, marked unpriced with the reason, and with a warning in the server log. Claude Code's own estimate is not used when the catalogue has a price, because for a model it does not know it guesses at a Claude rate.
- **Budgets apply.** Gateway turns count toward budget limits like any other metered spend.
- **Tool use is weaker off Claude.** Claude Code is built for Claude models; OpenRouter says other models may not work correctly through it, and multi-step tool use is where they fall short. The gateway is a deliberate, labelled choice, never a default.
Expand Down
Loading
Loading