@turmind/halo 1.5.4 → 1.5.5

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 (75) hide show
  1. package/admin-out/404.html +1 -1
  2. package/admin-out/_next/static/chunks/{133-33aa091fba853037.js → 422-cc74b1fdd3af5415.js} +1 -1
  3. package/admin-out/_next/static/chunks/989-bc743317498d3fbc.js +1 -0
  4. package/admin-out/_next/static/chunks/app/page-0a390059f1914664.js +1 -0
  5. package/admin-out/_next/static/chunks/{webpack-1a264efd965db7ab.js → webpack-edf248e07333566d.js} +1 -1
  6. package/admin-out/_next/static/css/b53542687ebd4c65.css +3 -0
  7. package/admin-out/index.html +1 -1
  8. package/admin-out/index.txt +5 -5
  9. package/bundled-docs/design/agent.md +4 -4
  10. package/bundled-docs/design/agentcore.md +13 -10
  11. package/bundled-docs/design/architecture.md +24 -14
  12. package/bundled-docs/design/command.md +5 -5
  13. package/bundled-docs/design/cron.md +2 -2
  14. package/bundled-docs/design/evolution.md +34 -35
  15. package/bundled-docs/design/express-self.md +6 -4
  16. package/bundled-docs/design/feishu.md +18 -14
  17. package/bundled-docs/design/halo-city.md +5 -4
  18. package/bundled-docs/design/observability.md +9 -9
  19. package/bundled-docs/design/prompt-system.md +12 -12
  20. package/bundled-docs/design/session.md +12 -12
  21. package/bundled-docs/design/slack.md +22 -20
  22. package/bundled-docs/design/storage.md +18 -19
  23. package/bundled-docs/design/telegram.md +26 -8
  24. package/bundled-docs/design/web.md +3 -1
  25. package/bundled-docs/design/wechat.md +8 -6
  26. package/bundled-docs/design/wecom.md +23 -9
  27. package/bundled-docs/design/ws.md +6 -6
  28. package/bundled-docs/dev/acp-adapter.md +8 -8
  29. package/bundled-docs/dev/add-channel.md +12 -9
  30. package/bundled-docs/dev/add-model-provider.md +31 -20
  31. package/bundled-docs/dev/api.md +83 -57
  32. package/bundled-docs/dev/deploy.md +8 -6
  33. package/bundled-docs/dev/env.md +16 -11
  34. package/bundled-docs/dev/logging.md +15 -29
  35. package/bundled-docs/dev/previews.md +20 -11
  36. package/bundled-docs/dev/tools.md +24 -12
  37. package/bundled-docs/guide/README.md +1 -0
  38. package/bundled-docs/guide/agents.md +19 -53
  39. package/bundled-docs/guide/channels/README.md +7 -4
  40. package/bundled-docs/guide/channels/acp.md +3 -1
  41. package/bundled-docs/guide/channels/feishu.md +13 -11
  42. package/bundled-docs/guide/channels/slack.md +12 -10
  43. package/bundled-docs/guide/channels/telegram.md +21 -27
  44. package/bundled-docs/guide/channels/web.md +15 -11
  45. package/bundled-docs/guide/channels/wechat.md +15 -14
  46. package/bundled-docs/guide/channels/wecom.md +7 -2
  47. package/bundled-docs/guide/chat.md +10 -9
  48. package/bundled-docs/guide/cli.md +10 -8
  49. package/bundled-docs/guide/getting-started.md +3 -3
  50. package/bundled-docs/guide/secrets-and-credentials.md +3 -3
  51. package/bundled-docs/guide/sessions.md +3 -3
  52. package/bundled-docs/guide/skills.md +8 -6
  53. package/bundled-docs/guide/testing-agents-and-skills.md +14 -15
  54. package/bundled-docs/guide/workspace.md +27 -15
  55. package/bundled-docs/requirements/agent.md +19 -18
  56. package/bundled-docs/requirements/chat.md +16 -17
  57. package/bundled-docs/requirements/command.md +10 -10
  58. package/bundled-docs/requirements/editor.md +10 -9
  59. package/bundled-docs/requirements/explorer.md +10 -7
  60. package/bundled-docs/requirements/session.md +16 -17
  61. package/bundled-docs/requirements/settings.md +18 -14
  62. package/bundled-docs/requirements/skill.md +4 -4
  63. package/bundled-docs/requirements/source-control.md +4 -3
  64. package/bundled-docs/requirements/terminal.md +13 -13
  65. package/bundled-docs/requirements/workspace.md +19 -13
  66. package/dist/index.js +2038 -3558
  67. package/package.json +1 -1
  68. package/templates/config.yaml +6 -0
  69. package/templates/skills/workspace/share.md +11 -3
  70. package/templates/skills/workspace/stage.py +85 -5
  71. package/admin-out/_next/static/chunks/989-92e9683ae8d39750.js +0 -1
  72. package/admin-out/_next/static/chunks/app/page-7a635969198fc62b.js +0 -1
  73. package/admin-out/_next/static/css/4ea5f674146e54d8.css +0 -3
  74. /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → cuVcldfdg1PXevaQMQSzb}/_buildManifest.js +0 -0
  75. /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → cuVcldfdg1PXevaQMQSzb}/_ssgManifest.js +0 -0
@@ -6,7 +6,7 @@ How to verify an agent you just created behaves right, and how to exercise a ski
6
6
 
7
7
  ### 1. Open the agent
8
8
 
9
- Activity Bar → `👥 Agents` → click the agent you want to test.
9
+ Activity Bar → `🤖 Agents` → click the agent you want to test.
10
10
 
11
11
  ### 2. Click Test
12
12
 
@@ -32,21 +32,20 @@ The Explorer's Chat panel is now ready with your agent pre-selected. Send a mess
32
32
 
33
33
  - **Does it greet the way AGENT.md says?** Send "Hi". Check the reply matches the personality you wrote.
34
34
  - **Does it call the right tools?** If the agent is supposed to read files, ask it to. Watch the inline tool-call card — it shows the tool name, arguments, and (expandable) output.
35
- - **Is the system prompt what you expect?** Open the Sessions panel (Activity Bar → `🕘 Sessions`), select this session, toggle **Debug mode**, click the **Prompt** button on the first message. Full rendered system prompt (AGENT.md + USER.md + INSTRUCTIONS.md + skill metadata + tool list).
35
+ - **Is the system prompt what you expect?** Open the Sessions panel (Activity Bar → `📨 Sessions`), select this session, toggle **Debug mode**, click the **Prompt** button in the viewer header. Full rendered system prompt (AGENT.md + USER.md + INSTRUCTIONS.md + skill metadata + tool list).
36
36
  - **Is the model correct?** Debug mode's usage badges show `model` on every assistant turn.
37
37
 
38
38
  ### 5. Iterate
39
39
 
40
- Edits take effect on the **next** session, not retroactively:
40
+ Edits are never retroactive — the turn that is running finishes on the old config — but a session is rebuilt from disk after every run, so the **next turn** of an existing session already picks them up:
41
41
 
