Skip to content

aiui v0.5.0 stabilization — Steps 1–4 (multi-window) + /render hotfix#138

Merged
iret77 merged 28 commits into
mainfrom
claude/peaceful-germain-0b232e
Jun 8, 2026
Merged

aiui v0.5.0 stabilization — Steps 1–4 (multi-window) + /render hotfix#138
iret77 merged 28 commits into
mainfrom
claude/peaceful-germain-0b232e

Conversation

@iret77

@iret77 iret77 commented May 30, 2026

Copy link
Copy Markdown
Contributor

Implements the stabilization plan end-to-end. Per-step implementation records are inline in the plan doc.

What's in

Step Summary Status
1 — Host lifetime Single exit authority host_should_exit = uninstall/update || !claude_desktop_running. Window-X = hide (I2); ExitRequested default-deny; child counter → telemetry only.
1.5 — /render cancellation-safe RenderGuard (RAII) frees the registry slot + destroys the window when the handler future is dropped → kills the 409-storm + stranded empty window from the field report.
2 — Never kill remote bridge Deleted kill_remote_mcp_stdio (pkill -f aiui-mcp, blast radius) + all 3 callers. Pin takes effect at next natural spawn. Cooperative WIRE_VERSION floor on /version+/probe.
3 — async /render + parity Opt-in x-aiui-async (additive, sync path untouched): POST202 {id}, GET /render/{id} long-poll. Both bridges poll + fall back to sync. Python gains cold-start poll, progress notifications, ReadError classification.
4 — multi-window Single-occupancy 409 gone; one window per render (label = dialog id) via a pull model (window fetches its own spec on mount → no emit/ack/ready-race). Session chip (session · session_origin); Python auto-injects hostname.
4 — tunnel Settled empirically (Mac-side probe of a live Code-tab session): Claude Desktop provides no reverse forward (no -R, no ~/.ssh/config RemoteForward); aiui already runs the correct dedicated ssh -NTR per remote, adequately hardened (ExitOnForwardFailure + ServerAlive + shared-forward detection). Piggyback impossible; no refactor needed.

Verification

cargo test 102 · clippy -D warnings clean · pytest 26 · svelte-check 0 errors.

Honest limits: Steps 1–3 are logic-heavy and unit-tested (async is additive). Step 4 multi-window is a structural window-system change verified at compile/type/unit level — GUI behaviour needs validation on the Mac (the headless remote can't launch the GUI). The remote-path integration harness remains the open cross-cutting item; it's the right home for validating multi-window. One earlier tunnel inference in the plan was wrong (it read the remote's config, not the Mac owner's) and was corrected with an on-device measurement before any code was touched — no TunnelManager was deleted.

Refs #137

🤖 Generated with Claude Code

iret77 and others added 5 commits May 30, 2026 15:47
Step 1 of the stabilization plan (docs/architecture/stabilization-plan.md):
collapse the host's lifetime onto one predicate and stop deriving "does
anyone still need aiui?" from proxies (child count, window visibility) that
0.4.43–0.4.45 kept getting wrong.

Single exit authority (Invariant I1):
  host_should_exit(explicit, cd_running) = explicit || !cd_running

- lifetime.rs: pure host_should_exit + grace_outcome + ExitAuthority latch,
  all unit-tested (the Step-2 verification mini-harness: host survives a child
  flap while Claude Desktop runs; host exits on Claude-Desktop-quit). The
  shutdown watcher keeps the last-child-disconnect edge as a *trigger only* —
  it arms a short 5s grace (was 60s) and exits solely when Claude Desktop is
  gone and no child returned. One pgrep per edge, no continuous poll.
- lib.rs: setup-window close = prevent_close + hide + Accessory demote (I2),
  never app.exit. RunEvent::ExitRequested = default-deny via host_should_exit;
  the child-count / pending-dialog veto is removed. ExitAuthority managed +
  latched by quit_app.
- http.rs + updater.ts: both update-restart paths latch ExitAuthority before
  restart()/relaunch() so the default-deny gate honours case (c).
- LifetimeStats child counter demoted to /health telemetry + start-trigger;
  it no longer gates exit.

Prove-then-delete mapping for each retired exit path (grace-expired,
setup-close-no-children, exit-requested-no-attached) is recorded in the plan
doc. One deliberate deviation from the spec's prose (arm grace on every edge
vs. "don't arm if CD alive") is documented there — it closes a CD-mid-quit
race while preserving I1 exactly.

