claude-simple 0.2.9 → 0.2.11
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/README.md +13 -5
- package/dist/app.0.2.9.min.js +143 -139
- package/dist/style.0.2.9.min.css +1 -1
- package/package.json +11 -3
- package/public/chat.js +5 -5
- package/public/files.js +5 -5
- package/public/index.html +6 -7
- package/public/lib.js +1 -1
- package/public/router.js +36 -8
- package/public/settings.js +54 -2
- package/public/shell/boot.js +7 -7
- package/public/shell/layout.js +2 -3
- package/public/shell/session-list.js +5 -12
- package/public/shell/templates.js +34 -28
- package/public/shell/util.js +3 -1
- package/public/shell.js +54 -13
- package/public/state.js +36 -5
- package/public/style.css +13 -0
- package/public/sw.js +6 -2
- package/public/ui.min.css +2 -0
- package/public/vendor/highlight-dark.min.css +10 -0
- package/public/vendor/highlight.min.js +1262 -0
- package/public/vendor/jszip.min.js +13 -0
- package/public/vendor/mammoth.browser.min.js +21 -0
- package/public/vendor/marked.min.js +6 -0
- package/public/vendor/mini-react.min.js +4 -0
- package/public/vendor/pdf.min.js +22 -0
- package/public/vendor/pdf.worker.min.js +22 -0
- package/public/vendor/xlsx.full.min.js +22 -0
- package/src/agents/cursor.js +189 -0
- package/src/agents/gemini.js +182 -0
- package/src/agents/meta-agent.js +9 -5
- package/src/agents/models.js +112 -2
- package/src/agents/sessions.js +303 -14
- package/src/app.js +119 -7
- package/src/mcp.js +275 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-simple",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
@@ -370,7 +370,7 @@ function handleWsMessage(msg) {
|
|
|
370
370
|
finalizeThoughtStream();
|
|
371
371
|
finalizeAssistantStream();
|
|
372
372
|
flushToolBatch();
|
|
373
|
-
// result already printed "Done" for
|
|
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: {
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
'
|
|
246
|
+
'/vendor/pdf.worker.min.js';
|
|
247
247
|
return pdfjsLib;
|
|
248
248
|
}
|
|
249
249
|
|
package/public/index.html
CHANGED
|
@@ -78,12 +78,11 @@
|
|
|
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="
|
|
82
|
-
<script src="
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
<
|
|
86
|
-
<link rel="stylesheet" href="style.css?v=ui-11">
|
|
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>
|
|
85
|
+
<link rel="stylesheet" href="style.css?v=ui-12">
|
|
87
86
|
</head>
|
|
88
87
|
<body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
|
|
89
88
|
<div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
|
|
@@ -99,6 +98,6 @@
|
|
|
99
98
|
}
|
|
100
99
|
}, 10000);
|
|
101
100
|
</script>
|
|
102
|
-
<script type="module" src="app.js?v=ui-
|
|
101
|
+
<script type="module" src="app.js?v=ui-12"></script>
|
|
103
102
|
</body>
|
|
104
103
|
</html>
|
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 '
|
|
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
|
|
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)
|
|
29
|
+
if (ctx.sessionId) syncHash();
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** Parse `#/session/:id/:tab
|
|
33
|
+
/** Parse `#/session/:id/:tab?machine=&agent=` → identity + tab, or null. */
|
|
34
34
|
export function parseHashPath(path) {
|
|
35
|
-
const
|
|
35
|
+
const [pathname, query = ''] = String(path || '').split('?', 2);
|
|
36
|
+
const m = pathname.match(/^\/session\/([^/]+)(?:\/([^/]+))?$/);
|
|
36
37
|
if (!m) return null;
|
|
37
|
-
|
|
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)
|
|
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
|
}
|
package/public/settings.js
CHANGED
|
@@ -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"
|
|
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
|
+
}
|
package/public/shell/boot.js
CHANGED
|
@@ -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 ||
|
|
102
|
-
s?.configDir ||
|
|
103
|
-
|
|
104
|
-
|
|
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 ||
|
|
154
|
-
s?.configDir ||
|
|
155
|
-
|
|
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
|
);
|
package/public/shell/layout.js
CHANGED
|
@@ -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
|
-
|
|
117
|
-
|
|
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
|
|
|
@@ -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
|
-
//
|
|
29
|
-
|
|
30
|
-
q.set('limit', '
|
|
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
|
}
|
|
@@ -274,7 +273,6 @@ export function renderProjectGroups(groups) {
|
|
|
274
273
|
} else {
|
|
275
274
|
open = forceOpen || key === topCwd || groups.length <= 3;
|
|
276
275
|
}
|
|
277
|
-
const latestTitle = g.latest?.display || g.latest?.snippet || '';
|
|
278
276
|
const machineTag = g.machineId
|
|
279
277
|
? `<span class="text-[9px] font-mono text-base-content/40 flex-shrink-0">@${esc(g.machineId)}</span>`
|
|
280
278
|
: '';
|
|
@@ -287,11 +285,6 @@ export function renderProjectGroups(groups) {
|
|
|
287
285
|
${machineTag}
|
|
288
286
|
<span class="text-[9px] text-base-content/30 flex-shrink-0">${formatTime(g.updatedAt)}</span>
|
|
289
287
|
</div>
|
|
290
|
-
<div class="flex items-center gap-1.5 mt-0.5 pl-4">
|
|
291
|
-
<span class="text-[10px] text-base-content/35 font-mono truncate flex-1" title="${esc(g.cwd || '')}">${esc(shortPath(g.cwd))}</span>
|
|
292
|
-
<span class="flex items-center gap-0.5 flex-shrink-0">${agentCountPills(g.agents)}</span>
|
|
293
|
-
</div>
|
|
294
|
-
${latestTitle && !open ? `<div class="pl-4 mt-0.5 text-[10px] text-base-content/45 truncate" title="${esc(latestTitle)}">↳ ${esc(latestTitle)}</div>` : ''}
|
|
295
288
|
</div>`;
|
|
296
289
|
const sessions = g.sessions.slice(0, 40);
|
|
297
290
|
const more = g.sessions.length > 40
|
|
@@ -362,7 +355,7 @@ export function syncSidebarChrome() {
|
|
|
362
355
|
status.textContent = 'Searching content…';
|
|
363
356
|
} else if (q && hitResults) {
|
|
364
357
|
status.classList.remove('hidden');
|
|
365
|
-
status.textContent = `${hitResults.length} hit${hitResults.length === 1 ? '' : 's'}
|
|
358
|
+
status.textContent = `${hitResults.length} hit${hitResults.length === 1 ? '' : 's'}${hits.deep === false ? '' : ' (incl. content)'}`;
|
|
366
359
|
} else if (q) {
|
|
367
360
|
status.classList.remove('hidden');
|
|
368
361
|
status.textContent = 'Local filter only — type to deep-search';
|
|
@@ -391,7 +384,7 @@ export async function runActivitySearch(q) {
|
|
|
391
384
|
try {
|
|
392
385
|
const params = new URLSearchParams({
|
|
393
386
|
q: query,
|
|
394
|
-
days:
|
|
387
|
+
days: '0',
|
|
395
388
|
limit: '50',
|
|
396
389
|
deep: '1',
|
|
397
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,12 +79,17 @@ 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
|
-
<button id="btn-meta-agent"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<button id="btn-
|
|
85
|
+
<button id="btn-meta-agent" class="btn btn-ghost btn-xs border border-primary/40 text-primary topbar-icon-btn" title="Meta Agent — 报告 / 问答 / VLM" aria-label="打开 Meta Agent">
|
|
86
|
+
<svg class="topbar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="m12 2 1.6 6.4L20 10l-6.4 1.6L12 18l-1.6-6.4L4 10l6.4-1.6L12 2Z"/><path d="m19 16 .7 2.3L22 19l-2.3.7L19 22l-.7-2.3L16 19l2.3-.7L19 16Z"/></svg><span class="topbar-icon-label">Agent</span>
|
|
87
|
+
</button>
|
|
88
|
+
<button id="btn-new-session" class="btn btn-ghost btn-xs border border-base-300 topbar-icon-btn" title="New session" aria-label="新建会话">
|
|
89
|
+
<svg class="topbar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M12 8v8M8 12h8"/></svg><span class="topbar-icon-label">New</span>
|
|
90
|
+
</button>
|
|
91
|
+
<button id="btn-theme" class="btn btn-ghost btn-xs border border-base-300 topbar-icon-btn" title="切换主题" aria-label="切换主题"><svg class="topbar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><path d="M20 15.5A8 8 0 0 1 8.5 4 8 8 0 1 0 20 15.5Z"/></svg></button>
|
|
92
|
+
<button id="btn-settings" class="btn btn-ghost btn-xs border border-base-300 topbar-icon-btn" title="设置" aria-label="设置"><svg class="topbar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.12 2.12-.06-.06A1.7 1.7 0 0 0 15.74 18a1.7 1.7 0 0 0-1.02 1.56V20h-3v-.44A1.7 1.7 0 0 0 10.7 18a1.7 1.7 0 0 0-1.88.34l-.06.06-2.12-2.12.06-.06A1.7 1.7 0 0 0 7.04 14.3 1.7 1.7 0 0 0 5.5 13.28H5v-3h.5a1.7 1.7 0 0 0 1.54-1.02A1.7 1.7 0 0 0 6.7 7.38l-.06-.06L8.76 5.2l.06.06A1.7 1.7 0 0 0 10.7 5a1.7 1.7 0 0 0 1.02-1.56V3h3v.44A1.7 1.7 0 0 0 15.74 5a1.7 1.7 0 0 0 1.88-.34l.06-.06 2.12 2.12-.06.06a1.7 1.7 0 0 0-.34 1.88 1.7 1.7 0 0 0 1.56 1.02h.5v3h-.5A1.7 1.7 0 0 0 19.4 15Z"/></svg></button>
|
|
88
93
|
<button id="btn-logout" class="btn btn-ghost btn-xs border border-base-300">Sign out</button>
|
|
89
94
|
</div>
|
|
90
95
|
</div>
|
|
@@ -134,29 +139,26 @@ export const AppShell = (opts = {}) => {
|
|
|
134
139
|
</div>
|
|
135
140
|
</div>
|
|
136
141
|
<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
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<
|
|
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>
|
|
142
|
+
<div class="px-2 py-1.5 border-b border-base-300 flex-shrink-0 relative">
|
|
143
|
+
<div class="flex gap-1.5">
|
|
144
|
+
<input id="session-search" type="text" placeholder="Search projects & recent work…"
|
|
145
|
+
class="input input-xs input-bordered flex-1 min-w-0 text-xs" autocomplete="off">
|
|
146
|
+
<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
147
|
</div>
|
|
153
|
-
<div id="
|
|
154
|
-
<
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
148
|
+
<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">
|
|
149
|
+
<div class="text-[10px] uppercase tracking-wide text-base-content/45 mb-1.5">Agents · multi-select</div>
|
|
150
|
+
<div class="grid grid-cols-2 gap-1 text-xs">
|
|
151
|
+
<label><input type="checkbox" data-agent-option="claude"> Claude</label>
|
|
152
|
+
<label><input type="checkbox" data-agent-option="codex"> Codex</label>
|
|
153
|
+
<label><input type="checkbox" data-agent-option="grok"> Grok</label>
|
|
154
|
+
<label><input type="checkbox" data-agent-option="gemini"> Gemini</label>
|
|
155
|
+
<label><input type="checkbox" data-agent-option="cursor"> Cursor</label>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="mt-2 pt-2 border-t border-base-300 flex gap-3 text-xs">
|
|
158
|
+
<label><input id="filter-favorites" type="checkbox"> Favorites</label>
|
|
159
|
+
<label><input id="filter-hidden" type="checkbox"> Hidden</label>
|
|
160
|
+
<button id="filter-clear" type="button" class="ml-auto text-primary">Clear</button>
|
|
161
|
+
</div>
|
|
160
162
|
</div>
|
|
161
163
|
<div id="search-status" class="hidden text-[10px] text-base-content/40 px-0.5"></div>
|
|
162
164
|
</div>
|
|
@@ -178,7 +180,7 @@ export const AppShell = (opts = {}) => {
|
|
|
178
180
|
<div id="welcome" class="${welcomeCls}">
|
|
179
181
|
<div class="max-w-4xl w-full text-center flex flex-col items-center gap-2 mt-2 md:mt-4">
|
|
180
182
|
<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>
|
|
183
|
+
<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
184
|
<div class="flex gap-2 mt-0.5">
|
|
183
185
|
<button id="btn-new-session-welcome" class="btn btn-primary btn-sm shadow-md">+ New Session</button>
|
|
184
186
|
<button id="btn-meta-agent-welcome" class="btn btn-outline btn-primary btn-sm">✦ Meta Agent</button>
|
|
@@ -272,6 +274,8 @@ export const AppShell = (opts = {}) => {
|
|
|
272
274
|
<option value="claude">Claude</option>
|
|
273
275
|
<option value="codex">Codex</option>
|
|
274
276
|
<option value="grok">Grok</option>
|
|
277
|
+
<option value="gemini">Gemini</option>
|
|
278
|
+
<option value="cursor">Cursor</option>
|
|
275
279
|
</select>
|
|
276
280
|
<select id="sel-convert-agent" class="select select-xs select-bordered text-xs font-semibold" title="Convert session to another agent">
|
|
277
281
|
<option value="" disabled selected>Convert to...</option>
|
|
@@ -406,6 +410,8 @@ export const AppShell = (opts = {}) => {
|
|
|
406
410
|
<option value="claude">Claude Code</option>
|
|
407
411
|
<option value="codex">OpenAI Codex</option>
|
|
408
412
|
<option value="grok">Grok</option>
|
|
413
|
+
<option value="gemini">Google Gemini (agy)</option>
|
|
414
|
+
<option value="cursor">Cursor Agent</option>
|
|
409
415
|
</select>
|
|
410
416
|
</label>
|
|
411
417
|
<label class="form-control" id="new-session-machine-wrap" style="display:none">
|
package/public/shell/util.js
CHANGED
|
@@ -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
|
}
|