claude-simple 0.2.6 → 0.2.8

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 (60) hide show
  1. package/README.md +2 -0
  2. package/dist/app.0.2.8.min.js +1080 -0
  3. package/dist/index.html +2 -2
  4. package/dist/style.0.2.8.min.css +1598 -0
  5. package/gateway.js +0 -0
  6. package/package.json +1 -1
  7. package/public/addon-fit.min.js +0 -0
  8. package/public/agent-panel.js +245 -1
  9. package/public/api.js +4 -0
  10. package/public/app.js +0 -0
  11. package/public/base.css +0 -0
  12. package/public/cache.js +0 -0
  13. package/public/chat.js +118 -17
  14. package/public/files.js +0 -0
  15. package/public/git.js +0 -0
  16. package/public/icons/icon-180.png +0 -0
  17. package/public/icons/icon-192.png +0 -0
  18. package/public/icons/icon-512.png +0 -0
  19. package/public/icons/icon-maskable-512.png +0 -0
  20. package/public/index.html +2 -2
  21. package/public/lib.js +0 -0
  22. package/public/manifest.webmanifest +0 -0
  23. package/public/mobile.js +0 -0
  24. package/public/notifications.js +215 -0
  25. package/public/offline.html +0 -0
  26. package/public/pwa.js +0 -0
  27. package/public/router.js +0 -0
  28. package/public/settings.js +0 -0
  29. package/public/shell/boot.js +0 -0
  30. package/public/shell/dashboard.js +72 -23
  31. package/public/shell/hub.js +0 -0
  32. package/public/shell/layout.js +41 -12
  33. package/public/shell/notes.js +0 -0
  34. package/public/shell/session-context.js +0 -0
  35. package/public/shell/session-list.js +27 -28
  36. package/public/shell/session-nav.js +36 -1
  37. package/public/shell/templates.js +45 -7
  38. package/public/shell/util.js +0 -0
  39. package/public/shell.js +56 -7
  40. package/public/state.js +53 -13
  41. package/public/style.css +100 -6
  42. package/public/sw.js +2 -2
  43. package/public/terminal.js +77 -3
  44. package/public/wake-lock.js +0 -0
  45. package/public/xterm.min.css +0 -0
  46. package/public/xterm.min.js +0 -0
  47. package/src/agents/claude.js +1 -1
  48. package/src/agents/codex.js +1 -0
  49. package/src/agents/grok.js +180 -298
  50. package/src/agents/meta-agent.js +332 -1
  51. package/src/agents/models.js +0 -0
  52. package/src/agents/sessions.js +106 -26
  53. package/src/agents/stream-batch.js +0 -0
  54. package/src/app.js +88 -11
  55. package/src/auto-update.js +0 -0
  56. package/src/runtime/approvals.js +0 -0
  57. package/src/runtime/outbox.js +0 -0
  58. package/src/runtime/protocol.js +0 -0
  59. package/dist/app.0.2.6.min.js +0 -953
  60. package/dist/style.0.2.6.min.css +0 -1504
@@ -10,11 +10,12 @@ 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
14
  } from '../chat.js';
14
15
  import { setHash, syncHash } from '../router.js';
15
16
  import { refreshModelSelect } from './session-list.js';
16
17
  import { renderSessionNotesBar } from './notes.js';
17
- import { updateDashboard, getDashboardSessions } from './dashboard.js';
18
+ import { addRecentSession, updateDashboard, getDashboardSessions } from './dashboard.js';
18
19
 
19
20
  /** Show which edge machine Files/Git/Shell paths belong to. */
