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,299 @@
1
+ // shell/session-nav.js — open project, resume session, tabs, home
2
+ import { batch, esc, $ } from '../lib.js';
3
+ import {
4
+ sessionsData, sessionFilter, filesPath, viewingFile, filteredSessions,
5
+ currentProject, currentTab, filesRoot, gitRoot, currentAgent, setAgent,
6
+ selectedMachineId, setSelectedMachine, AGENT_LABELS, ctx,
7
+ } from '../state.js';
8
+ import { api } from '../api.js';
9
+ import { getLastSessionContext, setLastSessionContext } from './session-context.js';
10
+ import {
11
+ connectWS, clearMessages, appendMsg, appendSystemMsg, renderToolUse,
12
+ appendHistoryTokenBar, appendContextBar, flushToolBatch, coerceTs,
13
+ } from '../chat.js';
14
+ import { setHash, syncHash } from '../router.js';
15
+ import { refreshModelSelect } from './session-list.js';
16
+ import { renderSessionNotesBar } from './notes.js';
17
+ import { updateDashboard, getDashboardSessions } from './dashboard.js';
18
+
19
+ /** Show which edge machine Files/Git/Shell paths belong to. */
20
+ function syncMachinePathHints(machineId) {
21
+ for (const id of ['files-machine-hint', 'git-machine-hint']) {
22
+ const el = $(id);
23
+ if (!el) continue;
24
+ if (machineId) {
25
+ el.textContent = `@${machineId}`;
26
+ el.classList.remove('hidden');
27
+ el.title = `Paths are on machine ${machineId}`;
28
+ } else {
29
+ el.textContent = '';
30
+ el.classList.add('hidden');
31
+ el.title = '';
32
+ }
33
+ }
34
+ }
35
+
36
+ export function openProject(project) {
37
+ const path = project?.path || '';
38
+ batch(() => {
39
+ currentProject.value = project;
40
+ sessionFilter.value = path;
41
+ // Default Files/Git root = project path
42
+ if (path) {
43
+ filesRoot.value = path;
44
+ gitRoot.value = path;
45
+ filesPath.value = '';
46
+ viewingFile.value = null;
47
+ }
48
+ });
49
+ ctx.sessionId = null;
50
+ $('topbar-project').textContent = path || 'Project';
51
+ const fi = $('files-root-input'); if (fi && path) fi.value = path;
52
+ const gi = $('git-root-input'); if (gi && path) gi.value = path;
53
+ syncMachinePathHints(project?.machineId || selectedMachineId.peek() || ctx.machineId);
54
+ $('welcome').classList.add('hidden');
55
+ const pv = $('project-view');
56
+ pv.classList.remove('hidden');
57
+ pv.style.display = 'flex';
58
+ clearMessages();
59
+ connectWS();
60
+ switchTab('chat');
61
+ }
62
+
63
+ export function goHome() {
64
+ currentProject.value = null;
65
+ sessionFilter.value = null;
66
+ filesRoot.value = '';
67
+ gitRoot.value = '';
68
+ filesPath.value = '';
69
+ viewingFile.value = null;
70
+ ctx.sessionId = null;
71
+ syncMachinePathHints(null);
72
+
73
+ // Leaving session restore mode — calendar is allowed again
74
+ document.documentElement.classList.remove('boot-restore');
75
+
76
+ const topbar = $('topbar-project');
77
+ if (topbar) topbar.textContent = 'Select a session';
78
+ const welcome = $('welcome');
79
+ if (welcome) {
80
+ welcome.classList.remove('hidden');
81
+ welcome.style.display = '';
82
+ updateDashboard(getDashboardSessions());
83
+ }
84
+ const pv = $('project-view');
85
+ if (pv) {
86
+ pv.classList.add('hidden');
87
+ pv.style.display = 'none';
88
+ }
89
+ setHash('/');
90
+ }
91
+
92
+ /**
93
+ * @param {string} sid
94
+ * @param {string|null} cwd
95
+ * @param {string|null} configDir
96
+ * @param {string|null} agent
97
+ * @param {string|null} machineId
98
+ * @param {{ tab?: string, skipHistory?: boolean }} [opts]
99
+ */
100
+ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts = {}) {
101
+ const targetTab = opts.tab && ['chat', 'files', 'git', 'shell', 'memory'].includes(opts.tab)
102
+ ? opts.tab
103
+ : 'chat';
104
+ // Non-chat deep links: open tab immediately; don't block on chat history
105
+ const skipHistory = opts.skipHistory ?? (targetTab !== 'chat' && targetTab !== 'memory');
106
+
107
+ const fromList = sessionsData.peek().find(s =>
108
+ s.sessionId === sid && (!machineId || s.machineId === machineId || !s.machineId)
109
+ );
110
+ const last = getLastSessionContext();
111
+ const fromLast = last?.sessionId === sid ? last : null;
112
+
113
+ const resolvedAgent = agent || fromList?.agent || fromLast?.agent || 'claude';
114
+ const resolvedMachine = machineId || fromList?.machineId || fromLast?.machineId
115
+ || selectedMachineId.peek() || null;
116
+ const resolvedCwd = cwd || fromList?.cwd || fromLast?.cwd || null;
117
+ const resolvedConfig = configDir || fromList?.configDir || fromLast?.configDir || null;
118
+
119
+ if (resolvedMachine && resolvedMachine !== ctx.machineId) {
120
+ setSelectedMachine(resolvedMachine);
121
+ try { ctx.ws?.close(); } catch {}
122
+ ctx.ws = null;
123
+ }
124
+
125
+ ctx.sessionId = sid;
126
+ ctx.configDir = resolvedConfig;
127
+ ctx.agent = resolvedAgent;
128
+ setAgent(resolvedAgent);
129
+ refreshModelSelect();
130
+
131
+ // Default path = session cwd on the target machine (set immediately; no await)
132
+ let edgeCwd = resolvedCwd;
133
+ applyProjectPaths(edgeCwd, resolvedMachine, sid);
134
+ connectWS();
135
+
136
+ // Background: normalize path on the edge (does not block chat)
137
+ if (edgeCwd) {
138
+ api('POST', '/api/resolve-path', { path: edgeCwd }, undefined, {
139
+ machineId: resolvedMachine || undefined,
140
+ }).then(r => {
141
+ if (ctx.sessionId !== sid || !r?.path || r.path === edgeCwd) return;
142
+ edgeCwd = r.path;
143
+ applyProjectPaths(edgeCwd, resolvedMachine, sid);
144
+ setLastSessionContext({ sessionId: sid, cwd: edgeCwd });
145
+ }).catch(() => {});
146
+ }
147
+
148
+ // Switch to target tab BEFORE history (files/git/shell usable immediately)
149
+ clearMessages();
150
+ switchTab(targetTab);
151
+ {
152
+ const cur = sessionsData.peek();
153
+ sessionsData.value = Array.isArray(cur) ? [...cur] : [];
154
+ }
155
+
156
+ setLastSessionContext({
157
+ sessionId: sid,
158
+ cwd: edgeCwd || resolvedCwd,
159
+ configDir: resolvedConfig,
160
+ agent: resolvedAgent,
161
+ machineId: resolvedMachine,
162
+ tab: targetTab,
163
+ });
164
+
165
+ const label = AGENT_LABELS[resolvedAgent] || resolvedAgent;
166
+ const cacheKey = `${resolvedMachine || ''}:${resolvedAgent}:${sid}`;
167
+
168
+ const paintMessages = (msgs, context, meta = {}) => {
169
+ if (ctx.sessionId !== sid) return;
170
+ $('messages').innerHTML = '';
171
+ if (!msgs.length) {
172
+ appendSystemMsg(`${label} · ${sid.slice(0, 8)}… — no history`);
173
+ return;
174
+ }
175
+ flushToolBatch();
176
+ // Paint in chunks so the UI stays responsive on long histories
177
+ const CHUNK = 40;
178
+ let i = 0;
179
+ const paintChunk = () => {
180
+ if (ctx.sessionId !== sid) return;
181
+ const end = Math.min(i + CHUNK, msgs.length);
182
+ for (; i < end; i++) {
183
+ const m = msgs[i];
184
+ const ts = coerceTs(m.ts ?? m.timestamp);
185
+ if (m.type === 'text') {
186
+ if (m.role === 'assistant' || m.role === 'user') flushToolBatch();
187
+ appendMsg(m.role === 'user' ? 'user' : 'assistant', m.role === 'user' ? 'You' : label, m.content, { ts });
188
+ } else if (m.type === 'tool_use') {
189
+ renderToolUse({ name: m.name, input: m.input ?? {} }, { ts });
190
+ } else if (m.type === 'token_usage') {
191
+ flushToolBatch();
192
+ appendHistoryTokenBar(m.usage);
193
+ }
194
+ }
195
+ if (i < msgs.length) {
196
+ requestAnimationFrame(paintChunk);
197
+ return;
198
+ }
199
+ flushToolBatch();
200
+ if (context) appendContextBar(context);
201
+ const turnCount = msgs.filter(m => m.type === 'token_usage').length
202
+ || msgs.filter(m => m.role === 'user').length;
203
+ const more = meta.truncated
204
+ ? ` (showing last ${msgs.length} of ${meta.total || '?'})`
205
+ : '';
206
+ appendSystemMsg(`── ${label} history · ${turnCount} turn${turnCount !== 1 ? 's' : ''}${more} ──`);
207
+ };
208
+ paintChunk();
209
+ };
210
+
211
+ const loadHistory = async () => {
212
+ // Instant re-open from in-memory cache
213
+ const cached = historyCache.get(cacheKey);
214
+ if (cached && Date.now() - cached.at < HISTORY_CACHE_TTL) {
215
+ paintMessages(cached.msgs, cached.context, cached.meta);
216
+ return;
217
+ }
218
+ appendSystemMsg(`Loading ${label} history…`);
219
+ try {
220
+ // tail=120 → server returns only recent messages (much faster on long sessions)
221
+ const q = new URLSearchParams({ agent: resolvedAgent, tail: '120' });
222
+ const data = await api('GET', `/api/sessions/${sid}/messages?${q}`);
223
+ const msgs = Array.isArray(data) ? data : (data.messages || []);
224
+ const context = Array.isArray(data) ? null : (data.context || null);
225
+ const meta = Array.isArray(data) ? {} : { truncated: data.truncated, total: data.total };
226
+ historyCache.set(cacheKey, { msgs, context, meta, at: Date.now() });
227
+ paintMessages(msgs, context, meta);
228
+ } catch (e) {
229
+ if (ctx.sessionId !== sid) return;
230
+ $('messages').innerHTML = '';
231
+ appendSystemMsg(`${label} · ${sid.slice(0, 8)}… (history unavailable: ${e.message})`);
232
+ }
233
+ };
234
+
235
+ // Never block tab UI on history; chat still loads ASAP in background
236
+ if (skipHistory) {
237
+ loadHistory().catch(() => {});
238
+ } else {
239
+ // yield one frame so tab paint happens first
240
+ await new Promise(r => requestAnimationFrame(r));
241
+ loadHistory().catch(() => {});
242
+ }
243
+ syncHash();
244
+ renderSessionNotesBar();
245
+ }
246
+
247
+ /** Apply session cwd as default Files/Git/Shell root (target machine path). */
248
+ function applyProjectPaths(edgeCwd, resolvedMachine, sid) {
249
+ if (edgeCwd) {
250
+ const name = edgeCwd.split('/').filter(Boolean).pop() || edgeCwd;
251
+ batch(() => {
252
+ currentProject.value = {
253
+ id: name,
254
+ name,
255
+ path: edgeCwd,
256
+ machineId: resolvedMachine,
257
+ };
258
+ sessionFilter.value = edgeCwd;
259
+ filesRoot.value = edgeCwd;
260
+ gitRoot.value = edgeCwd;
261
+ filesPath.value = '';
262
+ viewingFile.value = null;
263
+ });
264
+ $('topbar-project').textContent = resolvedMachine
265
+ ? `${edgeCwd} @${resolvedMachine}`
266
+ : edgeCwd;
267
+ const fi = $('files-root-input'); if (fi) fi.value = edgeCwd;
268
+ const gi = $('git-root-input'); if (gi) gi.value = edgeCwd;
269
+ syncMachinePathHints(resolvedMachine);
270
+ $('welcome')?.classList.add('hidden');
271
+ const pv = $('project-view');
272
+ if (pv) {
273
+ pv.classList.remove('hidden');
274
+ pv.style.display = 'flex';
275
+ pv.style.flexDirection = 'column';
276
+ pv.style.flex = '1';
277
+ pv.style.minHeight = '0';
278
+ pv.style.overflow = 'hidden';
279
+ }
280
+ } else {
281
+ $('welcome')?.classList.add('hidden');
282
+ const pv = $('project-view');
283
+ if (pv) {
284
+ pv.classList.remove('hidden');
285
+ pv.style.display = 'flex';
286
+ pv.style.flexDirection = 'column';
287
+ pv.style.flex = '1';
288
+ pv.style.minHeight = '0';
289
+ }
290
+ syncMachinePathHints(resolvedMachine);
291
+ }
292
+ }
293
+
294
+ // In-memory chat history cache (instant re-open within TTL)
295
+ const historyCache = new Map();
296
+ const HISTORY_CACHE_TTL = 3 * 60 * 1000;
297
+
298
+ // ── Tabs ──────────────────────────────────────────────────────────────────────
299
+ export function switchTab(name) { currentTab.value = name; }
@@ -0,0 +1,342 @@
1
+ // shell/templates.js — Auth + main app HTML shells
2
+ import { ctx } from '../state.js';
3
+
4
+ export const AuthScreen = () => `
5
+ <div id="auth-screen" class="${ctx.token ? 'hidden' : ''} flex items-center justify-center h-full p-4" ${ctx.token ? 'style="display:none"' : ''}>
6
+ <div class="card bg-base-200 border border-base-300 w-full max-w-sm shadow-xl">
7
+ <div class="card-body gap-3 p-8">
8
+ <h1 class="text-xl font-bold">🤖 Agent UI</h1>
9
+ <p id="auth-subtitle" class="text-sm text-base-content/60 mb-1">Claude · Codex · Grok</p>
10
+ <label class="form-control">
11
+ <div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">Username</span></div>
12
+ <input id="auth-username" type="text" class="input input-bordered input-sm" placeholder="username" autocomplete="username">
13
+ </label>
14
+ <label class="form-control">
15
+ <div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">Password</span></div>
16
+ <input id="auth-password" type="password" class="input input-bordered input-sm" placeholder="••••••" autocomplete="current-password">
17
+ </label>
18
+ <button id="auth-btn" class="btn btn-primary btn-sm mt-1" data-mode="login">Sign In</button>
19
+ <div id="auth-error" class="alert alert-error text-xs py-2 hidden"></div>
20
+ </div>
21
+ </div>
22
+ </div>`;
23
+
24
+ /**
25
+ * @param {{ restoreSession?: boolean }} [opts]
26
+ * restoreSession: hide welcome calendar on first paint (deep-link / session restore)
27
+ */
28
+ export const AppShell = (opts = {}) => {
29
+ const restore = !!opts.restoreSession;
30
+ const appHidden = ctx.token ? '' : 'hidden';
31
+ const appStyle = ctx.token ? 'display:flex' : 'display:none';
32
+ const welcomeCls = restore
33
+ ? 'hidden flex-1 flex-col items-center justify-start overflow-y-auto p-4 md:p-6 gap-5'
34
+ : 'flex-1 flex flex-col items-center justify-start overflow-y-auto p-4 md:p-6 gap-5';
35
+ const projectCls = restore
36
+ ? 'flex flex-col flex-1 overflow-hidden min-h-0'
37
+ : 'hidden flex-col flex-1 overflow-hidden min-h-0';
38
+ const projectStyle = restore ? 'display:flex;flex-direction:column;flex:1;min-height:0' : '';
39
+ return `
40
+ <div id="sidebar-overlay" class="hidden fixed inset-0 bg-black/50 z-[9]"></div>
41
+ <div id="app" class="${appHidden} flex-col flex-1 overflow-hidden" style="${appStyle}">
42
+ <div class="flex items-center gap-2 px-3 bg-base-200 border-b border-base-300 flex-shrink-0" style="height:44px">
43
+ <button id="hamburger" class="btn btn-ghost btn-sm px-2 text-lg">☰</button>
44
+ <span id="btn-home" class="font-semibold text-sm hidden sm:inline cursor-pointer hover:text-primary transition-colors select-none">🤖 Agent UI</span>
45
+ <span class="text-base-300 hidden sm:inline">/</span>
46
+ <div id="machine-menu-wrap" class="relative hidden flex-shrink-0">
47
+ <button type="button" id="btn-machine-menu" class="btn btn-ghost btn-xs border border-base-300 font-mono gap-1 max-w-[11rem]"
48
+ title="机器列表 / 切换">
49
+ <span id="topbar-machine-dot" class="w-1.5 h-1.5 rounded-full bg-success flex-shrink-0"></span>
50
+ <span id="topbar-machine-label" class="truncate">机器</span>
51
+ <span class="text-[10px] opacity-50">▾</span>
52
+ </button>
53
+ <div id="machine-menu" class="hidden absolute left-0 top-full mt-1 z-50 w-72 max-h-80 overflow-y-auto
54
+ rounded-lg border border-base-300 bg-base-100 shadow-xl p-2">
55
+ <div class="flex items-center justify-between px-1 pb-1.5 mb-1 border-b border-base-300">
56
+ <span class="text-[10px] uppercase tracking-wide text-base-content/45 font-semibold">Machines</span>
57
+ <button type="button" id="btn-refresh-machines-menu" class="btn btn-ghost btn-xs px-1 text-[10px]">↻</button>
58
+ </div>
59
+ <div id="machine-menu-list" class="flex flex-col gap-1"></div>
60
+ <p id="machine-menu-empty" class="text-xs text-base-content/40 px-1 py-2 hidden">暂无机器在线</p>
61
+ </div>
62
+ </div>
63
+ <span id="topbar-project" class="text-sm text-base-content/50 flex-1 truncate">Select a session</span>
64
+ <div class="flex gap-2">
65
+ <button id="btn-meta-agent" class="btn btn-ghost btn-xs border border-primary/40 text-primary" title="Meta Agent — 报告 / 问答 / VLM">✦ Agent</button>
66
+ <button id="btn-new-session" class="btn btn-ghost btn-xs border border-base-300">+ New</button>
67
+ <button id="btn-theme" class="btn btn-ghost btn-xs border border-base-300">🌙</button>
68
+ <button id="btn-settings" class="btn btn-ghost btn-xs border border-base-300">⚙️</button>
69
+ <button id="btn-logout" class="btn btn-ghost btn-xs border border-base-300">Sign out</button>
70
+ </div>
71
+ </div>
72
+ <!-- Hub: only when no remembered machine (or last one offline) -->
73
+ <div id="machine-picker" class="hidden fixed inset-0 z-[60] bg-base-100 flex items-center justify-center p-4">
74
+ <div class="w-full max-w-md">
75
+ <h2 class="text-xl font-bold mb-1">选择机器</h2>
76
+ <p class="text-sm text-base-content/55 mb-4">首次使用或上次选择的机器不在线时需要选择。之后会记住你的选择,也可随时在顶栏切换。</p>
77
+ <div id="machine-picker-list" class="flex flex-col gap-2 mb-4"></div>
78
+ <p id="machine-picker-empty" class="text-sm text-base-content/40 hidden">暂无在线机器。请在业务机上运行 <code class="text-xs">npm run client</code> 后点刷新。</p>
79
+ <div class="flex items-center gap-2">
80
+ <button id="btn-refresh-machines" class="btn btn-ghost btn-sm border border-base-300">↻ 刷新</button>
81
+ <span id="machine-picker-status" class="text-xs text-base-content/40"></span>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ <div class="flex flex-1 overflow-hidden relative">
86
+ <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">
87
+ <div class="flex items-center justify-between px-3 py-2 border-b border-base-300 flex-shrink-0">
88
+ <span class="text-xs uppercase tracking-wider text-base-content/50 font-medium">Projects</span>
89
+ <div class="flex items-center gap-1">
90
+ <span id="session-filter-badge" class="hidden text-[10px] text-primary cursor-pointer">× filter</span>
91
+ <button id="sidebar-refresh" class="btn btn-ghost btn-xs px-1 text-[10px] text-base-content/40" title="Refresh">↻</button>
92
+ </div>
93
+ </div>
94
+ <div id="ws-tabs" class="flex overflow-x-auto border-b border-base-300 flex-shrink-0" style="scrollbar-width:none"></div>
95
+ <div class="px-2 py-1.5 border-b border-base-300 flex-shrink-0 flex flex-col gap-1.5">
96
+ <input id="session-search" type="text" placeholder="Search projects & recent work…"
97
+ class="input input-xs input-bordered w-full text-xs" autocomplete="off">
98
+ <div class="flex items-center gap-1 flex-wrap">
99
+ <div id="view-tabs" class="flex rounded-md border border-base-300 overflow-hidden text-[10px]">
100
+ <button data-view="projects" class="px-2 py-0.5 bg-primary/15 text-primary font-medium">Projects</button>
101
+ <button data-view="timeline" class="px-2 py-0.5 text-base-content/50 hover:bg-base-300">Recent</button>
102
+ </div>
103
+ <select id="time-range" class="select select-xs select-bordered text-[10px] h-6 min-h-0 py-0 pl-1 pr-6" title="Time range">
104
+ <option value="1">Today</option>
105
+ <option value="7">7d</option>
106
+ <option value="14" selected>14d</option>
107
+ <option value="30">30d</option>
108
+ <option value="0">All</option>
109
+ </select>
110
+ </div>
111
+ <div id="agent-filter" class="flex gap-1 flex-wrap">
112
+ <button data-agent-filter="all" class="agent-pill active">All</button>
113
+ <button data-agent-filter="claude" class="agent-pill">Claude</button>
114
+ <button data-agent-filter="codex" class="agent-pill">Codex</button>
115
+ <button data-agent-filter="grok" class="agent-pill">Grok</button>
116
+ <button id="btn-fav-filter" class="agent-pill" title="Show only favorites">★ Fav</button>
117
+ </div>
118
+ <div id="search-status" class="hidden text-[10px] text-base-content/40 px-0.5"></div>
119
+ </div>
120
+ <div id="session-list" class="overflow-y-auto flex-1"></div>
121
+ </div>
122
+ <div class="flex flex-col flex-shrink-0 relative" style="width:12px">
123
+ <div id="sidebar-resize" class="absolute inset-0 bg-transparent hover:bg-primary/30 transition-colors cursor-col-resize"></div>
124
+ <button id="sidebar-toggle" title="Toggle sidebar"
125
+ 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"
126
+ style="left:0;top:50%;transform:translateY(-50%)">◀</button>
127
+ </div>
128
+ <div id="main" class="flex-1 flex flex-col overflow-hidden min-w-0">
129
+ <div id="welcome" class="${welcomeCls}">
130
+ <div class="max-w-4xl w-full text-center flex flex-col items-center gap-2 mt-2 md:mt-4">
131
+ <h2 class="text-3xl font-extrabold tracking-tight bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent select-none">🤖 Agent UI</h2>
132
+ <p class="text-sm text-base-content/65 max-w-md">Claude · Codex · Grok — Select a session from the sidebar or click a day on the calendar to view activity.</p>
133
+ <div class="flex gap-2 mt-0.5">
134
+ <button id="btn-new-session-welcome" class="btn btn-primary btn-sm shadow-md">+ New Session</button>
135
+ <button id="btn-meta-agent-welcome" class="btn btn-outline btn-primary btn-sm">✦ Meta Agent</button>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- Stats Banner -->
140
+ <div class="max-w-4xl w-full grid grid-cols-2 md:grid-cols-4 gap-4 text-center select-none mt-1">
141
+ <div class="bg-base-200 border border-base-300 rounded-2xl p-3 shadow-md flex flex-col justify-center items-center">
142
+ <span class="text-[10px] uppercase tracking-wider text-base-content/40 font-bold">Total Sessions</span>
143
+ <span id="stats-total-sessions" class="text-xl md:text-2xl font-extrabold text-primary mt-0.5">0</span>
144
+ </div>
145
+ <div class="bg-base-200 border border-base-300 rounded-2xl p-3 shadow-md flex flex-col justify-center items-center">
146
+ <span class="text-[10px] uppercase tracking-wider text-base-content/40 font-bold">Active Projects</span>
147
+ <span id="stats-total-projects" class="text-xl md:text-2xl font-extrabold text-accent mt-0.5">0</span>
148
+ </div>
149
+ <div class="bg-base-200 border border-base-300 rounded-2xl p-3 shadow-md flex flex-col justify-center items-center">
150
+ <span class="text-[10px] uppercase tracking-wider text-base-content/40 font-bold">Total Tokens</span>
151
+ <span id="stats-total-tokens" class="text-xl md:text-2xl font-extrabold text-secondary mt-0.5">0</span>
152
+ </div>
153
+ <div class="bg-base-200 border border-base-300 rounded-2xl p-3 shadow-md flex flex-col justify-center items-center">
154
+ <span class="text-[10px] uppercase tracking-wider text-base-content/40 font-bold">Selected Day Tokens</span>
155
+ <span id="stats-selected-tokens" class="text-xl md:text-2xl font-extrabold text-success mt-0.5">0</span>
156
+ </div>
157
+ </div>
158
+ <div class="max-w-4xl w-full grid grid-cols-1 md:grid-cols-12 gap-5 items-start text-left">
159
+ <!-- Calendar Card -->
160
+ <div class="card bg-base-200 border border-base-300 md:col-span-7 shadow-xl">
161
+ <div class="card-body p-4">
162
+ <div class="flex items-center justify-between mb-3">
163
+ <button id="btn-cal-prev" class="btn btn-ghost btn-sm text-base font-bold">◀</button>
164
+ <h3 id="calendar-month-year" class="text-base font-extrabold text-base-content select-none"></h3>
165
+ <button id="btn-cal-next" class="btn btn-ghost btn-sm text-base font-bold">▶</button>
166
+ </div>
167
+
168
+ <div class="grid grid-cols-7 gap-2 text-center text-[10px] font-bold text-base-content/40 mb-1">
169
+ <div>Sun</div><div>Mon</div><div>Tue</div><div>Wed</div><div>Thu</div><div>Fri</div><div>Sat</div>
170
+ </div>
171
+
172
+ <div id="calendar-grid" class="grid grid-cols-7 gap-2">
173
+ <!-- JS Rendered -->
174
+ </div>
175
+ </div>
176
+ </div>
177
+
178
+ <!-- Details Card -->
179
+ <div class="card bg-base-200 border border-base-300 md:col-span-5 shadow-xl min-h-[290px] flex flex-col">
180
+ <div class="card-body p-4 flex flex-col flex-1">
181
+ <h3 id="selected-day-title" class="card-title text-xs font-bold uppercase tracking-wider text-base-content/50 mb-3 select-none">Daily Activity</h3>
182
+ <div id="selected-day-sessions" class="flex-1 overflow-y-auto flex flex-col gap-2 max-h-[250px]">
183
+ <div class="text-center text-xs text-base-content/40 my-auto py-6">Select a date on the calendar to view its sessions.</div>
184
+ </div>
185
+ </div>
186
+ </div>
187
+ </div>
188
+
189
+ <!-- Recent Sessions Section -->
190
+ <div class="max-w-4xl w-full border-t border-base-300/60 pt-4 mt-1 text-left">
191
+ <h3 class="text-xs font-bold uppercase tracking-wider text-base-content/50 mb-3 select-none">Recent Workspaces & Sessions</h3>
192
+ <div id="recent-sessions-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
193
+ <!-- JS Rendered -->
194
+ </div>
195
+ </div>
196
+ </div>
197
+ <div id="project-view" class="${projectCls}" style="${projectStyle}">
198
+ <div class="flex bg-base-200 border-b border-base-300 flex-shrink-0 overflow-x-auto" style="scrollbar-width:none">
199
+ <button class="px-4 py-2.5 text-sm border-b-2 flex-shrink-0 text-primary border-primary" data-tab="chat">Chat</button>
200
+ <button class="px-4 py-2.5 text-sm border-b-2 flex-shrink-0 text-base-content/50 border-transparent" data-tab="files">Files</button>
201
+ <button class="px-4 py-2.5 text-sm border-b-2 flex-shrink-0 text-base-content/50 border-transparent" data-tab="git">Git</button>
202
+ <button class="px-4 py-2.5 text-sm border-b-2 flex-shrink-0 text-base-content/50 border-transparent" data-tab="shell">Shell</button>
203
+ <button class="px-4 py-2.5 text-sm border-b-2 flex-shrink-0 text-base-content/50 border-transparent" data-tab="memory">🧠 Memory</button>
204
+ </div>
205
+ <div id="tab-chat" class="flex flex-col flex-1 overflow-hidden relative">
206
+ <div id="project-notes-bar" class="bg-base-200 border-b border-base-300 px-3 py-1.5 text-xs flex flex-col gap-1" style="display:none"></div>
207
+ <div id="session-notes-bar" class="bg-base-200/80 border-b border-base-300 px-3 py-1 text-xs flex justify-between items-center gap-2" style="display:none"></div>
208
+ <div id="messages" class="flex-1 overflow-y-auto p-3 flex flex-col gap-2"></div>
209
+ <div id="scroll-nav" class="absolute right-3 bottom-24 flex flex-col gap-1.5 z-10">
210
+ <button id="scroll-top-btn" class="scroll-fab hidden" title="Jump to top">↑</button>
211
+ <button id="scroll-bottom-btn" class="scroll-fab hidden" title="Jump to bottom">↓</button>
212
+ </div>
213
+ <div id="permission-requests"></div>
214
+ <div class="flex flex-col border-t border-base-300 bg-base-200 flex-shrink-0">
215
+ <div id="chat-opts" class="hidden flex items-center gap-1.5 px-3 pt-2 flex-wrap">
216
+ <select id="sel-agent" class="select select-xs select-bordered text-xs font-semibold" title="Agent">
217
+ <option value="claude">Claude</option>
218
+ <option value="codex">Codex</option>
219
+ <option value="grok">Grok</option>
220
+ </select>
221
+ <select id="sel-convert-agent" class="select select-xs select-bordered text-xs font-semibold" title="Convert session to another agent">
222
+ <option value="" disabled selected>Convert to...</option>
223
+ <option value="claude">Convert to Claude</option>
224
+ <option value="codex">Convert to Codex</option>
225
+ <option value="grok">Convert to Grok</option>
226
+ </select>
227
+ <div class="flex items-center gap-0.5">
228
+ <select id="sel-model" class="select select-xs select-bordered font-mono text-xs max-w-[11rem]" title="Model">
229
+ <option value="claude-sonnet-4-5">sonnet-4-5</option>
230
+ <option value="claude-sonnet-4-6">sonnet-4-6</option>
231
+ <option value="claude-opus-4-5">opus-4-5</option>
232
+ <option value="claude-haiku-4-5">haiku-4-5</option>
233
+ </select>
234
+ <button type="button" id="btn-model-add" class="btn btn-ghost btn-xs px-1.5 font-mono" title="Add / set custom model id">+</button>
235
+ <button type="button" id="btn-model-edit" class="btn btn-ghost btn-xs px-1.5" title="Edit current model id">✎</button>
236
+ </div>
237
+ <select id="sel-effort" class="select select-xs select-bordered text-xs" title="Effort">
238
+ <option value="">effort: off</option>
239
+ <option value="low">low</option>
240
+ <option value="medium">medium</option>
241
+ <option value="high">high</option>
242
+ <option value="xhigh">xhigh</option>
243
+ <option value="max">max</option>
244
+ </select>
245
+ <select id="sel-permission" class="select select-xs select-bordered text-xs" title="Permissions">
246
+ <option value="default">perms: default</option>
247
+ <option value="acceptEdits">acceptEdits</option>
248
+ <option value="auto">auto</option>
249
+ <option value="plan">plan</option>
250
+ <option value="bypassPermissions">⚠ bypass all</option>
251
+ </select>
252
+ <select id="sel-density" class="select select-xs select-bordered text-xs" title="Chat density — how tools are shown">
253
+ <option value="clean">density: clean</option>
254
+ <option value="normal">density: normal</option>
255
+ <option value="full">density: full</option>
256
+ </select>
257
+ </div>
258
+ <div class="flex gap-2 items-end px-3 pt-2 pb-2">
259
+ <button id="btn-opts" class="btn btn-ghost btn-sm px-2 text-base-content/30 hover:text-base-content flex-shrink-0" title="Options" style="height:40px">⚙</button>
260
+ <textarea id="chat-input" rows="1" placeholder="Ask… (!cmd shell · ⌘↵ send)"
261
+ class="textarea textarea-bordered flex-1 text-sm resize-none leading-relaxed"
262
+ style="min-height:40px;max-height:140px"></textarea>
263
+ <button id="send-btn" class="btn btn-primary btn-sm flex-shrink-0" style="height:40px">Send</button>
264
+ <button id="stop-btn" class="btn btn-error btn-sm hidden flex-shrink-0" style="height:40px">■</button>
265
+ </div>
266
+ </div>
267
+ </div>
268
+ <div id="tab-files" class="hidden flex-col flex-1 overflow-hidden">
269
+ <div class="flex items-center gap-2 px-3 py-1.5 border-b border-base-300 flex-shrink-0 bg-base-200">
270
+ <span class="text-xs text-base-content/40 flex-shrink-0">Root</span>
271
+ <span id="files-machine-hint" class="hidden text-[10px] font-mono font-semibold text-primary flex-shrink-0"></span>
272
+ <input id="files-root-input" type="text" placeholder="Absolute path on the target machine"
273
+ class="input input-xs flex-1 font-mono text-xs bg-transparent border-0 focus:outline-none px-1" autocomplete="off">
274
+ <button id="files-root-btn" class="btn btn-ghost btn-xs text-xs">Go</button>
275
+ </div>
276
+ <div id="files-breadcrumb" class="flex items-center flex-wrap gap-1 px-4 py-2 border-b border-base-300 text-xs text-base-content/60 flex-shrink-0"></div>
277
+ <div id="files-area" class="flex-1 overflow-y-auto"></div>
278
+ </div>
279
+ <div id="tab-shell" class="hidden flex-col flex-1 overflow-hidden bg-[#0d1117]">
280
+ <div class="flex items-center gap-2 px-3 py-1.5 border-b border-base-300 bg-base-200 flex-shrink-0">
281
+ <span class="text-xs text-base-content/40 font-mono flex-1" id="term-cwd"></span>
282
+ <button id="term-reconnect" class="btn btn-ghost btn-xs text-xs text-base-content/40">Reconnect</button>
283
+ </div>
284
+ <div id="terminal-container" class="flex-1 p-2 overflow-hidden"></div>
285
+ </div>
286
+ <div id="tab-git" class="hidden flex-col flex-1 overflow-hidden">
287
+ <div class="flex items-center gap-2 px-3 py-1.5 border-b border-base-300 flex-shrink-0 bg-base-200">
288
+ <span class="text-xs text-base-content/40 flex-shrink-0">Root</span>
289
+ <span id="git-machine-hint" class="hidden text-[10px] font-mono font-semibold text-primary flex-shrink-0"></span>
290
+ <input id="git-root-input" type="text" placeholder="Absolute path on the target machine"
291
+ class="input input-xs flex-1 font-mono text-xs bg-transparent border-0 focus:outline-none px-1" autocomplete="off">
292
+ <button id="git-root-btn" class="btn btn-ghost btn-xs text-xs">Go</button>
293
+ </div>
294
+ <div id="git-area" class="flex-1 overflow-y-auto"></div>
295
+ </div>
296
+ <div id="tab-memory" class="hidden flex-col flex-1 overflow-hidden">
297
+ <div class="flex items-center gap-2 px-3 py-1.5 border-b border-base-300 flex-shrink-0 bg-base-200">
298
+ <span class="text-xs text-base-content/40 flex-shrink-0">🧠 Project memory</span>
299
+ <span class="flex-1"></span>
300
+ <button id="memory-refresh" class="btn btn-ghost btn-xs text-xs">Refresh</button>
301
+ </div>
302
+ <div id="memory-area" class="flex-1 overflow-y-auto text-sm"></div>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ </div>
307
+ </div>
308
+ <dialog id="modal-new-session" class="modal">
309
+ <div class="modal-box max-w-sm">
310
+ <h3 class="font-bold text-base mb-4">+ New Session</h3>
311
+ <div class="flex flex-col gap-3">
312
+ <label class="form-control">
313
+ <div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">Agent</span></div>
314
+ <select id="new-session-agent" class="select select-bordered select-sm">
315
+ <option value="claude">Claude Code</option>
316
+ <option value="codex">OpenAI Codex</option>
317
+ <option value="grok">Grok</option>
318
+ </select>
319
+ </label>
320
+ <label class="form-control" id="new-session-machine-wrap" style="display:none">
321
+ <div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">Machine (edge server)</span></div>
322
+ <select id="new-session-machine" class="select select-bordered select-sm"></select>
323
+ </label>
324
+ <label class="form-control">
325
+ <div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">Working Directory</span></div>
326
+ <input id="new-session-path" type="text" class="input input-bordered input-sm font-mono"
327
+ placeholder="~/projects/my-app" autocomplete="off">
328
+ </label>
329
+ <label class="form-control" id="new-session-ws-wrap">
330
+ <div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">Claude Workspace</span></div>
331
+ <select id="new-session-workspace" class="select select-bordered select-sm"></select>
332
+ </label>
333
+ <div id="new-session-error" class="alert alert-error text-xs py-2 hidden"></div>
334
+ </div>
335
+ <div class="modal-action">
336
+ <button id="new-session-cancel" class="btn btn-ghost btn-sm">Cancel</button>
337
+ <button id="new-session-start" class="btn btn-primary btn-sm">Start</button>
338
+ </div>
339
+ </div>
340
+ <form method="dialog" class="modal-backdrop"><button>close</button></form>
341
+ </dialog>`;
342
+ };