diff --git a/slab/bin/install-remote-bridge-agent.sh b/slab/bin/install-remote-bridge-agent.sh new file mode 100644 index 0000000000..b33455febb --- /dev/null +++ b/slab/bin/install-remote-bridge-agent.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# install-remote-bridge-agent.sh — run claude-remote-bridge as a LaunchAgent so +# the remote-session mirror is always live (survives reboots / logout), not just +# while a `jasellite` window is open. Idempotent. +set -euo pipefail + +LABEL="computer.slab.remote-bridge" +PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" +BRIDGE="$HOME/aesthetic-computer/slab/bin/claude-remote-bridge.mjs" +LOG="/tmp/$LABEL.log" + +# fnm's per-shell node paths are volatile; the `default` alias is stable. +NODE="$HOME/.local/share/fnm/aliases/default/bin/node" +[ -x "$NODE" ] || NODE="$(command -v node)" +[ -x "$NODE" ] || { echo "no node found"; exit 1; } + +# Retire any hand-started bridge so launchd becomes the sole owner (its single- +# instance guard would otherwise make the launchd copy exit and KeepAlive-spin). +pkill -f 'claude-remote-bridge' 2>/dev/null || true +rm -f "$HOME/.local/share/slab/state/remote-bridge.pid" + +mkdir -p "$HOME/Library/LaunchAgents" +cat > "$PLIST" < + + + + Label$LABEL + ProgramArguments + + $NODE + $BRIDGE + + RunAtLoad + KeepAlive + ThrottleInterval5 + StandardOutPath$LOG + StandardErrorPath$LOG + + +PLISTEOF + +launchctl unload "$PLIST" 2>/dev/null || true +launchctl load "$PLIST" +sleep 2 +echo "node: $NODE" +echo "plist: $PLIST" +echo -n "status: " +launchctl list | grep "$LABEL" || echo "(not listed)" +echo -n "running: " +pgrep -f claude-remote-bridge | head -1 || echo "(none)" diff --git a/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift b/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift index d1b243c8f6..df8ffd82cf 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift @@ -57,6 +57,14 @@ struct ClaudeSession { /// live Workflow-tool agents (counted from the workflow journals). Drawn /// as one dot per subagent along this session's polygon edge. var subagentCount: Int = 0 + /// Non-empty when this session actually lives on a remote box (e.g. + /// jasellite) and is only mirrored here by the remote-claude bridge, + /// which copies the marker's `remote_host`. Drives the 🛰 menu badge so a + /// remote session reads distinctly from a local one. + var remoteHost: String = "" + + /// True for sessions running on another machine, surfaced via the bridge. + var isRemote: Bool { !remoteHost.isEmpty } var shortSubject: String { let trimmed = subject.trimmingCharacters(in: .whitespacesAndNewlines) @@ -264,7 +272,7 @@ enum ClaudeSessionReader { let parsedState: ClaudeSession.State = ((obj["state"] as? String) == "blank") ? .blank : .working - return ClaudeSession( + var session = ClaudeSession( sessionId: (obj["session_id"] as? String) ?? fallbackId, cwd: (obj["cwd"] as? String) ?? "", subject: (obj["subject"] as? String) ?? "(no subject)", @@ -275,6 +283,8 @@ enum ClaudeSessionReader { state: parsedState, awaitingMessage: nil ) + session.remoteHost = (obj["remote_host"] as? String) ?? "" + return session } private static func pidAlive(_ pid: Int) -> Bool { diff --git a/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift b/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift index 83dfd75939..c92d3d64d5 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift @@ -386,7 +386,10 @@ enum MenuBuilder { case .blank: dot = "·" // tiny midpoint — fresh, nothing yet case .stale: dot = "○" } - let tail = s.cwdLabel.isEmpty ? "" : " · \(s.cwdLabel)" + // Remote sessions (mirrored from jasellite et al. by the bridge) + // wear a 🛰 + host badge so they read distinctly from local ones. + let remoteBadge = s.isRemote ? " 🛰 \(s.remoteHost)" : "" + let tail = (s.cwdLabel.isEmpty ? "" : " · \(s.cwdLabel)") + remoteBadge // Blank sessions have no subject yet — substitute a placeholder // so the menu row reads as "· (new) · aesthetic-computer" instead // of the awkward "· · aesthetic-computer".