diff --git a/slab/bin/prox-mcp.mjs b/slab/bin/prox-mcp.mjs index 4572eda6b9..849d35118d 100755 --- a/slab/bin/prox-mcp.mjs +++ b/slab/bin/prox-mcp.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node // prox-mcp.mjs — an MCP over the slab "prompt rocks" ledger, so any // agent can LIST, FIND, POKE, and launch the little tumbling sigil stones the slab -// menubar parks over every live Claude session across the fleet. +// menubar parks over every live agent session across the fleet. // // A "rock" is one live session (or headless agent), advertised by its machine // as `host:name` — e.g. neo:regif, blueberry:flock, panda:iris. The name is the @@ -43,6 +43,7 @@ const SLAB_HOME = process.env.SLAB_HOME || join(homedir(), ".local", "share", "s const MARKER_DIRS = [join(SLAB_HOME, "state", "active-prompts"), join(SLAB_HOME, "state", "awaiting-prompts")]; const shellQuote = (s) => `'${String(s).replaceAll("'", `'"'"'`)}'`; +const isCodexBacked = (agent) => agent === "codex" || agent === "aesthetic-code"; async function findFile(root, suffix) { let entries; @@ -62,7 +63,7 @@ async function transcriptFor(rock, marker) { if (marker?.transcript_path) return marker.transcript_path; const agent = marker?.agent_type || rock.agentType || "claude"; const providerId = marker?.provider_session_id || marker?.codex_session_id || rock.id; - if (agent === "codex") { + if (isCodexBacked(agent)) { return findFile(join(homedir(), ".codex", "sessions"), `${providerId}.jsonl`); } return findFile(join(homedir(), ".claude", "projects"), `${rock.id}.jsonl`); @@ -302,7 +303,20 @@ async function toolDump({ handle, destination } = {}) { }; await writeFile(join(out, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", { mode: 0o600 }); - const resume = agent === "codex" ? `#!/bin/sh + let resume; + if (agent === "aesthetic-code") { + resume = `#!/bin/sh +set -eu +bundle=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +day=$(date +%Y/%m/%d) +store="$HOME/.codex/sessions/$day" +mkdir -p "$store" +cp "$bundle/transcript.jsonl" "$store/rollout-prox-${providerId}.jsonl" +cd ${shellQuote(marker.cwd || r.cwd || homedir())} 2>/dev/null || cd "$HOME" +exec aesthetic --resume ${shellQuote(providerId)} +`; + } else if (agent === "codex") { + resume = `#!/bin/sh set -eu bundle=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) day=$(date +%Y/%m/%d) @@ -311,7 +325,9 @@ mkdir -p "$store" cp "$bundle/transcript.jsonl" "$store/rollout-prox-${providerId}.jsonl" cd ${shellQuote(marker.cwd || r.cwd || homedir())} 2>/dev/null || cd "$HOME" exec codex resume ${shellQuote(providerId)} -` : `#!/bin/sh +`; + } else { + resume = `#!/bin/sh set -eu bundle=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) project=${shellQuote((marker.cwd || r.cwd || homedir()).replaceAll("/", "-") || "-")} @@ -321,6 +337,7 @@ cp "$bundle/transcript.jsonl" "$store/${r.id}.jsonl" cd ${shellQuote(marker.cwd || r.cwd || homedir())} 2>/dev/null || cd "$HOME" exec claude --resume ${shellQuote(r.id)} `; + } await writeFile(join(out, "resume.sh"), resume, { mode: 0o700 }); await chmod(join(out, "resume.sh"), 0o700); await writeFile(join(out, "README.txt"), @@ -334,8 +351,8 @@ async function toolLaunch({ host, agent, cwd, prompt = "", by, loopboyContact = const wanted = String(host || "").trim().toLowerCase().replace(/\.local$/, ""); if (!wanted) throw new Error("`host` is required (for example, poorslice)."); const agentName = String(agent || "").trim().toLowerCase(); - if (!new Set(["claude", "codex"]).has(agentName)) { - throw new Error("`agent` must be `claude` or `codex`."); + if (!new Set(["claude", "codex", "aesthetic-code"]).has(agentName)) { + throw new Error("`agent` must be `claude`, `codex`, or `aesthetic-code`."); } if (String(prompt).length > 4000) throw new Error("`prompt` exceeds 4000 characters."); const contactKey = String(loopboyContact || "").trim().toLowerCase(); @@ -525,14 +542,14 @@ const TOOLS = [ { name: "prox_list", description: - "List the 'prompt rocks' across the slab fleet — every live agent session (Claude or Codex) and headless agent the menubar advertises, as host:name with its status (working/awaiting/complete/rendering/blank/interrupted), kind, owning agent, age, and a one-line subject. Use this to see what every machine is working on right now. Reads the local fleet ledger cache (no SSH).", + "List the prompt rocks across the Slab fleet — every live Claude, Codex, or Aesthetic Code session and headless agent the menubar advertises, as host:name with its status, kind, owning interface, age, and subject. Reads the local fleet ledger cache (no SSH).", inputSchema: { type: "object", properties: { host: { type: "string", description: "Only rocks on this machine (e.g. neo, blueberry, panda)." }, status: { type: "string", description: "Filter by status: working | awaiting | complete | rendering | blank | interrupted." }, kind: { type: "string", description: "Filter by kind: session | agent." }, - agent: { type: "string", description: "Filter by owning agent: claude | codex." }, + agent: { type: "string", description: "Filter by owning interface: claude | codex | aesthetic-code." }, }, }, }, @@ -564,7 +581,7 @@ const TOOLS = [ { name: "prox_close", description: - "Close a prompt rock — end that Claude session and shut its terminal window. Resolves a `host:name` / fuzzy handle (refuses ambiguous matches), ends the session (SIGTERM then SIGKILL — claude traps SIGTERM), and closes its Terminal.app window. DESTRUCTIVE: the running session is terminated (its transcript persists and is resumable). Only closes rocks on THIS machine (it needs the terminal window); refuses to close the calling session.", + "Close a prompt rock — end that agent session and shut its terminal window. Resolves a `host:name` / fuzzy handle (refuses ambiguous matches), ends the session, and closes its Terminal.app window. DESTRUCTIVE: the running session is terminated; its transcript remains resumable. Only closes rocks on this machine and refuses to close the calling session.", inputSchema: { type: "object", properties: { @@ -589,12 +606,12 @@ const TOOLS = [ { name: "prox_launch", description: - "Launch a new interactive Claude or Codex prompt in Terminal.app on a Slab fleet host. SIDE EFFECT: opens a live agent session and may consume account usage. The target accepts only the fixed claude/codex launchers, limits cwd to that user's home folder, and binds the endpoint to its tailnet IP; no arbitrary command is accepted.", + "Launch a new interactive Claude, Codex, or Aesthetic Code prompt in Terminal.app on a Slab fleet host. SIDE EFFECT: opens a live agent session and may consume account usage. The target accepts only fixed allowlisted launchers, limits cwd to that user's home folder, and binds the endpoint to its tailnet IP; no arbitrary command is accepted.", inputSchema: { type: "object", properties: { host: { type: "string", description: "Target Slab hostname, for example poorslice." }, - agent: { type: "string", enum: ["claude", "codex"], description: "Agent CLI to launch." }, + agent: { type: "string", enum: ["claude", "codex", "aesthetic-code"], description: "Interface to launch." }, cwd: { type: "string", description: "Optional absolute directory on the target. Defaults to its aesthetic-computer checkout and must stay under its home folder." }, prompt: { type: "string", description: "Optional initial prompt, at most 4000 characters. Omit to open an idle TUI." }, by: { type: "string", description: "Optional caller label recorded by the target." }, diff --git a/slab/bin/zzz b/slab/bin/zzz index a1e84a39bc..963ec9eb77 100755 --- a/slab/bin/zzz +++ b/slab/bin/zzz @@ -39,6 +39,7 @@ const short = (value, n = 60) => { const s = clean(value); return s.length <= n ? s : `${s.slice(0, n - 1)}…`; }; +const isCodexBacked = (agent) => agent === "codex" || agent === "aesthetic-code"; async function entries() { let names = []; @@ -72,6 +73,10 @@ function resolve(rows, query) { function wakeCommand(entry) { const cwd = shellQuote(entry.cwd); + if (entry.agentType === "aesthetic-code") { + const launcher = join(HOME_DIR, ".local", "bin", "aesthetic"); + return `cd ${cwd} && exec ${shellQuote(launcher)} --resume ${shellQuote(entry.providerSessionId)}`; + } if (entry.agentType === "codex") { const launcher = join(SLAB_BIN, "codex-slab"); return `cd ${cwd} && exec ${shellQuote(launcher)} resume ${shellQuote(entry.providerSessionId)}`; @@ -146,7 +151,9 @@ async function trackedPrompts() { async function providerFromProcess(pid) { const { stdout = "" } = await execFileAsync("/bin/ps", ["-o", "command=", "-p", String(pid)]).catch(() => ({})); - return stdout.match(/\bcodex(?:-slab)?\s+resume\s+([0-9a-f-]{36})\b/i)?.[1] || ""; + return stdout.match(/\bcodex(?:-slab)?\s+resume\s+([0-9a-f-]{36})\b/i)?.[1] + || stdout.match(/\b(?:aesthetic|ac).*?--resume\s+([0-9a-f-]{36})\b/i)?.[1] + || ""; } async function markerForPid(pid) { @@ -156,9 +163,11 @@ async function markerForPid(pid) { const firstSpace = line.search(/\s/); const tty = firstSpace < 0 ? "" : line.slice(0, firstSpace); const command = firstSpace < 0 ? line : line.slice(firstSpace).trim(); - const agentType = /\bcodex\b/i.test(command) ? "codex" : (/\bclaude\b/i.test(command) ? "claude" : ""); + const agentType = /aesthetic-code\/src\/tui\.mjs/i.test(command) + ? "aesthetic-code" + : (/\bcodex\b/i.test(command) ? "codex" : (/\bclaude\b/i.test(command) ? "claude" : "")); if (!agentType) return null; - const providerSessionId = agentType === "codex" ? await providerFromProcess(pid) : ""; + const providerSessionId = isCodexBacked(agentType) ? await providerFromProcess(pid) : ""; let cwd = ""; try { const { stdout: lsof } = await execFileAsync("/usr/sbin/lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"]); @@ -204,9 +213,10 @@ async function park(marker, { force = false } = {}) { // activity/idle checks, never to sever a Loopboy's notification target. if ((await loopboyIds()).has(sid)) throw new Error("refusing to zzz a Loopboy-bound prompt"); if ((await ancestorPids()).has(pid)) throw new Error("refusing to zzz the session running this harness"); - const providerSessionId = marker.agent_type === "codex" - ? (marker.codex_session_id || await providerFromProcess(pid)) : sid; - if (!providerSessionId) throw new Error("Codex provider thread is unknown; refresh tracking before zzz"); + const providerSessionId = isCodexBacked(marker.agent_type) + ? (marker.provider_session_id || marker.codex_session_id || await providerFromProcess(pid)) + : sid; + if (!providerSessionId) throw new Error("Provider thread is unknown; refresh tracking before zzz"); const entry = { version: 1, @@ -280,7 +290,8 @@ async function harness() { rows.forEach((entry, index) => { const cursor = index === selected ? "\x1b[7m" : ""; const cwd = basename(entry.cwd || "") || entry.cwd || "?"; - write(`${cursor} ${String(index + 1).padStart(2)} ${entry.agentType === "codex" ? "C" : "A"} ${short(entry.subject, 54).padEnd(54)} ${cwd} ${age(entry.parkedAt)} \x1b[0m\n`); + const agentMark = entry.agentType === "aesthetic-code" ? "Æ" : entry.agentType === "codex" ? "C" : "A"; + write(`${cursor} ${String(index + 1).padStart(2)} ${agentMark} ${short(entry.subject, 54).padEnd(54)} ${cwd} ${age(entry.parkedAt)} \x1b[0m\n`); }); if (message) write(`\n\x1b[36m${message}\x1b[0m`); }; diff --git a/slab/menubar-swift/README.md b/slab/menubar-swift/README.md index 6971f9bedc..10ec72478e 100644 --- a/slab/menubar-swift/README.md +++ b/slab/menubar-swift/README.md @@ -93,13 +93,15 @@ session; older peers remain compatible because both fields are optional. ## Fleet prompt hosts -Each Slab host publishes its Claude and Codex rocks on tailnet-only port 5252. +Each Slab host publishes its Claude, Codex, and Aesthetic Code rocks on +tailnet-only port 5252. The `prox` MCP can list, resolve, and poke those handles, or use `prox_launch` -to open a new interactive Claude/Codex Terminal on a host. The launch endpoint -is not a general remote shell: it accepts only those two installed launchers, -a prompt of at most 4000 characters, and an existing cwd beneath the target -user’s home directory. Use `slab/install.sh --prompt-host` to install the agent -hooks and wrappers without the legacy ambient-audio or lid-control services. +to open a new interactive Claude, Codex, or Aesthetic Code Terminal on a host. +The launch endpoint is not a general remote shell: it accepts only those three +installed launchers, a prompt of at most 4000 characters, and an existing cwd +beneath the target user’s home directory. Use `slab/install.sh --prompt-host` +to install the agent hooks and wrappers without the legacy ambient-audio or +lid-control services. ### Spatial prompt navigation diff --git a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift index 31e4795852..724947c120 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift @@ -1861,14 +1861,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { @objc func restartAllActive() { let sessions = state.claudeSessions.filter { !$0.isRemote } if sessions.isEmpty { return } - let legacyCodexCount = sessions.filter { - $0.agentType == "codex" && $0.providerSessionId.isEmpty + let missingProviderCount = sessions.filter { + $0.isCodexBacked && $0.providerSessionId.isEmpty }.count let alert = NSAlert() alert.messageText = "Refresh all local agent sessions?" - var detail = "This will stop \(sessions.count) running Claude/Codex session\(sessions.count == 1 ? "" : "s"), reload current configuration, and resume each thread in a fresh terminal. In-flight work will be interrupted." - if legacyCodexCount > 0 { - detail += " \(legacyCodexCount) Codex session\(legacyCodexCount == 1 ? "" : "s") predates refresh tracking and will reopen as a new thread this once." + var detail = "This will stop \(sessions.count) running agent session\(sessions.count == 1 ? "" : "s"), reload current configuration, and resume each thread in a fresh terminal. In-flight work will be interrupted." + if missingProviderCount > 0 { + detail += " \(missingProviderCount) provider-backed session\(missingProviderCount == 1 ? "" : "s") predates refresh tracking and will reopen as a new thread this once." } alert.informativeText = detail alert.alertStyle = .warning @@ -2276,9 +2276,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { for state: ClaudeSession.State, dark: Bool, blink: Bool = false, agentType: String = "claude" ) -> (palette: Palette, glyph: String) { - // Codex completion is a stronger attention cue than Claude's calm - // slate: coral/red, distinct from approval/elicitation amber. - if agentType == "codex", state == .complete { + // Codex-backed completion is a stronger attention cue than Claude's + // calm slate: coral/red, distinct from approval/elicitation amber. + if (agentType == "codex" || agentType == "aesthetic-code"), state == .complete { if dark { return blink ? (Palette(bg: (23500, 3200, 4200), text: (65535, 48000, 46000), @@ -2293,7 +2293,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { bold: (21000, 300, 800), cursor: (62000, 5000, 5000)), "✓ complete") } let base = baseStatusDecor(for: state, dark: dark, blink: blink) - guard agentType == "codex" else { return base } + guard agentType == "codex" || agentType == "aesthetic-code" else { return base } return (palette: codexTint(base.palette, dark: dark), glyph: base.glyph) } @@ -2457,9 +2457,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { case .interrupted: s = "complete" case .stale: s = "stale" } - // Codex gets its own settings-set family so its slightly-cooler - // palette is provisioned + switched independently of Claude's. - let agentSuffix = agentType == "codex" ? "-codex" : "" + // Codex-backed interfaces share a settings-set family so their + // slightly-cooler palette is provisioned independently of Claude's. + let agentSuffix = (agentType == "codex" || agentType == "aesthetic-code") + ? "-codex" : "" let base = "Slab-\(s)-\(dark ? "dark" : "light")\(agentSuffix)" // Only attention states ever pulse; the suffix keeps the alt // settings set distinct so Terminal.app can flip between two @@ -2750,7 +2751,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { // deciding whether to warn on close. Include shells + dev runtimes so // Slab can close terminals (dev servers, REPLs) without a popover. tm.append(" try") - tm.append(" set clean commands of slabSS to {\"screen\", \"tmux\", \"less\", \"more\", \"view\", \"mandoc\", \"tail\", \"log\", \"top\", \"htop\", \"bash\", \"zsh\", \"sh\", \"fish\", \"node\", \"npm\", \"pnpm\", \"yarn\", \"bun\", \"deno\", \"turbo\", \"vite\", \"tsx\", \"ts-node\", \"nodemon\", \"esbuild\", \"git\", \"ssh\", \"python\", \"python3\", \"ruby\", \"claude\", \"codex\", \"codex-slab\"}") + tm.append(" set clean commands of slabSS to {\"screen\", \"tmux\", \"less\", \"more\", \"view\", \"mandoc\", \"tail\", \"log\", \"top\", \"htop\", \"bash\", \"zsh\", \"sh\", \"fish\", \"node\", \"npm\", \"pnpm\", \"yarn\", \"bun\", \"deno\", \"turbo\", \"vite\", \"tsx\", \"ts-node\", \"nodemon\", \"esbuild\", \"git\", \"ssh\", \"python\", \"python3\", \"ruby\", \"claude\", \"codex\", \"codex-slab\", \"ac\", \"aesthetic\"}") tm.append(" end try") // A fresh `make new settings set` inherits Terminal's FACTORY // title components (working dir + process + size all on), not @@ -3024,8 +3025,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { fontSize: fontSize, app: app) } - /// Provider-aware sibling used by Refresh Sessions. Codex is launched via - /// its Slab wrapper so the replacement remains visible as a prompt rock. + /// Provider-aware sibling used by Refresh Sessions. Each interface uses + /// its tracked launcher so the replacement remains visible as a rock. private static func openTerminalRunningAgent( cwd: String, sessionId: String, @@ -3038,7 +3039,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { _ = fontSize let safeCwd = cwd.replacingOccurrences(of: "'", with: "'\\''") let shellCmd: String - if agentType == "codex" { + if agentType == "aesthetic-code" { + let safeSid = providerSessionId.replacingOccurrences(of: "'", with: "'\\''") + shellCmd = providerSessionId.isEmpty + ? "cd '\(safeCwd)' && aesthetic" + : "cd '\(safeCwd)' && aesthetic --resume '\(safeSid)'" + } else if agentType == "codex" { if providerSessionId.isEmpty { shellCmd = "cd '\(safeCwd)' && codex-slab" } else { diff --git a/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift b/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift index 3cfe4b1b19..1fda0d752e 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift @@ -1,7 +1,8 @@ import Foundation /// Generic alias — a tracked agent session is no longer Claude-specific -/// (Codex sessions flow through the same markers + reducer). New code should +/// (Codex and Aesthetic Code sessions flow through the same markers + reducer). +/// New code should /// prefer `AgentSession`; the old name stays valid so existing call sites /// (overlay, snapshot, menu) keep compiling. typealias AgentSession = ClaudeSession @@ -77,12 +78,20 @@ struct ClaudeSession { /// remote session reads distinctly from a local one. var remoteHost: String = "" - /// Which CLI agent owns this session — "claude" (default) or "codex". + /// Which interface owns this session — "claude" (default), "codex", or + /// "aesthetic-code". /// Read from the marker's `agent_type`; drives per-agent labels/tooltips /// in the menu. The state/color engine is agent-agnostic, so this is /// display-only. var agentType: String = "claude" + /// Aesthetic Code owns its UI while using Codex app-server as its current + /// provider bridge. Provider-thread operations therefore share Codex's + /// transcript and resume path without presenting the Codex UI. + var isCodexBacked: Bool { + agentType == "codex" || agentType == "aesthetic-code" + } + /// Optional hardware/platform destination for this prompt (for example /// `xbox`). The active-prompt marker owns this metadata so the /// platform-target-awareness-identifier-badge follows the session across @@ -100,9 +109,12 @@ struct ClaudeSession { /// True for sessions running on another machine, surfaced via the bridge. var isRemote: Bool { !remoteHost.isEmpty } - /// Human label for the owning agent ("Claude", "Codex"). + /// Human label for the owning interface. var agentLabel: String { - agentType.isEmpty ? "Claude" : agentType.prefix(1).uppercased() + agentType.dropFirst() + if agentType == "aesthetic-code" { return "Aesthetic Code" } + return agentType.isEmpty + ? "Claude" + : agentType.prefix(1).uppercased() + agentType.dropFirst() } var shortSubject: String { @@ -190,6 +202,11 @@ enum ClaudeSessionReader { // has overwritten it yet) — preserve so applyTerminalDecor // paints the appearance-matched bg. // (no-op: keep s.state == .blank) + } else if s.agentType == "aesthetic-code", + s.state == .complete || s.state == .awaiting || s.state == .interrupted { + // Aesthetic Code receives app-server lifecycle events + // directly, so its marker can state this transition without + // waiting for hook-derived side channels. } else if isInterrupted(sessionId: s.sessionId, markerPath: path) { // No awaiting marker, but no tool is running and the session // has gone quiet → interrupted (Esc) and idle. NOT green. @@ -309,12 +326,19 @@ enum ClaudeSessionReader { return (attrs?[.creationDate] as? Date) ?? updated }() - // Only `blank` is read from the marker — every other state comes - // from the awaiting-prompts cross-check in `active()`. We surface - // blank early so the reducer can preserve it (as opposed to - // promoting to .working when no awaiting marker is present). - let parsedState: ClaudeSession.State = - ((obj["state"] as? String) == "blank") ? .blank : .working + // Hook-backed clients still derive most state from the side-channel + // markers. Aesthetic Code receives direct app-server lifecycle events + // and can publish exact idle/interrupted state itself. + let agentType = (obj["agent_type"] as? String) ?? "claude" + let parsedState: ClaudeSession.State = { + switch obj["state"] as? String { + case "blank": return .blank + case "complete" where agentType == "aesthetic-code": return .complete + case "awaiting" where agentType == "aesthetic-code": return .awaiting + case "interrupted" where agentType == "aesthetic-code": return .interrupted + default: return .working + } + }() var session = ClaudeSession( sessionId: (obj["session_id"] as? String) ?? fallbackId, @@ -330,7 +354,7 @@ enum ClaudeSessionReader { awaitingMessage: nil ) session.remoteHost = (obj["remote_host"] as? String) ?? "" - session.agentType = (obj["agent_type"] as? String) ?? "claude" + session.agentType = agentType session.providerSessionId = (obj["provider_session_id"] as? String) ?? (obj["codex_session_id"] as? String) ?? (session.agentType == "claude" ? session.sessionId : "") diff --git a/slab/menubar-swift/Sources/SlabMenubar/Ledger.swift b/slab/menubar-swift/Sources/SlabMenubar/Ledger.swift index 7bcf8ad83c..2ae4e48e8b 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/Ledger.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/Ledger.swift @@ -17,7 +17,7 @@ // off the main thread, failure-tolerant, so resolves stay local + instant. // // The overlay stays local-only: rocks are never rendered for remote machines. -// The only remote control action is an allowlisted Claude/Codex launch; the +// The only remote control action is an allowlisted agent launch; the // ledger never accepts an arbitrary executable or shell command. import Foundation @@ -154,9 +154,10 @@ final class LedgerStore { /// agent, an initial prompt, and a working directory inside this user's /// home folder. private static func launchPrompt(_ body: [String: Any]) -> [String: Any] { - let agent = ((body["agent"] as? String) ?? "").lowercased() - guard agent == "claude" || agent == "codex" else { - return ["ok": false, "error": "agent must be claude or codex"] + let requestedAgent = ((body["agent"] as? String) ?? "").lowercased() + let agent = requestedAgent == "aesthetic" ? "aesthetic-code" : requestedAgent + guard agent == "claude" || agent == "codex" || agent == "aesthetic-code" else { + return ["ok": false, "error": "agent must be claude, codex, or aesthetic-code"] } let prompt = (body["prompt"] as? String) ?? "" @@ -195,9 +196,14 @@ final class LedgerStore { return ["ok": false, "error": "cwd must stay inside the target user's home folder"] } - let binary = agent == "codex" - ? "\(Paths.slabBin)/codex-slab" - : "\(Paths.home)/.local/bin/claude" + let binary: String + if agent == "codex" { + binary = "\(Paths.slabBin)/codex-slab" + } else if agent == "aesthetic-code" { + binary = "\(Paths.home)/.local/bin/aesthetic" + } else { + binary = "\(Paths.home)/.local/bin/claude" + } guard fm.isExecutableFile(atPath: binary) else { return ["ok": false, "error": "\(agent) launcher is not installed"] } @@ -217,7 +223,11 @@ final class LedgerStore { } else { command = "cd \(shellQuote(cwd)) && exec \(shellQuote(binary))" } - if !prompt.isEmpty { command += " \(shellQuote(prompt))" } + if !prompt.isEmpty { + command += agent == "aesthetic-code" + ? " --prompt \(shellQuote(prompt))" + : " \(shellQuote(prompt))" + } // Hand Terminal a one-shot executable document through LaunchServices. // This avoids the macOS Automation permission required by AppleScript @@ -655,7 +665,7 @@ final class LedgerHTTPServer { return } - // POST /launch — start one fixed Claude/Codex launcher in Terminal. + // POST /launch — start one fixed, allowlisted agent launcher in Terminal. // The callback rejects arbitrary binaries, paths outside HOME, and // oversized prompts; this HTTP layer only handles framing. if line.hasPrefix("POST"), line.contains("/launch") { diff --git a/slab/menubar-swift/Sources/SlabMenubar/ProxMemoirs.swift b/slab/menubar-swift/Sources/SlabMenubar/ProxMemoirs.swift index c17ef3a5a9..0679aac96a 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/ProxMemoirs.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/ProxMemoirs.swift @@ -141,7 +141,7 @@ final class ProxMemoirs { /// new hooks/watchers provide the exact path and skip it entirely. private func locateTranscript(for session: ClaudeSession) -> String? { let fm = FileManager.default - if session.agentType == "codex" { + if session.isCodexBacked { let needle = session.providerSessionId.isEmpty ? session.sessionId : session.providerSessionId let root = "\(Paths.home)/.codex/sessions" guard let walk = fm.enumerator(atPath: root) else { return nil } @@ -325,7 +325,7 @@ final class ProxMemoirs { else { continue } var role = "" var content: Any? - if agentType == "codex" { + if agentType == "codex" || agentType == "aesthetic-code" { guard (obj["type"] as? String) == "response_item", let payload = obj["payload"] as? [String: Any], (payload["type"] as? String) == "message" else { continue } diff --git a/slab/menubar-swift/Sources/SlabMenubar/ZzzManager.swift b/slab/menubar-swift/Sources/SlabMenubar/ZzzManager.swift index 66e4415780..1fda224777 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/ZzzManager.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/ZzzManager.swift @@ -171,8 +171,8 @@ final class ZzzManager { if session.subagentCount > 0 { return "subagents are still running" } if session.claudePid <= 0 || session.tty.isEmpty { return "missing pid/tty" } if session.cwd.isEmpty { return "missing working directory" } - if session.agentType == "codex" && session.providerSessionId.isEmpty { - return "Codex provider thread is not known yet" + if session.isCodexBacked && session.providerSessionId.isEmpty { + return "Provider thread is not known yet" } if loopboySessionIds().contains(session.sessionId) { return "Loopboy-bound prompt" } if let seconds = minimumIdle, @@ -187,7 +187,7 @@ final class ZzzManager { parking.insert(session.sessionId) defer { parking.remove(session.sessionId) } - let provider = session.agentType == "codex" + let provider = session.isCodexBacked ? session.providerSessionId : session.sessionId let entry = ZzzEntry( version: 1,