Appearance renames to General and absorbs Files; Token savings moves under
Models. The onboarding provider gallery/key form is extracted to a shared
providers/ProviderSetup module rendered by both surfaces, with Settings-only
extras: last-used on cards, Remove key (new DELETE /v1/providers/{name}),
and a cross-provider composer-picker card.
Every persona used to default to enabled, so a new install's picker listed
Code/Ops/etc. before the user ever chose them. Now, absent explicit state,
only the default persona (Coworker) is enabled; everything else is opt-in
from Settings ▸ Personas. Explicit user choices persist and always win,
and disabled personas still resolve so live sessions keep working.
The sidecar binds to 127.0.0.1, but a page in the user's browser can
still reach loopback. With CORS set to '*' and no auth on the WebSocket,
any website the user visited could read GET /v1/sessions cross-origin,
pick a session, open /ws/session/{id}, and drive it into shell/file
tools (RCE-class, worst on the fixed-port browser build). CORS never
covers WebSockets, so the socket was fully open.
Pin both surfaces to the desktop webview origins (tauri://localhost,
Windows http(s)://tauri.localhost) and localhost dev/browser builds:
CORS via allow_origin_regex; the WS rejects a disallowed browser Origin
with 1008 before accept. Requests with no Origin (native clients, curl,
tests) still pass — the gate targets browsers, which send an unforgeable
Origin. Verified live: foreign origin gets no ACAO, localhost + tauri do;
the running app's session still loads over the WS.
Disabled personas kept their sidebar sections: the grouped layout's
never-orphan rule gives any persona with unarchived sessions a section,
so disabling looked like a no-op when history existed (owner's manual
testing pass, 2026-07-04).
Owner call: archive-all-on-disable. manager.set_persona_enabled flips
the flag and archives the persona's real sessions (unarchived, non-
__run__) atomically server-side; both persona routes delegate to it and
report archived_sessions. Re-enable never unarchives — that would
rewrite the user's archive state; history returns via Show archived.
GUI: unchecking Enabled on Settings ▸ Personas arms an inline confirm
(same two-step idiom as row delete) only when the persona has
conversations — 'Disabling archives its N conversations' with
Disable / Keep enabled; zero-session disables stay instant. App
refetches sessions on PERSONAS_CHANGED so the section leaves the
sidebar immediately instead of on the next poll.
Tests: server test for archive/skip/re-enable semantics across both
routes; two e2e flows (confirm + skip) with backend-parity archiving in
the fixture mock and a seeded ops session.
Browse was Tauri-gated (web pages can't obtain absolute paths from file
dialogs), leaving the browser GUI paste-a-path only — which also hurt the
open-source sidecar+browser path, not just dev. New POST /v1/workspaces/pick
runs the OS picker from the LOCAL sidecar (osascript on macOS, zenity on
Linux, off the event loop); chooseFolder() in the GUI prefers Tauri's dialog
and falls back to the sidecar's. Browse now shows everywhere: add-folder
form, folder gate, and the agent's directory-request card (which previously
rendered a Browse button that silently no-oped in the browser).
A __run__ session looked like any other chat: no way back to the runs list
and no hint it was automation-started; opening a RUNNING run showed a blank
transcript because /messages read only the persisted record (which doesn't
exist until the first turn completes). Now: session_messages prefers the
live engine's in-memory thread; run sessions (detected by the __run__ id
convention, enriched with the task title carried through navigation) get a
banner — 'Scheduled run — <task> · started by an automation · ← Back to
runs' — that reopens the task's detail page.
Chosen until the first turn, then locked for the session's life: the first
user_message's model binds the engine; later message models and set_model
are ignored server-side (not just GUI-disabled, so API callers and socket
races can't rebind a running conversation). The composer's picker becomes a
read-only pill on sessions with history. Mixed-model transcripts invite
provider-quirk breakage and unreasonable behavior — mid-session switching
was inherited chat-app convention, never a designed feature. Escape hatch
(explicit change-model with warning, for a dead provider mid-session)
deliberately deferred.
Owner repro: picked Opus in a new session, Kimi replied. The composer's
set_model could race the session's socket lifecycle — sessionRef is a silent
no-op before the socket exists, a queued set_model can die with a socket
torn down mid-reconnect (new cowork sessions ALWAYS reconnect once to adopt
their scratch dir), and the ready event overwrites the visible selection
with the resumed engine's model. Instead of patching each race, the message
now carries the composer's visible model and the engine adopts it before
running the turn — the UI is the single source of truth per turn. set_model
stays for live mid-session switching; ready still seeds initial state.
Verified live via bogus-model 404 probes (provider-named errors, zero
completion cost): message.model routes to the right vendor.
UX/IA polish (verified live, screenshots saved):
- Default-first + Chat hidden (registry): Cowork is registered first (leads the
picker as the default); Chat ships `default_surfaced=False` and is_surfaced()
falls back to each entry's default — so Chat is hidden from the picker but stays
enabled/recoverable from the Personas tab. Picker is now OpenCoworker, Code, Ops.
- Family-aware Projects (Sidebar): Projects/grouping keys off `familyOf(id) ===
"code"` instead of the hardcoded "code" id, so any code-family persona (incl.
third-party DevOps/SecOps) gets Projects, and knowledge personas (Ops, Cowork)
list flat Recents.
- Expand ≠ switch (Sidebar/App): a persona header only toggles its accordion;
the body shows the *browsed* persona's sessions (browseKey), and the chat area
changes only on session-select or New session. startNewSession(forAgent) creates
for the browsed persona.
Tests: persona/server assertions updated for the new order + Chat-hidden (Python
445 passed; 3 pre-existing SDK errors). GUI `npm run build` green.
Remaining Phase 4 (not yet built): the Pinned band, attention/liveness badges, and
making App's needsWorkspace/gatesWorkspace family-aware (still id-hardcoded).
Two launch-quality bugs. (1) Every app quit leaked the server process pair:
under PyInstaller onefile the python server is a grandchild of the GUI
(bootloader in between), so the getppid re-parenting watchdog never saw the
GUI die, and the Rust ExitRequested kill only reached the bootloader. The
GUI now passes its PID via COWORKER_PARENT_PID and the watchdog polls that
PID directly (POSIX kill(pid,0) probe; explicit handle wait on Windows),
with the Rust kill also firing on RunEvent::Exit. (2) Conversations were
persisted only when a turn finished, so a crash or quit during a session's
first turn (or while parked on an approval) silently lost it. The turn loop
now checkpoints at turn_start, permission_required, directory_requested and
iteration_end; a brand-new session gets its row at the first user message,
never at connect, so empty sessions still do not appear in Recents.
Co-authored-by: Devika <dr.drp8226@gmail.com>
Mechanical 'black .' over root package and platform/ (109 files; the
Lint workflow checks the whole repo and has been red since platform
landed unformatted, with a few more violations from community merges
whose branches never ran CI). No functional change: full platform
suite (264) and aisuite suites verified unchanged after formatting.
An agent harness built on aisuite's agent framework. Adds a coworker engine
with pluggable connectors (browser automation, integration tools), session
management, audit logging, and a permissions model, alongside a desktop GUI
surface (Tauri + React) and a multi-provider model layer with Ollama support.
Co-authored-by: Devika <dr.drp8226@gmail.com>