Full Headless Mode: daemon + thin clients#157
Open
matej21 wants to merge 357 commits into
Open
Conversation
matej21
force-pushed
the
refactorx/full-headless
branch
from
June 30, 2026 15:40
89b6931 to
dcf0ae5
Compare
jonasnobile
force-pushed
the
refactorx/full-headless
branch
from
July 2, 2026 09:51
d2012b2 to
95cbe89
Compare
jonasnobile
added a commit
that referenced
this pull request
Jul 2, 2026
Resolve the `clippy --workspace --all-targets -D warnings` failures on #157: - auth.rs: drop needless borrow in `std::fs::read(&path)` - persistence.rs: build `WindowState` via struct-init instead of field-reassign-after-`default()` (×2, test) - stream.rs: move `mod tests` below the helper fns (items-after-test-module) - port_detect.rs: gate `parse_lsof_output` on `cfg(test)` — macOS scans sockets via libproc, so it was dead outside tests - command_loop.rs / status_bar.rs: collapse nested `if` into the outer condition/match Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RemoteServer::start runs on a plain caller thread (daemon main / GPUI thread). TCP listeners bind via runtime.block_on, but the local unix socket bound via bind_unix_socket directly, with no ambient runtime, so UnixListener::bind panicked with "there is no reactor running". Enter the runtime explicitly around the unix socket bind. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWzd61qT2vWjhDnE7vTjVQ
PR #152 added `default_branch` to GitStatus so the ahead/behind chip hides the redundant base label when the review base IS the default branch (the common case: `⎇ +2` instead of `⎇ main +2`). But the desktop is now a pure daemon-client — `git_watcher` is always `None`, so `resolve_git_status` always reads the remote snapshot, where `default_branch` was hard-coded `None`. With it always `None`, `show_label` was always true and the label never hid: #152's feature was dead over the daemon connection. Carry it over the wire exactly like `review_base` (08a7769): - Add `default_branch: Option<String>` to `ApiGitStatus` (serde default). - Populate it in both GitStatus→ApiGitStatus converters (daemon git_poll, GUI watcher). - Map it through in project_column's remote-snapshot fallback so the base label hides on the default branch for daemon-backed projects too. okena-core api round-trip covers default_branch; workspace builds clean, daemon-core 30 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJrTmHn7J5eX8okTcggSwV
…oops Soft-close (busy-terminal grace + Undo/Close-now toast) lived only in okena-daemon-core; the `okena --headless` fallback (used when the okena-daemon binary isn't built) closed immediately with no toast. Extract the orchestration into a shared, runtime-agnostic engine in okena-workspace (actions::soft_close: probe_busy, build_soft_close_toast, begin_soft_close_flow, undo_soft_close_flow, close_now_flow, finalize_expired, SoftCloseDeadlines) and have both command loops call it. Headless gains a HookMonitor + deadline map + finalizer-tick and toast-drain spawn loops. Also log when spawn_daemon falls back to --headless. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qxP1RVoywgidDU68WDpr3
The daemon-core and headless command loops carried byte-parallel copies of the GetState snapshot builder (build_api_project + folder/orphan assembly) and the six service-action arms — every wire/loop feature had to be added twice, which is how soft-close and other features silently diverged. Extract the pure snapshot construction into okena-app-core::remote_snapshot (build_api_project, build_api_projects, build_folders, build_state_response, api_project_visibility) and the service-action logic into ServiceManager methods generic over ServiceCx (start/stop/restart/start_all/stop_all/reload) plus ServiceInstance::to_api. Both loops now gather their own runtime state (Mutex vs gpui Entity) and call the shared code. Wire shape is unchanged. Each loop sheds ~190 lines. Config modules (daemon_config vs remote_config) remain parallel — deferred as a higher-risk follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qxP1RVoywgidDU68WDpr3
…trait daemon_config (Arc<Mutex<AppSettings>> + disk) and remote_config (gpui GlobalSettings/GlobalTheme entities) were parallel copies of the same settings/theme logic. Extract the shared logic into okena-app-core::remote_config over a ConfigBackend trait that abstracts the three divergent pieces: settings load/store, live-theme apply (no-op on the headless daemon), and the active-theme color source. Daemon and GUI each impl the trait; the get/set settings, theme list/get/set, save-custom-theme, schema, JSON-merge and theme-resolution helpers now live once. Wire shape, error strings, GUI save_and_notify + live AppTheme update, and daemon disk persistence are all preserved. list_actions stays GUI-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qxP1RVoywgidDU68WDpr3
get_settings/get_themes now take &mut App, matching cx.update's closure signature, so `cx.update(|cx| f(cx))` is a redundant_closure under `clippy -D warnings`. Pass the fn directly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qxP1RVoywgidDU68WDpr3
The footer's REMOTE endpoint and Pair button used terminal ANSI accents (term_cyan / term_yellow), which read inconsistently against the rest of the footer chrome (version/time/Focused use the neutral UI palette) — most visibly in the Pastel theme. Switch REMOTE to text_secondary (gray) and Pair to text_primary (white, keeping the hover background), so the footer is consistent across all themes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qxP1RVoywgidDU68WDpr3
Rework the shared toast overlay (used by all levels + the soft-close alert) to match okena's notification style: drop the left accent stripe, put a level-colored badge on the left (circle for success/info/error, triangle glyph for warning), a bold title over a muted subtitle, close in the title row, and plain left-aligned text-link actions. The grace countdown is now a subtle accent wash that depletes across the whole card instead of a thin bottom line. Align the first action's text flush with the title (offset its hover-pill padding), bump the countdown-wash contrast and subtitle legibility, and even out the padding/margins. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qxP1RVoywgidDU68WDpr3
Release synchronous workspace locks before async cleanup, bundle oversized handler arguments, and apply the remaining behavior-neutral lint fixes exposed after the services errors were resolved.
The single-binary daemon (`okena --headless [--ui-owned]`) reuses the same src/main.rs logging init as the GUI, so both processes rotated and wrote the same okena.log, clobbering each other's history. The standalone okena-daemon.log tee only exists in the separate okena-daemon binary, so ui-owned mode produced no daemon log at all. Pick the log filename before the rotate/create block: when headless (or Linux --listen/--remote with no display), write okena-headless.log with its own .1 rotation, leaving the GUI's okena.log legible.
Reconcile dtach sessions against authoritative post-lock workspace state, isolate named-profile sockets, and tear down verified descendant trees before removing their masters.
Route remote terminal activity through the existing doorbell and app-wide pane registry instead of per-pane 8 ms timers.
Preserve birth-marker validation while allowing a stopped dtach master to reap killed children after teardown verification.
8 tasks
fix: recover failed worktree close teardown
`tmux kill-session` and `screen -X quit` exit non-zero when the session is already gone, which is the normal outcome after a shell exits on its own — the PTY EOF that reaches `kill_exited` fires precisely because the session terminated. Treating that exit status as a teardown failure made `ResolvedBackend::kill_session` report false, mark the teardown tracker failed, and abort the next worktree/project close with "checkout preserved". The liveness probe already answers the question teardown actually asks, and `has-session` / `-Q select .` correctly report a missing session as not live. Fall through to it on a non-zero kill status; keep failing closed only when the kill command could not be run at all. Affects tmux, screen and psmux; dtach has its own socket-based path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HqeVbA9vP6t9EL7rFiNznF
The reaper worker decrements the teardown tracker for every job it drains, but `shutdown_handle` took the matching count conditionally. `detach_all` passed no tracker while still passing the reaper sender, so a handle that reached the reaper was decremented without ever having been counted; a closed reaper queue did the reverse and left a count no worker would ever complete. Only `Drop` calls `detach_all` today, so neither could be observed by a flush. Move the handoff into one helper that takes the count exactly while a reaper owns the job, and pass the tracker from `detach_all`, so the invariant holds locally instead of depending on that call graph. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HqeVbA9vP6t9EL7rFiNznF
…eted The teardown tracker recorded an unverified session kill as a single manager-wide AtomicBool that any flush consumed. Three consequences: - a failed kill for one terminal aborted the next destructive operation on an unrelated project, with a message naming that project's checkout; - `flush()` cleared the flag, so a plain drain between the failure and the destructive flush erased the signal and the removal proceeded; - `swap` meant only the first of two concurrent flushes saw the failure and the second reported success — both fail-open, defeating the point. Record unverified teardown per terminal instead, keyed off the owning terminal id now carried on `TeardownKind::KillSession`, and have the bounded flush take the terminals the caller is about to delete. Reading is non-destructive so concurrent waiters agree; an entry is cleared only when a later teardown of that same terminal succeeds. Both destructive call sites already know their terminals. The hook paths additionally pass the before-remove hook terminal they killed themselves, which the project row no longer lists — the global flag covered it only by accident. Daemon shutdown asks about the drain alone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HqeVbA9vP6t9EL7rFiNznF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Full Headless Mode — daemon + thin clients
Migrates Okena from the single-process "local + mirrored-remote" architecture to a
two-process model: a headless daemon that owns all state, PTYs, logic and
persistence, and thin UI clients (desktop, web, mobile, remote) that all speak one
protocol over a socket. The in-process "local" code path is deleted; local and remote
projects now render through the same machinery.
Why
daemon (authoritative) and clients (views).
okena-daemonbuilds and runs with zero gpui linked(gate:
cargo tree -i gpui -p okena-daemonis empty), so it runs on a headlessserver / CI box / container with no windowing stack.
the daemon dies with the last UI (UI-owned lifecycle).
The boundary: DATA vs PRESENTATION
services, hooks, persisted config (incl. the theme preference), instance lock,
HTTP/WS server.
per-window visibility — and, for the CLI, output formatting.
What's in here
okena-daemon-corecrate (gpui-free): tokio reactor, observer reactor(autosave /
state_version/ service-sync), the PTY event loop, git-status poller,gpui-free settings/theme handlers, and the gpui-free command loop — assembled by
DaemonCore::{new,run}.okena-daemonbinary — first-class shippable artifact, now packagedby the installers.
okena-workspace,okena-theme,okena-services,okena-hooks,okena-files,okena-app-core,okena-remote-server(reactor-traitabstractions sever the gpui chain).
--daemon-client→ default): deleted theGUI's in-process
RemoteServer, PTY manager, git/worktree watchers, in-processServiceManagerand autosave observer.hooks, worktree create/close, branch switch/create, file viewer rename/delete,
project rename, shell switch, soft-close (Undo / Close-now), toasts + toast actions,
OS notifications, CI/PR enrichment, terminal buffer export, client-owned multi-window
layout persistence.
Scope
100 commits · 154 files · +12.4k / −4.4k. The daemon-core is complete with tests; the
gpui gate is empty; the standalone binary ships. Architectural goal met per the
roadmap doc; remaining items there are last-mile parity, not blockers.
Testing
Every commit is
cargo build/cargo testgreen; the gpui-free gate(
cargo tree -i gpui -p okena-daemon) is verified empty.