claude-simple 0.1.0 → 0.2.1
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/client.js +31 -2
- package/dist/app.0.2.1.min.js +868 -0
- package/dist/highlight.min.js +448 -430
- package/dist/index.html +2 -2
- package/dist/style.0.2.1.min.css +1182 -0
- package/package.json +1 -1
- package/public/agent-panel.js +44 -0
- package/public/api.js +61 -10
- package/public/app.js +4 -0
- package/public/chat.js +338 -31
- package/public/files.js +22 -30
- package/public/git.js +21 -17
- package/public/icons/icon-180.png +0 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon-maskable-512.png +0 -0
- package/public/index.html +16 -3
- package/public/lib.js +7 -2
- package/public/manifest.webmanifest +36 -0
- package/public/offline.html +33 -0
- package/public/pwa.js +78 -0
- package/public/shell/boot.js +3 -0
- package/public/shell/hub.js +4 -2
- package/public/shell/notes.js +93 -31
- package/public/shell/session-list.js +29 -10
- package/public/shell/session-nav.js +7 -2
- package/public/shell/templates.js +11 -3
- package/public/shell.js +101 -8
- package/public/state.js +13 -2
- package/public/style.css +135 -3
- package/public/sw.js +104 -0
- package/public/terminal.js +22 -0
- package/public/wake-lock.js +45 -0
- package/server.js +5 -1
- package/src/agents/claude.js +108 -10
- package/src/agents/codex.js +1 -1
- package/src/agents/grok.js +40 -32
- package/src/agents/meta-agent.js +40 -1
- package/src/agents/stream-batch.js +62 -0
- package/src/app.js +312 -41
- package/src/auto-update.js +215 -0
- package/src/runtime/approvals.js +67 -0
- package/src/runtime/outbox.js +143 -0
- package/src/runtime/protocol.js +224 -0
- package/dist/app.0.1.0.min.js +0 -825
- package/dist/style.0.1.0.min.css +0 -1050
package/public/files.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// files.js — Files tab with upload / download / delete + text/office preview
|
|
2
|
-
import { effect, delegate, esc, $ } from './lib.js';
|
|
2
|
+
import { effect, asyncEffect, delegate, esc, $ } from './lib.js';
|
|
3
3
|
import { currentProject, currentTab, filesRoot, filesPath, viewingFile, ctx } from './state.js';
|
|
4
4
|
import { api, authHeaders, authQuery } from './api.js';
|
|
5
5
|
|
|
@@ -407,7 +407,7 @@ export function initFilesTab() {
|
|
|
407
407
|
${!isFile ? `<button id="btn-upload" class="btn btn-ghost btn-xs text-base-content/50 hover:text-base-content">↑ Upload</button>` : ''}`;
|
|
408
408
|
});
|
|
409
409
|
|
|
410
|
-
|
|
410
|
+
asyncEffect(async signal => {
|
|
411
411
|
const tab = currentTab.value;
|
|
412
412
|
const proj = currentProject.value;
|
|
413
413
|
// Prefer explicit filesRoot; else session cwd on the selected machine
|
|
@@ -416,40 +416,32 @@ export function initFilesTab() {
|
|
|
416
416
|
const dir = filesPath.value;
|
|
417
417
|
const area = $('files-area');
|
|
418
418
|
if (!area) return;
|
|
419
|
-
|
|
420
|
-
if (tab !== 'files') { return () => ctrl.abort(); }
|
|
419
|
+
if (tab !== 'files') return;
|
|
421
420
|
if (!root) {
|
|
422
421
|
area.innerHTML = '<div class="p-4 text-xs text-base-content/40">No path yet — open a session (uses that machine's cwd) or type an absolute path and press Go.</div>';
|
|
423
|
-
return
|
|
422
|
+
return;
|
|
424
423
|
}
|
|
425
424
|
const id = proj?.id || '_';
|
|
426
425
|
area.innerHTML = '<div class="p-4 text-xs text-base-content/40">Loading…</div>';
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
})
|
|
446
|
-
.catch(e => {
|
|
447
|
-
if (!ctrl.signal.aborted && e?.name !== 'AbortError') {
|
|
448
|
-
area.innerHTML = `<div class="p-4 text-xs text-error">${esc(e.message)}</div>`;
|
|
449
|
-
}
|
|
450
|
-
});
|
|
426
|
+
try {
|
|
427
|
+
if (file !== null) {
|
|
428
|
+
const data = await api('GET', `/api/projects/${id}/file?root=${encodeURIComponent(root)}&path=${encodeURIComponent(file)}`, undefined, signal);
|
|
429
|
+
if (!signal.aborted) await showFile(area, file, data, signal);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const data = await api('GET', `/api/projects/${id}/files?root=${encodeURIComponent(root)}&path=${encodeURIComponent(dir)}`, undefined, signal);
|
|
433
|
+
if (signal.aborted) return;
|
|
434
|
+
const list = Array.isArray(data) ? data : (Array.isArray(data?.files) ? data.files : null);
|
|
435
|
+
if (!list) {
|
|
436
|
+
area.innerHTML = `<div class="p-4 text-xs text-error">${esc(data?.error || 'Invalid directory listing')}</div>`;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
area.innerHTML = renderDir(list);
|
|
440
|
+
} catch (e) {
|
|
441
|
+
if (!signal.aborted && e?.name !== 'AbortError') {
|
|
442
|
+
area.innerHTML = `<div class="p-4 text-xs text-error">${esc(e.message)}</div>`;
|
|
443
|
+
}
|
|
451
444
|
}
|
|
452
|
-
return () => ctrl.abort();
|
|
453
445
|
});
|
|
454
446
|
|
|
455
447
|
delegate.on('click', '[data-dir-path]', (_, el) => { viewingFile.value = null; filesPath.value = el.dataset.dirPath; });
|
package/public/git.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// git.js — Git tab
|
|
2
|
-
import { signal,
|
|
2
|
+
import { signal, asyncEffect, delegate, esc, $ } from './lib.js';
|
|
3
3
|
import { currentProject, currentTab, gitRoot } from './state.js';
|
|
4
4
|
import { api } from './api.js';
|
|
5
5
|
|
|
@@ -63,41 +63,45 @@ function renderDiff(diff) {
|
|
|
63
63
|
|
|
64
64
|
export function initGitTab() {
|
|
65
65
|
// Git status panel
|
|
66
|
-
|
|
66
|
+
asyncEffect(async signal => {
|
|
67
67
|
const tab = currentTab.value;
|
|
68
68
|
const proj = currentProject.value;
|
|
69
69
|
const root = gitRoot.value || proj?.path;
|
|
70
70
|
const area = $('git-area');
|
|
71
71
|
if (!area) return;
|
|
72
|
-
|
|
73
|
-
if (tab !== 'git') return () => ctrl.abort();
|
|
72
|
+
if (tab !== 'git') return;
|
|
74
73
|
if (!root) {
|
|
75
74
|
area.innerHTML = '<div class="p-4 text-sm text-base-content/50">Enter a project root above, or open a session with a working directory.</div>';
|
|
76
|
-
return
|
|
75
|
+
return;
|
|
77
76
|
}
|
|
78
77
|
const id = proj?.id || '_';
|
|
79
78
|
area.innerHTML = '<div class="p-4 text-xs text-base-content/40">Loading…</div>';
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
try {
|
|
80
|
+
const data = await api('GET', `/api/projects/${id}/git/status?root=${encodeURIComponent(root)}`, undefined, signal);
|
|
81
|
+
if (!signal.aborted) area.innerHTML = gitRenderer(data);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (!signal.aborted && e?.name !== 'AbortError') {
|
|
84
|
+
area.innerHTML = `<div class="p-4 text-xs text-error">${esc(e?.message ?? String(e))}</div>`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
85
87
|
});
|
|
86
88
|
|
|
87
89
|
// Diff panel
|
|
88
|
-
|
|
90
|
+
asyncEffect(async signal => {
|
|
89
91
|
const proj = currentProject.value;
|
|
90
92
|
const root = gitRoot.value || proj?.path;
|
|
91
93
|
const staged = diffStaged.value;
|
|
92
94
|
const target = $('diff-content');
|
|
93
95
|
if (!target || !root) return;
|
|
94
96
|
const id = proj?.id || '_';
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
try {
|
|
98
|
+
const data = await api('GET', `/api/projects/${id}/git/diff?root=${encodeURIComponent(root)}${staged ? '&staged=true' : ''}`, undefined, signal);
|
|
99
|
+
if (!signal.aborted) target.innerHTML = data?.diff ? renderDiff(data.diff) : '<span class="text-base-content/40">No changes</span>';
|
|
100
|
+
} catch (e) {
|
|
101
|
+
if (!signal.aborted && e?.name !== 'AbortError') {
|
|
102
|
+
target.innerHTML = `<span class="text-error">${esc(e?.message ?? String(e))}</span>`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
101
105
|
});
|
|
102
106
|
|
|
103
107
|
delegate.on('click', '#diff-unstaged, #diff-staged', (_, el) => {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/public/index.html
CHANGED
|
@@ -45,14 +45,27 @@
|
|
|
45
45
|
display: none !important;
|
|
46
46
|
}
|
|
47
47
|
</style>
|
|
48
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
49
|
-
<
|
|
48
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover">
|
|
49
|
+
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
|
50
|
+
<meta name="theme-color" content="#0f172a">
|
|
51
|
+
<meta name="color-scheme" content="dark light">
|
|
52
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
53
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
54
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
55
|
+
<meta name="apple-mobile-web-app-title" content="Agent UI">
|
|
56
|
+
<meta name="application-name" content="Agent UI">
|
|
57
|
+
<meta name="description" content="Claude · Codex · Grok multi-machine agent console">
|
|
58
|
+
<link rel="manifest" href="/manifest.webmanifest">
|
|
59
|
+
<link rel="apple-touch-icon" href="/icons/icon-180.png">
|
|
60
|
+
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png">
|
|
61
|
+
<link rel="icon" type="image/png" sizes="512x512" href="/icons/icon-512.png">
|
|
62
|
+
<title>Agent UI</title>
|
|
50
63
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/daisyui.css" rel="stylesheet">
|
|
51
64
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
52
65
|
<script src="https://cdn.jsdelivr.net/npm/marked@9/marked.min.js"></script>
|
|
53
66
|
<link id="hljs-theme" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github-dark.min.css">
|
|
54
67
|
<script src="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/highlight.min.js"></script>
|
|
55
|
-
<link rel="stylesheet" href="style.css?v=
|
|
68
|
+
<link rel="stylesheet" href="style.css?v=safe-3">
|
|
56
69
|
</head>
|
|
57
70
|
<body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
|
|
58
71
|
<div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
|
package/public/lib.js
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
1
|
+
// Pinned, not @latest: this file is served straight to production browsers by
|
|
2
|
+
// the live hub (see docs/deployment.md) with no build step in between, so an
|
|
3
|
+
// unpinned @latest here means every push to the mini-react repo — tested or
|
|
4
|
+
// not — goes live immediately. Bump deliberately after verifying against
|
|
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';
|
|
7
|
+
// doc from https://cdn.jsdelivr.net/gh/forechoandlook/mini-react@v0.1.13/README.mini.md
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "/",
|
|
3
|
+
"name": "Agent UI",
|
|
4
|
+
"short_name": "Agent UI",
|
|
5
|
+
"description": "Claude · Codex · Grok multi-machine agent console",
|
|
6
|
+
"start_url": "/?source=pwa",
|
|
7
|
+
"scope": "/",
|
|
8
|
+
"display": "standalone",
|
|
9
|
+
"display_override": ["standalone", "minimal-ui", "browser"],
|
|
10
|
+
"orientation": "any",
|
|
11
|
+
"background_color": "#0f172a",
|
|
12
|
+
"theme_color": "#0f172a",
|
|
13
|
+
"lang": "zh-CN",
|
|
14
|
+
"dir": "ltr",
|
|
15
|
+
"categories": ["developer", "productivity", "utilities"],
|
|
16
|
+
"icons": [
|
|
17
|
+
{
|
|
18
|
+
"src": "/icons/icon-192.png",
|
|
19
|
+
"sizes": "192x192",
|
|
20
|
+
"type": "image/png",
|
|
21
|
+
"purpose": "any"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"src": "/icons/icon-512.png",
|
|
25
|
+
"sizes": "512x512",
|
|
26
|
+
"type": "image/png",
|
|
27
|
+
"purpose": "any"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"src": "/icons/icon-maskable-512.png",
|
|
31
|
+
"sizes": "512x512",
|
|
32
|
+
"type": "image/png",
|
|
33
|
+
"purpose": "maskable"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<meta name="theme-color" content="#0f172a">
|
|
7
|
+
<title>离线 · Agent UI</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
margin: 0; min-height: 100dvh; display: flex; align-items: center; justify-content: center;
|
|
11
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
12
|
+
background: #0f172a; color: #e2e8f0; padding: 1.5rem; text-align: center;
|
|
13
|
+
}
|
|
14
|
+
.card {
|
|
15
|
+
max-width: 22rem; padding: 1.5rem; border-radius: 1rem;
|
|
16
|
+
background: #1e293b; border: 1px solid #334155;
|
|
17
|
+
}
|
|
18
|
+
h1 { font-size: 1.1rem; margin: 0 0 .5rem; }
|
|
19
|
+
p { font-size: .9rem; color: #94a3b8; line-height: 1.5; margin: 0 0 1rem; }
|
|
20
|
+
button {
|
|
21
|
+
appearance: none; border: 0; border-radius: .5rem; padding: .55rem 1rem;
|
|
22
|
+
background: #6366f1; color: #fff; font-weight: 600; font-size: .875rem; cursor: pointer;
|
|
23
|
+
}
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<div class="card">
|
|
28
|
+
<h1>当前无网络</h1>
|
|
29
|
+
<p>Agent UI 需要连接 Hub 才能聊天。恢复网络后点下方按钮重试。</p>
|
|
30
|
+
<button type="button" onclick="location.href='/'">重新加载</button>
|
|
31
|
+
</div>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
package/public/pwa.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// pwa.js — register service worker + optional install affordance
|
|
2
|
+
// Does not touch WebSocket; SW only caches static shell assets.
|
|
3
|
+
|
|
4
|
+
let deferredInstall = null;
|
|
5
|
+
|
|
6
|
+
function isStandalone() {
|
|
7
|
+
return (
|
|
8
|
+
window.matchMedia('(display-mode: standalone)').matches
|
|
9
|
+
|| window.navigator.standalone === true
|
|
10
|
+
|| document.referrer.includes('android-app://')
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function showInstallButton() {
|
|
15
|
+
document.getElementById('btn-pwa-install')?.classList.remove('hidden');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function initPwa() {
|
|
19
|
+
if (isStandalone()) {
|
|
20
|
+
document.documentElement.classList.add('pwa-standalone');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
window.addEventListener('beforeinstallprompt', (e) => {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
deferredInstall = e;
|
|
26
|
+
document.dispatchEvent(new CustomEvent('pwa-installable'));
|
|
27
|
+
showInstallButton();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
window.addEventListener('appinstalled', () => {
|
|
31
|
+
deferredInstall = null;
|
|
32
|
+
document.getElementById('btn-pwa-install')?.classList.add('hidden');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// iOS Safari has no beforeinstallprompt — still offer a tip button on mobile.
|
|
36
|
+
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
|
|
37
|
+
|| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
38
|
+
if (!isStandalone() && (isIOS || /Android/i.test(navigator.userAgent))) {
|
|
39
|
+
// Defer until shell HTML exists
|
|
40
|
+
requestAnimationFrame(() => showInstallButton());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!('serviceWorker' in navigator)) return;
|
|
44
|
+
|
|
45
|
+
// Register after first paint so install doesn't compete with boot.
|
|
46
|
+
const register = () => {
|
|
47
|
+
navigator.serviceWorker.register('/sw.js', { scope: '/' })
|
|
48
|
+
.then((reg) => {
|
|
49
|
+
reg.addEventListener('updatefound', () => {
|
|
50
|
+
const sw = reg.installing;
|
|
51
|
+
if (!sw) return;
|
|
52
|
+
sw.addEventListener('statechange', () => {
|
|
53
|
+
if (sw.state === 'installed' && navigator.serviceWorker.controller) {
|
|
54
|
+
console.info('[pwa] New version ready — reload to apply');
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
})
|
|
59
|
+
.catch((err) => console.warn('[pwa] SW register failed', err));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (document.readyState === 'complete') setTimeout(register, 0);
|
|
63
|
+
else window.addEventListener('load', () => setTimeout(register, 0));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Trigger native install sheet when available (Chrome/Edge Android, some desktop). */
|
|
67
|
+
export async function promptPwaInstall() {
|
|
68
|
+
if (!deferredInstall) return false;
|
|
69
|
+
deferredInstall.prompt();
|
|
70
|
+
const choice = await deferredInstall.userChoice.catch(() => null);
|
|
71
|
+
deferredInstall = null;
|
|
72
|
+
document.getElementById('btn-pwa-install')?.classList.add('hidden');
|
|
73
|
+
return choice?.outcome === 'accepted';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function canInstallPwa() {
|
|
77
|
+
return !!deferredInstall;
|
|
78
|
+
}
|
package/public/shell/boot.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getCachedSessions } from '../cache.js';
|
|
|
8
8
|
import { getLastSessionContext } from './session-context.js';
|
|
9
9
|
import { loadAllSessions, loadWorkspaces, loadAgentsMeta } from './session-list.js';
|
|
10
10
|
import { loadSessionMetaMap } from './notes.js';
|
|
11
|
+
import { loadMetaAgentBoot } from '../agent-panel.js';
|
|
11
12
|
import { resumeSession } from './session-nav.js';
|
|
12
13
|
import {
|
|
13
14
|
refreshMachinesList, hideMachinePicker, showMachinePicker,
|
|
@@ -59,6 +60,7 @@ export async function showApp(opts = {}) {
|
|
|
59
60
|
setSelectedMachine(remembered);
|
|
60
61
|
hideMachinePicker();
|
|
61
62
|
hubMachineReady.value = true;
|
|
63
|
+
loadMetaAgentBoot();
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
// Machines list: refresh in background; only block picker if no remembered id
|
|
@@ -128,6 +130,7 @@ export async function showApp(opts = {}) {
|
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
// Standalone: cache-first sessions + live model lists, then deep link
|
|
133
|
+
loadMetaAgentBoot();
|
|
131
134
|
const dataP = Promise.all([
|
|
132
135
|
loadAllSessions({ waitFresh: false }),
|
|
133
136
|
loadWorkspaces({ waitFresh: false }),
|
package/public/shell/hub.js
CHANGED
|
@@ -5,8 +5,9 @@ import {
|
|
|
5
5
|
currentProject, sessionFilter, viewingFile, ctx,
|
|
6
6
|
} from '../state.js';
|
|
7
7
|
import { api, probeHub } from '../api.js';
|
|
8
|
-
import { loadAllSessions, loadWorkspaces } from './session-list.js';
|
|
8
|
+
import { loadAllSessions, loadWorkspaces, loadAgentsMeta } from './session-list.js';
|
|
9
9
|
import { loadSessionMetaMap } from './notes.js';
|
|
10
|
+
import { loadMetaAgentBoot } from '../agent-panel.js';
|
|
10
11
|
// goHome imported dynamically in enterMachine to avoid circular deps
|
|
11
12
|
|
|
12
13
|
export async function refreshMachinesList() {
|
|
@@ -186,7 +187,8 @@ export async function enterMachine(machineId, { forceReload = false } = {}) {
|
|
|
186
187
|
bar.textContent = `Machine · ${machineId}`;
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
|
|
190
|
+
loadMetaAgentBoot();
|
|
191
|
+
await Promise.all([loadAllSessions(), loadWorkspaces(), loadSessionMetaMap(), loadAgentsMeta().catch(() => {})]);
|
|
190
192
|
hubMachineReady.value = true;
|
|
191
193
|
}
|
|
192
194
|
|
package/public/shell/notes.js
CHANGED
|
@@ -103,6 +103,7 @@ export function renderSessionNotesBar() {
|
|
|
103
103
|
const meta = getSessionMeta(agent, sid);
|
|
104
104
|
const note = (meta.notes || '').trim();
|
|
105
105
|
const fav = !!meta.favorite;
|
|
106
|
+
const hid = !!meta.hidden;
|
|
106
107
|
const title = (meta.title || '').trim();
|
|
107
108
|
const showTitle = title || sid.slice(0, 8);
|
|
108
109
|
bar.style.display = 'flex';
|
|
@@ -112,9 +113,15 @@ export function renderSessionNotesBar() {
|
|
|
112
113
|
data-fav-session-id="${esc(sid)}" data-fav-session-agent="${esc(agent)}"
|
|
113
114
|
title="${fav ? 'Unfavorite' : 'Favorite'}">${fav ? '★' : '☆'}</button>
|
|
114
115
|
<span id="session-title-display"
|
|
115
|
-
class="session-title-inline max-w-[40%] truncate font-semibold text-base-content/85
|
|
116
|
-
title="
|
|
117
|
-
|
|
116
|
+
class="session-title-inline max-w-[40%] truncate font-semibold text-base-content/85"
|
|
117
|
+
title="${esc(showTitle)}">${esc(showTitle)}</span>
|
|
118
|
+
<button type="button" class="session-rename-btn flex-shrink-0 opacity-70 hover:opacity-100"
|
|
119
|
+
data-rename-bar="1" title="Rename session">✎</button>
|
|
120
|
+
<button type="button" class="session-hide-btn flex-shrink-0 ${hid ? 'is-hidden' : ''}"
|
|
121
|
+
data-hide-toggle="1"
|
|
122
|
+
data-hide-session-id="${esc(sid)}"
|
|
123
|
+
data-hide-session-agent="${esc(agent)}"
|
|
124
|
+
title="${hid ? 'Unhide session' : 'Hide session'}">${hid ? '👁' : '🙈'}</button>
|
|
118
125
|
<span class="font-bold text-base-content/40 uppercase flex-shrink-0">Note:</span>
|
|
119
126
|
<span class="truncate text-base-content/70 ${note ? '' : 'italic text-base-content/40'}"
|
|
120
127
|
id="session-notes-text" title="${esc(note || 'Add a note for this session')}">${esc(note || 'No session note')}</span>
|
|
@@ -139,17 +146,43 @@ export function renderSessionNotesEditMode() {
|
|
|
139
146
|
input?.select();
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
function emptyMeta() {
|
|
150
|
+
return { favorite: false, notes: '', title: '', hidden: false };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function mergeMeta(prev, patch) {
|
|
154
|
+
return {
|
|
155
|
+
favorite: patch.favorite !== undefined ? Boolean(patch.favorite) : Boolean(prev.favorite),
|
|
156
|
+
notes: patch.notes !== undefined ? String(patch.notes || '') : String(prev.notes || ''),
|
|
157
|
+
title: patch.title !== undefined ? String(patch.title || '').trim() : String(prev.title || '').trim(),
|
|
158
|
+
hidden: patch.hidden !== undefined ? Boolean(patch.hidden) : Boolean(prev.hidden),
|
|
159
|
+
updatedAt: Date.now(),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isMetaEmpty(m) {
|
|
164
|
+
return !m?.favorite && !m?.hidden && !(m?.notes || '').trim() && !(m?.title || '').trim();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function toggleSessionFavorite(sessionId, agent, { machineId } = {}) {
|
|
168
|
+
const key = metaKey(agent, sessionId, machineId);
|
|
169
|
+
const prev = sessionMetaMap.peek()?.[key] || emptyMeta();
|
|
145
170
|
const nextFav = !prev.favorite;
|
|
146
|
-
// optimistic
|
|
147
171
|
sessionMetaMap.value = {
|
|
148
172
|
...sessionMetaMap.peek(),
|
|
149
173
|
[key]: { ...prev, favorite: nextFav, updatedAt: Date.now() },
|
|
150
174
|
};
|
|
151
175
|
try {
|
|
152
|
-
await api('PUT', '/api/sessions/meta', { sessionId, agent, favorite: nextFav });
|
|
176
|
+
const res = await api('PUT', '/api/sessions/meta', { sessionId, agent, favorite: nextFav });
|
|
177
|
+
sessionMetaMap.value = {
|
|
178
|
+
...sessionMetaMap.peek(),
|
|
179
|
+
[key]: mergeMeta(prev, {
|
|
180
|
+
favorite: res.favorite ?? nextFav,
|
|
181
|
+
notes: res.notes,
|
|
182
|
+
title: res.title,
|
|
183
|
+
hidden: res.hidden,
|
|
184
|
+
}),
|
|
185
|
+
};
|
|
153
186
|
} catch (e) {
|
|
154
187
|
sessionMetaMap.value = { ...sessionMetaMap.peek(), [key]: prev };
|
|
155
188
|
alert(`Favorite failed: ${e.message}`);
|
|
@@ -157,19 +190,53 @@ export async function toggleSessionFavorite(sessionId, agent) {
|
|
|
157
190
|
if (ctx.sessionId === sessionId) renderSessionNotesBar();
|
|
158
191
|
}
|
|
159
192
|
|
|
193
|
+
/** Hide / unhide a session (soft archive in the sidebar). */
|
|
194
|
+
export async function toggleSessionHidden(sessionId, agent, { machineId } = {}) {
|
|
195
|
+
if (!sessionId) return false;
|
|
196
|
+
const key = metaKey(agent, sessionId, machineId);
|
|
197
|
+
const prev = sessionMetaMap.peek()?.[key] || emptyMeta();
|
|
198
|
+
const nextHidden = !prev.hidden;
|
|
199
|
+
sessionMetaMap.value = {
|
|
200
|
+
...sessionMetaMap.peek(),
|
|
201
|
+
[key]: { ...prev, hidden: nextHidden, updatedAt: Date.now() },
|
|
202
|
+
};
|
|
203
|
+
try {
|
|
204
|
+
const res = await api('PUT', '/api/sessions/meta', { sessionId, agent, hidden: nextHidden });
|
|
205
|
+
const next = mergeMeta(prev, {
|
|
206
|
+
favorite: res.favorite,
|
|
207
|
+
notes: res.notes,
|
|
208
|
+
title: res.title,
|
|
209
|
+
hidden: res.hidden ?? nextHidden,
|
|
210
|
+
});
|
|
211
|
+
if (isMetaEmpty(next)) {
|
|
212
|
+
const map = { ...sessionMetaMap.peek() };
|
|
213
|
+
delete map[key];
|
|
214
|
+
sessionMetaMap.value = map;
|
|
215
|
+
} else {
|
|
216
|
+
sessionMetaMap.value = { ...sessionMetaMap.peek(), [key]: next };
|
|
217
|
+
}
|
|
218
|
+
} catch (e) {
|
|
219
|
+
sessionMetaMap.value = { ...sessionMetaMap.peek(), [key]: prev };
|
|
220
|
+
alert(`Hide failed: ${e.message}`);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
if (ctx.sessionId === sessionId) renderSessionNotesBar();
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
160
227
|
export async function saveSessionNotes(sessionId, agent, notes) {
|
|
161
228
|
const key = metaKey(agent, sessionId);
|
|
162
|
-
const prev = sessionMetaMap.peek()?.[key] ||
|
|
229
|
+
const prev = sessionMetaMap.peek()?.[key] || emptyMeta();
|
|
163
230
|
try {
|
|
164
231
|
const res = await api('PUT', '/api/sessions/meta', { sessionId, agent, notes });
|
|
165
232
|
sessionMetaMap.value = {
|
|
166
233
|
...sessionMetaMap.peek(),
|
|
167
|
-
[key]: {
|
|
168
|
-
favorite: res.favorite
|
|
234
|
+
[key]: mergeMeta(prev, {
|
|
235
|
+
favorite: res.favorite,
|
|
169
236
|
notes: res.notes ?? notes,
|
|
170
|
-
title: res.title
|
|
171
|
-
|
|
172
|
-
},
|
|
237
|
+
title: res.title,
|
|
238
|
+
hidden: res.hidden,
|
|
239
|
+
}),
|
|
173
240
|
};
|
|
174
241
|
} catch (e) {
|
|
175
242
|
alert(`Save note failed: ${e.message}`);
|
|
@@ -185,31 +252,26 @@ export async function saveSessionNotes(sessionId, agent, notes) {
|
|
|
185
252
|
export async function renameSession(sessionId, agent, title, { machineId } = {}) {
|
|
186
253
|
if (!sessionId) return false;
|
|
187
254
|
const key = metaKey(agent, sessionId, machineId);
|
|
188
|
-
const prev = sessionMetaMap.peek()?.[key] ||
|
|
255
|
+
const prev = sessionMetaMap.peek()?.[key] || emptyMeta();
|
|
189
256
|
const nextTitle = String(title || '').trim().slice(0, 200);
|
|
190
|
-
// optimistic
|
|
191
257
|
sessionMetaMap.value = {
|
|
192
258
|
...sessionMetaMap.peek(),
|
|
193
259
|
[key]: { ...prev, title: nextTitle, updatedAt: Date.now() },
|
|
194
260
|
};
|
|
195
261
|
try {
|
|
196
262
|
const res = await api('PUT', '/api/sessions/meta', { sessionId, agent, title: nextTitle });
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
},
|
|
205
|
-
};
|
|
206
|
-
// If server dropped empty entry, keep local clear
|
|
207
|
-
if (!res.title && !res.favorite && !res.notes) {
|
|
263
|
+
const next = mergeMeta(prev, {
|
|
264
|
+
favorite: res.favorite,
|
|
265
|
+
notes: res.notes,
|
|
266
|
+
title: res.title !== undefined ? res.title : nextTitle,
|
|
267
|
+
hidden: res.hidden,
|
|
268
|
+
});
|
|
269
|
+
if (isMetaEmpty(next)) {
|
|
208
270
|
const map = { ...sessionMetaMap.peek() };
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
271
|
+
delete map[key];
|
|
272
|
+
sessionMetaMap.value = map;
|
|
273
|
+
} else {
|
|
274
|
+
sessionMetaMap.value = { ...sessionMetaMap.peek(), [key]: next };
|
|
213
275
|
}
|
|
214
276
|
} catch (e) {
|
|
215
277
|
sessionMetaMap.value = { ...sessionMetaMap.peek(), [key]: prev };
|