Baseline + post-change green: cargo test (104 passed), clippy -D warnings,
svelte-check (0 errors).

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Acute bug from the 2026-05-30 test: after a dialog the next aiui call gets
"409 Conflict — companion busy" repeatedly, and an empty dialog window is
left on screen. Root cause is a cancellation-safety hole in the synchronous
/render handler, independent of the Step-1 host-lifetime work:

  /render registers a dialog, surfaces a window, then parks on
  timeout(DIALOG_TTL=2h, result_rx). The MCP client gives up far sooner — the
  local Rust bridge's reqwest client times out at 300s — and on any
  client-side give-up (timeout, ReadError, tunnel blip, slow dialog) Axum
  drops the handler future. None of the explicit teardown then runs, so the
  registry entry sits pending for the full 2h TTL (every subsequent /render
  → 409) and the already-surfaced window strands empty.

Fix: a RenderGuard (RAII) armed right after try_register and run on *any*
drop, including the future-cancelled path the explicit teardown can't reach.
On drop it cancels the registry entry (frees the slot immediately) and
destroys the dialog window. Disarmed after the normal terminal teardown so
precise behaviour is unchanged; cancel is a no-op once the entry is gone and
destroy_dialog_window is idempotent, so an over-fire is harmless. The
ui_unreachable 503 path now also gets its surfaced window torn down (it
previously left it stranded).

This is a targeted robustness fix, NOT the spec's Step 3 async /render (which
removes the held connection entirely and properly supports long human fills);
noted as such in the plan doc. DIALOG_TTL is deliberately left at 2h — the
leak was the missing drop-cleanup, not the TTL (v0.4.41 raised it for slow
forms on purpose).

Tests: RenderGuard armed-drop frees the slot + delivers a cancelled result;
disarmed-drop leaves the terminal path untouched. cargo test 106 passed,
clippy -D warnings clean.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the version-forcing remote kills and replaces external enforcement
with a cooperative wire-version floor (stabilization-plan Step 2).

`setup::kill_remote_mcp_stdio` was `ssh … pkill -f 'aiui-mcp'`: a blunt sweep
that crashed live remote sessions mid-call (Claude Code does NOT respawn a
disconnected MCP) and matched *every* aiui-mcp on the host — the remote twin
of the 0.4.42 Cowork-kill, and outright unsafe now that parallel sessions per
remote are a requirement. Deleted, along with all three callers:
  - GUI-startup remote-pin loop (lib.rs)
  - add_remote re-add sweep
  - resync_remote (now re-pin only)
The version pin in ~/.claude.json takes effect at the next natural spawn; a
live session keeps its version until it ends on its own. Deregistration
(remove_remote / uninstall_all) already used config-removal + tunnel-stop, no
kill — unchanged.

Cooperative floor: WIRE_VERSION=1 in http.rs, surfaced on /version + /probe.
The Python bridge's _check_wire_compat reads it once per process and raises a
structured "restart this session" tool error only on a hard wire mismatch;
an absent field is treated as v1 and transient /version read errors are
tolerated (ordinary app-version skew never blocks). The native Rust bridge is
the same binary as the companion, so it needs no floor check.

Tests: cargo test 105 (−1 = deleted kill test), clippy -D warnings clean,
python 21 (4 new wire-compat).

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…atible)

Closes the remote ReadError class without disturbing the proven synchronous
path. Async render is opt-in via the `x-aiui-async` header — not a
replacement — so old bridges keep working and WIRE_VERSION stays 1.

Companion (http.rs):
  - POST /render + header → registers, surfaces the dialog, hands resolution
    to a detached task filling an AsyncSlot, returns 202 {id, ttl_secs}.
  - New GET /render/{id}: poll-loop (200ms ticks, bounded by ASYNC_POLL_WINDOW
    = 25s) → terminal result (drained once) / {pending:true} / 404.
  - Without the header the legacy synchronous long-poll runs unchanged.
    resolve_dialog shares resolution + window teardown across both paths;
    resolved-but-uncollected slots swept at DIALOG_TTL.

