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.
@@ -0,0 +1,162 @@
1
+ let serverChanSettings = null;
2
+ let serverChanToastTimer = null;
3
+
4
+ function serverChanBellSvg() {
5
+ return '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"></path><path d="M10 21h4"></path></svg>';
6
+ }
7
+
8
+ function renderServerChanSessionAction(session) {
9
+ const enabled = Boolean(session.serverChanNotificationEnabled);
10
+ const title = enabled ? 'Disable ServerChan notifications for this chat' : 'Enable ServerChan notifications for this chat';
11
+ return `<button class="serverchan-toggle${enabled ? ' active' : ''}" type="button"
12
+ data-serverchan-session="${escapeHtml(session.id)}"
13
+ aria-label="${title}" title="${title}"
14
+ onclick="toggleServerChanSession('${session.id}', ${enabled ? 'false' : 'true'}, event)">
15
+ ${serverChanBellSvg()}
16
+ </button>`;
17
+ }
18
+
19
+ function showAppToast(message) {
20
+ const toast = document.getElementById('app-toast');
21
+ toast.textContent = message;
22
+ toast.classList.add('visible');
23
+ clearTimeout(serverChanToastTimer);
24
+ serverChanToastTimer = setTimeout(() => toast.classList.remove('visible'), 1800);
25
+ }
26
+
27
+ async function toggleServerChanSession(sessionId, enabled, event) {
28
+ event?.stopPropagation();
29
+ try {
30
+ const response = await fetchWithTimeout(`/api/sessions/${sessionId}/notifications/serverchan`, {
31
+ method: 'PUT',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify({ enabled })
34
+ }, 10000);
35
+ const data = await response.json();
36
+ if (!response.ok) {
37
+ if (data.code === 'SERVERCHAN_NOT_CONFIGURED') {
38
+ showAppToast('Configure ServerChan in Settings first');
39
+ return;
40
+ }
41
+ throw new Error(data.error || 'Could not update notifications');
42
+ }
43
+ await refreshSessionsNow();
44
+ } catch (error) {
45
+ showAppToast(error.message || 'Could not update notifications');
46
+ }
47
+ }
48
+
49
+ async function openSettings(event = null) {
50
+ event?.stopPropagation();
51
+ document.getElementById('settings-modal-overlay').style.display = 'flex';
52
+ setServerChanStatus('Loading configuration…');
53
+ try {
54
+ const response = await fetchWithTimeout('/api/notifications/serverchan', {}, 10000);
55
+ const data = await response.json();
56
+ if (!response.ok) throw new Error(data.error || 'Could not load configuration');
57
+ serverChanSettings = data;
58
+ document.getElementById('serverchan-client-type').value = data.clientType || 'wechat';
59
+ const keyInput = document.getElementById('serverchan-send-key');
60
+ keyInput.value = '';
61
+ keyInput.placeholder = data.configured ? data.maskedKey : 'SCT...';
62
+ document.getElementById('serverchan-key-hint').textContent = data.configured
63
+ ? `Saved: ${data.maskedKey}. Leave blank to keep the current SendKey.`
64
+ : 'The SendKey is stored only on this Glad host.';
65
+ document.getElementById('serverchan-remove-btn').style.display = data.configured ? 'inline-flex' : 'none';
66
+ setServerChanStatus(data.configured ? 'Configuration saved.' : 'ServerChan is not configured.');
67
+ } catch (error) {
68
+ setServerChanStatus(error.message || 'Could not load configuration', 'error');
69
+ }
70
+ }
71
+
72
+ function closeSettings(event = null) {
73
+ if (event && event.target.id !== 'settings-modal-overlay') return;
74
+ document.getElementById('settings-modal-overlay').style.display = 'none';
75
+ }
76
+
77
+ function currentServerChanForm() {
78
+ return {
79
+ sendKey: document.getElementById('serverchan-send-key').value.trim(),
80
+ clientType: document.getElementById('serverchan-client-type').value
81
+ };
82
+ }
83
+
84
+ function setServerChanStatus(message, type = '') {
85
+ const status = document.getElementById('serverchan-settings-status');
86
+ status.textContent = message;
87
+ status.className = `serverchan-settings-status${type ? ` ${type}` : ''}`;
88
+ }
89
+
90
+ function setServerChanBusy(busy) {
91
+ document.getElementById('serverchan-save-btn').disabled = busy;
92
+ document.getElementById('serverchan-test-btn').disabled = busy;
93
+ document.getElementById('serverchan-remove-btn').disabled = busy;
94
+ }
95
+
96
+ async function saveServerChanSettings() {
97
+ setServerChanBusy(true);
98
+ setServerChanStatus('Saving…');
99
+ try {
100
+ const response = await fetchWithTimeout('/api/notifications/serverchan', {
101
+ method: 'PUT',
102
+ headers: { 'Content-Type': 'application/json' },
103
+ body: JSON.stringify(currentServerChanForm())
104
+ }, 10000);
105
+ const data = await response.json();
106
+ if (!response.ok) throw new Error(data.error || 'Could not save configuration');
107
+ serverChanSettings = data.settings;
108
+ const keyInput = document.getElementById('serverchan-send-key');
109
+ keyInput.value = '';
110
+ keyInput.placeholder = data.settings.maskedKey;
111
+ document.getElementById('serverchan-key-hint').textContent =
112
+ `Saved: ${data.settings.maskedKey}. Leave blank to keep the current SendKey.`;
113
+ document.getElementById('serverchan-remove-btn').style.display = 'inline-flex';
114
+ setServerChanStatus('Configuration saved. Per-chat notification switches are unchanged.', 'success');
115
+ } catch (error) {
116
+ setServerChanStatus(error.message || 'Could not save configuration', 'error');
117
+ } finally {
118
+ setServerChanBusy(false);
119
+ }
120
+ }
121
+
122
+ async function testServerChanSettings() {
123
+ setServerChanBusy(true);
124
+ setServerChanStatus('Sending test message…');
125
+ try {
126
+ const response = await fetchWithTimeout('/api/notifications/serverchan/test', {
127
+ method: 'POST',
128
+ headers: { 'Content-Type': 'application/json' },
129
+ body: JSON.stringify(currentServerChanForm())
130
+ }, 15000);
131
+ const data = await response.json();
132
+ if (!response.ok) throw new Error(data.error || 'Could not send test message');
133
+ setServerChanStatus('Test message sent. The test did not save configuration.', 'success');
134
+ } catch (error) {
135
+ setServerChanStatus(error.message || 'Could not send test message', 'error');
136
+ } finally {
137
+ setServerChanBusy(false);
138
+ }
139
+ }
140
+
141
+ async function removeServerChanSettings() {
142
+ if (!confirm('Remove ServerChan configuration and disable notifications for every chat?')) return;
143
+ setServerChanBusy(true);
144
+ try {
145
+ const response = await fetchWithTimeout('/api/notifications/serverchan', {
146
+ method: 'DELETE'
147
+ }, 10000);
148
+ const data = await response.json();
149
+ if (!response.ok) throw new Error(data.error || 'Could not remove configuration');
150
+ serverChanSettings = data.settings;
151
+ document.getElementById('serverchan-send-key').value = '';
152
+ document.getElementById('serverchan-send-key').placeholder = 'SCT...';
153
+ document.getElementById('serverchan-key-hint').textContent = 'The SendKey is stored only on this Glad host.';
154
+ document.getElementById('serverchan-remove-btn').style.display = 'none';
155
+ setServerChanStatus('Configuration removed. Notifications are disabled for every chat.', 'success');
156
+ await refreshSessionsNow();
157
+ } catch (error) {
158
+ setServerChanStatus(error.message || 'Could not remove configuration', 'error');
159
+ } finally {
160
+ setServerChanBusy(false);
161
+ }
162
+ }
@@ -35,7 +35,7 @@
35
35
  editingScheduleId = data.id || null;
