@turmind/halo 1.5.8 → 1.5.10
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/220.7566bc89f3d98993.js +1 -0
- package/admin-out/_next/static/chunks/388-d5950f264d91625a.js +1 -0
- package/admin-out/_next/static/chunks/989-1797287c46139412.js +1 -0
- package/admin-out/_next/static/chunks/app/page-44ab337b2f071e4d.js +1 -0
- package/admin-out/_next/static/chunks/{webpack-edf248e07333566d.js → webpack-efd27999305b080a.js} +1 -1
- package/admin-out/_next/static/css/d53aa54212002364.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/canvas-extensions.md +60 -10
- package/bundled-docs/design/cron.md +6 -6
- package/bundled-docs/design/evolution.md +47 -15
- package/bundled-docs/design/express-self.md +90 -23
- package/bundled-docs/design/storage.md +1 -0
- package/bundled-docs/dev/api.md +7 -6
- package/bundled-docs/dev/desktop-packaging.md +26 -0
- package/bundled-docs/guide/agents.md +1 -1
- package/bundled-docs/guide/chat.md +3 -0
- package/bundled-docs/guide/workspace.md +2 -2
- package/bundled-docs/requirements/agent.md +1 -1
- package/bundled-docs/requirements/chat.md +29 -15
- package/bundled-docs/requirements/editor.md +4 -1
- package/bundled-docs/requirements/explorer.md +4 -2
- package/bundled-docs/requirements/settings.md +11 -3
- package/bundled-docs/requirements/workspace.md +2 -2
- package/dist/index.js +1798 -1325
- package/package.json +2 -1
- package/templates/agents/__evo_agent__/AGENT.md +7 -0
- package/templates/agents/__score__/AGENT.md +183 -71
- package/templates/canvas/self.html +469 -75
- package/templates/skills/cron/SKILL.md +59 -4
- package/templates/skills/cron/manage-cron.py +158 -24
- package/templates/skills/self/SKILL.md +59 -16
- package/admin-out/_next/static/chunks/220.e250bbbf65d24094.js +0 -1
- package/admin-out/_next/static/chunks/422-cc74b1fdd3af5415.js +0 -1
- package/admin-out/_next/static/chunks/989-df2401d289d09a7e.js +0 -1
- package/admin-out/_next/static/chunks/app/page-ed2976abf85a335d.js +0 -1
- package/admin-out/_next/static/css/b8c87b919758c52b.css +0 -3
- /package/admin-out/_next/static/{T3x-q3QCOgSpfydoH53XD → A-Rjx2yltZDKaSO1qVv7K}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{T3x-q3QCOgSpfydoH53XD → A-Rjx2yltZDKaSO1qVv7K}/_ssgManifest.js +0 -0
|
@@ -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
|
|
58
|
-
- **Rows**: one line per row,
|
|
59
|
-
- Each row
|
|
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.
|
|
@@ -102,18 +102,26 @@ Images ride along as base64; multimodal supported. Before sending, every attachm
|
|
|
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
|
-
|
|
106
|
-
Lets the agent *see something live* on demand. Desktop client (Electron) only — the entry points never render in a plain browser. Borrows the meeting-app "share" model: the user binds one source, then the agent requests a frame when it actually needs to look.
|
|
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`).
|
|
107
106
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
- **Camera** (Camera button) — toggles the webcam on. Hidden entirely on a machine with no camera.
|
|
107
|
+
### Live capture (screen share + camera)
|
|
108
|
+
Lets the agent *see something live* on demand. Works in the desktop client (Electron) **and** a plain browser. Borrows the meeting-app "share" model: the user binds a source, then the agent requests a frame when it actually needs to look.
|
|
111
109
|
|
|
112
|
-
|
|
110
|
+
Two source kinds in the chat-input toolbar, **independent** — either one alone, or both at once:
|
|
111
|
+
- **Screen / window share** (MonitorUp button). Desktop: opens a picker grid of screens + app windows; a bound window can be grabbed even while it sits in the background. Browser: the browser's own `getDisplayMedia` picker; the share is kept live while bound (frames drawn from it on demand). No screen button where `getDisplayMedia` is missing (mobile browsers) — the camera button stays.
|
|
112
|
+
- **Camera** (Camera button) — opens a picker with a live preview (even for a single webcam), then binds the chosen device. Hidden entirely on a machine with no camera. In the browser the camera stream is held open while bound (so a request that lands while the page has no focus doesn't re-prompt or stall) and released on unbind.
|
|
113
|
+
|
|
114
|
+
Each button is a toggle: grey when off, primary-coloured icon when on. **Clicking an active button again turns that source off** — no picker; in the browser the share / camera stream stops. To switch screen source or camera device, turn it off and on again. Each bound source also shows as a chip right after the agent selector (icon + name, primary colour, X to unbind just that one). In the browser, the browser's own "Stop sharing" bar unbinds the screen (the camera stays).
|
|
115
|
+
|
|
116
|
+
Once bound, a frame is **not** attached to every message. Instead a one-line instruction is injected into the next send telling the model how to ask for a look:
|
|
117
|
+
- Only one source bound — "the user is sharing the «X» window" / "sharing «X» from the browser" / "the user has turned the camera on", and the model outputs a line containing exactly `<<<CAPTURE>>>` when it needs the current view.
|
|
118
|
+
- Both bound — one combined instruction ("the user is sharing «X» and has the camera on"): `<<<CAPTURE:screen>>>` for the screen only, `<<<CAPTURE:camera>>>` for the camera only, bare `<<<CAPTURE>>>` for both.
|
|
119
|
+
|
|
120
|
+
On turn completion the frontend collects every capture marker in the turn's replies (union; a bare marker = every bound source; a request for a source that isn't on falls back to every bound one, so a request never goes unanswered), grabs each requested source and sends them back as **one visible image message** (screen first) — the model sees them on its following turn. So it's a cross-turn round-trip: model asks → frame(s) sent back → model answers next turn. The returned frames are shown inline on the user bubble so you can see exactly what was sent; a source that failed contributes a short failure note instead of its image. Markers are hidden from the rendered reply.
|
|
113
121
|
|
|
114
122
|
Constraints:
|
|
115
|
-
- Shown only when the selected agent's model accepts image input (capture is pointless on a text-only model); switching to a text-only model auto-unbinds.
|
|
116
|
-
-
|
|
123
|
+
- Shown only when the selected agent's model accepts image input (capture is pointless on a text-only model); switching to a text-only model auto-unbinds both.
|
|
124
|
+
- Desktop: screen share needs macOS **Screen Recording** permission; the camera prompts for **Camera** permission on first use, with an "Open Settings" path if previously denied. Browser: the page must be served over HTTPS or from localhost (`getDisplayMedia` / `getUserMedia` are absent elsewhere, so the buttons don't render); a denied camera shows a hint pointing at the address bar's site settings.
|
|
117
125
|
- Binding is **in-memory only** and shared by all chat tabs — a page reload or restart requires re-selecting.
|
|
118
126
|
|
|
119
127
|
### The agent's face (`self.html`)
|
|
@@ -121,11 +129,17 @@ A second channel beyond text: a visual space the agent drives in real time to ex
|
|
|
121
129
|
|
|
122
130
|
- **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
131
|
- **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
|
-
- **
|
|
125
|
-
- **
|
|
132
|
+
- **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.
|
|
133
|
+
- **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.
|
|
134
|
+
- **Off** → the tab goes away and the iframe unmounts; `self.html` opens as an ordinary file again (no auto-greeting).
|
|
135
|
+
- **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.
|
|
136
|
+
- **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.
|
|
137
|
+
- **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)`).
|
|
138
|
+
- **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
139
|
- **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
|
|
128
|
-
- **Identity.**
|
|
140
|
+
- **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).
|
|
141
|
+
- **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.
|
|
142
|
+
- **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
143
|
|
|
130
144
|
### Token usage
|
|
131
145
|
`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** —
|
|
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,9 @@ 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
|
+
- **Export from an editable extension** (e.g. draw.io *File ▸ Export as*, Excalidraw main menu *Export PNG / SVG*): the extension iframe can't download files, so the exported image is saved **into the workspace, next to the open file** (`flow.drawio` → `flow.png`). If that file already exists the user is asked whether to overwrite it. On success a banner "Exported to <path>" appears above the preview with a **Download** link. Exporting never marks the tab modified and never changes the open file. Only on Halo builds newer than 1.5.9; on older hosts the extension shows no working export (drawio ≥ 1.1.0 / excalidraw ≥ 1.1.0 hide or fall back on their own)
|
|
66
|
+
- **Bundle extensions** (manifest `bundle: true`, e.g. the `htrans` meeting recorder): a *directory* named with the extension's suffix (`Standup.htrans/`) is one document. In the Explorer it shows a package icon; clicking the row (or Enter) opens it as a tab, and only its chevron expands it so the files inside stay browsable. Right-click → Open to the Side works on it. **New File** with a bundle suffix (`Standup.htrans`) creates the directory and opens it. The extension reads and writes only inside that directory. While it reports busy (e.g. recording) the tab shows the dirty dot, asks before closing, and stays mounted when you switch tabs. Ctrl/Cmd+S / Save do nothing on it. A bundle tab survives a reload while the extension is installed and supported here; otherwise the tab is dropped and the directory is an ordinary folder again
|
|
67
|
+
- **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
68
|
- **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
69
|
- **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
70
|
- **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
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
-
|
|
|
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 | — |
|