aicodeman 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/docker-export.d.ts +107 -0
  2. package/dist/docker-export.d.ts.map +1 -0
  3. package/dist/docker-export.js +335 -0
  4. package/dist/docker-export.js.map +1 -0
  5. package/dist/docker-hosts.d.ts +184 -0
  6. package/dist/docker-hosts.d.ts.map +1 -0
  7. package/dist/docker-hosts.js +506 -0
  8. package/dist/docker-hosts.js.map +1 -0
  9. package/dist/mux-interface.d.ts +7 -1
  10. package/dist/mux-interface.d.ts.map +1 -1
  11. package/dist/session.d.ts +9 -2
  12. package/dist/session.d.ts.map +1 -1
  13. package/dist/session.js +48 -4
  14. package/dist/session.js.map +1 -1
  15. package/dist/tmux-manager.d.ts +50 -1
  16. package/dist/tmux-manager.d.ts.map +1 -1
  17. package/dist/tmux-manager.js +214 -7
  18. package/dist/tmux-manager.js.map +1 -1
  19. package/dist/types/api.d.ts +9 -1
  20. package/dist/types/api.d.ts.map +1 -1
  21. package/dist/types/api.js.map +1 -1
  22. package/dist/types/session.d.ts +102 -0
  23. package/dist/types/session.d.ts.map +1 -1
  24. package/dist/types/session.js.map +1 -1
  25. package/dist/web/middleware/auth.d.ts.map +1 -1
  26. package/dist/web/middleware/auth.js +13 -0
  27. package/dist/web/middleware/auth.js.map +1 -1
  28. package/dist/web/network-auth-policy.d.ts +9 -0
  29. package/dist/web/network-auth-policy.d.ts.map +1 -1
  30. package/dist/web/network-auth-policy.js +12 -0
  31. package/dist/web/network-auth-policy.js.map +1 -1
  32. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  33. package/dist/web/public/{app.6890f3d0.js → app.07e61fcf.js} +1 -1
  34. package/dist/web/public/app.07e61fcf.js.br +0 -0
  35. package/dist/web/public/app.07e61fcf.js.gz +0 -0
  36. package/dist/web/public/{constants.5b18ed64.js → constants.503109fa.js} +3 -0
  37. package/dist/web/public/constants.503109fa.js.br +0 -0
  38. package/dist/web/public/{constants.5b18ed64.js.gz → constants.503109fa.js.gz} +0 -0
  39. package/dist/web/public/cron-ui.js.gz +0 -0
  40. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  41. package/dist/web/public/index.html +139 -10
  42. package/dist/web/public/index.html.br +0 -0
  43. package/dist/web/public/index.html.gz +0 -0
  44. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  45. package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
  46. package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
  47. package/dist/web/public/mobile.0644e3ac.css.gz +0 -0
  48. package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
  49. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  50. package/dist/web/public/panels-ui.ca03853f.js.gz +0 -0
  51. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  52. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  53. package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
  54. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  55. package/dist/web/public/session-ui.9da1b71f.js +58 -0
  56. package/dist/web/public/session-ui.9da1b71f.js.br +0 -0
  57. package/dist/web/public/session-ui.9da1b71f.js.gz +0 -0
  58. package/dist/web/public/settings-ui.4acbe789.js +67 -0
  59. package/dist/web/public/settings-ui.4acbe789.js.br +0 -0
  60. package/dist/web/public/settings-ui.4acbe789.js.gz +0 -0
  61. package/dist/web/public/{styles.3d78fd0d.css → styles.546ddae4.css} +1 -1
  62. package/dist/web/public/styles.546ddae4.css.br +0 -0
  63. package/dist/web/public/styles.546ddae4.css.gz +0 -0
  64. package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
  65. package/dist/web/public/sw.js.gz +0 -0
  66. package/dist/web/public/terminal-ui.efda6083.js.gz +0 -0
  67. package/dist/web/public/ultracode-panel.js.gz +0 -0
  68. package/dist/web/public/ultracode-windows.js.gz +0 -0
  69. package/dist/web/public/upload.html.gz +0 -0
  70. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  71. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  72. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  73. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  74. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  75. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  76. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  77. package/dist/web/public/vendor/xterm.css.gz +0 -0
  78. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  79. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  80. package/dist/web/routes/case-routes.d.ts.map +1 -1
  81. package/dist/web/routes/case-routes.js +372 -3
  82. package/dist/web/routes/case-routes.js.map +1 -1
  83. package/dist/web/routes/session-routes.d.ts.map +1 -1
  84. package/dist/web/routes/session-routes.js +67 -4
  85. package/dist/web/routes/session-routes.js.map +1 -1
  86. package/dist/web/schemas.d.ts +71 -1
  87. package/dist/web/schemas.d.ts.map +1 -1
  88. package/dist/web/schemas.js +155 -0
  89. package/dist/web/schemas.js.map +1 -1
  90. package/dist/web/server.d.ts +11 -0
  91. package/dist/web/server.d.ts.map +1 -1
  92. package/dist/web/server.js +81 -1
  93. package/dist/web/server.js.map +1 -1
  94. package/dist/web/sse-events.d.ts +9 -0
  95. package/dist/web/sse-events.d.ts.map +1 -1
  96. package/dist/web/sse-events.js +11 -0
  97. package/dist/web/sse-events.js.map +1 -1
  98. package/package.json +1 -1
  99. package/dist/web/public/app.6890f3d0.js.br +0 -0
  100. package/dist/web/public/app.6890f3d0.js.gz +0 -0
  101. package/dist/web/public/constants.5b18ed64.js.br +0 -0
  102. package/dist/web/public/session-ui.c9094fe9.js +0 -49
  103. package/dist/web/public/session-ui.c9094fe9.js.br +0 -0
  104. package/dist/web/public/session-ui.c9094fe9.js.gz +0 -0
  105. package/dist/web/public/settings-ui.9538a0c1.js +0 -67
  106. package/dist/web/public/settings-ui.9538a0c1.js.br +0 -0
  107. package/dist/web/public/settings-ui.9538a0c1.js.gz +0 -0
  108. package/dist/web/public/styles.3d78fd0d.css.br +0 -0
  109. package/dist/web/public/styles.3d78fd0d.css.gz +0 -0
@@ -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.3d78fd0d.css">
19
+ <link rel="stylesheet" href="styles.546ddae4.css">
20
20
  <link rel="stylesheet" href="mobile.0644e3ac.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.5b18ed64.js" as="script">
28
- <link rel="preload" href="app.6890f3d0.js" as="script">
27
+ <link rel="preload" href="constants.503109fa.js" as="script">
28
+ <link rel="preload" href="app.07e61fcf.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>
@@ -118,8 +118,8 @@
118
118
  </div>
119
119
  <button class="btn-icon-header btn-redraw-terminal btn-redraw-terminal--hidden" onclick="app.restoreTerminalSize()" title="Redraw terminal to fit current screen (Ctrl+Shift+R)" aria-label="Redraw terminal"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg></button>
120
120
  <button class="btn-icon-header btn-response-viewer-header btn-response-viewer-header--hidden" onclick="app.toggleResponseViewer()" title="View last response" aria-label="View last response"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
121
- <button class="btn-icon-header btn-away-digest" onclick="app.openAwayDigest()" title="Away Digest" aria-label="Open away digest"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/></svg></button>
122
- <button class="btn-icon-header btn-session-manager" onclick="app.openSessionManager()" title="Session Manager" aria-label="Open session manager"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg></button>
121
+ <button class="btn-icon-header btn-away-digest btn-away-digest--hidden" onclick="app.openAwayDigest()" title="Away Digest" aria-label="Open away digest"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/></svg></button>
122
+ <button class="btn-icon-header btn-session-manager btn-session-manager--hidden" onclick="app.openSessionManager()" title="Session Manager" aria-label="Open session manager"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg></button>
123
123
  <button class="btn-icon-header btn-attachments-history btn-attachments-history--hidden" id="attachmentsHistoryBtn" onclick="app.toggleAttachmentHistory()" title="Attachments" aria-label="Open attachment history" aria-expanded="false">
124
124
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
125
125
  <span class="attachment-history-badge" id="attachmentHistoryBadge" style="display:none;">0</span>
@@ -557,7 +557,7 @@
557
557
  <div class="toolbar-right">
558
558
  <!-- Orchestrator button hidden until feature is ready -->
559
559
  <!-- <button class="btn-toolbar btn-sm" onclick="app.toggleOrchestratorPanel()" title="Orchestrator Loop">&#x2699; Orchestrator</button> -->
560
- <button class="btn-toolbar btn-sm" onclick="app.openCron()" title="Cron Jobs">&#x23F0; Cron</button>
560
+ <button class="btn-toolbar btn-sm btn-cron" onclick="app.openCron()" title="Cron Jobs">&#x23F0; Cron</button>
561
561
  <span class="version-display" id="versionDisplay" title="Codeman version">v0.0.0</span>
562
562
  </div>
563
563
  </footer>
@@ -1282,6 +1282,27 @@
1282
1282
  <span class="slider"></span>
1283
1283
  </label>
1284
1284
  </div>
