aicodeman 1.18.3 → 1.18.4

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 (71) hide show
  1. package/README.md +14 -0
  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.445f1584.js → app.e2cb9065.js} +1 -1
  5. package/dist/web/public/{app.445f1584.js.br → app.e2cb9065.js.br} +0 -0
  6. package/dist/web/public/{app.445f1584.js.gz → app.e2cb9065.js.gz} +0 -0
  7. package/dist/web/public/approvals-ui.js +24 -15
  8. package/dist/web/public/approvals-ui.js.br +0 -0
  9. package/dist/web/public/approvals-ui.js.gz +0 -0
  10. package/dist/web/public/{constants.822a486d.js → constants.04b7b284.js} +34 -20
  11. package/dist/web/public/constants.04b7b284.js.br +0 -0
  12. package/dist/web/public/constants.04b7b284.js.gz +0 -0
  13. package/dist/web/public/cron-ui.js.gz +0 -0
  14. package/dist/web/public/entrance-animations.js.gz +0 -0
  15. package/dist/web/public/home-sessions.js.gz +0 -0
  16. package/dist/web/public/i18n.86757605.js.gz +0 -0
  17. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  18. package/dist/web/public/index.html +13 -6
  19. package/dist/web/public/index.html.br +0 -0
  20. package/dist/web/public/index.html.gz +0 -0
  21. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  22. package/dist/web/public/keyboard-accessory.0454a3e9.js.gz +0 -0
  23. package/dist/web/public/mobile-handlers.bc45f976.js.gz +0 -0
  24. package/dist/web/public/mobile-overview.js.gz +0 -0
  25. package/dist/web/public/mobile.1eef31fe.css.gz +0 -0
  26. package/dist/web/public/notification-manager.5d229063.js.gz +0 -0
  27. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  28. package/dist/web/public/panels-ui.1e4cb4b2.js.gz +0 -0
  29. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  30. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  31. package/dist/web/public/readmymind-ui.js.gz +0 -0
  32. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  33. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  34. package/dist/web/public/session-lineage.js +35 -1
  35. package/dist/web/public/session-lineage.js.br +0 -0
  36. package/dist/web/public/session-lineage.js.gz +0 -0
  37. package/dist/web/public/{session-ui.20be58fa.js → session-ui.c67565f2.js} +3 -3
  38. package/dist/web/public/session-ui.c67565f2.js.br +0 -0
  39. package/dist/web/public/session-ui.c67565f2.js.gz +0 -0
  40. package/dist/web/public/settings-ui.eacb4f92.js.gz +0 -0
  41. package/dist/web/public/styles.d2fa3a17.css +1 -0
  42. package/dist/web/public/styles.d2fa3a17.css.br +0 -0
  43. package/dist/web/public/styles.d2fa3a17.css.gz +0 -0
  44. package/dist/web/public/subagent-windows.2a192c3e.js.gz +0 -0
  45. package/dist/web/public/sw.js.gz +0 -0
  46. package/dist/web/public/terminal-ui.c182a68d.js.gz +0 -0
  47. package/dist/web/public/ultracode-panel.js.gz +0 -0
  48. package/dist/web/public/ultracode-windows.js.gz +0 -0
  49. package/dist/web/public/upload.html.gz +0 -0
  50. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  51. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  52. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  53. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  54. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  55. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  56. package/dist/web/public/vendor/xterm-predictive-echo.6cec0fba.js.gz +0 -0
  57. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  58. package/dist/web/public/vendor/xterm.css.gz +0 -0
  59. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  60. package/dist/web/public/voice-input.4ec670b0.js.gz +0 -0
  61. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  62. package/dist/web/public/webview-tabs.js.gz +0 -0
  63. package/package.json +1 -1
  64. package/skills/codeman/SKILL.md +25 -11
  65. package/dist/web/public/constants.822a486d.js.br +0 -0
  66. package/dist/web/public/constants.822a486d.js.gz +0 -0
  67. package/dist/web/public/session-ui.20be58fa.js.br +0 -0
  68. package/dist/web/public/session-ui.20be58fa.js.gz +0 -0
  69. package/dist/web/public/styles.70bf9c2a.css +0 -1
  70. package/dist/web/public/styles.70bf9c2a.css.br +0 -0
  71. package/dist/web/public/styles.70bf9c2a.css.gz +0 -0
package/README.md CHANGED
@@ -406,6 +406,14 @@ The title is templated into the served HTML on first byte, so it's correct from
406
406
  | **110k tokens** | Auto `/compact` | Context summarized, work continues |
407
407
  | **140k tokens** | Auto `/clear` | Fresh start with `/init` |
408
408
 
409
+ ### Tab Alerts
410
+
411
+ <p align="center">
412
+ <img src="docs/images/tab-alerts-glow-20260815.gif" alt="Session tabs: a regular active tab beside a yellow waiting-for-input tab and a red needs-decision tab, both with a breathing glow" width="900">
413
+ </p>
414
+
415
+ Every tab tells you its state at a glance. A running session keeps its green status dot. When a session stops and waits for input, its tab turns **yellow**: steady ring, tinted background, yellow dot, with a slow breathing glow on top. When a permission prompt or question is **blocking** the agent, the tab turns **red** with a faster pulse. The base tint never blinks off, so even a split-second glance (or a screenshot) reads the true state; the ring stays visible while the tab is selected, and a page reload re-arms pending alerts from the server, so a blocked session can never hide behind a fresh-looking tab.
416
+
409
417
  ### Notifications
410
418
 
411
419
  Real-time desktop alerts when sessions need attention — `permission_prompt` and `elicitation_dialog` trigger critical red tab blinks, `idle_prompt` triggers yellow blinks. Click any notification to jump directly to the affected session. Hooks auto-configured per case directory.
@@ -1035,6 +1043,12 @@ See [CLAUDE.md](./CLAUDE.md) for full documentation.
1035
1043
 
1036
1044
  ---
1037
1045
 
