claude-simple 0.2.24 → 0.2.26

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,32 @@ 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
+ import { acquireSessionLock, releaseSessionLock } from '../locks.js';
15
16
  import { setHash, syncHash } from '../router.js';
16
17
  import { refreshModelSelect } from './session-list.js';
17
18
  import { renderSessionNotesBar } from './notes.js';
18
19
  import { addRecentSession, updateDashboard, getDashboardSessions } from './dashboard.js';
19
20
 
21
+ // A terminal-owned agent writes its native transcript independently of this
22
+ // UI. Keep just the currently open session hot and coalesce file updates into
23
+ // one fresh history read, rather than attempting to replay every vendor JSONL
24
+ // event in the browser.
25
+ let activeExternalHistory = null;
26
+ let externalHistoryTimer = null;
27
+
28
+ document.addEventListener('external-session-updated', ({ detail }) => {
29
+ const active = activeExternalHistory;
30
+ if (!active || !detail || detail.sessionId !== active.sessionId) return;
31
+ if (detail.agent && active.agent && detail.agent !== active.agent) return;
32
+ if (externalHistoryTimer) clearTimeout(externalHistoryTimer);
33
+ externalHistoryTimer = setTimeout(() => {
34
+ externalHistoryTimer = null;
35
+ activeExternalHistory?.refresh();
36
+ }, 140);
37
+ });
38
+
20
39
  /** Show which edge machine Files/Git/Shell paths belong to. */