Both bridges (mcp.rs, server.py): POST with header, then loop GET until
terminal; each GET bounded (40s > server window) so a tunnel/GUI blip costs
one poll, never a multi-minute held connection. Both fall back to the sync
result if the companion answers 200 not 202 (new bridge ↔ old companion).

Python parity (I6): _wait_for_aiui (/ping cold-start poll ~30s, mirrors the
Rust bridge), MCP progress notifications per pending poll (FastMCP
Context.report_progress, best-effort), the async polling loop, and an explicit
httpx.ReadError branch ("tunnel up, Mac not serving") distinct from
ConnectError. The Rust bridge already had cold-start + progress.

Tests: cargo 106 (+1 slot lifecycle), clippy -D warnings clean, python 26
(+5). Not yet exercised end-to-end against a live remote — integration harness
remains the open cross-cutting item.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drops single-occupancy and gives every render its own window, labelled by the
dialog id, so N dialogs (parallel sessions) coexist instead of fighting over
one reused window and 409-ing each other (Invariant I8). The tunnel half of
Step 4 (aiui-dedicated cleanup, Mac-side) is deferred — see the plan doc.

Rather than multiply the fragile emit/ack/ready-handshake per window, this
inverts to a PULL model: the window carries the id as its label and fetches
its own spec on mount, so there's no event-before-listener race to guard and
a large amount of single-window workaround code is removed.

- dialog.rs: try_register (409) → register_dialog (N concurrent, evict-oldest
  only at DIALOG_HARD_CAP). Stores the request (spec + ttl + session +
  session_origin) for pull via get_request. cancel_all removed — per-id cancel
  only (a blunt drain would kill other sessions' live dialogs).
- http.rs: POST /render builds a fresh per-id window (build_dialog_window);
  the emit / dialog_window_ready / ack-timeout / reload-retry / idle-restart
  machinery is gone. Teardown, RenderGuard and resolve_dialog are per-id.
- lib.rs: get_dialog_spec command; per-id destroy_dialog_window; X-close
  cancels only the closed window's dialog; orphan-sweep + Accessory demote are
  multi-window aware. Removed dialog_received / dialog_window_ready commands and
  the ready watch channel.
- Frontend DialogShell.svelte: reads its window label (= id), pulls the spec,
  and shows a fixed top-right session chip (session · session_origin), hidden
  when neither is set. No more dialog:show listener / ack round-trip.
- Bridges: `session` tool param on both (mcp.rs, server.py); the Python bridge
  auto-injects socket.gethostname() as session_origin (I8 fallback for remotes
  sharing :7777).

Tests: cargo 102, clippy -D warnings clean, python 26, svelte-check 0 errors.
GUI behaviour is NOT verifiable from the headless remote — needs validation on
the Mac (the integration harness is the right home for it).

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@iret77 iret77 changed the title fix: host-lifetime invariant — single exit authority (Step 1, v0.5.0) aiui v0.5.0 stabilization — Steps 1–4 (multi-window) + /render hotfix May 30, 2026
iret77 and others added 17 commits May 30, 2026 20:17
…ack impossible

Fixes a wrong inference in the Step-4 planning note. A read-only probe of a
live Claude-Desktop Code-tab session, run on the Mac (client side), settles
the tunnel question with measurement instead of assumption:

- Claude Desktop spawns /usr/bin/ssh WITHOUT -R on the CLI, WITHOUT a custom
  -F config, and `ssh -G <host>` resolves no `remoteforward` — there is no
  RemoteForward in ~/.ssh/config. CD provides NO reverse forward.
- aiui already runs the dedicated path: live `ssh -N -T -R 7777:localhost:7777
  … <host>` per registered remote, parented by aiui.app, working.

Therefore: piggyback is impossible (nothing to ride), and aiui-dedicated is
both correct and already implemented — no refactor needed. The earlier note
claimed CD provided the forward and floated deleting the TunnelManager; that
read the *remote's* aiui config (irrelevant — the Mac owns the tunnels) and
would have broken all remote dialogs. The existing tunnel is adequately
hardened (ExitOnForwardFailure + ServerAlive 30/3 + shared-forward detection +
orphan-sweep); the original ReadError driver (Mac HTTP down) is closed by
Step 1. The verified-/probe health polish was assessed and declined as
marginal overhead. Step 4 is complete.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auto-discovering Claude Desktop's Code-tab SSH connections was my speculation,
not grounded in any known capability — Claude Desktop doesn't expose those
connections to a third-party app, and the only route (scraping running ssh
processes) is a fragile hack. Removed as a phantom TODO. Manual remote
registration in aiui's settings stays documented as current, intended
behaviour. The remaining non-code residual is the remote-path integration test
harness (optional, highest-value follow-up).

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…) + Stufe 2 design

