glad-web 1.0.19 → 1.0.20

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,90 @@
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-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; }
82
+ .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; }
83
+ .claude-message.assistant { max-width: 100%; background: transparent; border: 0; color: #f5f5f7; padding: 0 4px; margin: 0 0 12px 0; }
84
+ .claude-message.event { max-width: 100%; background: transparent; border: 0; color: var(--text-dim); font-size: 12px; padding: 3px 2px; text-align: center; }
85
+ .claude-message.error { color: #ff6b61; }
86
+ .claude-md { white-space: normal; }
87
+ .claude-md > :first-child { margin-top: 0; }
88
+ .claude-md > :last-child { margin-bottom: 0; }
89
+ .claude-md p { margin: 0 0 8px 0; }
90
+ .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; }
91
+ .claude-md h1 { font-size: 20px; }
92
+ .claude-md h2 { font-size: 18px; }
93
+ .claude-md h3 { font-size: 16px; }
94
+ .claude-md h4 { font-size: 15px; }
95
+ .claude-md ul, .claude-md ol { margin: 0 0 9px 0; padding-left: 20px; }
96
+ .claude-md li { margin: 4px 0; }
97
+ .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; }
98
+ .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; }
99
+ .claude-md pre code { display: block; padding: 11px 12px; background: transparent; border: 0; border-radius: 0; white-space: pre; line-height: 1.45; }
100
+ .claude-md blockquote { margin: 8px 0; padding: 2px 0 2px 10px; border-left: 3px solid rgba(255,255,255,0.22); color: #c7c7cc; }
101
+ .claude-md table { width: 100%; border-collapse: collapse; margin: 8px 0 10px 0; font-size: 13px; display: block; overflow-x: auto; }
102
+ .claude-md th, .claude-md td { border: 1px solid rgba(255,255,255,0.12); padding: 6px 8px; text-align: left; vertical-align: top; }
103
+ .claude-md th { background: rgba(255,255,255,0.07); color: #fff; }
104
+ .claude-md a { color: #64b5ff; text-decoration: none; }
105
+ .claude-md img { max-width: 100%; border-radius: 8px; margin: 6px 0; }
106
+ .claude-md hr { border: 0; border-top: 1px solid rgba(255,255,255,0.12); margin: 12px 0; }
107
+ .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; }
108
+ .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; }
109
+ .claude-tool strong { color: #fff; }
110
+ .claude-tool-card { display: block; }
111
+ .claude-tool-card summary { list-style: none; cursor: pointer; }
112
+ .claude-tool-card summary::-webkit-details-marker { display: none; }
113
+ .claude-tool-header { display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 7px 9px; color: #f5f5f7; }
114
+ .claude-tool-icon { width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; color: var(--text-dim); flex: 0 0 auto; }
115
+ .claude-tool-icon::before { content: attr(data-icon); font-size: 11px; font-weight: 800; line-height: 1; }
116
+ .claude-tool-title { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; }
117
+ .claude-tool-status { color: var(--text-dim); font-size: 12px; white-space: nowrap; }
118
+ .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; }
119
+ .claude-tool-body { padding: 0 10px 10px 10px; }
120
+ .claude-tool-section { margin-top: 8px; }
121
+ .claude-tool-section-title { color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 5px; }
122
+ .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; }
123
+ .claude-tool.error { border-color: rgba(255,107,97,0.38); background: rgba(255,59,48,0.07); }
124
+ .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; }
125
+ .claude-work-group summary { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 7px 10px; cursor: pointer; color: #d1d5db; list-style: none; }
126
+ .claude-work-group summary::-webkit-details-marker { display: none; }
127
+ .claude-work-group-title { flex: 1; min-width: 0; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
128
+ .claude-work-group-body { padding: 2px 8px 8px 8px; }
129
+ .claude-permission { border-color: rgba(255,204,0,0.4); background: rgba(255,204,0,0.08); }
130
+ .claude-permission-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; }
131
+ .claude-status { color: var(--text-dim); font-size: 12px; text-align: center; padding: 6px 0 0 0; }
132
+ #claude-control-panel { display: none; padding: 0 14px 10px 14px; box-sizing: border-box; }
133
+ .claude-control-row { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 7px; }
134
+ .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 8px; outline: none; box-sizing: border-box; }
135
+ .claude-select { appearance: none; -webkit-appearance: none; text-align: center; text-align-last: center; }
136
+ .claude-picker-wrap { min-width: 0; position: relative; }
137
+ .claude-picker-wrap select { display: none; }
138
+ .claude-picker-btn { width: 100%; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
139
+ .claude-picker-btn.active { background: rgba(0,122,255,0.22); border-color: rgba(0,122,255,0.5); }
140
+ #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); }
141
+ #claude-picker-panel.active { display: block; }
142
+ .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); }
143
+ .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; }
144
+ .claude-picker-option:last-child { border-bottom: 0; }
145
+ .claude-picker-option:active, .claude-picker-option.selected { background: rgba(0,122,255,0.18); }
146
+ .claude-picker-option-main { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 13px; font-weight: 800; }
147
+ .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; }
148
+ .claude-ctrl-btn { cursor: pointer; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap; }
149
+ .claude-ctrl-btn:active, .claude-select:focus { background: rgba(255,255,255,0.14); border-color: rgba(0,122,255,0.45); }
150
+ .claude-ctrl-btn.danger { color: #ff6b61; background: rgba(255,59,48,0.12); }
151
+ .claude-ctrl-btn.primary { color: #fff; background: rgba(0,122,255,0.32); border-color: rgba(0,122,255,0.45); }
152
+ .claude-ctrl-btn:disabled { opacity: 0.45; cursor: default; }
153
+ #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; }
154
+ #claude-state-bar::-webkit-scrollbar { display: none; }
155
+ .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; }
156
+ .claude-state-pill.warn { color: #ffcc00; border-color: rgba(255,204,0,0.24); background: rgba(255,204,0,0.08); }
157
+ #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; }
158
+ #claude-resume-panel.active { display: block; }
159
+ .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; }
160
+ .claude-resume-item:last-child { border-bottom: 0; }
161
+ .claude-resume-item:active { background: rgba(255,255,255,0.08); }
162
+ .claude-resume-title { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; font-weight: 800; }
163
+ .claude-resume-meta { color: var(--text-dim); font-size: 11px; margin-top: 3px; overflow-wrap: anywhere; }
80
164
  #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
165
  #terminal .xterm-viewport { overflow-anchor: none; }
82
166
  #terminal .xterm-helper-textarea { user-select: none !important; -webkit-user-select: none !important; -webkit-touch-callout: none; }
@@ -187,6 +271,40 @@
187
271
  <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
272
  </button>
189
273
  </div>
274
+ <div id="claude-control-panel">
275
+ <div class="claude-control-row">
276
+ <div class="claude-picker-wrap">
277
+ <select id="claude-permission-select" class="claude-select" title="Permission mode" onchange="updateClaudeSettingsFromControls()">
278
+ <option value="default">P:Def</option>
279
+ <option value="acceptEdits">P:Edit</option>
280
+ <option value="plan">P:Plan</option>
281
+ <option value="bypassPermissions">P:Byp</option>
282
+ </select>
283
+ <button id="claude-permission-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('permission')" title="Permission mode">P</button>
284
+ </div>
285
+ <div class="claude-picker-wrap">
286
+ <select id="claude-model-select" class="claude-select" title="Model" onchange="updateClaudeSettingsFromControls()">
287
+ <option value="default">M:Def</option>
288
+ </select>
289
+ <button id="claude-model-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('model')" title="Model">M</button>
290
+ </div>
291
+ <div class="claude-picker-wrap">
292
+ <select id="claude-effort-select" class="claude-select" title="Effort" onchange="updateClaudeSettingsFromControls()">
293
+ <option value="low">E:Low</option>
294
+ <option value="medium">E:Med</option>
295
+ <option value="high">E:High</option>
296
+ <option value="xhigh">E:XHi</option>
297
+ <option value="max">E:Max</option>
298
+ </select>
299
+ <button id="claude-effort-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('effort')" title="Effort">E</button>
300
+ </div>
301
+ <button id="claude-abort-btn" class="claude-ctrl-btn danger" onclick="abortClaudeSession()" title="Abort current Claude turn">Abort</button>
302
+ <button id="claude-resume-btn" class="claude-ctrl-btn primary" onclick="toggleClaudeResumePanel()" title="Choose a Claude session to resume">Resume</button>
303
+ </div>
304
+ <div id="claude-state-bar"></div>
305
+ <div id="claude-picker-panel"></div>
306
+ <div id="claude-resume-panel"></div>
307
+ </div>
190
308
  <div id="timed-send-panel">
191
309
  <div class="timed-row">
192
310
  <select id="timed-hours" onchange="updateTimedSendPreview()" title="Hours"></select>
@@ -225,6 +343,7 @@
225
343
  </div>
226
344
  <div id="terminal-controls-spacer"></div>
227
345
  <div id="terminal-container"><div id="terminal"></div></div>
346
+ <div id="claude-chat-container"></div>
228
347
  </div>
229
348
 
230
349
  <!-- Plain Text History View -->
@@ -354,6 +473,23 @@
354
473
  let timedSendRefreshTimer = null;
355
474
  let timedTagTimer = null;
356
475
  let editingTimedInputId = null;
476
+ let claudeMessages = [];
477
+ let claudePendingPermissions = [];
478
+ let claudeStatus = 'idle';
479
+ let claudeRuntimeConfig = null;
480
+ let claudePickerOpen = null;
481
+ let claudeState = {
482
+ permissionMode: 'default',
483
+ model: 'default',
484
+ effort: 'medium',
485
+ contextRemaining: null,
486
+ claudeSessionId: null,
487
+ resumeSessionId: null,
488
+ canAbort: false,
489
+ pendingPermissionCount: 0
490
+ };
491
+ let claudeResumePanelOpen = false;
492
+ let claudeResumeItemsLoaded = false;
357
493
  const modifiers = { ctrl: false };
358
494
 
359
495
  function log(msg) {
@@ -641,13 +777,657 @@
641
777
  }
642
778
  }
