aicodeman 1.6.2 → 1.7.1

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 (92) hide show
  1. package/README.md +25 -3
  2. package/README.zh-CN.md +9 -3
  3. package/dist/utils/regex-patterns.d.ts.map +1 -1
  4. package/dist/utils/regex-patterns.js +1 -1
  5. package/dist/utils/regex-patterns.js.map +1 -1
  6. package/dist/web/public/admin-ui.js.gz +0 -0
  7. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  8. package/dist/web/public/{app.9367fd20.js → app.2e95c82d.js} +2 -2
  9. package/dist/web/public/app.2e95c82d.js.br +0 -0
  10. package/dist/web/public/app.2e95c82d.js.gz +0 -0
  11. package/dist/web/public/constants.21a0a83e.js.gz +0 -0
  12. package/dist/web/public/cron-ui.js.gz +0 -0
  13. package/dist/web/public/i18n.4dcf1b87.js +1 -0
  14. package/dist/web/public/i18n.4dcf1b87.js.br +0 -0
  15. package/dist/web/public/i18n.4dcf1b87.js.gz +0 -0
  16. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  17. package/dist/web/public/index.html +30 -9
  18. package/dist/web/public/index.html.br +0 -0
  19. package/dist/web/public/index.html.gz +0 -0
  20. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  21. package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
  22. package/dist/web/public/{mobile-handlers.b5a667ef.js → mobile-handlers.bc2b8c1d.js} +29 -0
  23. package/dist/web/public/mobile-handlers.bc2b8c1d.js.br +0 -0
  24. package/dist/web/public/mobile-handlers.bc2b8c1d.js.gz +0 -0
  25. package/dist/web/public/mobile.f2dcfa13.css +1 -0
  26. package/dist/web/public/mobile.f2dcfa13.css.br +0 -0
  27. package/dist/web/public/mobile.f2dcfa13.css.gz +0 -0
  28. package/dist/web/public/{notification-manager.ca9dc40d.js → notification-manager.fca811fd.js} +4 -2
  29. package/dist/web/public/notification-manager.fca811fd.js.br +0 -0
  30. package/dist/web/public/notification-manager.fca811fd.js.gz +0 -0
  31. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  32. package/dist/web/public/panels-ui.58582d4c.js.gz +0 -0
  33. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  34. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  35. package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
  36. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  37. package/dist/web/public/session-ui.713c8836.js +68 -0
  38. package/dist/web/public/session-ui.713c8836.js.br +0 -0
  39. package/dist/web/public/session-ui.713c8836.js.gz +0 -0
  40. package/dist/web/public/settings-ui.0259fbe8.js +67 -0
  41. package/dist/web/public/settings-ui.0259fbe8.js.br +0 -0
  42. package/dist/web/public/settings-ui.0259fbe8.js.gz +0 -0
  43. package/dist/web/public/styles.d2f415e2.css +1 -0
  44. package/dist/web/public/styles.d2f415e2.css.br +0 -0
  45. package/dist/web/public/styles.d2f415e2.css.gz +0 -0
  46. package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
  47. package/dist/web/public/sw.js.gz +0 -0
  48. package/dist/web/public/terminal-ui.9ea677e9.js.gz +0 -0
  49. package/dist/web/public/ultracode-panel.js.gz +0 -0
  50. package/dist/web/public/ultracode-windows.js.gz +0 -0
  51. package/dist/web/public/upload.html.gz +0 -0
  52. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  53. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  54. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  55. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  56. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  57. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  58. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  59. package/dist/web/public/vendor/xterm.css.gz +0 -0
  60. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  61. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  62. package/dist/web/schemas.d.ts +1 -0
  63. package/dist/web/schemas.d.ts.map +1 -1
  64. package/dist/web/schemas.js +12 -0
  65. package/dist/web/schemas.js.map +1 -1
  66. package/dist/web/server.d.ts +1 -1
  67. package/dist/web/server.d.ts.map +1 -1
  68. package/dist/web/server.js +11 -1
  69. package/dist/web/server.js.map +1 -1
  70. package/dist/web/sse-events.d.ts +7 -3
  71. package/dist/web/sse-events.d.ts.map +1 -1
  72. package/dist/web/sse-events.js +7 -3
  73. package/dist/web/sse-events.js.map +1 -1
  74. package/package.json +1 -1
  75. package/dist/web/public/app.9367fd20.js.br +0 -0
  76. package/dist/web/public/app.9367fd20.js.gz +0 -0
  77. package/dist/web/public/mobile-handlers.b5a667ef.js.br +0 -0
  78. package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
  79. package/dist/web/public/mobile.6a53092b.css +0 -1
  80. package/dist/web/public/mobile.6a53092b.css.br +0 -0
  81. package/dist/web/public/mobile.6a53092b.css.gz +0 -0
  82. package/dist/web/public/notification-manager.ca9dc40d.js.br +0 -0
  83. package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
  84. package/dist/web/public/session-ui.0cf86dd4.js +0 -68
  85. package/dist/web/public/session-ui.0cf86dd4.js.br +0 -0
  86. package/dist/web/public/session-ui.0cf86dd4.js.gz +0 -0
  87. package/dist/web/public/settings-ui.bc5f4792.js +0 -67
  88. package/dist/web/public/settings-ui.bc5f4792.js.br +0 -0
  89. package/dist/web/public/settings-ui.bc5f4792.js.gz +0 -0
  90. package/dist/web/public/styles.e9dc275f.css +0 -1
  91. package/dist/web/public/styles.e9dc275f.css.br +0 -0
  92. package/dist/web/public/styles.e9dc275f.css.gz +0 -0
@@ -16,8 +16,8 @@
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.e9dc275f.css">
20
- <link rel="stylesheet" href="mobile.6a53092b.css" media="(max-width: 1023px)">
19
+ <link rel="stylesheet" href="styles.d2f415e2.css">
20
+ <link rel="stylesheet" href="mobile.f2dcfa13.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'">
23
23
  <noscript><link rel="stylesheet" href="vendor/xterm.css"></noscript>
@@ -25,7 +25,7 @@
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
27
  <link rel="preload" href="constants.21a0a83e.js" as="script">
28
- <link rel="preload" href="app.9367fd20.js" as="script">
28
+ <link rel="preload" href="app.2e95c82d.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>
@@ -46,6 +46,10 @@
46
46
  <script>if(window.innerWidth<768||(('ontouchstart' in window||navigator.maxTouchPoints>0)&&window.innerWidth<1024))document.documentElement.classList.add('mobile-init');</script>
47
47
  <!-- Synchronous skin selection — runs before first paint to prevent theme flash -->
48
48
  <script>try{var s=localStorage.getItem('codeman:skin');if(s!=='og'&&s!=='daylight-green'&&s!=='daylight-blue')s='daylight-blue';document.documentElement.dataset.skin=s;window.__codemanSkin=s;}catch(e){document.documentElement.dataset.skin='daylight-blue';window.__codemanSkin='daylight-blue';}</script>
49
+ <!-- Apply the saved per-device language before first paint. The full translation
50
+ layer loads below; setting lang/dir here prevents an English accessibility
51
+ tree from flashing while the deferred scripts start. -->
52
+ <script>try{var m=window.innerWidth<768||(('ontouchstart' in window||navigator.maxTouchPoints>0)&&window.innerWidth<1024);var k=m?'codeman-app-settings-mobile':'codeman-app-settings';var l=JSON.parse(localStorage.getItem(k)||'{}').language;l=l==='zh-CN'?'zh-CN':'en';document.documentElement.lang=l;window.__codemanLanguage=l;}catch(e){document.documentElement.lang='en';window.__codemanLanguage='en';}</script>
49
53
  <!-- Inline critical CSS for instant skeleton paint (before styles.css loads) -->
50
54
  <style>