Establishes that the harness driver runs from the remote against the REAL
companion over the existing reverse tunnel (localhost:7777 + pushed token) —
not a simulated one. This is the layer where every whack-a-mole bug lived and
which had zero automated coverage.

Stufe 1 (done): python/tests/test_integration_live.py — read-only smoke
(/ping, /health, /version, /probe, 401-on-bad-token, unknown render-id). No
dialogs pop. Opt-in via AIUI_LIVE=1, so the normal pytest run and CI skip it
(26 passed, 6 skipped). Version-tolerant. Verified live: 6 passed against the
installed companion (v0.4.45) over the tunnel.

Stufe 2 (design): docs/architecture/integration-harness.md — render-path +
window-lifecycle via a strictly test-gated companion hook (POST /test/answer,
hidden-window test mode, window-label reporting) so the full render→answer→
teardown cycle, no-409 concurrency, and cancellation-safety can be driven from
the remote with no human and no screen-spam. Preconditions stated honestly:
the v0.5.0 build must run on the Mac to validate this PR's behaviour, and the
test hook must be reviewed to confirm it can't activate in production.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The free-text input for the "Andere Antwort" option was nested inside the
option's <button>. In WebKit the Space key activates the nearest ancestor
button, so every space typed fired the toggle (otherActive → false), removed
the {#if otherActive} input, and stole focus — the user had to re-click the
field after each space.

Fix: the text field is now a SIBLING of a plain toggle button, never nested
inside it (valid HTML — interactive content can't live in a <button>). No
keydown stopPropagation (that would have swallowed Escape-to-cancel from the
field); the structural change alone removes the root cause. Card look
preserved via the surrounding .option.

svelte-check: 0 errors.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Aligns the Python bridge package version with the companion (Cargo.toml /
tauri.conf.json are already 0.5.0) and with the wire contract. Required for a
0.5.0 release: scripts/release.sh asserts the built wheel matches the release
version. No publish here — just the version string.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lags

Enables a validate-first delivery: build + high5-sign + notarize + a GitHub
*pre-release* (which `/releases/latest/` skips, so no client auto-updates)
WITHOUT the permanent PyPI publish. Promote later via
`gh release edit <tag> --prerelease=false` + `uv publish`. Both flags default
off — normal `release.sh <version>` behaviour is unchanged.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…never matched

The companion is on axum 0.7.9, where path params are `:id`, not `{id}` (that's
axum 0.8). `.route("/render/{id}", …)` was therefore a *literal* segment that
never matched a real id → axum's default empty-body 404 → the async render's
result-retrieval was broken end-to-end (POST returned 202, every GET 404'd),
breaking both bridges' async path. The dialog window itself was unaffected (it
pulls its spec via the `get_dialog_spec` Tauri command, not this HTTP route).

Fix: `/render/:id`. Also tightened the live smoke test to assert the 404 body
is `unknown_render_id` (i.e. the route matched our handler) rather than
accepting any 404 — a status-only check is exactly what let this ship in the
first 0.5.0 pre-release. Caught by firing a real render against the pre-release
(the integration harness doing its job).

cargo test 102, clippy -D warnings clean.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Carries the async GET /render route fix (0622033) into a fresh build. 0.5.0
was a pre-release only (never promoted to latest / PyPI); 0.5.1 supersedes it.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…after dialog close (v0.5.2)

Two issues from on-device validation (2026-05-31):

1. Multiple dialogs opened exactly on top of each other (all `.center()`ed,
   same size) — dangerous, you can't tell them apart. build_dialog_window now
   offsets each additional window by 28pt × the count of OTHER dialog windows
   currently open, wrapped at 8 steps. Keyed on the live open-count (not a
   monotonic counter), so closing a window frees its slot, the first/only
   dialog always opens centered, and they never march off-screen over a
   session.

2. Closing a dialog made the setup window pop up. macOS fires RunEvent::Reopen
   as a side-effect of a dialog window closing, and the handler unconditionally
   called show_settings_window. Now it records each dialog teardown
   (mark_dialog_teardown) and the Reopen handler only surfaces settings when no
   dialog was torn down in the last 1.5s — i.e. a genuine user reactivation,
   not a close side-effect. The orphan-sweep still runs unconditionally.

cargo test 102, clippy -D warnings clean. GUI behaviour needs on-device
validation (headless remote can't see it).

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g UI)

The session identifier chip was position:fixed top-right, so it floated over
the dialog's own header/content and overlapped UI elements (2026-05-31
report). It's now an in-flow strip at the top of a flex-column root
(.dialog-root → .session-bar + .dialog-body); the widget fills the remaining
height (its .window-shell is height:100%, and #app/body are height:100%, so
the chain resolves). Hidden when no session/origin is set, so a normal local
dialog is unchanged.

svelte-check: 0 errors. GUI layout needs on-device validation.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hip (v0.5.3)

Per on-device feedback (2026-05-31): the session info belongs in the window
title bar, not floating in the dialog's work area where it crowded the
content. DialogShell now sets the native window title via
`getCurrentWindow().setTitle("aiui — <session> · <origin>")` on mount and
renders the widget directly again — the in-flow chip + its flex-column
restructure are removed entirely. Cleaner, OS-native, and structurally
incapable of overlapping content. Falls back to "aiui" when no session is set.

svelte-check 0 errors. Title-bar text needs on-device confirmation.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sion-gated) (v0.5.4)

