Remove the scope system end-to-end (llm_models.scope column, agent meta
scope field, scope-based tier in model selection, UI checkboxes/pills):
it was only a soft ranking hint, had drifted (6 UI scopes vs 3 used by
agents, 'general' not even selectable) and duplicated what strength
already decides. Strength stays the single AUTO-selection axis.
Compaction: the summary model is now pickable from the Settings page
via a new PropertyType::LlmModel config property (registry key
compaction_model), instance-wide and live (no restart). Fallback chain:
explicit pick -> compaction.strength from config.yml -> priority order;
a deleted configured model degrades to AUTO. ContextCompactor reads the
key at compact time through GlobalConfigManager.
Add an inline auth gate to mobile.html that checks /api/auth/me on load.
Skipped when ?native=true (the iOS shell handles auth in background).
Includes a vanilla JS login form (no Lit) reusing the existing login styles.
Bump version to 0.1.2.
Auto-scroll now yields when the reader scrolls away from the bottom, so a
fast-streaming reply no longer fights someone reading the start. Stickiness is
a flag driven by a passive scroll listener (not a per-flush distance check,
which breaks when one flush adds more than the threshold of content); scrolling
back within the band re-arms it.
Centralised in chat-session.js (_scrollToBottom(force) + _forceScrollToBottom
+ _messagesContainer hook), removing the duplicated overrides in copilot.js /
chat-page.js. Force-scroll is used where the latest must be shown: history
load, chat reopen, approval/clarification prompts, and message send.
A sticky 'jump to latest' affordance appears only while scrolled up.
- GET /api/file returns ETag (mtime+size) + X-Writable on disk files;
PUT /api/file accepts optional if_match -> 409 Conflict on stale version
(last-write-wins preserved when omitted), echoes the new ETag
- FileViewerBase: View | Edit tabs for .md when the caller can write;
source textarea with Save/Cancel, live preview while editing
- Watcher no longer clobbers the buffer mid-edit: while editing with
unsaved changes it probes the server ETag and only raises a conflict
when the remote actually moved on (own-save echo is ignored)
- Conflict banner: Reload remote | Copy mine, then reload | Overwrite
- i18n (en/it/fr) + CSS; docs/projects.md updated
install.sh / install-nightly.sh:
- Write .release-channel file ('release' or 'nightly') for future updates
- install.sh also writes .release-version (for update --check)
update.sh (new):
- Reads .release-channel to determine which channel to pull from
- Release: checks releases/LATEST vs .release-version, skips if current
- Nightly: always downloads latest
- Stops service before extraction, restarts after
- Rebuilds Python venv on update
ci/package.sh:
- Include update.sh in distribution tarball
Web / mobile:
- Add settings-page component for mobile
- Wire settings page into mobile-app navigation
- Chat page: load current user (/api/auth/me) for sender identity
- Full mobile.css redesign
- i18n: add mobile settings strings (en/fr/it)
Turn the flat sidebar into a data-driven nav: each entry declares a
group (workspace/extensions/config/dev) and a numeric priority, and each
section renders by sorting on that key. Split axis is function, not
permission — adminOnly/debugOnly gate individual entries and an empty
section is hidden, so Configuration vanishes for non-admins without a
section-level role check.
- Shared folders moves into 'Your space' (still admin-gated per-entry).
- Plugin pages merge into the workspace group on the same priority line
(>=100 by default); rebase the two in-repo plugins accordingly.
- Configuration and Development are collapsible, closed by default,
state persisted in localStorage. Simple mode unchanged.
- Rename nav.catalog to 'Connectors Catalog' to disambiguate from the
Plugin Catalog; add nav.section.* header keys (en/it/fr).
- Replace dead .sidebar-section-toggle CSS with real section classes.
Fills a gap the blueprint names: the admin had to hand-author every
`mcp_catalog` entry. A remote feed of vetted connectors now proposes them
and the admin installs — the feed is *consultative*, so §14's risk axis is
untouched and the trust anchor stays on the box.
Marketplace client (`src/frontend/api/marketplace.rs`):
- Fetches the feed server-side (it sends no CORS headers) and caches it;
icons are proxied for the same reason.
- Verifies every declared SHA-256 before writing, fail-closed and
all-or-nothing. Feed-supplied paths are refused if they escape
`./scripts/<id>/`. Importing an `mcp_local` entry still demands the
admin-only `mcp.register_local_script`.
- Translates the feed's vocabulary into Skald's: `user`→`per_user`,
`mcp_local`→`local_script`. Scope is read, never inferred from transport
(a remote connector can be per-user — that is what `mcp.register_remote`
is for), and an unreadable `type` fails closed to the answer needing more
authority. The feed's `llm_short_description` maps to `description`, the
column `render_mcp_list` puts in front of the LLM for `activate_tools()`.
- Feed URL is config (`marketplace.url`), not a constant: an on-premise
product must not hard-require reaching one vendor's host.
Two silent failures found while wiring it:
- `transport_of` maps anything unknown to Stdio, so the feed's
`streamable-http` would have tried to spawn a command. Normalised on import.
- Some servers want their key as a query param, not a bearer header, and say
so with a `{key}` placeholder. Substituted at connect time in
`global_row_spec`/`user_row_spec` — never at rest, so the key stays in its
own column and the stored URL stays a template.
Pages, split by the question each answers:
- Connectors — what runs (`UserMcpView` = global ∪ per-user) and what I can
add. Same page for everyone; the admin just has more verbs. One Available
list with the verb per row: `per_user`→Activate, `global`→Enable globally.
Enabling a global is the admin's counterpart to activating a per-user one,
so the catalog picker dropdown is gone — the entry comes from the row.
- Connector Catalog (admin) — what this box offers. One `Add connector`
with two sources: marketplace first (vetted, hashed), manual second
(unvetted by nature) — the order mirrors the trust model.
- Marketplace (admin) — reached from the catalog, not the sidebar: it is a
destination of an action, not a place.
`available()` no longer returns `McpGlobalServerRow`: that row carries
`api_key` and this view now reaches every logged-in user. A slim `GlobalView`
crosses instead, and an admin sees every global (with `can_use` marking their
own) so one enabled for someone else stays manageable.
Also fixes `connectors-page` having no CSS rule at all — every sibling page
has one, so it never got `flex: 1` and left an empty column beside it.
- New skald-setup crate: interactive first-run wizard that creates the
admin user, prompts for encryption choice and password
- Auth system: session-based login/logout with cookie, guard middleware
- Roles API: CRUD for data-driven roles, seeded on first boot
- Users management API: create, list, edit, delete users
- Setup state API: check if first admin has been created
- Frontend: login-page, setup-page, users-page, roles-page, profile-page
components with corresponding CSS
- Topbar: avatar dropdown with profile link and logout
- Sidebar: nav entries for Users and Roles (admin only)
- Page shell CSS: layout support for the new pages
- build.sh: builds both skald and skald-setup binaries
- run.sh: runs skald-setup before the server loop
- CLAUDE.md: updated workspace layout and build/run docs