claude-simple 0.2.5 → 0.2.7

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.
@@ -8,7 +8,7 @@ import {
8
8
  } from '../state.js';
9
9
  // filesRoot/gitRoot used when starting a new session (default paths)
10
10
  import { api } from '../api.js';
11
- import { connectWS, clearMessages, appendSystemMsg, applyChatDensity } from '../chat.js';
11
+ import { connectWS, clearMessages, appendSystemMsg, applyChatDensity, stashComposerDraft, restoreComposerDraft, discardComposerAttachments, parkQueuedMessages, detachProcessingForSessionSwitch } from '../chat.js';
12
12
  import { refreshModelSelect } from './session-list.js';
13
13
  import { switchTab } from './session-nav.js';
14
14
  import { showMachinePicker } from './hub.js';
@@ -16,27 +16,37 @@ import { syncFocusScope } from '../mobile.js';
16
16
 
17
17
  // ── Sidebar ───────────────────────────────────────────────────────────────────
18
18
  export function openSidebar() {
19
- $('sidebar').classList.add('open');
19
+ const sidebar = $('sidebar');
20
+ if (!sidebar) return;
21
+ sidebar.classList.add('open');
20
22
  $('sidebar-overlay').classList.remove('hidden');
23
+ $('hamburger')?.setAttribute('aria-expanded', 'true');
21
24
  syncFocusScope();
25
+ // Put the next action where it belongs. Delaying lets the drawer transition
26
+ // start first, which is more reliable in mobile Safari.
27
+ setTimeout(() => $('session-search')?.focus({ preventScroll: true }), 180);
22
28
  }
23
29
  export function closeSidebar() {
24
- $('sidebar').classList.remove('open');
30
+ const sidebar = $('sidebar');
31
+ if (!sidebar) return;
32
+ sidebar.classList.remove('open');
25
33
  $('sidebar-overlay').classList.add('hidden');
34
+ $('hamburger')?.setAttribute('aria-expanded', 'false');
26
35
  syncFocusScope();
36
+ $('hamburger')?.focus({ preventScroll: true });
27
37
  }
28
38
  export function closeSidebarOnMobile() { if (window.innerWidth <= 640) closeSidebar(); }
29
39
 
