claude-remote 0.5.2 → 0.6.1
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/bin/claude-remote.js +1 -1
- package/hooks/bridge-session-start.js +32 -32
- package/lib/cli.js +1 -0
- package/lib/http-server.js +60 -22
- package/lib/interactive-questions.js +183 -0
- package/lib/logger.js +172 -138
- package/lib/state.js +8 -6
- package/lib/ws-server.js +132 -96
- package/package.json +3 -2
- package/server.js +23 -16
- package/web/index.html +383 -0
- package/web/main.js +68 -0
- package/web/modules/chat-cache.js +118 -0
- package/web/modules/confirm.js +25 -0
- package/web/modules/constants.js +59 -0
- package/web/modules/debug.js +81 -0
- package/web/modules/dir-picker.js +128 -0
- package/web/modules/hub.js +619 -0
- package/web/modules/image-upload.js +290 -0
- package/web/modules/input.js +279 -0
- package/web/modules/interactions.js +423 -0
- package/web/modules/keyboard.js +78 -0
- package/web/modules/model-picker.js +47 -0
- package/web/modules/permissions.js +94 -0
- package/web/modules/renderer.js +863 -0
- package/web/modules/sessions.js +108 -0
- package/web/modules/settings.js +101 -0
- package/web/modules/state.js +61 -0
- package/web/modules/toast.js +68 -0
- package/web/modules/todo.js +292 -0
- package/web/modules/utils.js +102 -0
- package/web/modules/waiting.js +93 -0
- package/web/modules/websocket.js +486 -0
- package/web/styles.css +1959 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Interactions — AskUserQuestion + ExitPlanMode
|
|
3
|
+
// ============================================================
|
|
4
|
+
import { PLAN_OPTIONS } from './constants.js';
|
|
5
|
+
import { $, esc } from './utils.js';
|
|
6
|
+
import { S } from './state.js';
|
|
7
|
+
import { renderMd, renderPlanCard, consumePendingPlanCard } from './renderer.js';
|
|
8
|
+
import { showToast } from './toast.js';
|
|
9
|
+
|
|
10
|
+
let pendingInteractions = new Map();
|
|
11
|
+
let pendingInteractionOrder = [];
|
|
12
|
+
let questionQueue = [];
|
|
13
|
+
let currentQuestionItem = null;
|
|
14
|
+
let currentQuestionIdx = 0;
|
|
15
|
+
let activePlanToolUseId = null;
|
|
16
|
+
let currentAnswers = [];
|
|
17
|
+
let currentOtherTexts = [];
|
|
18
|
+
let questionSubmitting = false;
|
|
19
|
+
|
|
20
|
+
export function resetInteractionState() {
|
|
21
|
+
pendingInteractions.clear();
|
|
22
|
+
pendingInteractionOrder = [];
|
|
23
|
+
questionQueue = [];
|
|
24
|
+
currentQuestionItem = null;
|
|
25
|
+
currentQuestionIdx = 0;
|
|
26
|
+
activePlanToolUseId = null;
|
|
27
|
+
currentAnswers = [];
|
|
28
|
+
currentOtherTexts = [];
|
|
29
|
+
questionSubmitting = false;
|
|
30
|
+
$('question-overlay').classList.remove('visible');
|
|
31
|
+
$('plan-overlay').classList.remove('visible');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function enqueuePendingInteraction(toolUseId, kind, payload) {
|
|
35
|
+
if (!toolUseId) return;
|
|
36
|
+
if (!pendingInteractions.has(toolUseId)) pendingInteractionOrder.push(toolUseId);
|
|
37
|
+
pendingInteractions.set(toolUseId, { toolUseId, kind, payload });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function dropPendingInteraction(toolUseId) {
|
|
41
|
+
if (!toolUseId || !pendingInteractions.has(toolUseId)) return false;
|
|
42
|
+
pendingInteractions.delete(toolUseId);
|
|
43
|
+
pendingInteractionOrder = pendingInteractionOrder.filter(id => id !== toolUseId);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getNextPendingInteraction() {
|
|
48
|
+
while (pendingInteractionOrder.length > 0) {
|
|
49
|
+
const toolUseId = pendingInteractionOrder[0];
|
|
50
|
+
const interaction = pendingInteractions.get(toolUseId);
|
|
51
|
+
if (interaction) return interaction;
|
|
52
|
+
pendingInteractionOrder.shift();
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hasActiveInteractionUi() {
|
|
58
|
+
return !!currentQuestionItem || !!activePlanToolUseId ||
|
|
59
|
+
$('question-overlay').classList.contains('visible') ||
|
|
60
|
+
$('plan-overlay').classList.contains('visible');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function presentNextPendingInteraction() {
|
|
64
|
+
if (S.replaying || hasActiveInteractionUi()) return;
|
|
65
|
+
const next = getNextPendingInteraction();
|
|
66
|
+
if (!next) return;
|
|
67
|
+
if (next.kind === 'question') {
|
|
68
|
+
showQuestion(next.payload, { toolUseId: next.toolUseId });
|
|
69
|
+
} else if (next.kind === 'plan') {
|
|
70
|
+
showPlanApproval(next.payload, { toolUseId: next.toolUseId });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function registerInteractiveToolUse(block) {
|
|
75
|
+
const toolName = block.name || '';
|
|
76
|
+
if (toolName === 'AskUserQuestion' && block.input && block.input.questions) {
|
|
77
|
+
if (block.id) {
|
|
78
|
+
enqueuePendingInteraction(block.id, 'question', block.input.questions);
|
|
79
|
+
presentNextPendingInteraction();
|
|
80
|
+
} else if (!S.replaying) {
|
|
81
|
+
showQuestion(block.input.questions);
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (toolName === 'ExitPlanMode') {
|
|
86
|
+
if (S.replaying) {
|
|
87
|
+
const plan = normalizePlanContent(block.input?.plan || '');
|
|
88
|
+
if (plan) renderPlanCard(plan);
|
|
89
|
+
}
|
|
90
|
+
if (block.id) {
|
|
91
|
+
enqueuePendingInteraction(block.id, 'plan', block.input || {});
|
|
92
|
+
presentNextPendingInteraction();
|
|
93
|
+
} else if (!S.replaying) {
|
|
94
|
+
showPlanApproval(block.input);
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function resolveInteractiveToolResult(block) {
|
|
102
|
+
const toolUseId = typeof block.tool_use_id === 'string' ? block.tool_use_id : '';
|
|
103
|
+
if (!toolUseId) return;
|
|
104
|
+
const interaction = pendingInteractions.get(toolUseId);
|
|
105
|
+
if (!interaction) return;
|
|
106
|
+
dropPendingInteraction(toolUseId);
|
|
107
|
+
if (interaction.kind === 'question') dismissQuestionInteraction(toolUseId);
|
|
108
|
+
if (interaction.kind === 'plan') dismissPlanInteraction(toolUseId);
|
|
109
|
+
presentNextPendingInteraction();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---- Question system ----
|
|
113
|
+
export function showQuestion(questions, { toolUseId = '' } = {}) {
|
|
114
|
+
if (!Array.isArray(questions) || questions.length === 0) return;
|
|
115
|
+
if (toolUseId) {
|
|
116
|
+
if (currentQuestionItem?.toolUseId === toolUseId) return;
|
|
117
|
+
if (questionQueue.some(item => item.toolUseId === toolUseId)) return;
|
|
118
|
+
}
|
|
119
|
+
questionQueue.push({ toolUseId, questions });
|
|
120
|
+
if (!currentQuestionItem) showNextQuestion();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function showNextQuestion() {
|
|
124
|
+
if (questionQueue.length === 0) {
|
|
125
|
+
$('question-overlay').classList.remove('visible');
|
|
126
|
+
currentQuestionItem = null;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
currentQuestionItem = questionQueue[0];
|
|
130
|
+
currentQuestionIdx = 0;
|
|
131
|
+
currentAnswers = currentQuestionItem.questions.map(() => new Set());
|
|
132
|
+
currentOtherTexts = currentQuestionItem.questions.map(() => '');
|
|
133
|
+
questionSubmitting = false;
|
|
134
|
+
renderCurrentQuestion();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function finishCurrentQuestionItem(nextDelayMs = 0) {
|
|
138
|
+
const finishedToolUseId = currentQuestionItem?.toolUseId || '';
|
|
139
|
+
if (questionQueue.length > 0) questionQueue.shift();
|
|
140
|
+
currentQuestionItem = null;
|
|
141
|
+
currentQuestionIdx = 0;
|
|
142
|
+
currentAnswers = [];
|
|
143
|
+
currentOtherTexts = [];
|
|
144
|
+
questionSubmitting = false;
|
|
145
|
+
if (finishedToolUseId) dropPendingInteraction(finishedToolUseId);
|
|
146
|
+
if (questionQueue.length > 0) {
|
|
147
|
+
setTimeout(showNextQuestion, nextDelayMs);
|
|
148
|
+
} else {
|
|
149
|
+
presentNextPendingInteraction();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function dismissQuestionInteraction(toolUseId) {
|
|
154
|
+
if (!toolUseId) return;
|
|
155
|
+
const wasCurrent = currentQuestionItem?.toolUseId === toolUseId;
|
|
156
|
+
questionQueue = questionQueue.filter(item => item.toolUseId !== toolUseId);
|
|
157
|
+
if (wasCurrent) {
|
|
158
|
+
currentQuestionItem = null;
|
|
159
|
+
currentQuestionIdx = 0;
|
|
160
|
+
currentAnswers = [];
|
|
161
|
+
currentOtherTexts = [];
|
|
162
|
+
questionSubmitting = false;
|
|
163
|
+
$('question-overlay').classList.remove('visible');
|
|
164
|
+
if (questionQueue.length > 0) {
|
|
165
|
+
setTimeout(showNextQuestion, 0);
|
|
166
|
+
} else {
|
|
167
|
+
presentNextPendingInteraction();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderCurrentQuestion() {
|
|
173
|
+
if (!currentQuestionItem) return;
|
|
174
|
+
const questions = currentQuestionItem.questions;
|
|
175
|
+
if (currentQuestionIdx >= questions.length) {
|
|
176
|
+
submitAllAnswers();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const q = questions[currentQuestionIdx];
|
|
181
|
+
const isMulti = !!q.multiSelect;
|
|
182
|
+
const total = questions.length;
|
|
183
|
+
const selected = currentAnswers[currentQuestionIdx] || new Set();
|
|
184
|
+
const otherText = currentOtherTexts[currentQuestionIdx] || '';
|
|
185
|
+
|
|
186
|
+
$('question-header-text').textContent = q.header || 'Question';
|
|
187
|
+
$('question-text').textContent = q.question || '';
|
|
188
|
+
|
|
189
|
+
const progressEl = $('question-progress');
|
|
190
|
+
if (total > 1) {
|
|
191
|
+
progressEl.textContent = `${currentQuestionIdx + 1} / ${total}`;
|
|
192
|
+
progressEl.style.display = '';
|
|
193
|
+
} else {
|
|
194
|
+
progressEl.style.display = 'none';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const optionsEl = $('question-options');
|
|
198
|
+
const options = q.options || [];
|
|
199
|
+
optionsEl.innerHTML = options.map((opt, i) => {
|
|
200
|
+
const idx = i + 1;
|
|
201
|
+
const isSel = selected.has(idx);
|
|
202
|
+
return `
|
|
203
|
+
<button class="question-opt${isSel ? ' selected' : ''}${isMulti ? ' multi' : ''}" data-idx="${idx}"${questionSubmitting ? ' disabled' : ''}>
|
|
204
|
+
${isMulti
|
|
205
|
+
? `<span class="question-opt-check">${isSel ? '☑' : '☐'}</span>`
|
|
206
|
+
: `<span class="question-opt-num${isSel ? ' active' : ''}">${idx}</span>`}
|
|
207
|
+
<div class="question-opt-body">
|
|
208
|
+
<div class="question-opt-label">${esc(opt.label)}</div>
|
|
209
|
+
${opt.description ? `<div class="question-opt-desc">${esc(opt.description)}</div>` : ''}
|
|
210
|
+
</div>
|
|
211
|
+
</button>`;
|
|
212
|
+
}).join('');
|
|
213
|
+
|
|
214
|
+
optionsEl.querySelectorAll('.question-opt').forEach(btn => {
|
|
215
|
+
btn.addEventListener('click', () => {
|
|
216
|
+
if (questionSubmitting) return;
|
|
217
|
+
const idx = parseInt(btn.dataset.idx, 10);
|
|
218
|
+
isMulti ? toggleOption(idx) : selectOption(idx);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
$('question-other-input').value = otherText;
|
|
223
|
+
$('question-other-input').disabled = questionSubmitting;
|
|
224
|
+
|
|
225
|
+
const prevBtn = $('question-prev-btn');
|
|
226
|
+
const nextBtn = $('question-next-btn');
|
|
227
|
+
prevBtn.style.display = currentQuestionIdx > 0 ? '' : 'none';
|
|
228
|
+
prevBtn.disabled = questionSubmitting;
|
|
229
|
+
|
|
230
|
+
const isLast = currentQuestionIdx === total - 1;
|
|
231
|
+
nextBtn.disabled = questionSubmitting;
|
|
232
|
+
nextBtn.textContent = questionSubmitting
|
|
233
|
+
? 'Submitting...'
|
|
234
|
+
: ((!isLast && total > 1) ? 'Next \u2192' : (total > 1 ? 'Submit All' : 'Submit'));
|
|
235
|
+
|
|
236
|
+
$('question-overlay').classList.add('visible');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function selectOption(idx) {
|
|
240
|
+
if (!currentQuestionItem) return;
|
|
241
|
+
currentAnswers[currentQuestionIdx] = new Set([idx]);
|
|
242
|
+
currentOtherTexts[currentQuestionIdx] = '';
|
|
243
|
+
renderCurrentQuestion();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function toggleOption(idx) {
|
|
247
|
+
const selected = currentAnswers[currentQuestionIdx];
|
|
248
|
+
if (!selected) return;
|
|
249
|
+
if (selected.has(idx)) selected.delete(idx);
|
|
250
|
+
else selected.add(idx);
|
|
251
|
+
renderCurrentQuestion();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function goToPrev() {
|
|
255
|
+
if (!currentQuestionItem || currentQuestionIdx <= 0 || questionSubmitting) return;
|
|
256
|
+
currentQuestionIdx--;
|
|
257
|
+
renderCurrentQuestion();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function hasAnswerAt(index) {
|
|
261
|
+
const selected = currentAnswers[index];
|
|
262
|
+
if (selected && selected.size > 0) return true;
|
|
263
|
+
return !!String(currentOtherTexts[index] || '').trim();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function buildQuestionResponses() {
|
|
267
|
+
return currentQuestionItem.questions.map((_, index) => ({
|
|
268
|
+
selectedOptions: [...(currentAnswers[index] || [])].sort((a, b) => a - b),
|
|
269
|
+
otherText: String(currentOtherTexts[index] || '').trim(),
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function focusQuestion(index) {
|
|
274
|
+
currentQuestionIdx = index;
|
|
275
|
+
renderCurrentQuestion();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function goToNext() {
|
|
279
|
+
if (!currentQuestionItem || questionSubmitting) return;
|
|
280
|
+
if (!hasAnswerAt(currentQuestionIdx)) {
|
|
281
|
+
showToast('请先完成当前问题');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (currentQuestionIdx >= currentQuestionItem.questions.length - 1) {
|
|
285
|
+
submitAllAnswers();
|
|
286
|
+
} else {
|
|
287
|
+
currentQuestionIdx++;
|
|
288
|
+
renderCurrentQuestion();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function updateOtherText(value) {
|
|
293
|
+
if (!currentQuestionItem) return;
|
|
294
|
+
currentOtherTexts[currentQuestionIdx] = value;
|
|
295
|
+
const question = currentQuestionItem.questions[currentQuestionIdx];
|
|
296
|
+
if (!value.trim() || question?.multiSelect) return;
|
|
297
|
+
const selected = currentAnswers[currentQuestionIdx];
|
|
298
|
+
if (!selected || selected.size === 0) return;
|
|
299
|
+
currentAnswers[currentQuestionIdx] = new Set();
|
|
300
|
+
renderCurrentQuestion();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function handleQuestionSubmissionError(toolUseId, error) {
|
|
304
|
+
if (!currentQuestionItem) return;
|
|
305
|
+
if (toolUseId && currentQuestionItem.toolUseId && currentQuestionItem.toolUseId !== toolUseId) return;
|
|
306
|
+
questionSubmitting = false;
|
|
307
|
+
renderCurrentQuestion();
|
|
308
|
+
showToast(error || '提交问题答案失败');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function submitAllAnswers() {
|
|
312
|
+
if (!currentQuestionItem || questionSubmitting) return;
|
|
313
|
+
if (!S.ws || S.ws.readyState !== WebSocket.OPEN) {
|
|
314
|
+
showToast('Connection unavailable');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const firstIncompleteIdx = currentQuestionItem.questions.findIndex((_, index) => !hasAnswerAt(index));
|
|
319
|
+
if (firstIncompleteIdx >= 0) {
|
|
320
|
+
focusQuestion(firstIncompleteIdx);
|
|
321
|
+
showToast('请先完成所有问题');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
questionSubmitting = true;
|
|
326
|
+
renderCurrentQuestion();
|
|
327
|
+
S.ws.send(JSON.stringify({
|
|
328
|
+
type: 'answer_questions',
|
|
329
|
+
toolUseId: currentQuestionItem.toolUseId || '',
|
|
330
|
+
questions: currentQuestionItem.questions,
|
|
331
|
+
responses: buildQuestionResponses(),
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---- Plan approval ----
|
|
336
|
+
export function normalizePlanContent(plan) {
|
|
337
|
+
return String(plan || '').trim();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function dismissPlanInteraction(toolUseId) {
|
|
341
|
+
if (!toolUseId) return;
|
|
342
|
+
if (activePlanToolUseId !== toolUseId) return;
|
|
343
|
+
activePlanToolUseId = null;
|
|
344
|
+
$('plan-overlay').classList.remove('visible');
|
|
345
|
+
presentNextPendingInteraction();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function showPlanApproval(input, { toolUseId = '' } = {}) {
|
|
349
|
+
if (toolUseId && activePlanToolUseId === toolUseId && $('plan-overlay').classList.contains('visible')) return;
|
|
350
|
+
activePlanToolUseId = toolUseId || activePlanToolUseId || null;
|
|
351
|
+
const plan = normalizePlanContent(input?.plan || '');
|
|
352
|
+
S.pendingPlanContent = plan;
|
|
353
|
+
|
|
354
|
+
const contentEl = $('plan-content');
|
|
355
|
+
if (plan) {
|
|
356
|
+
contentEl.style.display = '';
|
|
357
|
+
contentEl.innerHTML = renderMd(plan);
|
|
358
|
+
} else {
|
|
359
|
+
contentEl.style.display = 'none';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const optionsEl = $('plan-options');
|
|
363
|
+
optionsEl.innerHTML = PLAN_OPTIONS.map(opt => `
|
|
364
|
+
<button class="question-opt" data-num="${opt.num}">
|
|
365
|
+
<span class="question-opt-num">${opt.num}</span>
|
|
366
|
+
<div class="question-opt-body">
|
|
367
|
+
<div class="question-opt-label">${esc(opt.label)}</div>
|
|
368
|
+
<div class="question-opt-desc">${esc(opt.desc)}</div>
|
|
369
|
+
</div>
|
|
370
|
+
</button>
|
|
371
|
+
`).join('');
|
|
372
|
+
|
|
373
|
+
optionsEl.querySelectorAll('.question-opt').forEach(btn => {
|
|
374
|
+
btn.addEventListener('click', () => {
|
|
375
|
+
if (!S.ws || S.ws.readyState !== WebSocket.OPEN) return;
|
|
376
|
+
if (btn.dataset.num === '1') {
|
|
377
|
+
S.ws.send(JSON.stringify({ type: 'expect_clear' }));
|
|
378
|
+
} else {
|
|
379
|
+
consumePendingPlanCard();
|
|
380
|
+
}
|
|
381
|
+
if (activePlanToolUseId) dropPendingInteraction(activePlanToolUseId);
|
|
382
|
+
activePlanToolUseId = null;
|
|
383
|
+
S.ws.send(JSON.stringify({ type: 'input', data: btn.dataset.num }));
|
|
384
|
+
$('plan-overlay').classList.remove('visible');
|
|
385
|
+
presentNextPendingInteraction();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
$('plan-other-input').value = '';
|
|
390
|
+
$('plan-overlay').classList.add('visible');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function sendPlanOther() {
|
|
394
|
+
const text = $('plan-other-input').value.trim();
|
|
395
|
+
if (!text || !S.ws || S.ws.readyState !== WebSocket.OPEN) return;
|
|
396
|
+
consumePendingPlanCard();
|
|
397
|
+
if (activePlanToolUseId) dropPendingInteraction(activePlanToolUseId);
|
|
398
|
+
activePlanToolUseId = null;
|
|
399
|
+
S.ws.send(JSON.stringify({ type: 'input', data: '4' }));
|
|
400
|
+
setTimeout(() => {
|
|
401
|
+
if (S.ws?.readyState === WebSocket.OPEN) {
|
|
402
|
+
S.ws.send(JSON.stringify({ type: 'chat', text }));
|
|
403
|
+
}
|
|
404
|
+
}, 500);
|
|
405
|
+
$('plan-overlay').classList.remove('visible');
|
|
406
|
+
presentNextPendingInteraction();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function initInteractions() {
|
|
410
|
+
$('question-prev-btn').addEventListener('click', goToPrev);
|
|
411
|
+
$('question-next-btn').addEventListener('click', goToNext);
|
|
412
|
+
$('question-other-input').addEventListener('input', e => {
|
|
413
|
+
if (questionSubmitting) return;
|
|
414
|
+
updateOtherText(e.target.value || '');
|
|
415
|
+
});
|
|
416
|
+
$('question-other-input').addEventListener('keydown', e => {
|
|
417
|
+
if (e.key === 'Enter') { e.preventDefault(); goToNext(); }
|
|
418
|
+
});
|
|
419
|
+
$('plan-other-btn').addEventListener('click', sendPlanOther);
|
|
420
|
+
$('plan-other-input').addEventListener('keydown', e => {
|
|
421
|
+
if (e.key === 'Enter') { e.preventDefault(); sendPlanOther(); }
|
|
422
|
+
});
|
|
423
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Keyboard & Lifecycle events
|
|
3
|
+
// ============================================================
|
|
4
|
+
import { $ } from './utils.js';
|
|
5
|
+
import { S } from './state.js';
|
|
6
|
+
import { debugLog, wsReadyStateName } from './debug.js';
|
|
7
|
+
import { recoverConnectionOnForeground } from './websocket.js';
|
|
8
|
+
|
|
9
|
+
function updateKeyboardOffset() {
|
|
10
|
+
if (!window.visualViewport) return;
|
|
11
|
+
const viewportGap = Math.max(
|
|
12
|
+
0,
|
|
13
|
+
Math.round(window.innerHeight - window.visualViewport.height - window.visualViewport.offsetTop)
|
|
14
|
+
);
|
|
15
|
+
document.documentElement.style.setProperty('--keyboard-offset', `${viewportGap}px`);
|
|
16
|
+
|
|
17
|
+
if (S.isAtBottom) {
|
|
18
|
+
const $chat = $('chat-area');
|
|
19
|
+
requestAnimationFrame(() => { $chat.scrollTop = $chat.scrollHeight; });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function logLifecycleEvent(event) {
|
|
24
|
+
debugLog(event, {
|
|
25
|
+
hidden: typeof document !== 'undefined' ? !!document.hidden : null,
|
|
26
|
+
visibilityState: typeof document !== 'undefined' ? document.visibilityState : null,
|
|
27
|
+
focused: typeof document !== 'undefined' && typeof document.hasFocus === 'function' ? document.hasFocus() : null,
|
|
28
|
+
online: typeof navigator !== 'undefined' && 'onLine' in navigator ? !!navigator.onLine : null,
|
|
29
|
+
wsState: wsReadyStateName(S.ws),
|
|
30
|
+
waiting: S.waiting,
|
|
31
|
+
sessionId: S.sessionId || null,
|
|
32
|
+
lastSeq: S.lastSeq,
|
|
33
|
+
replaying: S.replaying,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function initKeyboard() {
|
|
38
|
+
if (window.visualViewport) {
|
|
39
|
+
window.visualViewport.addEventListener('resize', updateKeyboardOffset);
|
|
40
|
+
window.visualViewport.addEventListener('scroll', updateKeyboardOffset);
|
|
41
|
+
window.addEventListener('orientationchange', updateKeyboardOffset);
|
|
42
|
+
updateKeyboardOffset();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
window.addEventListener('focus', () => {
|
|
46
|
+
logLifecycleEvent('window_focus');
|
|
47
|
+
recoverConnectionOnForeground('window_focus');
|
|
48
|
+
});
|
|
49
|
+
window.addEventListener('blur', () => logLifecycleEvent('window_blur'));
|
|
50
|
+
window.addEventListener('pageshow', () => {
|
|
51
|
+
logLifecycleEvent('window_pageshow');
|
|
52
|
+
recoverConnectionOnForeground('window_pageshow');
|
|
53
|
+
});
|
|
54
|
+
window.addEventListener('pagehide', () => logLifecycleEvent('window_pagehide'));
|
|
55
|
+
window.addEventListener('online', () => {
|
|
56
|
+
logLifecycleEvent('network_online');
|
|
57
|
+
recoverConnectionOnForeground('network_online');
|
|
58
|
+
});
|
|
59
|
+
window.addEventListener('offline', () => logLifecycleEvent('network_offline'));
|
|
60
|
+
document.addEventListener('visibilitychange', () => {
|
|
61
|
+
const becameVisible = !document.hidden;
|
|
62
|
+
logLifecycleEvent(becameVisible ? 'document_visible' : 'document_hidden');
|
|
63
|
+
if (becameVisible) recoverConnectionOnForeground('document_visible');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// External links — open in system browser
|
|
67
|
+
document.addEventListener('click', (e) => {
|
|
68
|
+
const a = e.target.closest('a[href]');
|
|
69
|
+
if (!a) return;
|
|
70
|
+
const href = a.getAttribute('href');
|
|
71
|
+
if (!href) return;
|
|
72
|
+
if (/^https?:\/\//i.test(href)) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
window.open(href, '_blank');
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Model Picker
|
|
3
|
+
// ============================================================
|
|
4
|
+
import { MODELS } from './constants.js';
|
|
5
|
+
import { $ } from './utils.js';
|
|
6
|
+
import { S } from './state.js';
|
|
7
|
+
import { showToast } from './toast.js';
|
|
8
|
+
import { sendControlInput, sendSlashCmd, sendControlLine } from './input.js';
|
|
9
|
+
|
|
10
|
+
export function showModelPicker() {
|
|
11
|
+
const list = $('model-list');
|
|
12
|
+
list.innerHTML = MODELS.map(m =>
|
|
13
|
+
`<div class="model-item" data-num="${m.num}">
|
|
14
|
+
<span class="mi-num">${m.num}</span>
|
|
15
|
+
<div class="mi-info">
|
|
16
|
+
<span class="mi-name">${m.label}</span>
|
|
17
|
+
<span class="mi-desc">${m.desc}</span>
|
|
18
|
+
</div>
|
|
19
|
+
</div>`
|
|
20
|
+
).join('');
|
|
21
|
+
|
|
22
|
+
list.querySelectorAll('.model-item').forEach(el => {
|
|
23
|
+
el.addEventListener('click', () => {
|
|
24
|
+
const picked = MODELS.find(m => m.num === el.dataset.num);
|
|
25
|
+
hideModelPicker();
|
|
26
|
+
if (!S.ws || S.ws.readyState !== WebSocket.OPEN) return;
|
|
27
|
+
showToast('Switching to ' + (picked ? picked.label : 'model') + '...');
|
|
28
|
+
sendControlInput('\x1b');
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
if (S.ws?.readyState === WebSocket.OPEN) sendSlashCmd('/model');
|
|
31
|
+
}, 250);
|
|
32
|
+
sendControlLine(el.dataset.num, { startDelayMs: 2400, submitDelayMs: 140 });
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
$('model-picker').classList.add('visible');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function hideModelPicker() {
|
|
40
|
+
$('model-picker').classList.remove('visible');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function initModelPicker() {
|
|
44
|
+
$('model-picker').addEventListener('click', e => {
|
|
45
|
+
if (e.target === $('model-picker')) hideModelPicker();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Permissions
|
|
3
|
+
// ============================================================
|
|
4
|
+
import { $ } from './utils.js';
|
|
5
|
+
import { S } from './state.js';
|
|
6
|
+
|
|
7
|
+
function permToolDetail(name, input) {
|
|
8
|
+
if (!input) return name;
|
|
9
|
+
switch (name) {
|
|
10
|
+
case 'Bash': return input.command || input.description || '';
|
|
11
|
+
case 'Read': return input.file_path || '';
|
|
12
|
+
case 'Write': return `Write \u2192 ${input.file_path || ''}`;
|
|
13
|
+
case 'Edit': return `Edit \u2192 ${input.file_path || ''}`;
|
|
14
|
+
case 'Glob': return `Glob: ${input.pattern || ''}`;
|
|
15
|
+
case 'Grep': return `Grep: ${input.pattern || ''}`;
|
|
16
|
+
case 'WebFetch': return input.url || '';
|
|
17
|
+
case 'WebSearch': return `Search: ${input.query || ''}`;
|
|
18
|
+
case 'Task': return input.description || '';
|
|
19
|
+
default: return JSON.stringify(input, null, 2).substring(0, 300);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function showPermission(m) {
|
|
24
|
+
if (!m || !m.id) return;
|
|
25
|
+
if (S.pendingPerms.some(item => item.id === m.id)) return;
|
|
26
|
+
S.pendingPerms.push({ id: m.id, toolName: m.toolName, toolInput: m.toolInput });
|
|
27
|
+
if (S.pendingPerms.length === 1) showNextPerm();
|
|
28
|
+
else updatePermCounter();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function showNextPerm() {
|
|
32
|
+
if (S.pendingPerms.length === 0) {
|
|
33
|
+
$('perm-overlay').classList.remove('visible');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const p = S.pendingPerms[0];
|
|
37
|
+
$('perm-tool-name').textContent = p.toolName;
|
|
38
|
+
$('perm-detail').textContent = permToolDetail(p.toolName, p.toolInput);
|
|
39
|
+
$('perm-overlay').classList.add('visible');
|
|
40
|
+
updatePermCounter();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function updatePermCounter() {
|
|
44
|
+
let counter = $('perm-counter');
|
|
45
|
+
if (!counter) {
|
|
46
|
+
counter = document.createElement('span');
|
|
47
|
+
counter.id = 'perm-counter';
|
|
48
|
+
counter.style.cssText = 'font-size:12px;color:var(--text-muted);margin-left:8px;';
|
|
49
|
+
$('perm-tool-name').parentNode.appendChild(counter);
|
|
50
|
+
}
|
|
51
|
+
if (S.pendingPerms.length > 1) {
|
|
52
|
+
counter.textContent = `(1/${S.pendingPerms.length})`;
|
|
53
|
+
} else {
|
|
54
|
+
counter.textContent = '';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolvePermission(decision) {
|
|
59
|
+
if (S.pendingPerms.length === 0 || !S.ws || S.ws.readyState !== WebSocket.OPEN) return;
|
|
60
|
+
const p = S.pendingPerms.shift();
|
|
61
|
+
S.ws.send(JSON.stringify({
|
|
62
|
+
type: 'permission_response',
|
|
63
|
+
id: p.id,
|
|
64
|
+
decision,
|
|
65
|
+
}));
|
|
66
|
+
showNextPerm();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function dismissPermissionById(id) {
|
|
70
|
+
const permId = typeof id === 'string' ? id : '';
|
|
71
|
+
if (!permId) return;
|
|
72
|
+
const idx = S.pendingPerms.findIndex(item => item.id === permId);
|
|
73
|
+
if (idx === -1) return;
|
|
74
|
+
const wasCurrent = idx === 0;
|
|
75
|
+
S.pendingPerms.splice(idx, 1);
|
|
76
|
+
if (S.pendingPerms.length === 0) {
|
|
77
|
+
$('perm-overlay').classList.remove('visible');
|
|
78
|
+
updatePermCounter();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (wasCurrent) showNextPerm();
|
|
82
|
+
else updatePermCounter();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function clearPermissions() {
|
|
86
|
+
S.pendingPerms = [];
|
|
87
|
+
$('perm-overlay').classList.remove('visible');
|
|
88
|
+
updatePermCounter();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function initPermissions() {
|
|
92
|
+
$('perm-allow').addEventListener('click', () => resolvePermission('allow'));
|
|
93
|
+
$('perm-deny').addEventListener('click', () => resolvePermission('deny'));
|
|
94
|
+
}
|