claude-remote 0.6.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Remote control bridge for Claude Code REPL - drive from phone/WebUI",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -1,28 +1,29 @@
1
1
  'use strict';
2
2
 
3
- const os = require('os');
4
- const { state, LOG_FILE } = require('./lib/state');
5
- const { initConfig } = require('./lib/cli');
6
- const { log } = require('./lib/logger');
7
- const { createHttpServer } = require('./lib/http-server');
8
- const { setupWebSocketServer } = require('./lib/ws-server');
9
- const { spawnClaude } = require('./lib/pty-manager');
10
- const { setupHooks } = require('./lib/hooks');
11
- const { startUploadCleanup } = require('./lib/image-upload');
3
+ const os = require('os');
4
+ const { state, LOG_FILE } = require('./lib/state');
5
+ const { initConfig } = require('./lib/cli');
6
+ const { initLogger, log } = require('./lib/logger');
7
+ const { createHttpServer } = require('./lib/http-server');
8
+ const { setupWebSocketServer } = require('./lib/ws-server');
9
+ const { spawnClaude } = require('./lib/pty-manager');
10
+ const { setupHooks } = require('./lib/hooks');
11
+ const { startUploadCleanup } = require('./lib/image-upload');
12
12
 
13
13
  // --- Initialize config from CLI args + env ---
14
14
  const config = initConfig();
15
15
  state.PORT = config.PORT;
16
16
  state.CWD = config.CWD;
17
17
  state.AUTH_TOKEN = config.AUTH_TOKEN;
18
- state.AUTH_DISABLED = config.AUTH_DISABLED;
19
- state.ENABLE_WEB = config.ENABLE_WEB;
20
- state.CLAUDE_EXTRA_ARGS = config.CLAUDE_EXTRA_ARGS;
21
- state.DEBUG_TTY_INPUT = config.DEBUG_TTY_INPUT;
22
-
23
- // --- Create servers ---
24
- const server = createHttpServer();
25
- setupWebSocketServer(server);
18
+ state.AUTH_DISABLED = config.AUTH_DISABLED;
19
+ state.ENABLE_WEB = config.ENABLE_WEB;
20
+ state.CLAUDE_EXTRA_ARGS = config.CLAUDE_EXTRA_ARGS;
21
+ state.DEBUG_TTY_INPUT = config.DEBUG_TTY_INPUT;
22
+ initLogger();
23
+
24
+ // --- Create servers ---
25
+ const server = createHttpServer();
26
+ setupWebSocketServer(server);
26
27
 
27
28
  // --- Start periodic cleanup ---
28
29
  startUploadCleanup();
package/web/index.html CHANGED
@@ -4,8 +4,14 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
6
6
  <title>Claude Remote</title>
7
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
8
7
  <link rel="stylesheet" href="styles.css">
8
+ <script>
9
+ (function(){
10
+ var t = localStorage.getItem('theme');
11
+ if (t === 'light' || t === 'dark')
12
+ document.documentElement.setAttribute('data-theme', t);
13
+ })();
14
+ </script>
9
15
  </head>
10
16
  <body>
11
17
 
@@ -198,16 +204,20 @@
198
204
  <div class="question-icon">
199
205
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"/><circle cx="12" cy="17" r=".5"/></svg>
200
206
  </div>
201
- <div>
207
+ <div style="flex:1">
202
208
  <div class="question-title" id="question-header-text">Question</div>
203
209
  <div class="question-sub">Claude needs your input</div>
204
210
  </div>
211
+ <div class="question-progress" id="question-progress" style="display:none">1 / 3</div>
205
212
  </div>
206
213
  <div class="question-text" id="question-text"></div>
207
214
  <div class="question-options" id="question-options"></div>
208
215
  <div class="question-other-row">
209
216
  <input class="question-other-input" id="question-other-input" type="text" placeholder="Other...">
210
- <button class="question-other-btn" id="question-other-btn">Send</button>
217
+ </div>
218
+ <div class="question-nav" id="question-nav">
219
+ <button class="question-nav-btn" id="question-prev-btn" style="display:none">&larr; Prev</button>
220
+ <button class="question-nav-btn question-nav-primary" id="question-next-btn">Submit</button>
211
221
  </div>
212
222
  </div>
213
223
  </div>
@@ -242,6 +252,33 @@
242
252
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
243
253
  </button>
244
254
  </div>