30
40
  export function initSidebarToggle() {
31
41
  const btn = $('sidebar-toggle');
32
42
  const sidebar = $('sidebar');
33
- let collapsed = false;
34
- let savedWidth = sidebar.style.width || '260px';
43
+ if (!btn || !sidebar) return;
44
+ let collapsed = localStorage.getItem('sidebarCollapsed') === '1';
45
+ let savedWidth = localStorage.getItem('sidebarWidth') || sidebar.style.width || '280px';
35
46
 
36
- btn.addEventListener('click', () => {
37
- collapsed = !collapsed;
47
+ const applyCollapsed = () => {
48
+ sidebar.classList.toggle('desktop-collapsed', collapsed);
38
49
  if (collapsed) {
39
- savedWidth = sidebar.style.width || '260px';
40
50
  sidebar.style.width = '0';
41
51
  sidebar.style.minWidth = '0';
42
52
  sidebar.style.overflow = 'hidden';
@@ -46,6 +56,17 @@ export function initSidebarToggle() {
46
56
  sidebar.style.overflow = '';
47
57
  }
48
58
  btn.textContent = collapsed ? '▶' : '◀';
59
+ btn.title = collapsed ? '展开侧栏' : '收起侧栏';
60
+ btn.setAttribute('aria-label', btn.title);
61
+ btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
62
+ };
63
+ applyCollapsed();
64
+
65
+ btn.addEventListener('click', () => {
66
+ collapsed = !collapsed;
67
+ if (collapsed) savedWidth = sidebar.style.width || savedWidth;
68
+ localStorage.setItem('sidebarCollapsed', collapsed ? '1' : '0');
69
+ applyCollapsed();
49
70
  });
50
71
  }
51
72
 
@@ -71,6 +92,8 @@ export function initSidebarResize() {
71
92
  dragging = false;
72
93
  document.body.style.userSelect = '';
73
94
  document.body.style.cursor = '';
95
+ const width = Math.round(sidebar.getBoundingClientRect().width);
96
+ if (width >= 160) localStorage.setItem('sidebarWidth', `${width}px`);
74
97
  });
75
98
  }
76
99
 
@@ -79,7 +102,9 @@ export function applyTheme(theme) {
79
102
  document.documentElement.setAttribute('data-theme', theme);
80
103
  localStorage.setItem('theme', theme);
81
104
  const btn = $('btn-theme');
82
- if (btn) btn.textContent = theme === 'dark' || theme === 'night' || theme === 'black' ? '🌙' : '☀️';
105
+ const icon = theme === 'dark' || theme === 'night' || theme === 'black' ? '🌙' : '☀️';
106
+ if (btn) btn.textContent = icon;
107
+ document.querySelectorAll('[data-sidebar-action="theme"]').forEach(el => { el.textContent = icon; });
83
108
  // Swap highlight.js stylesheet for light/dark
84
109
  let link = document.getElementById('hljs-theme');
85
110
  if (!link) {
@@ -171,6 +196,10 @@ export async function startNewSession() {
171
196
  $('modal-new-session').close();
172
197
 
173
198
  const name = result.path.split('/').filter(Boolean).pop() || result.path;
199
+ stashComposerDraft();
200
+ parkQueuedMessages();
201
+ discardComposerAttachments();
202
+ detachProcessingForSessionSwitch();
174
203
  ctx.sessionId = null;
175
204
  ctx.configDir = configDir || null;
176
205
  ctx.agent = agent;
@@ -194,6 +223,7 @@ export async function startNewSession() {
194
223
  pv.classList.remove('hidden');
195
224
  pv.style.display = 'flex';
196
225
  clearMessages();
226
+ restoreComposerDraft({ sessionId: null });
197
227
  connectWS();
198
228
  switchTab('chat');
199
229
  appendSystemMsg(`New ${AGENT_LABELS[agent] || agent} session · ${result.path}`);
@@ -430,7 +430,7 @@ export function mdToHtml(text) {
430
430
  }
431
431
  }
432
432
 
433
- // Load the persistent memory for the currently open session into the Memory tab.
433
+ // Memory is shown as an agent-specific file list. Content is loaded on click.
434
434
  export async function loadMemoryTab() {
435
435
  const body = $('memory-area');
436
436
  if (!body) return;
@@ -440,22 +440,30 @@ export async function loadMemoryTab() {
440
440
  }
441
441
  body.innerHTML = '<div class="p-4 text-xs text-base-content/40">Loading…</div>';
442
442
  try {
443
- const q = new URLSearchParams({ agent: ctx.agent || currentAgent.peek() || 'claude' });
443
+ const agent = ctx.agent || currentAgent.peek() || 'claude';
444
+ const q = new URLSearchParams({ agent });
444
445
  const mem = await api('GET', `/api/sessions/${ctx.sessionId}/memory?${q}`);
445
- const parts = [];
446
- if (mem.index) {
447
- parts.push(`<div class="mb-4"><div class="text-[11px] uppercase tracking-wide text-base-content/40 mb-1">Index (MEMORY.md)</div>
448
- <div class="md">${mdToHtml(mem.index)}</div></div>`);
446
+ const files = mem.files || [];
447
+ if (!files.length) {
448
+ body.innerHTML = '<div class="p-4 text-base-content/40 text-xs">No memory files available for this agent/session.</div>';
449
+ return;
449
450
  }
450
- for (const f of (mem.files || [])) {
451
- parts.push(`<div class="mb-3 border border-base-300 rounded-lg overflow-hidden">
452
- <div class="px-3 py-1.5 bg-base-300/50 text-xs font-mono text-base-content/70">${esc(f.name)}</div>
453
- <div class="px-3 py-2 md">${mdToHtml(f.content)}</div>
454
- </div>`);
455
- }
456
- if (!parts.length) parts.push('<div class="text-base-content/40 text-xs">No memory saved for this project yet.</div>');
457
- body.innerHTML = `<div class="p-4">${parts.join('')}</div>`;
451
+ body.innerHTML = `<div class="p-3 text-[11px] text-base-content/45">${esc(agent)} · ${files.length} memory file${files.length === 1 ? '' : 's'} · click to open</div>
452
+ <div class="px-3 pb-4 flex flex-col gap-1">${files.map(f => `<button type="button" class="memory-file text-left px-3 py-2 rounded-lg border border-base-300 hover:border-primary hover:bg-primary/5" data-memory-file="${esc(f.id)}" data-memory-agent="${esc(agent)}"><span class="font-mono text-xs">${esc(f.name)}</span><span class="ml-2 text-[10px] text-base-content/40">${esc(f.scope || '')}</span></button>`).join('')}<div id="memory-file-content"></div></div>`;
458
453
  } catch (e) {
459
454
  body.innerHTML = `<div class="p-4 text-error text-xs">${esc(e?.message ?? String(e))}</div>`;
460
455
  }
461
456
  }
457
+
458
+ export async function openMemoryFile(fileId, agent) {
459
+ const out = $('memory-file-content');
460
+ if (!out || !ctx.sessionId || !fileId) return;
461
+ out.innerHTML = '<div class="p-3 text-xs text-base-content/40">Loading file…</div>';
462
+ try {
463
+ const q = new URLSearchParams({ agent: agent || ctx.agent || currentAgent.peek() || 'claude', file: fileId });
464
+ const row = await api('GET', `/api/sessions/${ctx.sessionId}/memory/file?${q}`);
465
+ out.innerHTML = `<div class="mt-2 border border-base-300 rounded-lg overflow-hidden"><div class="px-3 py-1.5 bg-base-300/50 text-xs font-mono">${esc(row.name)}</div><div class="px-3 py-2 md">${mdToHtml(row.content)}</div></div>`;
466
+ } catch (e) {
467
+ out.innerHTML = `<div class="mt-2 p-3 text-error text-xs">${esc(e?.message ?? String(e))}</div>`;
468
+ }
469
+ }
@@ -10,6 +10,7 @@ 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';
@@ -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,6 +127,12 @@ 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 {}
@@ -127,6 +144,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
127
144
  ctx.agent = resolvedAgent;
128
145
  setAgent(resolvedAgent);
129
146
  refreshModelSelect();
147
+ restoreComposerDraft({ sessionId: sid, agent: resolvedAgent, machineId: resolvedMachine });
130
148
 
131
149
  // Default path = session cwd on the target machine (set immediately; no await)
132
150
  let edgeCwd = resolvedCwd;
@@ -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">
@@ -107,7 +107,7 @@ export const AppShell = (opts = {}) => {
107
107
  </div>
108
108
  </div>
109
109
  <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">
110
+ <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
111
  <div class="flex items-center justify-between px-3 py-2 border-b border-base-300 flex-shrink-0">
112
112
  <span class="text-xs uppercase tracking-wider text-base-content/50 font-medium">Projects</span>
113
113
  <div class="flex items-center gap-1">
@@ -143,10 +143,16 @@ export const AppShell = (opts = {}) => {
143
143
  <div id="search-status" class="hidden text-[10px] text-base-content/40 px-0.5"></div>
144
144
  </div>
145
145
  <div id="session-list" class="overflow-y-auto flex-1"></div>
146
- </div>
146
+ <div class="sidebar-mobile-actions hidden border-t border-base-300 px-2 py-2 flex-shrink-0">
147
+ <button type="button" class="btn btn-primary btn-sm flex-1" data-sidebar-action="new">+ 新会话</button>
148
+ <button type="button" class="btn btn-ghost btn-sm px-2" data-sidebar-action="theme" aria-label="切换深色或浅色主题">🌙</button>
149
+ <button type="button" class="btn btn-ghost btn-sm px-2" data-sidebar-action="settings" aria-label="设置">⚙️</button>
150
+ <button type="button" class="btn btn-ghost btn-sm px-2 text-error" data-sidebar-action="logout" aria-label="退出登录">⇥</button>
151
+ </div>
152
+ </aside>
147
153
  <div class="flex flex-col flex-shrink-0 relative" style="width:12px">
148
154
  <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"
155
+ <button id="sidebar-toggle" title="收起侧栏" aria-label="收起侧栏" aria-controls="sidebar" aria-expanded="true"
150
156
  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
157
  style="left:0;top:50%;transform:translateY(-50%)">◀</button>
152
158
  </div>
@@ -278,7 +284,7 @@ export const AppShell = (opts = {}) => {
278
284
  <option value="acceptEdits">acceptEdits</option>
279
285
  <option value="auto">auto</option>
280
286
  <option value="plan">plan</option>
281
- <option value="bypassPermissions">⚠ bypass all</option>
287
+ <option value="bypassPermissions" selected>⚠ bypass all</option>
282
288
  </select>
283
289
  <select id="sel-density" class="select select-xs select-bordered text-xs" title="Chat density — how tools are shown">
284
290
  <option value="clean">density: clean</option>
@@ -332,9 +338,23 @@ export const AppShell = (opts = {}) => {
332
338
  <div id="tab-shell" class="hidden flex-col flex-1 overflow-hidden bg-[#0d1117]">
333
339
  <div class="flex items-center gap-2 px-3 py-1.5 border-b border-base-300 bg-base-200 flex-shrink-0">
334
340
  <span class="text-xs text-base-content/40 font-mono flex-1" id="term-cwd"></span>
341
+ <button id="term-keyboard" class="btn btn-ghost btn-xs text-xs text-base-content/60" type="button">Keyboard</button>
335
342
  <button id="term-reconnect" class="btn btn-ghost btn-xs text-xs text-base-content/40">Reconnect</button>
336
343
  </div>
344
+ <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">
345
+ <button class="term-key" type="button" data-term-key="ctrl-c">Ctrl+C</button>
346
+ <button class="term-key" type="button" data-term-key="escape">Esc</button>
347
+ <button class="term-key" type="button" data-term-key="tab">Tab</button>
348
+ <button class="term-key" type="button" data-term-key="up">↑</button>
349
+ <button class="term-key" type="button" data-term-key="down">↓</button>
350
+ <button class="term-key" type="button" data-term-key="left">←</button>
351
+ <button class="term-key" type="button" data-term-key="right">→</button>
352
+ </div>
337
353
  <div id="terminal-container" class="flex-1 p-2 overflow-hidden"></div>
354
+ <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">
355
+ <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">
356
+ <button class="term-mobile-send" type="submit">发送</button>
357
+ </form>
338
358
  </div>
339
359
  <div id="tab-git" class="hidden flex-col flex-1 overflow-hidden">
340
360
  <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 +368,7 @@ export const AppShell = (opts = {}) => {
348
368
  </div>
349
369
  <div id="tab-memory" class="hidden flex-col flex-1 overflow-hidden">
350
370
  <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>
371
+ <span class="text-xs text-base-content/40 flex-shrink-0">🧠 Agent memory</span>
352
372
  <span class="flex-1"></span>
353
373
  <button id="memory-refresh" class="btn btn-ghost btn-xs text-xs">Refresh</button>
354
374
  </div>
package/public/shell.js CHANGED
@@ -28,11 +28,12 @@ import { AuthScreen, AppShell } from './shell/templates.js';
28
28
  import {
29
29
  updateDashboard, shiftCalMonth, setSelectedCalDate, getDashboardSessions,
30
30
  openRecentMenu, toggleRecentMenu, closeRecentMenu, positionRecentMenu,
31
+ dismissRecentSession,
31
32
  } from './shell/dashboard.js';
32
33
  import {
33
34
  loadAllSessions, loadWorkspaces, renderSessionList, syncSidebarChrome,
34
35
  refreshModelSelect, refreshEffortSelect, promptCustomModel,
35
- scheduleActivitySearch, runActivitySearch, loadMemoryTab,
36
+ scheduleActivitySearch, runActivitySearch, loadMemoryTab, openMemoryFile,
36
37
  } from './shell/session-list.js';
37
38
  import { openProject, goHome, resumeSession, switchTab } from './shell/session-nav.js';
38
39
  import { showApp } from './shell/boot.js';
@@ -336,6 +337,13 @@ export function initShell(opts = {}) {
336
337
  delegate.on('keydown', '#auth-username, #auth-password', e => { if (e.key === 'Enter') submitAuth(); });
337
338
  delegate.on('click', '#hamburger', () => $('sidebar').classList.contains('open') ? closeSidebar() : openSidebar());
338
339
  delegate.on('click', '#sidebar-overlay', closeSidebar);
340
+ document.addEventListener('keydown', e => {
341
+ if (e.key !== 'Escape') return;
342
+ if (window.innerWidth <= 640 && $('sidebar')?.classList.contains('open')) {
343
+ e.preventDefault();
344
+ closeSidebar();
345
+ }
346
+ });
339
347
 
340
348
  delegate.on('input', '#session-search', (_, el) => {
341
349
  sessionSearch.value = el.value;
@@ -383,6 +391,19 @@ export function initShell(opts = {}) {
383
391
  delegate.on('click', '#btn-meta-agent', () => toggleMetaAgent());
384
392
  delegate.on('click', '#btn-meta-agent-welcome', () => openMetaAgent('chat'));
385
393
  delegate.on('click', '#btn-theme', toggleTheme);
394
+ delegate.on('click', '[data-sidebar-action]', (_, el) => {
395
+ const action = el.dataset.sidebarAction;
396
+ if (action === 'new') openNewSessionModal();
397
+ if (action === 'theme') toggleTheme();
398
+ if (action === 'settings') openSettings();
399
+ if (action === 'logout') {
400
+ ctx.token = null;
401
+ localStorage.removeItem('token');
402
+ setSelectedMachine(null);
403
+ hubMachineReady.value = false;
404
+ location.reload();
405
+ }
406
+ });
386
407
 
387
408
  delegate.on('click', '#btn-cal-prev', () => {
388
409
  shiftCalMonth(-1);
@@ -462,6 +483,7 @@ export function initShell(opts = {}) {
462
483
  });
463
484
 
464
485
  delegate.on('click', '#memory-refresh', loadMemoryTab);
486
+ delegate.on('click', '[data-memory-file]', (el) => openMemoryFile(el.dataset.memoryFile, el.dataset.memoryAgent));
465
487
  delegate.on('click', '[data-tab]', (e, el) => switchTab(el.dataset.tab));
466
488
 
467
489
  const applyRoot = async (inputId, sig) => {
@@ -638,6 +660,15 @@ export function initShell(opts = {}) {
638
660
  closeRecentMenu();
639
661
  $('btn-recent-sessions')?.setAttribute('aria-expanded', 'false');
640
662
  });
663
+ delegate.on('click', '[data-dismiss-recent-session]', (e, el) => {
664
+ e.preventDefault();
665
+ e.stopPropagation();
666
+ dismissRecentSession({
667
+ sessionId: el.dataset.dismissRecentSession,
668
+ agent: el.dataset.dismissRecentAgent || 'claude',
669
+ machineId: el.dataset.dismissRecentMachine || null,
670
+ });
671
+ });
641
672
 
642
673
  delegate.on('click', '#btn-edit-project-notes, #project-goal-text, #project-notes-text', () => {
643
674
  renderProjectNotesEditMode();
package/public/state.js CHANGED
@@ -18,11 +18,9 @@ export const wsStatus = signal('idle');
18
18
  export const currentAgent = signal(localStorage.getItem('agent') || 'claude'); // 'claude'|'codex'|'grok'
19
19
  export const currentModel = signal(localStorage.getItem('model') || 'claude-sonnet-4-5');
20
20
  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'
21
+ // All local agents are trusted by default; users can still choose another mode
22
+ // from the expanded composer options before starting a turn.
23
+ export const currentPermission = signal('bypassPermissions'); // 'default'|'acceptEdits'|'bypassPermissions'|'plan'|'auto'
26
24
  export const filesRoot = signal(''); // custom root override for Files tab
27
25
  export const gitRoot = signal(''); // custom root override for Git tab
28
26
  export const agentsMeta = signal(null); // from /api/agents (dynamic model lists)
@@ -251,7 +249,7 @@ export function setAgent(agent) {
251
249
  const a = (agent || 'claude').toLowerCase();
252
250
  currentAgent.value = a;
253
251
  localStorage.setItem('agent', a);
254
- currentPermission.value = a === 'codex' ? 'bypassPermissions' : 'default';
252
+ currentPermission.value = 'bypassPermissions';
255
253
  const permissionSelect = typeof document === 'undefined' ? null : document.getElementById('sel-permission');
256
254
  if (permissionSelect) permissionSelect.value = currentPermission.peek();
257
255
  const models = getModelsForAgent(a);
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-v13';
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',