51
55
  .loading-skeleton{display:flex;flex-direction:column;height:100vh;height:100dvh;background:#11151c}
@@ -76,7 +80,9 @@
76
80
  <!-- Compact Header with Session Tabs -->
77
81
  <header class="header">
78
82
  <div class="header-brand">
79
- <span class="logo" onclick="app.goHome()" title="Go to main page">Codeman</span>
83
+ <span class="logo" onclick="app.goHome()" title="Go to main page"
84
+ ><span class="logo-text">Codeman</span><span class="logo-compact" aria-hidden="true">C</span></span
85
+ >
80
86
  </div>
81
87
 
82
88
  <!-- Session Tabs -->
@@ -1165,6 +1171,20 @@
1165
1171
  <!-- Display Tab -->
1166
1172
  <div class="modal-tab-content" id="settings-display">
1167
1173
  <div class="settings-grid">
1174
+ <!-- Branding & Language Section -->
1175
+ <div class="settings-section-header">Branding &amp; Language</div>
1176
+ <div class="settings-item" title="Name shown in the browser UI and window title. Supports Unicode, including Chinese.">
1177
+ <span class="settings-item-label">Display Name</span>
1178
+ <input type="text" id="appSettingsDisplayName" class="settings-inline-input" maxlength="40" placeholder="Codeman" autocomplete="off">
1179
+ </div>
1180
+ <div class="settings-item" title="Language for this device. Dynamic status messages and dialogs use the same language.">
1181
+ <span class="settings-item-label">Interface Language</span>
1182
+ <select id="appSettingsLanguage" class="form-select settings-inline-select">
1183
+ <option value="en">English</option>
1184
+ <option value="zh-CN">简体中文</option>
1185
+ </select>
1186
+ </div>
1187
+
1168
1188
  <!-- Appearance Section -->
1169
1189
  <div class="settings-section-header">Appearance</div>
1170
1190
  <div class="settings-item settings-item-skin" title="Visual theme for this device (not synced)">
@@ -2502,24 +2522,25 @@
2502
2522
  </svg>
2503
2523
 
2504
2524
  <script defer src="constants.21a0a83e.js"></script>
2505
- <script defer src="mobile-handlers.b5a667ef.js"></script>
2525
+ <script defer src="i18n.4dcf1b87.js"></script>
2526
+ <script defer src="mobile-handlers.bc2b8c1d.js"></script>
2506
2527
  <script defer src="voice-input.085e9e73.js"></script>
2507
- <script defer src="notification-manager.ca9dc40d.js"></script>
2528
+ <script defer src="notification-manager.fca811fd.js"></script>
2508
2529
  <script defer src="keyboard-accessory.3f91acef.js"></script>
2509
2530
  <script defer src="input-cjk.63794d0b.js"></script>
2510
2531
  <!-- Hardened markdown HTML sanitizer (wires DOMPurify). Must precede app.js. -->
2511
2532
  <script defer src="sanitize-html.bc7078d6.js"></script>
2512
- <script defer src="app.9367fd20.js"></script>
2533
+ <script defer src="app.2e95c82d.js"></script>
2513
2534
  <script defer src="terminal-ui.9ea677e9.js"></script>
2514
2535
  <script defer src="respawn-ui.2d249da9.js"></script>
2515
2536
  <script defer src="ralph-panel.6de2d0f8.js"></script>
2516
2537
  <script defer src="orchestrator-panel.js"></script>
2517
2538
  <script defer src="cron-ui.js"></script>
2518
- <script defer src="settings-ui.bc5f4792.js"></script>
2539
+ <script defer src="settings-ui.0259fbe8.js"></script>
2519
2540
  <script defer src="panels-ui.58582d4c.js"></script>
2520
2541
  <script defer src="ultracode-panel.js"></script>
2521
2542
  <script defer src="admin-ui.js"></script>
2522
- <script defer src="session-ui.0cf86dd4.js"></script>
2543
+ <script defer src="session-ui.713c8836.js"></script>
2523
2544
  <script defer src="ralph-wizard.13a1831e.js"></script>
2524
2545
  <script defer src="api-client.c9b1cddc.js"></script>
2525
2546
  <script defer src="subagent-windows.69b60c35.js"></script>
Binary file
Binary file
@@ -43,6 +43,35 @@ const MobileDetection = {
43
43
  );
44
44
  },
45
45
 
46
+ /**
47
+ * Check whether this browser belongs to a handheld device.
48
+ *
49
+ * Unlike getDeviceType(), this classification must remain stable when a
50
+ * foldable changes posture. An unfolded phone can expose a desktop-width
51
+ * viewport, but it still needs the same per-device settings that were saved
52
+ * while folded. User-Agent Client Hints are preferred where available; the
53
+ * legacy token fallback covers Android WebView and iPhone browsers.
54
+ */
55
+ isHandheldDevice() {
56
+ if (!this.isTouchDevice()) return false;
57
+
58
+ const userAgent = navigator.userAgent || '';
59
+
60
+ // Prefer explicit UA form-factor signals. Besides matching real browsers,
61
+ // this avoids Chromium emulation reporting userAgentData.mobile=true for
62
+ // an iPad/tablet context created with isMobile=true.
63
+ if (/iPad|Tablet|Silk|PlayBook|Kindle|Windows NT|CrOS|Macintosh/i.test(userAgent)) {
64
+ return false;
65
+ }
66
+ if (/Android/i.test(userAgent) && !/Mobile/i.test(userAgent)) return false;
67
+ if (/Mobi|iPhone|iPod/i.test(userAgent)) return true;
68
+
69
+ const uaDataMobile = navigator.userAgentData?.mobile;
70
+ if (typeof uaDataMobile === 'boolean') return uaDataMobile;
71
+
72
+ return false;
73
+ },
74
+
46
75
  /** Check if device is iOS (iPhone, iPad, iPod) */
