glad-web 1.0.29 → 1.0.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/lib/claude/cli-usage.js +95 -0
- package/lib/claude/structured-session.js +223 -51
- package/lib/claude/transcript-repository.js +216 -0
- package/lib/codex/image-store.js +175 -0
- package/lib/commands/web.js +52 -234
- package/lib/server/routes/providers.js +103 -0
- package/lib/server/routes/schedules.js +54 -0
- package/lib/server/routes/workspace.js +77 -0
- package/lib/session/session-manager.js +111 -340
- package/lib/web/claude.js +1074 -0
- package/lib/web/codex.js +475 -0
- package/lib/web/composer.js +230 -0
- package/lib/web/core.js +327 -0
- package/lib/web/git.js +533 -0
- package/lib/web/index.html +45 -3672
- package/lib/web/schedules.js +245 -0
- package/lib/web/session.js +351 -0
- package/lib/web/shell.js +56 -0
- package/lib/web/styles.css +377 -0
- package/lib/web/terminal-scroll.js +81 -0
- package/lib/web/timed-inputs.js +223 -0
- package/lib/workspace/service.js +3 -2
- package/package.json +10 -5
- package/scripts/check-syntax.js +26 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
let timedInputs = [];
|
|
2
|
+
|
|
3
|
+
function initTimedDelaySelectors() {
|
|
4
|
+
const hours = document.getElementById('timed-hours');
|
|
5
|
+
const minutes = document.getElementById('timed-minutes');
|
|
6
|
+
if (hours.options.length) return;
|
|
7
|
+
for (let i = 0; i <= 23; i++) {
|
|
8
|
+
hours.add(new Option(`${i} hr`, String(i)));
|
|
9
|
+
}
|
|
10
|
+
for (let i = 0; i <= 59; i++) {
|
|
11
|
+
minutes.add(new Option(`${i} min`, String(i)));
|
|
12
|
+
}
|
|
13
|
+
hours.value = '0';
|
|
14
|
+
minutes.value = '5';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getTimedDelayMs() {
|
|
18
|
+
const hours = Number(document.getElementById('timed-hours').value);
|
|
19
|
+
const minutes = Number(document.getElementById('timed-minutes').value);
|
|
20
|
+
const delayMs = ((hours * 60) + minutes) * 60 * 1000;
|
|
21
|
+
return Number.isFinite(delayMs) && delayMs > 0 ? delayMs : 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseSendAt() {
|
|
25
|
+
const delayMs = getTimedDelayMs();
|
|
26
|
+
return delayMs > 0 ? Date.now() + delayMs : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setTimedDelayFromMs(ms) {
|
|
30
|
+
const totalMinutes = Math.max(1, Math.ceil(ms / 60000));
|
|
31
|
+
const hours = Math.min(23, Math.floor(totalMinutes / 60));
|
|
32
|
+
const minutes = Math.min(59, totalMinutes - hours * 60);
|
|
33
|
+
document.getElementById('timed-hours').value = String(hours);
|
|
34
|
+
document.getElementById('timed-minutes').value = String(minutes);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function updateTimedSendPreview() {
|
|
38
|
+
const preview = document.getElementById('timed-preview');
|
|
39
|
+
const sendAt = parseSendAt();
|
|
40
|
+
preview.textContent = sendAt
|
|
41
|
+
? `Will run at ${new Date(sendAt).toLocaleString()}`
|
|
42
|
+
: 'Choose at least 1 minute.';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatCountdown(sendAt) {
|
|
46
|
+
const remaining = Math.max(0, sendAt - Date.now());
|
|
47
|
+
const totalSeconds = Math.ceil(remaining / 1000);
|
|
48
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
49
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
50
|
+
const seconds = totalSeconds % 60;
|
|
51
|
+
if (hours > 0) return `${hours}h\n${String(minutes).padStart(2, '0')}m`;
|
|
52
|
+
return `${minutes}:${String(seconds).padStart(2, '0')}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderTimedTags() {
|
|
56
|
+
const rail = document.getElementById('timed-tag-rail');
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const activeItems = timedInputs.filter(item => item.sendAt > now);
|
|
59
|
+
rail.innerHTML = activeItems.map(item => {
|
|
60
|
+
const encodedId = encodePathValue(item.id);
|
|
61
|
+
return `<button class="timed-tag${item.id === editingTimedInputId ? ' active' : ''}" title="${escapeHtml(item.text)}" onclick="editTimedInput(decodePathValue('${encodedId}'))">${escapeHtml(formatCountdown(item.sendAt))}</button>`;
|
|
62
|
+
}).join('');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function closeTimedSendPanel() {
|
|
66
|
+
document.getElementById('timed-send-panel').classList.remove('active');
|
|
67
|
+
editingTimedInputId = null;
|
|
68
|
+
renderTimedTags();
|
|
69
|
+
syncComposerButtonState();
|
|
70
|
+
updateTerminalControlsHeight();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stopTimedInputTimers() {
|
|
74
|
+
clearTimeout(timedSendRefreshTimer);
|
|
75
|
+
timedSendRefreshTimer = null;
|
|
76
|
+
if (timedTagTimer) clearInterval(timedTagTimer);
|
|
77
|
+
timedTagTimer = null;
|
|
78
|
+
timedInputs = [];
|
|
79
|
+
renderTimedTags();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resetTimedEditor(options = {}) {
|
|
83
|
+
editingTimedInputId = null;
|
|
84
|
+
document.getElementById('timed-save-btn').textContent = 'Add Timer';
|
|
85
|
+
document.getElementById('timed-cancel-edit-btn').style.display = 'none';
|
|
86
|
+
document.getElementById('timed-delete-btn').style.display = 'none';
|
|
87
|
+
if (!options.keepInput) {
|
|
88
|
+
inputEl.value = '';
|
|
89
|
+
inputEl.style.height = '38px';
|
|
90
|
+
}
|
|
91
|
+
setTimedDelayFromMs(5 * 60 * 1000);
|
|
92
|
+
updateTimedSendPreview();
|
|
93
|
+
renderTimedTags();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function loadTimedInputs() {
|
|
97
|
+
clearTimeout(timedSendRefreshTimer);
|
|
98
|
+
timedSendRefreshTimer = null;
|
|
99
|
+
if (!activeSessionId) return;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/timed-inputs`);
|
|
103
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
104
|
+
const data = await res.json();
|
|
105
|
+
timedInputs = data.items || [];
|
|
106
|
+
renderTimedTags();
|
|
107
|
+
if (!timedTagTimer) timedTagTimer = setInterval(renderTimedTags, 1000);
|
|
108
|
+
timedSendRefreshTimer = setTimeout(loadTimedInputs, 15000);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
timedInputs = [];
|
|
111
|
+
renderTimedTags();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function editTimedInput(id) {
|
|
116
|
+
const item = timedInputs.find(value => value.id === id);
|
|
117
|
+
if (!item) return;
|
|
118
|
+
closeComposerMenu();
|
|
119
|
+
initTimedDelaySelectors();
|
|
120
|
+
editingTimedInputId = id;
|
|
121
|
+
inputEl.value = item.text || '';
|
|
122
|
+
inputEl.style.height = 'auto';
|
|
123
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + 'px';
|
|
124
|
+
setTimedDelayFromMs(item.sendAt - Date.now());
|
|
125
|
+
document.getElementById('timed-save-btn').textContent = 'Update Timer';
|
|
126
|
+
document.getElementById('timed-cancel-edit-btn').style.display = '';
|
|
127
|
+
document.getElementById('timed-delete-btn').style.display = '';
|
|
128
|
+
document.getElementById('timed-send-panel').classList.add('active');
|
|
129
|
+
updateTimedSendPreview();
|
|
130
|
+
renderTimedTags();
|
|
131
|
+
syncComposerButtonState();
|
|
132
|
+
updateTerminalControlsHeight();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function saveTimedSend() {
|
|
136
|
+
if (!activeSessionId) return alert('No active session');
|
|
137
|
+
const text = inputEl.value;
|
|
138
|
+
if (!text.trim()) return alert('Type a message first');
|
|
139
|
+
const sendAt = parseSendAt();
|
|
140
|
+
if (!sendAt) return alert('Choose at least 1 minute');
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const url = editingTimedInputId
|
|
144
|
+
? `/api/sessions/${activeSessionId}/timed-inputs/${editingTimedInputId}`
|
|
145
|
+
: `/api/sessions/${activeSessionId}/timed-inputs`;
|
|
146
|
+
const res = await fetchWithTimeout(url, {
|
|
147
|
+
method: editingTimedInputId ? 'PATCH' : 'POST',
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
body: JSON.stringify({ text, sendAt })
|
|
150
|
+
});
|
|
151
|
+
const data = await res.json();
|
|
152
|
+
if (!res.ok || !data.success) throw new Error(data.error || 'Failed to save timer');
|
|
153
|
+
resetTimedEditor();
|
|
154
|
+
await loadTimedInputs();
|
|
155
|
+
} catch (e) {
|
|
156
|
+
alert('Save failed: ' + e.message);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function cancelTimedInput(id) {
|
|
161
|
+
if (!activeSessionId) return;
|
|
162
|
+
try {
|
|
163
|
+
await fetchWithTimeout(`/api/sessions/${activeSessionId}/timed-inputs/${id}`, { method: 'DELETE' });
|
|
164
|
+
if (editingTimedInputId === id) resetTimedEditor();
|
|
165
|
+
loadTimedInputs();
|
|
166
|
+
} catch (e) {
|
|
167
|
+
alert('Delete failed');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function deleteEditingTimedInput() {
|
|
172
|
+
if (editingTimedInputId) cancelTimedInput(editingTimedInputId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
document.getElementById('send-btn').addEventListener('click', performSend);
|
|
176
|
+
document.getElementById('timer-btn').addEventListener('click', () => {
|
|
177
|
+
const menu = document.getElementById('composer-menu');
|
|
178
|
+
const willOpen = !menu.classList.contains('active');
|
|
179
|
+
document.getElementById('timed-send-panel').classList.remove('active');
|
|
180
|
+
menu.classList.toggle('active', willOpen);
|
|
181
|
+
syncComposerButtonState();
|
|
182
|
+
updateTerminalControlsHeight();
|
|
183
|
+
});
|
|
184
|
+
document.getElementById('attach-image-btn').addEventListener('click', () => {
|
|
185
|
+
closeComposerMenu();
|
|
186
|
+
imageFileInput.click();
|
|
187
|
+
});
|
|
188
|
+
document.getElementById('schedule-send-btn').addEventListener('click', openTimedSendPanel);
|
|
189
|
+
imageFileInput.addEventListener('change', () => {
|
|
190
|
+
const files = imageFileInput.files;
|
|
191
|
+
if (files?.length) void uploadImageFiles(files);
|
|
192
|
+
imageFileInput.value = '';
|
|
193
|
+
});
|
|
194
|
+
inputEl.addEventListener('keydown', (e) => {
|
|
195
|
+
if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); performSend(); }
|
|
196
|
+
else if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete') {
|
|
197
|
+
markInputEditStart();
|
|
198
|
+
if (keepTerminalBottomForNextInput) requestAnimationFrame(restoreTerminalBottomSoon);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
document.getElementById('shortcut-rail').addEventListener('click', (e) => {
|
|
203
|
+
const commandBtn = e.target.closest('.command-key');
|
|
204
|
+
if (commandBtn) {
|
|
205
|
+
const command = commandBtn.dataset.command;
|
|
206
|
+
if (command) {
|
|
207
|
+
sendWS(command);
|
|
208
|
+
setTimeout(() => { sendWS('\r'); }, 1000);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const keyBtn = e.target.closest('.key-btn'); if (!keyBtn) return;
|
|
214
|
+
const key = keyBtn.dataset.key;
|
|
215
|
+
if (key === 'ctrl') { modifiers[key] = !modifiers[key]; keyBtn.classList.toggle('active', modifiers[key]); return; }
|
|
216
|
+
if (key === 'codex-perm') {
|
|
217
|
+
sendWS('/perm');
|
|
218
|
+
setTimeout(() => { sendWS('\r'); }, 1000);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const sequences = { up: '\x1b[A', down: '\x1b[B', left: '\x1b[D', right: '\x1b[C', enter: '\r', esc: '\x1b', tab: '\t', 'ctrl-c': '\x03', 'ctrl-y': '\x19' };
|
|
222
|
+
if (sequences[key]) sendWS(sequences[key]);
|
|
223
|
+
});
|
package/lib/workspace/service.js
CHANGED
|
@@ -7,8 +7,9 @@ class WorkspaceService {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
resolveInside(rootDir, targetPath = '') {
|
|
10
|
-
const root = path.resolve(rootDir);
|
|
11
|
-
const
|
|
10
|
+
const root = fs.realpathSync(path.resolve(rootDir));
|
|
11
|
+
const requestedPath = path.resolve(root, String(targetPath || ''));
|
|
12
|
+
const fullPath = fs.realpathSync(requestedPath);
|
|
12
13
|
const relative = path.relative(root, fullPath);
|
|
13
14
|
|
|
14
15
|
if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
|
package/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glad-web",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.30",
|
|
4
4
|
"description": "Glad transforms terminal-based AI coding tools into a polished, mobile-friendly local Web interface.",
|
|
5
|
-
"main": "index.js",
|
|
6
5
|
"bin": {
|
|
7
6
|
"glad": "bin/cli.js"
|
|
8
7
|
},
|
|
9
8
|
"scripts": {
|
|
10
9
|
"start": "node bin/cli.js",
|
|
10
|
+
"test:e2e": "playwright test",
|
|
11
11
|
"build:linux": "npx caxa -i . -e \".git\" \".github\" \"assets/Demo.mp4\" \"assets/demo.gif\" \"assets/demo.jpg\" \"assets/wechat.jpg\" \"glad-*\" \"*.tgz\" -m \"Extracting Glad...\" --output \"glad-linux-amd64\" -- \"{{caxa}}/node_modules/.bin/node\" \"{{caxa}}/bin/cli.js\"",
|
|
12
12
|
"build:macos:x64": "npx caxa -i . -e \".git\" \".github\" \"assets/Demo.mp4\" \"assets/demo.gif\" \"assets/demo.jpg\" \"assets/wechat.jpg\" \"glad-*\" \"*.tgz\" -m \"Extracting Glad...\" --output \"glad-macos-x64\" -- \"{{caxa}}/node_modules/.bin/node\" \"{{caxa}}/bin/cli.js\"",
|
|
13
13
|
"build:macos:arm64": "npx caxa -i . -e \".git\" \".github\" \"assets/Demo.mp4\" \"assets/demo.gif\" \"assets/demo.jpg\" \"assets/wechat.jpg\" \"glad-*\" \"*.tgz\" -m \"Extracting Glad...\" --output \"glad-macos-arm64\" -- \"{{caxa}}/node_modules/.bin/node\" \"{{caxa}}/bin/cli.js\"",
|
|
14
14
|
"build:windows": "npx caxa -i . -e \".git\" \".github\" \"assets/Demo.mp4\" \"assets/demo.gif\" \"assets/demo.jpg\" \"assets/wechat.jpg\" \"glad-*\" \"*.tgz\" -m \"Extracting Glad...\" --output \"glad-windows-amd64.exe\" -- \"{{caxa}}/node_modules/.bin/node\" \"{{caxa}}/bin/cli.js\"",
|
|
15
|
-
"test": "
|
|
15
|
+
"test": "node --test lib/**/*.test.js",
|
|
16
|
+
"check": "node scripts/check-syntax.js"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"terminal",
|
|
@@ -41,22 +42,26 @@
|
|
|
41
42
|
"files": [
|
|
42
43
|
"bin/",
|
|
43
44
|
"lib/",
|
|
45
|
+
"scripts/",
|
|
44
46
|
"!lib/**/*.test.js",
|
|
45
47
|
"assets/logo.svg",
|
|
46
48
|
"README.md"
|
|
47
49
|
],
|
|
48
50
|
"dependencies": {
|
|
49
51
|
"@anthropic-ai/claude-agent-sdk": "^0.3.201",
|
|
52
|
+
"@xterm/addon-fit": "^0.10.0",
|
|
53
|
+
"@xterm/headless": "^6.0.0",
|
|
54
|
+
"@xterm/xterm": "^5.5.0",
|
|
50
55
|
"chalk": "^4.1.2",
|
|
51
56
|
"commander": "^11.0.0",
|
|
52
57
|
"conf": "^10.2.0",
|
|
53
58
|
"express": "^5.2.1",
|
|
54
59
|
"node-pty": "npm:@lydell/node-pty@^1.1.0",
|
|
55
60
|
"uuid": "^11.1.1",
|
|
56
|
-
"ws": "^8.19.0"
|
|
57
|
-
"@xterm/headless": "^6.0.0"
|
|
61
|
+
"ws": "^8.19.0"
|
|
58
62
|
},
|
|
59
63
|
"devDependencies": {
|
|
64
|
+
"@playwright/test": "^1.61.1",
|
|
60
65
|
"caxa": "^3.0.1"
|
|
61
66
|
}
|
|
62
67
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
6
|
+
const sourceRoots = ['bin', 'lib', 'scripts', 'tests'];
|
|
7
|
+
const standaloneFiles = ['playwright.config.js'];
|
|
8
|
+
|
|
9
|
+
function collectJavaScriptFiles(directory) {
|
|
10
|
+
return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
|
|
11
|
+
const fullPath = path.join(directory, entry.name);
|
|
12
|
+
if (entry.isDirectory()) return collectJavaScriptFiles(fullPath);
|
|
13
|
+
return entry.isFile() && entry.name.endsWith('.js') ? [fullPath] : [];
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const files = [
|
|
18
|
+
...sourceRoots.flatMap(sourceRoot => collectJavaScriptFiles(path.join(projectRoot, sourceRoot))),
|
|
19
|
+
...standaloneFiles.map(file => path.join(projectRoot, file))
|
|
20
|
+
];
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
const result = spawnSync(process.execPath, ['--check', file], { stdio: 'inherit' });
|
|
23
|
+
if (result.status !== 0) process.exit(result.status || 1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(`Checked ${files.length} JavaScript files.`);
|