glad-web 1.0.29 → 1.0.30
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/cli-usage.js +95 -0
- package/lib/claude/structured-session.js +223 -51
- package/lib/claude/transcript-repository.js +216 -0
- package/lib/codex/image-store.js +175 -0
- package/lib/commands/web.js +52 -234
- package/lib/server/routes/providers.js +103 -0
- package/lib/server/routes/schedules.js +54 -0
- package/lib/server/routes/workspace.js +77 -0
- package/lib/session/session-manager.js +111 -340
- package/lib/web/claude.js +1074 -0
- package/lib/web/codex.js +475 -0
- package/lib/web/composer.js +230 -0
- package/lib/web/core.js +327 -0
- package/lib/web/git.js +533 -0
- package/lib/web/index.html +45 -3672
- package/lib/web/schedules.js +245 -0
- package/lib/web/session.js +351 -0
- package/lib/web/shell.js +56 -0
- package/lib/web/styles.css +377 -0
- package/lib/web/terminal-scroll.js +81 -0
- package/lib/web/timed-inputs.js +223 -0
- package/lib/workspace/service.js +3 -2
- package/package.json +10 -5
- package/scripts/check-syntax.js +26 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
async function ensureScheduleTools() {
|
|
2
|
+
if (scheduleTools.length) return;
|
|
3
|
+
const res = await fetchWithTimeout('/api/tools');
|
|
4
|
+
scheduleTools = await res.json();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function defaultSchedule() {
|
|
8
|
+
return {
|
|
9
|
+
id: null,
|
|
10
|
+
name: 'Scheduled Task',
|
|
11
|
+
enabled: true,
|
|
12
|
+
schedule: { time: '09:00', weekdays: [1, 2, 3, 4, 5] },
|
|
13
|
+
target: { toolKey: (scheduleTools[0] && scheduleTools[0].key) || 'demo', workingDirectory: '' },
|
|
14
|
+
steps: [
|
|
15
|
+
{ type: 'sleep', seconds: 60 },
|
|
16
|
+
{ type: 'sendText', text: 'hello' },
|
|
17
|
+
{ type: 'sleep', seconds: 1 },
|
|
18
|
+
{ type: 'sendKey', key: 'enter' },
|
|
19
|
+
{ type: 'sleep', seconds: 60 },
|
|
20
|
+
{ type: 'closeSession' }
|
|
21
|
+
]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function showScheduleModal(job = null) {
|
|
26
|
+
try {
|
|
27
|
+
await ensureScheduleTools();
|
|
28
|
+
loadAppConfig();
|
|
29
|
+
} catch (e) {
|
|
30
|
+
alert('Failed to load tools');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const data = job || defaultSchedule();
|
|
35
|
+
editingScheduleId = data.id || null;
|
|
36
|
+
editingSteps = (data.steps || []).map(step => ({ ...step }));
|
|
37
|
+
selectedWeekdays = [...(data.schedule.weekdays || [1, 2, 3, 4, 5])];
|
|
38
|
+
document.getElementById('schedule-modal-title').textContent = editingScheduleId ? 'Edit Schedule' : 'New Schedule';
|
|
39
|
+
document.getElementById('schedule-name').value = data.name || 'Scheduled Task';
|
|
40
|
+
document.getElementById('schedule-cwd').value = data.target.workingDirectory || '';
|
|
41
|
+
document.getElementById('schedule-time').value = data.schedule.time || '09:00';
|
|
42
|
+
|
|
43
|
+
const toolSelect = document.getElementById('schedule-tool');
|
|
44
|
+
toolSelect.innerHTML = scheduleTools.map(t => `<option value="${escapeHtml(t.key)}">${escapeHtml(t.displayName)}</option>`).join('');
|
|
45
|
+
const requestedTool = data.target.toolKey || '';
|
|
46
|
+
toolSelect.value = scheduleTools.some(t => t.key === requestedTool) ? requestedTool : ((scheduleTools[0] && scheduleTools[0].key) || 'demo');
|
|
47
|
+
renderWeekdays();
|
|
48
|
+
renderScheduleSteps();
|
|
49
|
+
document.getElementById('schedule-modal-overlay').style.display = 'flex';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function closeScheduleModal(e) {
|
|
53
|
+
if (!e || e.target.id === 'schedule-modal-overlay') {
|
|
54
|
+
document.getElementById('schedule-modal-overlay').style.display = 'none';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderWeekdays() {
|
|
59
|
+
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
60
|
+
document.getElementById('schedule-weekdays').innerHTML = labels.map((label, day) => {
|
|
61
|
+
const active = selectedWeekdays.includes(day);
|
|
62
|
+
return `<button type="button" class="weekday-btn ${active ? 'active' : ''}" onclick="toggleWeekday(${day})">${label}</button>`;
|
|
63
|
+
}).join('');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toggleWeekday(day) {
|
|
67
|
+
if (selectedWeekdays.includes(day)) {
|
|
68
|
+
selectedWeekdays = selectedWeekdays.filter(value => value !== day);
|
|
69
|
+
} else {
|
|
70
|
+
selectedWeekdays.push(day);
|
|
71
|
+
selectedWeekdays.sort((a, b) => a - b);
|
|
72
|
+
}
|
|
73
|
+
renderWeekdays();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function addScheduleStep(type) {
|
|
77
|
+
if (!type) return;
|
|
78
|
+
const defaults = {
|
|
79
|
+
sleep: { type: 'sleep', seconds: 1 },
|
|
80
|
+
sendText: { type: 'sendText', text: '' },
|
|
81
|
+
sendKey: { type: 'sendKey', key: 'enter' },
|
|
82
|
+
keyDown: { type: 'keyDown', key: 'ctrl' },
|
|
83
|
+
keyUp: { type: 'keyUp', key: 'ctrl' },
|
|
84
|
+
stop: { type: 'stop' },
|
|
85
|
+
closeSession: { type: 'closeSession' }
|
|
86
|
+
};
|
|
87
|
+
editingSteps.push(defaults[type] || { type });
|
|
88
|
+
renderScheduleSteps();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function updateStep(index, field, value) {
|
|
92
|
+
if (!editingSteps[index]) return;
|
|
93
|
+
if (field === 'seconds') editingSteps[index][field] = Math.max(0, Number(value) || 0);
|
|
94
|
+
else editingSteps[index][field] = value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function moveStep(index, delta) {
|
|
98
|
+
const nextIndex = index + delta;
|
|
99
|
+
if (nextIndex < 0 || nextIndex >= editingSteps.length) return;
|
|
100
|
+
const [step] = editingSteps.splice(index, 1);
|
|
101
|
+
editingSteps.splice(nextIndex, 0, step);
|
|
102
|
+
renderScheduleSteps();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function copyStep(index) {
|
|
106
|
+
editingSteps.splice(index + 1, 0, { ...editingSteps[index] });
|
|
107
|
+
renderScheduleSteps();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function removeStep(index) {
|
|
111
|
+
editingSteps.splice(index, 1);
|
|
112
|
+
renderScheduleSteps();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function stepInputHtml(step, index) {
|
|
116
|
+
if (step.type === 'sleep') {
|
|
117
|
+
return `<input type="number" min="0" step="0.1" value="${escapeHtml(step.seconds || 0)}" onchange="updateStep(${index}, 'seconds', this.value)">`;
|
|
118
|
+
}
|
|
119
|
+
if (step.type === 'sendText') {
|
|
120
|
+
return `<textarea rows="2" placeholder="Text to send" onchange="updateStep(${index}, 'text', this.value)" oninput="updateStep(${index}, 'text', this.value)">${escapeHtml(step.text || '')}</textarea>`;
|
|
121
|
+
}
|
|
122
|
+
if (step.type === 'sendKey') {
|
|
123
|
+
return `<select onchange="updateStep(${index}, 'key', this.value)">
|
|
124
|
+
${['enter','tab','esc','up','down','left','right','backspace','delete','home','end','ctrl+c','ctrl+d','ctrl+l'].map(key => `<option value="${key}" ${step.key === key ? 'selected' : ''}>${key}</option>`).join('')}
|
|
125
|
+
</select>`;
|
|
126
|
+
}
|
|
127
|
+
if (step.type === 'keyDown' || step.type === 'keyUp') {
|
|
128
|
+
return `<select onchange="updateStep(${index}, 'key', this.value)">
|
|
129
|
+
${['ctrl','alt'].map(key => `<option value="${key}" ${step.key === key ? 'selected' : ''}>${key}</option>`).join('')}
|
|
130
|
+
</select>`;
|
|
131
|
+
}
|
|
132
|
+
if (step.type === 'closeSession') {
|
|
133
|
+
return '<span style="color:var(--text-dim); font-size:13px;">Close the session and end this run</span>';
|
|
134
|
+
}
|
|
135
|
+
return '<span style="color:var(--text-dim); font-size:13px;">End this run</span>';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderScheduleSteps() {
|
|
139
|
+
const container = document.getElementById('schedule-steps');
|
|
140
|
+
if (!editingSteps.length) {
|
|
141
|
+
container.innerHTML = '<p style="color:var(--text-dim);">No steps yet.</p>';
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
container.innerHTML = editingSteps.map((step, index) => `
|
|
145
|
+
<div class="step-card">
|
|
146
|
+
<div class="step-grid">
|
|
147
|
+
<select onchange="editingSteps[${index}] = { type: this.value }; addDefaultStepFields(${index}); renderScheduleSteps();">
|
|
148
|
+
${['sleep','sendText','sendKey','keyDown','keyUp','stop','closeSession'].map(type => `<option value="${type}" ${step.type === type ? 'selected' : ''}>${type}</option>`).join('')}
|
|
149
|
+
</select>
|
|
150
|
+
<div>${stepInputHtml(step, index)}</div>
|
|
151
|
+
<div style="display:flex; gap:6px; justify-content:flex-end;">
|
|
152
|
+
<button class="small-btn" onclick="moveStep(${index}, -1)">↑</button>
|
|
153
|
+
<button class="small-btn" onclick="moveStep(${index}, 1)">↓</button>
|
|
154
|
+
<button class="small-btn" onclick="copyStep(${index})">Copy</button>
|
|
155
|
+
<button class="small-btn danger" onclick="removeStep(${index})">Del</button>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
`).join('');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function addDefaultStepFields(index) {
|
|
163
|
+
const type = editingSteps[index].type;
|
|
164
|
+
if (type === 'sleep') editingSteps[index].seconds = 1;
|
|
165
|
+
if (type === 'sendText') editingSteps[index].text = '';
|
|
166
|
+
if (type === 'sendKey') editingSteps[index].key = 'enter';
|
|
167
|
+
if (type === 'keyDown' || type === 'keyUp') editingSteps[index].key = 'ctrl';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function collectSchedulePayload() {
|
|
171
|
+
if (!selectedWeekdays.length) throw new Error('Select at least one weekday');
|
|
172
|
+
return {
|
|
173
|
+
name: document.getElementById('schedule-name').value.trim() || 'Scheduled Task',
|
|
174
|
+
enabled: true,
|
|
175
|
+
schedule: {
|
|
176
|
+
time: document.getElementById('schedule-time').value || '09:00',
|
|
177
|
+
weekdays: selectedWeekdays
|
|
178
|
+
},
|
|
179
|
+
target: {
|
|
180
|
+
toolKey: document.getElementById('schedule-tool').value,
|
|
181
|
+
workingDirectory: document.getElementById('schedule-cwd').value.trim()
|
|
182
|
+
},
|
|
183
|
+
steps: editingSteps
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function saveSchedule() {
|
|
188
|
+
try {
|
|
189
|
+
const payload = collectSchedulePayload();
|
|
190
|
+
const url = editingScheduleId ? `/api/schedules/${editingScheduleId}` : '/api/schedules';
|
|
191
|
+
const method = editingScheduleId ? 'PATCH' : 'POST';
|
|
192
|
+
const res = await fetchWithTimeout(url, {
|
|
193
|
+
method,
|
|
194
|
+
headers: { 'Content-Type': 'application/json' },
|
|
195
|
+
body: JSON.stringify(payload)
|
|
196
|
+
});
|
|
197
|
+
if (!res.ok) throw new Error((await res.json()).error || 'Save failed');
|
|
198
|
+
closeScheduleModal();
|
|
199
|
+
switchLobbyTab('schedules');
|
|
200
|
+
} catch (e) {
|
|
201
|
+
alert(e.message);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function editSchedule(id) {
|
|
206
|
+
const res = await fetchWithTimeout(`/api/schedules/${id}`);
|
|
207
|
+
if (!res.ok) return alert('Failed to load schedule');
|
|
208
|
+
showScheduleModal(await res.json());
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function simulateSchedule(id) {
|
|
212
|
+
try {
|
|
213
|
+
const res = await fetchWithTimeout(`/api/schedules/${id}/simulate`, { method: 'POST' }, 30000);
|
|
214
|
+
const data = await res.json();
|
|
215
|
+
if (!res.ok) throw new Error(data.error || 'Test failed');
|
|
216
|
+
await loadSchedules();
|
|
217
|
+
if (data.sessionId) {
|
|
218
|
+
const jobRes = await fetchWithTimeout(`/api/schedules/${id}`).catch(() => null);
|
|
219
|
+
const job = jobRes && jobRes.ok ? await jobRes.json() : null;
|
|
220
|
+
joinSession(data.sessionId, 'Schedule Test', job?.target?.toolKey || null);
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
alert(e.message);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function duplicateSchedule(id) {
|
|
228
|
+
await fetchWithTimeout(`/api/schedules/${id}/duplicate`, { method: 'POST' });
|
|
229
|
+
loadSchedules();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function toggleSchedule(id, enabled) {
|
|
233
|
+
await fetchWithTimeout(`/api/schedules/${id}/enabled`, {
|
|
234
|
+
method: 'PATCH',
|
|
235
|
+
headers: { 'Content-Type': 'application/json' },
|
|
236
|
+
body: JSON.stringify({ enabled })
|
|
237
|
+
});
|
|
238
|
+
loadSchedules();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function deleteSchedule(id) {
|
|
242
|
+
if (!confirm('Delete scheduled task?')) return;
|
|
243
|
+
await fetchWithTimeout(`/api/schedules/${id}`, { method: 'DELETE' });
|
|
244
|
+
loadSchedules();
|
|
245
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
function joinSession(id, sessionName, toolKey = null) {
|
|
2
|
+
if (typeof clearImageAttachments === 'function' && selectedImageAttachments.length) {
|
|
3
|
+
void clearImageAttachments();
|
|
4
|
+
}
|
|
5
|
+
stopTimedInputTimers();
|
|
6
|
+
activeSessionId = id;
|
|
7
|
+
window.activeSessionId = id;
|
|
8
|
+
activeToolKey = toolKey;
|
|
9
|
+
clearTimeout(sessionPollTimer);
|
|
10
|
+
sessionPollTimer = null;
|
|
11
|
+
markCompletionRead(id);
|
|
12
|
+
loadTimedInputs();
|
|
13
|
+
document.getElementById('session-title').innerText = sessionName;
|
|
14
|
+
updateToolShortcuts(activeToolKey);
|
|
15
|
+
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
16
|
+
document.getElementById('terminal-view').classList.add('active');
|
|
17
|
+
setClaudeModeEnabled(isClaudeSession());
|
|
18
|
+
if (currentSocket) { currentSocket.close(); currentSocket = null; }
|
|
19
|
+
initTerminal(id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function initTerminal(sessionId) {
|
|
23
|
+
if (isClaudeSession()) {
|
|
24
|
+
initClaudeSession(sessionId);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (isCodexSession() && codexState.presentation === 'structured') {
|
|
28
|
+
initCodexSession(sessionId);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (typeof Terminal === 'undefined') {
|
|
32
|
+
alert('Terminal library not loaded yet.');
|
|
33
|
+
showLobby();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
function isTerminalAutoResponse(data) {
|
|
37
|
+
return /^\x1b\[\?[\d;]*c$/.test(data) || /^\x1b\[>[\d;]*c$/.test(data);
|
|
38
|
+
}
|
|
39
|
+
if (!term) {
|
|
40
|
+
term = new Terminal({ theme: { background: '#000', foreground: '#fff', cursor: '#0f0' }, cursorBlink: true, fontSize: 14, cursorStyle: 'bar', scrollback: 10000 });
|
|
41
|
+
fitAddon = new FitAddon.FitAddon();
|
|
42
|
+
term.loadAddon(fitAddon);
|
|
43
|
+
term.open(document.getElementById('terminal'));
|
|
44
|
+
term.onData(data => {
|
|
45
|
+
if (isTerminalAutoResponse(data)) return;
|
|
46
|
+
let finalData = data;
|
|
47
|
+
if (modifiers.ctrl && data.length === 1) {
|
|
48
|
+
const code = data.toLowerCase().charCodeAt(0);
|
|
49
|
+
if (code >= 97 && code <= 122) finalData = String.fromCharCode(code - 96);
|
|
50
|
+
}
|
|
51
|
+
sendWS(finalData);
|
|
52
|
+
});
|
|
53
|
+
} else { term.clear(); }
|
|
54
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
55
|
+
currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
|
|
56
|
+
currentSocket.onopen = () => {
|
|
57
|
+
syncLayout();
|
|
58
|
+
setTimeout(() => logViewSnapshot('ws-open'), 300);
|
|
59
|
+
};
|
|
60
|
+
currentSocket.onmessage = (e) => {
|
|
61
|
+
const msg = JSON.parse(e.data);
|
|
62
|
+
if (msg.type === 'output') term.write(msg.data);
|
|
63
|
+
if (msg.type === 'reset') term.reset();
|
|
64
|
+
if (msg.type === 'codex-event' && isCodexSession()) {
|
|
65
|
+
applyCodexEvent(msg.event);
|
|
66
|
+
if (msg.event?.type === 'presentation' && msg.event.presentation === 'structured') {
|
|
67
|
+
currentSocket.close();
|
|
68
|
+
currentSocket = null;
|
|
69
|
+
initCodexSession(sessionId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (msg.type === 'exit') showLobby();
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function initClaudeSession(sessionId) {
|
|
77
|
+
claudeMessages = [];
|
|
78
|
+
claudePendingPermissions = [];
|
|
79
|
+
claudeStatus = 'idle';
|
|
80
|
+
const runtimeConfig = await refreshClaudeRuntimeConfig();
|
|
81
|
+
claudeState = {
|
|
82
|
+
permissionMode: 'default',
|
|
83
|
+
model: (runtimeConfig && runtimeConfig.defaultModel) || 'default',
|
|
84
|
+
effort: (runtimeConfig && runtimeConfig.defaultEffort) || 'medium',
|
|
85
|
+
claudeSessionId: null,
|
|
86
|
+
resumeSessionId: null,
|
|
87
|
+
canAbort: false,
|
|
88
|
+
pendingPermissionCount: 0
|
|
89
|
+
};
|
|
90
|
+
claudeResumePanelOpen = false;
|
|
91
|
+
claudeForkPanelOpen = false;
|
|
92
|
+
claudePickerOpen = null;
|
|
93
|
+
claudeUsagePending = false;
|
|
94
|
+
claudeContextPending = false;
|
|
95
|
+
claudeResumeItemsLoaded = false;
|
|
96
|
+
document.getElementById('claude-resume-panel').classList.remove('active');
|
|
97
|
+
document.getElementById('claude-resume-panel').innerHTML = '';
|
|
98
|
+
document.getElementById('claude-fork-panel').classList.remove('active');
|
|
99
|
+
document.getElementById('claude-fork-panel').innerHTML = '';
|
|
100
|
+
closeClaudePicker();
|
|
101
|
+
updateTerminalControlsHeight();
|
|
102
|
+
applyClaudeRuntimeConfig(runtimeConfig);
|
|
103
|
+
applyClaudeState(claudeState);
|
|
104
|
+
renderClaudeChat();
|
|
105
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
106
|
+
currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
|
|
107
|
+
currentSocket.onopen = () => logViewSnapshot('claude-ws-open');
|
|
108
|
+
currentSocket.onmessage = (e) => {
|
|
109
|
+
const msg = JSON.parse(e.data);
|
|
110
|
+
if (msg.type === 'claude-snapshot' && msg.snapshot) {
|
|
111
|
+
claudeMessages = msg.snapshot.messages || [];
|
|
112
|
+
claudePendingPermissions = msg.snapshot.pendingPermissions || [];
|
|
113
|
+
claudeStatus = msg.snapshot.status || 'idle';
|
|
114
|
+
applyClaudeState(msg.snapshot.state || {
|
|
115
|
+
status: claudeStatus,
|
|
116
|
+
pendingPermissionCount: claudePendingPermissions.filter(item => item.status === 'pending').length,
|
|
117
|
+
canAbort: claudeStatus === 'thinking'
|
|
118
|
+
});
|
|
119
|
+
renderClaudeChat();
|
|
120
|
+
}
|
|
121
|
+
if (msg.type === 'claude-event') applyClaudeEvent(msg.event);
|
|
122
|
+
if (msg.type === 'exit') showLobby();
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function initCodexSession(sessionId) {
|
|
127
|
+
codexMessages = [];
|
|
128
|
+
codexPendingPermissions = [];
|
|
129
|
+
codexModelPanelOpen = false;
|
|
130
|
+
codexModelCandidate = null;
|
|
131
|
+
codexResumePanelOpen = false;
|
|
132
|
+
codexForkPanelOpen = false;
|
|
133
|
+
document.getElementById('codex-model-panel').classList.remove('active');
|
|
134
|
+
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
135
|
+
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
136
|
+
document.getElementById('codex-control-rail').scrollLeft = 0;
|
|
137
|
+
codexState = { permissionMode: 'default', sandboxMode: 'default', effectivePermissionMode: null, effectiveSandboxMode: null, model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canSwitchToTerminal: false, canSwitchToStructured: false };
|
|
138
|
+
setClaudeModeEnabled(false);
|
|
139
|
+
applyCodexState(codexState);
|
|
140
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
141
|
+
currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
|
|
142
|
+
currentSocket.onmessage = (e) => {
|
|
143
|
+
const msg = JSON.parse(e.data);
|
|
144
|
+
if (msg.type === 'codex-snapshot' && msg.snapshot) {
|
|
145
|
+
codexMessages = msg.snapshot.messages || [];
|
|
146
|
+
codexPendingPermissions = msg.snapshot.pendingPermissions || [];
|
|
147
|
+
applyCodexState(msg.snapshot.state || {});
|
|
148
|
+
}
|
|
149
|
+
if (msg.type === 'codex-event') {
|
|
150
|
+
applyCodexEvent(msg.event);
|
|
151
|
+
if (msg.event?.type === 'presentation' && msg.event.presentation === 'terminal') {
|
|
152
|
+
currentSocket.close();
|
|
153
|
+
currentSocket = null;
|
|
154
|
+
initTerminal(sessionId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (msg.type === 'exit') showLobby();
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function sendWS(data) {
|
|
162
|
+
if (currentSocket && currentSocket.readyState === 1) currentSocket.send(JSON.stringify({ type: 'input', data }));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let layoutTimer = null;
|
|
166
|
+
let layoutKeepAtBottom = false;
|
|
167
|
+
|
|
168
|
+
function isTerminalAtBottom() {
|
|
169
|
+
if (!term || !term.buffer || !term.buffer.active) return true;
|
|
170
|
+
const active = term.buffer.active;
|
|
171
|
+
return active.viewportY >= active.baseY - 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function scrollTerminalViewportToBottom() {
|
|
175
|
+
if (!term) return;
|
|
176
|
+
term.scrollToBottom();
|
|
177
|
+
const viewport = document.querySelector('#terminal .xterm-viewport');
|
|
178
|
+
if (viewport) viewport.scrollTop = viewport.scrollHeight;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function restoreTerminalBottomSoon() {
|
|
182
|
+
scrollTerminalViewportToBottom();
|
|
183
|
+
requestAnimationFrame(() => {
|
|
184
|
+
scrollTerminalViewportToBottom();
|
|
185
|
+
requestAnimationFrame(scrollTerminalViewportToBottom);
|
|
186
|
+
});
|
|
187
|
+
setTimeout(scrollTerminalViewportToBottom, 120);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function syncLayout(options = {}) {
|
|
191
|
+
if (!fitAddon || !currentSocket) return;
|
|
192
|
+
const keepAtBottom = typeof options.keepAtBottom === 'boolean' ? options.keepAtBottom : isTerminalAtBottom();
|
|
193
|
+
layoutKeepAtBottom = layoutKeepAtBottom || keepAtBottom;
|
|
194
|
+
clearTimeout(layoutTimer);
|
|
195
|
+
layoutTimer = setTimeout(() => {
|
|
196
|
+
const shouldRestoreBottom = layoutKeepAtBottom;
|
|
197
|
+
layoutKeepAtBottom = false;
|
|
198
|
+
fitAddon.fit();
|
|
199
|
+
if (shouldRestoreBottom) restoreTerminalBottomSoon();
|
|
200
|
+
if (currentSocket.readyState === 1) currentSocket.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
|
201
|
+
}, 50);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function previewText(text, maxChars = 320) {
|
|
205
|
+
if (!text) return '';
|
|
206
|
+
const normalized = String(text)
|
|
207
|
+
.replace(/\r/g, '\\r')
|
|
208
|
+
.replace(/\n/g, '\\n')
|
|
209
|
+
.replace(/\t/g, '\\t')
|
|
210
|
+
.replace(/\x1b/g, '\\x1b');
|
|
211
|
+
return normalized.length > maxChars ? normalized.slice(-maxChars) : normalized;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function logClientDebug(event, payload) {
|
|
215
|
+
try {
|
|
216
|
+
await fetch('/api/debug/client-log', {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: { 'Content-Type': 'application/json' },
|
|
219
|
+
body: JSON.stringify({ sessionId: activeSessionId, event, payload })
|
|
220
|
+
});
|
|
221
|
+
} catch (_) {}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function snapshotTerminalBuffer(maxLines = 12) {
|
|
225
|
+
if (!term || !term.buffer || !term.buffer.active) return null;
|
|
226
|
+
const active = term.buffer.active;
|
|
227
|
+
const totalLines = active.length || 0;
|
|
228
|
+
const start = Math.max(0, totalLines - maxLines);
|
|
229
|
+
const tailLines = [];
|
|
230
|
+
for (let i = start; i < totalLines; i++) {
|
|
231
|
+
const line = active.getLine(i);
|
|
232
|
+
if (!line) continue;
|
|
233
|
+
tailLines.push(line.translateToString(true));
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
cols: term.cols,
|
|
237
|
+
rows: term.rows,
|
|
238
|
+
baseY: active.baseY,
|
|
239
|
+
viewportY: active.viewportY,
|
|
240
|
+
cursorX: active.cursorX,
|
|
241
|
+
cursorY: active.cursorY,
|
|
242
|
+
totalLines,
|
|
243
|
+
tailPreview: previewText(tailLines.join('\n'))
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function logViewSnapshot(event) {
|
|
248
|
+
logClientDebug(event, {
|
|
249
|
+
sessionId: activeSessionId,
|
|
250
|
+
terminalSnapshot: snapshotTerminalBuffer()
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Navigation
|
|
255
|
+
function showTerminal() {
|
|
256
|
+
clearTimeout(sessionPollTimer);
|
|
257
|
+
sessionPollTimer = null;
|
|
258
|
+
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
259
|
+
document.getElementById('terminal-view').classList.add('active');
|
|
260
|
+
logViewSnapshot('show-terminal');
|
|
261
|
+
syncLayout();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function showGitPreview() {
|
|
265
|
+
clearTimeout(sessionPollTimer);
|
|
266
|
+
sessionPollTimer = null;
|
|
267
|
+
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
268
|
+
document.getElementById('git-view').classList.add('active');
|
|
269
|
+
switchGitTab('changes');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let historyWrapEnabled = true;
|
|
273
|
+
|
|
274
|
+
function showHistory() {
|
|
275
|
+
clearTimeout(sessionPollTimer);
|
|
276
|
+
sessionPollTimer = null;
|
|
277
|
+
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
278
|
+
document.getElementById('history-view').classList.add('active');
|
|
279
|
+
logViewSnapshot('show-history-before-load');
|
|
280
|
+
loadHistory();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function toggleHistoryWrap() {
|
|
284
|
+
historyWrapEnabled = !historyWrapEnabled;
|
|
285
|
+
const contentEl = document.getElementById('history-content');
|
|
286
|
+
contentEl.classList.toggle('nowrap', !historyWrapEnabled);
|
|
287
|
+
document.getElementById('history-wrap-btn').style.color = historyWrapEnabled ? 'var(--primary)' : 'var(--text-dim)';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function loadHistory() {
|
|
291
|
+
const contentEl = document.getElementById('history-content');
|
|
292
|
+
const metaEl = document.getElementById('history-meta');
|
|
293
|
+
if (!activeSessionId) {
|
|
294
|
+
metaEl.textContent = 'No active session.';
|
|
295
|
+
contentEl.textContent = '';
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
metaEl.textContent = 'Loading...';
|
|
300
|
+
try {
|
|
301
|
+
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/history`, {}, 30000);
|
|
302
|
+
const data = await res.json();
|
|
303
|
+
if (!data.success) throw new Error(data.error || 'Failed to load history');
|
|
304
|
+
|
|
305
|
+
const updatedAt = data.updatedAt ? new Date(data.updatedAt).toLocaleTimeString() : 'unknown';
|
|
306
|
+
const size = typeof data.bytes === 'number' ? `${Math.round(data.bytes / 1024)} KB` : 'unknown size';
|
|
307
|
+
const truncated = data.truncated ? ' | truncated' : '';
|
|
308
|
+
metaEl.textContent = `${data.tool || 'Session'} | ${data.lines || 0} lines | ${size} | updated ${updatedAt}${truncated}`;
|
|
309
|
+
contentEl.textContent = data.text || 'No readable history yet.';
|
|
310
|
+
contentEl.classList.toggle('nowrap', !historyWrapEnabled);
|
|
311
|
+
document.getElementById('history-wrap-btn').style.color = historyWrapEnabled ? 'var(--primary)' : 'var(--text-dim)';
|
|
312
|
+
logClientDebug('history-loaded', {
|
|
313
|
+
meta: metaEl.textContent,
|
|
314
|
+
historyLines: data.lines || 0,
|
|
315
|
+
historyBytes: data.bytes || 0,
|
|
316
|
+
historyTail: previewText(data.text || '')
|
|
317
|
+
});
|
|
318
|
+
requestAnimationFrame(() => {
|
|
319
|
+
contentEl.scrollTop = contentEl.scrollHeight;
|
|
320
|
+
});
|
|
321
|
+
} catch (e) {
|
|
322
|
+
metaEl.textContent = 'Failed to load history.';
|
|
323
|
+
contentEl.textContent = e.message;
|
|
324
|
+
logClientDebug('history-load-failed', { message: e.message });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function copyHistoryContent() {
|
|
329
|
+
const contentEl = document.getElementById('history-content');
|
|
330
|
+
const text = contentEl.textContent || '';
|
|
331
|
+
if (!text || text === 'Loading...' || text === 'No readable history yet.') {
|
|
332
|
+
alert('No history to copy.');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
338
|
+
await navigator.clipboard.writeText(text);
|
|
339
|
+
} else {
|
|
340
|
+
const range = document.createRange();
|
|
341
|
+
range.selectNode(contentEl);
|
|
342
|
+
window.getSelection().removeAllRanges();
|
|
343
|
+
window.getSelection().addRange(range);
|
|
344
|
+
document.execCommand('copy');
|
|
345
|
+
window.getSelection().removeAllRanges();
|
|
346
|
+
}
|
|
347
|
+
alert('History copied.');
|
|
348
|
+
} catch (e) {
|
|
349
|
+
alert('Copy failed. You can select the history text manually.');
|
|
350
|
+
}
|
|
351
|
+
}
|
package/lib/web/shell.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
async function deleteSession(id, e) {
|
|
2
|
+
e.stopPropagation(); if (!confirm('Terminate session?')) return;
|
|
3
|
+
try { await fetchWithTimeout('/api/sessions/' + id, { method: 'DELETE' }); refreshSessionsNow(); } catch (e) { alert('Failed to delete'); }
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function showLobby() {
|
|
7
|
+
if (currentSocket) { currentSocket.close(); currentSocket = null; }
|
|
8
|
+
closeTimedSendPanel();
|
|
9
|
+
stopTimedInputTimers();
|
|
10
|
+
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
11
|
+
document.getElementById('lobby-view').classList.add('active');
|
|
12
|
+
refreshSessionsNow();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function updateToolShortcuts(toolKey) {
|
|
16
|
+
const usesUsageCommand = ['antigravity', 'claude-code'].includes(toolKey);
|
|
17
|
+
const usageCommand = usesUsageCommand ? '/usage' : '/stat';
|
|
18
|
+
const defaultShortcuts = [
|
|
19
|
+
{ key: 'left', label: '←', special: true },
|
|
20
|
+
{ key: 'right', label: '→', special: true },
|
|
21
|
+
{ key: 'ctrl-c', label: 'Ctrl+C' },
|
|
22
|
+
{ command: '/model' },
|
|
23
|
+
{ command: '/resume' },
|
|
24
|
+
{ command: usageCommand }
|
|
25
|
+
];
|
|
26
|
+
const toolShortcuts = {
|
|
27
|
+
codex: [
|
|
28
|
+
{ key: 'ctrl-c', label: 'Ctrl+C' },
|
|
29
|
+
{ key: 'codex-perm', label: '/perm', title: 'Send /perm' },
|
|
30
|
+
{ command: '/model' },
|
|
31
|
+
{ command: '/resume' },
|
|
32
|
+
{ command: '/stat' }
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
const shortcuts = toolShortcuts[toolKey] || defaultShortcuts;
|
|
36
|
+
const shortcutGroup = document.querySelector('[data-role="tool-shortcuts"]');
|
|
37
|
+
if (!shortcutGroup) return;
|
|
38
|
+
|
|
39
|
+
shortcutGroup.style.gridTemplateColumns = `repeat(${shortcuts.length}, minmax(0, 1fr))`;
|
|
40
|
+
shortcutGroup.replaceChildren(...shortcuts.map(shortcut => {
|
|
41
|
+
const button = document.createElement(shortcut.command ? 'button' : 'div');
|
|
42
|
+
button.className = shortcut.command
|
|
43
|
+
? 'command-key'
|
|
44
|
+
: `key-btn${shortcut.special ? ' special-key' : ''}`;
|
|
45
|
+
if (shortcut.command) {
|
|
46
|
+
button.dataset.command = shortcut.command;
|
|
47
|
+
button.textContent = shortcut.command;
|
|
48
|
+
button.title = `Send ${shortcut.command}`;
|
|
49
|
+
} else {
|
|
50
|
+
button.dataset.key = shortcut.key;
|
|
51
|
+
button.textContent = shortcut.label;
|
|
52
|
+
button.title = shortcut.title || '';
|
|
53
|
+
}
|
|
54
|
+
return button;
|
|
55
|
+
}));
|
|
56
|
+
}
|