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.
- package/lib/claude/config.js +82 -0
- package/lib/claude/structured-session.js +516 -0
- package/lib/commands/web.js +70 -12
- package/lib/session/session-manager.js +317 -2
- package/lib/web/index.html +842 -2
- package/package.json +2 -1
package/lib/web/index.html
CHANGED
|
@@ -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 = '';
|