glad-web 1.0.46 → 2.0.2

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.
Files changed (74) hide show
  1. package/README.md +4 -192
  2. package/THIRD_PARTY_NOTICES.md +27 -0
  3. package/bin/glad.cjs +56 -0
  4. package/package.json +19 -61
  5. package/README.zh-CN.md +0 -198
  6. package/assets/logo.svg +0 -43
  7. package/bin/cli.js +0 -65
  8. package/lib/ai-tools/demo/enhanced-demo.js +0 -625
  9. package/lib/ai-tools/demo/index.js +0 -24
  10. package/lib/ai-tools/demo/responses.js +0 -88
  11. package/lib/ai-tools/detector.js +0 -76
  12. package/lib/ai-tools/registry.js +0 -300
  13. package/lib/claude/cli-usage.js +0 -95
  14. package/lib/claude/config.js +0 -82
  15. package/lib/claude/structured-session.js +0 -884
  16. package/lib/claude/transcript-repository.js +0 -216
  17. package/lib/codex/image-store.js +0 -174
  18. package/lib/codex/structured-session.js +0 -1590
  19. package/lib/commands/config.js +0 -78
  20. package/lib/commands/tools.js +0 -128
  21. package/lib/commands/web.js +0 -605
  22. package/lib/config/constants.js +0 -17
  23. package/lib/config/manager.js +0 -108
  24. package/lib/git/service.js +0 -83
  25. package/lib/notifications/message-formatter.js +0 -94
  26. package/lib/notifications/notification-service.js +0 -143
  27. package/lib/notifications/serverchan-client.js +0 -58
  28. package/lib/notifications/serverchan-settings-store.js +0 -115
  29. package/lib/schedule/job-runner.js +0 -162
  30. package/lib/schedule/job-store.js +0 -167
  31. package/lib/schedule/key-sequences.js +0 -49
  32. package/lib/schedule/scheduler-service.js +0 -39
  33. package/lib/server/routes/notifications.js +0 -52
  34. package/lib/server/routes/providers.js +0 -114
  35. package/lib/server/routes/schedules.js +0 -54
  36. package/lib/server/routes/skillhub.js +0 -104
  37. package/lib/server/routes/usage.js +0 -23
  38. package/lib/server/routes/workspace.js +0 -77
  39. package/lib/session/buffer.js +0 -102
  40. package/lib/session/file-attachment-store.js +0 -168
  41. package/lib/session/pty-manager.js +0 -255
  42. package/lib/session/rendered-history.js +0 -225
  43. package/lib/session/session-manager.js +0 -1032
  44. package/lib/session/text-history.js +0 -274
  45. package/lib/skillhub/client.js +0 -121
  46. package/lib/skillhub/settings-store.js +0 -168
  47. package/lib/skillhub/skill-installer.js +0 -320
  48. package/lib/usage/ccusage-runner.js +0 -128
  49. package/lib/usage/source-catalog.js +0 -26
  50. package/lib/usage/usage-service.js +0 -226
  51. package/lib/utils/logger.js +0 -74
  52. package/lib/utils/pid.js +0 -67
  53. package/lib/utils/validation.js +0 -53
  54. package/lib/web/bootstrap.js +0 -34
  55. package/lib/web/claude.js +0 -1150
  56. package/lib/web/codex.js +0 -1045
  57. package/lib/web/composer.js +0 -493
  58. package/lib/web/core.js +0 -385
  59. package/lib/web/git.js +0 -535
  60. package/lib/web/gitgraph.js +0 -293
  61. package/lib/web/index.html +0 -547
  62. package/lib/web/layout.js +0 -69
  63. package/lib/web/notifications.js +0 -164
  64. package/lib/web/schedules.js +0 -245
  65. package/lib/web/session.js +0 -361
  66. package/lib/web/shell.js +0 -74
  67. package/lib/web/skillhub.js +0 -197
  68. package/lib/web/styles.css +0 -932
  69. package/lib/web/terminal-scroll.js +0 -81
  70. package/lib/web/theme.js +0 -60
  71. package/lib/web/timed-inputs.js +0 -216
  72. package/lib/web/usage.js +0 -323
  73. package/lib/workspace/service.js +0 -77
  74. package/scripts/check-syntax.js +0 -26
@@ -1,361 +0,0 @@
1
- function joinSession(id, sessionName, toolKey = null) {
2
- if (typeof clearComposerAttachments === 'function' && (selectedImageAttachments.length || selectedFileAttachments.length)) {
3
- void clearComposerAttachments();
4
- }
5
- stopTimedInputTimers();
6
- activeSessionId = id;
7
- window.activeSessionId = id;
8
- activeToolKey = toolKey;
9
- if (typeof resetComposerSendState === 'function') resetComposerSendState();
10
- clearTimeout(sessionPollTimer);
11
- sessionPollTimer = null;
12
- markCompletionRead(id);
13
- loadTimedInputs();
14
- document.getElementById('session-title').innerText = sessionName;
15
- updateToolShortcuts(activeToolKey);
16
- document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
17
- document.getElementById('terminal-view').classList.add('active');
18
- setClaudeModeEnabled(isClaudeSession());
19
- if (currentSocket) { currentSocket.close(); currentSocket = null; }
20
- initTerminal(id);
21
- if (typeof isSplitLayout === 'function' && isSplitLayout()) refreshSessionsNow();
22
- }
23
-
24
- function initTerminal(sessionId) {
25
- if (isClaudeSession()) {
26
- initClaudeSession(sessionId);
27
- return;
28
- }
29
- if (isCodexSession()) {
30
- initCodexSession(sessionId);
31
- return;
32
- }
33
- if (typeof Terminal === 'undefined') {
34
- alert('Terminal library not loaded yet.');
35
- showLobby();
36
- return;
37
- }
38
- function isTerminalAutoResponse(data) {
39
- return /^\x1b\[\?[\d;]*c$/.test(data) || /^\x1b\[>[\d;]*c$/.test(data);
40
- }
41
- if (!term) {
42
- term = new Terminal({ theme: typeof getGladTerminalTheme === 'function' ? getGladTerminalTheme() : { background: '#000', foreground: '#fff', cursor: '#0f0' }, cursorBlink: true, fontSize: 14, cursorStyle: 'bar', scrollback: 10000 });
43
- fitAddon = new FitAddon.FitAddon();
44
- term.loadAddon(fitAddon);
45
- term.open(document.getElementById('terminal'));
46
- term.onData(data => {
47
- if (isTerminalAutoResponse(data)) return;
48
- let finalData = data;
49
- if (modifiers.ctrl && data.length === 1) {
50
- const code = data.toLowerCase().charCodeAt(0);
51
- if (code >= 97 && code <= 122) finalData = String.fromCharCode(code - 96);
52
- }
53
- sendWS(finalData);
54
- });
55
- } else { term.clear(); }
56
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
57
- currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
58
- currentSocket.onopen = () => {
59
- syncLayout();
60
- setTimeout(() => logViewSnapshot('ws-open'), 300);
61
- };
62
- currentSocket.onmessage = (e) => {
63
- const msg = JSON.parse(e.data);
64
- if (msg.type === 'output') term.write(msg.data);
65
- if (msg.type === 'reset') term.reset();
66
- if (msg.type === 'exit') showLobby();
67
- };
68
- }
69
-
70
- async function initClaudeSession(sessionId) {
71
- claudeMessages = [];
72
- claudePendingPermissions = [];
73
- claudeStatus = 'idle';
74
- const runtimeConfig = await refreshClaudeRuntimeConfig();
75
- claudeState = {
76
- permissionMode: 'default',
77
- model: (runtimeConfig && runtimeConfig.defaultModel) || 'default',
78
- effort: (runtimeConfig && runtimeConfig.defaultEffort) || 'medium',
79
- claudeSessionId: null,
80
- resumeSessionId: null,
81
- canAbort: false,
82
- pendingPermissionCount: 0
83
- };
84
- claudeResumePanelOpen = false;
85
- claudeForkPanelOpen = false;
86
- claudePickerOpen = null;
87
- claudeUsagePending = false;
88
- claudeContextPending = false;
89
- claudeResumeItemsLoaded = false;
90
- document.getElementById('claude-resume-panel').classList.remove('active');
91
- document.getElementById('claude-resume-panel').innerHTML = '';
92
- document.getElementById('claude-fork-panel').classList.remove('active');
93
- document.getElementById('claude-fork-panel').innerHTML = '';
94
- closeClaudePicker();
95
- updateTerminalControlsHeight();
96
- applyClaudeRuntimeConfig(runtimeConfig);
97
- applyClaudeState(claudeState);
98
- renderClaudeChat();
99
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
100
- currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
101
- currentSocket.onopen = () => logViewSnapshot('claude-ws-open');
102
- currentSocket.onmessage = (e) => {
103
- const msg = JSON.parse(e.data);
104
- if (msg.type === 'claude-snapshot' && msg.snapshot) {
105
- claudeMessages = msg.snapshot.messages || [];
106
- claudePendingPermissions = msg.snapshot.pendingPermissions || [];
107
- claudeStatus = msg.snapshot.status || 'idle';
108
- applyClaudeState(msg.snapshot.state || {
109
- status: claudeStatus,
110
- pendingPermissionCount: claudePendingPermissions.filter(item => item.status === 'pending').length,
111
- canAbort: claudeStatus === 'thinking'
112
- }, { providerStateReceived: true });
113
- renderClaudeChat();
114
- }
115
- if (msg.type === 'claude-event') applyClaudeEvent(msg.event);
116
- if (msg.type === 'exit') showLobby();
117
- };
118
- }
119
-
120
- function initCodexSession(sessionId) {
121
- codexMessages = [];
122
- codexPendingPermissions = [];
123
- codexModelPanelOpen = false;
124
- codexModelCandidate = null;
125
- codexResumePanelOpen = false;
126
- codexForkPanelOpen = false;
127
- codexPromptPanelOpen = false;
128
- codexPromptItems = [];
129
- codexPromptNextOffset = 0;
130
- codexPromptHasMore = false;
131
- codexPromptTotal = 0;
132
- codexPromptLoading = false;
133
- codexExpandedPrompts = new Set();
134
- codexSkillPanelOpen = false;
135
- codexSkillItems = [];
136
- codexSkillLoading = false;
137
- codexSkillQuery = '';
138
- codexSkillError = '';
139
- selectedCodexSkill = null;
140
- codexDetailRequestSeq = 0;
141
- codexDetailRequests = new Map();
142
- codexDetailRevisions = new Map();
143
- clearTimeout(codexDetailRefreshTimer);
144
- codexDetailRefreshTimer = null;
145
- codexDetailRefreshIds = new Set();
146
- document.getElementById('codex-model-panel').classList.remove('active');
147
- document.getElementById('codex-resume-panel').classList.remove('active');
148
- document.getElementById('codex-fork-panel').classList.remove('active');
149
- document.getElementById('codex-prompt-panel').classList.remove('active');
150
- document.getElementById('codex-skill-panel').classList.remove('active');
151
- document.getElementById('codex-control-rail').scrollLeft = 0;
152
- codexState = createDefaultCodexState();
153
- setClaudeModeEnabled(false);
154
- applyCodexState(codexState);
155
- installCodexLazyDetailHandler();
156
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
157
- currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
158
- currentSocket.onmessage = (e) => {
159
- const msg = JSON.parse(e.data);
160
- if (msg.type === 'codex-snapshot' && msg.snapshot) {
161
- codexMessages = msg.snapshot.messages || [];
162
- codexPendingPermissions = msg.snapshot.pendingPermissions || [];
163
- applyCodexState(msg.snapshot.state || {}, { providerStateReceived: true });
164
- }
165
- if (msg.type === 'codex-detail-response' && msg.detail) applyCodexDetailResponse(msg);
166
- if (msg.type === 'codex-event') applyCodexEvent(msg.event);
167
- if (msg.type === 'exit') showLobby();
168
- };
169
- }
170
-
171
- function sendWS(data) {
172
- if (currentSocket && currentSocket.readyState === 1) currentSocket.send(JSON.stringify({ type: 'input', data }));
173
- }
174
-
175
- let layoutTimer = null;
176
- let layoutKeepAtBottom = false;
177
-
178
- function isTerminalAtBottom() {
179
- if (!term || !term.buffer || !term.buffer.active) return true;
180
- const active = term.buffer.active;
181
- return active.viewportY >= active.baseY - 1;
182
- }
183
-
184
- function scrollTerminalViewportToBottom() {
185
- if (!term) return;
186
- term.scrollToBottom();
187
- const viewport = document.querySelector('#terminal .xterm-viewport');
188
- if (viewport) viewport.scrollTop = viewport.scrollHeight;
189
- }
190
-
191
- function restoreTerminalBottomSoon() {
192
- scrollTerminalViewportToBottom();
193
- requestAnimationFrame(() => {
194
- scrollTerminalViewportToBottom();
195
- requestAnimationFrame(scrollTerminalViewportToBottom);
196
- });
197
- setTimeout(scrollTerminalViewportToBottom, 120);
198
- }
199
-
200
- function syncLayout(options = {}) {
201
- if (!fitAddon || !currentSocket) return;
202
- const keepAtBottom = typeof options.keepAtBottom === 'boolean' ? options.keepAtBottom : isTerminalAtBottom();
203
- layoutKeepAtBottom = layoutKeepAtBottom || keepAtBottom;
204
- clearTimeout(layoutTimer);
205
- layoutTimer = setTimeout(() => {
206
- const shouldRestoreBottom = layoutKeepAtBottom;
207
- layoutKeepAtBottom = false;
208
- fitAddon.fit();
209
- if (shouldRestoreBottom) restoreTerminalBottomSoon();
210
- if (currentSocket.readyState === 1) currentSocket.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
211
- }, 50);
212
- }
213
-
214
- function previewText(text, maxChars = 320) {
215
- if (!text) return '';
216
- const normalized = String(text)
217
- .replace(/\r/g, '\\r')
218
- .replace(/\n/g, '\\n')
219
- .replace(/\t/g, '\\t')
220
- .replace(/\x1b/g, '\\x1b');
221
- return normalized.length > maxChars ? normalized.slice(-maxChars) : normalized;
222
- }
223
-
224
- async function logClientDebug(event, payload) {
225
- try {
226
- await fetch('/api/debug/client-log', {
227
- method: 'POST',
228
- headers: { 'Content-Type': 'application/json' },
229
- body: JSON.stringify({ sessionId: activeSessionId, event, payload })
230
- });
231
- } catch (_) {}
232
- }
233
-
234
- function snapshotTerminalBuffer(maxLines = 12) {
235
- if (!term || !term.buffer || !term.buffer.active) return null;
236
- const active = term.buffer.active;
237
- const totalLines = active.length || 0;
238
- const start = Math.max(0, totalLines - maxLines);
239
- const tailLines = [];
240
- for (let i = start; i < totalLines; i++) {
241
- const line = active.getLine(i);
242
- if (!line) continue;
243
- tailLines.push(line.translateToString(true));
244
- }
245
- return {
246
- cols: term.cols,
247
- rows: term.rows,
248
- baseY: active.baseY,
249
- viewportY: active.viewportY,
250
- cursorX: active.cursorX,
251
- cursorY: active.cursorY,
252
- totalLines,
253
- tailPreview: previewText(tailLines.join('\n'))
254
- };
255
- }
256
-
257
- function logViewSnapshot(event) {
258
- logClientDebug(event, {
259
- sessionId: activeSessionId,
260
- terminalSnapshot: snapshotTerminalBuffer()
261
- });
262
- }
263
-
264
- // Navigation
265
- function showTerminal() {
266
- clearTimeout(sessionPollTimer);
267
- sessionPollTimer = null;
268
- document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
269
- document.getElementById('terminal-view').classList.add('active');
270
- logViewSnapshot('show-terminal');
271
- syncLayout();
272
- }
273
-
274
- function showGitPreview() {
275
- clearTimeout(sessionPollTimer);
276
- sessionPollTimer = null;
277
- document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
278
- document.getElementById('git-view').classList.add('active');
279
- switchGitTab('changes');
280
- }
281
-
282
- let historyWrapEnabled = true;
283
-
284
- function showHistory() {
285
- clearTimeout(sessionPollTimer);
286
- sessionPollTimer = null;
287
- document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
288
- document.getElementById('history-view').classList.add('active');
289
- logViewSnapshot('show-history-before-load');
290
- loadHistory();
291
- }
292
-
293
- function toggleHistoryWrap() {
294
- historyWrapEnabled = !historyWrapEnabled;
295
- const contentEl = document.getElementById('history-content');
296
- contentEl.classList.toggle('nowrap', !historyWrapEnabled);
297
- document.getElementById('history-wrap-btn').style.color = historyWrapEnabled ? 'var(--primary)' : 'var(--text-dim)';
298
- }
299
-
300
- async function loadHistory() {
301
- const contentEl = document.getElementById('history-content');
302
- const metaEl = document.getElementById('history-meta');
303
- if (!activeSessionId) {
304
- metaEl.textContent = 'No active session.';
305
- contentEl.textContent = '';
306
- return;
307
- }
308
-
309
- metaEl.textContent = 'Loading...';
310
- try {
311
- const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/history`, {}, 30000);
312
- const data = await res.json();
313
- if (!data.success) throw new Error(data.error || 'Failed to load history');
314
-
315
- const updatedAt = data.updatedAt ? new Date(data.updatedAt).toLocaleTimeString() : 'unknown';
316
- const size = typeof data.bytes === 'number' ? `${Math.round(data.bytes / 1024)} KB` : 'unknown size';
317
- const truncated = data.truncated ? ' | truncated' : '';
318
- metaEl.textContent = `${data.tool || 'Session'} | ${data.lines || 0} lines | ${size} | updated ${updatedAt}${truncated}`;
319
- contentEl.textContent = data.text || 'No readable history yet.';
320
- contentEl.classList.toggle('nowrap', !historyWrapEnabled);
321
- document.getElementById('history-wrap-btn').style.color = historyWrapEnabled ? 'var(--primary)' : 'var(--text-dim)';
322
- logClientDebug('history-loaded', {
323
- meta: metaEl.textContent,
324
- historyLines: data.lines || 0,
325
- historyBytes: data.bytes || 0,
326
- historyTail: previewText(data.text || '')
327
- });
328
- requestAnimationFrame(() => {
329
- contentEl.scrollTop = contentEl.scrollHeight;
330
- });
331
- } catch (e) {
332
- metaEl.textContent = 'Failed to load history.';
333
- contentEl.textContent = e.message;
334
- logClientDebug('history-load-failed', { message: e.message });
335
- }
336
- }
337
-
338
- async function copyHistoryContent() {
339
- const contentEl = document.getElementById('history-content');
340
- const text = contentEl.textContent || '';
341
- if (!text || text === 'Loading...' || text === 'No readable history yet.') {
342
- alert('No history to copy.');
343
- return;
344
- }
345
-
346
- try {
347
- if (navigator.clipboard && window.isSecureContext) {
348
- await navigator.clipboard.writeText(text);
349
- } else {
350
- const range = document.createRange();
351
- range.selectNode(contentEl);
352
- window.getSelection().removeAllRanges();
353
- window.getSelection().addRange(range);
354
- document.execCommand('copy');
355
- window.getSelection().removeAllRanges();
356
- }
357
- alert('History copied.');
358
- } catch (e) {
359
- alert('Copy failed. You can select the history text manually.');
360
- }
361
- }
package/lib/web/shell.js DELETED
@@ -1,74 +0,0 @@
1
- async function deleteSession(id, e) {
2
- e?.stopPropagation();
3
- if (!confirm('Terminate session?')) return;
4
- const button = e?.currentTarget;
5
- if (button) button.disabled = true;
6
- try {
7
- const response = await fetchWithTimeout('/api/sessions/' + encodeURIComponent(id), { method: 'DELETE' }, 10000);
8
- if (!response.ok) {
9
- const data = await response.json().catch(() => ({}));
10
- throw new Error(data.error || `HTTP ${response.status}`);
11
- }
12
- if (id === activeSessionId) showLobby();
13
- else await refreshSessionsNow();
14
- } catch (error) {
15
- if (button?.isConnected) button.disabled = false;
16
- alert(`Failed to delete session: ${error.message}`);
17
- }
18
- }
19
-
20
- function showLobby() {
21
- if (currentSocket) { currentSocket.close(); currentSocket = null; }
22
- closeTimedSendPanel();
23
- stopTimedInputTimers();
24
- activeSessionId = null;
25
- window.activeSessionId = null;
26
- activeToolKey = null;
27
- if (typeof resetComposerSendState === 'function') resetComposerSendState();
28
- document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
29
- document.getElementById('lobby-view').classList.add('active');
30
- refreshSessionsNow();
31
- }
32
-
33
- function updateToolShortcuts(toolKey) {
34
- const usesUsageCommand = ['antigravity', 'claude-code'].includes(toolKey);
35
- const usageCommand = usesUsageCommand ? '/usage' : '/stat';
36
- const defaultShortcuts = [
37
- { key: 'left', label: '←', special: true },
38
- { key: 'right', label: '→', special: true },
39
- { key: 'ctrl-c', label: 'Ctrl+C' },
40
- { command: '/model' },
41
- { command: '/resume' },
42
- { command: usageCommand }
43
- ];
44
- const toolShortcuts = {
45
- codex: [
46
- { key: 'ctrl-c', label: 'Ctrl+C' },
47
- { key: 'codex-perm', label: '/perm', title: 'Send /perm' },
48
- { command: '/model' },
49
- { command: '/resume' },
50
- { command: '/stat' }
51
- ]
52
- };
53
- const shortcuts = toolShortcuts[toolKey] || defaultShortcuts;
54
- const shortcutGroup = document.querySelector('[data-role="tool-shortcuts"]');
55
- if (!shortcutGroup) return;
56
-
57
- shortcutGroup.style.gridTemplateColumns = `repeat(${shortcuts.length}, minmax(0, 1fr))`;
58
- shortcutGroup.replaceChildren(...shortcuts.map(shortcut => {
59
- const button = document.createElement(shortcut.command ? 'button' : 'div');
60
- button.className = shortcut.command
61
- ? 'command-key'
62
- : `key-btn${shortcut.special ? ' special-key' : ''}`;
63
- if (shortcut.command) {
64
- button.dataset.command = shortcut.command;
65
- button.textContent = shortcut.command;
66
- button.title = `Send ${shortcut.command}`;
67
- } else {
68
- button.dataset.key = shortcut.key;
69
- button.textContent = shortcut.label;
70
- button.title = shortcut.title || '';
71
- }
72
- return button;
73
- }));
74
- }
@@ -1,197 +0,0 @@
1
- let skillHubSettings = null;
2
- let skillHallSkills = [];
3
- let skillHallQuery = '';
4
-
5
- function applySkillHubAvailability(available) {
6
- const button = document.getElementById('skill-hall-button');
7
- if (!button) return;
8
- button.disabled = !available;
9
- button.title = available ? 'Skill Hall' : 'Skill暂不可用';
10
- button.setAttribute('aria-label', button.title);
11
- }
12
-
13
- async function refreshSkillHubAvailability() {
14
- try {
15
- const response = await fetchWithTimeout('/api/skillhub/status', {}, 5000);
16
- const data = await response.json();
17
- applySkillHubAvailability(Boolean(response.ok && data.available));
18
- } catch (_) {
19
- applySkillHubAvailability(false);
20
- }
21
- }
22
-
23
- function setSkillHubStatus(message, type = '') {
24
- const target = document.getElementById('skillhub-settings-status');
25
- if (!target) return;
26
- target.textContent = message;
27
- target.className = `serverchan-settings-status${type ? ` ${type}` : ''}`;
28
- }
29
-
30
- function setSkillHubBusy(busy) {
31
- for (const id of ['skillhub-save-btn', 'skillhub-test-btn', 'skillhub-remove-btn']) {
32
- const button = document.getElementById(id);
33
- if (button) button.disabled = busy;
34
- }
35
- }
36
-
37
- function currentSkillHubForm() {
38
- return {
39
- baseUrl: document.getElementById('skillhub-base-url').value.trim(),
40
- token: document.getElementById('skillhub-token').value.trim()
41
- };
42
- }
43
-
44
- async function loadSkillHubSettings() {
45
- setSkillHubStatus('Loading configuration…');
46
- try {
47
- const response = await fetchWithTimeout('/api/skillhub/settings', {}, 10000);
48
- const data = await response.json();
49
- if (!response.ok) throw new Error(data.error || 'Could not load SkillHub configuration');
50
- skillHubSettings = data;
51
- document.getElementById('skillhub-base-url').value = data.baseUrl || '';
52
- const token = document.getElementById('skillhub-token');
53
- token.value = '';
54
- token.placeholder = data.configured ? data.maskedToken : 'clh_...';
55
- document.getElementById('skillhub-token-hint').textContent = data.configured
56
- ? `Saved: ${data.maskedToken}. Leave blank to keep the current Token.`
57
- : 'The Token is encrypted on this Glad host.';
58
- document.getElementById('skillhub-remove-btn').style.display = data.configured ? 'inline-flex' : 'none';
59
- setSkillHubStatus(data.configured ? 'Configuration saved.' : 'SkillHub is not configured.');
60
- } catch (error) {
61
- setSkillHubStatus(error.message || 'Could not load SkillHub configuration', 'error');
62
- }
63
- }
64
-
65
- async function saveSkillHubSettings() {
66
- setSkillHubBusy(true);
67
- setSkillHubStatus('Testing and saving…');
68
- try {
69
- const response = await fetchWithTimeout('/api/skillhub/settings', {
70
- method: 'PUT',
71
- headers: { 'Content-Type': 'application/json' },
72
- body: JSON.stringify(currentSkillHubForm())
73
- }, 20000);
74
- const data = await response.json();
75
- if (!response.ok) throw new Error(data.error || 'Could not save SkillHub configuration');
76
- skillHubSettings = data.settings;
77
- const token = document.getElementById('skillhub-token');
78
- token.value = '';
79
- token.placeholder = data.settings.maskedToken;
80
- document.getElementById('skillhub-token-hint').textContent =
81
- `Saved: ${data.settings.maskedToken}. Leave blank to keep the current Token.`;
82
- document.getElementById('skillhub-remove-btn').style.display = 'inline-flex';
83
- const handle = data.user?.handle || data.user?.data?.handle || '';
84
- setSkillHubStatus(`Connected${handle ? ` as ${handle}` : ''}.`, 'success');
85
- } catch (error) {
86
- setSkillHubStatus(error.message || 'Could not save SkillHub configuration', 'error');
87
- } finally {
88
- setSkillHubBusy(false);
89
- }
90
- }
91
-
92
- async function testSkillHubSettings() {
93
- setSkillHubBusy(true);
94
- setSkillHubStatus('Testing connection…');
95
- try {
96
- const response = await fetchWithTimeout('/api/skillhub/settings/test', {
97
- method: 'POST',
98
- headers: { 'Content-Type': 'application/json' },
99
- body: JSON.stringify(currentSkillHubForm())
100
- }, 20000);
101
- const data = await response.json();
102
- if (!response.ok) throw new Error(data.error || 'SkillHub connection failed');
103
- const handle = data.user?.handle || data.user?.data?.handle || '';
104
- setSkillHubStatus(`Connection successful${handle ? ` · ${handle}` : ''}.`, 'success');
105
- } catch (error) {
106
- setSkillHubStatus(error.message || 'SkillHub connection failed', 'error');
107
- } finally {
108
- setSkillHubBusy(false);
109
- }
110
- }
111
-
112
- async function removeSkillHubSettings() {
113
- if (!confirm('Remove the SkillHub connection?')) return;
114
- setSkillHubBusy(true);
115
- try {
116
- const response = await fetchWithTimeout('/api/skillhub/settings', { method: 'DELETE' }, 10000);
117
- const data = await response.json();
118
- if (!response.ok) throw new Error(data.error || 'Could not remove SkillHub configuration');
119
- skillHubSettings = data.settings;
120
- document.getElementById('skillhub-base-url').value = '';
121
- document.getElementById('skillhub-token').value = '';
122
- document.getElementById('skillhub-token').placeholder = 'clh_...';
123
- document.getElementById('skillhub-token-hint').textContent = 'The Token is encrypted on this Glad host.';
124
- document.getElementById('skillhub-remove-btn').style.display = 'none';
125
- setSkillHubStatus('SkillHub configuration removed.', 'success');
126
- } catch (error) {
127
- setSkillHubStatus(error.message || 'Could not remove SkillHub configuration', 'error');
128
- } finally {
129
- setSkillHubBusy(false);
130
- }
131
- }
132
-
133
- function skillHallCard(skill, index) {
134
- const name = String(skill.displayName || skill.name || skill.slug || 'Unnamed Skill');
135
- const description = String(skill.description || '');
136
- const meta = [skill.kind, skill.version].filter(Boolean).join(' · ');
137
- return `<button type="button" class="skill-hall-card" onclick="selectSkillHallSkill(${index})"><span class="skill-hall-card-name">${escapeHtml(name)}</span>${description ? `<span class="skill-hall-card-description">${escapeHtml(description)}</span>` : ''}<span class="skill-hall-card-meta">${escapeHtml(meta)}</span></button>`;
138
- }
139
-
140
- function renderSkillHall() {
141
- const grid = document.getElementById('skill-hall-grid');
142
- const status = document.getElementById('skill-hall-status');
143
- if (!grid || !status) return;
144
- const query = skillHallQuery.trim().toLocaleLowerCase();
145
- const matches = skillHallSkills.map((skill, index) => ({ skill, index })).filter(({ skill }) => {
146
- if (!query) return true;
147
- return [skill.displayName, skill.name, skill.slug, skill.description, skill.kind, skill.version]
148
- .some(value => String(value || '').toLocaleLowerCase().includes(query));
149
- });
150
- status.textContent = matches.length ? `${matches.length} Skill${matches.length === 1 ? '' : 's'}`
151
- : (query ? 'No matching Skills.' : 'No Skills are available for this account.');
152
- grid.innerHTML = matches.map(({ skill, index }) => skillHallCard(skill, index)).join('');
153
- }
154
-
155
- async function openSkillHall(event = null) {
156
- event?.stopPropagation();
157
- if (document.getElementById('skill-hall-button')?.disabled) {
158
- showAppToast('Skill暂不可用');
159
- return;
160
- }
161
- document.getElementById('skill-hall-overlay').style.display = 'flex';
162
- document.getElementById('skill-hall-search').value = '';
163
- document.getElementById('skill-hall-status').textContent = 'Loading Skills…';
164
- document.getElementById('skill-hall-grid').innerHTML = '';
165
- skillHallQuery = '';
166
- try {
167
- const response = await fetchWithTimeout('/api/skillhub/skills', {}, 30000);
168
- const data = await response.json();
169
- if (!response.ok || !data.success) throw new Error(data.error || 'Could not load Skills');
170
- skillHallSkills = Array.isArray(data.skills) ? data.skills : [];
171
- renderSkillHall();
172
- requestAnimationFrame(() => document.getElementById('skill-hall-search')?.focus());
173
- } catch (error) {
174
- skillHallSkills = [];
175
- document.getElementById('skill-hall-status').textContent = error.message || 'Could not load Skills';
176
- if (error.message === 'Skill暂不可用') applySkillHubAvailability(false);
177
- }
178
- }
179
-
180
- function closeSkillHall(event = null) {
181
- if (event && event.target.id !== 'skill-hall-overlay') return;
182
- document.getElementById('skill-hall-overlay').style.display = 'none';
183
- }
184
-
185
- function filterSkillHall(value) {
186
- skillHallQuery = String(value || '');
187
- renderSkillHall();
188
- }
189
-
190
- function selectSkillHallSkill(index) {
191
- const skill = skillHallSkills[index];
192
- if (!skill) return;
193
- closeSkillHall();
194
- showToolModal(skill);
195
- }
196
-
197
- void refreshSkillHubAvailability();