21
40
  function syncMachinePathHints(machineId) {
22
41
  for (const id of ['files-machine-hint', 'git-machine-hint']) {
@@ -35,6 +54,9 @@ function syncMachinePathHints(machineId) {
35
54
  }
36
55
 
37
56
  export function openProject(project) {
57
+ activeExternalHistory = null;
58
+ releaseSessionLock();
59
+ setExternalSessionReadOnly(false);
38
60
  stashComposerDraft();
39
61
  parkQueuedMessages();
40
62
  discardComposerAttachments();
@@ -67,6 +89,9 @@ export function openProject(project) {
67
89
  }
68
90
 
69
91
  export function goHome() {
92
+ activeExternalHistory = null;
93
+ releaseSessionLock();
94
+ setExternalSessionReadOnly(false);
70
95
  stashComposerDraft();
71
96
  parkQueuedMessages();
72
97
  discardComposerAttachments();
@@ -144,6 +169,8 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
144
169
  }
145
170
 
146
171
  ctx.sessionId = sid;
172
+ setExternalSessionReadOnly(false);
173
+ acquireSessionLock({ sessionId: sid, agent: resolvedAgent, machineId: resolvedMachine }).catch(() => {});
147
174
  ctx.configDir = resolvedConfig;
148
175
  ctx.agent = resolvedAgent;
149
176
  setAgent(resolvedAgent);
@@ -153,7 +180,6 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
153
180
  // Default path = session cwd on the target machine (set immediately; no await)
154
181
  let edgeCwd = resolvedCwd;
155
182
  applyProjectPaths(edgeCwd, resolvedMachine, sid);
156
- connectWS();
157
183
 
158
184
  // Background: normalize path on the edge (does not block chat)
159
185
  if (edgeCwd) {
@@ -169,6 +195,9 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
169
195
 
170
196
  // Switch to target tab BEFORE history (files/git/shell usable immediately)
171
197
  clearMessages();
198
+ // Subscribe after clearing the prior transcript. A queued Grok snapshot can
199
+ // arrive immediately on socket-open and must belong to the new history.
200
+ connectWS();
172
201
  switchTab(targetTab);
173
202
  {
174
203
  const cur = sessionsData.peek();
@@ -227,7 +256,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
227
256
  ? m.content.map(p => p?.text || '').filter(Boolean).join('\n')
228
257
  : String(m.content ?? ''));
229
258
  if (content) {
230
- appendMsg(m.role === 'user' ? 'user' : 'assistant', m.role === 'user' ? 'You' : label, content, { ts });
259
+ appendMsg(m.role === 'user' ? 'user' : 'assistant', m.role === 'user' ? 'You' : label, content, { ts, historyIndex: i });
231
260
  }
232
261
  } else if (typ === 'tool_use' || m.name) {
233
262
  renderToolUse({ name: m.name || 'tool', input: m.input ?? m.arguments ?? {} }, { ts });
@@ -252,14 +281,13 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
252
281
  paintChunk();
253
282
  };
254
283
 
255
- const loadHistory = async () => {
284
+ const loadHistory = async ({ preferCache = true } = {}) => {
256
285
  // Instant re-open from in-memory cache (never trust empty cache — race/stale)
257
286
  const cached = historyCache.get(cacheKey);
258
- if (cached && cached.msgs?.length && Date.now() - cached.at < HISTORY_CACHE_TTL) {
287
+ if (preferCache && cached && cached.msgs?.length && Date.now() - cached.at < HISTORY_CACHE_TTL) {
259
288
  paintMessages(cached.msgs, cached.context, cached.meta);
260
- return;
261
289
  }
262
- appendSystemMsg(`Loading ${label} history…`);
290
+ if (!cached?.msgs?.length) appendSystemMsg(`Loading ${label} history…`);
263
291
  try {
264
292
  // Hub mode must route to the edge that owns the session.
265
293
  const mid = resolvedMachine || selectedMachineId.peek() || ctx.machineId || null;
@@ -273,10 +301,6 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
273
301
  const q = new URLSearchParams({
274
302
  tail: '0',
275
303
  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
304
  });
281
305
  if (agent) q.set('agent', agent);
282
306
  // Also put machine in query for proxies that drop custom headers.
@@ -284,6 +308,9 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
284
308
  return api('GET', `/api/sessions/${sid}/messages?${q}`, undefined, undefined, {
285
309
  machineId: mid || undefined,
286
310
  dedupe: false,
311
+ // A transcript can be written shortly after the final stream frame.
312
+ // Never let an HTTP/browser cache hide a newer user prompt.
313
+ cache: 'no-store',
287
314
  });
288
315
  };
289
316
 
@@ -299,6 +326,10 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
299
326
  const meta = Array.isArray(data) ? {} : { truncated: data?.truncated, total: data?.total };
300
327
  if (msgs.length) historyCache.set(cacheKey, { msgs, context, meta, at: Date.now() });
301
328
  else historyCache.delete(cacheKey);
329
+ // A reconnect can receive new stream frames while this verification read
330
+ // is in flight. Keep those live frames authoritative; the fresh response
331
+ // is still retained for the next open.
332
+ if (isProcessing.peek() || $('messages')?.querySelector('.live-stream')) return;
302
333
  paintMessages(msgs, context, meta);
303
334
  } catch (e) {
304
335
  if (ctx.sessionId !== sid) return;
@@ -308,6 +339,12 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
308
339
  }
309
340
  };
310
341
 
342
+ activeExternalHistory = {
343
+ sessionId: sid,
344
+ agent: resolvedAgent,
345
+ refresh: () => loadHistory({ preferCache: false }).catch(() => {}),
346
+ };
347
+
311
348
  // Never block tab UI on history; chat still loads ASAP in background
312
349
  if (skipHistory) {
313
350
  loadHistory().catch(() => {});
@@ -79,7 +79,6 @@ export const AppShell = (opts = {}) => {
79
79
  <span id="topbar-project" class="text-sm text-base-content/50 flex-1 truncate min-w-0">Select a session</span>
80
80
  <span id="conn-dot" class="w-2 h-2 rounded-full bg-base-content/30 flex-shrink-0" title="连接状态"></span>
81
81
  <div class="flex gap-2 topbar-actions">
82
- <button id="btn-share-session" class="btn btn-ghost btn-xs border border-base-300" title="复制当前会话分享链接">分享</button>
83
82
  <button id="btn-pwa-install" class="btn btn-ghost btn-xs border border-base-300 hidden" title="安装到主屏幕">安装</button>
84
83
  <button id="btn-meta-agent" class="btn btn-ghost btn-xs border border-primary/40 text-primary topbar-icon-btn" title="Meta Agent — 报告 / 问答 / VLM" aria-label="打开 Meta Agent">
85
84
  <svg class="topbar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="m12 2 1.6 6.4L20 10l-6.4 1.6L12 18l-1.6-6.4L4 10l6.4-1.6L12 2Z"/><path d="m19 16 .7 2.3L22 19l-2.3.7L19 22l-.7-2.3L16 19l2.3-.7L19 16Z"/></svg><span class="topbar-icon-label">Agent</span>
@@ -92,6 +91,10 @@ export const AppShell = (opts = {}) => {
92
91
  <button id="btn-logout" class="btn btn-ghost btn-xs border border-base-300">Sign out</button>
93
92
  </div>
94
93
  </div>
94
+ <button id="btn-expand-chat-chrome" class="chat-chrome-fab hidden" type="button"
95
+ title="显示会话工具栏" aria-label="显示会话工具栏">
96
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M7 14l5-5 5 5"/><path d="M5 19h14"/></svg>
97
+ </button>
95
98
  <!-- Fixed panel + backdrop: siblings of topbar so flex layout never clips them -->
96
99
  <div id="recent-menu-backdrop" class="hidden fixed inset-0 z-[55] bg-black/40" aria-hidden="true"></div>
97
100
  <div id="recent-menu" class="hidden fixed z-[60] w-80 max-h-[min(24rem,70vh)] overflow-y-auto
@@ -255,6 +258,7 @@ export const AppShell = (opts = {}) => {
255
258
  <div id="tab-chat" class="flex flex-col flex-1 overflow-hidden relative">
256
259
  <div id="project-notes-bar" class="bg-base-200 border-b border-base-300 px-3 py-1.5 text-xs flex flex-col gap-1" style="display:none"></div>
257
260
  <div id="session-notes-bar" class="bg-base-200/80 border-b border-base-300 px-3 py-1 text-xs flex justify-between items-center gap-2" style="display:none"></div>
261
+ <div id="session-lock-banner" class="session-lock-banner hidden px-3 py-1.5 text-[11px] text-center border-b"></div>
258
262
  <div id="ws-banner" class="hidden px-3 py-1.5 text-[11px] text-center bg-warning/15 text-warning border-b border-warning/20 flex items-center justify-center gap-2">
259
263
  <span id="ws-banner-text"></span>
260
264
  <button type="button" id="ws-banner-retry" class="btn btn-ghost btn-xs h-6 min-h-0 px-2 text-warning border border-warning/30">重连</button>
package/public/shell.js CHANGED
@@ -310,6 +310,12 @@ export function initShell(opts = {}) {
310
310
  initSidebarResize();
311
311
  initSidebarToggle();
312
312
  initNotifications({ openSession: resumeSession });
313
+ const setChatChromeCollapsed = (collapsed) => {
314
+ $('app')?.classList.toggle('chat-chrome-collapsed', collapsed);
315
+ $('btn-expand-chat-chrome')?.classList.toggle('hidden', !collapsed);
316
+ localStorage.setItem('chatChromeCollapsed', collapsed ? '1' : '0');
317
+ };
318
+ setChatChromeCollapsed(localStorage.getItem('chatChromeCollapsed') === '1');
313
319
 
314
320
  let sessionsChangedTimer = null;
315
321
  document.addEventListener('sessions-changed', () => {
@@ -336,13 +342,17 @@ export function initShell(opts = {}) {
336
342
  });
337
343
 
338
344
  delegate.on('click', '#btn-home', goHome);
339
- delegate.on('click', '#btn-share-session', async (_, el) => {
345
+ delegate.on('click', '#btn-share-session, #btn-share-session-current', async (_, el) => {
340
346
  const url = currentSessionShareUrl();
341
347
  if (!url) return;
342
348
  try {
343
349
  await navigator.clipboard.writeText(url);
344
- el.textContent = '已复制';
345
- setTimeout(() => { el.textContent = '分享'; }, 1400);
350
+ el.classList.add('is-copied');
351
+ el.title = '已复制分享链接';
352
+ setTimeout(() => {
353
+ el.classList.remove('is-copied');
354
+ el.title = '复制当前会话分享链接';
355
+ }, 1400);
346
356
  } catch {
347
357
  window.prompt('复制会话链接:', url);
348
358
  }
@@ -432,6 +442,8 @@ export function initShell(opts = {}) {
432
442
  document.getElementById('btn-pwa-install')?.classList.remove('hidden');
433
443
  });
434
444
  delegate.on('click', '#btn-settings', openSettings);
445
+ delegate.on('click', '#btn-collapse-chat-chrome', () => setChatChromeCollapsed(true));
446
+ delegate.on('click', '#btn-expand-chat-chrome', () => setChatChromeCollapsed(false));
435
447
  delegate.on('click', '#btn-meta-agent', () => toggleMetaAgent());
436
448
  delegate.on('click', '#btn-meta-agent-welcome', () => openMetaAgent('chat'));
437
449
  delegate.on('click', '#btn-theme', toggleTheme);
@@ -610,7 +622,7 @@ export function initShell(opts = {}) {
610
622
  });
611
623
  document.addEventListener('agent-changed', refreshModelSelect);
612
624
 
613
- delegate.on('click', '#btn-new-session, #btn-new-session-welcome', openNewSessionModal);
625
+ delegate.on('click', '#btn-new-session, #btn-new-session-current, #btn-new-session-welcome', openNewSessionModal);
614
626
  delegate.on('click', '#new-session-cancel', () => $('modal-new-session').close());
615
627
  delegate.on('click', '#new-session-start', startNewSession);
616
628
  delegate.on('change', '#new-session-agent', updateNewSessionAgentUI);
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,10 @@ export const ctx = {
107
107
  ws: null,
108
108
  shellBubble: null,
109
109
  configDir: null,
110
+ externalSessionReadOnly: false,
111
+ role: null,
112
+ username: null,
113
+ lock: { key: null, token: null, holder: null, held: false, foreign: false },
110
114
  };
111
115
 
112
116
  /** Select edge machine (hub). Keeps signal + ctx + localStorage in sync. */
@@ -129,7 +133,7 @@ export function metaKey(agent, sessionId, machineId) {
129
133
 
130
134
  export function getSessionMeta(agent, sessionId, machineId) {
131
135
  const m = sessionMetaMap.peek()?.[metaKey(agent, sessionId, machineId)];
132
- return m || { favorite: false, notes: '', title: '', hidden: false };
136
+ return m || { favorite: false, notes: '', title: '', hidden: false, branch: null };
133
137
  }
134
138
 
135
139
  /** Display title: user rename map → original display → short id. */
package/public/style.css CHANGED
@@ -23,13 +23,17 @@
23
23
  #chat-input {
24
24
  min-height: 32px !important;
25
25
  max-height: 72px !important;
26
- padding: 0 !important;
26
+ padding: 8px 12px 4px !important;
27
27
  border-radius: 0.65rem 0.65rem 0 0;
28
28
  /* 16px prevents iOS Safari auto-zoom on focus */
29
29
  font-size: 16px !important;
30
30
  line-height: 1.4 !important;
31
31
  }
32
- .composer-icon-btn { width: 40px; height: 40px; font-size: 19px; }
32
+ /* Two fixed icon slots: settings + attachment never wrap below the input. */
33
+ .composer-tools { flex: 0 0 64px; flex-wrap: nowrap; gap: 0; }
34
+ .composer-icon-btn { width: 32px; height: 32px; min-width: 32px; font-size: 17px; }
35
+ .composer-actions { flex-wrap: nowrap; gap: .35rem; }
36
+ .composer-submit { min-width: 0; flex-shrink: 1; }
33
37
  .composer-send-btn, .composer-stop-btn { min-height: 40px; height: 40px; }
34
38
  .composer-hint { display: none; }
35
39
  #chat-opts {
@@ -541,6 +545,37 @@ html.pwa-standalone .ma-panel {
541
545
  white-space: pre;
542
546
  overflow-x: auto;
543
547
  }
548
+
549
+ /* Markdown files open as a rendered document; source remains one tap away. */
550
+ .file-markdown-toolbar {
551
+ display: flex;
552
+ gap: 0.25rem;
553
+ padding: 0.4rem 1rem;
554
+ border-bottom: 1px solid color-mix(in oklch, var(--color-base-content) 10%, transparent);
555
+ background: color-mix(in oklch, var(--color-base-200) 75%, transparent);
556
+ }
557
+ .file-markdown-mode {
558
+ border: 1px solid transparent;
559
+ border-radius: 0.375rem;
560
+ padding: 0.18rem 0.55rem;
561
+ color: color-mix(in oklch, var(--color-base-content) 58%, transparent);
562
+ background: transparent;
563
+ font-size: 0.72rem;
564
+ font-weight: 600;
565
+ cursor: pointer;
566
+ }
567
+ .file-markdown-mode:hover {
568
+ color: var(--color-base-content);
569
+ background: color-mix(in oklch, var(--color-base-content) 7%, transparent);
570
+ }
571
+ .file-markdown-mode.active {
572
+ color: var(--color-primary);
573
+ border-color: color-mix(in oklch, var(--color-primary) 35%, transparent);
574
+ background: color-mix(in oklch, var(--color-primary) 12%, transparent);
575
+ }
576
+ .file-markdown-preview {
577
+ min-height: 0;
578
+ }
544
579
  /* Reuse chat token palette outside .md so file preview is colored even if CDN theme lags */
545
580
  .file-preview .hljs-comment,
546
581
  .file-preview .hljs-quote {
@@ -733,6 +768,37 @@ html.pwa-standalone .ma-panel {
733
768
  .session-row.session-thin {
734
769
  opacity: 0.88;
735
770
  }
771
+ .session-row.session-branch {
772
+ margin-left: calc(var(--branch-depth, 1) * 0.7rem);
773
+ border-left-style: dashed;
774
+ }
775
+ .session-branch-line {
776
+ margin-top: 1px;
777
+ font-size: 9px;
778
+ line-height: 1.25;
779
+ font-family: ui-monospace, Menlo, monospace;
780
+ color: color-mix(in oklch, var(--color-primary) 58%, var(--color-base-content));
781
+ overflow: hidden;
782
+ text-overflow: ellipsis;
783
+ white-space: nowrap;
784
+ }
785
+ .session-branch-badge {
786
+ font-size: 9px;
787
+ color: color-mix(in oklch, var(--color-primary) 82%, var(--color-base-content));
788
+ border: 1px solid color-mix(in oklch, var(--color-primary) 28%, transparent);
789
+ border-radius: 3px;
790
+ padding: 0 3px;
791
+ }
792
+ .session-branch-current {
793
+ margin-left: auto;
794
+ max-width: 10rem;
795
+ overflow: hidden;
796
+ text-overflow: ellipsis;
797
+ white-space: nowrap;
798
+ font-family: ui-monospace, Menlo, monospace;
799
+ font-size: 10px;
800
+ color: color-mix(in oklch, var(--color-primary) 72%, var(--color-base-content));
801
+ }
736
802
  .session-thin-badge {
737
803
  font-size: 9px;
738
804
  font-family: ui-monospace, Menlo, monospace;
@@ -1095,7 +1161,7 @@ html.pwa-standalone .ma-panel {
1095
1161
  width: 100%;
1096
1162
  min-height: 32px;
1097
1163
  max-height: 72px;
1098
- padding: 0;
1164
+ padding: 8px 12px 4px;
1099
1165
  border: 0;
1100
1166
  border-radius: 0.75rem 0.75rem 0 0;
1101
1167
  background: transparent;
@@ -1153,6 +1219,51 @@ html.pwa-standalone .ma-panel {
1153
1219
  flex: 0 0 auto;
1154
1220
  }
1155
1221
  .topbar-icon-label { display: inline; }
1222
+ .session-title-actions { margin-left: .1rem; }
1223
+ #session-notes-text { min-width: 0; flex: 1 1 4rem; }
1224
+ .session-bar-icon,
1225
+ #session-notes-bar .session-hide-btn {
1226
+ width: 26px;
1227
+ height: 26px;
1228
+ padding: 0;
1229
+ display: inline-flex;
1230
+ align-items: center;
1231
+ justify-content: center;
1232
+ border: 0;
1233
+ border-radius: .4rem;
1234
+ background: transparent;
1235
+ color: color-mix(in oklch, var(--color-base-content) 62%, transparent);
1236
+ cursor: pointer;
1237
+ }
1238
+ .session-bar-icon:hover,
1239
+ #session-notes-bar .session-hide-btn:hover { background: color-mix(in oklch, var(--color-base-content) 9%, transparent); color: var(--color-base-content); }
1240
+ .session-bar-icon.is-copied { color: var(--color-success); }
1241
+ .session-bar-icon svg,
1242
+ #session-notes-bar .session-hide-btn svg,
1243
+ .chat-chrome-fab svg { width: 15px; height: 15px; }
1244
+ .chat-chrome-fab {
1245
+ position: fixed;
1246
+ top: max(.6rem, env(safe-area-inset-top, 0px));
1247
+ left: max(.6rem, env(safe-area-inset-left, 0px));
1248
+ z-index: 40;
1249
+ width: 34px;
1250
+ height: 34px;
1251
+ display: inline-flex;
1252
+ align-items: center;
1253
+ justify-content: center;
1254
+ border: 1px solid color-mix(in oklch, var(--color-base-content) 16%, transparent);
1255
+ border-radius: 999px;
1256
+ color: var(--color-base-content);
1257
+ background: color-mix(in oklch, var(--color-base-100) 88%, transparent);
1258
+ box-shadow: 0 4px 14px color-mix(in oklch, black 24%, transparent);
1259
+ }
1260
+ .chat-chrome-fab.hidden { display: none; }
1261
+ #app.chat-chrome-collapsed #topbar,
1262
+ #app.chat-chrome-collapsed #project-view > .flex.bg-base-200,
1263
+ #app.chat-chrome-collapsed #project-notes-bar,
1264
+ #app.chat-chrome-collapsed #session-notes-bar,
1265
+ #app.chat-chrome-collapsed #session-lock-banner,
1266
+ #app.chat-chrome-collapsed #ws-banner { display: none !important; }
1156
1267
  .mermaid-diagram {
1157
1268
  overflow-x: auto;
1158
1269
  padding: .75rem .25rem .25rem;
@@ -1304,6 +1415,41 @@ html.pwa-standalone .ma-panel {
1304
1415
  user-select: none;
1305
1416
  }
1306
1417
  .msg-row.user .msg-ts { text-align: right; }
1418
+ .msg-row.queued-user-msg > div:not(.msg-ts) {
1419
+ opacity: .76;
1420
+ border-style: dashed;
1421
+ }
1422
+ .msg-row.cancelled-queued-user-msg > div:not(.msg-ts) {
1423
+ opacity: .48;
1424
+ text-decoration: line-through;
1425
+ }
1426
+ .msg-actions {
1427
+ display: flex;
1428
+ align-items: center;
1429
+ gap: 0.25rem;
1430
+ padding: 0 0.45rem;
1431
+ opacity: 0;
1432
+ transition: opacity .12s ease;
1433
+ }
1434
+ .msg-row.assistant:hover .msg-actions,
1435
+ .msg-row.assistant:focus-within .msg-actions { opacity: 1; }
1436
+ .msg-action-btn {
1437
+ appearance: none;
1438
+ border: 0;
1439
+ background: transparent;
1440
+ color: color-mix(in oklch, var(--color-base-content) 48%, transparent);
1441
+ font-size: 10px;
1442
+ line-height: 1.4;
1443
+ padding: 1px 4px;
1444
+ border-radius: 4px;
1445
+ cursor: pointer;
1446
+ }
1447
+ .msg-action-btn:hover:not(:disabled) {
1448
+ color: var(--color-primary);
1449
+ background: color-mix(in oklch, var(--color-primary) 12%, transparent);
1450
+ }
1451
+ .msg-action-btn:disabled { cursor: wait; opacity: .55; }
1452
+ @media (hover: none) { .msg-actions { opacity: 1; } }
1307
1453
 
1308
1454
  .token-bar {
1309
1455
  display: flex;
@@ -1623,3 +1769,81 @@ html.pwa-standalone .ma-panel {
1623
1769
  padding: 0.05em 0 0.15em 0.5em !important;
1624
1770
  }
1625
1771
  }
1772
+
1773
+ /* ── Settings: full-page overlay using the same DaisyUI tokens as #app ── */
1774
+
1775
+ html.settings-open #app { visibility: hidden; }
1776
+ .set-page {
1777
+ position: fixed;
1778
+ inset: 0;
1779
+ z-index: 50;
1780
+ background: hsl(var(--b1));
1781
+ color: hsl(var(--bc));
1782
+ overflow: auto;
1783
+ min-height: 100dvh;
1784
+ }
1785
+ .set-island {
1786
+ position: sticky;
1787
+ top: 0;
1788
+ z-index: 2;
1789
+ display: flex;
1790
+ align-items: center;
1791
+ gap: 0.5rem;
1792
+ height: 44px;
1793
+ padding: 0 0.75rem;
1794
+ background: hsl(var(--b2));
1795
+ border-bottom: 1px solid hsl(var(--b3));
1796
+ }
1797
+ .set-split {
1798
+ display: grid;
1799
+ grid-template-columns: minmax(140px, 200px) minmax(0, 1fr);
1800
+ gap: 0.75rem;
1801
+ max-width: 56rem;
1802
+ margin: 1rem auto 2.5rem;
1803
+ padding: 0 1rem 2rem;
1804
+ }
1805
+ .set-nav { display: flex; flex-direction: column; gap: 0.25rem; padding-top: 0.25rem; }
1806
+ .set-nav-item.is-on {
1807
+ background: hsl(var(--b3));
1808
+ color: hsl(var(--bc));
1809
+ }
1810
+ .set-main { min-width: 0; }
1811
+ .set-panel { padding: 0.25rem 0 1.5rem; }
1812
+ .set-fields { display: grid; gap: 0.5rem; }
1813
+ .set-list { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
1814
+ .set-row {
1815
+ display: flex;
1816
+ align-items: center;
1817
+ gap: 0.75rem;
1818
+ padding: 0.65rem 0.75rem;
1819
+ border-radius: 0.5rem;
1820
+ background: hsl(var(--b2));
1821
+ border: 1px solid hsl(var(--b3));
1822
+ }
1823
+ .set-row-body { flex: 1; min-width: 0; }
1824
+ .set-add { display: grid; gap: 0.5rem; }
1825
+ .set-token { margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; }
1826
+ .set-token textarea { min-height: 7rem; resize: vertical; }
1827
+ @media (max-width: 767px) {
1828
+ .set-split { grid-template-columns: 1fr; }
1829
+ .set-nav { flex-direction: row; overflow-x: auto; gap: 0.25rem; }
1830
+ .set-nav-item { white-space: nowrap; }
1831
+ }
1832
+
1833
+ /* Overlays must not eat keystrokes when hidden */
1834
+ #sidebar-overlay.hidden,
1835
+ .mermaid-zoom-overlay.hidden,
1836
+ #session-lock-banner.hidden {
1837
+ display: none !important;
1838
+ pointer-events: none !important;
1839
+ }
1840
+ #session-lock-banner {
1841
+ background: color-mix(in oklch, var(--color-warning) 16%, var(--color-base-200));
1842
+ color: color-mix(in oklch, var(--color-warning) 80%, var(--color-base-content));
1843
+ pointer-events: none;
1844
+ }
1845
+ #chat-input:disabled,
1846
+ #chat-input[readonly] {
1847
+ cursor: not-allowed;
1848
+ opacity: .78;
1849
+ }
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-v25';
4
+ const CACHE = 'agent-ui-shell-v27';
5
5
  const PRECACHE = [
6
6
  '/',
7
7
  '/index.html',
8
8
  '/manifest.webmanifest',
9
- '/style.css?v=ui-20',
9
+ '/style.css?v=ui-22',
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-20',
19
+ '/app.js?v=ui-22',
20
20
  '/icons/icon-192.png',
21
21
  '/icons/icon-512.png',
22
22
  '/icons/icon-maskable-512.png',