Toggling the sidebar off used to hide it entirely, losing the per-workspace
status rings. It now collapses to a 48px rail showing each workspace's
aggregate status ring (and unread dot), still clickable to switch, plus the
new-workspace button and the daemon live/offline dot. Full sidebar returns
when toggled back on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds Cmd::ClearEvents + Evt::EventsCleared: the daemon drops the persistent
event log (keeping next_id monotonic), persists, and broadcasts so every
client empties its list. A red trash icon next to 'Mark all read' triggers it;
disabled when the list is empty. Threaded through proto, the daemon handler,
the Tauri bridge, and socketBridge. Includes an EventLog::clear test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pointer-events:none lets clicks/hover pass through the unread badge to the
bell button beneath, which the badge was previously swallowing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The daemon outlives the GUI, so after an update an OLD daemon can keep serving
the socket and the new GUI just connects to it (stale code — e.g. the missing
TERM fix). Both binaries are now stamped with the git build id (build.rs):
the daemon reports it in `health.build`, and on connect the bridge compares it
to the GUI's own SPACESH_BUILD; on mismatch it shuts the daemon down and lets
ensure_daemon respawn the bundled (matching) one. No-op for unstamped dev
builds or daemons too old to report a build. Build id is shown in Settings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The stale bin/spaceshd-universal-apple-darwin sidecar (left over from an
earlier approach) poisoned the universal bundle — tauri shipped that old
daemon instead of the freshly built one, so the packaged daemon lacked the
TERM fix. dmg/dmg-native now wipe the sidecar dir first. install copies the
native bundle (was preferring the universal one, which could be stale) and
kills the running daemon; reinstall = native rebuild + install for fast
self-updates. Universal stays for distribution via dmg / install-universal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuilding the .app never replaced the RUNNING daemon (it outlives the GUI),
so a stale spaceshd kept serving old code (e.g. the pre-TERM-fix daemon).
`make install` now stops the daemon, copies the fresh bundle to /Applications,
and clears quarantine; `make reinstall` does dmg+install in one shot.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
reconnect() spawned a new reader/writer but left the previous reader task
running. A reconnect triggered while the old connection was still alive (e.g.
a request timing out during a slow daemon start) left TWO live connections;
the daemon broadcast Output to both, so every byte — including input echo —
arrived twice ("ccucurcurl"). The bridge now stores the reader's JoinHandle
and aborts it before establishing the new connection, guaranteeing a single
live reader.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A GUI/launchd-spawned daemon has no TERM in its environment, so child shells
inherited none and tput/zsh/ncurses failed ('tput: No value for $TERM').
The PTY now defaults TERM=xterm-256color and COLORTERM=truecolor (matching
xterm.js) unless the caller already provides them. Adds a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Double-click a sidebar workspace name to edit it inline; Enter/blur commits
via setWorkspaceMeta({name}) (empty/unchanged is a no-op), Esc cancels. The
input stops pointer/key propagation so it doesn't trigger select or drag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tauri's universal target builds each arch separately and resolves externalBin
with that arch's triple (spaceshd-aarch64-apple-darwin / -x86_64-apple-darwin),
lipo'ing them itself. The previous single -universal-apple-darwin sidecar made
the per-arch sub-build fail with 'resource path ... doesn't exist'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tauri build bundled only the GUI binary, so the packaged app had no daemon:
find_daemon() looks for a sibling `spaceshd` next to the GUI
(Contents/MacOS/spaceshd) and ensure_daemon failed to spawn it → "offline".
(Dev worked only because find_daemon falls back to the repo-root target path.)
- tauri.bundle.conf.json: a build-only overlay adding bundle.externalBin
["bin/spaceshd"], kept out of tauri.conf.json so `tauri dev` doesn't require
a sidecar file.
- Makefile: `make dmg` now builds spaceshd for both arches, lipo's a universal
sidecar into src-tauri/bin/spaceshd-universal-apple-darwin, and passes
--config so it lands in Contents/MacOS/spaceshd. `make dmg-native` does the
host-arch equivalent.
- .gitignore: ignore the generated app/src-tauri/bin/.
After install, the unsigned helper runs once quarantine is cleared recursively:
xattr -dr com.apple.quarantine /Applications/spacesh.app
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
No self-hosted macOS runner exists and Tauri can't cross-compile a macOS
bundle on Linux, so the DMG is produced locally with `make dmg`. The Gitea
workflow is now landing-only (build & push the nginx image + Max notify);
removed the dmg job, the changes/paths-filter job, and the app/crates triggers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
No macOS CI runner available, so the universal .dmg is built locally:
`make dmg` (adds the rust targets + tauri build --target universal-apple-darwin).
Also: deps, dmg-native, dev, daemon, test, landing-image/run/push, clean.
`make` / `make help` lists targets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- landing/: the spaceshell.ru terminal-dark landing (index.html + screenshots),
containerized as an nginx:alpine image (Dockerfile + nginx.conf with gzip and
asset caching, VERSION, .dockerignore).
- .gitea/workflows/build.yaml: adapted from the coddykinder pipeline to this repo.
Path-gated jobs — `landing` builds & pushes the nginx image to the Gitea
registry on landing changes; `dmg` builds a universal (Intel + Apple Silicon)
.dmg via `tauri build` on app/crates changes and uploads it as an artifact;
Max notification summarizes both. Tags build everything (release).
- DOCS/landing/spaceshell-landing.md: build brief + copy + SEO meta.
Notes: the DMG job needs a self-hosted macOS runner labelled `macos` (Tauri
can't cross-compile macOS from Linux); the DMG is unsigned until Developer ID
secrets are wired. Landing image verified locally (HTTP 200, assets served).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Tauri bridge connected to the daemon once at startup and held a single
stream with no recovery: when the daemon exited (Restart/Stop, crash, or an
update), the reader emitted spacesh:disconnected and died, and every later
request went through the dead writer forever — the GUI was permanently stuck
(settings frozen, offline). Since the bridge is Rust-side state that survives
a webview reload, even Cmd+R didn't recover it.
- bridge.rs: requests now reconnect-and-retry on failure with a single-flight
guard (generation counter) so concurrent failures collapse into one
reconnect and never open duplicate connections; a 5s reply timeout catches
silently-dropped connections. ensure_daemon respawns the daemon if it
exited. On success the bridge emits spacesh:reconnected.
- App.tsx: on spacesh:reconnected, bump a connection epoch that keys
LayoutEngine, remounting terminals so they re-attach (snapshot + live stream)
to the restarted daemon; also reload health/config/status.
- Settings: drop the Stop button — with lazy daemon spawn any GUI request
resurrects the daemon, so an in-GUI "stop" is contradictory. Restart now
works end to end (shutdown → reconnect respawns → panels re-attach).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Font change now applies to already-open terminals: the WebGL renderer
caches glyphs in a texture atlas keyed by the old font/size, so the live
re-apply effect now calls webglAddon.clearTextureAtlas() (via a new ref)
after updating fontFamily/fontSize, before refitting.
- Daemon uptime now reflects a restart: the Settings daemon section ticks
every second for live uptime, and Stop/Restart trigger an onReload callback
that re-fetches health/status in App so a restarted daemon's new
started_at_ms is shown instead of the stale value.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Config in config.toml via daemon (Get/Set/ConfigChanged), low-churn CSS-var
theme switching (Dark/Light + accent), terminal font, default shell, and
daemon status with Stop/Restart.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- FAVORITES section at the top collects pinned workspaces (removed from their
group listing); a star toggle on each row pins/unpins via setWorkspaceMeta.
- Drag-to-reorder within a section using raw pointer events (HTML5 DnD is
unreliable in the macOS WKWebView), with a drop-line indicator; on drop the
section's `order` is reassigned sequentially and persisted. Cross-section
drops are ignored (group membership unchanged).
- Trash icon on row hover opens a ConfirmDelete modal that shows the live
terminal count and warns before terminating them, then calls close_workspace
and re-points the active workspace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `pinned` bool to the workspace model, threaded through proto
(Workspace + WorkspaceView + SetWorkspaceMeta), the registry, the
set_workspace_meta handler, persistence, the CLI mapping, and the Tauri
bridge. serde(default) keeps existing state.json compatible (pinned=false).
Backs the sidebar Favorites section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Search fixes:
- TerminalView sets allowProposedApi (the search addon's match decorations
use registerMarker/registerDecoration); without it findNext threw before
firing results, so the counter was stuck at 0/0.
- The search bar now renders inside the panel it targets (in the header)
instead of a global top-right overlay, so it's obvious which panel is
searched.
- Search is anchored to the panel it was opened on (searchSurfaceId) and no
longer follows focus — opening it in one panel and switching away no longer
shows it open elsewhere.
Prompt duplication:
- The focus border was 1px when unfocused, 2px when focused; with border-box
that resized the content on every focus switch, firing ResizeObserver -> fit
-> PTY SIGWINCH and making zsh/powerlevel10k reprint its prompt. The border
is now a constant 2px, color-only on focus.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A new surface spawned its shell immediately at the default 80x24, so the
shell (e.g. zsh + powerlevel10k) printed its first prompt before the GUI
fit-resized the panel. The post-print resize reflowed the grid, leaving the
prompt invisible until Enter and shifted by a line.
The surface actor now defers the child spawn: it waits for the first Resize
(spawning at that geometry) or, for headless/CLI surfaces that never attach a
GUI, falls back to the spec geometry after 250ms. Attaches received before
the spawn get an empty-grid snapshot plus a live subscription; pre-spawn
input is buffered and replayed. Adds two deterministic tests via `stty size`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>