36
36
  editingSteps = (data.steps || []).map(step => ({ ...step }));
37
37
  selectedWeekdays = [...(data.schedule.weekdays || [1, 2, 3, 4, 5])];
38
- document.getElementById('schedule-modal-title').textContent = editingScheduleId ? 'Edit Schedule' : 'New Schedule';
38
+ document.getElementById('schedule-modal-title').textContent = editingScheduleId ? 'Edit Scheduled Task' : 'New Scheduled Task';
39
39
  document.getElementById('schedule-name').value = data.name || 'Scheduled Task';
40
40
  document.getElementById('schedule-cwd').value = data.target.workingDirectory || '';
41
41
  document.getElementById('schedule-time').value = data.schedule.time || '09:00';
@@ -143,11 +143,12 @@
143
143
  codexSkillQuery = '';
144
144
  codexSkillError = '';
145
145
  selectedCodexSkill = null;
146
- codexHistoryBeforeId = null;
147
- codexHistoryHasMore = false;
148
- codexHistoryLoading = false;
149
- codexHistoryUserScrolled = false;
150
- codexHistoryPrependAnchor = null;
146
+ codexDetailRequestSeq = 0;
147
+ codexDetailRequests = new Map();
148
+ codexDetailRevisions = new Map();
149
+ clearTimeout(codexDetailRefreshTimer);
150
+ codexDetailRefreshTimer = null;
151
+ codexDetailRefreshIds = new Set();
151
152
  document.getElementById('codex-model-panel').classList.remove('active');