1046
+ ## Community
1047
+
1048
+ Questions, setup help, and ideas live in [GitHub Discussions](https://github.com/Ark0N/Codeman/discussions): the [Q&A section](https://github.com/Ark0N/Codeman/discussions/categories/q-a) answers the most common ones (phone access, overnight runs, updating), and the roadmap gets decided in [Ideas](https://github.com/Ark0N/Codeman/discussions/categories/ideas). Bugs go to [issues](https://github.com/Ark0N/Codeman/issues); reports usually get a response within a day, and every release credits its reporters and contributors by name. Want to contribute? [CONTRIBUTING.md](.github/CONTRIBUTING.md) has the map: skins, translations, and docs make great first PRs, and bigger features start life as a Discussion. And if you're proud of your rig, post it in [Show and tell](https://github.com/Ark0N/Codeman/discussions/300).
1049
+
1050
+ ---
1051
+
1038
1052
  ## Codebase Quality
1039
1053
 
1040
1054
  The codebase went through a comprehensive 7-phase refactoring that eliminated god objects, centralized configuration, and established modular architecture:
Binary file
@@ -13,7 +13,7 @@
13
13
 
14
14
  `).trim()}_preprocessAsciiArt(e){const t=/[─-╿▀-▟]/,s=/```[\s\S]*?```/g,n=[];return e.replace(s,a=>(n.push(a),`__CODEMAN_FENCE_${n.length-1}__`)).split(/(\n{2,})/).map(a=>/^\n{2,}$/.test(a)||!a.trim()||a.includes("__CODEMAN_FENCE_")?a:t.test(a)?"\n```\n"+a+"\n```\n":a).join("").replace(/__CODEMAN_FENCE_(\d+)__/g,(a,r)=>n[Number(r)])}_renderMarkdown(e){const t=e||"";if(typeof marked<"u"&&marked.parse)try{const n=this._preprocessAsciiArt(t);let i=this._sanitizeHtml(marked.parse(n,{breaks:!0,gfm:!0}));i=i.replace(/<table>/g,'<div class="rv-table-wrap"><table>').replace(/<\/table>/g,"</table></div>");const o=/[─-╿▀-▟]/,a=document.createElement("template");return a.innerHTML=i,a.content.querySelectorAll("pre > code").forEach(r=>{const l=r.parentElement,d=o.test(r.textContent||""),c=document.createElement("div");c.className=d?"rv-code-wrap rv-diagram-wrap":"rv-code-wrap";const S=document.createElement("div");S.className="rv-code-actions";const u=document.createElement("button");if(u.className="rv-copy-btn",u.type="button",u.setAttribute("aria-label","Copy code"),u.setAttribute("title","Copy code"),S.appendChild(u),d){l.classList.add("rv-diagram");const E=document.createElement("button");E.className="rv-wrap-toggle",E.type="button",E.setAttribute("aria-label","Toggle line wrapping"),E.setAttribute("title","Toggle line wrapping"),S.appendChild(E)}l.parentNode.insertBefore(c,l),c.appendChild(S),c.appendChild(l)}),a.innerHTML}catch{}return`<pre style="white-space:pre-wrap;word-break:break-word">${t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}</pre>`}_bindResponseViewerInteractions(e){!e||e.dataset.rvBound==="1"||(e.dataset.rvBound="1",e.addEventListener("click",async t=>{const s=t.target.closest(".rv-copy-btn");if(s){t.preventDefault(),t.stopPropagation();const r=s.closest(".rv-code-wrap")?.querySelector("pre code"),l=r?await this._copyText(r.textContent||""):!1;s.classList.remove("rv-copied","rv-copy-failed"),s.classList.add(l?"rv-copied":"rv-copy-failed"),clearTimeout(s._resetTimer),s._resetTimer=setTimeout(()=>{s.classList.remove("rv-copied","rv-copy-failed")},1500);return}const n=t.target.closest(".rv-wrap-toggle");if(!n)return;t.preventDefault(),t.stopPropagation();const i=n.closest(".rv-diagram-wrap"),o=i?.querySelector("pre.rv-diagram");if(!o||!i)return;const a=o.classList.toggle("rv-nowrap");i.classList.toggle("rv-wrap-nowrap",a)}))}async _copyText(e){if(!e)return!1;try{if(navigator.clipboard?.writeText)return await navigator.clipboard.writeText(e),!0}catch{}try{const t=document.createElement("textarea");t.value=e,t.setAttribute("readonly",""),t.style.cssText="position:fixed;top:0;left:0;opacity:0;pointer-events:none",document.body.appendChild(t),t.select();const s=document.execCommand("copy");return document.body.removeChild(t),s}catch{return!1}}_buildResponseViewerMessage(e,t,s){const n=document.createElement("div"),i=t==="user";n.className="rv-message "+(i?"rv-msg-user":"rv-msg-assistant");const o=document.createElement("div");o.className="rv-role "+(i?"rv-role-user":"rv-role-assistant"),o.textContent=i?"You":s,n.appendChild(o);const a=document.createElement("div");return a.className="rv-text",a.innerHTML=this._renderMarkdown(e),n.appendChild(a),n}_getResponseViewerAgentLabel(){const e=this.sessions.get(this.activeSessionId)?.mode;return e==="codex"?"Codex":e==="gemini"?"Gemini":e==="antigravity"?"Antigravity":e==="pi"?"Pi":e==="opencode"?"OpenCode":"Claude"}async toggleResponseViewer(){const e=document.getElementById("responseViewer"),t=document.getElementById("responseViewerBackdrop");if(!e)return;if(e.classList.contains("visible")){e.classList.remove("visible"),t.classList.remove("visible");return}if(this.activeSessionId)try{let o=((await(await fetch(`/api/sessions/${this.activeSessionId}/last-response`)).json())?.data??{}).text||"";const a=this.sessions.get(this.activeSessionId)?.mode||"claude";if(!o&&(a==="claude"||a==="shell")){const S=(await(await fetch(`/api/sessions/${this.activeSessionId}/terminal`)).json())?.data??{};S.terminalBuffer&&(o=this._cleanTerminalBuffer(S.terminalBuffer))}const r=document.getElementById("responseViewerBody");o?(r.innerHTML="",r.appendChild(this._buildResponseViewerMessage(o,"assistant",this._getResponseViewerAgentLabel())),this._bindResponseViewerInteractions(r)):r.textContent=window.codemanT?.("No response yet \u2014 send a message in this session first.")||"No response yet \u2014 send a message in this session first.";const l=document.getElementById("responseViewerTitle"),d=document.getElementById("responseViewerMore");l&&(l.textContent="Last Response"),d&&(d.style.display="",d.textContent="More"),e.classList.add("visible"),t.classList.add("visible"),r.scrollTop=0}catch(n){console.error("Failed to load response:",n)}}async loadFullContext(){if(!this.activeSessionId)return;const e=document.getElementById("responseViewerMore");e&&(e.textContent="...");try{const n=((await(await fetch(`/api/sessions/${this.activeSessionId}/last-response?context=full`)).json())?.data??{}).messages||[],i=document.getElementById("responseViewerBody"),o=document.getElementById("responseViewerTitle");if(!i)return;if(n.length===0){i.textContent="No conversation history available";return}const a=this._getResponseViewerAgentLabel();i.innerHTML="";for(const r of n)i.appendChild(this._buildResponseViewerMessage(r.text,r.role,a));this._bindResponseViewerInteractions(i),o&&(o.textContent=`Conversation (${n.length} messages)`),e&&(e.style.display="none"),i.scrollTop=i.scrollHeight}catch(t){console.error("Failed to load context:",t)}finally{e&&(e.textContent="More")}}async _onSessionNeedsRefresh(){if(!this.activeSessionId||!this.terminal||this._isLoadingBuffer)return;const e=this.activeSessionId;try{let t=await fetch(`/api/sessions/${e}/terminal?full=1`),s=(await t.json())?.data??{};if(s.terminalBuffer&&this._replayWouldShrinkBuffer(s.terminalBuffer)&&(t=await fetch(`/api/sessions/${e}/terminal?tail=${TERMINAL_TAIL_SIZE}`),s=(await t.json())?.data??{}),this.activeSessionId!==e)return;if(s.terminalBuffer){const n=this.terminal.buffer?.active,i=n?Math.max(0,(n.baseY||0)-(n.viewportY||0)):0;this.terminal.clear(),this.terminal.reset(),await this.chunkedTerminalWrite(s.terminalBuffer),this._setHistoryTruncation(e,s);const o=computeRewriteScrollLine({linesFromBottom:i,baseY:this.terminal.buffer?.active?.baseY??0});o===null||typeof this.terminal.scrollToLine!="function"?this.terminal.scrollToBottom():this.terminal.scrollToLine(o),this._localEchoOverlay?.rerender(),this.activeSessionId&&this.sendResize(this.activeSessionId)}}catch(t){console.error("needsRefresh reload failed:",t)}}async _onSessionClearTerminal(e){if(e.id===this.activeSessionId){if(this._isLoadingBuffer)return;try{const s=(await(await fetch(`/api/sessions/${e.id}/terminal`)).json())?.data??{};if(this.terminal.clear(),this.terminal.reset(),s.terminalBuffer){const n=s.terminalBuffer.replace(DEC_SYNC_STRIP_RE,"");await this.chunkedTerminalWrite(n)}this.sendResize(e.id),this._localEchoOverlay?.rerender()}catch(t){console.error("clearTerminal refresh failed:",t)}}}_onSessionCompletion(e){this.totalCost+=e.cost||0,this.updateCost(),e.id===this.activeSessionId&&(this.terminal.writeln(""),this.terminal.writeln(`\x1B[1;32m Done (Cost: $${(e.cost||0).toFixed(4)})\x1B[0m`))}_onSessionError(e){e.id===this.activeSessionId&&this.terminal.writeln(`\x1B[1;31m Error: ${e.error}\x1B[0m`),this._notifySession(e.id,"critical","session-error","Session Error",e.error||"Unknown error")}_onSessionExit(e){this._wsSessionId===e.id&&this._disconnectWs();const t=this.sessions.get(e.id);t&&(t.status="stopped",this.renderSessionTabs(),e.id===this.activeSessionId&&this._updateLocalEchoState()),e.code&&e.code!==0&&this._notifySession(e.id,"critical","session-crash","Session Crashed",`Exited with code ${e.code}`)}_onSessionIdle(e){const t=this.sessions.get(e.id);if(t&&(t.status="idle",this.renderSessionTabs(),this.sendPendingCtrlL(e.id),e.id===this.activeSessionId&&this._updateLocalEchoState()),!this.respawnStatus[e.id]?.enabled){const s=this.notificationManager?.preferences?.stuckThresholdMs||6e5;clearTimeout(this.idleTimers.get(e.id)),this.idleTimers.set(e.id,setTimeout(()=>{this._notifySession(e.id,"warning","session-stuck","Session Idle",`Idle for ${Math.round(s/6e4)}+ minutes`),this.idleTimers.delete(e.id)},s))}}_onSessionWorking(e){const t=this.sessions.get(e.id);t&&(t.status="busy",this.pendingHooks.has(e.id)||this.tabAlerts.delete(e.id),this.renderSessionTabs(),this.sendPendingCtrlL(e.id),e.id===this.activeSessionId&&this._updateLocalEchoState());const s=this.idleTimers.get(e.id);s&&(clearTimeout(s),this.idleTimers.delete(e.id))}_onSessionAutoClear(e){e.sessionId===this.activeSessionId&&(this.showToast(`Auto-cleared at ${e.tokens.toLocaleString()} tokens`,"info"),this.updateRespawnTokens(0)),this._notifySession(e.sessionId,"info","auto-clear","Auto-Cleared",`Context reset at ${(e.tokens||0).toLocaleString()} tokens`)}_onSessionLimitPauseScheduled(e){const t=this.sessions.get(e.sessionId);t&&(t.autoResumeAt=e.resumeAt);const s=new Date(e.resumeAt).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});e.sessionId===this.activeSessionId&&this.showToast(`Usage limit reached \u2014 auto-resume at ${s}`,"warning"),this._notifySession(e.sessionId,"warning","limit-pause","Usage Limit Reached",`Auto-resume scheduled for ${s}`),this.updateAutoResumeStatus(e.sessionId)}_onSessionLimitResume(e){const t=this.sessions.get(e.sessionId);t&&(t.autoResumeAt=void 0),e.sessionId===this.activeSessionId&&this.showToast("Usage limit reset \u2014 work resumed automatically","success"),this._notifySession(e.sessionId,"info","limit-resume","Auto-Resumed","Usage limit reset \u2014 continuing work"),this.updateAutoResumeStatus(e.sessionId)}_onSessionLimitResumeCancelled(e){const t=this.sessions.get(e.sessionId);t&&(t.autoResumeAt=void 0),this.updateAutoResumeStatus(e.sessionId)}_onSessionRespawnBreakerTripped(e){const s=this.sessions.get(e.sessionId)?.name||"Session";this.showToast?.(`${s} stopped: repeated crashes detected. Restart to retry.`,"error")}_onSessionCliInfo(e){const t=this.sessions.get(e.sessionId);t&&(e.version&&(t.cliVersion=e.version),e.model&&(t.cliModel=e.model),e.accountType&&(t.cliAccountType=e.accountType),e.latestVersion&&(t.cliLatestVersion=e.latestVersion)),e.sessionId===this.activeSessionId&&this.updateCliInfoDisplay()}_onSessionStatusTelemetry(e){this.updatePlanUsageChip(e);try{localStorage.setItem("codeman:planUsage",JSON.stringify({t:Date.now(),data:e}))}catch{}}restorePlanUsageChip(){try{const e=localStorage.getItem("codeman:planUsage");if(!e)return;const t=JSON.parse(e);t?.data&&Date.now()-(t.t||0)<12*3600*1e3&&this.updatePlanUsageChip(t.data)}catch{}}updatePlanUsageChip(e){const t=document.getElementById("planUsageChip");if(!t||!e)return;const s=l=>l&&typeof l.usedPercentage=="number"?Math.round(l.usedPercentage):null,n=s(e.fiveHour),i=s(e.sevenDay);if(n===null&&i===null)return;const o=l=>l>=85?"pu-red":l>=60?"pu-yellow":"pu-green",a=(l,d)=>{if(d===null)return"";const c=Math.round(Number(d));return Number.isFinite(c)?`<span class="pu-win"><span class="pu-label">${l}</span><span class="pu-val ${o(c)}">${c}%</span></span>`:""};t.innerHTML=[a("5h",n),a("7d",i)].filter(Boolean).join('<span class="pu-sep">\xB7</span>');const r=l=>l&&l.resetAt?new Date(l.resetAt).toLocaleString():"\u2014";t.title=`Claude plan usage
15
15
  5-hour limit: ${n??"\u2014"}% used (resets ${r(e.fiveHour)})
16
- Weekly limit: ${i??"\u2014"}% used (resets ${r(e.sevenDay)})`}_onScheduledCreated(e){this.currentRun=e,this.showTimer()}_onScheduledUpdated(e){this.currentRun=e,this.updateTimer()}_onScheduledCompleted(e){this.currentRun=e,this.hideTimer(),this.showToast("Scheduled run completed!","success")}_onScheduledStopped(){this.currentRun=null,this.hideTimer()}setConnectionStatus(e){this._connectionStatus=e,e==="connected"?(this._connDownSince=null,this._nextSseRetryAt=null,this._offlineOverlayDismissed=!1):this._connDownSince===null&&(this._connDownSince=Date.now()),this._updateConnectionIndicator(),this._updateConnectionLossUi(),e==="connected"&&this._redeliverSweep()}_connectWs(e){this._disconnectWs(),this._wsState="connecting",this._updateConnectionIndicator();const t=location.protocol==="https:"?"wss:":"ws:",s=this._clientId?`${this._clientId}:${this._wsTabNonce}`:"",n=s?`?cid=${encodeURIComponent(s)}`:"",i=`${t}//${location.host}/ws/sessions/${e}/terminal${n}`,o=new WebSocket(i);this._ws=o,this._wsSessionId=e,o.onopen=()=>{this._ws===o&&(this._wsReady=!0,this._wsState="connected",this._wsReconnectAttempts=0,this._updateConnectionIndicator(),this.sendResize(e)?.catch?.(()=>{}),this._startMobileResizeRetry(e),this._onWsReady(e))},o.onmessage=a=>{if(this._ws===o){this._wsLastRecvAt=Date.now();try{const r=JSON.parse(a.data);r.t==="o"?this._onSessionTerminal({id:e,data:r.d}):r.t==="c"?this._onSessionClearTerminal({id:e}):r.t==="r"?this._onSessionNeedsRefresh({id:e}):r.t==="ia"&&this._onWsInputAck(r.seq)}catch{}}},o.onclose=a=>{if(this._ws!==o)return;this._ws=null,this._wsSessionId=null,this._wsReady=!1,this._stopMobileResizeRetry();const r=window.CodemanWsReconnect.plan(a.code,this._wsReconnectAttempts||0);_crashDiag.log(`WS CLOSE code=${a.code} reason=${a.reason||""} action=${r.action} attempts=${this._wsReconnectAttempts||0}`);const l=this.activeSessionId===e;if(r.action==="give-up")this._wsState=l?"fallback":"disconnected",this._updateConnectionIndicator();else if(r.action==="reconnect")if(l){this._wsState="reconnecting",this._updateConnectionIndicator();const d=r.delayMs+Math.floor(Math.random()*250);this._wsReconnectAttempts=(this._wsReconnectAttempts||0)+1,this._wsReconnectTimer=setTimeout(()=>{this._wsReconnectTimer=null,this.activeSessionId===e&&this._connectWs(e)},d)}else this._wsState="disconnected",this._updateConnectionIndicator();else this._wsState=l?"fallback":"disconnected",this._updateConnectionIndicator(),l&&(this._wsReconnectAttempts=(this._wsReconnectAttempts||0)+1,this._wsReconnectTimer=setTimeout(()=>{this._wsReconnectTimer=null,this.activeSessionId===e&&this._connectWs(e)},r.delayMs))},o.onerror=()=>{}}_disconnectWs(){this._clearTimer("_wsReconnectTimer"),this._wsState="disconnected",this._stopMobileResizeRetry(),this._ws&&(this._ws.onclose=null,this._ws.close(),this._ws=null,this._wsSessionId=null,this._wsReady=!1)}_startMobileResizeRetry(e){this._stopMobileResizeRetry(),(typeof MobileDetection<"u"&&MobileDetection.getDeviceType?MobileDetection.getDeviceType():"desktop")!=="desktop"&&(this._mobileResizeRetryTimer=setInterval(()=>{document.visibilityState==="visible"&&(!this._wsReady||this._wsSessionId!==e||typeof KeyboardHandler<"u"&&KeyboardHandler.keyboardVisible||this.sendResize(e)?.catch?.(()=>{}))},MOBILE_RESIZE_RETRY_MS))}_stopMobileResizeRetry(){this._mobileResizeRetryTimer&&(clearInterval(this._mobileResizeRetryTimer),this._mobileResizeRetryTimer=null)}_sendInputAsync(e,t,s){!e||!t||this._reliableSend(e,t,s?.useMux===!0)}_sendInputEphemeral(e,t){if(!(!e||!t)){if(this._ws&&this._ws.readyState===WebSocket.OPEN&&this._wsSessionId===e)try{this._ws.send(JSON.stringify({t:"i",d:t}));return}catch{}try{fetch(`/api/sessions/${e}/input`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input:t}),keepalive:!0}).catch(()=>{})}catch{}}}_reliableSend(e,t,s){const i={seq:this._nextSeq(e),data:t,useMux:!!s,ts:Date.now(),tries:0,sentAt:0};let o=this._pendingDeliveries.get(e);o||(o=[],this._pendingDeliveries.set(e,o)),o.push(i),this._persistReliableState(),this._updateConnectionIndicator(),this._drainSession(e)}_nextSeq(e){const t=(this._seqCounters.get(e)||0)+1;return this._seqCounters.set(e,t),t}_drainSession(e){const t=this._pendingDeliveries.get(e);if(!(!t||t.length===0)){if(this._ws&&this._ws.readyState===WebSocket.OPEN&&this._wsSessionId===e){for(const s of t)if(s.sentAt===0)try{this._ws.send(JSON.stringify({t:"i",d:s.data,seq:s.seq,cid:this._clientId})),s.sentAt=Date.now(),s.tries++}catch{break}return}this._postDraining.has(e)||(this._postDraining.add(e),(async()=>{try{for(;;){const s=this._pendingDeliveries.get(e);if(!s||s.length===0||this._ws&&this._ws.readyState===WebSocket.OPEN&&this._wsSessionId===e)break;const n=s[0];n.tries++,n.sentAt=Date.now();let i=null;try{const o={input:n.data,seq:n.seq,clientId:this._clientId};n.useMux&&(o.useMux=!0),i=await fetch(`/api/sessions/${e}/input`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o),keepalive:n.data.length<65536})}catch{i=null}if(i&&i.ok)this._ackDelivery(e,n.seq);else if(i&&(i.status===404||i.status===410))this._ackDelivery(e,n.seq);else break}}finally{this._postDraining.delete(e)}})())}}_ackDelivery(e,t){const s=this._pendingDeliveries.get(e);if(s){const n=s.findIndex(i=>i.seq===t);n!==-1&&(s.splice(n,1),s.length===0&&this._pendingDeliveries.delete(e),this._pendingDeliveries.size===0?this._persistReliableNow():this._persistReliableState(),this._updateConnectionIndicator())}this.clearPendingHooks?.(e)}_onWsInputAck(e){this._wsSessionId&&Number.isInteger(e)&&this._ackDelivery(this._wsSessionId,e)}_onWsReady(e){const t=this._pendingDeliveries.get(e);if(t)for(const s of t)s.sentAt=0;this._drainSession(e)}_redeliverSweep(){if(this._pendingDeliveries.size!==0)for(const e of[...this._pendingDeliveries.keys()]){const t=this._pendingDeliveries.get(e);if(!t||t.length===0)continue;if(this._ws&&this._ws.readyState===WebSocket.OPEN&&this._wsSessionId===e){const n=t[0],i=n&&n.sentAt&&Date.now()-n.sentAt>this._reliableAckTimeoutMs,o=Date.now()-this._wsLastRecvAt>this._reliableAckTimeoutMs;if(i&&o){try{this._ws.close()}catch{}continue}if(i)for(const a of t)a.sentAt&&Date.now()-a.sentAt>this._reliableAckTimeoutMs&&(a.sentAt=0)}this._drainSession(e)}}_pendingBytes(){let e=0,t=0;for(const s of this._pendingDeliveries.values())for(const n of s)e+=n.data.length,t++;return{bytes:e,count:t}}_loadReliableState(){try{this._clientId=localStorage.getItem("codeman:clientId")||""}catch{this._clientId=""}if(!this._clientId){this._clientId="c-"+Math.random().toString(36).slice(2)+"-"+Date.now().toString(36);try{localStorage.setItem("codeman:clientId",this._clientId)}catch{}}try{const e=localStorage.getItem("codeman:pendingInput");if(!e)return;const t=JSON.parse(e);if(t&&t.seqs)for(const[s,n]of Object.entries(t.seqs))Number.isFinite(n)&&this._seqCounters.set(s,n);if(t&&t.pending)for(const[s,n]of Object.entries(t.pending))Array.isArray(n)&&n.length&&this._pendingDeliveries.set(s,n.filter(i=>i&&typeof i.data=="string"&&Number.isInteger(i.seq)).map(i=>({seq:i.seq,data:i.data,useMux:!!i.useMux,ts:i.ts||Date.now(),tries:0,sentAt:0})))}catch{}}_persistReliableState(){this._persistReliableTimer||(this._persistReliableTimer=setTimeout(()=>{this._persistReliableTimer=null,this._persistReliableNow()},250))}_persistReliableNow(){this._persistReliableTimer&&(clearTimeout(this._persistReliableTimer),this._persistReliableTimer=null);try{const e={};for(const[i,o]of this._seqCounters)e[i]=o;const t={};let s=0;for(const[i,o]of this._pendingDeliveries)if(o.length){t[i]=o.map(a=>({seq:a.seq,data:a.data,useMux:a.useMux,ts:a.ts,tries:a.tries}));for(const a of o)s+=a.data.length}const n=s>this._reliableMaxBytes?{seqs:e}:{seqs:e,pending:t};localStorage.setItem("codeman:pendingInput",JSON.stringify(n))}catch{}}_computeConnectionDescriptor(){const{bytes:e,count:t}=this._pendingBytes(),s=t>0,i=e>4,o=r=>r<1024?`${r}B`:`${(r/1024).toFixed(1)}KB`,a=i?` \xB7 ${o(e)} queued`:"";if(!this.isOnline||this._connectionStatus==="offline")return{display:"flex",dotClass:"connection-dot offline",text:i?`Offline (${o(e)} queued)`:"Offline",title:"No network connection"};if(this.activeSessionId){let r,l,d;switch(this._wsState){case"connected":r="connected",l="WS",d="Terminal connected over WebSocket";break;case"fallback":r="fallback",l="HTTP",d="WebSocket unavailable \u2014 input sent over HTTP";break;case"reconnecting":r="reconnecting",l="WS\u2026",d="Reconnecting WebSocket";break;default:r="reconnecting",l="WS\u2026",d="Connecting WebSocket";break}return{display:"flex",dotClass:`connection-dot ${r}`,text:`${l}${a}`,title:d}}return this._connectionStatus==="reconnecting"||this._connectionStatus==="disconnected"?{display:"flex",dotClass:"connection-dot reconnecting",text:i?`Reconnecting (${o(e)} queued)`:"Reconnecting...",title:"Reconnecting to server"}:s?{display:"flex",dotClass:"connection-dot draining",text:i?`Sending ${o(e)}...`:"Sending...",title:"Delivering queued input"}:{display:"none",dotClass:"",text:"",title:""}}_updateConnectionIndicator(){const e=this.$("connectionIndicator"),t=this.$("connectionDot"),s=this.$("connectionText");if(!e||!t||!s)return;const n=this._computeConnectionDescriptor(),i=this._lastIndicatorDescriptor;i&&i.display===n.display&&i.dotClass===n.dotClass&&i.text===n.text&&i.title===n.title||(this._lastIndicatorDescriptor=n,e.style.display=n.display,n.display!=="none"&&(t.className=n.dotClass,s.textContent=n.text,e.title=n.title))}setupOnlineDetection(){window.addEventListener("online",()=>{this.isOnline=!0,this.reconnectAttempts=0,this._connDownSince=Date.now(),this.connectSSE(),this._redeliverSweep()}),window.addEventListener("offline",()=>{this.isOnline=!1,this.setConnectionStatus("offline")})}_updateConnectionLossUi(){const e=window.CodemanConnectionLoss,t=this.$("offlineBanner"),s=this.$("offlineOverlay");if(!e||!t||!s)return;const n=e.compute({isOnline:this.isOnline,status:this._connectionStatus,everLoaded:this._initGeneration>0,downSince:this._connDownSince,now:Date.now(),nextRetryAt:this._nextSseRetryAt,overlayDismissed:this._offlineOverlayDismissed,retryPending:this._offlineRetryPending});this._connDownSince===null?this._stopOfflineTicker():this._startOfflineTicker();const i=this._offlineRetryPending?"Reconnecting\u2026":n.retryInSec!=null&&n.retryInSec>0?`Retrying in ${n.retryInSec}s`:"Retrying\u2026",o=`${n.mode}|${n.kind}|${i}`;if(o!==this._lastOfflineUiKey){if(this._lastOfflineUiKey=o,t.hidden=n.mode!=="banner",s.hidden=n.mode!=="overlay",document.body.classList.toggle("connection-lost",n.mode!=="hidden"),n.mode==="banner"){const a=this.$("offlineBannerText"),r=this.$("offlineBannerDetail");a&&(a.textContent=n.title),r&&(r.textContent=i)}else if(n.mode==="overlay"){const a=this.$("offlineOverlayTitle"),r=this.$("offlineOverlayBody"),l=this.$("offlineOverlayHost"),d=this.$("offlineOverlayStatus");a&&(a.textContent=n.title),r&&(r.textContent=n.detail),l&&(l.textContent=location.host),d&&(d.textContent=i)}}}_startOfflineTicker(){this._offlineUiTicker||(this._offlineUiTicker=setInterval(()=>this._updateConnectionLossUi(),1e3))}_stopOfflineTicker(){this._offlineUiTicker&&(clearInterval(this._offlineUiTicker),this._offlineUiTicker=null)}retryConnection(){this._offlineRetryPending=!0,this._nextSseRetryAt=null,this.reconnectAttempts=0,this._clearTimer("sseReconnectTimeout"),this.isOnline=navigator.onLine,this._lastOfflineUiKey="",this._updateConnectionLossUi(),this.connectSSE(),this.activeSessionId&&this._wsState!=="connected"&&(this._wsReconnectAttempts=0,this._connectWs(this.activeSessionId)),this._clearTimer("_offlineRetryTimer"),this._offlineRetryTimer=setTimeout(()=>{this._offlineRetryPending=!1,this._lastOfflineUiKey="",this._updateConnectionLossUi()},1500)}dismissOfflineOverlay(){this._offlineOverlayDismissed=!0,this._lastOfflineUiKey="",this._updateConnectionLossUi()}_updateCjkInputState(){const e=document.getElementById("cjkInput");if(!e)return;const t=this.loadAppSettingsFromStorage(),s=this.getDefaultSettings?.()||{},i=(this._serverCjkOverride||(t.cjkInputEnabled??s.cjkInputEnabled??!1))&&!!this.activeSessionId;e.classList.toggle("cjk-input-visible",!!i),document.body.classList.toggle("cjk-input-visible",!!i),e.style.display=i?"block":"none",e.setAttribute("aria-hidden",i?"false":"true"),i||(window.cjkActive=!1),typeof KeyboardHandler<"u"&&KeyboardHandler.updateLayoutForKeyboard()}_resetAllAppState(){this.sessions.clear(),this.ralphStates.clear(),this.terminalBuffers.clear(),this.terminalBufferCache.clear(),this._xtermSnapshots?.clear(),this.projectInsights.clear(),this.teams.clear(),this.teamTasks.clear();for(const e of this.idleTimers.values())clearTimeout(e);if(this.idleTimers.clear(),this._clearTimer("flickerFilterTimeout"),this.flickerFilterBuffer="",this.flickerFilterActive=!1,this._clearTimer("syncWaitTimeout"),this.pendingWrites=[],this.writeFrameScheduled=!1,this._isLoadingBuffer=!1,this._loadBufferQueue=null,this._bufferLoadOwner=null,this._chunkedWriteGen=(this._chunkedWriteGen||0)+1,this._localEchoOverlay?.rerender(),this._predictiveEcho?.clearPredictions(),this.pendingHooks.clear(),this.approvals?.clear(),this._parentNameCache&&this._parentNameCache.clear(),this.subagentActivity.clear(),this.subagentToolResults.clear(),this.workflowRuns&&this.workflowRuns.clear(),this.workflowRunDetails&&this.workflowRunDetails.clear(),this.activeWorkflowRunId=null,this.activeWorkflowPhaseIndex=null,MobileDetection.cleanup(),KeyboardHandler.cleanup(),MobileDetection.init(),KeyboardHandler.init(),this.tabAlerts.clear(),this.attachmentHistoryCounts.clear(),this._shownCompletions&&this._shownCompletions.clear(),this.notificationManager?.titleFlashInterval&&(clearInterval(this.notificationManager.titleFlashInterval),this.notificationManager.titleFlashInterval=null),this.notificationManager?.groupingMap){for(const{timeout:e}of this.notificationManager.groupingMap.values())clearTimeout(e);this.notificationManager.groupingMap.clear()}this.terminalResizeObserver&&(this.terminalResizeObserver.disconnect(),this.terminalResizeObserver=null),this.planLoadingTimer&&(clearInterval(this.planLoadingTimer),this.planLoadingTimer=null),this.timerCountdownInterval&&(clearInterval(this.timerCountdownInterval),this.timerCountdownInterval=null),this.runSummaryAutoRefreshTimer&&(clearInterval(this.runSummaryAutoRefreshTimer),this.runSummaryAutoRefreshTimer=null)}handleInit(e){this._clearTimer("_initFallbackTimer");const t=++this._initGeneration;if(this._serverCjkOverride=e.inputCjkForm||!1,this._updateCjkInputState(),e.planUsage&&this.updatePlanUsageChip(e.planUsage),e.version){const i=this.$("versionDisplay"),o=this.$("headerVersion");i&&(i.textContent=`v${e.version}`,i.title=`Codeman v${e.version}`),o&&(o.textContent=`v${e.version}`,o.title=`Codeman v${e.version}`)}VoiceInput.cleanup(),this._resetAllAppState(),e.sessions.forEach(i=>{this.sessions.set(i.id,i),(i.ralphLoop||i.ralphTodos)&&!this.ralphClosedSessions.has(i.id)&&this.ralphStates.set(i.id,{loop:i.ralphLoop||null,todos:i.ralphTodos||[]})});try{localStorage.removeItem("codeman-tab-meta")}catch{}if(Array.isArray(e.sessionOrder)&&e.sessionOrder.length)try{localStorage.setItem("codeman-session-order",JSON.stringify(e.sessionOrder))}catch{}this.syncSessionOrder(),e.respawnStatus?this.respawnStatus=e.respawnStatus:this.respawnStatus={},this.respawnTimers={},this.respawnCountdownTimers={},this.respawnActionLogs={},e.globalStats&&(this.globalStats=e.globalStats),this.totalCost=e.sessions.reduce((i,o)=>i+(o.totalCost||0),0),this.totalCost+=e.scheduledRuns.reduce((i,o)=>i+(o.totalCost||0),0);const s=e.scheduledRuns.find(i=>i.status==="running");if(s&&(this.currentRun=s,this.showTimer()),this.updateCost(),this.renderSessionTabs(),this.seedApprovals?.(),this.sessions.size>0?this.startSystemStatsPolling():this.stopSystemStatsPolling(),this.cleanupAllFloatingWindows(),e.subagents&&(this.subagents.clear(),this.subagentActivity.clear(),this.subagentToolResults.clear(),e.subagents.forEach(i=>{this.subagents.set(i.agentId,i)}),this.renderSubagentPanel(),this.subagentParentMap.clear(),this.loadSubagentParentMap().then(()=>{for(const[i,o]of this.subagentParentMap){const a=this.subagents.get(i);if(a&&this.sessions.has(o)){a.parentSessionId=o;const r=this.sessions.get(o);r&&(a.parentSessionName=this.getSessionName(r)),this.subagents.set(i,a)}}for(const[i]of this.subagents)this.subagentParentMap.has(i)||this.findParentSessionForSubagent(i);this.restoreSubagentWindowStates()})),e.workflowRuns&&this.seedWorkflowRuns(e.workflowRuns),t!==this._initGeneration)return;if(this.isSoloWindow){this._applySoloMode();return}const n=this.activeSessionId;if(this.activeSessionId=null,this.sessionOrder.length>0){let i=n;if(!i||!this.sessions.has(i))try{i=localStorage.getItem("codeman-active-session")}catch{}i&&this.sessions.has(i)?this.selectSession(i):this.selectSession(this.sessionOrder[0])}}async loadState(){try{const t=await(await fetch("/api/status")).json();this.handleInit(t?.data??{})}catch(e){console.error("Failed to load state:",e)}}_debouncedCall(e,t,s=100){this._debounceTimers[e]&&clearTimeout(this._debounceTimers[e]),this._debounceTimers[e]=setTimeout(()=>{this._debounceTimers[e]=null,t.call(this)},s)}renderSessionTabs(){this._inlineRenameActive||this._debouncedCall("sessionTabs",this._renderSessionTabsImmediate)}_updateActiveTabImmediate(e){const t=this.$("sessionTabs");if(!t)return;const s=t.querySelectorAll(".session-tab[data-id]");for(const n of s)n.dataset.id===e?n.classList.add("active"):n.classList.remove("active");this._scrollActiveTabIntoView(e)}_scrollActiveTabIntoView(e,t="smooth"){const s=this.$("sessionTabs");if(!s)return;const n=e&&s.querySelector(`.session-tab[data-id="${e}"]`)||s.querySelector(".session-tab.active");if(!n)return;const i=window.CodemanTabOverflow?.computeTabScrollLeft;if(!i)return;const o=s.getBoundingClientRect(),a=n.getBoundingClientRect(),r=i({scrollLeft:s.scrollLeft,clientWidth:s.clientWidth,scrollWidth:s.scrollWidth,tabLeft:a.left-o.left+s.scrollLeft,tabWidth:a.width});if(Math.abs(r-s.scrollLeft)<1)return;const l=window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;typeof s.scrollTo=="function"?s.scrollTo({left:r,behavior:l?"auto":t}):s.scrollLeft=r}_setTerminalLoadState(e,t,s){this.terminalLoadStates.set(e,{generation:t,phase:s}),this._updateTerminalLoadTab(e)}_clearTerminalLoadState(e,t){const s=this.terminalLoadStates.get(e);s&&s.generation!==t||(this.terminalLoadStates.delete(e),this._updateTerminalLoadTab(e))}_updateTerminalLoadTab(e){const t=this.$("sessionTabs")?.querySelector(`.session-tab[data-id="${e}"]`);if(!t)return;const s=this.terminalLoadStates.get(e);if(t.classList.toggle("tab-loading",!!s),s){if(t.setAttribute("aria-busy","true"),t.dataset.loadPhase=s.phase,!t.querySelector(".tab-load-spinner")){const n=document.createElement("span");n.className="tab-load-spinner",n.setAttribute("aria-hidden","true");const i=t.querySelector(".tab-number");i?i.insertAdjacentElement("afterend",n):t.insertBefore(n,t.firstChild)}}else t.setAttribute("aria-busy","false"),delete t.dataset.loadPhase,t.querySelector(".tab-load-spinner")?.remove()}_renderSessionTabsImmediate(){if(this._inlineRenameActive)return;const e=this.$("sessionTabs"),t=e.querySelectorAll(".session-tab[data-id]"),s=new Set([...t].map(l=>l.dataset.id)),n=new Set(this.sessions.keys()),i=[...e.querySelectorAll(".session-tab[data-webview-id]")].map(l=>l.dataset.webviewId),o=(this.webviewOrder||[]).filter(l=>this.webviews?.has(l)),a=i.length===o.length&&i.every((l,d)=>l===o[d]);if(s.size===n.size&&[...s].every(l=>n.has(l))&&a)for(const[l,d]of this.sessions){const c=e.querySelector(`.session-tab[data-id="${l}"]`);if(!c)continue;const S=l===this.activeSessionId&&!this.activeWebviewId,u=d.status||"idle",E=this.getSessionName(d),y=d.taskStats||{running:0,total:0},C=y.running>0,R=this.terminalLoadStates.get(l);if(S&&!c.classList.contains("active")?c.classList.add("active"):!S&&c.classList.contains("active")&&c.classList.remove("active"),c.classList.toggle("tab-loading",!!R),R){if(c.setAttribute("aria-busy","true"),c.dataset.loadPhase=R.phase,!c.querySelector(".tab-load-spinner")){const f=document.createElement("span");f.className="tab-load-spinner",f.setAttribute("aria-hidden","true");const g=c.querySelector(".tab-number");g?g.insertAdjacentElement("afterend",f):c.insertBefore(f,c.firstChild)}}else c.setAttribute("aria-busy","false"),delete c.dataset.loadPhase,c.querySelector(".tab-load-spinner")?.remove();const O=this.tabAlerts.get(l),A=O==="action",T=O==="idle",k=c.classList.contains("tab-alert-action"),p=c.classList.contains("tab-alert-idle");if(A&&!k?(c.classList.add("tab-alert-action"),c.classList.remove("tab-alert-idle")):T&&!p?(c.classList.add("tab-alert-idle"),c.classList.remove("tab-alert-action")):!O&&(k||p)&&c.classList.remove("tab-alert-action","tab-alert-idle"),!c.querySelector(".tab-number")){const f=this.sessionOrder.indexOf(l);if(f>=0&&f<9){const g=document.createElement("span");g.className="tab-number",g.textContent=String(f+1),c.insertBefore(g,c.firstChild)}}const b=c.querySelector(".tab-status");b&&!b.classList.contains(u)&&(b.className=`tab-status ${u}`);const L=c.querySelector(".tab-name");if(L){const f=parseSessionPrefix(E),g=f&&f.suffix?f.suffix:E;L.textContent!==g&&(L.textContent=g,c.title=f&&f.suffix?d.workingDir?`${f.prefix} (${d.workingDir})`:f.prefix:d.workingDir||"")}const _=c.querySelector(".tab-badge");if(C)if(_)_.textContent!==String(y.running)&&(_.textContent=y.running);else{this._fullRenderSessionTabs();return}else if(_){this._fullRenderSessionTabs();return}const w=c.querySelector(".tab-subagent-badge"),m=this.minimizedSubagents.get(l),v=m?.size||0;if(v>0&&w){const f=w.querySelector(".subagent-label"),g=v===1?"AGENT":`AGENTS (${v})`;f&&f.textContent!==g&&(f.textContent=g);const I=w.querySelector(".subagent-dropdown");if(I){const x=this.renderSubagentTabBadge(l,m),N=document.createElement("div");N.innerHTML=x;const D=N.querySelector(".subagent-dropdown");D&&(I.innerHTML=D.innerHTML)}}else if(v>0&&!w){const f=this.renderSubagentTabBadge(l,m),g=c.querySelector(".tab-actions");g&&g.insertAdjacentHTML("beforebegin",f)}else v===0&&w&&w.remove()}else this._fullRenderSessionTabs();this._lastRenderedActiveTabId=this.activeSessionId,this.updateTabOverflowMode(),this._applyTabEntrances?.(),this._refreshMobileOverviewIfVisible?.(),this._refreshHomeSessionsIfVisible?.(),this._lineageEdgeCount>0&&this.updateConnectionLines()}updateTabOverflowMode(){const e=this.$("sessionTabs");if(!e)return;const t=MobileDetection.getDeviceType(),s=this.loadAppSettingsFromStorage(),n=this.getDefaultSettings(),i=t==="desktop"?s.tabTwoRows??n.tabTwoRows??!1:!1;if(i||t!=="desktop"){e.classList.remove("tabs-auto-wrap");return}e.classList.remove("tabs-auto-wrap");const o=window.CodemanTabOverflow?.shouldAutoWrapTabs?window.CodemanTabOverflow.shouldAutoWrapTabs({deviceType:t,manualTwoRows:i,tabCount:this.sessions.size,scrollWidth:e.scrollWidth,clientWidth:e.clientWidth}):e.scrollWidth>e.clientWidth+1;e.classList.toggle("tabs-auto-wrap",o)}_setupTabMiddleClickClose(){const e=this.$("sessionTabs");!e||this._tabAuxClickBound||(this._tabAuxClickBound=!0,e.addEventListener("auxclick",t=>{if(t.button!==1)return;const s=t.target.closest?.(".session-tab");s&&(t.preventDefault(),t.stopPropagation(),s.dataset.id?this.requestCloseSession(s.dataset.id):s.dataset.webviewId&&this.closeWebviewTab?.(s.dataset.webviewId))}))}_fullRenderSessionTabs(){if(this._inlineRenameActive)return;const e=this.$("sessionTabs");document.querySelectorAll("body > .subagent-dropdown").forEach(r=>r.remove()),this.cancelHideSubagentDropdown();const t=e.scrollLeft,s=this._lastRenderedActiveTabId,n=!e.querySelector(".session-tab"),i=[],o=this.sessionOrder;let a=0;for(const r of o){const l=this.sessions.get(r);if(!l)continue;const d=r===this.activeSessionId&&!this.activeWebviewId,c=l.status||"idle",S=this.getSessionName(l),u=l.mode||"claude",E=l.color||"default",y=l.taskStats||{running:0,total:0},C=y.running>0,R=this.tabAlerts.get(r),O=R==="action"?" tab-alert-action":R==="idle"?" tab-alert-idle":"",A=this.terminalLoadStates.get(r),T=this.minimizedSubagents.get(r),p=(T?.size||0)>0?this.renderSubagentTabBadge(r,T):"",b=this.renderUltracodeTabBadge?this.renderUltracodeTabBadge(r):"",L=l.workingDir&&l.workingDir.split("/").pop()||"",w=(this._tallTabsEnabled??!1)&&l.name&&L&&L!==S,m=parseSessionPrefix(S),v=m&&m.suffix?m.suffix:S,f=m&&m.suffix?l.workingDir?`${m.prefix} (${l.workingDir})`:m.prefix:l.workingDir||"";i.push(`<div class="session-tab ${d?"active":""}${O}${A?" tab-loading":""}" data-id="${r}" data-color="${E}" ${A?`data-load-phase="${escapeHtml(A.phase)}"`:""} onclick="app.handleSessionTabClick(event, ${escapeHtml(JSON.stringify(r))})" oncontextmenu="event.preventDefault(); app.startInlineRename(${escapeHtml(JSON.stringify(r))})" tabindex="0" role="tab" aria-selected="${d?"true":"false"}" aria-busy="${A?"true":"false"}" aria-label="${escapeHtml(S)} session" ${f?`title="${escapeHtml(f)}"`:""}>
16
+ Weekly limit: ${i??"\u2014"}% used (resets ${r(e.sevenDay)})`}_onScheduledCreated(e){this.currentRun=e,this.showTimer()}_onScheduledUpdated(e){this.currentRun=e,this.updateTimer()}_onScheduledCompleted(e){this.currentRun=e,this.hideTimer(),this.showToast("Scheduled run completed!","success")}_onScheduledStopped(){this.currentRun=null,this.hideTimer()}setConnectionStatus(e){this._connectionStatus=e,e==="connected"?(this._connDownSince=null,this._nextSseRetryAt=null,this._offlineOverlayDismissed=!1):this._connDownSince===null&&(this._connDownSince=Date.now()),this._updateConnectionIndicator(),this._updateConnectionLossUi(),e==="connected"&&this._redeliverSweep()}_connectWs(e){this._disconnectWs(),this._wsState="connecting",this._updateConnectionIndicator();const t=location.protocol==="https:"?"wss:":"ws:",s=this._clientId?`${this._clientId}:${this._wsTabNonce}`:"",n=s?`?cid=${encodeURIComponent(s)}`:"",i=`${t}//${location.host}/ws/sessions/${e}/terminal${n}`,o=new WebSocket(i);this._ws=o,this._wsSessionId=e,o.onopen=()=>{this._ws===o&&(this._wsReady=!0,this._wsState="connected",this._wsReconnectAttempts=0,this._updateConnectionIndicator(),this.sendResize(e)?.catch?.(()=>{}),this._startMobileResizeRetry(e),this._onWsReady(e))},o.onmessage=a=>{if(this._ws===o){this._wsLastRecvAt=Date.now();try{const r=JSON.parse(a.data);r.t==="o"?this._onSessionTerminal({id:e,data:r.d}):r.t==="c"?this._onSessionClearTerminal({id:e}):r.t==="r"?this._onSessionNeedsRefresh({id:e}):r.t==="ia"&&this._onWsInputAck(r.seq)}catch{}}},o.onclose=a=>{if(this._ws!==o)return;this._ws=null,this._wsSessionId=null,this._wsReady=!1,this._stopMobileResizeRetry();const r=window.CodemanWsReconnect.plan(a.code,this._wsReconnectAttempts||0);_crashDiag.log(`WS CLOSE code=${a.code} reason=${a.reason||""} action=${r.action} attempts=${this._wsReconnectAttempts||0}`);const l=this.activeSessionId===e;if(r.action==="give-up")this._wsState=l?"fallback":"disconnected",this._updateConnectionIndicator();else if(r.action==="reconnect")if(l){this._wsState="reconnecting",this._updateConnectionIndicator();const d=r.delayMs+Math.floor(Math.random()*250);this._wsReconnectAttempts=(this._wsReconnectAttempts||0)+1,this._wsReconnectTimer=setTimeout(()=>{this._wsReconnectTimer=null,this.activeSessionId===e&&this._connectWs(e)},d)}else this._wsState="disconnected",this._updateConnectionIndicator();else this._wsState=l?"fallback":"disconnected",this._updateConnectionIndicator(),l&&(this._wsReconnectAttempts=(this._wsReconnectAttempts||0)+1,this._wsReconnectTimer=setTimeout(()=>{this._wsReconnectTimer=null,this.activeSessionId===e&&this._connectWs(e)},r.delayMs))},o.onerror=()=>{}}_disconnectWs(){this._clearTimer("_wsReconnectTimer"),this._wsState="disconnected",this._stopMobileResizeRetry(),this._ws&&(this._ws.onclose=null,this._ws.close(),this._ws=null,this._wsSessionId=null,this._wsReady=!1)}_startMobileResizeRetry(e){this._stopMobileResizeRetry(),(typeof MobileDetection<"u"&&MobileDetection.getDeviceType?MobileDetection.getDeviceType():"desktop")!=="desktop"&&(this._mobileResizeRetryTimer=setInterval(()=>{document.visibilityState==="visible"&&(!this._wsReady||this._wsSessionId!==e||typeof KeyboardHandler<"u"&&KeyboardHandler.keyboardVisible||this.sendResize(e)?.catch?.(()=>{}))},MOBILE_RESIZE_RETRY_MS))}_stopMobileResizeRetry(){this._mobileResizeRetryTimer&&(clearInterval(this._mobileResizeRetryTimer),this._mobileResizeRetryTimer=null)}_sendInputAsync(e,t,s){!e||!t||this._reliableSend(e,t,s?.useMux===!0)}_sendInputEphemeral(e,t){if(!(!e||!t)){if(this._ws&&this._ws.readyState===WebSocket.OPEN&&this._wsSessionId===e)try{this._ws.send(JSON.stringify({t:"i",d:t}));return}catch{}try{fetch(`/api/sessions/${e}/input`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input:t}),keepalive:!0}).catch(()=>{})}catch{}}}_reliableSend(e,t,s){const i={seq:this._nextSeq(e),data:t,useMux:!!s,ts:Date.now(),tries:0,sentAt:0};let o=this._pendingDeliveries.get(e);o||(o=[],this._pendingDeliveries.set(e,o)),o.push(i),this._persistReliableState(),this._updateConnectionIndicator(),this._drainSession(e)}_nextSeq(e){const t=(this._seqCounters.get(e)||0)+1;return this._seqCounters.set(e,t),t}_drainSession(e){const t=this._pendingDeliveries.get(e);if(!(!t||t.length===0)){if(this._ws&&this._ws.readyState===WebSocket.OPEN&&this._wsSessionId===e){for(const s of t)if(s.sentAt===0)try{this._ws.send(JSON.stringify({t:"i",d:s.data,seq:s.seq,cid:this._clientId})),s.sentAt=Date.now(),s.tries++}catch{break}return}this._postDraining.has(e)||(this._postDraining.add(e),(async()=>{try{for(;;){const s=this._pendingDeliveries.get(e);if(!s||s.length===0||this._ws&&this._ws.readyState===WebSocket.OPEN&&this._wsSessionId===e)break;const n=s[0];n.tries++,n.sentAt=Date.now();let i=null;try{const o={input:n.data,seq:n.seq,clientId:this._clientId};n.useMux&&(o.useMux=!0),i=await fetch(`/api/sessions/${e}/input`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o),keepalive:n.data.length<65536})}catch{i=null}if(i&&i.ok)this._ackDelivery(e,n.seq);else if(i&&(i.status===404||i.status===410))this._ackDelivery(e,n.seq);else break}}finally{this._postDraining.delete(e)}})())}}_ackDelivery(e,t){const s=this._pendingDeliveries.get(e);if(s){const n=s.findIndex(i=>i.seq===t);n!==-1&&(s.splice(n,1),s.length===0&&this._pendingDeliveries.delete(e),this._pendingDeliveries.size===0?this._persistReliableNow():this._persistReliableState(),this._updateConnectionIndicator())}this.clearPendingHooks?.(e)}_onWsInputAck(e){this._wsSessionId&&Number.isInteger(e)&&this._ackDelivery(this._wsSessionId,e)}_onWsReady(e){const t=this._pendingDeliveries.get(e);if(t)for(const s of t)s.sentAt=0;this._drainSession(e)}_redeliverSweep(){if(this._pendingDeliveries.size!==0)for(const e of[...this._pendingDeliveries.keys()]){const t=this._pendingDeliveries.get(e);if(!t||t.length===0)continue;if(this._ws&&this._ws.readyState===WebSocket.OPEN&&this._wsSessionId===e){const n=t[0],i=n&&n.sentAt&&Date.now()-n.sentAt>this._reliableAckTimeoutMs,o=Date.now()-this._wsLastRecvAt>this._reliableAckTimeoutMs;if(i&&o){try{this._ws.close()}catch{}continue}if(i)for(const a of t)a.sentAt&&Date.now()-a.sentAt>this._reliableAckTimeoutMs&&(a.sentAt=0)}this._drainSession(e)}}_pendingBytes(){let e=0,t=0;for(const s of this._pendingDeliveries.values())for(const n of s)e+=n.data.length,t++;return{bytes:e,count:t}}_loadReliableState(){try{this._clientId=localStorage.getItem("codeman:clientId")||""}catch{this._clientId=""}if(!this._clientId){this._clientId="c-"+Math.random().toString(36).slice(2)+"-"+Date.now().toString(36);try{localStorage.setItem("codeman:clientId",this._clientId)}catch{}}try{const e=localStorage.getItem("codeman:pendingInput");if(!e)return;const t=JSON.parse(e);if(t&&t.seqs)for(const[s,n]of Object.entries(t.seqs))Number.isFinite(n)&&this._seqCounters.set(s,n);if(t&&t.pending)for(const[s,n]of Object.entries(t.pending))Array.isArray(n)&&n.length&&this._pendingDeliveries.set(s,n.filter(i=>i&&typeof i.data=="string"&&Number.isInteger(i.seq)).map(i=>({seq:i.seq,data:i.data,useMux:!!i.useMux,ts:i.ts||Date.now(),tries:0,sentAt:0})))}catch{}}_persistReliableState(){this._persistReliableTimer||(this._persistReliableTimer=setTimeout(()=>{this._persistReliableTimer=null,this._persistReliableNow()},250))}_persistReliableNow(){this._persistReliableTimer&&(clearTimeout(this._persistReliableTimer),this._persistReliableTimer=null);try{const e={};for(const[i,o]of this._seqCounters)e[i]=o;const t={};let s=0;for(const[i,o]of this._pendingDeliveries)if(o.length){t[i]=o.map(a=>({seq:a.seq,data:a.data,useMux:a.useMux,ts:a.ts,tries:a.tries}));for(const a of o)s+=a.data.length}const n=s>this._reliableMaxBytes?{seqs:e}:{seqs:e,pending:t};localStorage.setItem("codeman:pendingInput",JSON.stringify(n))}catch{}}_computeConnectionDescriptor(){const{bytes:e,count:t}=this._pendingBytes(),s=t>0,i=e>4,o=r=>r<1024?`${r}B`:`${(r/1024).toFixed(1)}KB`,a=i?` \xB7 ${o(e)} queued`:"";if(!this.isOnline||this._connectionStatus==="offline")return{display:"flex",dotClass:"connection-dot offline",text:i?`Offline (${o(e)} queued)`:"Offline",title:"No network connection"};if(this.activeSessionId){let r,l,d;switch(this._wsState){case"connected":r="connected",l="WS",d="Terminal connected over WebSocket";break;case"fallback":r="fallback",l="HTTP",d="WebSocket unavailable \u2014 input sent over HTTP";break;case"reconnecting":r="reconnecting",l="WS\u2026",d="Reconnecting WebSocket";break;default:r="reconnecting",l="WS\u2026",d="Connecting WebSocket";break}return{display:"flex",dotClass:`connection-dot ${r}`,text:`${l}${a}`,title:d}}return this._connectionStatus==="reconnecting"||this._connectionStatus==="disconnected"?{display:"flex",dotClass:"connection-dot reconnecting",text:i?`Reconnecting (${o(e)} queued)`:"Reconnecting...",title:"Reconnecting to server"}:s?{display:"flex",dotClass:"connection-dot draining",text:i?`Sending ${o(e)}...`:"Sending...",title:"Delivering queued input"}:{display:"none",dotClass:"",text:"",title:""}}_updateConnectionIndicator(){const e=this.$("connectionIndicator"),t=this.$("connectionDot"),s=this.$("connectionText");if(!e||!t||!s)return;const n=this._computeConnectionDescriptor(),i=this._lastIndicatorDescriptor;i&&i.display===n.display&&i.dotClass===n.dotClass&&i.text===n.text&&i.title===n.title||(this._lastIndicatorDescriptor=n,e.style.display=n.display,n.display!=="none"&&(t.className=n.dotClass,s.textContent=n.text,e.title=n.title))}setupOnlineDetection(){window.addEventListener("online",()=>{this.isOnline=!0,this.reconnectAttempts=0,this._connDownSince=Date.now(),this.connectSSE(),this._redeliverSweep()}),window.addEventListener("offline",()=>{this.isOnline=!1,this.setConnectionStatus("offline")})}_updateConnectionLossUi(){const e=window.CodemanConnectionLoss,t=this.$("offlineBanner"),s=this.$("offlineOverlay");if(!e||!t||!s)return;const n=e.compute({isOnline:this.isOnline,status:this._connectionStatus,everLoaded:this._initGeneration>0,downSince:this._connDownSince,now:Date.now(),nextRetryAt:this._nextSseRetryAt,overlayDismissed:this._offlineOverlayDismissed,retryPending:this._offlineRetryPending});this._connDownSince===null?this._stopOfflineTicker():this._startOfflineTicker();const i=this._offlineRetryPending?"Reconnecting\u2026":n.retryInSec!=null&&n.retryInSec>0?`Retrying in ${n.retryInSec}s`:"Retrying\u2026",o=`${n.mode}|${n.kind}|${i}`;if(o!==this._lastOfflineUiKey){if(this._lastOfflineUiKey=o,t.hidden=n.mode!=="banner",s.hidden=n.mode!=="overlay",document.body.classList.toggle("connection-lost",n.mode!=="hidden"),n.mode==="banner"){const a=this.$("offlineBannerText"),r=this.$("offlineBannerDetail");a&&(a.textContent=n.title),r&&(r.textContent=i)}else if(n.mode==="overlay"){const a=this.$("offlineOverlayTitle"),r=this.$("offlineOverlayBody"),l=this.$("offlineOverlayHost"),d=this.$("offlineOverlayStatus");a&&(a.textContent=n.title),r&&(r.textContent=n.detail),l&&(l.textContent=location.host),d&&(d.textContent=i)}}}_startOfflineTicker(){this._offlineUiTicker||(this._offlineUiTicker=setInterval(()=>this._updateConnectionLossUi(),1e3))}_stopOfflineTicker(){this._offlineUiTicker&&(clearInterval(this._offlineUiTicker),this._offlineUiTicker=null)}retryConnection(){this._offlineRetryPending=!0,this._nextSseRetryAt=null,this.reconnectAttempts=0,this._clearTimer("sseReconnectTimeout"),this.isOnline=navigator.onLine,this._lastOfflineUiKey="",this._updateConnectionLossUi(),this.connectSSE(),this.activeSessionId&&this._wsState!=="connected"&&(this._wsReconnectAttempts=0,this._connectWs(this.activeSessionId)),this._clearTimer("_offlineRetryTimer"),this._offlineRetryTimer=setTimeout(()=>{this._offlineRetryPending=!1,this._lastOfflineUiKey="",this._updateConnectionLossUi()},1500)}dismissOfflineOverlay(){this._offlineOverlayDismissed=!0,this._lastOfflineUiKey="",this._updateConnectionLossUi()}_updateCjkInputState(){const e=document.getElementById("cjkInput");if(!e)return;const t=this.loadAppSettingsFromStorage(),s=this.getDefaultSettings?.()||{},i=(this._serverCjkOverride||(t.cjkInputEnabled??s.cjkInputEnabled??!1))&&!!this.activeSessionId;e.classList.toggle("cjk-input-visible",!!i),document.body.classList.toggle("cjk-input-visible",!!i),e.style.display=i?"block":"none",e.setAttribute("aria-hidden",i?"false":"true"),i||(window.cjkActive=!1),typeof KeyboardHandler<"u"&&KeyboardHandler.updateLayoutForKeyboard()}_resetAllAppState(){this.sessions.clear(),this.ralphStates.clear(),this.terminalBuffers.clear(),this.terminalBufferCache.clear(),this._xtermSnapshots?.clear(),this.projectInsights.clear(),this.teams.clear(),this.teamTasks.clear();for(const e of this.idleTimers.values())clearTimeout(e);if(this.idleTimers.clear(),this._clearTimer("flickerFilterTimeout"),this.flickerFilterBuffer="",this.flickerFilterActive=!1,this._clearTimer("syncWaitTimeout"),this.pendingWrites=[],this.writeFrameScheduled=!1,this._isLoadingBuffer=!1,this._loadBufferQueue=null,this._bufferLoadOwner=null,this._chunkedWriteGen=(this._chunkedWriteGen||0)+1,this._localEchoOverlay?.rerender(),this._predictiveEcho?.clearPredictions(),this.pendingHooks.clear(),this.approvals?.clear(),this._parentNameCache&&this._parentNameCache.clear(),this.subagentActivity.clear(),this.subagentToolResults.clear(),this.workflowRuns&&this.workflowRuns.clear(),this.workflowRunDetails&&this.workflowRunDetails.clear(),this.activeWorkflowRunId=null,this.activeWorkflowPhaseIndex=null,MobileDetection.cleanup(),KeyboardHandler.cleanup(),MobileDetection.init(),KeyboardHandler.init(),this.tabAlerts.clear(),this.attachmentHistoryCounts.clear(),this._shownCompletions&&this._shownCompletions.clear(),this.notificationManager?.titleFlashInterval&&(clearInterval(this.notificationManager.titleFlashInterval),this.notificationManager.titleFlashInterval=null),this.notificationManager?.groupingMap){for(const{timeout:e}of this.notificationManager.groupingMap.values())clearTimeout(e);this.notificationManager.groupingMap.clear()}this.terminalResizeObserver&&(this.terminalResizeObserver.disconnect(),this.terminalResizeObserver=null),this.planLoadingTimer&&(clearInterval(this.planLoadingTimer),this.planLoadingTimer=null),this.timerCountdownInterval&&(clearInterval(this.timerCountdownInterval),this.timerCountdownInterval=null),this.runSummaryAutoRefreshTimer&&(clearInterval(this.runSummaryAutoRefreshTimer),this.runSummaryAutoRefreshTimer=null)}handleInit(e){this._clearTimer("_initFallbackTimer");const t=++this._initGeneration;if(this._serverCjkOverride=e.inputCjkForm||!1,this._updateCjkInputState(),e.planUsage&&this.updatePlanUsageChip(e.planUsage),e.version){const i=this.$("versionDisplay"),o=this.$("headerVersion");i&&(i.textContent=`v${e.version}`,i.title=`Codeman v${e.version}`),o&&(o.textContent=`v${e.version}`,o.title=`Codeman v${e.version}`)}VoiceInput.cleanup(),this._resetAllAppState(),e.sessions.forEach(i=>{this.sessions.set(i.id,i),(i.ralphLoop||i.ralphTodos)&&!this.ralphClosedSessions.has(i.id)&&this.ralphStates.set(i.id,{loop:i.ralphLoop||null,todos:i.ralphTodos||[]})});try{localStorage.removeItem("codeman-tab-meta")}catch{}if(Array.isArray(e.sessionOrder)&&e.sessionOrder.length)try{localStorage.setItem("codeman-session-order",JSON.stringify(e.sessionOrder))}catch{}this.syncSessionOrder(),e.respawnStatus?this.respawnStatus=e.respawnStatus:this.respawnStatus={},this.respawnTimers={},this.respawnCountdownTimers={},this.respawnActionLogs={},e.globalStats&&(this.globalStats=e.globalStats),this.totalCost=e.sessions.reduce((i,o)=>i+(o.totalCost||0),0),this.totalCost+=e.scheduledRuns.reduce((i,o)=>i+(o.totalCost||0),0);const s=e.scheduledRuns.find(i=>i.status==="running");if(s&&(this.currentRun=s,this.showTimer()),this.updateCost(),this.renderSessionTabs(),this.seedApprovals?.(),this.sessions.size>0?this.startSystemStatsPolling():this.stopSystemStatsPolling(),this.cleanupAllFloatingWindows(),e.subagents&&(this.subagents.clear(),this.subagentActivity.clear(),this.subagentToolResults.clear(),e.subagents.forEach(i=>{this.subagents.set(i.agentId,i)}),this.renderSubagentPanel(),this.subagentParentMap.clear(),this.loadSubagentParentMap().then(()=>{for(const[i,o]of this.subagentParentMap){const a=this.subagents.get(i);if(a&&this.sessions.has(o)){a.parentSessionId=o;const r=this.sessions.get(o);r&&(a.parentSessionName=this.getSessionName(r)),this.subagents.set(i,a)}}for(const[i]of this.subagents)this.subagentParentMap.has(i)||this.findParentSessionForSubagent(i);this.restoreSubagentWindowStates()})),e.workflowRuns&&this.seedWorkflowRuns(e.workflowRuns),t!==this._initGeneration)return;if(this.isSoloWindow){this._applySoloMode();return}const n=this.activeSessionId;if(this.activeSessionId=null,this.sessionOrder.length>0){let i=n;if(!i||!this.sessions.has(i))try{i=localStorage.getItem("codeman-active-session")}catch{}i&&this.sessions.has(i)?this.selectSession(i):this.selectSession(this.sessionOrder[0])}}async loadState(){try{const t=await(await fetch("/api/status")).json();this.handleInit(t?.data??{})}catch(e){console.error("Failed to load state:",e)}}_debouncedCall(e,t,s=100){this._debounceTimers[e]&&clearTimeout(this._debounceTimers[e]),this._debounceTimers[e]=setTimeout(()=>{this._debounceTimers[e]=null,t.call(this)},s)}renderSessionTabs(){this._inlineRenameActive||this._debouncedCall("sessionTabs",this._renderSessionTabsImmediate)}_updateActiveTabImmediate(e){const t=this.$("sessionTabs");if(!t)return;const s=t.querySelectorAll(".session-tab[data-id]");for(const n of s)n.dataset.id===e?n.classList.add("active"):n.classList.remove("active");this._scrollActiveTabIntoView(e)}_scrollActiveTabIntoView(e,t="smooth"){const s=this.$("sessionTabs");if(!s)return;const n=e&&s.querySelector(`.session-tab[data-id="${e}"]`)||s.querySelector(".session-tab.active");if(!n)return;const i=window.CodemanTabOverflow?.computeTabScrollLeft;if(!i)return;const o=s.getBoundingClientRect(),a=n.getBoundingClientRect(),r=i({scrollLeft:s.scrollLeft,clientWidth:s.clientWidth,scrollWidth:s.scrollWidth,tabLeft:a.left-o.left+s.scrollLeft,tabWidth:a.width});if(Math.abs(r-s.scrollLeft)<1)return;const l=window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;typeof s.scrollTo=="function"?s.scrollTo({left:r,behavior:l?"auto":t}):s.scrollLeft=r}_setTerminalLoadState(e,t,s){this.terminalLoadStates.set(e,{generation:t,phase:s}),this._updateTerminalLoadTab(e)}_clearTerminalLoadState(e,t){const s=this.terminalLoadStates.get(e);s&&s.generation!==t||(this.terminalLoadStates.delete(e),this._updateTerminalLoadTab(e))}_updateTerminalLoadTab(e){const t=this.$("sessionTabs")?.querySelector(`.session-tab[data-id="${e}"]`);if(!t)return;const s=this.terminalLoadStates.get(e);if(t.classList.toggle("tab-loading",!!s),s){if(t.setAttribute("aria-busy","true"),t.dataset.loadPhase=s.phase,!t.querySelector(".tab-load-spinner")){const n=document.createElement("span");n.className="tab-load-spinner",n.setAttribute("aria-hidden","true");const i=t.querySelector(".tab-number");i?i.insertAdjacentElement("afterend",n):t.insertBefore(n,t.firstChild)}}else t.setAttribute("aria-busy","false"),delete t.dataset.loadPhase,t.querySelector(".tab-load-spinner")?.remove()}_renderSessionTabsImmediate(){if(this._inlineRenameActive)return;const e=this.$("sessionTabs"),t=e.querySelectorAll(".session-tab[data-id]"),s=new Set([...t].map(l=>l.dataset.id)),n=new Set(this.sessions.keys()),i=[...e.querySelectorAll(".session-tab[data-webview-id]")].map(l=>l.dataset.webviewId),o=(this.webviewOrder||[]).filter(l=>this.webviews?.has(l)),a=i.length===o.length&&i.every((l,d)=>l===o[d]);if(s.size===n.size&&[...s].every(l=>n.has(l))&&a)for(const[l,d]of this.sessions){const c=e.querySelector(`.session-tab[data-id="${l}"]`);if(!c)continue;const S=l===this.activeSessionId&&!this.activeWebviewId,u=d.status||"idle",E=this.getSessionName(d),y=d.taskStats||{running:0,total:0},C=y.running>0,R=this.terminalLoadStates.get(l);if(S&&!c.classList.contains("active")?c.classList.add("active"):!S&&c.classList.contains("active")&&c.classList.remove("active"),c.classList.toggle("tab-loading",!!R),R){if(c.setAttribute("aria-busy","true"),c.dataset.loadPhase=R.phase,!c.querySelector(".tab-load-spinner")){const f=document.createElement("span");f.className="tab-load-spinner",f.setAttribute("aria-hidden","true");const g=c.querySelector(".tab-number");g?g.insertAdjacentElement("afterend",f):c.insertBefore(f,c.firstChild)}}else c.setAttribute("aria-busy","false"),delete c.dataset.loadPhase,c.querySelector(".tab-load-spinner")?.remove();const O=this.tabAlerts.get(l),A=O==="action",T=O==="idle",k=c.classList.contains("tab-alert-action"),p=c.classList.contains("tab-alert-idle");if(A&&!k?(c.classList.add("tab-alert-action"),c.classList.remove("tab-alert-idle")):T&&!p?(c.classList.add("tab-alert-idle"),c.classList.remove("tab-alert-action")):!O&&(k||p)&&c.classList.remove("tab-alert-action","tab-alert-idle"),!c.querySelector(".tab-number")){const f=this.sessionOrder.indexOf(l);if(f>=0&&f<9){const g=document.createElement("span");g.className="tab-number",g.textContent=String(f+1),c.insertBefore(g,c.firstChild)}}const b=c.querySelector(".tab-status");b&&!b.classList.contains(u)&&(b.className=`tab-status ${u}`);const L=c.querySelector(".tab-name");if(L){const f=parseSessionPrefix(E),g=f&&f.suffix?f.suffix:E;L.textContent!==g&&(L.textContent=g,c.title=f&&f.suffix?d.workingDir?`${f.prefix} (${d.workingDir})`:f.prefix:d.workingDir||"")}const _=c.querySelector(".tab-badge");if(C)if(_)_.textContent!==String(y.running)&&(_.textContent=y.running);else{this._fullRenderSessionTabs();return}else if(_){this._fullRenderSessionTabs();return}const w=c.querySelector(".tab-subagent-badge"),m=this.minimizedSubagents.get(l),v=m?.size||0;if(v>0&&w){const f=w.querySelector(".subagent-label"),g=v===1?"AGENT":`AGENTS (${v})`;f&&f.textContent!==g&&(f.textContent=g);const I=w.querySelector(".subagent-dropdown");if(I){const x=this.renderSubagentTabBadge(l,m),N=document.createElement("div");N.innerHTML=x;const D=N.querySelector(".subagent-dropdown");D&&(I.innerHTML=D.innerHTML)}}else if(v>0&&!w){const f=this.renderSubagentTabBadge(l,m),g=c.querySelector(".tab-actions");g&&g.insertAdjacentHTML("beforebegin",f)}else v===0&&w&&w.remove()}else this._fullRenderSessionTabs();this._lastRenderedActiveTabId=this.activeSessionId,this.updateTabOverflowMode(),this._applyTabEntrances?.(),this._refreshMobileOverviewIfVisible?.(),this._refreshHomeSessionsIfVisible?.(),this._lineageEdgeCount>0&&this.updateConnectionLines()}updateTabOverflowMode(){const e=this.$("sessionTabs");if(!e)return;const t=MobileDetection.getDeviceType(),s=this.loadAppSettingsFromStorage(),n=this.getDefaultSettings(),i=t==="desktop"?s.tabTwoRows??n.tabTwoRows??!1:!1;if(i||t!=="desktop"){e.classList.remove("tabs-auto-wrap");return}e.classList.remove("tabs-auto-wrap");const o=window.CodemanTabOverflow?.shouldAutoWrapTabs?window.CodemanTabOverflow.shouldAutoWrapTabs({deviceType:t,manualTwoRows:i,tabCount:this.sessions.size,scrollWidth:e.scrollWidth,clientWidth:e.clientWidth}):e.scrollWidth>e.clientWidth+1;e.classList.toggle("tabs-auto-wrap",o)}_setupTabMiddleClickClose(){const e=this.$("sessionTabs");!e||this._tabAuxClickBound||(this._tabAuxClickBound=!0,e.addEventListener("auxclick",t=>{if(t.button!==1)return;const s=t.target.closest?.(".session-tab");s&&(t.preventDefault(),t.stopPropagation(),s.dataset.id?this.requestCloseSession(s.dataset.id):s.dataset.webviewId&&this.closeWebviewTab?.(s.dataset.webviewId))}))}_fullRenderSessionTabs(){if(this._inlineRenameActive)return;const e=this.$("sessionTabs");document.querySelectorAll("body > .subagent-dropdown").forEach(r=>r.remove()),this.cancelHideSubagentDropdown();const t=e.scrollLeft,s=this._lastRenderedActiveTabId,n=!e.querySelector(".session-tab"),i=[],o=this.sessionOrder;let a=0;for(const r of o){const l=this.sessions.get(r);if(!l)continue;const d=r===this.activeSessionId&&!this.activeWebviewId,c=l.status||"idle",S=this.getSessionName(l),u=l.mode||"claude",E=l.color||"default",y=l.taskStats||{running:0,total:0},C=y.running>0,R=this.tabAlerts.get(r),O=R==="action"?" tab-alert-action":R==="idle"?" tab-alert-idle":"",A=this.terminalLoadStates.get(r),T=this.minimizedSubagents.get(r),p=(T?.size||0)>0?this.renderSubagentTabBadge(r,T):"",b=this.renderUltracodeTabBadge?this.renderUltracodeTabBadge(r):"",L=l.workingDir&&l.workingDir.split("/").pop()||"",w=(this._tallTabsEnabled??!1)&&l.name&&L&&L!==S,m=parseSessionPrefix(S),v=m&&m.suffix?m.suffix:S,f=m&&m.suffix?l.workingDir?`${m.prefix} (${l.workingDir})`:m.prefix:l.workingDir||"";i.push(`<div class="session-tab ${d?"active":""}${O}${A?" tab-loading":""}${this.hasTabDetachOverride(r)?" tab-show-detach":""}" data-id="${r}" data-color="${E}" ${A?`data-load-phase="${escapeHtml(A.phase)}"`:""} onclick="app.handleSessionTabClick(event, ${escapeHtml(JSON.stringify(r))})" oncontextmenu="event.preventDefault(); app.startInlineRename(${escapeHtml(JSON.stringify(r))})" tabindex="0" role="tab" aria-selected="${d?"true":"false"}" aria-busy="${A?"true":"false"}" aria-label="${escapeHtml(S)} session" ${f?`title="${escapeHtml(f)}"`:""}>
17
17
  ${a<9?'<span class="tab-number">'+(a+1)+"</span>":""}
18
18
  ${A?'<span class="tab-load-spinner" aria-hidden="true"></span>':""}
19
19
  <span class="tab-status ${c}" aria-hidden="true"></span>
@@ -37,13 +37,21 @@ Object.assign(CodemanApp.prototype, {
37
37
  async seedApprovals() {
38
38
  if (!this.approvals) this.approvals = new Map();
39
39
  this.approvals.clear();
40
- if (this.approvalsInboxEnabled()) {
41
- const data = await this._apiJson('/api/approvals');
42
- for (const item of (data && data.approvals) || []) {
43
- this.approvals.set(item.id, item);
44
- // Re-arm the tab alert state machine (idempotent set-add).
45
- this.setPendingHook(item.sessionId, approvalKindToHook(item.kind));
46
- }
40
+ // Fetch and re-arm the tab-alert state machine REGARDLESS of the inbox
41
+ // setting. The server-side approval store runs unconditionally (only the
42
+ // inbox SURFACES are opt-in), and the red/yellow tab alert predates the
43
+ // inbox: gating the seed on the setting meant that with the inbox off, a
44
+ // reload landed with every alert store empty while a permission dialog sat
45
+ // blocking a session (owner report 2026-08-15: rail said NEEDS YOU from
46
+ // the live SSE event, the reloaded-elsewhere tab showed a plain green
47
+ // dot). Only populating `this.approvals` (bell/drawer/answer strips) stays
48
+ // behind the setting.
49
+ const data = await this._apiJson('/api/approvals');
50
+ const inboxOn = this.approvalsInboxEnabled();
51
+ for (const item of (data && data.approvals) || []) {
52
+ if (inboxOn) this.approvals.set(item.id, item);
53
+ // Re-arm the tab alert state machine (idempotent set-add).
54
+ this.setPendingHook(item.sessionId, approvalKindToHook(item.kind));
47
55
  }
48
56
  this.renderApprovals();
49
57
  },
@@ -68,14 +76,15 @@ Object.assign(CodemanApp.prototype, {
68
76
  },
69
77
 
70
78
  _onApprovalResolved(info) {
71
- if (!info || !info.id || !this.approvals) return;
72
- if (this.approvals.delete(info.id)) {
73
- // Clear the matching tab alert: the inbox resolves on more signals than
74
- // the hook handlers do (superseded, expired, answered from another
75
- // device), and clearPendingHooks is a no-op when nothing is set.
76
- this.clearPendingHooks(info.sessionId, approvalKindToHook(info.kind));
77
- this.renderApprovals();
78
- }
79
+ if (!info || !info.id) return;
80
+ // Clear the matching tab alert UNCONDITIONALLY: the inbox resolves on more
81
+ // signals than the hook handlers do (superseded, expired, answered from
82
+ // another device), clearPendingHooks is a no-op when nothing is set, and
83
+ // with the inbox setting OFF the item was never stored in `this.approvals`
84
+ // even though seedApprovals armed the alert — gating the clear on a map hit
85
+ // would strand that alert forever.
86
+ this.clearPendingHooks(info.sessionId, approvalKindToHook(info.kind));
87
+ if (this.approvals?.delete(info.id)) this.renderApprovals();
79
88
  },
80
89
 
81
90
  // ─── Actions ─────────────────────────────────────────────────
Binary file
Binary file
@@ -222,23 +222,35 @@ function computeTabScrollLeft(input) {
222
222
  // endpoint scrolled outside the strip. `.session-tabs` is `overflow-x: auto`, so a
223
223
  // scrolled-out tab still HAS a rect — one lying over the logo or the header
224
224
  // buttons. Skipping is honest; clamping would point at a tab that isn't there.
225
- // ⚠ THE DIP IS WHAT MAKES THE ARC AN ARC, and the first shipped numbers were tuned
226
- // against two tabs sitting side by side. A worker the agent skill starts is appended
227
- // to the END of the strip, so the real span between a lead and its worker is 800-1500px,
228
- // not 200, and a 44px cap over 1300px of span is a 33px sag, i.e. a line that reads as
229
- // STRAIGHT and crosses the terminal instead of bracketing under the strip. The dip now
230
- // keeps growing with the span (0.085/px, ~3x steeper against the old cap) so the bracket
231
- // survives the distance the feature is actually used at. The ceiling is what keeps a
232
- // full-width pair out of the terminal's fourth line: 104 + the sibling step lands the
233
- // deepest sag around y=140 on a 1080 screen, the same proportion two adjacent tabs get.
225
+ // ⚠ THE DIP IS WHAT MAKES THE ARC AN ARC, and it has now been mis-tuned in BOTH
226
+ // directions, so treat these numbers as a corridor rather than a dial to crank:
227
+ // - Too shallow (the first ship, 44px cap): a skill worker is appended to the END of
228
+ // the strip, so a lead-to-worker span is 800-1500px, and a 44px cap over 1300px is
229
+ // a 33px sag, a line that reads as STRAIGHT across the terminal (#285).
230
+ // - Too deep (the 104px cap that replaced it): in the wrapped-strip case the cap and
231
+ // the FULL row offset stacked, bowing the bracket ~106px into the terminal text
232
+ // (owner screenshot 2026-08-15, "die Linien machen einen grossen Bogen nach unten").
233
+ // The dip is measured from the STRIP'S BOTTOM EDGE (falling back to the lower tab
234
+ // bottom when the strip rect is missing or shorter than its tabs), which buys two
235
+ // things at once: the bow needs no per-row offsets stacked on top, and a same-row
236
+ // arc between ROW-1 tabs of a wrapped strip clears row 2's labels instead of being
237
+ // drawn through them (the retune's own first draft had exactly that regression).
234
238
  const LINEAGE_DIP_BASE_PX = 14;
235
- const LINEAGE_DIP_PER_PX = 0.085;
239
+ const LINEAGE_DIP_PER_PX = 0.06;
236
240
  const LINEAGE_DIP_MIN_PX = 22;
237
- const LINEAGE_DIP_MAX_PX = 104;
241
+ const LINEAGE_DIP_MAX_PX = 64;
238
242
  // Siblings nest by this much. Widened with the stroke: at 2.5px plus its glow, arcs 6px
239
243
  // apart bled into one thick band instead of reading as three separate lines.
240
244
  const LINEAGE_SIBLING_STEP_PX = 8;
241
245
  const LINEAGE_STRIP_TOLERANCE_PX = 4;
246
+ // Lineage palette, assigned per CHILD in first-seen order and cycled (session-lineage.js).
247
+ // The empty FIRST entry means "no override": the CSS then falls back to --session-blue,
248
+ // which every skin block tunes for its own background, so a lone arc keeps the
249
+ // skin-aware blue that shipped in 1.18.2. The fixed entries are deliberately vivid
250
+ // (owner call 2026-08-15: matrix green, pinkish, violet, red, turquoise "and so on");
251
+ // they ride the same double glow as the blue, which is what keeps them legible over
252
+ // terminal text on every skin.
253
+ const LINEAGE_COLORS = ['', '#00ff66', '#ff5ea8', '#a78bfa', '#ff5252', '#2dd4bf', '#ffa940'];
242
254
 
243
255
  function computeLineagePath(input) {
244
256
  const parent = input?.parent;
@@ -269,18 +281,19 @@ function computeLineagePath(input) {
269
281
  const cBottom = cTop + ch;
270
282
  const sameRow = Math.abs(pTop + ph / 2 - (cTop + ch / 2)) <= Math.min(ph, ch) / 2;
271
283
 
272
- // Both ends anchor on the tab BOTTOM, and the control points hang below whichever
273
- // row is lower, so one formula covers a flat strip and a wrapped one.
284
+ // Both ends anchor on the tab BOTTOM, and the control points hang below the WHOLE
285
+ // strip, so one formula covers a flat strip, a wrapped pair, and a same-row pair
286
+ // sitting above further rows (see the corridor note above the constants).
274
287
  const span = Math.abs(cx - px);
275
- const rowDrop = Math.abs(cBottom - pBottom);
276
- // A wrapped pair needs the dip measured from the LOWER row, or the bracket would
277
- // only reach the row gap again. Adding the row offset also keeps the curve clear of
278
- // the row it crosses instead of grazing its bottom edge.
288
+ const stripBottom =
289
+ strip && Number(strip.height) > 0 && Number.isFinite(Number(strip.top))
290
+ ? Number(strip.top) + Number(strip.height)
291
+ : Number.NEGATIVE_INFINITY;
292
+ const baseline = Math.max(pBottom, cBottom, stripBottom);
279
293
  const dip =
280
294
  Math.min(LINEAGE_DIP_MAX_PX, Math.max(LINEAGE_DIP_MIN_PX, LINEAGE_DIP_BASE_PX + span * LINEAGE_DIP_PER_PX)) +
281
- depth * LINEAGE_SIBLING_STEP_PX +
282
- rowDrop;
283
- const yc = Math.max(pBottom, cBottom) + dip;
295
+ depth * LINEAGE_SIBLING_STEP_PX;
296
+ const yc = baseline + dip;
284
297
  const d = `M ${r1(px)} ${r1(pBottom)} C ${r1(px)} ${r1(yc)}, ${r1(cx)} ${r1(yc)}, ${r1(cx)} ${r1(cBottom)}`;
285
298
  return { d, endX: cx, endY: cBottom, sameRow };
286
299
  }
@@ -441,6 +454,7 @@ if (typeof window !== 'undefined') {
441
454
  DIP_MIN_PX: LINEAGE_DIP_MIN_PX,
442
455
  DIP_MAX_PX: LINEAGE_DIP_MAX_PX,
443
456
  SIBLING_STEP_PX: LINEAGE_SIBLING_STEP_PX,
457
+ COLORS: LINEAGE_COLORS,
444
458
  };
445
459
  window.CodemanConnectionLoss = {
446
460
  compute: computeConnectionLossUi,
Binary file
Binary file
Binary file
@@ -16,7 +16,7 @@
16
16
  <link rel="manifest" href="manifest.json">
17
17
  <title>Codeman</title>
18
18
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%2360a5fa'/%3E%3Cstop offset='100%25' stop-color='%233b82f6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='32' height='32' rx='6' fill='%230a0a0a'/%3E%3Cpath d='M18 4L8 18h6l-2 10 10-14h-6z' fill='url(%23g)'/%3E%3C/svg%3E">
19
- <link rel="stylesheet" href="styles.70bf9c2a.css">
19
+ <link rel="stylesheet" href="styles.d2fa3a17.css">
20
20
  <link rel="stylesheet" href="mobile.1eef31fe.css" media="(max-width: 1023px)">
21
21
  <!-- xterm.css loaded async — terminal won't display until xterm.js runs anyway -->
22
22
  <link rel="preload" href="vendor/xterm.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
@@ -24,8 +24,8 @@
24
24
  <!-- Preload critical resources — lets browser discover these during HTML parse
25
25
  instead of waiting until <script> tags at bottom-of-body are reached. -->
26
26
  <link rel="preload" href="vendor/xterm.min.js" as="script">
27
- <link rel="preload" href="constants.822a486d.js" as="script">
28
- <link rel="preload" href="app.445f1584.js" as="script">
27
+ <link rel="preload" href="constants.04b7b284.js" as="script">
28
+ <link rel="preload" href="app.e2cb9065.js" as="script">
29
29
  <!-- Self-hosted xterm.js — eliminates CDN DNS/TLS latency (~100ms).
30
30
  'defer' preserves execution order (xterm loads before fit addon). -->
31
31
  <script defer src="vendor/xterm.min.js"></script>
@@ -1211,6 +1211,13 @@
1211
1211
  </button>
1212
1212
  </div>
1213
1213
  </div>
1214
+ <div class="set-row" data-search="pop out detach tab window this session">
1215
+ <div class="set-row-text">
1216
+ <span class="set-row-label">Pop-out button on this tab</span>
1217
+ <span class="set-row-desc">Show the open-in-a-window button on this tab even while the general App Settings toggle is off.</span>
1218
+ </div>
1219
+ <label class="switch switch-sm"><input type="checkbox" id="sessionOptShowTabDetach" onchange="app.onSessionTabDetachToggle(this.checked)"><span class="slider"></span></label>
1220
+ </div>
1214
1221
  </div>
1215
1222
  </div>
1216
1223
 
@@ -3192,7 +3199,7 @@
3192
3199
  <div class="approvals-list"></div>
3193
3200
  </div>
3194
3201
 
3195
- <script defer src="constants.822a486d.js"></script>
3202
+ <script defer src="constants.04b7b284.js"></script>
3196
3203
  <script defer src="i18n.86757605.js"></script>
3197
3204
  <script defer src="mobile-handlers.bc45f976.js"></script>
3198
3205
  <script defer src="voice-input.4ec670b0.js"></script>
@@ -3201,7 +3208,7 @@
3201
3208
  <script defer src="input-cjk.63794d0b.js"></script>
3202
3209
  <!-- Hardened markdown HTML sanitizer (wires DOMPurify). Must precede app.js. -->
3203
3210
  <script defer src="sanitize-html.bc7078d6.js"></script>
3204
- <script defer src="app.445f1584.js"></script>
3211
+ <script defer src="app.e2cb9065.js"></script>
3205
3212
  <script defer src="terminal-ui.c182a68d.js"></script>
3206
3213
  <script defer src="respawn-ui.ff0dae4c.js"></script>
3207
3214
  <script defer src="ralph-panel.6de2d0f8.js"></script>
@@ -3213,7 +3220,7 @@
3213
3220
  <script defer src="ultracode-panel.js"></script>
3214
3221
  <script defer src="approvals-ui.js"></script>
3215
3222
  <script defer src="admin-ui.js"></script>
3216
- <script defer src="session-ui.20be58fa.js"></script>
3223
+ <script defer src="session-ui.c67565f2.js"></script>
3217
3224
  <script defer src="webview-tabs.js"></script>
3218
3225
  <script defer src="mobile-overview.js"></script>
3219
3226
  <script defer src="home-sessions.js"></script>
Binary file
Binary file
Binary file
@@ -23,7 +23,7 @@
23
23
  *
24
24
  * @mixin Extends CodemanApp.prototype via Object.assign
25
25
  * @dependency subagent-windows.js (_updateConnectionLinesImmediate, #connectionLines)
26
- * @dependency constants.js (window.CodemanLineage.computePath)
26
+ * @dependency constants.js (window.CodemanLineage.computePath + .COLORS)
27
27
  * @dependency settings-ui.js (loadAppSettingsFromStorage, getDefaultSettings)
28
28
  * @loadorder 15.6 (after ultracode-windows.js — appended to the same SVG pass)
29
29
  */
@@ -90,6 +90,35 @@ Object.assign(CodemanApp.prototype, {
90
90
  return edges;
91
91
  },
92
92
 
93
+ /**
94
+ * Colour for one child's arc, from CodemanLineage.COLORS, assigned in FIRST-SEEN
95
+ * order and remembered per child id. First-seen rather than draw-index keeps a
96
+ * line's colour stable across re-renders, tab reorders and sibling closes (the
97
+ * SVG is wiped and rebuilt constantly, so an index-based colour would flicker).
98
+ * An empty string means "no override": the CSS falls back to --session-blue.
99
+ */
100
+ _lineageColorFor(childId) {
101
+ const palette = (window.CodemanLineage && window.CodemanLineage.COLORS) || [];
102
+ if (palette.length === 0) return '';
103
+ if (!this._lineageColorByChild) {
104
+ this._lineageColorByChild = new Map();
105
+ this._lineageColorNext = 0;
106
+ }
107
+ let idx = this._lineageColorByChild.get(childId);
108
+ if (idx === undefined) {
109
+ idx = this._lineageColorNext++ % palette.length;
110
+ this._lineageColorByChild.set(childId, idx);
111
+ // Bounded: entries for long-gone sessions are pruned once the map is clearly
112
+ // stale, so a day-long dashboard cannot grow it without limit.
113
+ if (this._lineageColorByChild.size > 200 && this.sessions) {
114
+ for (const key of this._lineageColorByChild.keys()) {
115
+ if (!this.sessions.has(key)) this._lineageColorByChild.delete(key);
116
+ }
117
+ }
118
+ }
119
+ return palette[idx] || '';
120
+ },
121
+
93
122
  /**
94
123
  * Append the lineage layer to the shared SVG pass.
95
124
  *
@@ -137,6 +166,10 @@ Object.assign(CodemanApp.prototype, {
137
166
  // the line itself. `status` is the CHILD's, which is the interesting end.
138
167
  const working = edge.status === 'working' ? ' lineage-line--working' : '';
139
168
  line.setAttribute('class', 'connection-line lineage-line' + working);
169
+ // Per-child colour rides a CSS custom property so the stylesheet keeps owning
170
+ // opacity, glow and dash; an empty colour leaves the --session-blue fallback.
171
+ const color = this._lineageColorFor(edge.childId);
172
+ if (color) line.style.setProperty('--lineage-color', color);
140
173
  // `data-agent-id` is what _applyLineEntrances() queries — see the file header.
141
174
  line.setAttribute('data-agent-id', 'lineage:' + edge.childId);
142
175
  line.setAttribute('data-parent-tab', edge.parentId);
@@ -153,6 +186,7 @@ Object.assign(CodemanApp.prototype, {
153
186
  dot.setAttribute('r', '3.5');
154
187
  dot.setAttribute('class', 'lineage-line-dot' + working);
155
188
  dot.setAttribute('data-child-tab', edge.childId);
189
+ if (color) dot.style.setProperty('--lineage-color', color);
156
190
  svg.appendChild(dot);
157
191
  }
158
192
  },