aicodeman 1.31.0 → 1.32.0

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.
Files changed (101) hide show
  1. package/README.md +7 -6
  2. package/dist/web/public/admin-ui.js.gz +0 -0
  3. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  4. package/dist/web/public/{app.8df358cd.js → app.a792bf71.js} +2 -2
  5. package/dist/web/public/app.a792bf71.js.br +0 -0
  6. package/dist/web/public/{app.8df358cd.js.gz → app.a792bf71.js.gz} +0 -0
  7. package/dist/web/public/approvals-ui.js.gz +0 -0
  8. package/dist/web/public/{constants.85ed12f3.js → constants.b26748e9.js} +49 -0
  9. package/dist/web/public/constants.b26748e9.js.br +0 -0
  10. package/dist/web/public/constants.b26748e9.js.gz +0 -0
  11. package/dist/web/public/cron-ui.js.gz +0 -0
  12. package/dist/web/public/entrance-animations.js.gz +0 -0
  13. package/dist/web/public/home-sessions.js.gz +0 -0
  14. package/dist/web/public/host-wake-ui.js.gz +0 -0
  15. package/dist/web/public/i18n.d22be5c4.js +1 -0
  16. package/dist/web/public/i18n.d22be5c4.js.br +0 -0
  17. package/dist/web/public/i18n.d22be5c4.js.gz +0 -0
  18. package/dist/web/public/{image-input.cd4b97c4.js → image-input.a216cf9e.js} +8 -4
  19. package/dist/web/public/image-input.a216cf9e.js.br +0 -0
  20. package/dist/web/public/image-input.a216cf9e.js.gz +0 -0
  21. package/dist/web/public/index.html +14 -11
  22. package/dist/web/public/index.html.br +0 -0
  23. package/dist/web/public/index.html.gz +0 -0
  24. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  25. package/dist/web/public/{keyboard-accessory.2cf04f17.js → keyboard-accessory.c1e7c7b7.js} +280 -15
  26. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.br +0 -0
  27. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
  28. package/dist/web/public/mobile-handlers.6f354a87.js.gz +0 -0
  29. package/dist/web/public/mobile-overview.js.gz +0 -0
  30. package/dist/web/public/mobile.6caaa28a.css.gz +0 -0
  31. package/dist/web/public/notification-manager.36ea4624.js.gz +0 -0
  32. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  33. package/dist/web/public/panels-ui.28df5053.js.gz +0 -0
  34. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  35. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  36. package/dist/web/public/readmymind-ui.js.gz +0 -0
  37. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  38. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  39. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  40. package/dist/web/public/session-lineage.js.gz +0 -0
  41. package/dist/web/public/session-ui.7df17fd3.js +88 -0
  42. package/dist/web/public/session-ui.7df17fd3.js.br +0 -0
  43. package/dist/web/public/session-ui.7df17fd3.js.gz +0 -0
  44. package/dist/web/public/settings-ui.df5ffb68.js +78 -0
  45. package/dist/web/public/settings-ui.df5ffb68.js.br +0 -0
  46. package/dist/web/public/settings-ui.df5ffb68.js.gz +0 -0
  47. package/dist/web/public/styles.74f312e5.css +1 -0
  48. package/dist/web/public/styles.74f312e5.css.br +0 -0
  49. package/dist/web/public/styles.74f312e5.css.gz +0 -0
  50. package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
  51. package/dist/web/public/sw.js.gz +0 -0
  52. package/dist/web/public/tab-rail-resize.42c24949.js.gz +0 -0
  53. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.gz +0 -0
  54. package/dist/web/public/terminal-split.js +804 -0
  55. package/dist/web/public/terminal-split.js.br +0 -0
  56. package/dist/web/public/terminal-split.js.gz +0 -0
  57. package/dist/web/public/terminal-ui.32fbeaf6.js +2 -0
  58. package/dist/web/public/terminal-ui.32fbeaf6.js.br +0 -0
  59. package/dist/web/public/terminal-ui.32fbeaf6.js.gz +0 -0
  60. package/dist/web/public/ultracode-panel.js.gz +0 -0
  61. package/dist/web/public/ultracode-windows.js.gz +0 -0
  62. package/dist/web/public/upload.html.gz +0 -0
  63. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  64. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  65. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  66. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  67. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  68. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  69. package/dist/web/public/vendor/xterm-predictive-echo.bd6882b8.js.gz +0 -0
  70. package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
  71. package/dist/web/public/vendor/xterm.css.gz +0 -0
  72. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  73. package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
  74. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  75. package/dist/web/public/webview-tabs.js.gz +0 -0
  76. package/dist/web/server.d.ts.map +1 -1
  77. package/dist/web/server.js +13 -5
  78. package/dist/web/server.js.map +1 -1
  79. package/package.json +1 -1
  80. package/dist/web/public/app.8df358cd.js.br +0 -0
  81. package/dist/web/public/constants.85ed12f3.js.br +0 -0
  82. package/dist/web/public/constants.85ed12f3.js.gz +0 -0
  83. package/dist/web/public/i18n.83924614.js +0 -1
  84. package/dist/web/public/i18n.83924614.js.br +0 -0
  85. package/dist/web/public/i18n.83924614.js.gz +0 -0
  86. package/dist/web/public/image-input.cd4b97c4.js.br +0 -0
  87. package/dist/web/public/image-input.cd4b97c4.js.gz +0 -0
  88. package/dist/web/public/keyboard-accessory.2cf04f17.js.br +0 -0
  89. package/dist/web/public/keyboard-accessory.2cf04f17.js.gz +0 -0
  90. package/dist/web/public/session-ui.d4aea71a.js +0 -88
  91. package/dist/web/public/session-ui.d4aea71a.js.br +0 -0
  92. package/dist/web/public/session-ui.d4aea71a.js.gz +0 -0
  93. package/dist/web/public/settings-ui.4762f480.js +0 -78
  94. package/dist/web/public/settings-ui.4762f480.js.br +0 -0
  95. package/dist/web/public/settings-ui.4762f480.js.gz +0 -0
  96. package/dist/web/public/styles.e34fea04.css +0 -1
  97. package/dist/web/public/styles.e34fea04.css.br +0 -0
  98. package/dist/web/public/styles.e34fea04.css.gz +0 -0
  99. package/dist/web/public/terminal-ui.35812aa2.js +0 -2
  100. package/dist/web/public/terminal-ui.35812aa2.js.br +0 -0
  101. package/dist/web/public/terminal-ui.35812aa2.js.gz +0 -0