152
153
  document.getElementById('codex-resume-panel').classList.remove('active');
153
154
  document.getElementById('codex-fork-panel').classList.remove('active');
@@ -157,7 +158,7 @@
157
158
  codexState = { permissionMode: 'default', sandboxMode: 'default', effectivePermissionMode: null, effectiveSandboxMode: null, model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canCompact: false, compacting: false, canSwitchToTerminal: false, canSwitchToStructured: false };
158
159
  setClaudeModeEnabled(false);
159
160
  applyCodexState(codexState);
160
- document.getElementById('codex-chat-container').onscroll = handleCodexHistoryScroll;
161
+ installCodexLazyDetailHandler();
161
162
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
162
163
  currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
163
164
  currentSocket.onmessage = (e) => {
@@ -165,10 +166,9 @@
165
166
  if (msg.type === 'codex-snapshot' && msg.snapshot) {
166
167
  codexMessages = msg.snapshot.messages || [];
167
168
  codexPendingPermissions = msg.snapshot.pendingPermissions || [];
168
- applyCodexHistoryPageMeta(msg.snapshot.historyPage);
169
169
  applyCodexState(msg.snapshot.state || {});
170
170
  }
171
- if (msg.type === 'codex-history-page' && msg.page) applyCodexHistoryPage(msg.page);
171
+ if (msg.type === 'codex-detail-response' && msg.detail) applyCodexDetailResponse(msg);
172
172
  if (msg.type === 'codex-event') {
173
173
  applyCodexEvent(msg.event);
174
174
  if (msg.event?.type === 'presentation' && msg.event.presentation === 'terminal') {
@@ -6,7 +6,10 @@
6
6
  .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
7
7
  .header h1 { font-size: 28px; font-weight: 700; margin: 0; display: flex; align-items: center; gap: 10px; }
8
8
  .header-logo { width: 32px; height: 32px; flex-shrink: 0; }
9
- .btn-new { background: var(--primary); color: #fff; border: none; padding: 8px 11px; border-radius: 16px; font-weight: 700; font-size: 13px; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; min-height: 32px; white-space: nowrap; }
9
+ .header-actions { display: flex; align-items: center; gap: 5px; }
10
+ .header-action-btn { height: 36px; min-width: 36px; padding: 0 10px; border: 0; border-radius: 10px; background: var(--primary); color: #fff; display: inline-flex; align-items: center; justify-content: center; gap: 3px; font-size: 13px; font-weight: 750; line-height: 1; white-space: nowrap; cursor: pointer; flex-shrink: 0; }
11
+ .header-action-btn.icon-only { width: 36px; padding: 0; }
12
+ .header-action-btn:active { background: #0062cc; transform: scale(.97); }
10
13
  .btn-retry { background: #333; color: #fff; border: none; padding: 8px 16px; border-radius: 20px; margin-top: 10px; cursor: pointer; }
11
14
  .session-card { background: var(--card-bg); border-radius: 12px; padding: 16px; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; transition: transform 0.1s; position: relative; }
12
15
  .session-card:active { transform: scale(0.98); }
@@ -20,12 +23,39 @@
20
23
  .copy-dir-btn { color: var(--text-dim); background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; width: 28px; height: 28px; padding: 0; display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; }
21
24
  .copy-dir-btn:active { color: #fff; background: rgba(255,255,255,0.12); }
22
25
  .session-actions { display: flex; gap: 12px; align-items: center; margin-left: 10px; }
26
+ .serverchan-toggle { width: 34px; height: 34px; padding: 0; border: 1px solid rgba(255,255,255,0.1); border-radius: 50%; background: rgba(255,255,255,0.05); color: var(--text-dim); cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
27
+ .serverchan-toggle.active { color: #34c759; background: rgba(52,199,89,0.13); }
28
+ .serverchan-toggle:active { color: #fff; background: rgba(255,255,255,0.12); }
23
29
  .btn-join { background: rgba(255,255,255,0.1); border: none; color: var(--primary); padding: 8px 14px; border-radius: 18px; font-weight: 600; font-size: 14px; cursor: pointer; }
24
30
  .icon-btn { color: var(--text-dim); background: none; border: none; padding: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
25
31
  .icon-btn:active { color: var(--text); }
26
32
  .btn-delete { color: #ff3b30; }
27
33
  #modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 10000; display: none; align-items: center; justify-content: center; padding: 20px; }
28
34
  #tool-modal { background: var(--card-bg); width: 100%; max-width: 400px; border-radius: 16px; padding: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.4); }
35
+ #settings-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 10020; display: none; align-items: center; justify-content: center; padding: 20px; box-sizing: border-box; }
36
+ #settings-modal { background: var(--card-bg); width: 100%; max-width: 450px; border-radius: 16px; padding: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.4); box-sizing: border-box; }
37
+ .settings-modal-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 20px; }
38
+ .settings-modal-header h2 { margin: 0; font-size: 20px; }
39
+ .settings-modal-header p { margin: 5px 0 0; color: var(--text-dim); font-size: 12px; line-height: 1.45; }
40
+ .settings-close { font-size: 25px; line-height: 1; }
41
+ .settings-section { padding-top: 14px; border-top: 1px solid rgba(255,255,255,0.09); }
42
+ .settings-section-label { margin-bottom: 12px; color: var(--text-dim); font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
43
+ .settings-provider-header { margin-bottom: 16px; }
44
+ .settings-provider-header h3 { margin: 0; font-size: 16px; }
45
+ .settings-provider-header p { margin: 5px 0 0; color: var(--text-dim); font-size: 12px; line-height: 1.45; }
46
+ #settings-modal .form-field { margin-bottom: 14px; }
47
+ #settings-modal .form-field label { display: block; margin-bottom: 7px; color: #f5f5f7; font-size: 13px; font-weight: 650; }
48
+ #settings-modal input, #settings-modal select { width: 100%; min-height: 42px; padding: 9px 10px; box-sizing: border-box; border: 1px solid rgba(255,255,255,0.12); border-radius: 9px; background: rgba(255,255,255,0.08); color: #fff; font-size: 14px; outline: none; }
49
+ #settings-modal input:focus, #settings-modal select:focus { border-color: rgba(0,122,255,0.6); }
50
+ .serverchan-help { margin-top: 6px; color: var(--text-dim); font-size: 11px; line-height: 1.4; }
51
+ .serverchan-settings-status { min-height: 20px; margin: 4px 0 10px; color: var(--text-dim); font-size: 12px; }
52
+ .serverchan-settings-status.success { color: #34c759; }
53
+ .serverchan-settings-status.error { color: #ff6b61; }
54
+ .serverchan-modal-actions { display: flex; align-items: center; gap: 8px; }
55
+ .serverchan-action-spacer { flex: 1; }
56
+ #serverchan-remove-btn { display: none; }
57
+ #app-toast { position: fixed; left: 50%; bottom: max(24px, env(safe-area-inset-bottom)); z-index: 10100; max-width: calc(100vw - 32px); transform: translate(-50%, 18px); opacity: 0; pointer-events: none; padding: 9px 14px; border-radius: 999px; background: rgba(44,44,46,0.96); border: 1px solid rgba(255,255,255,0.12); color: #fff; font-size: 13px; box-shadow: 0 10px 28px rgba(0,0,0,0.35); transition: opacity .18s ease, transform .18s ease; }
58
+ #app-toast.visible { opacity: 1; transform: translate(-50%, 0); }
29
59
  .tool-item { padding: 12px; border-bottom: 1px solid #333; cursor: pointer; display: flex; align-items: center; border-radius: 8px; margin-top: 4px; }
30
60
  .tool-item:hover { background: rgba(255,255,255,0.05); }
31
61
  .tool-icon { width: 32px; height: 32px; background: #333; border-radius: 8px; margin-right: 12px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
@@ -85,6 +115,7 @@
85
115
  #claude-chat-container { display: none; flex: 1; min-height: 0; overflow-y: auto; background: #050505; padding: 12px 12px 24px 12px; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
86
116
  .claude-conversation { width: min(100%, var(--chat-content-max)); min-height: 100%; margin: 0 auto; }
87
117
  #codex-chat-container { display: none; flex: 1; min-height: 0; overflow-y: auto; background: #050505; padding: 12px 12px 24px 12px; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
118
+ .codex-lazy-detail { padding: 10px 12px; color: var(--text-dim); font-size: 12px; }
88
119
  .codex-conversation { width: min(100%, var(--chat-content-max)); margin: 0 auto; }
89
120
  .codex-working-indicator, .claude-working-indicator { position: sticky; top: 0; z-index: 4; width: 28px; height: 28px; margin: 0 0 -28px auto; border: 1px solid rgba(255,255,255,.1); border-radius: 50%; background: rgba(28,28,30,.68); box-shadow: 0 5px 16px rgba(0,0,0,.24); backdrop-filter: blur(8px); pointer-events: none; }
90
121
  .codex-working-indicator::after, .claude-working-indicator::after { content: ''; position: absolute; inset: 8px; border: 1.5px solid rgba(255,255,255,.7); border-top-color: transparent; border-radius: 50%; animation: codex-spin .8s linear infinite; }
@@ -379,6 +410,7 @@
379
410
  .weekday-btn { border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.05); color: var(--text-dim); border-radius: 8px; padding: 8px 0; font-size: 12px; font-weight: 700; cursor: pointer; }
380
411
  .weekday-btn.active { border-color: var(--primary); background: rgba(0,122,255,0.25); color: #fff; }
381
412
  #schedule-modal { background: var(--card-bg); width: 100%; max-width: 720px; max-height: 92dvh; overflow-y: auto; border-radius: 16px; padding: 18px; box-shadow: 0 20px 40px rgba(0,0,0,0.4); box-sizing: border-box; }
413
+ .schedule-modal-subtitle { margin: 4px 0 0; color: var(--text-dim); font-size: 12px; line-height: 1.4; }
382
414
  .step-card { border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 10px; margin-bottom: 8px; background: rgba(255,255,255,0.04); }
383
415
  .step-grid { display: grid; grid-template-columns: minmax(110px, 150px) 1fr auto; gap: 8px; align-items: center; }
384
416
  @media (max-width: 640px) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glad-web",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "Glad transforms terminal-based AI coding tools into a polished, mobile-friendly local Web interface.",
5
5
  "bin": {
6
6
  "glad": "bin/cli.js"