claude-simple 0.1.0

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 (50) hide show
  1. package/README.md +225 -0
  2. package/client.js +139 -0
  3. package/dist/addon-fit.min.js +2 -0
  4. package/dist/app.0.1.0.min.js +825 -0
  5. package/dist/highlight.min.css +10 -0
  6. package/dist/highlight.min.js +1244 -0
  7. package/dist/index.html +16 -0
  8. package/dist/marked.min.js +6 -0
  9. package/dist/style.0.1.0.min.css +1050 -0
  10. package/dist/xterm.min.css +209 -0
  11. package/dist/xterm.min.js +2 -0
  12. package/gateway.js +349 -0
  13. package/package.json +41 -0
  14. package/public/addon-fit.min.js +2 -0
  15. package/public/agent-panel.js +1079 -0
  16. package/public/api.js +78 -0
  17. package/public/app.js +95 -0
  18. package/public/base.css +4 -0
  19. package/public/cache.js +27 -0
  20. package/public/chat.js +1087 -0
  21. package/public/files.js +476 -0
  22. package/public/git.js +130 -0
  23. package/public/index.html +61 -0
  24. package/public/lib.js +2 -0
  25. package/public/router.js +71 -0
  26. package/public/settings.js +90 -0
  27. package/public/shell/boot.js +159 -0
  28. package/public/shell/dashboard.js +269 -0
  29. package/public/shell/hub.js +192 -0
  30. package/public/shell/layout.js +195 -0
  31. package/public/shell/notes.js +323 -0
  32. package/public/shell/session-context.js +19 -0
  33. package/public/shell/session-list.js +442 -0
  34. package/public/shell/session-nav.js +299 -0
  35. package/public/shell/templates.js +342 -0
  36. package/public/shell/util.js +63 -0
  37. package/public/shell.js +589 -0
  38. package/public/state.js +362 -0
  39. package/public/style.css +1048 -0
  40. package/public/terminal.js +177 -0
  41. package/public/xterm.min.css +209 -0
  42. package/public/xterm.min.js +2 -0
  43. package/server.js +12 -0
  44. package/src/agents/claude.js +58 -0
  45. package/src/agents/codex.js +284 -0
  46. package/src/agents/grok.js +318 -0
  47. package/src/agents/meta-agent.js +1952 -0
  48. package/src/agents/models.js +339 -0
  49. package/src/agents/sessions.js +1508 -0
  50. package/src/app.js +1282 -0
