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.
- package/README.md +4 -192
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/bin/glad.cjs +56 -0
- package/package.json +19 -61
- package/README.zh-CN.md +0 -198
- package/assets/logo.svg +0 -43
- package/bin/cli.js +0 -65
- package/lib/ai-tools/demo/enhanced-demo.js +0 -625
- package/lib/ai-tools/demo/index.js +0 -24
- package/lib/ai-tools/demo/responses.js +0 -88
- package/lib/ai-tools/detector.js +0 -76
- package/lib/ai-tools/registry.js +0 -300
- package/lib/claude/cli-usage.js +0 -95
- package/lib/claude/config.js +0 -82
- package/lib/claude/structured-session.js +0 -884
- package/lib/claude/transcript-repository.js +0 -216
- package/lib/codex/image-store.js +0 -174
- package/lib/codex/structured-session.js +0 -1590
- package/lib/commands/config.js +0 -78
- package/lib/commands/tools.js +0 -128
- package/lib/commands/web.js +0 -605
- package/lib/config/constants.js +0 -17
- package/lib/config/manager.js +0 -108
- package/lib/git/service.js +0 -83
- package/lib/notifications/message-formatter.js +0 -94
- package/lib/notifications/notification-service.js +0 -143
- package/lib/notifications/serverchan-client.js +0 -58
- package/lib/notifications/serverchan-settings-store.js +0 -115
- package/lib/schedule/job-runner.js +0 -162
- package/lib/schedule/job-store.js +0 -167
- package/lib/schedule/key-sequences.js +0 -49
- package/lib/schedule/scheduler-service.js +0 -39
- package/lib/server/routes/notifications.js +0 -52
- package/lib/server/routes/providers.js +0 -114
- package/lib/server/routes/schedules.js +0 -54
- package/lib/server/routes/skillhub.js +0 -104
- package/lib/server/routes/usage.js +0 -23
- package/lib/server/routes/workspace.js +0 -77
- package/lib/session/buffer.js +0 -102
- package/lib/session/file-attachment-store.js +0 -168
- package/lib/session/pty-manager.js +0 -255
- package/lib/session/rendered-history.js +0 -225
- package/lib/session/session-manager.js +0 -1032
- package/lib/session/text-history.js +0 -274
- package/lib/skillhub/client.js +0 -121
- package/lib/skillhub/settings-store.js +0 -168
- package/lib/skillhub/skill-installer.js +0 -320
- package/lib/usage/ccusage-runner.js +0 -128
- package/lib/usage/source-catalog.js +0 -26
- package/lib/usage/usage-service.js +0 -226
- package/lib/utils/logger.js +0 -74
- package/lib/utils/pid.js +0 -67
- package/lib/utils/validation.js +0 -53
- package/lib/web/bootstrap.js +0 -34
- package/lib/web/claude.js +0 -1150
- package/lib/web/codex.js +0 -1045
- package/lib/web/composer.js +0 -493
- package/lib/web/core.js +0 -385
- package/lib/web/git.js +0 -535
- package/lib/web/gitgraph.js +0 -293
- package/lib/web/index.html +0 -547
- package/lib/web/layout.js +0 -69
- package/lib/web/notifications.js +0 -164
- package/lib/web/schedules.js +0 -245
- package/lib/web/session.js +0 -361
- package/lib/web/shell.js +0 -74
- package/lib/web/skillhub.js +0 -197
- package/lib/web/styles.css +0 -932
- package/lib/web/terminal-scroll.js +0 -81
- package/lib/web/theme.js +0 -60
- package/lib/web/timed-inputs.js +0 -216
- package/lib/web/usage.js +0 -323
- package/lib/workspace/service.js +0 -77
- package/scripts/check-syntax.js +0 -26
package/lib/web/layout.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
const GLAD_SPLIT_QUERY = window.gladLayout.splitQuery;
|
|
2
|
-
const GLAD_SIDEBAR_KEY = window.gladLayout.sidebarStorageKey;
|
|
3
|
-
|
|
4
|
-
function isSplitLayout() {
|
|
5
|
-
return window.matchMedia(GLAD_SPLIT_QUERY).matches;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function clampSidebarWidth(value) {
|
|
9
|
-
return window.gladLayout.clampSidebarWidth(value);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function applySidebarWidth(value) {
|
|
13
|
-
return window.gladLayout.applySidebarWidth(value);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function initializeResponsiveLayout() {
|
|
17
|
-
const storedWidth = Number(localStorage.getItem(GLAD_SIDEBAR_KEY));
|
|
18
|
-
applySidebarWidth(storedWidth || 348);
|
|
19
|
-
|
|
20
|
-
const handle = document.getElementById('sidebar-resizer');
|
|
21
|
-
if (handle) {
|
|
22
|
-
let dragging = false;
|
|
23
|
-
const onPointerMove = event => {
|
|
24
|
-
if (!dragging) return;
|
|
25
|
-
applySidebarWidth(event.clientX);
|
|
26
|
-
if (typeof syncLayout === 'function') syncLayout({ keepAtBottom: false });
|
|
27
|
-
};
|
|
28
|
-
const stopDragging = () => {
|
|
29
|
-
if (!dragging) return;
|
|
30
|
-
dragging = false;
|
|
31
|
-
handle.classList.remove('dragging');
|
|
32
|
-
document.body.style.removeProperty('cursor');
|
|
33
|
-
document.body.style.removeProperty('user-select');
|
|
34
|
-
const width = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-w'));
|
|
35
|
-
localStorage.setItem(GLAD_SIDEBAR_KEY, String(clampSidebarWidth(width)));
|
|
36
|
-
};
|
|
37
|
-
handle.addEventListener('pointerdown', event => {
|
|
38
|
-
if (!isSplitLayout()) return;
|
|
39
|
-
dragging = true;
|
|
40
|
-
handle.classList.add('dragging');
|
|
41
|
-
handle.setPointerCapture?.(event.pointerId);
|
|
42
|
-
document.body.style.cursor = 'col-resize';
|
|
43
|
-
document.body.style.userSelect = 'none';
|
|
44
|
-
event.preventDefault();
|
|
45
|
-
});
|
|
46
|
-
window.addEventListener('pointermove', onPointerMove);
|
|
47
|
-
window.addEventListener('pointerup', stopDragging);
|
|
48
|
-
window.addEventListener('pointercancel', stopDragging);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const media = window.matchMedia(GLAD_SPLIT_QUERY);
|
|
52
|
-
media.addEventListener('change', () => {
|
|
53
|
-
applySidebarWidth(parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-w')));
|
|
54
|
-
if (isSplitLayout()) scheduleSessionPolling();
|
|
55
|
-
if (typeof syncLayout === 'function') requestAnimationFrame(() => syncLayout({ keepAtBottom: false }));
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const controls = document.getElementById('terminal-controls');
|
|
59
|
-
if (controls && typeof ResizeObserver !== 'undefined') {
|
|
60
|
-
new ResizeObserver(() => {
|
|
61
|
-
if (typeof syncLayout === 'function') syncLayout();
|
|
62
|
-
}).observe(controls);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
window.addEventListener('resize', () => {
|
|
67
|
-
if (isSplitLayout()) applySidebarWidth(parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-w')));
|
|
68
|
-
});
|
|
69
|
-
document.addEventListener('DOMContentLoaded', initializeResponsiveLayout);
|
package/lib/web/notifications.js
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
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
|
-
if (typeof syncGladThemeControls === 'function') syncGladThemeControls();
|
|
53
|
-
if (typeof loadSkillHubSettings === 'function') void loadSkillHubSettings();
|
|
54
|
-
setServerChanStatus('Loading configuration…');
|
|
55
|
-
try {
|
|
56
|
-
const response = await fetchWithTimeout('/api/notifications/serverchan', {}, 10000);
|
|
57
|
-
const data = await response.json();
|
|
58
|
-
if (!response.ok) throw new Error(data.error || 'Could not load configuration');
|
|
59
|
-
serverChanSettings = data;
|
|
60
|
-
document.getElementById('serverchan-client-type').value = data.clientType || 'wechat';
|
|
61
|
-
const keyInput = document.getElementById('serverchan-send-key');
|
|
62
|
-
keyInput.value = '';
|
|
63
|
-
keyInput.placeholder = data.configured ? data.maskedKey : 'SCT...';
|
|
64
|
-
document.getElementById('serverchan-key-hint').textContent = data.configured
|
|
65
|
-
? `Saved: ${data.maskedKey}. Leave blank to keep the current SendKey.`
|
|
66
|
-
: 'The SendKey is stored only on this Glad host.';
|
|
67
|
-
document.getElementById('serverchan-remove-btn').style.display = data.configured ? 'inline-flex' : 'none';
|
|
68
|
-
setServerChanStatus(data.configured ? 'Configuration saved.' : 'ServerChan is not configured.');
|
|
69
|
-
} catch (error) {
|
|
70
|
-
setServerChanStatus(error.message || 'Could not load configuration', 'error');
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function closeSettings(event = null) {
|
|
75
|
-
if (event && event.target.id !== 'settings-modal-overlay') return;
|
|
76
|
-
document.getElementById('settings-modal-overlay').style.display = 'none';
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function currentServerChanForm() {
|
|
80
|
-
return {
|
|
81
|
-
sendKey: document.getElementById('serverchan-send-key').value.trim(),
|
|
82
|
-
clientType: document.getElementById('serverchan-client-type').value
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function setServerChanStatus(message, type = '') {
|
|
87
|
-
const status = document.getElementById('serverchan-settings-status');
|
|
88
|
-
status.textContent = message;
|
|
89
|
-
status.className = `serverchan-settings-status${type ? ` ${type}` : ''}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function setServerChanBusy(busy) {
|
|
93
|
-
document.getElementById('serverchan-save-btn').disabled = busy;
|
|
94
|
-
document.getElementById('serverchan-test-btn').disabled = busy;
|
|
95
|
-
document.getElementById('serverchan-remove-btn').disabled = busy;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function saveServerChanSettings() {
|
|
99
|
-
setServerChanBusy(true);
|
|
100
|
-
setServerChanStatus('Saving…');
|
|
101
|
-
try {
|
|
102
|
-
const response = await fetchWithTimeout('/api/notifications/serverchan', {
|
|
103
|
-
method: 'PUT',
|
|
104
|
-
headers: { 'Content-Type': 'application/json' },
|
|
105
|
-
body: JSON.stringify(currentServerChanForm())
|
|
106
|
-
}, 10000);
|
|
107
|
-
const data = await response.json();
|
|
108
|
-
if (!response.ok) throw new Error(data.error || 'Could not save configuration');
|
|
109
|
-
serverChanSettings = data.settings;
|
|
110
|
-
const keyInput = document.getElementById('serverchan-send-key');
|
|
111
|
-
keyInput.value = '';
|
|
112
|
-
keyInput.placeholder = data.settings.maskedKey;
|
|
113
|
-
document.getElementById('serverchan-key-hint').textContent =
|
|
114
|
-
`Saved: ${data.settings.maskedKey}. Leave blank to keep the current SendKey.`;
|
|
115
|
-
document.getElementById('serverchan-remove-btn').style.display = 'inline-flex';
|
|
116
|
-
setServerChanStatus('Configuration saved. Per-chat notification switches are unchanged.', 'success');
|
|
117
|
-
} catch (error) {
|
|
118
|
-
setServerChanStatus(error.message || 'Could not save configuration', 'error');
|
|
119
|
-
} finally {
|
|
120
|
-
setServerChanBusy(false);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function testServerChanSettings() {
|
|
125
|
-
setServerChanBusy(true);
|
|
126
|
-
setServerChanStatus('Sending test message…');
|
|
127
|
-
try {
|
|
128
|
-
const response = await fetchWithTimeout('/api/notifications/serverchan/test', {
|
|
129
|
-
method: 'POST',
|
|
130
|
-
headers: { 'Content-Type': 'application/json' },
|
|
131
|
-
body: JSON.stringify(currentServerChanForm())
|
|
132
|
-
}, 15000);
|
|
133
|
-
const data = await response.json();
|
|
134
|
-
if (!response.ok) throw new Error(data.error || 'Could not send test message');
|
|
135
|
-
setServerChanStatus('Test message sent. The test did not save configuration.', 'success');
|
|
136
|
-
} catch (error) {
|
|
137
|
-
setServerChanStatus(error.message || 'Could not send test message', 'error');
|
|
138
|
-
} finally {
|
|
139
|
-
setServerChanBusy(false);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function removeServerChanSettings() {
|
|
144
|
-
if (!confirm('Remove ServerChan configuration and disable notifications for every chat?')) return;
|
|
145
|
-
setServerChanBusy(true);
|
|
146
|
-
try {
|
|
147
|
-
const response = await fetchWithTimeout('/api/notifications/serverchan', {
|
|
148
|
-
method: 'DELETE'
|
|
149
|
-
}, 10000);
|
|
150
|
-
const data = await response.json();
|
|
151
|
-
if (!response.ok) throw new Error(data.error || 'Could not remove configuration');
|
|
152
|
-
serverChanSettings = data.settings;
|
|
153
|
-
document.getElementById('serverchan-send-key').value = '';
|
|
154
|
-
document.getElementById('serverchan-send-key').placeholder = 'SCT...';
|
|
155
|
-
document.getElementById('serverchan-key-hint').textContent = 'The SendKey is stored only on this Glad host.';
|
|
156
|
-
document.getElementById('serverchan-remove-btn').style.display = 'none';
|
|
157
|
-
setServerChanStatus('Configuration removed. Notifications are disabled for every chat.', 'success');
|
|
158
|
-
await refreshSessionsNow();
|
|
159
|
-
} catch (error) {
|
|
160
|
-
setServerChanStatus(error.message || 'Could not remove configuration', 'error');
|
|
161
|
-
} finally {
|
|
162
|
-
setServerChanBusy(false);
|
|
163
|
-
}
|
|
164
|
-
}
|
package/lib/web/schedules.js
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
async function ensureScheduleTools() {
|
|
2
|
-
if (scheduleTools.length) return;
|
|
3
|
-
const res = await fetchWithTimeout('/api/tools');
|
|
4
|
-
scheduleTools = await res.json();
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function defaultSchedule() {
|
|
8
|
-
return {
|
|
9
|
-
id: null,
|
|
10
|
-
name: 'Scheduled Task',
|
|
11
|
-
enabled: true,
|
|
12
|
-
schedule: { time: '09:00', weekdays: [1, 2, 3, 4, 5] },
|
|
13
|
-
target: { toolKey: (scheduleTools[0] && scheduleTools[0].key) || 'demo', workingDirectory: '' },
|
|
14
|
-
steps: [
|
|
15
|
-
{ type: 'sleep', seconds: 60 },
|
|
16
|
-
{ type: 'sendText', text: 'hello' },
|
|
17
|
-
{ type: 'sleep', seconds: 1 },
|
|
18
|
-
{ type: 'sendKey', key: 'enter' },
|
|
19
|
-
{ type: 'sleep', seconds: 60 },
|
|
20
|
-
{ type: 'closeSession' }
|
|
21
|
-
]
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function showScheduleModal(job = null) {
|
|
26
|
-
try {
|
|
27
|
-
await ensureScheduleTools();
|
|
28
|
-
loadAppConfig();
|
|
29
|
-
} catch (e) {
|
|
30
|
-
alert('Failed to load tools');
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const data = job || defaultSchedule();
|
|
35
|
-
editingScheduleId = data.id || null;
|
|
36
|
-
editingSteps = (data.steps || []).map(step => ({ ...step }));
|
|
37
|
-
selectedWeekdays = [...(data.schedule.weekdays || [1, 2, 3, 4, 5])];
|
|
38
|
-
document.getElementById('schedule-modal-title').textContent = editingScheduleId ? 'Edit Scheduled Task' : 'New Scheduled Task';
|
|
39
|
-
document.getElementById('schedule-name').value = data.name || 'Scheduled Task';
|
|
40
|
-
document.getElementById('schedule-cwd').value = data.target.workingDirectory || '';
|
|
41
|
-
document.getElementById('schedule-time').value = data.schedule.time || '09:00';
|
|
42
|
-
|
|
43
|
-
const toolSelect = document.getElementById('schedule-tool');
|
|
44
|
-
toolSelect.innerHTML = scheduleTools.map(t => `<option value="${escapeHtml(t.key)}">${escapeHtml(t.displayName)}</option>`).join('');
|
|
45
|
-
const requestedTool = data.target.toolKey || '';
|
|
46
|
-
toolSelect.value = scheduleTools.some(t => t.key === requestedTool) ? requestedTool : ((scheduleTools[0] && scheduleTools[0].key) || 'demo');
|
|
47
|
-
renderWeekdays();
|
|
48
|
-
renderScheduleSteps();
|
|
49
|
-
document.getElementById('schedule-modal-overlay').style.display = 'flex';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function closeScheduleModal(e) {
|
|
53
|
-
if (!e || e.target.id === 'schedule-modal-overlay') {
|
|
54
|
-
document.getElementById('schedule-modal-overlay').style.display = 'none';
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function renderWeekdays() {
|
|
59
|
-
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
60
|
-
document.getElementById('schedule-weekdays').innerHTML = labels.map((label, day) => {
|
|
61
|
-
const active = selectedWeekdays.includes(day);
|
|
62
|
-
return `<button type="button" class="weekday-btn ${active ? 'active' : ''}" onclick="toggleWeekday(${day})">${label}</button>`;
|
|
63
|
-
}).join('');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function toggleWeekday(day) {
|
|
67
|
-
if (selectedWeekdays.includes(day)) {
|
|
68
|
-
selectedWeekdays = selectedWeekdays.filter(value => value !== day);
|
|
69
|
-
} else {
|
|
70
|
-
selectedWeekdays.push(day);
|
|
71
|
-
selectedWeekdays.sort((a, b) => a - b);
|
|
72
|
-
}
|
|
73
|
-
renderWeekdays();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function addScheduleStep(type) {
|
|
77
|
-
if (!type) return;
|
|
78
|
-
const defaults = {
|
|
79
|
-
sleep: { type: 'sleep', seconds: 1 },
|
|
80
|
-
sendText: { type: 'sendText', text: '' },
|
|
81
|
-
sendKey: { type: 'sendKey', key: 'enter' },
|
|
82
|
-
keyDown: { type: 'keyDown', key: 'ctrl' },
|
|
83
|
-
keyUp: { type: 'keyUp', key: 'ctrl' },
|
|
84
|
-
stop: { type: 'stop' },
|
|
85
|
-
closeSession: { type: 'closeSession' }
|
|
86
|
-
};
|
|
87
|
-
editingSteps.push(defaults[type] || { type });
|
|
88
|
-
renderScheduleSteps();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function updateStep(index, field, value) {
|
|
92
|
-
if (!editingSteps[index]) return;
|
|
93
|
-
if (field === 'seconds') editingSteps[index][field] = Math.max(0, Number(value) || 0);
|
|
94
|
-
else editingSteps[index][field] = value;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function moveStep(index, delta) {
|
|
98
|
-
const nextIndex = index + delta;
|
|
99
|
-
if (nextIndex < 0 || nextIndex >= editingSteps.length) return;
|
|
100
|
-
const [step] = editingSteps.splice(index, 1);
|
|
101
|
-
editingSteps.splice(nextIndex, 0, step);
|
|
102
|
-
renderScheduleSteps();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function copyStep(index) {
|
|
106
|
-
editingSteps.splice(index + 1, 0, { ...editingSteps[index] });
|
|
107
|
-
renderScheduleSteps();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function removeStep(index) {
|
|
111
|
-
editingSteps.splice(index, 1);
|
|
112
|
-
renderScheduleSteps();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function stepInputHtml(step, index) {
|
|
116
|
-
if (step.type === 'sleep') {
|
|
117
|
-
return `<input type="number" min="0" step="0.1" value="${escapeHtml(step.seconds || 0)}" onchange="updateStep(${index}, 'seconds', this.value)">`;
|
|
118
|
-
}
|
|
119
|
-
if (step.type === 'sendText') {
|
|
120
|
-
return `<textarea rows="2" placeholder="Text to send" onchange="updateStep(${index}, 'text', this.value)" oninput="updateStep(${index}, 'text', this.value)">${escapeHtml(step.text || '')}</textarea>`;
|
|
121
|
-
}
|
|
122
|
-
if (step.type === 'sendKey') {
|
|
123
|
-
return `<select onchange="updateStep(${index}, 'key', this.value)">
|
|
124
|
-
${['enter','tab','esc','up','down','left','right','backspace','delete','home','end','ctrl+c','ctrl+d','ctrl+l'].map(key => `<option value="${key}" ${step.key === key ? 'selected' : ''}>${key}</option>`).join('')}
|
|
125
|
-
</select>`;
|
|
126
|
-
}
|
|
127
|
-
if (step.type === 'keyDown' || step.type === 'keyUp') {
|
|
128
|
-
return `<select onchange="updateStep(${index}, 'key', this.value)">
|
|
129
|
-
${['ctrl','alt'].map(key => `<option value="${key}" ${step.key === key ? 'selected' : ''}>${key}</option>`).join('')}
|
|
130
|
-
</select>`;
|
|
131
|
-
}
|
|
132
|
-
if (step.type === 'closeSession') {
|
|
133
|
-
return '<span style="color:var(--text-dim); font-size:13px;">Close the session and end this run</span>';
|
|
134
|
-
}
|
|
135
|
-
return '<span style="color:var(--text-dim); font-size:13px;">End this run</span>';
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function renderScheduleSteps() {
|
|
139
|
-
const container = document.getElementById('schedule-steps');
|
|
140
|
-
if (!editingSteps.length) {
|
|
141
|
-
container.innerHTML = '<p style="color:var(--text-dim);">No steps yet.</p>';
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
container.innerHTML = editingSteps.map((step, index) => `
|
|
145
|
-
<div class="step-card">
|
|
146
|
-
<div class="step-grid">
|
|
147
|
-
<select onchange="editingSteps[${index}] = { type: this.value }; addDefaultStepFields(${index}); renderScheduleSteps();">
|
|
148
|
-
${['sleep','sendText','sendKey','keyDown','keyUp','stop','closeSession'].map(type => `<option value="${type}" ${step.type === type ? 'selected' : ''}>${type}</option>`).join('')}
|
|
149
|
-
</select>
|
|
150
|
-
<div>${stepInputHtml(step, index)}</div>
|
|
151
|
-
<div style="display:flex; gap:6px; justify-content:flex-end;">
|
|
152
|
-
<button class="small-btn" onclick="moveStep(${index}, -1)">↑</button>
|
|
153
|
-
<button class="small-btn" onclick="moveStep(${index}, 1)">↓</button>
|
|
154
|
-
<button class="small-btn" onclick="copyStep(${index})">Copy</button>
|
|
155
|
-
<button class="small-btn danger" onclick="removeStep(${index})">Del</button>
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
`).join('');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function addDefaultStepFields(index) {
|
|
163
|
-
const type = editingSteps[index].type;
|
|
164
|
-
if (type === 'sleep') editingSteps[index].seconds = 1;
|
|
165
|
-
if (type === 'sendText') editingSteps[index].text = '';
|
|
166
|
-
if (type === 'sendKey') editingSteps[index].key = 'enter';
|
|
167
|
-
if (type === 'keyDown' || type === 'keyUp') editingSteps[index].key = 'ctrl';
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function collectSchedulePayload() {
|
|
171
|
-
if (!selectedWeekdays.length) throw new Error('Select at least one weekday');
|
|
172
|
-
return {
|
|
173
|
-
name: document.getElementById('schedule-name').value.trim() || 'Scheduled Task',
|
|
174
|
-
enabled: true,
|
|
175
|
-
schedule: {
|
|
176
|
-
time: document.getElementById('schedule-time').value || '09:00',
|
|
177
|
-
weekdays: selectedWeekdays
|
|
178
|
-
},
|
|
179
|
-
target: {
|
|
180
|
-
toolKey: document.getElementById('schedule-tool').value,
|
|
181
|
-
workingDirectory: document.getElementById('schedule-cwd').value.trim()
|
|
182
|
-
},
|
|
183
|
-
steps: editingSteps
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async function saveSchedule() {
|
|
188
|
-
try {
|
|
189
|
-
const payload = collectSchedulePayload();
|
|
190
|
-
const url = editingScheduleId ? `/api/schedules/${editingScheduleId}` : '/api/schedules';
|
|
191
|
-
const method = editingScheduleId ? 'PATCH' : 'POST';
|
|
192
|
-
const res = await fetchWithTimeout(url, {
|
|
193
|
-
method,
|
|
194
|
-
headers: { 'Content-Type': 'application/json' },
|
|
195
|
-
body: JSON.stringify(payload)
|
|
196
|
-
});
|
|
197
|
-
if (!res.ok) throw new Error((await res.json()).error || 'Save failed');
|
|
198
|
-
closeScheduleModal();
|
|
199
|
-
switchLobbyTab('schedules');
|
|
200
|
-
} catch (e) {
|
|
201
|
-
alert(e.message);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async function editSchedule(id) {
|
|
206
|
-
const res = await fetchWithTimeout(`/api/schedules/${id}`);
|
|
207
|
-
if (!res.ok) return alert('Failed to load schedule');
|
|
208
|
-
showScheduleModal(await res.json());
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function simulateSchedule(id) {
|
|
212
|
-
try {
|
|
213
|
-
const res = await fetchWithTimeout(`/api/schedules/${id}/simulate`, { method: 'POST' }, 30000);
|
|
214
|
-
const data = await res.json();
|
|
215
|
-
if (!res.ok) throw new Error(data.error || 'Test failed');
|
|
216
|
-
await loadSchedules();
|
|
217
|
-
if (data.sessionId) {
|
|
218
|
-
const jobRes = await fetchWithTimeout(`/api/schedules/${id}`).catch(() => null);
|
|
219
|
-
const job = jobRes && jobRes.ok ? await jobRes.json() : null;
|
|
220
|
-
joinSession(data.sessionId, 'Schedule Test', job?.target?.toolKey || null);
|
|
221
|
-
}
|
|
222
|
-
} catch (e) {
|
|
223
|
-
alert(e.message);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async function duplicateSchedule(id) {
|
|
228
|
-
await fetchWithTimeout(`/api/schedules/${id}/duplicate`, { method: 'POST' });
|
|
229
|
-
loadSchedules();
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function toggleSchedule(id, enabled) {
|
|
233
|
-
await fetchWithTimeout(`/api/schedules/${id}/enabled`, {
|
|
234
|
-
method: 'PATCH',
|
|
235
|
-
headers: { 'Content-Type': 'application/json' },
|
|
236
|
-
body: JSON.stringify({ enabled })
|
|
237
|
-
});
|
|
238
|
-
loadSchedules();
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function deleteSchedule(id) {
|
|
242
|
-
if (!confirm('Delete scheduled task?')) return;
|
|
243
|
-
await fetchWithTimeout(`/api/schedules/${id}`, { method: 'DELETE' });
|
|
244
|
-
loadSchedules();
|
|
245
|
-
}
|