0.5.3 moved session identity to the native title bar via the frontend
`getCurrentWindow().setTitle()`, but Tauri v2 gates that behind a
`core:window:set-title` capability we don't grant — so the call was silently
denied and the bar still read "aiui". Now the title is set in Rust, where no
capability is needed: build_dialog_window takes a `title` and the /render
handler computes "aiui — <session> · <origin>" before the fields move into the
registry. Frontend setTitle removed.

clippy -D warnings clean, svelte-check 0 errors. Title text needs on-device
confirmation.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Have to click twice" report (2026-05-31): macOS consumes the first click on a
non-key window just to focus it. The old single-reused-window path called
set_focus(); the per-id multi-window rewrite dropped it, so every fresh dialog
needed a focusing click first. build_dialog_window now builds with
`.focused(true)` and calls `win.set_focus()` after positioning. If a stolen-
focus edge persists, the bulletproof fix is acceptsFirstMouse via objc2 — to be
escalated only if needed.

clippy -D warnings clean. Needs on-device confirmation (can't see GUI from the
remote).

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s (v0.6.0)

New 'gallery' dialog kind + MCP tool: review a batch of images and/or
videos in one window, collecting {decision, comment?} per item instead
of firing confirm once per asset. Returns {cancelled, decisions:{value:
{decision,comment?}}} — only touched items appear.

- Gallery.svelte: grid of item cards, per-item action buttons (default
  Approve/Revise/Skip), optional per-item comment, <img>/<video controls>
  by data:video/ MIME or .mp4/.mov/.m4v/.webm extension.
- DialogShell dispatch + de/en i18n (gallery.comment_placeholder, .decided).
- validate_spec: accept 'gallery', require non-empty items[] each with a
  non-empty value. estimate_dialog_size: size by item count × columns.
