@turmind/halo 1.5.4 → 1.5.6
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-e0faeff1c4a0b0b9.js +1 -0
- package/admin-out/_next/static/chunks/app/page-44f707ea8dad57da.js +1 -0
- package/admin-out/_next/static/chunks/{webpack-1a264efd965db7ab.js → webpack-edf248e07333566d.js} +1 -1
- package/admin-out/_next/static/css/b8c87b919758c52b.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +10 -8
- package/bundled-docs/design/agentcore.md +13 -10
- package/bundled-docs/design/architecture.md +24 -14
- package/bundled-docs/design/canvas-extensions.md +19 -4
- 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 +14 -14
- package/bundled-docs/design/slack.md +22 -20
- package/bundled-docs/design/storage.md +30 -29
- 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 +86 -57
- package/bundled-docs/dev/deploy.md +8 -6
- package/bundled-docs/dev/env.md +29 -21
- package/bundled-docs/dev/logging.md +15 -29
- package/bundled-docs/dev/previews.md +20 -11
- package/bundled-docs/dev/tools.md +26 -14
- 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/delegation-and-access.md +1 -1
- 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 +9 -7
- package/bundled-docs/guide/testing-agents-and-skills.md +14 -15
- package/bundled-docs/guide/workspace.md +29 -17
- 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 +12 -10
- package/bundled-docs/requirements/explorer.md +10 -7
- package/bundled-docs/requirements/session.md +16 -17
- package/bundled-docs/requirements/settings.md +30 -18
- 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 +10587 -12088
- package/package.json +1 -1
- package/templates/agents/default/agent.yaml +1 -0
- package/templates/config.yaml +6 -0
- package/templates/skills/extension/SKILL.md +25 -14
- package/templates/skills/extension/config.yaml +9 -0
- package/templates/skills/extension/templates/ext.sh +166 -34
- package/templates/skills/web-search/SKILL.md +20 -6
- package/templates/skills/web-search/config.yaml +13 -0
- package/templates/skills/web-search/search.py +27 -6
- 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 → visjvN510yFK-Ff5zwx-0}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → visjvN510yFK-Ff5zwx-0}/_ssgManifest.js +0 -0
|
@@ -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` |
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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-
|
|
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 **
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
-
|
|
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
|
|
@@ -59,20 +60,21 @@ File types without a built-in plugin can be handled by an **installed extension*
|
|
|
59
60
|
- **Resolution order** for a file extension: `default`-priority extensions (newest install wins) → built-in plugins → `option`-priority extensions. `.glb` with the `glb` extension installed opens in the viewer directly; an `option` extension only shows up in the header's **Open with** menu
|
|
60
61
|
- **Opening and tab restore**: whether a file opens as text or in a viewer is decided when it is clicked, restored after a page reload, or opened to the side. Since an installed extension may claim a type (`.glb`) or even a text suffix (`.json`), a file that would open as text first waits for the page's initial extension list (up to 3 s), so a `.glb` tab survives a reload as a viewer rather than coming back as text. Files that already have a built-in viewer open without waiting. If the list isn't back within 3 s (or failed to load), files open as if no extension were installed — a `.glb` opens as text; a list that arrives later still updates the viewer of tabs that are already previews
|
|
61
62
|
- **Open with** (header menu, shown only when there are >2 candidates counting *Open as text*): switches the current tab's viewer; the choice is per tab and not persisted
|
|
62
|
-
- **Fallback**: a file with no built-in plugin and no installed extension shows a static page — "no built-in preview for this type; extensions for more file types are on
|
|
63
|
+
- **Fallback**: a file with no built-in plugin and no installed extension shows a static page — "no built-in preview for this type; extensions for more file types are on <hub> ↗", where `<hub>` is the extension skill's `hub_repo` (default halo-hub; a local path / ssh remote shows as text) — plus Open as text / Download. No online lookup
|
|
63
64
|
- **Editable extensions** (manifest `capabilities: ["save"]`): the iframe reports dirty → tab shows the dirty dot → header **Save** button (Ctrl/Cmd+S is *not* wired for extension tabs — Monaco isn't mounted) → `PUT /api/files/raw`. If the file changed on disk since it was loaded the save gets a 409 and the user is asked "changed on disk … Overwrite?". An external change while the tab is clean reloads it; while dirty, local edits are kept silently and the conflict surfaces at the next save
|
|
65
|
+
- **Install sources**: Settings → Extensions zip upload; `/extension install <id|zip|url>` (listed by the built-in `default` agent, full access), where an id comes from the hub in Settings → Skills → extension → `hub_repo` (default halo-hub; GitHub / Gitea / Forgejo / GitLab release zips, or tags of any other git repo); or a manual directory drop
|
|
64
66
|
- **Live install / upgrade / uninstall** (from Settings → Extensions upload / remove, the `/extension` skill, or a manual directory change): pushed over WS, no reload. An upgraded extension remounts open tabs (or shows a "updated — reload" banner if the tab is dirty); uninstalling drops clean tabs to the fallback page and keeps dirty tabs alive with a banner
|
|
65
67
|
- **Settings → Extensions**: lists installed extensions (name, version, file types, license/homepage) and manifest errors; upload `.zip` (≤ 100 MB) and remove. Any logged-in admin can do both — the admin cookie has no access level
|
|
66
68
|
|
|
67
69
|
### Renderable text formats (Markdown / HTML)
|
|
68
70
|
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
|
-
-
|
|
71
|
+
- 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
72
|
- **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"
|
|
73
|
+
- **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
74
|
- Toggle to Edit → Monaco source, Cmd+S saves as usual
|
|
73
75
|
|
|
74
76
|
### 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).
|
|
77
|
+
Recently opened preview tabs stay mounted (up to 5, MRU) so switching between them doesn't re-fetch or re-parse. Plugins flagged `heavy: true` bypass the cache — only the active instance mounts, others unmount; switching between two heavy tabs of the same type (e.g. two `.ipynb`) remounts the viewer, so it shows the newly active file. An extension tab with unsaved edits stays pinned beyond the 5. Closing a preview tab removes it from the cache immediately (aborting any in-flight fetch).
|
|
76
78
|
|
|
77
79
|
### File metadata in header
|
|
78
80
|
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 +93,5 @@ Canvas (including Monaco instances, file tree, and open tab contents) stays moun
|
|
|
91
93
|
|---|---|
|
|
92
94
|
| Cmd/Ctrl + P | Quick Open (fuzzy file search) |
|
|
93
95
|
| Cmd/Ctrl + S | Save current tab |
|
|
94
|
-
| Alt + W | Close current tab (Cmd+W cannot be overridden in browsers) |
|
|
96
|
+
| 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
97
|
| 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` →
|
|
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
|
|
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
|
|---|---|---|
|
|
@@ -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: "~/.
|
|
21
|
-
hidden_files: "~/.
|
|
20
|
+
hidden_dirs: "~/.kube" # extras — appended to the built-in list, never replace it
|
|
21
|
+
hidden_files: "~/.pgpass" # extras — appended to the built-in list, never replace it
|
|
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,19 @@ params:
|
|
|
91
91
|
secrets: []
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
Built-in skills declare theirs the same way — today `extension`, with a plain `hub_repo` param (the hub `/extension install <id>` pulls from; empty = turmind/halo-hub), and `web-search`, with plain `fast_region` / `deep_region` params (AWS regions for its two Bedrock gears; empty = us-east-1 fast, us-west-2 deep / us-east-1 for sol). `halo setup` asks only for built-in skills' `secret: true` params; plain params are configured in Settings → Skills only.
|
|
95
|
+
|
|
96
|
+
Global agents declare theirs the same way in `agents/<agent-id>/agent-config.yaml` (same `params:` / `secrets:` lists; shown under Settings → Agents).
|
|
97
|
+
|
|
94
98
|
### General — built-in
|
|
95
99
|
|
|
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
|
|
100
|
+
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.max_retries`, `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
101
|
|
|
98
102
|
`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
103
|
|
|
100
|
-
`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).
|
|
104
|
+
`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). `hidden_dirs` / `hidden_files` hold **extra** entries: the effective list is the built-in default (`DEFAULT_HIDDEN_DIRS` / `DEFAULT_HIDDEN_FILES` in `tools/sandbox.ts`, the single source — `~/.halo/secrets`, `~/.aws`, `~/.ssh`, `~/.gnupg`, `~/.docker`, `~/.config/gh`, `~/.halo/global/internal-sessions`, `~/.halo/global/logs`; `~/.npmrc`, `~/.bash_history`, `~/.gitconfig`, `~/.git-credentials`, `~/.netrc`, the global `evo` / `cron` / `runs` databases + `-wal` / `-shm`) plus the user's entries, deduped (`config.ts` `resolveSandboxPaths()`, used by both the server and the CLI). A default entry cannot be removed from the list in settings. The lists apply only to workspace / readonly sessions (bwrap / Seatbelt, or `assertPathAllowed` without an OS sandbox); Full sessions, the admin file explorer (`validatePath`, workspace-bound only) and the terminal never consult them. The schema `default` is empty so the placeholder doesn't suggest copying the built-ins; the field description names them. `writable_dirs` is a plain list (no built-ins).
|
|
101
105
|
|
|
102
|
-
`evolution.*` controls the self-evolution subsystem (see [
|
|
106
|
+
`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
107
|
|
|
104
108
|
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
109
|
|
|
@@ -110,18 +114,26 @@ general:
|
|
|
110
114
|
|
|
111
115
|
The value is read once at process start, so unlike other settings it is **not** picked up by the mtime-watching reload.
|
|
112
116
|
|
|
113
|
-
|
|
117
|
+
There is no "default provider" setting: a newly created agent's `model:` is a copy of the **default agent's** `model:` mapping — for workspace scope `<project>/.halo/agents/default/agent.yaml` when it exists, else the global `~/.halo/global/agents/default/agent.yaml`; for global scope the global one. When that file is missing or has no `model` mapping, the scaffold falls back to the provider registry (`aws-bedrock-claude-invoke` if installed, else the first provider on disk), deriving model id / endpoint / prompt caching / thinking from that provider's YAML. Only `model` is copied; `context` stays the scaffold default. Existing agents are not retroactively touched. A leftover `general.agent.default_provider` in `settings.yaml` is ignored. Implementation: [packages/server/src/routes/agent-configs.ts](../../../packages/server/src/routes/agent-configs.ts) `scaffoldModelBlock()`.
|
|
118
|
+
|
|
119
|
+
**Advanced fold**: on the General page, `language` and `theme` are shown in the open; every other General field is in an "Advanced" area, collapsed by default (schema flag `advanced: true`, set in `generalSection()`).
|
|
120
|
+
|
|
121
|
+
**Restart-required fields**: `agent.max_retries`, `session.max_queue_size`, `session.max_nesting_depth`, `compact.keep_messages`, `compact.max_summary_input`, `compact.max_message_slice`, `logging.level` and the four `observability.*` keys are read once at server start (object literals in `config.ts`; observability is mapped onto `OTEL_*` env by `initObservability()` at boot). They carry `restartRequired: true`; after a save or Reset of one of them the Settings page shows a notice above the section listing the saved keys and saying the Halo server must be restarted (dismissable; it stays until dismissed). Every other General key is read live.
|
|
114
122
|
|
|
115
123
|
### Field attributes
|
|
116
124
|
|
|
117
125
|
| Attribute | Required | Purpose |
|
|
118
126
|
|---|---|---|
|
|
119
127
|
| `key` | yes | Leaf key under the namespace |
|
|
128
|
+
| `type` | no | `string` (default) / `int` / `float` / `boolean` / `enum` — picks the input widget |
|
|
129
|
+
| `options` / `optionLabels` | no | For `type: enum`: the allowed values, and optional display labels parallel to them |
|
|
120
130
|
| `description` | no | English description rendered as help text |
|
|
121
131
|
| `description_zh` | no | Chinese description (UI picks based on lang) |
|
|
122
|
-
| `default` | no | Placeholder shown when the value is unset; supports `<<ENV
|
|
132
|
+
| `default` | no | Placeholder shown when the value is unset; supports `<<ENV>>`. For provider / skill / agent fields it is display-only: an unset param's `{{<id>.params.<key>}}` stays literal at substitution, so the consumer applies its own fallback (the `extension` skill's `ext.sh` treats `{{…}}` as "use the default hub"; `web-search`'s `search.py` as "use the default region") |
|
|
123
133
|
| `secret` | no | `true` → masked in API responses + password input in UI |
|
|
124
|
-
| `
|
|
134
|
+
| `restartRequired` | no | `true` → read once at server start; the Settings page shows a restart notice after saving it. Built-in `general` section only |
|
|
135
|
+
| `advanced` | no | `true` → rendered in the collapsed "Advanced" area. Built-in `general` section only (every field except `language` / `theme`) |
|
|
136
|
+
| `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
137
|
|
|
126
138
|
## Scope: global vs. workspace
|
|
127
139
|
|
|
@@ -133,7 +145,8 @@ The value is read once at process start, so unlike other settings it is **not**
|
|
|
133
145
|
Read order: `<schema default> <- <global> <- <workspace>` (workspace layer applies to namespaced `params` / `secrets` only — see General above).
|
|
134
146
|
|
|
135
147
|
The Settings page shows source badges per field:
|
|
136
|
-
- `workspace` (
|
|
148
|
+
- `workspace` (blue, override applied here)
|
|
149
|
+
- `global` (green, value set at the global layer)
|
|
137
150
|
- `inherited from global` (grey, value pulled from global because workspace has none)
|
|
138
151
|
- `unset` (no value at any layer; the `default` is shown as placeholder)
|
|
139
152
|
|
|
@@ -147,7 +160,7 @@ Values can carry `<<ENV_NAME>>` placeholders. They're expanded:
|
|
|
147
160
|
|
|
148
161
|
**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
162
|
|
|
150
|
-
Env var unset → the `<<ENV_NAME>>` literal stays verbatim, plus `[
|
|
163
|
+
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
164
|
|
|
152
165
|
## Agent visibility
|
|
153
166
|
|
|
@@ -169,7 +182,7 @@ A malicious skill that tries `curl -H "Bearer {{aws-bedrock-claude-invoke.secret
|
|
|
169
182
|
|
|
170
183
|
## Orphans
|
|
171
184
|
|
|
172
|
-
|
|
185
|
+
`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
186
|
|
|
174
187
|
`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
188
|
|
|
@@ -177,7 +190,7 @@ Values present in `settings.yaml` whose namespace doesn't appear in any current
|
|
|
177
190
|
|
|
178
191
|
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
192
|
|
|
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.
|
|
193
|
+
**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
194
|
|
|
182
195
|
**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
196
|
|
|
@@ -185,7 +198,6 @@ A **Security** entry in the left nav (below the System group) opens a page with
|
|
|
185
198
|
|
|
186
199
|
| Operation | Method | Endpoint | Purpose |
|
|
187
200
|
|---|---|---|---|
|
|
188
|
-
| Read merged settings (legacy) | GET | `/api/settings?projectId=xxx` | Raw read, used by older tooling |
|
|
189
201
|
| Read schema + resolved values | GET | `/api/settings/schema?projectId=xxx` | Drives the new Settings page |
|
|
190
202
|
| Replace scope | PUT | `/api/settings` | Bulk replace one yaml file |
|
|
191
203
|
| Patch single key | PATCH | `/api/settings` | Set a leaf at `<dotted-key>` |
|
|
@@ -208,7 +220,7 @@ A **Security** entry in the left nav (below the System group) opens a page with
|
|
|
208
220
|
"kind": "secret",
|
|
209
221
|
"description": "AWS Access Key ID",
|
|
210
222
|
"description_zh": "...",
|
|
211
|
-
"default":
|
|
223
|
+
"default": "...", // omitted when the schema declares none
|
|
212
224
|
"secret": true,
|
|
213
225
|
"value": "AK****ST",
|
|
214
226
|
"hasValue": true,
|
|
@@ -237,7 +249,7 @@ A **Security** entry in the left nav (below the System group) opens a page with
|
|
|
237
249
|
|
|
238
250
|
### DELETE body
|
|
239
251
|
|
|
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.
|
|
252
|
+
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
253
|
|
|
242
254
|
## i18n
|
|
243
255
|
|
|
@@ -245,4 +257,4 @@ The admin UI is bilingual (en/zh). Field descriptions are localized via `descrip
|
|
|
245
257
|
|
|
246
258
|
## Config caching
|
|
247
259
|
|
|
248
|
-
`config.ts` reads `settings.yaml` lazily with mtime-watching: every read stats the file and reparses if the mtime has changed. UI saves bump the mtime, so the server picks up new secrets on the next
|
|
260
|
+
`config.ts` reads `settings.yaml` lazily with mtime-watching: every read stats the file and reparses if the mtime has changed. UI saves bump the mtime, so the server picks up new secrets, params and most General keys on the next read without a restart. The exceptions are the `restartRequired` General keys (see General above) and `general.goal_mode_enabled`, which are read once at boot — those need a server restart. See [packages/server/src/config.ts](../../../packages/server/src/config.ts) `getSettings()`.
|
|
@@ -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.
|