diff --git a/frontend/components/sections/AgentsSection.tsx b/frontend/components/sections/AgentsSection.tsx index be18a88..19c4b61 100644 --- a/frontend/components/sections/AgentsSection.tsx +++ b/frontend/components/sections/AgentsSection.tsx @@ -144,11 +144,12 @@ export function AgentsSection() { - {a.status === "working" && a.last_output?.trim() && ( + {(a.status === "working" || a.status === "crashed") && a.last_output?.trim() && (
- live output{a.output_at ? ` · ${timeAgo(a.output_at)}` : ""} + {a.status === "crashed" ? "last output before crash" : "live output"} + {a.output_at ? ` · ${timeAgo(a.output_at)}` : ""}
         )}
 
+        {/* Headless run event stream (empty for legacy tmux agents) */}
+        {agent?.session_id && (
+          
+
+ Run events + {agent.worker_id ? ( + + on {agent.worker_id} + + ) : null} +
+ {s.events.length === 0 ? ( +
No events yet.
+ ) : ( +
+ {s.events.map((e) => ( + + ))} +
+ )} +
+ )} + {/* Log */}
Log · newest first
@@ -304,3 +340,99 @@ function RunDetail() { ); } + +/* One stream-json event, rendered by type: assistant text as prose, tool calls as chips, + * the result as a cost/turns footer, worker notices as callouts, raw lines verbatim. */ +function EventLine({ e }: { e: AgentEvent }) { + const p = (e.payload ?? {}) as Record; + const xs = { fontSize: "var(--text-xs)" } as const; + + if (e.type === "system") { + return ( +
+ ▸ session {p.subtype ?? "event"} + {p.session_id ? ` · ${String(p.session_id).slice(0, 8)}` : ""} + {Array.isArray(p.tools) ? ` · ${p.tools.length} tools` : ""} +
+ ); + } + if (e.type === "assistant") { + const content = p.message?.content; + const blocks: any[] = Array.isArray(content) ? content : []; + const text = blocks + .filter((b) => b?.type === "text" && b.text) + .map((b) => b.text) + .join("\n"); + const tools = blocks.filter((b) => b?.type === "tool_use"); + return ( +
+ {text && ( +
{text}
+ )} + {tools.length > 0 && ( +
+ {tools.map((t, i) => ( + + {t.name} + {t.input ? `: ${oneLine(t.input)}` : ""} + + ))} +
+ )} +
+ ); + } + if (e.type === "result") { + const err = Boolean(p.is_error); + return ( +
+ {err ? "run errored" : "run finished"} + + {p.num_turns != null ? `${p.num_turns} turns` : ""} + {p.total_cost_usd != null ? ` · $${Number(p.total_cost_usd).toFixed(4)}` : ""} + + {typeof p.result === "string" && p.result && ( + + {p.result} + + )} +
+ ); + } + if (e.type === "worker") { + return ( + + {p.notice ?? "runner notice"} + {p.stderr_tail ? ( +
+            {p.stderr_tail}
+          
+ ) : null} +
+ ); + } + if (e.type === "raw") { + return ( +
+ {typeof p.line === "string" ? p.line.trimEnd() : JSON.stringify(p)} +
+ ); + } + // user (tool results) and anything future: a quiet one-liner, nothing lost, no noise. + return ( +
+ ▸ {e.type} +
+ ); +} + +/* Compact single-line preview of a tool_use input object. */ +function oneLine(input: unknown): string { + const s = + typeof input === "string" + ? input + : (input as Record)?.command + ? String((input as Record).command) + : JSON.stringify(input); + return s.length > 80 ? `${s.slice(0, 77)}…` : s; +} diff --git a/frontend/components/store.tsx b/frontend/components/store.tsx index aece0dd..62532d0 100644 --- a/frontend/components/store.tsx +++ b/frontend/components/store.tsx @@ -17,6 +17,7 @@ import { AuthError, createClient, type Agent, + type AgentEvent, type ApiError, type Approval, type Checkmark, @@ -83,6 +84,9 @@ interface StoreValue { log: LogEntry[]; logOffset: number; pageLog: (dir: 1 | -1) => void; + /* Headless run event stream for the selected run, oldest-first, appended by cursor + * polls (empty for legacy tmux agents). */ + events: AgentEvent[]; approvals: Approval[]; hosts: Host[]; @@ -204,6 +208,9 @@ export function DashboardProvider({ const [checkmarkMissing, setCheckmarkMissing] = useState(false); const [log, setLog] = useState([]); const [logOffset, setLogOffset] = useState(0); + const [events, setEvents] = useState([]); + const eventsRef = useRef([]); + eventsRef.current = events; const [approvals, setApprovals] = useState([]); const [hosts, setHosts] = useState([]); const [commands, setCommands] = useState([]); @@ -282,6 +289,17 @@ export function DashboardProvider({ } catch (e) { swallow(e); } + try { + // Cursor poll: only events newer than what we already hold come back. + const cur = eventsRef.current; + const after = cur.length ? cur[cur.length - 1].id : 0; + const fresh = await clientRef.current.api( + `${path}/events?after_id=${after}&limit=500`, + ); + if (fresh.length) setEvents((prev) => [...prev, ...fresh]); + } catch (e) { + swallow(e); + } }, []); const loadApprovals = useCallback(async (projectId: string) => { @@ -407,6 +425,8 @@ export function DashboardProvider({ setCheckmark(null); setCheckmarkMissing(false); setLog([]); + setEvents([]); + eventsRef.current = []; void loadRun(projectId, name); }, [loadRun], @@ -925,6 +945,7 @@ export function DashboardProvider({ log, logOffset, pageLog, + events, approvals, hosts, commands, diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts index d54d333..1cd937b 100644 --- a/frontend/lib/api.ts +++ b/frontend/lib/api.ts @@ -30,10 +30,28 @@ export interface Agent { working_dir: string; status: string; role?: string | null; - /* Latest tmux pane-tail snapshot from the worker, so the UI can show what a running - * agent is actually doing (and expose one wedged on an interactive prompt). */ + /* Latest output snapshot from the worker: the tmux pane tail for legacy agents, the + * latest assistant text for headless runs. For a crashed agent this is the evidence + * frame — the last thing the process said. */ last_output?: string | null; output_at?: string | null; + /* Headless runner: claude session UUID (null = legacy tmux agent) + supervising worker. */ + session_id?: string | null; + worker_id?: string | null; + created_at: string; +} + +/* One persisted stream-json event of a headless run (GET .../events, cursor-paged by id). + * `type` mirrors the stream (system/assistant/user/result) plus `worker` (runner notices) + * and `raw` (unparseable line kept verbatim). */ +export interface AgentEvent { + id: number; + agent_id: number; + run_id: number; + session_id?: string | null; + seq: number; + type: string; + payload?: Record | null; created_at: string; } diff --git a/frontend/lib/format.ts b/frontend/lib/format.ts index e60fb80..05e219a 100644 --- a/frontend/lib/format.ts +++ b/frontend/lib/format.ts @@ -21,6 +21,7 @@ export function statusTone(status: string | null | undefined): Tone { case "blocked": case "rejected": case "error": + case "crashed": return "danger"; case "pending": case "queued": diff --git a/src/handler/api/routes/agents.py b/src/handler/api/routes/agents.py index 056eae8..80b9683 100644 --- a/src/handler/api/routes/agents.py +++ b/src/handler/api/routes/agents.py @@ -15,7 +15,15 @@ from sqlalchemy.exc import IntegrityError from ...config import get_settings from ...db import repository as repo from ..deps import db_conn, require_admin, require_auth -from ..schemas import AgentIn, AgentOut, CheckmarkOut, CommandOut, LogEntryOut, SpawnIn +from ..schemas import ( + AgentEventOut, + AgentIn, + AgentOut, + CheckmarkOut, + CommandOut, + LogEntryOut, + SpawnIn, +) from .common import resolve_agent router = APIRouter( @@ -123,6 +131,23 @@ def get_checkmark(project: str, name: str, conn: Connection = Depends(db_conn)) return checkmark +@router.get("/{name}/events", response_model=list[AgentEventOut]) +def get_events( + project: str, + name: str, + after_id: int = Query(0, ge=0), + limit: int = Query(200, ge=1, le=1000), + conn: Connection = Depends(db_conn), +) -> list[dict]: + """The headless run event stream, oldest-first, cursor-paged by row id. + + The UI polls with ``after_id`` = the largest id it has seen, so each poll returns + only new events (an empty list for a legacy tmux agent or an idle one). + """ + agent = resolve_agent(conn, project, name) + return repo.list_agent_events(conn, agent["id"], after_id=after_id, limit=limit) + + @router.get("/{name}/log", response_model=list[LogEntryOut]) def get_log( project: str, diff --git a/src/handler/api/schemas.py b/src/handler/api/schemas.py index cbf825b..29fe621 100644 --- a/src/handler/api/schemas.py +++ b/src/handler/api/schemas.py @@ -128,10 +128,14 @@ class AgentOut(BaseModel): working_dir: str status: str role: Role | None = None - # Latest tmux pane-tail snapshot (worker poll loop) so the UI can show what a running - # agent is doing — including one wedged on an interactive prompt no one can answer. + # Latest output snapshot so the UI can show what a running agent is doing: the tmux + # pane tail for legacy agents, the latest assistant text for headless runs. last_output: str | None = None output_at: datetime | None = None + # Headless runner: the claude session UUID (null = legacy tmux agent) and the worker + # container supervising (or last to supervise) this agent's runs. + session_id: str | None = None + worker_id: str | None = None created_at: datetime @@ -306,6 +310,25 @@ class LogEntryOut(BaseModel): ci_checked_at: datetime | None = None +class AgentEventOut(BaseModel): + """One persisted stream-json event of a headless run (the UI's live log panel). + + ``type`` mirrors the stream's top-level type (system/assistant/user/result), plus + ``worker`` (runner-generated notices) and ``raw`` (unparseable line, kept verbatim). + """ + + model_config = ConfigDict(from_attributes=True) + + id: int + agent_id: int + run_id: int + session_id: str | None = None + seq: int + type: str + payload: dict | None = None + created_at: datetime + + class AnswerIn(BaseModel): answer: str # If omitted, the answer targets the agent's latest open question. diff --git a/src/handler/api/static/404.html b/src/handler/api/static/404.html index cc28808..7ebb5db 100644 --- a/src/handler/api/static/404.html +++ b/src/handler/api/static/404.html @@ -1 +1 @@ -404: This page could not be found.Handler · Claude Activity

404

This page could not be found.

\ No newline at end of file +404: This page could not be found.Handler · Claude Activity

404

This page could not be found.

\ No newline at end of file diff --git a/src/handler/api/static/_next/static/chunks/app/layout-b3a2b6165ef8a4d0.js b/src/handler/api/static/_next/static/chunks/app/layout-b3a2b6165ef8a4d0.js deleted file mode 100644 index 942cb25..0000000 --- a/src/handler/api/static/_next/static/chunks/app/layout-b3a2b6165ef8a4d0.js +++ /dev/null @@ -1 +0,0 @@ -(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{3156:function(n,e,u){Promise.resolve().then(u.t.bind(u,7960,23))},7960:function(){}},function(n){n.O(0,[587,971,117,744],function(){return n(n.s=3156)}),_N_E=n.O()}]); \ No newline at end of file diff --git a/src/handler/api/static/_next/static/chunks/app/layout-c871fabc4fd47962.js b/src/handler/api/static/_next/static/chunks/app/layout-c871fabc4fd47962.js new file mode 100644 index 0000000..607d9eb --- /dev/null +++ b/src/handler/api/static/_next/static/chunks/app/layout-c871fabc4fd47962.js @@ -0,0 +1 @@ +(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{2385:function(n,e,u){Promise.resolve().then(u.t.bind(u,7960,23))},7960:function(){}},function(n){n.O(0,[587,971,117,744],function(){return n(n.s=2385)}),_N_E=n.O()}]); \ No newline at end of file diff --git a/src/handler/api/static/_next/static/chunks/app/page-0637b8cf149a88ec.js b/src/handler/api/static/_next/static/chunks/app/page-0637b8cf149a88ec.js new file mode 100644 index 0000000..493e613 --- /dev/null +++ b/src/handler/api/static/_next/static/chunks/app/page-0637b8cf149a88ec.js @@ -0,0 +1 @@ +(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[931],{3963:function(e,t,s){Promise.resolve().then(s.bind(s,9859))},9859:function(e,t,s){"use strict";s.r(t),s.d(t,{default:function(){return er}});var a,n=s(7437),r=s(2265);let l=null!==(a=s(257).env.NEXT_PUBLIC_API_BASE)&&void 0!==a?a:"";class i extends Error{constructor(e="unauthorized"){super(e),this.name="AuthError"}}let c=(0,r.createContext)(null);function o(){let e=(0,r.useContext)(c);if(!e)throw Error("useDashboard outside provider");return e}function d(e){let{token:t,onUnauthorized:s,children:a}=e,o=(0,r.useMemo)(()=>(function(e,t){async function s(s,a){var n;let r=(null==a?void 0:a.body)!==void 0&&(null==a?void 0:a.body)!==null,c=await fetch(l+s,{method:null!==(n=null==a?void 0:a.method)&&void 0!==n?n:r?"POST":"GET",headers:{Authorization:"Bearer ".concat(e),...r?{"Content-Type":"application/json"}:{}},body:r?JSON.stringify(a.body):void 0});if(401===c.status)throw t(),new i;if(!c.ok){let e=c.statusText;try{let t=await c.json();t&&void 0!==t.detail&&(e="string"==typeof t.detail?t.detail:JSON.stringify(t.detail))}catch(e){}let t=Error(e);throw t.status=c.status,t}if(204===c.status)return;let o=await c.text();return o?JSON.parse(o):void 0}async function a(e,t){var a,n;let r=null!==(a=null==t?void 0:t.attempts)&&void 0!==a?a:60,l=null!==(n=null==t?void 0:t.intervalMs)&&void 0!==n?n:500;for(let t=0;tsetTimeout(e,l))}return null}return{api:s,trackCommand:a}})(t,s),[t,s]),d=(0,r.useRef)(o);d.current=o;let[u,h]=(0,r.useState)("runs"),[m,p]=(0,r.useState)([]),[x,v]=(0,r.useState)([]),[j,g]=(0,r.useState)(""),[y,f]=(0,r.useState)(null),[b,k]=(0,r.useState)(null),[w,N]=(0,r.useState)(!1),[_,C]=(0,r.useState)([]),[S,R]=(0,r.useState)(0),[I,T]=(0,r.useState)([]),A=(0,r.useRef)([]);A.current=I;let[z,E]=(0,r.useState)([]),[P,O]=(0,r.useState)([]),[M,L]=(0,r.useState)([]),[D,U]=(0,r.useState)([]),[F,B]=(0,r.useState)({log:[],context:[]}),[H,W]=(0,r.useState)({text:"",error:!1,busy:!1}),[q,G]=(0,r.useState)(""),[J,K]=(0,r.useState)(!0),[Y,V]=(0,r.useState)({status:"idle",url:"",message:""}),Q=(0,r.useRef)(u);Q.current=u;let X=(0,r.useRef)(j);X.current=j;let $=(0,r.useRef)(y);$.current=y;let Z=(0,r.useRef)(S);Z.current=S;let ee=e=>{e instanceof i||G(e.message)},et=(0,r.useCallback)(async()=>{try{let e=await d.current.api("/projects");p(e),G(""),g(t=>{var s,a;return t||(null!==(a=null===(s=e[0])||void 0===s?void 0:s.id)&&void 0!==a?a:"")})}catch(e){ee(e)}},[]),es=(0,r.useCallback)(async e=>{try{let t=await Promise.all(e.map(e=>d.current.api("/projects/".concat(encodeURIComponent(e.id),"/agents")).catch(()=>[])));v(t.flat())}catch(e){ee(e)}},[]),ea=(0,r.useCallback)(async(e,t)=>{let s="/projects/".concat(encodeURIComponent(e),"/agents/").concat(encodeURIComponent(t));try{let e=await d.current.api("".concat(s,"/checkmark"));k(e),N(!1)}catch(e){if(e instanceof i)return;404===e.status?(k(null),N(!0)):ee(e)}try{let e=await d.current.api("".concat(s,"/log?limit=").concat(100,"&offset=").concat(Z.current));C(e)}catch(e){ee(e)}try{let e=A.current,t=e.length?e[e.length-1].id:0,a=await d.current.api("".concat(s,"/events?after_id=").concat(t,"&limit=500"));a.length&&T(e=>[...e,...a])}catch(e){ee(e)}},[]),en=(0,r.useCallback)(async e=>{if(!e){E([]);return}try{E(await d.current.api("/projects/".concat(encodeURIComponent(e),"/approvals")))}catch(e){ee(e)}},[]),er=(0,r.useCallback)(async()=>{try{O(await d.current.api("/hosts"))}catch(e){ee(e)}},[]),el=(0,r.useCallback)(async()=>{try{L(await d.current.api("/commands?limit=50"))}catch(e){ee(e)}},[]),ei=(0,r.useCallback)(async()=>{try{U(await d.current.api("/schedules"))}catch(e){ee(e)}},[]),ec=(0,r.useCallback)(async()=>{try{let[e,t]=await Promise.all([d.current.api("/shared/log"),d.current.api("/shared/context")]);B({log:e,context:t})}catch(e){ee(e)}},[]),eo=(0,r.useCallback)(async()=>{let e=await d.current.api("/projects").catch(e=>(ee(e),null));e&&(p(e),g(t=>{var s,a;return t||(null!==(a=null===(s=e[0])||void 0===s?void 0:s.id)&&void 0!==a?a:"")}),await es(e));let t=Q.current,s=$.current;s&&await ea(s.projectId,s.name),"approvals"===t&&await en(X.current),"servers"===t&&await er(),"activity"===t&&await el(),"schedules"===t&&await ei(),"shared"===t&&await ec()},[es,ea,en,er,el,ei,ec]);(0,r.useEffect)(()=>{let e=!0;(async()=>{K(!0),await eo(),e&&K(!1)})();let t=setInterval(()=>{document.hidden||eo()},5e3);return()=>{e=!1,clearInterval(t)}},[eo]);let ed=(0,r.useCallback)(e=>{h(e),W({text:"",error:!1,busy:!1}),"approvals"===e&&en(X.current),"servers"===e&&er(),"activity"===e&&el(),"schedules"===e&&ei(),"shared"===e&&ec()},[en,er,el,ei,ec]),eu=(0,r.useCallback)(e=>{g(e),"approvals"===Q.current&&en(e)},[en]),eh=(0,r.useCallback)((e,t)=>{f({projectId:e,name:t}),R(0),Z.current=0,k(null),N(!1),C([]),T([]),A.current=[],ea(e,t)},[ea]),em=(0,r.useCallback)(e=>{let t=Math.max(0,S+100*e);if(t===S)return;R(t),Z.current=t;let s=$.current;s&&ea(s.projectId,s.name)},[S,ea]),ep=(0,r.useCallback)(()=>{eo()},[eo]),ex=(0,r.useCallback)(async(e,t,s)=>{W({text:"".concat(s,": queued…"),error:!1,busy:!0});try{let a=await d.current.api(e,{method:"POST",body:t}),n=await d.current.trackCommand(a.id);if(!n)return W({text:"".concat(s,": still running (see Activity). Is the worker up?"),error:!1,busy:!1}),null;let r="done"===n.status,l=n.error||(n.result?JSON.stringify(n.result):"");return W({text:"".concat(s," ").concat(r?"done":"failed").concat(l?" — "+l:""),error:!r,busy:!1}),n}catch(e){if(e instanceof i)return null;return W({text:"".concat(s," failed: ").concat(e.message),error:!0,busy:!1}),null}},[]),ev=(0,r.useCallback)(async e=>{let t={name:e.name.trim(),role:e.role||null,task:e.task.trim()||null};"worktree"===e.placement&&e.worktree.trim()&&(t.worktree=e.worktree.trim()),"subdir"===e.placement&&e.subdir.trim()&&(t.subdir=e.subdir.trim());let s=encodeURIComponent(X.current),a=await ex("/projects/".concat(s,"/agents/spawn"),t,"spawn ".concat(t.name));return await es(m),(null==a?void 0:a.status)==="done"},[ex,es,m]),ej=(0,r.useCallback)(async(e,t)=>{let s=encodeURIComponent(e);await ex("/projects/".concat(s,"/agents/").concat(encodeURIComponent(t),"/kill"),void 0,"kill ".concat(t)),await es(m)},[ex,es,m]),eg=(0,r.useCallback)(async(e,t)=>{let s=encodeURIComponent(e);try{var a;await d.current.api("/projects/".concat(s,"/agents/").concat(encodeURIComponent(t)),{method:"DELETE"}),W({text:"agent '".concat(t,"' row deleted"),error:!1,busy:!1}),(null===(a=$.current)||void 0===a?void 0:a.name)===t&&f(null),await es(m)}catch(e){if(e instanceof i)return;W({text:e.message,error:!0,busy:!1})}},[es,m]),ey=(0,r.useCallback)(async(e,t)=>{let s=$.current;if(!s)return!1;let a="/projects/".concat(encodeURIComponent(s.projectId),"/agents/").concat(encodeURIComponent(s.name));try{return await d.current.api("".concat(a,"/answer"),{method:"POST",body:{answer:e}}),t?await ex("".concat(a,"/resume"),{answer:e},"resume"):W({text:"Answer saved (agent still paused).",error:!1,busy:!1}),await es(m),await ea(s.projectId,s.name),!0}catch(e){if(e instanceof i)return!1;return W({text:e.message,error:!0,busy:!1}),!1}},[ex,es,ea,m]),ef=(0,r.useCallback)(async e=>{try{var t,s;let a="server"===e.mode?{git_server:e.git_server,repo:e.repo.trim(),id:e.id.trim()||null,credential_ref:e.credential_ref.trim()||null,init_mise:e.init_mise}:{id:e.id.trim(),root_dir:e.root_dir.trim(),git_remote:e.git_remote.trim()||null,credential_ref:e.credential_ref.trim()||null,init_mise:e.init_mise},n=await d.current.api("/projects",{method:"POST",body:a});if(await et(),null!=n.sync_command_id){W({text:"repository '".concat(n.id,"': cloning…"),error:!1,busy:!0});let e=await d.current.trackCommand(n.sync_command_id);e?"done"===e.status?W({text:"repository '".concat(n.id,"' registered and cloned"),error:!1,busy:!1}):W({text:"repository '".concat(n.id,"' registered but the clone failed — ").concat(null!==(t=e.error)&&void 0!==t?t:""),error:!0,busy:!1}):W({text:"repository '".concat(n.id,"' registered; clone still running (see Activity). Is the worker up?"),error:!1,busy:!1})}else W({text:"repository '".concat(n.id,"' registered"),error:!1,busy:!1});if(null!=n.mise_init_command_id){W({text:"repository '".concat(n.id,"': launching a mise-init agent to create .mise.toml…"),error:!1,busy:!0});let e=await d.current.trackCommand(n.mise_init_command_id);e?"done"===e.status?W({text:"repository '".concat(n.id,"' registered; a mise-init agent is now writing, committing, and pushing .mise.toml (watch it in Runs)."),error:!1,busy:!1}):W({text:"repository '".concat(n.id,"' registered but the mise-init agent failed to launch — ").concat(null!==(s=e.error)&&void 0!==s?s:""),error:!0,busy:!1}):W({text:"repository '".concat(n.id,"' registered; mise-init still starting (see Activity). Is the worker up?"),error:!1,busy:!1})}return!0}catch(e){if(e instanceof i)return!1;return W({text:e.message,error:!0,busy:!1}),!1}},[et]),eb=(0,r.useCallback)(async e=>{await ex("/projects/".concat(encodeURIComponent(e),"/sync"),void 0,"pull ".concat(e))},[ex]),ek=(0,r.useCallback)(async(e,t)=>{try{return await d.current.api("/projects/".concat(encodeURIComponent(e)),{method:"PATCH",body:{root_dir:t.root_dir.trim(),git_remote:t.git_remote.trim()||null,credential_ref:t.credential_ref.trim()||null}}),W({text:"repository '".concat(e,"' updated"),error:!1,busy:!1}),await et(),!0}catch(e){if(e instanceof i)return!1;return W({text:e.message,error:!0,busy:!1}),!1}},[et]),ew=(0,r.useCallback)(async e=>{try{await d.current.api("/projects/".concat(encodeURIComponent(e)),{method:"DELETE"}),W({text:"repository '".concat(e,"' removed"),error:!1,busy:!1}),g(t=>t===e?"":t),await et()}catch(e){if(e instanceof i)return;W({text:e.message,error:!0,busy:!1})}},[et]),eN=(0,r.useCallback)(async e=>{let t=encodeURIComponent(X.current);await ex("/projects/".concat(t,"/approvals"),{branch:e.branch.trim(),status:e.status,agent_name:e.agent_name.trim()||null,sha:e.sha.trim()||null,note:e.note.trim()||null},"".concat(e.status," ").concat(e.branch)),await en(X.current)},[ex,en]),e_=(0,r.useCallback)(async e=>{try{return await d.current.api("/hosts",{method:"POST",body:{hostname:e.hostname.trim(),forge_type:e.forge_type,token_env_var:e.token_env_var.trim()||null,base_url:e.base_url.trim()||null,token:e.token.trim()||null,generate_ssh_key:e.generate_ssh_key}}),W({text:"git server '".concat(e.hostname,"' added"),error:!1,busy:!1}),await er(),!0}catch(e){if(e instanceof i)return!1;return W({text:e.message,error:!0,busy:!1}),!1}},[er]),eC=(0,r.useCallback)(async(e,t)=>{try{let s={forge_type:t.forge_type,token_env_var:t.token_env_var.trim()||null,base_url:t.base_url.trim()||null};return t.token.trim()&&(s.token=t.token.trim()),t.generate_ssh_key&&(s.regenerate_ssh_key=!0),await d.current.api("/hosts/".concat(encodeURIComponent(e)),{method:"PATCH",body:s}),W({text:"git server '".concat(e,"' updated"),error:!1,busy:!1}),await er(),!0}catch(e){if(e instanceof i)return!1;return W({text:e.message,error:!0,busy:!1}),!1}},[er]),eS=(0,r.useCallback)(async(e,t)=>{try{return await d.current.api("/projects/".concat(encodeURIComponent(e),"/schedules"),{method:"POST",body:{name_prefix:t.name_prefix.trim(),task:t.task.trim(),interval_seconds:t.interval_seconds,role:t.role||null}}),W({text:"schedule '".concat(t.name_prefix,"' created — first run on the worker's next pass"),error:!1,busy:!1}),await ei(),!0}catch(e){if(e instanceof i)return!1;return W({text:e.message,error:!0,busy:!1}),!1}},[ei]),eR=(0,r.useCallback)(async(e,t)=>{try{return await d.current.api("/schedules/".concat(e),{method:"PATCH",body:t}),await ei(),!0}catch(e){if(e instanceof i)return!1;return W({text:e.message,error:!0,busy:!1}),!1}},[ei]),eI=(0,r.useCallback)(async e=>{try{await d.current.api("/schedules/".concat(e),{method:"DELETE"}),W({text:"schedule ".concat(e," removed"),error:!1,busy:!1}),await ei()}catch(e){if(e instanceof i)return;W({text:e.message,error:!0,busy:!1})}},[ei]),eT=(0,r.useCallback)(async e=>{try{await d.current.api("/hosts/".concat(encodeURIComponent(e)),{method:"DELETE"}),W({text:"git server '".concat(e,"' removed"),error:!1,busy:!1}),await er()}catch(e){if(e instanceof i)return;W({text:e.message,error:!0,busy:!1})}},[er]),eA=(0,r.useCallback)(async()=>{await ex("/poll-ci",void 0,"poll-ci (all projects)"),await el()},[ex,el]),ez=(0,r.useCallback)(async()=>{V({status:"starting",url:"",message:"Opening `claude /login` in the control container and selecting the subscription account…"});try{let e=await d.current.api("/login/start",{method:"POST"}),t=await d.current.trackCommand(e.id,{attempts:180});if(!t){V({status:"error",url:"",message:"Still starting (see Activity). Is the control worker running?"});return}if("done"!==t.status){V({status:"error",url:"",message:t.error||"Failed to start login."});return}let s=t.result&&"string"==typeof t.result.url?t.result.url:"";if(!s){V({status:"error",url:"",message:"No login URL was returned by claude."});return}V({status:"awaiting",url:s,message:"Authorize in the window below (or open it in a new tab), then paste the code claude gives you."})}catch(e){if(e instanceof i)return;V({status:"error",url:"",message:e.message})}},[]),eE=(0,r.useCallback)(async e=>{let t=e.trim();if(!t)return!1;V(e=>({...e,status:"submitting",message:"Submitting the authorization code…"}));try{let e=await d.current.api("/login/submit",{method:"POST",body:{code:t}}),s=await d.current.trackCommand(e.id,{attempts:60});if(!s)return V(e=>({...e,status:"awaiting",message:"Submit still running (see Activity). Is the control worker running?"})),!1;if("done"===s.status)return V({status:"done",url:"",message:"Claude Code is now logged in on the host — new agents will use this account."}),!0;return V(e=>({...e,status:"awaiting",message:s.error||"Login was not confirmed. Re-check the code, or restart the flow."})),!1}catch(e){if(e instanceof i)return!1;return V(t=>({...t,status:"awaiting",message:e.message})),!1}},[]),eP=(0,r.useCallback)(()=>{V({status:"idle",url:"",message:""})},[]),eO=(0,r.useCallback)(async(e,t)=>{try{return await d.current.api("/shared/context/".concat(encodeURIComponent(e)),{method:"PUT",body:{value:t}}),W({text:"shared context '".concat(e,"' set"),error:!1,busy:!1}),await ec(),!0}catch(e){if(e instanceof i)return!1;return W({text:e.message,error:!0,busy:!1}),!1}},[ec]);return(0,n.jsx)(c.Provider,{value:{section:u,setSection:ed,projects:m,agents:x,selectedProjectId:j,selectProject:eu,selectedRun:y,selectRun:eh,checkmark:b,checkmarkMissing:w,log:_,logOffset:S,pageLog:em,events:I,approvals:z,hosts:P,commands:M,schedules:D,shared:F,cmd:H,lastError:q,loading:J,refresh:ep,spawnAgent:ev,killAgent:ej,deleteAgent:eg,submitAnswer:ey,createProject:ef,updateProject:ek,deleteProject:ew,syncProject:eb,submitApproval:eN,createHost:e_,updateHost:eC,deleteHost:eT,createSchedule:eS,updateSchedule:eR,deleteSchedule:eI,pollCi:eA,setSharedKey:eO,claudeLogin:Y,startClaudeLogin:ez,submitClaudeCode:eE,resetClaudeLogin:eP},children:a})}function u(e){switch((null!=e?e:"").toLowerCase()){case"pass":case"done":case"completed":case"approved":case"success":return"success";case"fail":case"failed":case"blocked":case"rejected":case"error":case"crashed":return"danger";case"pending":case"queued":case"running":case"working":case"paused_for_input":return"warning";case"not_applicable":case"unknown":case"":return"neutral";default:return"info"}}let h={paused_for_input:"Needs input",not_applicable:"N/A"};function m(e){if(!e)return"—";let t=new Date(e);return Number.isNaN(t.getTime())?String(e):t.toLocaleString(void 0,{year:"numeric",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})}function p(e){return e?e.slice(0,7):"—"}function x(e){if(!e)return"—";let t=new Date(e).getTime();if(Number.isNaN(t))return"—";let s=Math.max(0,Math.floor((Date.now()-t)/1e3));if(s<60)return"".concat(s,"s");let a=Math.floor(s/60);if(a<60)return"".concat(a,"m");let n=Math.floor(a/60);if(n<24)return"".concat(n,"h");let r=Math.floor(n/24);if(r<30)return"".concat(r,"d");let l=Math.floor(r/30);return l<12?"".concat(l,"mo"):"".concat(Math.floor(l/12),"y")}function v(e){let{tone:t="neutral",pill:s=!1,dot:a=!1,children:r}=e;return(0,n.jsxs)("span",{className:"badge badge-".concat(t).concat(s?" pill":""),children:[a&&(0,n.jsx)("span",{className:"dot"}),r]})}function j(e){let{status:t}=e;return(0,n.jsx)(v,{tone:u(t),children:function(e){let t=(null!=e?e:"").trim();if(!t)return"—";let s=t.toLowerCase();return h[s]?h[s]:s.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase())}(t)})}function g(e){let{children:t,interactive:s=!1,onClick:a,className:r=""}=e;return(0,n.jsx)("div",{className:"card".concat(s?" interactive":""," ").concat(r).trim(),onClick:a,role:s?"button":void 0,tabIndex:s?0:void 0,children:t})}function y(e){let{variant:t="secondary",size:s="md",onClick:a,disabled:r,type:l="button",children:i}=e;return(0,n.jsx)("button",{type:l,className:"btn btn-".concat(t).concat("sm"===s?" btn-sm":""),onClick:a,disabled:r,children:i})}function f(e){let{label:t,children:s}=e;return(0,n.jsxs)("label",{className:"field",children:[t&&(0,n.jsx)("span",{className:"field-label",children:t}),s]})}function b(e){let{label:t,value:s,onChange:a,placeholder:r,type:l="text",disabled:i}=e;return(0,n.jsx)(f,{label:t,children:(0,n.jsx)("input",{className:"input",type:l,value:s,placeholder:r,disabled:i,onChange:e=>a(e.target.value)})})}function k(e){let{label:t,value:s,onChange:a,placeholder:r,rows:l=3}=e;return(0,n.jsx)(f,{label:t,children:(0,n.jsx)("textarea",{className:"textarea",value:s,rows:l,placeholder:r,onChange:e=>a(e.target.value)})})}function w(e){let{label:t,value:s,onChange:a,options:r}=e;return(0,n.jsx)(f,{label:t,children:(0,n.jsx)("select",{className:"select",value:s,onChange:e=>a(e.target.value),children:r.map(e=>(0,n.jsx)("option",{value:e.value,children:e.label},e.value))})})}function N(e){let{tabs:t,value:s,onChange:a}=e;return(0,n.jsx)("div",{className:"tabs",role:"tablist",children:t.map(e=>(0,n.jsx)("button",{role:"tab","aria-selected":s===e.value,className:"tab".concat(s===e.value?" active":""),onClick:()=>a(e.value),children:e.label},e.value))})}function _(e){let{value:t,label:s,sub:a,accent:r=!1}=e;return(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"stat-value".concat(r?" accent":""),children:t}),(0,n.jsx)("div",{className:"stat-label",children:s}),a&&(0,n.jsx)("div",{className:"stat-sub",children:a})]})}function C(e){let{tone:t="info",children:s}=e;return(0,n.jsx)("div",{className:"callout callout-".concat(t),children:s})}function S(e){let{on:t,onClick:s}=e;return(0,n.jsx)("button",{type:"button",className:"toggle".concat(t?" on":""),"aria-pressed":t,onClick:s,children:(0,n.jsx)("span",{className:"knob"})})}let R=[{value:"all",label:"All"},{value:"needs",label:"Needs Input"},{value:"working",label:"Working"},{value:"done",label:"Done"},{value:"crashed",label:"Crashed"}];function I(){let e=o(),[t,s]=(0,r.useState)("all"),a=(0,r.useMemo)(()=>[...e.agents.filter(e=>{var s;return s=e.status,"all"===t||("needs"===t?"paused_for_input"===s:"working"===t?"working"===s||"running"===s:"done"===t?"done"===s||"completed"===s:"crashed"!==t||"crashed"===s||"blocked"===s)})].sort((e,t)=>e.created_at"paused_for_input"===e.status).length,c=e.agents.filter(e=>"working"===e.status||"running"===e.status).length;return(0,n.jsxs)("div",{className:"runs",children:[(0,n.jsx)("div",{className:"runs-stats",children:(0,n.jsxs)("div",{className:"stat-row",children:[(0,n.jsx)("div",{className:"stat-cell",children:(0,n.jsx)(_,{value:e.agents.length,label:"Runs tracked"})}),(0,n.jsx)("div",{className:"stat-cell",children:(0,n.jsx)(_,{value:i,label:"Needs input",accent:!0})}),(0,n.jsx)("div",{className:"stat-cell",children:(0,n.jsx)(_,{value:c,label:"Working"})}),(0,n.jsx)("div",{className:"stat-cell",children:(0,n.jsx)(_,{value:e.projects.length,label:"Repositories"})})]})}),(0,n.jsxs)("div",{className:"split",children:[(0,n.jsxs)("div",{className:"split-list",children:[(0,n.jsxs)("div",{className:"split-list-head",children:[(0,n.jsx)("div",{className:"section-title",style:{fontSize:"var(--text-lg)"},children:"Runs"}),(0,n.jsx)(N,{tabs:R,value:t,onChange:s})]}),(0,n.jsxs)("div",{className:"split-list-scroll",children:[0===a.length&&(0,n.jsx)(C,{tone:"info",children:"No runs match this filter."}),a.map(t=>(0,n.jsx)(T,{agent:t,selected:(null==l?void 0:l.projectId)===t.project_id&&(null==l?void 0:l.name)===t.name,onSelect:()=>e.selectRun(t.project_id,t.name)},"".concat(t.project_id,"/").concat(t.name)))]})]}),(0,n.jsx)("div",{className:"split-detail",children:l?(0,n.jsx)(z,{}):(0,n.jsx)(A,{})})]})]})}function T(e){let{agent:t,selected:s,onSelect:a}=e;return(0,n.jsxs)("button",{className:"run-row".concat(s?" selected":""),onClick:a,children:[(0,n.jsxs)("div",{className:"run-row-top",children:[(0,n.jsx)("span",{className:"run-project",children:t.project_id}),(0,n.jsx)("span",{className:"faint mono",style:{fontSize:"var(--text-xs)"},children:x(t.created_at)})]}),(0,n.jsxs)("div",{className:"truncate muted",style:{fontSize:"var(--text-sm)"},children:[t.name,t.role?" \xb7 ".concat(t.role):""]}),(0,n.jsx)("div",{className:"hstack",style:{gap:8},children:(0,n.jsx)(j,{status:t.status})})]})}function A(){return(0,n.jsx)("div",{style:{padding:"60px 32px",color:"var(--text-muted)"},children:"Select a run to see its checkmark, log, and any open question."})}function z(){var e;let t=o(),s=t.selectedRun,a=t.agents.find(e=>e.project_id===s.projectId&&e.name===s.name),l=t.checkmark,[i,c]=(0,r.useState)(""),[d,h]=(0,r.useState)(!1),x=(null==a?void 0:a.status)==="paused_for_input",g=async e=>{if(!i.trim())return;h(!0);let s=await t.submitAnswer(i.trim(),e);h(!1),s&&c("")};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{style:{padding:"24px 28px",borderBottom:"1px solid var(--border-default)",display:"flex",flexDirection:"column",gap:10},children:[(0,n.jsxs)("div",{className:"hstack",children:[(0,n.jsx)("span",{style:{color:"var(--accent)",fontWeight:"var(--fw-bold)",fontSize:"var(--text-xl)"},children:s.projectId}),(0,n.jsx)("span",{className:"faint",children:"/"}),(0,n.jsx)("span",{style:{color:"var(--text-heading)",fontWeight:"var(--fw-semibold)",fontSize:"var(--text-lg)"},children:s.name}),(0,n.jsx)(j,{status:null==a?void 0:a.status}),(null==a?void 0:a.role)&&(0,n.jsx)(v,{tone:"info",children:a.role}),(0,n.jsx)("span",{className:"spacer"}),(0,n.jsx)(y,{size:"sm",variant:"secondary",onClick:()=>t.killAgent(s.projectId,s.name),children:"Kill"}),(0,n.jsx)(y,{size:"sm",variant:"danger",onClick:()=>t.deleteAgent(s.projectId,s.name),children:"Delete row"})]}),(0,n.jsxs)("div",{className:"mono faint",style:{fontSize:"var(--text-xs)"},children:[null!==(e=null==a?void 0:a.working_dir)&&void 0!==e?e:"—"," \xb7 created ",m(null==a?void 0:a.created_at)]})]}),(0,n.jsxs)("div",{style:{padding:"20px 28px",display:"flex",flexDirection:"column",gap:16},children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"eyebrow",style:{marginBottom:10},children:"Checkmark"}),t.checkmarkMissing&&(0,n.jsx)(C,{tone:"info",children:"No checkpoint recorded yet."}),l&&!t.checkmarkMissing&&(0,n.jsxs)("dl",{className:"kv",children:[(0,n.jsx)("dt",{children:"Status"}),(0,n.jsx)("dd",{children:(0,n.jsx)(j,{status:l.status})}),(0,n.jsx)("dt",{children:"Where it stopped"}),(0,n.jsx)("dd",{children:l.where_it_stopped||"—"}),(0,n.jsx)("dt",{children:"Open question"}),(0,n.jsx)("dd",{children:l.open_question||"—"}),(0,n.jsx)("dt",{children:"Next steps"}),(0,n.jsx)("dd",{children:l.next_steps&&l.next_steps.length>0?(0,n.jsx)("ul",{children:l.next_steps.map((e,t)=>(0,n.jsx)("li",{children:e},t))}):"—"}),(0,n.jsx)("dt",{children:"Tests"}),(0,n.jsxs)("dd",{className:"hstack",children:[(0,n.jsx)(v,{tone:u(l.tests_status),children:l.tests_status}),(0,n.jsx)("span",{className:"faint mono",style:{fontSize:"var(--text-xs)"},children:l.tested_at?m(l.tested_at):""})]}),(0,n.jsx)("dt",{children:"Build"}),(0,n.jsxs)("dd",{className:"hstack",children:[(0,n.jsx)(v,{tone:u(l.build_status),children:l.build_status}),(0,n.jsx)("span",{className:"faint mono",style:{fontSize:"var(--text-xs)"},children:l.built_at?m(l.built_at):""})]}),(0,n.jsx)("dt",{children:"Checkpoint at"}),(0,n.jsx)("dd",{className:"faint",children:m(l.checkpoint_at)})]})]}),x&&(0,n.jsxs)("div",{style:{display:"flex",flexDirection:"column",gap:10},children:[(0,n.jsx)("div",{className:"eyebrow",children:"Answer this question"}),(0,n.jsx)(C,{tone:"danger",children:(null==l?void 0:l.open_question)||"(no question text on the checkmark)"}),(0,n.jsx)(k,{value:i,onChange:c,rows:3,placeholder:"Your answer…"}),(0,n.jsxs)("div",{className:"hstack",children:[(0,n.jsx)(y,{variant:"secondary",disabled:d||!i.trim(),onClick:()=>g(!1),children:"Answer"}),(0,n.jsx)(y,{variant:"primary",disabled:d||!i.trim(),onClick:()=>g(!0),children:"Answer & Resume"})]})]}),(null==a?void 0:a.session_id)&&(0,n.jsxs)("div",{style:{display:"flex",flexDirection:"column",gap:10},children:[(0,n.jsxs)("div",{className:"eyebrow",children:["Run events",a.worker_id?(0,n.jsxs)("span",{className:"faint mono",style:{fontSize:"var(--text-xs)",marginLeft:8},children:["on ",a.worker_id]}):null]}),0===t.events.length?(0,n.jsx)("div",{className:"empty",children:"No events yet."}):(0,n.jsx)("div",{style:{display:"flex",flexDirection:"column",gap:6,maxHeight:420,overflow:"auto",padding:"10px 12px",background:"var(--surface-2, rgba(0,0,0,0.25))",borderRadius:6},children:t.events.map(e=>(0,n.jsx)(E,{e:e},e.id))})]}),(0,n.jsxs)("div",{style:{display:"flex",flexDirection:"column",gap:10},children:[(0,n.jsx)("div",{className:"eyebrow",children:"Log \xb7 newest first"}),0===t.log.length?(0,n.jsx)("div",{className:"empty",children:"No log entries."}):(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"When"}),(0,n.jsx)("th",{children:"Status"}),(0,n.jsx)("th",{children:"Summary"}),(0,n.jsx)("th",{children:"Q / A"}),(0,n.jsx)("th",{children:"Push"}),(0,n.jsx)("th",{children:"CI"})]})}),(0,n.jsx)("tbody",{children:t.log.map(e=>(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{className:"faint nowrap",children:m(e.created_at)}),(0,n.jsx)("td",{children:(0,n.jsx)(j,{status:e.status})}),(0,n.jsx)("td",{children:e.summary||"—"}),(0,n.jsxs)("td",{children:[e.question&&(0,n.jsxs)("div",{children:[(0,n.jsx)("strong",{children:"Q:"})," ",e.question]}),e.answer&&(0,n.jsxs)("div",{children:[(0,n.jsx)("strong",{children:"A:"})," ",e.answer]}),!e.question&&!e.answer&&"—"]}),(0,n.jsx)("td",{className:"mono",children:p(e.push_sha)}),(0,n.jsx)("td",{children:(0,n.jsx)(v,{tone:u(e.ci_status),children:e.ci_status})})]},e.id))})]})}),(0,n.jsxs)("div",{className:"pager",children:[(0,n.jsx)(y,{size:"sm",variant:"ghost",disabled:0===t.logOffset,onClick:()=>t.pageLog(-1),children:"‹ Newer"}),(0,n.jsxs)("span",{className:"faint mono",style:{fontSize:"var(--text-xs)"},children:["offset ",t.logOffset]}),(0,n.jsx)(y,{size:"sm",variant:"ghost",disabled:t.log.length<100,onClick:()=>t.pageLog(1),children:"Older ›"})]})]})]})]})}function E(e){var t,s,a,r;let{e:l}=e,i=null!==(t=l.payload)&&void 0!==t?t:{},c={fontSize:"var(--text-xs)"};if("system"===l.type)return(0,n.jsxs)("div",{className:"faint mono",style:c,children:["▸ session ",null!==(s=i.subtype)&&void 0!==s?s:"event",i.session_id?" \xb7 ".concat(String(i.session_id).slice(0,8)):"",Array.isArray(i.tools)?" \xb7 ".concat(i.tools.length," tools"):""]});if("assistant"===l.type){let e=null===(a=i.message)||void 0===a?void 0:a.content,t=Array.isArray(e)?e:[],s=t.filter(e=>(null==e?void 0:e.type)==="text"&&e.text).map(e=>e.text).join("\n"),r=t.filter(e=>(null==e?void 0:e.type)==="tool_use");return(0,n.jsxs)("div",{style:{display:"flex",flexDirection:"column",gap:4},children:[s&&(0,n.jsx)("div",{style:{fontSize:"var(--text-sm)",whiteSpace:"pre-wrap"},children:s}),r.length>0&&(0,n.jsx)("div",{className:"hstack",style:{gap:6,flexWrap:"wrap"},children:r.map((e,t)=>(0,n.jsxs)(v,{tone:"info",children:[e.name,e.input?": ".concat(function(e){let t="string"==typeof e?e:(null==e?void 0:e.command)?String(e.command):JSON.stringify(e);return t.length>80?"".concat(t.slice(0,77),"…"):t}(e.input)):""]},t))})]})}if("result"===l.type){let e=!!i.is_error;return(0,n.jsxs)("div",{className:"hstack",style:{gap:8,flexWrap:"wrap"},children:[(0,n.jsx)(v,{tone:e?"danger":"success",children:e?"run errored":"run finished"}),(0,n.jsxs)("span",{className:"faint mono",style:c,children:[null!=i.num_turns?"".concat(i.num_turns," turns"):"",null!=i.total_cost_usd?" \xb7 $".concat(Number(i.total_cost_usd).toFixed(4)):""]}),"string"==typeof i.result&&i.result&&(0,n.jsx)("span",{className:"muted",style:{...c,whiteSpace:"pre-wrap",width:"100%"},children:i.result})]})}return"worker"===l.type?(0,n.jsxs)(C,{tone:"danger",children:[null!==(r=i.notice)&&void 0!==r?r:"runner notice",i.stderr_tail?(0,n.jsx)("pre",{className:"mono",style:{...c,margin:"6px 0 0",whiteSpace:"pre-wrap"},children:i.stderr_tail}):null]}):"raw"===l.type?(0,n.jsx)("div",{className:"faint mono",style:{...c,whiteSpace:"pre-wrap"},children:"string"==typeof i.line?i.line.trimEnd():JSON.stringify(i)}):(0,n.jsxs)("div",{className:"faint mono",style:c,children:["▸ ",l.type]})}let P={mode:"server",git_server:"",repo:"",id:"",root_dir:"",git_remote:"",credential_ref:"",init_mise:!1};function O(){let e=o(),[t,s]=(0,r.useState)(P),[a,l]=(0,r.useState)(!1),i=(0,r.useMemo)(()=>{var t;let s=new Map;for(let a of e.agents)s.set(a.project_id,(null!==(t=s.get(a.project_id))&&void 0!==t?t:0)+1);return s},[e.agents]),c=(0,r.useMemo)(()=>[{value:"",label:e.hosts.length?"Pick a git server…":"No git servers configured"},...e.hosts.map(e=>({value:e.hostname,label:"".concat(e.hostname," (").concat(e.forge_type,")")}))],[e.hosts]),d=()=>{s(P),l(!1)},u=async()=>{(a?await e.updateProject(t.id,t):await e.createProject(t))&&d()},h=e=>{var t,a;s({...P,mode:"manual",id:e.id,root_dir:e.root_dir,git_remote:null!==(t=e.git_remote)&&void 0!==t?t:"",credential_ref:null!==(a=e.credential_ref)&&void 0!==a?a:""}),l(!0)},p=a?!!t.root_dir.trim():"server"===t.mode?!!t.git_server&&/^[\w.-]+\/[\w.-]+$/.test(t.repo.trim()):!!t.id.trim()&&!!t.root_dir.trim();return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Repositories"}),(0,n.jsx)("div",{className:"section-desc",children:"Repos Handler manages. Each carries its own agents, history, and credentials."})]}),(0,n.jsxs)("div",{className:"section-body",children:[(0,n.jsxs)(g,{children:[(0,n.jsx)("div",{className:"card-head",style:{marginBottom:14},children:(0,n.jsx)("span",{className:"card-title",style:{fontSize:"var(--text-md)",color:"var(--text-heading)"},children:a?"Edit repository \xb7 ".concat(t.id):"Add a repository"})}),!a&&(0,n.jsx)("div",{style:{marginBottom:14},children:(0,n.jsx)(N,{tabs:[{value:"server",label:"From a git server"},{value:"manual",label:"Manual (existing checkout)"}],value:t.mode,onChange:e=>s({...t,mode:e})})}),a||"server"!==t.mode?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(b,{label:"ID / slug",value:t.id,onChange:e=>s({...t,id:e}),placeholder:"leeworks-api",disabled:a}),(0,n.jsx)(b,{label:"Root dir",value:t.root_dir,onChange:e=>s({...t,root_dir:e}),placeholder:"/var/lib/handler/projects/leeworks"}),(0,n.jsx)(b,{label:"Git remote",value:t.git_remote,onChange:e=>s({...t,git_remote:e}),placeholder:"git@github.com:user/repo.git (optional)"}),(0,n.jsx)(b,{label:"Credential ref",value:t.credential_ref,onChange:e=>s({...t,credential_ref:e}),placeholder:"env:VAR / file:/path / db:host:github.com"})]}),(0,n.jsx)("p",{className:"faint",style:{fontSize:"var(--text-xs)",margin:"10px 0 0"},children:"Optional override — projects on a configured git server use its stored token automatically. credential_ref is a pointer, never the token (env: / file: / db:host:)."})]}):(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(w,{label:"Git server",value:t.git_server,onChange:e=>s({...t,git_server:e}),options:c}),(0,n.jsx)(b,{label:"Repository (owner/name)",value:t.repo,onChange:e=>s({...t,repo:e}),placeholder:"me/coolproj"}),(0,n.jsx)(b,{label:"ID / slug (optional — defaults to the repo name)",value:t.id,onChange:e=>s({...t,id:e}),placeholder:"coolproj"})]}),(0,n.jsx)("p",{className:"faint",style:{fontSize:"var(--text-xs)",margin:"10px 0 0"},children:"The repo is always pulled: Handler derives the remote from the server (ssh when it has a deploy key, https via the stored token otherwise), clones it under PROJECTS_ROOT, and keeps it fresh before every run."})]}),!a&&(0,n.jsxs)("label",{className:"hstack",style:{gap:8,cursor:"pointer",marginTop:14},children:[(0,n.jsx)("input",{type:"checkbox",checked:t.init_mise,onChange:e=>s({...t,init_mise:e.target.checked})}),(0,n.jsxs)("span",{style:{fontSize:"var(--text-sm)"},children:["Initialize mise — after the clone, run an agent that writes a"," ",(0,n.jsx)("span",{className:"mono",children:".mise.toml"})," with a"," ",(0,n.jsx)("span",{className:"mono",children:"[tasks.test]"})," task for this repo’s stack, then commits and pushes it. Needed for repos that don’t define one yet."]})]}),!a&&t.init_mise&&"manual"===t.mode&&!t.git_remote.trim()&&(0,n.jsxs)("p",{className:"faint",style:{fontSize:"var(--text-xs)",margin:"6px 0 0"},children:["A git remote is required to push the new ",(0,n.jsx)("span",{className:"mono",children:".mise.toml"})," — add one above, or mise won’t be initialized."]}),(0,n.jsxs)("div",{className:"hstack mt14",children:[(0,n.jsx)(y,{variant:"primary",disabled:e.cmd.busy||!p,onClick:u,children:a?"Save changes":"server"===t.mode?"Add & pull":"Register"}),a&&(0,n.jsx)(y,{variant:"ghost",onClick:d,children:"Cancel"})]})]}),0===e.projects.length&&(0,n.jsx)("div",{className:"empty",children:"No repositories registered."}),e.projects.map(t=>{var s,a;return(0,n.jsxs)(g,{children:[(0,n.jsxs)("div",{className:"card-head",children:[(0,n.jsx)("span",{className:"card-title",children:t.id}),(0,n.jsxs)(v,{tone:"info",pill:!0,children:[null!==(s=i.get(t.id))&&void 0!==s?s:0," ",(null!==(a=i.get(t.id))&&void 0!==a?a:0)===1?"agent":"agents"]})]}),(0,n.jsxs)("div",{className:"mono faint",style:{fontSize:"var(--text-xs)",marginTop:4},children:[t.root_dir,t.git_remote?" \xb7 ".concat(t.git_remote):""]}),(0,n.jsxs)("div",{className:"hstack",style:{marginTop:12,justifyContent:"space-between"},children:[(0,n.jsxs)("span",{className:"faint mono",style:{fontSize:"var(--text-xs)"},children:["cred ",t.credential_ref||"server default"," \xb7 added ",m(t.created_at)]}),(0,n.jsxs)("div",{className:"hstack",children:[t.git_remote&&(0,n.jsx)(y,{size:"sm",variant:"secondary",onClick:()=>e.syncProject(t.id),children:"Pull now"}),(0,n.jsx)(y,{size:"sm",variant:"secondary",onClick:()=>h(t),children:"Edit"}),(0,n.jsx)(y,{size:"sm",variant:"danger",onClick:()=>e.deleteProject(t.id),children:"Remove"})]})]})]},t.id)})]})]})}let M=[{value:"",label:"Role — none"},{value:"junior",label:"junior"},{value:"senior",label:"senior"},{value:"deploy",label:"deploy"}],L=[{value:"worktree",label:"git worktree on branch"},{value:"subdir",label:"subdir under root"}],D={name:"",role:"",placement:"worktree",worktree:"",subdir:"",task:""};function U(){let e=o(),[t,s]=(0,r.useState)(D),a=(0,r.useMemo)(()=>e.projects.map(e=>({value:e.id,label:e.id})),[e.projects]),l=(0,r.useMemo)(()=>e.agents.filter(t=>t.project_id===e.selectedProjectId),[e.agents,e.selectedProjectId]),i=async()=>{await e.spawnAgent(t)&&s(D)};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Agents"}),(0,n.jsx)("div",{className:"section-desc",children:"Spawn agents into a repository and manage running sessions."})]}),(0,n.jsx)("div",{className:"section-body",children:0===e.projects.length?(0,n.jsx)("div",{className:"empty",children:"Register a repository first."}):(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("div",{className:"row",children:(0,n.jsx)("div",{style:{width:260},children:(0,n.jsx)(w,{label:"Repository",value:e.selectedProjectId,onChange:e.selectProject,options:a})})}),(0,n.jsxs)(g,{children:[(0,n.jsx)("div",{className:"card-head",style:{marginBottom:14},children:(0,n.jsx)("span",{className:"card-title",style:{fontSize:"var(--text-md)",color:"var(--text-heading)"},children:"Spawn an agent"})}),(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(b,{label:"Name",value:t.name,onChange:e=>s({...t,name:e}),placeholder:"junior"}),(0,n.jsx)(w,{label:"Role",value:t.role,onChange:e=>s({...t,role:e}),options:M}),(0,n.jsx)(w,{label:"Placement",value:t.placement,onChange:e=>s({...t,placement:e}),options:L}),"worktree"===t.placement?(0,n.jsx)(b,{label:"Branch",value:t.worktree,onChange:e=>s({...t,worktree:e}),placeholder:"feat/auth"}):(0,n.jsx)(b,{label:"Subdir",value:t.subdir,onChange:e=>s({...t,subdir:e}),placeholder:"api"})]}),(0,n.jsx)("div",{className:"mt14",children:(0,n.jsx)(k,{label:"Initial task",value:t.task,onChange:e=>s({...t,task:e}),rows:2,placeholder:"initial task / prompt (optional)"})}),(0,n.jsx)("div",{className:"hstack mt14",children:(0,n.jsx)(y,{variant:"primary",disabled:e.cmd.busy||!t.name.trim(),onClick:i,children:"Spawn"})})]}),0===l.length?(0,n.jsx)("div",{className:"empty",children:"No agents in this repository."}):(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"Name"}),(0,n.jsx)("th",{children:"Role"}),(0,n.jsx)("th",{children:"Status"}),(0,n.jsx)("th",{children:"Working dir"}),(0,n.jsx)("th",{children:"Created"}),(0,n.jsx)("th",{})]})}),(0,n.jsx)("tbody",{children:l.map(t=>{var s;return(0,n.jsxs)(r.Fragment,{children:[(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{className:"mono",children:t.name}),(0,n.jsx)("td",{children:t.role?(0,n.jsx)(v,{tone:"info",children:t.role}):"—"}),(0,n.jsx)("td",{children:(0,n.jsx)(j,{status:t.status})}),(0,n.jsx)("td",{className:"mono faint",children:t.working_dir}),(0,n.jsx)("td",{className:"faint nowrap",children:m(t.created_at)}),(0,n.jsx)("td",{className:"nowrap",children:(0,n.jsxs)("div",{className:"hstack",children:[(0,n.jsx)(y,{size:"sm",variant:"ghost",onClick:()=>e.selectRun(t.project_id,t.name),children:"Open"}),(0,n.jsx)(y,{size:"sm",variant:"secondary",onClick:()=>e.killAgent(t.project_id,t.name),children:"Kill"}),(0,n.jsx)(y,{size:"sm",variant:"danger",onClick:()=>e.deleteAgent(t.project_id,t.name),children:"Delete"})]})})]}),("working"===t.status||"crashed"===t.status)&&(null===(s=t.last_output)||void 0===s?void 0:s.trim())&&(0,n.jsx)("tr",{children:(0,n.jsxs)("td",{colSpan:6,style:{paddingTop:0},children:[(0,n.jsxs)("div",{className:"faint",style:{fontSize:"var(--text-xs)",marginBottom:4},children:["crashed"===t.status?"last output before crash":"live output",t.output_at?" \xb7 ".concat(x(t.output_at)):""]}),(0,n.jsx)("pre",{className:"mono",style:{margin:0,padding:"8px 10px",background:"var(--surface-2, rgba(0,0,0,0.25))",borderRadius:6,fontSize:"var(--text-xs)",lineHeight:1.4,maxHeight:220,overflow:"auto",whiteSpace:"pre"},children:t.last_output})]})})]},t.id)})})]})})]})})]})}let F=[{value:"",label:"Role — none"},{value:"junior",label:"junior"},{value:"senior",label:"senior"},{value:"deploy",label:"deploy"}],B=[{value:"900",label:"every 15 minutes"},{value:"1800",label:"every 30 minutes"},{value:"3600",label:"every hour"},{value:"21600",label:"every 6 hours"},{value:"86400",label:"every day"},{value:"604800",label:"every week"}],H={name_prefix:"",task:"",interval:"3600",role:""};function W(){let e=o(),[t,s]=(0,r.useState)(H),a=(0,r.useMemo)(()=>e.projects.map(e=>({value:e.id,label:e.id})),[e.projects]),l=async()=>{await e.createSchedule(e.selectedProjectId,{name_prefix:t.name_prefix,task:t.task,interval_seconds:Number(t.interval),role:t.role})&&s(H)};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Schedules"}),(0,n.jsx)("div",{className:"section-desc",children:"Spawn a fresh agent on an interval. Each run is stateless — keep continuity in a file the prompt reads and overwrites."})]}),(0,n.jsx)("div",{className:"section-body",children:0===e.projects.length?(0,n.jsx)("div",{className:"empty",children:"Register a repository first."}):(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("div",{className:"row",children:(0,n.jsx)("div",{style:{width:260},children:(0,n.jsx)(w,{label:"Repository",value:e.selectedProjectId,onChange:e.selectProject,options:a})})}),(0,n.jsxs)(g,{children:[(0,n.jsx)("div",{className:"card-head",style:{marginBottom:14},children:(0,n.jsx)("span",{className:"card-title",style:{fontSize:"var(--text-md)",color:"var(--text-heading)"},children:"New schedule"})}),(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(b,{label:"Name prefix",value:t.name_prefix,onChange:e=>s({...t,name_prefix:e}),placeholder:"nightly"}),(0,n.jsx)(w,{label:"Interval",value:t.interval,onChange:e=>s({...t,interval:e}),options:B}),(0,n.jsx)(w,{label:"Role",value:t.role,onChange:e=>s({...t,role:e}),options:F})]}),(0,n.jsx)("div",{className:"mt14",children:(0,n.jsx)(k,{label:"Prompt (the task every run starts with)",value:t.task,onChange:e=>s({...t,task:e}),rows:3,placeholder:"Read @notes.md and continue from where it left off. Before finishing, overwrite @notes.md with the current state so the next run can pick up from there."})}),(0,n.jsxs)("p",{className:"faint",style:{fontSize:"var(--text-xs)",margin:"10px 0 0"},children:["Runs are named ",(0,n.jsxs)("span",{className:"mono",children:[t.name_prefix.trim()||"prefix","-YYYYMMDD-HHMMSS"]}),". The repo is pulled before every run; the first run fires on the worker's next pass."]}),(0,n.jsx)("div",{className:"hstack mt14",children:(0,n.jsx)(y,{variant:"primary",disabled:e.cmd.busy||!t.name_prefix.trim()||!t.task.trim(),onClick:l,children:"Create schedule"})})]}),0===e.schedules.length?(0,n.jsx)("div",{className:"empty",children:"No schedules yet."}):(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"On"}),(0,n.jsx)("th",{children:"Name"}),(0,n.jsx)("th",{children:"Repository"}),(0,n.jsx)("th",{children:"Interval"}),(0,n.jsx)("th",{children:"Prompt"}),(0,n.jsx)("th",{children:"Next run"}),(0,n.jsx)("th",{children:"Last run"}),(0,n.jsx)("th",{})]})}),(0,n.jsx)("tbody",{children:e.schedules.map(t=>(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{children:(0,n.jsx)(S,{on:t.enabled,onClick:()=>e.updateSchedule(t.id,{enabled:!t.enabled})})}),(0,n.jsxs)("td",{className:"mono",children:[t.name_prefix,t.role?(0,n.jsxs)(n.Fragment,{children:[" ",(0,n.jsx)(v,{tone:"info",children:t.role})]}):null]}),(0,n.jsx)("td",{className:"mono faint",children:t.project_id}),(0,n.jsx)("td",{className:"nowrap",children:function(e){let t=B.find(t=>Number(t.value)===e);return t?t.label:e%3600==0?"every ".concat(e/3600,"h"):e%60==0?"every ".concat(e/60,"m"):"every ".concat(e,"s")}(t.interval_seconds)}),(0,n.jsx)("td",{className:"faint",style:{maxWidth:340},children:(0,n.jsx)("span",{className:"truncate",style:{display:"block"},title:t.task,children:t.task})}),(0,n.jsx)("td",{className:"faint nowrap",children:t.enabled?m(t.next_run_at):"paused"}),(0,n.jsx)("td",{className:"faint nowrap",children:m(t.last_run_at)}),(0,n.jsx)("td",{className:"nowrap",children:(0,n.jsx)(y,{size:"sm",variant:"danger",onClick:()=>e.deleteSchedule(t.id),children:"Delete"})})]},t.id))})]})})]})})]})}let q=[{value:"approved",label:"approve"},{value:"rejected",label:"reject"}],G={branch:"",status:"approved",agent_name:"",sha:"",note:""};function J(){let e=o(),[t,s]=(0,r.useState)(G),a=(0,r.useMemo)(()=>e.projects.map(e=>({value:e.id,label:e.id})),[e.projects]),l=async()=>{await e.submitApproval(t),s(G)};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Approvals"}),(0,n.jsx)("div",{className:"section-desc",children:"A merge is denied unless a standing approval exists — made by a different agent, pinned to the reviewed commit."})]}),(0,n.jsx)("div",{className:"section-body",children:0===e.projects.length?(0,n.jsx)("div",{className:"empty",children:"Register a repository first."}):(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("div",{className:"row",children:(0,n.jsx)("div",{style:{width:260},children:(0,n.jsx)(w,{label:"Repository",value:e.selectedProjectId,onChange:e.selectProject,options:a})})}),(0,n.jsxs)(g,{children:[(0,n.jsx)("div",{className:"card-head",style:{marginBottom:14},children:(0,n.jsx)("span",{className:"card-title",style:{fontSize:"var(--text-md)",color:"var(--text-heading)"},children:"Record a verdict"})}),(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(b,{label:"Branch",value:t.branch,onChange:e=>s({...t,branch:e}),placeholder:"feat/auth"}),(0,n.jsx)(w,{label:"Verdict",value:t.status,onChange:e=>s({...t,status:e}),options:q}),(0,n.jsx)(b,{label:"Agent",value:t.agent_name,onChange:e=>s({...t,agent_name:e}),placeholder:"reads its HEAD (optional)"}),(0,n.jsx)(b,{label:"SHA",value:t.sha,onChange:e=>s({...t,sha:e}),placeholder:"pins the approval (optional)"})]}),(0,n.jsx)("div",{className:"mt14",children:(0,n.jsx)(b,{label:"Note",value:t.note,onChange:e=>s({...t,note:e}),placeholder:"optional"})}),(0,n.jsx)("div",{className:"hstack mt14",children:(0,n.jsx)(y,{variant:"primary",disabled:e.cmd.busy||!t.branch.trim(),onClick:l,children:"Enqueue verdict"})})]}),0===e.approvals.length?(0,n.jsx)("div",{className:"empty",children:"No approvals recorded."}):(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"When"}),(0,n.jsx)("th",{children:"Branch"}),(0,n.jsx)("th",{children:"Verdict"}),(0,n.jsx)("th",{children:"By"}),(0,n.jsx)("th",{children:"SHA"}),(0,n.jsx)("th",{children:"Note"})]})}),(0,n.jsx)("tbody",{children:e.approvals.map(e=>(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{className:"faint nowrap",children:m(e.created_at)}),(0,n.jsx)("td",{className:"mono",children:e.branch}),(0,n.jsx)("td",{children:(0,n.jsx)(j,{status:e.status})}),(0,n.jsx)("td",{children:e.approved_by_agent_id?"agent ".concat(e.approved_by_agent_id):e.actor||"—"}),(0,n.jsx)("td",{className:"mono",children:p(e.approved_sha)}),(0,n.jsx)("td",{children:e.note||"—"})]},e.id))})]})})]})})]})}let K=[{value:"github",label:"github"},{value:"gitlab",label:"gitlab"},{value:"gitea",label:"gitea"},{value:"forgejo",label:"forgejo"},{value:"bitbucket",label:"bitbucket"}],Y={hostname:"",forge_type:"github",token_env_var:"",base_url:"",token:"",generate_ssh_key:!0};function V(e){let{value:t}=e,[s,a]=(0,r.useState)(!1),l=async()=>{try{await navigator.clipboard.writeText(t),a(!0),setTimeout(()=>a(!1),1500)}catch(e){}};return(0,n.jsxs)("div",{style:{marginTop:10},children:[(0,n.jsxs)("div",{className:"hstack",style:{justifyContent:"space-between"},children:[(0,n.jsx)("span",{className:"eyebrow",children:"SSH public key — add it to the forge (deploy key)"}),(0,n.jsx)(y,{size:"sm",variant:"secondary",onClick:l,children:s?"Copied":"Copy"})]}),(0,n.jsx)("pre",{className:"mono",style:{fontSize:"var(--text-xs)",whiteSpace:"pre-wrap",wordBreak:"break-all",margin:"6px 0 0",padding:8,border:"1px solid var(--border-default)",borderRadius:6,userSelect:"all"},children:t})]})}function Q(){let e=o(),[t,s]=(0,r.useState)(Y),[a,l]=(0,r.useState)(!1),i=()=>{s(Y),l(!1)},c=async()=>{(a?await e.updateHost(t.hostname,t):await e.createHost(t))&&i()},d=e=>{var t,a;s({hostname:e.hostname,forge_type:e.forge_type,token_env_var:null!==(t=e.token_env_var)&&void 0!==t?t:"",base_url:null!==(a=e.base_url)&&void 0!==a?a:"",token:"",generate_ssh_key:!1}),l(!0)};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Git Servers"}),(0,n.jsxs)("div",{className:"section-desc",children:["Each server carries its own credentials: a forge token (encrypted at rest, used by agents' ",(0,n.jsx)("span",{className:"mono",children:"forge"})," + git) and an SSH deploy key — paste the public key into the forge. New repositories are added by picking a server and typing owner/name."]})]}),(0,n.jsxs)("div",{className:"section-body",children:[(0,n.jsxs)(g,{children:[(0,n.jsx)("div",{className:"card-head",style:{marginBottom:14},children:(0,n.jsx)("span",{className:"card-title",style:{fontSize:"var(--text-md)",color:"var(--text-heading)"},children:a?"Edit server \xb7 ".concat(t.hostname):"Add a git server"})}),(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(b,{label:"Hostname",value:t.hostname,onChange:e=>s({...t,hostname:e}),placeholder:"github.com",disabled:a}),(0,n.jsx)(w,{label:"Type",value:t.forge_type,onChange:e=>s({...t,forge_type:e}),options:K}),(0,n.jsx)(b,{label:a?"Forge token (blank = keep current)":"Forge token",type:"password",value:t.token,onChange:e=>s({...t,token:e}),placeholder:"stored encrypted; used by forge + git"}),(0,n.jsx)(b,{label:"Base URL (optional)",value:t.base_url,onChange:e=>s({...t,base_url:e}),placeholder:"https://git.corp.internal:8443"}),(0,n.jsx)(b,{label:"Token env var override (optional)",value:t.token_env_var,onChange:e=>s({...t,token_env_var:e}),placeholder:"GITEA_TOKEN"}),(0,n.jsxs)("label",{className:"field",children:[(0,n.jsx)("span",{className:"field-label",children:"SSH deploy key"}),(0,n.jsxs)("label",{className:"hstack",style:{gap:8,cursor:"pointer"},children:[(0,n.jsx)("input",{type:"checkbox",checked:t.generate_ssh_key,onChange:e=>s({...t,generate_ssh_key:e.target.checked})}),(0,n.jsx)("span",{style:{fontSize:"var(--text-sm)"},children:a?"Regenerate keypair (replaces the current key)":"Generate a keypair"})]})]})]}),(0,n.jsxs)("div",{className:"hstack mt14",children:[(0,n.jsx)(y,{variant:"primary",disabled:e.cmd.busy||!t.hostname.trim(),onClick:c,children:a?"Save changes":"Add server"}),a&&(0,n.jsx)(y,{variant:"ghost",onClick:i,children:"Cancel"})]})]}),0===e.hosts.length&&(0,n.jsx)("div",{className:"empty",children:"No git servers registered (built-in host map still applies)."}),e.hosts.map(t=>(0,n.jsxs)(g,{children:[(0,n.jsxs)("div",{className:"card-head",children:[(0,n.jsx)("span",{className:"mono",style:{fontWeight:"var(--fw-bold)",fontSize:"var(--text-lg)",color:"var(--text-heading)"},children:t.hostname}),(0,n.jsxs)("div",{className:"hstack",children:[(0,n.jsx)(v,{tone:"info",children:t.forge_type}),(0,n.jsx)(v,{tone:t.has_token?"success":"neutral",children:t.has_token?"token stored":"no token"}),(0,n.jsx)(v,{tone:t.ssh_public_key?"success":"neutral",children:t.ssh_public_key?"ssh key":"no ssh key"}),(0,n.jsx)(y,{size:"sm",variant:"secondary",onClick:()=>d(t),children:"Edit"}),(0,n.jsx)(y,{size:"sm",variant:"danger",onClick:()=>e.deleteHost(t.hostname),children:"Remove"})]})]}),(0,n.jsxs)("div",{className:"mono faint",style:{fontSize:"var(--text-xs)",marginTop:8},children:["token env ",t.token_env_var||"—",t.base_url?" \xb7 ".concat(t.base_url):""]}),t.ssh_public_key&&(0,n.jsx)(V,{value:t.ssh_public_key})]},t.hostname))]})]})}function X(){let e=o();return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("div",{className:"section-head",children:(0,n.jsxs)("div",{className:"hstack",style:{justifyContent:"space-between"},children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"section-title",children:"Activity"}),(0,n.jsx)("div",{className:"section-desc",children:"Control commands the worker drains from the queue."})]}),(0,n.jsx)(y,{variant:"secondary",disabled:e.cmd.busy,onClick:()=>e.pollCi(),children:"Sweep CI now"})]})}),(0,n.jsx)("div",{className:"section-body",children:0===e.commands.length?(0,n.jsx)("div",{className:"empty",children:"No commands yet."}):(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"When"}),(0,n.jsx)("th",{children:"Type"}),(0,n.jsx)("th",{children:"Repository"}),(0,n.jsx)("th",{children:"Agent"}),(0,n.jsx)("th",{children:"Status"}),(0,n.jsx)("th",{children:"Result / Error"})]})}),(0,n.jsx)("tbody",{children:e.commands.map(e=>(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{className:"faint nowrap",children:m(e.created_at)}),(0,n.jsx)("td",{className:"mono",children:e.type}),(0,n.jsx)("td",{className:"mono",children:e.project_id||"—"}),(0,n.jsx)("td",{className:"mono",children:e.agent_name||"—"}),(0,n.jsx)("td",{children:(0,n.jsx)(j,{status:e.status})}),(0,n.jsx)("td",{className:"mono faint",style:{fontSize:"var(--text-xs)"},children:e.error||(e.result?JSON.stringify(e.result):"—")})]},e.id))})]})})})]})}function $(){let e=o(),[t,s]=(0,r.useState)(""),[a,l]=(0,r.useState)(""),i=async()=>{t.trim()&&a.trim()&&await e.setSharedKey(t.trim(),a.trim())&&(s(""),l(""))};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Shared"}),(0,n.jsx)("div",{className:"section-desc",children:"The cross-project global feed and shared facts."})]}),(0,n.jsxs)("div",{className:"section-body",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"eyebrow",style:{marginBottom:10},children:"Global feed"}),0===e.shared.log.length?(0,n.jsx)("div",{className:"empty",children:"No global log entries."}):(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"When"}),(0,n.jsx)("th",{children:"Agent"}),(0,n.jsx)("th",{children:"Status"}),(0,n.jsx)("th",{children:"Summary"}),(0,n.jsx)("th",{children:"CI"})]})}),(0,n.jsx)("tbody",{children:e.shared.log.map(e=>(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{className:"faint nowrap",children:m(e.created_at)}),(0,n.jsx)("td",{className:"mono",children:e.agent_id}),(0,n.jsx)("td",{children:(0,n.jsx)(j,{status:e.status})}),(0,n.jsx)("td",{children:e.summary||"—"}),(0,n.jsx)("td",{children:(0,n.jsx)(j,{status:e.ci_status})})]},e.id))})]})})]}),(0,n.jsxs)(g,{children:[(0,n.jsx)("div",{className:"card-head",style:{marginBottom:14},children:(0,n.jsx)("span",{className:"card-title",style:{fontSize:"var(--text-md)",color:"var(--text-heading)"},children:"Set a shared key"})}),(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(b,{label:"Key",value:t,onChange:s,placeholder:"key"}),(0,n.jsx)(b,{label:"Value",value:a,onChange:l,placeholder:"value"})]}),(0,n.jsx)("p",{className:"faint",style:{fontSize:"var(--text-xs)",margin:"10px 0 0"},children:"Requires the shared-context write token (or admin/global if unset)."}),(0,n.jsx)("div",{className:"hstack mt14",children:(0,n.jsx)(y,{variant:"primary",disabled:!t.trim()||!a.trim(),onClick:i,children:"Set"})})]}),e.shared.context.length>0&&(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"Key"}),(0,n.jsx)("th",{children:"Value"}),(0,n.jsx)("th",{children:"Updated"})]})}),(0,n.jsx)("tbody",{children:e.shared.context.map(e=>(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{className:"mono",children:e.key}),(0,n.jsx)("td",{children:e.value}),(0,n.jsx)("td",{className:"faint nowrap",children:m(e.updated_at)})]},e.key))})]})})]})]})}function Z(e){let t=window.screenX+Math.max(0,(window.outerWidth-520)/2),s=window.screenY+Math.max(0,(window.outerHeight-760)/2);return window.open(e,"claude-login","popup=yes,width=".concat(520,",height=").concat(760,",left=").concat(Math.round(t),",top=").concat(Math.round(s)))}function ee(){let e=o(),{status:t,url:s,message:a}=e.claudeLogin,[l,i]=(0,r.useState)(""),c=(0,r.useRef)(null),d="starting"===t||"submitting"===t,u="awaiting"===t||"submitting"===t;(0,r.useEffect)(()=>{if("awaiting"===t&&s&&c.current&&!c.current.closed)try{c.current.location.href=s}catch(e){}if("done"===t||"error"===t){var e;null===(e=c.current)||void 0===e||e.close(),c.current=null}},[t,s]);let h=()=>{c.current=Z("about:blank"),e.startClaudeLogin()},m=async()=>{await e.submitClaudeCode(l)&&i("")};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Claude Login"}),(0,n.jsxs)("div",{className:"section-desc",children:["Log Claude Code in on the host so agents can run. This drives"," ",(0,n.jsx)("span",{className:"mono",children:"claude /login"})," in the control container and picks the Claude account with a subscription."]})]}),(0,n.jsxs)("div",{className:"section-body",style:{display:"flex",flexDirection:"column",gap:16},children:[a&&(0,n.jsx)(C,{tone:"error"===t?"danger":"done"===t?"success":"info",children:a}),"done"===t?(0,n.jsx)("div",{children:(0,n.jsx)(y,{variant:"secondary",onClick:e.resetClaudeLogin,children:"Log in again"})}):u?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(C,{tone:"info",children:"A Claude sign-in window should have opened. Authorize there, copy the code Claude shows you, and paste it below. If the window didn't open (popups blocked), use the button."}),(0,n.jsxs)("div",{className:"hstack",style:{gap:10,flexWrap:"wrap"},children:[(0,n.jsx)(y,{variant:"secondary",disabled:!s,onClick:()=>{s&&(c.current=Z(s))},children:"Open Claude sign-in window ↗"}),s&&(0,n.jsx)("a",{className:"btn btn-ghost",href:s,target:"_blank",rel:"noopener noreferrer",children:"Open in a new tab"}),(0,n.jsx)(y,{variant:"ghost",disabled:d,onClick:h,children:"Restart"})]}),(0,n.jsxs)("div",{className:"hstack",style:{gap:10,alignItems:"flex-end",flexWrap:"wrap"},children:[(0,n.jsx)("div",{style:{flex:"1 1 320px"},children:(0,n.jsx)(b,{label:"Authorization code",value:l,onChange:i,placeholder:"Paste the code from claude.com",disabled:"submitting"===t})}),(0,n.jsx)(y,{variant:"primary",disabled:"submitting"===t||!l.trim(),onClick:m,children:"submitting"===t?"Submitting…":"Finish login"})]})]}):(0,n.jsxs)("div",{className:"hstack",style:{gap:10},children:[(0,n.jsx)(y,{variant:"primary",disabled:d,onClick:h,children:"starting"===t?"Starting…":"Log in to Claude"}),"error"===t&&(0,n.jsx)(y,{variant:"ghost",disabled:d,onClick:h,children:"Retry"})]})]})]})}let et=[{key:"runs",label:"Runs",count:e=>e.agents.length,accent:e=>e.agents.some(e=>"paused_for_input"===e.status)},{key:"repositories",label:"Repositories",count:e=>e.projects.length},{key:"agents",label:"Agents",count:e=>e.agents.length},{key:"schedules",label:"Schedules",count:e=>e.schedules.length},{key:"approvals",label:"Approvals",count:e=>e.approvals.length},{key:"servers",label:"Git Servers",count:e=>e.hosts.length},{key:"activity",label:"Activity",count:e=>e.commands.length},{key:"shared",label:"Shared",count:e=>e.shared.context.length},{key:"login",label:"Claude Login",count:()=>0,accent:e=>"done"!==e.claudeLogin.status}];function es(e){let{onSignOut:t}=e,s=o();return(0,n.jsxs)("div",{className:"app",children:[(0,n.jsxs)("aside",{className:"sidebar",children:[(0,n.jsxs)("div",{className:"brand",children:[(0,n.jsx)("span",{className:"logo"}),"Claude Monitor"]}),et.map(e=>{var t,a;let r=e.count(s),l=null!==(a=null===(t=e.accent)||void 0===t?void 0:t.call(e,s))&&void 0!==a&&a;return(0,n.jsxs)("button",{className:"nav-item".concat(s.section===e.key?" active":""),onClick:()=>s.setSection(e.key),children:[(0,n.jsx)("span",{children:e.label}),(0,n.jsx)("span",{className:"count",style:l?{color:"var(--lw-warning-fg)"}:void 0,children:r||""})]},e.key)}),(0,n.jsx)("div",{className:"sidebar-spacer"}),(0,n.jsxs)("div",{className:"sidebar-foot",children:[(0,n.jsxs)("button",{className:"nav-item",onClick:s.refresh,title:"Refresh now",children:[(0,n.jsx)("span",{children:"Refresh"}),(0,n.jsx)("span",{className:"count",children:"↻"})]}),(0,n.jsx)("button",{className:"nav-item",onClick:t,title:"Sign out / change token",children:(0,n.jsx)("span",{children:"Sign out"})})]})]}),(0,n.jsxs)("main",{className:"main",children:[s.cmd.text&&(0,n.jsx)("p",{className:"banner ".concat(s.cmd.error?"err":"ok"),style:{marginTop:16},children:s.cmd.text}),s.lastError&&(0,n.jsx)("p",{className:"banner err",style:{marginTop:12},children:s.lastError}),"runs"===s.section?(0,n.jsx)(I,{}):(0,n.jsxs)("div",{className:"main-scroll",children:["repositories"===s.section&&(0,n.jsx)(O,{}),"agents"===s.section&&(0,n.jsx)(U,{}),"schedules"===s.section&&(0,n.jsx)(W,{}),"approvals"===s.section&&(0,n.jsx)(J,{}),"servers"===s.section&&(0,n.jsx)(Q,{}),"activity"===s.section&&(0,n.jsx)(X,{}),"shared"===s.section&&(0,n.jsx)($,{}),"login"===s.section&&(0,n.jsx)(ee,{})]})]})]})}function ea(e){let{error:t,onSubmit:s}=e,[a,l]=(0,r.useState)("");return(0,n.jsx)("div",{className:"gate",children:(0,n.jsxs)("form",{className:"gate-card",onSubmit:e=>{e.preventDefault();let t=a.trim();t&&s(t)},children:[(0,n.jsxs)("div",{className:"gate-brand",children:[(0,n.jsx)("span",{className:"logo",style:{width:26,height:26,borderRadius:7}}),"Claude Monitor"]}),(0,n.jsx)("p",{className:"muted",style:{fontSize:"var(--text-sm)",margin:0},children:"Paste your API token to continue. Management actions require the admin token; read-only views work with the plain auth token."}),(0,n.jsx)("input",{className:"input",type:"password",autoComplete:"current-password",placeholder:"API token",value:a,onChange:e=>l(e.target.value),autoFocus:!0}),t&&(0,n.jsx)("p",{className:"callout callout-danger",style:{margin:0},children:t}),(0,n.jsx)("button",{className:"btn btn-primary",type:"submit",children:"Continue"})]})})}let en="handler_token";function er(){let[e,t]=(0,r.useState)(null),[s,a]=(0,r.useState)("");(0,r.useEffect)(()=>{let e=window.localStorage.getItem(en);e&&t(e)},[]);let l=(0,r.useCallback)(e=>{window.localStorage.setItem(en,e),a(""),t(e)},[]),i=(0,r.useCallback)(()=>{window.localStorage.removeItem(en),t(null)},[]),c=(0,r.useCallback)(()=>{window.localStorage.removeItem(en),t(null),a("Invalid token — please try again.")},[]);return e?(0,n.jsx)(d,{token:e,onUnauthorized:c,children:(0,n.jsx)(es,{onSignOut:i})}):(0,n.jsx)(ea,{error:s,onSubmit:l})}},257:function(e,t,s){"use strict";var a,n;e.exports=(null==(a=s.g.process)?void 0:a.env)&&"object"==typeof(null==(n=s.g.process)?void 0:n.env)?s.g.process:s(4227)},4227:function(e){!function(){var t={229:function(e){var t,s,a,n=e.exports={};function r(){throw Error("setTimeout has not been defined")}function l(){throw Error("clearTimeout has not been defined")}function i(e){if(t===setTimeout)return setTimeout(e,0);if((t===r||!t)&&setTimeout)return t=setTimeout,setTimeout(e,0);try{return t(e,0)}catch(s){try{return t.call(null,e,0)}catch(s){return t.call(this,e,0)}}}!function(){try{t="function"==typeof setTimeout?setTimeout:r}catch(e){t=r}try{s="function"==typeof clearTimeout?clearTimeout:l}catch(e){s=l}}();var c=[],o=!1,d=-1;function u(){o&&a&&(o=!1,a.length?c=a.concat(c):d=-1,c.length&&h())}function h(){if(!o){var e=i(u);o=!0;for(var t=c.length;t;){for(a=c,c=[];++d1)for(var s=1;s(function(e,t){async function s(s,a){var n;let r=(null==a?void 0:a.body)!==void 0&&(null==a?void 0:a.body)!==null,c=await fetch(l+s,{method:null!==(n=null==a?void 0:a.method)&&void 0!==n?n:r?"POST":"GET",headers:{Authorization:"Bearer ".concat(e),...r?{"Content-Type":"application/json"}:{}},body:r?JSON.stringify(a.body):void 0});if(401===c.status)throw t(),new i;if(!c.ok){let e=c.statusText;try{let t=await c.json();t&&void 0!==t.detail&&(e="string"==typeof t.detail?t.detail:JSON.stringify(t.detail))}catch(e){}let t=Error(e);throw t.status=c.status,t}if(204===c.status)return;let o=await c.text();return o?JSON.parse(o):void 0}async function a(e,t){var a,n;let r=null!==(a=null==t?void 0:t.attempts)&&void 0!==a?a:60,l=null!==(n=null==t?void 0:t.intervalMs)&&void 0!==n?n:500;for(let t=0;tsetTimeout(e,l))}return null}return{api:s,trackCommand:a}})(t,s),[t,s]),d=(0,r.useRef)(o);d.current=o;let[u,h]=(0,r.useState)("runs"),[m,x]=(0,r.useState)([]),[p,j]=(0,r.useState)([]),[v,g]=(0,r.useState)(""),[y,b]=(0,r.useState)(null),[f,k]=(0,r.useState)(null),[w,N]=(0,r.useState)(!1),[_,C]=(0,r.useState)([]),[S,R]=(0,r.useState)(0),[I,T]=(0,r.useState)([]),[A,z]=(0,r.useState)([]),[E,P]=(0,r.useState)([]),[M,O]=(0,r.useState)([]),[L,U]=(0,r.useState)({log:[],context:[]}),[D,F]=(0,r.useState)({text:"",error:!1,busy:!1}),[B,H]=(0,r.useState)(""),[q,W]=(0,r.useState)(!0),[G,J]=(0,r.useState)({status:"idle",url:"",message:""}),K=(0,r.useRef)(u);K.current=u;let Y=(0,r.useRef)(v);Y.current=v;let V=(0,r.useRef)(y);V.current=y;let Q=(0,r.useRef)(S);Q.current=S;let X=e=>{e instanceof i||H(e.message)},$=(0,r.useCallback)(async()=>{try{let e=await d.current.api("/projects");x(e),H(""),g(t=>{var s,a;return t||(null!==(a=null===(s=e[0])||void 0===s?void 0:s.id)&&void 0!==a?a:"")})}catch(e){X(e)}},[]),Z=(0,r.useCallback)(async e=>{try{let t=await Promise.all(e.map(e=>d.current.api("/projects/".concat(encodeURIComponent(e.id),"/agents")).catch(()=>[])));j(t.flat())}catch(e){X(e)}},[]),ee=(0,r.useCallback)(async(e,t)=>{let s="/projects/".concat(encodeURIComponent(e),"/agents/").concat(encodeURIComponent(t));try{let e=await d.current.api("".concat(s,"/checkmark"));k(e),N(!1)}catch(e){if(e instanceof i)return;404===e.status?(k(null),N(!0)):X(e)}try{let e=await d.current.api("".concat(s,"/log?limit=").concat(100,"&offset=").concat(Q.current));C(e)}catch(e){X(e)}},[]),et=(0,r.useCallback)(async e=>{if(!e){T([]);return}try{T(await d.current.api("/projects/".concat(encodeURIComponent(e),"/approvals")))}catch(e){X(e)}},[]),es=(0,r.useCallback)(async()=>{try{z(await d.current.api("/hosts"))}catch(e){X(e)}},[]),ea=(0,r.useCallback)(async()=>{try{P(await d.current.api("/commands?limit=50"))}catch(e){X(e)}},[]),en=(0,r.useCallback)(async()=>{try{O(await d.current.api("/schedules"))}catch(e){X(e)}},[]),er=(0,r.useCallback)(async()=>{try{let[e,t]=await Promise.all([d.current.api("/shared/log"),d.current.api("/shared/context")]);U({log:e,context:t})}catch(e){X(e)}},[]),el=(0,r.useCallback)(async()=>{let e=await d.current.api("/projects").catch(e=>(X(e),null));e&&(x(e),g(t=>{var s,a;return t||(null!==(a=null===(s=e[0])||void 0===s?void 0:s.id)&&void 0!==a?a:"")}),await Z(e));let t=K.current,s=V.current;s&&await ee(s.projectId,s.name),"approvals"===t&&await et(Y.current),"servers"===t&&await es(),"activity"===t&&await ea(),"schedules"===t&&await en(),"shared"===t&&await er()},[Z,ee,et,es,ea,en,er]);(0,r.useEffect)(()=>{let e=!0;(async()=>{W(!0),await el(),e&&W(!1)})();let t=setInterval(()=>{document.hidden||el()},5e3);return()=>{e=!1,clearInterval(t)}},[el]);let ei=(0,r.useCallback)(e=>{h(e),F({text:"",error:!1,busy:!1}),"approvals"===e&&et(Y.current),"servers"===e&&es(),"activity"===e&&ea(),"schedules"===e&&en(),"shared"===e&&er()},[et,es,ea,en,er]),ec=(0,r.useCallback)(e=>{g(e),"approvals"===K.current&&et(e)},[et]),eo=(0,r.useCallback)((e,t)=>{b({projectId:e,name:t}),R(0),Q.current=0,k(null),N(!1),C([]),ee(e,t)},[ee]),ed=(0,r.useCallback)(e=>{let t=Math.max(0,S+100*e);if(t===S)return;R(t),Q.current=t;let s=V.current;s&&ee(s.projectId,s.name)},[S,ee]),eu=(0,r.useCallback)(()=>{el()},[el]),eh=(0,r.useCallback)(async(e,t,s)=>{F({text:"".concat(s,": queued…"),error:!1,busy:!0});try{let a=await d.current.api(e,{method:"POST",body:t}),n=await d.current.trackCommand(a.id);if(!n)return F({text:"".concat(s,": still running (see Activity). Is the worker up?"),error:!1,busy:!1}),null;let r="done"===n.status,l=n.error||(n.result?JSON.stringify(n.result):"");return F({text:"".concat(s," ").concat(r?"done":"failed").concat(l?" — "+l:""),error:!r,busy:!1}),n}catch(e){if(e instanceof i)return null;return F({text:"".concat(s," failed: ").concat(e.message),error:!0,busy:!1}),null}},[]),em=(0,r.useCallback)(async e=>{let t={name:e.name.trim(),role:e.role||null,task:e.task.trim()||null};"worktree"===e.placement&&e.worktree.trim()&&(t.worktree=e.worktree.trim()),"subdir"===e.placement&&e.subdir.trim()&&(t.subdir=e.subdir.trim());let s=encodeURIComponent(Y.current),a=await eh("/projects/".concat(s,"/agents/spawn"),t,"spawn ".concat(t.name));return await Z(m),(null==a?void 0:a.status)==="done"},[eh,Z,m]),ex=(0,r.useCallback)(async(e,t)=>{let s=encodeURIComponent(e);await eh("/projects/".concat(s,"/agents/").concat(encodeURIComponent(t),"/kill"),void 0,"kill ".concat(t)),await Z(m)},[eh,Z,m]),ep=(0,r.useCallback)(async(e,t)=>{let s=encodeURIComponent(e);try{var a;await d.current.api("/projects/".concat(s,"/agents/").concat(encodeURIComponent(t)),{method:"DELETE"}),F({text:"agent '".concat(t,"' row deleted"),error:!1,busy:!1}),(null===(a=V.current)||void 0===a?void 0:a.name)===t&&b(null),await Z(m)}catch(e){if(e instanceof i)return;F({text:e.message,error:!0,busy:!1})}},[Z,m]),ej=(0,r.useCallback)(async(e,t)=>{let s=V.current;if(!s)return!1;let a="/projects/".concat(encodeURIComponent(s.projectId),"/agents/").concat(encodeURIComponent(s.name));try{return await d.current.api("".concat(a,"/answer"),{method:"POST",body:{answer:e}}),t?await eh("".concat(a,"/resume"),{answer:e},"resume"):F({text:"Answer saved (agent still paused).",error:!1,busy:!1}),await Z(m),await ee(s.projectId,s.name),!0}catch(e){if(e instanceof i)return!1;return F({text:e.message,error:!0,busy:!1}),!1}},[eh,Z,ee,m]),ev=(0,r.useCallback)(async e=>{try{var t,s;let a="server"===e.mode?{git_server:e.git_server,repo:e.repo.trim(),id:e.id.trim()||null,credential_ref:e.credential_ref.trim()||null,init_mise:e.init_mise}:{id:e.id.trim(),root_dir:e.root_dir.trim(),git_remote:e.git_remote.trim()||null,credential_ref:e.credential_ref.trim()||null,init_mise:e.init_mise},n=await d.current.api("/projects",{method:"POST",body:a});if(await $(),null!=n.sync_command_id){F({text:"repository '".concat(n.id,"': cloning…"),error:!1,busy:!0});let e=await d.current.trackCommand(n.sync_command_id);e?"done"===e.status?F({text:"repository '".concat(n.id,"' registered and cloned"),error:!1,busy:!1}):F({text:"repository '".concat(n.id,"' registered but the clone failed — ").concat(null!==(t=e.error)&&void 0!==t?t:""),error:!0,busy:!1}):F({text:"repository '".concat(n.id,"' registered; clone still running (see Activity). Is the worker up?"),error:!1,busy:!1})}else F({text:"repository '".concat(n.id,"' registered"),error:!1,busy:!1});if(null!=n.mise_init_command_id){F({text:"repository '".concat(n.id,"': launching a mise-init agent to create .mise.toml…"),error:!1,busy:!0});let e=await d.current.trackCommand(n.mise_init_command_id);e?"done"===e.status?F({text:"repository '".concat(n.id,"' registered; a mise-init agent is now writing, committing, and pushing .mise.toml (watch it in Runs)."),error:!1,busy:!1}):F({text:"repository '".concat(n.id,"' registered but the mise-init agent failed to launch — ").concat(null!==(s=e.error)&&void 0!==s?s:""),error:!0,busy:!1}):F({text:"repository '".concat(n.id,"' registered; mise-init still starting (see Activity). Is the worker up?"),error:!1,busy:!1})}return!0}catch(e){if(e instanceof i)return!1;return F({text:e.message,error:!0,busy:!1}),!1}},[$]),eg=(0,r.useCallback)(async e=>{await eh("/projects/".concat(encodeURIComponent(e),"/sync"),void 0,"pull ".concat(e))},[eh]),ey=(0,r.useCallback)(async(e,t)=>{try{return await d.current.api("/projects/".concat(encodeURIComponent(e)),{method:"PATCH",body:{root_dir:t.root_dir.trim(),git_remote:t.git_remote.trim()||null,credential_ref:t.credential_ref.trim()||null}}),F({text:"repository '".concat(e,"' updated"),error:!1,busy:!1}),await $(),!0}catch(e){if(e instanceof i)return!1;return F({text:e.message,error:!0,busy:!1}),!1}},[$]),eb=(0,r.useCallback)(async e=>{try{await d.current.api("/projects/".concat(encodeURIComponent(e)),{method:"DELETE"}),F({text:"repository '".concat(e,"' removed"),error:!1,busy:!1}),g(t=>t===e?"":t),await $()}catch(e){if(e instanceof i)return;F({text:e.message,error:!0,busy:!1})}},[$]),ef=(0,r.useCallback)(async e=>{let t=encodeURIComponent(Y.current);await eh("/projects/".concat(t,"/approvals"),{branch:e.branch.trim(),status:e.status,agent_name:e.agent_name.trim()||null,sha:e.sha.trim()||null,note:e.note.trim()||null},"".concat(e.status," ").concat(e.branch)),await et(Y.current)},[eh,et]),ek=(0,r.useCallback)(async e=>{try{return await d.current.api("/hosts",{method:"POST",body:{hostname:e.hostname.trim(),forge_type:e.forge_type,token_env_var:e.token_env_var.trim()||null,base_url:e.base_url.trim()||null,token:e.token.trim()||null,generate_ssh_key:e.generate_ssh_key}}),F({text:"git server '".concat(e.hostname,"' added"),error:!1,busy:!1}),await es(),!0}catch(e){if(e instanceof i)return!1;return F({text:e.message,error:!0,busy:!1}),!1}},[es]),ew=(0,r.useCallback)(async(e,t)=>{try{let s={forge_type:t.forge_type,token_env_var:t.token_env_var.trim()||null,base_url:t.base_url.trim()||null};return t.token.trim()&&(s.token=t.token.trim()),t.generate_ssh_key&&(s.regenerate_ssh_key=!0),await d.current.api("/hosts/".concat(encodeURIComponent(e)),{method:"PATCH",body:s}),F({text:"git server '".concat(e,"' updated"),error:!1,busy:!1}),await es(),!0}catch(e){if(e instanceof i)return!1;return F({text:e.message,error:!0,busy:!1}),!1}},[es]),eN=(0,r.useCallback)(async(e,t)=>{try{return await d.current.api("/projects/".concat(encodeURIComponent(e),"/schedules"),{method:"POST",body:{name_prefix:t.name_prefix.trim(),task:t.task.trim(),interval_seconds:t.interval_seconds,role:t.role||null}}),F({text:"schedule '".concat(t.name_prefix,"' created — first run on the worker's next pass"),error:!1,busy:!1}),await en(),!0}catch(e){if(e instanceof i)return!1;return F({text:e.message,error:!0,busy:!1}),!1}},[en]),e_=(0,r.useCallback)(async(e,t)=>{try{return await d.current.api("/schedules/".concat(e),{method:"PATCH",body:t}),await en(),!0}catch(e){if(e instanceof i)return!1;return F({text:e.message,error:!0,busy:!1}),!1}},[en]),eC=(0,r.useCallback)(async e=>{try{await d.current.api("/schedules/".concat(e),{method:"DELETE"}),F({text:"schedule ".concat(e," removed"),error:!1,busy:!1}),await en()}catch(e){if(e instanceof i)return;F({text:e.message,error:!0,busy:!1})}},[en]),eS=(0,r.useCallback)(async e=>{try{await d.current.api("/hosts/".concat(encodeURIComponent(e)),{method:"DELETE"}),F({text:"git server '".concat(e,"' removed"),error:!1,busy:!1}),await es()}catch(e){if(e instanceof i)return;F({text:e.message,error:!0,busy:!1})}},[es]),eR=(0,r.useCallback)(async()=>{await eh("/poll-ci",void 0,"poll-ci (all projects)"),await ea()},[eh,ea]),eI=(0,r.useCallback)(async()=>{J({status:"starting",url:"",message:"Opening `claude /login` in the control container and selecting the subscription account…"});try{let e=await d.current.api("/login/start",{method:"POST"}),t=await d.current.trackCommand(e.id,{attempts:180});if(!t){J({status:"error",url:"",message:"Still starting (see Activity). Is the control worker running?"});return}if("done"!==t.status){J({status:"error",url:"",message:t.error||"Failed to start login."});return}let s=t.result&&"string"==typeof t.result.url?t.result.url:"";if(!s){J({status:"error",url:"",message:"No login URL was returned by claude."});return}J({status:"awaiting",url:s,message:"Authorize in the window below (or open it in a new tab), then paste the code claude gives you."})}catch(e){if(e instanceof i)return;J({status:"error",url:"",message:e.message})}},[]),eT=(0,r.useCallback)(async e=>{let t=e.trim();if(!t)return!1;J(e=>({...e,status:"submitting",message:"Submitting the authorization code…"}));try{let e=await d.current.api("/login/submit",{method:"POST",body:{code:t}}),s=await d.current.trackCommand(e.id,{attempts:60});if(!s)return J(e=>({...e,status:"awaiting",message:"Submit still running (see Activity). Is the control worker running?"})),!1;if("done"===s.status)return J({status:"done",url:"",message:"Claude Code is now logged in on the host — new agents will use this account."}),!0;return J(e=>({...e,status:"awaiting",message:s.error||"Login was not confirmed. Re-check the code, or restart the flow."})),!1}catch(e){if(e instanceof i)return!1;return J(t=>({...t,status:"awaiting",message:e.message})),!1}},[]),eA=(0,r.useCallback)(()=>{J({status:"idle",url:"",message:""})},[]),ez=(0,r.useCallback)(async(e,t)=>{try{return await d.current.api("/shared/context/".concat(encodeURIComponent(e)),{method:"PUT",body:{value:t}}),F({text:"shared context '".concat(e,"' set"),error:!1,busy:!1}),await er(),!0}catch(e){if(e instanceof i)return!1;return F({text:e.message,error:!0,busy:!1}),!1}},[er]);return(0,n.jsx)(c.Provider,{value:{section:u,setSection:ei,projects:m,agents:p,selectedProjectId:v,selectProject:ec,selectedRun:y,selectRun:eo,checkmark:f,checkmarkMissing:w,log:_,logOffset:S,pageLog:ed,approvals:I,hosts:A,commands:E,schedules:M,shared:L,cmd:D,lastError:B,loading:q,refresh:eu,spawnAgent:em,killAgent:ex,deleteAgent:ep,submitAnswer:ej,createProject:ev,updateProject:ey,deleteProject:eb,syncProject:eg,submitApproval:ef,createHost:ek,updateHost:ew,deleteHost:eS,createSchedule:eN,updateSchedule:e_,deleteSchedule:eC,pollCi:eR,setSharedKey:ez,claudeLogin:G,startClaudeLogin:eI,submitClaudeCode:eT,resetClaudeLogin:eA},children:a})}function u(e){switch((null!=e?e:"").toLowerCase()){case"pass":case"done":case"completed":case"approved":case"success":return"success";case"fail":case"failed":case"blocked":case"rejected":case"error":return"danger";case"pending":case"queued":case"running":case"working":case"paused_for_input":return"warning";case"not_applicable":case"unknown":case"":return"neutral";default:return"info"}}let h={paused_for_input:"Needs input",not_applicable:"N/A"};function m(e){if(!e)return"—";let t=new Date(e);return Number.isNaN(t.getTime())?String(e):t.toLocaleString(void 0,{year:"numeric",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})}function x(e){return e?e.slice(0,7):"—"}function p(e){if(!e)return"—";let t=new Date(e).getTime();if(Number.isNaN(t))return"—";let s=Math.max(0,Math.floor((Date.now()-t)/1e3));if(s<60)return"".concat(s,"s");let a=Math.floor(s/60);if(a<60)return"".concat(a,"m");let n=Math.floor(a/60);if(n<24)return"".concat(n,"h");let r=Math.floor(n/24);if(r<30)return"".concat(r,"d");let l=Math.floor(r/30);return l<12?"".concat(l,"mo"):"".concat(Math.floor(l/12),"y")}function j(e){let{tone:t="neutral",pill:s=!1,dot:a=!1,children:r}=e;return(0,n.jsxs)("span",{className:"badge badge-".concat(t).concat(s?" pill":""),children:[a&&(0,n.jsx)("span",{className:"dot"}),r]})}function v(e){let{status:t}=e;return(0,n.jsx)(j,{tone:u(t),children:function(e){let t=(null!=e?e:"").trim();if(!t)return"—";let s=t.toLowerCase();return h[s]?h[s]:s.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase())}(t)})}function g(e){let{children:t,interactive:s=!1,onClick:a,className:r=""}=e;return(0,n.jsx)("div",{className:"card".concat(s?" interactive":""," ").concat(r).trim(),onClick:a,role:s?"button":void 0,tabIndex:s?0:void 0,children:t})}function y(e){let{variant:t="secondary",size:s="md",onClick:a,disabled:r,type:l="button",children:i}=e;return(0,n.jsx)("button",{type:l,className:"btn btn-".concat(t).concat("sm"===s?" btn-sm":""),onClick:a,disabled:r,children:i})}function b(e){let{label:t,children:s}=e;return(0,n.jsxs)("label",{className:"field",children:[t&&(0,n.jsx)("span",{className:"field-label",children:t}),s]})}function f(e){let{label:t,value:s,onChange:a,placeholder:r,type:l="text",disabled:i}=e;return(0,n.jsx)(b,{label:t,children:(0,n.jsx)("input",{className:"input",type:l,value:s,placeholder:r,disabled:i,onChange:e=>a(e.target.value)})})}function k(e){let{label:t,value:s,onChange:a,placeholder:r,rows:l=3}=e;return(0,n.jsx)(b,{label:t,children:(0,n.jsx)("textarea",{className:"textarea",value:s,rows:l,placeholder:r,onChange:e=>a(e.target.value)})})}function w(e){let{label:t,value:s,onChange:a,options:r}=e;return(0,n.jsx)(b,{label:t,children:(0,n.jsx)("select",{className:"select",value:s,onChange:e=>a(e.target.value),children:r.map(e=>(0,n.jsx)("option",{value:e.value,children:e.label},e.value))})})}function N(e){let{tabs:t,value:s,onChange:a}=e;return(0,n.jsx)("div",{className:"tabs",role:"tablist",children:t.map(e=>(0,n.jsx)("button",{role:"tab","aria-selected":s===e.value,className:"tab".concat(s===e.value?" active":""),onClick:()=>a(e.value),children:e.label},e.value))})}function _(e){let{value:t,label:s,sub:a,accent:r=!1}=e;return(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"stat-value".concat(r?" accent":""),children:t}),(0,n.jsx)("div",{className:"stat-label",children:s}),a&&(0,n.jsx)("div",{className:"stat-sub",children:a})]})}function C(e){let{tone:t="info",children:s}=e;return(0,n.jsx)("div",{className:"callout callout-".concat(t),children:s})}function S(e){let{on:t,onClick:s}=e;return(0,n.jsx)("button",{type:"button",className:"toggle".concat(t?" on":""),"aria-pressed":t,onClick:s,children:(0,n.jsx)("span",{className:"knob"})})}let R=[{value:"all",label:"All"},{value:"needs",label:"Needs Input"},{value:"working",label:"Working"},{value:"done",label:"Done"}];function I(){let e=o(),[t,s]=(0,r.useState)("all"),a=(0,r.useMemo)(()=>[...e.agents.filter(e=>{var s;return s=e.status,"all"===t||("needs"===t?"paused_for_input"===s:"working"===t?"working"===s||"running"===s:"done"!==t||"done"===s||"completed"===s)})].sort((e,t)=>e.created_at"paused_for_input"===e.status).length,c=e.agents.filter(e=>"working"===e.status||"running"===e.status).length;return(0,n.jsxs)("div",{className:"runs",children:[(0,n.jsx)("div",{className:"runs-stats",children:(0,n.jsxs)("div",{className:"stat-row",children:[(0,n.jsx)("div",{className:"stat-cell",children:(0,n.jsx)(_,{value:e.agents.length,label:"Runs tracked"})}),(0,n.jsx)("div",{className:"stat-cell",children:(0,n.jsx)(_,{value:i,label:"Needs input",accent:!0})}),(0,n.jsx)("div",{className:"stat-cell",children:(0,n.jsx)(_,{value:c,label:"Working"})}),(0,n.jsx)("div",{className:"stat-cell",children:(0,n.jsx)(_,{value:e.projects.length,label:"Repositories"})})]})}),(0,n.jsxs)("div",{className:"split",children:[(0,n.jsxs)("div",{className:"split-list",children:[(0,n.jsxs)("div",{className:"split-list-head",children:[(0,n.jsx)("div",{className:"section-title",style:{fontSize:"var(--text-lg)"},children:"Runs"}),(0,n.jsx)(N,{tabs:R,value:t,onChange:s})]}),(0,n.jsxs)("div",{className:"split-list-scroll",children:[0===a.length&&(0,n.jsx)(C,{tone:"info",children:"No runs match this filter."}),a.map(t=>(0,n.jsx)(T,{agent:t,selected:(null==l?void 0:l.projectId)===t.project_id&&(null==l?void 0:l.name)===t.name,onSelect:()=>e.selectRun(t.project_id,t.name)},"".concat(t.project_id,"/").concat(t.name)))]})]}),(0,n.jsx)("div",{className:"split-detail",children:l?(0,n.jsx)(z,{}):(0,n.jsx)(A,{})})]})]})}function T(e){let{agent:t,selected:s,onSelect:a}=e;return(0,n.jsxs)("button",{className:"run-row".concat(s?" selected":""),onClick:a,children:[(0,n.jsxs)("div",{className:"run-row-top",children:[(0,n.jsx)("span",{className:"run-project",children:t.project_id}),(0,n.jsx)("span",{className:"faint mono",style:{fontSize:"var(--text-xs)"},children:p(t.created_at)})]}),(0,n.jsxs)("div",{className:"truncate muted",style:{fontSize:"var(--text-sm)"},children:[t.name,t.role?" \xb7 ".concat(t.role):""]}),(0,n.jsx)("div",{className:"hstack",style:{gap:8},children:(0,n.jsx)(v,{status:t.status})})]})}function A(){return(0,n.jsx)("div",{style:{padding:"60px 32px",color:"var(--text-muted)"},children:"Select a run to see its checkmark, log, and any open question."})}function z(){var e;let t=o(),s=t.selectedRun,a=t.agents.find(e=>e.project_id===s.projectId&&e.name===s.name),l=t.checkmark,[i,c]=(0,r.useState)(""),[d,h]=(0,r.useState)(!1),p=(null==a?void 0:a.status)==="paused_for_input",g=async e=>{if(!i.trim())return;h(!0);let s=await t.submitAnswer(i.trim(),e);h(!1),s&&c("")};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{style:{padding:"24px 28px",borderBottom:"1px solid var(--border-default)",display:"flex",flexDirection:"column",gap:10},children:[(0,n.jsxs)("div",{className:"hstack",children:[(0,n.jsx)("span",{style:{color:"var(--accent)",fontWeight:"var(--fw-bold)",fontSize:"var(--text-xl)"},children:s.projectId}),(0,n.jsx)("span",{className:"faint",children:"/"}),(0,n.jsx)("span",{style:{color:"var(--text-heading)",fontWeight:"var(--fw-semibold)",fontSize:"var(--text-lg)"},children:s.name}),(0,n.jsx)(v,{status:null==a?void 0:a.status}),(null==a?void 0:a.role)&&(0,n.jsx)(j,{tone:"info",children:a.role}),(0,n.jsx)("span",{className:"spacer"}),(0,n.jsx)(y,{size:"sm",variant:"secondary",onClick:()=>t.killAgent(s.projectId,s.name),children:"Kill"}),(0,n.jsx)(y,{size:"sm",variant:"danger",onClick:()=>t.deleteAgent(s.projectId,s.name),children:"Delete row"})]}),(0,n.jsxs)("div",{className:"mono faint",style:{fontSize:"var(--text-xs)"},children:[null!==(e=null==a?void 0:a.working_dir)&&void 0!==e?e:"—"," \xb7 created ",m(null==a?void 0:a.created_at)]})]}),(0,n.jsxs)("div",{style:{padding:"20px 28px",display:"flex",flexDirection:"column",gap:16},children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"eyebrow",style:{marginBottom:10},children:"Checkmark"}),t.checkmarkMissing&&(0,n.jsx)(C,{tone:"info",children:"No checkpoint recorded yet."}),l&&!t.checkmarkMissing&&(0,n.jsxs)("dl",{className:"kv",children:[(0,n.jsx)("dt",{children:"Status"}),(0,n.jsx)("dd",{children:(0,n.jsx)(v,{status:l.status})}),(0,n.jsx)("dt",{children:"Where it stopped"}),(0,n.jsx)("dd",{children:l.where_it_stopped||"—"}),(0,n.jsx)("dt",{children:"Open question"}),(0,n.jsx)("dd",{children:l.open_question||"—"}),(0,n.jsx)("dt",{children:"Next steps"}),(0,n.jsx)("dd",{children:l.next_steps&&l.next_steps.length>0?(0,n.jsx)("ul",{children:l.next_steps.map((e,t)=>(0,n.jsx)("li",{children:e},t))}):"—"}),(0,n.jsx)("dt",{children:"Tests"}),(0,n.jsxs)("dd",{className:"hstack",children:[(0,n.jsx)(j,{tone:u(l.tests_status),children:l.tests_status}),(0,n.jsx)("span",{className:"faint mono",style:{fontSize:"var(--text-xs)"},children:l.tested_at?m(l.tested_at):""})]}),(0,n.jsx)("dt",{children:"Build"}),(0,n.jsxs)("dd",{className:"hstack",children:[(0,n.jsx)(j,{tone:u(l.build_status),children:l.build_status}),(0,n.jsx)("span",{className:"faint mono",style:{fontSize:"var(--text-xs)"},children:l.built_at?m(l.built_at):""})]}),(0,n.jsx)("dt",{children:"Checkpoint at"}),(0,n.jsx)("dd",{className:"faint",children:m(l.checkpoint_at)})]})]}),p&&(0,n.jsxs)("div",{style:{display:"flex",flexDirection:"column",gap:10},children:[(0,n.jsx)("div",{className:"eyebrow",children:"Answer this question"}),(0,n.jsx)(C,{tone:"danger",children:(null==l?void 0:l.open_question)||"(no question text on the checkmark)"}),(0,n.jsx)(k,{value:i,onChange:c,rows:3,placeholder:"Your answer…"}),(0,n.jsxs)("div",{className:"hstack",children:[(0,n.jsx)(y,{variant:"secondary",disabled:d||!i.trim(),onClick:()=>g(!1),children:"Answer"}),(0,n.jsx)(y,{variant:"primary",disabled:d||!i.trim(),onClick:()=>g(!0),children:"Answer & Resume"})]})]}),(0,n.jsxs)("div",{style:{display:"flex",flexDirection:"column",gap:10},children:[(0,n.jsx)("div",{className:"eyebrow",children:"Log \xb7 newest first"}),0===t.log.length?(0,n.jsx)("div",{className:"empty",children:"No log entries."}):(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"When"}),(0,n.jsx)("th",{children:"Status"}),(0,n.jsx)("th",{children:"Summary"}),(0,n.jsx)("th",{children:"Q / A"}),(0,n.jsx)("th",{children:"Push"}),(0,n.jsx)("th",{children:"CI"})]})}),(0,n.jsx)("tbody",{children:t.log.map(e=>(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{className:"faint nowrap",children:m(e.created_at)}),(0,n.jsx)("td",{children:(0,n.jsx)(v,{status:e.status})}),(0,n.jsx)("td",{children:e.summary||"—"}),(0,n.jsxs)("td",{children:[e.question&&(0,n.jsxs)("div",{children:[(0,n.jsx)("strong",{children:"Q:"})," ",e.question]}),e.answer&&(0,n.jsxs)("div",{children:[(0,n.jsx)("strong",{children:"A:"})," ",e.answer]}),!e.question&&!e.answer&&"—"]}),(0,n.jsx)("td",{className:"mono",children:x(e.push_sha)}),(0,n.jsx)("td",{children:(0,n.jsx)(j,{tone:u(e.ci_status),children:e.ci_status})})]},e.id))})]})}),(0,n.jsxs)("div",{className:"pager",children:[(0,n.jsx)(y,{size:"sm",variant:"ghost",disabled:0===t.logOffset,onClick:()=>t.pageLog(-1),children:"‹ Newer"}),(0,n.jsxs)("span",{className:"faint mono",style:{fontSize:"var(--text-xs)"},children:["offset ",t.logOffset]}),(0,n.jsx)(y,{size:"sm",variant:"ghost",disabled:t.log.length<100,onClick:()=>t.pageLog(1),children:"Older ›"})]})]})]})]})}let E={mode:"server",git_server:"",repo:"",id:"",root_dir:"",git_remote:"",credential_ref:"",init_mise:!1};function P(){let e=o(),[t,s]=(0,r.useState)(E),[a,l]=(0,r.useState)(!1),i=(0,r.useMemo)(()=>{var t;let s=new Map;for(let a of e.agents)s.set(a.project_id,(null!==(t=s.get(a.project_id))&&void 0!==t?t:0)+1);return s},[e.agents]),c=(0,r.useMemo)(()=>[{value:"",label:e.hosts.length?"Pick a git server…":"No git servers configured"},...e.hosts.map(e=>({value:e.hostname,label:"".concat(e.hostname," (").concat(e.forge_type,")")}))],[e.hosts]),d=()=>{s(E),l(!1)},u=async()=>{(a?await e.updateProject(t.id,t):await e.createProject(t))&&d()},h=e=>{var t,a;s({...E,mode:"manual",id:e.id,root_dir:e.root_dir,git_remote:null!==(t=e.git_remote)&&void 0!==t?t:"",credential_ref:null!==(a=e.credential_ref)&&void 0!==a?a:""}),l(!0)},x=a?!!t.root_dir.trim():"server"===t.mode?!!t.git_server&&/^[\w.-]+\/[\w.-]+$/.test(t.repo.trim()):!!t.id.trim()&&!!t.root_dir.trim();return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Repositories"}),(0,n.jsx)("div",{className:"section-desc",children:"Repos Handler manages. Each carries its own agents, history, and credentials."})]}),(0,n.jsxs)("div",{className:"section-body",children:[(0,n.jsxs)(g,{children:[(0,n.jsx)("div",{className:"card-head",style:{marginBottom:14},children:(0,n.jsx)("span",{className:"card-title",style:{fontSize:"var(--text-md)",color:"var(--text-heading)"},children:a?"Edit repository \xb7 ".concat(t.id):"Add a repository"})}),!a&&(0,n.jsx)("div",{style:{marginBottom:14},children:(0,n.jsx)(N,{tabs:[{value:"server",label:"From a git server"},{value:"manual",label:"Manual (existing checkout)"}],value:t.mode,onChange:e=>s({...t,mode:e})})}),a||"server"!==t.mode?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(f,{label:"ID / slug",value:t.id,onChange:e=>s({...t,id:e}),placeholder:"leeworks-api",disabled:a}),(0,n.jsx)(f,{label:"Root dir",value:t.root_dir,onChange:e=>s({...t,root_dir:e}),placeholder:"/var/lib/handler/projects/leeworks"}),(0,n.jsx)(f,{label:"Git remote",value:t.git_remote,onChange:e=>s({...t,git_remote:e}),placeholder:"git@github.com:user/repo.git (optional)"}),(0,n.jsx)(f,{label:"Credential ref",value:t.credential_ref,onChange:e=>s({...t,credential_ref:e}),placeholder:"env:VAR / file:/path / db:host:github.com"})]}),(0,n.jsx)("p",{className:"faint",style:{fontSize:"var(--text-xs)",margin:"10px 0 0"},children:"Optional override — projects on a configured git server use its stored token automatically. credential_ref is a pointer, never the token (env: / file: / db:host:)."})]}):(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(w,{label:"Git server",value:t.git_server,onChange:e=>s({...t,git_server:e}),options:c}),(0,n.jsx)(f,{label:"Repository (owner/name)",value:t.repo,onChange:e=>s({...t,repo:e}),placeholder:"me/coolproj"}),(0,n.jsx)(f,{label:"ID / slug (optional — defaults to the repo name)",value:t.id,onChange:e=>s({...t,id:e}),placeholder:"coolproj"})]}),(0,n.jsx)("p",{className:"faint",style:{fontSize:"var(--text-xs)",margin:"10px 0 0"},children:"The repo is always pulled: Handler derives the remote from the server (ssh when it has a deploy key, https via the stored token otherwise), clones it under PROJECTS_ROOT, and keeps it fresh before every run."})]}),!a&&(0,n.jsxs)("label",{className:"hstack",style:{gap:8,cursor:"pointer",marginTop:14},children:[(0,n.jsx)("input",{type:"checkbox",checked:t.init_mise,onChange:e=>s({...t,init_mise:e.target.checked})}),(0,n.jsxs)("span",{style:{fontSize:"var(--text-sm)"},children:["Initialize mise — after the clone, run an agent that writes a"," ",(0,n.jsx)("span",{className:"mono",children:".mise.toml"})," with a"," ",(0,n.jsx)("span",{className:"mono",children:"[tasks.test]"})," task for this repo’s stack, then commits and pushes it. Needed for repos that don’t define one yet."]})]}),!a&&t.init_mise&&"manual"===t.mode&&!t.git_remote.trim()&&(0,n.jsxs)("p",{className:"faint",style:{fontSize:"var(--text-xs)",margin:"6px 0 0"},children:["A git remote is required to push the new ",(0,n.jsx)("span",{className:"mono",children:".mise.toml"})," — add one above, or mise won’t be initialized."]}),(0,n.jsxs)("div",{className:"hstack mt14",children:[(0,n.jsx)(y,{variant:"primary",disabled:e.cmd.busy||!x,onClick:u,children:a?"Save changes":"server"===t.mode?"Add & pull":"Register"}),a&&(0,n.jsx)(y,{variant:"ghost",onClick:d,children:"Cancel"})]})]}),0===e.projects.length&&(0,n.jsx)("div",{className:"empty",children:"No repositories registered."}),e.projects.map(t=>{var s,a;return(0,n.jsxs)(g,{children:[(0,n.jsxs)("div",{className:"card-head",children:[(0,n.jsx)("span",{className:"card-title",children:t.id}),(0,n.jsxs)(j,{tone:"info",pill:!0,children:[null!==(s=i.get(t.id))&&void 0!==s?s:0," ",(null!==(a=i.get(t.id))&&void 0!==a?a:0)===1?"agent":"agents"]})]}),(0,n.jsxs)("div",{className:"mono faint",style:{fontSize:"var(--text-xs)",marginTop:4},children:[t.root_dir,t.git_remote?" \xb7 ".concat(t.git_remote):""]}),(0,n.jsxs)("div",{className:"hstack",style:{marginTop:12,justifyContent:"space-between"},children:[(0,n.jsxs)("span",{className:"faint mono",style:{fontSize:"var(--text-xs)"},children:["cred ",t.credential_ref||"server default"," \xb7 added ",m(t.created_at)]}),(0,n.jsxs)("div",{className:"hstack",children:[t.git_remote&&(0,n.jsx)(y,{size:"sm",variant:"secondary",onClick:()=>e.syncProject(t.id),children:"Pull now"}),(0,n.jsx)(y,{size:"sm",variant:"secondary",onClick:()=>h(t),children:"Edit"}),(0,n.jsx)(y,{size:"sm",variant:"danger",onClick:()=>e.deleteProject(t.id),children:"Remove"})]})]})]},t.id)})]})]})}let M=[{value:"",label:"Role — none"},{value:"junior",label:"junior"},{value:"senior",label:"senior"},{value:"deploy",label:"deploy"}],O=[{value:"worktree",label:"git worktree on branch"},{value:"subdir",label:"subdir under root"}],L={name:"",role:"",placement:"worktree",worktree:"",subdir:"",task:""};function U(){let e=o(),[t,s]=(0,r.useState)(L),a=(0,r.useMemo)(()=>e.projects.map(e=>({value:e.id,label:e.id})),[e.projects]),l=(0,r.useMemo)(()=>e.agents.filter(t=>t.project_id===e.selectedProjectId),[e.agents,e.selectedProjectId]),i=async()=>{await e.spawnAgent(t)&&s(L)};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Agents"}),(0,n.jsx)("div",{className:"section-desc",children:"Spawn agents into a repository and manage running sessions."})]}),(0,n.jsx)("div",{className:"section-body",children:0===e.projects.length?(0,n.jsx)("div",{className:"empty",children:"Register a repository first."}):(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("div",{className:"row",children:(0,n.jsx)("div",{style:{width:260},children:(0,n.jsx)(w,{label:"Repository",value:e.selectedProjectId,onChange:e.selectProject,options:a})})}),(0,n.jsxs)(g,{children:[(0,n.jsx)("div",{className:"card-head",style:{marginBottom:14},children:(0,n.jsx)("span",{className:"card-title",style:{fontSize:"var(--text-md)",color:"var(--text-heading)"},children:"Spawn an agent"})}),(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(f,{label:"Name",value:t.name,onChange:e=>s({...t,name:e}),placeholder:"junior"}),(0,n.jsx)(w,{label:"Role",value:t.role,onChange:e=>s({...t,role:e}),options:M}),(0,n.jsx)(w,{label:"Placement",value:t.placement,onChange:e=>s({...t,placement:e}),options:O}),"worktree"===t.placement?(0,n.jsx)(f,{label:"Branch",value:t.worktree,onChange:e=>s({...t,worktree:e}),placeholder:"feat/auth"}):(0,n.jsx)(f,{label:"Subdir",value:t.subdir,onChange:e=>s({...t,subdir:e}),placeholder:"api"})]}),(0,n.jsx)("div",{className:"mt14",children:(0,n.jsx)(k,{label:"Initial task",value:t.task,onChange:e=>s({...t,task:e}),rows:2,placeholder:"initial task / prompt (optional)"})}),(0,n.jsx)("div",{className:"hstack mt14",children:(0,n.jsx)(y,{variant:"primary",disabled:e.cmd.busy||!t.name.trim(),onClick:i,children:"Spawn"})})]}),0===l.length?(0,n.jsx)("div",{className:"empty",children:"No agents in this repository."}):(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"Name"}),(0,n.jsx)("th",{children:"Role"}),(0,n.jsx)("th",{children:"Status"}),(0,n.jsx)("th",{children:"Working dir"}),(0,n.jsx)("th",{children:"Created"}),(0,n.jsx)("th",{})]})}),(0,n.jsx)("tbody",{children:l.map(t=>{var s;return(0,n.jsxs)(r.Fragment,{children:[(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{className:"mono",children:t.name}),(0,n.jsx)("td",{children:t.role?(0,n.jsx)(j,{tone:"info",children:t.role}):"—"}),(0,n.jsx)("td",{children:(0,n.jsx)(v,{status:t.status})}),(0,n.jsx)("td",{className:"mono faint",children:t.working_dir}),(0,n.jsx)("td",{className:"faint nowrap",children:m(t.created_at)}),(0,n.jsx)("td",{className:"nowrap",children:(0,n.jsxs)("div",{className:"hstack",children:[(0,n.jsx)(y,{size:"sm",variant:"ghost",onClick:()=>e.selectRun(t.project_id,t.name),children:"Open"}),(0,n.jsx)(y,{size:"sm",variant:"secondary",onClick:()=>e.killAgent(t.project_id,t.name),children:"Kill"}),(0,n.jsx)(y,{size:"sm",variant:"danger",onClick:()=>e.deleteAgent(t.project_id,t.name),children:"Delete"})]})})]}),"working"===t.status&&(null===(s=t.last_output)||void 0===s?void 0:s.trim())&&(0,n.jsx)("tr",{children:(0,n.jsxs)("td",{colSpan:6,style:{paddingTop:0},children:[(0,n.jsxs)("div",{className:"faint",style:{fontSize:"var(--text-xs)",marginBottom:4},children:["live output",t.output_at?" \xb7 ".concat(p(t.output_at)):""]}),(0,n.jsx)("pre",{className:"mono",style:{margin:0,padding:"8px 10px",background:"var(--surface-2, rgba(0,0,0,0.25))",borderRadius:6,fontSize:"var(--text-xs)",lineHeight:1.4,maxHeight:220,overflow:"auto",whiteSpace:"pre"},children:t.last_output})]})})]},t.id)})})]})})]})})]})}let D=[{value:"",label:"Role — none"},{value:"junior",label:"junior"},{value:"senior",label:"senior"},{value:"deploy",label:"deploy"}],F=[{value:"900",label:"every 15 minutes"},{value:"1800",label:"every 30 minutes"},{value:"3600",label:"every hour"},{value:"21600",label:"every 6 hours"},{value:"86400",label:"every day"},{value:"604800",label:"every week"}],B={name_prefix:"",task:"",interval:"3600",role:""};function H(){let e=o(),[t,s]=(0,r.useState)(B),a=(0,r.useMemo)(()=>e.projects.map(e=>({value:e.id,label:e.id})),[e.projects]),l=async()=>{await e.createSchedule(e.selectedProjectId,{name_prefix:t.name_prefix,task:t.task,interval_seconds:Number(t.interval),role:t.role})&&s(B)};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Schedules"}),(0,n.jsx)("div",{className:"section-desc",children:"Spawn a fresh agent on an interval. Each run is stateless — keep continuity in a file the prompt reads and overwrites."})]}),(0,n.jsx)("div",{className:"section-body",children:0===e.projects.length?(0,n.jsx)("div",{className:"empty",children:"Register a repository first."}):(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("div",{className:"row",children:(0,n.jsx)("div",{style:{width:260},children:(0,n.jsx)(w,{label:"Repository",value:e.selectedProjectId,onChange:e.selectProject,options:a})})}),(0,n.jsxs)(g,{children:[(0,n.jsx)("div",{className:"card-head",style:{marginBottom:14},children:(0,n.jsx)("span",{className:"card-title",style:{fontSize:"var(--text-md)",color:"var(--text-heading)"},children:"New schedule"})}),(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(f,{label:"Name prefix",value:t.name_prefix,onChange:e=>s({...t,name_prefix:e}),placeholder:"nightly"}),(0,n.jsx)(w,{label:"Interval",value:t.interval,onChange:e=>s({...t,interval:e}),options:F}),(0,n.jsx)(w,{label:"Role",value:t.role,onChange:e=>s({...t,role:e}),options:D})]}),(0,n.jsx)("div",{className:"mt14",children:(0,n.jsx)(k,{label:"Prompt (the task every run starts with)",value:t.task,onChange:e=>s({...t,task:e}),rows:3,placeholder:"Read @notes.md and continue from where it left off. Before finishing, overwrite @notes.md with the current state so the next run can pick up from there."})}),(0,n.jsxs)("p",{className:"faint",style:{fontSize:"var(--text-xs)",margin:"10px 0 0"},children:["Runs are named ",(0,n.jsxs)("span",{className:"mono",children:[t.name_prefix.trim()||"prefix","-YYYYMMDD-HHMMSS"]}),". The repo is pulled before every run; the first run fires on the worker's next pass."]}),(0,n.jsx)("div",{className:"hstack mt14",children:(0,n.jsx)(y,{variant:"primary",disabled:e.cmd.busy||!t.name_prefix.trim()||!t.task.trim(),onClick:l,children:"Create schedule"})})]}),0===e.schedules.length?(0,n.jsx)("div",{className:"empty",children:"No schedules yet."}):(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"On"}),(0,n.jsx)("th",{children:"Name"}),(0,n.jsx)("th",{children:"Repository"}),(0,n.jsx)("th",{children:"Interval"}),(0,n.jsx)("th",{children:"Prompt"}),(0,n.jsx)("th",{children:"Next run"}),(0,n.jsx)("th",{children:"Last run"}),(0,n.jsx)("th",{})]})}),(0,n.jsx)("tbody",{children:e.schedules.map(t=>(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{children:(0,n.jsx)(S,{on:t.enabled,onClick:()=>e.updateSchedule(t.id,{enabled:!t.enabled})})}),(0,n.jsxs)("td",{className:"mono",children:[t.name_prefix,t.role?(0,n.jsxs)(n.Fragment,{children:[" ",(0,n.jsx)(j,{tone:"info",children:t.role})]}):null]}),(0,n.jsx)("td",{className:"mono faint",children:t.project_id}),(0,n.jsx)("td",{className:"nowrap",children:function(e){let t=F.find(t=>Number(t.value)===e);return t?t.label:e%3600==0?"every ".concat(e/3600,"h"):e%60==0?"every ".concat(e/60,"m"):"every ".concat(e,"s")}(t.interval_seconds)}),(0,n.jsx)("td",{className:"faint",style:{maxWidth:340},children:(0,n.jsx)("span",{className:"truncate",style:{display:"block"},title:t.task,children:t.task})}),(0,n.jsx)("td",{className:"faint nowrap",children:t.enabled?m(t.next_run_at):"paused"}),(0,n.jsx)("td",{className:"faint nowrap",children:m(t.last_run_at)}),(0,n.jsx)("td",{className:"nowrap",children:(0,n.jsx)(y,{size:"sm",variant:"danger",onClick:()=>e.deleteSchedule(t.id),children:"Delete"})})]},t.id))})]})})]})})]})}let q=[{value:"approved",label:"approve"},{value:"rejected",label:"reject"}],W={branch:"",status:"approved",agent_name:"",sha:"",note:""};function G(){let e=o(),[t,s]=(0,r.useState)(W),a=(0,r.useMemo)(()=>e.projects.map(e=>({value:e.id,label:e.id})),[e.projects]),l=async()=>{await e.submitApproval(t),s(W)};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Approvals"}),(0,n.jsx)("div",{className:"section-desc",children:"A merge is denied unless a standing approval exists — made by a different agent, pinned to the reviewed commit."})]}),(0,n.jsx)("div",{className:"section-body",children:0===e.projects.length?(0,n.jsx)("div",{className:"empty",children:"Register a repository first."}):(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("div",{className:"row",children:(0,n.jsx)("div",{style:{width:260},children:(0,n.jsx)(w,{label:"Repository",value:e.selectedProjectId,onChange:e.selectProject,options:a})})}),(0,n.jsxs)(g,{children:[(0,n.jsx)("div",{className:"card-head",style:{marginBottom:14},children:(0,n.jsx)("span",{className:"card-title",style:{fontSize:"var(--text-md)",color:"var(--text-heading)"},children:"Record a verdict"})}),(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(f,{label:"Branch",value:t.branch,onChange:e=>s({...t,branch:e}),placeholder:"feat/auth"}),(0,n.jsx)(w,{label:"Verdict",value:t.status,onChange:e=>s({...t,status:e}),options:q}),(0,n.jsx)(f,{label:"Agent",value:t.agent_name,onChange:e=>s({...t,agent_name:e}),placeholder:"reads its HEAD (optional)"}),(0,n.jsx)(f,{label:"SHA",value:t.sha,onChange:e=>s({...t,sha:e}),placeholder:"pins the approval (optional)"})]}),(0,n.jsx)("div",{className:"mt14",children:(0,n.jsx)(f,{label:"Note",value:t.note,onChange:e=>s({...t,note:e}),placeholder:"optional"})}),(0,n.jsx)("div",{className:"hstack mt14",children:(0,n.jsx)(y,{variant:"primary",disabled:e.cmd.busy||!t.branch.trim(),onClick:l,children:"Enqueue verdict"})})]}),0===e.approvals.length?(0,n.jsx)("div",{className:"empty",children:"No approvals recorded."}):(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"When"}),(0,n.jsx)("th",{children:"Branch"}),(0,n.jsx)("th",{children:"Verdict"}),(0,n.jsx)("th",{children:"By"}),(0,n.jsx)("th",{children:"SHA"}),(0,n.jsx)("th",{children:"Note"})]})}),(0,n.jsx)("tbody",{children:e.approvals.map(e=>(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{className:"faint nowrap",children:m(e.created_at)}),(0,n.jsx)("td",{className:"mono",children:e.branch}),(0,n.jsx)("td",{children:(0,n.jsx)(v,{status:e.status})}),(0,n.jsx)("td",{children:e.approved_by_agent_id?"agent ".concat(e.approved_by_agent_id):e.actor||"—"}),(0,n.jsx)("td",{className:"mono",children:x(e.approved_sha)}),(0,n.jsx)("td",{children:e.note||"—"})]},e.id))})]})})]})})]})}let J=[{value:"github",label:"github"},{value:"gitlab",label:"gitlab"},{value:"gitea",label:"gitea"},{value:"forgejo",label:"forgejo"},{value:"bitbucket",label:"bitbucket"}],K={hostname:"",forge_type:"github",token_env_var:"",base_url:"",token:"",generate_ssh_key:!0};function Y(e){let{value:t}=e,[s,a]=(0,r.useState)(!1),l=async()=>{try{await navigator.clipboard.writeText(t),a(!0),setTimeout(()=>a(!1),1500)}catch(e){}};return(0,n.jsxs)("div",{style:{marginTop:10},children:[(0,n.jsxs)("div",{className:"hstack",style:{justifyContent:"space-between"},children:[(0,n.jsx)("span",{className:"eyebrow",children:"SSH public key — add it to the forge (deploy key)"}),(0,n.jsx)(y,{size:"sm",variant:"secondary",onClick:l,children:s?"Copied":"Copy"})]}),(0,n.jsx)("pre",{className:"mono",style:{fontSize:"var(--text-xs)",whiteSpace:"pre-wrap",wordBreak:"break-all",margin:"6px 0 0",padding:8,border:"1px solid var(--border-default)",borderRadius:6,userSelect:"all"},children:t})]})}function V(){let e=o(),[t,s]=(0,r.useState)(K),[a,l]=(0,r.useState)(!1),i=()=>{s(K),l(!1)},c=async()=>{(a?await e.updateHost(t.hostname,t):await e.createHost(t))&&i()},d=e=>{var t,a;s({hostname:e.hostname,forge_type:e.forge_type,token_env_var:null!==(t=e.token_env_var)&&void 0!==t?t:"",base_url:null!==(a=e.base_url)&&void 0!==a?a:"",token:"",generate_ssh_key:!1}),l(!0)};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Git Servers"}),(0,n.jsxs)("div",{className:"section-desc",children:["Each server carries its own credentials: a forge token (encrypted at rest, used by agents' ",(0,n.jsx)("span",{className:"mono",children:"forge"})," + git) and an SSH deploy key — paste the public key into the forge. New repositories are added by picking a server and typing owner/name."]})]}),(0,n.jsxs)("div",{className:"section-body",children:[(0,n.jsxs)(g,{children:[(0,n.jsx)("div",{className:"card-head",style:{marginBottom:14},children:(0,n.jsx)("span",{className:"card-title",style:{fontSize:"var(--text-md)",color:"var(--text-heading)"},children:a?"Edit server \xb7 ".concat(t.hostname):"Add a git server"})}),(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(f,{label:"Hostname",value:t.hostname,onChange:e=>s({...t,hostname:e}),placeholder:"github.com",disabled:a}),(0,n.jsx)(w,{label:"Type",value:t.forge_type,onChange:e=>s({...t,forge_type:e}),options:J}),(0,n.jsx)(f,{label:a?"Forge token (blank = keep current)":"Forge token",type:"password",value:t.token,onChange:e=>s({...t,token:e}),placeholder:"stored encrypted; used by forge + git"}),(0,n.jsx)(f,{label:"Base URL (optional)",value:t.base_url,onChange:e=>s({...t,base_url:e}),placeholder:"https://git.corp.internal:8443"}),(0,n.jsx)(f,{label:"Token env var override (optional)",value:t.token_env_var,onChange:e=>s({...t,token_env_var:e}),placeholder:"GITEA_TOKEN"}),(0,n.jsxs)("label",{className:"field",children:[(0,n.jsx)("span",{className:"field-label",children:"SSH deploy key"}),(0,n.jsxs)("label",{className:"hstack",style:{gap:8,cursor:"pointer"},children:[(0,n.jsx)("input",{type:"checkbox",checked:t.generate_ssh_key,onChange:e=>s({...t,generate_ssh_key:e.target.checked})}),(0,n.jsx)("span",{style:{fontSize:"var(--text-sm)"},children:a?"Regenerate keypair (replaces the current key)":"Generate a keypair"})]})]})]}),(0,n.jsxs)("div",{className:"hstack mt14",children:[(0,n.jsx)(y,{variant:"primary",disabled:e.cmd.busy||!t.hostname.trim(),onClick:c,children:a?"Save changes":"Add server"}),a&&(0,n.jsx)(y,{variant:"ghost",onClick:i,children:"Cancel"})]})]}),0===e.hosts.length&&(0,n.jsx)("div",{className:"empty",children:"No git servers registered (built-in host map still applies)."}),e.hosts.map(t=>(0,n.jsxs)(g,{children:[(0,n.jsxs)("div",{className:"card-head",children:[(0,n.jsx)("span",{className:"mono",style:{fontWeight:"var(--fw-bold)",fontSize:"var(--text-lg)",color:"var(--text-heading)"},children:t.hostname}),(0,n.jsxs)("div",{className:"hstack",children:[(0,n.jsx)(j,{tone:"info",children:t.forge_type}),(0,n.jsx)(j,{tone:t.has_token?"success":"neutral",children:t.has_token?"token stored":"no token"}),(0,n.jsx)(j,{tone:t.ssh_public_key?"success":"neutral",children:t.ssh_public_key?"ssh key":"no ssh key"}),(0,n.jsx)(y,{size:"sm",variant:"secondary",onClick:()=>d(t),children:"Edit"}),(0,n.jsx)(y,{size:"sm",variant:"danger",onClick:()=>e.deleteHost(t.hostname),children:"Remove"})]})]}),(0,n.jsxs)("div",{className:"mono faint",style:{fontSize:"var(--text-xs)",marginTop:8},children:["token env ",t.token_env_var||"—",t.base_url?" \xb7 ".concat(t.base_url):""]}),t.ssh_public_key&&(0,n.jsx)(Y,{value:t.ssh_public_key})]},t.hostname))]})]})}function Q(){let e=o();return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("div",{className:"section-head",children:(0,n.jsxs)("div",{className:"hstack",style:{justifyContent:"space-between"},children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"section-title",children:"Activity"}),(0,n.jsx)("div",{className:"section-desc",children:"Control commands the worker drains from the queue."})]}),(0,n.jsx)(y,{variant:"secondary",disabled:e.cmd.busy,onClick:()=>e.pollCi(),children:"Sweep CI now"})]})}),(0,n.jsx)("div",{className:"section-body",children:0===e.commands.length?(0,n.jsx)("div",{className:"empty",children:"No commands yet."}):(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"When"}),(0,n.jsx)("th",{children:"Type"}),(0,n.jsx)("th",{children:"Repository"}),(0,n.jsx)("th",{children:"Agent"}),(0,n.jsx)("th",{children:"Status"}),(0,n.jsx)("th",{children:"Result / Error"})]})}),(0,n.jsx)("tbody",{children:e.commands.map(e=>(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{className:"faint nowrap",children:m(e.created_at)}),(0,n.jsx)("td",{className:"mono",children:e.type}),(0,n.jsx)("td",{className:"mono",children:e.project_id||"—"}),(0,n.jsx)("td",{className:"mono",children:e.agent_name||"—"}),(0,n.jsx)("td",{children:(0,n.jsx)(v,{status:e.status})}),(0,n.jsx)("td",{className:"mono faint",style:{fontSize:"var(--text-xs)"},children:e.error||(e.result?JSON.stringify(e.result):"—")})]},e.id))})]})})})]})}function X(){let e=o(),[t,s]=(0,r.useState)(""),[a,l]=(0,r.useState)(""),i=async()=>{t.trim()&&a.trim()&&await e.setSharedKey(t.trim(),a.trim())&&(s(""),l(""))};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Shared"}),(0,n.jsx)("div",{className:"section-desc",children:"The cross-project global feed and shared facts."})]}),(0,n.jsxs)("div",{className:"section-body",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"eyebrow",style:{marginBottom:10},children:"Global feed"}),0===e.shared.log.length?(0,n.jsx)("div",{className:"empty",children:"No global log entries."}):(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"When"}),(0,n.jsx)("th",{children:"Agent"}),(0,n.jsx)("th",{children:"Status"}),(0,n.jsx)("th",{children:"Summary"}),(0,n.jsx)("th",{children:"CI"})]})}),(0,n.jsx)("tbody",{children:e.shared.log.map(e=>(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{className:"faint nowrap",children:m(e.created_at)}),(0,n.jsx)("td",{className:"mono",children:e.agent_id}),(0,n.jsx)("td",{children:(0,n.jsx)(v,{status:e.status})}),(0,n.jsx)("td",{children:e.summary||"—"}),(0,n.jsx)("td",{children:(0,n.jsx)(v,{status:e.ci_status})})]},e.id))})]})})]}),(0,n.jsxs)(g,{children:[(0,n.jsx)("div",{className:"card-head",style:{marginBottom:14},children:(0,n.jsx)("span",{className:"card-title",style:{fontSize:"var(--text-md)",color:"var(--text-heading)"},children:"Set a shared key"})}),(0,n.jsxs)("div",{className:"form-grid",children:[(0,n.jsx)(f,{label:"Key",value:t,onChange:s,placeholder:"key"}),(0,n.jsx)(f,{label:"Value",value:a,onChange:l,placeholder:"value"})]}),(0,n.jsx)("p",{className:"faint",style:{fontSize:"var(--text-xs)",margin:"10px 0 0"},children:"Requires the shared-context write token (or admin/global if unset)."}),(0,n.jsx)("div",{className:"hstack mt14",children:(0,n.jsx)(y,{variant:"primary",disabled:!t.trim()||!a.trim(),onClick:i,children:"Set"})})]}),e.shared.context.length>0&&(0,n.jsx)("div",{className:"table-wrap",children:(0,n.jsxs)("table",{className:"tbl",children:[(0,n.jsx)("thead",{children:(0,n.jsxs)("tr",{children:[(0,n.jsx)("th",{children:"Key"}),(0,n.jsx)("th",{children:"Value"}),(0,n.jsx)("th",{children:"Updated"})]})}),(0,n.jsx)("tbody",{children:e.shared.context.map(e=>(0,n.jsxs)("tr",{children:[(0,n.jsx)("td",{className:"mono",children:e.key}),(0,n.jsx)("td",{children:e.value}),(0,n.jsx)("td",{className:"faint nowrap",children:m(e.updated_at)})]},e.key))})]})})]})]})}function $(e){let t=window.screenX+Math.max(0,(window.outerWidth-520)/2),s=window.screenY+Math.max(0,(window.outerHeight-760)/2);return window.open(e,"claude-login","popup=yes,width=".concat(520,",height=").concat(760,",left=").concat(Math.round(t),",top=").concat(Math.round(s)))}function Z(){let e=o(),{status:t,url:s,message:a}=e.claudeLogin,[l,i]=(0,r.useState)(""),c=(0,r.useRef)(null),d="starting"===t||"submitting"===t,u="awaiting"===t||"submitting"===t;(0,r.useEffect)(()=>{if("awaiting"===t&&s&&c.current&&!c.current.closed)try{c.current.location.href=s}catch(e){}if("done"===t||"error"===t){var e;null===(e=c.current)||void 0===e||e.close(),c.current=null}},[t,s]);let h=()=>{c.current=$("about:blank"),e.startClaudeLogin()},m=async()=>{await e.submitClaudeCode(l)&&i("")};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)("div",{className:"section-head",children:[(0,n.jsx)("div",{className:"section-title",children:"Claude Login"}),(0,n.jsxs)("div",{className:"section-desc",children:["Log Claude Code in on the host so agents can run. This drives"," ",(0,n.jsx)("span",{className:"mono",children:"claude /login"})," in the control container and picks the Claude account with a subscription."]})]}),(0,n.jsxs)("div",{className:"section-body",style:{display:"flex",flexDirection:"column",gap:16},children:[a&&(0,n.jsx)(C,{tone:"error"===t?"danger":"done"===t?"success":"info",children:a}),"done"===t?(0,n.jsx)("div",{children:(0,n.jsx)(y,{variant:"secondary",onClick:e.resetClaudeLogin,children:"Log in again"})}):u?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(C,{tone:"info",children:"A Claude sign-in window should have opened. Authorize there, copy the code Claude shows you, and paste it below. If the window didn't open (popups blocked), use the button."}),(0,n.jsxs)("div",{className:"hstack",style:{gap:10,flexWrap:"wrap"},children:[(0,n.jsx)(y,{variant:"secondary",disabled:!s,onClick:()=>{s&&(c.current=$(s))},children:"Open Claude sign-in window ↗"}),s&&(0,n.jsx)("a",{className:"btn btn-ghost",href:s,target:"_blank",rel:"noopener noreferrer",children:"Open in a new tab"}),(0,n.jsx)(y,{variant:"ghost",disabled:d,onClick:h,children:"Restart"})]}),(0,n.jsxs)("div",{className:"hstack",style:{gap:10,alignItems:"flex-end",flexWrap:"wrap"},children:[(0,n.jsx)("div",{style:{flex:"1 1 320px"},children:(0,n.jsx)(f,{label:"Authorization code",value:l,onChange:i,placeholder:"Paste the code from claude.com",disabled:"submitting"===t})}),(0,n.jsx)(y,{variant:"primary",disabled:"submitting"===t||!l.trim(),onClick:m,children:"submitting"===t?"Submitting…":"Finish login"})]})]}):(0,n.jsxs)("div",{className:"hstack",style:{gap:10},children:[(0,n.jsx)(y,{variant:"primary",disabled:d,onClick:h,children:"starting"===t?"Starting…":"Log in to Claude"}),"error"===t&&(0,n.jsx)(y,{variant:"ghost",disabled:d,onClick:h,children:"Retry"})]})]})]})}let ee=[{key:"runs",label:"Runs",count:e=>e.agents.length,accent:e=>e.agents.some(e=>"paused_for_input"===e.status)},{key:"repositories",label:"Repositories",count:e=>e.projects.length},{key:"agents",label:"Agents",count:e=>e.agents.length},{key:"schedules",label:"Schedules",count:e=>e.schedules.length},{key:"approvals",label:"Approvals",count:e=>e.approvals.length},{key:"servers",label:"Git Servers",count:e=>e.hosts.length},{key:"activity",label:"Activity",count:e=>e.commands.length},{key:"shared",label:"Shared",count:e=>e.shared.context.length},{key:"login",label:"Claude Login",count:()=>0,accent:e=>"done"!==e.claudeLogin.status}];function et(e){let{onSignOut:t}=e,s=o();return(0,n.jsxs)("div",{className:"app",children:[(0,n.jsxs)("aside",{className:"sidebar",children:[(0,n.jsxs)("div",{className:"brand",children:[(0,n.jsx)("span",{className:"logo"}),"Claude Monitor"]}),ee.map(e=>{var t,a;let r=e.count(s),l=null!==(a=null===(t=e.accent)||void 0===t?void 0:t.call(e,s))&&void 0!==a&&a;return(0,n.jsxs)("button",{className:"nav-item".concat(s.section===e.key?" active":""),onClick:()=>s.setSection(e.key),children:[(0,n.jsx)("span",{children:e.label}),(0,n.jsx)("span",{className:"count",style:l?{color:"var(--lw-warning-fg)"}:void 0,children:r||""})]},e.key)}),(0,n.jsx)("div",{className:"sidebar-spacer"}),(0,n.jsxs)("div",{className:"sidebar-foot",children:[(0,n.jsxs)("button",{className:"nav-item",onClick:s.refresh,title:"Refresh now",children:[(0,n.jsx)("span",{children:"Refresh"}),(0,n.jsx)("span",{className:"count",children:"↻"})]}),(0,n.jsx)("button",{className:"nav-item",onClick:t,title:"Sign out / change token",children:(0,n.jsx)("span",{children:"Sign out"})})]})]}),(0,n.jsxs)("main",{className:"main",children:[s.cmd.text&&(0,n.jsx)("p",{className:"banner ".concat(s.cmd.error?"err":"ok"),style:{marginTop:16},children:s.cmd.text}),s.lastError&&(0,n.jsx)("p",{className:"banner err",style:{marginTop:12},children:s.lastError}),"runs"===s.section?(0,n.jsx)(I,{}):(0,n.jsxs)("div",{className:"main-scroll",children:["repositories"===s.section&&(0,n.jsx)(P,{}),"agents"===s.section&&(0,n.jsx)(U,{}),"schedules"===s.section&&(0,n.jsx)(H,{}),"approvals"===s.section&&(0,n.jsx)(G,{}),"servers"===s.section&&(0,n.jsx)(V,{}),"activity"===s.section&&(0,n.jsx)(Q,{}),"shared"===s.section&&(0,n.jsx)(X,{}),"login"===s.section&&(0,n.jsx)(Z,{})]})]})]})}function es(e){let{error:t,onSubmit:s}=e,[a,l]=(0,r.useState)("");return(0,n.jsx)("div",{className:"gate",children:(0,n.jsxs)("form",{className:"gate-card",onSubmit:e=>{e.preventDefault();let t=a.trim();t&&s(t)},children:[(0,n.jsxs)("div",{className:"gate-brand",children:[(0,n.jsx)("span",{className:"logo",style:{width:26,height:26,borderRadius:7}}),"Claude Monitor"]}),(0,n.jsx)("p",{className:"muted",style:{fontSize:"var(--text-sm)",margin:0},children:"Paste your API token to continue. Management actions require the admin token; read-only views work with the plain auth token."}),(0,n.jsx)("input",{className:"input",type:"password",autoComplete:"current-password",placeholder:"API token",value:a,onChange:e=>l(e.target.value),autoFocus:!0}),t&&(0,n.jsx)("p",{className:"callout callout-danger",style:{margin:0},children:t}),(0,n.jsx)("button",{className:"btn btn-primary",type:"submit",children:"Continue"})]})})}let ea="handler_token";function en(){let[e,t]=(0,r.useState)(null),[s,a]=(0,r.useState)("");(0,r.useEffect)(()=>{let e=window.localStorage.getItem(ea);e&&t(e)},[]);let l=(0,r.useCallback)(e=>{window.localStorage.setItem(ea,e),a(""),t(e)},[]),i=(0,r.useCallback)(()=>{window.localStorage.removeItem(ea),t(null)},[]),c=(0,r.useCallback)(()=>{window.localStorage.removeItem(ea),t(null),a("Invalid token — please try again.")},[]);return e?(0,n.jsx)(d,{token:e,onUnauthorized:c,children:(0,n.jsx)(et,{onSignOut:i})}):(0,n.jsx)(es,{error:s,onSubmit:l})}},257:function(e,t,s){"use strict";var a,n;e.exports=(null==(a=s.g.process)?void 0:a.env)&&"object"==typeof(null==(n=s.g.process)?void 0:n.env)?s.g.process:s(4227)},4227:function(e){!function(){var t={229:function(e){var t,s,a,n=e.exports={};function r(){throw Error("setTimeout has not been defined")}function l(){throw Error("clearTimeout has not been defined")}function i(e){if(t===setTimeout)return setTimeout(e,0);if((t===r||!t)&&setTimeout)return t=setTimeout,setTimeout(e,0);try{return t(e,0)}catch(s){try{return t.call(null,e,0)}catch(s){return t.call(this,e,0)}}}!function(){try{t="function"==typeof setTimeout?setTimeout:r}catch(e){t=r}try{s="function"==typeof clearTimeout?clearTimeout:l}catch(e){s=l}}();var c=[],o=!1,d=-1;function u(){o&&a&&(o=!1,a.length?c=a.concat(c):d=-1,c.length&&h())}function h(){if(!o){var e=i(u);o=!0;for(var t=c.length;t;){for(a=c,c=[];++d1)for(var s=1;sHandler · Claude Activity
Claude Monitor

