glad-web 1.0.19 → 1.0.21

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.
@@ -77,6 +77,104 @@
77
77
  .key-btn.active { background: rgba(0, 122, 255, 0.25); border-color: #007aff; color: #00a2ff; }
78
78
  #terminal-container { flex: 1; min-height: 0; width: 100%; background: #000; position: relative; overflow: hidden; overflow-anchor: none; }
79
79
  #terminal { height: 100%; width: 100%; }
80
+ #claude-chat-container { display: none; flex: 1; min-height: 0; overflow-y: auto; background: #050505; padding: 12px 12px 24px 12px; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
81
+ .claude-context-size-badge { position: sticky; top: 0; z-index: 2; width: max-content; max-width: 100%; margin: 0 0 8px auto; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.94); border-radius: 999px; padding: 4px 9px; color: #d1d5db; font-size: 11px; font-weight: 800; box-shadow: 0 8px 18px rgba(0,0,0,0.24); }
82
+ .claude-message { max-width: 92%; margin: 0 0 10px 0; padding: 10px 12px; border-radius: 8px; overflow-wrap: anywhere; line-height: 1.45; font-size: 14px; }
83
+ .claude-message.user { margin-left: auto; background: rgba(0,122,255,0.24); border: 1px solid rgba(0,122,255,0.32); color: #fff; border-radius: 12px; padding-top: 7px; padding-bottom: 7px; }
84
+ .claude-message.assistant { max-width: 100%; background: transparent; border: 0; color: #f5f5f7; padding: 0 4px; margin: 0 0 12px 0; }
85
+ .claude-message.event { max-width: 100%; background: transparent; border: 0; color: var(--text-dim); font-size: 12px; padding: 3px 2px; text-align: center; }
86
+ .claude-message.error { color: #ff6b61; }
87
+ .claude-md { white-space: normal; }
88
+ .claude-md > :first-child { margin-top: 0; }
89
+ .claude-md > :last-child { margin-bottom: 0; }
90
+ .claude-md p { margin: 0 0 8px 0; }
91
+ .claude-md h1, .claude-md h2, .claude-md h3, .claude-md h4 { margin: 12px 0 7px 0; line-height: 1.22; font-weight: 800; letter-spacing: 0; }
92
+ .claude-md h1 { font-size: 20px; }
93
+ .claude-md h2 { font-size: 18px; }
94
+ .claude-md h3 { font-size: 16px; }
95
+ .claude-md h4 { font-size: 15px; }
96
+ .claude-md ul, .claude-md ol { margin: 0 0 9px 0; padding-left: 20px; }
97
+ .claude-md li { margin: 4px 0; }
98
+ .claude-md code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.92em; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; padding: 1px 4px; }
99
+ .claude-md pre { margin: 8px 0 10px 0; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.34); border-radius: 8px; overflow: auto; }
100
+ .claude-md pre code { display: block; padding: 11px 12px; background: transparent; border: 0; border-radius: 0; white-space: pre; line-height: 1.45; }
101
+ .claude-md blockquote { margin: 8px 0; padding: 2px 0 2px 10px; border-left: 3px solid rgba(255,255,255,0.22); color: #c7c7cc; }
102
+ .claude-md table { width: 100%; border-collapse: collapse; margin: 8px 0 10px 0; font-size: 13px; display: block; overflow-x: auto; }
103
+ .claude-md th, .claude-md td { border: 1px solid rgba(255,255,255,0.12); padding: 6px 8px; text-align: left; vertical-align: top; }
104
+ .claude-md th { background: rgba(255,255,255,0.07); color: #fff; }
105
+ .claude-md a { color: #64b5ff; text-decoration: none; }
106
+ .claude-md img { max-width: 100%; border-radius: 8px; margin: 6px 0; }
107
+ .claude-md hr { border: 0; border-top: 1px solid rgba(255,255,255,0.12); margin: 12px 0; }
108
+ .claude-command-chip { display: inline-flex; align-items: center; max-width: 100%; border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.08); color: #f5f5f7; border-radius: 999px; padding: 4px 9px; font-size: 12px; font-weight: 800; }
109
+ .claude-tool { max-width: 100%; margin: 0 0 10px 0; border: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.045); border-radius: 8px; color: #d1d5db; font-size: 13px; overflow: hidden; overflow-wrap: anywhere; }
110
+ .claude-tool strong { color: #fff; }
111
+ .claude-tool-card { display: block; }
112
+ .claude-tool-card summary { list-style: none; cursor: pointer; }
113
+ .claude-tool-card summary::-webkit-details-marker { display: none; }
114
+ .claude-tool-header { display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 7px 9px; color: #f5f5f7; }
115
+ .claude-tool-icon { width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; color: var(--text-dim); flex: 0 0 auto; }
116
+ .claude-tool-icon::before { content: attr(data-icon); font-size: 11px; font-weight: 800; line-height: 1; }
117
+ .claude-tool-title { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; }
118
+ .claude-tool-status { color: var(--text-dim); font-size: 12px; white-space: nowrap; }
119
+ .claude-tool-command { flex: 1; min-width: 0; color: var(--text-dim); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
120
+ .claude-tool-body { padding: 0 10px 10px 10px; }
121
+ .claude-tool-section { margin-top: 8px; }
122
+ .claude-tool-section-title { color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 5px; }
123
+ .claude-tool-code { margin: 0; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.32); border-radius: 7px; padding: 9px 10px; color: #d1d5db; white-space: pre-wrap; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.42; overflow-x: auto; }
124
+ .claude-tool.error { border-color: rgba(255,107,97,0.38); background: rgba(255,59,48,0.07); }
125
+ .claude-work-group { max-width: 100%; margin: 0 0 10px 0; border: 1px solid rgba(255,255,255,0.09); background: rgba(255,255,255,0.035); border-radius: 8px; overflow: hidden; }
126
+ .claude-work-group summary { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 7px 10px; cursor: pointer; color: #d1d5db; list-style: none; }
127
+ .claude-work-group summary::-webkit-details-marker { display: none; }
128
+ .claude-work-group-title { flex: 1; min-width: 0; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
129
+ .claude-work-group-body { padding: 2px 8px 8px 8px; }
130
+ .claude-permission { border-color: rgba(255,204,0,0.4); background: rgba(255,204,0,0.08); }
131
+ .claude-permission-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; flex-wrap: wrap; }
132
+ .claude-permission-actions .small-btn { min-height: 30px; max-width: 100%; white-space: normal; line-height: 1.2; }
133
+ .claude-status { color: var(--text-dim); font-size: 12px; text-align: center; padding: 6px 0 0 0; }
134
+ #claude-control-panel { display: none; padding: 0 14px 10px 14px; box-sizing: border-box; }
135
+ .claude-control-row { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 7px; }
136
+ .claude-select, .claude-ctrl-btn { min-width: 0; height: 32px; border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; background: rgba(255,255,255,0.08); color: #f5f5f7; font-size: 11px; font-weight: 800; letter-spacing: 0; padding: 0 10px; outline: none; box-sizing: border-box; }
137
+ .claude-select { appearance: none; -webkit-appearance: none; text-align: center; text-align-last: center; }
138
+ .claude-picker-wrap { min-width: 0; position: relative; }
139
+ .claude-picker-wrap select { display: none; }
140
+ .claude-picker-btn { width: 100%; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
141
+ .claude-picker-btn.active { background: rgba(0,122,255,0.22); border-color: rgba(0,122,255,0.5); }
142
+ #claude-picker-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.99); border-radius: 12px; max-height: min(280px, 42dvh); overflow-y: auto; box-shadow: 0 16px 36px rgba(0,0,0,0.34); }
143
+ #claude-picker-panel.active { display: block; }
144
+ #claude-usage-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.99); border-radius: 12px; padding: 10px 11px; box-shadow: 0 16px 36px rgba(0,0,0,0.34); color: #f5f5f7; }
145
+ #claude-usage-panel.active { display: block; }
146
+ .claude-usage-title { color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 9px; }
147
+ .claude-usage-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
148
+ .claude-usage-item { border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.045); border-radius: 8px; padding: 8px; min-width: 0; }
149
+ .claude-usage-label { color: var(--text-dim); font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
150
+ .claude-usage-value { margin-top: 4px; color: #f5f5f7; font-size: 13px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
151
+ .claude-picker-title { padding: 9px 11px 7px 11px; color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid rgba(255,255,255,0.06); }
152
+ .claude-picker-option { width: 100%; border: 0; border-bottom: 1px solid rgba(255,255,255,0.06); background: transparent; color: #f5f5f7; padding: 10px 11px; text-align: left; cursor: pointer; box-sizing: border-box; }
153
+ .claude-picker-option:last-child { border-bottom: 0; }
154
+ .claude-picker-option:active, .claude-picker-option.selected { background: rgba(0,122,255,0.18); }
155
+ .claude-picker-option-main { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 13px; font-weight: 800; }
156
+ .claude-picker-option-main .selected-label { color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; flex: 0 0 auto; }
157
+ .claude-picker-option-value { min-width: 0; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: #d1d5db; font-size: 12px; margin-top: 3px; line-height: 1.35; }
158
+ .claude-ctrl-btn { cursor: pointer; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap; }
159
+ .claude-ctrl-btn:active, .claude-select:focus { background: rgba(255,255,255,0.14); border-color: rgba(0,122,255,0.45); }
160
+ .claude-ctrl-btn.danger { color: #ff6b61; background: rgba(255,59,48,0.12); }
161
+ .claude-ctrl-btn.primary { color: #fff; background: rgba(0,122,255,0.32); border-color: rgba(0,122,255,0.45); }
162
+ .claude-ctrl-btn:disabled { opacity: 0.45; cursor: default; }
163
+ #claude-state-bar { display: flex; align-items: center; gap: 7px; min-height: 24px; margin-top: 8px; color: var(--text-dim); font-size: 11px; overflow-x: auto; scrollbar-width: none; white-space: nowrap; }
164
+ #claude-state-bar::-webkit-scrollbar { display: none; }
165
+ .claude-state-pill { display: inline-flex; align-items: center; gap: 4px; border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.045); border-radius: 999px; padding: 3px 8px; flex: 0 0 auto; }
166
+ .claude-state-pill.warn { color: #ffcc00; border-color: rgba(255,204,0,0.24); background: rgba(255,204,0,0.08); }
167
+ .claude-state-pill.perm { margin-left: auto; }
168
+ .claude-state-pill.perm.acceptEdits { color: #34c759; border-color: rgba(52,199,89,0.26); background: rgba(52,199,89,0.08); }
169
+ .claude-state-pill.perm.bypassPermissions { color: #ff9f0a; border-color: rgba(255,159,10,0.26); background: rgba(255,159,10,0.08); }
170
+ .claude-state-pill.perm.plan { color: #64d2ff; border-color: rgba(100,210,255,0.26); background: rgba(100,210,255,0.08); }
171
+ #claude-resume-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.98); border-radius: 12px; max-height: 180px; overflow-y: auto; }
172
+ #claude-resume-panel.active { display: block; }
173
+ .claude-resume-item { width: 100%; text-align: left; border: 0; border-bottom: 1px solid rgba(255,255,255,0.06); background: transparent; color: #fff; padding: 10px 11px; cursor: pointer; box-sizing: border-box; }
174
+ .claude-resume-item:last-child { border-bottom: 0; }
175
+ .claude-resume-item:active { background: rgba(255,255,255,0.08); }
176
+ .claude-resume-title { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; font-weight: 800; }
177
+ .claude-resume-meta { color: var(--text-dim); font-size: 11px; margin-top: 3px; overflow-wrap: anywhere; }
80
178
  #terminal .xterm, #terminal .xterm-viewport, #terminal .xterm-screen, #terminal .xterm-rows, #terminal .xterm-rows span { user-select: text !important; -webkit-user-select: text !important; -webkit-touch-callout: default; touch-action: pan-y; }
81
179
  #terminal .xterm-viewport { overflow-anchor: none; }
82
180
  #terminal .xterm-helper-textarea { user-select: none !important; -webkit-user-select: none !important; -webkit-touch-callout: none; }
@@ -187,6 +285,42 @@
187
285
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
188
286
  </button>
189
287
  </div>
288
+ <div id="claude-control-panel">
289
+ <div class="claude-control-row">
290
+ <div class="claude-picker-wrap">
291
+ <select id="claude-permission-select" class="claude-select" title="Permission mode" onchange="updateClaudeSettingsFromControls()">
292
+ <option value="default">Default</option>
293
+ <option value="acceptEdits">Accept edits</option>
294
+ <option value="plan">Plan mode</option>
295
+ <option value="bypassPermissions">Bypass</option>
296
+ </select>
297
+ <button id="claude-permission-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('permission')" title="Permission mode">P</button>
298
+ </div>
299
+ <div class="claude-picker-wrap">
300
+ <select id="claude-model-select" class="claude-select" title="Model" onchange="updateClaudeSettingsFromControls()">
301
+ <option value="default">M:Def</option>
302
+ </select>
303
+ <button id="claude-model-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('model')" title="Model">M</button>
304
+ </div>
305
+ <div class="claude-picker-wrap">
306
+ <select id="claude-effort-select" class="claude-select" title="Effort" onchange="updateClaudeSettingsFromControls()">
307
+ <option value="low">E:Low</option>
308
+ <option value="medium">E:Med</option>
309
+ <option value="high">E:High</option>
310
+ <option value="xhigh">E:XHi</option>
311
+ <option value="max">E:Max</option>
312
+ </select>
313
+ <button id="claude-effort-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('effort')" title="Effort">E</button>
314
+ </div>
315
+ <button id="claude-usage-btn" class="claude-ctrl-btn" onclick="toggleClaudeUsagePanel()" title="Usage">Usage</button>
316
+ <button id="claude-abort-btn" class="claude-ctrl-btn danger" onclick="abortClaudeSession()" title="Abort current Claude turn">Abort</button>
317
+ <button id="claude-resume-btn" class="claude-ctrl-btn primary" onclick="toggleClaudeResumePanel()" title="Choose a Claude session to resume">Resume</button>
318
+ </div>
319
+ <div id="claude-state-bar"></div>
320
+ <div id="claude-picker-panel"></div>
321
+ <div id="claude-usage-panel"></div>
322
+ <div id="claude-resume-panel"></div>
323
+ </div>
190
324
  <div id="timed-send-panel">
191
325
  <div class="timed-row">
192
326
  <select id="timed-hours" onchange="updateTimedSendPreview()" title="Hours"></select>
@@ -225,6 +359,7 @@
225
359
  </div>
226
360
  <div id="terminal-controls-spacer"></div>
227
361
  <div id="terminal-container"><div id="terminal"></div></div>
362
+ <div id="claude-chat-container"></div>
228
363
  </div>
229
364
 
230
365
  <!-- Plain Text History View -->
@@ -354,6 +489,26 @@
354
489
  let timedSendRefreshTimer = null;
355
490
  let timedTagTimer = null;
356
491
  let editingTimedInputId = null;
492
+ let claudeMessages = [];
493
+ let claudePendingPermissions = [];
494
+ let claudeStatus = 'idle';
495
+ let claudeRuntimeConfig = null;
496
+ let claudePickerOpen = null;
497
+ let claudeUsagePanelOpen = false;
498
+ const CLAUDE_CONTEXT_SIZE = 190000;
499
+ let claudeState = {
500
+ permissionMode: 'default',
501
+ model: 'default',
502
+ effort: 'medium',
503
+ contextRemaining: null,
504
+ latestUsage: null,
505
+ claudeSessionId: null,
506
+ resumeSessionId: null,
507
+ canAbort: false,
508
+ pendingPermissionCount: 0
509
+ };
510
+ let claudeResumePanelOpen = false;
511
+ let claudeResumeItemsLoaded = false;
357
512
  const modifiers = { ctrl: false };
358
513
 
359
514
  function log(msg) {
@@ -641,13 +796,802 @@
641
796
  }
642
797
  }
643
798
 
799
+ function isClaudeSession() {
800
+ return activeToolKey === 'claude-code';
801
+ }
802
+
803
+ function setClaudeModeEnabled(enabled) {
804
+ document.getElementById('terminal-container').style.display = enabled ? 'none' : '';
805
+ document.getElementById('claude-chat-container').style.display = enabled ? 'block' : 'none';
806
+ document.getElementById('claude-control-panel').style.display = enabled ? 'block' : 'none';
807
+ document.getElementById('timer-btn').style.display = '';
808
+ document.getElementById('shortcut-rail').style.display = enabled ? 'none' : '';
809
+ document.getElementById('scroll-controls').style.display = enabled ? 'none' : '';
810
+ document.getElementById('cmd-input').placeholder = enabled ? 'Message Claude...' : 'Type a message...';
811
+ updateTerminalControlsHeight();
812
+ }
813
+
814
+ function updateTerminalControlsHeight() {
815
+ const timedOpen = document.getElementById('timed-send-panel')?.classList.contains('active');
816
+ if (!isClaudeSession()) {
817
+ document.documentElement.style.setProperty('--terminal-controls-rest-height', timedOpen ? '252px' : '108px');
818
+ return;
819
+ }
820
+ const baseHeight = claudeResumePanelOpen ? 308 : ((claudePickerOpen || claudeUsagePanelOpen) ? 322 : 154);
821
+ document.documentElement.style.setProperty('--terminal-controls-rest-height', `${baseHeight + (timedOpen ? 144 : 0)}px`);
822
+ }
823
+
824
+ function shortModelLabel(value, resolved) {
825
+ const source = String(value || 'default');
826
+ const target = String(resolved || source);
827
+ const compact = target
828
+ .replace(/^claude-/, '')
829
+ .replace(/-20\d{6,}.*$/, '')
830
+ .slice(0, 9);
831
+ if (source === 'default') return 'M:Def';
832
+ if (source === 'env') return `M:${compact || 'Env'}`;
833
+ if (source === 'sonnet') return `M:${compact || 'Son'}`;
834
+ if (source === 'opus') return `M:${compact || 'Opus'}`;
835
+ if (source === 'haiku') return `M:${compact || 'Hai'}`;
836
+ return `M:${compact || source.slice(0, 9)}`;
837
+ }
838
+
839
+ function fullModelLabel(value, resolved, label) {
840
+ const source = String(value || 'default');
841
+ if (source === 'default') return 'Default';
842
+ if (source === 'env') return resolved ? `Environment (${resolved})` : 'Environment';
843
+ if (label && label !== value) return label;
844
+ return String(resolved || value || 'Model');
845
+ }
846
+
847
+ function permissionModeLabel(value) {
848
+ const labels = {
849
+ default: 'Default',
850
+ acceptEdits: 'Accept edits',
851
+ plan: 'Plan mode',
852
+ bypassPermissions: 'Bypass'
853
+ };
854
+ return labels[value] || String(value || 'Default');
855
+ }
856
+
857
+ function effortLabel(value) {
858
+ const labels = {
859
+ low: 'Low',
860
+ medium: 'Medium',
861
+ high: 'High',
862
+ xhigh: 'Extra high',
863
+ max: 'Max'
864
+ };
865
+ return labels[value] || String(value || 'Medium');
866
+ }
867
+
868
+ function pickerLabelFromOption(option) {
869
+ return option ? (option.textContent || option.value || '') : '';
870
+ }
871
+
872
+ function pickerFullValue(option) {
873
+ if (!option) return '';
874
+ return option.dataset.resolved || '';
875
+ }
876
+
877
+ function syncClaudePickerButtons() {
878
+ const mappings = [
879
+ ['permission', 'claude-permission-select', 'claude-permission-picker-btn', 'Perm'],
880
+ ['model', 'claude-model-select', 'claude-model-picker-btn', 'Model'],
881
+ ['effort', 'claude-effort-select', 'claude-effort-picker-btn', 'Effort']
882
+ ];
883
+ mappings.forEach(([type, selectId, buttonId, prefix]) => {
884
+ const select = document.getElementById(selectId);
885
+ const button = document.getElementById(buttonId);
886
+ if (!select || !button) return;
887
+ const option = select.selectedOptions && select.selectedOptions[0];
888
+ const label = pickerLabelFromOption(option);
889
+ const fullValue = pickerFullValue(option);
890
+ button.textContent = prefix;
891
+ button.title = fullValue || label || prefix;
892
+ button.classList.toggle('active', claudePickerOpen === type);
893
+ });
894
+ }
895
+
896
+ function closeClaudePicker() {
897
+ claudePickerOpen = null;
898
+ const panel = document.getElementById('claude-picker-panel');
899
+ if (panel) {
900
+ panel.classList.remove('active');
901
+ panel.innerHTML = '';
902
+ }
903
+ syncClaudePickerButtons();
904
+ updateTerminalControlsHeight();
905
+ }
906
+
907
+ function closeClaudeUsagePanel() {
908
+ claudeUsagePanelOpen = false;
909
+ const panel = document.getElementById('claude-usage-panel');
910
+ if (panel) panel.classList.remove('active');
911
+ updateTerminalControlsHeight();
912
+ }
913
+
914
+ function pickerTitle(type) {
915
+ if (type === 'permission') return 'Permission mode';
916
+ if (type === 'model') return 'Model';
917
+ if (type === 'effort') return 'Effort';
918
+ return 'Options';
919
+ }
920
+
921
+ function selectIdForPicker(type) {
922
+ if (type === 'permission') return 'claude-permission-select';
923
+ if (type === 'model') return 'claude-model-select';
924
+ if (type === 'effort') return 'claude-effort-select';
925
+ return '';
926
+ }
927
+
928
+ function renderClaudePicker(type) {
929
+ const panel = document.getElementById('claude-picker-panel');
930
+ const select = document.getElementById(selectIdForPicker(type));
931
+ if (!panel || !select) return;
932
+ const options = Array.from(select.options);
933
+ panel.innerHTML = `<div class="claude-picker-title">${escapeHtml(pickerTitle(type))}</div>${options.map(option => {
934
+ const selected = option.value === select.value;
935
+ const full = pickerFullValue(option);
936
+ return `<button class="claude-picker-option${selected ? ' selected' : ''}" onclick="chooseClaudePickerOption('${type}', decodePathValue('${encodePathValue(option.value)}'))">
937
+ <div class="claude-picker-option-main">
938
+ <span>${escapeHtml(option.textContent || option.value)}</span>
939
+ ${selected ? '<span class="selected-label">Selected</span>' : ''}
940
+ </div>
941
+ ${full ? `<div class="claude-picker-option-value">${escapeHtml(full)}</div>` : ''}
942
+ </button>`;
943
+ }).join('')}`;
944
+ panel.classList.add('active');
945
+ syncClaudePickerButtons();
946
+ updateTerminalControlsHeight();
947
+ }
948
+
949
+ async function toggleClaudePicker(type) {
950
+ if (claudePickerOpen === type) {
951
+ closeClaudePicker();
952
+ return;
953
+ }
954
+ if (type === 'model') await refreshClaudeRuntimeConfig();
955
+ closeClaudeUsagePanel();
956
+ claudeResumePanelOpen = false;
957
+ const resumePanel = document.getElementById('claude-resume-panel');
958
+ if (resumePanel) resumePanel.classList.remove('active');
959
+ claudePickerOpen = type;
960
+ renderClaudePicker(type);
961
+ }
962
+
963
+ function chooseClaudePickerOption(type, value) {
964
+ const select = document.getElementById(selectIdForPicker(type));
965
+ if (!select) return;
966
+ select.value = value;
967
+ closeClaudePicker();
968
+ updateClaudeSettingsFromControls();
969
+ }
970
+
971
+ function applyClaudeRuntimeConfig(config) {
972
+ if (!config || !Array.isArray(config.models)) return;
973
+ claudeRuntimeConfig = config;
974
+ const modelEl = document.getElementById('claude-model-select');
975
+ if (modelEl) {
976
+ const current = modelEl.value || claudeState.model || config.defaultModel || 'default';
977
+ modelEl.innerHTML = '';
978
+ config.models.forEach(item => {
979
+ const option = new Option(fullModelLabel(item.value, item.resolved, item.label), item.value);
980
+ option.title = item.resolved ? `${item.label}: ${item.resolved}` : item.label;
981
+ option.dataset.resolved = item.resolved || '';
982
+ option.dataset.shortLabel = shortModelLabel(item.value, item.resolved);
983
+ modelEl.add(option);
984
+ });
985
+ const next = Array.from(modelEl.options).some(option => option.value === current)
986
+ ? current
987
+ : (config.defaultModel || 'default');
988
+ modelEl.value = next;
989
+ claudeState.model = next;
990
+ }
991
+ if (config.defaultEffort && claudeState.effort === 'medium') {
992
+ claudeState.effort = config.defaultEffort;
993
+ }
994
+ syncClaudePickerButtons();
995
+ renderClaudeStateBar();
996
+ }
997
+
998
+ async function refreshClaudeRuntimeConfig() {
999
+ try {
1000
+ const res = await fetchWithTimeout('/api/claude-config', {}, 10000);
1001
+ const data = await res.json();
1002
+ if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load Claude config');
1003
+ applyClaudeRuntimeConfig(data.config);
1004
+ return data.config;
1005
+ } catch (e) {
1006
+ log('Failed to load Claude config: ' + e.message);
1007
+ return null;
1008
+ }
1009
+ }
1010
+
1011
+ function ensureSelectOption(selectEl, value, labelPrefix) {
1012
+ const normalized = String(value || '').trim();
1013
+ if (!normalized) return;
1014
+ const exists = Array.from(selectEl.options).some(option => option.value === normalized);
1015
+ if (!exists) {
1016
+ const label = `${labelPrefix}:${normalized.replace(/^claude-/, '').replace(/-20\d{6,}.*$/, '').slice(0, 10)}`;
1017
+ selectEl.add(new Option(label, normalized));
1018
+ }
1019
+ }
1020
+
1021
+ function applyClaudeState(state = {}) {
1022
+ claudeState = { ...claudeState, ...state };
1023
+ claudeStatus = claudeState.status || claudeStatus;
1024
+ const permissionEl = document.getElementById('claude-permission-select');
1025
+ const modelEl = document.getElementById('claude-model-select');
1026
+ const effortEl = document.getElementById('claude-effort-select');
1027
+ if (permissionEl) permissionEl.value = claudeState.permissionMode || 'default';
1028
+ if (modelEl) {
1029
+ ensureSelectOption(modelEl, claudeState.model || 'default', 'M');
1030
+ modelEl.value = claudeState.model || 'default';
1031
+ }
1032
+ if (effortEl) effortEl.value = claudeState.effort || 'medium';
1033
+ syncClaudePickerButtons();
1034
+ const abortBtn = document.getElementById('claude-abort-btn');
1035
+ if (abortBtn) abortBtn.disabled = !(claudeState.canAbort || claudeStatus === 'thinking');
1036
+ renderClaudeStateBar();
1037
+ renderClaudeUsagePanel();
1038
+ renderClaudeChat();
1039
+ }
1040
+
1041
+ function shortValue(value, fallback = 'N/A') {
1042
+ const text = String(value || fallback);
1043
+ return text.length > 16 ? text.slice(0, 13) + '...' : text;
1044
+ }
1045
+
1046
+ function formatTokenCount(value) {
1047
+ const number = Number(value || 0);
1048
+ if (number >= 1000000) return `${(number / 1000000).toFixed(2)}M`;
1049
+ if (number >= 1000) return `${(number / 1000).toFixed(1)}K`;
1050
+ return String(Math.round(number));
1051
+ }
1052
+
1053
+ function formatContextK(value) {
1054
+ return `${(Number(value || 0) / 1000).toFixed(1)}K`;
1055
+ }
1056
+
1057
+ function contextRemainingPercent(usage) {
1058
+ if (!usage || typeof usage.contextSize !== 'number') return null;
1059
+ const used = Math.max(0, usage.contextSize);
1060
+ return Math.max(0, Math.min(100, 100 - (used / CLAUDE_CONTEXT_SIZE) * 100));
1061
+ }
1062
+
1063
+ function renderClaudeContextSizeBadge() {
1064
+ const usage = claudeState.latestUsage;
1065
+ if (!usage || typeof usage.contextSize !== 'number') return '';
1066
+ return `<div class="claude-context-size-badge">Context ${escapeHtml(formatContextK(usage.contextSize))}</div>`;
1067
+ }
1068
+
1069
+ function renderClaudeStateBar() {
1070
+ const el = document.getElementById('claude-state-bar');
1071
+ if (!el) return;
1072
+ const pending = Number(claudeState.pendingPermissionCount || claudePendingPermissions.filter(item => item.status === 'pending').length) || 0;
1073
+ const mode = claudeState.permissionMode || 'default';
1074
+ const parts = [];
1075
+ if (pending) {
1076
+ parts.push(`<span class="claude-state-pill warn">${pending} approval${pending > 1 ? 's' : ''}</span>`);
1077
+ } else if (claudeStatus === 'thinking') {
1078
+ parts.push('<span class="claude-state-pill">Working</span>');
1079
+ } else if (claudeStatus && !['idle', 'stopped'].includes(claudeStatus)) {
1080
+ parts.push(`<span class="claude-state-pill warn">${escapeHtml(shortValue(claudeStatus))}</span>`);
1081
+ }
1082
+ const remainingPercent = contextRemainingPercent(claudeState.latestUsage);
1083
+ if (remainingPercent !== null && remainingPercent <= 10) {
1084
+ parts.push(`<span class="claude-state-pill warn">${Math.round(remainingPercent)}% left</span>`);
1085
+ }
1086
+ if (mode !== 'default') {
1087
+ parts.push(`<span class="claude-state-pill perm ${escapeHtml(mode)}">${escapeHtml(permissionModeLabel(mode))}</span>`);
1088
+ }
1089
+ el.innerHTML = parts.join('');
1090
+ el.style.display = parts.length ? 'flex' : 'none';
1091
+ }
1092
+
1093
+ function usageItem(label, value) {
1094
+ return `<div class="claude-usage-item">
1095
+ <div class="claude-usage-label">${escapeHtml(label)}</div>
1096
+ <div class="claude-usage-value">${escapeHtml(value)}</div>
1097
+ </div>`;
1098
+ }
1099
+
1100
+ function renderClaudeUsagePanel() {
1101
+ const panel = document.getElementById('claude-usage-panel');
1102
+ if (!panel) return;
1103
+ const usage = claudeState.latestUsage;
1104
+ if (!usage) {
1105
+ panel.innerHTML = '<div class="claude-usage-title">Usage</div><div class="claude-resume-meta">No usage yet.</div>';
1106
+ } else {
1107
+ panel.innerHTML = `<div class="claude-usage-title">Usage</div>
1108
+ <div class="claude-usage-grid">
1109
+ ${usageItem('Total', formatTokenCount(usage.totalTokens))}
1110
+ ${usageItem('Context', formatContextK(usage.contextSize))}
1111
+ ${usageItem('Input', formatTokenCount(usage.inputTokens))}
1112
+ ${usageItem('Output', formatTokenCount(usage.outputTokens))}
1113
+ ${usageItem('Cache write', formatTokenCount(usage.cacheCreation))}
1114
+ ${usageItem('Cache read', formatTokenCount(usage.cacheRead))}
1115
+ </div>`;
1116
+ }
1117
+ panel.classList.toggle('active', claudeUsagePanelOpen);
1118
+ }
1119
+
1120
+ function toggleClaudeUsagePanel() {
1121
+ claudeUsagePanelOpen = !claudeUsagePanelOpen;
1122
+ if (claudeUsagePanelOpen) {
1123
+ closeClaudePicker();
1124
+ claudeResumePanelOpen = false;
1125
+ const resumePanel = document.getElementById('claude-resume-panel');
1126
+ if (resumePanel) resumePanel.classList.remove('active');
1127
+ }
1128
+ renderClaudeUsagePanel();
1129
+ updateTerminalControlsHeight();
1130
+ }
1131
+
1132
+ async function updateClaudeSettingsFromControls() {
1133
+ await refreshClaudeRuntimeConfig();
1134
+ const settings = {
1135
+ permissionMode: document.getElementById('claude-permission-select').value,
1136
+ model: document.getElementById('claude-model-select').value,
1137
+ effort: document.getElementById('claude-effort-select').value
1138
+ };
1139
+ applyClaudeState(settings);
1140
+ if (currentSocket && currentSocket.readyState === 1) {
1141
+ currentSocket.send(JSON.stringify({ type: 'claude-settings', settings }));
1142
+ }
1143
+ }
1144
+
1145
+ function abortClaudeSession() {
1146
+ if (!currentSocket || currentSocket.readyState !== 1) return;
1147
+ currentSocket.send(JSON.stringify({ type: 'claude-abort' }));
1148
+ }
1149
+
1150
+ async function toggleClaudeResumePanel() {
1151
+ claudeResumePanelOpen = !claudeResumePanelOpen;
1152
+ if (claudeResumePanelOpen) closeClaudePicker();
1153
+ if (claudeResumePanelOpen) closeClaudeUsagePanel();
1154
+ const panel = document.getElementById('claude-resume-panel');
1155
+ panel.classList.toggle('active', claudeResumePanelOpen);
1156
+ updateTerminalControlsHeight();
1157
+ if (claudeResumePanelOpen && !claudeResumeItemsLoaded) {
1158
+ await loadClaudeResumeSessions();
1159
+ }
1160
+ }
1161
+
1162
+ async function loadClaudeResumeSessions() {
1163
+ const panel = document.getElementById('claude-resume-panel');
1164
+ if (!activeSessionId) return;
1165
+ panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading sessions...</div>';
1166
+ try {
1167
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/claude-resume-sessions`, {}, 15000);
1168
+ const data = await res.json();
1169
+ if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load resume sessions');
1170
+ const items = data.items || [];
1171
+ claudeResumeItemsLoaded = true;
1172
+ if (!items.length) {
1173
+ panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">No local Claude sessions found for this folder.</div>';
1174
+ return;
1175
+ }
1176
+ panel.innerHTML = items.map(item => {
1177
+ const encodedId = encodePathValue(item.id);
1178
+ const updated = item.updatedAt ? new Date(item.updatedAt).toLocaleString() : 'Unknown';
1179
+ const active = item.id === claudeState.resumeSessionId || item.id === claudeState.claudeSessionId;
1180
+ return `<button class="claude-resume-item" onclick="selectClaudeResumeSession(decodePathValue('${encodedId}'))">
1181
+ <div class="claude-resume-title"><span>${escapeHtml(item.id.slice(0, 8))}${active ? ' · current' : ''}</span><span>${escapeHtml(updated)}</span></div>
1182
+ <div class="claude-resume-meta">${escapeHtml(item.lastText || item.firstText || item.cwd || 'No preview')}</div>
1183
+ </button>`;
1184
+ }).join('');
1185
+ } catch (e) {
1186
+ panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px; color:#ff6b61;">${escapeHtml(e.message)}</div>`;
1187
+ }
1188
+ }
1189
+
1190
+ function selectClaudeResumeSession(id) {
1191
+ if (!id || !currentSocket || currentSocket.readyState !== 1) return;
1192
+ currentSocket.send(JSON.stringify({ type: 'claude-resume', resumeSessionId: id }));
1193
+ applyClaudeState({ resumeSessionId: id, claudeSessionId: id });
1194
+ claudeResumePanelOpen = false;
1195
+ document.getElementById('claude-resume-panel').classList.remove('active');
1196
+ updateTerminalControlsHeight();
1197
+ }
1198
+
1199
+ function textFromClaudeMessage(message) {
1200
+ if (!message) return '';
1201
+ if (message.text) return String(message.text);
1202
+ if (message.summary) return String(message.summary);
1203
+ return '';
1204
+ }
1205
+
1206
+ function escapeRegExp(text) {
1207
+ return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1208
+ }
1209
+
1210
+ function inlineMarkdown(text) {
1211
+ let html = escapeHtml(text || '');
1212
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
1213
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
1214
+ html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
1215
+ html = html.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
1216
+ html = html.replace(/_([^_\n]+)_/g, '<em>$1</em>');
1217
+ html = html.replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/g, '<img src="$2" alt="$1">');
1218
+ html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
1219
+ return html;
1220
+ }
1221
+
1222
+ function splitMarkdownBlocks(markdown) {
1223
+ const lines = String(markdown || '').replace(/\r\n/g, '\n').split('\n');
1224
+ const blocks = [];
1225
+ let i = 0;
1226
+ while (i < lines.length) {
1227
+ if (!lines[i].trim()) {
1228
+ i++;
1229
+ continue;
1230
+ }
1231
+ const fence = lines[i].match(/^```(\w+)?\s*$/);
1232
+ if (fence) {
1233
+ const language = fence[1] || '';
1234
+ const content = [];
1235
+ i++;
1236
+ while (i < lines.length && !/^```\s*$/.test(lines[i])) {
1237
+ content.push(lines[i]);
1238
+ i++;
1239
+ }
1240
+ if (i < lines.length) i++;
1241
+ blocks.push({ type: 'code', language, content: content.join('\n') });
1242
+ continue;
1243
+ }
1244
+ if (/^\s*[-*_]{3,}\s*$/.test(lines[i])) {
1245
+ blocks.push({ type: 'hr' });
1246
+ i++;
1247
+ continue;
1248
+ }
1249
+ if (/^\s{0,3}#{1,4}\s+/.test(lines[i])) {
1250
+ const match = lines[i].match(/^(\s{0,3})(#{1,4})\s+(.+)$/);
1251
+ blocks.push({ type: 'header', level: match[2].length, text: match[3] });
1252
+ i++;
1253
+ continue;
1254
+ }
1255
+ if (lines[i].includes('|') && i + 1 < lines.length && /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[i + 1])) {
1256
+ const rows = [lines[i]];
1257
+ i += 2;
1258
+ while (i < lines.length && lines[i].includes('|') && lines[i].trim()) {
1259
+ rows.push(lines[i]);
1260
+ i++;
1261
+ }
1262
+ blocks.push({ type: 'table', rows });
1263
+ continue;
1264
+ }
1265
+ if (/^\s*([-*+])\s+/.test(lines[i]) || /^\s*\d+\.\s+/.test(lines[i])) {
1266
+ const ordered = /^\s*\d+\.\s+/.test(lines[i]);
1267
+ const items = [];
1268
+ while (i < lines.length && (ordered ? /^\s*\d+\.\s+/.test(lines[i]) : /^\s*[-*+]\s+/.test(lines[i]))) {
1269
+ items.push(lines[i].replace(ordered ? /^\s*\d+\.\s+/ : /^\s*[-*+]\s+/, ''));
1270
+ i++;
1271
+ }
1272
+ blocks.push({ type: ordered ? 'ol' : 'ul', items });
1273
+ continue;
1274
+ }
1275
+ if (/^\s*>\s?/.test(lines[i])) {
1276
+ const quote = [];
1277
+ while (i < lines.length && /^\s*>\s?/.test(lines[i])) {
1278
+ quote.push(lines[i].replace(/^\s*>\s?/, ''));
1279
+ i++;
1280
+ }
1281
+ blocks.push({ type: 'quote', text: quote.join('\n') });
1282
+ continue;
1283
+ }
1284
+ const paragraph = [];
1285
+ while (i < lines.length && lines[i].trim()) {
1286
+ if (/^```/.test(lines[i]) || /^\s{0,3}#{1,4}\s+/.test(lines[i]) || /^\s*([-*+])\s+/.test(lines[i]) || /^\s*\d+\.\s+/.test(lines[i])) break;
1287
+ paragraph.push(lines[i]);
1288
+ i++;
1289
+ }
1290
+ blocks.push({ type: 'paragraph', text: paragraph.join('\n') });
1291
+ }
1292
+ return blocks;
1293
+ }
1294
+
1295
+ function renderMarkdown(markdown) {
1296
+ const blocks = splitMarkdownBlocks(markdown);
1297
+ return `<div class="claude-md">${blocks.map(block => {
1298
+ if (block.type === 'code') {
1299
+ const language = block.language ? `<div class="claude-tool-section-title">${escapeHtml(block.language)}</div>` : '';
1300
+ return `<pre>${language}<code>${escapeHtml(block.content)}</code></pre>`;
1301
+ }
1302
+ if (block.type === 'hr') return '<hr>';
1303
+ if (block.type === 'header') return `<h${block.level}>${inlineMarkdown(block.text)}</h${block.level}>`;
1304
+ if (block.type === 'ul') return `<ul>${block.items.map(item => `<li>${inlineMarkdown(item)}</li>`).join('')}</ul>`;
1305
+ if (block.type === 'ol') return `<ol>${block.items.map(item => `<li>${inlineMarkdown(item)}</li>`).join('')}</ol>`;
1306
+ if (block.type === 'quote') return `<blockquote>${renderMarkdown(block.text)}</blockquote>`;
1307
+ if (block.type === 'table') return renderMarkdownTable(block.rows);
1308
+ return `<p>${inlineMarkdown(block.text).replace(/\n/g, '<br>')}</p>`;
1309
+ }).join('')}</div>`;
1310
+ }
1311
+
1312
+ function renderMarkdownTable(rows) {
1313
+ const parsed = rows.map(row => row.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(cell => cell.trim()));
1314
+ if (!parsed.length) return '';
1315
+ const [head, ...body] = parsed;
1316
+ return `<table><thead><tr>${head.map(cell => `<th>${inlineMarkdown(cell)}</th>`).join('')}</tr></thead><tbody>${body.map(row => `<tr>${row.map(cell => `<td>${inlineMarkdown(cell)}</td>`).join('')}</tr>`).join('')}</tbody></table>`;
1317
+ }
1318
+
1319
+ function parseLocalCommandMessage(text) {
1320
+ const value = String(text || '');
1321
+ if (/^\s*<local-command-caveat>[\s\S]*?<\/local-command-caveat>\s*$/.test(value)) return { kind: 'hidden' };
1322
+ const stdoutMatch = value.match(/^\s*<local-command-stdout>\s*([\s\S]*?)\s*<\/local-command-stdout>\s*$/);
1323
+ if (stdoutMatch) {
1324
+ const stdout = stdoutMatch[1].trim();
1325
+ if (/^Goal set:/i.test(stdout)) return { kind: 'hidden' };
1326
+ return { kind: 'text', text: stdout };
1327
+ }
1328
+ const raw = value.trim().match(/^\/([a-zA-Z][\w:-]*)(?:\s+([\s\S]*?))?$/);
1329
+ if (raw) {
1330
+ return { kind: 'command', commandName: raw[1], args: raw[2] && raw[2].trim() };
1331
+ }
1332
+ const nameMatch = value.match(/<command-name>\s*\/?([^<]+?)\s*<\/command-name>/);
1333
+ if (nameMatch) {
1334
+ const argsMatch = value.match(/<command-args>\s*([\s\S]*?)\s*<\/command-args>/);
1335
+ const stripped = value
1336
+ .replace(/<command-message>[\s\S]*?<\/command-message>/g, '')
1337
+ .replace(/<command-name>[\s\S]*?<\/command-name>/g, '')
1338
+ .replace(/<command-args>[\s\S]*?<\/command-args>/g, '')
1339
+ .trim();
1340
+ if (!stripped) return { kind: 'command', commandName: nameMatch[1].trim(), args: argsMatch && argsMatch[1].trim() };
1341
+ return { kind: 'text', text: stripped };
1342
+ }
1343
+ return { kind: 'text', text: value };
1344
+ }
1345
+
1346
+ function toolCategory(name) {
1347
+ if (['Bash', 'CodexBash', 'execute'].includes(name)) return 'terminal';
1348
+ if (['Edit', 'MultiEdit', 'Write', 'NotebookEdit'].includes(name)) return 'edit';
1349
+ if (['Read', 'LS'].includes(name)) return 'read';
1350
+ if (['Grep', 'Glob'].includes(name)) return 'search';
1351
+ if (['WebFetch', 'WebSearch'].includes(name)) return 'web';
1352
+ if (['Task', 'Agent'].includes(name)) return 'task';
1353
+ return 'other';
1354
+ }
1355
+
1356
+ function toolIcon(category) {
1357
+ const labels = { terminal: '$', edit: '+/-', read: 'R', search: '?', web: 'W', task: 'A', other: '*' };
1358
+ return labels[category] || '*';
1359
+ }
1360
+
1361
+ function toolCommand(tool) {
1362
+ const input = tool && tool.input;
1363
+ if (!input || typeof input !== 'object') return '';
1364
+ if (typeof input.command === 'string') return input.command;
1365
+ if (Array.isArray(input.command)) return input.command.join(' ');
1366
+ if (typeof input.file_path === 'string') return input.file_path;
1367
+ if (typeof input.path === 'string') return input.path;
1368
+ if (typeof input.pattern === 'string') return input.pattern;
1369
+ if (typeof input.prompt === 'string') return input.prompt;
1370
+ return tool.summary || '';
1371
+ }
1372
+
1373
+ function toolTitle(tool) {
1374
+ const name = tool.name || 'Tool';
1375
+ const input = tool.input || {};
1376
+ if (name === 'Bash') return (tool.description || 'Terminal');
1377
+ if (name === 'Read' && input.file_path) return input.file_path;
1378
+ if (name === 'Edit' || name === 'MultiEdit' || name === 'Write') return input.file_path || name;
1379
+ if (name === 'Grep') return input.pattern ? `grep(pattern: ${input.pattern})` : 'Search Content';
1380
+ if (name === 'Glob') return input.pattern || 'Search Files';
1381
+ if ((name === 'Task' || name === 'Agent') && input.description) return input.description;
1382
+ if (name.startsWith('mcp__')) return name.replace(/^mcp__/, '').replace(/__/g, ': ');
1383
+ return name;
1384
+ }
1385
+
1386
+ function renderToolSection(title, value) {
1387
+ if (value === undefined || value === null || value === '') return '';
1388
+ const code = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
1389
+ return `<div class="claude-tool-section"><div class="claude-tool-section-title">${escapeHtml(title)}</div><pre class="claude-tool-code">${escapeHtml(code)}</pre></div>`;
1390
+ }
1391
+
1392
+ function renderClaudeTool(tool) {
1393
+ const category = toolCategory(tool.name || '');
1394
+ const command = toolCommand(tool);
1395
+ const hasResult = tool.resultText || tool.isError;
1396
+ const running = claudeStatus === 'thinking' && !hasResult;
1397
+ const status = tool.isError ? 'error' : (hasResult ? 'completed' : (running ? 'running' : 'running'));
1398
+ const open = tool.isError || (!hasResult && !running) ? ' open' : '';
1399
+ const compact = category === 'terminal' || category === 'search' || category === 'read';
1400
+ return `<details class="claude-tool claude-tool-card${tool.isError ? ' error' : ''}"${open}>
1401
+ <summary class="claude-tool-header">
1402
+ <span class="claude-tool-icon" data-icon="${escapeHtml(toolIcon(category))}"></span>
1403
+ <span class="claude-tool-title">${escapeHtml(toolTitle(tool))}</span>
1404
+ <span class="claude-tool-status">${escapeHtml(status)}</span>
1405
+ <span class="claude-tool-command">${escapeHtml(command)}</span>
1406
+ </summary>
1407
+ ${compact && !tool.isError ? '' : `<div class="claude-tool-body">
1408
+ ${renderToolSection('Input', tool.input)}
1409
+ ${renderToolSection(tool.isError ? 'Error' : 'Output', tool.resultText)}
1410
+ </div>`}
1411
+ </details>`;
1412
+ }
1413
+
1414
+ function renderWorkGroup(items) {
1415
+ const running = items.some(item => item.kind === 'tool' && !item.resultText);
1416
+ const label = `${items.length} tool call${items.length > 1 ? 's' : ''}`;
1417
+ return `<details class="claude-work-group"${running ? ' open' : ''}>
1418
+ <summary><span class="claude-tool-icon" data-icon="*"></span><span class="claude-work-group-title">Worked on ${escapeHtml(label)}</span>${running ? '<span class="claude-tool-status">running</span>' : ''}</summary>
1419
+ <div class="claude-work-group-body">${items.map(renderDisplayItem).join('')}</div>
1420
+ </details>`;
1421
+ }
1422
+
1423
+ function buildClaudeDisplayItems(messages) {
1424
+ const items = [];
1425
+ const byToolUseId = new Map();
1426
+ for (const message of messages) {
1427
+ if (!message) continue;
1428
+ if (message.kind === 'tool') {
1429
+ const item = { ...message, kind: 'tool', resultText: '', isError: false };
1430
+ items.push(item);
1431
+ if (item.toolUseId) byToolUseId.set(item.toolUseId, item);
1432
+ continue;
1433
+ }
1434
+ if (message.kind === 'tool-result') {
1435
+ const parent = message.toolUseId && byToolUseId.get(message.toolUseId);
1436
+ if (parent) {
1437
+ parent.resultText = message.text || '';
1438
+ parent.isError = Boolean(message.isError);
1439
+ } else {
1440
+ items.push(message);
1441
+ }
1442
+ continue;
1443
+ }
1444
+ items.push(message);
1445
+ }
1446
+
1447
+ const grouped = [];
1448
+ let run = [];
1449
+ const flush = () => {
1450
+ if (run.length > 1) grouped.push({ kind: 'work-group', items: run });
1451
+ else if (run.length === 1) grouped.push(run[0]);
1452
+ run = [];
1453
+ };
1454
+ for (const item of items) {
1455
+ if (item.kind === 'tool') {
1456
+ run.push(item);
1457
+ continue;
1458
+ }
1459
+ flush();
1460
+ grouped.push(item);
1461
+ }
1462
+ flush();
1463
+ return grouped;
1464
+ }
1465
+
1466
+ function renderUserMessage(message) {
1467
+ const parsed = parseLocalCommandMessage(textFromClaudeMessage(message));
1468
+ if (parsed.kind === 'hidden') return '';
1469
+ if (parsed.kind === 'command') {
1470
+ const args = parsed.args ? `<div class="claude-message user">${renderMarkdown(parsed.args)}</div>` : '';
1471
+ return `${args}<div class="claude-message user"><span class="claude-command-chip">/${escapeHtml(parsed.commandName)}</span></div>`;
1472
+ }
1473
+ return `<div class="claude-message user">${renderMarkdown(parsed.text)}</div>`;
1474
+ }
1475
+
1476
+ function renderDisplayItem(message) {
1477
+ if (!message) return '';
1478
+ if (message.kind === 'user') return renderUserMessage(message);
1479
+ if (message.kind === 'assistant') return `<div class="claude-message assistant">${renderMarkdown(textFromClaudeMessage(message))}</div>`;
1480
+ if (message.kind === 'tool') return renderClaudeTool(message);
1481
+ if (message.kind === 'tool-result') return `<div class="claude-tool${message.isError ? ' error' : ''}">${renderToolSection(message.isError ? 'Error' : 'Output', message.text || '')}</div>`;
1482
+ if (message.kind === 'work-group') return renderWorkGroup(message.items);
1483
+ if (message.kind === 'event') return `<div class="claude-message event${message.level === 'error' ? ' error' : ''}">${escapeHtml(message.text || '')}</div>`;
1484
+ return '';
1485
+ }
1486
+
1487
+ function isClaudeEditTool(name) {
1488
+ return ['Edit', 'MultiEdit', 'Write', 'NotebookEdit'].includes(name || '');
1489
+ }
1490
+
1491
+ function isClaudeExitPlanTool(name) {
1492
+ return name === 'exit_plan_mode' || name === 'ExitPlanMode';
1493
+ }
1494
+
1495
+ function claudeAllowToolLabel(req) {
1496
+ if (req.toolName === 'Bash' && req.input && typeof req.input.command === 'string') return 'Allow command';
1497
+ return 'Allow tool';
1498
+ }
1499
+
1500
+ function renderClaudePermissionActions(req) {
1501
+ const id = escapeHtml(req.id);
1502
+ const toolName = req.toolName || '';
1503
+ const parts = [
1504
+ `<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'allow-once')">Yes</button>`
1505
+ ];
1506
+ if (isClaudeEditTool(toolName) || isClaudeExitPlanTool(toolName) || req.canAllowEdits) {
1507
+ parts.push(`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'allow-edits')">Allow edits</button>`);
1508
+ }
1509
+ if (isClaudeExitPlanTool(toolName) || req.canBypass) {
1510
+ parts.push(`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'bypass')">Allow all</button>`);
1511
+ }
1512
+ if (toolName && !isClaudeEditTool(toolName) && !isClaudeExitPlanTool(toolName) && req.canAllowTool !== false) {
1513
+ parts.push(`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'allow-tool')">${escapeHtml(claudeAllowToolLabel(req))}</button>`);
1514
+ }
1515
+ parts.push(`<button class="small-btn danger" onclick="respondClaudePermission('${id}', 'deny')">Deny</button>`);
1516
+ return parts.join('');
1517
+ }
1518
+
1519
+ function renderClaudeChat() {
1520
+ const container = document.getElementById('claude-chat-container');
1521
+ const parts = [renderClaudeContextSizeBadge(), ...buildClaudeDisplayItems(claudeMessages).map(renderDisplayItem)].filter(Boolean);
1522
+ for (const req of claudePendingPermissions.filter(item => item.status === 'pending')) {
1523
+ parts.push(`<div class="claude-tool claude-permission">
1524
+ <div class="claude-tool-header">
1525
+ <span class="claude-tool-icon" data-icon="!"></span>
1526
+ <strong>${escapeHtml(req.title || req.toolName || 'Permission required')}</strong>
1527
+ </div>
1528
+ <div class="claude-tool-body">
1529
+ ${req.reason ? `<div>${escapeHtml(req.reason)}</div>` : ''}
1530
+ ${req.blockedPath ? `<div class="claude-resume-meta">${escapeHtml(req.blockedPath)}</div>` : ''}
1531
+ ${renderToolSection('Input', req.input || {})}
1532
+ <div class="claude-permission-actions">
1533
+ ${renderClaudePermissionActions(req)}
1534
+ </div>
1535
+ </div>
1536
+ </div>`);
1537
+ }
1538
+ if (claudeStatus === 'thinking') {
1539
+ parts.push('<div class="claude-status">Claude is working...</div>');
1540
+ }
1541
+ container.innerHTML = parts.join('') || '<div class="claude-message event">Send a message to start Claude.</div>';
1542
+ renderClaudeStateBar();
1543
+ requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
1544
+ }
1545
+
1546
+ function applyClaudeEvent(event) {
1547
+ if (!event) return;
1548
+ if (event.type === 'message' && event.message) {
1549
+ claudeMessages.push(event.message);
1550
+ } else if (event.type === 'status') {
1551
+ claudeStatus = event.status || 'idle';
1552
+ } else if (event.type === 'permission-request' && event.request) {
1553
+ claudePendingPermissions = claudePendingPermissions.filter(item => item.id !== event.request.id);
1554
+ claudePendingPermissions.push(event.request);
1555
+ } else if (event.type === 'permission-updated' && event.request) {
1556
+ claudePendingPermissions = claudePendingPermissions.map(item => item.id === event.request.id ? event.request : item);
1557
+ } else if (event.type === 'history-reset' && Array.isArray(event.messages)) {
1558
+ claudeMessages = event.messages;
1559
+ } else if (event.type === 'state' && event.state) {
1560
+ applyClaudeState(event.state);
1561
+ }
1562
+ if (event.type !== 'state') applyClaudeState({
1563
+ status: claudeStatus,
1564
+ pendingPermissionCount: claudePendingPermissions.filter(item => item.status === 'pending').length,
1565
+ canAbort: claudeStatus === 'thinking'
1566
+ });
1567
+ renderClaudeChat();
1568
+ }
1569
+
1570
+ function respondClaudePermission(id, actionOrApproved) {
1571
+ if (!currentSocket || currentSocket.readyState !== 1) return;
1572
+ const action = typeof actionOrApproved === 'string'
1573
+ ? actionOrApproved
1574
+ : (actionOrApproved ? 'allow-once' : 'deny');
1575
+ currentSocket.send(JSON.stringify({
1576
+ type: 'claude-permission',
1577
+ id,
1578
+ action,
1579
+ approved: action !== 'deny'
1580
+ }));
1581
+ }
1582
+
644
1583
  async function createSession(toolKey, sessionName) {
645
1584
  try {
646
1585
  const workingDirectory = document.getElementById('cwd-field').value;
1586
+ const runtimeConfig = toolKey === 'claude-code' ? await refreshClaudeRuntimeConfig() : null;
1587
+ const claudeOptions = runtimeConfig ? {
1588
+ model: runtimeConfig.defaultModel || 'default',
1589
+ effort: runtimeConfig.defaultEffort || 'medium'
1590
+ } : undefined;
647
1591
  const res = await fetchWithTimeout('/api/sessions', {
648
1592
  method: 'POST',
649
1593
  headers: { 'Content-Type': 'application/json' },
650
- body: JSON.stringify({ toolKey, workingDirectory })
1594
+ body: JSON.stringify({ toolKey, workingDirectory, claudeOptions })
651
1595
  });
652
1596
  const data = await res.json();
653
1597
  if (!res.ok || !data.id) throw new Error(data.error || 'Failed to create session');
@@ -972,10 +1916,16 @@
972
1916
  updateToolShortcuts(activeToolKey);
973
1917
  document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
974
1918
  document.getElementById('terminal-view').classList.add('active');
1919
+ setClaudeModeEnabled(isClaudeSession());
1920
+ if (currentSocket) { currentSocket.close(); currentSocket = null; }
975
1921
  initTerminal(id);
976
1922
  }
977
1923
 
978
1924
  function initTerminal(sessionId) {
1925
+ if (isClaudeSession()) {
1926
+ initClaudeSession(sessionId);
1927
+ return;
1928
+ }
979
1929
  if (typeof Terminal === 'undefined') {
980
1930
  alert('Terminal library not loaded yet.');
981
1931
  showLobby();
@@ -1013,6 +1963,53 @@
1013
1963
  };
1014
1964
  }
1015
1965
 
1966
+ async function initClaudeSession(sessionId) {
1967
+ claudeMessages = [];
1968
+ claudePendingPermissions = [];
1969
+ claudeStatus = 'idle';
1970
+ const runtimeConfig = await refreshClaudeRuntimeConfig();
1971
+ claudeState = {
1972
+ permissionMode: 'default',
1973
+ model: (runtimeConfig && runtimeConfig.defaultModel) || 'default',
1974
+ effort: (runtimeConfig && runtimeConfig.defaultEffort) || 'medium',
1975
+ contextRemaining: null,
1976
+ latestUsage: null,
1977
+ claudeSessionId: null,
1978
+ resumeSessionId: null,
1979
+ canAbort: false,
1980
+ pendingPermissionCount: 0
1981
+ };
1982
+ claudeResumePanelOpen = false;
1983
+ claudePickerOpen = null;
1984
+ claudeResumeItemsLoaded = false;
1985
+ document.getElementById('claude-resume-panel').classList.remove('active');
1986
+ document.getElementById('claude-resume-panel').innerHTML = '';
1987
+ closeClaudePicker();
1988
+ updateTerminalControlsHeight();
1989
+ applyClaudeRuntimeConfig(runtimeConfig);
1990
+ applyClaudeState(claudeState);
1991
+ renderClaudeChat();
1992
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1993
+ currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
1994
+ currentSocket.onopen = () => logViewSnapshot('claude-ws-open');
1995
+ currentSocket.onmessage = (e) => {
1996
+ const msg = JSON.parse(e.data);
1997
+ if (msg.type === 'claude-snapshot' && msg.snapshot) {
1998
+ claudeMessages = msg.snapshot.messages || [];
1999
+ claudePendingPermissions = msg.snapshot.pendingPermissions || [];
2000
+ claudeStatus = msg.snapshot.status || 'idle';
2001
+ applyClaudeState(msg.snapshot.state || {
2002
+ status: claudeStatus,
2003
+ pendingPermissionCount: claudePendingPermissions.filter(item => item.status === 'pending').length,
2004
+ canAbort: claudeStatus === 'thinking'
2005
+ });
2006
+ renderClaudeChat();
2007
+ }
2008
+ if (msg.type === 'claude-event') applyClaudeEvent(msg.event);
2009
+ if (msg.type === 'exit') showLobby();
2010
+ };
2011
+ }
2012
+
1016
2013
  function sendWS(data) {
1017
2014
  if (currentSocket && currentSocket.readyState === 1) currentSocket.send(JSON.stringify({ type: 'input', data }));
1018
2015
  }
@@ -1223,7 +2220,15 @@
1223
2220
 
1224
2221
  function performSend() {
1225
2222
  const val = inputEl.value;
1226
- if (val) {
2223
+ if (val && isClaudeSession()) {
2224
+ if (currentSocket && currentSocket.readyState === 1) {
2225
+ currentSocket.send(JSON.stringify({ type: 'claude-input', text: val }));
2226
+ }
2227
+ inputEl.value = '';
2228
+ inputEl.style.height = '38px';
2229
+ return;
2230
+ }
2231
+ if (val) {
1227
2232
  const formattedVal = val.replace(/\n/g, '\r');
1228
2233
  sendWS(formattedVal);
1229
2234
  inputEl.value = '';
@@ -1302,6 +2307,7 @@
1302
2307
  document.getElementById('timer-btn').classList.remove('active');
1303
2308
  editingTimedInputId = null;
1304
2309
  renderTimedTags();
2310
+ updateTerminalControlsHeight();
1305
2311
  }
1306
2312
 
1307
2313
  function stopTimedInputTimers() {
@@ -1362,6 +2368,7 @@
1362
2368
  document.getElementById('timer-btn').classList.add('active');
1363
2369
  updateTimedSendPreview();
1364
2370
  renderTimedTags();
2371
+ updateTerminalControlsHeight();
1365
2372
  }
1366
2373
 
1367
2374
  async function saveTimedSend() {
@@ -1410,12 +2417,19 @@
1410
2417
  const isOpen = panel.classList.toggle('active');
1411
2418
  document.getElementById('timer-btn').classList.toggle('active', isOpen);
1412
2419
  if (isOpen) {
2420
+ if (isClaudeSession()) {
2421
+ closeClaudePicker();
2422
+ closeClaudeUsagePanel();
2423
+ claudeResumePanelOpen = false;
2424
+ document.getElementById('claude-resume-panel').classList.remove('active');
2425
+ }
1413
2426
  initTimedDelaySelectors();
1414
2427
  resetTimedEditor({ keepInput: true });
1415
2428
  updateTimedSendPreview();
1416
2429
  loadTimedInputs();
1417
2430
  }
1418
2431
  else closeTimedSendPanel();
2432
+ updateTerminalControlsHeight();
1419
2433
  });
1420
2434
  inputEl.addEventListener('keydown', (e) => {
1421
2435
  if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); performSend(); }