claude-simple 0.2.23 → 0.2.25

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.
@@ -2,7 +2,7 @@
2
2
  import { batch, esc, $ } from '../lib.js';
3
3
  import {
4
4
  sessionsData, sessionFilter, filesPath, viewingFile, filteredSessions,
5
- currentProject, currentTab, filesRoot, gitRoot, currentAgent, setAgent,
5
+ currentProject, currentTab, filesRoot, gitRoot, currentAgent, setAgent, isProcessing,
6
6
  selectedMachineId, setSelectedMachine, AGENT_LABELS, ctx, hubMode,
7
7
  } from '../state.js';
8
8
  import { api } from '../api.js';
@@ -10,13 +10,31 @@ import { getLastSessionContext, setLastSessionContext } from './session-context.
10
10
  import {
11
11
  connectWS, clearMessages, appendMsg, appendSystemMsg, renderToolUse,
12
12
  appendHistoryTokenBar, appendContextBar, flushToolBatch, coerceTs,
13
- stashComposerDraft, restoreComposerDraft, discardComposerAttachments, parkQueuedMessages, detachProcessingForSessionSwitch,
13
+ stashComposerDraft, restoreComposerDraft, discardComposerAttachments, parkQueuedMessages, detachProcessingForSessionSwitch, setExternalSessionReadOnly,
14
14
  } from '../chat.js';
15
15
  import { setHash, syncHash } from '../router.js';
16
16
  import { refreshModelSelect } from './session-list.js';
17
17
  import { renderSessionNotesBar } from './notes.js';
18
18
  import { addRecentSession, updateDashboard, getDashboardSessions } from './dashboard.js';
19
19
 
20
+ // A terminal-owned agent writes its native transcript independently of this
21
+ // UI. Keep just the currently open session hot and coalesce file updates into
22
+ // one fresh history read, rather than attempting to replay every vendor JSONL
23
+ // event in the browser.
24
+ let activeExternalHistory = null;
25
+ let externalHistoryTimer = null;
26
+
27
+ document.addEventListener('external-session-updated', ({ detail }) => {
28
+ const active = activeExternalHistory;
29
+ if (!active || !detail || detail.sessionId !== active.sessionId) return;
30
+ if (detail.agent && active.agent && detail.agent !== active.agent) return;
31
+ if (externalHistoryTimer) clearTimeout(externalHistoryTimer);
32
+ externalHistoryTimer = setTimeout(() => {
33
+ externalHistoryTimer = null;
34
+ activeExternalHistory?.refresh();
35
+ }, 140);
36
+ });
37
+
20
38
  /** Show which edge machine Files/Git/Shell paths belong to. */