Paste your API token to continue. Management actions require the admin token; read-only views work with the plain auth token.

\ No newline at end of file +Handler · Claude Activity
Claude Monitor

Paste your API token to continue. Management actions require the admin token; read-only views work with the plain auth token.

\ No newline at end of file diff --git a/src/handler/api/static/index.txt b/src/handler/api/static/index.txt index 22a12d7..847ed74 100644 --- a/src/handler/api/static/index.txt +++ b/src/handler/api/static/index.txt @@ -1,7 +1,7 @@ 2:I[9107,[],"ClientPageRoot"] -3:I[9859,["931","static/chunks/app/page-a511d608db773c67.js"],"default",1] +3:I[9859,["931","static/chunks/app/page-0637b8cf149a88ec.js"],"default",1] 4:I[4707,[],""] 5:I[6423,[],""] -0:["tQnVLKX-Dr8H3frHzPrCa",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]] +0:["ga21jhjKhYsutf8F3vo_-",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]] 6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Handler · Claude Activity"}],["$","meta","3",{"name":"description","content":"Monitor and manage Claude Code agents across projects."}],["$","link","4",{"rel":"icon","href":"/icon.svg?bab509b45a421e43","type":"image/svg+xml","sizes":"any"}]] 1:null diff --git a/src/handler/control/worker.py b/src/handler/control/worker.py index aeff4e2..36c2b36 100644 --- a/src/handler/control/worker.py +++ b/src/handler/control/worker.py @@ -298,6 +298,63 @@ def _run_one(command: dict) -> None: repo.finish_command(conn, command["id"], "failed", error=str(exc)) +def heartbeat(worker_id: str) -> None: + """Refresh this worker's registry row — the reaper's proof-of-life.""" + with connection() as conn: + active = len(repo.list_running_runs(conn, worker_id=worker_id)) + repo.upsert_worker_heartbeat( + conn, + worker_id, + hostname=socket.gethostname(), + pid=os.getpid(), + max_runs=get_settings().max_concurrent_runs, + active_runs=active, + ) + + +def reap_dead_workers(now: datetime | None = None) -> int: + """Settle the record for workers that stopped heartbeating: their ``running`` runs + become ``crashed``, and agents those runs left in ``working`` become ``crashed`` too. + + Any surviving worker may reap any dead one — ``finish_run``'s still-running guard + makes concurrent reapers idempotent. Deliberately NO auto-requeue: a half-done run + may have already pushed commits, so re-running it isn't safe to assume; the operator + sees the crashed badge (with the frozen last_output as evidence) and resumes + explicitly. Agents in ``paused_for_input``/``blocked`` keep their status — those are + still accurate descriptions of what the agent needs. Returns runs reaped. + """ + now = now or datetime.now(UTC) + cutoff = now - timedelta(seconds=get_settings().worker_stale_after) + reaped = 0 + with connection() as conn: + stale = repo.list_stale_workers(conn, cutoff) + for dead in stale: + with connection() as conn: + for run in repo.list_running_runs(conn, worker_id=dead["id"]): + if not repo.finish_run(conn, run["id"], "crashed"): + continue # another reaper won the race + agent = repo.get_agent_by_id(conn, run["agent_id"]) + if agent is not None and agent["status"] == "working": + repo.set_agent_status(conn, agent["id"], "crashed") + repo.insert_agent_event( + conn, + run["agent_id"], + run["id"], + seq=0, # runner-generated, outside the supervisor's line counter + type="worker", + payload={ + "notice": "worker died mid-run; run marked crashed", + "worker_id": dead["id"], + }, + session_id=run["session_id"], + ) + reaped += 1 + # Row served its purpose; dropping it keeps the registry to live workers + # (and stops every future pass re-scanning long-dead ids). + repo.delete_worker(conn, dead["id"]) + return reaped + + def fire_due_schedules(now: datetime | None = None) -> int: """Enqueue a spawn command for every schedule whose ``next_run_at`` has passed. @@ -419,10 +476,11 @@ def run( ci_interval: float = 30.0, capture_interval: float = 2.0, credsync_interval: float = 30.0, + reap_interval: float = 15.0, iterations: int | None = None, ) -> None: """The control-container main loop: drain the command queue, snapshot live agent - output, sync claude credentials, and sweep CI periodically. + output, sync claude credentials, heartbeat + reap dead workers, and sweep CI. ``iterations`` bounds the loop for tests; production runs unbounded. Sleeps ``poll_interval`` only when a pass found no commands, so bursts drain promptly. @@ -431,8 +489,19 @@ def run( last_ci = 0.0 last_capture = 0.0 last_credsync = 0.0 + last_reap = 0.0 count = 0 while iterations is None or count < iterations: + try: + heartbeat(worker_id) + except Exception: # noqa: BLE001 - a heartbeat hiccup must not kill the worker + pass + if reap_interval > 0 and time.monotonic() - last_reap >= reap_interval: + try: + reap_dead_workers() + except Exception: # noqa: BLE001 - a reap hiccup must not kill the worker + pass + last_reap = time.monotonic() try: fire_due_schedules() except Exception: # noqa: BLE001 - a schedule hiccup must not kill the worker diff --git a/src/handler/db/repository.py b/src/handler/db/repository.py index 3684c27..0f8e0aa 100644 --- a/src/handler/db/repository.py +++ b/src/handler/db/repository.py @@ -727,6 +727,12 @@ def list_stale_workers(conn: Connection, cutoff: datetime) -> list[dict]: return [dict(r._mapping) for r in rows] +def delete_worker(conn: Connection, worker_id: str) -> bool: + """Drop a (dead) worker's registry row after its runs have been settled.""" + result = conn.execute(workers.delete().where(workers.c.id == worker_id)) + return result.rowcount > 0 + + def create_run( conn: Connection, agent_id: int, session_id: str, worker_id: str, kind: str ) -> dict: diff --git a/tests/test_reaper.py b/tests/test_reaper.py new file mode 100644 index 0000000..db594f8 --- /dev/null +++ b/tests/test_reaper.py @@ -0,0 +1,116 @@ +"""Worker heartbeat + reaper: a worker that stops heartbeating gets its running runs +(and their still-'working' agents) marked crashed by any surviving worker — the positive +liveness that replaces tmux scraping. No auto-requeue: half-done runs may have pushed.""" + +from __future__ import annotations + +from datetime import UTC, datetime, timedelta + +from handler.control import worker +from handler.db import repository as repo +from handler.db.engine import get_engine + + +def _seed_run(worker_id, agent_name="a", agent_status="working"): + with get_engine().begin() as conn: + if repo.get_project(conn, "p") is None: + repo.create_project(conn, "p", "/tmp/p") + agent = repo.create_agent(conn, "p", agent_name, f"/tmp/p/{agent_name}", + status=agent_status) + run = repo.create_run(conn, agent["id"], f"sid-{agent_name}", worker_id, "spawn") + return agent, run + + +def _hb(worker_id, age_seconds=0.0): + with get_engine().begin() as conn: + repo.upsert_worker_heartbeat(conn, worker_id, hostname="h", pid=1) + if age_seconds: + from handler.db.tables import workers as workers_table + + conn.execute( + workers_table.update() + .where(workers_table.c.id == worker_id) + .values(heartbeat_at=datetime.now(UTC) - timedelta(seconds=age_seconds)) + ) + + +def test_reaper_marks_stale_workers_runs_crashed(env): + agent, run = _seed_run("w-dead") + _hb("w-dead", age_seconds=120) # stale: default worker_stale_after is 60s + + assert worker.reap_dead_workers() == 1 + + with get_engine().begin() as conn: + assert repo.get_run(conn, run["id"])["status"] == "crashed" + assert repo.get_agent_by_id(conn, agent["id"])["status"] == "crashed" + events = repo.list_agent_events(conn, agent["id"]) + # The dead worker's registry row is dropped once settled. + assert repo.list_stale_workers(conn, datetime.now(UTC) + timedelta(days=1)) == [] + assert events[0]["type"] == "worker" + assert events[0]["payload"]["worker_id"] == "w-dead" + + +def test_reaper_leaves_fresh_workers_alone(env): + agent, run = _seed_run("w-live") + _hb("w-live") # fresh heartbeat + + assert worker.reap_dead_workers() == 0 + with get_engine().begin() as conn: + assert repo.get_run(conn, run["id"])["status"] == "running" + assert repo.get_agent_by_id(conn, agent["id"])["status"] == "working" + + +def test_reaper_preserves_paused_agent_status(env): + """paused_for_input still accurately describes what the agent needs — only agents + stuck in 'working' get flipped to crashed.""" + agent, run = _seed_run("w-dead2", agent_name="paused", agent_status="paused_for_input") + _hb("w-dead2", age_seconds=120) + + assert worker.reap_dead_workers() == 1 + with get_engine().begin() as conn: + assert repo.get_run(conn, run["id"])["status"] == "crashed" + assert repo.get_agent_by_id(conn, agent["id"])["status"] == "paused_for_input" + + +def test_reaper_idempotent_against_races(env): + agent, run = _seed_run("w-dead3", agent_name="raced") + _hb("w-dead3", age_seconds=120) + assert worker.reap_dead_workers() == 1 + # A second reaper (or the same one next pass) finds nothing left to settle. + assert worker.reap_dead_workers() == 0 + with get_engine().begin() as conn: + assert len(repo.list_agent_events(conn, agent["id"])) == 1 + + +def test_heartbeat_registers_worker(env): + worker.heartbeat("w-me") + with get_engine().begin() as conn: + rows = repo.list_stale_workers(conn, datetime.now(UTC) + timedelta(seconds=1)) + assert [w["id"] for w in rows] == ["w-me"] + assert rows[0]["hostname"] + assert rows[0]["active_runs"] == 0 + + +def test_events_route_serves_stream(env, client, auth): + agent, run = _seed_run("w-api", agent_name="api-agent") + with get_engine().begin() as conn: + for seq in range(1, 4): + repo.insert_agent_event( + conn, agent["id"], run["id"], seq=seq, type="assistant", + payload={"n": seq}, session_id=run["session_id"], + ) + + resp = client.get("/projects/p/agents/api-agent/events", headers=auth) + assert resp.status_code == 200 + events = resp.json() + assert [e["seq"] for e in events] == [1, 2, 3] + assert events[0]["type"] == "assistant" + + # Cursor paging: only events after the given id come back. + resp = client.get( + f"/projects/p/agents/api-agent/events?after_id={events[1]['id']}", headers=auth + ) + assert [e["seq"] for e in resp.json()] == [3] + + resp = client.get("/projects/p/agents/missing/events", headers=auth) + assert resp.status_code == 404