1285
+ <div class="settings-item" title="Show the session manager button in the header (opens the session manager — sessions also stay reachable via the Ctrl+K palette)">
1286
+ <span class="settings-item-label">Session Manager Button</span>
1287
+ <label class="switch switch-sm">
1288
+ <input type="checkbox" id="appSettingsShowSessionButton">
1289
+ <span class="slider"></span>
1290
+ </label>
1291
+ </div>
1292
+ <div class="settings-item" title="Show the away digest button in the header (opens the 'what happened while you were away' summary)">
1293
+ <span class="settings-item-label">Away Digest Button</span>
1294
+ <label class="switch switch-sm">
1295
+ <input type="checkbox" id="appSettingsShowAwayDigestButton">
1296
+ <span class="slider"></span>
1297
+ </label>
1298
+ </div>
1299
+ <div class="settings-item" title="Show the Cron button in the footer toolbar (opens the cron jobs manager)">
1300
+ <span class="settings-item-label">Cron Button</span>
1301
+ <label class="switch switch-sm">
1302
+ <input type="checkbox" id="appSettingsShowCronButton">
1303
+ <span class="slider"></span>
1304
+ </label>
1305
+ </div>
1285
1306
  <div class="settings-item" title="Show a terminal redraw button in the header — refit the terminal to the current screen size (useful when switching between devices)">
1286
1307
  <span class="settings-item-label">Redraw Terminal Button</span>
1287
1308
  <label class="switch switch-sm">
@@ -1828,6 +1849,7 @@
1828
1849
  <button class="modal-tab-btn active" data-tab="case-create">Create New</button>
1829
1850
  <button class="modal-tab-btn" data-tab="case-link">Link Existing</button>
1830
1851
  <button class="modal-tab-btn" data-tab="case-remote">Remote</button>
1852
+ <button class="modal-tab-btn" data-tab="case-docker">Docker</button>
1831
1853
  <button class="modal-tab-btn" data-tab="case-manage">Manage</button>
1832
1854
  </div>
1833
1855
  <div class="modal-body">
@@ -1842,6 +1864,53 @@
1842
1864
  <label>Description (optional)</label>
1843
1865
  <input type="text" id="newCaseDescription" placeholder="A brief description..." autocomplete="off">
1844
1866
  </div>
1867
+ <div class="form-row">
1868
+ <label class="checkbox-row"><input type="checkbox" id="newCaseDocker" onchange="app.toggleDockerQuickSettings()"> 🐳 Run in an isolated Docker container</label>
1869
+ <span class="form-hint">One checkbox is enough — it creates the case folder AND a hardened container with default settings, then starts the session inside it. Click to expand for optional presets. Requires the base image (<code>node scripts/build-agent-image.mjs</code>).</span>
1870
+ </div>
1871
+ <details class="advanced-options" id="dockerQuickSettings" style="display:none;">
1872
+ <summary>Container settings (optional — sensible defaults)</summary>
1873
+ <div class="advanced-options-content">
1874
+ <div class="form-row">
1875
+ <label>Template</label>
1876
+ <select id="quickDockerTemplate" onchange="app.applyDockerTemplate()">
1877
+ <option value="small">Small — 2 GB RAM, 1 CPU</option>
1878
+ <option value="medium" selected>Medium — 4 GB RAM, 2 CPU (default)</option>
1879
+ <option value="large">Large — 8 GB RAM, 4 CPU</option>
1880
+ <option value="gpu">GPU — 8 GB RAM, 4 CPU, all GPUs</option>
1881
+ <option value="custom">Custom</option>
1882
+ </select>
1883
+ <span class="form-hint">Disk is elastic — storage grows automatically as data flows in (no fixed cap).</span>
1884
+ </div>
1885
+ <div class="form-row">
1886
+ <label>Memory</label>
1887
+ <input type="text" id="quickDockerMemory" placeholder="4g" autocomplete="off" spellcheck="false">
1888
+ </div>
1889
+ <div class="form-row">
1890
+ <label>CPUs</label>
1891
+ <input type="text" id="quickDockerCpus" placeholder="2" autocomplete="off" spellcheck="false">
1892
+ </div>
1893
+ <div class="form-row">
1894
+ <label>GPUs</label>
1895
+ <input type="text" id="quickDockerGpus" placeholder="none (e.g. all, or 1)" autocomplete="off" spellcheck="false">
1896
+ <span class="form-hint">Needs the NVIDIA container toolkit on the host.</span>
1897
+ </div>
1898
+ <div class="form-row">
1899
+ <label>Network</label>
1900
+ <select id="quickDockerNetwork">
1901
+ <option value="bridge">bridge (internet on)</option>
1902
+ <option value="none">none (fully isolated)</option>
1903
+ </select>
1904
+ </div>
1905
+ <div class="form-row">
1906
+ <label>Image</label>
1907
+ <input type="text" id="quickDockerImage" placeholder="codeman/agent:base" autocomplete="off" spellcheck="false">
1908
+ </div>
1909
+ <div class="form-row">
1910
+ <label class="checkbox-row"><input type="checkbox" id="quickDockerMountCreds" checked> Mount host credentials (~/.claude etc.)</label>
1911
+ </div>
1912
+ </div>
1913
+ </details>
1845
1914
  </div>
1846
1915
  <!-- Link Existing Tab -->
1847
1916
  <div class="modal-tab-content hidden" id="case-link">
@@ -1916,12 +1985,72 @@
1916
1985
  </div>
1917
1986
  </details>
1918
1987
  </div>
1988
+ <!-- Docker Tab -->
1989
+ <div class="modal-tab-content hidden" id="case-docker">
1990
+ <div class="form-row">
1991
+ <label>Case Name</label>
1992
+ <input type="text" id="dockerCaseName" placeholder="sandbox" pattern="[a-zA-Z0-9_-]+" autocomplete="off" autocapitalize="off" spellcheck="false">
1993
+ <span class="form-hint">Runs inside an isolated container. Multiple sessions can share the same container.</span>
1994
+ </div>
1995
+ <div class="form-row">
1996
+ <label>Workspace Path</label>
1997
+ <input type="text" id="dockerWorkspacePath" placeholder="/home/user/projects/sandbox" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false">
1998
+ <span class="form-hint">Absolute HOST directory, bind-mounted into the container. Codeman scaffolds CLAUDE.md + hooks into it.</span>
1999
+ </div>
2000
+ <div class="form-row">
2001
+ <label>Host ID</label>
2002
+ <input type="text" id="dockerHostId" placeholder="local" pattern="[a-zA-Z0-9_-]+" autocomplete="off" autocapitalize="off" spellcheck="false">
2003
+ <span class="form-hint">A reusable docker host profile. Reuse the same ID across cases to share settings.</span>
2004
+ </div>
2005
+ <div class="form-row">
2006
+ <label>Image</label>
2007
+ <input type="text" id="dockerImage" placeholder="codeman/agent:base" autocomplete="off" autocapitalize="off" spellcheck="false">
2008
+ <span class="form-hint">Build it once with <code>node scripts/build-agent-image.mjs</code>. Contains node + claude/codex/gemini + tmux.</span>
2009
+ </div>
2010
+ <div class="form-row">
2011
+ <label>Network</label>
2012
+ <select id="dockerNetwork">
2013
+ <option value="bridge">bridge (internet on, default)</option>
2014
+ <option value="none">none (fully isolated, no network)</option>
2015
+ <option value="custom">custom bridge</option>
2016
+ </select>
2017
+ </div>
2018
+ <details class="advanced-options">
2019
+ <summary>Advanced container settings</summary>
2020
+ <div class="advanced-options-content">
2021
+ <div class="form-row">
2022
+ <label>Memory</label>
2023
+ <input type="text" id="dockerMemory" placeholder="4g" autocomplete="off" spellcheck="false">
2024
+ <span class="form-hint">Optional, e.g. 4g / 512m. Enforced as a hard OOM cap where the engine supports it.</span>
2025
+ </div>
2026
+ <div class="form-row">
2027
+ <label>CPUs</label>
2028
+ <input type="text" id="dockerCpus" placeholder="2" autocomplete="off" spellcheck="false">
2029
+ </div>
2030
+ <div class="form-row">
2031
+ <label class="checkbox-row"><input type="checkbox" id="dockerMountCredentials" checked> Mount host credentials (~/.claude etc.)</label>
2032
+ <span class="form-hint">On: your existing login just works (creds stay on the host, never in exports). Off: sealed sandbox, log in inside the container.</span>
2033
+ </div>
2034
+ <div class="form-row">
2035
+ <label class="checkbox-row"><input type="checkbox" id="dockerResumeOnStart" checked> Resume last conversation on relaunch</label>
2036
+ </div>
2037
+ </div>
2038
+ </details>
2039
+ <span class="form-hint" id="dockerLinkStatus" style="margin-top: 8px; display: block;"></span>
2040
+ </div>
1919
2041
  <!-- Manage Tab -->
1920
2042
  <div class="modal-tab-content hidden" id="case-manage">
1921
2043
  <div class="case-manage-list" id="caseManageList">
1922
2044
  <!-- Populated by JS -->
1923
2045
  </div>
1924
2046
  <span class="form-hint" style="margin-top: 8px; display: block;">Use arrows to reorder. Changes are saved automatically.</span>
2047
+ <div id="dockerExportsSection" style="margin-top: 16px; border-top: 1px solid var(--border, #333); padding-top: 12px;">
2048
+ <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;">
2049
+ <strong style="font-size: 13px;">Docker exports</strong>
2050
+ <button class="btn-toolbar" onclick="app.refreshDockerExports()">Refresh</button>
2051
+ </div>
2052
+ <div class="case-manage-list" id="dockerExportsList"><span class="form-hint">No exports yet. Export a docker case from its tab.</span></div>
2053
+ </div>
1925
2054
  </div>
1926
2055
  </div>
1927
2056
  <div class="form-actions">
@@ -2340,7 +2469,7 @@
2340
2469
  <!-- Lines drawn dynamically -->
2341
2470
  </svg>
2342
2471
 
2343
- <script defer src="constants.5b18ed64.js"></script>
2472
+ <script defer src="constants.503109fa.js"></script>
2344
2473
  <script defer src="mobile-handlers.b5a667ef.js"></script>
2345
2474
  <script defer src="voice-input.085e9e73.js"></script>
2346
2475
  <script defer src="notification-manager.ca9dc40d.js"></script>
