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/README.md +13 -5
- package/dist/app.0.2.9.min.js +1084 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.9.min.css +1598 -0
- package/package.json +11 -3
- package/public/chat.js +14 -11
- package/public/files.js +5 -5
- package/public/index.html +4 -5
- 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 +22 -16
- package/public/shell/templates.js +26 -24
- package/public/shell/util.js +3 -1
- package/public/shell.js +54 -13
- package/public/state.js +50 -4
- 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/grok.js +300 -37
- package/src/agents/meta-agent.js +9 -5
- package/src/agents/models.js +114 -3
- package/src/agents/sessions.js +258 -14
- package/src/app.js +131 -9
- package/src/mcp.js +275 -0
- package/dist/app.0.2.8.min.js +0 -1080
- package/dist/style.0.2.8.min.css +0 -1598
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-simple",
|
|
3
|
-
"version": "0.2.
|
|
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
|
-
"
|
|
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
|
@@ -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
|
|
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
|
-
//
|
|
777
|
-
//
|
|
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
|
-
|
|
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('
|
|
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,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="
|
|
82
|
-
<script src="
|
|
83
|
-
<
|
|
84
|
-
<
|
|
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 '
|
|
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
|
|
|
@@ -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
|
-
//
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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'}
|
|
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:
|
|
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
|
|
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>
|
|
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="
|
|
154
|
-
<
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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">
|
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
|
}
|