255
+ <div class="settings-section">
256
+ <div class="settings-label">主题</div>
257
+ <div class="settings-desc">选择界面外观</div>
258
+ <div class="settings-options" id="theme-options">
259
+ <label class="settings-opt" data-theme-mode="system">
260
+ <input type="radio" name="theme-mode" value="system" checked>
261
+ <div class="settings-opt-body">
262
+ <div class="settings-opt-label">跟随系统</div>
263
+ <div class="settings-opt-desc">自动跟随设备的深色/浅色设置</div>
264
+ </div>
265
+ </label>
266
+ <label class="settings-opt" data-theme-mode="light">
267
+ <input type="radio" name="theme-mode" value="light">
268
+ <div class="settings-opt-body">
269
+ <div class="settings-opt-label">浅色</div>
270
+ <div class="settings-opt-desc">始终使用浅色主题</div>
271
+ </div>
272
+ </label>
273
+ <label class="settings-opt" data-theme-mode="dark">
274
+ <input type="radio" name="theme-mode" value="dark">
275
+ <div class="settings-opt-body">
276
+ <div class="settings-opt-label">深色</div>
277
+ <div class="settings-opt-desc">始终使用深色主题</div>
278
+ </div>
279
+ </label>
280
+ </div>
281
+ </div>
245
282
  <div class="settings-section">
246
283
  <div class="settings-label">命令审批</div>
247
284
  <div class="settings-desc">控制哪些工具需要手动审批</div>
@@ -339,8 +376,8 @@
339
376
  </div>
340
377
  </div>
341
378
  </div>
342
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
343
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
379
+ <script async src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" crossorigin="anonymous"></script>
380
+ <script async src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" crossorigin="anonymous"></script>
344
381
  <script type="module" src="main.js"></script>
345
382
  </body>
346
383
  </html>
@@ -1,17 +1,21 @@
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';
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';
8
9
 
9
10
  let pendingInteractions = new Map();
10
11
  let pendingInteractionOrder = [];
11
12
  let questionQueue = [];
12
13
  let currentQuestionItem = null;
13
- let currentQuestionIdx = 0;
14
- let activePlanToolUseId = null;
14
+ let currentQuestionIdx = 0;
15
+ let activePlanToolUseId = null;
16
+ let currentAnswers = [];
17
+ let currentOtherTexts = [];
18
+ let questionSubmitting = false;
15
19
 
16
20
  export function resetInteractionState() {
17
21
  pendingInteractions.clear();
@@ -19,10 +23,13 @@ export function resetInteractionState() {
19
23
  questionQueue = [];
20
24
  currentQuestionItem = null;
21
25
  currentQuestionIdx = 0;
22
- activePlanToolUseId = null;
23
- $('question-overlay').classList.remove('visible');
24
- $('plan-overlay').classList.remove('visible');
25
- }
26
+ activePlanToolUseId = null;
27
+ currentAnswers = [];
28
+ currentOtherTexts = [];
29
+ questionSubmitting = false;
30
+ $('question-overlay').classList.remove('visible');
31
+ $('plan-overlay').classList.remove('visible');
32
+ }
26
33
 
27
34
  export function enqueuePendingInteraction(toolUseId, kind, payload) {
28
35
  if (!toolUseId) return;
@@ -119,19 +126,25 @@ function showNextQuestion() {
119
126
  currentQuestionItem = null;
120
127
  return;
121
128
  }
122
- currentQuestionItem = questionQueue[0];
123
- currentQuestionIdx = 0;
124
- renderCurrentQuestion();
125
- }
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
+ }
126
136
 
127
137
  function finishCurrentQuestionItem(nextDelayMs = 0) {
128
138
  const finishedToolUseId = currentQuestionItem?.toolUseId || '';
129
139
  if (questionQueue.length > 0) questionQueue.shift();
130
- currentQuestionItem = null;
131
- currentQuestionIdx = 0;
132
- if (finishedToolUseId) dropPendingInteraction(finishedToolUseId);
133
- if (questionQueue.length > 0) {
134
- setTimeout(showNextQuestion, nextDelayMs);
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);
135
148
  } else {
136
149
  presentNextPendingInteraction();
137
150
  }
