claude-simple 0.2.1 → 0.2.3
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 +58 -2
- package/dist/app.0.2.3.min.js +924 -0
- package/dist/index.html +2 -2
- package/dist/{style.0.2.1.min.css → style.0.2.3.min.css} +129 -4
- package/gateway.js +7 -2
- package/package.json +1 -1
- package/public/agent-panel.js +10 -1
- package/public/chat.js +9 -1
- package/public/files.js +60 -21
- package/public/index.html +2 -2
- package/public/shell/dashboard.js +125 -4
- package/public/shell/notes.js +24 -9
- package/public/shell/session-nav.js +53 -21
- package/public/shell/templates.js +34 -7
- package/public/shell.js +42 -3
- package/public/style.css +128 -3
- package/public/sw.js +2 -2
- package/src/app.js +27 -9
- package/src/runtime/protocol.js +2 -2
- package/dist/app.0.2.1.min.js +0 -868
package/gateway.js
CHANGED
|
@@ -229,11 +229,16 @@ app.all('/machine/:machineId/*', async (req, res) => {
|
|
|
229
229
|
}, body);
|
|
230
230
|
|
|
231
231
|
// Don't forward hop-by-hop headers
|
|
232
|
-
const skip = new Set(['transfer-encoding', 'connection', 'keep-alive']);
|
|
232
|
+
const skip = new Set(['transfer-encoding', 'connection', 'keep-alive', 'content-length']);
|
|
233
233
|
Object.entries(result.headers || {}).forEach(([k, v]) => {
|
|
234
234
|
if (!skip.has(k.toLowerCase())) res.setHeader(k, v);
|
|
235
235
|
});
|
|
236
|
-
|
|
236
|
+
// Edge sends binary downloads as base64 (see client.js encoding:'base64').
|
|
237
|
+
let body = result.body ?? '';
|
|
238
|
+
if (String(result.encoding || '').toLowerCase() === 'base64' && typeof body === 'string') {
|
|
239
|
+
body = Buffer.from(body, 'base64');
|
|
240
|
+
}
|
|
241
|
+
res.status(result.status).send(body);
|
|
237
242
|
} catch (e) {
|
|
238
243
|
res.status(502).json({ error: e.message });
|
|
239
244
|
}
|
package/package.json
CHANGED
package/public/agent-panel.js
CHANGED
|
@@ -118,7 +118,16 @@ function panelHTML() {
|
|
|
118
118
|
</label>
|
|
119
119
|
<textarea id="ma-input" rows="1" placeholder="继续提问… (⌘↵ 发送) · 历史会话会带着上下文"
|
|
120
120
|
class="textarea textarea-bordered flex-1 text-sm resize-none leading-relaxed"
|
|
121
|
-
style="min-height:40px;max-height:120px"
|
|
121
|
+
style="min-height:40px;max-height:120px"
|
|
122
|
+
name="meta-agent-message"
|
|
123
|
+
autocomplete="off"
|
|
124
|
+
autocorrect="off"
|
|
125
|
+
autocapitalize="off"
|
|
126
|
+
spellcheck="false"
|
|
127
|
+
data-1p-ignore="true"
|
|
128
|
+
data-lpignore="true"
|
|
129
|
+
data-form-type="other"
|
|
130
|
+
data-bwignore="true"></textarea>
|
|
122
131
|
<button type="button" id="ma-send" class="btn btn-primary btn-sm" style="height:40px">发送</button>
|
|
123
132
|
</div>
|
|
124
133
|
</div>
|
package/public/chat.js
CHANGED
|
@@ -788,9 +788,17 @@ function updateInputMode(value) {
|
|
|
788
788
|
input.placeholder = isShell ? 'Shell command (without !)…' : `Ask ${label}… or !cmd for shell`;
|
|
789
789
|
}
|
|
790
790
|
|
|
791
|
+
function chatInputMaxHeight() {
|
|
792
|
+
// Keep in sync with #chat-input max-height in style.css (mobile vs desktop).
|
|
793
|
+
if (typeof window !== 'undefined' && window.matchMedia('(max-width: 640px)').matches) {
|
|
794
|
+
return 96;
|
|
795
|
+
}
|
|
796
|
+
return 140;
|
|
797
|
+
}
|
|
798
|
+
|
|
791
799
|
function autoResize(el) {
|
|
792
800
|
el.style.height = 'auto';
|
|
793
|
-
el.style.height = Math.min(el.scrollHeight,
|
|
801
|
+
el.style.height = Math.min(el.scrollHeight, chatInputMaxHeight()) + 'px';
|
|
794
802
|
}
|
|
795
803
|
|
|
796
804
|
// ── Message rendering ─────────────────────────────────────────────────────────
|
package/public/files.js
CHANGED
|
@@ -111,7 +111,7 @@ function fileChrome(path, size, kindLabel) {
|
|
|
111
111
|
return `
|
|
112
112
|
<div class="flex items-center gap-2 px-4 py-1.5 border-b border-base-300 flex-shrink-0 bg-base-200">
|
|
113
113
|
<span class="text-xs text-base-content/40 flex-1 truncate">${esc(path)} · ${esc(sizeStr)}${kindLabel ? ` · <span class="text-primary/70">${esc(kindLabel)}</span>` : ''}</span>
|
|
114
|
-
<button class="btn btn-ghost btn-xs" data-download-path="${esc(path)}">↓ Download</button>
|
|
114
|
+
<button type="button" class="btn btn-ghost btn-xs" data-download-path="${esc(path)}">↓ Download</button>
|
|
115
115
|
</div>`;
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -127,10 +127,10 @@ function renderDir(files) {
|
|
|
127
127
|
<span class="cursor-pointer flex-shrink-0">${f.isDir ? '📁' : fileIcon(f.name)}</span>
|
|
128
128
|
<span class="text-sm flex-1 truncate cursor-pointer ${f.isDir ? 'text-primary' : ''}">${esc(f.name)}</span>
|
|
129
129
|
${!f.isDir ? `
|
|
130
|
-
<button class="
|
|
131
|
-
data-download-path="${esc(f.path)}" title="Download">↓</button>
|
|
132
|
-
<button class="
|
|
133
|
-
data-delete-path="${esc(f.path)}" title="Delete">✕</button>` : ''}
|
|
130
|
+
<button type="button" class="file-row-action btn btn-ghost btn-xs px-1.5 text-base-content/50 hover:text-base-content"
|
|
131
|
+
data-download-path="${esc(f.path)}" title="Download" aria-label="Download">↓</button>
|
|
132
|
+
<button type="button" class="file-row-action btn btn-ghost btn-xs px-1.5 text-error/60 hover:text-error"
|
|
133
|
+
data-delete-path="${esc(f.path)}" title="Delete" aria-label="Delete">✕</button>` : ''}
|
|
134
134
|
</div>`).join('');
|
|
135
135
|
}
|
|
136
136
|
|
|
@@ -343,19 +343,52 @@ function triggerUpload() {
|
|
|
343
343
|
input.click();
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
function downloadFile(proj, filePath) {
|
|
346
|
+
async function downloadFile(proj, filePath) {
|
|
347
347
|
const root = filesRoot.peek() || proj?.path || '';
|
|
348
|
+
if (!root) {
|
|
349
|
+
alert('No path selected — set root first');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
348
352
|
const id = proj?.id || '_';
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
});
|
|
353
|
+
const name = (filePath || '').split('/').pop() || 'download';
|
|
354
|
+
// Prefer Authorization header (no token in URL / logs). authQuery kept as
|
|
355
|
+
// fallback for environments that only support plain navigations.
|
|
356
|
+
const qs = new URLSearchParams({ root, path: filePath, download: 'true' });
|
|
354
357
|
const url = `/api/projects/${id}/file?${qs}`;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
358
|
+
try {
|
|
359
|
+
const res = await fetch(url, { headers: authHeaders(), credentials: 'same-origin' });
|
|
360
|
+
if (!res.ok) {
|
|
361
|
+
const d = await res.json().catch(() => ({}));
|
|
362
|
+
throw new Error(d.error || `HTTP ${res.status}`);
|
|
363
|
+
}
|
|
364
|
+
const blob = await res.blob();
|
|
365
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
366
|
+
const a = document.createElement('a');
|
|
367
|
+
a.href = objectUrl;
|
|
368
|
+
a.download = name;
|
|
369
|
+
a.rel = 'noopener';
|
|
370
|
+
a.style.display = 'none';
|
|
371
|
+
document.body.appendChild(a);
|
|
372
|
+
a.click();
|
|
373
|
+
a.remove();
|
|
374
|
+
// iOS may need the URL briefly; revoke after a beat.
|
|
375
|
+
setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);
|
|
376
|
+
} catch (e) {
|
|
377
|
+
// Last resort: open with token query (works when fetch is blocked, e.g. some WebViews).
|
|
378
|
+
try {
|
|
379
|
+
const fallback = `/api/projects/${id}/file?${authQuery({ root, path: filePath, download: 'true' })}`;
|
|
380
|
+
const a = document.createElement('a');
|
|
381
|
+
a.href = fallback;
|
|
382
|
+
a.download = name;
|
|
383
|
+
a.rel = 'noopener';
|
|
384
|
+
a.target = '_blank';
|
|
385
|
+
document.body.appendChild(a);
|
|
386
|
+
a.click();
|
|
387
|
+
a.remove();
|
|
388
|
+
} catch {
|
|
389
|
+
alert(`Download failed: ${e.message}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
359
392
|
}
|
|
360
393
|
|
|
361
394
|
async function deleteFile(proj, filePath) {
|
|
@@ -447,13 +480,19 @@ export function initFilesTab() {
|
|
|
447
480
|
delegate.on('click', '[data-dir-path]', (_, el) => { viewingFile.value = null; filesPath.value = el.dataset.dirPath; });
|
|
448
481
|
delegate.on('click', '[data-file-path]', (_, el) => { viewingFile.value = el.dataset.filePath; });
|
|
449
482
|
delegate.on('click', '#btn-upload', () => triggerUpload());
|
|
450
|
-
delegate.on('click', '[data-download-path]', (
|
|
451
|
-
|
|
452
|
-
|
|
483
|
+
delegate.on('click', '[data-download-path]', (e, el) => {
|
|
484
|
+
e.preventDefault();
|
|
485
|
+
e.stopPropagation();
|
|
486
|
+
const path = el.dataset.downloadPath;
|
|
487
|
+
if (!path) return;
|
|
488
|
+
downloadFile(currentProject.peek(), path);
|
|
453
489
|
});
|
|
454
|
-
delegate.on('click', '[data-delete-path]', (
|
|
455
|
-
|
|
456
|
-
|
|
490
|
+
delegate.on('click', '[data-delete-path]', (e, el) => {
|
|
491
|
+
e.preventDefault();
|
|
492
|
+
e.stopPropagation();
|
|
493
|
+
const path = el.dataset.deletePath;
|
|
494
|
+
if (!path) return;
|
|
495
|
+
deleteFile(currentProject.peek(), path);
|
|
457
496
|
});
|
|
458
497
|
// Excel sheet tabs
|
|
459
498
|
delegate.on('click', '.office-sheet-tab', (_, el) => {
|
package/public/index.html
CHANGED
|
@@ -65,10 +65,10 @@
|
|
|
65
65
|
<script src="https://cdn.jsdelivr.net/npm/marked@9/marked.min.js"></script>
|
|
66
66
|
<link id="hljs-theme" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github-dark.min.css">
|
|
67
67
|
<script src="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/highlight.min.js"></script>
|
|
68
|
-
<link rel="stylesheet" href="style.css?v=
|
|
68
|
+
<link rel="stylesheet" href="style.css?v=ui-9">
|
|
69
69
|
</head>
|
|
70
70
|
<body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
|
|
71
71
|
<div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
|
|
72
|
-
<script type="module" src="app.js"></script>
|
|
72
|
+
<script type="module" src="app.js?v=ui-9"></script>
|
|
73
73
|
</body>
|
|
74
74
|
</html>
|
|
@@ -224,14 +224,21 @@ export function showSelectedDaySessions(sessions) {
|
|
|
224
224
|
}).join('');
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/** How many sessions in the topbar “最近” quick-jump menu. */
|
|
228
|
+
export const RECENT_MENU_LIMIT = 15;
|
|
229
|
+
|
|
230
|
+
function sortRecentSessions(sessions, limit) {
|
|
231
|
+
return [...asSessionArray(sessions)]
|
|
232
|
+
.filter(s => s && s.sessionId)
|
|
233
|
+
.sort((a, b) => (coerceTs(b.updatedAt) || 0) - (coerceTs(a.updatedAt) || 0))
|
|
234
|
+
.slice(0, limit);
|
|
235
|
+
}
|
|
236
|
+
|
|
227
237
|
export function updateRecentSessionsList(sessions) {
|
|
228
238
|
const container = $('recent-sessions-list');
|
|
229
239
|
if (!container) return;
|
|
230
240
|
|
|
231
|
-
|
|
232
|
-
const recent = [...sessions]
|
|
233
|
-
.sort((a, b) => (coerceTs(b.updatedAt) || 0) - (coerceTs(a.updatedAt) || 0))
|
|
234
|
-
.slice(0, 6);
|
|
241
|
+
const recent = sortRecentSessions(sessions, 6);
|
|
235
242
|
|
|
236
243
|
if (!recent.length) {
|
|
237
244
|
container.innerHTML = `<div class="col-span-full text-center text-xs text-base-content/40 py-4">No recent sessions</div>`;
|
|
@@ -258,6 +265,120 @@ export function updateRecentSessionsList(sessions) {
|
|
|
258
265
|
}).join('');
|
|
259
266
|
}
|
|
260
267
|
|
|
268
|
+
export function closeRecentMenu() {
|
|
269
|
+
const menu = $('recent-menu');
|
|
270
|
+
if (menu) {
|
|
271
|
+
menu.classList.add('hidden');
|
|
272
|
+
menu.classList.remove('recent-menu-open');
|
|
273
|
+
// clear inline placement for next open
|
|
274
|
+
menu.style.top = '';
|
|
275
|
+
menu.style.left = '';
|
|
276
|
+
menu.style.right = '';
|
|
277
|
+
menu.style.width = '';
|
|
278
|
+
menu.style.maxHeight = '';
|
|
279
|
+
}
|
|
280
|
+
$('recent-menu-backdrop')?.classList.add('hidden');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Place the panel under the topbar with position:fixed so it is never clipped
|
|
285
|
+
* by #app / #topbar overflow (the previous top-full + fixed combo put it
|
|
286
|
+
* below the viewport on phones — looked like “no response”).
|
|
287
|
+
*/
|
|
288
|
+
export function positionRecentMenu() {
|
|
289
|
+
const menu = $('recent-menu');
|
|
290
|
+
const topbar = $('topbar');
|
|
291
|
+
if (!menu || !topbar) return;
|
|
292
|
+
const rect = topbar.getBoundingClientRect();
|
|
293
|
+
const gap = 6;
|
|
294
|
+
const top = Math.round(rect.bottom + gap);
|
|
295
|
+
const side = window.matchMedia('(max-width: 640px)').matches ? 8 : null;
|
|
296
|
+
menu.style.top = `${top}px`;
|
|
297
|
+
if (side != null) {
|
|
298
|
+
menu.style.left = `${side}px`;
|
|
299
|
+
menu.style.right = `${side}px`;
|
|
300
|
+
} else {
|
|
301
|
+
// desktop: align under the “最近” button when possible
|
|
302
|
+
const btn = $('btn-recent-sessions');
|
|
303
|
+
const br = btn?.getBoundingClientRect();
|
|
304
|
+
if (br) {
|
|
305
|
+
const width = Math.min(320, window.innerWidth - 16);
|
|
306
|
+
let left = br.left;
|
|
307
|
+
if (left + width > window.innerWidth - 8) left = window.innerWidth - width - 8;
|
|
308
|
+
if (left < 8) left = 8;
|
|
309
|
+
menu.style.left = `${Math.round(left)}px`;
|
|
310
|
+
menu.style.right = 'auto';
|
|
311
|
+
menu.style.width = `${width}px`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const maxH = Math.max(160, window.innerHeight - top - 12);
|
|
315
|
+
menu.style.maxHeight = `${Math.min(maxH, Math.round(window.innerHeight * 0.7))}px`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function openRecentMenu(sessions) {
|
|
319
|
+
const menu = $('recent-menu');
|
|
320
|
+
if (!menu) return;
|
|
321
|
+
renderRecentSessionsMenu(sessions ?? getDashboardSessions());
|
|
322
|
+
menu.classList.remove('hidden');
|
|
323
|
+
menu.classList.add('recent-menu-open');
|
|
324
|
+
positionRecentMenu();
|
|
325
|
+
$('recent-menu-backdrop')?.classList.remove('hidden');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function toggleRecentMenu(sessions) {
|
|
329
|
+
const menu = $('recent-menu');
|
|
330
|
+
if (!menu) return;
|
|
331
|
+
if (menu.classList.contains('hidden')) openRecentMenu(sessions);
|
|
332
|
+
else closeRecentMenu();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Topbar quick-jump: last N sessions by updatedAt. */
|
|
336
|
+
export function renderRecentSessionsMenu(sessions) {
|
|
337
|
+
const listEl = $('recent-menu-list');
|
|
338
|
+
const emptyEl = $('recent-menu-empty');
|
|
339
|
+
const countEl = $('recent-menu-count');
|
|
340
|
+
if (!listEl) return;
|
|
341
|
+
|
|
342
|
+
const recent = sortRecentSessions(sessions ?? getDashboardSessions(), RECENT_MENU_LIMIT);
|
|
343
|
+
if (countEl) countEl.textContent = recent.length ? `${recent.length}` : '';
|
|
344
|
+
|
|
345
|
+
if (!recent.length) {
|
|
346
|
+
listEl.innerHTML = '';
|
|
347
|
+
emptyEl?.classList.remove('hidden');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
emptyEl?.classList.add('hidden');
|
|
351
|
+
|
|
352
|
+
listEl.innerHTML = recent.map(s => {
|
|
353
|
+
const timeStr = formatTime(s.updatedAt);
|
|
354
|
+
const active = ctx.sessionId === s.sessionId
|
|
355
|
+
&& (!s.machineId || !ctx.machineId || s.machineId === ctx.machineId);
|
|
356
|
+
const machine = s.machineId
|
|
357
|
+
? `<span class="text-[9px] font-mono text-base-content/35 truncate max-w-[4.5rem]" title="${esc(s.machineId)}">@${esc(s.machineId)}</span>`
|
|
358
|
+
: '';
|
|
359
|
+
return `
|
|
360
|
+
<button type="button"
|
|
361
|
+
class="recent-menu-item w-full text-left px-2 py-1.5 rounded-md flex items-start gap-2
|
|
362
|
+
${active ? 'bg-primary/15 text-primary' : 'hover:bg-base-200'}"
|
|
363
|
+
data-session-id="${esc(s.sessionId)}"
|
|
364
|
+
data-session-cwd="${esc(s.cwd || '')}"
|
|
365
|
+
data-session-config-dir="${esc(s.configDir || '')}"
|
|
366
|
+
data-session-agent="${esc(s.agent || 'claude')}"
|
|
367
|
+
data-session-machine="${esc(s.machineId || '')}">
|
|
368
|
+
<span class="flex-shrink-0 mt-0.5">${agentBadge(s.agent)}</span>
|
|
369
|
+
<span class="flex-1 min-w-0">
|
|
370
|
+
<span class="block text-xs font-semibold truncate leading-snug">${esc(sessionDisplayTitle(s))}</span>
|
|
371
|
+
<span class="block text-[10px] text-base-content/40 truncate mt-0.5">${esc(s.projectName || shortPath(s.cwd) || '—')}</span>
|
|
372
|
+
</span>
|
|
373
|
+
<span class="flex flex-col items-end gap-0.5 flex-shrink-0 self-start">
|
|
374
|
+
<span class="text-[9px] text-base-content/40 whitespace-nowrap">${esc(timeStr)}</span>
|
|
375
|
+
${machine}
|
|
376
|
+
${active ? '<span class="text-[9px] opacity-70">当前</span>' : ''}
|
|
377
|
+
</span>
|
|
378
|
+
</button>`;
|
|
379
|
+
}).join('');
|
|
380
|
+
}
|
|
381
|
+
|
|
261
382
|
export function setCalMonthYear(m, y) { calMonth = m; calYear = y; }
|
|
262
383
|
export function setSelectedCalDate(d) { selectedCalDate = d; }
|
|
263
384
|
|
package/public/shell/notes.js
CHANGED
|
@@ -165,7 +165,8 @@ function isMetaEmpty(m) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
export async function toggleSessionFavorite(sessionId, agent, { machineId } = {}) {
|
|
168
|
-
const
|
|
168
|
+
const mid = machineId || ctx.machineId || null;
|
|
169
|
+
const key = metaKey(agent, sessionId, mid);
|
|
169
170
|
const prev = sessionMetaMap.peek()?.[key] || emptyMeta();
|
|
170
171
|
const nextFav = !prev.favorite;
|
|
171
172
|
sessionMetaMap.value = {
|
|
@@ -173,7 +174,9 @@ export async function toggleSessionFavorite(sessionId, agent, { machineId } = {}
|
|
|
173
174
|
[key]: { ...prev, favorite: nextFav, updatedAt: Date.now() },
|
|
174
175
|
};
|
|
175
176
|
try {
|
|
176
|
-
const res = await api('PUT', '/api/sessions/meta', {
|
|
177
|
+
const res = await api('PUT', '/api/sessions/meta', {
|
|
178
|
+
sessionId, agent, favorite: nextFav, machineId: mid || undefined,
|
|
179
|
+
}, undefined, { machineId: mid || undefined });
|
|
177
180
|
sessionMetaMap.value = {
|
|
178
181
|
...sessionMetaMap.peek(),
|
|
179
182
|
[key]: mergeMeta(prev, {
|
|
@@ -193,7 +196,8 @@ export async function toggleSessionFavorite(sessionId, agent, { machineId } = {}
|
|
|
193
196
|
/** Hide / unhide a session (soft archive in the sidebar). */
|
|
194
197
|
export async function toggleSessionHidden(sessionId, agent, { machineId } = {}) {
|
|
195
198
|
if (!sessionId) return false;
|
|
196
|
-
const
|
|
199
|
+
const mid = machineId || ctx.machineId || null;
|
|
200
|
+
const key = metaKey(agent, sessionId, mid);
|
|
197
201
|
const prev = sessionMetaMap.peek()?.[key] || emptyMeta();
|
|
198
202
|
const nextHidden = !prev.hidden;
|
|
199
203
|
sessionMetaMap.value = {
|
|
@@ -201,7 +205,9 @@ export async function toggleSessionHidden(sessionId, agent, { machineId } = {})
|
|
|
201
205
|
[key]: { ...prev, hidden: nextHidden, updatedAt: Date.now() },
|
|
202
206
|
};
|
|
203
207
|
try {
|
|
204
|
-
const res = await api('PUT', '/api/sessions/meta', {
|
|
208
|
+
const res = await api('PUT', '/api/sessions/meta', {
|
|
209
|
+
sessionId, agent, hidden: nextHidden, machineId: mid || undefined,
|
|
210
|
+
}, undefined, { machineId: mid || undefined });
|
|
205
211
|
const next = mergeMeta(prev, {
|
|
206
212
|
favorite: res.favorite,
|
|
207
213
|
notes: res.notes,
|
|
@@ -224,11 +230,17 @@ export async function toggleSessionHidden(sessionId, agent, { machineId } = {})
|
|
|
224
230
|
return true;
|
|
225
231
|
}
|
|
226
232
|
|
|
227
|
-
export async function saveSessionNotes(sessionId, agent, notes) {
|
|
228
|
-
const
|
|
233
|
+
export async function saveSessionNotes(sessionId, agent, notes, { machineId } = {}) {
|
|
234
|
+
const mid = machineId || ctx.machineId || null;
|
|
235
|
+
const key = metaKey(agent, sessionId, mid);
|
|
229
236
|
const prev = sessionMetaMap.peek()?.[key] || emptyMeta();
|
|
230
237
|
try {
|
|
231
|
-
const res = await api('PUT', '/api/sessions/meta', {
|
|
238
|
+
const res = await api('PUT', '/api/sessions/meta', {
|
|
239
|
+
sessionId,
|
|
240
|
+
agent,
|
|
241
|
+
notes,
|
|
242
|
+
machineId: mid || undefined,
|
|
243
|
+
}, undefined, { machineId: mid || undefined });
|
|
232
244
|
sessionMetaMap.value = {
|
|
233
245
|
...sessionMetaMap.peek(),
|
|
234
246
|
[key]: mergeMeta(prev, {
|
|
@@ -251,7 +263,8 @@ export async function saveSessionNotes(sessionId, agent, notes) {
|
|
|
251
263
|
*/
|
|
252
264
|
export async function renameSession(sessionId, agent, title, { machineId } = {}) {
|
|
253
265
|
if (!sessionId) return false;
|
|
254
|
-
const
|
|
266
|
+
const mid = machineId || ctx.machineId || null;
|
|
267
|
+
const key = metaKey(agent, sessionId, mid);
|
|
255
268
|
const prev = sessionMetaMap.peek()?.[key] || emptyMeta();
|
|
256
269
|
const nextTitle = String(title || '').trim().slice(0, 200);
|
|
257
270
|
sessionMetaMap.value = {
|
|
@@ -259,7 +272,9 @@ export async function renameSession(sessionId, agent, title, { machineId } = {})
|
|
|
259
272
|
[key]: { ...prev, title: nextTitle, updatedAt: Date.now() },
|
|
260
273
|
};
|
|
261
274
|
try {
|
|
262
|
-
const res = await api('PUT', '/api/sessions/meta', {
|
|
275
|
+
const res = await api('PUT', '/api/sessions/meta', {
|
|
276
|
+
sessionId, agent, title: nextTitle, machineId: mid || undefined,
|
|
277
|
+
}, undefined, { machineId: mid || undefined });
|
|
263
278
|
const next = mergeMeta(prev, {
|
|
264
279
|
favorite: res.favorite,
|
|
265
280
|
notes: res.notes,
|
|
@@ -3,7 +3,7 @@ import { batch, esc, $ } from '../lib.js';
|
|
|
3
3
|
import {
|
|
4
4
|
sessionsData, sessionFilter, filesPath, viewingFile, filteredSessions,
|
|
5
5
|
currentProject, currentTab, filesRoot, gitRoot, currentAgent, setAgent,
|
|
6
|
-
selectedMachineId, setSelectedMachine, AGENT_LABELS, ctx,
|
|
6
|
+
selectedMachineId, setSelectedMachine, AGENT_LABELS, ctx, hubMode,
|
|
7
7
|
} from '../state.js';
|
|
8
8
|
import { api } from '../api.js';
|
|
9
9
|
import { getLastSessionContext, setLastSessionContext } from './session-context.js';
|
|
@@ -182,14 +182,23 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
182
182
|
for (; i < end; i++) {
|
|
183
183
|
const m = msgs[i];
|
|
184
184
|
const ts = coerceTs(m.ts ?? m.timestamp);
|
|
185
|
-
|
|
185
|
+
// Accept both normalized history rows and a few legacy shapes.
|
|
186
|
+
const typ = m.type || (m.role ? 'text' : null);
|
|
187
|
+
if (typ === 'text' || (m.role && m.content != null && !m.name)) {
|
|
186
188
|
if (m.role === 'assistant' || m.role === 'user') flushToolBatch();
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
const content = typeof m.content === 'string'
|
|
190
|
+
? m.content
|
|
191
|
+
: (Array.isArray(m.content)
|
|
192
|
+
? m.content.map(p => p?.text || '').filter(Boolean).join('\n')
|
|
193
|
+
: String(m.content ?? ''));
|
|
194
|
+
if (content) {
|
|
195
|
+
appendMsg(m.role === 'user' ? 'user' : 'assistant', m.role === 'user' ? 'You' : label, content, { ts });
|
|
196
|
+
}
|
|
197
|
+
} else if (typ === 'tool_use' || m.name) {
|
|
198
|
+
renderToolUse({ name: m.name || 'tool', input: m.input ?? m.arguments ?? {} }, { ts });
|
|
199
|
+
} else if (typ === 'token_usage' || m.usage) {
|
|
191
200
|
flushToolBatch();
|
|
192
|
-
appendHistoryTokenBar(m.usage);
|
|
201
|
+
appendHistoryTokenBar(m.usage || m);
|
|
193
202
|
}
|
|
194
203
|
}
|
|
195
204
|
if (i < msgs.length) {
|
|
@@ -209,29 +218,52 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
209
218
|
};
|
|
210
219
|
|
|
211
220
|
const loadHistory = async () => {
|
|
212
|
-
// Instant re-open from in-memory cache
|
|
221
|
+
// Instant re-open from in-memory cache (never trust empty cache — race/stale)
|
|
213
222
|
const cached = historyCache.get(cacheKey);
|
|
214
|
-
if (cached && Date.now() - cached.at < HISTORY_CACHE_TTL) {
|
|
223
|
+
if (cached && cached.msgs?.length && Date.now() - cached.at < HISTORY_CACHE_TTL) {
|
|
215
224
|
paintMessages(cached.msgs, cached.context, cached.meta);
|
|
216
225
|
return;
|
|
217
226
|
}
|
|
218
227
|
appendSystemMsg(`Loading ${label} history…`);
|
|
219
228
|
try {
|
|
220
|
-
//
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
// Hub mode must route to the edge that owns the session.
|
|
230
|
+
const mid = resolvedMachine || selectedMachineId.peek() || ctx.machineId || null;
|
|
231
|
+
if (hubMode.peek() && !mid) {
|
|
232
|
+
throw new Error('未选择机器,无法加载历史');
|
|
233
|
+
}
|
|
234
|
+
if (mid && ctx.machineId !== mid) setSelectedMachine(mid);
|
|
235
|
+
|
|
236
|
+
// Full history (tail=0); tool inputs compacted server-side (~100 chars each).
|
|
237
|
+
const fetchMsgs = async (agent) => {
|
|
238
|
+
const q = new URLSearchParams({
|
|
239
|
+
tail: '0',
|
|
240
|
+
compact: '1',
|
|
241
|
+
});
|
|
242
|
+
if (agent) q.set('agent', agent);
|
|
243
|
+
// Also put machine in query for proxies that drop custom headers.
|
|
244
|
+
if (mid) q.set('machine', mid);
|
|
245
|
+
return api('GET', `/api/sessions/${sid}/messages?${q}`, undefined, undefined, {
|
|
246
|
+
machineId: mid || undefined,
|
|
247
|
+
dedupe: false,
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
let data = await fetchMsgs(resolvedAgent);
|
|
252
|
+
let msgs = Array.isArray(data) ? data : (data?.messages || []);
|
|
253
|
+
// Prefer stated agent; if empty, retry scanning all agents on the edge.
|
|
254
|
+
if (!msgs.length && resolvedAgent) {
|
|
255
|
+
data = await fetchMsgs(null);
|
|
256
|
+
msgs = Array.isArray(data) ? data : (data?.messages || []);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const context = Array.isArray(data) ? null : (data?.context || null);
|
|
260
|
+
const meta = Array.isArray(data) ? {} : { truncated: data?.truncated, total: data?.total };
|
|
261
|
+
if (msgs.length) historyCache.set(cacheKey, { msgs, context, meta, at: Date.now() });
|
|
262
|
+
else historyCache.delete(cacheKey);
|
|
232
263
|
paintMessages(msgs, context, meta);
|
|
233
264
|
} catch (e) {
|
|
234
265
|
if (ctx.sessionId !== sid) return;
|
|
266
|
+
historyCache.delete(cacheKey);
|
|
235
267
|
$('messages').innerHTML = '';
|
|
236
268
|
appendSystemMsg(`${label} · ${sid.slice(0, 8)}… (history unavailable: ${e.message})`);
|
|
237
269
|
}
|
|
@@ -60,9 +60,16 @@ export const AppShell = (opts = {}) => {
|
|
|
60
60
|
<p id="machine-menu-empty" class="text-xs text-base-content/40 px-1 py-2 hidden">暂无机器在线</p>
|
|
61
61
|
</div>
|
|
62
62
|
</div>
|
|
63
|
-
<
|
|
63
|
+
<div id="recent-menu-wrap" class="relative flex-shrink-0">
|
|
64
|
+
<button type="button" id="btn-recent-sessions" class="btn btn-ghost btn-xs border border-base-300 gap-1 min-h-8 px-2"
|
|
65
|
+
title="最近会话 / 快速跳转" aria-haspopup="listbox" aria-expanded="false">
|
|
66
|
+
<span>最近</span>
|
|
67
|
+
<span class="text-[10px] opacity-50">▾</span>
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
<span id="topbar-project" class="text-sm text-base-content/50 flex-1 truncate min-w-0">Select a session</span>
|
|
64
71
|
<span id="conn-dot" class="w-2 h-2 rounded-full bg-base-content/30 flex-shrink-0" title="连接状态"></span>
|
|
65
|
-
<div class="flex gap-2">
|
|
72
|
+
<div class="flex gap-2 topbar-actions">
|
|
66
73
|
<button id="btn-refresh-conn" class="btn btn-ghost btn-xs border border-base-300" title="重新连接并同步">↻</button>
|
|
67
74
|
<button id="btn-pwa-install" class="btn btn-ghost btn-xs border border-base-300 hidden" title="安装到主屏幕">安装</button>
|
|
68
75
|
<button id="btn-meta-agent" class="btn btn-ghost btn-xs border border-primary/40 text-primary" title="Meta Agent — 报告 / 问答 / VLM">✦ Agent</button>
|
|
@@ -72,6 +79,18 @@ export const AppShell = (opts = {}) => {
|
|
|
72
79
|
<button id="btn-logout" class="btn btn-ghost btn-xs border border-base-300">Sign out</button>
|
|
73
80
|
</div>
|
|
74
81
|
</div>
|
|
82
|
+
<!-- Fixed panel + backdrop: siblings of topbar so flex layout never clips them -->
|
|
83
|
+
<div id="recent-menu-backdrop" class="hidden fixed inset-0 z-[55] bg-black/40" aria-hidden="true"></div>
|
|
84
|
+
<div id="recent-menu" class="hidden fixed z-[60] w-80 max-h-[min(24rem,70vh)] overflow-y-auto
|
|
85
|
+
rounded-lg border border-base-300 bg-base-100 shadow-xl p-2"
|
|
86
|
+
role="listbox" aria-label="最近会话">
|
|
87
|
+
<div class="flex items-center justify-between px-1 pb-1.5 mb-1 border-b border-base-300 sticky top-0 bg-base-100 z-[1]">
|
|
88
|
+
<span class="text-[10px] uppercase tracking-wide text-base-content/45 font-semibold">最近会话</span>
|
|
89
|
+
<span id="recent-menu-count" class="text-[10px] text-base-content/35 font-mono"></span>
|
|
90
|
+
</div>
|
|
91
|
+
<div id="recent-menu-list" class="flex flex-col gap-0.5"></div>
|
|
92
|
+
<p id="recent-menu-empty" class="text-xs text-base-content/40 px-1 py-2 hidden">暂无最近会话</p>
|
|
93
|
+
</div>
|
|
75
94
|
<!-- Hub: only when no remembered machine (or last one offline) -->
|
|
76
95
|
<div id="machine-picker" class="hidden fixed inset-0 z-[60] bg-base-100 flex items-center justify-center p-4">
|
|
77
96
|
<div class="w-full max-w-md">
|
|
@@ -263,13 +282,21 @@ export const AppShell = (opts = {}) => {
|
|
|
263
282
|
<option value="full">density: full</option>
|
|
264
283
|
</select>
|
|
265
284
|
</div>
|
|
266
|
-
<div class="flex gap-2 items-end px-3 pt-2 pb-2">
|
|
267
|
-
<button id="btn-opts" class="btn btn-ghost btn-sm px-2 text-base-content/30 hover:text-base-content flex-shrink-0" title="Options"
|
|
285
|
+
<div id="composer-row" class="flex gap-2 items-end px-3 pt-2 pb-2">
|
|
286
|
+
<button id="btn-opts" class="btn btn-ghost btn-sm px-2 text-base-content/30 hover:text-base-content flex-shrink-0" title="Options">⚙</button>
|
|
268
287
|
<textarea id="chat-input" rows="1" placeholder="Ask… (!cmd shell · ⌘↵ send)"
|
|
269
288
|
class="chat-input flex-1 text-sm resize-none leading-relaxed"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
289
|
+
name="agent-chat-message"
|
|
290
|
+
autocomplete="off"
|
|
291
|
+
autocorrect="off"
|
|
292
|
+
autocapitalize="off"
|
|
293
|
+
spellcheck="false"
|
|
294
|
+
data-1p-ignore="true"
|
|
295
|
+
data-lpignore="true"
|
|
296
|
+
data-form-type="other"
|
|
297
|
+
data-bwignore="true"></textarea>
|
|
298
|
+
<button id="send-btn" class="btn btn-primary btn-sm flex-shrink-0">Send</button>
|
|
299
|
+
<button id="stop-btn" class="btn btn-error btn-sm hidden flex-shrink-0">■</button>
|
|
273
300
|
</div>
|
|
274
301
|
</div>
|
|
275
302
|
</div>
|