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