47
76
  isIOS() {
48
77
  return (
@@ -0,0 +1 @@
1
+ html.mobile-init .header-font-controls,html.mobile-init .header-system-stats,html.mobile-init .header-tokens,html.mobile-init .monitor-panel,html.mobile-init .subagents-panel,html.mobile-init .project-insights-panel,html.mobile-init .file-browser-panel{display:none!important}@media(max-width:768px){input,textarea,select{font-size:16px!important}html{touch-action:manipulation}.session-tab .tab-detach{display:none!important}}@media(max-width:768px)and (min-width:430px){.header{position:fixed;top:0;left:0;right:0;min-height:48px;max-height:48px;padding:.35rem .5rem;padding-left:calc(.5rem + var(--safe-area-left));padding-right:calc(.5rem + var(--safe-area-right));background:#0a0a0a;border-bottom:1px solid rgba(255,255,255,.08);contain:none;overflow:visible;z-index:1200}.ios-device .header{padding-top:calc(.35rem + var(--safe-area-top));min-height:calc(48px + var(--safe-area-top));max-height:calc(48px + var(--safe-area-top))}.app{padding-top:54px}.ios-device .app{padding-top:calc(54px + var(--safe-area-top))}.header-font-controls{gap:.15rem;padding:.15rem .25rem}.header-font-controls .btn-icon-sm{width:18px;height:18px}.header-system-stats{font-size:.6rem;gap:.25rem}.header-right{gap:.35rem}.btn-icon-header:not(.btn-sm){width:44px;height:44px}.session-tabs,.session-tabs.tabs-two-rows{flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;scrollbar-width:none;max-height:52px;gap:3px}.session-tabs::-webkit-scrollbar{display:none}.session-tab{padding:.4rem .6rem;font-size:.75rem;min-height:40px}.session-tab .tab-name{max-width:80px}.toolbar{padding:.4rem .5rem;padding-left:calc(.5rem + var(--safe-area-left));padding-right:calc(.5rem + var(--safe-area-right));gap:.4rem}.tab-count-group{display:none}.btn-toolbar{padding:.4rem .8rem;font-size:.8rem;min-height:40px}.case-select-group{max-width:none}.case-combobox{width:160px}.toolbar-select{font-size:.75rem}.monitor-panel,.subagents-panel{width:100%;max-width:100%;left:0;right:0;border-radius:8px 8px 0 0;max-height:40vh}.project-insights-panel{max-width:280px;font-size:.7rem}.modal{z-index:1300}.command-palette-modal{padding:10vh .75rem 0}.command-palette-shell{width:100%;max-height:74vh}.command-palette-input-row{grid-template-columns:20px minmax(0,1fr)}.command-palette-input-row kbd{display:none}.command-palette-item{min-height:56px}.modal-tabs{overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex-wrap:nowrap}.modal-tabs::-webkit-scrollbar{display:none}.modal-tab-btn{padding:.4rem .75rem;font-size:.7rem;white-space:nowrap;flex-shrink:0}.settings-grid{gap:.4rem .75rem}.settings-item{font-size:.7rem}.event-type-grid{grid-template-columns:1fr 40px 40px 40px;gap:5px 6px}.event-label{font-size:.7rem}.subagent-window{width:320px;height:280px;min-width:240px;min-height:160px}.subagent-window-header{padding:.35rem .5rem}.subagent-window-title .id{font-size:.7rem;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.subagent-window-title .status{font-size:.55rem}.subagent-window-body{font-size:.7rem}.respawn-banner{padding:.25rem .5rem;padding-left:calc(.5rem + var(--safe-area-left));padding-right:calc(.5rem + var(--safe-area-right));font-size:.65rem}.respawn-compact-layout{gap:.75rem}.respawn-status-row1{gap:.4rem}.respawn-action-log{max-height:2.6em;font-size:.6rem}.respawn-countdown-timer{font-size:.55rem}.respawn-timer-bar{width:24px}.toolbar-center .btn-toolbar.btn-voice{display:flex!important}.toolbar{padding:0 .5rem;gap:.5rem}.toolbar-left,.toolbar-right,.toolbar-group{gap:.5rem}.btn-toolbar{padding:.4rem .75rem;font-size:.75rem;min-height:unset}}@media(max-width:430px){.header-brand{padding-right:.25rem;margin-right:.2rem;border-right:none}.header-brand .logo{font-size:.85rem}.header-brand .logo .logo-text{display:none}.header-brand .logo .logo-compact{display:inline}.header-font-controls{gap:.1rem;padding:.1rem .2rem}.header-font-controls .btn-icon-sm{width:16px;height:16px}.header-font-controls .font-size-display{font-size:.6rem;min-width:14px}.header-system-stats{font-size:.55rem;gap:.15rem}.header-tokens{font-size:.6rem;padding:.1rem .3rem}.header{position:fixed;top:0;left:0;right:0;min-height:36px;max-height:36px;padding:.15rem .3rem;padding-left:calc(.3rem + var(--safe-area-left));padding-right:calc(.3rem + var(--safe-area-right));gap:.15rem;overflow:hidden;background:#0a0a0a;border-bottom:1px solid rgba(255,255,255,.08);contain:none;z-index:1200}.ios-device .header{padding-top:calc(.15rem + var(--safe-area-top));min-height:calc(36px + var(--safe-area-top));max-height:calc(36px + var(--safe-area-top))}.app{padding-top:42px}.ios-device .app{padding-top:calc(42px + var(--safe-area-top))}.main{padding-bottom:calc(40px + var(--safe-area-bottom))}.ios-device.safari-browser .main{padding-bottom:calc(40px + var(--safe-area-bottom) + (100vh - var(--app-height, 100vh)))}.header-right{padding-left:.2rem;gap:.1rem;flex-shrink:0;border-left:none}.btn-icon-header{width:26px;height:26px;padding:0}.btn-icon-header svg{width:12px;height:12px}.btn-icon-header.btn-settings,.btn-icon-header.btn-lifecycle-log,.btn-icon-header.btn-away-digest,.btn-icon-header.btn-session-manager,.btn-icon-header.btn-file-viewer,.btn-admin-panel{display:none!important}.btn-voice-mobile{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;background:#1a2a3f;border:1px solid rgba(59,130,246,.3);border-radius:4px;color:#93c5fd;cursor:pointer;flex-shrink:0;order:5}.btn-voice-mobile svg{width:13px;height:13px}.btn-voice-mobile:active{background:#2a3a5f}.btn-voice-mobile.recording{background:#ef444440;border-color:#ef444499;color:#ef4444;animation:mobile-voice-pulse 1.2s ease-in-out infinite}@keyframes mobile-voice-pulse{0%,to{box-shadow:inset 0 0 0 1px #ef444459;background:#ef444433}50%{box-shadow:inset 0 0 0 2px #ef4444bf;background:#ef444459}}.btn-settings-mobile{display:inline-flex!important;align-items:center;justify-content:center;width:26px;height:26px;background:transparent;border:1px solid rgba(255,255,255,.2);border-radius:4px;color:#9ca3af;font-size:.85rem;cursor:pointer;flex-shrink:0;order:6;position:relative}.btn-settings-mobile svg{width:13px;height:13px}.btn-settings-mobile:active{background:#ffffff1a;color:#fff}.app{overflow:hidden;position:relative}.keyboard-visible .app{position:fixed;inset:0 0 auto;height:var(--app-height, 100dvh)}.session-tabs,.session-tabs.tabs-two-rows{flex:1;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;scrollbar-width:none;max-height:36px;gap:2px;padding:0}.session-tabs::-webkit-scrollbar{display:none}.session-tab{flex-shrink:0;min-height:32px;max-height:32px;padding:.35rem .5rem;font-size:.7rem;gap:.25rem;border-radius:4px}.session-tab .tab-status{width:4px;height:4px}.session-tab .tab-name{max-width:50px;overflow:hidden;text-overflow:ellipsis}.session-tab .tab-close,.session-tab .tab-gear{display:none}.session-tab.active .tab-gear{display:inline-flex;align-items:center;justify-content:center;font-size:.65rem;line-height:1;width:32px;height:32px;margin-left:auto;opacity:.6}.session-tab.active .tab-close{display:inline-flex;align-items:center;justify-content:center;font-size:1.1rem;line-height:1;width:20px;height:18px;margin-left:-2px}.toolbar{position:fixed;bottom:var(--safe-area-bottom);left:0;right:0;height:40px;min-height:40px;max-height:40px;padding:.3rem .4rem;padding-left:calc(.4rem + var(--safe-area-left));padding-right:calc(.4rem + var(--safe-area-right));flex-wrap:nowrap;gap:.3rem;align-items:center;justify-content:space-between;background:#111;border-top:1px solid rgba(255,255,255,.1);z-index:50;transition:transform .15s ease-out;will-change:transform}.ios-device.safari-browser .toolbar{bottom:calc(var(--safe-area-bottom) + (100vh - var(--app-height, 100vh)))}.keyboard-visible.ios-device.safari-browser .toolbar{bottom:var(--safe-area-bottom)}.keyboard-visible.ios-device.safari-browser .keyboard-accessory-bar{bottom:calc(var(--safe-area-bottom) + 40px)}.toolbar-center{display:flex!important;align-items:center}.toolbar-center .btn-toolbar.btn-voice,.toolbar-right,.version-display{display:none!important}.toolbar-left{flex:1 1 0;min-width:0;align-items:center;gap:.3rem}.toolbar-left .toolbar-group{display:flex;flex:1 1 0;min-width:0;align-items:center;gap:.3rem}.toolbar-left .toolbar-group:first-child{flex:1 1 0;min-width:0;justify-content:space-between;gap:.3rem;flex-wrap:nowrap;align-items:center}.tab-count-group{display:none!important}.btn-toolbar{min-height:26px!important;max-height:26px!important;height:26px!important;padding:0 .5rem!important;font-size:.65rem!important;border-radius:4px;line-height:26px;display:inline-flex;align-items:center;justify-content:center;font-weight:500;letter-spacing:.01em;color:#fff}.run-btn-group{display:flex;flex:0 0 auto}.btn-toolbar.btn-run{flex:0 0 auto;padding:0 .6rem!important;font-weight:500;border-radius:4px 0 0 4px!important;border-right:none!important}.btn-toolbar.btn-run svg{width:10px;height:10px;margin-right:3px}.btn-toolbar.btn-run-gear{flex:0 0 auto;padding:0 .35rem!important;min-width:unset!important;border-radius:0 4px 4px 0!important;border-left:1px solid rgba(255,255,255,.15)!important}.btn-toolbar.btn-run-gear svg{width:10px;height:10px}.btn-toolbar.btn-run.mode-claude,.btn-toolbar.btn-run-gear.mode-claude{background:#1e3a5f;border-color:#3b82f64d;color:#93c5fd}.btn-toolbar.btn-run.mode-claude:active,.btn-toolbar.btn-run-gear.mode-claude:active{background:#2563eb;border-color:#3b82f680}.btn-toolbar.btn-run.mode-opencode,.btn-toolbar.btn-run-gear.mode-opencode{background:#0a2e2a;border-color:#10b9814d;color:#6ee7b7}.btn-toolbar.btn-run.mode-opencode:active,.btn-toolbar.btn-run-gear.mode-opencode:active{background:#0d4a40;border-color:#10b98180}.btn-toolbar.btn-run.mode-gemini,.btn-toolbar.btn-run-gear.mode-gemini{background:#10243f;border-color:#60a5fa4d;color:#dbeafe}.btn-toolbar.btn-run.mode-gemini:active,.btn-toolbar.btn-run-gear.mode-gemini:active{background:#174ea6;border-color:#60a5fa80}.run-mode-menu{bottom:100%;left:0;margin-bottom:6px;min-width:160px;max-width:80vw}.run-mode-option{padding:10px 12px;font-size:.8rem;cursor:pointer;-webkit-tap-highlight-color:rgba(255,255,255,.1)}.run-mode-history{-webkit-overflow-scrolling:touch;touch-action:manipulation}.btn-toolbar.btn-stop{display:flex!important;flex:0 0 auto;padding:0 8px!important;min-width:unset;order:3}.btn-toolbar.btn-stop svg{width:10px;height:10px;margin-right:0}.btn-toolbar.btn-shell{flex:0 0 auto;background:transparent;border:1px solid rgba(255,255,255,.2);color:#9ca3af;order:4}.btn-toolbar.btn-shell:hover,.btn-toolbar.btn-shell:active{background:#ffffff1a;color:#fff}.case-select-group{display:none!important}.toolbar-left .toolbar-group:first-child{width:100%;gap:8px}.btn-toolbar.btn-shell{flex:0 0 auto;min-width:54px;width:54px;white-space:nowrap;padding:0 8px!important;overflow:hidden;font-size:0!important}.btn-toolbar.btn-shell:after{content:"Shell";font-size:.65rem}.btn-toolbar.btn-case-mobile{display:flex!important;flex:1 1 0!important;min-width:0;padding:0 8px!important;order:2;overflow:hidden;justify-content:center;gap:4px}.btn-toolbar.btn-case-mobile #mobileCaseName{max-width:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.btn-case-settings-mobile{display:inline-flex!important;align-items:center;justify-content:center;width:26px;height:26px;background:transparent;border:1px solid rgba(255,255,255,.2);border-radius:4px;color:#9ca3af;cursor:pointer;flex-shrink:0;order:1}.btn-case-settings-mobile:active{background:#ffffff1a;color:#fff}@media(max-width:374px){.toolbar{padding:0 2px}.toolbar-left,.toolbar-left .toolbar-group,.toolbar-left .toolbar-group:first-child{gap:2px}}.btn-case-settings-mobile{display:none!important}.case-settings-popover-mobile{position:fixed;bottom:calc(var(--safe-area-bottom) + 46px);left:8px;right:8px;background:#1e1e1e;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:.6rem .75rem;z-index:1000;box-shadow:0 4px 16px #00000080;display:block}.case-settings-popover-mobile.hidden{display:none!important}.case-settings-popover-mobile .checkbox-inline{display:flex;align-items:center;gap:6px;font-size:.75rem;color:#e5e7eb}.case-settings-popover-mobile .form-hint{display:block;margin-top:.2rem;font-size:.6rem;color:#6b7280}.keyboard-accessory-bar{display:none;position:fixed;bottom:calc(var(--safe-area-bottom) + 40px);left:0;right:0;height:44px;background:#1a1a1a;border-top:1px solid rgba(255,255,255,.1);padding:6px 8px;padding-left:calc(8px + var(--safe-area-left));padding-right:calc(8px + var(--safe-area-right));gap:8px;align-items:center;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;z-index:51;transition:transform .15s ease-out;will-change:transform}.keyboard-accessory-bar.visible{display:flex}.keyboard-accessory-bar::-webkit-scrollbar{display:none}.accessory-btn{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;gap:4px;padding:6px 12px;background:#2a2a2a;border:1px solid rgba(255,255,255,.15);border-radius:6px;color:#e5e5e5;font-size:.65rem;font-weight:500;cursor:pointer;transition:background .15s,border-color .15s}.accessory-btn.confirming{background:#6b4f00;border-color:#b8860b;color:#ffd54f}.accessory-btn:active{background:#3a3a3a}.accessory-btn svg{width:14px;height:14px}.accessory-btn-arrow{padding:6px 10px;background:#2563eb;border-color:#3b82f680;color:#fff}.accessory-btn-arrow:active{background:#1d4ed8}.accessory-btn-dismiss{margin-left:auto;flex:1 1 0;max-width:100px;padding:10px 8px;background:#2563eb;border-color:#3b82f680;color:#fff;font-weight:600}.accessory-btn-dismiss svg{width:22px;height:22px}.accessory-btn-dismiss:active{background:#1d4ed8}.voice-preview{bottom:calc(var(--safe-area-bottom) + 94px);font-size:.8rem;max-width:90%;padding:6px 14px}.btn-case-add,.btn-case-settings,.toolbar-select{display:none!important}.toolbar .btn-case-add{min-width:26px!important;max-width:26px!important;width:26px!important;min-height:26px!important;max-height:26px!important;height:26px!important;padding:0!important;font-size:.9rem;font-weight:700;display:inline-flex!important;align-items:center;justify-content:center;line-height:1;border-radius:4px;background:transparent;border:1px solid rgba(255,255,255,.15);color:#9ca3af}.btn-case-add:hover,.btn-case-add:active{background:#ffffff1a;color:#fff}.monitor-panel,.subagents-panel{width:100%;max-width:100%;left:0;right:0;border-radius:8px 8px 0 0;bottom:calc(44px + 2rem + var(--safe-area-bottom));max-height:35vh}.modal{z-index:1300}.modal-content{width:100%;max-width:100%;height:100%;max-height:100%;border-radius:0;margin:0;display:flex;flex-direction:column}.modal-content.modal-sm{height:auto;max-height:85vh;border-radius:12px;margin:1rem;width:calc(100% - 2rem)}.ios-device .modal-content{padding-top:var(--safe-area-top);padding-bottom:var(--safe-area-bottom);padding-left:var(--safe-area-left);padding-right:var(--safe-area-right)}.modal-header{padding:.75rem 1rem}.modal-header h3{font-size:1rem}.modal-body{padding:.75rem 1rem;flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch}.away-digest-modal{width:100%;max-width:100%;height:100%;max-height:100%}.away-digest-ranges{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.4rem}.away-digest-ranges .filter-btn{min-height:38px;padding:.4rem .5rem}.away-digest-custom-range,.away-digest-custom-range.active{grid-template-columns:1fr}.away-digest-summary{grid-template-columns:repeat(2,minmax(0,1fr));gap:.5rem}.away-digest-item{grid-template-columns:1fr;gap:.5rem}.away-digest-action{width:100%;min-height:38px}.modal-footer,.form-actions{padding:.75rem 1rem;padding-bottom:calc(.75rem + var(--safe-area-bottom))}.ios-device .terminal-container{padding-bottom:var(--safe-area-bottom)}.main{flex:1;min-height:0}.cli-info-bar{display:block}.terminal-container{height:100%;min-height:0;position:relative;overflow:visible;touch-action:none}.terminal-container .xterm,.terminal-container .xterm-viewport,.terminal-container .xterm-screen{touch-action:none}.response-viewer{padding-bottom:var(--safe-area-bottom, 0px);max-height:88vh;max-height:92dvh}.response-viewer-body{font-size:14.5px;line-height:1.65;padding:16px 16px 24px;--rv-content-max: 100%}.response-viewer-body .rv-text pre,.response-viewer-body pre{padding:12px 14px;margin-left:-4px;margin-right:-4px;border-radius:6px}.response-viewer-body .rv-text pre code,.response-viewer-body pre code{font-size:11.5px;line-height:1.5}.response-viewer-body .rv-text h1,.response-viewer-body>h1{font-size:1.35em}.response-viewer-body .rv-text h2,.response-viewer-body>h2{font-size:1.2em}.response-viewer-body .rv-text h3,.response-viewer-body>h3{font-size:1.08em}.welcome-content{max-width:calc(100vw - 1.5rem);padding:1rem .75rem}.welcome-title{font-size:1.2rem;margin-bottom:.5rem}.welcome-desc{font-size:.8rem;margin-bottom:.4rem}.welcome-actions{flex-direction:column;gap:.5rem;margin-top:1rem}.welcome-btn{width:100%;justify-content:center;min-height:44px;padding:.75rem 1rem;font-size:.85rem}.history-show-more{display:block;margin-bottom:.75rem}.welcome-ralph-link{display:block;margin:.75rem auto 0}.welcome-hint{font-size:.7rem;margin-top:.75rem}.modal-wizard{display:flex;flex-direction:column}.wizard-progress{padding:.5rem}.wizard-step{padding:.3rem}.wizard-step-number{width:24px;height:24px;font-size:.7rem}.wizard-step-label{display:none}.timer-banner{padding-left:calc(.5rem + var(--safe-area-left));padding-right:calc(.5rem + var(--safe-area-right))}.respawn-banner{padding:.3rem .5rem;padding-left:calc(.5rem + var(--safe-area-left));padding-right:calc(.5rem + var(--safe-area-right));font-size:.65rem}.respawn-compact-layout{flex-direction:column;gap:.15rem}.respawn-status-col{flex-shrink:1;min-width:0;gap:.1rem}.respawn-status-row1{flex-wrap:wrap;gap:.25rem;row-gap:.1rem}.respawn-state{font-size:.6rem;padding:.05rem .3rem}.respawn-banner .detection-confidence{font-size:.5rem;padding:.02rem .2rem}.respawn-cycles{font-size:.55rem}.respawn-timer{font-size:.55rem;padding:0 .25rem}.respawn-tokens{font-size:.55rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:120px}.respawn-indicator{font-size:.65rem}.respawn-banner .btn-icon-only{min-width:36px;min-height:36px;font-size:1rem;display:flex;align-items:center;justify-content:center;margin-left:auto;border-radius:6px;flex-shrink:0}.respawn-status-row2{font-size:.55rem;gap:.3rem;min-height:0;flex-wrap:wrap}.respawn-banner .detection-hook,.respawn-banner .detection-ai-check,.respawn-banner .detection-status{font-size:.55rem;padding:.02rem .2rem}.respawn-countdown-timers{gap:.2rem}.respawn-countdown-timer{font-size:.5rem;padding:.02rem .2rem;gap:.15rem}.respawn-countdown-timer .respawn-timer-bar{display:none}.respawn-action-log{border-left:none;border-top:1px solid rgba(34,197,94,.15);padding-left:0;padding-top:.15rem;max-height:1.4em;font-size:.55rem;overflow:hidden}.respawn-header{flex-direction:row;gap:.4rem;align-items:center}.respawn-header .respawn-actions{display:flex;gap:.3rem;flex-shrink:0}.respawn-header .respawn-actions .btn-toolbar{min-height:28px!important;font-size:.7rem!important;padding:.2rem .5rem!important;border-radius:5px}.duration-presets{display:flex;flex-wrap:wrap;gap:.2rem}.duration-preset-btn{min-height:24px;padding:.1rem .4rem;font-size:.6rem;border-radius:4px;text-align:center}.duration-custom{display:flex;gap:.2rem;align-items:center}.duration-custom-input.visible{flex:0 1 auto}.duration-custom-input input{width:64px;min-height:24px;font-size:16px;padding:.1rem .3rem}.preset-selector{display:grid;grid-template-columns:1fr 1fr;gap:.25rem}.preset-selector select{grid-column:1 / -1;min-height:32px;font-size:16px;border-radius:5px;padding:.2rem .4rem}.preset-selector .btn{min-height:32px;font-size:.65rem;border-radius:5px;padding:.2rem .4rem}#sessionOptionsModal textarea{font-size:16px;min-height:32px!important;max-height:56px;height:auto!important;border-radius:6px;padding:.3rem .5rem}#modalRespawnPrompt{min-height:32px!important;max-height:56px}#modalRespawnKickstart{min-height:32px!important;max-height:48px}#sessionOptionsModal .checkbox-inline{min-height:30px;font-size:.75rem;padding:.15rem 0;gap:.4rem}#sessionOptionsModal .checkbox-inline input[type=checkbox]{width:18px;height:18px}#sessionOptionsModal .respawn-loop-title{font-size:.75rem}#sessionOptionsModal .respawn-loop-box .checkbox-inline{font-size:.65rem}#sessionOptionsModal .respawn-options-row{gap:.5rem}#sessionOptionsModal .form-section-header{margin-top:.75rem;padding-top:.5rem;font-size:.65rem}#sessionOptionsModal .form-row{margin-bottom:.5rem}#sessionOptionsModal .form-row label{font-size:.65rem;margin-bottom:.2rem}#sessionOptionsModal .form-hint{font-size:.6rem;margin-top:.2rem}.context-setting{padding:.625rem}.context-setting-header{flex-wrap:wrap;gap:.4rem}.context-setting-header .input-suffix-sm input{width:60px;font-size:16px;min-height:36px}.context-setting input[type=text]{font-size:16px;min-height:36px;padding:.4rem .6rem;border-radius:6px}.color-picker{gap:8px}.color-swatch{width:36px;height:36px;border-radius:6px}.form-row-switch{min-height:40px;gap:.4rem}.context-settings-grid input[type=number]{font-size:16px;min-height:36px}.ralph-limits-grid{grid-template-columns:1fr 1fr;gap:.5rem}.ralph-limits-grid .form-col input[type=number]{font-size:16px;min-height:36px;padding:.4rem .5rem;border-radius:6px}#modalRalphPhrase{font-size:16px;min-height:40px;border-radius:6px}.ralph-config-actions{margin-top:1rem}.ralph-config-actions .btn-toolbar{width:100%;min-height:44px;font-size:.85rem;border-radius:8px}.run-summary-filters{gap:.3rem;margin-bottom:.5rem}.filter-btn{padding:.35rem .75rem;font-size:.7rem;min-height:32px}.timeline-event{padding:.4rem .5rem;margin-bottom:.25rem;font-size:.7rem}.run-summary-footer{flex-direction:column;gap:.4rem;padding:.5rem 0}.run-summary-actions{flex-wrap:wrap;gap:.3rem}.run-summary-actions .btn-toolbar{flex:1;min-height:36px;font-size:.7rem;white-space:nowrap}.auto-refresh-label{font-size:.7rem}.ralph-panel{font-size:.75rem}.ralph-summary{padding:.4rem .5rem;padding-left:calc(.5rem + var(--safe-area-left));padding-right:calc(.5rem + var(--safe-area-right))}.project-insights-panel{max-width:100%;font-size:.65rem;bottom:calc(44px + 2rem + var(--safe-area-bottom))}.file-browser-panel{max-width:100%;max-height:50vh;bottom:calc(44px + 2rem + var(--safe-area-bottom))}.notification-drawer{width:100%;max-width:100%;right:0;border-radius:0;padding-left:var(--safe-area-left);padding-right:var(--safe-area-right);padding-bottom:var(--safe-area-bottom)}input,textarea,[contenteditable]{scroll-margin-bottom:200px;scroll-margin-top:80px}.keyboard-visible .modal-body{max-height:40vh;overflow-y:auto}.keyboard-visible #createCaseModal .modal-body{max-height:60vh}.mobile-case-picker .modal-backdrop{background:#00000080}.mobile-case-picker-sheet{max-height:60vh;padding-bottom:var(--safe-area-bottom);animation:slideUp .2s ease-out}@keyframes slideUp{0%{transform:translateY(100%)}to{transform:translateY(0)}}.mobile-case-picker-header .modal-close{width:32px;height:32px;font-size:1.5rem}.mobile-case-picker-footer{padding-bottom:calc(12px + var(--safe-area-bottom))}.modal-tabs{overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;gap:.25rem;padding:0 .75rem .5rem;flex-wrap:nowrap}.modal-tabs::-webkit-scrollbar{display:none}.modal-tab-btn{padding:.35rem .6rem;font-size:.65rem;white-space:nowrap;flex-shrink:0}#createCaseModal .modal-tabs{gap:.5rem;padding:.5rem 1rem .75rem}#createCaseModal .modal-tab-btn{flex:1;min-height:44px;padding:.6rem 1rem;font-size:.8rem;font-weight:500;border-radius:8px;justify-content:center;text-align:center}#createCaseModal .form-row{margin-bottom:1rem}#createCaseModal .form-row label{font-size:.8rem;margin-bottom:.4rem;font-weight:500;color:#e5e7eb}#createCaseModal .form-row input[type=text]{min-height:44px;font-size:16px;padding:.5rem .75rem;border-radius:8px}#createCaseModal .form-row .form-hint{font-size:.65rem;margin-top:.35rem;line-height:1.4}#createCaseModal .form-actions{padding:.75rem 1rem;padding-bottom:calc(.75rem + var(--safe-area-bottom));gap:.75rem}#createCaseModal .form-actions .btn-toolbar{min-height:44px!important;max-height:44px!important;height:44px!important;font-size:.85rem!important;font-weight:500;border-radius:8px}#caseModalSubmit.loading{opacity:.6;pointer-events:none}#createCaseModal.from-mobile .modal-content{animation:caseModalSlideUp .25s ease-out}@keyframes caseModalSlideUp{0%{transform:translateY(30px);opacity:0}to{transform:translateY(0);opacity:1}}.btn-case-create-mobile{min-height:48px!important;max-height:48px!important;height:48px!important;font-size:.85rem!important;font-weight:500;border-radius:10px}.settings-grid{grid-template-columns:1fr;gap:.35rem}.settings-grid-3col{grid-template-columns:1fr 1fr 1fr}.settings-item{padding:.35rem .5rem;font-size:.7rem}.settings-item-label{font-size:.7rem}.settings-section-header{font-size:.6rem;padding:.35rem 0 .2rem;margin-top:.5rem}.form-row{margin-bottom:.5rem}.form-row label{font-size:.7rem;margin-bottom:.2rem}.form-row .form-hint{font-size:.6rem}.form-row input[type=text],.form-row input[type=number],.form-row textarea,.form-row .form-select,.form-row select{font-size:.75rem;padding:.35rem .5rem;min-height:32px}.form-row-switch{gap:.3rem}.form-row-switch>label:first-child{font-size:.7rem}.form-section-header{font-size:.6rem;margin-top:.75rem}.event-type-grid{grid-template-columns:1fr 36px 36px 36px;gap:4px 4px;padding:6px;margin-top:6px}.event-header{font-size:.55rem}.event-label{font-size:.65rem}.event-type-grid input[type=checkbox]{width:12px;height:12px}.form-actions{gap:.5rem}.form-actions .btn-toolbar{flex:1;min-height:36px!important;max-height:36px!important;height:36px!important;font-size:.75rem!important}.subagent-window{position:fixed;width:calc(100% - 8px);max-width:calc(100% - 8px);height:110px;min-height:80px;max-height:110px;min-width:200px;border-radius:6px;resize:none;box-shadow:0 2px 8px #0006}.subagent-window:after{display:none}.subagent-window-header{padding:.2rem .4rem;min-height:24px}.subagent-window-title{gap:.2rem;overflow:hidden}.subagent-window-title .icon{font-size:.7rem}.subagent-window-title .id{font-size:.6rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:45vw}.subagent-window-title .status{font-size:.45rem;padding:.05rem .15rem}.subagent-model-badge{font-size:.45rem!important;padding:.05rem .15rem!important}.subagent-window-actions button{font-size:.7rem;padding:.15rem .35rem;min-width:26px;min-height:26px;display:flex;align-items:center;justify-content:center}.subagent-window-body{font-size:.6rem;padding:.2rem .35rem}.subagent-window-body .activity-line{gap:.15rem;padding:.05rem 0}.subagent-window-body .activity-line .time{font-size:.5rem}.subagent-window-body .activity-line .tool-name,.subagent-window-body .activity-line .tool-detail{font-size:.55rem}.subagent-window-parent{display:none}.session-tab .tab-subagent-badge{height:14px;padding:0 3px;border-radius:7px;margin-left:2px}.session-tab .tab-subagent-badge .subagent-label{font-size:.45rem}.subagent-dropdown{position:fixed!important;left:8px!important;right:8px!important;bottom:auto!important;min-width:auto!important;max-width:none!important;border-radius:8px;transform:none!important}}.ios-device.safari-browser{overscroll-behavior:none}@media(hover:none)and (pointer:coarse){.session-tab .tab-close,.session-tab .tab-gear{opacity:1;width:auto;padding:.15rem .25rem;align-items:center;justify-content:center}.btn-toolbar:hover,.btn-icon-header:hover{transform:none}.subagent-dropdown-trigger:active+.subagent-dropdown-menu,.subagent-dropdown-trigger:focus+.subagent-dropdown-menu{display:block}}@media(max-width:430px){.attachment-history-drawer{top:0;bottom:0;width:100%;max-width:100%;height:100vh;height:100dvh;border-left:none;border-radius:0;padding-top:var(--safe-area-top);padding-left:var(--safe-area-left);padding-right:var(--safe-area-right);padding-bottom:var(--safe-area-bottom)}.attachment-history-header{padding:12px}.attachment-history-list{padding:6px}.attachment-history-item{grid-template-columns:104px minmax(0,1fr);gap:8px;padding:8px 6px}.attachment-history-thumb{width:104px}.attachment-history-actions{gap:4px}.attachment-history-actions button{padding:4px 7px}}
@@ -330,8 +330,10 @@ class NotificationManager {
330
330
  if (now - this.lastBrowserNotifTime < BROWSER_NOTIF_RATE_LIMIT_MS) return;
331
331
  this.lastBrowserNotifTime = now;
332
332
 
333
- const notif = new Notification(`${this.originalTitle}: ${title}`, {
334
- body,
333
+ const localizedTitle = window.codemanT?.(title) || title;
334
+ const localizedBody = window.codemanT?.(body) || body;
335
+ const notif = new Notification(`${this.originalTitle}: ${localizedTitle}`, {
336
+ body: localizedBody,
335
337
  tag, // Groups same-tag notifications
336
338
  icon: '/favicon.ico',
337
339
  silent: true, // We handle audio ourselves
@@ -0,0 +1,68 @@
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.name} (${this.dockerCaseTag(e.docker?.hostId)})`:e?.name||""},dockerCaseTag(e){return!e||e==="default"||e==="local"||/^q-/.test(e)?"docker":e},buildCasePickerOptions(e=[]){const t=[],s=new Set;for(const 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 o=this.formatCasePickerLabel(a),n=[a.name,o,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:o,case:a,searchText:n}}).sort((a,o)=>a.label.localeCompare(o.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(o=>a.searchText.includes(o)))},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||""),o=s.value||"testcase",n=Math.max(0,a.length-1);if(this._casePickerActiveIndex=Math.min(Math.max(this._casePickerActiveIndex||0,0),n),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((i,r)=>{const c=r===this._casePickerActiveIndex,m=i.name===o;return`
2
+ <button
3
+ type="button"
4
+ id="${`quickStartCaseOption-${r}`}"
5
+ class="case-combobox-option ${c?"active":""} ${m?"selected":""}"
6
+ role="option"
7
+ aria-selected="${m?"true":"false"}"
8
+ data-case="${escapeHtml(i.name)}"
9
+ title="${escapeHtml(i.label)}">
10
+ <span class="case-combobox-check">${m?"\u2713":""}</span>
11
+ <span class="case-combobox-option-label">${escapeHtml(i.label)}</span>
12
+ </button>
13
+ `}).join(""),t.classList.remove("hidden"),e.setAttribute("aria-activedescendant",`quickStartCaseOption-${this._casePickerActiveIndex}`)},selectQuickStartCase(e,{save:t=!0}={}){const s=document.getElementById("quickStartCase");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",o=>{const n=this.filterCasePickerOptions(this.getCasePickerOptions(),this._casePickerFilter||t.value);if(o.key==="ArrowDown")o.preventDefault(),this._casePickerActiveIndex=Math.min((this._casePickerActiveIndex||0)+1,Math.max(0,n.length-1)),this._casePickerOpen?this.renderCasePickerList():this.openCasePicker(t.value);else if(o.key==="ArrowUp")o.preventDefault(),this._casePickerActiveIndex=Math.max((this._casePickerActiveIndex||0)-1,0),this._casePickerOpen?this.renderCasePickerList():this.openCasePicker(t.value);else if(o.key==="Enter"){const i=n[this._casePickerActiveIndex||0];i&&(o.preventDefault(),this.selectQuickStartCase(i.name),this.run?.())}else o.key==="Escape"?(o.preventDefault(),this.updateCasePickerInput(e.value),this.closeCasePicker()):o.key==="Tab"&&(this.updateCasePickerInput(e.value),this.closeCasePicker())}),s.addEventListener("mousedown",o=>o.preventDefault()),s.addEventListener("click",o=>{const n=o.target.closest?.(".case-combobox-option");n?.dataset?.case&&this.selectQuickStartCase(n.dataset.case)}),document.addEventListener&&!this._casePickerDocumentListenerAdded&&(document.addEventListener("pointerdown",o=>{a.contains(o.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 o=(await(await fetch("/api/cases")).json()).data;this.cases=o,console.log("[loadQuickStartCases] Loaded cases:",o.map(r=>r.name),"lastUsedCase:",s);const n=document.getElementById("quickStartCase"),i=this.getCasePickerOptions();if(this.renderQuickStartCaseSelectOptions(n,i),console.log("[loadQuickStartCases] Set options:",n.innerHTML.substring(0,200)),e)n.value=e,this.updateDirDisplayForCase(e),this.updateMobileCaseLabel(e);else if(s&&o.some(r=>r.name===s))n.value=s,this.updateDirDisplayForCase(s),this.updateMobileCaseLabel(s);else if(o.length>0){const r=o.find(c=>c.name==="testcase")||o[0];n.value=r.name,this.updateDirDisplayForCase(r.name),this.updateMobileCaseLabel(r.name)}else n.value="testcase",document.getElementById("dirDisplay").textContent="~/codeman-cases/testcase",this.updateMobileCaseLabel("testcase");this.updateCasePickerInput(n.value),this.renderCasePickerList(),this.closeCasePicker(),n.dataset.listenerAdded||(n.addEventListener("change",()=>{this.updateDirDisplayForCase(n.value),this.saveLastUsedCase(n.value),this.updateMobileCaseLabel(n.value),this.updateCasePickerInput(n.value)}),n.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 _ensureCreatedSessionVisible(e,t){if(!e)return;let s=t;if(!s&&!this.sessions?.has(e)){const o=await(await fetch(`/api/sessions/${encodeURIComponent(e)}`)).json();if(!o.success)throw new Error(o.error||"Failed to load the new session");s=o.data?.session||o.data}s?.id&&this._onSessionCreated(s),this._renderSessionTabsImmediate?.()},async run(){if(this._runInFlight)return;const e=Date.now(),t=Number.isFinite(this._runMinLockMs)?this._runMinLockMs:500,s=document.getElementById("runBtn");this._runInFlight=!0,s&&(s.disabled=!0,s.setAttribute("aria-busy","true"));try{const a=this._runMode||"claude";return a==="opencode"?await this.runOpenCode():a==="codex"?await this.runCodex():a==="gemini"?await this.runGemini():await this.runClaude()}finally{const a=t-(Date.now()-e);a>0&&await new Promise(o=>setTimeout(o,a)),this._runInFlight=!1,s&&(s.disabled=!1,s.removeAttribute("aria-busy"))}},setRunMode(e){this._runMode=e;try{localStorage.setItem("codeman_runMode",e)}catch{}this._applyRunMode(),this._apiPut("/api/settings",{runMode:e}).catch(()=>{}),document.getElementById("runModeMenu")?.classList.remove("active")},toggleRunModeMenu(e){e?.stopPropagation();const t=document.getElementById("runModeMenu");if(t&&(t.classList.toggle("active"),t.querySelectorAll(".run-mode-option").forEach(s=>{s.classList.toggle("selected",s.dataset.mode===this.runMode)}),t.classList.contains("active"))){this._loadRunModeHistory();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),o=a.toLocaleDateString("en",{month:"short",day:"numeric"})+" "+a.toLocaleTimeString("en",{hour:"2-digit",minute:"2-digit",hour12:!1}),n=s.workingDir.replace(/^\/home\/[^/]+\//,"~/"),i=document.createElement("button");i.className="run-mode-option",i.title=s.workingDir,i.dataset.sessionId=s.sessionId,i.dataset.workingDir=s.workingDir;const r=document.createElement("span");r.className="hist-dir",r.textContent=n;const c=document.createElement("span");c.className="hist-meta",c.textContent=o,i.append(r,c),i.addEventListener("click",m=>{m.stopPropagation(),this.resumeHistorySession(s.sessionId,s.workingDir,s.name)}),e.appendChild(i)}}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)},_nextCaseSessionStartNumber(e,t="w"){const s=new RegExp(`^${t}(\\d+)-([a-zA-Z0-9_-]+)`);let a=1;for(const[,o]of this.sessions||[]){const n=o.name&&o.name.match(s);if(n&&n[2]===e){const i=parseInt(n[1]);i>=a&&(a=i+1)}}return a},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 o=a.path;if(!o)throw new Error("Case path not found");if(a.location==="remote"||a.location==="docker"){const f=this._nextCaseSessionStartNumber(e);let C;if(a.location==="docker"){const w=this.loadAppSettingsFromStorage(),S=this.getCaseSettings(e).opusContext1m||w.opusContext1mEnabled;C=w.claudeModel||(S?"opus[1m]":"")}const E=[];let I=!1;for(let w=0;w<t;w++){const x=JSON.stringify({caseName:e,mode:"claude",sessionName:`w${f+w}-${e}`,...C!==void 0?{modelOverride:C}:{}}),S=async()=>(await fetch("/api/quick-start",{method:"POST",headers:{"Content-Type":"application/json"},body:x})).json();let b=await S();if(!b.success&&b.errorCode==="CONFLICT"&&a.location==="docker"&&!I&&(I=!0,confirm(`Container config for "${e}" changed since its container was created.
14
+
15
+ Recreate the container to apply the new config? Workspace files and the conversation survive (the conversation resumes on launch).`))){const T=await(await fetch(`/api/docker-cases/${encodeURIComponent(e)}/recreate`,{method:"POST"})).json();if(!T.success)throw new Error(T.error||"Failed to recreate container");b=await S()}if(!b.success)throw new Error(b.error||"Failed to start remote Claude session");await this._ensureCreatedSessionVisible(b.data.sessionId,b.data.session),E.push(b.data.sessionId)}this.terminal.writeln(`\x1B[90m All ${t} remote session(s) ready\x1B[0m`),E[0]&&(await this.selectSession(E[0]),this.loadQuickStartCases()),this.terminal.focus();return}let n=null;const i=this._nextCaseSessionStartNumber(e),r=this.isRalphTrackerEnabledByDefault(),c=[];for(let f=0;f<t;f++)c.push(`w${i+f}-${e}`);const m=this.getCaseSettings(e),l=this.loadAppSettingsFromStorage(),u=this.buildEnvOverrides(m,l),p=Object.keys(u).length>0,d=this.getEffortSetting(l),h=m.opusContext1m||l.opusContext1mEnabled,y=l.claudeModel||(h?"opus[1m]":"");this.terminal.writeln(`\x1B[90m Creating ${t} session(s)...\x1B[0m`);const g=c.map(f=>fetch("/api/sessions",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({workingDir:o,name:f,...p?{envOverrides:u}:{},...d?{effort:d}:{},...y!==void 0?{modelOverride:y}:{},statusLineTelemetry:l.showPlanUsageLimits===!0})}).then(C=>C.json())),k=await Promise.all(g),v=[];for(const f of k){if(!f.success)throw new Error(f.error);await this._ensureCreatedSessionVisible(f.data.session.id,f.data.session),v.push(f.data.session.id)}n=v[0],await Promise.all(v.map(f=>fetch(`/api/sessions/${f}/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(v.map(f=>fetch(`/api/sessions/${f}/interactive`,{method:"POST"}))),this.terminal.writeln(`\x1B[90m All ${t} sessions ready\x1B[0m`),n&&(await this.selectSession(n),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 h=await(await fetch("/api/cases",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,description:""})})).json();if(!h.success)throw new Error(h.error||"Failed to create case");a=h.data.case}const o=(this.cases||[]).find(d=>d.name===e),n=a.location==="remote"||a.location==="docker"||o?.location==="remote"||o?.location==="docker",i=a.path;if(!i)throw new Error("Case path not found");if(a.location==="remote"||a.location==="docker"){const d=this._nextCaseSessionStartNumber(e,"s"),h=[];for(let y=0;y<t;y++){const k=await(await fetch("/api/quick-start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({caseName:e,mode:"shell",sessionName:`s${d+y}-${e}`})})).json();if(!k.success)throw new Error(k.error||"Failed to start remote shell session");await this._ensureCreatedSessionVisible(k.data.sessionId,k.data.session),h.push(k.data.sessionId)}h[0]&&await this.selectSession(h[0]),this.terminal.focus();return}const r=this._nextCaseSessionStartNumber(e,"s"),c=[];for(let d=0;d<t;d++)c.push(`s${r+d}-${e}`);const m=c.map(d=>fetch("/api/sessions",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({...n?{caseName:e}:{workingDir:i},mode:"shell",name:d})}).then(h=>h.json())),l=await Promise.all(m),u=[];for(const d of l){if(!d.success)throw new Error(d.error);await this._ensureCreatedSessionVisible(d.data.session.id,d.data.session),u.push(d.data.session.id)}await Promise.all(u.map(d=>fetch(`/api/sessions/${d}/shell`,{method:"POST"})));const p=this.getTerminalDimensions();p&&await Promise.all(u.map(d=>fetch(`/api/sessions/${d}/resize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p)}))),u.length>0&&await this.selectSession(u[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()),n=await(await fetch("/api/quick-start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({caseName:e,mode:"opencode",sessionName:`w${this._nextCaseSessionStartNumber(e)}-${e}`,...s?{}:{openCodeConfig:{autoAllowTools:!0},...Object.keys(a).length>0?{envOverrides:a}:{}}})})).json();if(!n.success)throw new Error(n.error||"Failed to start OpenCode");await this._ensureCreatedSessionVisible(n.data.sessionId,n.data.session),n.data.sessionId&&await this.selectSession(n.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(),o=this.buildEnvOverrides(this.getCaseSettings(e),a),i=await(await fetch("/api/quick-start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({caseName:e,mode:"codex",sessionName:`w${this._nextCaseSessionStartNumber(e)}-${e}`,...s?{}:{codexConfig:{dangerouslyBypassApprovals:a.codexDangerouslyBypassApprovals??!1,renderMode:"hybrid"},...Object.keys(o).length>0?{envOverrides:o}:{}}})})).json();if(!i.success)throw new Error(i.error||"Failed to start Codex");await this._ensureCreatedSessionVisible(i.data.sessionId,i.data.session),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 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()),n=await(await fetch("/api/quick-start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({caseName:e,mode:"gemini",sessionName:`w${this._nextCaseSessionStartNumber(e)}-${e}`,...s?{}:{geminiConfig:{approvalMode:"yolo"},...Object.keys(a).length>0?{envOverrides:a}:{}}})})).json();if(!n.success)throw new Error(n.error||"Failed to start Gemini");await this._ensureCreatedSessionVisible(n.data.sessionId,n.data.session),n.data.sessionId&&await this.selectSession(n.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"),o=document.getElementById("modalEnableRespawnBtn"),n=document.getElementById("modalStopRespawnBtn");this.respawnStatus[e]?(a.classList.add("active"),a.querySelector(".respawn-status-text").textContent=this.respawnStatus[e].state||"Active",o.style.display="none",n.style.display=""):(a.classList.remove("active"),a.querySelector(".respawn-status-text").textContent="Not active",o.style.display="",n.style.display="none");const i=document.getElementById("sessionRespawnSection");t.mode==="claude"&&t.pid?i.style.display="":i.style.display="none";const r=t.mode==="opencode"||t.mode==="codex"||t.mode==="gemini";document.querySelectorAll("[data-claude-only]").forEach(g=>{g.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),l=document.getElementById("modalSessionPrefix");m?(l.textContent=m.prefix+": ",l.style.display="",document.getElementById("modalSessionName").value=m.suffix,document.getElementById("modalSessionName").placeholder="Add description..."):(l.style.display="none",l.textContent="",document.getElementById("modalSessionName").value=t.name||"",document.getElementById("modalSessionName").placeholder="Auto (directory name)");const u=t.color||"default";document.getElementById("sessionColorPicker")?.querySelectorAll(".color-swatch").forEach(g=>{g.classList.toggle("selected",g.dataset.color===u)}),this.renderPresetDropdown(),document.getElementById("respawnPresetSelect").value="",document.getElementById("presetDescriptionHint").textContent="";const d=document.querySelector('#sessionOptionsModal .modal-tab-btn[data-tab="ralph"]'),h=document.querySelector('#sessionOptionsModal .modal-tab-btn[data-tab="respawn"]');if(r?(d&&(d.style.display="none"),h&&(h.style.display="none"),this.switchOptionsTab("context")):(d&&(d.style.display=""),h&&(h.style.display="")),!r){const g=this.ralphStates.get(e);this.populateRalphForm({enabled:g?.loop?.enabled??t.ralphLoop?.enabled??!1,completionPhrase:g?.loop?.completionPhrase||t.ralphLoop?.completionPhrase||"",maxIterations:g?.loop?.maxIterations||t.ralphLoop?.maxIterations||0,maxTodos:g?.loop?.maxTodos||t.ralphLoop?.maxTodos,todoExpirationMinutes:g?.loop?.todoExpirationMinutes||t.ralphLoop?.todoExpirationMinutes})}const y=document.getElementById("sessionOptionsModal");y.classList.add("active"),this.activeFocusTrap=new FocusTrap(y),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(o){this.showToast("Failed to save session name: "+o.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(o=>o.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),o=parseSessionPrefix(t.name),n=s.textContent;for(s.textContent="";s.firstChild;)s.removeChild(s.firstChild);if(o){const c=document.createElement("span");c.textContent=o.prefix+": ",c.style.cssText="color: var(--text-muted); font-size: 0.75rem; white-space: nowrap;",s.appendChild(c)}const i=document.createElement("input");i.type="text",i.value=o?o.suffix:t.name||"",i.placeholder=o?"Add description...":a,i.className="tab-rename-input",i.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(i),i.focus(),i.select();const r=async({commit:c})=>{if(!this._inlineRenameActive)return;if(this._inlineRenameActive=!1,this._activeRename=null,!c){this.renderSessionTabs();return}const m=i.value.trim(),l=o?o.prefix+(m?": "+m:""):m;if(s.textContent=l||n,this.sessions.has(e)&&l!==t.name)try{await fetch(`/api/sessions/${e}/name`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:l})})}catch{s.textContent=n,this.showToast("Failed to rename","error")}this.renderSessionTabs()};this._activeRename={sessionId:e,cancel:()=>r({commit:!1})},i.addEventListener("blur",()=>r({commit:!0})),i.addEventListener("keydown",c=>{c.isComposing||c.keyCode===229||(c.key==="Enter"?(c.preventDefault(),i.blur()):c.key==="Escape"&&(i.value="",i.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=o=>{!e.contains(o.target)&&!o.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=o=>{!e.contains(o.target)&&!o.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",o=s?{name:e,description:t,...this._collectDockerQuickSettings()}:{name:e,description:t};try{const i=await(await fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();if(i.success)if(this.closeCreateCaseModal(),await this.loadQuickStartCases(e),await this.saveLastUsedCase(e),s){const r=i.data?.capsEnforced===!1?" (resource caps advisory on this engine)":"";this.showToast(`Docker case "${e}" created${r} \u2014 starting session\u2026`,"success"),await this.runClaude()}else this.showToast(`Case "${e}" created`,"success");else this.showToast(i.error||"Failed to create case","error")}catch(n){console.error("Failed to create case:",n),this.showToast("Failed to create case: "+n.message,"error")}},applyDockerTemplate(){const e=document.getElementById("quickDockerTemplate")?.value,s={small:{m:"2g",c:"1",g:""},medium:{m:"",c:"",g:""},large:{m:"8g",c:"4",g:""},gpu:{m:"8g",c:"4",g:"all"}}[e];if(!s)return;const a=(o,n)=>{const i=document.getElementById(o);i&&(i.value=n)};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 o=e("quickDockerGpus");o&&o.toLowerCase()!=="none"&&(t.gpus=o);const n=document.getElementById("quickDockerNetwork")?.value;n&&n!=="bridge"&&(t.network=n);const i=e("quickDockerImage");i&&(t.image=i);const r=document.getElementById("quickDockerMountCreds");return r&&!r.checked&&(t.mountCredentials=!1),t},async linkCase(){const e=document.getElementById("linkCaseName").value.trim(),t=document.getElementById("linkCasePath").value.trim();if(!e){this.showToast("Please enter a case name","error");return}if(!/^[a-zA-Z0-9_-]+$/.test(e)){this.showToast("Invalid name. Use only letters, numbers, hyphens, underscores.","error");return}if(!t){this.showToast("Please enter a folder path","error");return}try{const 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(),o=document.getElementById("remoteHostUsername").value.trim(),n=document.getElementById("remoteHostCodexCommand").value.trim(),i=document.getElementById("remoteHostPort").value.trim(),r=document.getElementById("remoteHostIdentityFile").value.trim(),c=document.getElementById("remoteHostSocksProxy").value.trim(),m=document.getElementById("remoteHostJumpHost").value.trim(),l=document.getElementById("remoteHostExtraSshOptions").value.split(`
16
+ `).map(p=>p.trim()).filter(p=>p.length>0);if(!e||!t||!s||!a||!o){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 u;if(i&&(u=Number(i),!Number.isInteger(u)||u<1||u>65535)){this.showToast("SSH port must be a number between 1 and 65535","error");return}try{const p={id:s,label:s,host:a,username:o,...u?{port:u}:{},...r?{identityFile:r}:{},...c?{socksProxy:c}:{},...m?{jumpHost:m}:{},...l.length?{extraSshOptions:l}:{},...n?{commands:{codex:n}}:{}},h=await(await fetch("/api/remote-hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p)})).json();if(!h.success&&h.errorCode!=="ALREADY_EXISTS")throw new Error(h.error||"Failed to save remote host");const g=await(await fetch("/api/cases/remote-link",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,hostId:s,remotePath:t})})).json();g.success?(this.closeCreateCaseModal(),this.showToast(`Remote case "${e}" linked`,"success"),await this.loadQuickStartCases(e),await this.saveLastUsedCase(e)):this.showToast(g.error||"Failed to link remote case","error")}catch(p){console.error("Failed to link remote case:",p),this.showToast("Failed to link remote case: "+p.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",o=document.getElementById("dockerNetwork").value,n=document.getElementById("dockerMemory").value.trim(),i=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 l={};n&&(l.memory=n),i&&(l.cpus=i);const u={id:s,label:s,image:a,network:o,mountCredentials:r,resumeOnStart:c,...Object.keys(l).length?{resources:l}:{}};let p=await fetch("/api/docker-hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u)}),d=await p.json();if(!d.success&&d.errorCode==="ALREADY_EXISTS"&&(p=await fetch(`/api/docker-hosts/${encodeURIComponent(s)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(u)}),d=await p.json()),!d.success)throw new Error(d.error||"Failed to save docker host");const y=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(y.success){this.closeCreateCaseModal();const g=y.data?.capsEnforced===!1?" (resource caps are advisory on this engine)":"";this.showToast(`Docker case "${e}" linked${g}`,"success"),await this.loadQuickStartCases(e),await this.saveLastUsedCase(e)}else m&&(m.textContent=y.error||"Failed to link docker case"),this.showToast(y.error||"Failed to link docker case","error")}catch(l){console.error("Failed to link docker case:",l),m&&(m.textContent=l.message),this.showToast("Failed to link docker case: "+l.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(o=>{const n=(o.sizeBytes/1e6).toFixed(1),i=escapeHtml(o.name);return`<div class="case-manage-item" style="display:flex; align-items:center; gap:8px; justify-content:space-between;">
17
+ <span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${i}">${i} <span class="form-hint">(${n} MB)</span></span>
18
+ <span style="flex-shrink:0;">
19
+ <a class="btn-toolbar" href="/api/docker-exports/${encodeURIComponent(o.name)}" download>Download</a>
20
+ <button class="btn-toolbar" onclick="app.importDockerBundle('${i.replace(/'/g,"\\'")}')">Import</button>
21
+ <button class="btn-toolbar" onclick="app.deleteDockerExport('${i.replace(/'/g,"\\'")}')">Delete</button>
22
+ </span>
23
+ </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 o=await(await fetch("/api/docker-cases/import",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({bundle:e,newCaseName:t,destWorkspacePath:s})})).json();o.success?(this.showToast(`Imported as "${t}"`,"success"),await this.loadQuickStartCases(t)):this.showToast(o.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")}},_readRemoteHostFromForm(){const e=document.getElementById("remoteHostId").value.trim(),t=document.getElementById("remoteHostAddress").value.trim(),s=document.getElementById("remoteHostUsername").value.trim(),a=document.getElementById("remoteHostPort").value.trim(),o=document.getElementById("remoteHostIdentityFile").value.trim(),n=document.getElementById("remoteHostSocksProxy").value.trim(),i=document.getElementById("remoteHostJumpHost").value.trim(),r=document.getElementById("remoteHostCodexCommand").value.trim(),c=document.getElementById("remoteHostExtraSshOptions").value.split(`
24
+ `).map(l=>l.trim()).filter(l=>l.length>0);let m;if(a){const l=Number(a);Number.isInteger(l)&&l>=1&&l<=65535&&(m=l)}return{id:e,label:e,host:t,username:s,...m?{port:m}:{},...o?{identityFile:o}:{},...n?{socksProxy:n}:{},...i?{jumpHost:i}:{},...c.length?{extraSshOptions:c}:{},...r?{commands:{codex:r}}:{}}},async discoverRemoteSessions(){const e=document.getElementById("remoteDiscoverResults"),t=document.getElementById("remoteDiscoverBtn"),s=this._readRemoteHostFromForm();if(!s.id||!s.host||!s.username){this.showToast("Fill in Host ID, address, and username first","error");return}if(!/^[a-zA-Z0-9_-]+$/.test(s.id)){this.showToast("Invalid Host ID. Use letters, numbers, hyphens, underscores.","error");return}t&&(t.disabled=!0),e&&(e.innerHTML='<div class="form-hint">Discovering\u2026</div>');try{const o=await(await fetch("/api/remote-hosts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();if(!o.success&&o.errorCode!=="ALREADY_EXISTS")throw new Error(o.error||"Failed to save remote host");const i=await(await fetch(`/api/remote-hosts/${encodeURIComponent(s.id)}/sessions`)).json();if(!i.success)throw new Error(i.error||"Discovery failed");this._renderDiscoveredSessions(s.id,i.data.sessions||[])}catch(a){console.error("Discover remote sessions failed:",a),e&&(e.innerHTML=`<div class="form-hint" style="color: var(--error, #e06c75);">${escapeHtml(a.message)}</div>`)}finally{t&&(t.disabled=!1)}},_renderDiscoveredSessions(e,t){const s=document.getElementById("remoteDiscoverResults");if(!s)return;if(!t.length){s.innerHTML='<div class="form-hint">No <code>codeman-*</code> sessions running on this host (or it is unreachable).</div>';return}const a=Math.floor(Date.now()/1e3),o=t.map(n=>{const i=Math.max(0,a-(n.created||0)),r=i<3600?`${Math.floor(i/60)}m`:i<86400?`${Math.floor(i/3600)}h`:`${Math.floor(i/86400)}d`,c=n.attachedClients!=null?n.attachedClients:n.attached?1:0,m=c>1?`<span class="case-location-badge" style="background: var(--warning, #e5c07b); color: #000;">shared \xB7 ${c} clients</span>`:c===1?'<span class="case-location-badge" style="background: var(--accent, #61afef);">attached</span>':"";return`
25
+ <div class="remote-discover-item">
26
+ <div class="remote-discover-info">
27
+ <span class="remote-discover-name">${escapeHtml(n.name)} ${m}</span>
28
+ <span class="form-hint">age ${r} \xB7 ${n.windows||1} window(s)</span>
29
+ </div>
30
+ <button type="button" class="btn-toolbar" onclick="app.attachDiscoveredSession('${escapeHtml(e)}', '${escapeHtml(n.name)}')">Attach</button>
31
+ </div>`}).join("");s.innerHTML=o},async attachDiscoveredSession(e,t){try{const a=await(await fetch("/api/sessions",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({mode:"shell",name:t,attachRemoteSession:{hostId:e,remoteSessionName:t}})})).json();if(!a.success)throw new Error(a.error||"Failed to create session");const o=a.data.session.id;await fetch(`/api/sessions/${o}/shell`,{method:"POST"});const n=this.getTerminalDimensions();n&&await fetch(`/api/sessions/${o}/resize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}),this.closeCreateCaseModal(),this.showToast(`Attached to ${t} (detach on close)`,"success"),this.activeSessionId=o,await this.selectSession(o),this.terminal&&typeof this.terminal.focus=="function"&&this.terminal.focus()}catch(s){console.error("Attach discovered session failed:",s),this.showToast("Failed to attach: "+s.message,"error")}},renderCaseManageList(){const e=document.getElementById("caseManageList"),t=this.cases||[];if(t.length===0){e.innerHTML='<div class="form-hint" style="text-align: center; padding: 2rem 0;">No cases yet</div>';return}let s="";t.forEach((a,o)=>{const n=o===0,i=o===t.length-1,r=a.path?a.path.replace(/^\/Users\/[^/]+/,"~"):"";s+=`
32
+ <div class="case-manage-item" data-case="${escapeHtml(a.name)}">
33
+ <div class="case-manage-info">
34
+ <span class="case-manage-name">${escapeHtml(a.name)}</span>
35
+ <span class="case-manage-path">${escapeHtml(r)}</span>
36
+ </div>
37
+ <div class="case-manage-actions">
38
+ ${a.location==="docker"?`<button class="case-manage-btn" onclick="app.exportDockerCaseBundle(${escapeHtml(JSON.stringify(a.name))}, 'full')"
39
+ title="Export container (full image + workspace) to move to another machine">&#x1F4E6;</button>`:""}
40
+ <button class="case-manage-btn" onclick="app.moveCaseUp(${escapeHtml(JSON.stringify(a.name))})"
41
+ title="Move up" ${n?"disabled":""}>&#x25B2;</button>
42
+ <button class="case-manage-btn" onclick="app.moveCaseDown(${escapeHtml(JSON.stringify(a.name))})"
43
+ title="Move down" ${i?"disabled":""}>&#x25BC;</button>
44
+ <button class="case-manage-btn case-manage-btn-delete" onclick="app.deleteCase(${escapeHtml(JSON.stringify(a.name))})"
45
+ title="Delete case">&#x2715;</button>
46
+ </div>
47
+ </div>
48
+ `}),e.innerHTML=s},async moveCaseUp(e){const t=this.cases||[],s=t.findIndex(o=>o.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(o=>o.name))},async moveCaseDown(e){const t=this.cases||[],s=t.findIndex(o=>o.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(o=>o.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(n=>n.name!==e),this.renderCaseManageList();const a=document.getElementById("quickStartCase"),o=a.value;o===e&&a.blur?.(),await this.loadQuickStartCases(o===e?null:o),o===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 o="";const n=this.getCasePickerOptions();for(const i of n){const r=i.name===a;o+=`
49
+ <button class="mobile-case-item ${r?"selected":""}"
50
+ onclick="app.selectMobileCase(${escapeHtml(JSON.stringify(i.name))})">
51
+ <span class="mobile-case-item-icon">
52
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
53
+ <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"/>
54
+ </svg>
55
+ </span>
56
+ <span class="mobile-case-item-name">${escapeHtml(i.label)}</span>
57
+ <span class="mobile-case-item-delete" onclick="event.stopPropagation(); app.deleteCaseMobile(${escapeHtml(JSON.stringify(i.name))})" title="Delete">
58
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
59
+ <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
60
+ </svg>
61
+ </span>
62
+ <span class="mobile-case-item-check">
63
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
64
+ <polyline points="20 6 9 17 4 12"/>
65
+ </svg>
66
+ </span>
67
+ </button>
68
+ `}t.innerHTML=o,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"}});