Commit Graph

154 Commits

Author SHA1 Message Date
vasyansk 09e7a2b526 Merge sidebar-rail: collapsed icon rail 2026-06-15 13:41:05 +07:00
vasyansk 5d7a80e2a2 feat(app): collapsed sidebar becomes an icon rail (keeps activity visible)
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>
2026-06-15 13:41:05 +07:00
vasyansk 569aa39444 Merge events-clear-and-settings-x: clear events + settings close button 2026-06-15 13:38:35 +07:00
vasyansk f9a565a712 feat(app): clear all events from the Event Center (red trash icon)
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>
2026-06-15 13:38:35 +07:00
vasyansk bcc88b6be7 fix(app): add a close (X) button to the settings modal
Esc and click-outside already closed it, but there was no visible affordance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 13:38:35 +07:00
vasyansk 3dc3da072c fix(app): make the bell badge click-through (number opens the log too)
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>
2026-06-15 13:31:47 +07:00
vasyansk 897a3be659 Merge version-handshake: auto-restart stale daemon on GUI launch 2026-06-15 12:39:46 +07:00
vasyansk cf7410b46a feat(app): version handshake — GUI restarts a stale running daemon
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>
2026-06-15 12:39:46 +07:00
vasyansk 8f431eaa40 fix(build): clean sidecar dir each build; native-focused install/reinstall
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>
2026-06-15 12:34:57 +07:00
vasyansk 79b47d42e7 build: make kill-daemon/install/reinstall — daemon survives reinstall
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>
2026-06-15 12:26:49 +07:00
vasyansk df0389b38f Merge fix-double-echo: single reader across reconnects 2026-06-15 11:56:35 +07:00
vasyansk c84b96abc0 fix(app): abort the old reader on reconnect (fixes doubled keystroke echo)
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>
2026-06-15 11:56:35 +07:00
vasyansk ee969371c9 Merge rename-and-term: workspace rename + TERM env fix 2026-06-15 11:47:21 +07:00
vasyansk 07cf7f9ed4 fix(pty): always set TERM/COLORTERM for spawned shells
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>
2026-06-15 11:47:21 +07:00
vasyansk a929c166a3 feat(app): rename a workspace by double-clicking its name
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>
2026-06-15 11:47:21 +07:00
vasyansk 99a916fed6 fix(bundle): provide per-arch spaceshd sidecars for universal build
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>
2026-06-15 11:41:57 +07:00
vasyansk 2fc58105a5 Merge bundle-daemon: package spaceshd in the macOS app 2026-06-15 11:39:03 +07:00
vasyansk a7272fc92e fix(bundle): ship spaceshd inside the .app (packaged GUI was offline)
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>
2026-06-15 11:39:03 +07:00
vasyansk df6eabcd32 Update VERSION
Build / Build & push landing (push) Successful in 18s
Build / Notify Max (push) Successful in 2s
2026-06-15 10:58:31 +07:00
vasyansk d9ea6206c8 Merge ci-landing-only: Gitea builds landing only, DMG is local 2026-06-15 10:57:22 +07:00
vasyansk 8015f329ed ci: drop macOS DMG job — build the .dmg locally via make
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>
2026-06-15 10:57:22 +07:00
vasyansk 4dad6075a5 Merge makefile: local build helpers 2026-06-15 10:55:48 +07:00
vasyansk 78b2e2a162 build: Makefile for local builds (DMG, dev, daemon, tests, landing)
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>
2026-06-15 10:55:48 +07:00
vasyansk ad09ea6c01 Merge landing-ci: landing site, nginx image, Gitea CI for landing + DMG 2026-06-15 10:54:11 +07:00
vasyansk 2f2159a468 feat(landing): static site + nginx image + Gitea CI (landing + macOS DMG)
- 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>
2026-06-15 10:54:11 +07:00
vasyansk a9836f28b7 Merge bridge-reconnect: GUI self-heals after daemon restart 2026-06-15 10:22:24 +07:00
vasyansk 9ca0164d0b fix(app): bridge auto-reconnect so daemon restart no longer bricks the GUI
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>
2026-06-15 10:22:24 +07:00
vasyansk 99f5708cbf Merge settings-bugfixes: live font apply + daemon uptime refresh 2026-06-15 10:08:59 +07:00
vasyansk f8d3876c68 fix(app): settings live-apply — font on open terminals, daemon uptime
- 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>
2026-06-15 10:08:59 +07:00
vasyansk b63ed2ea83 Merge settings-modal: daemon config, Get/Set/ConfigChanged, CSS-var theming, settings modal with Stop/Restart 2026-06-15 09:05:45 +07:00
vasyansk 5e6cf4d982 fix(app): settings review — startup theme default, slider/shell input UX, dedupe accents, memoize palette
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 19:10:19 +07:00
vasyansk 9ca1ff3bc5 feat(app): daemon status with Stop/Restart in settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 18:57:53 +07:00
vasyansk a2087a0de5 feat(app): settings modal — terminal, appearance, shell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 18:34:14 +07:00
vasyansk 61c69adb17 feat(app): terminal font and xterm theme from daemon config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 09:43:38 +07:00
vasyansk 0f28be1300 feat(app): apply theme from daemon config on load and live
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 09:41:15 +07:00
vasyansk dc95381870 feat(app): CSS-variable theming with dark/light palettes and accents
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 09:39:07 +07:00
vasyansk b9f46a407d feat(app): socketBridge getConfig/setConfig + config_changed 2026-06-14 09:37:45 +07:00
vasyansk 62f1f8e9a8 feat(app): tauri get_config/set_config bridge commands 2026-06-14 09:36:53 +07:00
vasyansk ad29665352 feat(spaceshd): GetConfig/SetConfig handlers with live ConfigChanged broadcast 2026-06-14 09:35:27 +07:00
vasyansk c4746f9864 feat(proto): GetConfig/SetConfig commands and ConfigChanged event 2026-06-14 09:22:39 +07:00
vasyansk e990e694b5 feat(proto): ConfigView wire type 2026-06-14 09:21:22 +07:00
vasyansk 80113da066 feat(spaceshd): config terminal+appearance sections and save 2026-06-14 09:20:24 +07:00
vasyansk 052f484142 docs: settings modal implementation plan
11 TDD tasks: daemon config model/save, ConfigView + Get/Set/ConfigChanged
protocol, bridge + socketBridge, CSS-var theming, TerminalView font/theme,
settings modal, daemon Stop/Restart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 09:05:22 +07:00
vasyansk 4aacebcc60 docs: settings modal design spec
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>
2026-06-14 09:01:53 +07:00
vasyansk 5b08b204b6 Merge workspace-list-features: pinned/favorites, drag-reorder, delete-with-confirm 2026-06-14 08:56:31 +07:00
vasyansk a55555983b feat(app): sidebar favorites, drag-reorder, and delete-with-confirm
- 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>
2026-06-14 08:56:20 +07:00
vasyansk 7b47052a6f feat(spaceshd): pinned workspace field
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>
2026-06-14 08:56:09 +07:00
vasyansk 2c8ac8ebac Merge fix-spawn-search: lazy PTY spawn, working search, prompt-dup fix 2026-06-14 08:34:57 +07:00
vasyansk 04ac7cdec2 fix(app): working scrollback search + stop prompt duplication on focus
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>
2026-06-14 08:34:43 +07:00
vasyansk a30ec1cc7f fix(spaceshd): lazy PTY spawn so prompts render at the correct size
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>
2026-06-14 08:34:28 +07:00