@@ -0,0 +1,63 @@
1
+ // shell/util.js — shared formatting helpers
2
+ import { esc } from '../lib.js';
3
+ import { AGENT_LABELS } from '../state.js';
4
+
5
+ /** Coerce history/API timestamps to epoch milliseconds. */
6
+ export function coerceTs(ts) {
7
+ if (ts == null || ts === '') return null;
8
+ if (typeof ts === 'number') {
9
+ if (!Number.isFinite(ts) || ts <= 0) return null;
10
+ // Unix seconds (~1e9–1e10) → ms; already-ms is ~1e12–1e13
11
+ return ts < 1e12 ? Math.round(ts * 1000) : Math.round(ts);
12
+ }
13
+ if (typeof ts === 'string') {
14
+ const trimmed = ts.trim();
15
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
16
+ return coerceTs(Number(trimmed));
17
+ }
18
+ const ms = Date.parse(trimmed);
19
+ return Number.isNaN(ms) ? null : ms;
20
+ }
21
+ return null;
22
+ }
23
+
24
+ export function formatTime(ts) {
25
+ const ms = coerceTs(ts);
26
+ if (ms == null) return '';
27
+ const diff = Date.now() - ms;
28
+ if (diff < 0) return new Date(ms).toLocaleDateString();
29
+ if (diff < 60000) return 'just now';
30
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
31
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
32
+ if (diff < 7 * 86400000) return `${Math.floor(diff / 86400000)}d ago`;
33
+ return new Date(ms).toLocaleDateString();
34
+ }
35
+
36
+ export function agentBadge(agent, compact = true) {
37
+ const a = agent || 'claude';
38
+ const colors = {
39
+ claude: 'bg-orange-500/15 text-orange-400',
40
+ codex: 'bg-emerald-500/15 text-emerald-400',
41
+ grok: 'bg-sky-500/15 text-sky-400',
42
+ };
43
+ const cls = colors[a] || 'bg-base-300 text-base-content/50';
44
+ const label = compact
45
+ ? (a === 'claude' ? 'CC' : a === 'codex' ? 'CX' : a === 'grok' ? 'GX' : a.slice(0, 2).toUpperCase())
46
+ : (AGENT_LABELS[a] || a);
47
+ return `<span class="text-[9px] px-1 py-0.5 rounded font-mono font-semibold flex-shrink-0 ${cls}" title="${esc(AGENT_LABELS[a] || a)}">${label}</span>`;
48
+ }
49
+
50
+ export function agentCountPills(agents) {
51
+ return Object.entries(agents || {})
52
+ .filter(([, n]) => n > 0)
53
+ .map(([a, n]) => `${agentBadge(a)}<span class="text-[9px] text-base-content/40 -ml-0.5">${n}</span>`)
54
+ .join('');
55
+ }
56
+
57
+ export function shortPath(cwd) {
58
+ if (!cwd) return '';
59
+ const home = ''; // browser can't know home reliably; show last 2 segments if long
60
+ const parts = cwd.split('/').filter(Boolean);
61
+ if (parts.length <= 3) return cwd;
62
+ return '…/' + parts.slice(-2).join('/');
63
+ }
@@ -0,0 +1,589 @@
1
+ // shell.js — public facade: re-exports + init wiring
2
+ // Implementation lives under ./shell/*
3
+
4
+ import { watch, effect, batch, delegate, esc, $ } from './lib.js';
5
+ import {
6
+ sessionsData, workspacesData, sessionFilter, sessionSearch,
7
+ expandedFolders, collapsedFolders, filesPath, viewingFile, filteredSessions, projectGroups,
8
+ currentProject, currentTab, filesRoot, gitRoot,
9
+ currentModel, currentEffort, currentPermission, currentAgent, setAgent,
10
+ sidebarView, agentFilter, timeRange, activityHits, activityLoading,
11
+ chatDensity, setChatDensity,
12
+ favoritesOnly, hubMode, hubMachineReady, selectedMachineId, setSelectedMachine,
13
+ machinesList, AGENT_LABELS, getDefaultModel, sessionMetaMap, ctx,
14
+ } from './state.js';
15
+ import { api } from './api.js';
16
+ import { getLastSessionContext } from './shell/session-context.js';
17
+ import {
18
+ connectWS, clearMessages, appendMsg, appendSystemMsg, sendMessage, stopProcessing,
19
+ applyChatDensity,
20
+ } from './chat.js';
21
+ import { syncHash } from './router.js';
22
+ import { initSettings, openSettings } from './settings.js';
23
+ import { openMetaAgent, toggleMetaAgent } from './agent-panel.js';
24
+
25
+ import { AuthScreen, AppShell } from './shell/templates.js';
26
+ import { updateDashboard, shiftCalMonth, setSelectedCalDate, getDashboardSessions } from './shell/dashboard.js';
27
+ import {
28
+ loadAllSessions, loadWorkspaces, renderSessionList, syncSidebarChrome,
29
+ refreshModelSelect, refreshEffortSelect, promptCustomModel,
30
+ scheduleActivitySearch, runActivitySearch, loadMemoryTab,
31
+ } from './shell/session-list.js';
32
+ import { openProject, goHome, resumeSession, switchTab } from './shell/session-nav.js';
33
+ import { showApp } from './shell/boot.js';
34
+ import {
35
+ fetchProjectNotes, renderProjectNotesDisplayMode, renderProjectNotesEditMode,
36
+ renderSessionNotesBar, renderSessionNotesEditMode,
37
+ toggleSessionFavorite, saveSessionNotes, startInlineRename, setProjectNoteFields,
38
+ } from './shell/notes.js';
39
+ import {
40
+ refreshMachinesList, renderMachinePickerList, renderMachineMenuList,
41
+ syncTopbarMachine, closeMachineMenu, enterMachine,
42
+ } from './shell/hub.js';
43
+ import {
44
+ openSidebar, closeSidebar, closeSidebarOnMobile,
45
+ initSidebarToggle, initSidebarResize,
46
+ applyTheme, toggleTheme,
47
+ openNewSessionModal, updateNewSessionAgentUI, startNewSession,
48
+ } from './shell/layout.js';
49
+
50
+ // Re-export public API (app.js and others)
51
+ export { showApp, resumeSession, switchTab, openProject, goHome, loadAllSessions };
52
+
53
+ export async function initAuth() {
54
+ const authScreen = $('auth-screen');
55
+ if (authScreen) {
56
+ authScreen.classList.remove('hidden');
57
+ authScreen.style.display = 'flex';
58
+ }
59
+ const app = $('app');
60
+ if (app) {
61
+ app.classList.add('hidden');
62
+ app.style.display = 'none';
63
+ }
64
+ const status = await api('GET', '/api/auth/status');
65
+ const btn = $('auth-btn');
66
+ if (status.needsSetup) {
67
+ $('auth-subtitle').textContent = 'Create your account to get started';
68
+ btn.textContent = 'Create Account';
69
+ btn.dataset.mode = 'register';
70
+ } else {
71
+ btn.dataset.mode = 'login';
72
+ }
73
+ }
74
+
75
+ async function submitAuth() {
76
+ const username = $('auth-username').value.trim();
77
+ const password = $('auth-password').value;
78
+ const errEl = $('auth-error');
79
+ errEl.classList.add('hidden');
80
+ try {
81
+ const res = await api('POST', `/api/auth/${$('auth-btn').dataset.mode || 'login'}`, { username, password });
82
+ ctx.token = res.token;
83
+ localStorage.setItem('token', res.token);
84
+ showApp();
85
+ } catch (e) {
86
+ errEl.textContent = e.message;
87
+ errEl.classList.remove('hidden');
88
+ }
89
+ }
90
+
91
+ /** First paint: restore session shell (no calendar flash) when hash is a session. */
92
+ export function shouldRestoreSessionOnBoot() {
93
+ if (!ctx.token) return false;
94
+ return /^#\/session\//.test(location.hash || '');
95
+ }
96
+
97
+ // ── Init — render HTML, wire everything up ────────────────────────────────────
98
+ export function initShell(opts = {}) {
99
+ applyTheme(localStorage.getItem('theme') || 'dark');
100
+
101
+ if (!document.getElementById('xterm-css')) {
102
+ const css = document.createElement('link');
103
+ css.id = 'xterm-css';
104
+ css.rel = 'stylesheet';
105
+ css.href = '/xterm.min.css';
106
+ document.head.appendChild(css);
107
+ }
108
+ if (!document.getElementById('xterm-js')) {
109
+ const js = document.createElement('script');
110
+ js.id = 'xterm-js';
111
+ js.src = '/xterm.min.js';
112
+ document.head.appendChild(js);
113
+ }
114
+ if (!document.getElementById('xterm-fit-js')) {
115
+ const jsFit = document.createElement('script');
116
+ jsFit.id = 'xterm-fit-js';
117
+ jsFit.src = '/addon-fit.min.js';
118
+ document.head.appendChild(jsFit);
119
+ }
120
+
121
+ const restoreSession = opts.restoreSession ?? shouldRestoreSessionOnBoot();
122
+ const root = document.getElementById('root');
123
+ root.style.cssText = 'display:flex;flex-direction:column;flex:1;height:100%';
124
+ root.innerHTML = AuthScreen() + AppShell({ restoreSession });
125
+
126
+ // Avoid sidebar session-list thrashing until first data arrives when restoring
127
+ if (restoreSession) {
128
+ const list = $('session-list');
129
+ if (list) list.innerHTML = '<div class="px-3 py-3 text-[10px] text-base-content/35">Loading sessions…</div>';
130
+ }
131
+
132
+ effect(() => {
133
+ const wss = workspacesData.value;
134
+ const tabs = $('ws-tabs');
135
+ if (!tabs) return;
136
+ if (wss.length <= 1) { tabs.innerHTML = ''; return; }
137
+ const activeId = sessionFilter.peek();
138
+ tabs.innerHTML = [{ id: null, name: 'All' }, ...wss].map(w => {
139
+ const active = w.id === null ? !activeId : activeId === w.configDir;
140
+ return `<button class="px-3 py-1.5 text-[11px] flex-shrink-0 border-b-2 whitespace-nowrap
141
+ ${active ? 'border-primary text-primary' : 'border-transparent text-base-content/50 hover:text-base-content'}"
142
+ data-ws-tab="${esc(w.configDir || '')}">
143
+ ${esc(w.name)}
144
+ </button>`;
145
+ }).join('');
146
+ });
147
+
148
+ effect(() => {
149
+ void filteredSessions.value;
150
+ void projectGroups.value;
151
+ void sidebarView.value;
152
+ void activityLoading.value;
153
+ void activityHits.value;
154
+ void sessionMetaMap.value;
155
+ const el = $('session-list');
156
+ if (!el) return;
157
+ // Keep inline rename input mounted until blur/Enter
158
+ if (el.querySelector('.session-rename-input')) return;
159
+ el.innerHTML = renderSessionList();
160
+ syncSidebarChrome();
161
+ });
162
+
163
+ // Home calendar: full session list (not sidebar time-range / 120-cap filters)
164
+ effect(() => {
165
+ // Subscribe to raw list + agent/machine filters
166
+ void sessionsData.value;
167
+ void agentFilter.value;
168
+ void selectedMachineId.value;
169
+ void hubMode.value;
170
+ const welcome = $('welcome');
171
+ if (!welcome) return;
172
+ if (document.documentElement.classList.contains('boot-restore')) return;
173
+ if (document.documentElement.classList.contains('boot-pending')) return;
174
+ if (welcome.classList.contains('hidden') || welcome.style.display === 'none') return;
175
+ updateDashboard();
176
+ });
177
+
178
+ watch(sessionFilter, f => {
179
+ const badge = $('session-filter-badge');
180
+ if (!badge) return;
181
+ badge.style.display = f ? 'inline' : 'none';
182
+ badge.title = f || '';
183
+ });
184
+
185
+ effect(() => {
186
+ const proj = currentProject.value;
187
+ if (proj && proj.path) {
188
+ fetchProjectNotes(proj.path);
189
+ } else {
190
+ const bar = $('project-notes-bar');
191
+ if (bar) bar.style.display = 'none';
192
+ }
193
+ });
194
+
195
+ watch(currentTab, tab => {
196
+ document.querySelectorAll('[data-tab]').forEach(btn => {
197
+ const active = btn.dataset.tab === tab;
198
+ btn.classList.toggle('text-primary', active);
199
+ btn.classList.toggle('border-primary', active);
200
+ btn.classList.toggle('text-base-content/50', !active);
201
+ btn.classList.toggle('border-transparent', !active);
202
+ });
203
+ // Keep project-view as a column flex container so tab panels can fill height
204
+ const pv = $('project-view');
205
+ if (pv && !pv.classList.contains('hidden')) {
206
+ pv.style.display = 'flex';
207
+ pv.style.flexDirection = 'column';
208
+ pv.style.minHeight = '0';
209
+ pv.style.flex = '1';
210
+ }
211
+ ['chat', 'files', 'git', 'shell', 'memory'].forEach(id => {
212
+ const el = $(`tab-${id}`);
213
+ if (!el) return;
214
+ const on = id === tab;
215
+ el.classList.toggle('hidden', !on);
216
+ if (on) {
217
+ el.style.display = 'flex';
218
+ el.style.flexDirection = 'column';
219
+ el.style.flex = '1 1 0';
220
+ el.style.minHeight = '0';
221
+ el.style.overflow = 'hidden';
222
+ } else {
223
+ el.style.display = 'none';
224
+ }
225
+ });
226
+ if (tab === 'memory') loadMemoryTab();
227
+ // Shell needs a re-fit after becoming visible
228
+ if (tab === 'shell') {
229
+ requestAnimationFrame(() => {
230
+ document.dispatchEvent(new CustomEvent('shell-tab-shown'));
231
+ });
232
+ }
233
+ });
234
+
235
+ initSidebarResize();
236
+ initSidebarToggle();
237
+
238
+ document.addEventListener('sessions-changed', () => loadAllSessions());
239
+ document.addEventListener('router:home', goHome);
240
+
241
+ document.addEventListener('router:session', async ({ detail: { id, tab } }) => {
242
+ const s = sessionsData.peek().find(x => x.sessionId === id);
243
+ const last = getLastSessionContext();
244
+ await resumeSession(
245
+ id,
246
+ s?.cwd || (last?.sessionId === id ? last.cwd : null) || null,
247
+ s?.configDir || (last?.sessionId === id ? last.configDir : null) || null,
248
+ s?.agent || (last?.sessionId === id ? last.agent : null) || null,
249
+ s?.machineId || (last?.sessionId === id ? last.machineId : null) || null,
250
+ { tab: tab || 'chat' },
251
+ );
252
+ });
253
+
254
+ delegate.on('click', '#btn-home', goHome);
255
+ delegate.on('click', '#auth-btn', submitAuth);
256
+ delegate.on('keydown', '#auth-username, #auth-password', e => { if (e.key === 'Enter') submitAuth(); });
257
+ delegate.on('click', '#hamburger', () => $('sidebar').classList.contains('open') ? closeSidebar() : openSidebar());
258
+ delegate.on('click', '#sidebar-overlay', closeSidebar);
259
+
260
+ delegate.on('input', '#session-search', (_, el) => {
261
+ sessionSearch.value = el.value;
262
+ scheduleActivitySearch(el.value);
263
+ });
264
+ delegate.on('click', '#session-filter-badge', () => { sessionFilter.value = null; });
265
+ delegate.on('click', '[data-ws-tab]', (_, el) => { sessionFilter.value = el.dataset.wsTab || null; });
266
+ delegate.on('click', '#sidebar-refresh', () => {
267
+ loadAllSessions();
268
+ if (sessionSearch.peek().trim()) runActivitySearch(sessionSearch.peek());
269
+ });
270
+ delegate.on('click', '[data-view]', (_, el) => {
271
+ sidebarView.value = el.dataset.view;
272
+ localStorage.setItem('sidebarView', el.dataset.view);
273
+ });
274
+ delegate.on('click', '[data-agent-filter]', (_, el) => {
275
+ agentFilter.value = el.dataset.agentFilter;
276
+ localStorage.setItem('agentFilter', el.dataset.agentFilter);
277
+ if (sessionSearch.peek().trim()) scheduleActivitySearch(sessionSearch.peek());
278
+ });
279
+ delegate.on('change', '#time-range', (_, el) => {
280
+ timeRange.value = el.value;
281
+ localStorage.setItem('timeRange', el.value);
282
+ if (sessionSearch.peek().trim()) scheduleActivitySearch(sessionSearch.peek());
283
+ });
284
+ delegate.on('dblclick', '[data-project-cwd]', (_, el) => {
285
+ const cwd = el.dataset.projectCwd;
286
+ if (!cwd) return;
287
+ const name = cwd.split('/').filter(Boolean).pop() || cwd;
288
+ openProject({ id: name, name, path: cwd });
289
+ appendSystemMsg(`Project · ${cwd}`);
290
+ });
291
+ delegate.on('click', '#btn-settings', openSettings);
292
+ delegate.on('click', '#btn-meta-agent', () => toggleMetaAgent());
293
+ delegate.on('click', '#btn-meta-agent-welcome', () => openMetaAgent('chat'));
294
+ delegate.on('click', '#btn-theme', toggleTheme);
295
+
296
+ delegate.on('click', '#btn-cal-prev', () => {
297
+ shiftCalMonth(-1);
298
+ updateDashboard(getDashboardSessions());
299
+ });
300
+ delegate.on('click', '#btn-cal-next', () => {
301
+ shiftCalMonth(1);
302
+ updateDashboard(getDashboardSessions());
303
+ });
304
+ delegate.on('click', '[data-cal-date]', (_, el) => {
305
+ setSelectedCalDate(el.dataset.calDate);
306
+ updateDashboard(getDashboardSessions());
307
+ });
308
+
309
+ delegate.on('click', '[data-session-id]', (e, el) => {
310
+ if (e.target.closest('[data-fav-toggle]')) return;
311
+ if (e.target.closest('[data-rename-session]')) return;
312
+ if (e.target.closest('.session-rename-input')) return;
313
+ if (e.target.closest('.session-title-wrap.session-rename-active')) return;
314
+ resumeSession(
315
+ el.dataset.sessionId,
316
+ el.dataset.sessionCwd || null,
317
+ el.dataset.sessionConfigDir || null,
318
+ el.dataset.sessionAgent || 'claude',
319
+ el.dataset.sessionMachine || null,
320
+ );
321
+ closeSidebarOnMobile();
322
+ });
323
+ // Inline rename (sidebar): click title or ✎ — no dialog
324
+ delegate.on('click', '[data-rename-session]', (e, el) => {
325
+ e.preventDefault();
326
+ e.stopPropagation();
327
+ const wrap = el.closest('.session-title-wrap') || el;
328
+ const sessionId = wrap.dataset.renameSessionId || wrap.dataset.sessionId;
329
+ if (!sessionId) return;
330
+ startInlineRename({
331
+ sessionId,
332
+ agent: wrap.dataset.renameSessionAgent || 'claude',
333
+ machineId: wrap.dataset.renameSessionMachine || null,
334
+ fallbackDisplay: wrap.dataset.renameSessionDisplay || '',
335
+ mountEl: wrap,
336
+ });
337
+ });
338
+ // Inline rename (session bar title)
339
+ delegate.on('click', '[data-rename-bar], #session-title-display', (e, el) => {
340
+ e.preventDefault();
341
+ e.stopPropagation();
342
+ if (!ctx.sessionId) return;
343
+ const mountEl = el.id === 'session-title-display' ? el : (el.closest('#session-title-display') || el);
344
+ startInlineRename({
345
+ sessionId: ctx.sessionId,
346
+ agent: ctx.agent || currentAgent.peek() || 'claude',
347
+ machineId: ctx.machineId || null,
348
+ mountEl,
349
+ });
350
+ });
351
+
352
+ delegate.on('click', '[data-folder]', (_, el) => {
353
+ const cwd = el.dataset.folder;
354
+ const chevron = el.querySelector('span')?.textContent.trim();
355
+ const wasOpen = chevron === '▾';
356
+ const nextExpanded = new Set(expandedFolders.peek());
357
+ const nextCollapsed = new Set(collapsedFolders.peek());
358
+ if (wasOpen) {
359
+ nextExpanded.delete(cwd);
360
+ nextCollapsed.add(cwd);
361
+ } else {
362
+ nextExpanded.add(cwd);
363
+ nextCollapsed.delete(cwd);
364
+ }
365
+ batch(() => {
366
+ expandedFolders.value = nextExpanded;
367
+ collapsedFolders.value = nextCollapsed;
368
+ });
369
+ });
370
+
371
+ delegate.on('click', '#memory-refresh', loadMemoryTab);
372
+ delegate.on('click', '[data-tab]', (e, el) => switchTab(el.dataset.tab));
373
+
374
+ const applyRoot = async (inputId, sig) => {
375
+ const input = $(inputId);
376
+ const raw = input?.value.trim();
377
+ if (!raw) { sig.value = ''; return; }
378
+ try {
379
+ // Resolve on the selected edge machine (hub attaches X-Machine-Id)
380
+ const result = await api('POST', '/api/resolve-path', { path: raw });
381
+ if (result.isDir === false) throw new Error('Path is not a directory on the target machine');
382
+ sig.value = result.path;
383
+ input.value = result.path;
384
+ const proj = currentProject.peek();
385
+ if (proj) currentProject.value = { ...proj, path: result.path };
386
+ } catch (e) {
387
+ input.style.color = 'oklch(var(--er))';
388
+ input.title = e.message;
389
+ setTimeout(() => { input.style.color = ''; input.title = ''; }, 2000);
390
+ }
391
+ };
392
+ delegate.on('click', '#files-root-btn', () => applyRoot('files-root-input', filesRoot));
393
+ delegate.on('click', '#git-root-btn', () => applyRoot('git-root-input', gitRoot));
394
+ delegate.on('keydown', '#files-root-input', e => { if (e.key === 'Enter') applyRoot('files-root-input', filesRoot); });
395
+ delegate.on('keydown', '#git-root-input', e => { if (e.key === 'Enter') applyRoot('git-root-input', gitRoot); });
396
+ delegate.on('click', '#btn-opts', () => { $('chat-opts').classList.toggle('hidden'); });
397
+
398
+ delegate.on('change', '#sel-agent', (_, el) => {
399
+ const prev = currentAgent.peek();
400
+ const next = el.value;
401
+ if (prev !== next) {
402
+ ctx.sessionId = null;
403
+ setAgent(next);
404
+ currentModel.value = getDefaultModel(next) || currentModel.peek();
405
+ localStorage.setItem('model', currentModel.peek());
406
+ refreshModelSelect();
407
+ appendSystemMsg(`Agent → ${AGENT_LABELS[next] || next} · model ${currentModel.peek()} · new session`);
408
+ }
409
+ });
410
+ delegate.on('change', '#sel-convert-agent', async (_, el) => {
411
+ const target = el.value;
412
+ if (!target) return;
413
+ const sid = ctx.sessionId;
414
+ if (!sid) { alert('No active session to convert.'); el.value = ''; return; }
415
+ const source = ctx.agent || currentAgent.peek() || 'claude';
416
+ if (source === target) { alert('Cannot convert to the same agent.'); el.value = ''; return; }
417
+ el.disabled = true;
418
+ appendSystemMsg(`Converting session ${sid.slice(0, 8)} from ${source} to ${target}...`);
419
+ try {
420
+ const res = await api('POST', `/api/sessions/${sid}/convert`, { targetAgent: target, sourceAgent: source });
421
+ if (res && res.success) {
422
+ appendSystemMsg(`✓ Converted successfully. New session: ${res.targetSessionId.slice(0, 8)}`);
423
+ await loadAllSessions();
424
+ await resumeSession(res.targetSessionId, currentProject.peek()?.path || null, null, res.targetAgent);
425
+ syncHash();
426
+ } else {
427
+ appendSystemMsg(`Failed to convert session.`);
428
+ }
429
+ } catch (err) {
430
+ appendSystemMsg(`Error converting session: ${err.message}`);
431
+ alert(`Error converting session: ${err.message}`);
432
+ } finally {
433
+ el.disabled = false;
434
+ el.value = '';
435
+ }
436
+ });
437
+ delegate.on('change', '#sel-model', (_, el) => {
438
+ currentModel.value = el.value;
439
+ localStorage.setItem('model', el.value);
440
+ refreshEffortSelect();
441
+ });
442
+ delegate.on('click', '#btn-model-add', () => promptCustomModel({ editCurrent: false }));
443
+ delegate.on('click', '#btn-model-edit', () => promptCustomModel({ editCurrent: true }));
444
+ delegate.on('change', '#sel-effort', (_, el) => { currentEffort.value = el.value; });
445
+ delegate.on('change', '#sel-permission', (_, el) => { currentPermission.value = el.value; });
446
+ delegate.on('change', '#sel-density', (_, el) => {
447
+ setChatDensity(el.value);
448
+ appendSystemMsg(`Density → ${el.value} (applies to new tool calls; reload session to re-group history)`);
449
+ });
450
+ document.addEventListener('agent-changed', refreshModelSelect);
451
+
452
+ delegate.on('click', '#btn-new-session, #btn-new-session-welcome', openNewSessionModal);
453
+ delegate.on('click', '#new-session-cancel', () => $('modal-new-session').close());
454
+ delegate.on('click', '#new-session-start', startNewSession);
455
+ delegate.on('change', '#new-session-agent', updateNewSessionAgentUI);
456
+ delegate.on('keydown', '#new-session-path', e => { if (e.key === 'Enter') startNewSession(); });
457
+ delegate.on('click', '#send-btn', sendMessage);
458
+ delegate.on('click', '#stop-btn', stopProcessing);
459
+ delegate.on('click', '#btn-logout', () => {
460
+ ctx.token = null;
461
+ localStorage.removeItem('token');
462
+ setSelectedMachine(null);
463
+ hubMachineReady.value = false;
464
+ location.reload();
465
+ });
466
+
467
+ delegate.on('click', '[data-pick-machine]', async (_, el) => {
468
+ const id = el.dataset.pickMachine;
469
+ if (!id || id === selectedMachineId.peek()) {
470
+ closeMachineMenu();
471
+ return;
472
+ }
473
+ await enterMachine(id, { forceReload: true });
474
+ });
475
+ const refreshMachinesUi = async () => {
476
+ const st = $('machine-picker-status');
477
+ if (st) st.textContent = '刷新中…';
478
+ await refreshMachinesList();
479
+ renderMachinePickerList();
480
+ renderMachineMenuList();
481
+ syncTopbarMachine();
482
+ if (st) {
483
+ const n = (machinesList.peek() || []).filter(m => m.online !== false).length;
484
+ st.textContent = `${n} online`;
485
+ }
486
+ };
487
+ delegate.on('click', '#btn-refresh-machines', refreshMachinesUi);
488
+ delegate.on('click', '#btn-refresh-machines-menu', async e => {
489
+ e.stopPropagation();
490
+ await refreshMachinesUi();
491
+ });
492
+ delegate.on('click', '#btn-machine-menu', async e => {
493
+ e.stopPropagation();
494
+ const menu = $('machine-menu');
495
+ if (!menu) return;
496
+ if (menu.classList.contains('hidden')) {
497
+ await refreshMachinesList();
498
+ renderMachineMenuList();
499
+ menu.classList.remove('hidden');
500
+ } else {
501
+ menu.classList.add('hidden');
502
+ }
503
+ });
504
+ document.addEventListener('click', e => {
505
+ const wrap = $('machine-menu-wrap');
506
+ if (wrap && !wrap.contains(e.target)) closeMachineMenu();
507
+ });
508
+
509
+ delegate.on('click', '#btn-edit-project-notes, #project-goal-text, #project-notes-text', () => {
510
+ renderProjectNotesEditMode();
511
+ });
512
+ delegate.on('click', '#btn-cancel-project-notes', () => {
513
+ renderProjectNotesDisplayMode();
514
+ });
515
+
516
+ const handleSaveProjectNotes = async () => {
517
+ const goal = $('project-goal-input')?.value?.trim() ?? '';
518
+ const notes = $('project-notes-input')?.value?.trim() ?? '';
519
+ const proj = currentProject.peek();
520
+ if (!proj || !proj.path) return;
521
+ try {
522
+ const res = await api('POST', '/api/projects/notes', { root: proj.path, goal, notes });
523
+ setProjectNoteFields(res.goal || '', res.notes || '');
524
+ renderProjectNotesDisplayMode();
525
+ } catch (err) {
526
+ alert(`Error saving project notes: ${err.message}`);
527
+ }
528
+ };
529
+ delegate.on('click', '#btn-save-project-notes', handleSaveProjectNotes);
530
+ delegate.on('keydown', '#project-goal-input', e => {
531
+ if (e.key === 'Enter') handleSaveProjectNotes();
532
+ if (e.key === 'Escape') renderProjectNotesDisplayMode();
533
+ });
534
+ delegate.on('keydown', '#project-notes-input', e => {
535
+ if (e.key === 'Escape') renderProjectNotesDisplayMode();
536
+ });
537
+
538
+ delegate.on('click', '[data-fav-toggle]', (e, el) => {
539
+ e.stopPropagation();
540
+ e.preventDefault();
541
+ const mid = el.dataset.favSessionMachine || ctx.machineId;
542
+ if (mid) { ctx.machineId = mid; localStorage.setItem('machineId', mid); }
543
+ toggleSessionFavorite(el.dataset.favSessionId, el.dataset.favSessionAgent || 'claude');
544
+ });
545
+ delegate.on('click', '#btn-edit-session-notes, #session-notes-text', () => {
546
+ renderSessionNotesEditMode();
547
+ });
548
+ delegate.on('click', '#btn-cancel-session-notes', () => renderSessionNotesBar());
549
+ delegate.on('click', '#btn-save-session-notes', async () => {
550
+ const notes = $('session-notes-input')?.value?.trim() || '';
551
+ if (!ctx.sessionId) return;
552
+ const agent = ctx.agent || currentAgent.peek() || 'claude';
553
+ if (await saveSessionNotes(ctx.sessionId, agent, notes)) renderSessionNotesBar();
554
+ });
555
+ delegate.on('keydown', '#session-notes-input', async e => {
556
+ if (e.key === 'Escape') renderSessionNotesBar();
557
+ if (e.key === 'Enter') {
558
+ const notes = e.target.value.trim();
559
+ if (!ctx.sessionId) return;
560
+ const agent = ctx.agent || currentAgent.peek() || 'claude';
561
+ if (await saveSessionNotes(ctx.sessionId, agent, notes)) renderSessionNotesBar();
562
+ }
563
+ });
564
+ delegate.on('click', '#btn-fav-filter', () => {
565
+ const next = !favoritesOnly.peek();
566
+ favoritesOnly.value = next;
567
+ localStorage.setItem('favoritesOnly', next ? '1' : '0');
568
+ $('btn-fav-filter')?.classList.toggle('active', next);
569
+ });
570
+
571
+ refreshModelSelect();
572
+ applyChatDensity();
573
+ const dens = $('sel-density');
574
+ if (dens) dens.value = chatDensity.peek() || 'normal';
575
+ const tr = $('time-range');
576
+ if (tr) tr.value = timeRange.peek() || '14';
577
+ document.querySelectorAll('[data-agent-filter]').forEach(btn => {
578
+ btn.classList.toggle('active', btn.dataset.agentFilter === agentFilter.peek());
579
+ });
580
+ $('btn-fav-filter')?.classList.toggle('active', favoritesOnly.peek());
581
+ document.querySelectorAll('[data-view]').forEach(btn => {
582
+ const on = btn.dataset.view === sidebarView.peek();
583
+ btn.classList.toggle('bg-primary/15', on);
584
+ btn.classList.toggle('text-primary', on);
585
+ btn.classList.toggle('font-medium', on);
586
+ btn.classList.toggle('text-base-content/50', !on);
587
+ });
588
+ }
589
+