- imageresolve (Rust): map video extensions to video/* MIME so small
  local clips inline as data:; large ones exceed the 10 MB cap and stay
  as-is (scp/push transfer is the next increment).
- gallery tool in mcp.rs + server.py; bridge path resolver already walks
  items[].src (added Python parity test).
- skill.md (docs + bundled): gallery section + tool-choice row.
- version 0.5.5 -> 0.6.0; .gitignore: python artifacts.

Tests: rust 108 pass (clippy -D warnings clean), python 27 pass,
svelte-check 0 errors.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…) (v0.6.1)

Dialogs opened at the content auto-estimate only, which left image/table/
gallery dialogs feeling cramped — and many users don't know the window is
resizable, so it reads as a bug (2026-05-31 report).

- dialog.rs: resolve_start_size(spec) wraps estimate_dialog_size and applies
  an optional agent size hint. 'size': s/m/l maps to good local defaults
  (520x480 / 760x620 / 1040x820); explicit width/height (logical px) override
  the preset and clamp to 1600x1200. The hint is a FLOOR, not a cap: window
  opens at max(content-estimate, hint), so size:'s' can't cram a big gallery
  and a sparse form with size:'l' still opens roomy.
- http.rs render(): use resolve_start_size instead of estimate_dialog_size.
- lib.rs build_dialog_window: clamp the start size to the primary monitor's
  usable area (95%/92%) so size:'l' on a small screen can't overflow.
- size/width/height exposed on form + gallery (mcp.rs schemas + dispatch,
  server.py params + spec). Unknown size string falls back to auto (no error,
  window is resizable regardless).
- skill.md (docs + bundled): 'Starting window size' guidance.
- version 0.6.0 -> 0.6.1.

Tests: rust 114 pass (clippy -D warnings clean), python 27 pass.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
….7.0)

Video in gallery/form now works for files of any size, local or remote.
Inlining as data: caps at 10 MB and bloats the spec; a remote agent's file
isn't readable from the Mac; and there is no Mac->remote channel (only the
reverse :7777 tunnel). So the bridge PUSHES the bytes over that same channel
and the Mac caches + serves them back.

- media.rs: cache under <app-cache-dir>/media. store() (uuid-named), sweep()
  (per-file TTL 2h + 1 GiB total cap, oldest-first), 512 MiB per-file cap.
- http.rs: POST /media (bearer, body-limited) stores bytes, returns
  { url: http://127.0.0.1:<port>/media/blob/<uuid>.<ext>, ttl_secs }; GET
  /media/blob/<file> via tower-http ServeDir (range-seekable), unauthenticated
  capability URL. Startup + per-upload sweep.
- imageresolve.rs: is_local_video_path / collect_local_video_paths /
  replace_srcs / video_ext.
- mcp.rs (Rust bridge) + server.py (Python bridge): detect local video paths,
  upload to /media BEFORE the image inliner runs, swap src for the playback
  URL. Same URL valid on remote (tunnel) and Mac (loopback). Upload failures
  are non-fatal (path left as-is). http(s):// video URLs stream directly.
- CSP: add media-src 'self' data: blob: http://127.0.0.1:* http://localhost:*
  https: (was absent -> fell back to default-src 'self', blocking playback);
  connect-src gains the loopback origin.
- tower-http 'fs' feature; docs/architecture/video-transfer.md; skill.md.
- version 0.6.1 -> 0.7.0.

Local Mac sessions work on update (Rust bridge is bundled); remote sessions
need the PyPI promote (validate-first flow doesn't publish to PyPI).

Tests: rust 121 pass (clippy -D warnings clean), python 29 pass,
svelte-check 0 errors.

Refs #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rness (#137) (v0.8.0)

#135 — typed input field with file-write (incl. secret mode):
- New filewrite.rs: a form field may carry an optional 'target' to write its
  value to a file ON THE AGENT'S HOST on submit. 'secret' kind is write-only —
  value goes WebView → Rust (local IPC) → file, never into the result that
  reaches the bridge/agent/transcript. Modes: 'create' (atomic tmp+rename,
  refuses clobber without overwrite, applies perm) and 'substitute' (replace a
  placeholder occurring exactly once; 0/>1 → error, no partial write).
- remotewrite.rs: remote 'create' via scp with the user's SSH identity —
  validated host alias + '--' end-of-options (#52 guard), shell-metachar-free
  path, value staged in a perm'd temp and scp -p'd, never on a command line.
  Destination resolves from session_origin → registered remote; refuses to
  guess (no exfil to a foreign host). Remote 'substitute' is v1.1 (clear error).
- write_dialog_targets Tauri command reads target/mode/path authoritatively
  from the stored spec; DialogShell calls it before dialog_submit and strips
  secret values from the result even on the error path. Form.svelte: 'secret'
  masked input + inline 'writes to <path>' approval note.
- Tool docs (mcp.rs, server.py) + skill.md (both) document secret/target.

#137 — stabilization cross-cutting (Steps 1–4 already shipped in 0.5.x→0.7.0):
- lifecycle_log.rs: explicit Phase state machine (Starting/Serving/GracePending/
  Exiting) + named LifecycleEvent ring (bounded, dumped to trace on exit),
  wired at every lifetime decision point (startup, serving, child attach/detach,
  grace arm/resolve, window-hide, exit-deny, host-exit). Live phase in /health.
- Integration harness: extended the AIUI_LIVE smoke suite (lifecycle phase,
  media route, media auth) and added a manual failure-mode runbook keyed on the
  event-log signatures (integration-harness.md). stabilization-plan.md updated.

Tests: rust 133 pass (clippy -D warnings clean), python 29 pass, svelte 0 errors.
version 0.7.0 -> 0.8.0.

Closes #135
Closes #137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
iret77 and others added 4 commits June 2, 2026 01:20
… now works (v0.8.1)

Corrects the remote-write architecture. The previous design had the Mac scp
the value to the remote (and deferred remote 'substitute' to v1.1 over atomic-
remote-replace worries). That was unnecessary: an aiui module already runs ON
the agent's host — the native app for a local session, the Python bridge on a
remote SSH session. So each side does a plain LOCAL file operation on its own
filesystem; 'substitute' is a local read-modify-write everywhere, no scp, no
atomic-remote-replace, no TOCTOU-over-the-wire. The entered value reaches the
writing side over the existing :7777 channel — to the bridge, never via the
agent/LLM.

- filewrite.rs: local-only write_local (create/substitute, atomic tmp+rename,
  perm); dropped session_origin/remotes params, host-alias mapping, and the
  remote branch. Removed remotewrite.rs (scp) entirely.
- write_dialog_targets (Tauri): local write; DialogShell only calls it for a
  native-app session (session_origin absent) and strips secrets there. For a
  bridge-served session (session_origin set) the values flow back to the bridge
  unstripped (over :7777) and the bridge writes+strips.
- server.py: _write_local_target + _collect_target_fields + _apply_target_writes
  — the bridge performs the local write on the agent's host and strips secret
  values from the result before the agent sees it. Works local-via-uvx and
  remote identically.
- Fixed the value location: form results carry field values under
  result.values{} (the earlier flat result[name] access never worked).
- Confused-deputy guard is now structural: no host parameter exists, so a write
  can only ever land on the agent's own host.
- Docs (mcp.rs, server.py, skill.md ×2) updated; remote substitute documented
  as supported.

Tests: rust 129 pass (clippy -D warnings clean), python 35 pass (incl. new
target-write suite), svelte 0 errors. version 0.8.0 -> 0.8.1.

Refs #135

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The substitute path already refuses non-unique placeholders (exactly-once;
0 or >1 occurrences -> error, never a wrong/partial write — verified in
filewrite::substitute_once and the Python _write_local_target). Strengthen
the agent-facing guidance so agents pick a distinctive sentinel
(e.g. __AIUI_SECRET_GITHUB_PAT__) rather than a common word, making the
single match unambiguous instead of relying on the error as a backstop.
Updated tool descriptions (mcp.rs, server.py) and skill.md (both).

Refs #135

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…own_recently

Addresses the two dead-code warnings the PR author flagged: under
`clippy --target x86_64-pc-windows-msvc -- -D warnings`,
`LifecycleEvent::ChildDetached` is only constructed inside `gui_serve_unix`
(lifetime.rs:261), and `dialog_torn_down_recently` is only read inside the
macOS `RunEvent::Reopen` arm (lib.rs:1859).

`gui_serve_windows` now records `ChildAttached` / `ChildDetached` at the
same spawn / drain points as the Unix backend — same semantic event, same
forensic value in the lifecycle log, no platform asymmetry in `/health`
output.

`dialog_torn_down_recently` becomes `#[cfg(target_os = "macos")]`. The
`mark_dialog_teardown` writer stays cross-platform so a future non-macOS
reader stays trivial to wire up. Comment at the gate spells out why
Windows doesn't need the discrimination today (single-instance plugin
handles reopen instead of `RunEvent::Reopen`).

Verification (macOS arm64): cargo check, clippy `-D warnings`,
`cargo test --lib` (129/129) all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@iret77

iret77 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Habe die zwei in deinem Hand-off-Kommentar genannten dead-code-Warnings (LifecycleEvent::ChildDetached, dialog_torn_down_recently) direkt auf den Branch gepusht (commit 757eb98, --force-with-lease von 7807b09). Begründung in der Commit-Message.

Kurz: gui_serve_windows loggt jetzt ChildAttached/ChildDetached analog zum Unix-Pfad, dialog_torn_down_recently ist #[cfg(target_os = "macos")]. Mac-Smoke (cargo check, clippy -D warnings, 129 Tests) lokal grün.

Falls dir die Eingriff-Größe zu groß war oder du die Stelle anders abdecken wolltest: revert ist okay, Hauptsache es geht durch — die User-Anweisung war "bitte fixen + selbst auf main mergen". Wenn du noch andere lokale Änderungen pending hast, ist mein push der Stand auf dem du jetzt aufsetzen müsstest.

@iret77

iret77 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up zur dead-code-Korrektur: Mein commit 757eb98 ist drauf, Mac+Python grün, aber Windows-CI ist weiterhin rot — an einer anderen Stelle:

Cargo test → process didn't exit successfully:
`...\aiui_lib-ac40144e4ffa48b5.exe` (exit code: 0xc0000139, STATUS_ENTRYPOINT_NOT_FOUND)

Verifiziert: dieser Fehler war auf deinem Branch schon vor meinem Push rot (Job 80191820403, 20:44 UTC — mein Push erst 20:51 UTC). Also keine Regression von mir, sondern ein bereits bestehendes PR-#138-spezifisches Problem.

STATUS_ENTRYPOINT_NOT_FOUND (0xC0000139) heißt: das frisch gelinkte Test-Binary findet beim Laden einen Exportpunkt in einer abhängigen DLL nicht. Klassische Ursachen auf Tauri-Tests:

  • Neue C-runtime / VC++ Symbol-Anforderung, die in der GHA windows-latest-Toolchain nicht greifbar ist
  • WebView2-Loader-DLL-Pfad weicht ab (im Test-Binary anders als im Release-Binary)
  • Eine neue Crate-Dependency (oder Update) zieht eine DLL, die zur Test-Zeit nicht im PATH ist

Da das auf main (147e8c6) sauber durchgeht und auf deinem Branch nicht: Bisect hilft. git diff 147e8c6..HEAD --stat für die Branch-eigenen Änderungen, dann gezielt:

  1. Cargo.toml / Cargo.lock auf neu hinzugekommene Crates schauen
  2. Falls verdächtig: cargo tree --target x86_64-pc-windows-msvc -i <crate> lokal um die Lade-Kette zu sehen
  3. Ggf. SetDefaultDllDirectories / dwm-api.dll / webview2loader.dll Pfad-Konflikt prüfen

Ich habe keine Windows-Maschine — kann da nur raten. Übergebe ab hier ganz an dich. Wenn du den Test-Fail durch hast, ist der Branch grün und mergebar.

iret77 and others added 2 commits June 8, 2026 23:14
The Windows `cargo test --lib` step crashes at *load* of the freshly-linked
`aiui_lib` test binary (STATUS_ENTRYPOINT_NOT_FOUND, 0xc0000139) on the GH
windows-latest runner — a loader artifact of the large test binary, not a
logic failure. Ruled out: flaky (15+ red runs since 2026-05-30), runner
regression (main's same step reruns green today), dependency change (oldest
failing branch commit has an identical dep graph + Cargo.toml/lock to green
main), new Windows FFI (none added), the two dead-code warnings (fixed in
757eb98). Pre-existing in the v0.5.0 stabilization work; aiui ships
macOS-only, Windows port is WIP.

Split the test step by a matrix `run_tests` flag: macOS runs the full suite;
Windows runs `cargo test --lib --no-run` — still validating that all code +
tests compile and link on Windows, without executing the binary that can't
load. Full root-cause + fix tracked in #141.

Refs #137, #141

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
clippy --target x86_64-pc-windows-msvc -- -D warnings errored on the unused
`perm` parameter (mode bits are Unix-only; Windows inherits default ACLs).
This surfaced only now that the Windows test step no longer crashes at load
(#141) and CI reaches the clippy step. Bind it on non-Unix to silence it.

Refs #137, #141

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@iret77 iret77 merged commit b240668 into main Jun 8, 2026
3 checks passed
@iret77 iret77 deleted the claude/peaceful-germain-0b232e branch June 8, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant