glad-web 1.0.35 → 1.0.37
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/lib/codex/structured-session.js +57 -49
- package/lib/commands/web.js +23 -2
- package/lib/config/manager.js +18 -0
- package/lib/notifications/message-formatter.js +94 -0
- package/lib/notifications/notification-service.js +143 -0
- package/lib/notifications/serverchan-client.js +58 -0
- package/lib/notifications/serverchan-settings-store.js +115 -0
- package/lib/server/routes/notifications.js +52 -0
- package/lib/session/session-manager.js +1 -0
- package/lib/web/codex.js +130 -43
- package/lib/web/core.js +6 -5
- package/lib/web/index.html +60 -6
- package/lib/web/notifications.js +162 -0
- package/lib/web/schedules.js +1 -1
- package/lib/web/session.js +8 -8
- package/lib/web/styles.css +33 -1
- package/package.json +1 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const {
|
|
3
|
+
getConfig,
|
|
4
|
+
setConfig,
|
|
5
|
+
getConfigPath
|
|
6
|
+
} = require('../config/manager');
|
|
7
|
+
|
|
8
|
+
const CLIENT_TYPES = new Set(['wechat', 'pushdeer']);
|
|
9
|
+
|
|
10
|
+
function normalizeClientType(value) {
|
|
11
|
+
const clientType = String(value || 'wechat').trim().toLowerCase();
|
|
12
|
+
if (!CLIENT_TYPES.has(clientType)) {
|
|
13
|
+
const error = new Error('接收客户端必须是微信或 PushDeer');
|
|
14
|
+
error.statusCode = 400;
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
return clientType;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeSendKey(value) {
|
|
21
|
+
const sendKey = String(value || '').trim();
|
|
22
|
+
if (!sendKey || sendKey.length < 8 || sendKey.length > 512 || /\s/.test(sendKey)) {
|
|
23
|
+
const error = new Error('请输入有效的 Server酱 SendKey');
|
|
24
|
+
error.statusCode = 400;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
return sendKey;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function maskSendKey(sendKey) {
|
|
31
|
+
if (!sendKey) return '';
|
|
32
|
+
return `${sendKey.slice(0, Math.min(3, sendKey.length))}${'•'.repeat(10)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class ServerChanSettingsStore {
|
|
36
|
+
constructor({
|
|
37
|
+
readConfig = getConfig,
|
|
38
|
+
writeConfig = setConfig,
|
|
39
|
+
configPath = getConfigPath,
|
|
40
|
+
chmod = fs.chmodSync
|
|
41
|
+
} = {}) {
|
|
42
|
+
this.readConfig = readConfig;
|
|
43
|
+
this.writeConfig = writeConfig;
|
|
44
|
+
this.configPath = configPath;
|
|
45
|
+
this.chmod = chmod;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get() {
|
|
49
|
+
const stored = this.readConfig('serverChan') || {};
|
|
50
|
+
return {
|
|
51
|
+
sendKey: String(stored.sendKey || '').trim(),
|
|
52
|
+
clientType: CLIENT_TYPES.has(stored.clientType) ? stored.clientType : 'wechat'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getPublic() {
|
|
57
|
+
const settings = this.get();
|
|
58
|
+
return {
|
|
59
|
+
configured: Boolean(settings.sendKey),
|
|
60
|
+
maskedKey: maskSendKey(settings.sendKey),
|
|
61
|
+
clientType: settings.clientType
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
save(input = {}) {
|
|
66
|
+
const existing = this.get();
|
|
67
|
+
const sendKey = input.sendKey == null || String(input.sendKey).trim() === ''
|
|
68
|
+
? existing.sendKey
|
|
69
|
+
: normalizeSendKey(input.sendKey);
|
|
70
|
+
if (!sendKey) {
|
|
71
|
+
const error = new Error('请先填写 Server酱 SendKey');
|
|
72
|
+
error.statusCode = 400;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
const settings = {
|
|
76
|
+
sendKey,
|
|
77
|
+
clientType: normalizeClientType(input.clientType ?? existing.clientType)
|
|
78
|
+
};
|
|
79
|
+
this.writeConfig('serverChan', settings);
|
|
80
|
+
this.restrictConfigFile();
|
|
81
|
+
return this.getPublic();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
resolve(input = {}) {
|
|
85
|
+
const existing = this.get();
|
|
86
|
+
return {
|
|
87
|
+
sendKey: input.sendKey == null || String(input.sendKey).trim() === ''
|
|
88
|
+
? normalizeSendKey(existing.sendKey)
|
|
89
|
+
: normalizeSendKey(input.sendKey),
|
|
90
|
+
clientType: normalizeClientType(input.clientType ?? existing.clientType)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
clear() {
|
|
95
|
+
this.writeConfig('serverChan', { sendKey: '', clientType: 'wechat' });
|
|
96
|
+
this.restrictConfigFile();
|
|
97
|
+
return this.getPublic();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
restrictConfigFile() {
|
|
101
|
+
try {
|
|
102
|
+
const configPath = typeof this.configPath === 'function' ? this.configPath() : this.configPath;
|
|
103
|
+
if (configPath && fs.existsSync(configPath)) this.chmod(configPath, 0o600);
|
|
104
|
+
} catch (_) {
|
|
105
|
+
// Best effort: configuration remains usable on filesystems without chmod support.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
ServerChanSettingsStore,
|
|
112
|
+
normalizeClientType,
|
|
113
|
+
normalizeSendKey,
|
|
114
|
+
maskSendKey
|
|
115
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function statusCode(error) {
|
|
2
|
+
return Number(error?.statusCode) || 500;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function registerNotificationRoutes(app, {
|
|
6
|
+
settingsStore,
|
|
7
|
+
notificationService
|
|
8
|
+
}) {
|
|
9
|
+
app.get('/api/notifications/serverchan', (_req, res) => {
|
|
10
|
+
res.json(settingsStore.getPublic());
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
app.put('/api/notifications/serverchan', (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
res.json({ success: true, settings: settingsStore.save(req.body || {}) });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
res.status(statusCode(error)).json({ error: error.message });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
app.delete('/api/notifications/serverchan', (_req, res) => {
|
|
22
|
+
const settings = settingsStore.clear();
|
|
23
|
+
notificationService.disableAllSessions();
|
|
24
|
+
res.json({ success: true, settings });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
app.post('/api/notifications/serverchan/test', async (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
await notificationService.sendTest(req.body || {});
|
|
30
|
+
res.json({ success: true });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
res.status(statusCode(error)).json({ error: error.message });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
app.put('/api/sessions/:id/notifications/serverchan', async (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
const state = await notificationService.setSessionEnabled(
|
|
39
|
+
req.params.id,
|
|
40
|
+
Boolean(req.body?.enabled)
|
|
41
|
+
);
|
|
42
|
+
res.json({ success: true, state });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
res.status(statusCode(error)).json({
|
|
45
|
+
error: error.message,
|
|
46
|
+
...(error.code ? { code: error.code } : {})
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = registerNotificationRoutes;
|
|
@@ -69,6 +69,7 @@ class SessionManager extends EventEmitter {
|
|
|
69
69
|
workingDirectory: this.getSessionWorkingDirectory(session),
|
|
70
70
|
mode: ['claude-structured', 'codex-structured'].includes(session.kind) ? (session.presentation || 'structured') : 'terminal',
|
|
71
71
|
hasUnreadCompletion: Boolean(session.hasUnreadCompletion),
|
|
72
|
+
serverChanNotificationEnabled: Boolean(session.serverChanNotificationEnabled),
|
|
72
73
|
timedInputCount: session.timedInputs
|
|
73
74
|
? Array.from(session.timedInputs.values()).filter(item => item.sendAt > Date.now()).length
|
|
74
75
|
: 0
|
package/lib/web/codex.js
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
if (status === 'completed') return item.exitCode && item.exitCode !== 0 ? 'failed' : 'completed';
|
|
11
11
|
return status;
|
|
12
12
|
}
|
|
13
|
+
function codexMessageNeedsDetail(item) {
|
|
14
|
+
if (!item?.hasDetail) return false;
|
|
15
|
+
const loadedRevision = Number(codexDetailRevisions.get(String(item.id)) || 0);
|
|
16
|
+
return !item.detailLoaded || loadedRevision < Number(item.detailRevision || 0);
|
|
17
|
+
}
|
|
13
18
|
function formatCodexDuration(durationMs) {
|
|
14
19
|
const value = Number(durationMs || 0);
|
|
15
20
|
if (!(value > 0)) return '';
|
|
@@ -108,7 +113,7 @@
|
|
|
108
113
|
}
|
|
109
114
|
function renderCodexTool(item, permission = null) {
|
|
110
115
|
const status = codexToolStatus(item);
|
|
111
|
-
const isError = status === 'failed' || Boolean(item.error) || (item.exitCode != null && item.exitCode !== 0);
|
|
116
|
+
const isError = status === 'failed' || Boolean(item.error) || Boolean(item.hasError) || (item.exitCode != null && item.exitCode !== 0);
|
|
112
117
|
const runningClass = status === 'running' ? ' running' : '';
|
|
113
118
|
if (item.name === 'CodexPatch') {
|
|
114
119
|
return `<div class="codex-tool${isError ? ' error' : ''}">${renderCodexPatch(item)}${permission ? renderCodexPermission(permission, true) : ''}</div>`;
|
|
@@ -138,7 +143,11 @@
|
|
|
138
143
|
}).join('');
|
|
139
144
|
const label = running ? 'Working' : failed ? 'Work finished with errors' : duration ? `Worked for ${duration}` : 'Worked';
|
|
140
145
|
const key = items.map(item => item.id || item.providerId || '').join('-');
|
|
141
|
-
|
|
146
|
+
const allDetailIds = items.filter(item => item.hasDetail).map(item => item.id).filter(Boolean);
|
|
147
|
+
const detailIds = items.filter(codexMessageNeedsDetail).map(item => item.id).filter(Boolean);
|
|
148
|
+
const lazy = detailIds.length
|
|
149
|
+
? '<div class="codex-lazy-detail">Open to load tool details…</div>' : '';
|
|
150
|
+
return `<details class="codex-work-group" data-codex-key="group-${escapeHtml(key)}"${allDetailIds.length ? ` data-codex-detail-ids="${escapeHtml(allDetailIds.join(','))}"` : ''}><summary>${label} · ${items.length} ${items.length === 1 ? 'tool' : 'tools'}</summary><div class="codex-work-group-body">${tools}${lazy}</div></details>`;
|
|
142
151
|
}
|
|
143
152
|
function isCodexSubagentItem(item) {
|
|
144
153
|
return Boolean(item?.threadId && codexState.threadId && item.threadId !== codexState.threadId);
|
|
@@ -159,8 +168,13 @@
|
|
|
159
168
|
if (tools.length) counts.push(`${tools.length} ${tools.length === 1 ? 'tool' : 'tools'}`);
|
|
160
169
|
if (messages.length) counts.push(`${messages.length} ${messages.length === 1 ? 'message' : 'messages'}`);
|
|
161
170
|
const label = running ? 'Subagent working' : duration ? `Subagent worked for ${duration}` : 'Subagent worked';
|
|
171
|
+
const needsDetail = items.some(codexMessageNeedsDetail);
|
|
172
|
+
const deferContent = needsDetail && !items.some(item => item.detailLoaded);
|
|
162
173
|
const body = [];
|
|
163
|
-
|
|
174
|
+
if (needsDetail) {
|
|
175
|
+
body.push(`<div class="codex-lazy-detail">${deferContent ? 'Open to load subagent details…' : 'Updating subagent details…'}</div>`);
|
|
176
|
+
}
|
|
177
|
+
for (let i = 0; !deferContent && i < content.length;) {
|
|
164
178
|
const item = content[i];
|
|
165
179
|
if (item.kind === 'tool') {
|
|
166
180
|
const group = [];
|
|
@@ -171,14 +185,14 @@
|
|
|
171
185
|
continue;
|
|
172
186
|
}
|
|
173
187
|
if (item.kind === 'assistant' || item.kind === 'user') {
|
|
174
|
-
body.push(`<div class="codex-subagent-message${item.kind === 'user' ? ' task' : ''}">${renderMarkdown(item.text
|
|
188
|
+
if (item.text) body.push(`<div class="codex-subagent-message${item.kind === 'user' ? ' task' : ''}">${renderMarkdown(item.text)}</div>`);
|
|
175
189
|
} else if (item.text) {
|
|
176
190
|
body.push(`<div class="codex-subagent-message">${codexText(item.text)}</div>`);
|
|
177
191
|
}
|
|
178
192
|
i += 1;
|
|
179
193
|
}
|
|
180
194
|
const suffix = counts.length ? ` · ${counts.join(' · ')}` : '';
|
|
181
|
-
return `<details class="codex-work-group codex-subagent-group" data-codex-key="subagent-${escapeHtml(threadId)}"><summary>${escapeHtml(label + suffix)}</summary><div class="codex-work-group-body">${body.join('')}</div></details>`;
|
|
195
|
+
return `<details class="codex-work-group codex-subagent-group" data-codex-key="subagent-${escapeHtml(threadId)}" data-codex-thread-id="${escapeHtml(threadId)}"><summary>${escapeHtml(label + suffix)}</summary><div class="codex-work-group-body">${body.join('')}</div></details>`;
|
|
182
196
|
}
|
|
183
197
|
function renderCodexMessageTime(item, finalOnly = false) {
|
|
184
198
|
if (!item || (finalOnly && item.streaming)) return '';
|
|
@@ -320,46 +334,89 @@
|
|
|
320
334
|
const current = container.firstElementChild;
|
|
321
335
|
if (!current) container.appendChild(next);
|
|
322
336
|
else syncCodexDom(current, next);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
337
|
+
container.scrollTop = shouldStickToBottom ? container.scrollHeight : previousScrollTop;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function installCodexLazyDetailHandler() {
|
|
341
|
+
const container = document.getElementById('codex-chat-container');
|
|
342
|
+
if (!container || container.dataset.lazyDetailHandler === 'true') return;
|
|
343
|
+
container.dataset.lazyDetailHandler = 'true';
|
|
344
|
+
container.addEventListener('toggle', handleCodexLazyToggle, true);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function handleCodexLazyToggle(event) {
|
|
348
|
+
const group = event.target;
|
|
349
|
+
if (!(group instanceof HTMLDetailsElement) || !group.open || !group.classList.contains('codex-work-group')) return;
|
|
350
|
+
const threadId = group.dataset.codexThreadId || '';
|
|
351
|
+
const ids = String(group.dataset.codexDetailIds || '').split(',').filter(Boolean)
|
|
352
|
+
.filter(id => codexMessageNeedsDetail(codexMessages.find(item => item.id === id)));
|
|
353
|
+
if (threadId && codexMessages.some(item => item.threadId === threadId && codexMessageNeedsDetail(item))) {
|
|
354
|
+
void requestCodexDetails({ threadId });
|
|
329
355
|
}
|
|
356
|
+
else if (ids.length) void requestCodexDetails({ ids });
|
|
330
357
|
}
|
|
331
358
|
|
|
332
|
-
function
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
359
|
+
function requestCodexDetails({ ids = [], threadId = '' } = {}) {
|
|
360
|
+
if (currentSocket?.readyState !== 1) return Promise.resolve(false);
|
|
361
|
+
const normalizedIds = Array.from(new Set(ids.map(value => String(value || '')).filter(Boolean))).sort();
|
|
362
|
+
const key = threadId ? `thread:${threadId}` : `ids:${normalizedIds.join(',')}`;
|
|
363
|
+
for (const pending of codexDetailRequests.values()) {
|
|
364
|
+
if (pending.key === key) return pending.promise;
|
|
365
|
+
}
|
|
366
|
+
const requestId = `detail-${++codexDetailRequestSeq}`;
|
|
367
|
+
let resolveRequest;
|
|
368
|
+
const promise = new Promise(resolve => { resolveRequest = resolve; });
|
|
369
|
+
const timer = setTimeout(() => {
|
|
370
|
+
const pending = codexDetailRequests.get(requestId);
|
|
371
|
+
if (!pending) return;
|
|
372
|
+
codexDetailRequests.delete(requestId);
|
|
373
|
+
pending.resolve(false);
|
|
374
|
+
}, 15000);
|
|
375
|
+
codexDetailRequests.set(requestId, { key, promise, resolve: resolveRequest, timer });
|
|
376
|
+
currentSocket.send(JSON.stringify({
|
|
377
|
+
type: 'codex-detail-request',
|
|
378
|
+
requestId,
|
|
379
|
+
...(threadId ? { threadId } : { ids: normalizedIds })
|
|
380
|
+
}));
|
|
381
|
+
return promise;
|
|
336
382
|
}
|
|
337
383
|
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
if (!
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
384
|
+
function applyCodexDetailResponse(response) {
|
|
385
|
+
const pending = codexDetailRequests.get(response.requestId);
|
|
386
|
+
if (!pending) return;
|
|
387
|
+
const detail = response.detail || {};
|
|
388
|
+
for (const message of detail.messages || []) {
|
|
389
|
+
const index = codexMessages.findIndex(item => item.id === message.id);
|
|
390
|
+
if (index >= 0) codexMessages[index] = { ...codexMessages[index], ...message, detailLoaded: true };
|
|
391
|
+
else codexMessages.push({ ...message, detailLoaded: true });
|
|
392
|
+
codexDetailRevisions.set(String(message.id), Number(message.updatedAt || message.createdAt || 0));
|
|
393
|
+
}
|
|
394
|
+
clearTimeout(pending.timer);
|
|
395
|
+
codexDetailRequests.delete(response.requestId);
|
|
396
|
+
pending.resolve(true);
|
|
397
|
+
renderCodexChat();
|
|
347
398
|
}
|
|
348
399
|
|
|
349
|
-
function
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (uniqueOlder.length && container) {
|
|
355
|
-
codexHistoryPrependAnchor = {
|
|
356
|
-
scrollTop: container.scrollTop,
|
|
357
|
-
scrollHeight: container.scrollHeight
|
|
358
|
-
};
|
|
359
|
-
codexMessages = [...uniqueOlder, ...codexMessages];
|
|
400
|
+
function codexDetailIsOpen(message) {
|
|
401
|
+
if (!message?.id) return false;
|
|
402
|
+
if (isCodexSubagentItem(message)) {
|
|
403
|
+
return Array.from(document.querySelectorAll('.codex-subagent-group[open]'))
|
|
404
|
+
.some(group => group.dataset.codexKey === `subagent-${message.threadId}`);
|
|
360
405
|
}
|
|
361
|
-
|
|
362
|
-
|
|
406
|
+
return Array.from(document.querySelectorAll('.codex-work-group[open][data-codex-detail-ids]'))
|
|
407
|
+
.some(group => String(group.dataset.codexDetailIds || '').split(',').includes(String(message.id)));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function scheduleCodexDetailRefresh(message) {
|
|
411
|
+
if (!message || !codexDetailIsOpen(message)) return;
|
|
412
|
+
if (!message.id || (!message.detailLoaded && !isCodexSubagentItem(message))) return;
|
|
413
|
+
codexDetailRefreshIds.add(String(message.id));
|
|
414
|
+
clearTimeout(codexDetailRefreshTimer);
|
|
415
|
+
codexDetailRefreshTimer = setTimeout(() => {
|
|
416
|
+
const ids = Array.from(codexDetailRefreshIds);
|
|
417
|
+
codexDetailRefreshIds.clear();
|
|
418
|
+
if (ids.length) void requestCodexDetails({ ids });
|
|
419
|
+
}, 300);
|
|
363
420
|
}
|
|
364
421
|
|
|
365
422
|
function applyCodexState(state = {}) {
|
|
@@ -418,7 +475,18 @@
|
|
|
418
475
|
if (!pending.length) return false;
|
|
419
476
|
const request = pending[codexApprovalJumpIndex % pending.length];
|
|
420
477
|
codexApprovalJumpIndex = (codexApprovalJumpIndex + 1) % pending.length;
|
|
421
|
-
|
|
478
|
+
void loadAndFocusCodexApproval(request);
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function loadAndFocusCodexApproval(request) {
|
|
483
|
+
const permissionId = String(request?.id || '');
|
|
484
|
+
const message = codexMessages.find(item => String(item.providerId || item.id || '') === permissionId);
|
|
485
|
+
if (message && codexMessageNeedsDetail(message)) {
|
|
486
|
+
if (isCodexSubagentItem(message)) await requestCodexDetails({ threadId: String(message.threadId) });
|
|
487
|
+
else await requestCodexDetails({ ids: [String(message.id)] });
|
|
488
|
+
}
|
|
489
|
+
requestAnimationFrame(() => focusCodexApproval(permissionId));
|
|
422
490
|
}
|
|
423
491
|
|
|
424
492
|
function focusCodexApproval(permissionId, retry = true) {
|
|
@@ -482,13 +550,32 @@
|
|
|
482
550
|
|
|
483
551
|
function applyCodexEvent(event) {
|
|
484
552
|
if (!event) return;
|
|
485
|
-
if (event.type === 'message' && event.message)
|
|
486
|
-
|
|
553
|
+
if (event.type === 'message' && event.message) {
|
|
554
|
+
codexMessages.push(event.message);
|
|
555
|
+
scheduleCodexDetailRefresh(event.message);
|
|
556
|
+
}
|
|
557
|
+
else if (event.type === 'message-updated' && event.message) {
|
|
558
|
+
const i = codexMessages.findIndex(item => item.id === event.message.id);
|
|
559
|
+
if (i >= 0) {
|
|
560
|
+
const existing = codexMessages[i];
|
|
561
|
+
codexMessages[i] = existing.detailLoaded
|
|
562
|
+
? { ...existing, ...event.message, detailLoaded: true }
|
|
563
|
+
: event.message;
|
|
564
|
+
scheduleCodexDetailRefresh(codexMessages[i]);
|
|
565
|
+
} else codexMessages.push(event.message);
|
|
566
|
+
}
|
|
487
567
|
else if (event.type === 'history-reset') {
|
|
488
568
|
codexMessages = event.messages || [];
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
569
|
+
codexDetailRevisions.clear();
|
|
570
|
+
clearTimeout(codexDetailRefreshTimer);
|
|
571
|
+
codexDetailRefreshTimer = null;
|
|
572
|
+
codexDetailRefreshIds.clear();
|
|
573
|
+
for (const pending of codexDetailRequests.values()) {
|
|
574
|
+
clearTimeout(pending.timer);
|
|
575
|
+
pending.resolve(false);
|
|
576
|
+
}
|
|
577
|
+
codexDetailRequests.clear();
|
|
578
|
+
document.getElementById('codex-chat-container')?.replaceChildren();
|
|
492
579
|
}
|
|
493
580
|
else if (event.type === 'permission-request' && event.request) { codexPendingPermissions = [...codexPendingPermissions.filter(item => item.id !== event.request.id), event.request]; }
|
|
494
581
|
else if (event.type === 'permission-updated' && event.request) codexPendingPermissions = codexPendingPermissions.map(item => item.id === event.request.id ? event.request : item);
|
package/lib/web/core.js
CHANGED
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
let selectedCodexSkill = null;
|
|
53
53
|
let codexRenderFrame = null;
|
|
54
54
|
let codexApprovalJumpIndex = 0;
|
|
55
|
-
let
|
|
56
|
-
let
|
|
57
|
-
let
|
|
58
|
-
let
|
|
59
|
-
let
|
|
55
|
+
let codexDetailRequestSeq = 0;
|
|
56
|
+
let codexDetailRequests = new Map();
|
|
57
|
+
let codexDetailRevisions = new Map();
|
|
58
|
+
let codexDetailRefreshTimer = null;
|
|
59
|
+
let codexDetailRefreshIds = new Set();
|
|
60
60
|
const modifiers = { ctrl: false };
|
|
61
61
|
|
|
62
62
|
function log(msg) {
|
|
@@ -237,6 +237,7 @@
|
|
|
237
237
|
</div>
|
|
238
238
|
</div>
|
|
239
239
|
<div class="session-actions">
|
|
240
|
+
${renderServerChanSessionAction(s)}
|
|
240
241
|
<button class="btn-join" onclick="joinSession('${s.id}', decodePathValue('${encodedName}'), '${escapeHtml(s.toolKey || '')}')">Connect</button>
|
|
241
242
|
<button class="icon-btn btn-delete" onclick="deleteSession('${s.id}', event)"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></button>
|
|
242
243
|
</div>
|
package/lib/web/index.html
CHANGED
|
@@ -19,14 +19,21 @@
|
|
|
19
19
|
<div id="lobby">
|
|
20
20
|
<div class="header">
|
|
21
21
|
<h1><img class="header-logo" src="logo.svg" alt="">Glad</h1>
|
|
22
|
-
<div
|
|
23
|
-
<button class="btn
|
|
24
|
-
<button class="btn
|
|
22
|
+
<div class="header-actions">
|
|
23
|
+
<button class="header-action-btn" onclick="showToolModal()" title="New AI session"><span>+</span><span>Session</span></button>
|
|
24
|
+
<button class="header-action-btn" onclick="showScheduleModal()" title="New scheduled task"><span>+</span><span>Task</span></button>
|
|
25
|
+
<button id="app-settings-button" class="header-action-btn icon-only" type="button"
|
|
26
|
+
onclick="openSettings()" title="Settings" aria-label="Settings">
|
|
27
|
+
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
28
|
+
<circle cx="12" cy="12" r="3"></circle>
|
|
29
|
+
<path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.83 2.83-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 .6 1.7 1.7 0 0 0-.4 1.1V21h-4v-.1A1.7 1.7 0 0 0 8.6 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06-2.83-2.83.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-.6-1 1.7 1.7 0 0 0-1.1-.4H3v-4h.1A1.7 1.7 0 0 0 4.6 8.6a1.7 1.7 0 0 0-.34-1.88l-.06-.06 2.83-2.83.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-.6 1.7 1.7 0 0 0 .4-1.1V3h4v.1A1.7 1.7 0 0 0 15.4 4.6a1.7 1.7 0 0 0 1.88-.34l.06-.06 2.83 2.83-.06.06A1.7 1.7 0 0 0 19.4 9c.1.37.31.7.6 1 .3.25.67.39 1.1.4h.1v4h-.1a1.7 1.7 0 0 0-1.7.6z"></path>
|
|
30
|
+
</svg>
|
|
31
|
+
</button>
|
|
25
32
|
</div>
|
|
26
33
|
</div>
|
|
27
34
|
<div class="lobby-tabs">
|
|
28
|
-
<button id="lobby-tab-sessions" class="lobby-tab active" onclick="switchLobbyTab('sessions')">Sessions</button>
|
|
29
|
-
<button id="lobby-tab-schedules" class="lobby-tab" onclick="switchLobbyTab('schedules')">
|
|
35
|
+
<button id="lobby-tab-sessions" class="lobby-tab active" onclick="switchLobbyTab('sessions')" title="AI sessions">Sessions</button>
|
|
36
|
+
<button id="lobby-tab-schedules" class="lobby-tab" onclick="switchLobbyTab('schedules')" title="Scheduled tasks">Tasks</button>
|
|
30
37
|
</div>
|
|
31
38
|
<div id="sessions-list">
|
|
32
39
|
<p style="color:#888; text-align:center; margin-top:50px;">Loading sessions...</p>
|
|
@@ -252,11 +259,57 @@
|
|
|
252
259
|
</div>
|
|
253
260
|
</div>
|
|
254
261
|
|
|
262
|
+
<!-- App Settings Modal -->
|
|
263
|
+
<div id="settings-modal-overlay" onclick="closeSettings(event)">
|
|
264
|
+
<div id="settings-modal" role="dialog" aria-modal="true" aria-labelledby="settings-modal-title" onclick="event.stopPropagation()">
|
|
265
|
+
<div class="settings-modal-header">
|
|
266
|
+
<div>
|
|
267
|
+
<h2 id="settings-modal-title">Settings</h2>
|
|
268
|
+
<p>Manage Glad preferences and integrations.</p>
|
|
269
|
+
</div>
|
|
270
|
+
<button class="icon-btn settings-close" type="button" onclick="closeSettings()" aria-label="Close">×</button>
|
|
271
|
+
</div>
|
|
272
|
+
<section class="settings-section" aria-labelledby="notifications-settings-title">
|
|
273
|
+
<div class="settings-section-label" id="notifications-settings-title">Notifications</div>
|
|
274
|
+
<div class="settings-provider-header">
|
|
275
|
+
<div>
|
|
276
|
+
<h3>ServerChan</h3>
|
|
277
|
+
<p>Save the integration here, then enable notifications per chat using its bell.</p>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
<div class="form-field">
|
|
281
|
+
<label for="serverchan-client-type">Delivery</label>
|
|
282
|
+
<select id="serverchan-client-type">
|
|
283
|
+
<option value="wechat">WeChat</option>
|
|
284
|
+
<option value="pushdeer">PushDeer</option>
|
|
285
|
+
</select>
|
|
286
|
+
<div class="serverchan-help">This choice adjusts message formatting. Delivery is managed by ServerChan.</div>
|
|
287
|
+
</div>
|
|
288
|
+
<div class="form-field">
|
|
289
|
+
<label for="serverchan-send-key">SendKey</label>
|
|
290
|
+
<input id="serverchan-send-key" type="password" autocomplete="off" placeholder="SCT...">
|
|
291
|
+
<div id="serverchan-key-hint" class="serverchan-help"></div>
|
|
292
|
+
</div>
|
|
293
|
+
<div id="serverchan-settings-status" class="serverchan-settings-status" aria-live="polite"></div>
|
|
294
|
+
<div class="serverchan-modal-actions">
|
|
295
|
+
<button id="serverchan-remove-btn" class="small-btn danger" type="button" onclick="removeServerChanSettings()">Remove</button>
|
|
296
|
+
<span class="serverchan-action-spacer"></span>
|
|
297
|
+
<button id="serverchan-save-btn" class="small-btn primary" type="button" onclick="saveServerChanSettings()">Save</button>
|
|
298
|
+
<button id="serverchan-test-btn" class="small-btn" type="button" onclick="testServerChanSettings()">Send Test</button>
|
|
299
|
+
</div>
|
|
300
|
+
</section>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
<div id="app-toast" role="status" aria-live="polite"></div>
|
|
304
|
+
|
|
255
305
|
<!-- Schedule Modal -->
|
|
256
306
|
<div id="schedule-modal-overlay" onclick="closeScheduleModal(event)" style="position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 10001; display: none; align-items: center; justify-content: center; padding: 16px;">
|
|
257
307
|
<div id="schedule-modal" onclick="event.stopPropagation()">
|
|
258
308
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:14px;">
|
|
259
|
-
<
|
|
309
|
+
<div>
|
|
310
|
+
<h2 id="schedule-modal-title" style="margin:0; font-size:20px;">Scheduled Task</h2>
|
|
311
|
+
<p class="schedule-modal-subtitle">Runs automatically at the selected time.</p>
|
|
312
|
+
</div>
|
|
260
313
|
<button class="icon-btn" onclick="closeScheduleModal()" style="color: var(--text-dim); font-size: 24px;">×</button>
|
|
261
314
|
</div>
|
|
262
315
|
<div class="form-field">
|
|
@@ -304,6 +357,7 @@
|
|
|
304
357
|
<script defer src="vendor/xterm-addon-fit.js"></script>
|
|
305
358
|
<script src="gitgraph.js"></script>
|
|
306
359
|
<script src="core.js"></script>
|
|
360
|
+
<script src="notifications.js"></script>
|
|
307
361
|
<script src="claude.js"></script>
|
|
308
362
|
<script src="schedules.js"></script>
|
|
309
363
|
<script src="shell.js"></script>
|