claude-simple 0.2.6 → 0.2.7
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 +2 -0
- package/dist/app.0.2.7.min.js +1039 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.7.min.css +1598 -0
- package/package.json +1 -1
- package/public/agent-panel.js +245 -1
- package/public/api.js +2 -0
- package/public/chat.js +81 -11
- package/public/index.html +2 -2
- package/public/shell/dashboard.js +39 -10
- package/public/shell/layout.js +39 -9
- package/public/shell/session-list.js +22 -14
- package/public/shell/session-nav.js +18 -0
- package/public/shell/templates.js +27 -7
- package/public/shell.js +32 -1
- package/public/state.js +4 -6
- package/public/style.css +100 -6
- package/public/sw.js +2 -2
- package/public/terminal.js +77 -3
- package/src/agents/claude.js +1 -1
- package/src/agents/codex.js +1 -0
- package/src/agents/grok.js +3 -2
- package/src/agents/meta-agent.js +332 -1
- package/src/agents/sessions.js +106 -26
- package/src/app.js +88 -11
- package/dist/app.0.2.6.min.js +0 -953
- package/dist/style.0.2.6.min.css +0 -1504
package/package.json
CHANGED
package/public/agent-panel.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { esc, $ } from './lib.js';
|
|
6
6
|
import { api, authHeaders, currentMachineId } from './api.js';
|
|
7
|
-
import { hubMode, ctx } from './state.js';
|
|
7
|
+
import { hubMode, ctx, currentAgent, currentProject } from './state.js';
|
|
8
8
|
|
|
9
9
|
let panelOpen = false;
|
|
10
10
|
let mode = 'chat'; // chat | report | history | config
|
|
@@ -19,6 +19,10 @@ let autoTimer = null;
|
|
|
19
19
|
let lastAutoDay = localStorage.getItem('ai_last_auto_report') || '';
|
|
20
20
|
let historyCache = [];
|
|
21
21
|
let historySearch = '';
|
|
22
|
+
let notesCache = [];
|
|
23
|
+
let notesSearch = '';
|
|
24
|
+
let editingNoteId = null;
|
|
25
|
+
let currentNoteSnapshot = null;
|
|
22
26
|
const LAST_SESSION_KEY = 'ma_last_session_id';
|
|
23
27
|
|
|
24
28
|
function mdHtml(text) {
|
|
@@ -90,6 +94,7 @@ function panelHTML() {
|
|
|
90
94
|
<div class="flex items-center gap-1">
|
|
91
95
|
<button type="button" id="ma-btn-chat" class="btn btn-xs btn-ghost border border-primary/40 text-primary">对话</button>
|
|
92
96
|
<button type="button" id="ma-btn-history" class="btn btn-xs btn-ghost border border-base-300" title="历史对话">历史</button>
|
|
97
|
+
<button type="button" id="ma-btn-notes" class="btn btn-xs btn-ghost border border-base-300" title="工作记事">记事</button>
|
|
93
98
|
<button type="button" id="ma-btn-report" class="btn btn-xs btn-ghost border border-base-300">报告</button>
|
|
94
99
|
<button type="button" id="ma-btn-config" class="btn btn-xs btn-ghost border border-base-300" title="AI 配置">⚙</button>
|
|
95
100
|
<button type="button" id="ma-btn-close" class="btn btn-xs btn-ghost" data-ma-close="1">✕</button>
|
|
@@ -145,6 +150,56 @@ function panelHTML() {
|
|
|
145
150
|
</p>
|
|
146
151
|
</div>
|
|
147
152
|
|
|
153
|
+
<div id="ma-mode-notes" class="meta-agent-body hidden flex min-h-0 flex-1">
|
|
154
|
+
<div class="w-[44%] min-w-[180px] max-w-[240px] border-r border-base-300 flex flex-col">
|
|
155
|
+
<div class="flex items-center gap-2 px-3 py-2 border-b border-base-300 flex-shrink-0">
|
|
156
|
+
<input id="ma-notes-search" type="search" placeholder="搜索记事…"
|
|
157
|
+
class="input input-xs input-bordered flex-1 text-xs" autocomplete="off">
|
|
158
|
+
<button type="button" id="ma-notes-refresh" class="btn btn-ghost btn-xs">↻</button>
|
|
159
|
+
<button type="button" id="ma-notes-new" class="btn btn-primary btn-xs">+</button>
|
|
160
|
+
</div>
|
|
161
|
+
<div id="ma-notes-list" class="flex-1 overflow-y-auto p-2 flex flex-col gap-1"></div>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="flex-1 min-w-0 flex flex-col">
|
|
164
|
+
<div class="px-3 py-2 border-b border-base-300 text-[11px] text-base-content/45 flex items-center gap-2 flex-wrap">
|
|
165
|
+
<span>记事保存在 Hub,可被 Meta Agent 检索。</span>
|
|
166
|
+
<span id="ma-note-links" class="truncate"></span>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="p-3 flex flex-col gap-3 flex-1 overflow-y-auto">
|
|
169
|
+
<label class="form-control">
|
|
170
|
+
<span class="label-text text-xs">标题</span>
|
|
171
|
+
<input id="ma-note-title" class="input input-sm input-bordered text-sm" placeholder="例如:发布前遗留问题">
|
|
172
|
+
</label>
|
|
173
|
+
<label class="form-control flex-1">
|
|
174
|
+
<span class="label-text text-xs">内容</span>
|
|
175
|
+
<textarea id="ma-note-content" class="textarea textarea-bordered text-sm min-h-[220px]" placeholder="记录决策、风险、待办、排查线索…"></textarea>
|
|
176
|
+
</label>
|
|
177
|
+
<div class="flex flex-wrap items-center gap-3 text-xs">
|
|
178
|
+
<label class="flex items-center gap-1 cursor-pointer">
|
|
179
|
+
<input type="checkbox" id="ma-note-pin" class="checkbox checkbox-xs">
|
|
180
|
+
置顶
|
|
181
|
+
</label>
|
|
182
|
+
<label class="flex items-center gap-1 cursor-pointer">
|
|
183
|
+
<input type="checkbox" id="ma-note-link-project" class="checkbox checkbox-xs">
|
|
184
|
+
关联当前项目
|
|
185
|
+
</label>
|
|
186
|
+
<label class="flex items-center gap-1 cursor-pointer">
|
|
187
|
+
<input type="checkbox" id="ma-note-link-session" class="checkbox checkbox-xs">
|
|
188
|
+
关联当前 session
|
|
189
|
+
</label>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="flex items-center justify-between gap-2">
|
|
192
|
+
<span id="ma-note-status" class="text-xs text-base-content/45"></span>
|
|
193
|
+
<div class="flex items-center gap-2">
|
|
194
|
+
<button type="button" id="ma-note-ask" class="btn btn-ghost btn-sm">快捷提问</button>
|
|
195
|
+
<button type="button" id="ma-note-delete" class="btn btn-ghost btn-sm text-error">删除</button>
|
|
196
|
+
<button type="button" id="ma-note-save" class="btn btn-primary btn-sm">保存记事</button>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
148
203
|
<div id="ma-mode-report" class="meta-agent-body hidden flex-col min-h-0 flex-1">
|
|
149
204
|
<div class="flex items-center gap-2 px-3 py-2 border-b border-base-300 flex-shrink-0 flex-wrap">
|
|
150
205
|
<select id="ma-report-days" class="select select-xs select-bordered">
|
|
@@ -214,11 +269,13 @@ function setMode(m) {
|
|
|
214
269
|
mode = m;
|
|
215
270
|
const chat = $('ma-mode-chat');
|
|
216
271
|
const history = $('ma-mode-history');
|
|
272
|
+
const notes = $('ma-mode-notes');
|
|
217
273
|
const report = $('ma-mode-report');
|
|
218
274
|
const config = $('ma-mode-config');
|
|
219
275
|
for (const [el, key] of [
|
|
220
276
|
[chat, 'chat'],
|
|
221
277
|
[history, 'history'],
|
|
278
|
+
[notes, 'notes'],
|
|
222
279
|
[report, 'report'],
|
|
223
280
|
[config, 'config'],
|
|
224
281
|
]) {
|
|
@@ -235,8 +292,10 @@ function setMode(m) {
|
|
|
235
292
|
};
|
|
236
293
|
tab('ma-btn-chat', m === 'chat');
|
|
237
294
|
tab('ma-btn-history', m === 'history');
|
|
295
|
+
tab('ma-btn-notes', m === 'notes');
|
|
238
296
|
tab('ma-btn-report', m === 'report');
|
|
239
297
|
if (m === 'history') renderHistoryList();
|
|
298
|
+
if (m === 'notes') renderNotesList();
|
|
240
299
|
}
|
|
241
300
|
|
|
242
301
|
function updateSessionChrome() {
|
|
@@ -484,6 +543,155 @@ async function refreshHistoryCache() {
|
|
|
484
543
|
return historyCache;
|
|
485
544
|
}
|
|
486
545
|
|
|
546
|
+
function currentNoteLinks() {
|
|
547
|
+
const proj = currentProject.peek();
|
|
548
|
+
const sessionLinked = ctx.sessionId ? `${ctx.agent || currentAgent.peek() || 'claude'}:${ctx.sessionId.slice(0, 8)}` : '';
|
|
549
|
+
return {
|
|
550
|
+
project: proj?.path || '',
|
|
551
|
+
sessionId: ctx.sessionId || '',
|
|
552
|
+
agent: ctx.agent || currentAgent.peek() || 'claude',
|
|
553
|
+
summary: [
|
|
554
|
+
proj?.name ? `项目 ${proj.name}` : '',
|
|
555
|
+
sessionLinked ? `会话 ${sessionLinked}` : '',
|
|
556
|
+
].filter(Boolean).join(' · '),
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function resetNoteEditor() {
|
|
561
|
+
editingNoteId = null;
|
|
562
|
+
currentNoteSnapshot = null;
|
|
563
|
+
$('ma-note-title').value = '';
|
|
564
|
+
$('ma-note-content').value = '';
|
|
565
|
+
$('ma-note-pin').checked = false;
|
|
566
|
+
$('ma-note-link-project').checked = !!currentProject.peek()?.path;
|
|
567
|
+
$('ma-note-link-session').checked = !!ctx.sessionId;
|
|
568
|
+
$('ma-note-status').textContent = '新记事';
|
|
569
|
+
$('ma-note-delete').disabled = true;
|
|
570
|
+
$('ma-note-links').textContent = currentNoteLinks().summary || '未关联上下文';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function fillNoteEditor(note) {
|
|
574
|
+
editingNoteId = note?.id || null;
|
|
575
|
+
currentNoteSnapshot = note || null;
|
|
576
|
+
$('ma-note-title').value = note?.title || '';
|
|
577
|
+
$('ma-note-content').value = note?.content || '';
|
|
578
|
+
$('ma-note-pin').checked = !!note?.pinned;
|
|
579
|
+
$('ma-note-link-project').checked = !!note?.cwd;
|
|
580
|
+
$('ma-note-link-session').checked = !!note?.sessionId;
|
|
581
|
+
$('ma-note-status').textContent = note?.updatedAt ? `上次更新 ${fmtSessionWhen(note.updatedAt)}` : '';
|
|
582
|
+
$('ma-note-delete').disabled = !note?.id;
|
|
583
|
+
const links = [];
|
|
584
|
+
if (note?.cwd) links.push(`项目 ${note.cwd.split('/').pop()}`);
|
|
585
|
+
if (note?.sessionId) links.push(`会话 ${(note.agent || 'claude')}:${note.sessionId.slice(0, 8)}`);
|
|
586
|
+
$('ma-note-links').textContent = links.join(' · ') || '未关联上下文';
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function askFromCurrentNote() {
|
|
590
|
+
const title = $('ma-note-title')?.value?.trim() || '';
|
|
591
|
+
const content = $('ma-note-content')?.value?.trim() || '';
|
|
592
|
+
if (!title && !content) {
|
|
593
|
+
$('ma-note-status').textContent = '先选一条记事或输入内容';
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const links = [];
|
|
597
|
+
if ($('ma-note-link-project')?.checked && currentProject.peek()?.path) {
|
|
598
|
+
links.push(`项目路径:${currentProject.peek().path}`);
|
|
599
|
+
}
|
|
600
|
+
if ($('ma-note-link-session')?.checked && ctx.sessionId) {
|
|
601
|
+
links.push(`关联会话:${ctx.agent || currentAgent.peek() || 'claude'} / ${ctx.sessionId}`);
|
|
602
|
+
}
|
|
603
|
+
const prompt = [
|
|
604
|
+
'基于下面这条工作记事回答,并结合相关会话与项目上下文:',
|
|
605
|
+
`标题:${title || '未命名'}`,
|
|
606
|
+
content ? `内容:\n${content}` : '',
|
|
607
|
+
links.length ? links.join('\n') : '',
|
|
608
|
+
'',
|
|
609
|
+
'请先总结这条记事的核心,再给出下一步建议和可能风险。',
|
|
610
|
+
].filter(Boolean).join('\n');
|
|
611
|
+
setMode('chat');
|
|
612
|
+
$('ma-input').value = prompt;
|
|
613
|
+
$('ma-input').dispatchEvent(new Event('input'));
|
|
614
|
+
$('ma-input').focus();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function refreshNotesCache() {
|
|
618
|
+
const q = (notesSearch || '').trim();
|
|
619
|
+
const data = await api('GET', `/api/ai/notes?limit=80${q ? `&q=${encodeURIComponent(q)}` : ''}`);
|
|
620
|
+
notesCache = data.notes || [];
|
|
621
|
+
return notesCache;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function renderNotesList() {
|
|
625
|
+
const list = $('ma-notes-list');
|
|
626
|
+
if (!list) return;
|
|
627
|
+
if (!notesCache.length) {
|
|
628
|
+
list.innerHTML = '<div class="text-center text-xs text-base-content/40 py-10">还没有记事。</div>';
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
list.innerHTML = notesCache.map((note) => {
|
|
632
|
+
const active = note.id === editingNoteId;
|
|
633
|
+
const tags = [
|
|
634
|
+
note.scope === 'project' ? '项目' : note.scope === 'session' ? '会话' : '通用',
|
|
635
|
+
note.pinned ? '置顶' : '',
|
|
636
|
+
].filter(Boolean).join(' · ');
|
|
637
|
+
return `
|
|
638
|
+
<button type="button" class="ma-note-item ${active ? 'active' : ''}" data-note-id="${esc(note.id)}">
|
|
639
|
+
<div class="text-left">
|
|
640
|
+
<div class="font-medium text-xs truncate">${esc(note.title || '未命名')}</div>
|
|
641
|
+
<div class="text-[10px] text-base-content/45 mt-0.5 line-clamp-2">${esc(note.content || '')}</div>
|
|
642
|
+
<div class="text-[10px] text-base-content/35 mt-1">${esc(tags)}${tags ? ' · ' : ''}${esc(fmtSessionWhen(note.updatedAt) || '')}</div>
|
|
643
|
+
</div>
|
|
644
|
+
</button>`;
|
|
645
|
+
}).join('');
|
|
646
|
+
list.querySelectorAll('[data-note-id]').forEach((btn) => {
|
|
647
|
+
btn.addEventListener('click', async () => {
|
|
648
|
+
try {
|
|
649
|
+
const note = await api('GET', `/api/ai/notes/${btn.dataset.noteId}`);
|
|
650
|
+
fillNoteEditor(note);
|
|
651
|
+
renderNotesList();
|
|
652
|
+
} catch (e) {
|
|
653
|
+
$('ma-note-status').textContent = e.message;
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async function saveCurrentNote() {
|
|
660
|
+
const title = $('ma-note-title')?.value?.trim() || '';
|
|
661
|
+
const content = $('ma-note-content')?.value?.trim() || '';
|
|
662
|
+
if (!title || !content) {
|
|
663
|
+
$('ma-note-status').textContent = '标题和内容都不能为空';
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const links = currentNoteLinks();
|
|
667
|
+
const body = {
|
|
668
|
+
id: editingNoteId || undefined,
|
|
669
|
+
title,
|
|
670
|
+
content,
|
|
671
|
+
pinned: !!$('ma-note-pin')?.checked,
|
|
672
|
+
cwd: $('ma-note-link-project')?.checked ? links.project : '',
|
|
673
|
+
sessionId: $('ma-note-link-session')?.checked ? links.sessionId : '',
|
|
674
|
+
agent: $('ma-note-link-session')?.checked ? links.agent : '',
|
|
675
|
+
};
|
|
676
|
+
$('ma-note-status').textContent = '保存中…';
|
|
677
|
+
const note = await api('POST', '/api/ai/notes', body);
|
|
678
|
+
fillNoteEditor(note);
|
|
679
|
+
await refreshNotesCache();
|
|
680
|
+
renderNotesList();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async function deleteCurrentNoteEditor() {
|
|
684
|
+
if (!editingNoteId) {
|
|
685
|
+
resetNoteEditor();
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (!confirm('删除这条记事?不可恢复。')) return;
|
|
689
|
+
await api('DELETE', `/api/ai/notes/${editingNoteId}`);
|
|
690
|
+
resetNoteEditor();
|
|
691
|
+
await refreshNotesCache();
|
|
692
|
+
renderNotesList();
|
|
693
|
+
}
|
|
694
|
+
|
|
487
695
|
function renderHistoryList() {
|
|
488
696
|
const list = $('ma-history-list');
|
|
489
697
|
if (!list) return;
|
|
@@ -986,6 +1194,11 @@ export function initMetaAgent() {
|
|
|
986
1194
|
await refreshHistoryCache();
|
|
987
1195
|
setMode('history');
|
|
988
1196
|
});
|
|
1197
|
+
$('ma-btn-notes')?.addEventListener('click', async () => {
|
|
1198
|
+
await refreshNotesCache();
|
|
1199
|
+
if (!editingNoteId) resetNoteEditor();
|
|
1200
|
+
setMode('notes');
|
|
1201
|
+
});
|
|
989
1202
|
$('ma-open-history')?.addEventListener('click', async () => {
|
|
990
1203
|
await refreshHistoryCache();
|
|
991
1204
|
setMode('history');
|
|
@@ -1042,6 +1255,35 @@ export function initMetaAgent() {
|
|
|
1042
1255
|
});
|
|
1043
1256
|
$('ma-gen-report')?.addEventListener('click', generateReport);
|
|
1044
1257
|
$('ma-load-reports')?.addEventListener('click', loadReportList);
|
|
1258
|
+
$('ma-notes-refresh')?.addEventListener('click', async () => {
|
|
1259
|
+
await refreshNotesCache();
|
|
1260
|
+
renderNotesList();
|
|
1261
|
+
});
|
|
1262
|
+
$('ma-notes-new')?.addEventListener('click', () => {
|
|
1263
|
+
resetNoteEditor();
|
|
1264
|
+
renderNotesList();
|
|
1265
|
+
$('ma-note-title')?.focus();
|
|
1266
|
+
});
|
|
1267
|
+
$('ma-notes-search')?.addEventListener('input', async (e) => {
|
|
1268
|
+
notesSearch = e.target.value || '';
|
|
1269
|
+
await refreshNotesCache();
|
|
1270
|
+
renderNotesList();
|
|
1271
|
+
});
|
|
1272
|
+
$('ma-note-save')?.addEventListener('click', async () => {
|
|
1273
|
+
try {
|
|
1274
|
+
await saveCurrentNote();
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
$('ma-note-status').textContent = e.message;
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
$('ma-note-delete')?.addEventListener('click', async () => {
|
|
1280
|
+
try {
|
|
1281
|
+
await deleteCurrentNoteEditor();
|
|
1282
|
+
} catch (e) {
|
|
1283
|
+
$('ma-note-status').textContent = e.message;
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
$('ma-note-ask')?.addEventListener('click', askFromCurrentNote);
|
|
1045
1287
|
$('ma-cfg-save')?.addEventListener('click', saveConfig);
|
|
1046
1288
|
$('ma-auto-report')?.addEventListener('change', async () => {
|
|
1047
1289
|
try {
|
|
@@ -1114,6 +1356,7 @@ export function initMetaAgent() {
|
|
|
1114
1356
|
// status + selected machine are known (boot.js / hub.js), not here: at this
|
|
1115
1357
|
// point probeHub() hasn't resolved yet, so hubMode always reads as false.
|
|
1116
1358
|
loadSession(null, { silent: true });
|
|
1359
|
+
resetNoteEditor();
|
|
1117
1360
|
}
|
|
1118
1361
|
|
|
1119
1362
|
/**
|
|
@@ -1124,6 +1367,7 @@ export function loadMetaAgentBoot() {
|
|
|
1124
1367
|
if (!ctx.token) return;
|
|
1125
1368
|
if (hubMode.peek() && !currentMachineId()) return;
|
|
1126
1369
|
refreshHistoryCache();
|
|
1370
|
+
refreshNotesCache().catch(() => {});
|
|
1127
1371
|
api('GET', '/api/ai/config').then(scheduleAutoReport).catch(() => {});
|
|
1128
1372
|
}
|
|
1129
1373
|
|
package/public/api.js
CHANGED
package/public/chat.js
CHANGED
|
@@ -32,8 +32,57 @@ let lifecycleBound = false;
|
|
|
32
32
|
let sawDisconnect = false;
|
|
33
33
|
let lastWsMachine = null;
|
|
34
34
|
let reconnectQuiet = false;
|
|
35
|
-
/**
|
|
36
|
-
let
|
|
35
|
+
/** FIFO messages deliberately queued while a turn is running. */
|
|
36
|
+
let pendingUserTexts = [];
|
|
37
|
+
// Composer state belongs to a conversation, never to the shared textarea.
|
|
38
|
+
// Keeping this in memory deliberately avoids persisting potentially sensitive
|
|
39
|
+
// prompts to disk while still making session switches lossless.
|
|
40
|
+
const composerDrafts = new Map();
|
|
41
|
+
|
|
42
|
+
function draftKey({ sessionId = ctx.sessionId, agent = ctx.agent, machineId = ctx.machineId } = {}) {
|
|
43
|
+
if (!sessionId) return null;
|
|
44
|
+
return `${machineId || ''}:${agent || 'claude'}:${sessionId}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Save the visible, unsent prompt under its current session identity. */
|
|
48
|
+
export function stashComposerDraft(identity) {
|
|
49
|
+
const key = draftKey(identity);
|
|
50
|
+
const input = $('chat-input');
|
|
51
|
+
if (!key || !input) return;
|
|
52
|
+
if (input.value) composerDrafts.set(key, input.value);
|
|
53
|
+
else composerDrafts.delete(key);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Restore only the target session's draft; a missing one must mean blank. */
|
|
57
|
+
export function restoreComposerDraft(identity) {
|
|
58
|
+
const input = $('chat-input');
|
|
59
|
+
if (!input) return;
|
|
60
|
+
input.value = composerDrafts.get(draftKey(identity)) || '';
|
|
61
|
+
autoResize(input);
|
|
62
|
+
updateInputMode(input.value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Leaving a session turns its queued sends back into a visible draft. This is
|
|
67
|
+
* intentionally conservative: a background completion must never send work
|
|
68
|
+
* into whichever session the user is viewing now.
|
|
69
|
+
*/
|
|
70
|
+
export function parkQueuedMessages(identity) {
|
|
71
|
+
const key = draftKey(identity);
|
|
72
|
+
if (!key) return;
|
|
73
|
+
const queued = pendingUserTexts.filter(item => item.key === key);
|
|
74
|
+
if (!queued.length) return;
|
|
75
|
+
pendingUserTexts = pendingUserTexts.filter(item => item.key !== key);
|
|
76
|
+
const existing = composerDrafts.get(key) || '';
|
|
77
|
+
composerDrafts.set(key, [existing, ...queued.map(item => item.text)].filter(Boolean).join('\n\n'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A run may continue server-side after navigation, but it is no longer active UI state. */
|
|
81
|
+
export function detachProcessingForSessionSwitch() {
|
|
82
|
+
if (!isProcessing.peek()) return;
|
|
83
|
+
isProcessing.value = false;
|
|
84
|
+
syncWakeLock();
|
|
85
|
+
}
|
|
37
86
|
|
|
38
87
|
function bindSocketLifecycle() {
|
|
39
88
|
if (lifecycleBound) return;
|
|
@@ -238,16 +287,27 @@ export function connectWS(opts = {}) {
|
|
|
238
287
|
}
|
|
239
288
|
|
|
240
289
|
function flushPendingUserText() {
|
|
241
|
-
if (!
|
|
242
|
-
const
|
|
243
|
-
|
|
290
|
+
if (!pendingUserTexts.length || isProcessing.peek()) return;
|
|
291
|
+
const currentKey = draftKey();
|
|
292
|
+
const index = pendingUserTexts.findIndex(item => item.key === currentKey);
|
|
293
|
+
// Queues belonging to another session stay parked until that session is
|
|
294
|
+
// opened (or are converted to a draft during navigation).
|
|
295
|
+
if (index < 0) return;
|
|
296
|
+
const [pending] = pendingUserTexts.splice(index, 1);
|
|
244
297
|
const input = $('chat-input');
|
|
245
298
|
if (input) {
|
|
246
|
-
input.value = text;
|
|
299
|
+
input.value = pending.text;
|
|
247
300
|
autoResize(input);
|
|
248
301
|
}
|
|
249
|
-
// Defer so isProcessing watch can re-enable send button first
|
|
250
|
-
|
|
302
|
+
// Defer so isProcessing watch can re-enable send button first. Re-check the
|
|
303
|
+
// identity inside the callback: a click can switch sessions in this tiny gap.
|
|
304
|
+
setTimeout(() => {
|
|
305
|
+
if (pending.key !== draftKey() || isProcessing.peek()) {
|
|
306
|
+
composerDrafts.set(pending.key, pending.text);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
sendMessage();
|
|
310
|
+
}, 0);
|
|
251
311
|
}
|
|
252
312
|
|
|
253
313
|
function markTurnDone() {
|
|
@@ -257,6 +317,10 @@ function markTurnDone() {
|
|
|
257
317
|
}
|
|
258
318
|
|
|
259
319
|
function handleWsMessage(msg) {
|
|
320
|
+
// The socket can still receive a final frame from the previously viewed
|
|
321
|
+
// session while its subscription is changing. Do not let that frame alter
|
|
322
|
+
// the new session's transcript, processing state, or queue.
|
|
323
|
+
if (msg?.sessionId && ctx.sessionId && msg.sessionId !== ctx.sessionId) return;
|
|
260
324
|
// Server outbox may pack multiple events into one frame
|
|
261
325
|
if (msg.type === 'batch' && Array.isArray(msg.items)) {
|
|
262
326
|
for (const item of msg.items) handleWsMessage(item);
|
|
@@ -702,7 +766,7 @@ export function sendMessage() {
|
|
|
702
766
|
|
|
703
767
|
// While a turn is running, queue the next message (common Grok complaint).
|
|
704
768
|
if (isProcessing.peek()) {
|
|
705
|
-
|
|
769
|
+
pendingUserTexts.push({ key: draftKey(), text });
|
|
706
770
|
input.value = '';
|
|
707
771
|
autoResize(input);
|
|
708
772
|
updateInputMode('');
|
|
@@ -712,6 +776,7 @@ export function sendMessage() {
|
|
|
712
776
|
}
|
|
713
777
|
|
|
714
778
|
input.value = '';
|
|
779
|
+
composerDrafts.delete(draftKey());
|
|
715
780
|
autoResize(input);
|
|
716
781
|
updateInputMode('');
|
|
717
782
|
clearPendingAttachments();
|
|
@@ -754,7 +819,7 @@ export function sendMessage() {
|
|
|
754
819
|
configDir: ctx.configDir,
|
|
755
820
|
model: currentModel.peek(),
|
|
756
821
|
...(effort && { effort }),
|
|
757
|
-
...(permission &&
|
|
822
|
+
...(permission && { permissionMode: permission }),
|
|
758
823
|
...(permission === 'bypassPermissions' && { allowDangerouslySkipPermissions: true }),
|
|
759
824
|
},
|
|
760
825
|
});
|
|
@@ -766,7 +831,7 @@ export function stopProcessing() {
|
|
|
766
831
|
if (ctx.sessionId) {
|
|
767
832
|
sendWs({ type: 'command', cmd: 'turn.interrupt', sessionId: ctx.sessionId });
|
|
768
833
|
}
|
|
769
|
-
|
|
834
|
+
pendingUserTexts = pendingUserTexts.filter(item => item.key !== draftKey());
|
|
770
835
|
markTurnDone();
|
|
771
836
|
}
|
|
772
837
|
|
|
@@ -1019,6 +1084,11 @@ function clearPendingAttachments() {
|
|
|
1019
1084
|
renderAttachBar();
|
|
1020
1085
|
}
|
|
1021
1086
|
|
|
1087
|
+
/** Attachments are bound to the visible composer; discard them before session navigation. */
|
|
1088
|
+
export function discardComposerAttachments() {
|
|
1089
|
+
clearPendingAttachments();
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1022
1092
|
function renderAttachBar() {
|
|
1023
1093
|
const bar = $('chat-attach-bar');
|
|
1024
1094
|
if (!bar) return;
|
package/public/index.html
CHANGED
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
<script src="https://cdn.jsdelivr.net/npm/marked@9/marked.min.js"></script>
|
|
84
84
|
<link id="hljs-theme" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github-dark.min.css">
|
|
85
85
|
<script src="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/highlight.min.js"></script>
|
|
86
|
-
<link rel="stylesheet" href="style.css?v=ui-
|
|
86
|
+
<link rel="stylesheet" href="style.css?v=ui-11">
|
|
87
87
|
</head>
|
|
88
88
|
<body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
|
|
89
89
|
<div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
|
|
@@ -99,6 +99,6 @@
|
|
|
99
99
|
}
|
|
100
100
|
}, 10000);
|
|
101
101
|
</script>
|
|
102
|
-
<script type="module" src="app.js?v=ui-
|
|
102
|
+
<script type="module" src="app.js?v=ui-11"></script>
|
|
103
103
|
</body>
|
|
104
104
|
</html>
|
|
@@ -226,10 +226,33 @@ export function showSelectedDaySessions(sessions) {
|
|
|
226
226
|
|
|
227
227
|
/** How many sessions in the topbar “最近” quick-jump menu. */
|
|
228
228
|
export const RECENT_MENU_LIMIT = 15;
|
|
229
|
+
const RECENT_DISMISSED_KEY = 'recentSessionsDismissed';
|
|
230
|
+
|
|
231
|
+
function recentDismissKey(s) {
|
|
232
|
+
return `${s.machineId || ''}:${s.agent || 'claude'}:${s.sessionId}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function dismissedRecentKeys() {
|
|
236
|
+
try {
|
|
237
|
+
const saved = JSON.parse(localStorage.getItem(RECENT_DISMISSED_KEY) || '[]');
|
|
238
|
+
return new Set(Array.isArray(saved) ? saved : []);
|
|
239
|
+
} catch { return new Set(); }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Remove an item from this device's quick-jump list; the actual session remains intact. */
|
|
243
|
+
export function dismissRecentSession(session) {
|
|
244
|
+
if (!session?.sessionId) return;
|
|
245
|
+
const keys = dismissedRecentKeys();
|
|
246
|
+
keys.add(recentDismissKey(session));
|
|
247
|
+
// Keep storage bounded even after many one-off sessions.
|
|
248
|
+
localStorage.setItem(RECENT_DISMISSED_KEY, JSON.stringify([...keys].slice(-200)));
|
|
249
|
+
renderRecentSessionsMenu(getDashboardSessions());
|
|
250
|
+
}
|
|
229
251
|
|
|
230
252
|
function sortRecentSessions(sessions, limit) {
|
|
253
|
+
const dismissed = dismissedRecentKeys();
|
|
231
254
|
return [...asSessionArray(sessions)]
|
|
232
|
-
.filter(s => s && s.sessionId)
|
|
255
|
+
.filter(s => s && s.sessionId && !dismissed.has(recentDismissKey(s)))
|
|
233
256
|
.sort((a, b) => (coerceTs(b.updatedAt) || 0) - (coerceTs(a.updatedAt) || 0))
|
|
234
257
|
.slice(0, limit);
|
|
235
258
|
}
|
|
@@ -357,14 +380,14 @@ export function renderRecentSessionsMenu(sessions) {
|
|
|
357
380
|
? `<span class="text-[9px] font-mono text-base-content/35 truncate max-w-[4.5rem]" title="${esc(s.machineId)}">@${esc(s.machineId)}</span>`
|
|
358
381
|
: '';
|
|
359
382
|
return `
|
|
360
|
-
<
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
383
|
+
<div class="recent-menu-item w-full text-left px-2 py-1.5 rounded-md flex items-start gap-2
|
|
384
|
+
${active ? 'bg-primary/15 text-primary' : 'hover:bg-base-200'}">
|
|
385
|
+
<button type="button" class="flex min-w-0 flex-1 items-start gap-2 text-left" aria-label="打开 ${esc(sessionDisplayTitle(s))}"
|
|
386
|
+
data-session-id="${esc(s.sessionId)}"
|
|
387
|
+
data-session-cwd="${esc(s.cwd || '')}"
|
|
388
|
+
data-session-config-dir="${esc(s.configDir || '')}"
|
|
389
|
+
data-session-agent="${esc(s.agent || 'claude')}"
|
|
390
|
+
data-session-machine="${esc(s.machineId || '')}">
|
|
368
391
|
<span class="flex-shrink-0 mt-0.5">${agentBadge(s.agent)}</span>
|
|
369
392
|
<span class="flex-1 min-w-0">
|
|
370
393
|
<span class="block text-xs font-semibold truncate leading-snug">${esc(sessionDisplayTitle(s))}</span>
|
|
@@ -375,7 +398,13 @@ export function renderRecentSessionsMenu(sessions) {
|
|
|
375
398
|
${machine}
|
|
376
399
|
${active ? '<span class="text-[9px] opacity-70">当前</span>' : ''}
|
|
377
400
|
</span>
|
|
378
|
-
|
|
401
|
+
</button>
|
|
402
|
+
<button type="button" class="recent-remove btn btn-ghost btn-xs min-h-7 h-7 w-7 px-0 flex-shrink-0 text-base-content/45 hover:text-error"
|
|
403
|
+
title="从最近会话移除" aria-label="从最近会话移除 ${esc(sessionDisplayTitle(s))}"
|
|
404
|
+
data-dismiss-recent-session="${esc(s.sessionId)}"
|
|
405
|
+
data-dismiss-recent-agent="${esc(s.agent || 'claude')}"
|
|
406
|
+
data-dismiss-recent-machine="${esc(s.machineId || '')}">×</button>
|
|
407
|
+
</div>`;
|
|
379
408
|
}).join('');
|
|
380
409
|
}
|
|
381
410
|
|