42
42
  | What you changed | When it takes effect |
43
43
  |---|---|
44
- | `AGENT.md` body | Next `/session new` session |
45
- | `agent.yaml` tools / skills / model | Next session spawn |
46
- | `settings.yaml` values | Next session spawn (or next `activate_skill` call for SKILL bodies) |
47
- | Env vars (`<<ENV>>` placeholders) | Restart server, then next session spawn |
44
+ | `AGENT.md` body, `agent.yaml` tools / skills / model | Next turn (and any new session) |
45
+ | `settings.yaml` values | Next turn (or next `activate_skill` call for SKILL bodies) |
46
+ | Env vars (`<<ENV>>` placeholders) | Restart server, then next turn |
48
47
 
49
- So the loop is: edit → `/session new` → re-test. No server restart for MD/YAML/settings changes.
48
+ So the loop is: edit → send another message (or `/session new` for a clean slate) → re-test. No server restart for MD/YAML/settings changes. Full table in [delegation-and-access.md](delegation-and-access.md#2-when-a-config-edit-takes-effect).
50
49
 
51
50
  ---
52
51
 
@@ -56,7 +55,7 @@ There is **no separate "test this skill" button**. Skills exist to be called by
56
55
 
57
56
  ### 1. Attach the skill to an agent
58
57
 
59
- In the agent's Form view, the **Skills** section lists every skill in the workspace. Check the one you want to test. Save.
58
+ In the agent's Form view, the **Skills** section lists every skill in the workspace. Check the one you want to test (the form auto-saves).
60
59
 
61
60
  Equivalent YAML edit:
62
61
 
@@ -114,22 +113,22 @@ The env var is unset. Halo renders the literal placeholder so you (and the agent
114
113
 
115
114
  ### Saved agent changes don't seem to apply
116
115
 
117
- You might be looking at a session that started *before* the edit. Every session caches its own assembled system prompt at spawn time. `/session new` to start fresh, or switch to a different session and back.
116
+ You might be looking at a turn that was already running when you saved — it finishes on the old config, and the edit applies from the next turn. If the next turn still behaves the old way, `/session new` to start fresh.
118
117
 
119
118
  ### Deleting a test session
120
119
 
121
- Sessions → select → context menu → Delete. Or programmatically: `DELETE /api/sessions/logs/:id`. Deletion cascades to child sessions (sub-agents), their JSON files, and the SQLite rows. See [dev/api.md#session-logs](../dev/api.md#session-logs-unified).
120
+ Sessions → hover the session → trash icon. Or programmatically: `DELETE /api/sessions/logs/:id`. Deletion cascades to child sessions (sub-agents), their JSON files, and the SQLite rows. See [dev/api.md#session-logs](../dev/api.md#session-logs-unified).
122
121
 
123
122
  ### "Test button does something weird" — the session is orphaned
124
123
 
125
- Since 1.5.3-alpha, Test no longer clears any stored session: it only opens a new draft tab (the old `localStorage.halo_session_<projectId>` key is gone). The new session is created by the first message you send from that tab. If you see an old conversation instead of an empty one, the draft tab isn't the one on screen. Click **+** at the bottom of the session tabs to bring it back (an unused draft is reused).
124
+ Since 1.5.3-alpha, Test no longer clears any stored session: it only opens a new draft tab (the old `localStorage.halo_session_<projectId>` key is gone). The new session is created by the first message you send from that tab. If you see an old conversation instead of an empty one, the draft tab isn't the one on screen. Click **+** below the session tabs to bring it back (an unused draft is reused).
126
125
 
127
126
  ---
128
127
 
129
128
  ## References
130
129
 
131
130
  - Test button implementation: `testAgent` in [packages/admin/src/features/agents/agent-management-main.tsx](../../../packages/admin/src/features/agents/agent-management-main.tsx)
132
- - `activate_skill` runtime: [packages/server/src/agents/agent-loader.ts:111-159](../../../packages/server/src/agents/agent-loader.ts#L111-L159) (function `createSkillTool`)
133
- - Skill metadata → system prompt: [packages/server/src/agents/agent-loader.ts:97-102](../../../packages/server/src/agents/agent-loader.ts#L97-L102) (function `buildSkillPrompt`)
134
- - Chat session creation on first message: [packages/server/src/ws/handler.ts:263-344](../../../packages/server/src/ws/handler.ts#L263-L344) (function `handleChat`)
131
+ - `activate_skill` runtime: [packages/server/src/agents/agent-loader.ts](../../../packages/server/src/agents/agent-loader.ts) (function `createSkillTool`)
132
+ - Skill metadata → system prompt: [packages/server/src/agents/agent-loader.ts](../../../packages/server/src/agents/agent-loader.ts) (function `buildSkillPrompt`)
133
+ - Chat session creation on first message: [packages/server/src/ws/handler.ts](../../../packages/server/src/ws/handler.ts) (function `handleChat`)
135
134
  - End-to-end test scenarios for the session system: [test/session.md](../test/session.md)
@@ -21,17 +21,23 @@ Overall interface layout plus the three sidebar features.
21
21
  - **Sidebar** is resizable
22
22
  - **Bottom Panel** is resizable; the header has a Chat / Terminal switch
23
23
 
24
- The Skills tab is an exception — it takes the full right side, no Bottom Panel.
24
+ The docked Bottom Panel belongs to the Explorer tab; every other tab fills the whole right side (a floating panel stays visible on any tab).
25
25
 
26
26
  ## Activity Bar tabs
27
27
 
28
28
  | Icon | Tab | Purpose |
29
29
  |---|---|---|
30
30
  | 📄 Files | Explorer | File tree + Monaco editor |
31
+ | 🌿 Branch | Source Control | Changes, commit, push, history (hidden for non-git workspaces) |
31
32
  | 📨 Messages | Sessions | Session history + debug viewer |
32
- | 🪄 Wand | Skills | Skill editing (mini workspace) |
33
- | 👥 Users | Agents | Agent configuration (Form/YAML/MD) |
34
- | ⚙️ Gear | Settings | Settings editor (Form/YAML) |
33
+ | ⚡ Zap | Skills | Skill editing (mini workspace) |
34
+ | 🤖 Bot | Agents | Agent configuration (Form / Edit) |
35
+ | 💬 Chat bubble | Channels | Web / Telegram / Slack / Feishu / WeCom / WeChat accounts |
36
+ | ✨ Sparkles | Evolution | Self-evolution runs and review |
37
+ | 🕐 Clock | Cron | Scheduled agent runs |
38
+ | 🎚️ Sliders | Settings | Settings form (bottom of the bar) |
39
+
40
+ Below Settings sits the **quick toggles** gear. The thin bar under it has one segment per item: network (green = connected, amber = probing, red = disconnected; the gear turns red too), finish notification, and in the desktop app also pin-on-top and keep-screen-awake (amber when on). Hover for a summary. Click to open a panel where you switch them; it stays open until you click outside, press Esc, or click the gear again.
35
41
 
36
42
  ## Switching workspace
37
43
 
@@ -44,29 +50,30 @@ The Explorer top bar has a path input + 📁🔍 picker button:
44
50
  - Single click to select / double click to enter
45
51
  - Open button to confirm
46
52
 
47
- The URL carries `?folder=/abs/path`, persisting the workspace across refreshes. When `folder` is absent, Halo falls back to the home directory.
53
+ The URL carries `?folder=/abs/path`, persisting the workspace across refreshes. When `folder` is absent, Halo reopens the last workspace you used, or the home directory if there isn't one.
48
54
 
49
55
  ## Explorer (file tree)
50
56
 
51
57
  VS Code style:
52
- - Click: select
53
- - Double click: open in a new Tab
58
+ - Click a file: select, then open it after a short delay (300 ms)
59
+ - Double click: open the file right away; on a folder, expand / collapse it
60
+ - Click a folder: expand / collapse
54
61
  - Ctrl/Cmd + click: toggle multi-select
55
62
  - Shift + click: range select
56
63
  - Drag: move files/folders (multi-select drag supported)
57
64
 
58
- Right-click menu: New File / New Folder / Rename / Delete / Download.
65
+ Right-click menu: New File / New Folder / Open in Integrated Terminal / Reveal in File Manager (desktop app only) / Open to the Side / Download / Rename / Delete / Open as Workspace (folders).
59
66
 
60
67
  **Quick Open**: `Cmd/Ctrl+P` opens fuzzy file search — server-side, full-project, independent of expansion state.
61
68
 
62
69
  ### Skipped directories
63
- `node_modules`, `__pycache__`, `.halo` (knowledge dir, not shown in the tree; use `file_read` directly).
70
+ `.git`, `.DS_Store`, `node_modules`, `__pycache__`.
64
71
 
65
72
  ## Editor
66
73
 
67
74
  Monaco, multi-tab:
68
75
  - Drag tabs to reorder
69
- - Unsaved tabs show a red dot
76
+ - Unsaved tabs show an amber dot
70
77
  - Closing an unsaved tab prompts for confirmation
71
78
  - **Auto-refresh**: detects agent-written file changes by mtime and updates contents
72
79
  - **Tab persistence**: stored in localStorage, survives refreshes
@@ -91,13 +98,13 @@ Once installed, matching files open in the extension's viewer. If more than one
91
98
 
92
99
  Bottom-panel Terminal tab:
93
100
  - xterm.js frontend + node-pty backend
94
- - Multi-instance (each tab owns its own PTY)
101
+ - Multi-instance (each terminal owns its own PTY), listed as a collapsible vertical tab strip on the right
95
102
  - cwd = current project root
96
- - Reconnect grace: 60 s
103
+ - Reconnect grace: 5 min (`timeout.terminal_grace`)
97
104
 
98
105
  ## Settings
99
106
 
100
- `⚙️ Settings` tab. Form / YAML split view:
107
+ `⚙️ Settings` tab. Form view:
101
108
  - Auto-generated controls (text / number / password / toggle) per declared field
102
109
  - Each row shows source (`workspace` / `inherited from global` / `unset`) + Reset
103
110
 
@@ -105,7 +112,10 @@ Scope: Global / Workspace toggle. Same key: workspace overrides global, leaf by
105
112
 
106
113
  Sections are grouped by declarer:
107
114
  - **System** — server-built-in knobs (session limits, compaction, sandbox, logging)
115
+ - **Security** — change password, log out
116
+ - **Extensions** — installed canvas preview extensions
108
117
  - **Model Providers** — secrets declared by each `models/<id>.yaml` (AWS, Kimi, DeepSeek, …)
118
+ - **Agents** — params/secrets declared by each global agent's `agent-config.yaml`
109
119
  - **Skills** — params/secrets declared by each `skills/<id>/config.yaml`
110
120
  - **Orphans** — values in settings.yaml whose namespace isn't currently declared (uninstalled skill leftovers); manual cleanup only
111
121
 
@@ -127,11 +137,13 @@ See [requirements/settings.md](../requirements/settings.md), [secrets-and-creden
127
137
  |---|---|
128
138
  | `Cmd/Ctrl + P` | Quick Open |
129
139
  | `Cmd/Ctrl + S` | Save file |
130
- | `Cmd/Ctrl + W` | Close tab |
140
+ | `Alt + W` | Close editor tab (browser; the desktop app uses `Cmd/Ctrl + W`) |
141
+ | `Cmd/Ctrl + \`` | Switch the bottom panel between Chat and Terminal |
142
+ | `Esc` | Leave editor maximize |
131
143
 
132
144
  ## Bottom panel switching
133
145
 
134
- Two tabs in the header: Chat / Terminal. Current tab lives in `editorStore.bottomTab` and survives refresh.
146
+ Two tabs in the header: Chat / Terminal, plus maximize and float / dock buttons. The panel opens on Chat after a refresh.
135
147
 
136
148
  ## Login / logout
137
149
 
@@ -1,10 +1,10 @@
1
1
  # Agent — Requirements
2
2
 
3
- Agent configuration management: creating, editing YAML / Form / AGENT.md.
3
+ Agent configuration management: creating, editing via Form / mini workspace (agent.yaml, AGENT.md).
4
4
 
5
5
  ## Layout
6
6
 
7
- Left sidebar has two collapsible groups (Global / Workspace); right side is the editor:
7
+ Left sidebar has two collapsible groups (Global / Workspace — the latter only while a workspace is open); right side is the editor. Each group header has a `+` to create an agent in that scope, and the sidebar header has a refresh button:
8
8
 
9
9
  ```
10
10
  ┌─────────────────┬───────────────────────────────┐
@@ -20,22 +20,22 @@ Left sidebar has two collapsible groups (Global / Workspace); right side is the
20
20
 
21
21
  ## Core behaviour
22
22
 
23
- ### YAML / Form dual view
24
- - **YAML view**: Monaco-edit the raw `agent.yaml`
25
- - **Form view**: data-driven form derived from YAML fields (name / description / model / tools / skills / thinking)
26
- - Switching views preserves edits
23
+ ### Form / Edit dual view
24
+ - **Form view** (default): data-driven form derived from the YAML fields (name / description / priority / model incl. thinking and capabilities / system_prompt / team / tools / skills). Edits auto-save to `agent.yaml` (debounced 500 ms). If `agent.yaml` fails to load or parse, the form is replaced by an error with a Retry button and nothing is auto-saved (an unparsable file is fixed in Edit view); below the form, a read-only preview of `AGENT.md` is shown when it has content
25
+ - **Edit view**: the **Edit** button opens a mini workspace (file tree + editor) rooted at the agent's folder, for editing `agent.yaml` / `AGENT.md` directly; **Back** returns to the form, which reloads from disk
26
+ - The chosen view is remembered per agent in `localStorage`
27
27
 
28
28
  ### CRUD
29
29
  | Operation | API |
30
30
  |---|---|
31
31
  | List | `GET /api/agent-configs?projectId=xxx` |
32
32
  | Create | `POST /api/agent-configs` (body: `{name, description, scope, projectId?}`) |
33
- | Read YAML | `GET /api/agent-configs/:id/yaml` |
34
- | Write YAML | `PUT /api/agent-configs/:id/yaml` |
35
- | Delete | `DELETE /api/agent-configs/:id` |
33
+ | Read YAML | `GET /api/agent-configs/:id/yaml` (`scope=workspace` requires `projectId`, else 400) |
34
+ | Write YAML | `PUT /api/agent-configs/:id/yaml` (same `projectId` rule) |
35
+ | Delete | `DELETE /api/agent-configs/:id` (same `projectId` rule — never falls back to deleting the same-named global agent) |
36
36
  | Toggle disabled | `PATCH /api/agent-configs/:id/toggle` → `{ ok, disabled }` |
37
37
 
38
- Creating requires name + description; the backend uses `defaultAgentYaml(name, description)` to produce a full YAML.
38
+ Creating requires a name (description is optional); the id is derived from the name. The backend uses `defaultAgentYaml(name, description)` to produce a full YAML and also scaffolds an `AGENT.md`.
39
39
 
40
40
  ### Scope (Global / Workspace)
41
41
  - **Global**: `~/.halo/global/agents/<id>/agent.yaml` — shared across projects
@@ -45,18 +45,18 @@ Same id present in both scopes: workspace wins; the overridden global is greyed
45
45
 
46
46
  **Cross-scope conflict**: creating a same-name agent in the other scope prompts about the overwrite behaviour.
47
47
 
48
- **Delete protection**: at least one global agent must remain (server-enforced).
48
+ **Delete protection**: at least one global agent must remain (server-enforced). The sidebar offers delete (hover trash) on workspace agents only; global rows show a crown instead.
49
49
 
50
50
  ### Disable / Enable
51
- - Toggle switch on each agent row in the admin sidebar. Disabled state is stored per workspace in the `disabled_items` table of `halo.db` (not in agent.yaml). Both global and workspace agents can be independently toggled per workspace.
51
+ - Toggle switch on each agent row in the admin sidebar (shown on hover while enabled; not offered for internal agents or when no workspace is open). Disabled state is stored per workspace in the `disabled_items` table of `halo.db` (not in agent.yaml). Both global and workspace agents can be independently toggled per workspace.
52
52
  - Disabled agents are greyed out (opacity-40) with sub-text "disabled"; the toggle stays visible.
53
53
  - Hidden from: the delegation roster, chat agent selector, `/workspace share` export.
54
54
  - Still visible in the admin management sidebar for re-enabling.
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` / `file_write` / `file_edit` / `file_list` / `shell_exec` / `grep` / `glob` / `web_fetch`, returned by `GET /api/agent-configs/tools`
59
- - **`activate_skill`**: auto-injected whenever the YAML lists `skills`; loads the full SKILL.md on demand
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)
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)
62
62
  - Optional `team: [id, …]` field in `agent.yaml`. A **non-empty** list is the on/off switch for delegation: it grants the whole session-tool bundle (`start_session` / `query_agent` / …) plus the prompt roster, AND scopes which agents this one may reach.
@@ -67,7 +67,7 @@ Same id present in both scopes: workspace wins; the overridden global is greyed
67
67
  - **Picker only offers effective, enabled agents.** A chip shows iff the *effective* agent for that id is runnable — same resolve-then-check the runtime uses: a workspace agent shadows the same-id global (the overridden global is skipped), then anything disabled is dropped. So a global stays out of the picker when its workspace override is disabled, even though that global's own record isn't flagged disabled. Matches the chat / cron selectors and the runtime roster — you can never pick a teammate that can't actually run.
68
68
 
69
69
  ### Skill selection
70
- `GET /api/skills?projectId=xxx` lists available skills; `agent.yaml`'s `skills` references them by id.
70
+ `GET /api/skills?projectId=xxx` lists available skills; `agent.yaml`'s `skills` references them by id. The form offers enabled skills only; ids that are referenced but not installed show as red ⚠ chips (click to remove).
71
71
 
72
72
  ### MD file editing
73
73
 
@@ -75,13 +75,14 @@ Same id present in both scopes: workspace wins; the overridden global is greyed
75
75
  |---|---|---|
76
76
  | AGENT.md | yes | Agent personality / behaviour (overrides YAML `system_prompt`) |
77
77
  | INSTRUCTIONS.md | yes | User preferences (global or workspace scope) |
78
+ | INDEX.md | no | Project index, read-only through this API |
78
79
 
79
- The MD editor has a **Global / Workspace** scope toggle (visible when a workspace is open).
80
+ The admin edits `AGENT.md` through the Edit mini workspace; `INSTRUCTIONS.md` is not edited from this tab.
80
81
 
81
- API: `GET/PUT /api/agent-configs/:id/md/:fileType`
82
+ API: `GET/PUT /api/agent-configs/:id/md/:fileType`, `GET /api/agent-configs/:id/md-all`
82
83
 
83
84
  ### Test button
84
- - Sets `selectedAgentId` in the chat store
85
+ - Opens a fresh chat draft tab (an untouched draft is reused) with this agent selected
85
86
  - Dispatches a `halo:navigate` event to switch to explorer/chat
86
87
  - The user chats with the selected agent in the main chat panel
87
88
  - **Not rendered for internal agents** (`internal: true`) — they're delegated to by other agents, never driven directly
@@ -5,10 +5,9 @@ The primary surface for talking to an agent.
5
5
  ## Core behaviour
6
6
 
7
7
  ### Agent selection
8
- - Bottom-left dropdown in the chat panel selects which agent to use
9
- - Lists every available agent (from `GET /api/agent-configs`; overridden and disabled agents are hidden)
10
- - Default agent has a "default" badge
11
- - **Locked during an active session** — the agent is bound to the session; to change, start a new session (/session new)
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
+ - 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
+ - **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)
12
11
  - The Agents panel's "Test" button can also preselect an agent
13
12
 
14
13
  ### Access level
@@ -22,8 +21,8 @@ The primary surface for talking to an agent.
22
21
  ### Message rendering
23
22
  - Markdown + code-block highlighting
24
23
  - Tool-call card: expandable, shows tool name / input / output
25
- - Sub-agent messages carry the agent-name label (e.g. "Coder", "Researcher")
26
- - Streaming text has a cursor animation
24
+ - 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..."
27
26
  - 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
28
27
  - 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
29
28
 
@@ -39,7 +38,7 @@ Actions on user-role turns: the blue sticky user bubble, plus the sub-agent-repo
39
38
  - An expanded body is capped at 40vh and scrolls inside the bubble, because the bubbles are sticky.
40
39
  - The compact-summary callout is collapsed by default and toggles from its own header, as before.
41
40
  - **Copy** — copies the prompt text to the clipboard
42
- - **Delete** (confirm dialog) — removes the whole exchange (the user turn + all responses up to the next user turn) with **two-layer semantics**: the LLM context (`rawMessages`) drops the turn physically — the model never sees it again, freeing context; the UI keeps the messages, rendered greyed-out with a "deleted" tag, as an audit trail. No undo; a deleted exchange loses its Delete button. Rejected with an error toast while the agent is running or compacting. Root sessions only (sub-session logs don't offer Delete). If the turn was already compacted out of raw context, only the UI marking happens (silent degrade). Once a session has **archived history** the action is refused for the whole session with a plain explanatory notice (not an `Error:` bubble) — per-turn positions can no longer be mapped once older turns left the active file. Design details in [design/session.md](../design/session.md#exchange-deletion-soft-ui--hard-raw), protocol in [design/ws.md](../design/ws.md).
41
+ - **Delete** (confirm dialog) — removes the whole exchange (the user turn + all responses up to the next user turn) with **two-layer semantics**: the LLM context (`rawMessages`) drops the turn physically — the model never sees it again, freeing context; the UI keeps the messages, rendered greyed-out with a "deleted" tag, as an audit trail. No undo; a deleted exchange loses its Delete button. Rejected with an `Error:` message in the chat while the agent is running or compacting. Root sessions only (sub-session logs don't offer Delete). If the turn was already compacted out of raw context, only the UI marking happens (silent degrade). If a compact archived history while the panel was open, the action is refused with a plain explanatory notice (not an `Error:` bubble) — the panel's turn positions no longer match the active file; reopening the session re-anchors it and Delete works again. Design details in [design/session.md](../design/session.md#exchange-deletion-soft-ui--hard-raw), protocol in [design/ws.md](../design/ws.md).
43
42
 
44
43
  ### Archived history (scroll up to load)
45
44
  Long sessions get their older exchanges moved out of the active log on compact, so opening a session stays fast no matter how long it has run. Loading older messages is **two-tiered** — a local, no-network tier first, then the network-backed archive:
@@ -49,19 +48,19 @@ Long sessions get their older exchanges moved out of the active log on compact,
49
48
  - Only once the local window is fully expanded does the next scroll-to-top (or the "Load earlier messages" row that takes over the same slot) reach for the **network** tier: it loads one archived segment per gesture, oldest-newer order preserved, and becomes "No earlier messages" once everything is loaded. The two tiers can't be skipped or reordered — the archive row only appears once there's nothing left to expand locally
50
49
  - Either tier preserves the reading position when older messages prepend — the view doesn't jump
51
50
  - Loaded archive segments sit in a **collapsed** block ("Archived · N segment(s) · M messages") — the user scrolled up for older context, not to have hundreds of exchanges re-flow the view; expanding renders them like normal messages, growing **upward** so the viewport stays pinned to the newest end, with the expand/collapse toggle below the revealed content
52
- - Archived exchanges are **read-only**: Copy still works, Delete is absent (the server refuses it for archived sessions)
51
+ - Archived exchanges are **read-only**: Copy still works, Delete is absent (they are no longer in the active log, so a turn position can't address them)
53
52
  - Segments already loaded aren't re-fetched; a failed load can simply be retried with the same row
54
53
 
55
54
  ### Session tabs (right side)
56
- The workspace's root sessions as **Chrome-style vertical tabs** on the right edge of the chat panel (`VerticalTabList` rows inside a `ResizableSidebar`, default 200px). The list **is** the tab list: every session is "open", clicking a row shows it, and there is no close — deleting the session is the only way a tab goes away. Since 1.5.3-alpha this replaces the top tab strip, the History and New-session buttons beside the composer, and the "N previous sessions" link in the empty state.
55
+ The workspace's root sessions as **Chrome-style vertical tabs** on the right edge of the chat panel (`VerticalTabRow` rows inside a `ResizableSidebar`, default 200px). The list **is** the tab list: every session is "open", clicking a row shows it, and there is no close — deleting the session is the only way a tab goes away. Since 1.5.3-alpha this replaces the top tab strip, the History and New-session buttons beside the composer, and the "N previous sessions" link in the empty state.
57
56
 
58
57
  - **Resize / collapse**: drag the left edge to resize (120–480px). Collapse from the header; the collapsed list is a `w-10` rail of square tabs, each showing the title's first letter plus the status dot in a corner, with a "+" at the bottom. Both settings are global preferences in `localStorage` (`halo_session_sidebar_open`, `halo_session_sidebar_width`); the list is open by default.
59
- - **Rows**: one line per row, 🎯 + title. Message count, time-ago and model are in the tooltip. `N msgs` counts the session's user turns **over its whole lifetime**, so compacting or archiving history never makes it go backwards.
58
+ - **Rows**: one line per row, status dot + title. Message count, time-ago and model are in the tooltip. `N msgs` counts the session's user turns **over its whole lifetime**, so compacting or archiving history never makes it go backwards.
60
59
  - Each row leads with one fixed-size status dot (fixed so a state change never reflows a narrow list — the spinner it replaced did): **amber, pulsing** while a loaded tab's turn is running; **blue** 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. The list endpoint's `status` is not used.
61
60
  - Infinite scroll pages older sessions. A "+" footer starts a new session.
62
61
  - **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.
63
62
  - **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.
64
- - **Delete (✕)**: 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 ✕ is always visible on the active tab and appears on hover for the others; hover also reveals a **rename** pencil.
63
+ - **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.
65
64
  - **Inline rename**: pencil → input in place, Enter/blur commits (`PATCH /sessions/logs/:id`), Esc cancels; empty or unchanged title is a no-op. Other session-list consumers refresh via the `session:changed` WS push
66
65
  - **Persistence**: only the session on screen is remembered, per workspace, as `{active}` under `halo_chat_tabs_<projectId>`. A refresh restores and loads only that tab. The pre-tabs keys `halo_session_<projectId>` / `halo_session_id` are read once to seed it and then removed.
67
66
  - **Switch loading**: clicking a row that hasn't loaded yet shows a "Loading session…" state in the message area, cleared only when the server's `state:snapshot` for that exact sessionId arrives (empty sessions included). Past 30s it degrades to "Slow network — still loading…" plus a Retry button (re-subscribes) — slow ≠ failed, nothing aborts on its own. Switching to an already-loaded tab is instant, and its scroll position is restored
@@ -72,9 +71,9 @@ The full command list is fetched from `GET /api/commands` per session and includ
72
71
 
73
72
  | Command | Type | Purpose |
74
73
  |---|---|---|
75
- | `/help` | client | List available commands |
74
+ | `/help` | server | List available commands |
76
75
  | `/clear` | client | Alias for `/session new` (admin-UI shortcut, no server registration) |
77
- | `/session new` | server | Start a new session |
76
+ | `/session new` | server | Start a new session (the admin handles it client-side as a new draft tab, like `/clear`) |
78
77
  | `/session context` | server | Show context window usage, agent info |
79
78
  | `/session compact` | server | LLM-summary compact of the conversation |
80
79
 
@@ -93,7 +92,7 @@ When `contextEnabled` is on (default), user messages are auto-prepended with:
93
92
  - `[Currently viewing: path/to/file.ts]`
94
93
  - `[Selected text in file.ts:10-25]\n\`\`\`...\n\`\`\``
95
94
 
96
- ### File attachments
95
+ ### File attachments (images)
97
96
  - Drag to chat input
98
97
  - Clipboard paste
99
98
  - File-picker button
@@ -101,7 +100,7 @@ When `contextEnabled` is on (default), user messages are auto-prepended with:
101
100
  Images ride along as base64; multimodal supported. 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).
102
101
 
103
102
  ### Inline media chips
104
- 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 `/tmp/` are previewable; everything else degrades to a non-clickable chip.
103
+ 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.
105
104
 
106
105
  ### Live capture (desktop only)
107
106
  Lets the agent *see something live* on demand. Desktop client (Electron) only — the entry points never render in a plain browser. Borrows the meeting-app "share" model: the user binds one source, then the agent requests a frame when it actually needs to look.
@@ -115,7 +114,7 @@ Once bound, a frame is **not** attached to every message. Instead a one-line ins
115
114
  Constraints:
116
115
  - Shown only when the selected agent's model accepts image input (capture is pointless on a text-only model); switching to a text-only model auto-unbinds.
117
116
  - Screen share needs macOS **Screen Recording** permission; the camera prompts for **Camera** permission on first use, with an "Open Settings" path if previously denied.
118
- - Binding is **in-memory only** — switching sessions or restarting requires re-selecting.
117
+ - Binding is **in-memory only** and shared by all chat tabs — a page reload or restart requires re-selecting.
119
118
 
120
119
  ### The agent's face (`self.html`)
121
120
  A second channel beyond text: a visual space the agent drives in real time to express itself. Works everywhere (pure HTML/canvas, no Electron dependency) — desktop **and** plain browser.
@@ -123,7 +122,7 @@ A second channel beyond text: a visual space the agent drives in real time to ex
123
122
  - **What it is.** A self-contained animated particle canvas at `<workspace>/.halo/canvas/self.html` — a breathing core that reacts to the cursor (knows when it's watched), can form words/CJK/ASCII-from-emoji, play choreographed sequences, and gesture (pulse/flash/shake). Zero external references (no CDN/remote fonts) — ships and runs offline.
124
123
  - **Seeding.** Force-copied from `packages/server/templates/canvas/self.html` into every workspace on open (platform-owned, like built-in skills). The `self` built-in skill (wired into the default agent) teaches the agent it has this face and how to drive it.
125
124
  - **Opening it.** The ✨ button in the chat-input toolbar opens the face in the editor preview (switches to Explorer, render-mode on → lands on the live face) and posts `self.intro()` once it has mounted, so there's always a greeting when a human turns to look. The greeting is driven solely by this open action — the page does **not** self-fire it on load — so it plays exactly once, on both first and subsequent opens. The user can also just open the file directly (no auto-greeting in that path).
126
- - **Driving it.** The agent emits `<<<SHOW: …js… >>>` markers in a reply; Halo forwards the payload **verbatim** (it never parses it) to the open preview iframe via `postMessage`, where it's `eval`'d against the face's `self` API (`say`/`play`/`react`/`pulse`/`flash`/`shake`/`intro`) inside the sandboxed iframe. Markers are stripped from the rendered chat (like `<<<CAPTURE>>>`) — the user sees the face move, not the code. Multiple markers in one reply **queue and play in order**.
125
+ - **Driving it.** The agent emits `<<<SHOW: …js… >>>` markers in a reply; Halo forwards the payload **verbatim** (it never parses it) to the open preview iframe via `postMessage`, where it's `eval`'d against the face's `self` API (`say`/`play`/`react`/`pulse`/`flash`/`shake`/`intro`/`voice`) inside the sandboxed iframe. Markers are stripped from the rendered chat (like `<<<CAPTURE>>>`) — the user sees the face move, not the code. Multiple markers in one reply **queue and play in order**.
127
126
  - **Engine vs. expression.** `self.html` is a stable *engine* (defines how the face can move); the agent expresses itself by sending runtime JS, **never** by editing the file — so the force-copy-on-open never clobbers anything meaningful. The engine only changes when the platform adds a new capability (template edit + `TEMPLATE_VERSION` bump).
128
127
  - **No open preview = no-op.** `<<<SHOW>>>` only reaches a mounted preview; if the face isn't open the marker is silently dropped (the skill tells the agent to invite the user to open it rather than rely on a marker landing in the void).
129
128
  - **Identity.** Deliberately nameless ("HELLO / A MIND / IS HERE / BEYOND WORDS") — the conversational identity is user-configurable and the model may not be Claude, so the face never hard-codes a name.
@@ -10,8 +10,8 @@ Top-level built-in slash commands: `/help` `/evo` `/session` `/agent` `/skill` `
10
10
 
11
11
  | Name | Slash | Type | Purpose |
12
12
  |---|---|---|---|
13
- | `help` | `/help` | client | List commands — object commands show only the verbs the user can run; a command is hidden entirely if no verb is runnable |
14
- | `evo` | `/evo [text]` | server | Flat command (no verbs), **full-only**. Trigger self-evolution on the current root session: snapshot the conversation + queue an evo run. Optional text becomes a hint for what to focus on. Available only when `general.evolution.level=L1`. See [plans/self-evolution.md](../plans/self-evolution.md). |
13
+ | `help` | `/help` | server | List commands — object commands show only the verbs the user can run; a command is hidden entirely if no verb is runnable |
14
+ | `evo` | `/evo [text]` | server | Flat command (no verbs), **full-only**. Trigger self-evolution on the current root session: snapshot the conversation + queue an evo run. Optional text becomes a hint for what to focus on. Works at every `general.evolution.level` (L0 = manual only; L1 also drafts automatically on pre-compact). Hidden from `/help` for non-full users. See [design/evolution.md](../design/evolution.md). |
15
15
 
16
16
  The rest are noun-verb **object commands**: `/<obj> <verb> [args]`. Some verbs are built-in deterministic code (`SUBCOMMAND_ROUTES`); the others fall through to the same-name skill (LLM, dispatched via `$1`). Bare `/<obj>` or `/<obj> help` lists the verbs available to the user (filtered by access level).
17
17
 
@@ -38,7 +38,7 @@ Resolution starts from the caller's active session, walks up to its root (`id.sp
38
38
 
39
39
  ## Command aliases
40
40
 
41
- `dispatchCommand` expands shorthand aliases before routing, so every channel (WS, WeChat, Telegram, Web, CLI/TUI) gets the same shortcuts uniformly. Aliases are read from a single seed file:
41
+ `dispatchCommand` expands shorthand aliases before routing, so every channel (WS, Web, the IM channels, CLI/TUI) gets the same shortcuts uniformly. Aliases are read from a single seed file:
42
42
 
43
43
  - **`~/.halo/global/aliases.yaml`** — seeded once on first run (`init.ts`, via `writeIfMissing`), **never overwritten**, so the user can freely edit it. Deleting the file disables all aliases. Changes take effect immediately: the loader caches the parsed YAML keyed by file **mtime**, re-reading only when the file changes (no restart, no per-command disk parse on the hot path).
44
44
 
@@ -58,7 +58,7 @@ The canonical command is `/workspace`; `/w` is its bare alias. `/ws` and `/wi` a
58
58
 
59
59
  ## Cross-channel commands
60
60
 
61
- All channels (WS, WeChat, Telegram, Web, CLI/TUI) share `dispatchCommand` for common commands. Channel-specific commands (e.g. WeChat `/name`) are handled in the channel handler before reaching the shared dispatch.
61
+ All channels (WS, Web, Telegram, Slack, Feishu, WeCom, WeChat, CLI/TUI) share `dispatchCommand` for common commands. Channel-specific commands (e.g. WeChat `/qr`) are handled in the channel handler.
62
62
 
63
63
  ## Skill-as-command
64
64
 
@@ -88,30 +88,30 @@ When triggered, the skill body is read from disk and sent to the agent session a
88
88
 
89
89
  User args are filled via `$ARGUMENTS` / `$1`–`$9` placeholders (args are no longer appended verbatim to the body end); a verb reaches the body as `$1` for dispatch.
90
90
 
91
- Every `GET /api/commands?projectId=xxx` rescans skill directories.
91
+ Every `GET /api/commands` call that carries `sessionId` / `agentId` rescans skill directories.
92
92
 
93
93
  ### Conflict detection
94
94
 
95
95
  A skill's `command` must not collide with a built-in slash command or with another skill's. Collisions are resolved at scan time in `scanSkillDescriptors`, the single source feeding both dispatch and the discovery API:
96
96
 
97
- - **Built-ins always win** — a skill command colliding with a built-in is not registered as a command. This is by design for object commands: their same-name skill (e.g. `agent`, `skill`, `workspace`) is shadowed as a command, but its body still serves the skill verbs (create / update etc.) via the verb fallback.
97
+ - **Built-ins always win** (hidden ones too) — a skill command colliding with a built-in is not registered as a command. This is by design for object commands: their same-name skill (e.g. `agent`, `skill`, `workspace`) is shadowed as a command, but its body still serves the skill verbs (create / update etc.) via the verb fallback.
98
98
  - **Among skills, first-come wins** (workspace scope is merged over global before this check); the later one is dropped with a warning.
99
- - Colliding entries are **dropped** from the command list and a warning is logged (`[CommandRegistry] skill "<id>" command "/x" shadowed by …`). Dropping at the shared source guarantees a command can never appear in the palette that dispatch is unable to route ("visible but unreachable").
99
+ - Colliding entries are **dropped** from the command list; a skill-vs-skill clash also logs a warning (`[CommandRegistry] skill "<id>" command "/x" shadowed by …`), a built-in owning the name doesn't. Dropping at the shared source guarantees a command can never appear in the palette that dispatch is unable to route ("visible but unreachable").
100
100
 
101
101
  Note: a skill's `command` collision only removes the *slash command*; the skill itself remains usable via the agent's normal skill activation.
102
102
 
103
103
  ## Command discovery API
104
104
 
105
105
  ```
106
- GET /api/commands?projectId=xxx
106
+ GET /api/commands?projectId=xxx[&sessionId=][&agentId=]
107
107
  → { commands: CommandDescriptor[] }
108
108
  ```
109
109
 
110
- Returns every non-hidden command (built-in + skill). The frontend calls this on project switch to populate the command palette.
110
+ Returns every non-hidden built-in command, plus the skill commands the session's / agent's skill whitelist and access level allow when `sessionId` or `agentId` is given (with neither, built-ins only). The frontend calls this on project switch to populate the command palette.
111
111
 
112
112
  ## Adding a new command
113
113
 
114
- A server-handled command lives in **three** places that must stay in sync —
114
+ A server-handled command lives in **four** places that must stay in sync —
115
115
  miss any one and the server either throws at startup or silently no-ops:
116
116
 
117
117
  1. Add a `exec<Name>` function in `channels/shared/commands.ts`
@@ -15,18 +15,19 @@ Monaco-based multi-tab code editor + binary file previewer — surfaced in the U
15
15
  ## Core behaviour
16
16
 
17
17
  ### Multi-tab editing
18
- - Click a file in Explorer (single click = select, double click = open tab)
18
+ - Click a file in Explorer (single click = select, then opens after a 300 ms delay; double click opens immediately)
19
19
  - Each tab independently tracks content, original content, language, mtime
20
- - Unsaved tabs show a small red dot
20
+ - Unsaved tabs show a small amber dot (the ✕ appears on hover)
21
21
  - Closing an unsaved tab prompts for confirmation
22
+ - The tab bar's **Split right** button opens a second pane (two at most); each pane has its own tabs, Edit/Preview and Diff state
22
23
 
23
24
  ### Save
24
25
  - `Cmd+S` / `Ctrl+S` calls `PUT /api/files`
25
26
  - Server returns the new `modifiedAt`; the tab updates its mtime
26
27
 
27
28
  ### Auto-refresh (mtime-based)
28
- When agents write files through tools, the canvas detects it:
29
- 1. Periodically poll `GET /api/files/stat`
29
+ When agents write files through tools, the canvas detects it — push-based, no polling:
30
+ 1. The server's WS `file:changed` (`change`) event re-reads the matching open tab; the active tab is also re-checked with `GET /api/files/stat` on tab switch, window focus and WS reconnect
30
31
  2. If `diskMtime > tab.mtime`, pull fresh content
31
32
  3. Skip if the user has unsaved edits (don't overwrite)
32
33
 
@@ -34,7 +35,7 @@ When agents write files through tools, the canvas detects it:
34
35
  Open tabs and the active tab live in localStorage; survive refresh.
35
36
 
36
37
  ### Diff view
37
- - Tracked files → `GET /api/git/diff` (returns `{ original, modified }`) for git diff
38
+ - A tab with unsaved edits shows a **Diff** button in its pane's tab bar: last saved content vs. the current buffer, computed in the client (no git involved; `GET /api/git/diff` is the Source Control panel's)
38
39
  - Monaco left/right compare view
39
40
 
40
41
  ### Binary previews
@@ -66,13 +67,13 @@ File types without a built-in plugin can be handled by an **installed extension*
66
67
 
67
68
  ### Renderable text formats (Markdown / HTML)
68
69
  Markdown and HTML open as text in Monaco *and* have a rendered view — Canvas defaults to the **rendered** view since the primary audience is an AI generating reports / pages for humans to read.
69
- - Header shows an **Edit / Preview** toggle (same `textRenderMode` state for both formats)
70
+ - Each pane's tab bar shows an **Edit / Preview** toggle for the active Markdown / HTML tab; the state is per pane, shared by both formats
70
71
  - **Markdown** → `MarkdownPreview` (react-markdown + GFM; relative image `src` rewritten to the download endpoint so local images work)
71
- - **HTML** → sandboxed iframe (`sandbox="allow-same-origin"`; scripts, top-navigation, forms, pop-ups all blocked — safe against hostile HTML)
72
+ - **HTML** → sandboxed iframe (`sandbox="allow-scripts allow-same-origin"`: the page's own JS runs, so a self-contained page renders live; top-navigation, forms and pop-ups stay blocked. Trusted like opening the file in Monaco — it comes from the user's own workspace)
72
73
  - Toggle to Edit → Monaco source, Cmd+S saves as usual
73
74
 
74
75
  ### Preview caching (MRU)
75
- 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. Closing a preview tab removes it from the cache immediately (aborting any in-flight fetch).
76
+ 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. 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).
76
77
 
77
78
  ### File metadata in header
78
79
  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.
@@ -91,5 +92,5 @@ Canvas (including Monaco instances, file tree, and open tab contents) stays moun
91
92
  |---|---|
92
93
  | Cmd/Ctrl + P | Quick Open (fuzzy file search) |
93
94
  | Cmd/Ctrl + S | Save current tab |
94
- | Alt + W | Close current tab (Cmd+W cannot be overridden in browsers) |
95
+ | 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) |
95
96
  | Esc | Exit maximized Canvas (when not inside an input / Monaco / Quick Open) |
@@ -6,22 +6,25 @@ VS Code-style file tree sidebar with multi-select, drag-drop, right-click menu.
6
6
 
7
7
  ### Initialization
8
8
  - URL `?folder=/abs/path` → use that directly as workspace
9
- - No `?folder` → call `GET /api/fs/home`, take the home directory as fallback, write it back into the URL; UI behaves identically
9
+ - No `?folder` → reopen the last workspace (`halo_last_folder` in localStorage); if none is stored or it no longer resolves, fall back to `GET /api/fs/home`. Either way the path is written back into the URL; UI behaves identically
10
+ - The path is resolved through `POST /api/fs/workspace/resolve`, which rejects non-directories and filesystem roots and seeds the workspace's `.halo/` scaffold
10
11
  - There's always a valid workspace; every frontend feature (file tree / sessions / agents / settings) depends on it
11
12
 
12
13
  ### Path input
13
14
  - Explorer top bar has a path input + 📁🔍 picker button
14
- - Pressing Enter calls `GET /api/fs/exists?path=...` first — a miss shows an alert, no switch
15
- - **Recent-workspaces dropdown**: focusing the input shows the most-recently-used list; typing filters it by substring. Each row shows the folder name + full path; clicking one switches to it; a hover "×" removes a single entry. Entries are recorded only after a switch validates the path (so invalid paths never land), in canonical resolved form, MRU-ordered and deduped, capped at 8. Persisted in localStorage (`halo_recent_workspaces`)
15
+ - Pressing Enter calls `GET /api/fs/exists?path=...` first — a miss (or a non-directory) shows an alert, no switch
16
+ - **Recent-workspaces dropdown**: focusing the input shows the most-recently-used list; typing filters it by substring. The current workspace is left out. Each row shows the folder name + full path; clicking one switches to it; a hover "×" removes a single entry. Entries are recorded only after a switch validates the path (so invalid paths never land), in canonical resolved form, MRU-ordered and deduped, capped at 8. Persisted in localStorage (`halo_recent_workspaces`)
16
17
 
17
18
  ### Desktop windows (multi-window)
18
19
  Desktop app only — the browser has one tab per window natively.
19
20
  - **New window**: `Cmd+N` (macOS, via the File menu) / `Ctrl+N` (Windows/Linux, bound at the webContents level since those platforms have no app menu). Each window is a fresh view onto the **one shared local server** — no second server process spawns.
20
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.
21
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
+ - **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`).
22
24
  - **Pin (always-on-top)** acts per-window — each window pins independently.
25
+ - **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.
23
26
  - **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.
24
- - **Finish notification** (works in both the desktop app and a plain browser): an opt-in bell toggle sits in the activity bar next to the pin, **off by default**, persisted per-machine in localStorage (`halo_notify_on_finish`). It's shown whenever a notification can be raised — the desktop shell (`window.haloNotify` bridge) or a browser with the Web Notification API; turning it on in a browser requests Notification permission from within the toggle click (the required user gesture). When on, a turn finishing in **any loaded chat tab** — on screen or in the background — triggers two decoupled cues (sessions no tab has loaded, or released by a reconnect, never ring; tab switches and snapshot replays aren't completions): a **self-synthesized WebAudio chime** that always plays (regardless of focus — an audible "it's done" even while you're watching the tab, and not subject to the OS per-site notification-sound setting), plus — **only when the window is unfocused** — a banner (desktop: native notification + Dock bounce / taskbar flash; browser: a Web Notification whose click refocuses the tab) whose body names the session by its title ("“<title>” finished responding."; generic text if the title isn't known). Focused → chime only, no banner.
27
+ - **Finish notification** (works in both the desktop app and a plain browser): an opt-in toggle in the quick-toggles panel, **off by default**, persisted per-machine in localStorage (`halo_notify_on_finish`). It's shown whenever a notification can be raised — the desktop shell (`window.haloNotify` bridge) or a browser with the Web Notification API; turning it on in a browser requests Notification permission from within the toggle click (the required user gesture). When on, a turn finishing in **any loaded chat tab** — on screen or in the background — triggers two decoupled cues (sessions no tab has loaded, or released by a reconnect, never ring; tab switches and snapshot replays aren't completions; queued messages drained back to back ring once, after the last one): a **self-synthesized WebAudio chime** that always plays (regardless of focus — an audible "it's done" even while you're watching the tab, and not subject to the OS per-site notification-sound setting), plus — **only when the window is unfocused** — a banner (desktop: native notification + Dock bounce / taskbar flash; browser: a Web Notification whose click refocuses the tab) whose body names the session by its title ("“<title>” finished responding."; generic text if the title isn't known). Focused → chime only, no banner.
25
28
 
26
29
  ### Directory picker (FolderPicker modal)
27
30
  Visual directory browser opened by the 📁🔍 button:
@@ -47,8 +50,8 @@ Visual directory browser opened by the 📁🔍 button:
47
50
 
48
51
  ### Selection model
49
52
  VS Code-style highlight selection (no checkboxes):
50
- - **Click**: select and highlight
51
- - **Double click**: open in editor tab
53
+ - **Click**: select and highlight; a file also opens in the editor after a short (300 ms) delay, a folder toggles open/closed
54
+ - **Double click**: open the file in an editor tab right away (cancels the delayed open)
52
55
  - **Ctrl/Cmd + click**: toggle multi-select
53
56
  - **Shift + click**: range select
54
57
 
@@ -60,7 +63,7 @@ VS Code-style highlight selection (no checkboxes):
60
63
 
61
64
  ### Right-click menu
62
65
 
63
- Items shown depend on the click target:
66
+ Items shown depend on the click target. With several items selected the menu offers only Delete; on the workspace root row only New File / New Folder / Open in Integrated Terminal / Reveal in File Manager apply:
64
67
 
65
68
  | Action | Shown when | API / behavior |
66
69
  |---|---|---|