claude-simple 0.2.34 → 0.2.36

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.
Files changed (62) hide show
  1. package/README.md +0 -0
  2. package/client.js +1 -1
  3. package/dist/addon-fit.min.js +0 -0
  4. package/dist/{app.0.2.34.min.js → app.0.2.36.min.js} +147 -147
  5. package/dist/index.html +2 -2
  6. package/dist/{style.0.2.34.min.css → style.0.2.36.min.css} +23 -0
  7. package/dist/xterm.min.css +0 -0
  8. package/dist/xterm.min.js +0 -0
  9. package/gateway.js +0 -0
  10. package/package.json +1 -1
  11. package/public/addon-fit.min.js +0 -0
  12. package/public/agent-panel.js +0 -0
  13. package/public/api.js +0 -0
  14. package/public/app.js +2 -2
  15. package/public/base.css +0 -0
  16. package/public/cache.js +0 -0
  17. package/public/chat.js +0 -0
  18. package/public/download-status.js +25 -0
  19. package/public/files.js +71 -38
  20. package/public/git.js +0 -0
  21. package/public/icons/icon-180.png +0 -0
  22. package/public/icons/icon-192.png +0 -0
  23. package/public/icons/icon-512.png +0 -0
  24. package/public/icons/icon-maskable-512.png +0 -0
  25. package/public/index.html +2 -2
  26. package/public/lib.js +0 -0
  27. package/public/manifest.webmanifest +0 -0
  28. package/public/mobile.js +0 -0
  29. package/public/notifications.js +0 -0
  30. package/public/offline.html +0 -0
  31. package/public/pwa.js +1 -1
  32. package/public/router.js +0 -0
  33. package/public/settings.js +0 -0
  34. package/public/shell/boot.js +0 -0
  35. package/public/shell/dashboard.js +0 -0
  36. package/public/shell/hub.js +0 -0
  37. package/public/shell/layout.js +0 -0
  38. package/public/shell/notes.js +0 -0
  39. package/public/shell/session-context.js +0 -0
  40. package/public/shell/session-list.js +0 -0
  41. package/public/shell/session-nav.js +0 -0
  42. package/public/shell/templates.js +0 -0
  43. package/public/shell/util.js +0 -0
  44. package/public/shell.js +0 -0
  45. package/public/state.js +0 -0
  46. package/public/style.css +23 -0
  47. package/public/sw.js +3 -3
  48. package/public/terminal.js +0 -0
  49. package/public/wake-lock.js +0 -0
  50. package/public/xterm.min.css +0 -0
  51. package/public/xterm.min.js +0 -0
  52. package/src/agents/claude.js +0 -0
  53. package/src/agents/meta-agent.js +0 -0
  54. package/src/agents/models.js +0 -0
  55. package/src/agents/sessions.js +0 -0
  56. package/src/agents/stream-batch.js +0 -0
  57. package/src/app.js +0 -0
  58. package/src/auto-update.js +0 -0
  59. package/src/runtime/approvals.js +0 -0
  60. package/src/runtime/http-transfer.js +16 -7
  61. package/src/runtime/outbox.js +0 -0
  62. package/src/runtime/protocol.js +0 -0
package/dist/index.html CHANGED
@@ -4,13 +4,13 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
6
6
  <title>Claude Code</title>
7
- <link rel="stylesheet" href="/style.0.2.34.min.css">
7
+ <link rel="stylesheet" href="/style.0.2.36.min.css">
8
8
  <link rel="stylesheet" href="/highlight.min.css">
9
9
  <script src="/marked.min.js"></script>
10
10
  <script src="/highlight.min.js"></script>
11
11
  </head>
12
12
  <body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
13
13
  <div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
14
- <script type="module" src="/app.0.2.34.min.js"></script>
14
+ <script type="module" src="/app.0.2.36.min.js"></script>
15
15
  </body>
16
16
  </html>
@@ -140,6 +140,29 @@
140
140
  }
141
141
  }
142
142
 
