claude-simple 0.2.27 → 0.2.28
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 +21 -2
- package/dist/app.0.2.28.min.js +1220 -0
- package/dist/index.html +2 -2
- package/dist/{style.0.2.27.min.css → style.0.2.28.min.css} +157 -1
- package/package.json +1 -1
- package/public/agent-panel.js +7 -2
- package/public/api.js +7 -4
- package/public/app.js +2 -0
- package/public/chat.js +34 -0
- package/public/notes.js +316 -0
- package/public/router.js +4 -0
- package/public/shell/session-list.js +30 -5
- package/public/shell/session-nav.js +6 -4
- package/public/shell/templates.js +3 -0
- package/public/shell.js +15 -2
- package/public/style.css +156 -0
- package/public/sw.js +1 -1
- package/src/agents/grok.js +16 -0
- package/src/agents/sessions.js +137 -12
- package/src/app.js +31 -15
- package/src/session-history.js +31 -0
- package/dist/app.0.2.27.min.js +0 -1182
package/package.json
CHANGED
package/public/agent-panel.js
CHANGED
|
@@ -48,7 +48,8 @@ function highlight(root) {
|
|
|
48
48
|
|
|
49
49
|
/** Fetch SSE with auth (+ hub machine routing). */
|
|
50
50
|
async function streamSSE(path, body, handlers) {
|
|
51
|
-
|
|
51
|
+
// Meta Agent is hub-owned. Do not attach the currently selected edge.
|
|
52
|
+
const headers = authHeaders({ 'Content-Type': 'application/json' }, { hub: true });
|
|
52
53
|
const res = await fetch(path, {
|
|
53
54
|
method: 'POST',
|
|
54
55
|
headers,
|
|
@@ -466,7 +467,11 @@ function insertAtCursor(ta, text) {
|
|
|
466
467
|
|
|
467
468
|
/** Upload blob → server path; insert [imgN](path) into input. */
|
|
468
469
|
async function uploadAndInsertImage(blob, filename) {
|
|
469
|
-
|
|
470
|
+
// Attachments used by Meta Agent belong to the Hub conversation too.
|
|
471
|
+
const headers = authHeaders(
|
|
472
|
+
{ 'x-filename': filename || `paste-${Date.now()}.png` },
|
|
473
|
+
{ hub: true },
|
|
474
|
+
);
|
|
470
475
|
// raw body upload — do not set Content-Type (browser sets multipart/stream)
|
|
471
476
|
delete headers['Content-Type'];
|
|
472
477
|
const res = await fetch('/api/upload-image', {
|
package/public/api.js
CHANGED
|
@@ -24,8 +24,11 @@ function isHubOnlyPath(path) {
|
|
|
24
24
|
|| p === '/api/machines'
|
|
25
25
|
|| p === '/api/notifications'
|
|
26
26
|
|| p === '/api/notifications/read'
|
|
27
|
-
|| p === '/api/
|
|
28
|
-
|| p.startsWith('/api/
|
|
27
|
+
|| p === '/api/notes'
|
|
28
|
+
|| p.startsWith('/api/notes/')
|
|
29
|
+
|| p === '/api/queue'
|
|
30
|
+
|| p.startsWith('/api/queue/')
|
|
31
|
+
|| p.startsWith('/api/ai/')
|
|
29
32
|
|| p.startsWith('/api/auth/')
|
|
30
33
|
|| p.startsWith('/api/collab/');
|
|
31
34
|
}
|
|
@@ -114,11 +117,11 @@ export function authQuery(extra = {}) {
|
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
/** Headers for raw fetch (e.g. file upload) in hub mode. */
|
|
117
|
-
export function authHeaders(extra = {}) {
|
|
120
|
+
export function authHeaders(extra = {}, opts = {}) {
|
|
118
121
|
const h = { ...extra };
|
|
119
122
|
if (ctx.token) h.Authorization = `Bearer ${ctx.token}`;
|
|
120
123
|
const mid = currentMachineId();
|
|
121
|
-
if (hubMode.peek() && mid) h['X-Machine-Id'] = mid;
|
|
124
|
+
if (!opts.hub && hubMode.peek() && mid) h['X-Machine-Id'] = mid;
|
|
122
125
|
return h;
|
|
123
126
|
}
|
|
124
127
|
|
package/public/app.js
CHANGED
|
@@ -6,6 +6,7 @@ import { initChat } from './chat.js';
|
|
|
6
6
|
import { initFilesTab } from './files.js?v=ui-22';
|
|
7
7
|
import { initGitTab } from './git.js';
|
|
8
8
|
import { initSettings } from './settings.js';
|
|
9
|
+
import { initNotes } from './notes.js';
|
|
9
10
|
import { initTerminal } from './terminal.js';
|
|
10
11
|
import { initImagePaste } from './chat.js';
|
|
11
12
|
import { initMetaAgent } from './agent-panel.js';
|
|
@@ -36,6 +37,7 @@ initChat();
|
|
|
36
37
|
initFilesTab();
|
|
37
38
|
initGitTab();
|
|
38
39
|
initSettings();
|
|
40
|
+
initNotes();
|
|
39
41
|
initTerminal();
|
|
40
42
|
initImagePaste();
|
|
41
43
|
initMetaAgent();
|
package/public/chat.js
CHANGED
|
@@ -441,6 +441,29 @@ function handleWsMessage(msg) {
|
|
|
441
441
|
// session while its subscription is changing. Do not let that frame alter
|
|
442
442
|
// the new session's transcript, processing state, or queue.
|
|
443
443
|
if (msg?.sessionId && ctx.sessionId && msg.sessionId !== ctx.sessionId) return;
|
|
444
|
+
// Fan out note/queue WS events to any module that subscribes via the
|
|
445
|
+
// hub-ws-message custom event (e.g. personal notes, queue outbox). Done
|
|
446
|
+
// before session-scope filtering so global widgets (notes page) still
|
|
447
|
+
// receive updates for non-active sessions. We also dispatch the
|
|
448
|
+
// type-specific hub:note-* events used by the legacy notes page.
|
|
449
|
+
if (msg && typeof msg.type === 'string' && (
|
|
450
|
+
msg.type === 'note.added' ||
|
|
451
|
+
msg.type === 'note.updated' ||
|
|
452
|
+
msg.type === 'note.removed' ||
|
|
453
|
+
msg.type === 'queue.item.added' ||
|
|
454
|
+
msg.type === 'queue.item.updated' ||
|
|
455
|
+
msg.type === 'queue.item.removed' ||
|
|
456
|
+
msg.type === 'queue.item.moved' ||
|
|
457
|
+
msg.type === 'queue.item.sending'
|
|
458
|
+
)) {
|
|
459
|
+
window.dispatchEvent(new CustomEvent('hub-ws-message', { detail: msg }));
|
|
460
|
+
const noteEvent = ({
|
|
461
|
+
'note.added': 'hub:note-added',
|
|
462
|
+
'note.updated': 'hub:note-updated',
|
|
463
|
+
'note.removed': 'hub:note-removed',
|
|
464
|
+
})[msg.type];
|
|
465
|
+
if (noteEvent) window.dispatchEvent(new CustomEvent(noteEvent, { detail: msg }));
|
|
466
|
+
}
|
|
444
467
|
// Server outbox may pack multiple events into one frame
|
|
445
468
|
if (msg.type === 'batch' && Array.isArray(msg.items)) {
|
|
446
469
|
for (const item of msg.items) handleWsMessage(item);
|
|
@@ -551,10 +574,21 @@ function handleWsMessage(msg) {
|
|
|
551
574
|
|
|
552
575
|
// ── Live streaming (Grok / future agents) ─────────────────────────────────
|
|
553
576
|
if (msg.type === 'assistant_stream_start') {
|
|
577
|
+
// A replay can arrive before the edge's run-status snapshot. Mark the
|
|
578
|
+
// turn busy as soon as its first stream frame reaches us so the concurrent
|
|
579
|
+
// history read cannot repaint the transcript over live output.
|
|
580
|
+
if (!isProcessing.peek()) {
|
|
581
|
+
isProcessing.value = true;
|
|
582
|
+
syncWakeLock();
|
|
583
|
+
}
|
|
554
584
|
ensureAssistantStream(msg.ts);
|
|
555
585
|
return;
|
|
556
586
|
}
|
|
557
587
|
if (msg.type === 'assistant_delta') {
|
|
588
|
+
if (!isProcessing.peek()) {
|
|
589
|
+
isProcessing.value = true;
|
|
590
|
+
syncWakeLock();
|
|
591
|
+
}
|
|
558
592
|
appendAssistantDelta(msg.data || '');
|
|
559
593
|
return;
|
|
560
594
|
}
|
package/public/notes.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// notes.js — personal notes in the main pane (sidebar stays visible),
|
|
2
|
+
// matching the Flutter HomePage "记事" chip: list + add/edit/delete cards
|
|
3
|
+
// over the same /api/notes store.
|
|
4
|
+
|
|
5
|
+
import { api } from './api.js';
|
|
6
|
+
import { ctx } from './state.js';
|
|
7
|
+
|
|
8
|
+
let notesCache = null;
|
|
9
|
+
let notesEditingId = null;
|
|
10
|
+
let notesBooted = false;
|
|
11
|
+
let notesShowForm = false;
|
|
12
|
+
|
|
13
|
+
export function openNotes() {
|
|
14
|
+
ensureNotesBoot();
|
|
15
|
+
const page = document.getElementById('notes-page');
|
|
16
|
+
if (!page) return;
|
|
17
|
+
page.hidden = false;
|
|
18
|
+
document.getElementById('welcome')?.classList.add('hidden');
|
|
19
|
+
const pv = document.getElementById('project-view');
|
|
20
|
+
if (pv) {
|
|
21
|
+
pv.classList.add('hidden');
|
|
22
|
+
pv.style.display = 'none';
|
|
23
|
+
}
|
|
24
|
+
document.documentElement.classList.remove('notes-open');
|
|
25
|
+
if ((location.hash || '') !== '#/notes') location.hash = '/notes';
|
|
26
|
+
loadNotes({ keepList: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function closeNotes({ restore = true } = {}) {
|
|
30
|
+
const page = document.getElementById('notes-page');
|
|
31
|
+
if (!page) return;
|
|
32
|
+
page.hidden = true;
|
|
33
|
+
document.documentElement.classList.remove('notes-open');
|
|
34
|
+
notesEditingId = null;
|
|
35
|
+
notesShowForm = false;
|
|
36
|
+
const form = document.getElementById('notes-add-form');
|
|
37
|
+
if (form) form.hidden = true;
|
|
38
|
+
if (!restore) return;
|
|
39
|
+
if (ctx.sessionId) {
|
|
40
|
+
const pv = document.getElementById('project-view');
|
|
41
|
+
if (pv) {
|
|
42
|
+
pv.classList.remove('hidden');
|
|
43
|
+
pv.style.cssText = 'display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden';
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
const welcome = document.getElementById('welcome');
|
|
47
|
+
if (welcome) {
|
|
48
|
+
welcome.classList.remove('hidden');
|
|
49
|
+
welcome.style.display = '';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ensureNotesBoot() {
|
|
55
|
+
if (notesBooted) return;
|
|
56
|
+
notesBooted = true;
|
|
57
|
+
const main = document.getElementById('main') || document.getElementById('root');
|
|
58
|
+
main.insertAdjacentHTML('beforeend', notesMarkup());
|
|
59
|
+
document.addEventListener('router:notes', () => openNotes());
|
|
60
|
+
|
|
61
|
+
const page = document.getElementById('notes-page');
|
|
62
|
+
page.addEventListener('click', (event) => {
|
|
63
|
+
const close = event.target.closest('[data-notes-close]');
|
|
64
|
+
if (close) {
|
|
65
|
+
closeNotes();
|
|
66
|
+
if ((location.hash || '') === '#/notes') history.back();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const add = event.target.closest('[data-notes-add]');
|
|
70
|
+
if (add) {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
notesShowForm = !notesShowForm;
|
|
73
|
+
const form = document.getElementById('notes-add-form');
|
|
74
|
+
if (form) form.hidden = !notesShowForm;
|
|
75
|
+
add.textContent = notesShowForm ? '收起' : '+ 新增';
|
|
76
|
+
if (notesShowForm) document.getElementById('notes-add-text')?.focus();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const submit = event.target.closest('[data-notes-submit]');
|
|
80
|
+
if (submit) {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
submitNote();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const edit = event.target.closest('[data-notes-edit]');
|
|
86
|
+
if (edit) {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
startEditNote(edit.dataset.notesEdit);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const save = event.target.closest('[data-notes-save]');
|
|
92
|
+
if (save) {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
saveEditNote();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const cancel = event.target.closest('[data-notes-cancel]');
|
|
98
|
+
if (cancel) {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
if (notesEditingId) cancelEditNote();
|
|
101
|
+
else {
|
|
102
|
+
notesShowForm = false;
|
|
103
|
+
const form = document.getElementById('notes-add-form');
|
|
104
|
+
if (form) form.hidden = true;
|
|
105
|
+
const addBtn = document.querySelector('[data-notes-add]');
|
|
106
|
+
if (addBtn) addBtn.textContent = '+ 新增';
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const remove = event.target.closest('[data-notes-remove]');
|
|
111
|
+
if (remove) {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
const id = remove.dataset.notesRemove;
|
|
114
|
+
if (confirm('删除这条记录?')) deleteNote(id);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Submit on Enter inside the textarea (Cmd/Ctrl+Enter), plain Enter inserts a newline.
|
|
119
|
+
const ta = document.getElementById('notes-add-text');
|
|
120
|
+
if (ta) {
|
|
121
|
+
ta.addEventListener('keydown', (e) => {
|
|
122
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
submitNote();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function notesMarkup() {
|
|
131
|
+
return `
|
|
132
|
+
<section id="notes-page" class="notes-page" hidden>
|
|
133
|
+
<header class="notes-header">
|
|
134
|
+
<div>
|
|
135
|
+
<h2>记事</h2>
|
|
136
|
+
<p id="notes-count" class="notes-count">多端实时同步</p>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="notes-header-actions">
|
|
139
|
+
<button class="btn btn-sm btn-ghost border border-base-300" data-notes-add>+ 新增</button>
|
|
140
|
+
<button class="btn btn-sm btn-ghost border border-base-300" data-notes-close>关闭</button>
|
|
141
|
+
</div>
|
|
142
|
+
</header>
|
|
143
|
+
<form id="notes-add-form" class="notes-add" hidden>
|
|
144
|
+
<textarea id="notes-add-text" placeholder="记一条… (Cmd/Ctrl+Enter 提交)" rows="3" maxlength="8000"></textarea>
|
|
145
|
+
<div class="notes-form-actions">
|
|
146
|
+
<button class="btn btn-sm btn-primary" data-notes-submit>保存</button>
|
|
147
|
+
<button class="btn btn-sm btn-ghost" type="button" data-notes-cancel>取消</button>
|
|
148
|
+
</div>
|
|
149
|
+
</form>
|
|
150
|
+
<ul id="notes-list" class="notes-list">
|
|
151
|
+
<li class="notes-empty">加载中…</li>
|
|
152
|
+
</ul>
|
|
153
|
+
</section>`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function loadNotes({ keepList = false } = {}) {
|
|
157
|
+
const list = document.getElementById('notes-list');
|
|
158
|
+
if (!list) return;
|
|
159
|
+
if (!keepList || !notesCache) {
|
|
160
|
+
list.innerHTML = '<li class="notes-empty">加载中…</li>';
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const res = await api('GET', '/api/notes?limit=200', undefined, undefined, { hub: true });
|
|
164
|
+
notesCache = res?.items || [];
|
|
165
|
+
renderNotes();
|
|
166
|
+
} catch (err) {
|
|
167
|
+
list.innerHTML = `<li class="notes-empty notes-error">加载失败:${err?.message || err}</li>`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function renderNotes() {
|
|
172
|
+
const list = document.getElementById('notes-list');
|
|
173
|
+
if (!list) return;
|
|
174
|
+
const count = document.getElementById('notes-count');
|
|
175
|
+
if (count) {
|
|
176
|
+
count.textContent = notesCache?.length
|
|
177
|
+
? `${notesCache.length} 条记事 · 多端实时同步`
|
|
178
|
+
: '多端实时同步';
|
|
179
|
+
}
|
|
180
|
+
const form = document.getElementById('notes-add-form');
|
|
181
|
+
if (form) form.hidden = !notesShowForm;
|
|
182
|
+
if (!notesCache || notesCache.length === 0) {
|
|
183
|
+
list.innerHTML = '<li class="notes-empty">还没有记事。点上面的「新增」,或者在 Meta Agent 里输入「记: xxx」</li>';
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
list.innerHTML = '';
|
|
187
|
+
for (const note of notesCache) {
|
|
188
|
+
const li = document.createElement('li');
|
|
189
|
+
li.className = 'notes-item';
|
|
190
|
+
li.dataset.id = note.id;
|
|
191
|
+
if (notesEditingId === note.id) {
|
|
192
|
+
li.innerHTML = `
|
|
193
|
+
<textarea class="notes-edit-text" rows="4">${escapeHtml(note.text)}</textarea>
|
|
194
|
+
<div class="notes-item-actions">
|
|
195
|
+
<button class="btn btn-sm btn-primary" data-notes-save>保存</button>
|
|
196
|
+
<button class="btn btn-sm btn-ghost" data-notes-cancel>取消</button>
|
|
197
|
+
</div>`;
|
|
198
|
+
} else {
|
|
199
|
+
li.innerHTML = `
|
|
200
|
+
<div class="notes-text">${escapeHtml(note.text)}</div>
|
|
201
|
+
<div class="notes-item-foot">
|
|
202
|
+
<span class="notes-meta">${formatTime(note.createdAt)}</span>
|
|
203
|
+
<div class="notes-item-actions">
|
|
204
|
+
<button class="btn btn-ghost btn-xs" data-notes-edit="${note.id}" title="编辑">编辑</button>
|
|
205
|
+
<button class="btn btn-ghost btn-xs text-error" data-notes-remove="${note.id}" title="删除">删除</button>
|
|
206
|
+
</div>
|
|
207
|
+
</div>`;
|
|
208
|
+
}
|
|
209
|
+
list.appendChild(li);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function startEditNote(id) {
|
|
214
|
+
notesEditingId = id;
|
|
215
|
+
renderNotes();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function cancelEditNote() {
|
|
219
|
+
notesEditingId = null;
|
|
220
|
+
renderNotes();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function saveEditNote() {
|
|
224
|
+
const list = document.getElementById('notes-list');
|
|
225
|
+
if (!list) return;
|
|
226
|
+
const li = list.querySelector(`li[data-id="${cssEscape(notesEditingId)}"]`);
|
|
227
|
+
if (!li) return;
|
|
228
|
+
const ta = li.querySelector('.notes-edit-text');
|
|
229
|
+
if (!ta) return;
|
|
230
|
+
const text = (ta.value || '').trim();
|
|
231
|
+
if (!text) {
|
|
232
|
+
if (confirm('文本为空,删除这条记录?')) {
|
|
233
|
+
await deleteNote(notesEditingId);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
await api('PATCH', `/api/notes/${notesEditingId}`, { text }, undefined, { hub: true });
|
|
239
|
+
notesEditingId = null;
|
|
240
|
+
await loadNotes({ keepList: true });
|
|
241
|
+
} catch (err) {
|
|
242
|
+
alert('保存失败:' + (err?.message || err));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function submitNote() {
|
|
247
|
+
const ta = document.getElementById('notes-add-text');
|
|
248
|
+
if (!ta) return;
|
|
249
|
+
const text = (ta.value || '').trim();
|
|
250
|
+
if (!text) {
|
|
251
|
+
ta.focus();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
await api('POST', '/api/notes', { text }, undefined, { hub: true });
|
|
256
|
+
ta.value = '';
|
|
257
|
+
notesShowForm = false;
|
|
258
|
+
const form = document.getElementById('notes-add-form');
|
|
259
|
+
if (form) form.hidden = true;
|
|
260
|
+
const addBtn = document.querySelector('[data-notes-add]');
|
|
261
|
+
if (addBtn) addBtn.textContent = '+ 新增';
|
|
262
|
+
await loadNotes({ keepList: true });
|
|
263
|
+
} catch (err) {
|
|
264
|
+
alert('保存失败:' + (err?.message || err));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function deleteNote(id) {
|
|
269
|
+
try {
|
|
270
|
+
await api('DELETE', `/api/notes/${id}`, undefined, undefined, { hub: true });
|
|
271
|
+
if (notesEditingId === id) notesEditingId = null;
|
|
272
|
+
await loadNotes({ keepList: true });
|
|
273
|
+
} catch (err) {
|
|
274
|
+
alert('删除失败:' + (err?.message || err));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function formatTime(ms) {
|
|
279
|
+
if (!ms) return '';
|
|
280
|
+
const d = new Date(ms);
|
|
281
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
282
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// cssEscape polyfill (modern browsers expose CSS.escape on globalThis).
|
|
286
|
+
function cssEscape(value) {
|
|
287
|
+
if (typeof CSS !== 'undefined' && CSS.escape) return CSS.escape(value);
|
|
288
|
+
return String(value).replace(/[^a-zA-Z0-9_-]/g, (c) => '\\' + c);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function escapeHtml(value) {
|
|
292
|
+
return String(value ?? '')
|
|
293
|
+
.replace(/&/g, '&')
|
|
294
|
+
.replace(/</g, '<')
|
|
295
|
+
.replace(/>/g, '>')
|
|
296
|
+
.replace(/"/g, '"')
|
|
297
|
+
.replace(/'/g, ''');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Re-render if a Hub note event arrives while the page is open. The events
|
|
301
|
+
// come from the chat WebSocket, so we listen on ctx.ws indirectly via state.
|
|
302
|
+
if (typeof window !== 'undefined') {
|
|
303
|
+
window.addEventListener('hub:note-added', () => {
|
|
304
|
+
if (!document.getElementById('notes-page')?.hidden) loadNotes();
|
|
305
|
+
});
|
|
306
|
+
window.addEventListener('hub:note-updated', () => {
|
|
307
|
+
if (!document.getElementById('notes-page')?.hidden) loadNotes();
|
|
308
|
+
});
|
|
309
|
+
window.addEventListener('hub:note-removed', () => {
|
|
310
|
+
if (!document.getElementById('notes-page')?.hidden) loadNotes();
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function initNotes() {
|
|
315
|
+
ensureNotesBoot();
|
|
316
|
+
}
|
package/public/router.js
CHANGED
|
@@ -59,6 +59,10 @@ function applyHash(path) {
|
|
|
59
59
|
document.dispatchEvent(new CustomEvent('router:settings'));
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
|
+
if (pathname === '/notes') {
|
|
63
|
+
document.dispatchEvent(new CustomEvent('router:notes'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
62
66
|
// /session/:id or /session/:id/:tab
|
|
63
67
|
const parsed = parseHashPath(path);
|
|
64
68
|
if (!parsed) {
|
|
@@ -16,6 +16,28 @@ import { formatTime, agentBadge, agentCountPills, shortPath } from './util.js';
|
|
|
16
16
|
import { normalizeMarkdownText } from '../markdown.js';
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
function sessionsFingerprint(list) {
|
|
20
|
+
if (!Array.isArray(list) || !list.length) return '';
|
|
21
|
+
return list.map(s => `${s.sessionId}:${s.updatedAt || 0}:${s.turnCount || 0}:${s.agent || ''}`).join('|');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function markActiveSessionRow(sessionId, agent) {
|
|
25
|
+
const wantAgent = agent || 'claude';
|
|
26
|
+
document.querySelectorAll('#session-list .session-row').forEach(row => {
|
|
27
|
+
const on = row.dataset.sessionId === sessionId
|
|
28
|
+
&& (row.dataset.sessionAgent || 'claude') === wantAgent;
|
|
29
|
+
row.classList.toggle('border-primary', on);
|
|
30
|
+
row.classList.toggle('bg-primary/5', on);
|
|
31
|
+
row.classList.toggle('border-transparent', !on);
|
|
32
|
+
const title = row.querySelector('.session-title-text');
|
|
33
|
+
if (title) {
|
|
34
|
+
title.classList.toggle('text-primary', on);
|
|
35
|
+
title.classList.toggle('font-medium', on);
|
|
36
|
+
title.classList.toggle('text-base-content/85', !on);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
function normalizeSessions(v) {
|
|
20
42
|
if (Array.isArray(v)) return v;
|
|
21
43
|
if (v && Array.isArray(v.results)) return v.results;
|
|
@@ -67,9 +89,10 @@ function sessionsQuery({ hubIndex = false } = {}) {
|
|
|
67
89
|
// Project index is small and should remain complete; the Hub/browser cache
|
|
68
90
|
// keeps this inexpensive after the first fetch.
|
|
69
91
|
q.set('limit', '2000');
|
|
70
|
-
//
|
|
71
|
-
// Edge
|
|
72
|
-
|
|
92
|
+
// A Hub keeps this catalog locally from Edge deltas. Always use it in Hub
|
|
93
|
+
// mode: an Edge scan can be slow while an agent is busy, and must never make
|
|
94
|
+
// the sidebar (or a page refresh) fail with a 502.
|
|
95
|
+
if (hubMode.peek() || hubIndex) q.set('index', '1');
|
|
73
96
|
const s = q.toString();
|
|
74
97
|
return s ? `/api/sessions?${s}` : '/api/sessions';
|
|
75
98
|
}
|
|
@@ -93,8 +116,10 @@ export async function loadAllSessions({ waitFresh = true, hubIndex = false } = {
|
|
|
93
116
|
const fresh = await api('GET', path, undefined, undefined, { etag: true });
|
|
94
117
|
if (fresh === NOT_MODIFIED) return sessionsData.peek();
|
|
95
118
|
const list = normalizeSessions(fresh);
|
|
96
|
-
sessionsData.
|
|
97
|
-
|
|
119
|
+
if (sessionsFingerprint(list) !== sessionsFingerprint(sessionsData.peek())) {
|
|
120
|
+
sessionsData.value = list;
|
|
121
|
+
setCachedSessions(list);
|
|
122
|
+
}
|
|
98
123
|
return list;
|
|
99
124
|
};
|
|
100
125
|
|
|
@@ -111,6 +111,7 @@ export function goHome() {
|
|
|
111
111
|
|
|
112
112
|
const topbar = $('topbar-project');
|
|
113
113
|
if (topbar) topbar.textContent = 'Select a session';
|
|
114
|
+
import('../notes.js').then(({ closeNotes }) => closeNotes({ restore: false })).catch(() => {});
|
|
114
115
|
const welcome = $('welcome');
|
|
115
116
|
if (welcome) {
|
|
116
117
|
welcome.classList.remove('hidden');
|
|
@@ -152,6 +153,8 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
152
153
|
const resolvedCwd = cwd || fromList?.cwd || fromLast?.cwd || null;
|
|
153
154
|
const resolvedConfig = configDir || fromList?.configDir || fromLast?.configDir || null;
|
|
154
155
|
|
|
156
|
+
import('../notes.js').then(({ closeNotes }) => closeNotes({ restore: false })).catch(() => {});
|
|
157
|
+
|
|
155
158
|
// Capture before changing ctx, then restore only after the new identity is set.
|
|
156
159
|
stashComposerDraft();
|
|
157
160
|
parkQueuedMessages();
|
|
@@ -199,10 +202,9 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
199
202
|
// arrive immediately on socket-open and must belong to the new history.
|
|
200
203
|
connectWS();
|
|
201
204
|
switchTab(targetTab);
|
|
202
|
-
{
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
205
|
+
import('./session-list.js').then(({ markActiveSessionRow }) => {
|
|
206
|
+
markActiveSessionRow(sid, resolvedAgent);
|
|
207
|
+
}).catch(() => {});
|
|
206
208
|
|
|
207
209
|
setLastSessionContext({
|
|
208
210
|
sessionId: sid,
|
|
@@ -85,6 +85,9 @@ export const AppShell = (opts = {}) => {
|
|
|
85
85
|
<button id="btn-meta-agent" class="btn btn-ghost btn-xs border border-primary/40 text-primary topbar-icon-btn" title="Meta Agent — 报告 / 问答 / VLM" aria-label="打开 Meta Agent">
|
|
86
86
|
<svg class="topbar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="m12 2 1.6 6.4L20 10l-6.4 1.6L12 18l-1.6-6.4L4 10l6.4-1.6L12 2Z"/><path d="m19 16 .7 2.3L22 19l-2.3.7L19 22l-.7-2.3L16 19l2.3-.7L19 16Z"/></svg><span class="topbar-icon-label">Agent</span>
|
|
87
87
|
</button>
|
|
88
|
+
<button id="btn-notes" class="btn btn-ghost btn-xs border border-base-300 topbar-icon-btn" title="记事" aria-label="记事">
|
|
89
|
+
<svg class="topbar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><path d="M5 4h11l3 3v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z"/><path d="M8 9h7M8 13h7M8 17h4"/></svg><span class="topbar-icon-label">记事</span>
|
|
90
|
+
</button>
|
|
88
91
|
<button id="btn-new-session" class="btn btn-ghost btn-xs border border-base-300 topbar-icon-btn" title="New session" aria-label="新建会话">
|
|
89
92
|
<svg class="topbar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M12 8v8M8 12h8"/></svg><span class="topbar-icon-label">New</span>
|
|
90
93
|
</button>
|
package/public/shell.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from './chat.js';
|
|
23
23
|
import { syncHash, currentSessionShareUrl } from './router.js';
|
|
24
24
|
import { initSettings, openSettings } from './settings.js';
|
|
25
|
+
import { openNotes } from './notes.js';
|
|
25
26
|
import { openMetaAgent, toggleMetaAgent } from './agent-panel.js';
|
|
26
27
|
import {
|
|
27
28
|
initNotifications, refreshNotifications, toggleNotificationsMenu, closeNotificationsMenu,
|
|
@@ -224,14 +225,25 @@ export function initShell(opts = {}) {
|
|
|
224
225
|
void filteredSessions.value;
|
|
225
226
|
void projectGroups.value;
|
|
226
227
|
void sidebarView.value;
|
|
227
|
-
void activityLoading.value;
|
|
228
228
|
void activityHits.value;
|
|
229
229
|
void sessionMetaMap.value;
|
|
230
230
|
const el = $('session-list');
|
|
231
231
|
if (!el) return;
|
|
232
232
|
// Keep inline rename input mounted until blur/Enter
|
|
233
233
|
if (el.querySelector('.session-rename-input')) return;
|
|
234
|
-
|
|
234
|
+
const html = renderSessionList();
|
|
235
|
+
if (el._lastHtml === html) {
|
|
236
|
+
syncSidebarChrome();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const scroll = el.scrollTop;
|
|
240
|
+
el.innerHTML = html;
|
|
241
|
+
el._lastHtml = html;
|
|
242
|
+
el.scrollTop = scroll;
|
|
243
|
+
syncSidebarChrome();
|
|
244
|
+
});
|
|
245
|
+
effect(() => {
|
|
246
|
+
void activityLoading.value;
|
|
235
247
|
syncSidebarChrome();
|
|
236
248
|
});
|
|
237
249
|
|
|
@@ -443,6 +455,7 @@ export function initShell(opts = {}) {
|
|
|
443
455
|
document.getElementById('btn-pwa-install')?.classList.remove('hidden');
|
|
444
456
|
});
|
|
445
457
|
delegate.on('click', '#btn-settings', openSettings);
|
|
458
|
+
delegate.on('click', '#btn-notes', () => openNotes());
|
|
446
459
|
delegate.on('click', '#btn-collapse-chat-chrome', () => setChatChromeCollapsed(true));
|
|
447
460
|
delegate.on('click', '#btn-expand-chat-chrome', () => setChatChromeCollapsed(false));
|
|
448
461
|
delegate.on('click', '#btn-meta-agent', () => toggleMetaAgent());
|