@turmind/halo 1.5.7 → 1.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/admin-out/404.html +1 -1
  2. package/admin-out/_next/static/chunks/220.7566bc89f3d98993.js +1 -0
  3. package/admin-out/_next/static/chunks/597-399e2e1de26aa747.js +1 -0
  4. package/admin-out/_next/static/chunks/989-c404f6f43c400911.js +1 -0
  5. package/admin-out/_next/static/chunks/app/page-59bfce35bbb30b05.js +1 -0
  6. package/admin-out/_next/static/chunks/{webpack-edf248e07333566d.js → webpack-efd27999305b080a.js} +1 -1
  7. package/admin-out/_next/static/css/27b44c85485eabb9.css +3 -0
  8. package/admin-out/index.html +1 -1
  9. package/admin-out/index.txt +5 -5
  10. package/bundled-docs/design/architecture.md +3 -2
  11. package/bundled-docs/design/canvas-extensions.md +51 -11
  12. package/bundled-docs/design/cron.md +6 -6
  13. package/bundled-docs/design/evolution.md +47 -15
  14. package/bundled-docs/design/express-self.md +96 -20
  15. package/bundled-docs/design/storage.md +10 -2
  16. package/bundled-docs/design/ws.md +1 -0
  17. package/bundled-docs/dev/add-model-provider.md +16 -7
  18. package/bundled-docs/dev/api.md +7 -6
  19. package/bundled-docs/dev/desktop-packaging.md +26 -0
  20. package/bundled-docs/guide/agents.md +3 -3
  21. package/bundled-docs/guide/chat.md +3 -0
  22. package/bundled-docs/guide/cli.md +10 -1
  23. package/bundled-docs/guide/workspace.md +2 -2
  24. package/bundled-docs/requirements/agent.md +1 -1
  25. package/bundled-docs/requirements/chat.md +17 -9
  26. package/bundled-docs/requirements/editor.md +3 -1
  27. package/bundled-docs/requirements/explorer.md +4 -2
  28. package/bundled-docs/requirements/settings.md +11 -3
  29. package/bundled-docs/requirements/workspace.md +2 -2
  30. package/dist/index.js +2514 -1669
  31. package/package.json +2 -1
  32. package/templates/agents/__evo_agent__/AGENT.md +7 -0
  33. package/templates/agents/__score__/AGENT.md +183 -71
  34. package/templates/canvas/self.html +617 -71
  35. package/templates/models/anthropic.yaml +1 -0
  36. package/templates/models/aws-bedrock-claude-invoke.yaml +1 -0
  37. package/templates/models/aws-bedrock-mantle.yaml +1 -0
  38. package/templates/models/aws-bedrock-openai.yaml +1 -0
  39. package/templates/models/deepseek.yaml +1 -0
  40. package/templates/models/doubao.yaml +1 -0
  41. package/templates/models/hunyuan.yaml +1 -0
  42. package/templates/models/kimi.yaml +1 -0
  43. package/templates/models/mimo-token-plan-china.yaml +1 -0
  44. package/templates/models/minimax.yaml +1 -0
  45. package/templates/models/openai.yaml +1 -0
  46. package/templates/models/qwen.yaml +1 -0
  47. package/templates/models/zhipu.yaml +1 -0
  48. package/templates/skills/cron/SKILL.md +59 -4
  49. package/templates/skills/cron/manage-cron.py +158 -24
  50. package/templates/skills/extension/SKILL.md +30 -2
  51. package/templates/skills/extension/config.yaml +2 -2
  52. package/templates/skills/extension/templates/ext.sh +69 -16
  53. package/templates/skills/self/SKILL.md +79 -17
  54. package/admin-out/_next/static/chunks/220.e250bbbf65d24094.js +0 -1
  55. package/admin-out/_next/static/chunks/422-cc74b1fdd3af5415.js +0 -1
  56. package/admin-out/_next/static/chunks/989-e0faeff1c4a0b0b9.js +0 -1
  57. package/admin-out/_next/static/chunks/app/page-44f707ea8dad57da.js +0 -1
  58. package/admin-out/_next/static/css/b8c87b919758c52b.css +0 -3
  59. /package/admin-out/_next/static/{rTD3zkMvzwtqUwzhz6QJH → RD5w3FmDZ6HtyQGGaUb8t}/_buildManifest.js +0 -0
  60. /package/admin-out/_next/static/{rTD3zkMvzwtqUwzhz6QJH → RD5w3FmDZ6HtyQGGaUb8t}/_ssgManifest.js +0 -0
@@ -405,6 +405,32 @@ Still good practice: build server + admin **before** `pnpm dist:arm64`.
405
405
  confirm does nothing, because `if (!confirm(...)) return` always returns). In
406
406
  a plain browser the helpers fall back to the native sync dialogs.
407
407
 