@@ -0,0 +1,88 @@
1
+ "use strict";const RUN_MODE_LAUNCH={opencode:{label:"OpenCode",installHint:"OpenCode CLI not found. Install with: curl -fsSL https://opencode.ai/install | bash",supportsCustomModel:!0,buildConfig:()=>({openCodeConfig:{autoAllowTools:!0}})},codex:{label:"Codex",installHint:"Codex CLI not found. Install with: npm install -g @openai/codex",supportsCustomModel:!0,buildConfig:e=>({codexConfig:{dangerouslyBypassApprovals:e.codexDangerouslyBypassApprovals??!1,animations:e.codexAnimationsEnabled??!1,renderMode:"hybrid"}})},gemini:{label:"Gemini",installHint:"Gemini CLI not found. Install with: npm install -g @google/gemini-cli",supportsCustomModel:!0,buildConfig:()=>({geminiConfig:{approvalMode:"yolo"}})},antigravity:{label:"Antigravity",installHint:"Antigravity CLI not found. Install with: curl -fsSL https://antigravity.google/cli/install.sh | bash",supportsCustomModel:!1,buildConfig:()=>({antigravityConfig:{dangerouslySkipPermissions:!0}})},pi:{label:"Pi",installHint:"Pi CLI not found. Install with: npm install -g --ignore-scripts @earendil-works/pi-coding-agent",supportsCustomModel:!0,buildConfig:()=>null},omp:{label:"OMP",installHint:"OMP CLI not found. Install with: curl -fsSL https://omp.sh/install | sh",supportsCustomModel:!0,buildConfig:()=>null},grok:{label:"Grok",installHint:"Grok CLI not found. Install with: curl -fsSL https://x.ai/cli/install.sh | bash",supportsCustomModel:!0,buildConfig:()=>({grokConfig:{alwaysApprove:!0}})},deepseek:{label:"DeepSeek",installHint:"DeepSeek Harness CLI (dsh) not found. Install with: npm install -g @deepseek-ai/dsh",unrunnableHint:"No interactive DeepSeek Harness profile is installed. DeepSeek ships only web and headless profiles, so the terminal agent comes from a plugin. Install one from the Run menu, or run: dsh plugin --profile dsh-tui add @deepseek-harness-tui/dsh-tui",supportsCustomModel:!0,buildConfig:()=>({deepSeekConfig:{permissionMode:"danger-full-access"}})}},EXTERNAL_CLI_MODES=new Set(Object.keys(RUN_MODE_LAUNCH));Object.assign(CodemanApp.prototype,{buildEnvOverrides(e,t){const s={};return(e?.agentTeams||t?.agentTeamsEnabled)&&(s.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS="1"),s},getEffortSetting(e){const t=e?.thinkingEffort;return["low","medium","high","xhigh","max","ultracode"].includes(t)?t:void 0},formatCasePickerLabel(e){return e?.location==="remote"&&e.remote?.hostId?`${e.name} @ ${e.remote.hostId}`:e?.location==="docker"?`${e.name} (${this.dockerCaseTag(e.docker?.hostId)})`:e?.name||""},dockerCaseTag(e){return!e||e==="default"||e==="local"||/^q-/.test(e)?"docker":e},buildCasePickerOptions(e=[]){const t=[],s=new Set;for(const o of e)!o?.name||s.has(o.name)||(s.add(o.name),t.push(o));return s.has("testcase")||t.push({name:"testcase"}),t.map(o=>{const n=this.formatCasePickerLabel(o),a=[o.name,n,o.path,o.location,o.remote?.hostId,o.remote?.label,o.remote?.path,o.docker?.container,o.docker?.image,o.docker?.path].filter(Boolean).join(" ").toLowerCase();return{name:o.name,label:n,case:o,searchText:a}}).sort((o,n)=>o.label.localeCompare(n.label,void 0,{sensitivity:"base",numeric:!0}))},filterCasePickerOptions(e,t){const s=String(t||"").trim().toLowerCase().split(/\s+/).filter(Boolean);return s.length===0?e:e.filter(o=>s.every(n=>o.searchText.includes(n)))},getCasePickerOptions(){return this.buildCasePickerOptions(this.cases||[])},updateCasePickerInput(e){const t=document.getElementById("quickStartCaseSearch");if(!t)return;const s=this.getCasePickerOptions().find(o=>o.name===e);t.value=s?.label||e||"testcase",t.title=s?.label||t.value},renderQuickStartCaseSelectOptions(e,t){e&&(e.innerHTML=t.map(s=>`<option value="${escapeHtml(s.name)}">${escapeHtml(s.label)}</option>`).join(""))},openCasePicker(e=""){const t=document.getElementById("quickStartCaseSearch"),s=document.getElementById("quickStartCaseList");!t||!s||(this._casePickerOpen=!0,this._casePickerFilter=e,this._casePickerActiveIndex=0,t.setAttribute("aria-expanded","true"),this.renderCasePickerList())},closeCasePicker(){const e=document.getElementById("quickStartCaseSearch"),t=document.getElementById("quickStartCaseList");this._casePickerOpen=!1,this._casePickerFilter="",e?.setAttribute("aria-expanded","false"),e?.removeAttribute("aria-activedescendant"),t?.classList.add("hidden")},renderCasePickerList(){const e=document.getElementById("quickStartCaseSearch"),t=document.getElementById("quickStartCaseList"),s=document.getElementById("quickStartCase");if(!e||!t||!s)return;const o=this.filterCasePickerOptions(this.getCasePickerOptions(),this._casePickerFilter||""),n=s.value||"testcase",a=Math.max(0,o.length-1);if(this._casePickerActiveIndex=Math.min(Math.max(this._casePickerActiveIndex||0,0),a),o.length===0){t.innerHTML='<div class="case-combobox-empty">No cases match</div>',t.classList.remove("hidden"),e.removeAttribute("aria-activedescendant");return}t.innerHTML=o.map((i,r)=>{const c=r===this._casePickerActiveIndex,m=i.name===n;return`
2
+ <button
3
+ type="button"
4
+ id="${`quickStartCaseOption-${r}`}"
5
+ class="case-combobox-option ${c?"active":""} ${m?"selected":""}"
6
+ role="option"
7
+ aria-selected="${m?"true":"false"}"
8
+ data-case="${escapeHtml(i.name)}"
9
+ title="${escapeHtml(i.label)}">
10
+ <span class="case-combobox-check">${m?"\u2713":""}</span>
11
+ <span class="case-combobox-option-label">${escapeHtml(i.label)}</span>
12
+ </button>
13
+ `}).join(""),t.classList.remove("hidden"),e.setAttribute("aria-activedescendant",`quickStartCaseOption-${this._casePickerActiveIndex}`)},selectQuickStartCase(e,{save:t=!0}={}){const s=document.getElementById("quickStartCase");if(!s)return;s.value=e||"testcase",this.updateCasePickerInput(s.value),this.closeCasePicker(),this.updateDirDisplayForCase(s.value),this.updateMobileCaseLabel(s.value);const o=(this.cases||[]).find(n=>n.name===s.value);o?.location==="docker"&&this._probeDockerCaseModes(o,null),t&&this.saveLastUsedCase(s.value)},setupQuickStartCasePicker(){const e=document.getElementById("quickStartCase"),t=document.getElementById("quickStartCaseSearch"),s=document.getElementById("quickStartCaseList"),o=document.getElementById("quickStartCasePicker");!e||!t||!s||!o||t.dataset.listenerAdded||(t.addEventListener("focus",()=>{t.select?.(),this.openCasePicker("")}),t.addEventListener("click",()=>{t.select?.(),this.openCasePicker("")}),t.addEventListener("input",()=>{this.openCasePicker(t.value)}),t.addEventListener("keydown",n=>{const a=this.filterCasePickerOptions(this.getCasePickerOptions(),this._casePickerFilter||t.value);if(n.key==="ArrowDown")n.preventDefault(),this._casePickerActiveIndex=Math.min((this._casePickerActiveIndex||0)+1,Math.max(0,a.length-1)),this._casePickerOpen?this.renderCasePickerList():this.openCasePicker(t.value);else if(n.key==="ArrowUp")n.preventDefault(),this._casePickerActiveIndex=Math.max((this._casePickerActiveIndex||0)-1,0),this._casePickerOpen?this.renderCasePickerList():this.openCasePicker(t.value);else if(n.key==="Enter"){const i=a[this._casePickerActiveIndex||0];i&&(n.preventDefault(),this.selectQuickStartCase(i.name),this.run?.())}else n.key==="Escape"?(n.preventDefault(),this.updateCasePickerInput(e.value),this.closeCasePicker()):n.key==="Tab"&&(this.updateCasePickerInput(e.value),this.closeCasePicker())}),s.addEventListener("mousedown",n=>n.preventDefault()),s.addEventListener("click",n=>{const a=n.target.closest?.(".case-combobox-option");a?.dataset?.case&&this.selectQuickStartCase(a.dataset.case)}),document.addEventListener&&!this._casePickerDocumentListenerAdded&&(document.addEventListener("pointerdown",n=>{o.contains(n.target)||(this.updateCasePickerInput(e.value),this.closeCasePicker())}),this._casePickerDocumentListenerAdded=!0),t.dataset.listenerAdded="true")},async loadQuickStartCases(e=null,t=null){try{let s=null;try{const r=t?await t:await fetch("/api/settings").then(c=>c.ok?c.json():null).then(c=>c?.data??null);r&&(s=r.lastUsedCase||null)}catch{}const n=(await(await fetch("/api/cases")).json()).data;this.cases=n,console.log("[loadQuickStartCases] Loaded cases:",n.map(r=>r.name),"lastUsedCase:",s);const a=document.getElementById("quickStartCase"),i=this.getCasePickerOptions();if(this.renderQuickStartCaseSelectOptions(a,i),console.log("[loadQuickStartCases] Set options:",a.innerHTML.substring(0,200)),e)a.value=e,this.updateDirDisplayForCase(e),this.updateMobileCaseLabel(e);else if(s&&n.some(r=>r.name===s))a.value=s,this.updateDirDisplayForCase(s),this.updateMobileCaseLabel(s);else if(n.length>0){const r=n.find(c=>c.name==="testcase")||n[0];a.value=r.name,this.updateDirDisplayForCase(r.name),this.updateMobileCaseLabel(r.name)}else a.value="testcase",document.getElementById("dirDisplay").textContent="~/codeman-cases/testcase",this.updateMobileCaseLabel("testcase");this.updateCasePickerInput(a.value),this.renderCasePickerList(),this.closeCasePicker(),a.dataset.listenerAdded||(a.addEventListener("change",()=>{this.updateDirDisplayForCase(a.value),this.saveLastUsedCase(a.value),this.updateMobileCaseLabel(a.value),this.updateCasePickerInput(a.value)}),a.dataset.listenerAdded="true"),this.setupQuickStartCasePicker(),this._refreshMobileOverviewIfVisible?.()}catch(s){console.error("Failed to load cases:",s)}},async updateDirDisplayForCase(e){try{const s=(await(await fetch(`/api/cases/${e}`)).json()).data;s.path&&(document.getElementById("dirDisplay").textContent=s.path,document.getElementById("dirInput").value=s.path)}catch{document.getElementById("dirDisplay").textContent=e}},async saveLastUsedCase(e){try{await fetch("/api/settings",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({lastUsedCase:e})})}catch(t){console.error("Failed to save last used case:",t)}},async quickStart(){return this.run()},async _ensureCreatedSessionVisible(e,t){if(!e)return;let s=t;if(!s&&!this.sessions?.has(e)){const n=await(await fetch(`/api/sessions/${encodeURIComponent(e)}`)).json();if(!n.success)throw new Error(n.error||"Failed to load the new session");s=n.data?.session||n.data}s?.id&&this._onSessionCreated(s),this._renderSessionTabsImmediate?.()},async run(){if(this._runInFlight)return;const e=Date.now(),t=Number.isFinite(this._runMinLockMs)?this._runMinLockMs:500,s=document.getElementById("runBtn");this._runInFlight=!0,s&&(s.disabled=!0,s.setAttribute("aria-busy","true"));try{const o=this._runMode||"claude";return o==="shell"?await this.runShell():o==="claude"||!EXTERNAL_CLI_MODES.has(o)?await this.runClaude():await this._runCliMode(o)}finally{const o=t-(Date.now()-e);o>0&&await new Promise(n=>setTimeout(n,o)),this._runInFlight=!1,s&&(s.disabled=!1,s.removeAttribute("aria-busy"))}},setRunMode(e){this._runMode=e;try{localStorage.setItem("codeman_runMode",e)}catch{}this._applyRunMode(),this._apiPut("/api/settings",{runMode:e}).catch(()=>{}),document.getElementById("runModeMenu")?.classList.remove("active")},toggleRunModeMenu(e){e?.stopPropagation();const t=document.getElementById("runModeMenu");if(t&&(t.classList.toggle("active"),t.querySelectorAll(".run-mode-option").forEach(s=>{s.classList.toggle("selected",s.dataset.mode===this.runMode)}),t.classList.contains("active"))){this._loadRunModeHistory(),this._refreshRunModeAvailability(t),this._refreshCustomModelRunOptions(t);const s=o=>{t.contains(o.target)||(t.classList.remove("active"),document.removeEventListener("click",s))};setTimeout(()=>document.addEventListener("click",s),0)}},_refreshRunModeAvailability(e){const t=document.getElementById("quickStartCase")?.value,s=t?(this.cases||[]).find(m=>m.name===t):null,o=s?.location==="docker",n=o&&(this._dockerCaseModes?.[t]||s.docker?.availableModes)||null;o&&!this._dockerCaseModes?.[t]&&this._probeDockerCaseModes(s,e);const a=o?this._dockerCaseProbeError?.[t]:null;for(const m of["claude","opencode","codex","gemini","antigravity","pi","grok","deepseek","omp"]){const d=e.querySelector(`.run-mode-option[data-mode="${m}"]`);if(!d)continue;let l;o?l=a?!1:n?n.includes(m):!0:l=this.isCliAvailable(m),d.style.display=l?"flex":"none"}this._renderRunModeNotice(e,a);const i=window.__codemanCliAvailable||{},r=e.querySelector("#runModeDeepSeekInstall");r&&(r.style.display=!i.deepseek&&i.deepseekBinary?"flex":"none");const c=e.querySelector("#runModeDeepSeekWeb");c&&(c.style.display=i.deepseekBinary?"flex":"none")},async _refreshCustomModelRunOptions(e){const t=e.querySelector("#runModeCustomModelSep"),s=e.querySelector("#runModeCustomModelHeader"),o=e.querySelector("#runModeCustomModels");if(!o)return;const n=()=>{t&&(t.style.display="none"),s&&(s.style.display="none"),o.innerHTML=""},a=this.loadAppSettingsFromStorage(),i=(window.__codemanCustomModelClis||[]).filter(l=>this.isCliAvailable(l.id));if(!a.customModelEndpointsEnabled||i.length===0)return n();const r=document.getElementById("quickStartCase")?.value,c=r?(this.cases||[]).find(l=>l.name===r):null;if(c?.location==="remote"||c?.location==="docker")return n();const m=await this._apiJson("/api/model-endpoints");if(!Array.isArray(m)||m.length===0)return n();const d=[];for(const l of m){const h=l.models||[];if(h.length===0)continue;const p=l.defaultModelId||h[0];for(const u of i){const f=[u.id,l.id].map(C=>escapeHtml(JSON.stringify(C))).join(", ");d.push(`
14
+ <button class="run-mode-option" data-mode="${escapeHtml(u.id)}" data-endpoint="${escapeHtml(l.id)}"
15
+ onclick="app.selectCustomModelEntry(${f})"
16
+ title="${escapeHtml(u.label)} \u2192 ${escapeHtml(l.baseUrl)} (${escapeHtml(p)}${h.length>1?`, +${h.length-1} more`:""})">
17
+ <span class="run-mode-dot ${escapeHtml(u.id)}"></span>${escapeHtml(u.label)} (${escapeHtml(l.label)})
18
+ </button>`)}}if(d.length===0)return n();t&&(t.style.display=""),s&&(s.style.display=""),o.innerHTML=d.join("")},async selectCustomModelEntry(e,t){document.getElementById("runModeMenu")?.classList.remove("active");const o=(await this._apiJson("/api/model-endpoints")||[]).find(a=>a.id===t);if(!o){this.showToast("That endpoint no longer exists","error");return}const n=o.models||[];if(n.length===0){this.showToast("No models discovered for this endpoint yet","warning");return}if(n.length===1)return this.runCustomModelEntry(e,t,n[0]);await this._openCustomModelPickModal(e,o)},_customModelLastUsedKey(e,t){return`codeman:customModelLastUsed:${e}:${t}`},_getCustomModelLastUsed(e,t){try{return localStorage.getItem(this._customModelLastUsedKey(e,t))}catch{return null}},_setCustomModelLastUsed(e,t,s){try{localStorage.setItem(this._customModelLastUsedKey(e,t),s)}catch{}},async _getCustomModelCurrentlyLoaded(e,t=800){const s=this._apiJson(`/api/model-endpoints/${encodeURIComponent(e.id)}/running-status`).catch(()=>null),o=new Promise(i=>setTimeout(()=>i(null),t)),n=await Promise.race([s,o]);return n?.isLlamaSwap&&(n.running||[]).find(i=>i.state==="ready"&&(e.models||[]).includes(i.model))?.model||null},async _openCustomModelPickModal(e,t){const s=document.getElementById("customModelPickModal"),o=document.getElementById("customModelPickList");if(!s||!o)return;const n=this._customModelPickGeneration=(this._customModelPickGeneration||0)+1,a=()=>this._customModelPickGeneration===n,i=await this._getCustomModelCurrentlyLoaded(t);if(!a())return;const r=i?null:this._getCustomModelLastUsed(e,t.id),c=i||r,m=[...t.models||[]];c&&m.includes(c)&&(m.splice(m.indexOf(c),1),m.unshift(c)),this._pendingCustomModelPick={mode:e,endpointId:t.id};const d=(window.__codemanCustomModelClis||[]).find(l=>l.id===e)?.label||e;document.getElementById("customModelPickTitle").textContent="Choose a model",document.getElementById("customModelPickHint").textContent=`${d} \u2192 ${t.label} \u2014 ${(t.models||[]).length} models discovered.`,o.innerHTML=m.map(l=>{const p=[l===i?"Currently loaded":l===r?"Last used":null,l===t.defaultModelId?"Default":null].filter(Boolean);return`
19
+ <button class="run-mode-option" onclick="app.chooseCustomModelAndRun(${escapeHtml(JSON.stringify(l))})">
20
+ <span class="run-mode-dot ${escapeHtml(e)}"></span>${escapeHtml(l)}${p.map(f=>` <span class="set-scope">${escapeHtml(f)}</span>`).join("")}
21
+ </button>`}).join(""),s.classList.add("active")},closeCustomModelPickModal(){document.getElementById("customModelPickModal")?.classList.remove("active"),this._pendingCustomModelPick=null},_confirmModelSwap(e){const t=document.getElementById("customModelSwapConfirmModal"),s=document.getElementById("customModelSwapConfirmMessage");return s&&(s.textContent=e),t?.classList.add("active"),new Promise(o=>{this._resolveModelSwapConfirmPromise=o})},_resolveModelSwapConfirm(e){document.getElementById("customModelSwapConfirmModal")?.classList.remove("active");const t=this._resolveModelSwapConfirmPromise;this._resolveModelSwapConfirmPromise=null,t?.(e)},_confirmContextWarning(e,t,s){const o=document.getElementById("customModelContextWarningModal"),n=document.getElementById("customModelContextWarningMessage");if(n){const a=typeof t=="number";n.textContent=`${e} is configured with `+(a?`only ${t.toLocaleString()} tokens of`:"an unknown (too small)")+` context, but this CLI needs roughly ${s.toLocaleString()}+ tokens just for its own system prompt and tools \u2014 before any conversation history. Its very first message will fail outright, no matter what context size Codeman tells it to expect.
22
+
23
+ To fix this, reconfigure llama-swap to give this model (or a smaller one) an explicit larger context instead of relying on auto-fit (--fit-ctx), which optimizes for the biggest MODEL that fits, not the biggest CONTEXT \u2014 e.g. add "-c 65536" (or as large a --ctx-size as your hardware holds) to its llama-swap config entry. A smaller model at a much larger explicit context often fits in the same VRAM a bigger model's auto-fit context gets shrunk to make room for.`}return o?.classList.add("active"),new Promise(a=>{this._resolveContextWarningConfirmPromise=a})},_resolveContextWarningConfirm(e){document.getElementById("customModelContextWarningModal")?.classList.remove("active");const t=this._resolveContextWarningConfirmPromise;this._resolveContextWarningConfirmPromise=null,t?.(e)},chooseCustomModelAndRun(e){const t=this._pendingCustomModelPick;this.closeCustomModelPickModal(),t&&this.runCustomModelEntry(t.mode,t.endpointId,e)},async runCustomModelEntry(e,t,s){return e==="claude"?this._runCustomModelEntryViaRestart(e,t,s):this._runCustomModelEntryOneShot(e,t,s)},async _runCustomModelEntryOneShot(e,t,s){document.getElementById("runModeMenu")?.classList.remove("active");const o=this._runMode,n=document.getElementById("tabCount"),a=n?.value;this._runMode=e,this._pendingCustomModelForLaunch={endpointId:t,modelId:s},n&&(n.value="1");try{await this.run()}finally{this._runMode=o,this._pendingCustomModelForLaunch=void 0,n&&a!==void 0&&(n.value=a)}const i=this._lastCustomModelLaunchResult;this._lastCustomModelLaunchResult=void 0,i?.modelSwapInProgress&&this._watchLlamaSwapLoading(t,s,i.sessionId)},async _quickStartWithCustomModelConfirm(e){const t=async a=>(await fetch("/api/quick-start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();let s={},o=await t(e);if(o?.data?.requiresContextWarning){const{modelId:a,contextLength:i,minSafeContextTokens:r}=o.data;if(!await this._confirmContextWarning(a,i,r))return this._lastCustomModelLaunchResult=void 0,{success:!1,error:"Launch cancelled \u2014 context window too small"};s={...s,confirmedContext:!0},o=await t({...e,customModel:{...e.customModel,...s}})}if(o?.data?.requiresConfirmation){const{currentlyLoadedModel:a,affectedSessions:i}=o.data,r=i.map(l=>l.name||l.id).join(", "),c=r?`${r} ${i.length===1?"is":"are"} currently using`:"Another session on this endpoint is currently using",m=r&&i.length>1?"those sessions":"that session";if(!await this._confirmModelSwap(`${c} ${a} on this endpoint. Switching will unload it for ${m} too. Continue?`))return this._lastCustomModelLaunchResult=void 0,{success:!1,error:"Model switch cancelled"};s={...s,confirmedSwap:!0},o=await t({...e,customModel:{...e.customModel,...s}})}const n=o?.success!==!1;return this._lastCustomModelLaunchResult=n?o?.data:void 0,n&&e.customModel&&this._setCustomModelLastUsed(e.mode,e.customModel.endpointId,e.customModel.modelId),o},async _runCustomModelEntryViaRestart(e,t,s){document.getElementById("runModeMenu")?.classList.remove("active");const o=this._runMode,n=this.activeSessionId,a=document.getElementById("tabCount"),i=a?.value;this._runMode=e,a&&(a.value="1");try{await this.run()}finally{this._runMode=o,a&&i!==void 0&&(a.value=i)}const r=this.activeSessionId;if(!r||r===n)return;const c=this._showCenterStatus(`Claude started \u2014 switching to ${t}\u2026`);await this._apiJson(`/api/sessions/${r}/wait?until=idle&timeout=20000`);let{ok:m,data:d,res:l}=await this._applyCustomModelToSession(r,t,s),h=d?.success!==!1?d?.data:void 0,p={};if(m&&h?.requiresContextWarning){if(!await this._confirmContextWarning(h.modelId,h.contextLength,h.minSafeContextTokens)){c?.dismiss(),this.showToast("Kept the native backend \u2014 context window too small","info");return}p={...p,confirmedContext:!0},{ok:m,data:d,res:l}=await this._applyCustomModelToSession(r,t,s,p),h=d?.success!==!1?d?.data:void 0}if(m&&h?.requiresConfirmation){const u=h.affectedSessions.map(g=>g.name||g.id).join(", "),f=u?`${u} ${h.affectedSessions.length===1?"is":"are"} currently using`:"Another session on this endpoint is currently using",C=u&&h.affectedSessions.length>1?"those sessions":"that session";if(!await this._confirmModelSwap(`${f} ${h.currentlyLoadedModel} on this endpoint. Switching to ${s} will unload it for ${C} too. Continue?`)){c?.dismiss(),this.showToast("Kept the native backend \u2014 model switch cancelled","info");return}p={...p,confirmedSwap:!0},{ok:m,data:d,res:l}=await this._applyCustomModelToSession(r,t,s,p),h=d?.success!==!1?d?.data:void 0}if(!m||!d||d.success===!1){c?.dismiss();const u=d?.error?`: ${d.error}`:l?` (HTTP ${l.status})`:" (request failed)";this.showToast(`Session started on the native backend \u2014 could not apply the custom endpoint${u}`,"error",{duration:0});return}if(this._setCustomModelLastUsed(e,t,s),h?.modelSwapInProgress){c?.dismiss(),this._watchLlamaSwapLoading(t,s,r);return}c?.setMessage(`Pointed at ${t} \u2014 restarting the session...`),setTimeout(()=>c?.dismiss(),3e3)},async _applyCustomModelToSession(e,t,s,o){const n=await this._api(`/api/sessions/${e}/custom-model`,{method:"POST",body:{endpointId:t,modelId:s,...o||{}}}),a=n?await n.json().catch(()=>null):null;return{ok:!!n,data:a,res:n}},async _lookupModelSizeGB(e,t){const s=await this._apiJson("/api/model-endpoints").catch(()=>null);if(!Array.isArray(s))return;const n=s.find(a=>a.id===e)?.modelSizesGB?.[t];return typeof n=="number"&&Number.isFinite(n)&&n>0?n:void 0},_formatLlamaLogLine(e){return typeof e=="string"?e.replace(/^[\d.]+\s+[IWE]\s+\S+\s+/,""):e},async _watchLlamaSwapLoading(e,t,s,o=1e3){const n=this._watchLlamaSwapGeneration=(this._watchLlamaSwapGeneration||0)+1,a=()=>this._watchLlamaSwapGeneration===n,i=await this._lookupModelSizeGB(e,t);if(!a())return;const r=i?` (${i.toFixed(1)} GB)`:"",c=`Loading ${t}${r} on ${e} \u2014 this can take a while depending on your hardware and the model size.`,m=h=>{const p=this._formatLlamaLogLine(h);return c+(p?`
24
+ llama.cpp: ${p}`:"")};let d=!1;const l=this._showCenterStatus(m(),{onCancel:()=>{d=!0}});for(;!d;){const h=await this._apiJson(`/api/model-endpoints/${encodeURIComponent(e)}/running-status`);if(!a())return;if(d)break;if(h)if(h.isLlamaSwap){if(h.running.some(p=>p.model===t&&p.state==="ready")){l?.dismiss(),this.showToast(`${t} is ready`,"success",{duration:2500});return}}else{l?.dismiss();return}if(!a()||d)break;l?.setMessage(m(h?.logLine)),await new Promise(p=>setTimeout(p,o))}if(a()&&(l?.dismiss(),this.showToast(`Cancelled loading ${t} on ${e}`+(s?" \u2014 the session has been closed.":"."),"info"),s))try{await this.closeSession(s)}catch{}},async runDeepSeekWeb(){document.getElementById("runModeMenu")?.classList.remove("active");const e=this._beginSessionLaunchStatus("Starting the DeepSeek web UI...");try{const s=await(await fetch("/api/deepseek/web",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({authority:location.host})})).json();if(!s.success)throw new Error(s.error||"Failed to start the DeepSeek web UI");const o=s.data.url;let n=[...this.webviews?.values()||[]].find(a=>a.managed==="deepseek-web");if(n){const i=await(await fetch(`/api/webviews/${n.id}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:o,trusted:!0})})).json();if(!i.success)throw new Error(i.error||"Failed to update the web tab");n=i.data.webview||i.data}else{const i=await(await fetch("/api/webviews",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:"DeepSeek Harness",url:o,icon:"\u{1F433}",managed:"deepseek-web",trusted:!0})})).json();if(!i.success)throw new Error(i.error||"Failed to save the web tab");n=i.data.webview||i.data}await this.refreshWebviews?.(),this._appendSessionLaunchStatus(e,`Serving on ${o} - opening it as a tab.`),n?.id&&await this.openWebview(n.id)}catch(t){this._reportSessionLaunchError(e,t.message)}},async installDeepSeekProfile(){const t=this._beginSessionLaunchStatus("Installing a DeepSeek terminal profile (this can take a minute)...");try{const o=await(await fetch("/api/deepseek/install-profile",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})})).json();if(!o.success)throw new Error(o.error||"Failed to install the profile");window.__codemanCliAvailable={...window.__codemanCliAvailable||{},deepseek:!!o.data.runnable},this._appendSessionLaunchStatus(t,`Installed ${o.data.package} into profile "${o.data.profile}".`),this.showToast?.(`DeepSeek profile "${o.data.profile}" installed`,"success");const n=document.getElementById("runModeMenu");n&&this._refreshRunModeAvailability(n)}catch(s){this._reportSessionLaunchError(t,s.message)}},_renderRunModeNotice(e,t){if(!e)return;let s=e.querySelector(".run-mode-notice");if(!t){s?.remove();return}s||(s=document.createElement("div"),s.className="run-mode-notice",e.prepend(s)),s.textContent=t},async _probeDockerCaseModes(e,t){const s=e?.name,o=e?.docker?.container,n=e?.docker?.hostId;if(!(!s||!o||!n)&&(this._dockerCaseModes=this._dockerCaseModes||{},!this._dockerModeProbeInFlight?.[s])){this._dockerModeProbeInFlight=this._dockerModeProbeInFlight||{},this._dockerModeProbeInFlight[s]=!0;try{const a=await this._apiJson("/api/docker-cases/adopt-preflight",{method:"POST",body:{hostId:n,container:o}});a?.ok&&Array.isArray(a.availableModes)?(this._dockerCaseModes[s]=a.availableModes,delete this._dockerCaseProbeError?.[s]):(e?.docker?.owned===!1&&(this._dockerCaseProbeError=this._dockerCaseProbeError||{},this._dockerCaseProbeError[s]=a?.error||`Could not read container "${o}".`),delete this._dockerCaseModes[s]),t?.classList.contains("active")&&this._refreshRunModeAvailability(t)}finally{delete this._dockerModeProbeInFlight[s]}}},async _loadRunModeHistory(){const e=document.getElementById("runModeHistory");if(e){e.innerHTML='<div class="run-mode-hist-empty">Loading...</div>';try{const t=await this._fetchHistorySessions(10);if(t.length===0){e.innerHTML='<div class="run-mode-hist-empty">No history</div>';return}e.replaceChildren();for(const s of t){const o=new Date(s.lastModified),n=o.toLocaleDateString("en",{month:"short",day:"numeric"})+" "+o.toLocaleTimeString("en",{hour:"2-digit",minute:"2-digit",hour12:!1}),a=this._shortenHomePath(s.workingDir),i=a.lastIndexOf("/"),r=i===-1?a:a.slice(i+1),c=(i===-1?"":a.slice(0,i)).replace(/\/\.claude\/worktrees$/,""),m=document.createElement("button");m.className="run-mode-option run-mode-hist-row",m.title=s.workingDir,m.dataset.sessionId=s.sessionId,m.dataset.workingDir=s.workingDir;const d=document.createElement("span");d.className="hist-name",d.textContent=r;const l=[d],h=this._worktreeLabel?this._worktreeLabel(s):"";if(h){const u=document.createElement("span");u.className="hist-wt",u.textContent=h,l.push(u)}if(c){const u=document.createElement("span");u.className="hist-dir",u.textContent=c,l.push(u)}const p=document.createElement("span");p.className="hist-meta",p.textContent=n,l.push(p),m.append(...l),m.addEventListener("click",u=>{u.stopPropagation(),this.resumeHistorySession(s.sessionId,s.workingDir,s.name,s.mode,s.resumeId)}),e.appendChild(m)}}catch{e.innerHTML='<div class="run-mode-hist-empty">Failed to load</div>'}}},_applyRunMode(){const e=this.runMode,t=document.getElementById("runBtn"),s=t?.nextElementSibling,o=document.getElementById("runBtnLabel");t&&(t.className=`btn-toolbar btn-run mode-${e}`),s&&(s.className=`btn-toolbar btn-run-gear mode-${e}`),o&&(o.textContent=e==="opencode"?"Run OC":e==="codex"?"Run CX":e==="gemini"?"Run GM":e==="antigravity"?"Run AG":e==="pi"?"Run PI":e==="grok"?"Run GK":e==="deepseek"?"Run DS":e==="omp"?"Run OMP":e==="shell"?"Run SH":"Run")},sendEnterKey(){if(!this.activeSessionId)return;const e=this.terminal?._core?.coreService;if(e&&typeof e.triggerDataEvent=="function"){e.triggerDataEvent("\r",!0);return}this.sendInput("\r")},_initRunMode(){try{this._runMode=localStorage.getItem("codeman_runMode")||"claude"}catch{this._runMode="claude"}this._applyRunMode()},incrementTabCount(){const e=document.getElementById("tabCount"),t=parseInt(e.value)||1;e.value=Math.min(20,t+1)},decrementTabCount(){const e=document.getElementById("tabCount"),t=parseInt(e.value)||1;e.value=Math.max(1,t-1)},incrementShellCount(){const e=document.getElementById("shellCount"),t=parseInt(e.value)||1;e.value=Math.min(20,t+1)},decrementShellCount(){const e=document.getElementById("shellCount"),t=parseInt(e.value)||1;e.value=Math.max(1,t-1)},_nextCaseSessionStartNumber(e,t="w"){const s=new RegExp(`^${t}(\\d+)-([a-zA-Z0-9_-]+)`);let o=1;for(const[,n]of this.sessions||[]){const a=n.name&&n.name.match(s);if(a&&a[2]===e){const i=parseInt(a[1]);i>=o&&(o=i+1)}}return o},_beginSessionLaunchStatus(e,t="1;32"){const s=!this.activeSessionId;return s?(this.terminal.clear(),this.terminal.writeln(`\x1B[${t}m ${e}\x1B[0m`),this.terminal.writeln("")):this.showToast?.(e,"info"),s},_appendSessionLaunchStatus(e,t,s="90"){!e||this.activeSessionId||this.terminal.writeln(`\x1B[${s}m ${t}\x1B[0m`)},_reportSessionLaunchError(e,t){e&&!this.activeSessionId?this.terminal.writeln(`\x1B[1;31m Error: ${t}\x1B[0m`):this.showToast?.(t,"error")},async runClaude(){const e=document.getElementById("quickStartCase").value||"testcase",t=this._readTabCount(),s=this._beginSessionLaunchStatus(`Starting ${t} Claude session(s) in ${e}...`);this.terminal.focus();try{let n=(await(await fetch(`/api/cases/${e}`)).json())?.data??{};if(!n.path){const v=await(await fetch("/api/cases",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,description:""})})).json();if(!v.success)throw new Error(v.error||"Failed to create case");n=v.data.case}const a=n.path;if(!a)throw new Error("Case path not found");if(n.location==="remote"||n.location==="docker"){const k=this._nextCaseSessionStartNumber(e);let v;if(n.location==="docker"){const E=this.loadAppSettingsFromStorage(),M=this.getCaseSettings(e).opusContext1m||E.opusContext1mEnabled;v=E.claudeModel||(M?"opus[1m]":"")}const S=[];let I=!1;for(let E=0;E<t;E++){const _=JSON.stringify({caseName:e,mode:"claude",sessionName:`w${k+E}-${e}`,...v!==void 0?{modelOverride:v}:{}}),M=async()=>(await fetch("/api/quick-start",{method:"POST",headers:{"Content-Type":"application/json"},body:_})).json();let b=await M();if(!b.success&&b.errorCode==="CONFLICT"&&n.location==="docker"&&!I&&(I=!0,confirm(`Container config for "${e}" changed since its container was created.
25
+
26
+ Recreate the container to apply the new config? Workspace files and the conversation survive (the conversation resumes on launch).`))){const B=await(await fetch(`/api/docker-cases/${encodeURIComponent(e)}/recreate`,{method:"POST"})).json();if(!B.success)throw new Error(B.error||"Failed to recreate container");b=await M()}if(!b.success)throw new Error(b.error||"Failed to start remote Claude session");await this._ensureCreatedSessionVisible(b.data.sessionId,b.data.session),S.push(b.data.sessionId)}this._appendSessionLaunchStatus(s,`All ${t} remote session(s) ready`),S[0]&&(await this.selectSession(S[0]),this.loadQuickStartCases()),this.terminal.focus();return}let i=null;const r=this._nextCaseSessionStartNumber(e),c=this.isRalphTrackerEnabledByDefault(),m=[];for(let k=0;k<t;k++)m.push(`w${r+k}-${e}`);const d=this.getCaseSettings(e),l=this.loadAppSettingsFromStorage(),h=this.buildEnvOverrides(d,l),p=Object.keys(h).length>0,u=this.getEffortSetting(l),f=d.opusContext1m||l.opusContext1mEnabled,C=l.claudeModel||(f?"opus[1m]":"");this._appendSessionLaunchStatus(s,`Creating ${t} session(s)...`);const y=m.map(k=>fetch("/api/sessions",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({workingDir:a,name:k,...p?{envOverrides:h}:{},...u?{effort:u}:{},...C!==void 0?{modelOverride:C}:{}})}).then(v=>v.json())),g=await Promise.all(y),w=[];for(const k of g){if(!k.success)throw new Error(k.error);await this._ensureCreatedSessionVisible(k.data.session.id,k.data.session),w.push(k.data.session.id)}i=w[0],await Promise.all(w.map(k=>fetch(`/api/sessions/${k}/ralph-config`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:c,disableAutoEnable:!c})}))),this._appendSessionLaunchStatus(s,`Starting ${t} session(s) in parallel...`),await Promise.all(w.map(k=>fetch(`/api/sessions/${k}/interactive`,{method:"POST"}))),this._appendSessionLaunchStatus(s,`All ${t} sessions ready`),i&&(await this.selectSession(i),this.loadQuickStartCases()),this.terminal.focus()}catch(o){this._reportSessionLaunchError(s,o.message)}},stopClaude(){if(!this.activeSessionId)return;const e=document.querySelector(".btn-toolbar.btn-stop");e&&(this._stopConfirmTimer?(clearTimeout(this._stopConfirmTimer),this._stopConfirmTimer=null,e.innerHTML=e.dataset.origHtml,delete e.dataset.origHtml,e.classList.remove("confirming"),fetch(`/api/sessions/${this.activeSessionId}/input`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input:""})})):(e.dataset.origHtml=e.innerHTML,e.textContent="Tap again",e.classList.add("confirming"),this._stopConfirmTimer=setTimeout(()=>{this._stopConfirmTimer=null,e.dataset.origHtml&&(e.innerHTML=e.dataset.origHtml,delete e.dataset.origHtml),e.classList.remove("confirming")},2e3)))},async runShell(){const e=document.getElementById("quickStartCase").value||"testcase",t=Math.min(20,Math.max(1,parseInt(document.getElementById("shellCount").value)||1)),s=this._beginSessionLaunchStatus(`Starting ${t} Shell session(s) in ${e}...`,"1;33");try{let n=(await(await fetch(`/api/cases/${e}`)).json())?.data??{};if(!n.path){const f=await(await fetch("/api/cases",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,description:""})})).json();if(!f.success)throw new Error(f.error||"Failed to create case");n=f.data.case}const a=(this.cases||[]).find(u=>u.name===e),i=n.location==="remote"||n.location==="docker"||a?.location==="remote"||a?.location==="docker",r=n.path;if(!r)throw new Error("Case path not found");if(n.location==="remote"||n.location==="docker"){const u=this._nextCaseSessionStartNumber(e,"s"),f=[];for(let C=0;C<t;C++){const g=await(await fetch("/api/quick-start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({caseName:e,mode:"shell",sessionName:`s${u+C}-${e}`})})).json();if(!g.success)throw new Error(g.error||"Failed to start remote shell session");await this._ensureCreatedSessionVisible(g.data.sessionId,g.data.session),f.push(g.data.sessionId)}f[0]&&await this.selectSession(f[0]),this.terminal.focus();return}const c=this._nextCaseSessionStartNumber(e,"s"),m=[];for(let u=0;u<t;u++)m.push(`s${c+u}-${e}`);const d=m.map(u=>fetch("/api/sessions",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({...i?{caseName:e}:{workingDir:r},mode:"shell",name:u})}).then(f=>f.json())),l=await Promise.all(d),h=[];for(const u of l){if(!u.success)throw new Error(u.error);await this._ensureCreatedSessionVisible(u.data.session.id,u.data.session),h.push(u.data.session.id)}await Promise.all(h.map(u=>fetch(`/api/sessions/${u}/shell`,{method:"POST"})));const p=this.getTerminalDimensions();p&&await Promise.all(h.map(u=>fetch(`/api/sessions/${u}/resize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p)}))),h.length>0&&await this.selectSession(h[0]),this.terminal.focus()}catch(o){this._reportSessionLaunchError(s,o.message)}},_readTabCount(){return Math.min(20,Math.max(1,parseInt(document.getElementById("tabCount")?.value)||1))},async _launchQuickStartInstances(e,t,s,o,n){const a=this._nextCaseSessionStartNumber(e);let i=null,r=!1;for(let c=0;c<t;c++){const m=`w${a+c}-${e}`,d=o(m),l=await this._quickStartWithCustomModelConfirm(r&&d.customModel?{...d,customModel:{...d.customModel,confirmedContext:!0,confirmedSwap:!0}}:d);if(!l.success)throw new Error(l.error||`Failed to start ${s}`);r=!0,await this._ensureCreatedSessionVisible(l.data.sessionId,l.data.session),i||(i=l.data.sessionId)}return t>1&&this._appendSessionLaunchStatus(n,`All ${t} ${s} session(s) ready`),i},async _runCliMode(e){const t=RUN_MODE_LAUNCH[e],s=document.getElementById("quickStartCase").value||"testcase",o=(this.cases||[]).find(r=>r.name===s)?.location,n=o==="remote"||o==="docker",a=this._readTabCount(),i=this._beginSessionLaunchStatus(`Starting ${a} ${t.label} session(s) in ${s}...`);this.terminal.focus();try{if(!n){const l=(await(await fetch(`/api/${e}/status`)).json()).data;if(!l.available){this._reportSessionLaunchError(i,t.installHint);return}if(t.unrunnableHint&&!l.runnable){this._reportSessionLaunchError(i,t.unrunnableHint);return}}const r=this.loadAppSettingsFromStorage(),c=this.buildEnvOverrides(this.getCaseSettings(s),r),m=await this._launchQuickStartInstances(s,a,t.label,d=>({caseName:s,mode:e,sessionName:d,...n?{}:{...t.buildConfig(r)||{},...Object.keys(c).length>0?{envOverrides:c}:{},...t.supportsCustomModel&&this._pendingCustomModelForLaunch?{customModel:this._pendingCustomModelForLaunch}:{}}}),i);m&&await this.selectSession(m),this.terminal.focus()}catch(r){this._reportSessionLaunchError(i,r.message)}},async runOpenCode(){return this._runCliMode("opencode")},async runCodex(){return this._runCliMode("codex")},async runGemini(){return this._runCliMode("gemini")},async runAntigravity(){return this._runCliMode("antigravity")},async runPi(){return this._runCliMode("pi")},async runOmp(){return this._runCliMode("omp")},async runGrok(){return this._runCliMode("grok")},async runDeepSeek(){return this._runCliMode("deepseek")},_tabDetachOverrides(){if(this._tabDetachOverrideMap===void 0)try{this._tabDetachOverrideMap=JSON.parse(localStorage.getItem("codeman:tab-detach-overrides")||"{}")||{}}catch{this._tabDetachOverrideMap={}}return this._tabDetachOverrideMap},hasTabDetachOverride(e){return!!this._tabDetachOverrides()[e]},onSessionTabDetachToggle(e){const t=this.editingSessionId;if(!t)return;const s=this._tabDetachOverrides();e?s[t]=1:delete s[t];for(const n of Object.keys(s))n!==t&&this.sessions&&!this.sessions.has(n)&&delete s[n];try{localStorage.setItem("codeman:tab-detach-overrides",JSON.stringify(s))}catch{}const o=document.querySelector(`.session-tab[data-id="${CSS.escape(t)}"]`);o&&o.classList.toggle("tab-show-detach",!!e)},openSessionOptions(e){const t=this.sessions.get(e);if(!t)return;this.editingSessionId=e;const s=document.getElementById("sessionOptShowTabDetach");s&&(s.checked=this.hasTabDetachOverride(e));const o=EXTERNAL_CLI_MODES.has(t.mode);this.switchOptionsTab(o?"summary":"respawn");const n=document.getElementById("sessionRespawnStatus"),a=document.getElementById("modalEnableRespawnBtn"),i=document.getElementById("modalStopRespawnBtn");this.respawnStatus[e]?(n.classList.add("active"),n.querySelector(".respawn-status-text").textContent=this.respawnStatus[e].state||"Active",a.style.display="none",i.style.display=""):(n.classList.remove("active"),n.querySelector(".respawn-status-text").textContent="Not active",a.style.display="",i.style.display="none");const r=document.getElementById("sessionRespawnSection");t.mode==="claude"&&t.pid?r.style.display="":r.style.display="none";const c=o;document.querySelectorAll("[data-claude-only]").forEach(y=>{y.style.display=c?"none":""}),this.selectDurationPreset(""),this.loadSavedRespawnConfig(e),document.getElementById("modalAutoCompactEnabled").checked=t.autoCompactEnabled??!1,document.getElementById("modalAutoCompactThreshold").value=t.autoCompactThreshold??11e4,document.getElementById("modalAutoCompactPrompt").value=t.autoCompactPrompt??"",document.getElementById("modalAutoClearEnabled").checked=t.autoClearEnabled??!1,document.getElementById("modalAutoClearThreshold").value=t.autoClearThreshold??14e4,document.getElementById("modalAutoResumeEnabled").checked=t.autoResumeEnabled??!1,this.updateAutoResumeStatus(e),document.getElementById("modalImageWatcherEnabled").checked=t.imageWatcherEnabled??!0,document.getElementById("modalFlickerFilterEnabled").checked=t.flickerFilterEnabled??!1;const d=parseSessionPrefix(t.name),l=document.getElementById("modalSessionPrefix");d?(l.textContent=d.prefix+": ",l.style.display="",document.getElementById("modalSessionName").value=d.suffix,document.getElementById("modalSessionName").placeholder="Add description..."):(l.style.display="none",l.textContent="",document.getElementById("modalSessionName").value=t.name||"",document.getElementById("modalSessionName").placeholder="Auto (directory name)");const h=t.color||"default";document.getElementById("sessionColorPicker")?.querySelectorAll(".color-swatch").forEach(y=>{y.classList.toggle("selected",y.dataset.color===h)}),this.renderPresetDropdown(),document.getElementById("respawnPresetSelect").value="",document.getElementById("presetDescriptionHint").textContent="";const u=document.querySelector('#sessionOptionsModal .set-rail-item[data-tab="ralph"]'),f=document.querySelector('#sessionOptionsModal .set-rail-item[data-tab="respawn"]');if(c?(u&&(u.style.display="none"),f&&(f.style.display="none"),this.switchOptionsTab("context")):(u&&(u.style.display=""),f&&(f.style.display="")),!c){const y=this.ralphStates.get(e);this.populateRalphForm({enabled:y?.loop?.enabled??t.ralphLoop?.enabled??!1,completionPhrase:y?.loop?.completionPhrase||t.ralphLoop?.completionPhrase||"",maxIterations:y?.loop?.maxIterations||t.ralphLoop?.maxIterations||0,maxTodos:y?.loop?.maxTodos||t.ralphLoop?.maxTodos,todoExpirationMinutes:y?.loop?.todoExpirationMinutes||t.ralphLoop?.todoExpirationMinutes})}const C=document.getElementById("sessionOptionsModal");C.dataset.chipsReady!=="1"&&(C.dataset.chipsReady="1",C.addEventListener("change",y=>{y.target?.closest?.(".set-chip")&&this._syncSettingsChips()})),this._syncSettingsChips(),C.classList.add("active"),this.activeFocusTrap=new FocusTrap(C),this.activeFocusTrap.activate()},_applyLocalSessionName(e,t){if(typeof t!="string")return;const s=this.sessions.get(e);s&&(s.name=t,this.sessions.set(e,s),this.updateSubagentParentNames?.(e))},async _putSessionName(e,t){const s=await this._apiPut(`/api/sessions/${e}/name`,{name:t});if(!s||!s.ok)return null;let o=null;try{o=await s.json()}catch{return null}if(o&&o.success===!1)return null;const n=o?.data?.name;return typeof n=="string"?n:t},async saveSessionName(){if(!this.editingSessionId)return;const e=this.editingSessionId,t=this.sessions.get(e),s=t?parseSessionPrefix(t.name):null,o=document.getElementById("modalSessionName").value.trim();let n;s?n=s.prefix+(o?": "+o:""):n=o;const a=await this._putSessionName(e,n);if(a===null){this.showToast("Failed to save session name","error");return}this._applyLocalSessionName(e,a),this.renderSessionTabs()},async autoSaveAutoCompact(){if(this.editingSessionId)try{await this._apiPost(`/api/sessions/${this.editingSessionId}/auto-compact`,{enabled:document.getElementById("modalAutoCompactEnabled").checked,threshold:parseInt(document.getElementById("modalAutoCompactThreshold").value)||11e4,prompt:document.getElementById("modalAutoCompactPrompt").value.trim()||void 0})}catch{}},async autoSaveAutoClear(){if(this.editingSessionId)try{await this._apiPost(`/api/sessions/${this.editingSessionId}/auto-clear`,{enabled:document.getElementById("modalAutoClearEnabled").checked,threshold:parseInt(document.getElementById("modalAutoClearThreshold").value)||14e4})}catch{}},async autoSaveAutoResume(){if(!this.editingSessionId)return;const e=document.getElementById("modalAutoResumeEnabled").checked;try{await this._apiPost(`/api/sessions/${this.editingSessionId}/auto-resume`,{enabled:e});const t=this.sessions.get(this.editingSessionId);t&&(t.autoResumeEnabled=e,e||(t.autoResumeAt=void 0)),this.updateAutoResumeStatus(this.editingSessionId),this.showToast(`Auto-resume on usage limit ${e?"enabled":"disabled"}`,"success")}catch(t){this.showToast("Failed to toggle auto-resume: "+t.message,"error")}},updateAutoResumeStatus(e){const t=document.getElementById("autoResumeStatus");if(!t||this.editingSessionId!==e)return;const s=this.sessions.get(e);if(s?.autoResumeAt&&s.autoResumeAt>Date.now()){const o=new Date(s.autoResumeAt).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});t.textContent=`Usage limit pause active \u2014 resumes at ${o}`,t.classList.add("active")}else t.textContent="",t.classList.remove("active")},async toggleSessionImageWatcher(){if(!this.editingSessionId)return;const e=document.getElementById("modalImageWatcherEnabled").checked;try{await this._apiPost(`/api/sessions/${this.editingSessionId}/image-watcher`,{enabled:e});const t=this.sessions.get(this.editingSessionId);t&&(t.imageWatcherEnabled=e),this.showToast(`Image watcher ${e?"enabled":"disabled"}`,"success")}catch{this.showToast("Failed to toggle image watcher","error")}},async toggleFlickerFilter(){if(!this.editingSessionId)return;const e=document.getElementById("modalFlickerFilterEnabled").checked;try{await this._apiPost(`/api/sessions/${this.editingSessionId}/flicker-filter`,{enabled:e});const t=this.sessions.get(this.editingSessionId);t&&(t.flickerFilterEnabled=e),this.showToast(`Flicker filter ${e?"enabled":"disabled"}`,"success")}catch{this.showToast("Failed to toggle flicker filter","error")}},async autoSaveRespawnConfig(){if(!this.editingSessionId)return;const e={updatePrompt:document.getElementById("modalRespawnPrompt").value,sendClear:document.getElementById("modalRespawnSendClear").checked,sendInit:document.getElementById("modalRespawnSendInit").checked,kickstartPrompt:document.getElementById("modalRespawnKickstart").value.trim()||void 0,autoAcceptPrompts:document.getElementById("modalRespawnAutoAccept").checked};try{await this._apiPut(`/api/sessions/${this.editingSessionId}/respawn/config`,e)}catch{}},async loadSavedRespawnConfig(e){try{const s=await(await fetch(`/api/sessions/${e}/respawn/config`)).json();if(s.success&&s.data&&s.data.config){const o=s.data.config;document.getElementById("modalRespawnPrompt").value=o.updatePrompt||"update all the docs and CLAUDE.md",document.getElementById("modalRespawnSendClear").checked=o.sendClear??!0,document.getElementById("modalRespawnSendInit").checked=o.sendInit??!0,document.getElementById("modalRespawnKickstart").value=o.kickstartPrompt||"",document.getElementById("modalRespawnAutoAccept").checked=o.autoAcceptPrompts??!0,o.durationMinutes&&(document.querySelector(`.duration-preset-btn[data-minutes="${o.durationMinutes}"]`)?this.selectDurationPreset(String(o.durationMinutes)):(this.selectDurationPreset("custom"),document.getElementById("modalRespawnDuration").value=o.durationMinutes))}}catch{}},selectDurationPreset(e){document.querySelectorAll(".duration-preset-btn").forEach(n=>n.classList.remove("active"));const t=document.querySelector(`.duration-preset-btn[data-minutes="${e}"]`);t&&t.classList.add("active");const s=document.querySelector(".duration-custom-input"),o=document.getElementById("modalRespawnDuration");e==="custom"?(s.classList.add("visible"),o.focus()):(s.classList.remove("visible"),o.value="")},getSelectedDuration(){const e=document.querySelector(".duration-custom-input"),t=document.getElementById("modalRespawnDuration");if(e.classList.contains("visible"))return t.value?parseInt(t.value):null;{const o=document.querySelector(".duration-preset-btn.active")?.dataset.minutes;return o?parseInt(o):null}},switchOptionsTab(e){document.querySelectorAll("#sessionOptionsModal .set-rail-item").forEach(s=>{s.classList.toggle("active",s.dataset.tab===e)}),document.getElementById("respawn-tab").classList.toggle("hidden",e!=="respawn"),document.getElementById("context-tab").classList.toggle("hidden",e!=="context"),document.getElementById("ralph-tab").classList.toggle("hidden",e!=="ralph"),document.getElementById("summary-tab").classList.toggle("hidden",e!=="summary");const t=document.getElementById("sessionOptionsDoc");t&&(t.scrollTop=0),e==="summary"&&this.editingSessionId&&this.loadRunSummary(this.editingSessionId)},getRalphConfig(){return{enabled:document.getElementById("modalRalphEnabled").checked,completionPhrase:document.getElementById("modalRalphPhrase").value.trim(),maxIterations:parseInt(document.getElementById("modalRalphMaxIterations").value)||0,maxTodos:parseInt(document.getElementById("modalRalphMaxTodos").value)||50,todoExpirationMinutes:parseInt(document.getElementById("modalRalphTodoExpiration").value)||60}},populateRalphForm(e){document.getElementById("modalRalphEnabled").checked=e?.enabled??!1,document.getElementById("modalRalphPhrase").value=e?.completionPhrase||"",document.getElementById("modalRalphMaxIterations").value=e?.maxIterations||0,document.getElementById("modalRalphMaxTodos").value=e?.maxTodos||50,document.getElementById("modalRalphTodoExpiration").value=e?.todoExpirationMinutes||60},async saveRalphConfig(){if(!this.editingSessionId){this.showToast("No session selected","warning");return}const e=this.getRalphConfig();e.enabled&&this.ralphClosedSessions.delete(this.editingSessionId);try{const s=await(await fetch(`/api/sessions/${this.editingSessionId}/ralph-config`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)})).json();if(s.error)throw new Error(s.error);this.showToast("Ralph config saved","success")}catch(t){this.showToast("Failed to save Ralph config: "+t.message,"error")}},startInlineRename(e){const t=this.sessions.get(e);if(!t)return;this._activeRename?.cancel();const s=document.querySelector(`.tab-name[data-session-id="${e}"]`);if(!s)return;this._inlineRenameActive=!0,s.classList.add("tab-name-renaming");const o=this.getSessionName(t),n=parseSessionPrefix(t.name),a=s.textContent,i=[...s.childNodes].map(g=>g.cloneNode(!0)),r=()=>{s.replaceChildren(...i.map(g=>g.cloneNode(!0)))};for(s.textContent="";s.firstChild;)s.removeChild(s.firstChild);if(n){const g=document.createElement("span");g.className="tab-rename-prefix",g.textContent=n.prefix+": ",g.style.cssText="color: var(--text-muted); font-size: 0.75rem; white-space: nowrap;",s.appendChild(g)}const c=document.createElement("input");c.type="text",c.value=n?n.suffix:t.name||"",c.placeholder=n?"Add description...":o,c.className="tab-rename-input";const m=s.closest(".tab-rail")?"auto":this.isSessionSidebarActive?.()?"100%":"80px";c.style.cssText=`width: ${m}; min-width: 0; font-size: 0.75rem; padding: 2px 4px; background: var(--bg-input); border: 1px solid var(--accent); border-radius: 3px; color: var(--text); outline: none;`,s.appendChild(c),c.focus(),c.select();let d=!1,l=!1,h=!1;const p=()=>{this._activeRename===y&&(this._inlineRenameActive=!1)},u=()=>{this._activeRename===y&&(h=!0,p(),this._activeRename=null,this.renderSessionTabs())},f=()=>{l||h||(l=!0,d=!0,s.classList.remove("tab-name-renaming"),r(),u())},C=async({commit:g})=>{if(d||l)return;if(d=!0,s.classList.remove("tab-name-renaming"),!g){f();return}if(this._activeRename!==y)return;p();const w=c.value.trim(),k=n?n.prefix+(w?": "+w:""):w;if(k===t.name?r():s.textContent=k||a,this.sessions.has(e)&&k!==t.name){const S=await this._putSessionName(e,k);if(l||this._activeRename!==y||!this.sessions.has(e))return;S===null?(r(),this.showToast("Failed to rename","error")):this._applyLocalSessionName(e,S)}u()},y={sessionId:e,cancel:f};this._activeRename=y,c.addEventListener("blur",()=>C({commit:!0})),c.addEventListener("keydown",g=>{g.isComposing||g.keyCode===229||(g.key==="Enter"?(g.preventDefault(),c.blur()):g.key==="Escape"&&(g.preventDefault(),f()))})},toggleCaseSettings(){const e=document.getElementById("caseSettingsPopover");if(e.classList.contains("hidden")){const t=document.getElementById("quickStartCase").value||"testcase",s=this.getCaseSettings(t);document.getElementById("caseAgentTeams").checked=s.agentTeams,document.getElementById("caseOpusContext1m").checked=s.opusContext1m,e.classList.remove("hidden");const o=n=>{!e.contains(n.target)&&!n.target.classList.contains("btn-case-settings")&&(e.classList.add("hidden"),document.removeEventListener("click",o))};setTimeout(()=>document.addEventListener("click",o),0)}else e.classList.add("hidden")},getCaseSettings(e){try{const t=localStorage.getItem("caseSettings_"+e);if(t)return JSON.parse(t)}catch{}return{agentTeams:!1,opusContext1m:!0}},saveCaseSettings(e,t){localStorage.setItem("caseSettings_"+e,JSON.stringify(t))},onCaseSettingChanged(){const e=document.getElementById("quickStartCase").value||"testcase",t=this.getCaseSettings(e);t.agentTeams=document.getElementById("caseAgentTeams").checked,t.opusContext1m=document.getElementById("caseOpusContext1m").checked,this.saveCaseSettings(e,t);const s=document.getElementById("caseAgentTeamsMobile");s&&(s.checked=t.agentTeams);const o=document.getElementById("caseOpusContext1mMobile");o&&(o.checked=t.opusContext1m)},toggleCaseSettingsMobile(){const e=document.getElementById("caseSettingsPopoverMobile");if(e.classList.contains("hidden")){const t=document.getElementById("quickStartCase").value||"testcase",s=this.getCaseSettings(t);document.getElementById("caseAgentTeamsMobile").checked=s.agentTeams,document.getElementById("caseOpusContext1mMobile").checked=s.opusContext1m,e.classList.remove("hidden");const o=n=>{!e.contains(n.target)&&!n.target.classList.contains("btn-case-settings-mobile")&&(e.classList.add("hidden"),document.removeEventListener("click",o))};setTimeout(()=>document.addEventListener("click",o),0)}else e.classList.add("hidden")},onCaseSettingChangedMobile(){const e=document.getElementById("quickStartCase").value||"testcase",t=this.getCaseSettings(e);t.agentTeams=document.getElementById("caseAgentTeamsMobile").checked,t.opusContext1m=document.getElementById("caseOpusContext1mMobile").checked,this.saveCaseSettings(e,t);const s=document.getElementById("caseAgentTeams");s&&(s.checked=t.agentTeams);const o=document.getElementById("caseOpusContext1m");o&&(o.checked=t.opusContext1m)},showCreateCaseModal(){document.getElementById("newCaseName").value="",document.getElementById("newCaseDescription").value="",document.getElementById("linkCaseName").value="",document.getElementById("linkCasePath").value="",["remoteCaseName","remoteCasePath","remoteHostId","remoteHostAddress","remoteHostUsername","remoteHostPort","remoteHostCodexCommand","remoteHostIdentityFile","remoteHostSocksProxy","remoteHostJumpHost","remoteHostExtraSshOptions","remoteHostWakeMac","remoteHostWakeCommand"].forEach(r=>{const c=document.getElementById(r);c&&(c.value="")}),this._resetCloneForm();const t=document.getElementById("caseCloneTabBtn");t&&(t.style.display=this.isCliAvailable("git")?"":"none"),this.caseModalTab="case-create",this.switchCaseModalTab("case-create");const s=document.getElementById("createCaseModal");s.querySelectorAll(".set-rail-item").forEach(r=>{r.onclick=()=>this.switchCaseModalTab(r.dataset.tab)});const o=document.getElementById("dockerAdoptExisting");o&&(o.onchange=()=>this._syncDockerAdoptMode());const n=document.getElementById("dockerAdoptCheckBtn");n&&(n.onclick=()=>this._dockerAdoptPreflight());const a=document.getElementById("dockerAdoptJumpBtn");a&&(a.onclick=()=>this.jumpToDockerAdopt());const i=document.getElementById("dockerHostId");i&&(i.onchange=()=>{delete document.getElementById("dockerContainerList")?.dataset.loadedFor,document.getElementById("dockerAdoptExisting")?.checked&&this._loadDockerContainerOptions()}),delete document.getElementById("dockerContainerList")?.dataset.loadedFor,this._syncDockerAdoptMode(),s.querySelectorAll('input[type="text"]').forEach(r=>{r._mobileScrollWired||(r._mobileScrollWired=!0,r.addEventListener("focus",()=>{window.innerWidth<600&&setTimeout(()=>r.scrollIntoView({behavior:"smooth",block:"center"}),300)}))}),s.classList.add("active"),document.getElementById("newCaseName").focus()},switchCaseModalTab(e){this.caseModalTab=e;const t=document.getElementById("createCaseModal");t.querySelectorAll(".set-rail-item").forEach(n=>{n.classList.toggle("active",n.dataset.tab===e)}),t.querySelectorAll(".set-section").forEach(n=>{n.classList.toggle("hidden",n.id!==e)});const s=document.getElementById("createCaseDoc");s&&(s.scrollTop=0);const o=["caseModalSubmit","caseModalSubmitMobile"].map(n=>document.getElementById(n)).filter(Boolean);if(e==="case-manage")o.forEach(n=>{n.style.display="none"}),this.renderCaseManageList(),this.refreshDockerExports();else{const n=e==="case-create"?"Create":e==="case-clone"?"Clone":e==="case-remote"?"Link Remote":e==="case-docker"?"Link Docker":"Link";o.forEach(a=>{a.style.display="",a.textContent=n})}e==="case-create"?document.getElementById("newCaseName").focus():e==="case-clone"?document.getElementById("cloneRepoUrl").focus():e==="case-link"?document.getElementById("linkCaseName").focus():e==="case-remote"?document.getElementById("remoteCaseName").focus():e==="case-docker"&&document.getElementById("dockerCaseName").focus()},closeCreateCaseModal(){document.getElementById("createCaseModal").classList.remove("active")},async submitCaseModal(){const e=["caseModalSubmit","caseModalSubmitMobile"].map(o=>document.getElementById(o)).filter(Boolean),t=e.map(o=>o.textContent),s=this.caseModalTab==="case-create"?"Creating...":this.caseModalTab==="case-clone"?"Cloning...":"Linking...";e.forEach(o=>{o.classList.add("loading"),o.textContent=s,o.disabled=!0});try{this.caseModalTab==="case-create"?await this.createCase():this.caseModalTab==="case-clone"?await this.cloneCase():this.caseModalTab==="case-remote"?await this.linkRemoteCase():this.caseModalTab==="case-docker"?await this.linkDockerCase():await this.linkCase()}finally{e.forEach((o,n)=>{o.classList.remove("loading"),o.disabled=!1,o.textContent=t[n]})}},async createCase(){const e=document.getElementById("newCaseName").value.trim(),t=document.getElementById("newCaseDescription").value.trim();if(!e){this.showToast("Please enter a case name","error");return}if(!/^[a-zA-Z0-9_-]+$/.test(e)){this.showToast("Invalid name. Use only letters, numbers, hyphens, underscores.","error");return}const s=document.getElementById("newCaseDocker")?.checked,o=s?"/api/cases/docker-quickcreate":"/api/cases",n=s?{name:e,description:t,...this._collectDockerQuickSettings()}:{name:e,description:t};try{const i=await(await fetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json();if(i.success)if(this.closeCreateCaseModal(),await this.loadQuickStartCases(e),await this.saveLastUsedCase(e),s){const r=i.data?.capsEnforced===!1?" (resource caps advisory on this engine)":"";this.showToast(`Docker case "${e}" created${r} \u2014 starting session\u2026`,"success"),await this.runClaude()}else this.showToast(`Case "${e}" created`,"success");else this.showToast(i.error||"Failed to create case","error")}catch(a){console.error("Failed to create case:",a),this.showToast("Failed to create case: "+a.message,"error")}},applyDockerTemplate(){const e=document.getElementById("quickDockerTemplate")?.value,s={small:{m:"2g",c:"1",g:""},medium:{m:"",c:"",g:""},large:{m:"8g",c:"4",g:""},gpu:{m:"8g",c:"4",g:"all"}}[e];if(!s)return;const o=(n,a)=>{const i=document.getElementById(n);i&&(i.value=a)};o("quickDockerMemory",s.m),o("quickDockerCpus",s.c),o("quickDockerGpus",s.g)},_collectDockerQuickSettings(){const e=c=>(document.getElementById(c)?.value||"").trim(),t={},s=e("quickDockerMemory");s&&(t.memory=s);const o=e("quickDockerCpus");o&&(t.cpus=o);const n=e("quickDockerGpus");n&&n.toLowerCase()!=="none"&&(t.gpus=n);const a=document.getElementById("quickDockerNetwork")?.value;a&&a!=="bridge"&&(t.network=a);const i=e("quickDockerImage");i&&(t.image=i);const r=document.getElementById("quickDockerMountCreds");return r&&!r.checked&&(t.mountCredentials=!1),t},async linkCase(){const e=document.getElementById("linkCaseName").value.trim(),t=document.getElementById("linkCasePath").value.trim();if(!e){this.showToast("Please enter a case name","error");return}if(!/^[a-zA-Z0-9_-]+$/.test(e)){this.showToast("Invalid name. Use only letters, numbers, hyphens, underscores.","error");return}if(!t){this.showToast("Please enter a folder path","error");return}try{const o=await(await fetch("/api/cases/link",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,path:t})})).json();o.success?(this.closeCreateCaseModal(),this.showToast(`Case "${e}" linked to ${t}`,"success"),await this.loadQuickStartCases(e),await this.saveLastUsedCase(e)):this.showToast(o.error||"Failed to link case","error")}catch(s){console.error("Failed to link case:",s),this.showToast("Failed to link case: "+s.message,"error")}},_resetCloneForm(){const e=(i,r)=>{const c=document.getElementById(i);c&&(c.value=r)};e("cloneRepoUrl",""),e("cloneCaseName",""),e("cloneRepoRef","");const t=document.getElementById("cloneShallow");t&&(t.checked=!1);const s=document.getElementById("cloneStartSession");s&&(s.checked=!1);const o=document.getElementById("cloneRepoRefOptions");o&&o.replaceChildren();const n=document.getElementById("cloneRefHint");n&&(n.textContent="Leave blank for the repository's default branch."),this._cloneNameEdited=!1,this._clonePreflight=null,clearTimeout(this._clonePreflightTimer),this._clonePreflightAbort?.abort(),this._clonePreflightAbort=null,this._setCloneStatus("Public repositories only: Codeman clones with no credentials.","");const a=document.getElementById("cloneCaseBrain");if(a){for(const r of a.options){const c=r.dataset.cli;r.hidden=!!c&&!this.isCliAvailable(c)}const i=this.runMode||"claude";a.value=[...a.options].some(r=>r.value===i&&!r.hidden)?i:""}},_setCloneStatus(e,t){const s=document.getElementById("cloneRepoStatus");s&&(s.textContent=e,s.className=`form-hint clone-status${t?" clone-status-"+t:""}`)},_repoNameFromUrl(e){const t=(e||"").trim().replace(/\/+$/,"");return t?(t.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//,"").replace(/^[^@/]*@/,"").split(/[/:]/).filter(Boolean).pop()||"").replace(/\.git$/i,"").replace(/[^a-zA-Z0-9_-]+/g,"-").replace(/-{2,}/g,"-").replace(/^[-_]+|[-_]+$/g,"").slice(0,64):""},onCloneNameEdited(){this._cloneNameEdited=!!document.getElementById("cloneCaseName")?.value.trim()},onCloneUrlInput(){const e=document.getElementById("cloneRepoUrl")?.value.trim()||"",t=document.getElementById("cloneCaseName");if(t&&!this._cloneNameEdited&&(t.value=this._repoNameFromUrl(e)),clearTimeout(this._clonePreflightTimer),this._clonePreflightAbort?.abort(),this._clonePreflightAbort=null,!e){this._setCloneStatus("Public repositories only: Codeman clones with no credentials.","");return}if(this.isCliAvailable("git")===!1){this._setCloneStatus("git is not installed on the Codeman host, so cloning is unavailable.","err");return}this._setCloneStatus("Checking the repository\u2026",""),this._clonePreflightTimer=setTimeout(()=>this._runClonePreflight(e),450)},async _runClonePreflight(e){const t=new AbortController;this._clonePreflightAbort=t;try{const o=await(await fetch("/api/cases/clone-preflight",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({repository:e}),signal:t.signal})).json();if(document.getElementById("cloneRepoUrl")?.value.trim()!==e)return;if(!o.success){this._setCloneStatus(o.error||"Could not check that URL.","err");return}this._applyClonePreflight(o.data,e)}catch(s){if(s.name==="AbortError")return;this._setCloneStatus("Could not reach Codeman to check that URL.","err")}},_applyClonePreflight(e,t){this._clonePreflight=e;const s=e?.parse;if(!s?.cloneable){this._setCloneStatus(s?.message||"That URL cannot be cloned.","err");return}const o=document.getElementById("cloneCaseName");o&&!this._cloneNameEdited&&s.suggestedName&&(o.value=s.suggestedName);const n=s.owner?`${s.provider} ${s.owner}/${s.repo}`:`${s.provider} ${s.repo}`;if(e.gitAvailable===!1){this._setCloneStatus(`${n}: git is not installed on the Codeman host.`,"err");return}const a=e.remote;if(a&&!a.reachable){this._setCloneStatus(`${n}: ${a.failure?.message||"the remote could not be read."}`,"err");return}const i=document.getElementById("cloneRefHint"),r=document.getElementById("cloneRepoRefOptions");if(a&&r){r.replaceChildren();for(const m of[...a.branches||[],...a.tags||[]]){const d=document.createElement("option");d.value=m,r.appendChild(d)}if(i){const m=`${a.branches?.length||0} branches, ${a.tags?.length||0} tags`;i.textContent=a.defaultBranch?`Blank clones the default branch (${a.defaultBranch}). ${m} available.`:`Blank clones the default branch. ${m} available.`}}const c=s.warnings?.[0];this._setCloneStatus(c?`${n}: ${c}`:`${n}: ready to clone.`,c?"warn":"ok")},async cloneCase(){const e=document.getElementById("cloneRepoUrl").value.trim(),t=document.getElementById("cloneCaseName").value.trim(),s=document.getElementById("cloneRepoRef").value.trim(),o=!!document.getElementById("cloneShallow")?.checked,n=document.getElementById("cloneCaseBrain")?.value||"",a=!!document.getElementById("cloneStartSession")?.checked;if(!e){this.showToast("Please enter a repository URL","error");return}if(!t){this.showToast("Please enter a case name","error");return}if(!/^[a-zA-Z0-9_-]+$/.test(t)){this.showToast("Invalid name. Use only letters, numbers, hyphens, underscores.","error");return}this._setCloneStatus(`Cloning ${e}\u2026 this can take a while for a large repository.`,"");try{const r=await(await fetch("/api/cases/clone",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,repository:e,...s?{ref:s}:{},...o?{shallow:!0}:{}})})).json();if(!r.success){this._setCloneStatus(r.error||"Clone failed.","err"),this.showToast(r.error||"Failed to clone repository","error");return}n&&this.setRunMode(n),this.closeCreateCaseModal(),await this.loadQuickStartCases(t),await this.saveLastUsedCase(t),this.showToast(`Cloned into case "${t}"`,"success");for(const c of r.data?.warnings||[])this.showToast(c,"warning");a&&await this.run()}catch(i){console.error("Failed to clone repository:",i),this._setCloneStatus(`Lost the connection while cloning: ${i.message}. If git finishes, the case still appears in the list.`,"warn"),this.showToast("Clone request interrupted \u2014 watch the case list","error")}},openLinkCasePathPicker(){const e=document.getElementById("linkCasePath");PathPicker.open({title:"Select Existing Project Folder",initialPath:e.value.trim(),directoriesOnly:!0,onSelect:t=>{e.value=t;const s=document.getElementById("linkCaseName");if(!s.value.trim()){const o=t.split("/").filter(Boolean).pop()||"";/^[\p{L}\p{N}_-]+$/u.test(o)&&(s.value=o)}e.focus(),e.setSelectionRange(t.length,t.length)}})},openDockerWorkspacePathPicker(){const e=document.getElementById("dockerWorkspacePath");PathPicker.open({title:"Select Host Workspace Folder",initialPath:e.value.trim(),directoriesOnly:!0,onSelect:t=>{e.value=t;const s=document.getElementById("dockerCaseName");if(s&&!s.value.trim()){const o=t.split("/").filter(Boolean).pop()||"";/^[a-zA-Z0-9_-]+$/.test(o)&&(s.value=o)}}})},openDockerWorkdirPicker(){const e=document.getElementById("dockerAdoptWorkdir"),t=document.getElementById("dockerContainerName")?.value.trim(),s=document.getElementById("dockerHostId")?.value.trim()||"local";if(!t){this.showToast("Enter the container name first","error");return}PathPicker.open({title:`Select Folder Inside ${t}`,initialPath:e.value.trim()||"/",directoriesOnly:!0,fetchListing:async o=>{const n=await this._apiJson("/api/docker-cases/browse",{method:"POST",body:{hostId:s,container:t,path:o||"/"}});return n?n.error?{success:!1,error:n.error}:{success:!0,data:{...n,root:"/",roots:[{label:t,path:"/"}],truncated:!1}}:{success:!1,error:`Could not read ${t}. Is it running?`}},onSelect:o=>{e.value=o}})},async linkRemoteCase(){const e=document.getElementById("remoteCaseName").value.trim(),t=document.getElementById("remoteCasePath").value.trim(),s=document.getElementById("remoteHostId").value.trim(),o=document.getElementById("remoteHostAddress").value.trim(),n=document.getElementById("remoteHostUsername").value.trim(),a=document.getElementById("remoteHostCodexCommand").value.trim(),i=document.getElementById("remoteHostPort").value.trim(),r=document.getElementById("remoteHostIdentityFile").value.trim(),c=document.getElementById("remoteHostSocksProxy").value.trim(),m=document.getElementById("remoteHostJumpHost").value.trim(),d=document.getElementById("remoteHostWakeMac").value.trim(),l=document.getElementById("remoteHostWakeCommand").value.trim(),h=document.getElementById("remoteHostExtraSshOptions").value.split(`
27
+ `).map(u=>u.trim()).filter(u=>u.length>0);if(!e||!t||!s||!o||!n){this.showToast("Please complete all required remote fields","error");return}if(!/^[a-zA-Z0-9_-]+$/.test(e)||!/^[a-zA-Z0-9_-]+$/.test(s)){this.showToast("Invalid name. Use only letters, numbers, hyphens, underscores.","error");return}if(!t.startsWith("/")){this.showToast("Remote path must be absolute","error");return}let p;if(i&&(p=Number(i),!Number.isInteger(p)||p<1||p>65535)){this.showToast("SSH port must be a number between 1 and 65535","error");return}try{const u={id:s,label:s,host:o,username:n,...p?{port:p}:{},...r?{identityFile:r}:{},...c?{socksProxy:c}:{},...m?{jumpHost:m}:{},...h.length?{extraSshOptions:h}:{},...d?{wakeMac:d}:{},...l?{wakeCommand:l}:{},...a?{commands:{codex:a}}:{}},C=await(await fetch("/api/remote-hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u)})).json();if(!C.success&&C.errorCode!=="ALREADY_EXISTS")throw new Error(C.error||"Failed to save remote host");const g=await(await fetch("/api/cases/remote-link",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,hostId:s,remotePath:t})})).json();g.success?(this.closeCreateCaseModal(),this.showToast(`Remote case "${e}" linked`,"success"),await this.loadQuickStartCases(e),await this.saveLastUsedCase(e)):this.showToast(g.error||"Failed to link remote case","error")}catch(u){console.error("Failed to link remote case:",u),this.showToast("Failed to link remote case: "+u.message,"error")}},_syncDockerAdoptMode(){const e=document.getElementById("createCaseModal");if(!e)return;const t=document.getElementById("dockerAdoptExisting")?.checked;t?e.setAttribute("data-docker-adopt","1"):e.removeAttribute("data-docker-adopt"),t&&(this._loadDockerContainerOptions(),this._loadDockerCloneOptions())},async _loadDockerCloneOptions(){const e=document.getElementById("dockerAdoptCloneFrom"),t=document.getElementById("dockerAdoptCloneRow");if(!e||!t)return;let s=[];try{const a=await(await fetch("/api/cases")).json();s=(Array.isArray(a)?a:a?.data||[]).filter(i=>i?.docker&&i.docker.owned===!1)}catch{s=[]}e.textContent="";const o=document.createElement("option");o.value="",o.textContent="Start from scratch",e.appendChild(o);for(const n of s){const a=document.createElement("option");a.value=n.name,a.textContent=`${n.name} \u2014 ${n.docker.container}:${n.docker.containerWorkdir||n.docker.path}`,a.dataset.container=n.docker.container,a.dataset.hostId=n.docker.hostId,a.dataset.path=n.docker.path,a.dataset.workdir=n.docker.containerWorkdir||n.docker.path,e.appendChild(a)}t.hidden=s.length===0},applyDockerCloneSource(){const e=document.getElementById("dockerAdoptCloneFrom"),t=e?.selectedOptions?.[0];if(!t||!t.value)return;const s=(n,a)=>{const i=document.getElementById(n);i&&(i.value=a||"")};s("dockerContainerName",t.dataset.container),s("dockerHostId",t.dataset.hostId),s("dockerWorkspacePath",t.dataset.path),s("dockerCaseName",t.value),s("dockerAdoptWorkdir",t.dataset.workdir),e.dataset.appliedName=t.value,e.dataset.appliedWorkdir=t.dataset.workdir||"";const o=document.getElementById("dockerAdoptWorkdir");o?.focus(),o&&o.setSelectionRange(o.value.length,o.value.length)},dockerCloneGuard(){const e=document.getElementById("dockerAdoptCloneFrom");if(!e||!e.value)return null;const t=document.getElementById("dockerCaseName"),s=document.getElementById("dockerAdoptWorkdir");return t&&t.value.trim()===(e.dataset.appliedName||"")?{el:t,message:`"${t.value.trim()}" is the case you copied from \u2014 give this one a new name.`}:s&&s.value.trim()===(e.dataset.appliedWorkdir||"")?{el:s,message:"Same container and same directory as the case you copied from \u2014 point this one at another directory."}:null},async _loadDockerContainerOptions(){const e=document.getElementById("dockerContainerList");if(!e)return;const t=document.getElementById("dockerHostId")?.value.trim()||"local";if(e.dataset.loadedFor===t)return;const o=(await this._apiJson(`/api/docker-hosts/${encodeURIComponent(t)}/containers`))?.containers||[];e.textContent="";for(const n of o){const a=document.createElement("option");a.value=n.name,a.textContent=n.running?`${n.image} \xB7 ${n.status}`:`${n.image} \xB7 ${n.status} (not running)`,e.appendChild(a)}e.dataset.loadedFor=t},jumpToDockerAdopt(){this.switchCaseModalTab("case-docker");const e=document.getElementById("dockerAdoptExisting");e&&(e.checked=!0),this._syncDockerAdoptMode(),document.getElementById("dockerContainerName")?.focus()},async _dockerAdoptPreflight(){const e=document.getElementById("dockerLinkStatus"),t=document.getElementById("dockerContainerName")?.value.trim(),s=document.getElementById("dockerAdoptWorkdir")?.value.trim(),o=document.getElementById("dockerHostId").value.trim()||"local";if(!t){e&&(e.textContent="Enter a container name first.");return}e&&(e.textContent="Inspecting container...");const n=await this._apiJson("/api/docker-cases/adopt-preflight",{method:"POST",body:{hostId:o,container:t,...s?{containerWorkdir:s}:{}}});if(!e)return;if(!n){e.textContent="Could not reach the docker host profile. Save a Host ID first.";return}if(!n.ok){e.textContent=n.error||"Container is not adoptable.";return}const a=(n.availableModes||[]).filter(i=>i!=="shell");e.textContent=a.length?`Running (${n.image||"unknown image"}). Available: ${a.join(", ")}.`:`Running (${n.image||"unknown image"}), but no agent CLI found inside \u2014 only Shell will work.`},async linkDockerCase(){const e=document.getElementById("dockerCaseName").value.trim(),t=document.getElementById("dockerWorkspacePath").value.trim(),s=document.getElementById("dockerHostId").value.trim()||"local",o=!!document.getElementById("dockerAdoptExisting")?.checked,n=document.getElementById("dockerContainerName")?.value.trim()||"",a=document.getElementById("dockerAdoptWorkdir")?.value.trim()||"",i=document.getElementById("dockerImage").value.trim()||"codeman/agent:base",r=document.getElementById("dockerNetwork").value,c=document.getElementById("dockerMemory").value.trim(),m=document.getElementById("dockerCpus").value.trim(),d=document.getElementById("dockerMountCredentials").checked,l=document.getElementById("dockerResumeOnStart").checked,h=document.getElementById("dockerLinkStatus");if(!e||!t){this.showToast("Please enter a case name and workspace path","error");return}if(!/^[a-zA-Z0-9_-]+$/.test(e)||!/^[a-zA-Z0-9_-]+$/.test(s)){this.showToast("Invalid name. Use only letters, numbers, hyphens, underscores.","error");return}if(!t.startsWith("/")){this.showToast("Workspace path must be absolute","error");return}if(o&&!n){this.showToast("Enter the name of the running container to attach to","error");return}const p=o?this.dockerCloneGuard():null;if(p){this.showToast(p.message,"error");const u=document.getElementById("dockerLinkStatus");u&&(u.textContent=p.message),p.el.focus(),p.el.select?.();return}try{h&&(h.textContent=o?"Inspecting the existing container...":"Checking docker daemon + base image...");const u={};c&&(u.memory=c),m&&(u.cpus=m);const f={id:s,label:s,image:i,network:r,mountCredentials:d,resumeOnStart:l,...Object.keys(u).length?{resources:u}:{}};let C=await fetch("/api/docker-hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(f)}),y=await C.json();if(!y.success&&y.errorCode==="ALREADY_EXISTS"&&(C=await fetch(`/api/docker-hosts/${encodeURIComponent(s)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(f)}),y=await C.json()),!y.success)throw new Error(y.error||"Failed to save docker host");const w=await(await fetch(o?"/api/cases/docker-adopt":"/api/cases/docker-link",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o?{name:e,hostId:s,hostWorkspacePath:t,container:n,...a?{containerWorkdir:a}:{}}:{name:e,hostId:s,hostWorkspacePath:t})})).json();if(w.success){this.closeCreateCaseModal();const k=w.data?.capsEnforced===!1?" (resource caps are advisory on this engine)":"",v=(w.data?.availableModes||[]).filter(I=>I!=="shell"),S=o&&v.length?` \u2014 found ${v.join(", ")}`:"";this.showToast(`Docker case "${e}" ${o?"attached":"linked"}${k}${S}`,"success"),await this.loadQuickStartCases(e),await this.saveLastUsedCase(e)}else h&&(h.textContent=w.error||"Failed to link docker case"),this.showToast(w.error||"Failed to link docker case","error")}catch(u){console.error("Failed to link docker case:",u),h&&(h.textContent=u.message),this.showToast("Failed to link docker case: "+u.message,"error")}},async refreshDockerExports(){const e=document.getElementById("dockerExportsList");if(e)try{const o=(await(await fetch("/api/docker-exports")).json())?.data?.exports||[];if(o.length===0){e.innerHTML='<span class="form-hint">No exports yet. Export a docker case from its tab.</span>';return}e.innerHTML=o.map(n=>{const a=(n.sizeBytes/1e6).toFixed(1),i=escapeHtml(n.name);return`<div class="case-manage-item" style="display:flex; align-items:center; gap:8px; justify-content:space-between;">
28
+ <span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${i}">${i} <span class="form-hint">(${a} MB)</span></span>
29
+ <span style="flex-shrink:0;">
30
+ <a class="btn-toolbar" href="${CodemanBase.url(`/api/docker-exports/${encodeURIComponent(n.name)}`)}" download>Download</a>
31
+ <button class="btn-toolbar" onclick="app.importDockerBundle('${i.replace(/'/g,"\\'")}')">Import</button>
32
+ <button class="btn-toolbar" onclick="app.deleteDockerExport('${i.replace(/'/g,"\\'")}')">Delete</button>
33
+ </span>
34
+ </div>`}).join("")}catch(t){e.innerHTML=`<span class="form-hint">Failed to load exports: ${t.message}</span>`}},async exportDockerCaseBundle(e,t="full"){try{const o=await(await fetch(`/api/docker-cases/${encodeURIComponent(e)}/export`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({mode:t})})).json();o.success?this.showToast(`Exporting "${e}" (${t})... you'll be notified when the bundle is ready`,"info"):this.showToast(o.error||"Export failed","error")}catch(s){this.showToast("Export failed: "+s.message,"error")}},async importDockerBundle(e){const t=prompt("New case name for the imported bundle:",e.split("-")[0]+"-imported");if(!t)return;const s=prompt("Absolute host directory to restore the workspace into:","");if(s)try{const n=await(await fetch("/api/docker-cases/import",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({bundle:e,newCaseName:t,destWorkspacePath:s})})).json();n.success?(this.showToast(`Imported as "${t}"`,"success"),await this.loadQuickStartCases(t)):this.showToast(n.error||"Import failed","error")}catch(o){this.showToast("Import failed: "+o.message,"error")}},async deleteDockerExport(e){if(confirm(`Delete export bundle "${e}"?`))try{const s=await(await fetch(`/api/docker-exports/${encodeURIComponent(e)}`,{method:"DELETE"})).json();s.success?(this.showToast("Export deleted","success"),this.refreshDockerExports()):this.showToast(s.error||"Delete failed","error")}catch(t){this.showToast("Delete failed: "+t.message,"error")}},_readRemoteHostFromForm(){const e=document.getElementById("remoteHostId").value.trim(),t=document.getElementById("remoteHostAddress").value.trim(),s=document.getElementById("remoteHostUsername").value.trim(),o=document.getElementById("remoteHostPort").value.trim(),n=document.getElementById("remoteHostIdentityFile").value.trim(),a=document.getElementById("remoteHostSocksProxy").value.trim(),i=document.getElementById("remoteHostJumpHost").value.trim(),r=document.getElementById("remoteHostCodexCommand").value.trim(),c=document.getElementById("remoteHostWakeMac").value.trim(),m=document.getElementById("remoteHostWakeCommand").value.trim(),d=document.getElementById("remoteHostExtraSshOptions").value.split(`
35
+ `).map(h=>h.trim()).filter(h=>h.length>0);let l;if(o){const h=Number(o);Number.isInteger(h)&&h>=1&&h<=65535&&(l=h)}return{id:e,label:e,host:t,username:s,...l?{port:l}:{},...n?{identityFile:n}:{},...a?{socksProxy:a}:{},...i?{jumpHost:i}:{},...d.length?{extraSshOptions:d}:{},...c?{wakeMac:c}:{},...m?{wakeCommand:m}:{},...r?{commands:{codex:r}}:{}}},async discoverRemoteSessions(){const e=document.getElementById("remoteDiscoverResults"),t=document.getElementById("remoteDiscoverBtn"),s=this._readRemoteHostFromForm();if(!s.id||!s.host||!s.username){this.showToast("Fill in Host ID, address, and username first","error");return}if(!/^[a-zA-Z0-9_-]+$/.test(s.id)){this.showToast("Invalid Host ID. Use letters, numbers, hyphens, underscores.","error");return}t&&(t.disabled=!0),e&&(e.innerHTML='<div class="form-hint">Discovering\u2026</div>');try{const n=await(await fetch("/api/remote-hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();if(!n.success&&n.errorCode!=="ALREADY_EXISTS")throw new Error(n.error||"Failed to save remote host");const i=await(await fetch(`/api/remote-hosts/${encodeURIComponent(s.id)}/sessions`)).json();if(!i.success)throw new Error(i.error||"Discovery failed");this._renderDiscoveredSessions(s.id,i.data.sessions||[])}catch(o){console.error("Discover remote sessions failed:",o),e&&(e.innerHTML=`<div class="form-hint" style="color: var(--error, #e06c75);">${escapeHtml(o.message)}</div>`)}finally{t&&(t.disabled=!1)}},_renderDiscoveredSessions(e,t){const s=document.getElementById("remoteDiscoverResults");if(!s)return;if(!t.length){s.innerHTML='<div class="form-hint">No <code>codeman-*</code> sessions running on this host (or it is unreachable).</div>';return}const o=Math.floor(Date.now()/1e3),n=t.map(a=>{const i=Math.max(0,o-(a.created||0)),r=i<3600?`${Math.floor(i/60)}m`:i<86400?`${Math.floor(i/3600)}h`:`${Math.floor(i/86400)}d`,c=a.attachedClients!=null?a.attachedClients:a.attached?1:0,m=c>1?`<span class="case-location-badge" style="background: var(--warning, #e5c07b); color: #000;">shared \xB7 ${c} clients</span>`:c===1?'<span class="case-location-badge" style="background: var(--accent, #61afef);">attached</span>':"";return`
36
+ <div class="remote-discover-item">
37
+ <div class="remote-discover-info">
38
+ <span class="remote-discover-name">${escapeHtml(a.name)} ${m}</span>
39
+ <span class="form-hint">age ${r} \xB7 ${a.windows||1} window(s)</span>
40
+ </div>
41
+ <button type="button" class="btn-toolbar" onclick="app.attachDiscoveredSession('${escapeHtml(e)}', '${escapeHtml(a.name)}')">Attach</button>
42
+ </div>`}).join("");s.innerHTML=n},async attachDiscoveredSession(e,t){try{const o=await(await fetch("/api/sessions",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({mode:"shell",name:t,attachRemoteSession:{hostId:e,remoteSessionName:t}})})).json();if(!o.success)throw new Error(o.error||"Failed to create session");const n=o.data.session.id;await fetch(`/api/sessions/${n}/shell`,{method:"POST"});const a=this.getTerminalDimensions();a&&await fetch(`/api/sessions/${n}/resize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)}),this.closeCreateCaseModal(),this.showToast(`Attached to ${t} (detach on close)`,"success"),this.activeSessionId=n,await this.selectSession(n),this.terminal&&typeof this.terminal.focus=="function"&&this.terminal.focus()}catch(s){console.error("Attach discovered session failed:",s),this.showToast("Failed to attach: "+s.message,"error")}},renderCaseManageList(){const e=document.getElementById("caseManageList"),t=this.cases||[];if(t.length===0){e.innerHTML='<div class="form-hint" style="text-align: center; padding: 2rem 0;">No cases yet</div>';return}const s=t.filter(n=>n.agentCreated);let o=s.length>0?`<div class="case-manage-agent-bar">
43
+ <span class="case-manage-agent-count">${s.length} case${s.length===1?"":"s"} created by agent workers</span>
44
+ <button class="case-manage-btn case-manage-btn-cleanup" onclick="app.cleanupAgentCases()"
45
+ title="Review and delete the scratch cases agent workers left behind">Clean up</button>
46
+ </div>`:"";t.forEach((n,a)=>{const i=a===0,r=a===t.length-1,c=n.path?this._shortenHomePath(n.path):"",m=n.agentCreated?`Created by an agent worker${n.agentCreated.parentSessionName?` from ${n.agentCreated.parentSessionName}`:""} (${n.agentCreated.createdBy})${n.agentCreated.createdAt?` on ${new Date(n.agentCreated.createdAt).toLocaleString()}`:""}`:"";o+=`
47
+ <div class="case-manage-item" data-case="${escapeHtml(n.name)}">
48
+ <div class="case-manage-info">
49
+ <span class="case-manage-name">${escapeHtml(n.name)}${n.agentCreated?`<span class="case-manage-tag-agent" title="${escapeHtml(m)}" data-i18n-skip>agent</span>`:""}</span>
50
+ <span class="case-manage-path">${escapeHtml(c)}</span>
51
+ </div>
52
+ <div class="case-manage-actions">
53
+ ${n.location==="docker"?`<button class="case-manage-btn" onclick="app.exportDockerCaseBundle(${escapeHtml(JSON.stringify(n.name))}, 'full')"
54
+ title="Export container (full image + workspace) to move to another machine">&#x1F4E6;</button>`:""}
55
+ <button class="case-manage-btn" onclick="app.moveCaseUp(${escapeHtml(JSON.stringify(n.name))})"
56
+ title="Move up" ${i?"disabled":""}>&#x25B2;</button>
57
+ <button class="case-manage-btn" onclick="app.moveCaseDown(${escapeHtml(JSON.stringify(n.name))})"
58
+ title="Move down" ${r?"disabled":""}>&#x25BC;</button>
59
+ <button class="case-manage-btn case-manage-btn-delete" onclick="app.deleteCase(${escapeHtml(JSON.stringify(n.name))})"
60
+ title="Delete case">&#x2715;</button>
61
+ </div>
62
+ </div>
63
+ `}),e.innerHTML=o},async moveCaseUp(e){const t=this.cases||[],s=t.findIndex(n=>n.name===e);if(s<=0)return;const o=[...t];[o[s-1],o[s]]=[o[s],o[s-1]],this.cases=o,this.renderCaseManageList(),await this.saveCaseOrder(o.map(n=>n.name))},async moveCaseDown(e){const t=this.cases||[],s=t.findIndex(n=>n.name===e);if(s<0||s>=t.length-1)return;const o=[...t];[o[s],o[s+1]]=[o[s+1],o[s]],this.cases=o,this.renderCaseManageList(),await this.saveCaseOrder(o.map(n=>n.name))},async deleteCase(e){if(confirm(`Delete case "${e}"? Linked cases will only be unlinked (folder preserved). Created cases will be permanently deleted.`))try{const s=await(await fetch(`/api/cases/${encodeURIComponent(e)}`,{method:"DELETE"})).json();if(s.success){this.showToast(`Case "${e}" ${s.data?.type==="unlinked"?"unlinked":"deleted"}`,"success"),this.cases=(this.cases||[]).filter(a=>a.name!==e),this.renderCaseManageList();const o=document.getElementById("quickStartCase"),n=o.value;n===e&&o.blur?.(),await this.loadQuickStartCases(n===e?null:n),n===e&&await this.saveLastUsedCase(document.getElementById("quickStartCase")?.value||"testcase")}else this.showToast(s.error||"Failed to delete case","error")}catch(t){this.showToast("Failed to delete case: "+t.message,"error")}},async cleanupAgentCases(){let e;try{const l=await(await fetch("/api/cases/agent-created")).json();if(!l.success){this.showToast(l.error||"Failed to list agent cases","error");return}e=l.data.cases||[]}catch(d){this.showToast("Failed to list agent cases: "+d.message,"error");return}const t=e.filter(d=>d.inUse),s=e.filter(d=>!d.inUse);if(s.length===0){this.showToast(t.length>0?`All ${t.length} agent case(s) are still in use by a running session`:"No agent-created cases to clean up","info");return}const o=s.map(d=>` ${d.name}`).join(`
64
+ `),n=t.length>0?`
65
+
66
+ Skipping ${t.length} case(s) still in use by a running session.`:"";if(!confirm(`Permanently delete ${s.length} agent-created case folder(s) and everything in them?
67
+
68
+ ${o}${n}`))return;let a=0;const i=[];for(const d of s)try{(await(await fetch(`/api/cases/${encodeURIComponent(d.name)}`,{method:"DELETE"})).json()).success?a++:i.push(d.name)}catch{i.push(d.name)}this.showToast(i.length===0?`Deleted ${a} agent case(s)`:`Deleted ${a}, failed: ${i.join(", ")}`,i.length===0?"success":"error");const r=document.getElementById("quickStartCase"),c=r?.value,m=s.some(d=>d.name===c);m&&r?.blur?.(),await this.loadQuickStartCases(m?null:c),m&&await this.saveLastUsedCase(document.getElementById("quickStartCase")?.value||"testcase"),this.renderCaseManageList()},async saveCaseOrder(e){try{await fetch("/api/cases/order",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({order:e})});const s=document.getElementById("quickStartCase").value;await this.loadQuickStartCases(s)}catch(t){this.showToast("Failed to save case order: "+t.message,"error")}},showMobileCasePicker(){const e=document.getElementById("mobileCasePickerModal"),t=document.getElementById("mobileCaseList"),o=document.getElementById("quickStartCase").value;let n="";const a=this.getCasePickerOptions();for(const i of a){const r=i.name===o;n+=`
69
+ <button class="mobile-case-item ${r?"selected":""}"
70
+ onclick="app.selectMobileCase(${escapeHtml(JSON.stringify(i.name))})">
71
+ <span class="mobile-case-item-icon">
72
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
73
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
74
+ </svg>
75
+ </span>
76
+ <span class="mobile-case-item-name">${escapeHtml(i.label)}</span>
77
+ <span class="mobile-case-item-delete" onclick="event.stopPropagation(); app.deleteCaseMobile(${escapeHtml(JSON.stringify(i.name))})" title="Delete">
78
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
79
+ <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
80
+ </svg>
81
+ </span>
82
+ <span class="mobile-case-item-check">
83
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
84
+ <polyline points="20 6 9 17 4 12"/>
85
+ </svg>
86
+ </span>
87
+ </button>
88
+ `}t.innerHTML=n,e.classList.add("active")},closeMobileCasePicker(){document.getElementById("mobileCasePickerModal").classList.remove("active")},selectMobileCase(e){const t=document.getElementById("quickStartCase");t.value=e,this.updateMobileCaseLabel(e),this.updateDirDisplayForCase(e),this.saveLastUsedCase(e),this.closeMobileCasePicker(),this.showToast(`Selected: ${e}`,"success")},updateMobileCaseLabel(e){const t=document.getElementById("mobileCaseName");t&&(t.textContent=e)},async deleteCaseMobile(e){if(confirm(`Delete case "${e}"?`))try{const s=await(await fetch(`/api/cases/${encodeURIComponent(e)}`,{method:"DELETE"})).json();s.success?(this.showToast(`Case "${e}" ${s.data?.type==="unlinked"?"unlinked":"deleted"}`,"success"),this.cases=(this.cases||[]).filter(o=>o.name!==e),this.closeMobileCasePicker(),await this.loadQuickStartCases()):this.showToast(s.error||"Failed to delete case","error")}catch(t){this.showToast("Failed to delete case: "+t.message,"error")}},showCreateCaseFromMobile(){this.closeMobileCasePicker(),this.showCreateCaseModal();const e=document.getElementById("createCaseModal");e.classList.add("from-mobile"),setTimeout(()=>e.classList.remove("from-mobile"),300)}}),Object.defineProperty(CodemanApp.prototype,"runMode",{configurable:!0,enumerable:!0,get(){return this._runMode||"claude"},set(e){this._runMode=e==="opencode"||e==="codex"||e==="gemini"||e==="antigravity"||e==="pi"||e==="grok"||e==="deepseek"||e==="omp"||e==="claude"?e:"claude"}});