20
21
  function syncMachinePathHints(machineId) {
@@ -34,6 +35,10 @@ function syncMachinePathHints(machineId) {
34
35
  }
35
36
 
36
37
  export function openProject(project) {
38
+ stashComposerDraft();
39
+ parkQueuedMessages();
40
+ discardComposerAttachments();
41
+ detachProcessingForSessionSwitch();
37
42
  const path = project?.path || '';
38
43
  batch(() => {
39
44
  currentProject.value = project;
@@ -56,11 +61,16 @@ export function openProject(project) {
56
61
  pv.classList.remove('hidden');
57
62
  pv.style.display = 'flex';
58
63
  clearMessages();
64
+ restoreComposerDraft({ sessionId: null });
59
65
  connectWS();
60
66
  switchTab('chat');
61
67
  }
62
68
 
63
69
  export function goHome() {
70
+ stashComposerDraft();
71
+ parkQueuedMessages();
72
+ discardComposerAttachments();
73
+ detachProcessingForSessionSwitch();
64
74
  currentProject.value = null;
65
75
  sessionFilter.value = null;
66
76
  filesRoot.value = '';
@@ -68,6 +78,7 @@ export function goHome() {
68
78
  filesPath.value = '';
69
79
  viewingFile.value = null;
70
80
  ctx.sessionId = null;
81
+ restoreComposerDraft({ sessionId: null });
71
82
  syncMachinePathHints(null);
72
83
 
73
84
  // Leaving session restore mode — calendar is allowed again
@@ -116,10 +127,20 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
116
127
  const resolvedCwd = cwd || fromList?.cwd || fromLast?.cwd || null;
117
128
  const resolvedConfig = configDir || fromList?.configDir || fromLast?.configDir || null;
118
129
 
130
+ // Capture before changing ctx, then restore only after the new identity is set.
131
+ stashComposerDraft();
132
+ parkQueuedMessages();
133
+ discardComposerAttachments();
134
+ detachProcessingForSessionSwitch();
135
+
119
136
  if (resolvedMachine && resolvedMachine !== ctx.machineId) {
120
137
  setSelectedMachine(resolvedMachine);
121
138
  try { ctx.ws?.close(); } catch {}
122
139
  ctx.ws = null;
140
+ // A recent entry can belong to another edge. Route the session request
141
+ // immediately, then refresh the surrounding machine UI in the background.
142
+ import('./hub.js').then(({ syncTopbarMachine }) => syncTopbarMachine()).catch(() => {});
143
+ import('./session-list.js').then(({ loadAllSessions }) => loadAllSessions()).catch(() => {});
123
144
  }
124
145
 
125
146
  ctx.sessionId = sid;
@@ -127,6 +148,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
127
148
  ctx.agent = resolvedAgent;
128
149
  setAgent(resolvedAgent);
129
150
  refreshModelSelect();
151
+ restoreComposerDraft({ sessionId: sid, agent: resolvedAgent, machineId: resolvedMachine });
130
152
 
131
153
  // Default path = session cwd on the target machine (set immediately; no await)
132
154
  let edgeCwd = resolvedCwd;
@@ -161,6 +183,19 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
161
183
  machineId: resolvedMachine,
162
184
  tab: targetTab,
163
185
  });
186
+ // Recent is a deliberate browsing history, not an automatic list of every
187
+ // session on the currently selected machine. Keep the target machine in
188
+ // the entry so a later click can route back to the right edge.
189
+ addRecentSession({
190
+ sessionId: sid,
191
+ cwd: edgeCwd || resolvedCwd,
192
+ configDir: resolvedConfig,
193
+ agent: resolvedAgent,
194
+ machineId: resolvedMachine,
195
+ display: fromList?.display || fromLast?.display || '',
196
+ projectName: fromList?.projectName || fromLast?.projectName || '',
197
+ updatedAt: fromList?.updatedAt || fromLast?.updatedAt || null,
198
+ });
164
199
 
165
200
  const label = AGENT_LABELS[resolvedAgent] || resolvedAgent;
166
201
  const cacheKey = `${resolvedMachine || ''}:${resolvedAgent}:${sid}`;
@@ -39,10 +39,10 @@ export const AppShell = (opts = {}) => {
39
39
  : 'hidden flex-col flex-1 overflow-hidden min-h-0';
40
40
  const projectStyle = restore ? 'display:flex;flex-direction:column;flex:1;min-height:0' : '';
41
41
  return `
42
- <div id="sidebar-overlay" class="hidden fixed inset-0 bg-black/50 z-[9]"></div>
42
+ <div id="sidebar-overlay" class="hidden fixed inset-0 bg-black/50 z-[9]" aria-hidden="true"></div>
43
43
  <div id="app" class="${appHidden} flex-col flex-1 overflow-hidden" style="${appStyle}">
44
44
  <div id="topbar" class="flex items-center gap-2 px-3 bg-base-200 border-b border-base-300 flex-shrink-0" style="height:44px">
45
- <button id="hamburger" class="btn btn-ghost btn-sm px-2 text-lg">☰</button>
45
+ <button id="hamburger" class="btn btn-ghost btn-sm px-2 text-lg" aria-label="打开项目与会话侧栏" aria-controls="sidebar" aria-expanded="false">☰</button>
46
46
  <span id="btn-home" class="font-semibold text-sm hidden sm:inline cursor-pointer hover:text-primary transition-colors select-none">🤖 Agent UI</span>
47
47
  <span class="text-base-300 hidden sm:inline">/</span>
48
48
  <div id="machine-menu-wrap" class="relative hidden flex-shrink-0">
@@ -69,6 +69,13 @@ export const AppShell = (opts = {}) => {
69
69
  <span class="text-[10px] opacity-50">▾</span>
70
70
  </button>
71
71
  </div>
72
+ <div id="notifications-wrap" class="relative flex-shrink-0">
73
+ <button type="button" id="btn-notifications" class="btn btn-ghost btn-xs border border-base-300 gap-1 min-h-8 px-2"
74
+ title="通知" aria-haspopup="dialog" aria-expanded="false">
75
+ <span>🔔</span>
76
+ <span id="notifications-badge" class="hidden inline-flex min-w-5 h-5 px-1 items-center justify-center rounded-full bg-error text-error-content text-[10px] font-bold">0</span>
77
+ </button>
78
+ </div>
72
79
  <span id="topbar-project" class="text-sm text-base-content/50 flex-1 truncate min-w-0">Select a session</span>
73
80
  <span id="conn-dot" class="w-2 h-2 rounded-full bg-base-content/30 flex-shrink-0" title="连接状态"></span>
74
81
  <div class="flex gap-2 topbar-actions">
@@ -93,6 +100,17 @@ export const AppShell = (opts = {}) => {
93
100
  <div id="recent-menu-list" class="flex flex-col gap-0.5"></div>
94
101
  <p id="recent-menu-empty" class="text-xs text-base-content/40 px-1 py-2 hidden">暂无最近会话</p>
95
102
  </div>
103
+ <div id="notifications-backdrop" class="hidden fixed inset-0 z-[55] bg-black/40" aria-hidden="true"></div>
104
+ <div id="notifications-menu" class="hidden fixed z-[60] w-[min(26rem,calc(100vw-1rem))] max-h-[min(30rem,75vh)] overflow-y-auto
105
+ rounded-lg border border-base-300 bg-base-100 shadow-xl p-2"
106
+ role="dialog" aria-label="通知中心">
107
+ <div class="flex items-center justify-between px-1 pb-1.5 mb-1 border-b border-base-300 sticky top-0 bg-base-100 z-[1]">
108
+ <span class="text-[10px] uppercase tracking-wide text-base-content/45 font-semibold">通知</span>
109
+ <button type="button" id="btn-notifications-read-all" class="btn btn-ghost btn-xs px-1 text-[10px]">全部已读</button>
110
+ </div>
111
+ <div id="notifications-menu-list" class="flex flex-col gap-1"></div>
112
+ <p id="notifications-menu-empty" class="text-xs text-base-content/40 px-1 py-2 hidden">暂无通知</p>
113
+ </div>
96
114
  <!-- Hub: only when no remembered machine (or last one offline) -->
97
115
  <div id="machine-picker" class="hidden fixed inset-0 z-[60] bg-base-100 flex items-center justify-center p-4">
98
116
  <div class="w-full max-w-md">
@@ -107,7 +125,7 @@ export const AppShell = (opts = {}) => {
107
125
  </div>
108
126
  </div>
109
127
  <div class="flex flex-1 overflow-hidden relative">
110
- <div id="sidebar" class="bg-base-200 border-r border-base-300 flex flex-col flex-shrink-0 overflow-hidden" style="width:280px;min-width:180px;max-width:520px">
128
+ <aside id="sidebar" class="bg-base-200 border-r border-base-300 flex flex-col flex-shrink-0 overflow-hidden" style="width:280px;min-width:180px;max-width:520px" aria-label="项目与会话" tabindex="-1">
111
129
  <div class="flex items-center justify-between px-3 py-2 border-b border-base-300 flex-shrink-0">
112
130
  <span class="text-xs uppercase tracking-wider text-base-content/50 font-medium">Projects</span>
113
131
  <div class="flex items-center gap-1">
@@ -143,10 +161,16 @@ export const AppShell = (opts = {}) => {
143
161
  <div id="search-status" class="hidden text-[10px] text-base-content/40 px-0.5"></div>
144
162
  </div>
145
163
  <div id="session-list" class="overflow-y-auto flex-1"></div>
146
- </div>
164
+ <div class="sidebar-mobile-actions hidden border-t border-base-300 px-2 py-2 flex-shrink-0">
165
+ <button type="button" class="btn btn-primary btn-sm flex-1" data-sidebar-action="new">+ 新会话</button>
166
+ <button type="button" class="btn btn-ghost btn-sm px-2" data-sidebar-action="theme" aria-label="切换深色或浅色主题">🌙</button>
167
+ <button type="button" class="btn btn-ghost btn-sm px-2" data-sidebar-action="settings" aria-label="设置">⚙️</button>
168
+ <button type="button" class="btn btn-ghost btn-sm px-2 text-error" data-sidebar-action="logout" aria-label="退出登录">⇥</button>
169
+ </div>
170
+ </aside>
147
171
  <div class="flex flex-col flex-shrink-0 relative" style="width:12px">
148
172
  <div id="sidebar-resize" class="absolute inset-0 bg-transparent hover:bg-primary/30 transition-colors cursor-col-resize"></div>
149
- <button id="sidebar-toggle" title="Toggle sidebar"
173
+ <button id="sidebar-toggle" title="收起侧栏" aria-label="收起侧栏" aria-controls="sidebar" aria-expanded="true"
150
174
  class="absolute z-10 flex items-center justify-center w-5 h-8 bg-base-200 border border-base-300 rounded-r-md text-[10px] text-base-content/40 hover:text-base-content cursor-pointer"
151
175
  style="left:0;top:50%;transform:translateY(-50%)">◀</button>
152
176
  </div>
@@ -278,7 +302,7 @@ export const AppShell = (opts = {}) => {
278
302
  <option value="acceptEdits">acceptEdits</option>
279
303
  <option value="auto">auto</option>
280
304
  <option value="plan">plan</option>
281
- <option value="bypassPermissions">⚠ bypass all</option>
305
+ <option value="bypassPermissions" selected>⚠ bypass all</option>
282
306
  </select>
283
307
  <select id="sel-density" class="select select-xs select-bordered text-xs" title="Chat density — how tools are shown">
284
308
  <option value="clean">density: clean</option>
@@ -332,9 +356,23 @@ export const AppShell = (opts = {}) => {
332
356
  <div id="tab-shell" class="hidden flex-col flex-1 overflow-hidden bg-[#0d1117]">
333
357
  <div class="flex items-center gap-2 px-3 py-1.5 border-b border-base-300 bg-base-200 flex-shrink-0">
334
358
  <span class="text-xs text-base-content/40 font-mono flex-1" id="term-cwd"></span>
359
+ <button id="term-keyboard" class="btn btn-ghost btn-xs text-xs text-base-content/60" type="button">Keyboard</button>
335
360
  <button id="term-reconnect" class="btn btn-ghost btn-xs text-xs text-base-content/40">Reconnect</button>
336
361
  </div>
362
+ <div id="term-mobile-keys" class="hidden items-center gap-1 px-2 py-1 border-b border-slate-700 bg-[#161b22] flex-shrink-0" aria-label="Terminal shortcuts">
363
+ <button class="term-key" type="button" data-term-key="ctrl-c">Ctrl+C</button>
364
+ <button class="term-key" type="button" data-term-key="escape">Esc</button>
365
+ <button class="term-key" type="button" data-term-key="tab">Tab</button>
366
+ <button class="term-key" type="button" data-term-key="up">↑</button>
367
+ <button class="term-key" type="button" data-term-key="down">↓</button>
368
+ <button class="term-key" type="button" data-term-key="left">←</button>
369
+ <button class="term-key" type="button" data-term-key="right">→</button>
370
+ </div>
337
371
  <div id="terminal-container" class="flex-1 p-2 overflow-hidden"></div>
372
+ <form id="term-mobile-composer" class="hidden items-center gap-2 px-2 py-2 border-t border-slate-700 bg-[#161b22] flex-shrink-0">
373
+ <input id="term-mobile-input" class="term-mobile-input" type="text" placeholder="输入命令…" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" inputmode="text" enterkeyhint="send" aria-label="Terminal command">
374
+ <button class="term-mobile-send" type="submit">发送</button>
375
+ </form>
338
376
  </div>
339
377
  <div id="tab-git" class="hidden flex-col flex-1 overflow-hidden">
340
378
  <div class="flex items-center gap-2 px-3 py-1.5 border-b border-base-300 flex-shrink-0 bg-base-200">
@@ -348,7 +386,7 @@ export const AppShell = (opts = {}) => {
348
386
  </div>
349
387
  <div id="tab-memory" class="hidden flex-col flex-1 overflow-hidden">
350
388
  <div class="flex items-center gap-2 px-3 py-1.5 border-b border-base-300 flex-shrink-0 bg-base-200">
351
- <span class="text-xs text-base-content/40 flex-shrink-0">🧠 Project memory</span>
389
+ <span class="text-xs text-base-content/40 flex-shrink-0">🧠 Agent memory</span>
352
390
  <span class="flex-1"></span>
353
391
  <button id="memory-refresh" class="btn btn-ghost btn-xs text-xs">Refresh</button>
354
392
  </div>
File without changes
package/public/shell.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  sessionsData, workspacesData, sessionFilter, sessionSearch,
7
7
  expandedFolders, collapsedFolders, filesPath, viewingFile, filteredSessions, projectGroups,
8
8
  currentProject, currentTab, filesRoot, gitRoot,
9
- currentModel, currentEffort, currentPermission, currentAgent, setAgent,
9
+ currentModel, currentEffort, currentPermission, currentAgent, setAgent, setCurrentModel,
10
10
  sidebarView, agentFilter, timeRange, activityHits, activityLoading,
11
11
  chatDensity, setChatDensity,
12
12
  favoritesOnly, showHiddenOnly, hubMode, hubMachineReady, selectedMachineId, setSelectedMachine,
@@ -23,16 +23,20 @@ import {
23
23
  import { syncHash } from './router.js';
24
24
  import { initSettings, openSettings } from './settings.js';
25
25
  import { openMetaAgent, toggleMetaAgent } from './agent-panel.js';
26
+ import {
27
+ initNotifications, refreshNotifications, toggleNotificationsMenu, closeNotificationsMenu,
28
+ } from './notifications.js';
26
29
 
27
30
  import { AuthScreen, AppShell } from './shell/templates.js';
28
31
  import {
29
32
  updateDashboard, shiftCalMonth, setSelectedCalDate, getDashboardSessions,
30
33
  openRecentMenu, toggleRecentMenu, closeRecentMenu, positionRecentMenu,
34
+ dismissRecentSession,
31
35
  } from './shell/dashboard.js';
32
36
  import {
33
37
  loadAllSessions, loadWorkspaces, renderSessionList, syncSidebarChrome,
34
38
  refreshModelSelect, refreshEffortSelect, promptCustomModel,
35
- scheduleActivitySearch, runActivitySearch, loadMemoryTab,
39
+ scheduleActivitySearch, runActivitySearch, loadMemoryTab, openMemoryFile,
36
40
  } from './shell/session-list.js';
37
41
  import { openProject, goHome, resumeSession, switchTab } from './shell/session-nav.js';
38
42
  import { showApp } from './shell/boot.js';
@@ -305,6 +309,7 @@ export function initShell(opts = {}) {
305
309
 
306
310
  initSidebarResize();
307
311
  initSidebarToggle();
312
+ initNotifications({ openSession: resumeSession });
308
313
 
309
314
  let sessionsChangedTimer = null;
310
315
  document.addEventListener('sessions-changed', () => {
@@ -336,6 +341,13 @@ export function initShell(opts = {}) {
336
341
  delegate.on('keydown', '#auth-username, #auth-password', e => { if (e.key === 'Enter') submitAuth(); });
337
342
  delegate.on('click', '#hamburger', () => $('sidebar').classList.contains('open') ? closeSidebar() : openSidebar());
338
343
  delegate.on('click', '#sidebar-overlay', closeSidebar);
344
+ document.addEventListener('keydown', e => {
345
+ if (e.key !== 'Escape') return;
346
+ if (window.innerWidth <= 640 && $('sidebar')?.classList.contains('open')) {
347
+ e.preventDefault();
348
+ closeSidebar();
349
+ }
350
+ });
339
351
 
340
352
  delegate.on('input', '#session-search', (_, el) => {
341
353
  sessionSearch.value = el.value;
@@ -383,6 +395,19 @@ export function initShell(opts = {}) {
383
395
  delegate.on('click', '#btn-meta-agent', () => toggleMetaAgent());
384
396
  delegate.on('click', '#btn-meta-agent-welcome', () => openMetaAgent('chat'));
385
397
  delegate.on('click', '#btn-theme', toggleTheme);
398
+ delegate.on('click', '[data-sidebar-action]', (_, el) => {
399
+ const action = el.dataset.sidebarAction;
400
+ if (action === 'new') openNewSessionModal();
401
+ if (action === 'theme') toggleTheme();
402
+ if (action === 'settings') openSettings();
403
+ if (action === 'logout') {
404
+ ctx.token = null;
405
+ localStorage.removeItem('token');
406
+ setSelectedMachine(null);
407
+ hubMachineReady.value = false;
408
+ location.reload();
409
+ }
410
+ });
386
411
 
387
412
  delegate.on('click', '#btn-cal-prev', () => {
388
413
  shiftCalMonth(-1);
@@ -462,6 +487,7 @@ export function initShell(opts = {}) {
462
487
  });
463
488
 
464
489
  delegate.on('click', '#memory-refresh', loadMemoryTab);
490
+ delegate.on('click', '[data-memory-file]', (el) => openMemoryFile(el.dataset.memoryFile, el.dataset.memoryAgent));
465
491
  delegate.on('click', '[data-tab]', (e, el) => switchTab(el.dataset.tab));
466
492
 
467
493
  const applyRoot = async (inputId, sig) => {
@@ -497,8 +523,7 @@ export function initShell(opts = {}) {
497
523
  if (prev !== next) {
498
524
  ctx.sessionId = null;
499
525
  setAgent(next);
500
- currentModel.value = getDefaultModel(next) || currentModel.peek();
501
- localStorage.setItem('model', currentModel.peek());
526
+ setCurrentModel(getDefaultModel(next) || currentModel.peek());
502
527
  $('sel-permission').value = currentPermission.peek();
503
528
  refreshModelSelect();
504
529
  appendSystemMsg(`Agent → ${AGENT_LABELS[next] || next} · model ${currentModel.peek()} · new session`);
@@ -532,8 +557,7 @@ export function initShell(opts = {}) {
532
557
  }
533
558
  });
534
559
  delegate.on('change', '#sel-model', (_, el) => {
535
- currentModel.value = el.value;
536
- localStorage.setItem('model', el.value);
560
+ setCurrentModel(el.value);
537
561
  refreshEffortSelect();
538
562
  });
539
563
  delegate.on('click', '#btn-model-add', () => promptCustomModel({ editCurrent: false }));
@@ -568,6 +592,7 @@ export function initShell(opts = {}) {
568
592
  return;
569
593
  }
570
594
  await enterMachine(id, { forceReload: true });
595
+ refreshNotifications();
571
596
  });
572
597
  const refreshMachinesUi = async () => {
573
598
  const st = $('machine-picker-status');
@@ -589,6 +614,7 @@ export function initShell(opts = {}) {
589
614
  delegate.on('click', '#btn-machine-menu', async e => {
590
615
  e.stopPropagation();
591
616
  closeRecentMenu();
617
+ closeNotificationsMenu();
592
618
  const menu = $('machine-menu');
593
619
  if (!menu) return;
594
620
  if (menu.classList.contains('hidden')) {
@@ -603,7 +629,8 @@ export function initShell(opts = {}) {
603
629
  e.preventDefault();
604
630
  e.stopPropagation();
605
631
  closeMachineMenu();
606
- toggleRecentMenu(getDashboardSessions());
632
+ closeNotificationsMenu();
633
+ toggleRecentMenu();
607
634
  const open = !$('recent-menu')?.classList.contains('hidden');
608
635
  const btn = $('btn-recent-sessions');
609
636
  if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
@@ -627,6 +654,13 @@ export function initShell(opts = {}) {
627
654
  btn?.setAttribute('aria-expanded', 'false');
628
655
  }
629
656
  });
657
+ delegate.on('click', '#btn-notifications', e => {
658
+ e.preventDefault();
659
+ e.stopPropagation();
660
+ closeMachineMenu();
661
+ closeRecentMenu();
662
+ toggleNotificationsMenu();
663
+ });
630
664
  // Keep panel glued under topbar on rotate / URL bar show-hide
631
665
  window.addEventListener('resize', () => {
632
666
  if ($('recent-menu') && !$('recent-menu').classList.contains('hidden')) {
@@ -638,6 +672,15 @@ export function initShell(opts = {}) {
638
672
  closeRecentMenu();
639
673
  $('btn-recent-sessions')?.setAttribute('aria-expanded', 'false');
640
674
  });
675
+ delegate.on('click', '[data-dismiss-recent-session]', (e, el) => {
676
+ e.preventDefault();
677
+ e.stopPropagation();
678
+ dismissRecentSession({
679
+ sessionId: el.dataset.dismissRecentSession,
680
+ agent: el.dataset.dismissRecentAgent || 'claude',
681
+ machineId: el.dataset.dismissRecentMachine || null,
682
+ });
683
+ });
641
684
 
642
685
  delegate.on('click', '#btn-edit-project-notes, #project-goal-text, #project-notes-text', () => {
643
686
  renderProjectNotesEditMode();
@@ -730,6 +773,12 @@ export function initShell(opts = {}) {
730
773
  });
731
774
 
732
775
  refreshModelSelect();
776
+ effect(() => {
777
+ void hubMode.value;
778
+ void selectedMachineId.value;
779
+ if (!ctx.token) return;
780
+ refreshNotifications();
781
+ });
733
782
  $('sel-permission').value = currentPermission.peek();
734
783
  applyChatDensity();
735
784
  const dens = $('sel-density');
package/public/state.js CHANGED
@@ -15,14 +15,52 @@ export const currentTab = signal('chat');
15
15
  export const isProcessing = signal(false);
16
16
  /** Chat socket: idle | connecting | open | reconnecting | offline */
17
17
  export const wsStatus = signal('idle');
18
- export const currentAgent = signal(localStorage.getItem('agent') || 'claude'); // 'claude'|'codex'|'grok'
19
- export const currentModel = signal(localStorage.getItem('model') || 'claude-sonnet-4-5');
18
+ const MODEL_BY_SESSION_KEY = 'modelBySession';
19
+ const initialAgent = localStorage.getItem('agent') || 'claude';
20
+
21
+ function readModelBySession() {
22
+ try {
23
+ const value = JSON.parse(localStorage.getItem(MODEL_BY_SESSION_KEY) || '{}');
24
+ return value && typeof value === 'object' ? value : {};
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+
30
+ function modelSessionKey(agent, sessionId = ctx.sessionId, machineId = ctx.machineId) {
31
+ if (!sessionId) return '';
32
+ const a = (agent || 'claude').toLowerCase();
33
+ return `${machineId || ''}:${a}:${sessionId}`;
34
+ }
35
+
36
+ /** Restore the model explicitly selected for the active session. */
37
+ export function getSavedModel(agent, sessionId = ctx.sessionId, machineId = ctx.machineId) {
38
+ const key = modelSessionKey(agent, sessionId, machineId);
39
+ if (!key) return null;
40
+ const saved = readModelBySession()[key];
41
+ if (typeof saved === 'string' && saved.trim()) return saved;
42
+ return null;
43
+ }
44
+
45
+ /** Update the picker and persist its value only when a session exists. */
46
+ export function setCurrentModel(model, agent = currentAgent.peek(), sessionId = ctx.sessionId, machineId = ctx.machineId) {
47
+ const value = String(model || '').trim();
48
+ if (!value) return;
49
+ const key = modelSessionKey(agent, sessionId, machineId);
50
+ if (key) {
51
+ const saved = readModelBySession();
52
+ saved[key] = value;
53
+ localStorage.setItem(MODEL_BY_SESSION_KEY, JSON.stringify(saved));
54
+ }
55
+ currentModel.value = value;
56
+ }
57
+
58
+ export const currentAgent = signal(initialAgent); // 'claude'|'codex'|'grok'
59
+ export const currentModel = signal('claude-sonnet-4-5');
20
60
  export const currentEffort = signal('');
21
- // Codex is intentionally trusted by default; users can still choose another
22
- // mode from the expanded composer options before starting a turn.
23
- export const currentPermission = signal(
24
- currentAgent.peek() === 'codex' ? 'bypassPermissions' : 'default'
25
- ); // 'default'|'acceptEdits'|'bypassPermissions'|'plan'|'auto'
61
+ // All local agents are trusted by default; users can still choose another mode
62
+ // from the expanded composer options before starting a turn.
63
+ export const currentPermission = signal('bypassPermissions'); // 'default'|'acceptEdits'|'bypassPermissions'|'plan'|'auto'
26
64
  export const filesRoot = signal(''); // custom root override for Files tab
27
65
  export const gitRoot = signal(''); // custom root override for Git tab
28
66
  export const agentsMeta = signal(null); // from /api/agents (dynamic model lists)
@@ -251,14 +289,16 @@ export function setAgent(agent) {
251
289
  const a = (agent || 'claude').toLowerCase();
252
290
  currentAgent.value = a;
253
291
  localStorage.setItem('agent', a);
254
- currentPermission.value = a === 'codex' ? 'bypassPermissions' : 'default';
292
+ currentPermission.value = 'bypassPermissions';
255
293
  const permissionSelect = typeof document === 'undefined' ? null : document.getElementById('sel-permission');
256
294
  if (permissionSelect) permissionSelect.value = currentPermission.peek();
257
- const models = getModelsForAgent(a);
258
- const def = getDefaultModel(a);
259
- if (!models.some(m => m.value === currentModel.peek())) {
260
- currentModel.value = def;
261
- localStorage.setItem('model', def);
295
+ // Model selection belongs to a concrete session, not to the whole agent.
296
+ // New sessions start from the agent default until they receive a session id.
297
+ const saved = getSavedModel(a);
298
+ if (saved) {
299
+ currentModel.value = saved;
300
+ } else {
301
+ setCurrentModel(getDefaultModel(a), a);
262
302
  }
263
303
  }
264
304
 
package/public/style.css CHANGED
@@ -6,21 +6,25 @@
6
6
  top: calc(44px + env(safe-area-inset-top, 0px));
7
7
  left: 0;
8
8
  bottom: 0;
9
- transform: translateX(-100%); z-index: 10;
10
- transition: transform .2s ease;
9
+ width: min(86vw, 22rem) !important;
10
+ min-width: min(86vw, 22rem) !important;
11
+ max-width: min(86vw, 22rem) !important;
12
+ transform: translateX(-103%); z-index: 10;
13
+ transition: transform .22s cubic-bezier(.2,.8,.2,1), box-shadow .22s ease;
11
14
  padding-bottom: env(safe-area-inset-bottom, 0px);
15
+ overscroll-behavior: contain;
12
16
  }
13
17
  #sidebar.open { transform: translateX(0); box-shadow: 4px 0 20px rgba(0,0,0,.4); }
14
18
 
15
19
  /* Compact chat composer on phones — less height, less dead space under bar */
16
20
  #composer-row { padding: 0 !important; }
17
- .composer-surface { min-height: 0; border-radius: 1.35rem; }
21
+ .composer-surface { min-height: 0; border-radius: 0.8rem; }
18
22
  #chat-input.chat-input,
19
23
  #chat-input {
20
24
  min-height: 32px !important;
21
25
  max-height: 72px !important;
22
26
  padding: 0 !important;
23
- border-radius: 1.15rem 1.15rem 0 0;
27
+ border-radius: 0.65rem 0.65rem 0 0;
24
28
  /* 16px prevents iOS Safari auto-zoom on focus */
25
29
  font-size: 16px !important;
26
30
  line-height: 1.4 !important;
@@ -76,6 +80,25 @@
76
80
  #sidebar-toggle {
77
81
  display: none !important;
78
82
  }
83
+ #sidebar .input,
84
+ #sidebar .select {
85
+ min-height: 2.25rem;
86
+ font-size: 16px !important; /* prevents Safari focus zoom */
87
+ }
88
+ #sidebar #time-range { font-size: 12px !important; }
89
+ #sidebar #session-list {
90
+ padding-bottom: max(0.75rem, env(safe-area-inset-bottom, 0px));
91
+ -webkit-overflow-scrolling: touch;
92
+ }
93
+ #sidebar [data-view],
94
+ #sidebar .agent-pill {
95
+ min-height: 28px;
96
+ }
97
+ #sidebar .sidebar-mobile-actions {
98
+ display: flex;
99
+ gap: 0.35rem;
100
+ padding-bottom: max(0.5rem, env(safe-area-inset-bottom, 0px));
101
+ }
79
102
  .md {
80
103
  font-size: 0.9375rem;
81
104
  line-height: 1.6;
@@ -101,6 +124,16 @@
101
124
  }
102
125
  }
103
126
 
127
+ @media (min-width: 641px) {
128
+ #sidebar .sidebar-mobile-actions { display: none !important; }
129
+ #sidebar {
130
+ transition: width .18s ease, min-width .18s ease;
131
+ }
132
+ #sidebar.desktop-collapsed {
133
+ border-right-color: transparent;
134
+ }
135
+ }
136
+
104
137
  /* File row actions: hover-reveal on desktop only */
105
138
  @media (min-width: 641px) {
106
139
  .file-row-action {
@@ -962,7 +995,7 @@ html.pwa-standalone .ma-panel {
962
995
  flex-direction: column;
963
996
  overflow: hidden;
964
997
  border: 1px solid color-mix(in oklch, var(--color-base-content) 16%, transparent);
965
- border-radius: 1.35rem;
998
+ border-radius: 0.875rem;
966
999
  background: color-mix(in oklch, var(--color-base-100) 78%, var(--color-base-200));
967
1000
  box-shadow: 0 8px 24px color-mix(in oklch, black 12%, transparent), inset 0 1px 0 color-mix(in oklch, white 6%, transparent);
968
1001
  transition: border-color .15s ease, box-shadow .15s ease, background .15s ease;
@@ -1064,7 +1097,7 @@ html.pwa-standalone .ma-panel {
1064
1097
  max-height: 72px;
1065
1098
  padding: 0;
1066
1099
  border: 0;
1067
- border-radius: 1.25rem 1.25rem 0 0;
1100
+ border-radius: 0.75rem 0.75rem 0 0;
1068
1101
  background: transparent;
1069
1102
  color: var(--color-base-content);
1070
1103
  outline: none !important;
@@ -1297,6 +1330,52 @@ html.pwa-standalone .ma-panel {
1297
1330
  /* Shell tab terminal — keep dark intentionally (terminal aesthetic) */
1298
1331
  #tab-shell { background: #0d1117; }
1299
1332
  #term-output { color: #c9d1d9; }
1333
+ #term-keyboard { display: none; }
1334
+
1335
+ /* xterm parks its input off-screen, which is unreliable for mobile IMEs.
1336
+ * Phones use the visible native command composer below the terminal instead. */
1337
+ @media (max-width: 640px), (hover: none) and (pointer: coarse) {
1338
+ #term-keyboard { display: inline-flex; }
1339
+ #term-mobile-keys { display: flex !important; }
1340
+ #term-mobile-composer { display: flex !important; }
1341
+ #terminal-container { touch-action: manipulation; }
1342
+ .term-key {
1343
+ min-width: 38px;
1344
+ min-height: 32px;
1345
+ padding: 3px 7px;
1346
+ border: 1px solid #30363d;
1347
+ border-radius: 6px;
1348
+ color: #c9d1d9;
1349
+ background: #21262d;
1350
+ font: 600 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
1351
+ -webkit-tap-highlight-color: transparent;
1352
+ }
1353
+ .term-key:active { background: #30363d; }
1354
+ .term-mobile-input {
1355
+ flex: 1;
1356
+ min-width: 0;
1357
+ height: 38px;
1358
+ padding: 0 10px;
1359
+ border: 1px solid #30363d;
1360
+ border-radius: 7px;
1361
+ color: #f0f6fc;
1362
+ background: #0d1117;
1363
+ font: 16px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
1364
+ outline: none;
1365
+ }
1366
+ .term-mobile-input:focus { border-color: #58a6ff; }
1367
+ .term-mobile-send {
1368
+ height: 38px;
1369
+ padding: 0 12px;
1370
+ border: 0;
1371
+ border-radius: 7px;
1372
+ color: #fff;
1373
+ background: #1f6feb;
1374
+ font-size: 14px;
1375
+ font-weight: 600;
1376
+ }
1377
+ .term-mobile-send:active { background: #388bfd; }
1378
+ }
1300
1379
 
1301
1380
  /* ── Meta Agent panel (DaisyUI 5: use --color-* full values, solid surfaces) ── */
1302
1381
  .meta-agent-panel {
@@ -1343,6 +1422,21 @@ html.pwa-standalone .ma-panel {
1343
1422
  background: color-mix(in oklch, var(--color-primary) 12%, var(--color-base-200));
1344
1423
  }
1345
1424
  .ma-hist-open { cursor: pointer; }
1425
+ .ma-note-item {
1426
+ width: 100%;
1427
+ border: 1px solid var(--color-base-300);
1428
+ border-radius: 10px;
1429
+ padding: 8px 10px;
1430
+ background: var(--color-base-200);
1431
+ transition: border-color .12s, background .12s;
1432
+ }
1433
+ .ma-note-item:hover {
1434
+ border-color: color-mix(in oklch, var(--color-primary) 40%, var(--color-base-300));
1435
+ }
1436
+ .ma-note-item.active {
1437
+ border-color: color-mix(in oklch, var(--color-primary) 55%, var(--color-base-300));
1438
+ background: color-mix(in oklch, var(--color-primary) 12%, var(--color-base-200));
1439
+ }
1346
1440
  .line-clamp-2 {
1347
1441
  display: -webkit-box;
1348
1442
  -webkit-line-clamp: 2;
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-v9';
4
+ const CACHE = 'agent-ui-shell-v15';
5
5
  const PRECACHE = [
6
6
  '/',
7
7
  '/index.html',
8
8
  '/manifest.webmanifest',
9
- '/style.css?v=ui-10',
9
+ '/style.css?v=ui-11',
10
10
  '/style.css',
11
11
  '/app.js',
12
12
  '/icons/icon-192.png',