diff --git a/CLAUDE.md b/CLAUDE.md index df2034d..41d80b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,6 +66,11 @@ Server (`server/`, Node + Express 5 + ws, run by tsx in development; esbuild bun `ptyClient.ts` is the office's side (start/connect, spawn, adopt after a restart, local fallback); `ptyProtocol.ts` their JSON-lines messages. The launcher's `office:shutdown` says `restart: false` when quitting: CLIs stop then. A developer's CLI stays at its prompt after the task (`keepAlive`): follow-ups and prompts typed there reuse it. +- `boxes.ts`, `boxRunner.ts`: coders in Upstash Boxes (`@upstash/box`). `boxes.ts` is the box layer: the API key and + GitHub token (env or the Windows user environment), the base snapshot (`BASE_SETUP`), desk and sign-in scripts, and + the events program every hook and status line runs in a box. `boxRunner.ts`'s `BoxPty` implements `Pty` for a CLI + in tmux in the box: `tmux attach` over a PTY session (reconnects with backoff), events followed with `tail -F`. + `shared/cloudBoxes.ts` holds the settings and who may move into a box. Testers stay local. - `clis.ts`: the CLIs (Claude Code from the SDK's bundled binary, Codex, OpenCode): detection, Windows `.cmd` shim unwrapping, each one's command line, and the helper scripts they call back with. - `terminal.ts`: `AgentTerminal`, a headless xterm mirror per agent (replay for late viewers, saved to disk), its diff --git a/client/src/api.ts b/client/src/api.ts index 88083f7..b6bd32c 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -1,5 +1,5 @@ import { useStore } from './store'; -import type { AgentCli, GhRepoSummary, OfficeUpdateView, PreviewView, ProjectFolderView, RepoView, SwarmSettings } from '../../shared/types'; +import type { AgentCli, AgentMachine, BoxesView, GhRepoSummary, OfficeUpdateView, PreviewView, ProjectFolderView, RepoView, SwarmSettings } from '../../shared/types'; import type { ShipView } from '../../shared/ship'; import type { LedgerView } from '../../shared/ledger'; @@ -89,8 +89,11 @@ export const api = { sendBack: (repoId: string, n: number, note: string, agentId?: string) => call<{ to: string }>('POST', `${r(repoId)}/pulls/${n}/send-back`, { note, agentId }), hireAgent: (repoId: string, opts: { name?: string; model?: string; effort?: string; role?: 'dev' | 'qa'; title?: string; specialty?: string } = {}) => call('POST', `${r(repoId)}/agents`, opts), - updateAgent: (id: string, patch: { name?: string; model?: string; effort?: string; cli?: AgentCli | ''; look?: 'feminine' | 'masculine'; title?: string; specialty?: string; brief?: string }) => + updateAgent: (id: string, patch: { name?: string; model?: string; effort?: string; cli?: AgentCli | ''; look?: 'feminine' | 'masculine'; title?: string; specialty?: string; brief?: string; machine?: AgentMachine }) => call('PATCH', `/api/agents/${id}`, patch), + signInBox: (id: string) => call('POST', `/api/agents/${id}/box/sign-in`), + boxes: () => call('GET', '/api/boxes'), + buildBaseBox: () => call('POST', '/api/boxes/base'), fireAgent: (id: string) => call('DELETE', `/api/agents/${id}`), assign: (id: string, issueNumber: number, note?: string) => call('POST', `/api/agents/${id}/assign`, { issueNumber, note }), stop: (id: string) => call('POST', `/api/agents/${id}/stop`), diff --git a/client/src/ui/ManagerConsole.tsx b/client/src/ui/ManagerConsole.tsx index 1cbcf12..5870c55 100644 --- a/client/src/ui/ManagerConsole.tsx +++ b/client/src/ui/ManagerConsole.tsx @@ -3,7 +3,8 @@ import { api } from '../api'; import { PreviewPill, PreviewSettings } from './AppViewer'; import { agentsOnRepo, pendingRequests, useStore, type ManagerTab } from '../store'; import { CHAMBER_DYE_NAMES, CHAMBER_DYES } from '../../../shared/palette'; -import { CEO_ID, type AgentCli, type CliView, type EffortLevel, type OfficeUpdateView, type RepoView } from '../../../shared/types'; +import { CEO_ID, type AgentCli, type AgentMachine, type BoxesView, type CliView, type EffortLevel, type OfficeUpdateView, type RepoView } from '../../../shared/types'; +import { DEFAULT_CLOUD_BOXES } from '../../../shared/cloudBoxes'; import { effectiveModel } from '../../../shared/models'; import { BRIEF_MAX, MISSION_MAX, briefCount, tooLong } from '../../../shared/briefs'; import { canPostpone, canUpdateNow, drainDeadline, officeUpdateText } from '../officeUpdate'; @@ -558,6 +559,36 @@ function TeamTab() { )} + {terminal && settings.cloudBoxes?.enabled && ( + + + {a.machine === 'box' && ( + <> + {' '} + + {a.box?.signedIn === true ? '๐Ÿ”‘ signed in' : a.box?.signedIn === false ? '๐Ÿ”’ not signed in' : a.box?.id ? 'box ready' : 'no box yet'} + {' '} + {a.box?.signedIn !== true && ( + + )} + + )} + + )} s.settings); + const agents = useStore((s) => s.agents); + const cloud = settings.cloudBoxes ?? DEFAULT_CLOUD_BOXES; + const [view, setView] = useState(null); + const refresh = () => void attempt(() => api.boxes()).then((v) => v && setView(v)); + useEffect(() => { + refresh(); + const t = setInterval(refresh, 60_000); + return () => clearInterval(t); + }, [cloud.enabled, cloud.snapshotId]); + const set = (p: Partial) => void attempt(() => api.updateSettings({ cloudBoxes: { ...cloud, ...p } })); + const nameOf = (id: string | null) => (id ? (Object.values(agents).find((a) => a.id === id)?.name ?? 'someone gone') : 'no one'); + const used = view?.cpuSeconds ?? 0; + return ( +
+

โ˜ Cloud boxes

+

+ Coders can work in their own Upstash Box: a Linux machine in the cloud, signed in to your own Claude or Codex subscription. It takes their work off this computer. Testers stay here for now. +

+ + +
+
{view?.keyPresent ? 'โœ“ Upstash Box API key found' : 'โœ— No Upstash Box API key: set UPSTASH_BOX_API_KEY on this computer, then restart the dungeon.'}
+
{view?.githubTokenPresent ? 'โœ“ GitHub token for the boxes found' : 'โœ— No GitHub token for the boxes: set CODEDUNGEON_BOX_GITHUB_TOKEN (a fine-grained token for your repositories).'}
+
+ {cloud.snapshotId ? 'โœ“ Base box ready' : 'โœ— The base box isn\'t built yet.'}{' '} + +
+
+ {view && view.boxes.length > 0 && ( + + + {view.boxes.map((b) => ( + + + + + + + ))} + +
{b.name || b.id}{b.status}{nameOf(b.agentId)}{Math.round(b.cpuSeconds / 60)} CPU min
+ )} + {view && ( +
+ CPU used so far: {(used / 3600).toFixed(2)} h of the free plan's {FREE_CPU_SECONDS / 3600} h a month ({Math.round((100 * used) / FREE_CPU_SECONDS)}%). +
+ )} +
+ ); +} + function SettingsTab() { const settings = useStore((s) => s.settings); const suggested = useSystemFeed().latest?.sessions.suggested; @@ -815,6 +910,9 @@ function SettingsTab() { Coders work like your own coding agents in a terminal, with your skills, MCP servers and settings, and don't stop to ask. The dungeon's workflow (branches, pull requests, QA reporting back) is in their instructions.

+ +
+ {terminal && }

Your hold