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.
- package/README.md +14 -0
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/{app.445f1584.js → app.e2cb9065.js} +1 -1
- package/dist/web/public/{app.445f1584.js.br → app.e2cb9065.js.br} +0 -0
- package/dist/web/public/{app.445f1584.js.gz → app.e2cb9065.js.gz} +0 -0
- package/dist/web/public/approvals-ui.js +24 -15
- package/dist/web/public/approvals-ui.js.br +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.822a486d.js → constants.04b7b284.js} +34 -20
- package/dist/web/public/constants.04b7b284.js.br +0 -0
- package/dist/web/public/constants.04b7b284.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/i18n.86757605.js.gz +0 -0
- package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
- package/dist/web/public/index.html +13 -6
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.0454a3e9.js.gz +0 -0
- package/dist/web/public/mobile-handlers.bc45f976.js.gz +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.1eef31fe.css.gz +0 -0
- package/dist/web/public/notification-manager.5d229063.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/panels-ui.1e4cb4b2.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/session-lineage.js +35 -1
- package/dist/web/public/session-lineage.js.br +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.20be58fa.js → session-ui.c67565f2.js} +3 -3
- package/dist/web/public/session-ui.c67565f2.js.br +0 -0
- package/dist/web/public/session-ui.c67565f2.js.gz +0 -0
- package/dist/web/public/settings-ui.eacb4f92.js.gz +0 -0
- package/dist/web/public/styles.d2fa3a17.css +1 -0
- package/dist/web/public/styles.d2fa3a17.css.br +0 -0
- package/dist/web/public/styles.d2fa3a17.css.gz +0 -0
- package/dist/web/public/subagent-windows.2a192c3e.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.c182a68d.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.6cec0fba.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.4ec670b0.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/package.json +1 -1
- package/skills/codeman/SKILL.md +25 -11
- package/dist/web/public/constants.822a486d.js.br +0 -0
- package/dist/web/public/constants.822a486d.js.gz +0 -0
- package/dist/web/public/session-ui.20be58fa.js.br +0 -0
- package/dist/web/public/session-ui.20be58fa.js.gz +0 -0
- package/dist/web/public/styles.70bf9c2a.css +0 -1
- package/dist/web/public/styles.70bf9c2a.css.br +0 -0
- 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
|
|
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,"&").replace(/</g,"<").replace(/>/g,">")}</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>
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
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.
|
|
239
|
+
const LINEAGE_DIP_PER_PX = 0.06;
|
|
236
240
|
const LINEAGE_DIP_MIN_PX = 22;
|
|
237
|
-
const LINEAGE_DIP_MAX_PX =
|
|
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
|
|
273
|
-
//
|
|
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
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
|
Binary file
|
|
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.
|
|
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.
|
|
28
|
-
<link rel="preload" href="app.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|
},
|
|
Binary file
|
|
Binary file
|