@turmind/halo 1.5.10 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +2 -2
  2. package/admin-out/404.html +1 -1
  3. package/admin-out/_next/static/chunks/{388-d5950f264d91625a.js → 147-d4200c70445aabb6.js} +1 -1
  4. package/admin-out/_next/static/chunks/989-a7f752480eaaa2dc.js +1 -0
  5. package/admin-out/_next/static/chunks/app/{layout-5ec6c5f713a8562b.js → layout-ea161287d934ba96.js} +1 -1
  6. package/admin-out/_next/static/chunks/app/page-8173112f256bc483.js +1 -0
  7. package/admin-out/_next/static/chunks/{webpack-efd27999305b080a.js → webpack-dc73047e0279979b.js} +1 -1
  8. package/admin-out/_next/static/css/7b736202f0b1dbfd.css +1 -0
  9. package/admin-out/_next/static/css/981c9e5610ce535a.css +1 -0
  10. package/admin-out/index.html +1 -1
  11. package/admin-out/index.txt +6 -5
  12. package/bundled-docs/design/a2a.md +286 -0
  13. package/bundled-docs/design/agent.md +7 -5
  14. package/bundled-docs/design/agentcore.md +194 -122
  15. package/bundled-docs/design/architecture.md +2 -2
  16. package/bundled-docs/design/canvas-extensions.md +30 -9
  17. package/bundled-docs/design/halo-city.md +6 -0
  18. package/bundled-docs/design/observability.md +12 -4
  19. package/bundled-docs/design/prompt-system.md +1 -1
  20. package/bundled-docs/design/relay.md +6 -6
  21. package/bundled-docs/design/session.md +15 -12
  22. package/bundled-docs/design/storage.md +9 -2
  23. package/bundled-docs/design/web.md +36 -59
  24. package/bundled-docs/dev/acp-adapter.md +59 -175
  25. package/bundled-docs/dev/api.md +75 -6
  26. package/bundled-docs/dev/deploy.md +3 -2
  27. package/bundled-docs/dev/desktop-packaging.md +14 -0
  28. package/bundled-docs/dev/env.md +10 -6
  29. package/bundled-docs/dev/tools.md +101 -11
  30. package/bundled-docs/guide/README.md +1 -1
  31. package/bundled-docs/guide/channels/README.md +3 -3
  32. package/bundled-docs/guide/channels/acp.md +67 -42
  33. package/bundled-docs/guide/channels/feishu.md +1 -1
  34. package/bundled-docs/guide/channels/slack.md +1 -1
  35. package/bundled-docs/guide/channels/telegram.md +1 -1
  36. package/bundled-docs/guide/channels/web.md +10 -34
  37. package/bundled-docs/guide/channels/wechat.md +1 -1
  38. package/bundled-docs/guide/channels/wecom.md +1 -1
  39. package/bundled-docs/guide/chat.md +0 -1
  40. package/bundled-docs/guide/cli.md +41 -6
  41. package/bundled-docs/guide/delegation-and-access.md +3 -2
  42. package/bundled-docs/guide/getting-started.md +1 -1
  43. package/bundled-docs/guide/skills.md +2 -3
  44. package/bundled-docs/requirements/agent.md +1 -1
  45. package/bundled-docs/requirements/chat.md +17 -1
  46. package/bundled-docs/requirements/command.md +1 -2
  47. package/bundled-docs/requirements/editor.md +9 -3
  48. package/bundled-docs/requirements/explorer.md +1 -0
  49. package/bundled-docs/requirements/terminal.md +15 -0
  50. package/bundled-docs/requirements/workspace.md +6 -0
  51. package/dist/index.js +5392 -2786
  52. package/package.json +1 -1
  53. package/templates/agents/__score__/AGENT.md +1 -1
  54. package/templates/agents/default/agent.yaml +0 -1
  55. package/templates/models/aws-bedrock-claude-invoke.yaml +40 -1
  56. package/templates/models/aws-bedrock-openai.yaml +100 -6
  57. package/templates/prompts/all/RUNTIME.md +2 -1
  58. package/templates/prompts/all/TOOL_SHELL.windows.md +2 -1
  59. package/templates/skills/send-file/SKILL.md +1 -1
  60. package/templates/skills/send-file/a2a.md +13 -0
  61. package/templates/skills/skill/SKILL.md +1 -1
  62. package/templates/skills/workspace/stage.py +1 -1
  63. package/admin-out/_next/static/chunks/989-1797287c46139412.js +0 -1
  64. package/admin-out/_next/static/chunks/app/page-44ab337b2f071e4d.js +0 -1
  65. package/admin-out/_next/static/css/d53aa54212002364.css +0 -3
  66. package/templates/skills/acp/SKILL.md +0 -243
  67. package/templates/skills/acp/templates/SKILL.md.tmpl +0 -75
  68. package/templates/skills/acp/templates/ask.py +0 -177
  69. package/templates/skills/acp/templates/config.yaml.tmpl +0 -38
  70. /package/admin-out/_next/static/{A-Rjx2yltZDKaSO1qVv7K → _F_Fkyh9PainUNY82EEjY}/_buildManifest.js +0 -0
  71. /package/admin-out/_next/static/{A-Rjx2yltZDKaSO1qVv7K → _F_Fkyh9PainUNY82EEjY}/_ssgManifest.js +0 -0
@@ -2,8 +2,6 @@
2
2
 
3
3
  Talk to a halo agent over plain HTTP from any client you control — a browser, a curl script, a custom frontend, your own mobile app. The Web channel is the "build your own UI" channel: it gives you a token, you give the token to whatever client you wrote.
4
4
 
5
- > If you want a ready-made browser frontend without writing code, halo ships `packages/web-demo` — a tiny Express app that wraps the Web channel with its own password gate. See "Standalone web-demo frontend" at the bottom.
6
-
7
5
  ## What you'll end up with
8
6
 
9
7
  - A 24-byte random token (base64url-encoded) bound to one workspace
@@ -36,19 +34,20 @@ curl -N -H "x-token: $TOKEN" -H "Content-Type: application/json" \
36
34
  http://localhost:9527/api/web/chat
37
35
  ```
38
36
 
39
- Response is a stream of `data: {json}\n\n` SSE frames:
37
+ Response is a standard SSE stream: `data: {json}` event lines, plus a `: keepalive` comment line every 15 s so an idle proxy doesn't cut the connection during a long tool call:
40
38
 
41
39
  ```
42
40
  data: {"type":"session","sessionId":"web_abc123_m1xyz"}
43
41
  data: {"type":"thinking","text":"..."}
44
- data: {"type":"tool_call","toolName":"file_read","toolInput":{...}}
45
- data: {"type":"tool_result","toolName":"file_read","result":"..."}
42
+ data: {"type":"tool_call","toolName":"file_read","toolUseId":"toolu_01…","toolInput":{...}}
43
+ : keepalive
44
+ data: {"type":"tool_result","toolName":"file_read","toolUseId":"toolu_01…","result":"..."}
46
45
  data: {"type":"stream","text":"Hello! "}
47
46
  data: {"type":"stream","text":"How can I help?"}
48
47
  data: {"type":"complete"}