143
+ .file-download-toast {
144
+ position: fixed;
145
+ left: 50%;
146
+ bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
147
+ transform: translateX(-50%) translateY(8px);
148
+ z-index: 80;
149
+ max-width: min(92vw, 28rem);
150
+ padding: 0.7rem 1rem;
151
+ border-radius: 0.7rem;
152
+ background: var(--color-primary, #3b82f6);
153
+ color: var(--color-primary-content, #fff);
154
+ font-size: 0.85rem;
155
+ line-height: 1.35;
156
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
157
+ opacity: 0;
158
+ pointer-events: none;
159
+ transition: opacity .18s ease, transform .18s ease;
160
+ }
161
+ .file-download-toast.visible {
162
+ opacity: 1;
163
+ transform: translateX(-50%) translateY(0);
164
+ }
165
+
143
166
  /* File row actions: hover-reveal on desktop only */
144
167
  @media (min-width: 641px) {
145
168
  .file-row-action {
File without changes
package/dist/xterm.min.js CHANGED
File without changes
package/gateway.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-simple",
3
- "version": "0.2.34",
3
+ "version": "0.2.36",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude-simple": "server.js",
File without changes
File without changes
package/public/api.js CHANGED
File without changes
package/public/app.js CHANGED
@@ -3,7 +3,7 @@ import { ctx } from './state.js';
3
3
  import { probeHub } from './api.js?v=ui-24';
4
4
  import { initShell, showApp, initAuth, resumeSession, switchTab, shouldRestoreSessionOnBoot } from './shell.js';
5
5
  import { initChat } from './chat.js?v=ui-24';
6
- import { initFilesTab } from './files.js?v=ui-24';
6
+ import { initFilesTab } from './files.js?v=ui-29';
7
7
  import { initGitTab } from './git.js';
8
8
  import { initSettings } from './settings.js';
9
9
  import { initNotes } from './notes.js';
@@ -11,7 +11,7 @@ import { initTerminal } from './terminal.js';
11
11
  import { initImagePaste } from './chat.js?v=ui-24';
12
12
  import { initMetaAgent } from './agent-panel.js';
13
13
  import { initRouter, applyInitialRoute, parseHashPath } from './router.js';
14
- import { initPwa } from './pwa.js?v=ui-24';
14
+ import { initPwa } from './pwa.js?v=ui-29';
15
15
  import { initMobileChrome } from './mobile.js';
16
16
 
17
17
  function markBootReady() {
package/public/base.css CHANGED
File without changes
package/public/cache.js CHANGED
File without changes
package/public/chat.js CHANGED
File without changes
@@ -0,0 +1,25 @@
1
+ /** Shared download status copy for the Files tab toast. */
2
+ export function sizeLabel(size) {
3
+ if (size == null) return '';
4
+ if (size > 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`;
5
+ if (size > 1024) return `${(size / 1024).toFixed(1)} KB`;
6
+ return `${size} B`;
7
+ }
8
+
9
+ export function formatDownloadStatus(name, received, total) {
10
+ const n = name || 'file';
11
+ if (total > 0 && received != null) {
12
+ const pct = Math.min(100, Math.round((received / total) * 100));
13
+ return `正在下载 ${n}(${pct}%)`;
14
+ }
15
+ if (received > 0) return `正在下载 ${n}(${sizeLabel(received)})`;
16
+ return `正在下载 ${n}…`;
17
+ }
18
+
19
+ export function formatDownloadDone(name) {
20
+ return `已保存 ${name || 'file'},请查看浏览器下载栏`;
21
+ }
22
+
23
+ export function formatDownloadHandedOff(name) {
24
+ return `已交给浏览器下载 ${name || 'file'}`;
25
+ }
package/public/files.js CHANGED
@@ -6,6 +6,12 @@ import { resolveWorkspaceFile } from './workspace-path.js?v=ui-23';
6
6
  import { renderMermaid } from './mermaid.js';
7
7
  import { renderMath } from './math.js';
8
8
  import { parseMarkdownWithMath } from './markdown.js';
9
+ import {
10
+ sizeLabel,
11
+ formatDownloadStatus,
12
+ formatDownloadDone,
13
+ formatDownloadHandedOff,
14
+ } from './download-status.js?v=ui-29';
9
15
 
10
16
  // ── Helpers ───────────────────────────────────────────────────────────────────
11
17
  function fileIcon(name) {
@@ -33,13 +39,6 @@ function isMarkdownFile(path) {
33
39
  return ext === 'md' || ext === 'markdown' || ext === 'mdx';
34
40
  }
35
41
 
36
- function sizeLabel(size) {
37
- if (size == null) return '';
38
- if (size > 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`;
39
- if (size > 1024) return `${(size / 1024).toFixed(1)} KB`;
40
- return `${size} B`;
41
- }
42
-
43
42
  function base64ToArrayBuffer(b64) {
44
43
  const bin = atob(b64);
45
44
  const bytes = new Uint8Array(bin.length);
@@ -381,35 +380,75 @@ function triggerUpload() {
381
380
  input.click();
382
381
  }
383
382
 
383
+ let downloadBusy = false;
384
+ let downloadToastTimer = 0;
385
+
386
+ function downloadToastEl() {
387
+ let el = document.getElementById('file-download-toast');
388
+ if (el) return el;
389
+ el = document.createElement('div');
390
+ el.id = 'file-download-toast';
391
+ el.className = 'file-download-toast';
392
+ el.setAttribute('role', 'status');
393
+ el.setAttribute('aria-live', 'polite');
394
+ el.hidden = true;
395
+ document.body.appendChild(el);
396
+ return el;
397
+ }
398
+
399
+ function showDownloadToast(text, { sticky = false } = {}) {
400
+ const el = downloadToastEl();
401
+ el.textContent = text;
402
+ el.hidden = false;
403
+ el.classList.add('visible');
404
+ clearTimeout(downloadToastTimer);
405
+ if (!sticky) {
406
+ downloadToastTimer = setTimeout(() => {
407
+ el.classList.remove('visible');
408
+ }, 5000);
409
+ }
410
+ }
411
+
412
+ function triggerBrowserDownload(href, name) {
413
+ const a = document.createElement('a');
414
+ a.href = href;
415
+ a.download = name;
416
+ a.rel = 'noopener';
417
+ a.style.display = 'none';
418
+ document.body.appendChild(a);
419
+ a.click();
420
+ a.remove();
421
+ }
422
+
384
423
  async function downloadFile(proj, filePath) {
385
424
  const root = filesRoot.peek() || proj?.path || '';
386
425
  if (!root) {
387
426
  alert('No path selected — set root first');
388
427
  return;
389
428
  }
429
+ if (downloadBusy) {
430
+ showDownloadToast('已有文件正在下载');
431
+ return;
432
+ }
390
433
  const id = '_';
391
434
  const name = (filePath || '').split('/').pop() || 'download';
392
- // Prefer Authorization header (no token in URL / logs). authQuery kept as
393
- // fallback for environments that only support plain navigations.
394
435
  const qs = new URLSearchParams({ root, path: filePath, download: 'true' });
395
436
  const url = `/api/projects/${id}/file?${qs}`;
437
+ downloadBusy = true;
438
+ showDownloadToast(formatDownloadStatus(name), { sticky: true });
396
439
  try {
397
- const handle = typeof window.showSaveFilePicker === 'function'
398
- ? await window.showSaveFilePicker({ suggestedName: name }) : null;
399
440
  const res = await fetch(url, { headers: authHeaders(), credentials: 'same-origin' });
400
441
  if (!res.ok) {
401
442
  const d = await res.json().catch(() => ({}));
402
443
  throw new Error(d.error || `HTTP ${res.status}`);
403
444
  }
404
- if (handle && res.body) {
405
- await res.body.pipeTo(await handle.createWritable());
406
- return;
407
- }
408
- // Browsers without a file sink hand large downloads to their download manager.
409
445
  const maxBlob = 16 * 1024 * 1024;
410
- if (Number(res.headers.get('content-length')) > maxBlob) {
446
+ const total = Number(res.headers.get('content-length')) || 0;
447
+ if (total > maxBlob || !res.body) {
411
448
  await res.body?.cancel();
412
- throw new Error('Use native download');
449
+ triggerBrowserDownload(`/api/projects/${id}/file?${authQuery({ root, path: filePath, download: 'true' })}`, name);
450
+ showDownloadToast(formatDownloadHandedOff(name));
451
+ return;
413
452
  }
414
453
  const reader = res.body.getReader();
415
454
  const chunks = []; let bytes = 0;
@@ -417,37 +456,31 @@ async function downloadFile(proj, filePath) {
417
456
  const { done, value } = await reader.read();
418
457
  if (done) break;
419
458
  bytes += value.byteLength;
420
- if (bytes > maxBlob) { await reader.cancel(); throw new Error('Use native download'); }
459
+ if (bytes > maxBlob) {
460
+ await reader.cancel();
461
+ triggerBrowserDownload(`/api/projects/${id}/file?${authQuery({ root, path: filePath, download: 'true' })}`, name);
462
+ showDownloadToast(formatDownloadHandedOff(name));
463
+ return;
464
+ }
421
465
  chunks.push(value);
466
+ showDownloadToast(formatDownloadStatus(name, bytes, total), { sticky: true });
422
467
  }
423
468
  const blob = new Blob(chunks, { type: res.headers.get('content-type') || 'application/octet-stream' });
424
469
  const objectUrl = URL.createObjectURL(blob);
425
- const a = document.createElement('a');
426
- a.href = objectUrl;
427
- a.download = name;
428
- a.rel = 'noopener';
429
- a.style.display = 'none';
430
- document.body.appendChild(a);
431
- a.click();
432
- a.remove();
433
- // iOS may need the URL briefly; revoke after a beat.
470
+ triggerBrowserDownload(objectUrl, name);
434
471
  setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);
472
+ showDownloadToast(formatDownloadDone(name));
435
473
  } catch (e) {
436
474
  if (e.name === 'AbortError') return;
437
- // Last resort: open with token query (works when fetch is blocked, e.g. some WebViews).
438
475
  try {
439
- const fallback = `/api/projects/${id}/file?${authQuery({ root, path: filePath, download: 'true' })}`;
440
- const a = document.createElement('a');
441
- a.href = fallback;
442
- a.download = name;
443
- a.rel = 'noopener';
444
- a.target = '_blank';
445
- document.body.appendChild(a);
446
- a.click();
447
- a.remove();
476
+ triggerBrowserDownload(`/api/projects/${id}/file?${authQuery({ root, path: filePath, download: 'true' })}`, name);
477
+ showDownloadToast(formatDownloadHandedOff(name));
448
478
  } catch {
479
+ showDownloadToast(`下载失败:${e.message}`);
449
480
  alert(`Download failed: ${e.message}`);
450
481
  }
482
+ } finally {
483
+ downloadBusy = false;
451
484
  }
452
485
  }
453
486
 
package/public/git.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
package/public/index.html CHANGED
@@ -85,7 +85,7 @@
85
85
  <script src="/vendor/mermaid.min.js"></script>
86
86
  <link rel="stylesheet" href="/vendor/katex.min.css">
87
87
  <script src="/vendor/katex.min.js"></script>
88
- <link rel="stylesheet" href="style.css?v=ui-28">
88
+ <link rel="stylesheet" href="style.css?v=ui-29">
89
89
  </head>
90
90
  <body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
91
91
  <div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
@@ -101,6 +101,6 @@
101
101
  }
102
102
  }, 10000);
103
103
  </script>
104
- <script type="module" src="app.js?v=ui-28"></script>
104
+ <script type="module" src="app.js?v=ui-29"></script>
105
105
  </body>
106
106
  </html>
package/public/lib.js CHANGED
File without changes
File without changes
package/public/mobile.js CHANGED
File without changes
File without changes
File without changes
package/public/pwa.js CHANGED
@@ -69,7 +69,7 @@ export function initPwa() {
69
69
 
70
70
  // Register after first paint so install doesn't compete with boot.
71
71
  const register = () => {
72
- navigator.serviceWorker.register('/sw.js?v=33', { scope: '/' })
72
+ navigator.serviceWorker.register('/sw.js?v=38', { scope: '/' })
73
73
  .then((reg) => {
74
74
  reg.addEventListener('updatefound', () => {
75
75
  const sw = reg.installing;
package/public/router.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/public/shell.js CHANGED
File without changes
package/public/state.js CHANGED
File without changes
package/public/style.css CHANGED
@@ -138,6 +138,29 @@
138
138
  }
139
139
  }
140
140
 
141
+ .file-download-toast {
142
+ position: fixed;
143
+ left: 50%;
144
+ bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
145
+ transform: translateX(-50%) translateY(8px);
146
+ z-index: 80;
147
+ max-width: min(92vw, 28rem);
148
+ padding: 0.7rem 1rem;
149
+ border-radius: 0.7rem;
150
+ background: var(--color-primary, #3b82f6);
151
+ color: var(--color-primary-content, #fff);
152
+ font-size: 0.85rem;
153
+ line-height: 1.35;
154
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
155
+ opacity: 0;
156
+ pointer-events: none;
157
+ transition: opacity .18s ease, transform .18s ease;
158
+ }
159
+ .file-download-toast.visible {
160
+ opacity: 1;
161
+ transform: translateX(-50%) translateY(0);
162
+ }
163
+
141
164
  /* File row actions: hover-reveal on desktop only */
142
165
  @media (min-width: 641px) {
143
166
  .file-row-action {
package/public/sw.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /* Agent UI service worker — cache shell assets only.
2
2
  * Never intercepts WebSocket or mutates API semantics beyond offline fallback.
3
3
  * Bump CACHE when shipping UI that must invalidate old shells. */
4
- const CACHE = 'agent-ui-shell-v37';
4
+ const CACHE = 'agent-ui-shell-v38';
5
5
  const PRECACHE = [
6
6
  '/',
7
7
  '/index.html',
8
8
  '/manifest.webmanifest',
9
- '/style.css?v=ui-28',
9
+ '/style.css?v=ui-29',
10
10
  '/style.css',
11
11
  '/ui.min.css',
12
12
  '/vendor/mini-react.min.js',
@@ -16,7 +16,7 @@ const PRECACHE = [
16
16
  '/vendor/mermaid.min.js',
17
17
  '/vendor/katex.min.js',
18
18
  '/vendor/katex.min.css',
19
- '/app.js?v=ui-28',
19
+ '/app.js?v=ui-29',
20
20
  '/icons/icon-192.png',
21
21
  '/icons/icon-512.png',
22
22
  '/icons/icon-maskable-512.png',
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/app.js CHANGED
File without changes
File without changes
File without changes
@@ -1,4 +1,5 @@
1
1
  import { WebSocket } from 'ws';
2
+ import { request } from 'node:http';
2
3
  import { setTimeout as sleep } from 'node:timers/promises';
3
4
 
4
5
  // Aggregate wire budget across downloads/previews, separate from chat traffic.
@@ -46,15 +47,22 @@ export function createHTTPTransfer({ gatewayURL, token, machineId, budget = new
46
47
  ws.send(JSON.stringify({ ...data, reqId: msg.reqId }), err => err ? reject(err) : resolve());
47
48
  });
48
49
  try {
49
- const res = await fetch(url, { ...options, signal: controller.signal });
50
- const headers = sanitizeHeaders(Object.fromEntries(res.headers.entries()));
50
+ // A paced consumer can outlive the local server's keep-alive timeout.
51
+ // Read the dedicated HTTP stream directly instead of fetch's pooled parser.
52
+ const res = await new Promise((resolve, reject) => {
53
+ const req = request(url, { method: options.method, headers: { ...options.headers, Connection: 'close', 'Accept-Encoding': 'identity' }, signal: controller.signal }, resolve);
54
+ req.on('error', reject);
55
+ req.end(options.body);
56
+ });
57
+ const upstreamHeaders = Object.fromEntries(Object.entries(res.headers).filter(([, value]) => value != null).map(([key, value]) => [key, Array.isArray(value) ? value.join(', ') : String(value)]));
58
+ const headers = sanitizeHeaders(upstreamHeaders);
51
59
  // Identity bodies retain their exact length (ordinary proxy replies recompute it).
52
- if (!res.headers.get('content-encoding') || res.headers.get('content-encoding') === 'identity') {
53
- const length = res.headers.get('content-length');
54
- if (length !== null) headers['content-length'] = length;
60
+ if (!upstreamHeaders['content-encoding'] || upstreamHeaders['content-encoding'] === 'identity') {
61
+ const length = upstreamHeaders['content-length'];
62
+ if (length != null) headers['content-length'] = length;
55
63
  }
56
- await send({ type: 'http-res-start', status: res.status, headers });
57
- for await (const value of res.body || []) {
64
+ await send({ type: 'http-res-start', status: res.statusCode, headers });
65
+ for await (const value of res) {
58
66
  // Each frame is small; sends wait for socket backpressure and share a rate cap.
59
67
  for (let offset = 0; offset < value.byteLength; offset += 24 * 1024) {
60
68
  const chunk = Buffer.from(value.subarray(offset, offset + 24 * 1024));
@@ -65,6 +73,7 @@ export function createHTTPTransfer({ gatewayURL, token, machineId, budget = new
65
73
  await send({ type: 'http-end' });
66
74
  } catch (err) {
67
75
  if (ws.readyState === WebSocket.OPEN) await send({ type: 'http-error', message: err.message }).catch(() => {});
76
+ console.warn(`[transfer] ${msg.reqId}: ${err.message}`);
68
77
  throw err;
69
78
  }
70
79
  } finally {
File without changes
File without changes