claude-simple 0.2.8 → 0.2.10

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-simple",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude-simple": "server.js",
@@ -19,11 +19,13 @@
19
19
  "public"
20
20
  ],
21
21
  "scripts": {
22
- "build": "node build.mjs",
22
+ "assets": "node scripts/sync-vendor.mjs && npx @tailwindcss/cli -i public/base.css -o public/ui.min.css --minify",
23
+ "build": "npm run assets && node build.mjs",
23
24
  "prepublishOnly": "node build.mjs",
24
25
  "start": "NODE_ENV=production node server.js",
25
26
  "dev": "NODE_ENV=development node --watch server.js",
26
27
  "serve": "node server.js",
28
+ "test": "node --test",
27
29
  "gateway": "node gateway.js",
28
30
  "gateway:go": "cd gateway && go run .",
29
31
  "gateway:build": "cd gateway && make build",
@@ -31,10 +33,16 @@
31
33
  },
32
34
  "dependencies": {
33
35
  "@anthropic-ai/claude-agent-sdk": "*",
36
+ "@highlightjs/cdn-assets": "^11.11.1",
34
37
  "bcrypt": "^5.1.1",
35
38
  "express": "^4.18.2",
36
39
  "jsonwebtoken": "^9.0.2",
37
- "ws": "^8.16.0"
40
+ "jszip": "^3.10.1",
41
+ "mammoth": "^1.8.0",
42
+ "marked": "^9.1.6",
43
+ "pdfjs-dist": "^3.11.174",
44
+ "ws": "^8.16.0",
45
+ "xlsx": "^0.18.5"
38
46
  },
39
47
  "devDependencies": {
40
48
  "@tailwindcss/cli": "^4.2.4",
package/public/chat.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { watch, delegate, esc, $ } from './lib.js';
3
3
  import { ctx, isProcessing, currentProject, currentTab, currentModel, currentEffort, currentPermission,
4
4
  currentAgent, setAgent, AGENT_LABELS, getDefaultModel, getModelsForAgent, getEffortsForModel,
5
- addCustomModel, setCurrentModel, chatDensity, wsStatus } from './state.js';
5
+ getDefaultEffort, addCustomModel, setCurrentModel, chatDensity, wsStatus } from './state.js';
6
6
  import { sendWs, api, authHeaders, flushWsQueue, clearWsQueue } from './api.js';
7
7
  import { initWakeLock, setWakeLockDesired } from './wake-lock.js';
8
8
  import { pushNotification } from './notifications.js';