21
39
  function syncMachinePathHints(machineId) {
22
40
  for (const id of ['files-machine-hint', 'git-machine-hint']) {
@@ -35,6 +53,8 @@ function syncMachinePathHints(machineId) {
35
53
  }
36
54
 
37
55
  export function openProject(project) {
56
+ activeExternalHistory = null;
57
+ setExternalSessionReadOnly(false);
38
58
  stashComposerDraft();
39
59
  parkQueuedMessages();
40
60
  discardComposerAttachments();
@@ -67,6 +87,8 @@ export function openProject(project) {
67
87
  }
68
88
 
69
89
  export function goHome() {
90
+ activeExternalHistory = null;
91
+ setExternalSessionReadOnly(false);
70
92
  stashComposerDraft();
71
93
  parkQueuedMessages();
72
94
  discardComposerAttachments();
@@ -144,6 +166,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
144
166
  }
145
167
 
146
168
  ctx.sessionId = sid;
169
+ setExternalSessionReadOnly(false);
147
170
  ctx.configDir = resolvedConfig;
148
171
  ctx.agent = resolvedAgent;
149
172
  setAgent(resolvedAgent);
@@ -153,7 +176,6 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
153
176
  // Default path = session cwd on the target machine (set immediately; no await)
154
177
  let edgeCwd = resolvedCwd;
155
178
  applyProjectPaths(edgeCwd, resolvedMachine, sid);
156
- connectWS();
157
179
 
158
180
  // Background: normalize path on the edge (does not block chat)
159
181
  if (edgeCwd) {
@@ -169,6 +191,9 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
169
191
 
170
192
  // Switch to target tab BEFORE history (files/git/shell usable immediately)
171
193
  clearMessages();
194
+ // Subscribe after clearing the prior transcript. A queued Grok snapshot can
195
+ // arrive immediately on socket-open and must belong to the new history.
196
+ connectWS();
172
197
  switchTab(targetTab);
173
198
  {
174
199
  const cur = sessionsData.peek();
@@ -227,7 +252,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
227
252
  ? m.content.map(p => p?.text || '').filter(Boolean).join('\n')
228
253
  : String(m.content ?? ''));
229
254
  if (content) {
230
- appendMsg(m.role === 'user' ? 'user' : 'assistant', m.role === 'user' ? 'You' : label, content, { ts });
255
+ appendMsg(m.role === 'user' ? 'user' : 'assistant', m.role === 'user' ? 'You' : label, content, { ts, historyIndex: i });
231
256
  }
232
257
  } else if (typ === 'tool_use' || m.name) {
233
258
  renderToolUse({ name: m.name || 'tool', input: m.input ?? m.arguments ?? {} }, { ts });
@@ -252,14 +277,13 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
252
277
  paintChunk();
253
278
  };
254
279
 
255
- const loadHistory = async () => {
280
+ const loadHistory = async ({ preferCache = true } = {}) => {
256
281
  // Instant re-open from in-memory cache (never trust empty cache — race/stale)
257
282
  const cached = historyCache.get(cacheKey);
258
- if (cached && cached.msgs?.length && Date.now() - cached.at < HISTORY_CACHE_TTL) {
283
+ if (preferCache && cached && cached.msgs?.length && Date.now() - cached.at < HISTORY_CACHE_TTL) {
259
284
  paintMessages(cached.msgs, cached.context, cached.meta);
260
- return;
261
285
  }
262
- appendSystemMsg(`Loading ${label} history…`);
286
+ if (!cached?.msgs?.length) appendSystemMsg(`Loading ${label} history…`);
263
287
  try {
264
288
  // Hub mode must route to the edge that owns the session.
265
289
  const mid = resolvedMachine || selectedMachineId.peek() || ctx.machineId || null;
@@ -273,10 +297,6 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
273
297
  const q = new URLSearchParams({
274
298
  tail: '0',
275
299
  compact: '1',
276
- // Hub keeps a short-lived compact copy after completed turns. This
277
- // makes a mobile re-open cache-first while the live WS remains the
278
- // source for an in-progress answer.
279
- cache: 'prefer',
280
300
  });
281
301
  if (agent) q.set('agent', agent);
282
302
  // Also put machine in query for proxies that drop custom headers.
@@ -284,6 +304,9 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
284
304
  return api('GET', `/api/sessions/${sid}/messages?${q}`, undefined, undefined, {
285
305
  machineId: mid || undefined,
286
306
  dedupe: false,
307
+ // A transcript can be written shortly after the final stream frame.
308
+ // Never let an HTTP/browser cache hide a newer user prompt.
309
+ cache: 'no-store',
287
310
  });
288
311
  };
289
312
 
@@ -299,6 +322,10 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
299
322
  const meta = Array.isArray(data) ? {} : { truncated: data?.truncated, total: data?.total };
300
323
  if (msgs.length) historyCache.set(cacheKey, { msgs, context, meta, at: Date.now() });
301
324
  else historyCache.delete(cacheKey);
325
+ // A reconnect can receive new stream frames while this verification read
326
+ // is in flight. Keep those live frames authoritative; the fresh response
327
+ // is still retained for the next open.
328
+ if (isProcessing.peek() || $('messages')?.querySelector('.live-stream')) return;
302
329
  paintMessages(msgs, context, meta);
303
330
  } catch (e) {
304
331
  if (ctx.sessionId !== sid) return;
@@ -308,6 +335,12 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
308
335
  }
309
336
  };
310
337
 
338
+ activeExternalHistory = {
339
+ sessionId: sid,
340
+ agent: resolvedAgent,
341
+ refresh: () => loadHistory({ preferCache: false }).catch(() => {}),
342
+ };
343
+
311
344
  // Never block tab UI on history; chat still loads ASAP in background
312
345
  if (skipHistory) {
313
346
  loadHistory().catch(() => {});
package/public/state.js CHANGED
@@ -77,7 +77,7 @@ export const agentFilters = signal((() => {
77
77
  export const timeRange = signal(localStorage.getItem('timeRange') || '14'); // '0'|'1'|'7'|'14'|'30'
78
78
  export const activityHits = signal(null); // null | { q, results, loading, error }
79
79
  export const activityLoading = signal(false);
80
- /** agent:sessionId → { favorite, notes, title, hidden, updatedAt } (title = rename map) */
80
+ /** agent:sessionId → { favorite, notes, title, hidden, branch, updatedAt } (title = rename map) */
81
81
  export const sessionMetaMap = signal({});
82
82
  /** Sidebar: show only favorited sessions */
83
83
  export const favoritesOnly = signal(localStorage.getItem('favoritesOnly') === '1');
@@ -107,6 +107,7 @@ export const ctx = {
107
107
  ws: null,
108
108
  shellBubble: null,
109
109
  configDir: null,
110
+ externalSessionReadOnly: false,
110
111
  };
111
112
 
112
113
  /** Select edge machine (hub). Keeps signal + ctx + localStorage in sync. */
@@ -129,7 +130,7 @@ export function metaKey(agent, sessionId, machineId) {
129
130
 
130
131
  export function getSessionMeta(agent, sessionId, machineId) {
131
132
  const m = sessionMetaMap.peek()?.[metaKey(agent, sessionId, machineId)];
132
- return m || { favorite: false, notes: '', title: '', hidden: false };
133
+ return m || { favorite: false, notes: '', title: '', hidden: false, branch: null };
133
134
  }
134
135
 
135
136
  /** Display title: user rename map → original display → short id. */
package/public/style.css CHANGED
@@ -733,6 +733,37 @@ html.pwa-standalone .ma-panel {
733
733
  .session-row.session-thin {
734
734
  opacity: 0.88;
735
735
  }
736
+ .session-row.session-branch {
737
+ margin-left: calc(var(--branch-depth, 1) * 0.7rem);
738
+ border-left-style: dashed;
739
+ }
740
+ .session-branch-line {
741
+ margin-top: 1px;
742
+ font-size: 9px;
743
+ line-height: 1.25;
744
+ font-family: ui-monospace, Menlo, monospace;
745
+ color: color-mix(in oklch, var(--color-primary) 58%, var(--color-base-content));
746
+ overflow: hidden;
747
+ text-overflow: ellipsis;
748
+ white-space: nowrap;
749
+ }
750
+ .session-branch-badge {
751
+ font-size: 9px;
752
+ color: color-mix(in oklch, var(--color-primary) 82%, var(--color-base-content));
753
+ border: 1px solid color-mix(in oklch, var(--color-primary) 28%, transparent);
754
+ border-radius: 3px;
755
+ padding: 0 3px;
756
+ }
757
+ .session-branch-current {
758
+ margin-left: auto;
759
+ max-width: 10rem;
760
+ overflow: hidden;
761
+ text-overflow: ellipsis;
762
+ white-space: nowrap;
763
+ font-family: ui-monospace, Menlo, monospace;
764
+ font-size: 10px;
765
+ color: color-mix(in oklch, var(--color-primary) 72%, var(--color-base-content));
766
+ }
736
767
  .session-thin-badge {
737
768
  font-size: 9px;
738
769
  font-family: ui-monospace, Menlo, monospace;
@@ -1304,6 +1335,41 @@ html.pwa-standalone .ma-panel {
1304
1335
  user-select: none;
1305
1336
  }
1306
1337
  .msg-row.user .msg-ts { text-align: right; }
1338
+ .msg-row.queued-user-msg > div:not(.msg-ts) {
1339
+ opacity: .76;
1340
+ border-style: dashed;
1341
+ }
1342
+ .msg-row.cancelled-queued-user-msg > div:not(.msg-ts) {
1343
+ opacity: .48;
1344
+ text-decoration: line-through;
1345
+ }
1346
+ .msg-actions {
1347
+ display: flex;
1348
+ align-items: center;
1349
+ gap: 0.25rem;
1350
+ padding: 0 0.45rem;
1351
+ opacity: 0;
1352
+ transition: opacity .12s ease;
1353
+ }
1354
+ .msg-row.assistant:hover .msg-actions,
1355
+ .msg-row.assistant:focus-within .msg-actions { opacity: 1; }
1356
+ .msg-action-btn {
1357
+ appearance: none;
1358
+ border: 0;
1359
+ background: transparent;
1360
+ color: color-mix(in oklch, var(--color-base-content) 48%, transparent);
1361
+ font-size: 10px;
1362
+ line-height: 1.4;
1363
+ padding: 1px 4px;
1364
+ border-radius: 4px;
1365
+ cursor: pointer;
1366
+ }
1367
+ .msg-action-btn:hover:not(:disabled) {
1368
+ color: var(--color-primary);
1369
+ background: color-mix(in oklch, var(--color-primary) 12%, transparent);
1370
+ }
1371
+ .msg-action-btn:disabled { cursor: wait; opacity: .55; }
1372
+ @media (hover: none) { .msg-actions { opacity: 1; } }
1307
1373
 
1308
1374
  .token-bar {
1309
1375
  display: flex;
@@ -1623,3 +1689,63 @@ html.pwa-standalone .ma-panel {
1623
1689
  padding: 0.05em 0 0.15em 0.5em !important;
1624
1690
  }
1625
1691
  }
1692
+
1693
+ /* ── Settings: full-page overlay using the same DaisyUI tokens as #app ── */
1694
+
1695
+ html.settings-open #app { visibility: hidden; }
1696
+ .set-page {
1697
+ position: fixed;
1698
+ inset: 0;
1699
+ z-index: 50;
1700
+ background: hsl(var(--b1));
1701
+ color: hsl(var(--bc));
1702
+ overflow: auto;
1703
+ min-height: 100dvh;
1704
+ }
1705
+ .set-island {
1706
+ position: sticky;
1707
+ top: 0;
1708
+ z-index: 2;
1709
+ display: flex;
1710
+ align-items: center;
1711
+ gap: 0.5rem;
1712
+ height: 44px;
1713
+ padding: 0 0.75rem;
1714
+ background: hsl(var(--b2));
1715
+ border-bottom: 1px solid hsl(var(--b3));
1716
+ }
1717
+ .set-split {
1718
+ display: grid;
1719
+ grid-template-columns: minmax(140px, 200px) minmax(0, 1fr);
1720
+ gap: 0.75rem;
1721
+ max-width: 56rem;
1722
+ margin: 1rem auto 2.5rem;
1723
+ padding: 0 1rem 2rem;
1724
+ }
1725
+ .set-nav { display: flex; flex-direction: column; gap: 0.25rem; padding-top: 0.25rem; }
1726
+ .set-nav-item.is-on {
1727
+ background: hsl(var(--b3));
1728
+ color: hsl(var(--bc));
1729
+ }
1730
+ .set-main { min-width: 0; }
1731
+ .set-panel { padding: 0.25rem 0 1.5rem; }
1732
+ .set-fields { display: grid; gap: 0.5rem; }
1733
+ .set-list { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
1734
+ .set-row {
1735
+ display: flex;
1736
+ align-items: center;
1737
+ gap: 0.75rem;
1738
+ padding: 0.65rem 0.75rem;
1739
+ border-radius: 0.5rem;
1740
+ background: hsl(var(--b2));
1741
+ border: 1px solid hsl(var(--b3));
1742
+ }
1743
+ .set-row-body { flex: 1; min-width: 0; }
1744
+ .set-add { display: grid; gap: 0.5rem; }
1745
+ .set-token { margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; }
1746
+ .set-token textarea { min-height: 7rem; resize: vertical; }
1747
+ @media (max-width: 767px) {
1748
+ .set-split { grid-template-columns: 1fr; }
1749
+ .set-nav { flex-direction: row; overflow-x: auto; gap: 0.25rem; }
1750
+ .set-nav-item { white-space: nowrap; }
1751
+ }