@automagik/genie 4.260330.23 → 4.260330.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/genie.js +8 -8
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/scripts/tmux/tui-tmux.conf +5 -0
- package/src/term-commands/serve.ts +22 -18
- package/src/tui/components/Nav.tsx +11 -36
- package/src/tui/tmux.ts +40 -42
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "genie",
|
|
13
|
-
"version": "4.260330.
|
|
13
|
+
"version": "4.260330.24",
|
|
14
14
|
"source": "./plugins/genie",
|
|
15
15
|
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project."
|
|
16
16
|
}
|
package/dist/genie.js
CHANGED
|
@@ -1362,18 +1362,18 @@ Define your agent's mission here. What is their primary goal? What do they own?
|
|
|
1362
1362
|
`,failedCount++,deps.log({timestamp:now.toISOString(),level:"warn",event:"orphan_run_failed",run_id:run.id,worker_id:run.worker_id,dead_heartbeats:threshold})}if(failedCount>0)deps.log({timestamp:now.toISOString(),level:"info",event:"orphan_reconciliation_completed",failed_count:failedCount});return failedCount}async function collectMachineSnapshot(deps){let sql=await deps.getConnection(),now=deps.now(),snapshotId=deps.generateId(),workers=await deps.listWorkers(),activeWorkers=workers.filter((w)=>!["done","error","suspended"].includes(w.state)).length,teams=new Set(workers.filter((w)=>w.team).map((w)=>w.team)),tmuxSessions=await deps.countTmuxSessions(),cpuPercent=null,memoryMb=null;try{let mem=process.memoryUsage();memoryMb=Math.round(mem.rss/1024/1024)}catch{}try{let cpus=(await import("os")).cpus();if(cpus.length>0){let total=cpus.reduce((acc,cpu)=>{let t=Object.values(cpu.times).reduce((a,b2)=>a+b2,0);return acc+t-cpu.times.idle},0),totalAll=cpus.reduce((acc,cpu)=>acc+Object.values(cpu.times).reduce((a,b2)=>a+b2,0),0);cpuPercent=totalAll>0?Math.round(total/totalAll*100):null}}catch{}await sql`
|
|
1363
1363
|
INSERT INTO machine_snapshots (id, active_workers, active_teams, tmux_sessions, cpu_percent, memory_mb, created_at)
|
|
1364
1364
|
VALUES (${snapshotId}, ${activeWorkers}, ${teams.size}, ${tmuxSessions}, ${cpuPercent}, ${memoryMb}, ${now})
|
|
1365
|
-
`,deps.log({timestamp:now.toISOString(),level:"debug",event:"machine_snapshot",active_workers:activeWorkers,active_teams:teams.size,tmux_sessions:tmuxSessions,cpu_percent:cpuPercent,memory_mb:memoryMb})}async function emitWorkerEvents(deps){let workers=await deps.listWorkers(),now=deps.now().toISOString(),currentIds=new Set;for(let worker of workers){currentIds.add(worker.id);let prev=previousWorkerStates.get(worker.id),repoPath=worker.repoPath??process.cwd();if(!prev)await deps.publishEvent(`genie.agent.${worker.id}.spawned`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} spawned`,data:{state:worker.state},source:"registry"},repoPath);else if(prev.state!==worker.state){if(await deps.publishEvent(`genie.agent.${worker.id}.state`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} state: ${prev.state} \u2192 ${worker.state}`,data:{previousState:prev.state,state:worker.state},source:"registry"},repoPath),worker.state==="done"&&worker.wishSlug&&worker.groupNumber!=null)await deps.publishEvent(`genie.wish.${worker.wishSlug}.group.${worker.groupNumber}.done`,{timestamp:now,kind:"system",agent:worker.id,team:worker.team,text:`Wish ${worker.wishSlug} group ${worker.groupNumber} completed by ${worker.id}`,data:{wishSlug:worker.wishSlug,groupNumber:worker.groupNumber},source:"registry"},repoPath)}previousWorkerStates.set(worker.id,{...worker})}for(let[id,prev]of previousWorkerStates)if(!currentIds.has(id))await deps.publishEvent(`genie.agent.${id}.killed`,{timestamp:now,kind:"state",agent:id,team:prev.team,text:`Agent ${id} killed`,data:{lastState:prev.state},source:"registry"},prev.repoPath??process.cwd()),previousWorkerStates.delete(id)}function _resetWorkerStatesForTesting(){previousWorkerStates.clear()}function startInboxWatcherIfEnabled(deps){let pollMs=getInboxPollIntervalMs();if(pollMs===0)return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_disabled"}),null;return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_started",poll_interval_ms:pollMs}),startInboxWatcher()}function startDaemon(configOverrides,depsOverrides){let config=resolveConfig(configOverrides),deps={...createDefaultDeps(),...depsOverrides},daemonId=deps.generateId(),running2=!0,pollTimeout=null,pollResolve=null,listenConnection=null,heartbeatTimer=null,orphanTimer=null,inboxWatcherHandle=null,captureFallbackTimer=null,eventRouterHandle=null,stop=()=>{if(running2=!1,pollTimeout)clearTimeout(pollTimeout),pollTimeout=null;if(pollResolve)pollResolve(),pollResolve=null;if(heartbeatTimer)clearInterval(heartbeatTimer),heartbeatTimer=null;if(orphanTimer)clearInterval(orphanTimer),orphanTimer=null;if(inboxWatcherHandle)stopInboxWatcher(inboxWatcherHandle),inboxWatcherHandle=null;if(listenConnection)listenConnection.end().catch(()=>{}),listenConnection=null;if(captureFallbackTimer)clearInterval(captureFallbackTimer),captureFallbackTimer=null;eventRouterHandle?.stop().catch(()=>{}),eventRouterHandle=null,Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)).then((m)=>m.stopFilewatch()).catch(()=>{}),Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill)).then((m)=>m.stopBackfill()).catch(()=>{}),Promise.resolve().then(() => (init_db(),exports_db)).then(({getLockfilePath:getLockfilePath2})=>{try{__require("fs").unlinkSync(getLockfilePath2())}catch{}}).catch(()=>{})},processTriggers=async()=>{try{let claimed=await claimDueTriggers(deps,config,daemonId);if(claimed.length===0)return;if(claimed.length>config.jitterThreshold){let jitterMs=deps.jitter(config.maxJitterMs);deps.log({timestamp:deps.now().toISOString(),level:"info",event:"jitter_applied",count:claimed.length,jitter_ms:jitterMs}),await deps.sleep(jitterMs)}for(let trigger of claimed){if(!running2)break;await fireTrigger(deps,trigger,daemonId)}}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"process_cycle_error",error:message})}};async function setupListenNotify(d,onTrigger){try{let sql=await d.getConnection();return await sql.listen("genie_trigger_due",async()=>{if(!running2)return;await onTrigger()}),d.log({timestamp:d.now().toISOString(),level:"info",event:"listen_started",channel:"genie_trigger_due"}),sql}catch(err){let message=err instanceof Error?err.message:String(err);return d.log({timestamp:d.now().toISOString(),level:"warn",event:"listen_failed",error:message}),null}}function startOrphanTimer(d,cfg){return setInterval(async()=>{if(!running2)return;try{await reconcileOrphans(d,cfg)}catch(err){let message=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"orphan_reconciliation_error",error:message})}},cfg.orphanCheckIntervalMs)}async function startEventRouterSafe(d){try{let handle=await startEventRouter();return d.log({timestamp:d.now().toISOString(),level:"info",event:"event_router_started"}),handle}catch(err){let message=err instanceof Error?err.message:String(err);return d.log({timestamp:d.now().toISOString(),level:"warn",event:"event_router_start_failed",error:message}),null}}async function initSessionCapture(d,cfg){try{let captureSql=await d.getConnection(),{startFilewatch:startFilewatch2}=await Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)),{startBackfill:startBackfill2}=await Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill));if(!await startFilewatch2(captureSql)){let{ingestFileFull:ingestFileFull2,discoverAllJsonlFiles:discoverAllJsonlFiles2,buildWorkerMap:buildWorkerMap2}=await Promise.resolve().then(() => (init_session_capture(),exports_session_capture));d.log({timestamp:d.now().toISOString(),level:"warn",event:"filewatch_failed_fallback_polling"});let timer2=setInterval(async()=>{if(!running2)return;try{let files=await discoverAllJsonlFiles2(),workerMap=await buildWorkerMap2(captureSql);for(let f of files)await ingestFileFull2(captureSql,f.sessionId,f.jsonlPath,f.projectPath,0,{parentSessionId:f.parentSessionId,isSubagent:f.isSubagent,workerMap})}catch{}},cfg.heartbeatIntervalMs);return startBackfill2(captureSql).catch((err)=>{let msg=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"backfill_error",error:msg})}),timer2}return startBackfill2(captureSql).catch((err)=>{let msg=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"backfill_error",error:msg})}),null}catch(err){let message=err instanceof Error?err.message:String(err);return d.log({timestamp:d.now().toISOString(),level:"warn",event:"session_capture_init_failed",error:message}),null}}async function runHeartbeat(d){if(!running2)return;try{await collectHeartbeats(d),await collectMachineSnapshot(d),await emitWorkerEvents(d);try{let retSql=await d.getConnection();await retSql`DELETE FROM heartbeats WHERE created_at < now() - interval '7 days'`,await retSql`DELETE FROM machine_snapshots WHERE created_at < now() - interval '30 days'`,await retSql`DELETE FROM audit_events WHERE entity_type LIKE 'otel_%' AND created_at < now() - interval '30 days'`}catch{}}catch(err){let message=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"heartbeat_error",error:message})}}let done=(async()=>{deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_started",daemon_id:daemonId,max_concurrent:config.maxConcurrent,poll_interval_ms:config.pollIntervalMs});try{await recoverOnStartup(deps,daemonId,config)}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"recovery_error",error:message})}listenConnection=await setupListenNotify(deps,processTriggers),heartbeatTimer=setInterval(()=>runHeartbeat(deps),config.heartbeatIntervalMs),orphanTimer=startOrphanTimer(deps,config),inboxWatcherHandle=startInboxWatcherIfEnabled(deps),eventRouterHandle=await startEventRouterSafe(deps),captureFallbackTimer=await initSessionCapture(deps,config),await processTriggers();while(running2){if(await new Promise((resolve5)=>{pollResolve=resolve5,pollTimeout=setTimeout(resolve5,config.pollIntervalMs)}),pollResolve=null,!running2)break;await processTriggers()}deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_stopped",daemon_id:daemonId})})();return{stop,done,daemonId}}var RESUME_COOLDOWN_MS=60000,DEFAULT_MAX_RESUME_ATTEMPTS=3,previousWorkerStates;var init_scheduler_daemon=__esm(()=>{init_cron();init_event_router();init_inbox_watcher();init_run_spec();previousWorkerStates=new Map});var exports_serve={};__export(exports_serve,{registerServeCommands:()=>registerServeCommands,isTuiSessionReady:()=>isTuiSessionReady,isServeRunning:()=>isServeRunning,ensureTuiSession:()=>ensureTuiSession,autoStartServe:()=>autoStartServe});import{execSync as execSync6,spawn as spawn3}from"child_process";import{existsSync as existsSync24,mkdirSync as mkdirSync8,readFileSync as readFileSync11,unlinkSync as unlinkSync5,writeFileSync as writeFileSync8}from"fs";import{homedir as homedir20}from"os";import{join as join31}from"path";function genieHome(){return process.env.GENIE_HOME??join31(homedir20(),".genie")}function servePidPath(){return join31(genieHome(),"serve.pid")}function genieTmuxConf(){return[join31(genieHome(),"tmux.conf")].find((p)=>existsSync24(p))??"/dev/null"}function readServePid(){let path2=servePidPath();if(!existsSync24(path2))return null;let raw=readFileSync11(path2,"utf-8").trim(),pid=Number.parseInt(raw,10);if(Number.isNaN(pid)||pid<=0)return null;return pid}function writeServePid(pid){mkdirSync8(genieHome(),{recursive:!0}),writeFileSync8(servePidPath(),String(pid),"utf-8")}function removeServePid(){let path2=servePidPath();if(existsSync24(path2))try{unlinkSync5(path2)}catch{}}function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}function genieTmux(subcmd){return`tmux -L ${GENIE_SOCKET} -f ${genieTmuxConf()} ${subcmd}`}function tuiTmux(subcmd){return`tmux ${subcmd}`}function isGenieTmuxRunning(){try{return execSync6(genieTmux("list-sessions"),{stdio:"ignore"}),!0}catch{return!1}}function applyTuiStyle(){let cmds=[`set-option -t ${TUI_SESSION} pane-border-style 'fg=${TUI_STYLE.inactiveBorder}'`,`set-option -t ${TUI_SESSION} pane-active-border-style 'fg=${TUI_STYLE.activeBorder}'`,`set-option -t ${TUI_SESSION} mouse off`,`set-option -t ${TUI_SESSION} status off`,`set-option -t ${TUI_SESSION} pane-border-status off`];for(let cmd of cmds)try{execSync6(tuiTmux(cmd),{stdio:"ignore"})}catch{}}function setupTuiKeybindings(){let bindings=[`bind-key -T ${KEY_TABLE} Tab if-shell "[ '#{pane_index}' = '0' ]" "select-pane -t ${TUI_SESSION}:0.1" "select-pane -t ${TUI_SESSION}:0.0" \\; switch-client -T ${KEY_TABLE}`,`bind-key -T ${KEY_TABLE} C-b if-shell "[ $(tmux display-message -p '#\\{pane_width\\}' -t ${TUI_SESSION}:0.0) -gt 5 ]" "resize-pane -t ${TUI_SESSION}:0.0 -x 0" "resize-pane -t ${TUI_SESSION}:0.0 -x ${NAV_WIDTH}" \\; switch-client -T ${KEY_TABLE}`,`bind-key -T ${KEY_TABLE} C-q detach-client`,`bind-key -T ${KEY_TABLE} 'C-\\\\' detach-client`,`set-hook -t ${TUI_SESSION} client-session-changed "switch-client -T ${KEY_TABLE}"`];for(let cmd of bindings)try{execSync6(tuiTmux(cmd),{stdio:"ignore"})}catch{}}function startTuiTmuxServer(){try{execSync6(tuiTmux(`has-session -t ${TUI_SESSION}`),{stdio:"ignore"});let panes2=execSync6(tuiTmux(`list-panes -t ${TUI_SESSION}:0 -F '#{pane_id}'`),{encoding:"utf-8"}).trim().split(`
|
|
1366
|
-
`);return{leftPane:panes2[0],rightPane:panes2[1]||panes2[0]}}catch{}let cols=120;execSync6(tuiTmux(`new-session -d -s ${TUI_SESSION} -x ${cols} -y ${40}
|
|
1365
|
+
`,deps.log({timestamp:now.toISOString(),level:"debug",event:"machine_snapshot",active_workers:activeWorkers,active_teams:teams.size,tmux_sessions:tmuxSessions,cpu_percent:cpuPercent,memory_mb:memoryMb})}async function emitWorkerEvents(deps){let workers=await deps.listWorkers(),now=deps.now().toISOString(),currentIds=new Set;for(let worker of workers){currentIds.add(worker.id);let prev=previousWorkerStates.get(worker.id),repoPath=worker.repoPath??process.cwd();if(!prev)await deps.publishEvent(`genie.agent.${worker.id}.spawned`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} spawned`,data:{state:worker.state},source:"registry"},repoPath);else if(prev.state!==worker.state){if(await deps.publishEvent(`genie.agent.${worker.id}.state`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} state: ${prev.state} \u2192 ${worker.state}`,data:{previousState:prev.state,state:worker.state},source:"registry"},repoPath),worker.state==="done"&&worker.wishSlug&&worker.groupNumber!=null)await deps.publishEvent(`genie.wish.${worker.wishSlug}.group.${worker.groupNumber}.done`,{timestamp:now,kind:"system",agent:worker.id,team:worker.team,text:`Wish ${worker.wishSlug} group ${worker.groupNumber} completed by ${worker.id}`,data:{wishSlug:worker.wishSlug,groupNumber:worker.groupNumber},source:"registry"},repoPath)}previousWorkerStates.set(worker.id,{...worker})}for(let[id,prev]of previousWorkerStates)if(!currentIds.has(id))await deps.publishEvent(`genie.agent.${id}.killed`,{timestamp:now,kind:"state",agent:id,team:prev.team,text:`Agent ${id} killed`,data:{lastState:prev.state},source:"registry"},prev.repoPath??process.cwd()),previousWorkerStates.delete(id)}function _resetWorkerStatesForTesting(){previousWorkerStates.clear()}function startInboxWatcherIfEnabled(deps){let pollMs=getInboxPollIntervalMs();if(pollMs===0)return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_disabled"}),null;return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_started",poll_interval_ms:pollMs}),startInboxWatcher()}function startDaemon(configOverrides,depsOverrides){let config=resolveConfig(configOverrides),deps={...createDefaultDeps(),...depsOverrides},daemonId=deps.generateId(),running2=!0,pollTimeout=null,pollResolve=null,listenConnection=null,heartbeatTimer=null,orphanTimer=null,inboxWatcherHandle=null,captureFallbackTimer=null,eventRouterHandle=null,stop=()=>{if(running2=!1,pollTimeout)clearTimeout(pollTimeout),pollTimeout=null;if(pollResolve)pollResolve(),pollResolve=null;if(heartbeatTimer)clearInterval(heartbeatTimer),heartbeatTimer=null;if(orphanTimer)clearInterval(orphanTimer),orphanTimer=null;if(inboxWatcherHandle)stopInboxWatcher(inboxWatcherHandle),inboxWatcherHandle=null;if(listenConnection)listenConnection.end().catch(()=>{}),listenConnection=null;if(captureFallbackTimer)clearInterval(captureFallbackTimer),captureFallbackTimer=null;eventRouterHandle?.stop().catch(()=>{}),eventRouterHandle=null,Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)).then((m)=>m.stopFilewatch()).catch(()=>{}),Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill)).then((m)=>m.stopBackfill()).catch(()=>{}),Promise.resolve().then(() => (init_db(),exports_db)).then(({getLockfilePath:getLockfilePath2})=>{try{__require("fs").unlinkSync(getLockfilePath2())}catch{}}).catch(()=>{})},processTriggers=async()=>{try{let claimed=await claimDueTriggers(deps,config,daemonId);if(claimed.length===0)return;if(claimed.length>config.jitterThreshold){let jitterMs=deps.jitter(config.maxJitterMs);deps.log({timestamp:deps.now().toISOString(),level:"info",event:"jitter_applied",count:claimed.length,jitter_ms:jitterMs}),await deps.sleep(jitterMs)}for(let trigger of claimed){if(!running2)break;await fireTrigger(deps,trigger,daemonId)}}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"process_cycle_error",error:message})}};async function setupListenNotify(d,onTrigger){try{let sql=await d.getConnection();return await sql.listen("genie_trigger_due",async()=>{if(!running2)return;await onTrigger()}),d.log({timestamp:d.now().toISOString(),level:"info",event:"listen_started",channel:"genie_trigger_due"}),sql}catch(err){let message=err instanceof Error?err.message:String(err);return d.log({timestamp:d.now().toISOString(),level:"warn",event:"listen_failed",error:message}),null}}function startOrphanTimer(d,cfg){return setInterval(async()=>{if(!running2)return;try{await reconcileOrphans(d,cfg)}catch(err){let message=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"orphan_reconciliation_error",error:message})}},cfg.orphanCheckIntervalMs)}async function startEventRouterSafe(d){try{let handle=await startEventRouter();return d.log({timestamp:d.now().toISOString(),level:"info",event:"event_router_started"}),handle}catch(err){let message=err instanceof Error?err.message:String(err);return d.log({timestamp:d.now().toISOString(),level:"warn",event:"event_router_start_failed",error:message}),null}}async function initSessionCapture(d,cfg){try{let captureSql=await d.getConnection(),{startFilewatch:startFilewatch2}=await Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)),{startBackfill:startBackfill2}=await Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill));if(!await startFilewatch2(captureSql)){let{ingestFileFull:ingestFileFull2,discoverAllJsonlFiles:discoverAllJsonlFiles2,buildWorkerMap:buildWorkerMap2}=await Promise.resolve().then(() => (init_session_capture(),exports_session_capture));d.log({timestamp:d.now().toISOString(),level:"warn",event:"filewatch_failed_fallback_polling"});let timer2=setInterval(async()=>{if(!running2)return;try{let files=await discoverAllJsonlFiles2(),workerMap=await buildWorkerMap2(captureSql);for(let f of files)await ingestFileFull2(captureSql,f.sessionId,f.jsonlPath,f.projectPath,0,{parentSessionId:f.parentSessionId,isSubagent:f.isSubagent,workerMap})}catch{}},cfg.heartbeatIntervalMs);return startBackfill2(captureSql).catch((err)=>{let msg=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"backfill_error",error:msg})}),timer2}return startBackfill2(captureSql).catch((err)=>{let msg=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"backfill_error",error:msg})}),null}catch(err){let message=err instanceof Error?err.message:String(err);return d.log({timestamp:d.now().toISOString(),level:"warn",event:"session_capture_init_failed",error:message}),null}}async function runHeartbeat(d){if(!running2)return;try{await collectHeartbeats(d),await collectMachineSnapshot(d),await emitWorkerEvents(d);try{let retSql=await d.getConnection();await retSql`DELETE FROM heartbeats WHERE created_at < now() - interval '7 days'`,await retSql`DELETE FROM machine_snapshots WHERE created_at < now() - interval '30 days'`,await retSql`DELETE FROM audit_events WHERE entity_type LIKE 'otel_%' AND created_at < now() - interval '30 days'`}catch{}}catch(err){let message=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"heartbeat_error",error:message})}}let done=(async()=>{deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_started",daemon_id:daemonId,max_concurrent:config.maxConcurrent,poll_interval_ms:config.pollIntervalMs});try{await recoverOnStartup(deps,daemonId,config)}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"recovery_error",error:message})}listenConnection=await setupListenNotify(deps,processTriggers),heartbeatTimer=setInterval(()=>runHeartbeat(deps),config.heartbeatIntervalMs),orphanTimer=startOrphanTimer(deps,config),inboxWatcherHandle=startInboxWatcherIfEnabled(deps),eventRouterHandle=await startEventRouterSafe(deps),captureFallbackTimer=await initSessionCapture(deps,config),await processTriggers();while(running2){if(await new Promise((resolve5)=>{pollResolve=resolve5,pollTimeout=setTimeout(resolve5,config.pollIntervalMs)}),pollResolve=null,!running2)break;await processTriggers()}deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_stopped",daemon_id:daemonId})})();return{stop,done,daemonId}}var RESUME_COOLDOWN_MS=60000,DEFAULT_MAX_RESUME_ATTEMPTS=3,previousWorkerStates;var init_scheduler_daemon=__esm(()=>{init_cron();init_event_router();init_inbox_watcher();init_run_spec();previousWorkerStates=new Map});var exports_serve={};__export(exports_serve,{registerServeCommands:()=>registerServeCommands,isTuiSessionReady:()=>isTuiSessionReady,isServeRunning:()=>isServeRunning,ensureTuiSession:()=>ensureTuiSession,autoStartServe:()=>autoStartServe});import{execSync as execSync6,spawn as spawn3}from"child_process";import{existsSync as existsSync24,mkdirSync as mkdirSync8,readFileSync as readFileSync11,unlinkSync as unlinkSync5,writeFileSync as writeFileSync8}from"fs";import{homedir as homedir20}from"os";import{join as join31}from"path";function genieHome(){return process.env.GENIE_HOME??join31(homedir20(),".genie")}function servePidPath(){return join31(genieHome(),"serve.pid")}function genieTmuxConf(){return[join31(genieHome(),"tmux.conf")].find((p)=>existsSync24(p))??"/dev/null"}function readServePid(){let path2=servePidPath();if(!existsSync24(path2))return null;let raw=readFileSync11(path2,"utf-8").trim(),pid=Number.parseInt(raw,10);if(Number.isNaN(pid)||pid<=0)return null;return pid}function writeServePid(pid){mkdirSync8(genieHome(),{recursive:!0}),writeFileSync8(servePidPath(),String(pid),"utf-8")}function removeServePid(){let path2=servePidPath();if(existsSync24(path2))try{unlinkSync5(path2)}catch{}}function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}function genieTmux(subcmd){return`tmux -L ${GENIE_SOCKET} -f ${genieTmuxConf()} ${subcmd}`}function tuiTmuxConf(){return[join31(genieHome(),"tui-tmux.conf")].find((p)=>existsSync24(p))??"/dev/null"}function tuiTmux(subcmd){return`tmux -L genie-tui -f ${tuiTmuxConf()} ${subcmd}`}function isGenieTmuxRunning(){try{return execSync6(genieTmux("list-sessions"),{stdio:"ignore"}),!0}catch{return!1}}function applyTuiStyle(){let cmds=[`set-option -t ${TUI_SESSION} pane-border-style 'fg=${TUI_STYLE.inactiveBorder}'`,`set-option -t ${TUI_SESSION} pane-active-border-style 'fg=${TUI_STYLE.activeBorder}'`,`set-option -t ${TUI_SESSION} mouse on`,`set-option -t ${TUI_SESSION} status off`,`set-option -t ${TUI_SESSION} pane-border-status off`];for(let cmd of cmds)try{execSync6(tuiTmux(cmd),{stdio:"ignore"})}catch{}}function setupTuiKeybindings(){let bindings=[`bind-key -T root Tab if-shell "[ '#{pane_index}' = '0' ]" "select-pane -t ${TUI_SESSION}:0.1" "select-pane -t ${TUI_SESSION}:0.0"`,`bind-key -T root C-b if-shell "[ $(tmux display-message -p '#\\{pane_width\\}' -t ${TUI_SESSION}:0.0) -gt 5 ]" "resize-pane -t ${TUI_SESSION}:0.0 -x 0" "resize-pane -t ${TUI_SESSION}:0.0 -x ${NAV_WIDTH}"`,`bind-key -T root C-t send-keys -t ${TUI_SESSION}:0.1 C-b c`,"bind-key -T root C-q detach-client"];for(let cmd of bindings)try{execSync6(tuiTmux(cmd),{stdio:"ignore"})}catch{}}function startTuiTmuxServer(){try{execSync6(tuiTmux(`has-session -t ${TUI_SESSION}`),{stdio:"ignore"});let panes2=execSync6(tuiTmux(`list-panes -t ${TUI_SESSION}:0 -F '#{pane_id}'`),{encoding:"utf-8"}).trim().split(`
|
|
1366
|
+
`);return{leftPane:panes2[0],rightPane:panes2[1]||panes2[0]}}catch{}let cols=120;execSync6(tuiTmux(`new-session -d -s ${TUI_SESSION} -x ${cols} -y ${40}`),{stdio:"ignore"}),execSync6(tuiTmux(`split-window -h -t ${TUI_SESSION}:0 -l ${cols-NAV_WIDTH-1}`),{stdio:"ignore"});let panes=execSync6(tuiTmux(`list-panes -t ${TUI_SESSION}:0 -F '#{pane_id}'`),{encoding:"utf-8"}).trim().split(`
|
|
1367
1367
|
`);applyTuiStyle(),setupTuiKeybindings();try{execSync6(tuiTmux(`select-pane -t ${panes[0]}`),{stdio:"ignore"})}catch{}return{leftPane:panes[0],rightPane:panes[1]||panes[0]}}function sendTuiLaunchScript(leftPane,rightPane,workspaceRoot){let home=genieHome(),bunPath=process.execPath||"bun",genieBin=process.argv[1]||"genie",scriptPath=join31(home,"tui-launch.sh"),envVars=["GENIE_TUI_PANE=left",`GENIE_TUI_RIGHT=${rightPane}`];if(workspaceRoot)envVars.push(`GENIE_TUI_WORKSPACE=${workspaceRoot}`);let content=`#!/bin/sh
|
|
1368
1368
|
export ${envVars.join(`
|
|
1369
1369
|
export `)}
|
|
1370
1370
|
exec ${bunPath} ${genieBin}
|
|
1371
|
-
`;writeFileSync8(scriptPath,content,{mode:493});try{execSync6(tuiTmux(`send-keys -t '${leftPane}' '${scriptPath}' Enter`),{stdio:"ignore"})}catch{}}function killTuiSession(){try{execSync6(
|
|
1372
|
-
`).filter(Boolean)}catch{return[]}}function isServeRunning(){let pid=readServePid();return pid!==null&&isProcessAlive(pid)}async function autoStartServe(){if(isServeRunning())return;let bunPath=process.execPath??"bun",genieBin=process.argv[1]??"genie",{spawn:spawnChild}=await import("child_process");spawnChild(bunPath,[genieBin,"serve","--foreground"],{detached:!0,stdio:"ignore",env:{...process.env,GENIE_IS_DAEMON:"1"}}).unref();let deadline=Date.now()+15000;while(Date.now()<deadline)if(await new Promise((resolve5)=>setTimeout(resolve5,500)),isServeRunning()&&isTuiSessionReady())return;if(!isServeRunning())throw Error("genie serve failed to start within 15s. Run `genie serve` manually.")}function isTuiSessionReady(){try{return execSync6(`
|
|
1371
|
+
`;writeFileSync8(scriptPath,content,{mode:493});try{execSync6(tuiTmux(`send-keys -t '${leftPane}' '${scriptPath}' Enter`),{stdio:"ignore"})}catch{}}function killTuiSession(){try{execSync6(tuiTmux("kill-server"),{stdio:"ignore"})}catch{}}function listAgentSessions(){try{return execSync6(genieTmux("list-sessions -F '#{session_name}'"),{encoding:"utf-8"}).trim().split(`
|
|
1372
|
+
`).filter(Boolean)}catch{return[]}}function isServeRunning(){let pid=readServePid();return pid!==null&&isProcessAlive(pid)}async function autoStartServe(){if(isServeRunning())return;let bunPath=process.execPath??"bun",genieBin=process.argv[1]??"genie",{spawn:spawnChild}=await import("child_process");spawnChild(bunPath,[genieBin,"serve","--foreground"],{detached:!0,stdio:"ignore",env:{...process.env,GENIE_IS_DAEMON:"1"}}).unref();let deadline=Date.now()+15000;while(Date.now()<deadline)if(await new Promise((resolve5)=>setTimeout(resolve5,500)),isServeRunning()&&isTuiSessionReady())return;if(!isServeRunning())throw Error("genie serve failed to start within 15s. Run `genie serve` manually.")}function isTuiSessionReady(){try{return execSync6(tuiTmux(`has-session -t ${TUI_SESSION}`),{stdio:"ignore"}),!0}catch{return!1}}function ensureTuiSession(workspaceRoot){if(isTuiSessionReady())return;let{leftPane,rightPane}=startTuiTmuxServer();sendTuiLaunchScript(leftPane,rightPane,workspaceRoot)}async function startAgentSync(){try{let{findWorkspace:findWorkspace2}=(init_workspace(),__toCommonJS(exports_workspace)),ws=findWorkspace2();if(!ws)return null;let{syncAgentDirectory:syncAgentDirectory2,watchAgentDirectory:watchAgentDirectory2}=await Promise.resolve().then(() => (init_agent_sync(),exports_agent_sync)),syncResult=await syncAgentDirectory2(ws.root);if(syncResult.registered.length+syncResult.updated.length>0)console.log(` Agent sync: ${syncResult.registered.length} registered, ${syncResult.updated.length} updated`);let watcher2=watchAgentDirectory2(ws.root,{onSync:(name,action)=>{console.log(` [agent-watcher] ${name}: ${action}`)}});if(watcher2)console.log(" Agent watcher started (watching agents/ directory)");return watcher2}catch(err){let msg=err instanceof Error?err.message:String(err);return console.error(` Agent sync failed: ${msg}`),null}}async function startForeground(){let existingPid=readServePid();if(existingPid&&isProcessAlive(existingPid))console.log(`genie serve already running (PID ${existingPid})`),process.exit(0);if(existingPid)removeServePid();process.env.GENIE_IS_DAEMON="1",writeServePid(process.pid),console.log(`genie serve starting (PID ${process.pid})`),console.log(" Starting pgserve...");try{let{ensurePgserve:ensurePgserve2}=await Promise.resolve().then(() => (init_db(),exports_db)),port=await ensurePgserve2();console.log(` pgserve ready on port ${port}`)}catch(err){let msg=err instanceof Error?err.message:String(err);console.error(` pgserve failed: ${msg}`)}let sessions=listAgentSessions();if(sessions.length>0)console.log(` Agent server (-L ${GENIE_SOCKET}): ${sessions.length} sessions`);else console.log(` Agent server (-L ${GENIE_SOCKET}): no sessions yet (created on first spawn)`);handles.agentWatcher=await startAgentSync(),console.log(" Setting up TUI session...");let{leftPane,rightPane}=startTuiTmuxServer(),ws=(()=>{try{let{findWorkspace:findWorkspace2}=(init_workspace(),__toCommonJS(exports_workspace));return findWorkspace2()}catch{return null}})();sendTuiLaunchScript(leftPane,rightPane,ws?.root),console.log(" TUI server ready (session: genie-tui)"),console.log(" Starting scheduler daemon...");try{let{startDaemon:startDaemon2}=await Promise.resolve().then(() => (init_scheduler_daemon(),exports_scheduler_daemon));handles.schedulerHandle=startDaemon2(),console.log(" Scheduler started (includes event-router + inbox-watcher)")}catch(err){let msg=err instanceof Error?err.message:String(err);console.error(` Scheduler failed: ${msg}`)}console.log(`
|
|
1373
1373
|
genie serve is running. Press Ctrl+C to stop.`);let shutdown2=()=>{console.log(`
|
|
1374
|
-
Shutting down genie serve...`),handles.agentWatcher?.close(),handles.schedulerHandle?.stop(),killTuiSession(),removeServePid(),console.log("genie serve stopped.")};if(process.on("SIGTERM",()=>{shutdown2(),process.exit(143)}),process.on("SIGINT",()=>{shutdown2(),process.exit(130)}),handles.schedulerHandle)await handles.schedulerHandle.done;else await new Promise(()=>{});removeServePid()}async function startBackground(){let existingPid=readServePid();if(existingPid&&isProcessAlive(existingPid))console.log(`genie serve already running (PID ${existingPid})`),process.exit(0);if(existingPid)removeServePid();let bunPath=process.execPath??"bun",genieBin=process.argv[1]??"genie",child=spawn3(bunPath,[genieBin,"serve","--foreground"],{detached:!0,stdio:"ignore",env:{...process.env,GENIE_IS_DAEMON:"1"}});if(child.unref(),child.pid)if(await new Promise((resolve5)=>setTimeout(resolve5,1000)),isProcessAlive(child.pid))console.log(`genie serve started (PID ${child.pid})`);else console.error("Error: genie serve exited immediately."),process.exit(1);else console.error("Error: failed to spawn genie serve"),process.exit(1)}async function stopServe(){let pid=readServePid();if(!pid){console.log("genie serve is not running (no PID file).");return}if(!isProcessAlive(pid)){console.log(`Stale PID file (PID ${pid} not running). Cleaning up.`),removeServePid(),killTuiSession();return}console.log(`Stopping genie serve (PID ${pid})...`);try{process.kill(-pid,"SIGTERM")}catch{try{process.kill(pid,"SIGTERM")}catch{}}let deadline=Date.now()+1e4;while(Date.now()<deadline&&isProcessAlive(pid))await new Promise((resolve5)=>setTimeout(resolve5,250));if(isProcessAlive(pid)){console.log("Did not stop within 10s. Sending SIGKILL.");try{process.kill(-pid,"SIGKILL")}catch{try{process.kill(pid,"SIGKILL")}catch{}}}killTuiSession(),removeServePid(),console.log("genie serve stopped.")}async function printPgserveStatus(){try{let{isAvailable:isAvailable2,getActivePort:getActivePort2}=await Promise.resolve().then(() => (init_db(),exports_db)),dbOk=await isAvailable2();console.log(` pgserve: ${dbOk?`healthy (port ${getActivePort2()})`:"unreachable"}`)}catch{console.log(" pgserve: unavailable")}}function printTmuxStatus(){let agentRunning=isGenieTmuxRunning(),sessions=agentRunning?listAgentSessions():[];if(console.log(` tmux -L ${GENIE_SOCKET}: ${agentRunning?`running (${sessions.length} sessions)`:"stopped"}`),sessions.length>0)console.log(` ${sessions.join(", ")}`);let tuiReady=isTuiSessionReady();console.log(`
|
|
1375
|
-
Genie Serve`),console.log("\u2500".repeat(50)),console.log(` Status: ${running2?"running":"stopped"}`),running2&&pid)console.log(` PID: ${pid}`);await printPgserveStatus(),printTmuxStatus(),await printDaemonStatus(running2),console.log(` PID file: ${servePidPath()}`),console.log("")}function registerServeCommands(program2){let serve=program2.command("serve").description("Start all genie infrastructure (pgserve, tmux, scheduler)");serve.command("start",{isDefault:!0}).description("Start genie serve").option("--daemon","Run in background").option("--foreground","Run in foreground (default)").action(async(options)=>{if(options.daemon)await startBackground();else await startForeground()}),serve.command("stop").description("Stop genie serve and all services").action(async()=>{await stopServe()}),serve.command("status").description("Show service health").action(async()=>{await statusServe()})}var GENIE_SOCKET="genie",TUI_SESSION="genie-tui",NAV_WIDTH=30,
|
|
1376
|
-
`);return panes[1]||panes[0]}catch{return rightPane}}}function
|
|
1374
|
+
Shutting down genie serve...`),handles.agentWatcher?.close(),handles.schedulerHandle?.stop(),killTuiSession(),removeServePid(),console.log("genie serve stopped.")};if(process.on("SIGTERM",()=>{shutdown2(),process.exit(143)}),process.on("SIGINT",()=>{shutdown2(),process.exit(130)}),handles.schedulerHandle)await handles.schedulerHandle.done;else await new Promise(()=>{});removeServePid()}async function startBackground(){let existingPid=readServePid();if(existingPid&&isProcessAlive(existingPid))console.log(`genie serve already running (PID ${existingPid})`),process.exit(0);if(existingPid)removeServePid();let bunPath=process.execPath??"bun",genieBin=process.argv[1]??"genie",child=spawn3(bunPath,[genieBin,"serve","--foreground"],{detached:!0,stdio:"ignore",env:{...process.env,GENIE_IS_DAEMON:"1"}});if(child.unref(),child.pid)if(await new Promise((resolve5)=>setTimeout(resolve5,1000)),isProcessAlive(child.pid))console.log(`genie serve started (PID ${child.pid})`);else console.error("Error: genie serve exited immediately."),process.exit(1);else console.error("Error: failed to spawn genie serve"),process.exit(1)}async function stopServe(){let pid=readServePid();if(!pid){console.log("genie serve is not running (no PID file).");return}if(!isProcessAlive(pid)){console.log(`Stale PID file (PID ${pid} not running). Cleaning up.`),removeServePid(),killTuiSession();return}console.log(`Stopping genie serve (PID ${pid})...`);try{process.kill(-pid,"SIGTERM")}catch{try{process.kill(pid,"SIGTERM")}catch{}}let deadline=Date.now()+1e4;while(Date.now()<deadline&&isProcessAlive(pid))await new Promise((resolve5)=>setTimeout(resolve5,250));if(isProcessAlive(pid)){console.log("Did not stop within 10s. Sending SIGKILL.");try{process.kill(-pid,"SIGKILL")}catch{try{process.kill(pid,"SIGKILL")}catch{}}}killTuiSession(),removeServePid(),console.log("genie serve stopped.")}async function printPgserveStatus(){try{let{isAvailable:isAvailable2,getActivePort:getActivePort2}=await Promise.resolve().then(() => (init_db(),exports_db)),dbOk=await isAvailable2();console.log(` pgserve: ${dbOk?`healthy (port ${getActivePort2()})`:"unreachable"}`)}catch{console.log(" pgserve: unavailable")}}function printTmuxStatus(){let agentRunning=isGenieTmuxRunning(),sessions=agentRunning?listAgentSessions():[];if(console.log(` tmux -L ${GENIE_SOCKET}: ${agentRunning?`running (${sessions.length} sessions)`:"stopped"}`),sessions.length>0)console.log(` ${sessions.join(", ")}`);let tuiReady=isTuiSessionReady();console.log(` tmux -L genie-tui: ${tuiReady?"running":"stopped"}`)}async function printDaemonStatus(serveRunning){try{let schedulerPidPath=join31(genieHome(),"scheduler.pid");if(existsSync24(schedulerPidPath)){let sPid=Number.parseInt(readFileSync11(schedulerPidPath,"utf-8").trim(),10),sAlive=!Number.isNaN(sPid)&&isProcessAlive(sPid);console.log(` scheduler: ${sAlive?`running (PID ${sPid})`:"stopped"}`)}else if(serveRunning)console.log(" scheduler: integrated (in-process)");else console.log(" scheduler: stopped")}catch{console.log(" scheduler: unknown")}try{let{getInboxPollIntervalMs:getInboxPollIntervalMs2}=await Promise.resolve().then(() => (init_inbox_watcher(),exports_inbox_watcher)),pollMs=getInboxPollIntervalMs2();if(pollMs===0)console.log(" inbox: disabled");else console.log(` inbox: ${serveRunning?"watching":"stopped"} (poll ${pollMs/1000}s)`)}catch{console.log(" inbox: unavailable")}}async function statusServe(){let pid=readServePid(),running2=pid!==null&&isProcessAlive(pid);if(console.log(`
|
|
1375
|
+
Genie Serve`),console.log("\u2500".repeat(50)),console.log(` Status: ${running2?"running":"stopped"}`),running2&&pid)console.log(` PID: ${pid}`);await printPgserveStatus(),printTmuxStatus(),await printDaemonStatus(running2),console.log(` PID file: ${servePidPath()}`),console.log("")}function registerServeCommands(program2){let serve=program2.command("serve").description("Start all genie infrastructure (pgserve, tmux, scheduler)");serve.command("start",{isDefault:!0}).description("Start genie serve").option("--daemon","Run in background").option("--foreground","Run in foreground (default)").action(async(options)=>{if(options.daemon)await startBackground();else await startForeground()}),serve.command("stop").description("Stop genie serve and all services").action(async()=>{await stopServe()}),serve.command("status").description("Show service health").action(async()=>{await statusServe()})}var GENIE_SOCKET="genie",TUI_SESSION="genie-tui",NAV_WIDTH=30,TUI_STYLE,handles;var init_serve=__esm(()=>{TUI_STYLE={activeBorder:"#7c3aed",inactiveBorder:"#414868"};handles={schedulerHandle:null,agentWatcher:null}});var exports_tmux2={};__export(exports_tmux2,{attachTuiSession:()=>attachTuiSession,attachProjectWindow:()=>attachProjectWindow});import{execSync as execSync7,spawnSync as spawnSync2}from"child_process";function resolveRightPane(rightPane){try{return execSync7(`${TMUX} display-message -t ${rightPane} -p ''`,{stdio:"ignore"}),rightPane}catch{try{let panes=execSync7(`${TMUX} list-panes -t ${SESSION_NAME}:0 -F '#{pane_id}'`,{encoding:"utf-8"}).trim().split(`
|
|
1376
|
+
`);return panes[1]||panes[0]}catch{return rightPane}}}function attachProjectWindow(rightPane,targetSession,windowIndex){if(targetSession===SESSION_NAME)return;let pane=resolveRightPane(rightPane),agentTmux=`tmux -L ${GENIE_AGENT_SOCKET}`;try{execSync7(`${agentTmux} has-session -t '${targetSession}' 2>/dev/null`,{stdio:"ignore"})}catch{return}if(windowIndex!==void 0)try{execSync7(`${agentTmux} select-window -t '${targetSession}:${windowIndex}'`,{stdio:"ignore"})}catch{}try{execSync7(`${agentTmux} set-option -t '${targetSession}' status off 2>/dev/null`,{stdio:"ignore"})}catch{}try{let cmd=`while true; do TMUX='' ${agentTmux} attach-session -t '${targetSession}' 2>/dev/null; sleep 0.3; done`;execSync7(`${TMUX} respawn-pane -k -t ${pane} "bash -c '${cmd}'"`,{stdio:"ignore"})}catch{}try{execSync7(`${TMUX} select-pane -t ${SESSION_NAME}:0.0`,{stdio:"ignore"})}catch{}}function attachTuiSession(){spawnSync2("tmux",["-L",TMUX_SOCKET,"-f",TUI_TMUX_CONF,"attach-session","-t",SESSION_NAME],{stdio:"inherit"})}var SESSION_NAME="genie-tui",TMUX_SOCKET="genie-tui",GENIE_AGENT_SOCKET="genie",TUI_TMUX_CONF,TMUX;var init_tmux2=__esm(()=>{TUI_TMUX_CONF=(()=>{let{existsSync:existsSync25}=__require("fs"),tuiConf=`${process.env.GENIE_HOME??`${process.env.HOME}/.genie`}/tui-tmux.conf`;return existsSync25(tuiConf)?tuiConf:"/dev/null"})(),TMUX=`tmux -L ${TMUX_SOCKET} -f ${TUI_TMUX_CONF}`});function onBridgeEvent(handler){return listeners.add(handler),()=>{listeners.delete(handler)}}function emit2(event){for(let handler of listeners)handler(event)}async function listAgents2(){return(await getConnection())`
|
|
1377
1377
|
SELECT a.id, a.custom_name, a.role, a.team, a.title, a.state,
|
|
1378
1378
|
a.reports_to, a.current_executor_id, a.started_at
|
|
1379
1379
|
FROM agents a
|
|
@@ -2071,7 +2071,7 @@ $ bun add react-devtools-core@7 -d
|
|
|
2071
2071
|
AND a.ended_at IS NULL
|
|
2072
2072
|
ORDER BY a.started_at DESC
|
|
2073
2073
|
`).map(mapAssignment)}function mapExecutor(row){let meta=row.metadata;return{id:String(row.id),agentId:String(row.agent_id),agentName:row.agent_name?String(row.agent_name):null,provider:String(row.provider),transport:String(row.transport),pid:row.pid!=null?Number(row.pid):null,tmuxSession:row.tmux_session?String(row.tmux_session):null,tmuxPaneId:row.tmux_pane_id?String(row.tmux_pane_id):null,state:String(row.state),metadata:typeof meta==="string"?JSON.parse(meta):meta??{},startedAt:row.started_at instanceof Date?row.started_at.toISOString():String(row.started_at),role:row.role?String(row.role):null,team:row.team?String(row.team):null}}function mapAssignment(row){return{id:String(row.id),executorId:String(row.executor_id),taskId:row.task_id?String(row.task_id):null,taskTitle:row.task_title?String(row.task_title):null,wishSlug:row.wish_slug?String(row.wish_slug):null,groupNumber:row.group_number!=null?Number(row.group_number):null,startedAt:row.started_at instanceof Date?row.started_at.toISOString():String(row.started_at)}}import{execSync as execSync16}from"child_process";function execQuiet(cmd){try{return execSync16(cmd,{encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim()}catch{return""}}function parsePaneLine(parts){let[sessionName,winIdxStr,winName,winActive,winPanes,paneIdxStr,paneId,panePidStr,paneCmd,paneTitle,paneSize,sessAttached,sessWindows,sessCreated,paneDead]=parts;return{sessionName,winIdxStr,session:{name:sessionName,attached:sessAttached==="1",windowCount:Number.parseInt(sessWindows,10)||0,created:Number.parseInt(sessCreated,10)||0},window:{sessionName,index:Number.parseInt(winIdxStr,10)||0,name:winName,active:winActive==="1",paneCount:Number.parseInt(winPanes,10)||0},pane:{sessionName,windowIndex:Number.parseInt(winIdxStr,10)||0,paneIndex:Number.parseInt(paneIdxStr,10)||0,paneId,pid:Number.parseInt(panePidStr,10)||0,command:paneCmd,title:paneTitle,size:paneSize,isDead:paneDead==="1"}}}function getTmuxInventory(){let paneOutput=execQuiet("tmux -L genie list-panes -a -F '#{session_name}|#{window_index}|#{window_name}|#{window_active}|#{window_panes}|#{pane_index}|#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_title}|#{pane_width}x#{pane_height}|#{session_attached}|#{session_windows}|#{session_created}|#{pane_dead}'");if(!paneOutput)return[];let sessionMap=new Map,windowMap=new Map;for(let line of paneOutput.split(`
|
|
2074
|
-
`)){if(!line)continue;let parts=line.split("|");if(parts.length<15)continue;let parsed=parsePaneLine(parts);if(!sessionMap.has(parsed.sessionName))sessionMap.set(parsed.sessionName,{...parsed.session,windows:[]});let winKey=`${parsed.sessionName}:${parsed.winIdxStr}`;if(!windowMap.has(winKey)){let win={...parsed.window,panes:[]};windowMap.set(winKey,win),sessionMap.get(parsed.sessionName)?.windows.push(win)}windowMap.get(winKey)?.panes.push(parsed.pane)}return Array.from(sessionMap.values()).sort((a,b3)=>a.name.localeCompare(b3.name))}function isPidAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}function allClaudePanes(sessions2){return sessions2.flatMap((s)=>s.windows.flatMap((w2)=>w2.panes)).filter((p)=>p.command==="claude"||p.title.includes("claude"))}function detectGaps(executors,sessions2){let deadPidExecutors=executors.filter((e)=>e.pid!=null&&!isPidAlive(e.pid)),executorPaneIds=new Set(executors.map((e)=>e.tmuxPaneId).filter(Boolean)),claudePanes=allClaudePanes(sessions2),orphanPanes=claudePanes.filter((p)=>!executorPaneIds.has(p.paneId)),linkedCount=executors.filter((e)=>e.tmuxPaneId&&!deadPidExecutors.some((d2)=>d2.id===e.id)).length,deadPaneCount=sessions2.flatMap((s)=>s.windows.flatMap((w2)=>w2.panes)).filter((p)=>p.isDead).length;return{deadPidExecutors,orphanPanes,linkedCount,totalExecutors:executors.length,totalClaudePanes:claudePanes.length,deadPaneCount}}async function collectDiagnostics(){let{loadExecutors:loadExecutors2,loadAssignments:loadAssignments2}=await Promise.resolve().then(() => exports_db2),sessions2=getTmuxInventory(),executors=await loadExecutors2(),executorIds=executors.map((e)=>e.id),assignments=await loadAssignments2(executorIds),gaps=detectGaps(executors,sessions2);return{sessions:sessions2,executors,assignments,gaps,timestamp:Date.now()}}var init_diagnostics=()=>{};function buildSessionTree(snapshot){let executorByPaneId=new Map;for(let exec3 of snapshot.executors)if(exec3.tmuxPaneId)executorByPaneId.set(exec3.tmuxPaneId,exec3);return snapshot.sessions.filter((s)=>s.name!=="genie-tui").map((session)=>sessionToNode(session,executorByPaneId))}function buildWorkspaceTree(input){let{agentNames,sessions:sessions2,executors}=input,sessionByName=new Map;for(let s of sessions2)if(s.name!=="genie-tui")sessionByName.set(s.name,s);let executorByPaneId=new Map;for(let exec3 of executors)if(exec3.tmuxPaneId)executorByPaneId.set(exec3.tmuxPaneId,exec3);let executorsByAgent=new Map;for(let exec3 of executors){let name=exec3.agentName??exec3.metadata?.agentName;if(typeof name==="string"){let list2=executorsByAgent.get(name)??[];list2.push(exec3),executorsByAgent.set(name,list2)}}let nodes=agentNames.map((name)=>buildAgentNode(name,sessionByName.get(name),executorsByAgent.get(name)??[],executorByPaneId)),agentNameSet=new Set(agentNames);for(let[name,session]of sessionByName)if(!agentNameSet.has(name))nodes.push(sessionToNode(session,executorByPaneId));return nodes}function countClaudePanes(session){return session.windows.reduce((sum,w2)=>sum+w2.panes.filter((p)=>p.command==="claude"||p.title.includes("claude")).length,0)}function buildAgentNode(name,session,agentExecutors,executorByPaneId){let wsState=deriveWsAgentState(session,agentExecutors),children=[];if(session)for(let win of session.windows){if(win.index===0)continue;children.push(windowToNode(session.name,win,executorByPaneId))}return{id:`agent:${name}`,type:"agent",label:name,depth:0,expanded:children.length>0,children,data:{sessionName:name,windowCount:session?session.windows.length:0},activePanes:session?countClaudePanes(session):0,agentState:agentExecutors.length>0?deriveExecutorState(agentExecutors):void 0,wsAgentState:wsState}}function deriveWsAgentState(session,agentExecutors){if(!session)return"stopped";for(let exec3 of agentExecutors){if(exec3.state==="error"||exec3.state==="terminated")return"error";if(exec3.state==="spawning")return"spawning"}return"running"}function deriveExecutorState(execs){for(let e of execs)if(e.state==="working")return"working";for(let e of execs)if(e.state==="permission")return"permission";for(let e of execs)if(e.state==="error"||e.state==="terminated")return"error";return"idle"}function sessionToNode(session,executorMap){let claudePanes=session.windows.reduce((sum,w2)=>sum+w2.panes.filter((p)=>p.command==="claude"||p.title.includes("claude")).length,0);return{id:`session:${session.name}`,type:"session",label:session.name,depth:0,expanded:!0,children:session.windows.map((w2)=>windowToNode(session.name,w2,executorMap)),data:{attached:session.attached,windowCount:session.windowCount},activePanes:claudePanes,agentState:void 0,wsAgentState:void 0}}function windowToNode(sessionName,window2,executorMap){let activePanes=window2.panes.filter((p)=>!p.isDead&&(p.command==="claude"||p.title.includes("claude"))).length;return{id:`window:${sessionName}:${window2.index}`,type:"window",label:window2.name,depth:1,expanded:!0,children:window2.panes.map((p)=>paneToNode(sessionName,window2.index,p,executorMap)),data:{active:window2.active,paneCount:window2.paneCount},activePanes,agentState:void 0,wsAgentState:void 0}}function paneToNode(sessionName,windowIndex,pane,executorMap){let executor=executorMap.get(pane.paneId),isClaude=pane.command==="claude"||pane.title.includes("claude");return{id:`pane:${pane.paneId}`,type:"pane",label:derivePaneLabel(pane,executor,isClaude),depth:2,expanded:!1,children:[],data:{command:pane.command,isDead:pane.isDead,pid:pane.pid,size:pane.size,paneId:pane.paneId,sessionName,windowIndex},activePanes:0,agentState:derivePaneState(pane,executor),wsAgentState:void 0}}function derivePaneLabel(pane,executor,isClaude){if(executor?.agentName&&executor?.team)return`${executor.team}/${executor.agentName}`;if(executor?.agentName)return executor.agentName;if(isClaude)return"claude";return pane.command}function derivePaneState(pane,executor){if(pane.isDead)return"error";if(!executor)return;let s=executor.state;if(s==="working")return"working";if(s==="idle"||s==="spawning")return"idle";if(s==="permission")return"permission";if(s==="error"||s==="terminated")return"error";return}function getSessionTarget(node){if(node.type==="agent")return{sessionName:node.data.sessionName};if(node.type==="session")return{sessionName:node.label};if(node.type==="window"){let parts=node.id.split(":");return{sessionName:parts[1],windowIndex:Number(parts[2])}}if(node.type==="pane"){let data=node.data;return{sessionName:data.sessionName,windowIndex:data.windowIndex}}return null}var palette,icons;var init_theme2=__esm(()=>{palette={purple:"#a855f7",violet:"#7c3aed",cyan:"#22d3ee",emerald:"#34d399",bg:"#1a1028",bgLight:"#241838",bgLighter:"#2e2048",text:"#e2e8f0",textDim:"#94a3b8",textMuted:"#64748b",border:"#414868",borderActive:"#7c3aed",scrollTrack:"#414868",scrollThumb:"#7aa2f7",active:"#22d3ee",success:"#34d399",warning:"#fbbf24",error:"#f87171",idle:"#94a3b8"},icons={org:"\u25C6",project:"\u25B8",projectOpen:"\u25BE",board:"\u2261",boardOpen:"\u2261",column:"\u2502",task:"\u25CB",taskActive:"\u25CF",taskDone:"\u2713",agent:"\u25B6",collapsed:"\u25B8",expanded:"\u25BE"}});function flattenTree(nodes){let result=[];function walk(node,depth){if(result.push({node,depth,visible:!0}),node.expanded)for(let child of node.children)walk(child,depth+1)}for(let node of nodes)walk(node,0);return result}function toggleNode(nodes,id){return nodes.map((node)=>{if(node.id===id)return{...node,expanded:!node.expanded};return{...node,children:toggleNode(node.children,id)}})}var import_jsx_dev_runtime2;var init_jsx_dev_runtime=__esm(()=>{import_jsx_dev_runtime2=__toESM(require_react_jsx_dev_runtime_development(),1)});function getNodeIcon(node){if(node.type==="agent")return getAgentIcon(node);switch(node.type){case"session":return node.data.attached?"\u25B6":"\u25B8";case"window":return node.data.active?"\u25A0":"\u25A1";case"pane":return getPaneIcon(node);default:return" "}}function getAgentIcon(node){switch(node.wsAgentState){case"running":return"\u25CF";case"stopped":return"\u25CB";case"error":return"\u2298";case"spawning":return"\u231B";default:return"\u25CB"}}function getPaneIcon(node){if(node.data.isDead)return"\u2718";if(node.agentState==="working")return"\u25CF";if(node.agentState==="idle")return"\u25CB";if(node.agentState==="permission")return"\u26A0";if(node.agentState==="error")return"\u2718";if(node.data.command==="claude")return"\u25C6";return"\u25CB"}function getNodeColor(node){if(node.type==="agent")return getAgentColor(node);switch(node.type){case"session":return node.data.attached?palette.emerald:palette.textDim;case"window":return node.data.active?palette.cyan:palette.text;case"pane":return getPaneColor(node);default:return palette.text}}function getAgentColor(node){switch(node.wsAgentState){case"running":return palette.emerald;case"stopped":return palette.textDim;case"error":return palette.error;case"spawning":return palette.warning;default:return palette.textDim}}function getPaneColor(node){if(node.data.isDead)return palette.error;if(node.agentState==="working")return palette.cyan;if(node.agentState==="permission")return palette.warning;if(node.agentState==="error")return palette.error;if(node.agentState==="idle")return palette.textDim;if(node.data.command==="claude")return palette.cyan;return palette.textDim}function getNodeSuffix(node){if(node.type==="agent"){let wc=node.data.windowCount;if(wc>1)return` (${wc} windows)`;if(wc===1)return" (1 window)";return""}if(node.type==="session"||node.type==="pane"){let count=node.activePanes;if(count>0)return` ${icons.agent}${count}`}return""}function getStateColor(state){switch(state){case"working":return palette.cyan;case"idle":return palette.textDim;case"permission":return palette.warning;case"error":return palette.error;default:return palette.textMuted}}var import_react13,TreeNodeRow;var init_TreeNode=__esm(()=>{init_theme2();init_jsx_dev_runtime();import_react13=__toESM(require_react_development(),1),TreeNodeRow=import_react13.memo(function({node,selected,onSelect,onToggle}){let indent=" ".repeat(node.depth),hasChildren=node.children.length>0,expandIcon=hasChildren?node.expanded?icons.expanded:icons.collapsed:" ",icon=getNodeIcon(node),color=getNodeColor(node),suffix=getNodeSuffix(node);return import_jsx_dev_runtime2.jsxDEV("box",{height:1,width:"100%",backgroundColor:selected?palette.violet:void 0,onMouseDown:()=>{if(onSelect(node.id),hasChildren)onToggle(node.id)},children:import_jsx_dev_runtime2.jsxDEV("text",{children:[import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:[indent,expandIcon," "]},void 0,!0,void 0,this),import_jsx_dev_runtime2.jsxDEV("span",{fg:color,children:[icon," "]},void 0,!0,void 0,this),import_jsx_dev_runtime2.jsxDEV("span",{fg:selected?"#ffffff":palette.text,children:node.label},void 0,!1,void 0,this),suffix?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:suffix},void 0,!1,void 0,this):null,node.agentState?import_jsx_dev_runtime2.jsxDEV("span",{fg:getStateColor(node.agentState),children:[" ",node.agentState]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)},void 0,!1,void 0,this)})});function Nav({onTmuxSessionSelect,workspaceRoot,initialAgent}){let[diagnostics,setDiagnostics]=import_react15.useState(null),[sessionTree,setSessionTree]=import_react15.useState([]),[selectedIndex,setSelectedIndex]=import_react15.useState(0),lastTarget=import_react15.useRef(null),genieHome3=import_react15.useRef(process.env.GENIE_HOME??`${process.env.HOME}/.genie`);import_react15.useEffect(()=>{let active=!0;async function refresh(){try{let snap=await collectDiagnostics();if(active)setDiagnostics(snap)}catch(err){console.error("TUI: diagnostics failed:",err)}}refresh();let timer2=setInterval(refresh,2000);return()=>{active=!1,clearInterval(timer2)}},[]),import_react15.useEffect(()=>{if(!diagnostics)return;let newTree;if(workspaceRoot){let agentNames=scanAgents2(workspaceRoot);newTree=buildWorkspaceTree({agentNames,sessions:diagnostics.sessions,executors:diagnostics.executors})}else newTree=buildSessionTree(diagnostics);setSessionTree((prev)=>mergeExpandedState(prev,newTree))},[diagnostics,workspaceRoot]);let flatNodes=import_react15.useMemo(()=>flattenTree(sessionTree),[sessionTree]);import_react15.useEffect(()=>{if(flatNodes.length>0&&selectedIndex>=flatNodes.length)setSelectedIndex(flatNodes.length-1)},[flatNodes.length,selectedIndex]);let[pendingAgent,setPendingAgent]=import_react15.useState(initialAgent);import_react15.useEffect(()=>{if(!diagnostics)return;try{let fs3=__require("fs"),agentFile=`${genieHome3.current}/tui-initial-agent`;if(fs3.existsSync(agentFile)){let agent=fs3.readFileSync(agentFile,"utf-8").trim();if(fs3.unlinkSync(agentFile),agent)setPendingAgent(agent)}}catch{}},[diagnostics]),import_react15.useEffect(()=>{if(!pendingAgent||flatNodes.length===0)return;let idx=flatNodes.findIndex((n)=>n.node.id===`agent:${pendingAgent}`);if(idx>=0)setSelectedIndex(idx),setPendingAgent(void 0)},[pendingAgent,flatNodes]),import_react15.useEffect(()=>{let current=flatNodes[selectedIndex]?.node;if(!current)return;let target=getSessionTarget(current);if(!target)return;let key=`${target.sessionName}:${target.windowIndex??""}`;if(key===lastTarget.current)return;if(lastTarget.current=key,current.type==="agent"&¤t.wsAgentState!=="running")return;onTmuxSessionSelect(target.sessionName,target.windowIndex)},[selectedIndex,flatNodes,onTmuxSessionSelect]);let handleSelect=import_react15.useCallback((id)=>{let idx=flatNodes.findIndex((n)=>n.node.id===id);if(idx>=0)setSelectedIndex(idx)},[flatNodes]),handleToggle=import_react15.useCallback((id)=>{setSessionTree((prev)=>toggleNode(prev,id))},[]),handleVerticalNav=import_react15.useCallback((keyName2)=>{let rowCount=flatNodes.length;if(rowCount===0)return;if(keyName2==="up"||keyName2==="k")setSelectedIndex((prev)=>prev===0?rowCount-1:prev-1);else if(keyName2==="down"||keyName2==="j")setSelectedIndex((prev)=>prev>=rowCount-1?0:prev+1)},[flatNodes.length]),handleExpandCollapse=import_react15.useCallback((keyName2)=>{let node=flatNodes[selectedIndex]?.node;if(!node)return;if((keyName2==="right"||keyName2==="l")&&node.children.length>0&&!node.expanded)handleToggle(node.id);else if((keyName2==="left"||keyName2==="h")&&node.expanded)handleToggle(node.id)},[flatNodes,selectedIndex,handleToggle]),handleEnter=import_react15.useCallback(()=>{let node=flatNodes[selectedIndex]?.node;if(!node)return;if(node.type==="agent"){if(node.wsAgentState!=="running")spawnAgent(node.label);let target2=getSessionTarget(node);if(target2)onTmuxSessionSelect(target2.sessionName,target2.windowIndex);return}if(node.children.length>0)handleToggle(node.id);let target=getSessionTarget(node);if(target)onTmuxSessionSelect(target.sessionName,target.windowIndex)},[flatNodes,selectedIndex,handleToggle,onTmuxSessionSelect]);useKeyboard((key)=>{if(key.name==="up"||key.name==="k"||key.name==="down"||key.name==="j")handleVerticalNav(key.name);else if(key.name==="right"||key.name==="l"||key.name==="left"||key.name==="h")handleExpandCollapse(key.name);else if(key.name==="enter"||key.name==="return")handleEnter()});let agentCount=workspaceRoot?sessionTree.filter((n)=>n.type==="agent").length:diagnostics?.sessions.length??0,runningCount=workspaceRoot?sessionTree.filter((n)=>n.wsAgentState==="running").length:diagnostics?.sessions.reduce((sum,s)=>sum+s.windows.reduce((ws,w2)=>ws+w2.panes.length,0),0)??0,headerLabel=workspaceRoot?"Agents":"Sessions";return import_jsx_dev_runtime2.jsxDEV("box",{flexDirection:"column",width:"100%",height:"100%",backgroundColor:palette.bg,children:[import_jsx_dev_runtime2.jsxDEV("box",{height:1,paddingX:1,backgroundColor:palette.bgLight,children:import_jsx_dev_runtime2.jsxDEV("text",{children:[import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.purple,children:headerLabel},void 0,!1,void 0,this),diagnostics?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:[" ",workspaceRoot?`${runningCount}/${agentCount}`:`${agentCount}s ${runningCount}p`]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)},void 0,!1,void 0,this),diagnostics?import_jsx_dev_runtime2.jsxDEV("scrollbox",{focused:!0,height:"100%",style:{scrollbarOptions:{showArrows:!1,trackOptions:{foregroundColor:palette.scrollThumb,backgroundColor:palette.scrollTrack}}},children:flatNodes.map((flat,i2)=>import_jsx_dev_runtime2.jsxDEV(TreeNodeRow,{node:flat.node,selected:i2===selectedIndex,onSelect:handleSelect,onToggle:handleToggle},flat.node.id,!1,void 0,this))},void 0,!1,void 0,this):import_jsx_dev_runtime2.jsxDEV("box",{flexGrow:1,justifyContent:"center",alignItems:"center",children:import_jsx_dev_runtime2.jsxDEV("text",{fg:palette.textDim,children:"Collecting..."},void 0,!1,void 0,this)},void 0,!1,void 0,this),import_jsx_dev_runtime2.jsxDEV("box",{height:1,paddingX:1,backgroundColor:palette.bgLight,children:import_jsx_dev_runtime2.jsxDEV("text",{children:import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textMuted,children:["\u2191\u2193",":nav ","\u2190\u2192",":expand Enter:",workspaceRoot?"spawn/attach":"attach"]},void 0,!0,void 0,this)},void 0,!1,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function spawnAgent(name){try{let{spawn:spawn4}=__require("child_process"),{join:join53,resolve:resolve10}=__require("path"),{existsSync:existsSync40}=__require("fs"),wsRoot=process.env.GENIE_TUI_WORKSPACE,cwd;if(wsRoot){let agentDir=resolve10(join53(wsRoot,"agents",name));if(existsSync40(agentDir))cwd=agentDir}spawn4("genie",["spawn",name],{detached:!0,stdio:"ignore",cwd}).unref()}catch{}}function mergeExpandedState(oldTree,newTree){if(oldTree.length===0)return newTree;let oldState=new Map;function collect(nodes){for(let n of nodes)oldState.set(n.id,n.expanded),collect(n.children)}collect(oldTree);function apply(nodes){return nodes.map((n)=>({...n,expanded:oldState.has(n.id)?oldState.get(n.id):n.expanded,children:apply(n.children)}))}return apply(newTree)}var import_react15;var init_Nav=__esm(async()=>{init_workspace();init_diagnostics();init_theme2();init_TreeNode();init_jsx_dev_runtime();await init_react();import_react15=__toESM(require_react_development(),1)});function App({rightPane,workspaceRoot,initialAgent}){let handleTmuxSessionSelect=import_react16.useCallback((sessionName,windowIndex)=>{if(!rightPane)return;attachProjectWindow(rightPane,sessionName,windowIndex)},[rightPane]);return import_jsx_dev_runtime2.jsxDEV(Nav,{onTmuxSessionSelect:handleTmuxSessionSelect,workspaceRoot,initialAgent},void 0,!1,void 0,this)}var import_react16;var init_app=__esm(async()=>{init_tmux2();init_jsx_dev_runtime();await init_Nav();import_react16=__toESM(require_react_development(),1)});var exports_render={};__export(exports_render,{renderNav:()=>renderNav});async function renderNav(){let rightPane=process.env.GENIE_TUI_RIGHT||void 0,workspaceRoot=process.env.GENIE_TUI_WORKSPACE||void 0,initialAgent=process.env.GENIE_TUI_AGENT||void 0,renderer=await createCliRenderer({exitOnCtrlC:!1,useMouse:!0});createRoot(renderer).render(import_jsx_dev_runtime2.jsxDEV(App,{rightPane,workspaceRoot,initialAgent},void 0,!1,void 0,this)),await new Promise((resolve10)=>{renderer.once("destroy",resolve10)})}var init_render=__esm(async()=>{init_jsx_dev_runtime();await __promiseAll([init_core(),init_react(),init_app()])});var exports_tui={};__export(exports_tui,{launchTui:()=>launchTui});async function launchTui(){let{renderNav:renderNav2}=await init_render().then(() => exports_render);await renderNav2()}var import__=__toESM(require_commander(),1),{program,createCommand,createArgument,createOption,CommanderError,InvalidArgumentError,InvalidOptionArgumentError,Command,Argument,Option,Help}=import__.default;import{existsSync as existsSync3,unlinkSync as unlinkSync2}from"fs";import{homedir as homedir3}from"os";import{join as join3}from"path";var{$:$2}=globalThis.Bun;import{existsSync,unlinkSync}from"fs";import{homedir}from"os";import{join}from"path";var CLAUDE_DIR=join(homedir(),".claude"),CLAUDE_HOOKS_DIR=join(CLAUDE_DIR,"hooks"),CLAUDE_SETTINGS_FILE=join(CLAUDE_DIR,"settings.json"),GENIE_HOOK_SCRIPT_NAME="genie-bash-hook.sh";function getClaudeSettingsPath(){return CLAUDE_SETTINGS_FILE}function getGenieHookScriptPath(){return join(CLAUDE_HOOKS_DIR,GENIE_HOOK_SCRIPT_NAME)}function hookScriptExists(){return existsSync(getGenieHookScriptPath())}function removeHookScript(){let scriptPath=getGenieHookScriptPath();if(existsSync(scriptPath))unlinkSync(scriptPath)}function contractClaudePath(path){let home=homedir();if(path.startsWith(`${home}/`))return`~${path.slice(home.length)}`;if(path===home)return"~";return path}init_genie_config2();var{$}=globalThis.Bun;async function checkCommand(cmd){try{let cmdPath=(await $`which ${cmd}`.quiet().text()).trim();if(!cmdPath)return{exists:!1};let version;try{let firstLine=(await $`${cmd} --version`.quiet().text()).split(`
|
|
2074
|
+
`)){if(!line)continue;let parts=line.split("|");if(parts.length<15)continue;let parsed=parsePaneLine(parts);if(!sessionMap.has(parsed.sessionName))sessionMap.set(parsed.sessionName,{...parsed.session,windows:[]});let winKey=`${parsed.sessionName}:${parsed.winIdxStr}`;if(!windowMap.has(winKey)){let win={...parsed.window,panes:[]};windowMap.set(winKey,win),sessionMap.get(parsed.sessionName)?.windows.push(win)}windowMap.get(winKey)?.panes.push(parsed.pane)}return Array.from(sessionMap.values()).sort((a,b3)=>a.name.localeCompare(b3.name))}function isPidAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}function allClaudePanes(sessions2){return sessions2.flatMap((s)=>s.windows.flatMap((w2)=>w2.panes)).filter((p)=>p.command==="claude"||p.title.includes("claude"))}function detectGaps(executors,sessions2){let deadPidExecutors=executors.filter((e)=>e.pid!=null&&!isPidAlive(e.pid)),executorPaneIds=new Set(executors.map((e)=>e.tmuxPaneId).filter(Boolean)),claudePanes=allClaudePanes(sessions2),orphanPanes=claudePanes.filter((p)=>!executorPaneIds.has(p.paneId)),linkedCount=executors.filter((e)=>e.tmuxPaneId&&!deadPidExecutors.some((d2)=>d2.id===e.id)).length,deadPaneCount=sessions2.flatMap((s)=>s.windows.flatMap((w2)=>w2.panes)).filter((p)=>p.isDead).length;return{deadPidExecutors,orphanPanes,linkedCount,totalExecutors:executors.length,totalClaudePanes:claudePanes.length,deadPaneCount}}async function collectDiagnostics(){let{loadExecutors:loadExecutors2,loadAssignments:loadAssignments2}=await Promise.resolve().then(() => exports_db2),sessions2=getTmuxInventory(),executors=await loadExecutors2(),executorIds=executors.map((e)=>e.id),assignments=await loadAssignments2(executorIds),gaps=detectGaps(executors,sessions2);return{sessions:sessions2,executors,assignments,gaps,timestamp:Date.now()}}var init_diagnostics=()=>{};function buildSessionTree(snapshot){let executorByPaneId=new Map;for(let exec3 of snapshot.executors)if(exec3.tmuxPaneId)executorByPaneId.set(exec3.tmuxPaneId,exec3);return snapshot.sessions.filter((s)=>s.name!=="genie-tui").map((session)=>sessionToNode(session,executorByPaneId))}function buildWorkspaceTree(input){let{agentNames,sessions:sessions2,executors}=input,sessionByName=new Map;for(let s of sessions2)if(s.name!=="genie-tui")sessionByName.set(s.name,s);let executorByPaneId=new Map;for(let exec3 of executors)if(exec3.tmuxPaneId)executorByPaneId.set(exec3.tmuxPaneId,exec3);let executorsByAgent=new Map;for(let exec3 of executors){let name=exec3.agentName??exec3.metadata?.agentName;if(typeof name==="string"){let list2=executorsByAgent.get(name)??[];list2.push(exec3),executorsByAgent.set(name,list2)}}let nodes=agentNames.map((name)=>buildAgentNode(name,sessionByName.get(name),executorsByAgent.get(name)??[],executorByPaneId)),agentNameSet=new Set(agentNames);for(let[name,session]of sessionByName)if(!agentNameSet.has(name))nodes.push(sessionToNode(session,executorByPaneId));return nodes}function countClaudePanes(session){return session.windows.reduce((sum,w2)=>sum+w2.panes.filter((p)=>p.command==="claude"||p.title.includes("claude")).length,0)}function buildAgentNode(name,session,agentExecutors,executorByPaneId){let wsState=deriveWsAgentState(session,agentExecutors),children=[];if(session)for(let win of session.windows){if(win.index===0)continue;children.push(windowToNode(session.name,win,executorByPaneId))}return{id:`agent:${name}`,type:"agent",label:name,depth:0,expanded:children.length>0,children,data:{sessionName:name,windowCount:session?session.windows.length:0},activePanes:session?countClaudePanes(session):0,agentState:agentExecutors.length>0?deriveExecutorState(agentExecutors):void 0,wsAgentState:wsState}}function deriveWsAgentState(session,agentExecutors){if(!session)return"stopped";for(let exec3 of agentExecutors){if(exec3.state==="error"||exec3.state==="terminated")return"error";if(exec3.state==="spawning")return"spawning"}return"running"}function deriveExecutorState(execs){for(let e of execs)if(e.state==="working")return"working";for(let e of execs)if(e.state==="permission")return"permission";for(let e of execs)if(e.state==="error"||e.state==="terminated")return"error";return"idle"}function sessionToNode(session,executorMap){let claudePanes=session.windows.reduce((sum,w2)=>sum+w2.panes.filter((p)=>p.command==="claude"||p.title.includes("claude")).length,0);return{id:`session:${session.name}`,type:"session",label:session.name,depth:0,expanded:!0,children:session.windows.map((w2)=>windowToNode(session.name,w2,executorMap)),data:{attached:session.attached,windowCount:session.windowCount},activePanes:claudePanes,agentState:void 0,wsAgentState:void 0}}function windowToNode(sessionName,window2,executorMap){let activePanes=window2.panes.filter((p)=>!p.isDead&&(p.command==="claude"||p.title.includes("claude"))).length;return{id:`window:${sessionName}:${window2.index}`,type:"window",label:window2.name,depth:1,expanded:!0,children:window2.panes.map((p)=>paneToNode(sessionName,window2.index,p,executorMap)),data:{active:window2.active,paneCount:window2.paneCount},activePanes,agentState:void 0,wsAgentState:void 0}}function paneToNode(sessionName,windowIndex,pane,executorMap){let executor=executorMap.get(pane.paneId),isClaude=pane.command==="claude"||pane.title.includes("claude");return{id:`pane:${pane.paneId}`,type:"pane",label:derivePaneLabel(pane,executor,isClaude),depth:2,expanded:!1,children:[],data:{command:pane.command,isDead:pane.isDead,pid:pane.pid,size:pane.size,paneId:pane.paneId,sessionName,windowIndex},activePanes:0,agentState:derivePaneState(pane,executor),wsAgentState:void 0}}function derivePaneLabel(pane,executor,isClaude){if(executor?.agentName&&executor?.team)return`${executor.team}/${executor.agentName}`;if(executor?.agentName)return executor.agentName;if(isClaude)return"claude";return pane.command}function derivePaneState(pane,executor){if(pane.isDead)return"error";if(!executor)return;let s=executor.state;if(s==="working")return"working";if(s==="idle"||s==="spawning")return"idle";if(s==="permission")return"permission";if(s==="error"||s==="terminated")return"error";return}function getSessionTarget(node){if(node.type==="agent")return{sessionName:node.data.sessionName};if(node.type==="session")return{sessionName:node.label};if(node.type==="window"){let parts=node.id.split(":");return{sessionName:parts[1],windowIndex:Number(parts[2])}}if(node.type==="pane"){let data=node.data;return{sessionName:data.sessionName,windowIndex:data.windowIndex}}return null}var palette,icons;var init_theme2=__esm(()=>{palette={purple:"#a855f7",violet:"#7c3aed",cyan:"#22d3ee",emerald:"#34d399",bg:"#1a1028",bgLight:"#241838",bgLighter:"#2e2048",text:"#e2e8f0",textDim:"#94a3b8",textMuted:"#64748b",border:"#414868",borderActive:"#7c3aed",scrollTrack:"#414868",scrollThumb:"#7aa2f7",active:"#22d3ee",success:"#34d399",warning:"#fbbf24",error:"#f87171",idle:"#94a3b8"},icons={org:"\u25C6",project:"\u25B8",projectOpen:"\u25BE",board:"\u2261",boardOpen:"\u2261",column:"\u2502",task:"\u25CB",taskActive:"\u25CF",taskDone:"\u2713",agent:"\u25B6",collapsed:"\u25B8",expanded:"\u25BE"}});function flattenTree(nodes){let result=[];function walk(node,depth){if(result.push({node,depth,visible:!0}),node.expanded)for(let child of node.children)walk(child,depth+1)}for(let node of nodes)walk(node,0);return result}function toggleNode(nodes,id){return nodes.map((node)=>{if(node.id===id)return{...node,expanded:!node.expanded};return{...node,children:toggleNode(node.children,id)}})}var import_jsx_dev_runtime2;var init_jsx_dev_runtime=__esm(()=>{import_jsx_dev_runtime2=__toESM(require_react_jsx_dev_runtime_development(),1)});function getNodeIcon(node){if(node.type==="agent")return getAgentIcon(node);switch(node.type){case"session":return node.data.attached?"\u25B6":"\u25B8";case"window":return node.data.active?"\u25A0":"\u25A1";case"pane":return getPaneIcon(node);default:return" "}}function getAgentIcon(node){switch(node.wsAgentState){case"running":return"\u25CF";case"stopped":return"\u25CB";case"error":return"\u2298";case"spawning":return"\u231B";default:return"\u25CB"}}function getPaneIcon(node){if(node.data.isDead)return"\u2718";if(node.agentState==="working")return"\u25CF";if(node.agentState==="idle")return"\u25CB";if(node.agentState==="permission")return"\u26A0";if(node.agentState==="error")return"\u2718";if(node.data.command==="claude")return"\u25C6";return"\u25CB"}function getNodeColor(node){if(node.type==="agent")return getAgentColor(node);switch(node.type){case"session":return node.data.attached?palette.emerald:palette.textDim;case"window":return node.data.active?palette.cyan:palette.text;case"pane":return getPaneColor(node);default:return palette.text}}function getAgentColor(node){switch(node.wsAgentState){case"running":return palette.emerald;case"stopped":return palette.textDim;case"error":return palette.error;case"spawning":return palette.warning;default:return palette.textDim}}function getPaneColor(node){if(node.data.isDead)return palette.error;if(node.agentState==="working")return palette.cyan;if(node.agentState==="permission")return palette.warning;if(node.agentState==="error")return palette.error;if(node.agentState==="idle")return palette.textDim;if(node.data.command==="claude")return palette.cyan;return palette.textDim}function getNodeSuffix(node){if(node.type==="agent"){let wc=node.data.windowCount;if(wc>1)return` (${wc} windows)`;if(wc===1)return" (1 window)";return""}if(node.type==="session"||node.type==="pane"){let count=node.activePanes;if(count>0)return` ${icons.agent}${count}`}return""}function getStateColor(state){switch(state){case"working":return palette.cyan;case"idle":return palette.textDim;case"permission":return palette.warning;case"error":return palette.error;default:return palette.textMuted}}var import_react13,TreeNodeRow;var init_TreeNode=__esm(()=>{init_theme2();init_jsx_dev_runtime();import_react13=__toESM(require_react_development(),1),TreeNodeRow=import_react13.memo(function({node,selected,onSelect,onToggle}){let indent=" ".repeat(node.depth),hasChildren=node.children.length>0,expandIcon=hasChildren?node.expanded?icons.expanded:icons.collapsed:" ",icon=getNodeIcon(node),color=getNodeColor(node),suffix=getNodeSuffix(node);return import_jsx_dev_runtime2.jsxDEV("box",{height:1,width:"100%",backgroundColor:selected?palette.violet:void 0,onMouseDown:()=>{if(onSelect(node.id),hasChildren)onToggle(node.id)},children:import_jsx_dev_runtime2.jsxDEV("text",{children:[import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:[indent,expandIcon," "]},void 0,!0,void 0,this),import_jsx_dev_runtime2.jsxDEV("span",{fg:color,children:[icon," "]},void 0,!0,void 0,this),import_jsx_dev_runtime2.jsxDEV("span",{fg:selected?"#ffffff":palette.text,children:node.label},void 0,!1,void 0,this),suffix?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:suffix},void 0,!1,void 0,this):null,node.agentState?import_jsx_dev_runtime2.jsxDEV("span",{fg:getStateColor(node.agentState),children:[" ",node.agentState]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)},void 0,!1,void 0,this)})});function Nav({onTmuxSessionSelect,workspaceRoot,initialAgent}){let[diagnostics,setDiagnostics]=import_react15.useState(null),[sessionTree,setSessionTree]=import_react15.useState([]),[selectedIndex,setSelectedIndex]=import_react15.useState(0),lastTarget=import_react15.useRef(null),initialSelectDone=import_react15.useRef(!1);import_react15.useEffect(()=>{let active=!0;async function refresh(){try{let snap=await collectDiagnostics();if(active)setDiagnostics(snap)}catch(err){console.error("TUI: diagnostics failed:",err)}}refresh();let timer2=setInterval(refresh,2000);return()=>{active=!1,clearInterval(timer2)}},[]),import_react15.useEffect(()=>{if(!diagnostics)return;let newTree;if(workspaceRoot){let agentNames=scanAgents2(workspaceRoot);newTree=buildWorkspaceTree({agentNames,sessions:diagnostics.sessions,executors:diagnostics.executors})}else newTree=buildSessionTree(diagnostics);setSessionTree((prev)=>mergeExpandedState(prev,newTree))},[diagnostics,workspaceRoot]);let flatNodes=import_react15.useMemo(()=>flattenTree(sessionTree),[sessionTree]);import_react15.useEffect(()=>{if(flatNodes.length>0&&selectedIndex>=flatNodes.length)setSelectedIndex(flatNodes.length-1)},[flatNodes.length,selectedIndex]),import_react15.useEffect(()=>{if(!initialAgent||initialSelectDone.current||flatNodes.length===0)return;let idx=flatNodes.findIndex((n)=>n.node.id===`agent:${initialAgent}`);if(idx>=0)setSelectedIndex(idx),initialSelectDone.current=!0},[initialAgent,flatNodes]),import_react15.useEffect(()=>{let current=flatNodes[selectedIndex]?.node;if(!current)return;let target=getSessionTarget(current);if(!target)return;let key=`${target.sessionName}:${target.windowIndex??""}`;if(key===lastTarget.current)return;if(lastTarget.current=key,current.type==="agent"&¤t.wsAgentState!=="running")return;onTmuxSessionSelect(target.sessionName,target.windowIndex)},[selectedIndex,flatNodes,onTmuxSessionSelect]);let handleSelect=import_react15.useCallback((id)=>{let idx=flatNodes.findIndex((n)=>n.node.id===id);if(idx>=0)setSelectedIndex(idx)},[flatNodes]),handleToggle=import_react15.useCallback((id)=>{setSessionTree((prev)=>toggleNode(prev,id))},[]),handleVerticalNav=import_react15.useCallback((keyName2)=>{let rowCount=flatNodes.length;if(rowCount===0)return;if(keyName2==="up"||keyName2==="k")setSelectedIndex((prev)=>prev===0?rowCount-1:prev-1);else if(keyName2==="down"||keyName2==="j")setSelectedIndex((prev)=>prev>=rowCount-1?0:prev+1)},[flatNodes.length]),handleExpandCollapse=import_react15.useCallback((keyName2)=>{let node=flatNodes[selectedIndex]?.node;if(!node)return;if((keyName2==="right"||keyName2==="l")&&node.children.length>0&&!node.expanded)handleToggle(node.id);else if((keyName2==="left"||keyName2==="h")&&node.expanded)handleToggle(node.id)},[flatNodes,selectedIndex,handleToggle]),handleEnter=import_react15.useCallback(()=>{let node=flatNodes[selectedIndex]?.node;if(!node)return;if(node.type==="agent"){if(node.wsAgentState==="stopped")spawnAgent(node.label);let target2=getSessionTarget(node);if(target2)onTmuxSessionSelect(target2.sessionName,target2.windowIndex);return}if(node.children.length>0)handleToggle(node.id);let target=getSessionTarget(node);if(target)onTmuxSessionSelect(target.sessionName,target.windowIndex)},[flatNodes,selectedIndex,handleToggle,onTmuxSessionSelect]);useKeyboard((key)=>{if(key.name==="up"||key.name==="k"||key.name==="down"||key.name==="j")handleVerticalNav(key.name);else if(key.name==="right"||key.name==="l"||key.name==="left"||key.name==="h")handleExpandCollapse(key.name);else if(key.name==="enter"||key.name==="return")handleEnter()});let agentCount=workspaceRoot?sessionTree.filter((n)=>n.type==="agent").length:diagnostics?.sessions.length??0,runningCount=workspaceRoot?sessionTree.filter((n)=>n.wsAgentState==="running").length:diagnostics?.sessions.reduce((sum,s)=>sum+s.windows.reduce((ws,w2)=>ws+w2.panes.length,0),0)??0,headerLabel=workspaceRoot?"Agents":"Sessions";return import_jsx_dev_runtime2.jsxDEV("box",{flexDirection:"column",width:"100%",height:"100%",backgroundColor:palette.bg,children:[import_jsx_dev_runtime2.jsxDEV("box",{height:1,paddingX:1,backgroundColor:palette.bgLight,children:import_jsx_dev_runtime2.jsxDEV("text",{children:[import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.purple,children:headerLabel},void 0,!1,void 0,this),diagnostics?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:[" ",workspaceRoot?`${runningCount}/${agentCount}`:`${agentCount}s ${runningCount}p`]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)},void 0,!1,void 0,this),diagnostics?import_jsx_dev_runtime2.jsxDEV("scrollbox",{focused:!0,height:"100%",style:{scrollbarOptions:{showArrows:!1,trackOptions:{foregroundColor:palette.scrollThumb,backgroundColor:palette.scrollTrack}}},children:flatNodes.map((flat,i2)=>import_jsx_dev_runtime2.jsxDEV(TreeNodeRow,{node:flat.node,selected:i2===selectedIndex,onSelect:handleSelect,onToggle:handleToggle},flat.node.id,!1,void 0,this))},void 0,!1,void 0,this):import_jsx_dev_runtime2.jsxDEV("box",{flexGrow:1,justifyContent:"center",alignItems:"center",children:import_jsx_dev_runtime2.jsxDEV("text",{fg:palette.textDim,children:"Collecting..."},void 0,!1,void 0,this)},void 0,!1,void 0,this),import_jsx_dev_runtime2.jsxDEV("box",{height:1,paddingX:1,backgroundColor:palette.bgLight,children:import_jsx_dev_runtime2.jsxDEV("text",{children:import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textMuted,children:["\u2191\u2193",":nav ","\u2190\u2192",":expand Enter:",workspaceRoot?"spawn/attach":"attach"]},void 0,!0,void 0,this)},void 0,!1,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function spawnAgent(name){try{let{spawn:spawn4}=__require("child_process"),{join:join53,resolve:resolve10}=__require("path"),{existsSync:existsSync40}=__require("fs"),wsRoot=process.env.GENIE_TUI_WORKSPACE,cwd;if(wsRoot){let agentDir=resolve10(join53(wsRoot,"agents",name));if(existsSync40(agentDir))cwd=agentDir}spawn4("genie",["spawn",name,"--session",name],{detached:!0,stdio:"ignore",cwd}).unref()}catch{}}function mergeExpandedState(oldTree,newTree){if(oldTree.length===0)return newTree;let oldState=new Map;function collect(nodes){for(let n of nodes)oldState.set(n.id,n.expanded),collect(n.children)}collect(oldTree);function apply(nodes){return nodes.map((n)=>({...n,expanded:oldState.has(n.id)?oldState.get(n.id):n.expanded,children:apply(n.children)}))}return apply(newTree)}var import_react15;var init_Nav=__esm(async()=>{init_workspace();init_diagnostics();init_theme2();init_TreeNode();init_jsx_dev_runtime();await init_react();import_react15=__toESM(require_react_development(),1)});function App({rightPane,workspaceRoot,initialAgent}){let handleTmuxSessionSelect=import_react16.useCallback((sessionName,windowIndex)=>{if(!rightPane)return;attachProjectWindow(rightPane,sessionName,windowIndex)},[rightPane]);return import_jsx_dev_runtime2.jsxDEV(Nav,{onTmuxSessionSelect:handleTmuxSessionSelect,workspaceRoot,initialAgent},void 0,!1,void 0,this)}var import_react16;var init_app=__esm(async()=>{init_tmux2();init_jsx_dev_runtime();await init_Nav();import_react16=__toESM(require_react_development(),1)});var exports_render={};__export(exports_render,{renderNav:()=>renderNav});async function renderNav(){let rightPane=process.env.GENIE_TUI_RIGHT||void 0,workspaceRoot=process.env.GENIE_TUI_WORKSPACE||void 0,initialAgent=process.env.GENIE_TUI_AGENT||void 0,renderer=await createCliRenderer({exitOnCtrlC:!1,useMouse:!0});createRoot(renderer).render(import_jsx_dev_runtime2.jsxDEV(App,{rightPane,workspaceRoot,initialAgent},void 0,!1,void 0,this)),await new Promise((resolve10)=>{renderer.once("destroy",resolve10)})}var init_render=__esm(async()=>{init_jsx_dev_runtime();await __promiseAll([init_core(),init_react(),init_app()])});var exports_tui={};__export(exports_tui,{launchTui:()=>launchTui});async function launchTui(){let{renderNav:renderNav2}=await init_render().then(() => exports_render);await renderNav2()}var import__=__toESM(require_commander(),1),{program,createCommand,createArgument,createOption,CommanderError,InvalidArgumentError,InvalidOptionArgumentError,Command,Argument,Option,Help}=import__.default;import{existsSync as existsSync3,unlinkSync as unlinkSync2}from"fs";import{homedir as homedir3}from"os";import{join as join3}from"path";var{$:$2}=globalThis.Bun;import{existsSync,unlinkSync}from"fs";import{homedir}from"os";import{join}from"path";var CLAUDE_DIR=join(homedir(),".claude"),CLAUDE_HOOKS_DIR=join(CLAUDE_DIR,"hooks"),CLAUDE_SETTINGS_FILE=join(CLAUDE_DIR,"settings.json"),GENIE_HOOK_SCRIPT_NAME="genie-bash-hook.sh";function getClaudeSettingsPath(){return CLAUDE_SETTINGS_FILE}function getGenieHookScriptPath(){return join(CLAUDE_HOOKS_DIR,GENIE_HOOK_SCRIPT_NAME)}function hookScriptExists(){return existsSync(getGenieHookScriptPath())}function removeHookScript(){let scriptPath=getGenieHookScriptPath();if(existsSync(scriptPath))unlinkSync(scriptPath)}function contractClaudePath(path){let home=homedir();if(path.startsWith(`${home}/`))return`~${path.slice(home.length)}`;if(path===home)return"~";return path}init_genie_config2();var{$}=globalThis.Bun;async function checkCommand(cmd){try{let cmdPath=(await $`which ${cmd}`.quiet().text()).trim();if(!cmdPath)return{exists:!1};let version;try{let firstLine=(await $`${cmd} --version`.quiet().text()).split(`
|
|
2075
2075
|
`)[0].trim(),versionMatch=firstLine.match(/(\d+\.[\d.]+[a-z0-9-]*)/i);version=versionMatch?versionMatch[1]:firstLine.slice(0,50)}catch{try{let firstLine=(await $`${cmd} -v`.quiet().text()).split(`
|
|
2076
2076
|
`)[0].trim(),versionMatch=firstLine.match(/(\d+\.[\d.]+[a-z0-9-]*)/i);version=versionMatch?versionMatch[1]:firstLine.slice(0,50)}catch{}}return{exists:!0,version,path:cmdPath}}catch{return{exists:!1}}}function printSectionHeader(title){console.log(),console.log(`\x1B[1m${title}:\x1B[0m`)}function printCheckResult(result){let icon={pass:"\x1B[32m\u2713\x1B[0m",fail:"\x1B[31m\u2717\x1B[0m",warn:"\x1B[33m!\x1B[0m"}[result.status],message=result.message?` ${result.message}`:"";if(console.log(` ${icon} ${result.name}${message}`),result.suggestion)console.log(` \x1B[2m${result.suggestion}\x1B[0m`)}async function checkPrerequisites(){let results=[],tmuxCheck=await checkCommand("tmux");if(tmuxCheck.exists)results.push({name:"tmux",status:"pass",message:tmuxCheck.version||""});else results.push({name:"tmux",status:"fail",suggestion:"Install with: brew install tmux (or apt install tmux)"});let jqCheck=await checkCommand("jq");if(jqCheck.exists)results.push({name:"jq",status:"pass",message:jqCheck.version||""});else results.push({name:"jq",status:"fail",suggestion:"Install with: brew install jq (or apt install jq)"});let bunCheck=await checkCommand("bun");if(bunCheck.exists)results.push({name:"bun",status:"pass",message:bunCheck.version||""});else results.push({name:"bun",status:"fail",suggestion:"Install with: curl -fsSL https://bun.sh/install | bash"});let claudeCheck=await checkCommand("claude");if(claudeCheck.exists)results.push({name:"Claude Code",status:"pass",message:claudeCheck.version||""});else results.push({name:"Claude Code",status:"warn",suggestion:"Install with: npm install -g @anthropic-ai/claude-code"});return results}async function checkConfiguration(){let results=[];if(genieConfigExists())results.push({name:"Genie config exists",status:"pass",message:contractClaudePath(getGenieConfigPath())});else results.push({name:"Genie config exists",status:"warn",message:"not found",suggestion:"Run: genie setup"});if(isSetupComplete())results.push({name:"Setup complete",status:"pass"});else results.push({name:"Setup complete",status:"warn",message:"not completed",suggestion:"Run: genie setup"});let claudeSettingsPath=getClaudeSettingsPath();if(existsSync3(claudeSettingsPath))results.push({name:"Claude settings exists",status:"pass",message:contractClaudePath(claudeSettingsPath)});else results.push({name:"Claude settings exists",status:"warn",message:"not found",suggestion:"Claude Code creates this on first run"});return results}async function checkTmux(){let results=[];try{if((await $2`tmux -L genie list-sessions 2>/dev/null`.quiet()).exitCode===0)results.push({name:"Server running",status:"pass"});else return results.push({name:"Server running",status:"warn",message:"no sessions",suggestion:"Start with: tmux new-session -d -s genie"}),results}catch{return results.push({name:"Server running",status:"warn",message:"could not check"}),results}let sessionName=(await loadGenieConfig()).session.name;try{if((await $2`tmux -L genie has-session -t ${sessionName} 2>/dev/null`.quiet()).exitCode===0)results.push({name:`Session '${sessionName}' exists`,status:"pass"});else results.push({name:`Session '${sessionName}' exists`,status:"warn",suggestion:`Start with: tmux new-session -d -s ${sessionName}`})}catch{results.push({name:`Session '${sessionName}' exists`,status:"warn",message:"could not check"})}return results}async function checkWorkerProfiles(){let results=[];if(!genieConfigExists())return results.push({name:"Worker profiles",status:"warn",message:"no genie config",suggestion:"Run: genie setup"}),results;let config=await loadGenieConfig(),profiles=config.workerProfiles;if(!profiles||Object.keys(profiles).length===0)return results.push({name:"Worker profiles",status:"pass",message:"none configured (using defaults)"}),results;let totalProfiles=Object.keys(profiles).length;results.push({name:"Profiles configured",status:"pass",message:`${totalProfiles} profile${totalProfiles===1?"":"s"}`});for(let name of Object.keys(profiles))results.push({name:`Profile '${name}'`,status:"pass",message:"claude (direct)"});if(config.defaultWorkerProfile)if(profiles[config.defaultWorkerProfile])results.push({name:"Default profile",status:"pass",message:config.defaultWorkerProfile});else results.push({name:"Default profile",status:"warn",message:`'${config.defaultWorkerProfile}' not found`,suggestion:"Run: genie profiles default <profile>"});return results}function runCheckSection(label,results,counts){printSectionHeader(label);for(let result of results){if(printCheckResult(result),result.status==="fail")counts.errors=!0;if(result.status==="warn")counts.warnings=!0}}async function doctorCommand(options){if(options?.fix){await doctorFix();return}console.log(),console.log("\x1B[1mGenie Doctor\x1B[0m"),console.log(`\x1B[2m${"\u2500".repeat(40)}\x1B[0m`);let counts={errors:!1,warnings:!1};if(runCheckSection("Prerequisites",await checkPrerequisites(),counts),runCheckSection("Configuration",await checkConfiguration(),counts),runCheckSection("Tmux",await checkTmux(),counts),runCheckSection("Worker Profiles",await checkWorkerProfiles(),counts),console.log(),console.log(`\x1B[2m${"\u2500".repeat(40)}\x1B[0m`),counts.errors)console.log("\x1B[31mSome checks failed.\x1B[0m Run \x1B[36mgenie setup\x1B[0m to fix.");else if(counts.warnings)console.log("\x1B[33mSome warnings detected.\x1B[0m Everything should still work.");else console.log("\x1B[32mAll checks passed!\x1B[0m");if(console.log(),counts.errors)process.exit(1)}async function killStalePostgres(){console.log(" Killing stale postgres processes...");try{let{execSync}=await import("child_process");execSync('pkill -9 -f "postgres.*pgserve" 2>/dev/null || true',{stdio:"ignore",timeout:5000}),console.log(" \x1B[32m\u2713\x1B[0m Stale postgres processes killed")}catch{console.log(" \x1B[33m!\x1B[0m Could not kill stale postgres processes")}}async function cleanSharedMemory(){console.log(" Cleaning shared memory...");try{let{execSync}=await import("child_process");execSync("ipcs -m 2>/dev/null | awk '$6 == 0 {print $2}' | xargs -I{} ipcrm -m {} 2>/dev/null || true",{stdio:"ignore",timeout:5000}),console.log(" \x1B[32m\u2713\x1B[0m Shared memory cleaned")}catch{console.log(" \x1B[32m\u2713\x1B[0m No stale shared memory")}}async function stopExistingDaemon(pidFile){try{let{readFileSync:readFileSync2}=await import("fs"),pid=Number.parseInt(readFileSync2(pidFile,"utf-8").trim(),10);if(!Number.isNaN(pid)&&pid>0)try{process.kill(pid,"SIGTERM"),console.log(` \x1B[32m\u2713\x1B[0m Stopped existing daemon (PID ${pid})`),await new Promise((r)=>setTimeout(r,1000))}catch{}}catch{}}function removeStaleFiles(genieHome,pidFile){let portFile=join3(genieHome,"pgserve.port"),postmasterPid=join3(genieHome,"data","pgserve","postmaster.pid");for(let file of[portFile,pidFile,postmasterPid])if(existsSync3(file))try{unlinkSync2(file),console.log(` \x1B[32m\u2713\x1B[0m Removed ${file}`)}catch{console.log(` \x1B[33m!\x1B[0m Could not remove ${file}`)}}async function restartDaemon(){console.log(" Restarting daemon...");try{let{spawn}=await import("child_process"),bunPath=process.execPath??"bun",genieBin=process.argv[1]??"genie";spawn(bunPath,[genieBin,"daemon","start"],{detached:!0,stdio:"ignore"}).unref(),await new Promise((resolve)=>setTimeout(resolve,2000)),console.log(" \x1B[32m\u2713\x1B[0m Daemon restart initiated")}catch{console.log(" \x1B[33m!\x1B[0m Could not restart daemon \u2014 run: genie daemon start")}}async function doctorFix(){console.log(`
|
|
2077
2077
|
\x1B[1mGenie Doctor \u2014 Auto Fix\x1B[0m`),console.log(`\x1B[2m${"\u2500".repeat(40)}\x1B[0m
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.260330.
|
|
3
|
+
"version": "4.260330.24",
|
|
4
4
|
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Namastex Labs"
|
|
@@ -33,3 +33,8 @@ set -g pane-border-status off
|
|
|
33
33
|
# Pane borders (visual only, no #() shell commands)
|
|
34
34
|
set -g pane-border-style "fg=#0f3460"
|
|
35
35
|
set -g pane-active-border-style "fg=#7b2ff7"
|
|
36
|
+
|
|
37
|
+
# Unbind prefix key — all keystrokes go directly to the active pane
|
|
38
|
+
# TUI keybindings are set in the root table by serve.ts
|
|
39
|
+
set -g prefix None
|
|
40
|
+
unbind C-b
|
|
@@ -88,9 +88,15 @@ function genieTmux(subcmd: string): string {
|
|
|
88
88
|
return `tmux -L ${GENIE_SOCKET} -f ${genieTmuxConf()} ${subcmd}`;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/**
|
|
91
|
+
/** TUI tmux config — minimal, no shell probes, no prefix key interference */
|
|
92
|
+
function tuiTmuxConf(): string {
|
|
93
|
+
const candidates = [join(genieHome(), 'tui-tmux.conf')];
|
|
94
|
+
return candidates.find((p) => existsSync(p)) ?? '/dev/null';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** TUI tmux command — uses -L genie-tui socket + minimal TUI config */
|
|
92
98
|
function tuiTmux(subcmd: string): string {
|
|
93
|
-
return `tmux ${subcmd}`;
|
|
99
|
+
return `tmux -L genie-tui -f ${tuiTmuxConf()} ${subcmd}`;
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
/** Check if a tmux server is running on a socket */
|
|
@@ -104,7 +110,6 @@ function isGenieTmuxRunning(): boolean {
|
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
const NAV_WIDTH = 30;
|
|
107
|
-
const KEY_TABLE = 'genie-tui';
|
|
108
113
|
|
|
109
114
|
/** Theme colors for TUI tmux styling */
|
|
110
115
|
const TUI_STYLE = {
|
|
@@ -117,7 +122,7 @@ function applyTuiStyle(): void {
|
|
|
117
122
|
const cmds = [
|
|
118
123
|
`set-option -t ${TUI_SESSION} pane-border-style 'fg=${TUI_STYLE.inactiveBorder}'`,
|
|
119
124
|
`set-option -t ${TUI_SESSION} pane-active-border-style 'fg=${TUI_STYLE.activeBorder}'`,
|
|
120
|
-
`set-option -t ${TUI_SESSION} mouse
|
|
125
|
+
`set-option -t ${TUI_SESSION} mouse on`,
|
|
121
126
|
`set-option -t ${TUI_SESSION} status off`,
|
|
122
127
|
`set-option -t ${TUI_SESSION} pane-border-status off`,
|
|
123
128
|
];
|
|
@@ -128,18 +133,17 @@ function applyTuiStyle(): void {
|
|
|
128
133
|
}
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
/** Set up keybindings in
|
|
136
|
+
/** Set up keybindings in root table so they work immediately */
|
|
132
137
|
function setupTuiKeybindings(): void {
|
|
133
138
|
const bindings = [
|
|
134
139
|
// Tab: toggle focus between left nav (pane 0) and right terminal (pane 1)
|
|
135
|
-
`bind-key -T
|
|
140
|
+
`bind-key -T root Tab if-shell "[ '#{pane_index}' = '0' ]" "select-pane -t ${TUI_SESSION}:0.1" "select-pane -t ${TUI_SESSION}:0.0"`,
|
|
136
141
|
// Ctrl+B: toggle sidebar width (collapse/expand)
|
|
137
|
-
`bind-key -T
|
|
138
|
-
// Ctrl+
|
|
139
|
-
`bind-key -T ${
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`set-hook -t ${TUI_SESSION} client-session-changed "switch-client -T ${KEY_TABLE}"`,
|
|
142
|
+
`bind-key -T root C-b if-shell "[ $(tmux display-message -p '#\\{pane_width\\}' -t ${TUI_SESSION}:0.0) -gt 5 ]" "resize-pane -t ${TUI_SESSION}:0.0 -x 0" "resize-pane -t ${TUI_SESSION}:0.0 -x ${NAV_WIDTH}"`,
|
|
143
|
+
// Ctrl+T: new window in agent session (sends C-b c to the nested tmux in right pane)
|
|
144
|
+
`bind-key -T root C-t send-keys -t ${TUI_SESSION}:0.1 C-b c`,
|
|
145
|
+
// Ctrl+Q: detach from TUI
|
|
146
|
+
'bind-key -T root C-q detach-client',
|
|
143
147
|
];
|
|
144
148
|
for (const cmd of bindings) {
|
|
145
149
|
try {
|
|
@@ -169,7 +173,7 @@ function startTuiTmuxServer(): { leftPane: string; rightPane: string } {
|
|
|
169
173
|
const cols = 120;
|
|
170
174
|
const rows = 40;
|
|
171
175
|
|
|
172
|
-
execSync(tuiTmux(`new-session -d -s ${TUI_SESSION} -x ${cols} -y ${rows}
|
|
176
|
+
execSync(tuiTmux(`new-session -d -s ${TUI_SESSION} -x ${cols} -y ${rows}`), {
|
|
173
177
|
stdio: 'ignore',
|
|
174
178
|
});
|
|
175
179
|
execSync(tuiTmux(`split-window -h -t ${TUI_SESSION}:0 -l ${cols - NAV_WIDTH - 1}`), { stdio: 'ignore' });
|
|
@@ -210,10 +214,10 @@ function sendTuiLaunchScript(leftPane: string, rightPane: string, workspaceRoot?
|
|
|
210
214
|
} catch {}
|
|
211
215
|
}
|
|
212
216
|
|
|
213
|
-
/** Kill the TUI
|
|
217
|
+
/** Kill the TUI tmux server */
|
|
214
218
|
function killTuiSession(): void {
|
|
215
219
|
try {
|
|
216
|
-
execSync(
|
|
220
|
+
execSync(tuiTmux('kill-server'), { stdio: 'ignore' });
|
|
217
221
|
} catch {
|
|
218
222
|
// not running
|
|
219
223
|
}
|
|
@@ -269,10 +273,10 @@ export async function autoStartServe(): Promise<void> {
|
|
|
269
273
|
}
|
|
270
274
|
}
|
|
271
275
|
|
|
272
|
-
/** Check if the genie-tui session exists on the
|
|
276
|
+
/** Check if the genie-tui session exists on the TUI socket */
|
|
273
277
|
export function isTuiSessionReady(): boolean {
|
|
274
278
|
try {
|
|
275
|
-
execSync(`
|
|
279
|
+
execSync(tuiTmux(`has-session -t ${TUI_SESSION}`), { stdio: 'ignore' });
|
|
276
280
|
return true;
|
|
277
281
|
} catch {
|
|
278
282
|
return false;
|
|
@@ -539,7 +543,7 @@ function printTmuxStatus(): void {
|
|
|
539
543
|
}
|
|
540
544
|
|
|
541
545
|
const tuiReady = isTuiSessionReady();
|
|
542
|
-
console.log(`
|
|
546
|
+
console.log(` tmux -L genie-tui: ${tuiReady ? 'running' : 'stopped'}`);
|
|
543
547
|
}
|
|
544
548
|
|
|
545
549
|
/** Print scheduler and inbox status */
|
|
@@ -24,7 +24,7 @@ export function Nav({ onTmuxSessionSelect, workspaceRoot, initialAgent }: NavPro
|
|
|
24
24
|
const [sessionTree, setSessionTree] = useState<TreeNode[]>([]);
|
|
25
25
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
26
26
|
const lastTarget = useRef<string | null>(null);
|
|
27
|
-
const
|
|
27
|
+
const initialSelectDone = useRef(false);
|
|
28
28
|
|
|
29
29
|
// Refresh diagnostics every 2s
|
|
30
30
|
useEffect(() => {
|
|
@@ -76,34 +76,15 @@ export function Nav({ onTmuxSessionSelect, workspaceRoot, initialAgent }: NavPro
|
|
|
76
76
|
}
|
|
77
77
|
}, [flatNodes.length, selectedIndex]);
|
|
78
78
|
|
|
79
|
-
//
|
|
80
|
-
// Check on each diagnostics refresh (2s) so re-attach from a different agent dir works.
|
|
81
|
-
const [pendingAgent, setPendingAgent] = useState<string | undefined>(initialAgent);
|
|
82
|
-
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
if (!diagnostics) return;
|
|
85
|
-
try {
|
|
86
|
-
const fs = require('node:fs') as typeof import('node:fs');
|
|
87
|
-
const agentFile = `${genieHome.current}/tui-initial-agent`;
|
|
88
|
-
if (fs.existsSync(agentFile)) {
|
|
89
|
-
const agent = fs.readFileSync(agentFile, 'utf-8').trim();
|
|
90
|
-
fs.unlinkSync(agentFile);
|
|
91
|
-
if (agent) setPendingAgent(agent);
|
|
92
|
-
}
|
|
93
|
-
} catch {
|
|
94
|
-
// best-effort
|
|
95
|
-
}
|
|
96
|
-
}, [diagnostics]);
|
|
97
|
-
|
|
98
|
-
// Apply pending agent selection (from prop or file)
|
|
79
|
+
// Initial agent pre-selection (once)
|
|
99
80
|
useEffect(() => {
|
|
100
|
-
if (!
|
|
101
|
-
const idx = flatNodes.findIndex((n) => n.node.id === `agent:${
|
|
81
|
+
if (!initialAgent || initialSelectDone.current || flatNodes.length === 0) return;
|
|
82
|
+
const idx = flatNodes.findIndex((n) => n.node.id === `agent:${initialAgent}`);
|
|
102
83
|
if (idx >= 0) {
|
|
103
84
|
setSelectedIndex(idx);
|
|
104
|
-
|
|
85
|
+
initialSelectDone.current = true;
|
|
105
86
|
}
|
|
106
|
-
}, [
|
|
87
|
+
}, [initialAgent, flatNodes]);
|
|
107
88
|
|
|
108
89
|
// Auto-switch right pane when cursor moves to a new target
|
|
109
90
|
useEffect(() => {
|
|
@@ -162,11 +143,12 @@ export function Nav({ onTmuxSessionSelect, workspaceRoot, initialAgent }: NavPro
|
|
|
162
143
|
const node = flatNodes[selectedIndex]?.node;
|
|
163
144
|
if (!node) return;
|
|
164
145
|
|
|
165
|
-
// Agent node: spawn if not running, then attach
|
|
166
146
|
if (node.type === 'agent') {
|
|
167
|
-
|
|
147
|
+
// No session → spawn the agent (creates session + window 0 with Claude)
|
|
148
|
+
if (node.wsAgentState === 'stopped') {
|
|
168
149
|
spawnAgent(node.label);
|
|
169
150
|
}
|
|
151
|
+
// Attach right pane to the agent's session (existing or just-spawned)
|
|
170
152
|
const target = getSessionTarget(node);
|
|
171
153
|
if (target) onTmuxSessionSelect(target.sessionName, target.windowIndex);
|
|
172
154
|
return;
|
|
@@ -255,26 +237,19 @@ export function Nav({ onTmuxSessionSelect, workspaceRoot, initialAgent }: NavPro
|
|
|
255
237
|
);
|
|
256
238
|
}
|
|
257
239
|
|
|
258
|
-
/** Spawn a stopped agent by launching `genie spawn <name>`
|
|
240
|
+
/** Spawn a stopped agent by launching `genie spawn <name>` from its workspace directory */
|
|
259
241
|
function spawnAgent(name: string): void {
|
|
260
242
|
try {
|
|
261
243
|
const { spawn } = require('node:child_process') as typeof import('node:child_process');
|
|
262
244
|
const { join, resolve } = require('node:path') as typeof import('node:path');
|
|
263
245
|
const { existsSync } = require('node:fs') as typeof import('node:fs');
|
|
264
|
-
|
|
265
|
-
// Resolve agent CWD from workspace path
|
|
266
246
|
const wsRoot = process.env.GENIE_TUI_WORKSPACE;
|
|
267
247
|
let cwd: string | undefined;
|
|
268
248
|
if (wsRoot) {
|
|
269
249
|
const agentDir = resolve(join(wsRoot, 'agents', name));
|
|
270
250
|
if (existsSync(agentDir)) cwd = agentDir;
|
|
271
251
|
}
|
|
272
|
-
|
|
273
|
-
spawn('genie', ['spawn', name], {
|
|
274
|
-
detached: true,
|
|
275
|
-
stdio: 'ignore',
|
|
276
|
-
cwd,
|
|
277
|
-
}).unref();
|
|
252
|
+
spawn('genie', ['spawn', name, '--session', name], { detached: true, stdio: 'ignore', cwd }).unref();
|
|
278
253
|
} catch {
|
|
279
254
|
// best-effort spawn
|
|
280
255
|
}
|
package/src/tui/tmux.ts
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TUI tmux runtime helpers — attach, navigate, pane management.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* This module bridges the two for the right-pane attach.
|
|
4
|
+
* Session creation is handled by `genie serve` (see serve.ts).
|
|
5
|
+
* This module only provides runtime operations for the TUI client.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { execSync, spawnSync } from 'node:child_process';
|
|
10
9
|
|
|
11
10
|
const SESSION_NAME = 'genie-tui';
|
|
12
|
-
|
|
11
|
+
const TMUX_SOCKET = 'genie-tui';
|
|
13
12
|
const GENIE_AGENT_SOCKET = 'genie';
|
|
13
|
+
const TUI_TMUX_CONF = (() => {
|
|
14
|
+
const { existsSync } = require('node:fs') as typeof import('node:fs');
|
|
15
|
+
const home = process.env.GENIE_HOME ?? `${process.env.HOME}/.genie`;
|
|
16
|
+
const tuiConf = `${home}/tui-tmux.conf`;
|
|
17
|
+
return existsSync(tuiConf) ? tuiConf : '/dev/null';
|
|
18
|
+
})();
|
|
19
|
+
const TMUX = `tmux -L ${TMUX_SOCKET} -f ${TUI_TMUX_CONF}`;
|
|
14
20
|
|
|
15
|
-
/**
|
|
16
|
-
* Resolve the right pane ID — self-healing if the pane was killed/recreated.
|
|
17
|
-
* Falls back to re-discovering from the session layout.
|
|
18
|
-
*/
|
|
19
21
|
function resolveRightPane(rightPane: string): string {
|
|
20
22
|
try {
|
|
21
|
-
execSync(
|
|
23
|
+
execSync(`${TMUX} display-message -t ${rightPane} -p ''`, { stdio: 'ignore' });
|
|
22
24
|
return rightPane;
|
|
23
25
|
} catch {
|
|
24
26
|
try {
|
|
25
|
-
const panes = execSync(
|
|
27
|
+
const panes = execSync(`${TMUX} list-panes -t ${SESSION_NAME}:0 -F '#{pane_id}'`, { encoding: 'utf-8' })
|
|
26
28
|
.trim()
|
|
27
29
|
.split('\n');
|
|
28
30
|
return panes[1] || panes[0];
|
|
@@ -32,49 +34,45 @@ function resolveRightPane(rightPane: string): string {
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
/**
|
|
36
|
-
function
|
|
37
|
+
/** Switch right pane to a specific agent session. NEVER kills the pane. */
|
|
38
|
+
export function attachProjectWindow(rightPane: string, targetSession: string, windowIndex?: number): void {
|
|
39
|
+
if (targetSession === SESSION_NAME) return;
|
|
40
|
+
const pane = resolveRightPane(rightPane);
|
|
37
41
|
const agentTmux = `tmux -L ${GENIE_AGENT_SOCKET}`;
|
|
42
|
+
|
|
43
|
+
// Ensure agent session exists
|
|
38
44
|
try {
|
|
39
|
-
execSync(`${agentTmux} has-session -t '${
|
|
45
|
+
execSync(`${agentTmux} has-session -t '${targetSession}' 2>/dev/null`, { stdio: 'ignore' });
|
|
40
46
|
} catch {
|
|
41
|
-
|
|
42
|
-
execSync(`${agentTmux} new-session -d -s '${sessionName}'`, { stdio: 'ignore' });
|
|
43
|
-
} catch {
|
|
44
|
-
// race: another process created it
|
|
45
|
-
}
|
|
47
|
+
return; // No session — don't create empty ones, don't kill the pane
|
|
46
48
|
}
|
|
47
|
-
}
|
|
48
49
|
|
|
49
|
-
/** Switch right pane to a specific session window on the genie agent server */
|
|
50
|
-
export function attachProjectWindow(rightPane: string, targetSession: string, windowIndex?: number): void {
|
|
51
|
-
// Guard: never attach the TUI session to itself (causes infinite loop)
|
|
52
|
-
if (targetSession === SESSION_NAME) return;
|
|
53
|
-
const pane = resolveRightPane(rightPane);
|
|
54
|
-
ensureAgentSession(targetSession);
|
|
55
50
|
if (windowIndex !== undefined) {
|
|
56
51
|
try {
|
|
57
|
-
const agentTmux = `tmux -L ${GENIE_AGENT_SOCKET}`;
|
|
58
52
|
execSync(`${agentTmux} select-window -t '${targetSession}:${windowIndex}'`, { stdio: 'ignore' });
|
|
59
|
-
} catch {
|
|
60
|
-
// window may not exist
|
|
61
|
-
}
|
|
53
|
+
} catch {}
|
|
62
54
|
}
|
|
55
|
+
|
|
56
|
+
// Hide green status bar
|
|
63
57
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
72
|
-
} catch {
|
|
73
|
-
|
|
74
|
-
|
|
58
|
+
execSync(`${agentTmux} set-option -t '${targetSession}' status off 2>/dev/null`, { stdio: 'ignore' });
|
|
59
|
+
} catch {}
|
|
60
|
+
|
|
61
|
+
// respawn-pane with a loop wrapper — the pane process is bash running a loop,
|
|
62
|
+
// so if the attach ends (agent exit, detach), the loop retries and the pane survives.
|
|
63
|
+
try {
|
|
64
|
+
const cmd = `while true; do TMUX='' ${agentTmux} attach-session -t '${targetSession}' 2>/dev/null; sleep 0.3; done`;
|
|
65
|
+
execSync(`${TMUX} respawn-pane -k -t ${pane} "bash -c '${cmd}'"`, { stdio: 'ignore' });
|
|
66
|
+
} catch {}
|
|
67
|
+
|
|
68
|
+
// Restore focus to left pane
|
|
69
|
+
try {
|
|
70
|
+
execSync(`${TMUX} select-pane -t ${SESSION_NAME}:0.0`, { stdio: 'ignore' });
|
|
71
|
+
} catch {}
|
|
75
72
|
}
|
|
76
73
|
|
|
77
|
-
/** Attach to the TUI session on default tmux server (blocking call) */
|
|
78
74
|
export function attachTuiSession(): void {
|
|
79
|
-
spawnSync('tmux', ['attach-session', '-t', SESSION_NAME], {
|
|
75
|
+
spawnSync('tmux', ['-L', TMUX_SOCKET, '-f', TUI_TMUX_CONF, 'attach-session', '-t', SESSION_NAME], {
|
|
76
|
+
stdio: 'inherit',
|
|
77
|
+
});
|
|
80
78
|
}
|