@@ -370,7 +370,7 @@ function handleWsMessage(msg) {
370
370
  finalizeThoughtStream();
371
371
  finalizeAssistantStream();
372
372
  flushToolBatch();
373
- // result already printed "Done" for codex/grok; avoid double for those
373
+ // result already printed "Done" for CLI agents; avoid double for those
374
374
  if (!msg.aborted && msg.agent === 'claude') appendSystemMsg('✓ Done');
375
375
  else if (!msg.aborted && msg.agent && msg.agent !== 'claude' && !msg.error) { /* result handled it */ }
376
376
  else if (!msg.aborted && !msg.agent) appendSystemMsg('✓ Done');
@@ -576,12 +576,12 @@ function handleSlashCommand(text) {
576
576
  switch (cmd.toLowerCase()) {
577
577
  case 'agent': {
578
578
  if (!arg) {
579
- appendSystemMsg(`Current agent: ${currentAgent.peek()} · agents: claude | codex | grok`);
579
+ appendSystemMsg(`Current agent: ${currentAgent.peek()} · agents: claude | codex | grok | gemini | cursor`);
580
580
  return true;
581
581
  }
582
582
  const a = arg.toLowerCase();
583
- if (!['claude', 'codex', 'grok'].includes(a)) {
584
- appendSystemMsg(`Unknown agent "${arg}" · valid: claude | codex | grok`);
583
+ if (!['claude', 'codex', 'grok', 'gemini', 'cursor'].includes(a)) {
584
+ appendSystemMsg(`Unknown agent "${arg}" · valid: claude | codex | grok | gemini | cursor`);
585
585
  return true;
586
586
  }
587
587
  // Switching agent starts a fresh conversation
@@ -639,7 +639,7 @@ function handleSlashCommand(text) {
639
639
  return true;
640
640
  }
641
641
  case 'help': {
642
- appendSystemMsg(`Available slash commands:\n /agent [claude|codex|grok]\n /model [haiku|sonnet|opus|<model-id>]\n /effort [low|medium|high|xhigh|max]\n /usage\n /clear\n /help`);
642
+ appendSystemMsg(`Available slash commands:\n /agent [claude|codex|grok|gemini|cursor]\n /model [haiku|sonnet|opus|<model-id>]\n /effort [low|medium|high|xhigh|max]\n /usage\n /clear\n /help`);
643
643
  return true;
644
644
  }
645
645
  default: {
@@ -773,16 +773,17 @@ export function sendMessage() {
773
773
 
774
774
  // While a turn is running, queue the next message (common Grok complaint).
775
775
  if (isProcessing.peek()) {
776
- // Grok owns its queue on the edge through a long-lived ACP process. Send
777
- // immediately so a page reload cannot discard this follow-up in the tab.
776
+ // Send immediately so a page reload cannot discard this follow-up in the
777
+ // tab. In hub mode the hub serializes it; standalone uses the edge queue.
778
778
  if (currentAgent.peek() === 'grok' && ctx.sessionId) {
779
779
  const agent = 'grok';
780
+ const effort = currentEffort.peek() || getDefaultEffort(agent, currentModel.peek()) || 'low';
780
781
  const ok = sendWs({
781
782
  type: 'command', cmd: 'turn.start', agent, command: text, sessionId: ctx.sessionId,
782
783
  options: {
783
784
  agent, cwd: currentProject.peek().path, sessionId: ctx.sessionId,
784
785
  configDir: ctx.configDir, model: currentModel.peek(),
785
- ...(currentEffort.peek() && { effort: currentEffort.peek() }),
786
+ effort,
786
787
  ...(currentPermission.peek() && { permissionMode: currentPermission.peek() }),
787
788
  ...(currentPermission.peek() === 'bypassPermissions' && { allowDangerouslySkipPermissions: true }),
788
789
  },
@@ -831,9 +832,11 @@ export function sendMessage() {
831
832
  } else {
832
833
  flushToolBatch();
833
834
  appendMsg('user', 'You', text, { ts: now });
834
- const effort = currentEffort.peek();
835
- const permission = currentPermission.peek();
836
835
  const agent = currentAgent.peek() || 'claude';
836
+ const effort = currentEffort.peek()
837
+ || getDefaultEffort(agent, currentModel.peek())
838
+ || (agent === 'grok' ? 'low' : '');
839
+ const permission = currentPermission.peek();
837
840
  // Existing sessions retain their own picker value. New sessions are bound
838
841
  // on the session-created event above.
839
842
  if (ctx.sessionId) setCurrentModel(currentModel.peek(), agent, ctx.sessionId);
package/public/files.js CHANGED
@@ -146,7 +146,7 @@ function renderTextFile(path, content, size) {
146
146
 
147
147
  // ── Office previews ───────────────────────────────────────────────────────────
148
148
  async function renderDocx(arrayBuffer) {
149
- await loadScript('https://cdn.jsdelivr.net/npm/mammoth@1.8.0/mammoth.browser.min.js');
149
+ await loadScript('/vendor/mammoth.browser.min.js');
150
150
  if (typeof mammoth === 'undefined') throw new Error('mammoth failed to load');
151
151
  const result = await mammoth.convertToHtml({ arrayBuffer });
152
152
  const warn = result.messages?.length
@@ -156,7 +156,7 @@ async function renderDocx(arrayBuffer) {
156
156
  }
157
157
 
158
158
  async function renderXlsx(arrayBuffer) {
159
- await loadScript('https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js');
159
+ await loadScript('/vendor/xlsx.full.min.js');
160
160
  if (typeof XLSX === 'undefined') throw new Error('SheetJS failed to load');
161
161
  const wb = XLSX.read(new Uint8Array(arrayBuffer), { type: 'array' });
162
162
  const names = wb.SheetNames || [];
@@ -206,7 +206,7 @@ function pptxExtractSlideTexts(xml) {
206
206
  }
207
207
 
208
208
  async function renderPptx(arrayBuffer) {
209
- await loadScript('https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js');
209
+ await loadScript('/vendor/jszip.min.js');
210
210
  if (typeof JSZip === 'undefined') throw new Error('JSZip failed to load');
211
211
  const zip = await JSZip.loadAsync(arrayBuffer);
212
212
  const slideFiles = Object.keys(zip.files)
@@ -240,10 +240,10 @@ async function renderPptx(arrayBuffer) {
240
240
  }
241
241
 
242
242
  async function ensurePdfJs() {
243
- await loadScript('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js');
243
+ await loadScript('/vendor/pdf.min.js');
244
244
  if (typeof pdfjsLib === 'undefined') throw new Error('pdf.js failed to load');
245
245
  pdfjsLib.GlobalWorkerOptions.workerSrc =
246
- 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
246
+ '/vendor/pdf.worker.min.js';
247
247
  return pdfjsLib;
248
248
  }
249
249
 
package/public/index.html CHANGED
@@ -78,11 +78,10 @@
78
78
  <link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png">
79
79
  <link rel="icon" type="image/png" sizes="512x512" href="/icons/icon-512.png">
80
80
  <title>Agent UI</title>
81
- <link href="https://cdn.jsdelivr.net/npm/daisyui@5/daisyui.css" rel="stylesheet">
82
- <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
83
- <script src="https://cdn.jsdelivr.net/npm/marked@9/marked.min.js"></script>
84
- <link id="hljs-theme" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github-dark.min.css">
85
- <script src="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/highlight.min.js"></script>
81
+ <link href="/ui.min.css" rel="stylesheet">
82
+ <script src="/vendor/marked.min.js"></script>
83
+ <link id="hljs-theme" rel="stylesheet" href="/vendor/highlight-dark.min.css">
84
+ <script src="/vendor/highlight.min.js"></script>
86
85
  <link rel="stylesheet" href="style.css?v=ui-11">
87
86
  </head>
88
87
  <body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
package/public/lib.js CHANGED
@@ -3,5 +3,5 @@
3
3
  // unpinned @latest here means every push to the mini-react repo — tested or
4
4
  // not — goes live immediately. Bump deliberately after verifying against
5
5
  // this app, not as a side effect of publishing a mini-react release.
6
- export * from 'https://cdn.jsdelivr.net/gh/forechoandlook/mini-react@v0.1.13/dist/mini-react.min.js';
6
+ export * from './vendor/mini-react.min.js';
7
7
  // doc from https://cdn.jsdelivr.net/gh/forechoandlook/mini-react@v0.1.13/README.mini.md
package/public/router.js CHANGED
@@ -1,4 +1,4 @@
1
- // router.js — Hash routing: #/ #/session/:id #/session/:id/:tab
1
+ // router.js — Hash routing: #/ #/session/:id/:tab?machine=&agent=
2
2
  import { createRouter, watch } from './lib.js';
3
3
  import { currentTab, ctx } from './state.js';
4
4
 
@@ -26,15 +26,26 @@ export function initRouter(resumeSession, switchTab) {
26
26
 
27
27
  // Keep hash in sync when session/tab changes
28
28
  watch(currentTab, tab => {
29
- if (ctx.sessionId) setHash(`/session/${ctx.sessionId}/${tab}`);
29
+ if (ctx.sessionId) syncHash();
30
30
  });
31
31
  }
32
32
 
33
- /** Parse `#/session/:id/:tab?` → { id, tab } or null */
33
+ /** Parse `#/session/:id/:tab?machine=&agent=` → identity + tab, or null. */
34
34
  export function parseHashPath(path) {
35
- const m = (path || '').match(/^\/session\/([^/]+)(?:\/([^/]+))?$/);
35
+ const [pathname, query = ''] = String(path || '').split('?', 2);
36
+ const m = pathname.match(/^\/session\/([^/]+)(?:\/([^/]+))?$/);
36
37
  if (!m) return null;
37
- return { id: m[1], tab: m[2] || null };
38
+ const params = new URLSearchParams(query);
39
+ try {
40
+ return {
41
+ id: decodeURIComponent(m[1]),
42
+ tab: m[2] ? decodeURIComponent(m[2]) : null,
43
+ machineId: params.get('machine') || null,
44
+ agent: params.get('agent') || null,
45
+ };
46
+ } catch {
47
+ return null;
48
+ }
38
49
  }
39
50
 
40
51
  // Called by app.js when hash is not already handled by showApp deep-link
@@ -49,12 +60,12 @@ function applyHash(path) {
49
60
  document.dispatchEvent(new CustomEvent('router:home'));
50
61
  return;
51
62
  }
52
- const { id, tab } = parsed;
63
+ const { id, tab, machineId, agent } = parsed;
53
64
  if (!_resumeSession || !_switchTab) return;
54
65
 
55
66
  if (id !== ctx.sessionId) {
56
67
  // Need to look up cwd from loaded sessions — defer to shell
57
- document.dispatchEvent(new CustomEvent('router:session', { detail: { id, tab } }));
68
+ document.dispatchEvent(new CustomEvent('router:session', { detail: { id, tab, machineId, agent } }));
58
69
  } else if (tab && tab !== currentTab.peek()) {
59
70
  _switchTab(tab);
60
71
  }
@@ -67,5 +78,22 @@ export function setHash(path) {
67
78
 
68
79
  // Convenience: call after resumeSession completes to set URL
69
80
  export function syncHash() {
70
- if (ctx.sessionId) setHash(`/session/${ctx.sessionId}/${currentTab.peek()}`);
81
+ if (!ctx.sessionId) return;
82
+ const query = new URLSearchParams();
83
+ if (ctx.machineId) query.set('machine', ctx.machineId);
84
+ if (ctx.agent) query.set('agent', ctx.agent);
85
+ const suffix = query.size ? `?${query}` : '';
86
+ setHash(`/session/${encodeURIComponent(ctx.sessionId)}/${encodeURIComponent(currentTab.peek())}${suffix}`);
87
+ }
88
+
89
+ /** A portable, non-secret URL for the active session. Authentication is never embedded. */
90
+ export function currentSessionShareUrl() {
91
+ if (!ctx.sessionId) return null;
92
+ const url = new URL(location.href);
93
+ const query = new URLSearchParams();
94
+ if (ctx.machineId) query.set('machine', ctx.machineId);
95
+ if (ctx.agent) query.set('agent', ctx.agent);
96
+ const suffix = query.size ? `?${query}` : '';
97
+ url.hash = `/session/${encodeURIComponent(ctx.sessionId)}/${encodeURIComponent(currentTab.peek())}${suffix}`;
98
+ return url.toString();
71
99
  }
@@ -1,14 +1,14 @@
1
1
  // settings.js — Workspace settings modal
2
2
  import { esc, $ } from './lib.js';
3
3
  import { api } from './api.js';
4
- import { workspacesData } from './state.js';
4
+ import { workspacesData, hubMode, selectedMachineId } from './state.js';
5
5
  import { setCachedWorkspaces } from './cache.js';
6
6
 
7
7
  export function initSettings() {
8
8
  document.getElementById('root').insertAdjacentHTML('beforeend', `
9
9
  <dialog id="modal-settings" class="modal">
10
10
  <div class="modal-box max-w-lg">
11
- <h3 class="font-bold text-base mb-4">⚙️ Settings — Workspaces</h3>
11
+ <h3 class="font-bold text-base mb-4">Settings</h3>
12
12
  <div id="ws-list" class="flex flex-col gap-2 mb-4"></div>
13
13
  <div class="border-t border-base-300 pt-4">
14
14
  <div class="text-xs uppercase tracking-wider text-base-content/50 mb-2">Add workspace</div>
@@ -19,6 +19,25 @@ export function initSettings() {
19
19
  </div>
20
20
  <div id="ws-error" class="alert alert-error text-xs py-1.5 hidden"></div>
21
21
  </div>
22
+ <div id="mcp-token-settings" class="border-t border-base-300 pt-4 mt-4">
23
+ <div class="flex items-center justify-between gap-2 mb-2">
24
+ <div>
25
+ <div class="text-xs uppercase tracking-wider text-base-content/50">MCP access token</div>
26
+ <p class="text-[11px] text-base-content/45 mt-0.5">The token is shown once. Set its expiry before generating.</p>
27
+ </div>
28
+ <select id="mcp-token-expiry" class="select select-xs select-bordered">
29
+ <option value="86400">24 hours</option>
30
+ <option value="604800">7 days</option>
31
+ <option value="2592000">30 days</option>
32
+ </select>
33
+ </div>
34
+ <button id="mcp-token-generate" class="btn btn-sm btn-primary w-full">Generate MCP token</button>
35
+ <div id="mcp-token-result" class="hidden mt-2">
36
+ <textarea id="mcp-token-value" readonly class="textarea textarea-bordered textarea-xs w-full h-20 font-mono"></textarea>
37
+ <button id="mcp-token-copy" class="btn btn-ghost btn-xs mt-1">Copy token</button>
38
+ </div>
39
+ <div id="mcp-token-error" class="alert alert-error text-xs py-1.5 hidden mt-2"></div>
40
+ </div>
22
41
  <div class="modal-action">
23
42
  <form method="dialog"><button class="btn btn-ghost btn-sm">Close</button></form>
24
43
  </div>
@@ -31,6 +50,8 @@ export function initSettings() {
31
50
  document.getElementById('ws-add-btn').addEventListener('click', addWorkspace);
32
51
  document.getElementById('ws-new-name').addEventListener('keydown', e => { if (e.key === 'Enter') addWorkspace(); });
33
52
  document.getElementById('ws-new-dir').addEventListener('keydown', e => { if (e.key === 'Enter') addWorkspace(); });
53
+ document.getElementById('mcp-token-generate').addEventListener('click', generateMcpToken);
54
+ document.getElementById('mcp-token-copy').addEventListener('click', copyMcpToken);
34
55
  }
35
56
 
36
57
  function renderWsList() {
@@ -86,5 +107,36 @@ async function deleteWorkspace(id) {
86
107
 
87
108
  export function openSettings() {
88
109
  renderWsList();
110
+ const mcp = $('mcp-token-settings');
111
+ if (mcp) mcp.style.display = hubMode.peek() ? '' : 'none';
89
112
  $('modal-settings')?.showModal();
90
113
  }
114
+
115
+ async function generateMcpToken() {
116
+ const result = $('mcp-token-result');
117
+ const err = $('mcp-token-error');
118
+ result?.classList.add('hidden');
119
+ err?.classList.add('hidden');
120
+ const machineId = selectedMachineId.peek();
121
+ if (!machineId) {
122
+ err.textContent = '请选择一台在线机器后再生成 Token。';
123
+ err.classList.remove('hidden');
124
+ return;
125
+ }
126
+ try {
127
+ const expiresInSeconds = Number($('mcp-token-expiry').value);
128
+ const data = await api('POST', `/api/mcp/tokens?machine=${encodeURIComponent(machineId)}`, { expiresInSeconds });
129
+ $('mcp-token-value').value = data.token || '';
130
+ result.classList.remove('hidden');
131
+ } catch (e) {
132
+ err.textContent = e.message;
133
+ err.classList.remove('hidden');
134
+ }
135
+ }
136
+
137
+ async function copyMcpToken() {
138
+ const value = $('mcp-token-value')?.value;
139
+ if (!value) return;
140
+ try { await navigator.clipboard.writeText(value); }
141
+ catch { $('mcp-token-value')?.select(); document.execCommand('copy'); }
142
+ }
@@ -98,10 +98,10 @@ export async function showApp(opts = {}) {
98
98
  || (last?.sessionId === deep.id ? last : null);
99
99
  await resumeSession(
100
100
  deep.id,
101
- s?.cwd || last?.cwd || null,
102
- s?.configDir || last?.configDir || null,
103
- s?.agent || last?.agent || null,
104
- s?.machineId || last?.machineId || remembered || null,
101
+ s?.cwd || null,
102
+ s?.configDir || null,
103
+ deep.agent || s?.agent || null,
104
+ deep.machineId || s?.machineId || remembered || null,
105
105
  { tab: deep.tab || 'chat' },
106
106
  );
107
107
  // ensure machines eventually known
@@ -150,9 +150,9 @@ export async function showApp(opts = {}) {
150
150
  || (last?.sessionId === deep.id ? last : null);
151
151
  await resumeSession(
152
152
  deep.id,
153
- s?.cwd || last?.cwd || null,
154
- s?.configDir || last?.configDir || null,
155
- s?.agent || last?.agent || null,
153
+ s?.cwd || null,
154
+ s?.configDir || null,
155
+ deep.agent || s?.agent || null,
156
156
  null,
157
157
  { tab: deep.tab || 'chat' },
158
158
  );
@@ -113,9 +113,8 @@ export function applyTheme(theme) {
113
113
  }
114
114
  if (link) {
115
115
  const dark = theme === 'dark' || theme === 'night' || theme === 'black';
116
- link.href = dark
117
- ? 'https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github-dark.min.css'
118
- : 'https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github.min.css';
116
+ // Keep a local theme so a Hub remains usable on disconnected/restricted networks.
117
+ link.href = '/vendor/highlight-dark.min.css';
119
118
  }
120
119
  }
121
120
 
@@ -7,7 +7,7 @@ import {
7
7
  sidebarView, agentFilter, timeRange, activityHits, activityLoading,
8
8
  sessionMetaMap, LOW_TURN_THRESHOLD, metaKey, sessionDisplayTitle,
9
9
  hubMode, selectedMachineId, agentsMeta, currentEffort,
10
- getModelsForAgent, getDefaultModel, getEffortsForModel,
10
+ getModelsForAgent, getDefaultModel, getEffortsForModel, getDefaultEffort,
11
11
  addCustomModel, removeCustomModel, getCustomModels, setCurrentModel, ctx,
12
12
  } from '../state.js';
13
13
  import { api, NOT_MODIFIED } from '../api.js';
@@ -23,11 +23,10 @@ function normalizeSessions(v) {
23
23
  }
24
24
 
25
25
  function sessionsQuery() {
26
- const days = parseInt(timeRange.peek() || '14', 10);
27
26
  const q = new URLSearchParams();
28
- // Match sidebar time filter so we don't pull years of metadata on mobile.
29
- if (days > 0) q.set('days', String(days));
30
- q.set('limit', '400');
27
+ // Project index is small and should remain complete; the Hub/browser cache
28
+ // keeps this inexpensive after the first fetch.
29
+ q.set('limit', '2000');
31
30
  const s = q.toString();
32
31
  return s ? `/api/sessions?${s}` : '/api/sessions';
33
32
  }
@@ -174,15 +173,28 @@ export function refreshEffortSelect() {
174
173
  const agent = currentAgent.peek() || 'claude';
175
174
  const model = currentModel.peek();
176
175
  const efforts = getEffortsForModel(agent, model);
177
- const cur = currentEffort.peek() || '';
176
+ let cur = currentEffort.peek() || '';
177
+ if (!cur) {
178
+ const def = getDefaultEffort(agent, model);
179
+ if (def && efforts.includes(def)) {
180
+ cur = def;
181
+ currentEffort.value = def;
182
+ }
183
+ }
178
184
  const opts = [`<option value="">effort: off</option>`]
179
185
  .concat(efforts.map(e =>
180
186
  `<option value="${esc(e)}" ${e === cur ? 'selected' : ''}>${esc(e)}</option>`
181
187
  ));
182
188
  sel.innerHTML = opts.join('');
183
189
  if (cur && !efforts.includes(cur)) {
184
- currentEffort.value = '';
185
- sel.value = '';
190
+ const def = getDefaultEffort(agent, model);
191
+ if (def && efforts.includes(def)) {
192
+ currentEffort.value = def;
193
+ sel.value = def;
194
+ } else {
195
+ currentEffort.value = '';
196
+ sel.value = '';
197
+ }
186
198
  } else {
187
199
  sel.value = cur;
188
200
  }
@@ -261,7 +273,6 @@ export function renderProjectGroups(groups) {
261
273
  } else {
262
274
  open = forceOpen || key === topCwd || groups.length <= 3;
263
275
  }
264
- const latestTitle = g.latest?.display || g.latest?.snippet || '';
265
276
  const machineTag = g.machineId
266
277
  ? `<span class="text-[9px] font-mono text-base-content/40 flex-shrink-0">@${esc(g.machineId)}</span>`
267
278
  : '';
@@ -274,11 +285,6 @@ export function renderProjectGroups(groups) {
274
285
  ${machineTag}
275
286
  <span class="text-[9px] text-base-content/30 flex-shrink-0">${formatTime(g.updatedAt)}</span>
276
287
  </div>
277
- <div class="flex items-center gap-1.5 mt-0.5 pl-4">
278
- <span class="text-[10px] text-base-content/35 font-mono truncate flex-1" title="${esc(g.cwd || '')}">${esc(shortPath(g.cwd))}</span>
279
- <span class="flex items-center gap-0.5 flex-shrink-0">${agentCountPills(g.agents)}</span>
280
- </div>
281
- ${latestTitle && !open ? `<div class="pl-4 mt-0.5 text-[10px] text-base-content/45 truncate" title="${esc(latestTitle)}">↳ ${esc(latestTitle)}</div>` : ''}
282
288
  </div>`;
283
289
  const sessions = g.sessions.slice(0, 40);
284
290
  const more = g.sessions.length > 40
@@ -349,7 +355,7 @@ export function syncSidebarChrome() {
349
355
  status.textContent = 'Searching content…';
350
356
  } else if (q && hitResults) {
351
357
  status.classList.remove('hidden');
352
- status.textContent = `${hitResults.length} hit${hitResults.length === 1 ? '' : 's'} · ${timeRange.peek() === '0' ? 'all time' : timeRange.peek() + 'd'}${hits.deep === false ? '' : ' (incl. content)'}`;
358
+ status.textContent = `${hitResults.length} hit${hitResults.length === 1 ? '' : 's'}${hits.deep === false ? '' : ' (incl. content)'}`;
353
359
  } else if (q) {
354
360
  status.classList.remove('hidden');
355
361
  status.textContent = 'Local filter only — type to deep-search';
@@ -378,7 +384,7 @@ export async function runActivitySearch(q) {
378
384
  try {
379
385
  const params = new URLSearchParams({
380
386
  q: query,
381
- days: timeRange.peek() || '14',
387
+ days: '0',
382
388
  limit: '50',
383
389
  deep: '1',
384
390
  });
@@ -6,7 +6,7 @@ export const AuthScreen = () => `
6
6
  <div class="card bg-base-200 border border-base-300 w-full max-w-sm shadow-xl">
7
7
  <div class="card-body gap-3 p-8">
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>
9
+ <p id="auth-subtitle" class="text-sm text-base-content/60 mb-1">Claude · Codex · Grok · Gemini · Cursor</p>
10
10
  <form id="auth-form" class="flex flex-col gap-3" autocomplete="on" action="#">
11
11
  <label class="form-control">
12
12
  <div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">Username</span></div>
@@ -79,6 +79,7 @@ export const AppShell = (opts = {}) => {
79
79
  <span id="topbar-project" class="text-sm text-base-content/50 flex-1 truncate min-w-0">Select a session</span>
80
80
  <span id="conn-dot" class="w-2 h-2 rounded-full bg-base-content/30 flex-shrink-0" title="连接状态"></span>
81
81
  <div class="flex gap-2 topbar-actions">
82
+ <button id="btn-share-session" class="btn btn-ghost btn-xs border border-base-300" title="复制当前会话分享链接">分享</button>
82
83
  <button id="btn-refresh-conn" class="btn btn-ghost btn-xs border border-base-300" title="重新连接并同步">↻</button>
83
84
  <button id="btn-pwa-install" class="btn btn-ghost btn-xs border border-base-300 hidden" title="安装到主屏幕">安装</button>
84
85
  <button id="btn-meta-agent" class="btn btn-ghost btn-xs border border-primary/40 text-primary" title="Meta Agent — 报告 / 问答 / VLM"><span class="hidden sm:inline">✦ Agent</span><span class="sm:hidden">✦</span></button>
@@ -134,29 +135,26 @@ export const AppShell = (opts = {}) => {
134
135
  </div>
135
136
  </div>
136
137
  <div id="ws-tabs" class="flex overflow-x-auto border-b border-base-300 flex-shrink-0" style="scrollbar-width:none"></div>
137
- <div class="px-2 py-1.5 border-b border-base-300 flex-shrink-0 flex flex-col gap-1.5">
138
- <input id="session-search" type="text" placeholder="Search projects & recent work…"
139
- class="input input-xs input-bordered w-full text-xs" autocomplete="off">
140
- <div class="flex items-center gap-1 flex-wrap">
141
- <div id="view-tabs" class="flex rounded-md border border-base-300 overflow-hidden text-[10px]">
142
- <button data-view="projects" class="px-2 py-0.5 bg-primary/15 text-primary font-medium">Projects</button>
143
- <button data-view="timeline" class="px-2 py-0.5 text-base-content/50 hover:bg-base-300">Recent</button>
144
- </div>
145
- <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">
146
- <option value="1">Today</option>
147
- <option value="7">7d</option>
148
- <option value="14" selected>14d</option>
149
- <option value="30">30d</option>
150
- <option value="0">All</option>
151
- </select>
138
+ <div class="px-2 py-1.5 border-b border-base-300 flex-shrink-0 relative">
139
+ <div class="flex gap-1.5">
140
+ <input id="session-search" type="text" placeholder="Search projects & recent work…"
141
+ class="input input-xs input-bordered flex-1 min-w-0 text-xs" autocomplete="off">
142
+ <button id="sidebar-filter-toggle" type="button" class="btn btn-ghost btn-xs border border-base-300 px-2" title="筛选项目" aria-expanded="false">⚙</button>
152
143
  </div>
153
- <div id="agent-filter" class="flex gap-1 flex-wrap">
154
- <button data-agent-filter="all" class="agent-pill active">All</button>
155
- <button data-agent-filter="claude" class="agent-pill">Claude</button>
156
- <button data-agent-filter="codex" class="agent-pill">Codex</button>
157
- <button data-agent-filter="grok" class="agent-pill">Grok</button>
158
- <button id="btn-fav-filter" class="agent-pill" title="Show only favorites">★ Fav</button>
159
- <button id="btn-hidden-filter" class="agent-pill" title="Show only hidden sessions">🙈 Hidden</button>
144
+ <div id="sidebar-filter-menu" class="hidden absolute left-2 right-2 top-[calc(100%-.25rem)] z-30 rounded-lg border border-base-300 bg-base-100 p-2 shadow-xl">
145
+ <div class="text-[10px] uppercase tracking-wide text-base-content/45 mb-1.5">Agents · multi-select</div>
146
+ <div class="grid grid-cols-2 gap-1 text-xs">
147
+ <label><input type="checkbox" data-agent-option="claude"> Claude</label>
148
+ <label><input type="checkbox" data-agent-option="codex"> Codex</label>
149
+ <label><input type="checkbox" data-agent-option="grok"> Grok</label>
150
+ <label><input type="checkbox" data-agent-option="gemini"> Gemini</label>
151
+ <label><input type="checkbox" data-agent-option="cursor"> Cursor</label>
152
+ </div>
153
+ <div class="mt-2 pt-2 border-t border-base-300 flex gap-3 text-xs">
154
+ <label><input id="filter-favorites" type="checkbox"> Favorites</label>
155
+ <label><input id="filter-hidden" type="checkbox"> Hidden</label>
156
+ <button id="filter-clear" type="button" class="ml-auto text-primary">Clear</button>
157
+ </div>
160
158
  </div>
161
159
  <div id="search-status" class="hidden text-[10px] text-base-content/40 px-0.5"></div>
162
160
  </div>
@@ -178,7 +176,7 @@ export const AppShell = (opts = {}) => {
178
176
  <div id="welcome" class="${welcomeCls}">
179
177
  <div class="max-w-4xl w-full text-center flex flex-col items-center gap-2 mt-2 md:mt-4">
180
178
  <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>
181
- <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>
179
+ <p class="text-sm text-base-content/65 max-w-md">Claude · Codex · Grok · Gemini — Select a session from the sidebar or click a day on the calendar to view activity.</p>
182
180
  <div class="flex gap-2 mt-0.5">
183
181
  <button id="btn-new-session-welcome" class="btn btn-primary btn-sm shadow-md">+ New Session</button>
184
182
  <button id="btn-meta-agent-welcome" class="btn btn-outline btn-primary btn-sm">✦ Meta Agent</button>
@@ -272,6 +270,8 @@ export const AppShell = (opts = {}) => {
272
270
  <option value="claude">Claude</option>
273
271
  <option value="codex">Codex</option>
274
272
  <option value="grok">Grok</option>
273
+ <option value="gemini">Gemini</option>
274
+ <option value="cursor">Cursor</option>
275
275
  </select>
276
276
  <select id="sel-convert-agent" class="select select-xs select-bordered text-xs font-semibold" title="Convert session to another agent">
277
277
  <option value="" disabled selected>Convert to...</option>
@@ -406,6 +406,8 @@ export const AppShell = (opts = {}) => {
406
406
  <option value="claude">Claude Code</option>
407
407
  <option value="codex">OpenAI Codex</option>
408
408
  <option value="grok">Grok</option>
409
+ <option value="gemini">Google Gemini (agy)</option>
410
+ <option value="cursor">Cursor Agent</option>
409
411
  </select>
410
412
  </label>
411
413
  <label class="form-control" id="new-session-machine-wrap" style="display:none">
@@ -39,10 +39,12 @@ export function agentBadge(agent, compact = true) {
39
39
  claude: 'bg-orange-500/15 text-orange-400',
40
40
  codex: 'bg-emerald-500/15 text-emerald-400',
41
41
  grok: 'bg-sky-500/15 text-sky-400',
42
+ gemini: 'bg-violet-500/15 text-violet-400',
43
+ cursor: 'bg-amber-500/15 text-amber-400',
42
44
  };
43
45
  const cls = colors[a] || 'bg-base-300 text-base-content/50';
44
46
  const label = compact
45
- ? (a === 'claude' ? 'CC' : a === 'codex' ? 'CX' : a === 'grok' ? 'GX' : a.slice(0, 2).toUpperCase())
47
+ ? (a === 'claude' ? 'CC' : a === 'codex' ? 'CX' : a === 'grok' ? 'GX' : a === 'gemini' ? 'GM' : a === 'cursor' ? 'CU' : a.slice(0, 2).toUpperCase())
46
48
  : (AGENT_LABELS[a] || a);
47
49
  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
50
  }