643
779
 
780
+ function isClaudeSession() {
781
+ return activeToolKey === 'claude-code';
782
+ }
783
+
784
+ function setClaudeModeEnabled(enabled) {
785
+ document.getElementById('terminal-container').style.display = enabled ? 'none' : '';
786
+ document.getElementById('claude-chat-container').style.display = enabled ? 'block' : 'none';
787
+ document.getElementById('claude-control-panel').style.display = enabled ? 'block' : 'none';
788
+ document.getElementById('timer-btn').style.display = enabled ? 'none' : '';
789
+ if (enabled) closeTimedSendPanel();
790
+ document.getElementById('shortcut-rail').style.display = enabled ? 'none' : '';
791
+ document.getElementById('scroll-controls').style.display = enabled ? 'none' : '';
792
+ document.getElementById('cmd-input').placeholder = enabled ? 'Message Claude...' : 'Type a message...';
793
+ updateTerminalControlsHeight();
794
+ }
795
+
796
+ function updateTerminalControlsHeight() {
797
+ if (!isClaudeSession()) {
798
+ document.documentElement.style.setProperty('--terminal-controls-rest-height', '108px');
799
+ return;
800
+ }
801
+ const restHeight = claudeResumePanelOpen ? '308px' : (claudePickerOpen ? '322px' : '154px');
802
+ document.documentElement.style.setProperty('--terminal-controls-rest-height', restHeight);
803
+ }
804
+
805
+ function shortModelLabel(value, resolved) {
806
+ const source = String(value || 'default');
807
+ const target = String(resolved || source);
808
+ const compact = target
809
+ .replace(/^claude-/, '')
810
+ .replace(/-20\d{6,}.*$/, '')
811
+ .slice(0, 9);
812
+ if (source === 'default') return 'M:Def';
813
+ if (source === 'env') return `M:${compact || 'Env'}`;
814
+ if (source === 'sonnet') return `M:${compact || 'Son'}`;
815
+ if (source === 'opus') return `M:${compact || 'Opus'}`;
816
+ if (source === 'haiku') return `M:${compact || 'Hai'}`;
817
+ return `M:${compact || source.slice(0, 9)}`;
818
+ }
819
+
820
+ function pickerLabelFromOption(option) {
821
+ return option ? (option.textContent || option.value || '') : '';
822
+ }
823
+
824
+ function pickerFullValue(option) {
825
+ if (!option) return '';
826
+ return option.dataset.resolved || '';
827
+ }
828
+
829
+ function syncClaudePickerButtons() {
830
+ const mappings = [
831
+ ['permission', 'claude-permission-select', 'claude-permission-picker-btn', 'P'],
832
+ ['model', 'claude-model-select', 'claude-model-picker-btn', 'M'],
833
+ ['effort', 'claude-effort-select', 'claude-effort-picker-btn', 'E']
834
+ ];
835
+ mappings.forEach(([type, selectId, buttonId, fixedLabel]) => {
836
+ const select = document.getElementById(selectId);
837
+ const button = document.getElementById(buttonId);
838
+ if (!select || !button) return;
839
+ const option = select.selectedOptions && select.selectedOptions[0];
840
+ const label = pickerLabelFromOption(option);
841
+ const fullValue = pickerFullValue(option);
842
+ button.textContent = fixedLabel;
843
+ button.title = fullValue || label || fixedLabel;
844
+ button.classList.toggle('active', claudePickerOpen === type);
845
+ });
846
+ }
847
+
848
+ function closeClaudePicker() {
849
+ claudePickerOpen = null;
850
+ const panel = document.getElementById('claude-picker-panel');
851
+ if (panel) {
852
+ panel.classList.remove('active');
853
+ panel.innerHTML = '';
854
+ }
855
+ syncClaudePickerButtons();
856
+ updateTerminalControlsHeight();
857
+ }
858
+
859
+ function pickerTitle(type) {
860
+ if (type === 'permission') return 'Permission mode';
861
+ if (type === 'model') return 'Model';
862
+ if (type === 'effort') return 'Effort';
863
+ return 'Options';
864
+ }
865
+
866
+ function selectIdForPicker(type) {
867
+ if (type === 'permission') return 'claude-permission-select';
868
+ if (type === 'model') return 'claude-model-select';
869
+ if (type === 'effort') return 'claude-effort-select';
870
+ return '';
871
+ }
872
+
873
+ function renderClaudePicker(type) {
874
+ const panel = document.getElementById('claude-picker-panel');
875
+ const select = document.getElementById(selectIdForPicker(type));
876
+ if (!panel || !select) return;
877
+ const options = Array.from(select.options);
878
+ panel.innerHTML = `<div class="claude-picker-title">${escapeHtml(pickerTitle(type))}</div>${options.map(option => {
879
+ const selected = option.value === select.value;
880
+ const full = pickerFullValue(option);
881
+ return `<button class="claude-picker-option${selected ? ' selected' : ''}" onclick="chooseClaudePickerOption('${type}', decodePathValue('${encodePathValue(option.value)}'))">
882
+ <div class="claude-picker-option-main">
883
+ <span>${escapeHtml(option.textContent || option.value)}</span>
884
+ ${selected ? '<span>Selected</span>' : ''}
885
+ </div>
886
+ ${full ? `<div class="claude-picker-option-value">${escapeHtml(full)}</div>` : ''}
887
+ </button>`;
888
+ }).join('')}`;
889
+ panel.classList.add('active');
890
+ syncClaudePickerButtons();
891
+ updateTerminalControlsHeight();
892
+ }
893
+
894
+ async function toggleClaudePicker(type) {
895
+ if (claudePickerOpen === type) {
896
+ closeClaudePicker();
897
+ return;
898
+ }
899
+ if (type === 'model') await refreshClaudeRuntimeConfig();
900
+ claudeResumePanelOpen = false;
901
+ const resumePanel = document.getElementById('claude-resume-panel');
902
+ if (resumePanel) resumePanel.classList.remove('active');
903
+ claudePickerOpen = type;
904
+ renderClaudePicker(type);
905
+ }
906
+
907
+ function chooseClaudePickerOption(type, value) {
908
+ const select = document.getElementById(selectIdForPicker(type));
909
+ if (!select) return;
910
+ select.value = value;
911
+ closeClaudePicker();
912
+ updateClaudeSettingsFromControls();
913
+ }
914
+
915
+ function applyClaudeRuntimeConfig(config) {
916
+ if (!config || !Array.isArray(config.models)) return;
917
+ claudeRuntimeConfig = config;
918
+ const modelEl = document.getElementById('claude-model-select');
919
+ if (modelEl) {
920
+ const current = modelEl.value || claudeState.model || config.defaultModel || 'default';
921
+ modelEl.innerHTML = '';
922
+ config.models.forEach(item => {
923
+ const option = new Option(shortModelLabel(item.value, item.resolved), item.value);
924
+ option.title = item.resolved ? `${item.label}: ${item.resolved}` : item.label;
925
+ option.dataset.resolved = item.resolved || '';
926
+ modelEl.add(option);
927
+ });
928
+ const next = Array.from(modelEl.options).some(option => option.value === current)
929
+ ? current
930
+ : (config.defaultModel || 'default');
931
+ modelEl.value = next;
932
+ claudeState.model = next;
933
+ }
934
+ if (config.defaultEffort && claudeState.effort === 'medium') {
935
+ claudeState.effort = config.defaultEffort;
936
+ }
937
+ syncClaudePickerButtons();
938
+ renderClaudeStateBar();
939
+ }
940
+
941
+ async function refreshClaudeRuntimeConfig() {
942
+ try {
943
+ const res = await fetchWithTimeout('/api/claude-config', {}, 10000);
944
+ const data = await res.json();
945
+ if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load Claude config');
946
+ applyClaudeRuntimeConfig(data.config);
947
+ return data.config;
948
+ } catch (e) {
949
+ log('Failed to load Claude config: ' + e.message);
950
+ return null;
951
+ }
952
+ }
953
+
954
+ function ensureSelectOption(selectEl, value, labelPrefix) {
955
+ const normalized = String(value || '').trim();
956
+ if (!normalized) return;
957
+ const exists = Array.from(selectEl.options).some(option => option.value === normalized);
958
+ if (!exists) {
959
+ const label = `${labelPrefix}:${normalized.replace(/^claude-/, '').replace(/-20\d{6,}.*$/, '').slice(0, 10)}`;
960
+ selectEl.add(new Option(label, normalized));
961
+ }
962
+ }
963
+
964
+ function applyClaudeState(state = {}) {
965
+ claudeState = { ...claudeState, ...state };
966
+ claudeStatus = claudeState.status || claudeStatus;
967
+ const permissionEl = document.getElementById('claude-permission-select');
968
+ const modelEl = document.getElementById('claude-model-select');
969
+ const effortEl = document.getElementById('claude-effort-select');
970
+ if (permissionEl) permissionEl.value = claudeState.permissionMode || 'default';
971
+ if (modelEl) {
972
+ ensureSelectOption(modelEl, claudeState.model || 'default', 'M');
973
+ modelEl.value = claudeState.model || 'default';
974
+ }
975
+ if (effortEl) effortEl.value = claudeState.effort || 'medium';
976
+ syncClaudePickerButtons();
977
+ const abortBtn = document.getElementById('claude-abort-btn');
978
+ if (abortBtn) abortBtn.disabled = !(claudeState.canAbort || claudeStatus === 'thinking');
979
+ renderClaudeStateBar();
980
+ }
981
+
982
+ function shortValue(value, fallback = 'N/A') {
983
+ const text = String(value || fallback);
984
+ return text.length > 16 ? text.slice(0, 13) + '...' : text;
985
+ }
986
+
987
+ function renderClaudeStateBar() {
988
+ const el = document.getElementById('claude-state-bar');
989
+ if (!el) return;
990
+ const modelEl = document.getElementById('claude-model-select');
991
+ const modelOption = modelEl && modelEl.selectedOptions && modelEl.selectedOptions[0];
992
+ const modelLabel = modelOption && modelOption.dataset.resolved
993
+ ? modelOption.dataset.resolved
994
+ : (claudeState.model || 'default');
995
+ const context = typeof claudeState.contextRemaining === 'number'
996
+ ? `Ctx ${claudeState.contextRemaining}`
997
+ : 'Ctx N/A';
998
+ const pending = Number(claudeState.pendingPermissionCount || claudePendingPermissions.filter(item => item.status === 'pending').length) || 0;
999
+ const status = claudeStatus === 'thinking' ? 'Working' : shortValue(claudeStatus || 'Idle');
1000
+ el.innerHTML = [
1001
+ `<span class="claude-state-pill">${escapeHtml(shortValue(claudeState.permissionMode || 'default'))}</span>`,
1002
+ `<span class="claude-state-pill">${escapeHtml(shortValue(modelLabel))}</span>`,
1003
+ `<span class="claude-state-pill">Eff ${escapeHtml(shortValue(claudeState.effort || 'medium'))}</span>`,
1004
+ `<span class="claude-state-pill">${escapeHtml(context)}</span>`,
1005
+ `<span class="claude-state-pill${pending ? ' warn' : ''}">${pending ? `${pending} approval` : escapeHtml(status)}</span>`
1006
+ ].join('');
1007
+ }
1008
+
1009
+ async function updateClaudeSettingsFromControls() {
1010
+ await refreshClaudeRuntimeConfig();
1011
+ const settings = {
1012
+ permissionMode: document.getElementById('claude-permission-select').value,
1013
+ model: document.getElementById('claude-model-select').value,
1014
+ effort: document.getElementById('claude-effort-select').value
1015
+ };
1016
+ applyClaudeState(settings);
1017
+ if (currentSocket && currentSocket.readyState === 1) {
1018
+ currentSocket.send(JSON.stringify({ type: 'claude-settings', settings }));
1019
+ }
1020
+ }
1021
+
1022
+ function abortClaudeSession() {
1023
+ if (!currentSocket || currentSocket.readyState !== 1) return;
1024
+ currentSocket.send(JSON.stringify({ type: 'claude-abort' }));
1025
+ }
1026
+
1027
+ async function toggleClaudeResumePanel() {
1028
+ claudeResumePanelOpen = !claudeResumePanelOpen;
1029
+ if (claudeResumePanelOpen) closeClaudePicker();
1030
+ const panel = document.getElementById('claude-resume-panel');
1031
+ panel.classList.toggle('active', claudeResumePanelOpen);
1032
+ updateTerminalControlsHeight();
1033
+ if (claudeResumePanelOpen && !claudeResumeItemsLoaded) {
1034
+ await loadClaudeResumeSessions();
1035
+ }
1036
+ }
1037
+
1038
+ async function loadClaudeResumeSessions() {
1039
+ const panel = document.getElementById('claude-resume-panel');
1040
+ if (!activeSessionId) return;
1041
+ panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading sessions...</div>';
1042
+ try {
1043
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/claude-resume-sessions`, {}, 15000);
1044
+ const data = await res.json();
1045
+ if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load resume sessions');
1046
+ const items = data.items || [];
1047
+ claudeResumeItemsLoaded = true;
1048
+ if (!items.length) {
1049
+ panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">No local Claude sessions found for this folder.</div>';
1050
+ return;
1051
+ }
1052
+ panel.innerHTML = items.map(item => {
1053
+ const encodedId = encodePathValue(item.id);
1054
+ const updated = item.updatedAt ? new Date(item.updatedAt).toLocaleString() : 'Unknown';
1055
+ const active = item.id === claudeState.resumeSessionId || item.id === claudeState.claudeSessionId;
1056
+ return `<button class="claude-resume-item" onclick="selectClaudeResumeSession(decodePathValue('${encodedId}'))">
1057
+ <div class="claude-resume-title"><span>${escapeHtml(item.id.slice(0, 8))}${active ? ' · current' : ''}</span><span>${escapeHtml(updated)}</span></div>
1058
+ <div class="claude-resume-meta">${escapeHtml(item.lastText || item.firstText || item.cwd || 'No preview')}</div>
1059
+ </button>`;
1060
+ }).join('');
1061
+ } catch (e) {
1062
+ panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px; color:#ff6b61;">${escapeHtml(e.message)}</div>`;
1063
+ }
1064
+ }
1065
+
1066
+ function selectClaudeResumeSession(id) {
1067
+ if (!id || !currentSocket || currentSocket.readyState !== 1) return;
1068
+ currentSocket.send(JSON.stringify({ type: 'claude-resume', resumeSessionId: id }));
1069
+ applyClaudeState({ resumeSessionId: id, claudeSessionId: id });
1070
+ claudeResumePanelOpen = false;
1071
+ document.getElementById('claude-resume-panel').classList.remove('active');
1072
+ updateTerminalControlsHeight();
1073
+ }
1074
+
1075
+ function textFromClaudeMessage(message) {
1076
+ if (!message) return '';
1077
+ if (message.text) return String(message.text);
1078
+ if (message.summary) return String(message.summary);
1079
+ return '';
1080
+ }
1081
+
1082
+ function escapeRegExp(text) {
1083
+ return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1084
+ }
1085
+
1086
+ function inlineMarkdown(text) {
1087
+ let html = escapeHtml(text || '');
1088
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
1089
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
1090
+ html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
1091
+ html = html.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
1092
+ html = html.replace(/_([^_\n]+)_/g, '<em>$1</em>');
1093
+ html = html.replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/g, '<img src="$2" alt="$1">');
1094
+ html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
1095
+ return html;
1096
+ }
1097
+
1098
+ function splitMarkdownBlocks(markdown) {
1099
+ const lines = String(markdown || '').replace(/\r\n/g, '\n').split('\n');
1100
+ const blocks = [];
1101
+ let i = 0;
1102
+ while (i < lines.length) {
1103
+ if (!lines[i].trim()) {
1104
+ i++;
1105
+ continue;
1106
+ }
1107
+ const fence = lines[i].match(/^```(\w+)?\s*$/);
1108
+ if (fence) {
1109
+ const language = fence[1] || '';
1110
+ const content = [];
1111
+ i++;
1112
+ while (i < lines.length && !/^```\s*$/.test(lines[i])) {
1113
+ content.push(lines[i]);
1114
+ i++;
1115
+ }
1116
+ if (i < lines.length) i++;
1117
+ blocks.push({ type: 'code', language, content: content.join('\n') });
1118
+ continue;
1119
+ }
1120
+ if (/^\s*[-*_]{3,}\s*$/.test(lines[i])) {
1121
+ blocks.push({ type: 'hr' });
1122
+ i++;
1123
+ continue;
1124
+ }
1125
+ if (/^\s{0,3}#{1,4}\s+/.test(lines[i])) {
1126
+ const match = lines[i].match(/^(\s{0,3})(#{1,4})\s+(.+)$/);
1127
+ blocks.push({ type: 'header', level: match[2].length, text: match[3] });
1128
+ i++;
1129
+ continue;
1130
+ }
1131
+ if (lines[i].includes('|') && i + 1 < lines.length && /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[i + 1])) {
1132
+ const rows = [lines[i]];
1133
+ i += 2;
1134
+ while (i < lines.length && lines[i].includes('|') && lines[i].trim()) {
1135
+ rows.push(lines[i]);
1136
+ i++;
1137
+ }
1138
+ blocks.push({ type: 'table', rows });
1139
+ continue;
1140
+ }
1141
+ if (/^\s*([-*+])\s+/.test(lines[i]) || /^\s*\d+\.\s+/.test(lines[i])) {
1142
+ const ordered = /^\s*\d+\.\s+/.test(lines[i]);
1143
+ const items = [];
1144
+ while (i < lines.length && (ordered ? /^\s*\d+\.\s+/.test(lines[i]) : /^\s*[-*+]\s+/.test(lines[i]))) {
1145
+ items.push(lines[i].replace(ordered ? /^\s*\d+\.\s+/ : /^\s*[-*+]\s+/, ''));
1146
+ i++;
1147
+ }
1148
+ blocks.push({ type: ordered ? 'ol' : 'ul', items });
1149
+ continue;
1150
+ }
1151
+ if (/^\s*>\s?/.test(lines[i])) {
1152
+ const quote = [];
1153
+ while (i < lines.length && /^\s*>\s?/.test(lines[i])) {
1154
+ quote.push(lines[i].replace(/^\s*>\s?/, ''));
1155
+ i++;
1156
+ }
1157
+ blocks.push({ type: 'quote', text: quote.join('\n') });
1158
+ continue;
1159
+ }
1160
+ const paragraph = [];
1161
+ while (i < lines.length && lines[i].trim()) {
1162
+ 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;
1163
+ paragraph.push(lines[i]);
1164
+ i++;
1165
+ }
1166
+ blocks.push({ type: 'paragraph', text: paragraph.join('\n') });
1167
+ }
1168
+ return blocks;
1169
+ }
1170
+
1171
+ function renderMarkdown(markdown) {
1172
+ const blocks = splitMarkdownBlocks(markdown);
1173
+ return `<div class="claude-md">${blocks.map(block => {
1174
+ if (block.type === 'code') {
1175
+ const language = block.language ? `<div class="claude-tool-section-title">${escapeHtml(block.language)}</div>` : '';
1176
+ return `<pre>${language}<code>${escapeHtml(block.content)}</code></pre>`;
1177
+ }
1178
+ if (block.type === 'hr') return '<hr>';
1179
+ if (block.type === 'header') return `<h${block.level}>${inlineMarkdown(block.text)}</h${block.level}>`;
1180
+ if (block.type === 'ul') return `<ul>${block.items.map(item => `<li>${inlineMarkdown(item)}</li>`).join('')}</ul>`;
1181
+ if (block.type === 'ol') return `<ol>${block.items.map(item => `<li>${inlineMarkdown(item)}</li>`).join('')}</ol>`;
1182
+ if (block.type === 'quote') return `<blockquote>${renderMarkdown(block.text)}</blockquote>`;
1183
+ if (block.type === 'table') return renderMarkdownTable(block.rows);
1184
+ return `<p>${inlineMarkdown(block.text).replace(/\n/g, '<br>')}</p>`;
1185
+ }).join('')}</div>`;
1186
+ }
1187
+
1188
+ function renderMarkdownTable(rows) {
1189
+ const parsed = rows.map(row => row.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(cell => cell.trim()));
1190
+ if (!parsed.length) return '';
1191
+ const [head, ...body] = parsed;
1192
+ 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>`;
1193
+ }
1194
+
1195
+ function parseLocalCommandMessage(text) {
1196
+ const value = String(text || '');
1197
+ if (/^\s*<local-command-caveat>[\s\S]*?<\/local-command-caveat>\s*$/.test(value)) return { kind: 'hidden' };
1198
+ const stdoutMatch = value.match(/^\s*<local-command-stdout>\s*([\s\S]*?)\s*<\/local-command-stdout>\s*$/);
1199
+ if (stdoutMatch) {
1200
+ const stdout = stdoutMatch[1].trim();
1201
+ if (/^Goal set:/i.test(stdout)) return { kind: 'hidden' };
1202
+ return { kind: 'text', text: stdout };
1203
+ }
1204
+ const raw = value.trim().match(/^\/([a-zA-Z][\w:-]*)(?:\s+([\s\S]*?))?$/);
1205
+ if (raw) {
1206
+ return { kind: 'command', commandName: raw[1], args: raw[2] && raw[2].trim() };
1207
+ }
1208
+ const nameMatch = value.match(/<command-name>\s*\/?([^<]+?)\s*<\/command-name>/);
1209
+ if (nameMatch) {
1210
+ const argsMatch = value.match(/<command-args>\s*([\s\S]*?)\s*<\/command-args>/);
1211
+ const stripped = value
1212
+ .replace(/<command-message>[\s\S]*?<\/command-message>/g, '')
1213
+ .replace(/<command-name>[\s\S]*?<\/command-name>/g, '')
1214
+ .replace(/<command-args>[\s\S]*?<\/command-args>/g, '')
1215
+ .trim();
1216
+ if (!stripped) return { kind: 'command', commandName: nameMatch[1].trim(), args: argsMatch && argsMatch[1].trim() };
1217
+ return { kind: 'text', text: stripped };
1218
+ }
1219
+ return { kind: 'text', text: value };
1220
+ }
1221
+
1222
+ function toolCategory(name) {
1223
+ if (['Bash', 'CodexBash', 'execute'].includes(name)) return 'terminal';
1224
+ if (['Edit', 'MultiEdit', 'Write', 'NotebookEdit'].includes(name)) return 'edit';
1225
+ if (['Read', 'LS'].includes(name)) return 'read';
1226
+ if (['Grep', 'Glob'].includes(name)) return 'search';
1227
+ if (['WebFetch', 'WebSearch'].includes(name)) return 'web';
1228
+ if (['Task', 'Agent'].includes(name)) return 'task';
1229
+ return 'other';
1230
+ }
1231
+
1232
+ function toolIcon(category) {
1233
+ const labels = { terminal: '$', edit: '+/-', read: 'R', search: '?', web: 'W', task: 'A', other: '*' };
1234
+ return labels[category] || '*';
1235
+ }
1236
+
1237
+ function toolCommand(tool) {
1238
+ const input = tool && tool.input;
1239
+ if (!input || typeof input !== 'object') return '';
1240
+ if (typeof input.command === 'string') return input.command;
1241
+ if (Array.isArray(input.command)) return input.command.join(' ');
1242
+ if (typeof input.file_path === 'string') return input.file_path;
1243
+ if (typeof input.path === 'string') return input.path;
1244
+ if (typeof input.pattern === 'string') return input.pattern;
1245
+ if (typeof input.prompt === 'string') return input.prompt;
1246
+ return tool.summary || '';
1247
+ }
1248
+
1249
+ function toolTitle(tool) {
1250
+ const name = tool.name || 'Tool';
1251
+ const input = tool.input || {};
1252
+ if (name === 'Bash') return (tool.description || 'Terminal');
1253
+ if (name === 'Read' && input.file_path) return input.file_path;
1254
+ if (name === 'Edit' || name === 'MultiEdit' || name === 'Write') return input.file_path || name;
1255
+ if (name === 'Grep') return input.pattern ? `grep(pattern: ${input.pattern})` : 'Search Content';
1256
+ if (name === 'Glob') return input.pattern || 'Search Files';
1257
+ if ((name === 'Task' || name === 'Agent') && input.description) return input.description;
1258
+ if (name.startsWith('mcp__')) return name.replace(/^mcp__/, '').replace(/__/g, ': ');
1259
+ return name;
1260
+ }
1261
+
1262
+ function renderToolSection(title, value) {
1263
+ if (value === undefined || value === null || value === '') return '';
1264
+ const code = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
1265
+ return `<div class="claude-tool-section"><div class="claude-tool-section-title">${escapeHtml(title)}</div><pre class="claude-tool-code">${escapeHtml(code)}</pre></div>`;
1266
+ }
1267
+
1268
+ function renderClaudeTool(tool) {
1269
+ const category = toolCategory(tool.name || '');
1270
+ const command = toolCommand(tool);
1271
+ const hasResult = tool.resultText || tool.isError;
1272
+ const running = claudeStatus === 'thinking' && !hasResult;
1273
+ const status = tool.isError ? 'error' : (hasResult ? 'completed' : (running ? 'running' : 'running'));
1274
+ const open = tool.isError || (!hasResult && !running) ? ' open' : '';
1275
+ const compact = category === 'terminal' || category === 'search' || category === 'read';
1276
+ return `<details class="claude-tool claude-tool-card${tool.isError ? ' error' : ''}"${open}>
1277
+ <summary class="claude-tool-header">
1278
+ <span class="claude-tool-icon" data-icon="${escapeHtml(toolIcon(category))}"></span>
1279
+ <span class="claude-tool-title">${escapeHtml(toolTitle(tool))}</span>
1280
+ <span class="claude-tool-status">${escapeHtml(status)}</span>
1281
+ <span class="claude-tool-command">${escapeHtml(command)}</span>
1282
+ </summary>
1283
+ ${compact && !tool.isError ? '' : `<div class="claude-tool-body">
1284
+ ${renderToolSection('Input', tool.input)}
1285
+ ${renderToolSection(tool.isError ? 'Error' : 'Output', tool.resultText)}
1286
+ </div>`}
1287
+ </details>`;
1288
+ }
1289
+
1290
+ function renderWorkGroup(items) {
1291
+ const running = items.some(item => item.kind === 'tool' && !item.resultText);
1292
+ const label = `${items.length} tool call${items.length > 1 ? 's' : ''}`;
1293
+ return `<details class="claude-work-group"${running ? ' open' : ''}>
1294
+ <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>
1295
+ <div class="claude-work-group-body">${items.map(renderDisplayItem).join('')}</div>
1296
+ </details>`;
1297
+ }
1298
+
1299
+ function buildClaudeDisplayItems(messages) {
1300
+ const items = [];
1301
+ const byToolUseId = new Map();
1302
+ for (const message of messages) {
1303
+ if (!message) continue;
1304
+ if (message.kind === 'tool') {
1305
+ const item = { ...message, kind: 'tool', resultText: '', isError: false };
1306
+ items.push(item);
1307
+ if (item.toolUseId) byToolUseId.set(item.toolUseId, item);
1308
+ continue;
1309
+ }
1310
+ if (message.kind === 'tool-result') {
1311
+ const parent = message.toolUseId && byToolUseId.get(message.toolUseId);
1312
+ if (parent) {
1313
+ parent.resultText = message.text || '';
1314
+ parent.isError = Boolean(message.isError);
1315
+ } else {
1316
+ items.push(message);
1317
+ }
1318
+ continue;
1319
+ }
1320
+ items.push(message);
1321
+ }
1322
+
1323
+ const grouped = [];
1324
+ let run = [];
1325
+ const flush = () => {
1326
+ if (run.length > 1) grouped.push({ kind: 'work-group', items: run });
1327
+ else if (run.length === 1) grouped.push(run[0]);
1328
+ run = [];
1329
+ };
1330
+ for (const item of items) {
1331
+ if (item.kind === 'tool') {
1332
+ run.push(item);
1333
+ continue;
1334
+ }
1335
+ flush();
1336
+ grouped.push(item);
1337
+ }
1338
+ flush();
1339
+ return grouped;
1340
+ }
1341
+
1342
+ function renderUserMessage(message) {
1343
+ const parsed = parseLocalCommandMessage(textFromClaudeMessage(message));
1344
+ if (parsed.kind === 'hidden') return '';
1345
+ if (parsed.kind === 'command') {
1346
+ const args = parsed.args ? `<div class="claude-message user">${renderMarkdown(parsed.args)}</div>` : '';
1347
+ return `${args}<div class="claude-message user"><span class="claude-command-chip">/${escapeHtml(parsed.commandName)}</span></div>`;
1348
+ }
1349
+ return `<div class="claude-message user">${renderMarkdown(parsed.text)}</div>`;
1350
+ }
1351
+
1352
+ function renderDisplayItem(message) {
1353
+ if (!message) return '';
1354
+ if (message.kind === 'user') return renderUserMessage(message);
1355
+ if (message.kind === 'assistant') return `<div class="claude-message assistant">${renderMarkdown(textFromClaudeMessage(message))}</div>`;
1356
+ if (message.kind === 'tool') return renderClaudeTool(message);
1357
+ if (message.kind === 'tool-result') return `<div class="claude-tool${message.isError ? ' error' : ''}">${renderToolSection(message.isError ? 'Error' : 'Output', message.text || '')}</div>`;
1358
+ if (message.kind === 'work-group') return renderWorkGroup(message.items);
1359
+ if (message.kind === 'event') return `<div class="claude-message event${message.level === 'error' ? ' error' : ''}">${escapeHtml(message.text || '')}</div>`;
1360
+ return '';
1361
+ }
1362
+
1363
+ function renderClaudeChat() {
1364
+ const container = document.getElementById('claude-chat-container');
1365
+ const parts = buildClaudeDisplayItems(claudeMessages).map(renderDisplayItem);
1366
+ for (const req of claudePendingPermissions.filter(item => item.status === 'pending')) {
1367
+ parts.push(`<div class="claude-tool claude-permission">
1368
+ <div class="claude-tool-header">
1369
+ <span class="claude-tool-icon" data-icon="!"></span>
1370
+ <strong>${escapeHtml(req.title || req.toolName || 'Permission required')}</strong>
1371
+ </div>
1372
+ <div class="claude-tool-body">
1373
+ ${req.reason ? `<div>${escapeHtml(req.reason)}</div>` : ''}
1374
+ ${renderToolSection('Input', req.input || {})}
1375
+ <div class="claude-permission-actions">
1376
+ <button class="small-btn danger" onclick="respondClaudePermission('${req.id}', false)">Deny</button>
1377
+ <button class="small-btn primary" onclick="respondClaudePermission('${req.id}', true)">Allow</button>
1378
+ </div>
1379
+ </div>
1380
+ </div>`);
1381
+ }
1382
+ if (claudeStatus === 'thinking') {
1383
+ parts.push('<div class="claude-status">Claude is working...</div>');
1384
+ }
1385
+ container.innerHTML = parts.join('') || '<div class="claude-message event">Send a message to start Claude.</div>';
1386
+ renderClaudeStateBar();
1387
+ requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
1388
+ }
1389
+
1390
+ function applyClaudeEvent(event) {
1391
+ if (!event) return;
1392
+ if (event.type === 'message' && event.message) {
1393
+ claudeMessages.push(event.message);
1394
+ } else if (event.type === 'status') {
1395
+ claudeStatus = event.status || 'idle';
1396
+ } else if (event.type === 'permission-request' && event.request) {
1397
+ claudePendingPermissions = claudePendingPermissions.filter(item => item.id !== event.request.id);
1398
+ claudePendingPermissions.push(event.request);
1399
+ } else if (event.type === 'permission-updated' && event.request) {
1400
+ claudePendingPermissions = claudePendingPermissions.map(item => item.id === event.request.id ? event.request : item);
1401
+ } else if (event.type === 'history-reset' && Array.isArray(event.messages)) {
1402
+ claudeMessages = event.messages;
1403
+ } else if (event.type === 'state' && event.state) {
1404
+ applyClaudeState(event.state);
1405
+ }
1406
+ if (event.type !== 'state') applyClaudeState({
1407
+ status: claudeStatus,
1408
+ pendingPermissionCount: claudePendingPermissions.filter(item => item.status === 'pending').length,
1409
+ canAbort: claudeStatus === 'thinking'
1410
+ });
1411
+ renderClaudeChat();
1412
+ }
1413
+
1414
+ function respondClaudePermission(id, approved) {
1415
+ if (!currentSocket || currentSocket.readyState !== 1) return;
1416
+ currentSocket.send(JSON.stringify({ type: 'claude-permission', id, approved }));
1417
+ }
1418
+
644
1419
  async function createSession(toolKey, sessionName) {
645
1420
  try {
646
1421
  const workingDirectory = document.getElementById('cwd-field').value;
1422
+ const runtimeConfig = toolKey === 'claude-code' ? await refreshClaudeRuntimeConfig() : null;
1423
+ const claudeOptions = runtimeConfig ? {
1424
+ model: runtimeConfig.defaultModel || 'default',
1425
+ effort: runtimeConfig.defaultEffort || 'medium'
1426
+ } : undefined;
647
1427
  const res = await fetchWithTimeout('/api/sessions', {
648
1428
  method: 'POST',
649
1429
  headers: { 'Content-Type': 'application/json' },
650
- body: JSON.stringify({ toolKey, workingDirectory })
1430
+ body: JSON.stringify({ toolKey, workingDirectory, claudeOptions })
651
1431
  });
652
1432
  const data = await res.json();
653
1433
  if (!res.ok || !data.id) throw new Error(data.error || 'Failed to create session');
@@ -972,10 +1752,16 @@
972
1752
  updateToolShortcuts(activeToolKey);
973
1753
  document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
974
1754
  document.getElementById('terminal-view').classList.add('active');
1755
+ setClaudeModeEnabled(isClaudeSession());
1756
+ if (currentSocket) { currentSocket.close(); currentSocket = null; }
975
1757
  initTerminal(id);
976
1758
  }
977
1759
 
978
1760
  function initTerminal(sessionId) {
1761
+ if (isClaudeSession()) {
1762
+ initClaudeSession(sessionId);
1763
+ return;
1764
+ }
979
1765
  if (typeof Terminal === 'undefined') {
980
1766
  alert('Terminal library not loaded yet.');
981
1767
  showLobby();
@@ -1013,6 +1799,52 @@
1013
1799
  };
1014
1800
  }
1015
1801
 
1802
+ async function initClaudeSession(sessionId) {
1803
+ claudeMessages = [];
1804
+ claudePendingPermissions = [];
1805
+ claudeStatus = 'idle';
1806
+ const runtimeConfig = await refreshClaudeRuntimeConfig();
1807
+ claudeState = {
1808
+ permissionMode: 'default',
1809
+ model: (runtimeConfig && runtimeConfig.defaultModel) || 'default',
1810
+ effort: (runtimeConfig && runtimeConfig.defaultEffort) || 'medium',
1811
+ contextRemaining: null,
1812
+ claudeSessionId: null,
1813
+ resumeSessionId: null,
1814
+ canAbort: false,
1815
+ pendingPermissionCount: 0
1816
+ };
1817
+ claudeResumePanelOpen = false;
1818
+ claudePickerOpen = null;
1819
+ claudeResumeItemsLoaded = false;
1820
+ document.getElementById('claude-resume-panel').classList.remove('active');
1821
+ document.getElementById('claude-resume-panel').innerHTML = '';
1822
+ closeClaudePicker();
1823
+ updateTerminalControlsHeight();
1824
+ applyClaudeRuntimeConfig(runtimeConfig);
1825
+ applyClaudeState(claudeState);
1826
+ renderClaudeChat();
1827
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1828
+ currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
1829
+ currentSocket.onopen = () => logViewSnapshot('claude-ws-open');
1830
+ currentSocket.onmessage = (e) => {
1831
+ const msg = JSON.parse(e.data);
1832
+ if (msg.type === 'claude-snapshot' && msg.snapshot) {
1833
+ claudeMessages = msg.snapshot.messages || [];
1834
+ claudePendingPermissions = msg.snapshot.pendingPermissions || [];
1835
+ claudeStatus = msg.snapshot.status || 'idle';
1836
+ applyClaudeState(msg.snapshot.state || {
1837
+ status: claudeStatus,
1838
+ pendingPermissionCount: claudePendingPermissions.filter(item => item.status === 'pending').length,
1839
+ canAbort: claudeStatus === 'thinking'
1840
+ });
1841
+ renderClaudeChat();
1842
+ }
1843
+ if (msg.type === 'claude-event') applyClaudeEvent(msg.event);
1844
+ if (msg.type === 'exit') showLobby();
1845
+ };
1846
+ }
1847
+
1016
1848
  function sendWS(data) {
1017
1849
  if (currentSocket && currentSocket.readyState === 1) currentSocket.send(JSON.stringify({ type: 'input', data }));
1018
1850
  }
@@ -1223,7 +2055,15 @@
1223
2055
 
1224
2056
  function performSend() {
1225
2057
  const val = inputEl.value;
1226
- if (val) {
2058
+ if (val && isClaudeSession()) {
2059
+ if (currentSocket && currentSocket.readyState === 1) {
2060
+ currentSocket.send(JSON.stringify({ type: 'claude-input', text: val }));
2061
+ }
2062
+ inputEl.value = '';
2063
+ inputEl.style.height = '38px';
2064
+ return;
2065
+ }
2066
+ if (val) {
1227
2067
  const formattedVal = val.replace(/\n/g, '\r');
1228
2068
  sendWS(formattedVal);
1229
2069
  inputEl.value = '';