49
48
  ```
50
49
 
51
- Parse the `type` field on each event to render text vs tool calls vs completion.
50
+ Parse the `type` field on each event to render text vs tool calls vs completion. **Only lines starting with `data:` are events** — skip everything else (the keepalive comment, blank separators). `EventSource` does this for you; a hand-rolled parser that splits on `\n\n` and slices off `data: ` will break on the keepalive. `toolUseId` pairs a `tool_result` with its `tool_call` when several run at once (it can be empty for some model providers — then pair with the most recent call).
52
51
 
53
52
  ### Other endpoints
54
53
 
@@ -56,12 +55,13 @@ Parse the `type` field on each event to render text vs tool calls vs completion.
56
55
  |---|---|---|
57
56
  | `POST /api/web/chat` | Send a message; SSE response |
58
57
  | `POST /api/web/sessions` | Mint a new root session in the token's namespace; returns `{sessionId}` |
58
+ | `GET /api/web/sessions?cursor=…` | List the token's own conversations, newest first, 50 per page → `{workspace, sessions: [{sessionId, title, updatedAt}], nextCursor}`; pass `nextCursor` back as `cursor` for the next page (`null` = last page) |
59
59
  | `POST /api/web/stop` | Cancel the running task |
60
- | `GET /api/web/history` | Fetch session message history |
61
- | `GET /api/web/subscribe` | Reconnect to a running session's SSE stream |
60
+ | `GET /api/web/history` | Fetch session message history (optional `since=<epoch ms>`: only rows from then on) |
61
+ | `GET /api/web/subscribe` | Reconnect to a running session's SSE stream (an idle session answers with one `complete` straight away) |
62
62
  | `GET /api/web/file?path=…` | Fetch a file from the bound workspace (path relative to it; `.halo` runtime state is refused) |
63
63
 
64
- All six accept the same auth header.
64
+ All seven accept the same auth header.
65
65
 
66
66
  ### Per-request overrides
67
67
 
@@ -90,8 +90,6 @@ For browser apps you almost never want these — leave them off and use the per-
90
90
 
91
91
  Images go to the LLM as multimodal content. Any other `mimeType` (audio, PDF, …) in the same array is not sent to the model: the server saves it to `<workspace>/.halo/assets/web/inbound/<accountId>/<date>/` and appends a `[语音已保存: <path>]` line to your message so the agent can open it with its file tools.
92
92
 
93
- The standalone web-demo handles voice + arbitrary files for you; if you want that, use it as a reference implementation.
94
-
95
93
  ## Slash commands
96
94
 
97
95
  Slash commands are intercepted before they reach the agent — same set as every other channel:
@@ -102,7 +100,7 @@ Slash commands are intercepted before they reach the agent — same set as every
102
100
  | `/agent <verb>` | Manage agents (`list` / `switch` / `desc` open to all; `delete` full; `create` / `update` via skill, full) |
103
101
  | `/skill <verb>` | Manage skills (`list` / `desc` open; `disable` / `enable` workspace; `delete` full; `create` / `update` via skill, full) |
104
102
  | `/workspace <verb>` | Workspace: `info` (all) / `switch <path>` (full) / `setup` / `tidy` (workspace) / `share` (full) |
105
- | `/cron` `/acp` `/extension` | Skill-backed object commands (full access); `/evo [hint]` queues a self-evolution run |
103
+ | `/cron` `/extension` | Skill-backed object commands (full access); `/evo [hint]` queues a self-evolution run |
106
104
  | `/help` | List commands — object commands show only the verbs you can run |
107
105
 
108
106
  Send a slash command exactly like a normal message — the server detects the leading `/`.
@@ -118,28 +116,6 @@ Send a slash command exactly like a normal message — the server detects the le
118
116
  | Token leaked accidentally | Delete the account in admin, create a new one. The old token is invalidated immediately |
119
117
  | Want to share one token across multiple users | Don't — every request would land on the same active session. Create one account per user / app |
120
118
 
121
- ## Standalone web-demo frontend
122
-
123
- If you want a browser UI with login, image upload, voice recording, and multi-language support without writing it yourself:
124
-
125
- ```bash
126
- cd packages/web-demo
127
- node server.js
128
- ```
129
-
130
- Environment variables:
131
-
132
- | Variable | Required | Description |
133
- |---|---|---|
134
- | `HALO_API` | yes | Halo server URL, e.g. `http://localhost:9527` |
135
- | `HALO_TOKEN` | yes | The web-channel token from Step 1 |
136
- | `HALO_WEB_DEMO_PASSWORD` | no | Password gate; empty means open access |
137
- | `PORT` | no | Listen port; default `9528` |
138
-
139
- The demo holds your token server-side and exposes its own password-based session — the browser never sees the halo token. Open `http://localhost:9528` to use it.
140
-
141
- Alternatively, the gear icon (header / login screen) opens **direct-connect mode**: enter any halo server's URL + a web-channel token and the browser talks to that server's `/api/web/*` directly, skipping the proxy and its password. Handy for pointing one deployed web-demo at several halo servers. The token is kept in that browser's localStorage — only use it on devices you trust; clear it in the same panel to return to proxy mode.
142
-
143
119
  ## Security notes
144
120
 
145
121
  - **Tokens are unhashed** in `~/.halo/secrets/channels/channels.db`. Restrict that file to the user that runs halo
@@ -77,7 +77,7 @@ Same set as the other channels — type as plain text in your WeChat DM with the
77
77
  | `/agent <verb>` | Manage agents (`list` / `switch` / `desc` open to all; `delete` full; `create` / `update` via skill, full) |
78
78
  | `/skill <verb>` | Manage skills (`list` / `desc` open; `disable` / `enable` workspace; `delete` full; `create` / `update` via skill, full) |
79
79
  | `/workspace <verb>` | Workspace: `info` (all) / `switch <path>` (full) / `setup` / `tidy` (workspace) / `share` (full) |
80
- | `/cron` `/acp` `/extension` | Skill-backed object commands (full access); `/evo [hint]` queues a self-evolution run |
80
+ | `/cron` `/extension` | Skill-backed object commands (full access); `/evo [hint]` queues a self-evolution run |
81
81
  | `/help` | List commands — object commands show only the verbs you can run |
82
82
  | `/qr [level]` | Generate an invite QR (`full`-access bot only; `level` = `readonly` (default) / `workspace` / `full`) |
83
83
 
