claude-simple 0.2.6 → 0.2.8
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.8.min.js +1080 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.8.min.css +1598 -0
- package/gateway.js +0 -0
- package/package.json +1 -1
- package/public/addon-fit.min.js +0 -0
- package/public/agent-panel.js +245 -1
- package/public/api.js +4 -0
- package/public/app.js +0 -0
- package/public/base.css +0 -0
- package/public/cache.js +0 -0
- package/public/chat.js +118 -17
- package/public/files.js +0 -0
- package/public/git.js +0 -0
- 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 +2 -2
- package/public/lib.js +0 -0
- package/public/manifest.webmanifest +0 -0
- package/public/mobile.js +0 -0
- package/public/notifications.js +215 -0
- package/public/offline.html +0 -0
- package/public/pwa.js +0 -0
- package/public/router.js +0 -0
- package/public/settings.js +0 -0
- package/public/shell/boot.js +0 -0
- package/public/shell/dashboard.js +72 -23
- package/public/shell/hub.js +0 -0
- package/public/shell/layout.js +41 -12
- package/public/shell/notes.js +0 -0
- package/public/shell/session-context.js +0 -0
- package/public/shell/session-list.js +27 -28
- package/public/shell/session-nav.js +36 -1
- package/public/shell/templates.js +45 -7
- package/public/shell/util.js +0 -0
- package/public/shell.js +56 -7
- package/public/state.js +53 -13
- package/public/style.css +100 -6
- package/public/sw.js +2 -2
- package/public/terminal.js +77 -3
- package/public/wake-lock.js +0 -0
- package/public/xterm.min.css +0 -0
- package/public/xterm.min.js +0 -0
- package/src/agents/claude.js +1 -1
- package/src/agents/codex.js +1 -0
- package/src/agents/grok.js +180 -298
- package/src/agents/meta-agent.js +332 -1
- package/src/agents/models.js +0 -0
- package/src/agents/sessions.js +106 -26
- package/src/agents/stream-batch.js +0 -0
- package/src/app.js +88 -11
- package/src/auto-update.js +0 -0
- package/src/runtime/approvals.js +0 -0
- package/src/runtime/outbox.js +0 -0
- package/src/runtime/protocol.js +0 -0
- package/dist/app.0.2.6.min.js +0 -953
- package/dist/style.0.2.6.min.css +0 -1504
package/gateway.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/public/addon-fit.min.js
CHANGED
|
File without changes
|
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
|
@@ -22,6 +22,10 @@ function isHubOnlyPath(path) {
|
|
|
22
22
|
const p = path.split('?')[0];
|
|
23
23
|
return p === '/api/hub'
|
|
24
24
|
|| p === '/api/machines'
|
|
25
|
+
|| p === '/api/notifications'
|
|
26
|
+
|| p === '/api/notifications/read'
|
|
27
|
+
|| p === '/api/ai/notes'
|
|
28
|
+
|| p.startsWith('/api/ai/notes/')
|
|
25
29
|
|| p.startsWith('/api/auth/');
|
|
26
30
|
}
|
|
27
31
|
|
package/public/app.js
CHANGED
|
File without changes
|
package/public/base.css
CHANGED
|
File without changes
|
package/public/cache.js
CHANGED
|
File without changes
|
package/public/chat.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { watch, delegate, esc, $ } from './lib.js';
|
|
3
3
|
import { ctx, isProcessing, currentProject, currentTab, currentModel, currentEffort, currentPermission,
|
|
4
4
|
currentAgent, setAgent, AGENT_LABELS, getDefaultModel, getModelsForAgent, getEffortsForModel,
|
|
5
|
-
addCustomModel, chatDensity, wsStatus } from './state.js';
|
|
5
|
+
addCustomModel, setCurrentModel, chatDensity, wsStatus } from './state.js';
|
|
6
6
|
import { sendWs, api, authHeaders, flushWsQueue, clearWsQueue } from './api.js';
|
|
7
7
|
import { initWakeLock, setWakeLockDesired } from './wake-lock.js';
|
|
8
|
+
import { pushNotification } from './notifications.js';
|
|
8
9
|
|
|
9
10
|
function assistantLabel() {
|
|
10
11
|
return AGENT_LABELS[currentAgent.peek()] || 'Assistant';
|
|
@@ -32,8 +33,57 @@ let lifecycleBound = false;
|
|
|
32
33
|
let sawDisconnect = false;
|
|
33
34
|
let lastWsMachine = null;
|
|
34
35
|
let reconnectQuiet = false;
|
|
35
|
-
/**
|
|
36
|
-
let
|
|
36
|
+
/** FIFO messages deliberately queued while a turn is running. */
|
|
37
|
+
let pendingUserTexts = [];
|
|
38
|
+
// Composer state belongs to a conversation, never to the shared textarea.
|
|
39
|
+
// Keeping this in memory deliberately avoids persisting potentially sensitive
|
|
40
|
+
// prompts to disk while still making session switches lossless.
|
|
41
|
+
const composerDrafts = new Map();
|
|
42
|
+
|
|
43
|
+
function draftKey({ sessionId = ctx.sessionId, agent = ctx.agent, machineId = ctx.machineId } = {}) {
|
|
44
|
+
if (!sessionId) return null;
|
|
45
|
+
return `${machineId || ''}:${agent || 'claude'}:${sessionId}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Save the visible, unsent prompt under its current session identity. */
|
|
49
|
+
export function stashComposerDraft(identity) {
|
|
50
|
+
const key = draftKey(identity);
|
|
51
|
+
const input = $('chat-input');
|
|
52
|
+
if (!key || !input) return;
|
|
53
|
+
if (input.value) composerDrafts.set(key, input.value);
|
|
54
|
+
else composerDrafts.delete(key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Restore only the target session's draft; a missing one must mean blank. */
|
|
58
|
+
export function restoreComposerDraft(identity) {
|
|
59
|
+
const input = $('chat-input');
|
|
60
|
+
if (!input) return;
|
|
61
|
+
input.value = composerDrafts.get(draftKey(identity)) || '';
|
|
62
|
+
autoResize(input);
|
|
63
|
+
updateInputMode(input.value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Leaving a session turns its queued sends back into a visible draft. This is
|
|
68
|
+
* intentionally conservative: a background completion must never send work
|
|
69
|
+
* into whichever session the user is viewing now.
|
|
70
|
+
*/
|
|
71
|
+
export function parkQueuedMessages(identity) {
|
|
72
|
+
const key = draftKey(identity);
|
|
73
|
+
if (!key) return;
|
|
74
|
+
const queued = pendingUserTexts.filter(item => item.key === key);
|
|
75
|
+
if (!queued.length) return;
|
|
76
|
+
pendingUserTexts = pendingUserTexts.filter(item => item.key !== key);
|
|
77
|
+
const existing = composerDrafts.get(key) || '';
|
|
78
|
+
composerDrafts.set(key, [existing, ...queued.map(item => item.text)].filter(Boolean).join('\n\n'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A run may continue server-side after navigation, but it is no longer active UI state. */
|
|
82
|
+
export function detachProcessingForSessionSwitch() {
|
|
83
|
+
if (!isProcessing.peek()) return;
|
|
84
|
+
isProcessing.value = false;
|
|
85
|
+
syncWakeLock();
|
|
86
|
+
}
|
|
37
87
|
|
|
38
88
|
function bindSocketLifecycle() {
|
|
39
89
|
if (lifecycleBound) return;
|
|
@@ -238,16 +288,27 @@ export function connectWS(opts = {}) {
|
|
|
238
288
|
}
|
|
239
289
|
|
|
240
290
|
function flushPendingUserText() {
|
|
241
|
-
if (!
|
|
242
|
-
const
|
|
243
|
-
|
|
291
|
+
if (!pendingUserTexts.length || isProcessing.peek()) return;
|
|
292
|
+
const currentKey = draftKey();
|
|
293
|
+
const index = pendingUserTexts.findIndex(item => item.key === currentKey);
|
|
294
|
+
// Queues belonging to another session stay parked until that session is
|
|
295
|
+
// opened (or are converted to a draft during navigation).
|
|
296
|
+
if (index < 0) return;
|
|
297
|
+
const [pending] = pendingUserTexts.splice(index, 1);
|
|
244
298
|
const input = $('chat-input');
|
|
245
299
|
if (input) {
|
|
246
|
-
input.value = text;
|
|
300
|
+
input.value = pending.text;
|
|
247
301
|
autoResize(input);
|
|
248
302
|
}
|
|
249
|
-
// Defer so isProcessing watch can re-enable send button first
|
|
250
|
-
|
|
303
|
+
// Defer so isProcessing watch can re-enable send button first. Re-check the
|
|
304
|
+
// identity inside the callback: a click can switch sessions in this tiny gap.
|
|
305
|
+
setTimeout(() => {
|
|
306
|
+
if (pending.key !== draftKey() || isProcessing.peek()) {
|
|
307
|
+
composerDrafts.set(pending.key, pending.text);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
sendMessage();
|
|
311
|
+
}, 0);
|
|
251
312
|
}
|
|
252
313
|
|
|
253
314
|
function markTurnDone() {
|
|
@@ -257,18 +318,30 @@ function markTurnDone() {
|
|
|
257
318
|
}
|
|
258
319
|
|
|
259
320
|
function handleWsMessage(msg) {
|
|
321
|
+
// The socket can still receive a final frame from the previously viewed
|
|
322
|
+
// session while its subscription is changing. Do not let that frame alter
|
|
323
|
+
// the new session's transcript, processing state, or queue.
|
|
324
|
+
if (msg?.sessionId && ctx.sessionId && msg.sessionId !== ctx.sessionId) return;
|
|
260
325
|
// Server outbox may pack multiple events into one frame
|
|
261
326
|
if (msg.type === 'batch' && Array.isArray(msg.items)) {
|
|
262
327
|
for (const item of msg.items) handleWsMessage(item);
|
|
263
328
|
return;
|
|
264
329
|
}
|
|
265
330
|
if (msg.type === 'session-created') {
|
|
331
|
+
const selectedModel = currentModel.peek();
|
|
266
332
|
ctx.sessionId = msg.sessionId;
|
|
267
|
-
if (msg.agent) setAgent(msg.agent);
|
|
333
|
+
if (msg.agent && msg.agent !== currentAgent.peek()) setAgent(msg.agent);
|
|
334
|
+
// The first turn selected a model before a session id existed. Bind that
|
|
335
|
+
// choice as soon as the edge creates the session.
|
|
336
|
+
setCurrentModel(selectedModel, msg.agent || currentAgent.peek(), msg.sessionId);
|
|
268
337
|
// Notify shell to refresh session list — use custom event to avoid circular import
|
|
269
338
|
document.dispatchEvent(new CustomEvent('sessions-changed'));
|
|
270
339
|
return;
|
|
271
340
|
}
|
|
341
|
+
if (msg.type === 'notification' && msg.notification) {
|
|
342
|
+
pushNotification(msg.notification);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
272
345
|
if (msg.type === 'result') {
|
|
273
346
|
finalizeThoughtStream();
|
|
274
347
|
finalizeAssistantStream();
|
|
@@ -515,8 +588,7 @@ function handleSlashCommand(text) {
|
|
|
515
588
|
if (a !== currentAgent.peek()) {
|
|
516
589
|
ctx.sessionId = null;
|
|
517
590
|
setAgent(a);
|
|
518
|
-
|
|
519
|
-
localStorage.setItem('model', currentModel.peek());
|
|
591
|
+
setCurrentModel(getDefaultModel(a));
|
|
520
592
|
document.dispatchEvent(new CustomEvent('agent-changed'));
|
|
521
593
|
appendSystemMsg(`Agent → ${AGENT_LABELS[a]} · model ${currentModel.peek()} · new session`);
|
|
522
594
|
} else {
|
|
@@ -536,8 +608,7 @@ function handleSlashCommand(text) {
|
|
|
536
608
|
// Persist free-form ids so they stay in the model dropdown
|
|
537
609
|
const known = getModelsForAgent(agent).some(m => m.value === resolved);
|
|
538
610
|
if (!known) addCustomModel(agent, resolved);
|
|
539
|
-
|
|
540
|
-
localStorage.setItem('model', resolved);
|
|
611
|
+
setCurrentModel(resolved);
|
|
541
612
|
appendSystemMsg(`Model set to ${resolved}${known ? '' : ' (saved as custom ★)'}`);
|
|
542
613
|
document.dispatchEvent(new CustomEvent('agent-changed'));
|
|
543
614
|
return true;
|
|
@@ -702,7 +773,28 @@ export function sendMessage() {
|
|
|
702
773
|
|
|
703
774
|
// While a turn is running, queue the next message (common Grok complaint).
|
|
704
775
|
if (isProcessing.peek()) {
|
|
705
|
-
|
|
776
|
+
// Grok owns its queue on the edge through a long-lived ACP process. Send
|
|
777
|
+
// immediately so a page reload cannot discard this follow-up in the tab.
|
|
778
|
+
if (currentAgent.peek() === 'grok' && ctx.sessionId) {
|
|
779
|
+
const agent = 'grok';
|
|
780
|
+
const ok = sendWs({
|
|
781
|
+
type: 'command', cmd: 'turn.start', agent, command: text, sessionId: ctx.sessionId,
|
|
782
|
+
options: {
|
|
783
|
+
agent, cwd: currentProject.peek().path, sessionId: ctx.sessionId,
|
|
784
|
+
configDir: ctx.configDir, model: currentModel.peek(),
|
|
785
|
+
...(currentEffort.peek() && { effort: currentEffort.peek() }),
|
|
786
|
+
...(currentPermission.peek() && { permissionMode: currentPermission.peek() }),
|
|
787
|
+
...(currentPermission.peek() === 'bypassPermissions' && { allowDangerouslySkipPermissions: true }),
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
input.value = '';
|
|
791
|
+
autoResize(input);
|
|
792
|
+
updateInputMode('');
|
|
793
|
+
clearPendingAttachments();
|
|
794
|
+
appendSystemMsg(ok ? '已加入 Grok 服务端队列,刷新页面不会丢失' : '连接未就绪,消息将在重连后发送');
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
pendingUserTexts.push({ key: draftKey(), text });
|
|
706
798
|
input.value = '';
|
|
707
799
|
autoResize(input);
|
|
708
800
|
updateInputMode('');
|
|
@@ -712,6 +804,7 @@ export function sendMessage() {
|
|
|
712
804
|
}
|
|
713
805
|
|
|
714
806
|
input.value = '';
|
|
807
|
+
composerDrafts.delete(draftKey());
|
|
715
808
|
autoResize(input);
|
|
716
809
|
updateInputMode('');
|
|
717
810
|
clearPendingAttachments();
|
|
@@ -741,6 +834,9 @@ export function sendMessage() {
|
|
|
741
834
|
const effort = currentEffort.peek();
|
|
742
835
|
const permission = currentPermission.peek();
|
|
743
836
|
const agent = currentAgent.peek() || 'claude';
|
|
837
|
+
// Existing sessions retain their own picker value. New sessions are bound
|
|
838
|
+
// on the session-created event above.
|
|
839
|
+
if (ctx.sessionId) setCurrentModel(currentModel.peek(), agent, ctx.sessionId);
|
|
744
840
|
const ok = sendWs({
|
|
745
841
|
type: 'command',
|
|
746
842
|
cmd: 'turn.start',
|
|
@@ -754,7 +850,7 @@ export function sendMessage() {
|
|
|
754
850
|
configDir: ctx.configDir,
|
|
755
851
|
model: currentModel.peek(),
|
|
756
852
|
...(effort && { effort }),
|
|
757
|
-
...(permission &&
|
|
853
|
+
...(permission && { permissionMode: permission }),
|
|
758
854
|
...(permission === 'bypassPermissions' && { allowDangerouslySkipPermissions: true }),
|
|
759
855
|
},
|
|
760
856
|
});
|
|
@@ -766,7 +862,7 @@ export function stopProcessing() {
|
|
|
766
862
|
if (ctx.sessionId) {
|
|
767
863
|
sendWs({ type: 'command', cmd: 'turn.interrupt', sessionId: ctx.sessionId });
|
|
768
864
|
}
|
|
769
|
-
|
|
865
|
+
pendingUserTexts = pendingUserTexts.filter(item => item.key !== draftKey());
|
|
770
866
|
markTurnDone();
|
|
771
867
|
}
|
|
772
868
|
|
|
@@ -1019,6 +1115,11 @@ function clearPendingAttachments() {
|
|
|
1019
1115
|
renderAttachBar();
|
|
1020
1116
|
}
|
|
1021
1117
|
|
|
1118
|
+
/** Attachments are bound to the visible composer; discard them before session navigation. */
|
|
1119
|
+
export function discardComposerAttachments() {
|
|
1120
|
+
clearPendingAttachments();
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1022
1123
|
function renderAttachBar() {
|
|
1023
1124
|
const bar = $('chat-attach-bar');
|
|
1024
1125
|
if (!bar) return;
|
package/public/files.js
CHANGED
|
File without changes
|
package/public/git.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
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>
|
package/public/lib.js
CHANGED
|
File without changes
|
|
File without changes
|
package/public/mobile.js
CHANGED
|
File without changes
|