claude-simple 0.2.27 → 0.2.30
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 +17 -1
- package/bin/edge-ctl.js +151 -4
- package/client.js +29 -4
- package/dist/addon-fit.min.js +0 -0
- package/dist/app.0.2.30.min.js +1308 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.30.min.css +2012 -0
- package/dist/xterm.min.css +0 -0
- package/dist/xterm.min.js +0 -0
- package/gateway.js +0 -0
- package/package.json +2 -2
- package/public/addon-fit.min.js +0 -0
- package/public/agent-panel.js +54 -22
- package/public/api.js +8 -5
- package/public/app.js +7 -5
- package/public/base.css +0 -0
- package/public/cache.js +19 -0
- package/public/chat.js +110 -19
- package/public/files.js +26 -10
- package/public/git.js +2 -2
- 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/markdown.js +16 -0
- package/public/math.js +145 -7
- package/public/mobile.js +0 -0
- package/public/notes.js +316 -0
- package/public/notifications.js +0 -0
- package/public/offline.html +0 -0
- package/public/pwa.js +1 -1
- package/public/router.js +4 -0
- package/public/scroll-follow.js +78 -0
- package/public/settings.js +41 -0
- package/public/shell/boot.js +0 -0
- package/public/shell/dashboard.js +0 -0
- package/public/shell/hub.js +0 -0
- package/public/shell/layout.js +0 -0
- package/public/shell/notes.js +0 -0
- package/public/shell/session-context.js +0 -0
- package/public/shell/session-list.js +79 -21
- package/public/shell/session-nav.js +70 -21
- package/public/shell/templates.js +3 -0
- package/public/shell/util.js +0 -0
- package/public/shell.js +15 -2
- package/public/state.js +7 -7
- package/public/style.css +161 -0
- package/public/sw.js +10 -5
- package/public/terminal.js +0 -0
- package/public/wake-lock.js +0 -0
- package/public/workspace-path.js +61 -0
- package/public/xterm.min.css +0 -0
- package/public/xterm.min.js +0 -0
- package/src/agent-quota.js +228 -0
- package/src/agents/claude.js +0 -0
- package/src/agents/codex.js +2 -0
- package/src/agents/cursor.js +2 -1
- package/src/agents/gemini.js +2 -1
- package/src/agents/grok.js +22 -1
- package/src/agents/meta-agent.js +7 -4
- package/src/agents/models.js +21 -12
- package/src/agents/sessions.js +270 -81
- package/src/agents/stream-batch.js +0 -0
- package/src/app.js +111 -51
- package/src/auto-update.js +2 -0
- package/src/collab.js +3 -0
- package/src/platform.js +133 -0
- package/src/project-api-path.js +14 -0
- package/src/runtime/approvals.js +0 -0
- package/src/runtime/chat-broadcast.js +12 -0
- package/src/runtime/outbox.js +0 -0
- package/src/runtime/port-websocket.js +80 -0
- package/src/runtime/protocol.js +0 -0
- package/src/session-history.js +31 -0
- package/dist/app.0.2.27.min.js +0 -1182
- package/dist/style.0.2.27.min.css +0 -1851
package/public/git.js
CHANGED
|
@@ -74,7 +74,7 @@ export function initGitTab() {
|
|
|
74
74
|
area.innerHTML = '<div class="p-4 text-sm text-base-content/50">Enter a project root above, or open a session with a working directory.</div>';
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
|
-
const id =
|
|
77
|
+
const id = '_';
|
|
78
78
|
area.innerHTML = '<div class="p-4 text-xs text-base-content/40">Loading…</div>';
|
|
79
79
|
try {
|
|
80
80
|
const data = await api('GET', `/api/projects/${id}/git/status?root=${encodeURIComponent(root)}`, undefined, signal);
|
|
@@ -126,7 +126,7 @@ function loadGraph() {
|
|
|
126
126
|
const root = gitRoot.peek() || proj?.path;
|
|
127
127
|
const content = $('git-graph-content');
|
|
128
128
|
if (!root || !content) return;
|
|
129
|
-
const id =
|
|
129
|
+
const id = '_';
|
|
130
130
|
content.innerHTML = '<span class="text-base-content/40">Loading…</span>';
|
|
131
131
|
api('GET', `/api/projects/${id}/git/log?root=${encodeURIComponent(root)}`)
|
|
132
132
|
.then(d => { content.innerHTML = d.graph ? renderGraph(d.graph) : '<span class="text-base-content/40">No commits</span>'; })
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/public/index.html
CHANGED
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
<script src="/vendor/mermaid.min.js"></script>
|
|
86
86
|
<link rel="stylesheet" href="/vendor/katex.min.css">
|
|
87
87
|
<script src="/vendor/katex.min.js"></script>
|
|
88
|
-
<link rel="stylesheet" href="style.css?v=ui-
|
|
88
|
+
<link rel="stylesheet" href="style.css?v=ui-25">
|
|
89
89
|
</head>
|
|
90
90
|
<body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
|
|
91
91
|
<div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
|
|
@@ -101,6 +101,6 @@
|
|
|
101
101
|
}
|
|
102
102
|
}, 10000);
|
|
103
103
|
</script>
|
|
104
|
-
<script type="module" src="app.js?v=ui-
|
|
104
|
+
<script type="module" src="app.js?v=ui-25"></script>
|
|
105
105
|
</body>
|
|
106
106
|
</html>
|
package/public/lib.js
CHANGED
|
File without changes
|
|
File without changes
|
package/public/markdown.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// placeholder. It is not HTML, but a Markdown parser will otherwise pass it
|
|
4
4
|
// through as an element and a malformed placeholder can corrupt the rest of a
|
|
5
5
|
// message's DOM.
|
|
6
|
+
import { protectMath, restoreMath } from './math.js';
|
|
7
|
+
|
|
6
8
|
const TRUNCATED_TRANSCRIPT_MARKER = /(?:<|\[)\s*(?:…\s*)?truncated\s+([\d,]+)\s+bytes\s*(?:>|\])/gi;
|
|
7
9
|
|
|
8
10
|
export function normalizeMarkdownText(value) {
|
|
@@ -10,3 +12,17 @@ export function normalizeMarkdownText(value) {
|
|
|
10
12
|
return `\n\n> ⚠️ 原始记录已截断(${bytes} bytes)\n\n`;
|
|
11
13
|
});
|
|
12
14
|
}
|
|
15
|
+
|
|
16
|
+
export function parseMarkdownWithMath(value, { fallback } = {}) {
|
|
17
|
+
const { text, slots } = protectMath(normalizeMarkdownText(value));
|
|
18
|
+
if (typeof marked === 'undefined') {
|
|
19
|
+
const raw = fallback ? fallback(text) : text;
|
|
20
|
+
return restoreMath(raw, slots);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return restoreMath(marked.parse(text, { breaks: true, gfm: true }), slots);
|
|
24
|
+
} catch {
|
|
25
|
+
const raw = fallback ? fallback(text) : text;
|
|
26
|
+
return restoreMath(raw, slots);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/public/math.js
CHANGED
|
@@ -1,5 +1,137 @@
|
|
|
1
|
-
// KaTeX enhancement for Markdown output.
|
|
2
|
-
//
|
|
1
|
+
// KaTeX enhancement for Markdown output. Math is extracted before Marked so
|
|
2
|
+
// `\[...\]` is not treated as an escaped bracket and `_` inside TeX is not
|
|
3
|
+
// turned into emphasis. A leftover pass still runs on the DOM for any tokens
|
|
4
|
+
// that survive as text.
|
|
5
|
+
|
|
6
|
+
const LATEXISH = /\\[a-zA-Z]+|[_^]\{|\\[,;!]|\\[{}()[\]]|\\leq\b|\\geq\b|\\neq\b|\\in\b|\\sum\b|\\int\b|\\frac\b|\\min\b|\\max\b|\\mathrm\b|\\text\b|\\left\b|\\right\b/;
|
|
7
|
+
|
|
8
|
+
export function looksLikeLatex(s) {
|
|
9
|
+
return LATEXISH.test(String(s || ''));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function htmlEscape(s) {
|
|
13
|
+
return String(s)
|
|
14
|
+
.replace(/&/g, '&')
|
|
15
|
+
.replace(/</g, '<')
|
|
16
|
+
.replace(/>/g, '>');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function atLineStart(text, i) {
|
|
20
|
+
return i === 0 || text[i - 1] === '\n';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function matchFence(text, i) {
|
|
24
|
+
if (atLineStart(text, i) && (text.startsWith('```', i) || text.startsWith('~~~', i))) {
|
|
25
|
+
const fence = text.startsWith('```', i) ? '```' : '~~~';
|
|
26
|
+
const close = text.indexOf('\n' + fence, i + 3);
|
|
27
|
+
if (close === -1) return { end: text.length };
|
|
28
|
+
const after = close + 1 + fence.length;
|
|
29
|
+
return { end: after };
|
|
30
|
+
}
|
|
31
|
+
if (text[i] === '`') {
|
|
32
|
+
let n = 1;
|
|
33
|
+
while (text[i + n] === '`') n++;
|
|
34
|
+
let j = i + n;
|
|
35
|
+
while (j < text.length) {
|
|
36
|
+
if (text[j] === '`') {
|
|
37
|
+
let k = 0;
|
|
38
|
+
while (text[j + k] === '`') k++;
|
|
39
|
+
if (k === n) return { end: j + k };
|
|
40
|
+
j += k;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
j++;
|
|
44
|
+
}
|
|
45
|
+
return { end: i + n };
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function matchMath(text, i) {
|
|
51
|
+
const rest = text.slice(i);
|
|
52
|
+
|
|
53
|
+
if (rest.startsWith('$$')) {
|
|
54
|
+
const end = rest.indexOf('$$', 2);
|
|
55
|
+
if (end >= 2) {
|
|
56
|
+
const body = rest.slice(2, end);
|
|
57
|
+
if (body.trim()) return { body, display: true, end: i + end + 2 };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (rest.startsWith('\\[')) {
|
|
62
|
+
const end = rest.indexOf('\\]', 2);
|
|
63
|
+
if (end >= 2) {
|
|
64
|
+
const body = rest.slice(2, end);
|
|
65
|
+
if (body.trim()) return { body, display: true, end: i + end + 2 };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (rest.startsWith('\\(')) {
|
|
70
|
+
const end = rest.indexOf('\\)', 2);
|
|
71
|
+
if (end >= 2) {
|
|
72
|
+
const body = rest.slice(2, end);
|
|
73
|
+
if (body.trim()) return { body, display: false, end: i + end + 2 };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (rest[0] === '$' && rest[1] !== '$') {
|
|
78
|
+
const m = rest.match(/^\$([^$\n]|\\\$)+?(?<!\s)\$/);
|
|
79
|
+
if (m && m[0][1] !== ' ') {
|
|
80
|
+
const body = m[0].slice(1, -1);
|
|
81
|
+
if (body.trim()) return { body, display: false, end: i + m[0].length };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (atLineStart(text, i)) {
|
|
86
|
+
const block = rest.match(/^[ \t]*\[[ \t]*\n([\s\S]*?)\n[ \t]*\][ \t]*(?=\n|$)/);
|
|
87
|
+
if (block && looksLikeLatex(block[1])) {
|
|
88
|
+
return { body: block[1], display: true, end: i + block[0].length };
|
|
89
|
+
}
|
|
90
|
+
const line = rest.match(/^[ \t]*\[([^\]\n]+)\](?!\()/);
|
|
91
|
+
if (line && looksLikeLatex(line[1])) {
|
|
92
|
+
return { body: line[1], display: true, end: i + line[0].length };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function protectMath(src) {
|
|
100
|
+
const slots = [];
|
|
101
|
+
const text = String(src ?? '');
|
|
102
|
+
let out = '';
|
|
103
|
+
let i = 0;
|
|
104
|
+
while (i < text.length) {
|
|
105
|
+
const fence = matchFence(text, i);
|
|
106
|
+
if (fence) {
|
|
107
|
+
out += text.slice(i, fence.end);
|
|
108
|
+
i = fence.end;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const math = matchMath(text, i);
|
|
112
|
+
if (math) {
|
|
113
|
+
const id = slots.length;
|
|
114
|
+
slots.push({ body: math.body, display: math.display });
|
|
115
|
+
out += `@@MATH${id}@@`;
|
|
116
|
+
i = math.end;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
out += text[i];
|
|
120
|
+
i++;
|
|
121
|
+
}
|
|
122
|
+
return { text: out, slots };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function restoreMath(html, slots) {
|
|
126
|
+
if (!slots?.length) return html;
|
|
127
|
+
return String(html).replace(/@@MATH(\d+)@@/g, (full, id) => {
|
|
128
|
+
const slot = slots[Number(id)];
|
|
129
|
+
if (!slot) return full;
|
|
130
|
+
const open = slot.display ? '\\[' : '\\(';
|
|
131
|
+
const close = slot.display ? '\\]' : '\\)';
|
|
132
|
+
return htmlEscape(`${open}${slot.body}${close}`);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
3
135
|
|
|
4
136
|
function ignored(node) {
|
|
5
137
|
return node.parentElement?.closest('pre, code, .mermaid-diagram, .katex, script, style');
|
|
@@ -10,9 +142,9 @@ function nextToken(text, from) {
|
|
|
10
142
|
{ re: /\\\[([\s\S]+?)\\\]/g, display: true },
|
|
11
143
|
{ re: /\$\$([\s\S]+?)\$\$/g, display: true },
|
|
12
144
|
{ re: /\\\(([\s\S]+?)\\\)/g, display: false },
|
|
13
|
-
// A closing dollar must not be escaped or preceded by whitespace. This
|
|
14
|
-
// leaves ordinary currency such as "$5 and $6" untouched.
|
|
15
145
|
{ re: /(?<!\\)\$([^$\n]|\\\$)+?(?<!\s)(?<!\\)\$/g, display: false },
|
|
146
|
+
{ re: /(?:^|\n)[ \t]*\[[ \t]*\n([\s\S]*?)\n[ \t]*\][ \t]*(?=\n|$)/g, display: true, latex: true },
|
|
147
|
+
{ re: /(?:^|\n)[ \t]*\[([^\]\n]+\\[a-zA-Z{}_^][^\]\n]*)\](?!\()/g, display: true, latex: true },
|
|
16
148
|
];
|
|
17
149
|
let best = null;
|
|
18
150
|
for (const pattern of patterns) {
|
|
@@ -20,10 +152,16 @@ function nextToken(text, from) {
|
|
|
20
152
|
const match = pattern.re.exec(text);
|
|
21
153
|
if (!match || (best && match.index >= best.match.index)) continue;
|
|
22
154
|
const raw = match[0];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
155
|
+
let body;
|
|
156
|
+
if (match[1] != null && (pattern.latex || raw.startsWith('\\[') || raw.startsWith('\\(') || raw.startsWith('$$') || raw.startsWith('$'))) {
|
|
157
|
+
body = match[1];
|
|
158
|
+
} else {
|
|
159
|
+
body = raw.startsWith('\\[') || raw.startsWith('\\(')
|
|
160
|
+
? raw.slice(2, -2)
|
|
161
|
+
: raw.startsWith('$$') ? raw.slice(2, -2) : raw.slice(1, -1);
|
|
162
|
+
}
|
|
26
163
|
if (!body.trim()) continue;
|
|
164
|
+
if (pattern.latex && !looksLikeLatex(body)) continue;
|
|
27
165
|
best = { match, body, display: pattern.display };
|
|
28
166
|
}
|
|
29
167
|
return best;
|
package/public/mobile.js
CHANGED
|
File without changes
|
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/notifications.js
CHANGED
|
File without changes
|
package/public/offline.html
CHANGED
|
File without changes
|
package/public/pwa.js
CHANGED
|
@@ -69,7 +69,7 @@ export function initPwa() {
|
|
|
69
69
|
|
|
70
70
|
// Register after first paint so install doesn't compete with boot.
|
|
71
71
|
const register = () => {
|
|
72
|
-
navigator.serviceWorker.register('/sw.js', { scope: '/' })
|
|
72
|
+
navigator.serviceWorker.register('/sw.js?v=33', { scope: '/' })
|
|
73
73
|
.then((reg) => {
|
|
74
74
|
reg.addEventListener('updatefound', () => {
|
|
75
75
|
const sw = reg.installing;
|
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) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Pure helpers for chat scroll-follow. Keep reading history from being yanked. */
|
|
2
|
+
|
|
3
|
+
export const NEAR_BOTTOM_PX = 80;
|
|
4
|
+
|
|
5
|
+
export function isNearBottom(el, threshold = NEAR_BOTTOM_PX) {
|
|
6
|
+
if (!el) return true;
|
|
7
|
+
return el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Wheel toward older messages (negative deltaY) unpins immediately. */
|
|
11
|
+
export function followAfterWheel(deltaY, following) {
|
|
12
|
+
if (deltaY < 0) return false;
|
|
13
|
+
return following;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function followAfterScroll(nearBottom) {
|
|
17
|
+
return nearBottom;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function shouldStick({ force = false, following = true, userScrolling = false } = {}) {
|
|
21
|
+
if (force) return true;
|
|
22
|
+
if (userScrolling) return false;
|
|
23
|
+
return following;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Bind a messages container. Returns { stick, setFollowing, isFollowing, destroy }.
|
|
28
|
+
*/
|
|
29
|
+
export function bindScrollFollow(el) {
|
|
30
|
+
let following = true;
|
|
31
|
+
let userScrolling = false;
|
|
32
|
+
let touchStartY = 0;
|
|
33
|
+
|
|
34
|
+
const onScroll = () => {
|
|
35
|
+
following = isNearBottom(el);
|
|
36
|
+
userScrolling = false;
|
|
37
|
+
};
|
|
38
|
+
const onWheel = (e) => {
|
|
39
|
+
following = followAfterWheel(e.deltaY, following);
|
|
40
|
+
if (e.deltaY < 0) userScrolling = true;
|
|
41
|
+
};
|
|
42
|
+
const onTouchStart = (e) => {
|
|
43
|
+
touchStartY = e.touches?.[0]?.clientY || 0;
|
|
44
|
+
};
|
|
45
|
+
const onTouchMove = (e) => {
|
|
46
|
+
const y = e.touches?.[0]?.clientY || 0;
|
|
47
|
+
if (y - touchStartY > 8) {
|
|
48
|
+
following = false;
|
|
49
|
+
userScrolling = true;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
54
|
+
el.addEventListener('wheel', onWheel, { passive: true });
|
|
55
|
+
el.addEventListener('touchstart', onTouchStart, { passive: true });
|
|
56
|
+
el.addEventListener('touchmove', onTouchMove, { passive: true });
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
stick(force = false) {
|
|
60
|
+
if (shouldStick({ force, following, userScrolling })) {
|
|
61
|
+
el.scrollTop = el.scrollHeight;
|
|
62
|
+
following = true;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
setFollowing(v) {
|
|
66
|
+
following = !!v;
|
|
67
|
+
},
|
|
68
|
+
isFollowing() {
|
|
69
|
+
return following;
|
|
70
|
+
},
|
|
71
|
+
destroy() {
|
|
72
|
+
el.removeEventListener('scroll', onScroll);
|
|
73
|
+
el.removeEventListener('wheel', onWheel);
|
|
74
|
+
el.removeEventListener('touchstart', onTouchStart);
|
|
75
|
+
el.removeEventListener('touchmove', onTouchMove);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|