@@ -2348,16 +2477,16 @@
2348
2477
  <script defer src="input-cjk.63794d0b.js"></script>
2349
2478
  <!-- Hardened markdown HTML sanitizer (wires DOMPurify). Must precede app.js. -->
2350
2479
  <script defer src="sanitize-html.bc7078d6.js"></script>
2351
- <script defer src="app.6890f3d0.js"></script>
2480
+ <script defer src="app.07e61fcf.js"></script>
2352
2481
  <script defer src="terminal-ui.efda6083.js"></script>
2353
2482
  <script defer src="respawn-ui.2d249da9.js"></script>
2354
2483
  <script defer src="ralph-panel.6de2d0f8.js"></script>
2355
2484
  <script defer src="orchestrator-panel.js"></script>
2356
2485
  <script defer src="cron-ui.js"></script>
2357
- <script defer src="settings-ui.9538a0c1.js"></script>
2486
+ <script defer src="settings-ui.4acbe789.js"></script>
2358
2487
  <script defer src="panels-ui.ca03853f.js"></script>
2359
2488
  <script defer src="ultracode-panel.js"></script>
2360
- <script defer src="session-ui.c9094fe9.js"></script>
2489
+ <script defer src="session-ui.9da1b71f.js"></script>
2361
2490
  <script defer src="ralph-wizard.13a1831e.js"></script>
2362
2491
  <script defer src="api-client.c9b1cddc.js"></script>
2363
2492
  <script defer src="subagent-windows.69b60c35.js"></script>