@@ -141,12 +154,15 @@ function dismissQuestionInteraction(toolUseId) {
141
154
  if (!toolUseId) return;
142
155
  const wasCurrent = currentQuestionItem?.toolUseId === toolUseId;
143
156
  questionQueue = questionQueue.filter(item => item.toolUseId !== toolUseId);
144
- if (wasCurrent) {
145
- currentQuestionItem = null;
146
- currentQuestionIdx = 0;
147
- $('question-overlay').classList.remove('visible');
148
- if (questionQueue.length > 0) {
149
- setTimeout(showNextQuestion, 0);
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);
150
166
  } else {
151
167
  presentNextPendingInteraction();
152
168
  }
@@ -155,68 +171,166 @@ function dismissQuestionInteraction(toolUseId) {
155
171
 
156
172
  function renderCurrentQuestion() {
157
173
  if (!currentQuestionItem) return;
158
- if (currentQuestionIdx >= currentQuestionItem.questions.length) {
159
- finishCurrentQuestionItem();
174
+ const questions = currentQuestionItem.questions;
175
+ if (currentQuestionIdx >= questions.length) {
176
+ submitAllAnswers();
160
177
  return;
161
178
  }
162
- const q = currentQuestionItem.questions[currentQuestionIdx];
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
+
163
186
  $('question-header-text').textContent = q.header || 'Question';
164
187
  $('question-text').textContent = q.question || '';
165
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
+
166
197
  const optionsEl = $('question-options');
167
198
  const options = q.options || [];
168
- optionsEl.innerHTML = options.map((opt, i) => `
169
- <button class="question-opt" data-idx="${i + 1}">
170
- <span class="question-opt-num">${i + 1}</span>
171
- <div class="question-opt-body">
172
- <div class="question-opt-label">${esc(opt.label)}</div>
173
- ${opt.description ? `<div class="question-opt-desc">${esc(opt.description)}</div>` : ''}
174
- </div>
175
- </button>
176
- `).join('');
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 ? '&#9745;' : '&#9744;'}</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
+ }
177
238
 
178
- optionsEl.querySelectorAll('.question-opt').forEach(btn => {
179
- btn.addEventListener('click', () => {
180
- const idx = btn.dataset.idx;
181
- sendQuestionAnswer(idx);
182
- });
183
- });
184
-
185
- $('question-other-input').value = '';
186
- $('question-overlay').classList.add('visible');
187
- }
239
+ function selectOption(idx) {
240
+ if (!currentQuestionItem) return;
241
+ currentAnswers[currentQuestionIdx] = new Set([idx]);
242
+ currentOtherTexts[currentQuestionIdx] = '';
243
+ renderCurrentQuestion();
244
+ }
188
245
 
189
- function sendQuestionAnswer(numKey) {
190
- if (!S.ws || S.ws.readyState !== WebSocket.OPEN) return;
191
- S.ws.send(JSON.stringify({ type: 'input', data: String(numKey) }));
192
- $('question-overlay').classList.remove('visible');
193
- currentQuestionIdx++;
194
- if (currentQuestionItem && currentQuestionIdx < currentQuestionItem.questions.length) {
195
- setTimeout(renderCurrentQuestion, 500);
196
- } else {
197
- finishCurrentQuestionItem(500);
198
- }
199
- }
200
-
201
- function sendQuestionOther() {
202
- const text = $('question-other-input').value.trim();
203
- if (!text || !S.ws || S.ws.readyState !== WebSocket.OPEN) return;
204
- const options = currentQuestionItem?.questions?.[currentQuestionIdx]?.options || [];
205
- const otherNum = String(options.length + 1);
206
- S.ws.send(JSON.stringify({ type: 'input', data: otherNum }));
207
- setTimeout(() => {
208
- if (S.ws?.readyState === WebSocket.OPEN) {
209
- S.ws.send(JSON.stringify({ type: 'chat', text }));
210
- }
211
- }, 500);
212
- $('question-overlay').classList.remove('visible');
213
- currentQuestionIdx++;
214
- if (currentQuestionItem && currentQuestionIdx < currentQuestionItem.questions.length) {
215
- setTimeout(renderCurrentQuestion, 1000);
216
- } else {
217
- finishCurrentQuestionItem(1000);
218
- }
219
- }
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
+ }
220
334
 
221
335
  // ---- Plan approval ----
222
336
  export function normalizePlanContent(plan) {
@@ -292,11 +406,16 @@ function sendPlanOther() {
292
406
  presentNextPendingInteraction();
293
407
  }
294
408
 
295
- export function initInteractions() {
296
- $('question-other-btn').addEventListener('click', sendQuestionOther);
297
- $('question-other-input').addEventListener('keydown', e => {
298
- if (e.key === 'Enter') { e.preventDefault(); sendQuestionOther(); }
299
- });
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
+ });
300
419
  $('plan-other-btn').addEventListener('click', sendPlanOther);
301
420
  $('plan-other-input').addEventListener('keydown', e => {
302
421
  if (e.key === 'Enter') { e.preventDefault(); sendPlanOther(); }