claude-simple 0.2.17 → 0.2.19

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/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.17.min.css">
7
+ <link rel="stylesheet" href="/style.0.2.19.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.17.min.js"></script>
14
+ <script type="module" src="/app.0.2.19.min.js"></script>
15
15
  </body>
16
16
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-simple",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude-simple": "server.js",
package/public/chat.js CHANGED
@@ -34,6 +34,7 @@ let lifecycleBound = false;
34
34
  let sawDisconnect = false;
35
35
  let lastWsMachine = null;
36
36
  let reconnectQuiet = false;
37
+ const HUB_CURSOR_KEY = 'hubStreamCursors';
37
38
  /** FIFO messages deliberately queued while a turn is running. */
38
39
  let pendingUserTexts = [];
39
40
  // Composer state belongs to a conversation, never to the shared textarea.
@@ -41,6 +42,34 @@ let pendingUserTexts = [];
41
42
  // prompts to disk while still making session switches lossless.
42
43
  const composerDrafts = new Map();
43
44
 
45
+ function hubCursorKey(sessionId = ctx.sessionId, agent = ctx.agent, machineId = ctx.machineId) {
46
+ return sessionId ? `${machineId || ''}:${agent || 'claude'}:${sessionId}` : '';
47
+ }
48
+
49
+ function getHubCursor(identity = {}) {
50
+ const key = hubCursorKey(identity.sessionId, identity.agent, identity.machineId);
51
+ if (!key) return 0;
52
+ try { return Number(JSON.parse(localStorage.getItem(HUB_CURSOR_KEY) || '{}')[key]) || 0; }
53
+ catch { return 0; }
54
+ }
55
+
56
+ function acceptHubSequence(msg) {
57
+ const seq = Number(msg?.hubSeq) || 0;
58
+ if (!seq) return true;
59
+ const key = hubCursorKey(msg.sessionId || ctx.sessionId, msg.agent || ctx.agent, ctx.machineId);
60
+ if (!key) return true;
61
+ let all = {};
62
+ try { all = JSON.parse(localStorage.getItem(HUB_CURSOR_KEY) || '{}') || {}; } catch {}
63
+ const seen = Number(all[key]) || 0;
64
+ if (seq <= seen) return false;
65
+ all[key] = seq;
66
+ // Keep the small cursor map bounded; it is metadata, never chat content.
67
+ const keys = Object.keys(all);
68
+ if (keys.length > 500) keys.sort((a, b) => Number(all[b]) - Number(all[a])).slice(500).forEach(k => delete all[k]);
69
+ try { localStorage.setItem(HUB_CURSOR_KEY, JSON.stringify(all)); } catch {}
70
+ return true;
71
+ }
72
+
44
73
  function draftKey({ sessionId = ctx.sessionId, agent = ctx.agent, machineId = ctx.machineId } = {}) {
45
74
  if (!sessionId) return null;
46
75
  return `${machineId || ''}:${agent || 'claude'}:${sessionId}`;
@@ -223,6 +252,9 @@ export function connectWS(opts = {}) {
223
252
  if (ctx.token) q.set('token', ctx.token);
224
253
  if (mid) q.set('machine', mid);
225
254
  if (ctx.sessionId) q.set('sessionId', ctx.sessionId);
255
+ if (ctx.agent) q.set('agent', ctx.agent);
256
+ const lastSeq = getHubCursor();
257
+ if (lastSeq) q.set('lastSeq', String(lastSeq));
226
258
 
227
259
  const sock = new WebSocket(`${proto}://${location.host}/ws/chat?${q}`);
228
260
  ctx.ws = sock;
@@ -250,6 +282,7 @@ export function connectWS(opts = {}) {
250
282
  if (gen !== wsGen) return;
251
283
  let msg;
252
284
  try { msg = JSON.parse(e.data); } catch { return; }
285
+ if (!acceptHubSequence(msg)) return;
253
286
  if (msg.type === 'pong') { lastPongAt = Date.now(); return; }
254
287
  if (msg.type === 'ping') {
255
288
  lastPongAt = Date.now();
package/public/index.html CHANGED
@@ -83,7 +83,7 @@
83
83
  <link id="hljs-theme" rel="stylesheet" href="/vendor/highlight-dark.min.css">
84
84
  <script src="/vendor/highlight.min.js"></script>
85
85
  <script src="/vendor/mermaid.min.js"></script>
86
- <link rel="stylesheet" href="style.css?v=ui-16">
86
+ <link rel="stylesheet" href="style.css?v=ui-18">
87
87
  </head>
88
88
  <body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
89
89
  <div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
@@ -99,6 +99,6 @@
99
99
  }
100
100
  }, 10000);
101
101
  </script>
102
- <script type="module" src="app.js?v=ui-16"></script>
102
+ <script type="module" src="app.js?v=ui-18"></script>
103
103
  </body>
104
104
  </html>
@@ -137,22 +137,32 @@ export function syncTopbarMachine() {
137
137
  }
138
138
 
139
139
  let _machinePollTimer = null;
140
+ let _sessionIndexPollTimer = null;
140
141
  export function startMachinePolling() {
141
- if (_machinePollTimer) return;
142
- _machinePollTimer = setInterval(async () => {
143
- if (!hubMode.peek() || !ctx.token) return;
144
- await refreshMachinesList();
145
- syncTopbarMachine();
146
- // If current machine went offline, warn in topbar label only (don't force picker)
147
- const cur = selectedMachineId.peek();
148
- if (cur) {
149
- const m = (machinesList.peek() || []).find(x => x.id === cur);
150
- if (m && m.online === false) {
151
- const label = $('topbar-machine-label');
152
- if (label) label.textContent = `${cur} (offline)`;
142
+ if (!_machinePollTimer) {
143
+ _machinePollTimer = setInterval(async () => {
144
+ if (!hubMode.peek() || !ctx.token) return;
145
+ await refreshMachinesList();
146
+ syncTopbarMachine();
147
+ // If current machine went offline, warn in topbar label only (don't force picker)
148
+ const cur = selectedMachineId.peek();
149
+ if (cur) {
150
+ const m = (machinesList.peek() || []).find(x => x.id === cur);
151
+ if (m && m.online === false) {
152
+ const label = $('topbar-machine-label');
153
+ if (label) label.textContent = `${cur} (offline)`;
154
+ }
153
155
  }
154
- }
155
- }, 15000);
156
+ }, 15000);
157
+ }
158
+ if (!_sessionIndexPollTimer) {
159
+ _sessionIndexPollTimer = setInterval(() => {
160
+ // Background tabs do not keep a radio wake-lock merely to refresh a
161
+ // sidebar. Foreground clients get the latest Hub delta within 2 seconds.
162
+ if (document.visibilityState !== 'visible' || !hubMode.peek() || !ctx.token || !selectedMachineId.peek()) return;
163
+ loadAllSessions({ waitFresh: false, hubIndex: true }).catch(() => {});
164
+ }, 2000);
165
+ }
156
166
  }
157
167
 
158
168
  /** Enter / switch hub machine. */
@@ -22,17 +22,20 @@ function normalizeSessions(v) {
22
22
  return [];
23
23
  }
24
24
 
25
- function sessionsQuery() {
25
+ function sessionsQuery({ hubIndex = false } = {}) {
26
26
  const q = new URLSearchParams();
27
27
  // Project index is small and should remain complete; the Hub/browser cache
28
28
  // keeps this inexpensive after the first fetch.
29
29
  q.set('limit', '2000');
30
+ // This is a Hub-local, ETag-enabled catalog read. It never tunnels to an
31
+ // Edge or invokes the expensive multi-agent session scanner.
32
+ if (hubIndex && hubMode.peek()) q.set('index', '1');
30
33
  const s = q.toString();
31
34
  return s ? `/api/sessions?${s}` : '/api/sessions';
32
35
  }
33
36
 
34
- /** @param {{ waitFresh?: boolean }} opts waitFresh=false → paint cache first, refresh in bg */
35
- export async function loadAllSessions({ waitFresh = true } = {}) {
37
+ /** @param {{ waitFresh?: boolean, hubIndex?: boolean }} opts waitFresh=false → paint cache first, refresh in bg */
38
+ export async function loadAllSessions({ waitFresh = true, hubIndex = false } = {}) {
36
39
  const cached = normalizeSessions(await getCachedSessions());
37
40
  if (cached.length) {
38
41
  // Hub: if we already selected a machine, prefer cache entries for that machine
@@ -46,7 +49,7 @@ export async function loadAllSessions({ waitFresh = true } = {}) {
46
49
  }
47
50
 
48
51
  const fetchFresh = async () => {
49
- const path = sessionsQuery();
52
+ const path = sessionsQuery({ hubIndex });
50
53
  const fresh = await api('GET', path, undefined, undefined, { etag: true });
51
54
  if (fresh === NOT_MODIFIED) return sessionsData.peek();
52
55
  const list = normalizeSessions(fresh);
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-v21';
4
+ const CACHE = 'agent-ui-shell-v23';
5
5
  const PRECACHE = [
6
6
  '/',
7
7
  '/index.html',
8
8
  '/manifest.webmanifest',
9
- '/style.css?v=ui-16',
9
+ '/style.css?v=ui-18',
10
10
  '/style.css',
11
11
  '/ui.min.css',
12
12
  '/vendor/mini-react.min.js',
@@ -14,7 +14,7 @@ const PRECACHE = [
14
14
  '/vendor/highlight.min.js',
15
15
  '/vendor/highlight-dark.min.css',
16
16
  '/vendor/mermaid.min.js',
17
- '/app.js?v=ui-16',
17
+ '/app.js?v=ui-18',
18
18
  '/icons/icon-192.png',
19
19
  '/icons/icon-512.png',
20
20
  '/icons/icon-maskable-512.png',