Binary file
Binary file
@@ -0,0 +1,58 @@
1
+ "use strict";Object.assign(CodemanApp.prototype,{buildEnvOverrides(e,t){const s={};return(e?.agentTeams||t?.agentTeamsEnabled)&&(s.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS="1"),s},getEffortSetting(e){const t=e?.thinkingEffort;return["low","medium","high","xhigh","max","ultracode"].includes(t)?t:void 0},formatCasePickerLabel(e){return e?.location==="remote"&&e.remote?.hostId?`${e.name} @ ${e.remote.hostId}`:e?.location==="docker"&&e.docker?.container?`${e.name} @ ${e.docker.container}`:e?.name||""},buildCasePickerOptions(e=[]){const t=[],s=new Set;for(const a of e)!a?.name||s.has(a.name)||(s.add(a.name),t.push(a));return s.has("testcase")||t.push({name:"testcase"}),t.map(a=>{const n=this.formatCasePickerLabel(a),i=[a.name,n,a.path,a.location,a.remote?.hostId,a.remote?.label,a.remote?.path,a.docker?.container,a.docker?.image,a.docker?.path].filter(Boolean).join(" ").toLowerCase();return{name:a.name,label:n,case:a,searchText:i}}).sort((a,n)=>a.label.localeCompare(n.label,void 0,{sensitivity:"base",numeric:!0}))},filterCasePickerOptions(e,t){const s=String(t||"").trim().toLowerCase().split(/\s+/).filter(Boolean);return s.length===0?e:e.filter(a=>s.every(n=>a.searchText.includes(n)))},getCasePickerOptions(){return this.buildCasePickerOptions(this.cases||[])},updateCasePickerInput(e){const t=document.getElementById("quickStartCaseSearch");if(!t)return;const s=this.getCasePickerOptions().find(a=>a.name===e);t.value=s?.label||e||"testcase",t.title=s?.label||t.value},renderQuickStartCaseSelectOptions(e,t){e&&(e.innerHTML=t.map(s=>`<option value="${escapeHtml(s.name)}">${escapeHtml(s.label)}</option>`).join(""))},openCasePicker(e=""){const t=document.getElementById("quickStartCaseSearch"),s=document.getElementById("quickStartCaseList");!t||!s||(this._casePickerOpen=!0,this._casePickerFilter=e,this._casePickerActiveIndex=0,t.setAttribute("aria-expanded","true"),this.renderCasePickerList())},closeCasePicker(){const e=document.getElementById("quickStartCaseSearch"),t=document.getElementById("quickStartCaseList");this._casePickerOpen=!1,this._casePickerFilter="",e?.setAttribute("aria-expanded","false"),e?.removeAttribute("aria-activedescendant"),t?.classList.add("hidden")},renderCasePickerList(){const e=document.getElementById("quickStartCaseSearch"),t=document.getElementById("quickStartCaseList"),s=document.getElementById("quickStartCase");if(!e||!t||!s)return;const a=this.filterCasePickerOptions(this.getCasePickerOptions(),this._casePickerFilter||""),n=s.value||"testcase",i=Math.max(0,a.length-1);if(this._casePickerActiveIndex=Math.min(Math.max(this._casePickerActiveIndex||0,0),i),a.length===0){t.innerHTML='<div class="case-combobox-empty">No cases match</div>',t.classList.remove("hidden"),e.removeAttribute("aria-activedescendant");return}t.innerHTML=a.map((o,r)=>{const c=r===this._casePickerActiveIndex,m=o.name===n;return`
2
+ <button
3
+ type="button"
4
+ id="${`quickStartCaseOption-${r}`}"
5
+ class="case-combobox-option ${c?"active":""} ${m?"selected":""}"
6
+ role="option"
7
+ aria-selected="${m?"true":"false"}"
8
+ data-case="${escapeHtml(o.name)}"
9
+ title="${escapeHtml(o.label)}">
10
+ <span class="case-combobox-check">${m?"\u2713":""}</span>
11
+ <span class="case-combobox-option-label">${escapeHtml(o.label)}</span>
12
+ </button>
13
+ `}).join(""),t.classList.remove("hidden"),e.setAttribute("aria-activedescendant",`quickStartCaseOption-${this._casePickerActiveIndex}`)},selectQuickStartCase(e,{save:t=!0}={}){const s=document.getElementById("quickStartCase");s&&(s.value=e||"testcase",this.updateCasePickerInput(s.value),this.closeCasePicker(),this.updateDirDisplayForCase(s.value),this.updateMobileCaseLabel(s.value),t&&this.saveLastUsedCase(s.value))},setupQuickStartCasePicker(){const e=document.getElementById("quickStartCase"),t=document.getElementById("quickStartCaseSearch"),s=document.getElementById("quickStartCaseList"),a=document.getElementById("quickStartCasePicker");!e||!t||!s||!a||t.dataset.listenerAdded||(t.addEventListener("focus",()=>{t.select?.(),this.openCasePicker("")}),t.addEventListener("click",()=>{t.select?.(),this.openCasePicker("")}),t.addEventListener("input",()=>{this.openCasePicker(t.value)}),t.addEventListener("keydown",n=>{const i=this.filterCasePickerOptions(this.getCasePickerOptions(),this._casePickerFilter||t.value);if(n.key==="ArrowDown")n.preventDefault(),this._casePickerActiveIndex=Math.min((this._casePickerActiveIndex||0)+1,Math.max(0,i.length-1)),this._casePickerOpen?this.renderCasePickerList():this.openCasePicker(t.value);else if(n.key==="ArrowUp")n.preventDefault(),this._casePickerActiveIndex=Math.max((this._casePickerActiveIndex||0)-1,0),this._casePickerOpen?this.renderCasePickerList():this.openCasePicker(t.value);else if(n.key==="Enter"){const o=i[this._casePickerActiveIndex||0];o&&(n.preventDefault(),this.selectQuickStartCase(o.name),this.run?.())}else n.key==="Escape"?(n.preventDefault(),this.updateCasePickerInput(e.value),this.closeCasePicker()):n.key==="Tab"&&(this.updateCasePickerInput(e.value),this.closeCasePicker())}),s.addEventListener("mousedown",n=>n.preventDefault()),s.addEventListener("click",n=>{const i=n.target.closest?.(".case-combobox-option");i?.dataset?.case&&this.selectQuickStartCase(i.dataset.case)}),document.addEventListener&&!this._casePickerDocumentListenerAdded&&(document.addEventListener("pointerdown",n=>{a.contains(n.target)||(this.updateCasePickerInput(e.value),this.closeCasePicker())}),this._casePickerDocumentListenerAdded=!0),t.dataset.listenerAdded="true")},async loadQuickStartCases(e=null,t=null){try{let s=null;try{const r=t?await t:await fetch("/api/settings").then(c=>c.ok?c.json():null).then(c=>c?.data??null);r&&(s=r.lastUsedCase||null)}catch{}const n=(await(await fetch("/api/cases")).json()).data;this.cases=n,console.log("[loadQuickStartCases] Loaded cases:",n.map(r=>r.name),"lastUsedCase:",s);const i=document.getElementById("quickStartCase"),o=this.getCasePickerOptions();if(this.renderQuickStartCaseSelectOptions(i,o),console.log("[loadQuickStartCases] Set options:",i.innerHTML.substring(0,200)),e)i.value=e,this.updateDirDisplayForCase(e),this.updateMobileCaseLabel(e);else if(s&&n.some(r=>r.name===s))i.value=s,this.updateDirDisplayForCase(s),this.updateMobileCaseLabel(s);else if(n.length>0){const r=n.find(c=>c.name==="testcase")||n[0];i.value=r.name,this.updateDirDisplayForCase(r.name),this.updateMobileCaseLabel(r.name)}else i.value="testcase",document.getElementById("dirDisplay").textContent="~/codeman-cases/testcase",this.updateMobileCaseLabel("testcase");this.updateCasePickerInput(i.value),this.renderCasePickerList(),this.closeCasePicker(),i.dataset.listenerAdded||(i.addEventListener("change",()=>{this.updateDirDisplayForCase(i.value),this.saveLastUsedCase(i.value),this.updateMobileCaseLabel(i.value),this.updateCasePickerInput(i.value)}),i.dataset.listenerAdded="true"),this.setupQuickStartCasePicker()}catch(s){console.error("Failed to load cases:",s)}},async updateDirDisplayForCase(e){try{const s=(await(await fetch(`/api/cases/${e}`)).json()).data;s.path&&(document.getElementById("dirDisplay").textContent=s.path,document.getElementById("dirInput").value=s.path)}catch{document.getElementById("dirDisplay").textContent=e}},async saveLastUsedCase(e){try{await fetch("/api/settings",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({lastUsedCase:e})})}catch(t){console.error("Failed to save last used case:",t)}},async quickStart(){return this.run()},async run(){const e=this._runMode||"claude";return e==="opencode"?this.runOpenCode():e==="codex"?this.runCodex():e==="gemini"?this.runGemini():this.runClaude()},setRunMode(e){this._runMode=e;try{localStorage.setItem("codeman_runMode",e)}catch{}this._applyRunMode(),this._apiPut("/api/settings",{runMode:e}).catch(()=>{}),document.getElementById("runModeMenu")?.classList.remove("active")},toggleRunModeMenu(e){e?.stopPropagation();const t=document.getElementById("runModeMenu");if(t&&(t.classList.toggle("active"),t.querySelectorAll(".run-mode-option").forEach(s=>{s.classList.toggle("selected",s.dataset.mode===this.runMode)}),t.classList.contains("active"))){this._loadRunModeHistory();const s=a=>{t.contains(a.target)||(t.classList.remove("active"),document.removeEventListener("click",s))};setTimeout(()=>document.addEventListener("click",s),0)}},async _loadRunModeHistory(){const e=document.getElementById("runModeHistory");if(e){e.innerHTML='<div class="run-mode-hist-empty">Loading...</div>';try{const t=await this._fetchHistorySessions(10);if(t.length===0){e.innerHTML='<div class="run-mode-hist-empty">No history</div>';return}e.replaceChildren();for(const s of t){const a=new Date(s.lastModified),n=a.toLocaleDateString("en",{month:"short",day:"numeric"})+" "+a.toLocaleTimeString("en",{hour:"2-digit",minute:"2-digit",hour12:!1}),i=s.workingDir.replace(/^\/home\/[^/]+\//,"~/"),o=document.createElement("button");o.className="run-mode-option",o.title=s.workingDir,o.dataset.sessionId=s.sessionId,o.dataset.workingDir=s.workingDir;const r=document.createElement("span");r.className="hist-dir",r.textContent=i;const c=document.createElement("span");c.className="hist-meta",c.textContent=n,o.append(r,c),o.addEventListener("click",m=>{m.stopPropagation(),this.resumeHistorySession(s.sessionId,s.workingDir)}),e.appendChild(o)}}catch{e.innerHTML='<div class="run-mode-hist-empty">Failed to load</div>'}}},_applyRunMode(){const e=this.runMode,t=document.getElementById("runBtn"),s=t?.nextElementSibling,a=document.getElementById("runBtnLabel");t&&(t.className=`btn-toolbar btn-run mode-${e}`),s&&(s.className=`btn-toolbar btn-run-gear mode-${e}`),a&&(a.textContent=e==="opencode"?"Run OC":e==="codex"?"Run CX":e==="gemini"?"Run GM":"Run")},_initRunMode(){try{this._runMode=localStorage.getItem("codeman_runMode")||"claude"}catch{this._runMode="claude"}this._applyRunMode()},incrementTabCount(){const e=document.getElementById("tabCount"),t=parseInt(e.value)||1;e.value=Math.min(20,t+1)},decrementTabCount(){const e=document.getElementById("tabCount"),t=parseInt(e.value)||1;e.value=Math.max(1,t-1)},incrementShellCount(){const e=document.getElementById("shellCount"),t=parseInt(e.value)||1;e.value=Math.min(20,t+1)},decrementShellCount(){const e=document.getElementById("shellCount"),t=parseInt(e.value)||1;e.value=Math.max(1,t-1)},async runClaude(){const e=document.getElementById("quickStartCase").value||"testcase",t=Math.min(20,Math.max(1,parseInt(document.getElementById("tabCount").value)||1));this.terminal.clear(),this.terminal.writeln(`\x1B[1;32m Starting ${t} Claude session(s) in ${e}...\x1B[0m`),this.terminal.writeln(""),this.terminal.focus();try{let a=(await(await fetch(`/api/cases/${e}`)).json())?.data??{};if(!a.path){const C=await(await fetch("/api/cases",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,description:""})})).json();if(!C.success)throw new Error(C.error||"Failed to create case");a=C.data.case}const n=a.path;if(!n)throw new Error("Case path not found");if(a.location==="remote"||a.location==="docker"){const g=[];for(let C=0;C<t;C++){const w=await(await fetch("/api/quick-start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({caseName:e,mode:"claude"})})).json();if(!w.success)throw new Error(w.error||"Failed to start remote Claude session");g.push(w.data.sessionId)}this.terminal.writeln(`\x1B[90m All ${t} remote session(s) ready\x1B[0m`),g[0]&&(await this.selectSession(g[0]),this.loadQuickStartCases()),this.terminal.focus();return}let i=null,o=1;for(const[,g]of this.sessions){const C=g.name&&g.name.match(/^w(\d+)-([a-zA-Z0-9_-]+)/);if(C&&C[2]===e){const b=parseInt(C[1]);b>=o&&(o=b+1)}}const r=this.isRalphTrackerEnabledByDefault(),c=[];for(let g=0;g<t;g++)c.push(`w${o+g}-${e}`);const m=this.getCaseSettings(e),d=this.loadAppSettingsFromStorage(),h=this.buildEnvOverrides(m,d),y=Object.keys(h).length>0,l=this.getEffortSetting(d),u=m.opusContext1m||d.opusContext1mEnabled,f=d.claudeModel||(u?"opus[1m]":"");this.terminal.writeln(`\x1B[90m Creating ${t} session(s)...\x1B[0m`);const p=c.map(g=>fetch("/api/sessions",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({workingDir:n,name:g,...y?{envOverrides:h}:{},...l?{effort:l}:{},...f!==void 0?{modelOverride:f}:{},statusLineTelemetry:d.showPlanUsageLimits===!0})}).then(C=>C.json())),E=await Promise.all(p),k=[];for(const g of E){if(!g.success)throw new Error(g.error);k.push(g.data.session.id)}i=k[0],await Promise.all(k.map(g=>fetch(`/api/sessions/${g}/ralph-config`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:r,disableAutoEnable:!r})}))),this.terminal.writeln(`\x1B[90m Starting ${t} session(s) in parallel...\x1B[0m`),await Promise.all(k.map(g=>fetch(`/api/sessions/${g}/interactive`,{method:"POST"}))),this.terminal.writeln(`\x1B[90m All ${t} sessions ready\x1B[0m`),i&&(await this.selectSession(i),this.loadQuickStartCases()),this.terminal.focus()}catch(s){this.terminal.writeln(`\x1B[1;31m Error: ${s.message}\x1B[0m`)}},stopClaude(){if(!this.activeSessionId)return;const e=document.querySelector(".btn-toolbar.btn-stop");e&&(this._stopConfirmTimer?(clearTimeout(this._stopConfirmTimer),this._stopConfirmTimer=null,e.innerHTML=e.dataset.origHtml,delete e.dataset.origHtml,e.classList.remove("confirming"),fetch(`/api/sessions/${this.activeSessionId}/input`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input:""})})):(e.dataset.origHtml=e.innerHTML,e.textContent="Tap again",e.classList.add("confirming"),this._stopConfirmTimer=setTimeout(()=>{this._stopConfirmTimer=null,e.dataset.origHtml&&(e.innerHTML=e.dataset.origHtml,delete e.dataset.origHtml),e.classList.remove("confirming")},2e3)))},async runShell(){const e=document.getElementById("quickStartCase").value||"testcase",t=Math.min(20,Math.max(1,parseInt(document.getElementById("shellCount").value)||1));this.terminal.clear(),this.terminal.writeln(`\x1B[1;33m Starting ${t} Shell session(s) in ${e}...\x1B[0m`),this.terminal.writeln("");try{let a=(await(await fetch(`/api/cases/${e}`)).json())?.data??{};if(!a.path){const u=await(await fetch("/api/cases",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,description:""})})).json();if(!u.success)throw new Error(u.error||"Failed to create case");a=u.data.case}const n=(this.cases||[]).find(l=>l.name===e),i=a.location==="remote"||a.location==="docker"||n?.location==="remote"||n?.location==="docker",o=a.path;if(!o)throw new Error("Case path not found");if(a.location==="remote"||a.location==="docker"){const l=[];for(let u=0;u<t;u++){const p=await(await fetch("/api/quick-start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({caseName:e,mode:"shell"})})).json();if(!p.success)throw new Error(p.error||"Failed to start remote shell session");l.push(p.data.sessionId)}l[0]&&await this.selectSession(l[0]),this.terminal.focus();return}let r=1;for(const[,l]of this.sessions){const u=l.name&&l.name.match(/^s(\d+)-([a-zA-Z0-9_-]+)/);if(u&&u[2]===e){const f=parseInt(u[1]);f>=r&&(r=f+1)}}const c=[];for(let l=0;l<t;l++)c.push(`s${r+l}-${e}`);const m=c.map(l=>fetch("/api/sessions",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({...i?{caseName:e}:{workingDir:o},mode:"shell",name:l})}).then(u=>u.json())),d=await Promise.all(m),h=[];for(const l of d){if(!l.success)throw new Error(l.error);h.push(l.data.session.id)}await Promise.all(h.map(l=>fetch(`/api/sessions/${l}/shell`,{method:"POST"})));const y=this.getTerminalDimensions();y&&await Promise.all(h.map(l=>fetch(`/api/sessions/${l}/resize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(y)}))),h.length>0&&await this.selectSession(h[0]),this.terminal.focus()}catch(s){this.terminal.writeln(`\x1B[1;31m Error: ${s.message}\x1B[0m`)}},async runOpenCode(){const e=document.getElementById("quickStartCase").value||"testcase",t=(this.cases||[]).find(a=>a.name===e)?.location,s=t==="remote"||t==="docker";this.terminal.clear(),this.terminal.writeln(`\x1B[1;32m Starting OpenCode session in ${e}...\x1B[0m`),this.terminal.writeln(""),this.terminal.focus();try{if(!s&&!(await(await fetch("/api/opencode/status")).json()).data.available){this.terminal.writeln("\x1B[1;31m OpenCode CLI not found.\x1B[0m"),this.terminal.writeln("\x1B[90m Install with: curl -fsSL https://opencode.ai/install | bash\x1B[0m");return}const a=this.buildEnvOverrides(this.getCaseSettings(e),this.loadAppSettingsFromStorage()),i=await(await fetch("/api/quick-start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({caseName:e,mode:"opencode",...s?{}:{openCodeConfig:{autoAllowTools:!0},...Object.keys(a).length>0?{envOverrides:a}:{}}})})).json();if(!i.success)throw new Error(i.error||"Failed to start OpenCode");i.data.sessionId&&await this.selectSession(i.data.sessionId),this.terminal.focus()}catch(a){this.terminal.writeln(`\x1B[1;31m Error: ${a.message}\x1B[0m`)}},async runCodex(){const e=document.getElementById("quickStartCase").value||"testcase",t=(this.cases||[]).find(a=>a.name===e)?.location,s=t==="remote"||t==="docker";this.terminal.clear(),this.terminal.writeln(`\x1B[1;32m Starting Codex session in ${e}...\x1B[0m`),this.terminal.writeln(""),this.terminal.focus();try{if(!s&&!(await(await fetch("/api/codex/status")).json()).data.available){this.terminal.writeln("\x1B[1;31m Codex CLI not found.\x1B[0m"),this.terminal.writeln("\x1B[90m Install with: npm install -g @openai/codex\x1B[0m");return}const a=this.loadAppSettingsFromStorage(),n=this.buildEnvOverrides(this.getCaseSettings(e),a),o=await(await fetch("/api/quick-start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({caseName:e,mode:"codex",...s?{}:{codexConfig:{dangerouslyBypassApprovals:a.codexDangerouslyBypassApprovals??!1,renderMode:"hybrid"},...Object.keys(n).length>0?{envOverrides:n}:{}}})})).json();if(!o.success)throw new Error(o.error||"Failed to start Codex");o.data.sessionId&&await this.selectSession(o.data.sessionId),this.terminal.focus()}catch(a){this.terminal.writeln(`\x1B[1;31m Error: ${a.message}\x1B[0m`)}},async runGemini(){const e=document.getElementById("quickStartCase").value||"testcase",t=(this.cases||[]).find(a=>a.name===e)?.location,s=t==="remote"||t==="docker";this.terminal.clear(),this.terminal.writeln(`\x1B[1;32m Starting Gemini session in ${e}...\x1B[0m`),this.terminal.writeln(""),this.terminal.focus();try{if(!s&&!(await(await fetch("/api/gemini/status")).json()).data.available){this.terminal.writeln("\x1B[1;31m Gemini CLI not found.\x1B[0m"),this.terminal.writeln("\x1B[90m Install with: npm install -g @google/gemini-cli\x1B[0m");return}const a=this.buildEnvOverrides(this.getCaseSettings(e),this.loadAppSettingsFromStorage()),i=await(await fetch("/api/quick-start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({caseName:e,mode:"gemini",...s?{}:{geminiConfig:{approvalMode:"yolo"},...Object.keys(a).length>0?{envOverrides:a}:{}}})})).json();if(!i.success)throw new Error(i.error||"Failed to start Gemini");i.data.sessionId&&await this.selectSession(i.data.sessionId),this.terminal.focus()}catch(a){this.terminal.writeln(`\x1B[1;31m Error: ${a.message}\x1B[0m`)}},openSessionOptions(e){const t=this.sessions.get(e);if(!t)return;this.editingSessionId=e;const s=t.mode==="opencode"||t.mode==="codex"||t.mode==="gemini";this.switchOptionsTab(s?"summary":"respawn");const a=document.getElementById("sessionRespawnStatus"),n=document.getElementById("modalEnableRespawnBtn"),i=document.getElementById("modalStopRespawnBtn");this.respawnStatus[e]?(a.classList.add("active"),a.querySelector(".respawn-status-text").textContent=this.respawnStatus[e].state||"Active",n.style.display="none",i.style.display=""):(a.classList.remove("active"),a.querySelector(".respawn-status-text").textContent="Not active",n.style.display="",i.style.display="none");const o=document.getElementById("sessionRespawnSection");t.mode==="claude"&&t.pid?o.style.display="":o.style.display="none";const r=t.mode==="opencode"||t.mode==="codex"||t.mode==="gemini";document.querySelectorAll("[data-claude-only]").forEach(p=>{p.style.display=r?"none":""}),this.selectDurationPreset(""),this.loadSavedRespawnConfig(e),document.getElementById("modalAutoCompactEnabled").checked=t.autoCompactEnabled??!1,document.getElementById("modalAutoCompactThreshold").value=t.autoCompactThreshold??11e4,document.getElementById("modalAutoCompactPrompt").value=t.autoCompactPrompt??"",document.getElementById("modalAutoClearEnabled").checked=t.autoClearEnabled??!1,document.getElementById("modalAutoClearThreshold").value=t.autoClearThreshold??14e4,document.getElementById("modalAutoResumeEnabled").checked=t.autoResumeEnabled??!1,this.updateAutoResumeStatus(e),document.getElementById("modalImageWatcherEnabled").checked=t.imageWatcherEnabled??!0,document.getElementById("modalFlickerFilterEnabled").checked=t.flickerFilterEnabled??!1;const m=parseSessionPrefix(t.name),d=document.getElementById("modalSessionPrefix");m?(d.textContent=m.prefix+": ",d.style.display="",document.getElementById("modalSessionName").value=m.suffix,document.getElementById("modalSessionName").placeholder="Add description..."):(d.style.display="none",d.textContent="",document.getElementById("modalSessionName").value=t.name||"",document.getElementById("modalSessionName").placeholder="Auto (directory name)");const h=t.color||"default";document.getElementById("sessionColorPicker")?.querySelectorAll(".color-swatch").forEach(p=>{p.classList.toggle("selected",p.dataset.color===h)}),this.renderPresetDropdown(),document.getElementById("respawnPresetSelect").value="",document.getElementById("presetDescriptionHint").textContent="";const l=document.querySelector('#sessionOptionsModal .modal-tab-btn[data-tab="ralph"]'),u=document.querySelector('#sessionOptionsModal .modal-tab-btn[data-tab="respawn"]');if(r?(l&&(l.style.display="none"),u&&(u.style.display="none"),this.switchOptionsTab("context")):(l&&(l.style.display=""),u&&(u.style.display="")),!r){const p=this.ralphStates.get(e);this.populateRalphForm({enabled:p?.loop?.enabled??t.ralphLoop?.enabled??!1,completionPhrase:p?.loop?.completionPhrase||t.ralphLoop?.completionPhrase||"",maxIterations:p?.loop?.maxIterations||t.ralphLoop?.maxIterations||0,maxTodos:p?.loop?.maxTodos||t.ralphLoop?.maxTodos,todoExpirationMinutes:p?.loop?.todoExpirationMinutes||t.ralphLoop?.todoExpirationMinutes})}const f=document.getElementById("sessionOptionsModal");f.classList.add("active"),this.activeFocusTrap=new FocusTrap(f),this.activeFocusTrap.activate()},async saveSessionName(){if(!this.editingSessionId)return;const e=this.sessions.get(this.editingSessionId),t=e?parseSessionPrefix(e.name):null,s=document.getElementById("modalSessionName").value.trim();let a;t?a=t.prefix+(s?": "+s:""):a=s;try{await this._apiPut(`/api/sessions/${this.editingSessionId}/name`,{name:a})}catch(n){this.showToast("Failed to save session name: "+n.message,"error")}},async autoSaveAutoCompact(){if(this.editingSessionId)try{await this._apiPost(`/api/sessions/${this.editingSessionId}/auto-compact`,{enabled:document.getElementById("modalAutoCompactEnabled").checked,threshold:parseInt(document.getElementById("modalAutoCompactThreshold").value)||11e4,prompt:document.getElementById("modalAutoCompactPrompt").value.trim()||void 0})}catch{}},async autoSaveAutoClear(){if(this.editingSessionId)try{await this._apiPost(`/api/sessions/${this.editingSessionId}/auto-clear`,{enabled:document.getElementById("modalAutoClearEnabled").checked,threshold:parseInt(document.getElementById("modalAutoClearThreshold").value)||14e4})}catch{}},async autoSaveAutoResume(){if(!this.editingSessionId)return;const e=document.getElementById("modalAutoResumeEnabled").checked;try{await this._apiPost(`/api/sessions/${this.editingSessionId}/auto-resume`,{enabled:e});const t=this.sessions.get(this.editingSessionId);t&&(t.autoResumeEnabled=e,e||(t.autoResumeAt=void 0)),this.updateAutoResumeStatus(this.editingSessionId),this.showToast(`Auto-resume on usage limit ${e?"enabled":"disabled"}`,"success")}catch(t){this.showToast("Failed to toggle auto-resume: "+t.message,"error")}},updateAutoResumeStatus(e){const t=document.getElementById("autoResumeStatus");if(!t||this.editingSessionId!==e)return;const s=this.sessions.get(e);if(s?.autoResumeAt&&s.autoResumeAt>Date.now()){const a=new Date(s.autoResumeAt).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});t.textContent=`Usage limit pause active \u2014 resumes at ${a}`,t.classList.add("active")}else t.textContent="",t.classList.remove("active")},async toggleSessionImageWatcher(){if(!this.editingSessionId)return;const e=document.getElementById("modalImageWatcherEnabled").checked;try{await this._apiPost(`/api/sessions/${this.editingSessionId}/image-watcher`,{enabled:e});const t=this.sessions.get(this.editingSessionId);t&&(t.imageWatcherEnabled=e),this.showToast(`Image watcher ${e?"enabled":"disabled"}`,"success")}catch{this.showToast("Failed to toggle image watcher","error")}},async toggleFlickerFilter(){if(!this.editingSessionId)return;const e=document.getElementById("modalFlickerFilterEnabled").checked;try{await this._apiPost(`/api/sessions/${this.editingSessionId}/flicker-filter`,{enabled:e});const t=this.sessions.get(this.editingSessionId);t&&(t.flickerFilterEnabled=e),this.showToast(`Flicker filter ${e?"enabled":"disabled"}`,"success")}catch{this.showToast("Failed to toggle flicker filter","error")}},async autoSaveRespawnConfig(){if(!this.editingSessionId)return;const e={updatePrompt:document.getElementById("modalRespawnPrompt").value,sendClear:document.getElementById("modalRespawnSendClear").checked,sendInit:document.getElementById("modalRespawnSendInit").checked,kickstartPrompt:document.getElementById("modalRespawnKickstart").value.trim()||void 0,autoAcceptPrompts:document.getElementById("modalRespawnAutoAccept").checked};try{await this._apiPut(`/api/sessions/${this.editingSessionId}/respawn/config`,e)}catch{}},async loadSavedRespawnConfig(e){try{const s=await(await fetch(`/api/sessions/${e}/respawn/config`)).json();if(s.success&&s.data&&s.data.config){const a=s.data.config;document.getElementById("modalRespawnPrompt").value=a.updatePrompt||"update all the docs and CLAUDE.md",document.getElementById("modalRespawnSendClear").checked=a.sendClear??!0,document.getElementById("modalRespawnSendInit").checked=a.sendInit??!0,document.getElementById("modalRespawnKickstart").value=a.kickstartPrompt||"",document.getElementById("modalRespawnAutoAccept").checked=a.autoAcceptPrompts??!0,a.durationMinutes&&(document.querySelector(`.duration-preset-btn[data-minutes="${a.durationMinutes}"]`)?this.selectDurationPreset(String(a.durationMinutes)):(this.selectDurationPreset("custom"),document.getElementById("modalRespawnDuration").value=a.durationMinutes))}}catch{}},selectDurationPreset(e){document.querySelectorAll(".duration-preset-btn").forEach(n=>n.classList.remove("active"));const t=document.querySelector(`.duration-preset-btn[data-minutes="${e}"]`);t&&t.classList.add("active");const s=document.querySelector(".duration-custom-input"),a=document.getElementById("modalRespawnDuration");e==="custom"?(s.classList.add("visible"),a.focus()):(s.classList.remove("visible"),a.value="")},getSelectedDuration(){const e=document.querySelector(".duration-custom-input"),t=document.getElementById("modalRespawnDuration");if(e.classList.contains("visible"))return t.value?parseInt(t.value):null;{const a=document.querySelector(".duration-preset-btn.active")?.dataset.minutes;return a?parseInt(a):null}},switchOptionsTab(e){document.querySelectorAll("#sessionOptionsModal .modal-tab-btn").forEach(t=>{t.classList.toggle("active",t.dataset.tab===e)}),document.getElementById("respawn-tab").classList.toggle("hidden",e!=="respawn"),document.getElementById("context-tab").classList.toggle("hidden",e!=="context"),document.getElementById("ralph-tab").classList.toggle("hidden",e!=="ralph"),document.getElementById("summary-tab").classList.toggle("hidden",e!=="summary"),e==="summary"&&this.editingSessionId&&this.loadRunSummary(this.editingSessionId)},getRalphConfig(){return{enabled:document.getElementById("modalRalphEnabled").checked,completionPhrase:document.getElementById("modalRalphPhrase").value.trim(),maxIterations:parseInt(document.getElementById("modalRalphMaxIterations").value)||0,maxTodos:parseInt(document.getElementById("modalRalphMaxTodos").value)||50,todoExpirationMinutes:parseInt(document.getElementById("modalRalphTodoExpiration").value)||60}},populateRalphForm(e){document.getElementById("modalRalphEnabled").checked=e?.enabled??!1,document.getElementById("modalRalphPhrase").value=e?.completionPhrase||"",document.getElementById("modalRalphMaxIterations").value=e?.maxIterations||0,document.getElementById("modalRalphMaxTodos").value=e?.maxTodos||50,document.getElementById("modalRalphTodoExpiration").value=e?.todoExpirationMinutes||60},async saveRalphConfig(){if(!this.editingSessionId){this.showToast("No session selected","warning");return}const e=this.getRalphConfig();e.enabled&&this.ralphClosedSessions.delete(this.editingSessionId);try{const s=await(await fetch(`/api/sessions/${this.editingSessionId}/ralph-config`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)})).json();if(s.error)throw new Error(s.error);this.showToast("Ralph config saved","success")}catch(t){this.showToast("Failed to save Ralph config: "+t.message,"error")}},startInlineRename(e){const t=this.sessions.get(e);if(!t)return;const s=document.querySelector(`.tab-name[data-session-id="${e}"]`);if(!s)return;this._inlineRenameActive=!0;const a=this.getSessionName(t),n=parseSessionPrefix(t.name),i=s.textContent;for(s.textContent="";s.firstChild;)s.removeChild(s.firstChild);if(n){const c=document.createElement("span");c.textContent=n.prefix+": ",c.style.cssText="color: var(--text-muted); font-size: 0.75rem; white-space: nowrap;",s.appendChild(c)}const o=document.createElement("input");o.type="text",o.value=n?n.suffix:t.name||"",o.placeholder=n?"Add description...":a,o.className="tab-rename-input",o.style.cssText="width: 80px; font-size: 0.75rem; padding: 2px 4px; background: var(--bg-input); border: 1px solid var(--accent); border-radius: 3px; color: var(--text); outline: none;",s.appendChild(o),o.focus(),o.select();const r=async({commit:c})=>{if(!this._inlineRenameActive)return;if(this._inlineRenameActive=!1,this._activeRename=null,!c){this.renderSessionTabs();return}const m=o.value.trim(),d=n?n.prefix+(m?": "+m:""):m;if(s.textContent=d||i,this.sessions.has(e)&&d!==t.name)try{await fetch(`/api/sessions/${e}/name`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:d})})}catch{s.textContent=i,this.showToast("Failed to rename","error")}this.renderSessionTabs()};this._activeRename={sessionId:e,cancel:()=>r({commit:!1})},o.addEventListener("blur",()=>r({commit:!0})),o.addEventListener("keydown",c=>{c.isComposing||c.keyCode===229||(c.key==="Enter"?(c.preventDefault(),o.blur()):c.key==="Escape"&&(o.value="",o.blur()))})},toggleCaseSettings(){const e=document.getElementById("caseSettingsPopover");if(e.classList.contains("hidden")){const t=document.getElementById("quickStartCase").value||"testcase",s=this.getCaseSettings(t);document.getElementById("caseAgentTeams").checked=s.agentTeams,document.getElementById("caseOpusContext1m").checked=s.opusContext1m,e.classList.remove("hidden");const a=n=>{!e.contains(n.target)&&!n.target.classList.contains("btn-case-settings")&&(e.classList.add("hidden"),document.removeEventListener("click",a))};setTimeout(()=>document.addEventListener("click",a),0)}else e.classList.add("hidden")},getCaseSettings(e){try{const t=localStorage.getItem("caseSettings_"+e);if(t)return JSON.parse(t)}catch{}return{agentTeams:!1,opusContext1m:!0}},saveCaseSettings(e,t){localStorage.setItem("caseSettings_"+e,JSON.stringify(t))},onCaseSettingChanged(){const e=document.getElementById("quickStartCase").value||"testcase",t=this.getCaseSettings(e);t.agentTeams=document.getElementById("caseAgentTeams").checked,t.opusContext1m=document.getElementById("caseOpusContext1m").checked,this.saveCaseSettings(e,t);const s=document.getElementById("caseAgentTeamsMobile");s&&(s.checked=t.agentTeams);const a=document.getElementById("caseOpusContext1mMobile");a&&(a.checked=t.opusContext1m)},toggleCaseSettingsMobile(){const e=document.getElementById("caseSettingsPopoverMobile");if(e.classList.contains("hidden")){const t=document.getElementById("quickStartCase").value||"testcase",s=this.getCaseSettings(t);document.getElementById("caseAgentTeamsMobile").checked=s.agentTeams,document.getElementById("caseOpusContext1mMobile").checked=s.opusContext1m,e.classList.remove("hidden");const a=n=>{!e.contains(n.target)&&!n.target.classList.contains("btn-case-settings-mobile")&&(e.classList.add("hidden"),document.removeEventListener("click",a))};setTimeout(()=>document.addEventListener("click",a),0)}else e.classList.add("hidden")},onCaseSettingChangedMobile(){const e=document.getElementById("quickStartCase").value||"testcase",t=this.getCaseSettings(e);t.agentTeams=document.getElementById("caseAgentTeamsMobile").checked,t.opusContext1m=document.getElementById("caseOpusContext1mMobile").checked,this.saveCaseSettings(e,t);const s=document.getElementById("caseAgentTeams");s&&(s.checked=t.agentTeams);const a=document.getElementById("caseOpusContext1m");a&&(a.checked=t.opusContext1m)},showCreateCaseModal(){document.getElementById("newCaseName").value="",document.getElementById("newCaseDescription").value="",document.getElementById("linkCaseName").value="",document.getElementById("linkCasePath").value="",["remoteCaseName","remoteCasePath","remoteHostId","remoteHostAddress","remoteHostUsername","remoteHostPort","remoteHostCodexCommand","remoteHostIdentityFile","remoteHostSocksProxy","remoteHostJumpHost","remoteHostExtraSshOptions"].forEach(s=>{const a=document.getElementById(s);a&&(a.value="")}),this.caseModalTab="case-create",this.switchCaseModalTab("case-create");const t=document.getElementById("createCaseModal");t.querySelectorAll(".modal-tabs .modal-tab-btn").forEach(s=>{s.onclick=()=>this.switchCaseModalTab(s.dataset.tab)}),t.querySelectorAll('input[type="text"]').forEach(s=>{s._mobileScrollWired||(s._mobileScrollWired=!0,s.addEventListener("focus",()=>{window.innerWidth<=430&&setTimeout(()=>s.scrollIntoView({behavior:"smooth",block:"center"}),300)}))}),t.classList.add("active"),document.getElementById("newCaseName").focus()},switchCaseModalTab(e){this.caseModalTab=e;const t=document.getElementById("createCaseModal");t.querySelectorAll(".modal-tabs .modal-tab-btn").forEach(a=>{a.classList.toggle("active",a.dataset.tab===e)}),t.querySelectorAll(".modal-tab-content").forEach(a=>{a.classList.toggle("hidden",a.id!==e)});const s=document.getElementById("caseModalSubmit");e==="case-manage"?(s.style.display="none",this.renderCaseManageList(),this.refreshDockerExports()):(s.style.display="",s.textContent=e==="case-create"?"Create":e==="case-remote"?"Link Remote":e==="case-docker"?"Link Docker":"Link"),e==="case-create"?document.getElementById("newCaseName").focus():e==="case-link"?document.getElementById("linkCaseName").focus():e==="case-remote"?document.getElementById("remoteCaseName").focus():e==="case-docker"&&document.getElementById("dockerCaseName").focus()},closeCreateCaseModal(){document.getElementById("createCaseModal").classList.remove("active")},async submitCaseModal(){const e=document.getElementById("caseModalSubmit"),t=e.textContent;e.classList.add("loading"),e.textContent=this.caseModalTab==="case-create"?"Creating...":"Linking...";try{this.caseModalTab==="case-create"?await this.createCase():this.caseModalTab==="case-remote"?await this.linkRemoteCase():this.caseModalTab==="case-docker"?await this.linkDockerCase():await this.linkCase()}finally{e.classList.remove("loading"),e.textContent=t}},async createCase(){const e=document.getElementById("newCaseName").value.trim(),t=document.getElementById("newCaseDescription").value.trim();if(!e){this.showToast("Please enter a case name","error");return}if(!/^[a-zA-Z0-9_-]+$/.test(e)){this.showToast("Invalid name. Use only letters, numbers, hyphens, underscores.","error");return}const s=document.getElementById("newCaseDocker")?.checked,a=s?"/api/cases/docker-quickcreate":"/api/cases",n=s?{name:e,description:t,...this._collectDockerQuickSettings()}:{name:e,description:t};try{const o=await(await fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json();if(o.success)if(this.closeCreateCaseModal(),await this.loadQuickStartCases(e),await this.saveLastUsedCase(e),s){const r=o.data?.capsEnforced===!1?" (resource caps advisory on this engine)":"";this.showToast(`Docker case "${e}" created${r} \u2014 starting session\u2026`,"success"),await this.runClaude()}else this.showToast(`Case "${e}" created`,"success");else this.showToast(o.error||"Failed to create case","error")}catch(i){console.error("Failed to create case:",i),this.showToast("Failed to create case: "+i.message,"error")}},toggleDockerQuickSettings(){const e=document.getElementById("newCaseDocker")?.checked,t=document.getElementById("dockerQuickSettings");t&&(t.style.display=e?"":"none")},applyDockerTemplate(){const e=document.getElementById("quickDockerTemplate")?.value,s={small:{m:"2g",c:"1",g:""},medium:{m:"",c:"",g:""},large:{m:"8g",c:"4",g:""},gpu:{m:"8g",c:"4",g:"all"}}[e];if(!s)return;const a=(n,i)=>{const o=document.getElementById(n);o&&(o.value=i)};a("quickDockerMemory",s.m),a("quickDockerCpus",s.c),a("quickDockerGpus",s.g)},_collectDockerQuickSettings(){const e=c=>(document.getElementById(c)?.value||"").trim(),t={},s=e("quickDockerMemory");s&&(t.memory=s);const a=e("quickDockerCpus");a&&(t.cpus=a);const n=e("quickDockerGpus");n&&n.toLowerCase()!=="none"&&(t.gpus=n);const i=document.getElementById("quickDockerNetwork")?.value;i&&i!=="bridge"&&(t.network=i);const o=e("quickDockerImage");o&&(t.image=o);const r=document.getElementById("quickDockerMountCreds");return r&&!r.checked&&(t.mountCredentials=!1),t},async linkCase(){const e=document.getElementById("linkCaseName").value.trim(),t=document.getElementById("linkCasePath").value.trim();if(!e){this.showToast("Please enter a case name","error");return}if(!/^[a-zA-Z0-9_-]+$/.test(e)){this.showToast("Invalid name. Use only letters, numbers, hyphens, underscores.","error");return}if(!t){this.showToast("Please enter a folder path","error");return}try{const a=await(await fetch("/api/cases/link",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,path:t})})).json();a.success?(this.closeCreateCaseModal(),this.showToast(`Case "${e}" linked to ${t}`,"success"),await this.loadQuickStartCases(e),await this.saveLastUsedCase(e)):this.showToast(a.error||"Failed to link case","error")}catch(s){console.error("Failed to link case:",s),this.showToast("Failed to link case: "+s.message,"error")}},async linkRemoteCase(){const e=document.getElementById("remoteCaseName").value.trim(),t=document.getElementById("remoteCasePath").value.trim(),s=document.getElementById("remoteHostId").value.trim(),a=document.getElementById("remoteHostAddress").value.trim(),n=document.getElementById("remoteHostUsername").value.trim(),i=document.getElementById("remoteHostCodexCommand").value.trim(),o=document.getElementById("remoteHostPort").value.trim(),r=document.getElementById("remoteHostIdentityFile").value.trim(),c=document.getElementById("remoteHostSocksProxy").value.trim(),m=document.getElementById("remoteHostJumpHost").value.trim(),d=document.getElementById("remoteHostExtraSshOptions").value.split(`
14
+ `).map(y=>y.trim()).filter(y=>y.length>0);if(!e||!t||!s||!a||!n){this.showToast("Please complete all required remote fields","error");return}if(!/^[a-zA-Z0-9_-]+$/.test(e)||!/^[a-zA-Z0-9_-]+$/.test(s)){this.showToast("Invalid name. Use only letters, numbers, hyphens, underscores.","error");return}if(!t.startsWith("/")){this.showToast("Remote path must be absolute","error");return}let h;if(o&&(h=Number(o),!Number.isInteger(h)||h<1||h>65535)){this.showToast("SSH port must be a number between 1 and 65535","error");return}try{const y={id:s,label:s,host:a,username:n,...h?{port:h}:{},...r?{identityFile:r}:{},...c?{socksProxy:c}:{},...m?{jumpHost:m}:{},...d.length?{extraSshOptions:d}:{},...i?{commands:{codex:i}}:{}},u=await(await fetch("/api/remote-hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(y)})).json();if(!u.success&&u.errorCode!=="ALREADY_EXISTS")throw new Error(u.error||"Failed to save remote host");const p=await(await fetch("/api/cases/remote-link",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,hostId:s,remotePath:t})})).json();p.success?(this.closeCreateCaseModal(),this.showToast(`Remote case "${e}" linked`,"success"),await this.loadQuickStartCases(e),await this.saveLastUsedCase(e)):this.showToast(p.error||"Failed to link remote case","error")}catch(y){console.error("Failed to link remote case:",y),this.showToast("Failed to link remote case: "+y.message,"error")}},async linkDockerCase(){const e=document.getElementById("dockerCaseName").value.trim(),t=document.getElementById("dockerWorkspacePath").value.trim(),s=document.getElementById("dockerHostId").value.trim()||"local",a=document.getElementById("dockerImage").value.trim()||"codeman/agent:base",n=document.getElementById("dockerNetwork").value,i=document.getElementById("dockerMemory").value.trim(),o=document.getElementById("dockerCpus").value.trim(),r=document.getElementById("dockerMountCredentials").checked,c=document.getElementById("dockerResumeOnStart").checked,m=document.getElementById("dockerLinkStatus");if(!e||!t){this.showToast("Please enter a case name and workspace path","error");return}if(!/^[a-zA-Z0-9_-]+$/.test(e)||!/^[a-zA-Z0-9_-]+$/.test(s)){this.showToast("Invalid name. Use only letters, numbers, hyphens, underscores.","error");return}if(!t.startsWith("/")){this.showToast("Workspace path must be absolute","error");return}try{m&&(m.textContent="Checking docker daemon + base image...");const d={};i&&(d.memory=i),o&&(d.cpus=o);const h={id:s,label:s,image:a,network:n,mountCredentials:r,resumeOnStart:c,...Object.keys(d).length?{resources:d}:{}};let y=await fetch("/api/docker-hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(h)}),l=await y.json();if(!l.success&&l.errorCode==="ALREADY_EXISTS"&&(y=await fetch(`/api/docker-hosts/${encodeURIComponent(s)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(h)}),l=await y.json()),!l.success)throw new Error(l.error||"Failed to save docker host");const f=await(await fetch("/api/cases/docker-link",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,hostId:s,hostWorkspacePath:t})})).json();if(f.success){this.closeCreateCaseModal();const p=f.data?.capsEnforced===!1?" (resource caps are advisory on this engine)":"";this.showToast(`Docker case "${e}" linked${p}`,"success"),await this.loadQuickStartCases(e),await this.saveLastUsedCase(e)}else m&&(m.textContent=f.error||"Failed to link docker case"),this.showToast(f.error||"Failed to link docker case","error")}catch(d){console.error("Failed to link docker case:",d),m&&(m.textContent=d.message),this.showToast("Failed to link docker case: "+d.message,"error")}},async refreshDockerExports(){const e=document.getElementById("dockerExportsList");if(e)try{const a=(await(await fetch("/api/docker-exports")).json())?.data?.exports||[];if(a.length===0){e.innerHTML='<span class="form-hint">No exports yet. Export a docker case from its tab.</span>';return}e.innerHTML=a.map(n=>{const i=(n.sizeBytes/1e6).toFixed(1),o=this.escapeHtml?this.escapeHtml(n.name):n.name;return`<div class="case-manage-item" style="display:flex; align-items:center; gap:8px; justify-content:space-between;">
15
+ <span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${o}">${o} <span class="form-hint">(${i} MB)</span></span>
16
+ <span style="flex-shrink:0;">
17
+ <a class="btn-toolbar" href="/api/docker-exports/${encodeURIComponent(n.name)}" download>Download</a>
18
+ <button class="btn-toolbar" onclick="app.importDockerBundle('${o.replace(/'/g,"\\'")}')">Import</button>
19
+ <button class="btn-toolbar" onclick="app.deleteDockerExport('${o.replace(/'/g,"\\'")}')">Delete</button>
20
+ </span>
21
+ </div>`}).join("")}catch(t){e.innerHTML=`<span class="form-hint">Failed to load exports: ${t.message}</span>`}},async exportDockerCaseBundle(e,t="full"){try{const a=await(await fetch(`/api/docker-cases/${encodeURIComponent(e)}/export`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({mode:t})})).json();a.success?this.showToast(`Exporting "${e}" (${t})... you'll be notified when the bundle is ready`,"info"):this.showToast(a.error||"Export failed","error")}catch(s){this.showToast("Export failed: "+s.message,"error")}},async importDockerBundle(e){const t=prompt("New case name for the imported bundle:",e.split("-")[0]+"-imported");if(!t)return;const s=prompt("Absolute host directory to restore the workspace into:","");if(s)try{const n=await(await fetch("/api/docker-cases/import",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({bundle:e,newCaseName:t,destWorkspacePath:s})})).json();n.success?(this.showToast(`Imported as "${t}"`,"success"),await this.loadQuickStartCases(t)):this.showToast(n.error||"Import failed","error")}catch(a){this.showToast("Import failed: "+a.message,"error")}},async deleteDockerExport(e){if(confirm(`Delete export bundle "${e}"?`))try{const s=await(await fetch(`/api/docker-exports/${encodeURIComponent(e)}`,{method:"DELETE"})).json();s.success?(this.showToast("Export deleted","success"),this.refreshDockerExports()):this.showToast(s.error||"Delete failed","error")}catch(t){this.showToast("Delete failed: "+t.message,"error")}},renderCaseManageList(){const e=document.getElementById("caseManageList"),t=this.cases||[];if(t.length===0){e.innerHTML='<div class="form-hint" style="text-align: center; padding: 2rem 0;">No cases yet</div>';return}let s="";t.forEach((a,n)=>{const i=n===0,o=n===t.length-1,r=a.path?a.path.replace(/^\/Users\/[^/]+/,"~"):"";s+=`
22
+ <div class="case-manage-item" data-case="${escapeHtml(a.name)}">
23
+ <div class="case-manage-info">
24
+ <span class="case-manage-name">${escapeHtml(a.name)}</span>
25
+ <span class="case-manage-path">${escapeHtml(r)}</span>
26
+ </div>
27
+ <div class="case-manage-actions">
28
+ ${a.location==="docker"?`<button class="case-manage-btn" onclick="app.exportDockerCaseBundle(${escapeHtml(JSON.stringify(a.name))}, 'full')"
29
+ title="Export container (full image + workspace) to move to another machine">&#x1F4E6;</button>`:""}
30
+ <button class="case-manage-btn" onclick="app.moveCaseUp(${escapeHtml(JSON.stringify(a.name))})"
31
+ title="Move up" ${i?"disabled":""}>&#x25B2;</button>
32
+ <button class="case-manage-btn" onclick="app.moveCaseDown(${escapeHtml(JSON.stringify(a.name))})"
33
+ title="Move down" ${o?"disabled":""}>&#x25BC;</button>
34
+ <button class="case-manage-btn case-manage-btn-delete" onclick="app.deleteCase(${escapeHtml(JSON.stringify(a.name))})"
35
+ title="Delete case">&#x2715;</button>
36
+ </div>
37
+ </div>
38
+ `}),e.innerHTML=s},async moveCaseUp(e){const t=this.cases||[],s=t.findIndex(n=>n.name===e);if(s<=0)return;const a=[...t];[a[s-1],a[s]]=[a[s],a[s-1]],this.cases=a,this.renderCaseManageList(),await this.saveCaseOrder(a.map(n=>n.name))},async moveCaseDown(e){const t=this.cases||[],s=t.findIndex(n=>n.name===e);if(s<0||s>=t.length-1)return;const a=[...t];[a[s],a[s+1]]=[a[s+1],a[s]],this.cases=a,this.renderCaseManageList(),await this.saveCaseOrder(a.map(n=>n.name))},async deleteCase(e){if(confirm(`Delete case "${e}"? Linked cases will only be unlinked (folder preserved). Created cases will be permanently deleted.`))try{const s=await(await fetch(`/api/cases/${encodeURIComponent(e)}`,{method:"DELETE"})).json();if(s.success){this.showToast(`Case "${e}" ${s.data?.type==="unlinked"?"unlinked":"deleted"}`,"success"),this.cases=(this.cases||[]).filter(i=>i.name!==e),this.renderCaseManageList();const a=document.getElementById("quickStartCase"),n=a.value;n===e&&a.blur?.(),await this.loadQuickStartCases(n===e?null:n),n===e&&await this.saveLastUsedCase(document.getElementById("quickStartCase")?.value||"testcase")}else this.showToast(s.error||"Failed to delete case","error")}catch(t){this.showToast("Failed to delete case: "+t.message,"error")}},async saveCaseOrder(e){try{await fetch("/api/cases/order",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({order:e})});const s=document.getElementById("quickStartCase").value;await this.loadQuickStartCases(s)}catch(t){this.showToast("Failed to save case order: "+t.message,"error")}},showMobileCasePicker(){const e=document.getElementById("mobileCasePickerModal"),t=document.getElementById("mobileCaseList"),a=document.getElementById("quickStartCase").value;let n="";const i=this.getCasePickerOptions();for(const o of i){const r=o.name===a;n+=`
39
+ <button class="mobile-case-item ${r?"selected":""}"
40
+ onclick="app.selectMobileCase(${escapeHtml(JSON.stringify(o.name))})">
41
+ <span class="mobile-case-item-icon">
42
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
43
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
44
+ </svg>
45
+ </span>
46
+ <span class="mobile-case-item-name">${escapeHtml(o.label)}</span>
47
+ <span class="mobile-case-item-delete" onclick="event.stopPropagation(); app.deleteCaseMobile(${escapeHtml(JSON.stringify(o.name))})" title="Delete">
48
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
49
+ <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
50
+ </svg>
51
+ </span>
52
+ <span class="mobile-case-item-check">
53
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
54
+ <polyline points="20 6 9 17 4 12"/>
55
+ </svg>
56
+ </span>
57
+ </button>
58
+ `}t.innerHTML=n,e.classList.add("active")},closeMobileCasePicker(){document.getElementById("mobileCasePickerModal").classList.remove("active")},selectMobileCase(e){const t=document.getElementById("quickStartCase");t.value=e,this.updateMobileCaseLabel(e),this.updateDirDisplayForCase(e),this.saveLastUsedCase(e),this.closeMobileCasePicker(),this.showToast(`Selected: ${e}`,"success")},updateMobileCaseLabel(e){const t=document.getElementById("mobileCaseName");t&&(t.textContent=e)},async deleteCaseMobile(e){if(confirm(`Delete case "${e}"?`))try{const s=await(await fetch(`/api/cases/${encodeURIComponent(e)}`,{method:"DELETE"})).json();s.success?(this.showToast(`Case "${e}" ${s.data?.type==="unlinked"?"unlinked":"deleted"}`,"success"),this.cases=(this.cases||[]).filter(a=>a.name!==e),this.closeMobileCasePicker(),await this.loadQuickStartCases()):this.showToast(s.error||"Failed to delete case","error")}catch(t){this.showToast("Failed to delete case: "+t.message,"error")}},showCreateCaseFromMobile(){this.closeMobileCasePicker(),this.showCreateCaseModal();const e=document.getElementById("createCaseModal");e.classList.add("from-mobile"),setTimeout(()=>e.classList.remove("from-mobile"),300)}}),Object.defineProperty(CodemanApp.prototype,"runMode",{configurable:!0,enumerable:!0,get(){return this._runMode||"claude"},set(e){this._runMode=e==="opencode"||e==="codex"||e==="gemini"||e==="claude"?e:"claude"}});