408
+ - **Media capture for extensions (`media` capability, e.g. htrans) —
409
+ ⚠ not yet verified on a real Mac.** Three pieces, all needed:
410
+ - **Info.plist usage strings** (`electron-builder.yml` → `mac.extendInfo`):
411
+ `NSCameraUsageDescription`, `NSMicrophoneUsageDescription`, and
412
+ `NSAudioCaptureUsageDescription` (system-audio capture; required on macOS
413
+ 14.2+). Missing ones make macOS deny / kill the request silently.
414
+ - **`--enable-features=MacLoopbackAudioForScreenShare,MacSckSystemAudioLoopbackOverride`**
415
+ on macOS (`main.cjs`, appended before app `ready`, merged with any existing
416
+ `enable-features` value) — Chromium's loopback (system-audio) capture is
417
+ behind these flags; macOS 13+ (Darwin 22+) only, so older macOS never
418
+ offers or passes `audio:'loopback'`.
419
+ - **`getDisplayMedia()` picker**: Electron has no built-in one (the call just
420
+ fails), so `session.setDisplayMediaRequestHandler` (mac / win only; Linux
421
+ keeps Electron's default) lists `desktopCapturer` sources and asks the
422
+ requesting window's preload to render a picker (IPC `halo:display-pick` /
423
+ `halo:display-picked` / `halo:display-pick-cancel`; resolved against the
424
+ top frame, since preload doesn't run in the extension iframe). One answer
425
+ per request: cancel / 120 s timeout / page gone → `callback(null)` (the
426
+ page's promise rejects) — never `callback({})`, which throws when video was
427
+ requested. Audio = `'loopback'` only when requested AND supported (win, or
428
+ macOS 13+).
429
+ - **Unload confirm**: `will-prevent-unload` shows a native Leave / Stay box
430
+ (zh / en by app locale); the admin only blocks the unload while a tab is
431
+ busy (recording) or unsaved, so it doesn't fire on an ordinary close or a
432
+ workspace switch.
433
+
408
434
  - **Cross-staging needs a full restage — auto-fast skips the native fixup.**
409
435
  After editing `stage-runtime.mjs`, or any time you cross-stage a target whose
410
436
  `node_modules` differs from what's on disk, run with `HALO_STAGE_FULL=1`. The
@@ -42,7 +42,7 @@ Click an agent in the sidebar to open the right-hand editor. Two views:
42
42
  Data-driven form for name / description / model / tools / skills / thinking. Changes auto-save about half a second after you stop editing — there's no Save button. The agent's AGENT.md is shown below the form as a read-only preview.
43
43
 
44
44
  **Model section** has four fields:
45
- - `provider` — dropdown populated from `~/.halo/global/models/*.yaml`
45
+ - `provider` — dropdown populated from `~/.halo/global/models/*.yaml` plus hub-installed `~/.halo/global/models.d/*.yaml` (`/extension models`; updates the list live)
46
46
  - `id` — combobox (input + datalist); preset options filtered by current provider, also accepts manual input of any model ID
47
47
  - `endpoint` — combobox (input + datalist); full endpoint URL (e.g. `https://bedrock-runtime.ap-northeast-1.amazonaws.com`), supports custom proxy URLs
48
48
  - `maxTokens` — optional; defaults to `maxOutputTokens` in the provider yaml
@@ -113,7 +113,7 @@ Top-right `Test` button:
113
113
 
114
114
  Better than the old built-in test chat — it has full workspace tools + session persistence.
115
115
 
116
- Internal agents (`internal: true`, e.g. self-evolution agents) have no Test button and never appear in the chat agent selector or `/session new`'s default pick — they're delegated to by other agents, never driven directly. They remain editable in the management sidebar's collapsed **Internal** group.
116
+ Internal agents (`internal: true`, e.g. self-evolution agents) have no Test button and never appear in the chat agent selector, the Cron agent picker or `/session new`'s default pick — they're delegated to by other agents, never driven directly. They remain editable in the management sidebar's collapsed **Internal** group. The goal-mode judge (`goal`) isn't listed there either: `/goal` starts it, and there's nothing to configure for it in the UI.
117
117
 
118
118
  ## Delete
119
119
 
@@ -227,6 +227,6 @@ With `export NANO_BANANA_KEY=sk-xxx` in the env, the agent's system prompt gets
227
227
 
228
228
  ## Model registry — adding a new provider
229
229
 
230
- Halo ships 13 providers (`aws-bedrock-claude-invoke`, `aws-bedrock-mantle`, `aws-bedrock-openai`, `anthropic`, `openai`, `kimi`, `deepseek`, `minimax`, `qwen`, `hunyuan`, `doubao`, `zhipu`, `mimo-token-plan-china`); each one is a yaml under `~/.halo/global/models/` whose `runtime:` field names the implementation in [packages/server/src/agents/model-runtime.ts](../../../packages/server/src/agents/model-runtime.ts) (e.g. `anthropic-messages`, `openai-chat`, `bedrock-invoke`). For any OpenAI- or Anthropic-compatible endpoint you don't need new code: pick `openai` or `anthropic` as the provider and fill in the endpoint and model id in Form view.
230
+ Halo ships 13 providers (`aws-bedrock-claude-invoke`, `aws-bedrock-mantle`, `aws-bedrock-openai`, `anthropic`, `openai`, `kimi`, `deepseek`, `minimax`, `qwen`, `hunyuan`, `doubao`, `zhipu`, `mimo-token-plan-china`); each one is a yaml under `~/.halo/global/models/` (refreshed between releases by `/extension models`, which installs newer copies into `~/.halo/global/models.d/`) whose `runtime:` field names the implementation in [packages/server/src/agents/model-runtime.ts](../../../packages/server/src/agents/model-runtime.ts) (e.g. `anthropic-messages`, `openai-chat`, `bedrock-invoke`). For any OpenAI- or Anthropic-compatible endpoint you don't need new code: pick `openai` or `anthropic` as the provider and fill in the endpoint and model id in Form view.
231
231
 
232
232
  A provider that speaks an existing wire protocol needs only a yaml manifest with that `runtime:`; a new protocol also needs a runtime class. The step-by-step is in [dev/add-model-provider.md](../dev/add-model-provider.md). A provider yaml without `runtime:` (or with a name Halo doesn't know) makes session spawn fail with an error naming the yaml; for a bundled provider that means the seed is stale — run `halo setup` or restart the Halo server.
@@ -34,6 +34,9 @@ The input toolbar shows a context chip (the open file's name, or the selection)
34
34
 
35
35
  Click the chip (it turns struck-through) if you don't want that context injected.
36
36
 
37
+ Independent of the chip, while the ✨ **face** toggle in the toolbar is on (the agent's live face, `.halo/canvas/self.html`, pinned as an editor tab) every message also carries one line:
38
+ - `[Face open: .halo/canvas/self.html · last: …]` — tells the agent the face is on screen; the optional `last:` part is a short receipt of what its previous face actions did (e.g. `show a.png fail`, `voice blocked (needs a click)`). Receipts never wake the agent on their own; they ride along with your next message. The line is not shown in your bubble and is stripped from session titles.
39
+
37
40
  ## `@` file mention
38
41
 
39
42
  Typing `@` in the input opens a file search:
@@ -144,7 +144,7 @@ Multi-turn conversation. Supports all standard Halo slash commands:
144
144
  | `/workspace setup` / `/workspace tidy` / `/workspace share` | Set up / tidy the `.halo/` knowledge files, or export a shareable bundle (workspace skill) |
145
145
  | `/cron <verb>` | Manage scheduled agent runs: `create` / `list` / `update` / `enable` / `disable` / `delete` |
146
146
  | `/acp <verb>` | Ask other agents over ACP (`kiro <q>` / `claude <q>`) and manage `ask-*` bindings (`add` / `list` / `remove`) |
147
- | `/extension <verb>` | Canvas preview extensions: `install` / `list` / `remove` (full access only) |
147
+ | `/extension <verb>` | Canvas preview extensions: `install` / `list` / `remove`; `models` updates the model provider list from the hub (full access only) |
148
148
  | `/evo [hint]` | Queue a self-evolution run on this session (full access only) |
149
149
  | `/quit` | Exit |
150
150
 
@@ -211,6 +211,15 @@ Attach files or images to your message with `@file` and `@image`, or pull a dire
211
211
  - **Images**: skipped if larger than 5MB
212
212
  - If the current model does not support vision, a warning is shown and images are ignored
213
213
 
214
+ ## Model provider configs (`halo models`)
215
+
216
+ ```bash
217
+ halo models install <dir> [--yes] # install <dir>/*.yaml into ~/.halo/global/models.d/
218
+ halo models list # id · copy in effect (bundled | hub) · revision · runtime
219
+ ```
220
+
221
+ Normally run for you by `/extension models`, which downloads the hub's newest `models-v*` release (https GitHub / Gitea / Forgejo / GitLab only) and passes the unpacked directory to `halo models install`. Per provider id the copy with the higher `revision` is in effect (tie → the `models.d` copy); an older one is skipped, an identical one reported up to date. Refused per provider: a `runtime:` this halo doesn't know (needs a newer halo), no `revision`, a `secrets[].default` that isn't empty or `<<ENV_NAME>>`. If any provider is new or changes `defaultEndpoint` / `endpointPresets`, the changes are listed, **nothing is written**, and the exit code is `3` — re-run with `--yes` to apply. Writes are atomic (tmp + rename); a running server reloads its registry from `models.d/` without a restart and pushes the change to open admin pages. Existing agents keep the model / endpoint in their own `agent.yaml`. Exit codes: `0` ok (refusals listed in the summary line), `1` error, `3` needs `--yes`. To undo, delete the file from `~/.halo/global/models.d/`.
222
+
214
223
  ## Server Mode (background daemon)
215
224
 
216
225
  `halo server start|stop|restart|status|logs` manages a local server from the
@@ -28,10 +28,10 @@ The docked Bottom Panel belongs to the Explorer tab; every other tab fills the w
28
28
  | Icon | Tab | Purpose |
29
29
  |---|---|---|
30
30
  | 📄 Files | Explorer | File tree + Monaco editor |
31
- | 🌿 Branch | Source Control | Changes, commit, push, history (hidden for non-git workspaces) |
32
31
  | 📨 Messages | Sessions | Session history + debug viewer |
33
- | ⚡ Zap | Skills | Skill editing (mini workspace) |
32
+ | 🌿 Branch | Source Control | Changes, commit, push, history (hidden for non-git workspaces) |
34
33
  | 🤖 Bot | Agents | Agent configuration (Form / Edit) |
34
+ | ⚡ Zap | Skills | Skill editing (mini workspace) |
35
35
  | 💬 Chat bubble | Channels | Web / Telegram / Slack / Feishu / WeCom / WeChat accounts |
36
36
  | ✨ Sparkles | Evolution | Self-evolution runs and review |
37
37
  | 🕐 Clock | Cron | Scheduled agent runs |
@@ -88,6 +88,6 @@ API: `GET/PUT /api/agent-configs/:id/md/:fileType`, `GET /api/agent-configs/:id/
88
88
  - **Not rendered for internal agents** (`internal: true`) — they're delegated to by other agents, never driven directly
89
89
 
90
90
  ### Internal agents
91
- Agents flagged `internal: true` in agent.yaml (e.g. `__evo_agent__`, `__apply_agent__`, `__score__`) are platform tooling. They are hidden from every user-facing surface: the delegation roster, `/session new` default pick, and the chat agent selector — and have no Test button. They stay editable in the management sidebar's collapsed **Internal** group.
91
+ Agents flagged `internal: true` in agent.yaml (e.g. `__evo_agent__`, `__apply_agent__`, `__score__`, `goal`) are platform tooling. They are hidden from every user-facing surface: the delegation roster, `/session new` default pick, the chat agent selector, the Cron job agent picker and `halo cli`'s agent list (`halo agents`) — and have no Test button. They stay editable in the management sidebar's collapsed **Internal** group — except the goal-mode judge (`goal`), which is only ever started by `/goal` and isn't listed on the Agents page at all (edit its `agent.yaml` on disk if needed).
92
92
 
93
93
  Replaces the old built-in test chat panel, giving a more realistic environment (full workspace tools + session persistence).
@@ -32,7 +32,7 @@ A Bug-icon **Debug** button in the composer's left control cluster (next to the
32
32
  ### User-message actions (Expand / Copy / Delete)
33
33
  Actions on user-role turns: the blue sticky user bubble, plus the sub-agent-report (green) and compact-summary (purple) callouts — all three share the same Copy/Delete pair. The icons are **always visible** (not hover-only). On the callouts, Copy copies the body only (the `(from: session X)` / `[Conversation Summary…]` marker line is stripped); a deleted callout greys out and gains a "deleted" badge like the bubble does:
34
34
  - **Expand / Collapse** — from a **header row**, not an icon in the action group (the top-right expand arrow was removed in 1.5.3-alpha):
35
- - **User bubble**: the header row holds a chevron, the send time and, while collapsed, a preview (the first 20 characters, then `…`). A long message is **collapsed by default**: the body height is 0, so only the header row shows. Clicking anywhere on the row toggles it.
35
+ - **User bubble**: every message is a single header row — a marker, the send time and a one-line preview. A message that fits on that row (no line breaks) shows a muted `·` marker and has nothing to expand. One that has line breaks or overflows the row shows a chevron, is **collapsed by default**, and its preview is cut by width with `…` (a multi-line message previews its first line plus `…`); clicking anywhere on the row shows the full body below. The `·` and the chevron are the same width, so the times line up. Whether a message overflows is re-measured when the panel width changes.
36
36
  - **Report callout**: its "Report from sub-session" title row toggles the same way.
37
37
  - The chevron only appears when the body is actually clamped; a short body has no chevron and its row does nothing on click.
38
38
  - An expanded body is capped at 40vh and scrolls inside the bubble, because the bubbles are sticky.
@@ -54,9 +54,9 @@ Long sessions get their older exchanges moved out of the active log on compact,
54
54
  ### Session tabs (right side)
55
55
  The workspace's root sessions as **Chrome-style vertical tabs** on the right edge of the chat panel (`VerticalTabRow` rows inside a `ResizableSidebar`, default 200px). The list **is** the tab list: every session is "open", clicking a row shows it, and there is no close — deleting the session is the only way a tab goes away. Since 1.5.3-alpha this replaces the top tab strip, the History and New-session buttons beside the composer, and the "N previous sessions" link in the empty state.
56
56
 
57
- - **Resize / collapse**: drag the left edge to resize (120–480px). Collapse from the header; the collapsed list is a `w-10` rail of square tabs, each showing the title's first letter plus the status dot in a corner, with a "+" at the bottom. Both settings are global preferences in `localStorage` (`halo_session_sidebar_open`, `halo_session_sidebar_width`); the list is open by default.
58
- - **Rows**: one line per row, status dot + title. Message count, time-ago and model are in the tooltip. `N msgs` counts the session's user turns **over its whole lifetime**, so compacting or archiving history never makes it go backwards.
59
- - Each row leads with one fixed-size status dot (fixed so a state change never reflows a narrow list — the spinner it replaced did): **amber, pulsing** while a loaded tab's turn is running; **blue** when a loaded background tab is idle with unread output (output landed while it wasn't on screen; cleared when shown); **green** otherwise — including sessions no tab has loaded and tabs a reconnect released. The list endpoint's `status` is not used.
57
+ - **Resize / collapse**: drag the left edge to resize (120–480px). Collapse from the header; the collapsed list is a `w-10` rail of square tabs, each showing the title's first letter plus a short status bar along its bottom (about 60% wide, centered), with a "+" at the bottom. Both settings are global preferences in `localStorage` (`halo_session_sidebar_open`, `halo_session_sidebar_width`); the list is open by default.
58
+ - **Rows**: one line per row — just the title, no leading icon cell (the "New session" draft row has none either, so the titles line up). Message count, time-ago, model and the state text (`Working…` / `New messages` / `Idle`) are in the tooltip, the state after a ` · ` at the end of its second line. `N msgs` counts the session's user turns **over its whole lifetime**, so compacting or archiving history never makes it go backwards.
59
+ - Each row carries a **2px status bar** along its bottom edge, full row width with 6px inset on both sides and rounded ends (absolutely positioned, so a state change never reflows a narrow list — the spinner it replaced did; decorative, `aria-hidden`): **amber, breathing** (`animate-pulse`) while a loaded tab's turn is running; **blue**, solid, when a loaded background tab is idle with unread output (output landed while it wasn't on screen; cleared when shown); **green** otherwise — including sessions no tab has loaded and tabs a reconnect released. Green is the norm, so it is **dimmed** (`/40`) to recede into the background, and rises to `/70` on the selected and hovered row; amber and blue never dim. The list endpoint's `status` is not used. In the light and warm themes amber renders as the theme's darker orange, same as the rest of the UI.
60
60
  - Infinite scroll pages older sessions. A "+" footer starts a new session.
61
61
  - **New session**: "+", `/session new` and `/clear` open a **draft tab**. A "New session" row sits on top while it is on screen, and an untouched draft is reused rather than duplicated. The session is created by the draft's first message or command. The previous session keeps streaming in its own tab.
62
62
  - **Background tabs**: one WS connection carries every loaded tab, so a session that isn't on screen keeps receiving its turn. After a reconnect only the tab on screen is reattached; the others reload when clicked. Live capture and face (`<<<SHOW>>>`) markers only act for the tab on screen.
@@ -97,11 +97,13 @@ When `contextEnabled` is on (default), user messages are auto-prepended with:
97
97
  - Clipboard paste
98
98
  - File-picker button
99
99
 
100
- Images ride along as base64; multimodal supported. Pasted images are also persisted to `<workspace>/.halo/assets/web/inbound/web/<date>/` so a `[图片已保存: /abs/path]` marker survives page reload and renders as a click-to-preview chip (shared with the WeChat channel's inbound media flow).
100
+ Images ride along as base64; multimodal supported. Before sending, every attachment is downscaled (long edge ≤ 1568) and re-encoded as JPEG; an SVG is rasterized the same way (long edge 1024, on white), since vision input only takes jpeg / png / gif / webp. An image the browser can't decode (e.g. HEIC / TIFF in Chrome) and that isn't one of those four is refused when attached, with an inline notice naming the file — it is never sent only to be dropped server-side. Pasted images are also persisted to `<workspace>/.halo/assets/web/inbound/web/<date>/` so a `[图片已保存: /abs/path]` marker survives page reload and renders as a click-to-preview chip (shared with the WeChat channel's inbound media flow).
101
101
 
102
102
  ### Inline media chips
103
103
  Any message containing `[图片/视频/语音/文件 已保存: /path]` markers (WeChat + web) or a leading `MEDIA: /path` line (agent-emitted, e.g. from `wechat-send`) renders a compact chip with filename + icon. Clicking opens a full-size preview modal (image/video/audio inline, file → download link). The modal has a Download button (top-right, next to close) for image/video/audio; the media URL carries a per-open cache-buster (`&t=<timestamp>`) so overwritten files (same path, new bytes) always show current content. Paths inside the active workspace or under the OS temp dir (`/tmp/`) are previewable; everything else degrades to a non-clickable chip.
104
104
 
105
+ Images in chat (the modal's full-size image, a user message's screenshot strip and its zoom, markdown images in replies) never show the browser's broken-image icon: a themed pulsing box while loading, the image once loaded, and an `ImageOff` icon + "Image unavailable" / 「图片无法加载」 box if it fails (`shared/components/safe-image.tsx`).
106
+
105
107
  ### Live capture (desktop only)
106
108
  Lets the agent *see something live* on demand. Desktop client (Electron) only — the entry points never render in a plain browser. Borrows the meeting-app "share" model: the user binds one source, then the agent requests a frame when it actually needs to look.
107
109
 
@@ -121,11 +123,17 @@ A second channel beyond text: a visual space the agent drives in real time to ex
121
123
 
122
124
  - **What it is.** A self-contained animated particle canvas at `<workspace>/.halo/canvas/self.html` — a breathing core that reacts to the cursor (knows when it's watched), can form words/CJK/ASCII-from-emoji, play choreographed sequences, and gesture (pulse/flash/shake). Zero external references (no CDN/remote fonts) — ships and runs offline.
123
125
  - **Seeding.** Force-copied from `packages/server/templates/canvas/self.html` into every workspace on open (platform-owned, like built-in skills). The `self` built-in skill (wired into the default agent) teaches the agent it has this face and how to drive it.
124
- - **Opening it.** The ✨ button in the chat-input toolbar opens the face in the editor preview (switches to Explorer, render-mode on → lands on the live face) and posts `self.intro()` once it has mounted, so there's always a greeting when a human turns to look. The greeting is driven solely by this open action — the page does **not** self-fire it on load — so it plays exactly once, on both first and subsequent opens. The user can also just open the file directly (no auto-greeting in that path).
125
- - **Driving it.** The agent emits `<<<SHOW: …js… >>>` markers in a reply; Halo forwards the payload **verbatim** (it never parses it) to the open preview iframe via `postMessage`, where it's `eval`'d against the face's `self` API (`say`/`play`/`react`/`pulse`/`flash`/`shake`/`intro`/`voice`) inside the sandboxed iframe. Markers are stripped from the rendered chat (like `<<<CAPTURE>>>`) — the user sees the face move, not the code. Multiple markers in one reply **queue and play in order**.
126
+ - **The face toggle.** The ✨ button in the chat-input toolbar is a **toggle** (pressed = highlighted, filled icon), remembered **per workspace** in localStorage (`halo_face_on:<projectId>`) and restored on refresh.
127
+ - **On** → the editor gets a **pinned face tab**: first in its pane, no ×, skipped by every close action (Close / Close Others / Close to the Right / Close Saved / Close All, Ctrl+W), never written to the `halo_tabs:<projectId>` layout (the toggle restores it). Switching to another tab **hides it without unmounting** — the iframe stays alive, so voice and animation keep going and `<<<SHOW>>>` still lands. It lives in one pane only (the pane focused when it was turned on; a split never copies it). Turning it on switches to Explorer, focuses the tab and posts `self.intro()` once the iframe has loaded; a refresh-restore doesn't greet or steal focus. While on, opening `.halo/canvas/self.html` from the file tree / quick open jumps to the pinned tab instead of a second copy.
128
+ - **Off** → the tab goes away and the iframe unmounts; `self.html` opens as an ordinary file again (no auto-greeting).
129
+ - **Context line.** While on, every user message from the admin chat carries `[Face open: .halo/canvas/self.html]` — regardless of the "attach editor context" switch — followed by what the face reported since the previous message: `[Face open: … · last: js ok, show a.png fail, voice blocked (needs a click)]`. Session titles strip the line (as they strip `[Currently viewing: …]`). Nothing is sent when the toggle goes off; other channels never get it.
130
+ - **Face receipts.** The face reports back over `postMessage` (`haloFaceAck: '<short text>'`): code ran / threw, a picture shown / failed, a voice clip playing / blocked by autoplay / ended, the user's taps. The admin accepts these **only from a registered face iframe** (message `source` check), keeps the last 8 (identical text kept once), and **never wakes the agent** with them — they ride the next user message's `[Face open]` line, then clear; turning the toggle off (or on) clears them too.
131
+ - **Face snapshots.** `self.snap()` makes the face post a JPEG of what's on it (`haloFaceSnap: {data, mimeType}`); the admin sends it to the session on screen as an image message `[Face snapshot]` (raw send like `<<<CAPTURE>>>`: no `[Face open]` line, no receipts on it). Loop guard: at most one snapshot per round (one `chat:complete`'s replies), and a round the snapshot itself started can't snap again (receipt `snap skipped (chain)`) until the user sends a message. A snapshot arriving after the user switched chat tabs is dropped (`snap skipped (tab switched)`).
132
+ - **Driving it.** The agent emits `<<<SHOW: …js… >>>` markers in a reply; Halo forwards the payload **verbatim** (it never parses it) to the open face iframe via `postMessage`, where it's `eval`'d against the face's `self` API (`say`/`play`/`react`/`pulse`/`flash`/`shake`/`intro`/`voice`/`show`/`snap`/…) inside the sandboxed iframe. Only previews of `.halo/canvas/self.html` receive them — other HTML previews never do. Markers are stripped from the rendered chat (like `<<<CAPTURE>>>`) — the user sees the face move, not the code. Multiple markers in one reply **queue and play in order**.
126
133
  - **Engine vs. expression.** `self.html` is a stable *engine* (defines how the face can move); the agent expresses itself by sending runtime JS, **never** by editing the file — so the force-copy-on-open never clobbers anything meaningful. The engine only changes when the platform adds a new capability (template edit + `TEMPLATE_VERSION` bump).
127
- - **No open preview = no-op.** `<<<SHOW>>>` only reaches a mounted preview; if the face isn't open the marker is silently dropped (the skill tells the agent to invite the user to open it rather than rely on a marker landing in the void).
128
- - **Identity.** Deliberately nameless ("HELLO / A MIND / IS HERE / BEYOND WORDS") — the conversational identity is user-configurable and the model may not be Claude, so the face never hard-codes a name.
134
+ - **No open preview = no-op.** `<<<SHOW>>>` only reaches a mounted face; if the face isn't open the marker is silently dropped (the skill tells the agent to rely on `[Face open: …]` and otherwise invite the user to turn the face on, rather than rely on a marker landing in the void).
135
+ - **Identity.** The opening says only "Hi, I'm Halo." — or 「你好,我是 Halo。」 when the admin UI is in Chinese (the language at the moment the face opens; switching language later doesn't replay it) — Halo is the product the user opened, not a model name. The conversational identity stays user-configurable and the model may not be Claude, so the face never hard-codes a model name.
136
+ - **Theme.** The face wears the admin's current theme (dark / light / midnight / warm, and any theme added later) and re-colours live on a theme switch without interrupting what it's playing.
129
137
 
130
138
  ### Token usage
131
139
  `TokenRing` shows live context window usage. Crossing `model.compressAt` (default 90%) auto-triggers compact.
@@ -47,7 +47,7 @@ Non-text files go through `FilePreview`, which looks up a **plugin** in the prev
47
47
  - **Parquet** — server-paginated table (schema + 100-row pages), same pager as CSV/SQLite
48
48
  - **SQLite / DB (.db / .sqlite / .sqlite3)** — server-paginated table with a table-selector sidebar (name + row count per table); opened read-only, one page (100 rows) per request
49
49
  - **PPTX / PPT** — `pptx-preview` list mode. Fidelity is approximate — complex animations / SmartArt / embedded fonts may not render perfectly; users can download for the exact source. Flagged `heavy: true` so only the *active* pptx mounts (canvas-based rendering needs the main thread). **Speaker-notes sidebar**: notes are extracted from the pptx zip in presentation (play) order and listed on the left; clicking a note scrolls to the corresponding slide (a domIndex mapping bridges play order to pptx-preview's part-filename render order, so reordered decks still land on the right slide). Collapsible with the state remembered in localStorage (`halo.pptxNotesHidden`); the sidebar doesn't appear at all when no slide has notes. Decks with dangling `[Content_Types].xml` overrides (e.g. some WPS exports) are repaired before rendering; if zero slides render anyway, a real error UI is shown instead of a silent black wrapper
50
- - **Images / video / audio** — native `<img>` / `<video>` / `<audio>`, supports HTTP Range for seek-without-full-download
50
+ - **Images / video / audio** — `<img>` (via `SafeImage`: pulsing placeholder while loading, "Image unavailable" box on failure, never the broken-image icon; zoom / pan unchanged) / native `<video>` / `<audio>`, supports HTTP Range for seek-without-full-download. Markdown-preview images use the same placeholder.
51
51
 
52
52
  All previews share a **standard header** (`PreviewShell`) with filename, Download, Open-as-Text, and an `extraToolbar` slot for plugin-specific buttons (e.g. DOCX Print, XLSX sheet tabs).
53
53
 
@@ -62,6 +62,8 @@ File types without a built-in plugin can be handled by an **installed extension*
62
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
63
63
  - **Fallback**: a file with no built-in plugin and no installed extension shows a static page — "no built-in preview for this type; extensions for more file types are on <hub> ↗", where `<hub>` is the extension skill's `hub_repo` (default halo-hub; a local path / ssh remote shows as text) — plus Open as text / Download. No online lookup
64
64
  - **Editable extensions** (manifest `capabilities: ["save"]`): the iframe reports dirty → tab shows the dirty dot → header **Save** button (Ctrl/Cmd+S is *not* wired for extension tabs — Monaco isn't mounted) → `PUT /api/files/raw`. If the file changed on disk since it was loaded the save gets a 409 and the user is asked "changed on disk … Overwrite?". An external change while the tab is clean reloads it; while dirty, local edits are kept silently and the conflict surfaces at the next save
65
+ - **Bundle extensions** (manifest `bundle: true`, e.g. the `htrans` meeting recorder): a *directory* named with the extension's suffix (`Standup.htrans/`) is one document. In the Explorer it shows a package icon; clicking the row (or Enter) opens it as a tab, and only its chevron expands it so the files inside stay browsable. Right-click → Open to the Side works on it. **New File** with a bundle suffix (`Standup.htrans`) creates the directory and opens it. The extension reads and writes only inside that directory. While it reports busy (e.g. recording) the tab shows the dirty dot, asks before closing, and stays mounted when you switch tabs. Ctrl/Cmd+S / Save do nothing on it. A bundle tab survives a reload while the extension is installed and supported here; otherwise the tab is dropped and the directory is an ordinary folder again
66
+ - **Platforms** (manifest `platforms`: `web` / `desktop-mac` / `desktop-win` / `desktop-linux`; omitted = everywhere): on an excluded platform (the Halo desktop app is detected by its user agent) the extension acts as if it were not installed. The fallback page says `"<name>" only runs on: <platforms>`, and Settings → Extensions shows a platforms badge plus a gray "not supported here" tag. Manifest capability `media` lets the extension use the microphone and screen capture
65
67
  - **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
66
68
  - **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
67
69
  - **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
@@ -52,6 +52,7 @@ Visual directory browser opened by the 📁🔍 button:
52
52
  VS Code-style highlight selection (no checkboxes):
53
53
  - **Click**: select and highlight; a file also opens in the editor after a short (300 ms) delay, a folder toggles open/closed
54
54
  - **Double click**: open the file in an editor tab right away (cancels the delayed open)
55
+ - **Bundle directory** (a folder whose suffix an installed bundle extension claims, e.g. `Standup.htrans/`, shown with a package icon): click, double-click or Enter **opens** it in its extension like a file; only the chevron expands it
55
56
  - **Ctrl/Cmd + click**: toggle multi-select
56
57
  - **Shift + click**: range select
57
58
 
@@ -59,6 +60,7 @@ VS Code-style highlight selection (no checkboxes):
59
60
  - Selected files/folders drag onto a target dir
60
61
  - Multi-file drag supported
61
62
  - Each file goes through `POST /api/files/rename` with `{oldPath, newPath}`
63
+ - Moving (also deleting / renaming) a bundle directory — or a folder containing one — whose tab is busy (e.g. recording) first asks the "Close anyway?" confirm; on OK the tab is closed before the file operation, on Cancel nothing happens
62
64
  - **Spring-loaded expand** (applies to both tree-internal drags and OS-file drops): a collapsed folder does **not** expand the moment a drag passes over it — it unfolds only after the drag hovers on it for ~600ms (VSCode/Finder behaviour; the timer cancels on drag-leave). Dropping into a collapsed folder expands it immediately to show the result
63
65
 
64
66
  ### Right-click menu
@@ -67,10 +69,10 @@ Items shown depend on the click target. With several items selected the menu off
67
69
 
68
70
  | Action | Shown when | API / behavior |
69
71
  |---|---|---|
70
- | New File / New Folder | Always | `POST /api/files/new` · `mkdir`; auto-expands the parent folder so the inline input is visible |
72
+ | New File / New Folder | Always | `POST /api/files/new` · `mkdir`; auto-expands the parent folder so the inline input is visible. A New File name ending in a bundle suffix (`Standup.htrans`) creates the bundle **directory** and opens it |
71
73
  | Open in Integrated Terminal | Always | Spawns a terminal at the target dir (or file's parent) |
72
74
  | Reveal in File Manager | Desktop app only | Opens the OS file manager (Finder / Explorer / Linux FM) at the target — a folder opens itself, a file is highlighted in its parent dir. Hidden in a plain browser (gated on the `window.haloReveal` IPC bridge exposed by the desktop preload) |
73
- | Open to the Side | File only | Splits the editor and opens the file in the right pane |
75
+ | Open to the Side | File or bundle directory | Splits the editor and opens the file (or bundle) in the right pane |
74
76
  | Download | File only | `GET /api/files/download?path=...` |
75
77
  | Rename | Single file/folder | `POST /api/files/rename` |
76
78
  | Delete | Single or multi-select | `DELETE /api/files?path=...` (with confirm) |
@@ -42,7 +42,7 @@ tavily-web-search: # skill id from skills/<id>/config.yam
42
42
  ```
43
43
 
44
44
  The path always reads as `<namespace-id>.<kind>s.<key>`:
45
- - `<namespace-id>` is `general`, a provider id, a skill id, or an agent id
45
+ - `<namespace-id>` is `general`, a provider id, a skill id, an agent id, or `ext-<extension-id>` (a canvas extension)
46
46
  - `<kind>` is `param` or `secret`
47
47
  - `<key>` is the leaf, dotted for grouping (e.g. `general.compact.keep_messages`)
48
48
 
@@ -96,6 +96,10 @@ Built-in skills declare theirs the same way — today `extension`, with a plain
96
96
 
97
97
  Global agents declare theirs the same way in `agents/<agent-id>/agent-config.yaml` (same `params:` / `secrets:` lists; shown under Settings → Agents).
98
98
 
99
+ ### Extension params/secrets — `halo-extension.json` `settings`
100
+
101
+ An installed canvas extension declares its own config in its manifest: `"settings": { "params": [field…], "secrets": [field…] }`, same field format as a skill's `config.yaml` (`key`, `description`, `description_zh`, `default`, `type`, `options`) but validated strictly — a malformed field makes the whole manifest an error (red row in Settings → Extensions) instead of being skipped. Each such extension becomes one section with `source: "extension"`, namespace **`ext-<extension-id>`** (the prefix keeps it from colliding with a skill / provider namespace) and the manifest `name` as title, listed in the left nav under its own group **Extension settings / 扩展配置**, right below the Extensions entry. Every field is `globalOnly` (stored in `~/.halo/secrets/settings.yaml` only; a workspace write is rejected with 400) and every `secrets` field is masked. Only server code reads the values (e.g. the Meeting Recorder's Amazon Transcribe region and keys) — they never reach the extension, the manifest or the hub. The section appears / disappears live: installing or removing an extension pushes `extension:changed`, and the open Settings page re-fetches the schema. See [design/canvas-extensions.md → Extension settings](../design/canvas-extensions.md#extension-settings).
102
+
99
103
  ### General — built-in
100
104
 
101
105
  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`).
@@ -134,7 +138,7 @@ There is no "default provider" setting: a newly created agent's `model:` is a co
134
138
  | `secret` | no | `true` → masked in API responses + password input in UI |
135
139
  | `restartRequired` | no | `true` → read once at server start; the Settings page shows a restart notice after saving it. Built-in `general` section only |
136
140
  | `advanced` | no | `true` → rendered in the collapsed "Advanced" area. Built-in `general` section only (every field except `language` / `theme`) |
137
- | `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 |
141
+ | `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 and forced on every extension field — provider / skill / agent yaml declarations don't read it |
138
142
 
139
143
  ## Scope: global vs. workspace
140
144
 
@@ -187,9 +191,13 @@ A malicious skill that tries `curl -H "Bearer {{aws-bedrock-claude-invoke.secret
187
191
 
188
192
  `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.
189
193
 
194
+ ## Left nav
195
+
196
+ Groups top to bottom: **System** (General · Security · Extensions, each with a leading icon), **Extension settings**, **Model providers**, **Agents**, **Skills**, and **Orphans** when there are any. Each group header is a quiet toggle (chevron + smaller muted label) that collapses its items; all groups start expanded, and the collapsed ones are remembered per browser (`localStorage` `halo_settings_navCollapsed`, collapsed ids only — so a group that appears later starts expanded).
197
+
190
198
  ## Security view (change password + logout)
191
199
 
192
- 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.
200
+ A **Security** entry in the left nav (inside the System group, below General) 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.
193
201
 
194
202
  **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.
195
203
 
@@ -31,10 +31,10 @@ The Bottom Panel (Chat + Terminal) can also be **floated** into a draggable wind
31
31
  | Icon | Tab | Sidebar | Main content | Bottom panel |
32
32
  |---|---|---|---|---|
33
33
  | FolderTree | Explorer | File tree + ops | Canvas (Monaco + previews) | Chat + Terminal |
34
- | GitBranch | Source Control | Changes + commit box | Diff / history graph | — |
35
34
  | MessageSquare | Sessions | Session list | Session message viewer | — |
36
- | Zap | Skills | Skill list | SkillsMain (full height) | — |
35
+ | GitBranch | Source Control | Changes + commit box | Diff / history graph | — |
37
36
  | Bot | Agents | — | Agent config editor + Test (full width) | — |
37
+ | Zap | Skills | Skill list | SkillsMain (full height) | — |
38
38
  | MessageCircle | Channels | Channel list | Channel config editor | — |
39
39
  | Sparkles | Evolution | Run list + status filters | Evolution main (run/apply review) | — |
40
40
  | Clock | Cron | Job list | Cron job detail / form + run audit | — |