@@ -105,7 +105,7 @@ Same set as the other channels — type as plain text in a **single chat** (comm
105
105
  | `/agent <verb>` | Manage agents (`list` / `switch` / `desc` open to all; `delete` full; `create` / `update` via skill, full) |
106
106
  | `/skill <verb>` | Manage skills (`list` / `desc` open; `disable` / `enable` workspace; `delete` full; `create` / `update` via skill, full) |
107
107
  | `/workspace <verb>` | Workspace: `info` (all) / `switch <path>` (full) / `setup` / `tidy` (workspace) / `share` (full) |
108
- | `/cron` `/acp` `/extension` | Skill-backed object commands (full access); `/evo [hint]` queues a self-evolution run |
108
+ | `/cron` `/extension` | Skill-backed object commands (full access); `/evo [hint]` queues a self-evolution run |
109
109
  | `/help` | List commands — object commands show only the verbs you can run |
110
110
 
111
111
  ## Cron jobs targeting WeCom
@@ -68,7 +68,6 @@ Most commands are noun-verb **object commands**: `/<obj> <verb> [args]`. Bare `/
68
68
  | `/skill <verb>` | Manage skills — `list` / `desc` (built-in, open to all) · `disable` / `enable` (built-in, workspace access) · `delete` (built-in, full access) · `create` / `update` (handled by the `skill` skill, full access) |
69
69
  | `/workspace <verb>` | Manage the workspace — `info` (built-in, open to all) · `switch <path>` (built-in, full access) · `setup` / `tidy` (workspace skill, workspace access; init / reorganize `.halo/` INDEX.md / INSTRUCTIONS.md / memory/) · `share` (workspace skill, full access; export a shareable bundle) |
70
70
  | `/cron <verb>` | Scheduled agent runs — `create` / `list` / `update` / `enable` / `disable` / `delete` (cron skill, full access) |
71
- | `/acp <verb>` | Talk to other agents over ACP — `kiro <q>` / `claude <q>` ask a local agent directly; `add` / `list` / `remove` manage generated `ask-<label>` bindings (acp skill, full access) |
72
71
  | `/extension <verb>` | Canvas preview extensions — `install <id\|zip\|url>` / `list` / `remove <id>` (extension skill, full access) |
73
72
  | `/evo [hint]` | Queue a self-evolution run on this session (full access only) |
74
73
  | `/help` | List every command — object commands only show the verbs you can run |
@@ -143,7 +143,6 @@ Multi-turn conversation. Supports all standard Halo slash commands:
143
143
  | `/workspace switch <path>` | Switch workspace (full access only) |
144
144
  | `/workspace setup` / `/workspace tidy` / `/workspace share` | Set up / tidy the `.halo/` knowledge files, or export a shareable bundle (workspace skill) |
145
145
  | `/cron <verb>` | Manage scheduled agent runs: `create` / `list` / `update` / `enable` / `disable` / `delete` |
146
- | `/acp <verb>` | Ask other agents over ACP (`kiro <q>` / `claude <q>`) and manage `ask-*` bindings (`add` / `list` / `remove`) |
147
146
  | `/extension <verb>` | Canvas preview extensions: `install` / `list` / `remove`; `models` updates the model provider list from the hub (full access only) |
148
147
  | `/evo [hint]` | Queue a self-evolution run on this session (full access only) |
149
148
  | `/quit` | Exit |
@@ -181,7 +180,7 @@ The line under the input box:
181
180
  | `Ctrl+W` / `Alt+Backspace` | Delete the word before the cursor |
182
181
  | `Ctrl+U` / `Ctrl+K` | Delete to start / to end of input |
183
182
 
184
- On resume (`-s <id>`, or the default latest session), the TUI replays the session's prior conversation on screen so you see where you left off. `shell_exec` output is shown inline by default (other tools' output needs `-v`). Tool lines show a short argument summary and duration, e.g. `⚙ file_read hello.txt 4ms`.
183
+ On resume (`-s <id>`, or the default latest session), the TUI replays the session's prior conversation on screen so you see where you left off — only the last 100 messages; anything older collapses into a first line `── N earlier messages hidden · ctrl+o for the full log ──`. `shell_exec` output is shown inline by default (other tools' output needs `-v`). Tool lines show a short argument summary and duration, e.g. `⚙ file_read hello.txt 4ms`.
185
184
 
186
185
  **Multi-line paste**: pasting text with newlines (or >800 chars) collapses into a compact placeholder like `[#1 pasted 3 lines]` in the input; you can keep typing around it, and the original text is expanded back in full on submit. Pasted content is inserted at the cursor position.
187
186
 
@@ -211,6 +210,16 @@ Attach files or images to your message with `@file` and `@image`, or pull a dire
211
210
  - **Images**: skipped if larger than 5MB
212
211
  - If the current model does not support vision, a warning is shown and images are ignored
213
212
 
213
+ ## OpenTelemetry export
214
+
215
+ `halo cli` and `halo tui` export traces, metrics and logs the same way the server does: when the global `general.observability.endpoint` setting is non-empty, each run registers the OTLP exporters at start and the agent loop emits the same GenAI spans (`invoke_agent` / `chat` / `execute_tool`). With no endpoint nothing is loaded and startup is unaffected. The scheduled runs of [Cron Tasks](../design/cron.md) and the evolution pipeline are spawned `halo cli` children, so they show up in the collector too (`session.id` `cron-<jobId>` or the internal agent's name).
216
+
217
+ - **Telling processes apart**: they all share `service.name`; the `halo.process` resource attribute is `server`, `cli` or `tui`, and cron / evolution runs report `cli`.
218
+ - **Flushed on exit**: the batched telemetry is sent before the process ends — on normal completion, TUI quit, an error, and on SIGINT / SIGTERM (after the graceful stop, still exiting 130 / 143). The flush is capped at 3 s, so an unreachable collector delays the exit by at most that and never hangs it.
219
+ - `halo agents` and `halo sessions` only list and don't export.
220
+
221
+ Settings and the span / metric model: [design/observability.md](../design/observability.md).
222
+
214
223
  ## Model provider configs (`halo models`)
215
224
 
216
225
  ```bash
@@ -232,6 +241,31 @@ recognized as intentional and never respawns; `halo server stop|restart|status`
232
241
  still target the server's own pid. Full policy in
233
242
  [dev/deploy.md](../dev/deploy.md#crash-semantics--why-restarton-failure-is-load-bearing).
234
243
 
244
+ ## AgentCore runtime (`halo agentcore`)
245
+
246
+ ```bash
247
+ halo agentcore --workspace /mnt/efs/halo [-p 9000]
248
+ ```
249
+
250
+ Runs the server in the foreground as an Amazon Bedrock AgentCore Runtime
251
+ container speaking the A2A protocol (`HALO_RUNTIME_MODE=agentcore-a2a`): `GET
252
+ /ping`, `POST /` (A2A JSON-RPC) and `GET /.well-known/agent-card.json` on one
253
+ fixed workspace. No admin UI, password, channels, cron or evolution — AgentCore
254
+ verifies the caller's signed request before it reaches the container. It always
255
+ runs in the foreground (no `-d`) — meant as the container's entry command.
256
+
257
+ | Flag / env | Meaning |
258
+ |---|---|
259
+ | `-w, --workspace <path>` / `HALO_WORKSPACE` | Workspace to serve — required, must exist. Seeded with `.halo/agent-card.json` if missing. |
260
+ | `-p, --port <n>` | Listen port, default `9000` (the AgentCore contract) |
261
+ | `HALO_A2A_PUBLIC_URL` | Interface URL advertised in the card — the runtime's invoke URL (`…/runtimes/<escaped ARN>/invocations`) |
262
+ | `HALO_A2A_ACCESS` | Session access level: `workspace` (default) \| `full` \| `readonly` |
263
+
264
+ One runtime session id owns the workspace at a time (`.halo/agentcore.lease`);
265
+ requests on another id are refused until the holder has been gone ~45 s. Design,
266
+ the caller-side `auth: sigv4` remote and ops notes:
267
+ [design/agentcore.md](../design/agentcore.md).
268
+
235
269
  ## Architecture
236
270
 
237
271
  ```
@@ -261,7 +295,8 @@ Session prefix: `cli_`. Sessions are persisted to `<workspace>/.halo/sessions/`
261
295
 
262
296
  `tui` / `cli` / `agents` / `sessions` refuse to run ("~/.halo/global/ not initialized. Run halo setup first.") until `halo setup` has seeded `~/.halo/global/`. Then the CLI replicates the server's init sequence (`initRuntime()` in `harness.ts`):
263
297
  1. `refreshTemplatesIfOutdated()` — the server's template startup check: when `~/.halo/global/.template-version` is behind the bundled `TEMPLATE_VERSION` (typically right after `halo upgrade`), re-seed `~/.halo/global/` before anything reads the models registry; logs `[CLI] Templates outdated (vX → vY), refreshing ~/.halo/global/` to stderr. Runs once per upgrade — afterwards the stamp matches and it's a single file read
264
- 2. `initLogger()` — redirect console to stderr + file logger
265
- 3. `initBwrapCheck()` — probe sandbox availability
266
- 4. `setSandboxHiddenPaths()` — configure sandbox paths
267
- 5. `new SessionManager(workspace)` — create agent session manager (the workspace's `.halo/` is seeded via `ensureWorkspaceHalo`)
298
+ 2. `initObservability()` — `tui` / `cli` only (the list-only `agents` / `sessions` skip it): when `general.observability.endpoint` is set, register OTel providers so this run's traces, metrics and logs reach the collector, tagged `halo.process=cli` or `tui`; a no-op otherwise. Cron jobs and evolution runs are spawned `halo cli` children, so they export too. See [OpenTelemetry export](#opentelemetry-export)
299
+ 3. `initLogger()` — redirect console to stderr + file logger
300
+ 4. `initBwrapCheck()` — probe sandbox availability
301
+ 5. `setSandboxHiddenPaths()` — configure sandbox paths
302
+ 6. `new SessionManager(workspace)` — create agent session manager (the workspace's `.halo/` is seeded via `ensureWorkspaceHalo`)
@@ -103,7 +103,7 @@ Extra rules:
103
103
  - A readonly `shell_exec` gets no secret substitution — `{{…params…}}` / `<<ENV>>` placeholders stay literal.
104
104
  - `view_image` also needs a vision-capable model.
105
105
  - A skill with `requiresAccess` above the session level is hidden (no metadata, no `activate_skill`).
106
- - Relay tools are only built for `full` sessions.
106
+ - Relay tools are only built for `full` sessions. A2A tools (`a2a_send` in `tools:`) are built at every level except the A2A read-only profile; a non-full session's `files` must sit in the workspace or the temp dir ([dev/tools.md → A2A tools](../dev/tools.md#a2a-tools)).
107
107
  - Hidden from `workspace` / `readonly` sessions only (Full sessions, the admin file explorer and the terminal see everything): a built-in list that is always included (`~/.halo/secrets`, `~/.aws`, `~/.ssh`, `~/.gnupg`, `~/.docker`, `~/.config/gh`, `~/.gitconfig`, `~/.git-credentials`, `~/.npmrc`, `~/.netrc`, the global evo/cron/runs databases, internal-session transcripts, logs, …), any extra paths added in `general.sandbox.hidden_dirs` / `hidden_files` (they add to the built-in list, never replace it), plus the workspace's own `.halo/sessions`, `.halo/logs`, `.halo/evo`, `.halo/halo.db*` (fixed in code). The rest of `.halo/` stays readable.
108
108
 
109
109
  ### Where a session's level comes from
@@ -117,7 +117,8 @@ Extra rules:
117
117
  | Cron job | Full (the `halo cli` default) | Keeps its stored level (passed through as `--access`) |
118
118
  | `halo cli` / TUI | `--access`, default `full` | `--access` value (a different level is persisted) |
119
119
  | `relay_send` into another workspace | Full | Keeps its stored level |
120
- | AgentCore runtime | Full | — |
120
+ | A2A inbound (`/a2a/<path>`, a web-channel token bound to that workspace) | The token's `accessLevel`, mapped like a channel account. A `readonly` / `observer` token gets the fixed A2A read-only profile: `file_read` / `view_image` / `file_list` / `grep` / `glob` + `continue_task`, even with an OS sandbox — no shell, write or fetch, no delegation, no relay / a2a tools | Re-applied on every inbound message |
121
+ | AgentCore runtime (`halo agentcore`) | `HALO_A2A_ACCESS`: `workspace` (default) \| `full` \| `readonly` — every A2A session gets it | Re-applied on every inbound message |
121
122
 
122
123
  Consequences:
123
124
  - A sub-agent can never have more access than its parent had when it was spawned. A readonly channel user can't escalate by delegating.
@@ -79,7 +79,7 @@ After `halo setup`:
79
79
 
80
80
  **When does "refreshed on upgrade" actually run?** `halo setup` always re-runs the seed; the server's startup check — and the same check at the start of `halo cli` / `halo tui` — also re-runs it automatically when `~/.halo/global/.template-version` is behind the bundled `TEMPLATE_VERSION`. So the routine flow `halo upgrade && halo server restart` is enough — no need to remember `halo setup` — and a CLI-only user just runs `halo cli` / `halo tui` after `halo upgrade`.
81
81
 
82
- **Server-refreshed**: `INSTRUCTIONS.md`, `prompts/`, `models/`, `docs/`, the built-in agent ids (`default`, `executor`, `deep-executor`, `goal`, `__evo_agent__`, `__score__`, `__apply_agent__`), and the built-in skill ids (`agent`, `skill`, `workspace`, `cron`, `acp`, `send-file`, `self`, `aws-knowledge`, `web-search`, `halo`, `extension`). To customize one, copy it into the workspace scope (`<project>/.halo/...`) — workspace replaces global at runtime.
82
+ **Server-refreshed**: `INSTRUCTIONS.md`, `prompts/`, `models/`, `docs/`, the built-in agent ids (`default`, `executor`, `deep-executor`, `goal`, `__evo_agent__`, `__score__`, `__apply_agent__`), and the built-in skill ids (`agent`, `skill`, `workspace`, `cron`, `send-file`, `self`, `aws-knowledge`, `web-search`, `halo`, `extension`). To customize one, copy it into the workspace scope (`<project>/.halo/...`) — workspace replaces global at runtime.
83
83
 
84
84
  **Never overwritten**: anything else under `agents/` or `skills/` (your own creations), and everything under `secrets/`.
85
85
 
@@ -30,7 +30,7 @@ Putting those in AGENT.md would bloat the system prompt. Skills use **progressiv
30
30
 
31
31
  Workspace overrides global (same id).
32
32
 
33
- **Built-in skills** (`agent`, `skill`, `workspace`, `cron`, `acp`, `send-file`, `self`, `aws-knowledge`, `web-search`, `halo`, `extension`) are server-shipped and **force-overwritten on every server startup**. Local edits to these directories under `~/.halo/global/skills/` will be lost. To customize one, copy it into `<project>/.halo/skills/<id>/` (workspace replaces global) and edit there. Other skills under `~/.halo/global/skills/` — including any the user created via the admin UI — are untouched by the seeder.
33
+ **Built-in skills** (`agent`, `skill`, `workspace`, `cron`, `send-file`, `self`, `aws-knowledge`, `web-search`, `halo`, `extension`) are server-shipped and **force-overwritten on every server startup**. Local edits to these directories under `~/.halo/global/skills/` will be lost. To customize one, copy it into `<project>/.halo/skills/<id>/` (workspace replaces global) and edit there. Other skills under `~/.halo/global/skills/` — including any the user created via the admin UI — are untouched by the seeder.
34
34
 
35
35
  ## SKILL.md format
36
36
 
@@ -154,8 +154,7 @@ Halo seeds these skills on every startup (the ids in `BUILTIN_SKILL_IDS`, `packa
154
154
  | skill | Create / update skills — backs the `create` / `update` verbs of `/skill` |
155
155
  | workspace | Workspace maintenance — backs `/workspace setup` / `tidy` (init / reorganize `.halo/` INDEX.md / INSTRUCTIONS.md / memory/) and `/workspace share` (export a shareable bundle) |
156
156
  | cron | Create / list / update / enable / disable / delete scheduled agent runs — backs `/cron` |
157
- | acp | Talk to other agents over ACP (`/acp kiro\|claude <q>`) and manage `ask-<label>` bindings for halo-to-halo delegation (`/acp add\|list\|remove`) |
158
- | send-file | Deliver an image/video/file as a channel attachment by emitting `MEDIA:<absolute_path>` — works on Web / WeChat / Telegram / Slack / Feishu (no command; model-activated, workspace access) |
157
+ | send-file | Deliver an image/video/file as a channel attachment by emitting `MEDIA:<absolute_path>` — works on Web / WeChat / Telegram / Slack / Feishu / WeCom / A2A (no command; model-activated, workspace access) |
159
158
  | self | The agent's own visual space (`.halo/canvas/self.html`) for self-expression (no command; model-activated, full access) |
160
159
  | aws-knowledge | Query the official AWS Knowledge MCP server for up-to-date AWS docs (no command, `user-invocable: false`; model auto-activates) |
161
160
  | web-search | Real-time web search on AWS Bedrock (needs the machine's AWS credentials with Bedrock access) in two gears: fast (Amazon Nova grounding, default; US regions only) and deep (`--deep`, GPT-5.6 `web_search` via Bedrock Mantle, slower and token-expensive); regions set via its `fast_region` / `deep_region` params (Settings → Skills) (no command, `user-invocable: false`; model auto-activates) |
@@ -55,7 +55,7 @@ Same id present in both scopes: workspace wins; the overridden global is greyed
55
55
 
56
56
  ### Tool selection
57
57
  - **Session tools**: `start_session` / `session_list` / `query_session` / `interrupt_session` / `stop_session` / `archive_session` / `get_session_output` / `query_agent` — **not** selected by name; the whole bundle is granted automatically by a non-empty `team` (see below). Listing them under `agent.yaml tools` has no effect.
58
- - **Workspace tools**: `file_read` / `view_image` / `file_write` / `file_edit` / `file_list` / `shell_exec` / `grep` / `glob` / `web_fetch`, plus the single `relay_send` chip (grants the whole cross-workspace relay set, full-access sessions only), returned by `GET /api/agent-configs/tools`. `view_image` is dropped at runtime when the model can't take images (the form dims it)
58
+ - **Workspace tools**: `file_read` / `view_image` / `file_write` / `file_edit` / `file_list` / `shell_exec` / `grep` / `glob` / `web_fetch`, plus the single `relay_send` chip (grants the whole cross-workspace relay set, full-access sessions only) and the single `a2a_send` chip (grants the cross-server A2A set — `a2a_send` / `a2a_stop` / `a2a_read` / `a2a_list` — at every access level except the A2A read-only profile), returned by `GET /api/agent-configs/tools`. `view_image` is dropped at runtime when the model can't take images (the form dims it)
59
59
  - **`activate_skill`**: auto-injected when the YAML lists at least one usable skill (not disabled, allowed at the session's access level); loads the full SKILL.md on demand
60
60
 
61
61
  ### Team (delegation switch + whitelist)
@@ -8,6 +8,7 @@ The primary surface for talking to an agent.
8
8
  - Dropdown in the composer's left control cluster (after the Debug button) selects which agent to use; hidden when only one agent is usable
9
9
  - Lists every available agent (from `GET /api/agent-configs`; overridden, disabled and internal agents are hidden), highest priority first. A new session starts on the top one
10
10
  - **Locked during an active session** (and while a response is streaming) — the agent is bound to the session; to change, start a new session (/session new)
11
+ - While it can be changed, the Bot icon is primary-coloured (same "active" colour as the other toolbar toggles); locked, it stays muted grey and dimmed, and hovering it explains that the agent can't change once the conversation has started — start a new session to use another (`chat.agent.locked`)
11
12
  - The Agents panel's "Test" button can also preselect an agent
12
13
 
13
14
  ### Access level
@@ -22,7 +23,8 @@ The primary surface for talking to an agent.
22
23
  - Markdown + code-block highlighting
23
24
  - Tool-call card: expandable, shows tool name / input / output
24
25
  - Sub-agent streams stay out of the chat: their reports land as a green callout, and Debug mode adds agent start/done markers labelled with the agent name (e.g. "Coder", "Researcher")
25
- - A reply that has produced nothing yet shows a spinner with "Thinking..."
26
+ - A reply that has produced nothing yet shows a spinner with "Thinking..."; once the model streams reasoning text, the spinner gives way to a **live thinking panel**: expanded, muted, with a "Thinking..." header and char count, capped at about 9 lines and tailing its own text (so the chat log grows by at most the cap). It folds to the one-line collapsed "Thinking · N chars" header (click to expand; it stays in the log) as soon as text or a tool call follows, or the message settles. Providers that stream no reasoning text stay on the spinner; Debug mode is unchanged (spinner until text or a tool call arrives, collapsed headers only)
27
+ - **Follow the bottom by intent**: while a reply streams the log follows its end, and scrolling up stops the follow. Upward input — mouse wheel, touch drag, PageUp / ArrowUp / Home / Shift+Space, or grabbing the scrollbar — detaches the moment it arrives, so a fast stream can't snap the view back mid-gesture; upward input that a nested scroller (the live thinking panel, an expanded tool body) would take doesn't count. The log re-attaches only when the view is scrolled back down to within 80 px of the end. Applies to the chat panel, the Sessions tab viewer and the live thinking panel alike; a reader scrolling inside the thinking panel stops its tail the same way
26
28
  - The user bubble shows its send time (`HH:mm`, browser-local) at the left of its header row, after the expand chevron (see [User-message actions](#user-message-actions-expand--copy--delete)); hovering it gives the full date. Same bubble component as the Sessions tab, so both surfaces get it
27
29
  - In Debug mode the system-prompt `context` row stays in the list but doesn't open an exchange of its own, so there is no blank strip above the first bubble
28
30
 
@@ -58,6 +60,7 @@ The workspace's root sessions as **Chrome-style vertical tabs** on the right edg
58
60
  - **Rows**: one line per row — just the title, no leading icon cell (the "New session" draft row has none either, so the titles line up). Message count, time-ago, model and the state text (`Working…` / `New messages` / `Idle`) are in the tooltip, the state after a ` · ` at the end of its second line. `N msgs` counts the session's user turns **over its whole lifetime**, so compacting or archiving history never makes it go backwards.
59
61
  - Each row carries a **2px status bar** along its bottom edge, full row width with 6px inset on both sides and rounded ends (absolutely positioned, so a state change never reflows a narrow list — the spinner it replaced did; decorative, `aria-hidden`): **amber, breathing** (`animate-pulse`) while a loaded tab's turn is running; **blue**, solid, when a loaded background tab is idle with unread output (output landed while it wasn't on screen; cleared when shown); **green** otherwise — including sessions no tab has loaded and tabs a reconnect released. Green is the norm, so it is **dimmed** (`/40`) to recede into the background, and rises to `/70` on the selected and hovered row; amber and blue never dim. The list endpoint's `status` is not used. In the light and warm themes amber renders as the theme's darker orange, same as the rest of the UI.
60
62
  - Infinite scroll pages older sessions. A "+" footer starts a new session.
63
+ - **Hover peek (collapsed list)**: resting the mouse on the collapsed rail for 300 ms slides the **full list** (header + rows) in over the chat to its left, at the saved width, without reflowing the chat; leaving closes it after a 200 ms grace. Touch never opens it. It stays open while a rename input or the delete confirm started from it is in progress, and selecting a row closes it. Collapsing by mouse click does not peek straight back open under the pointer until it leaves and re-enters; the peek header's expand button pins the list open.
61
64
  - **New session**: "+", `/session new` and `/clear` open a **draft tab**. A "New session" row sits on top while it is on screen, and an untouched draft is reused rather than duplicated. The session is created by the draft's first message or command. The previous session keeps streaming in its own tab.
62
65
  - **Background tabs**: one WS connection carries every loaded tab, so a session that isn't on screen keeps receiving its turn. After a reconnect only the tab on screen is reattached; the others reload when clicked. Live capture and face (`<<<SHOW>>>`) markers only act for the tab on screen.
63
66
  - **Delete (trash icon)**: deletes the session after a confirm dialog in the UI language ("Delete this session? Its history cannot be recovered." / its Chinese counterpart). Deletion is the REST delete first, then the WS one, so sub-session files go too. The trash icon, like the **rename** pencil, appears only on hover — the active tab included.
@@ -96,9 +99,22 @@ When `contextEnabled` is on (default), user messages are auto-prepended with:
96
99
  - Drag to chat input
97
100
  - Clipboard paste
98
101
  - File-picker button
102
+ - Screenshot button (below)
99
103
 
100
104
  Images ride along as base64; multimodal supported. Before sending, every attachment is downscaled (long edge ≤ 1568) and re-encoded as JPEG; an SVG is rasterized the same way (long edge 1024, on white), since vision input only takes jpeg / png / gif / webp. An image the browser can't decode (e.g. HEIC / TIFF in Chrome) and that isn't one of those four is refused when attached, with an inline notice naming the file — it is never sent only to be dropped server-side. Pasted images are also persisted to `<workspace>/.halo/assets/web/inbound/web/<date>/` so a `[图片已保存: /abs/path]` marker survives page reload and renders as a click-to-preview chip (shared with the WeChat channel's inbound media flow).
101
105
 
106
+ ### Screenshot (drag-select a region)
107
+ A Scissors button in the toolbar's input group (upload · **screenshot** · screen · camera) takes one still of the screen and lets the user crop it into an image attachment:
108
+ - Click → a frozen frame opens in a full-window crop layer over the admin (dark backdrop, area outside the selection dimmed). Drag a box (dragging again redraws it); **Enter**, double-click or ✓ confirms, **Esc** or ✕ cancels. Confirming with no box (or one under 8 px) attaches the whole frame. Enter here never sends the message.
109
+ - The crop is cut from the full-resolution frame and attached as `screenshot-YYYYMMDD-HHMMSS.png` — from then on it's an ordinary attachment (chip, X to remove, downscaled to JPEG on send like any upload).
110
+ - Grey at rest, primary colour while the picker / crop layer is open; clicking it then cancels.
111
+ - Frame source differs by client:
112
+ - **Desktop client** (mac / win): no picker — grabs the display the Halo window is on (only that one on multi-monitor), Halo itself visible in the shot. Needs macOS **Screen Recording**; without it the permission hint shows in the attachment-notice row.
113
+ - **Browser**: the browser's own `getDisplayMedia` picker every time (a page can't read the screen otherwise), one frame, stream stopped right away. Picking "Entire screen" includes the browser's own "sharing" bar — crop it out. Cancelling the picker does nothing.
114
+ - An older desktop client without the screenshot bridge falls back to the browser path (Halo's own source picker).
115
+ - Independent of live capture: never reads or changes a bound screen share.
116
+ - Shown only when a frame source exists and the selected agent's model accepts images — same gate as live capture (no button on mobile browsers).
117
+
102
118
  ### Inline media chips
103
119
  Any message containing `[图片/视频/语音/文件 已保存: /path]` markers (WeChat + web) or a leading `MEDIA: /path` line (agent-emitted, e.g. from `wechat-send`) renders a compact chip with filename + icon. Clicking opens a full-size preview modal (image/video/audio inline, file → download link). The modal has a Download button (top-right, next to close) for image/video/audio; the media URL carries a per-open cache-buster (`&t=<timestamp>`) so overwritten files (same path, new bytes) always show current content. Paths inside the active workspace or under the OS temp dir (`/tmp/`) are previewable; everything else degrades to a non-clickable chip.
104
120
 
@@ -6,7 +6,7 @@ Unified command processing — all slash commands (built-in + skill) are dispatc
6
6
 
7
7
  Source: [commands/index.ts](../../../packages/server/src/commands/index.ts) (descriptors) + [channels/shared/commands.ts](../../../packages/server/src/channels/shared/commands.ts) (execution)
8
8
 
9
- Top-level built-in slash commands: `/help` `/evo` `/session` `/agent` `/skill` `/workspace`. (`/cron` and `/acp` look like top-level commands too but are provided by same-name skills — see the verb table below; they aren't registered in `commands/index.ts` or `DISPATCH_COMMANDS`.)
9
+ Top-level built-in slash commands: `/help` `/evo` `/session` `/agent` `/skill` `/workspace`. (`/cron` looks like a top-level command too but is provided by a same-name skill — see the verb table below; it isn't registered in `commands/index.ts` or `DISPATCH_COMMANDS`.)
10
10
 
11
11
  | Name | Slash | Type | Purpose |
12
12
  |---|---|---|---|
@@ -22,7 +22,6 @@ The rest are noun-verb **object commands**: `/<obj> <verb> [args]`. Some verbs a
22
22
  | `/skill` | list / desc (built-in, no gate) · disable / enable (built-in, workspace) · delete (built-in, full) · create / update (skill verb, full) |
23
23
  | `/workspace` | info (built-in, no gate) · switch (built-in, full) · setup / tidy (skill verb, workspace) · share (skill verb, full) — `/w` is a built-in alias (see [Command aliases](#command-aliases)) |
24
24
  | `/cron` | create / list / update / enable / disable / delete — all skill verbs, full |
25
- | `/acp` | kiro / claude (ask a local agent directly; question = rest of line) · add / list / remove (manage generated ask-* bindings) — all full |
26
25
 
27
26
  Session lifecycle actions (`subscribe` / `unsubscribe`, `session:delete`) are handled inline by the WS handler, not as slash commands. In the admin, a bare `/session new` (and `/clear`) is handled client-side: it opens a new draft chat tab and sends nothing; `/session new <args>` still goes to the server. The WS `session:clear` frame was removed in 1.5.3-alpha.
28
27
 
@@ -63,7 +63,8 @@ File types without a built-in plugin can be handled by an **installed extension*
63
63
  - **Fallback**: a file with no built-in plugin and no installed extension shows a static page — "no built-in preview for this type; extensions for more file types are on <hub> ↗", where `<hub>` is the extension skill's `hub_repo` (default halo-hub; a local path / ssh remote shows as text) — plus Open as text / Download. No online lookup
64
64
  - **Editable extensions** (manifest `capabilities: ["save"]`): the iframe reports dirty → tab shows the dirty dot → header **Save** button (Ctrl/Cmd+S is *not* wired for extension tabs — Monaco isn't mounted) → `PUT /api/files/raw`. If the file changed on disk since it was loaded the save gets a 409 and the user is asked "changed on disk … Overwrite?". An external change while the tab is clean reloads it; while dirty, local edits are kept silently and the conflict surfaces at the next save
65
65
  - **Export from an editable extension** (e.g. draw.io *File ▸ Export as*, Excalidraw main menu *Export PNG / SVG*): the extension iframe can't download files, so the exported image is saved **into the workspace, next to the open file** (`flow.drawio` → `flow.png`). If that file already exists the user is asked whether to overwrite it. On success a banner "Exported to <path>" appears above the preview with a **Download** link. Exporting never marks the tab modified and never changes the open file. Only on Halo builds newer than 1.5.9; on older hosts the extension shows no working export (drawio ≥ 1.1.0 / excalidraw ≥ 1.1.0 hide or fall back on their own)
66
- - **Bundle extensions** (manifest `bundle: true`, e.g. the `htrans` meeting recorder): a *directory* named with the extension's suffix (`Standup.htrans/`) is one document. In the Explorer it shows a package icon; clicking the row (or Enter) opens it as a tab, and only its chevron expands it so the files inside stay browsable. Right-click → Open to the Side works on it. **New File** with a bundle suffix (`Standup.htrans`) creates the directory and opens it. The extension reads and writes only inside that directory. While it reports busy (e.g. recording) the tab shows the dirty dot, asks before closing, and stays mounted when you switch tabs. Ctrl/Cmd+S / Save do nothing on it. A bundle tab survives a reload while the extension is installed and supported here; otherwise the tab is dropped and the directory is an ordinary folder again
66
+ - **Bundle extensions** (manifest `bundle: true`, e.g. the `htrans` meeting recorder): a *directory* named with the extension's suffix (`Standup.htrans/`) is one document. In the Explorer it shows a package icon; clicking the row (or Enter) opens it as a tab, and only its chevron expands it so the files inside stay browsable. Right-click → Open to the Side works on it. **New File** with a bundle suffix (`Standup.htrans`) creates the directory and opens it — from the file tree's inline New File and from the Explorer header's New File button alike. The extension reads and writes only inside that directory. While it reports busy (e.g. recording) the tab shows the dirty dot, asks before closing, and stays mounted when you switch tabs. Ctrl/Cmd+S / Save do nothing on it. A bundle tab survives a reload while the extension is installed and supported here; otherwise the tab is dropped and the directory is an ordinary folder again
67
+ - **File access beyond the open file** (manifest capability `fs-read`; Settings → Extensions tags it "Reads files on this machine"): a viewer such as an emulator or model viewer may read other files in the workspace or anywhere on the server machine (never write), and ask the user to choose one. The choice is made in a **file picker** the viewer opens on demand: Workspace and Home shortcuts, an editable path box, a breadcrumb, folders first, only files matching the viewer's suffix filter; arrow keys / Enter / Backspace to navigate, Esc or Cancel to close it. A file inside the workspace is handed back as a workspace-relative path, any other as an absolute one. Halo's own secrets (`~/.halo/secrets`, `~/.ssh`, git credentials, the global databases — the same paths the agent sandbox hides) never show up in the picker and can't be read this way, symlinks included
67
68
  - **Platforms** (manifest `platforms`: `web` / `desktop-mac` / `desktop-win` / `desktop-linux`; omitted = everywhere): on an excluded platform (the Halo desktop app is detected by its user agent) the extension acts as if it were not installed. The fallback page says `"<name>" only runs on: <platforms>`, and Settings → Extensions shows a platforms badge plus a gray "not supported here" tag. Manifest capability `media` lets the extension use the microphone and screen capture
68
69
  - **Install sources**: Settings → Extensions zip upload; `/extension install <id|zip|url>` (listed by the built-in `default` agent, full access), where an id comes from the hub in Settings → Skills → extension → `hub_repo` (default halo-hub; GitHub / Gitea / Forgejo / GitLab release zips, or tags of any other git repo); or a manual directory drop
69
70
  - **Live install / upgrade / uninstall** (from Settings → Extensions upload / remove, the `/extension` skill, or a manual directory change): pushed over WS, no reload. An upgraded extension remounts open tabs (or shows a "updated — reload" banner if the tab is dirty); uninstalling drops clean tabs to the fallback page and keeps dirty tabs alive with a banner
@@ -77,7 +78,7 @@ Markdown and HTML open as text in Monaco *and* have a rendered view — Canvas d
77
78
  - Toggle to Edit → Monaco source, Cmd+S saves as usual
78
79
 
79
80
  ### Preview caching (MRU)
80
- Recently opened preview tabs stay mounted (up to 5, MRU) so switching between them doesn't re-fetch or re-parse. Plugins flagged `heavy: true` bypass the cache — only the active instance mounts, others unmount; switching between two heavy tabs of the same type (e.g. two `.ipynb`) remounts the viewer, so it shows the newly active file. An extension tab with unsaved edits stays pinned beyond the 5. Closing a preview tab removes it from the cache immediately (aborting any in-flight fetch).
81
+ Recently opened preview tabs stay mounted (up to 5, MRU) so switching between them doesn't re-fetch or re-parse. Plugins flagged `heavy: true` bypass the cache — only the active instance mounts, others unmount; switching between two heavy tabs of the same type (e.g. two `.ipynb`) remounts the viewer, so it shows the newly active file. An extension tab with unsaved edits stays pinned beyond the 5. Cached previews keep a fixed (path-sorted) order in the DOM rather than moving with recency, so switching tabs never reloads an extension iframe — a running emulator keeps its state. Closing a preview tab removes it from the cache immediately (aborting any in-flight fetch).
81
82
 
82
83
  ### File metadata in header
83
84
  The Canvas header shows `(size · Created … · Modified …)` for the active tab. Both text tabs and preview tabs populate this — preview tabs fetch `GET /api/files/stat` on open (and on tab restore from localStorage) since they never read content.
@@ -90,6 +91,11 @@ The header has a maximize button on the far right. Toggling it expands Canvas to
90
91
 
91
92
  Canvas (including Monaco instances, file tree, and open tab contents) stays mounted across activity-tab switches and maximize toggles — switching away and back does **not** reload content.
92
93
 
94
+ **Immersive mode for viewer extensions.** When the focused pane's active tab is an extension viewer without `save` (a bundle counts), maximize goes further: the viewer fills the whole viewport and the Canvas header, tab bar, other pane and floating chat panel are hidden. A maximize click also enters real fullscreen (a maximize restored after a reload stays in-page). The viewer keeps running — nothing reloads.
95
+ - **Exit**: `Esc` (including while focus is inside the viewer), or the **× Exit · Esc** pill at the top — on mouse it appears for a moment on entering and whenever the pointer moves to the top edge; on touch it stays on screen as a small × at the top-left, and a tap near the top widens it. A viewer's own fullscreen (e.g. an emulator's) takes the first `Esc`.
96
+ - **Chat replies while immersive**: when an agent finishes a reply the chat can't be seen, so a toast "Agent replied · <session title>" (or "N sessions replied") appears at the top right for ~8 s with **Back to chat**, which leaves immersive, opens that session and puts the caret in the chat input.
97
+ - Editable extensions (`save`) and ordinary files keep the plain maximize described above.
98
+
93
99
  ## Shortcuts
94
100
 
95
101
  | Shortcut | Action |
@@ -97,4 +103,4 @@ Canvas (including Monaco instances, file tree, and open tab contents) stays moun
97
103
  | Cmd/Ctrl + P | Quick Open (fuzzy file search) |
98
104
  | Cmd/Ctrl + S | Save current tab |
99
105
  | Alt + W | Close current tab (Cmd+W cannot be overridden in browsers; the desktop app uses Cmd/Ctrl + W, and closes the window when no tab is open) |
100
- | Esc | Exit maximized Canvas (when not inside an input / Monaco / Quick Open) |
106
+ | Esc | Exit maximized Canvas (when not inside an input / Monaco / Quick Open); also exits the immersive viewer, including from inside it |
@@ -21,6 +21,7 @@ Desktop app only — the browser has one tab per window natively.
21
21
  - **Per-window workspace**: windows share the server origin, hence one localStorage, so a window tracks its own workspace via its URL `?folder=` (not localStorage, which would clobber across windows). Different windows can sit on different workspaces; the same workspace can be open in two windows.
22
22
  - **Quit semantics** (platform split): on macOS, closing every window keeps the app in the Dock (only `Cmd+Q` truly exits) and clicking the Dock icon reopens a window; on Windows/Linux there's no Dock to resummon from, so closing the last window quits.
23
23
  - **Quick toggles entry** (bottom of the activity bar, below Settings; browser and desktop): a gear with a thin segmented status bar under it — one segment per available item, in fixed order network → notify → pin → keep-awake (browser: network + notify, or network alone without the Web Notification API; desktop: all four). Network segment: emerald = fresh, pulsing amber = stale, red = down (the gear itself also turns red when down); notify / pin segments: primary when on; keep-awake: amber when on (it stops the screen locking, so it gets its own colour); off = dimmed muted grey. The native hover title lists every item's state. Clicking opens a panel (titled "Status & window") to the right of the entry: a read-only network row, then one switch row per toggle; switches apply immediately and the panel stays open; outside click, Esc or a second entry click closes it. Segments, rows and the hover summary all come from one item list (`useQuickToggleItems` in `features/workspace/quick-toggles.tsx`).
24
+ - **Right-click menu for page text**: Electron shows no menu by default, so the shell adds one where a browser would show its native menu — in an editable field (chat input, settings fields): Cut / Copy / Paste / Select all; on selected non-editable text: Copy; elsewhere: none. Menus the admin draws itself (file tree, editor tabs, terminal, Monaco) suppress it, so there's never a double menu. Labels follow the OS locale (zh → Chinese). Lives in `packages/desktop/src/main.cjs` (`buildContextMenuTemplate`), so it ships with the next desktop build.
24
25
  - **Pin (always-on-top)** acts per-window — each window pins independently.
25
26
  - **Keep screen awake** (desktop only) acts per-window and isn't persisted — a restart or a new window starts off, a reload keeps it. While any opted-in window is neither minimized nor hidden (merely covered by other windows still counts), the app holds one display-sleep blocker, so the screen won't dim or lock when you step away.
26
27
  - **Activity awareness**: while the agent streams a reply the sidebar's workspace name carries an amber pulsing dot (emerald when idle) and the window title gains a `●` prefix (`● Halo — <name>` busy vs `Halo — <name>` idle) — both are plain web behaviour that Electron mirrors to the native title bar.
@@ -47,6 +47,19 @@ When the WS drops, the bottom panel remounts `TerminalPanel` (fresh key on `_dis
47
47
  - Terminal type: `xterm-256color`
48
48
  - Default size: 80 × 24 (resize requests override)
49
49
  - Strips `npm_config_prefix` env (avoids nvm warnings when starting node)
50
+ - Font: JetBrains Mono 13px, line height 1 (no extra leading — box-drawing / TUI rows join up)
51
+
52
+ ### Copy & paste
53
+ Keys depend on the client's OS (the keyboard in front of the user — the PTY is always the server's):
54
+
55
+ | Client | Copy | Paste |
56
+ |---|---|---|
57
+ | macOS | `Cmd+C` | `Cmd+V` |
58
+ | Windows / Linux | `Ctrl+C` **with a selection** (copies and clears the selection; no selection → `^C` / SIGINT as usual) · `Ctrl+Shift+C` | `Ctrl+V` · `Ctrl+Shift+V` (literal `^V` is given up) |
59
+
60
+ - Keyboard copy / paste ride the browser's native copy / paste events (paste stays bracketed), so they work on a plain-http origin too
61
+ - **Right-click menu** (inside the terminal): Copy (disabled with no selection) · Paste · Select all · Clear. Menu copy / paste use the async Clipboard API; when paste is refused (plain-http origin or no clipboard-read permission) the menu says to use the keyboard shortcut instead
62
+ - Key mapping: `terminalClipboardKey` in [packages/admin/src/features/terminal/terminal-clipboard.ts](../../../packages/admin/src/features/terminal/terminal-clipboard.ts); menu in `terminal-context-menu.tsx`
50
63
 
51
64
  ### Lifecycle
52
65
 
@@ -111,6 +124,8 @@ Defined in [packages/server/src/config.ts](../../../packages/server/src/config.t
111
124
  | T6 | Open 3 tabs, close 1 explicitly | Other 2 keep their PTYs; closed one gets `terminal:exit` |
112
125
  | T7 | `exit` from within shell | `terminal:exit` with the shell's exit code; the terminal prints `[Process exited]` and its tab stays until closed |
113
126
  | T8 | Paste a 10 KB block | Sent as `terminal:input` without choking; shell echoes in chunks |
127
+ | T9 | Windows/Linux: select text → `Ctrl+C`; then `Ctrl+C` again with nothing selected | First copies and clears the selection; second sends `^C` (interrupts a running `sleep 30`) |
128
+ | T10 | Right-click in the terminal → Paste | Clipboard text inserted at the prompt (or the "use the shortcut" hint on a plain-http origin) |
114
129
 
115
130
  Follows the pattern of [test/session.md](../test/session.md).
116
131
 
@@ -44,6 +44,12 @@ The Source Control entry is hidden in workspaces that are not git repos.
44
44
 
45
45
  Below Settings sits the quick-toggles entry (network status, finish notification, and on desktop pin / keep-screen-awake) — see [explorer.md → Desktop windows](explorer.md#desktop-windows-multi-window).
46
46
 
47
+ ### Hover drawer
48
+ The bar is icon-only (48px) at rest. Resting the **mouse** on it for 300 ms widens it into a 240px drawer that shows each view's name plus a one-line description (the quick-toggles entry too); leaving closes it after a 200 ms grace, and moving back in within that grace keeps it open. The view icons no longer carry native tooltips.
49
+ - **No reflow**: the bar is an absolute layer over a fixed 48px slot, so the drawer overlays the sidebar / main area instead of pushing them.
50
+ - **Mouse only**: touch and pen never open it, and neither do touch-primary devices — a tap still just switches view.
51
+ - **Stays open** while the quick-toggles panel (anchored to the drawer's right edge) is up. **Esc** or picking a view closes it.
52
+
47
53
  ## Resizable panels
48
54
  - **Sidebar width**: drag between sidebar and main content
49
55
  - **Bottom panel height**: drag between main content and bottom panel