clay-server 2.7.1 → 2.8.0
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/project.js +176 -20
- package/lib/public/app.js +846 -92
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/clay-logo.png +0 -0
- package/lib/public/css/base.css +10 -0
- package/lib/public/css/filebrowser.css +1 -0
- package/lib/public/css/home-hub.css +455 -0
- package/lib/public/css/icon-strip.css +6 -5
- package/lib/public/css/loop.css +86 -29
- package/lib/public/css/messages.css +2 -0
- package/lib/public/css/mobile-nav.css +38 -12
- package/lib/public/css/overlays.css +205 -169
- package/lib/public/css/playbook.css +264 -0
- package/lib/public/css/profile.css +268 -0
- package/lib/public/css/scheduler-modal.css +883 -0
- package/lib/public/css/scheduler.css +379 -18
- package/lib/public/css/sidebar.css +305 -11
- package/lib/public/css/sticky-notes.css +23 -19
- package/lib/public/css/stt.css +155 -0
- package/lib/public/css/title-bar.css +14 -6
- package/lib/public/favicon-banded-32.png +0 -0
- package/lib/public/favicon-banded.png +0 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-banded-76.png +0 -0
- package/lib/public/icon-banded-96.png +0 -0
- package/lib/public/index.html +252 -32
- package/lib/public/modules/ascii-logo.js +389 -0
- package/lib/public/modules/filebrowser.js +2 -1
- package/lib/public/modules/markdown.js +108 -0
- package/lib/public/modules/notifications.js +50 -63
- package/lib/public/modules/playbook.js +578 -0
- package/lib/public/modules/profile.js +357 -0
- package/lib/public/modules/project-settings.js +4 -9
- package/lib/public/modules/scheduler.js +1620 -34
- package/lib/public/modules/server-settings.js +1 -1
- package/lib/public/modules/sidebar.js +378 -31
- package/lib/public/modules/sticky-notes.js +2 -0
- package/lib/public/modules/stt.js +272 -0
- package/lib/public/modules/terminal.js +32 -0
- package/lib/public/modules/theme.js +3 -10
- package/lib/public/modules/tools.js +2 -1
- package/lib/public/style.css +4 -0
- package/lib/public/sw.js +82 -3
- package/lib/public/wordmark-banded-20.png +0 -0
- package/lib/public/wordmark-banded-32.png +0 -0
- package/lib/public/wordmark-banded-64.png +0 -0
- package/lib/public/wordmark-banded-80.png +0 -0
- package/lib/scheduler.js +43 -3
- package/lib/sdk-bridge.js +3 -2
- package/lib/server.js +124 -3
- package/lib/sessions.js +34 -1
- package/package.json +1 -1
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// Speech-to-Text module using Web Speech API
|
|
2
|
+
// Uses browser's built-in speech recognition (Chrome/Edge/Safari → Google servers)
|
|
3
|
+
|
|
4
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
5
|
+
import { autoResize } from './input.js';
|
|
6
|
+
|
|
7
|
+
var ctx;
|
|
8
|
+
|
|
9
|
+
// --- State ---
|
|
10
|
+
var recording = false;
|
|
11
|
+
var recognition = null;
|
|
12
|
+
var selectedLang = null;
|
|
13
|
+
var textBeforeSTT = '';
|
|
14
|
+
var interimText = '';
|
|
15
|
+
|
|
16
|
+
// DOM refs
|
|
17
|
+
var sttBtn = null;
|
|
18
|
+
var langPopover = null;
|
|
19
|
+
|
|
20
|
+
// --- Language options ---
|
|
21
|
+
// Web Speech API uses BCP-47 language tags
|
|
22
|
+
var LANGUAGES = [
|
|
23
|
+
{ code: 'en-US', name: 'English' },
|
|
24
|
+
{ code: 'ko-KR', name: 'Korean' },
|
|
25
|
+
{ code: 'ja-JP', name: 'Japanese' },
|
|
26
|
+
{ code: 'zh-CN', name: 'Chinese' },
|
|
27
|
+
{ code: 'es-ES', name: 'Spanish' },
|
|
28
|
+
{ code: 'fr-FR', name: 'French' },
|
|
29
|
+
{ code: 'de-DE', name: 'German' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// --- Persist language choice ---
|
|
33
|
+
function saveLang(code) {
|
|
34
|
+
try { localStorage.setItem('stt-lang', code); } catch (e) { /* ignore */ }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadLang() {
|
|
38
|
+
try { return localStorage.getItem('stt-lang'); } catch (e) { return null; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Check browser support ---
|
|
42
|
+
function getSpeechRecognition() {
|
|
43
|
+
return window.SpeechRecognition || window.webkitSpeechRecognition || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Init ---
|
|
47
|
+
export function initSTT(_ctx) {
|
|
48
|
+
ctx = _ctx;
|
|
49
|
+
|
|
50
|
+
sttBtn = document.getElementById('stt-btn');
|
|
51
|
+
if (!sttBtn) return;
|
|
52
|
+
|
|
53
|
+
if (!getSpeechRecognition()) {
|
|
54
|
+
sttBtn.style.display = 'none';
|
|
55
|
+
console.warn('[STT] Web Speech API not supported in this browser');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Restore saved language
|
|
60
|
+
selectedLang = loadLang();
|
|
61
|
+
|
|
62
|
+
sttBtn.addEventListener('click', function(e) {
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
|
|
65
|
+
if (recording) {
|
|
66
|
+
stopRecording();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!selectedLang) {
|
|
71
|
+
showLangPopover();
|
|
72
|
+
} else {
|
|
73
|
+
startRecording();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Right-click to change language
|
|
78
|
+
sttBtn.addEventListener('contextmenu', function(e) {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
if (recording) stopRecording();
|
|
82
|
+
showLangPopover();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Language popover ---
|
|
87
|
+
function showLangPopover() {
|
|
88
|
+
if (langPopover) {
|
|
89
|
+
hideLangPopover();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
langPopover = document.createElement('div');
|
|
94
|
+
langPopover.className = 'stt-lang-popover';
|
|
95
|
+
|
|
96
|
+
var html = '<div class="stt-lang-title">Voice Input Language</div>';
|
|
97
|
+
for (var i = 0; i < LANGUAGES.length; i++) {
|
|
98
|
+
var l = LANGUAGES[i];
|
|
99
|
+
var activeClass = (selectedLang === l.code) ? ' stt-lang-active' : '';
|
|
100
|
+
html += '<button class="stt-lang-option' + activeClass + '" data-lang="' + l.code + '">' +
|
|
101
|
+
'<span class="stt-lang-name">' + l.name + '</span>' +
|
|
102
|
+
'</button>';
|
|
103
|
+
}
|
|
104
|
+
langPopover.innerHTML = html;
|
|
105
|
+
|
|
106
|
+
langPopover.querySelectorAll('.stt-lang-option').forEach(function(btn) {
|
|
107
|
+
btn.addEventListener('click', function() {
|
|
108
|
+
onLangSelected(btn.dataset.lang);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
var wrapper = document.getElementById('input-wrapper');
|
|
113
|
+
wrapper.appendChild(langPopover);
|
|
114
|
+
|
|
115
|
+
setTimeout(function() {
|
|
116
|
+
document.addEventListener('click', closeLangOnOutside);
|
|
117
|
+
}, 0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function closeLangOnOutside(e) {
|
|
121
|
+
if (langPopover && !langPopover.contains(e.target) && e.target !== sttBtn && !sttBtn.contains(e.target)) {
|
|
122
|
+
hideLangPopover();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hideLangPopover() {
|
|
127
|
+
if (langPopover) {
|
|
128
|
+
langPopover.remove();
|
|
129
|
+
langPopover = null;
|
|
130
|
+
}
|
|
131
|
+
document.removeEventListener('click', closeLangOnOutside);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function onLangSelected(code) {
|
|
135
|
+
selectedLang = code;
|
|
136
|
+
saveLang(code);
|
|
137
|
+
hideLangPopover();
|
|
138
|
+
startRecording();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- Recording ---
|
|
142
|
+
function startRecording() {
|
|
143
|
+
if (recording) return;
|
|
144
|
+
|
|
145
|
+
var SpeechRecognition = getSpeechRecognition();
|
|
146
|
+
if (!SpeechRecognition) return;
|
|
147
|
+
|
|
148
|
+
recognition = new SpeechRecognition();
|
|
149
|
+
recognition.lang = selectedLang || 'en-US';
|
|
150
|
+
recognition.continuous = true;
|
|
151
|
+
recognition.interimResults = true;
|
|
152
|
+
|
|
153
|
+
textBeforeSTT = ctx.inputEl.value;
|
|
154
|
+
interimText = '';
|
|
155
|
+
|
|
156
|
+
recognition.onresult = function(e) {
|
|
157
|
+
var final = '';
|
|
158
|
+
var interim = '';
|
|
159
|
+
|
|
160
|
+
for (var i = 0; i < e.results.length; i++) {
|
|
161
|
+
var result = e.results[i];
|
|
162
|
+
if (result.isFinal) {
|
|
163
|
+
final += result[0].transcript;
|
|
164
|
+
} else {
|
|
165
|
+
interim += result[0].transcript;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
var text = textBeforeSTT;
|
|
170
|
+
if (final) {
|
|
171
|
+
if (text && text.length > 0 && text[text.length - 1] !== ' ' && text[text.length - 1] !== '\n') {
|
|
172
|
+
text += ' ';
|
|
173
|
+
}
|
|
174
|
+
text += final;
|
|
175
|
+
}
|
|
176
|
+
if (interim) {
|
|
177
|
+
if (text && text.length > 0 && text[text.length - 1] !== ' ' && text[text.length - 1] !== '\n') {
|
|
178
|
+
text += ' ';
|
|
179
|
+
}
|
|
180
|
+
text += interim;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
ctx.inputEl.value = text;
|
|
184
|
+
autoResize();
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
recognition.onerror = function(e) {
|
|
188
|
+
console.error('[STT] Recognition error:', e.error);
|
|
189
|
+
if (e.error === 'not-allowed') {
|
|
190
|
+
if (ctx.addSystemMessage) {
|
|
191
|
+
ctx.addSystemMessage('Microphone access denied.\n\nTo fix: click the lock icon in the address bar → Site settings → Microphone → Allow, then reload.', true);
|
|
192
|
+
}
|
|
193
|
+
stopRecording();
|
|
194
|
+
} else if (e.error === 'no-speech') {
|
|
195
|
+
// Silence — just keep listening
|
|
196
|
+
} else if (e.error === 'network') {
|
|
197
|
+
if (ctx.addSystemMessage) {
|
|
198
|
+
ctx.addSystemMessage('Speech recognition unavailable.\n\nWeb Speech API sends audio to Google servers for recognition. Some Chromium forks (Arc, Brave) block this connection.\n\nSupported: Chrome, Edge, Safari 14.1+, Samsung Internet\nNot supported: Arc, Brave, Firefox', true);
|
|
199
|
+
}
|
|
200
|
+
stopRecording();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
recognition.onend = function() {
|
|
205
|
+
// Auto-restart if still recording (browser may stop after silence)
|
|
206
|
+
if (recording) {
|
|
207
|
+
// Save confirmed text so far
|
|
208
|
+
textBeforeSTT = ctx.inputEl.value;
|
|
209
|
+
try {
|
|
210
|
+
recognition.start();
|
|
211
|
+
} catch (e) {
|
|
212
|
+
// Already started or other error
|
|
213
|
+
stopRecording();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
recognition.start();
|
|
220
|
+
recording = true;
|
|
221
|
+
sttBtn.classList.add('stt-active');
|
|
222
|
+
sttBtn.innerHTML =
|
|
223
|
+
'<span class="stt-wave">' +
|
|
224
|
+
'<span class="stt-wave-bar"></span>' +
|
|
225
|
+
'<span class="stt-wave-bar"></span>' +
|
|
226
|
+
'<span class="stt-wave-bar"></span>' +
|
|
227
|
+
'<span class="stt-wave-bar"></span>' +
|
|
228
|
+
'<span class="stt-wave-bar"></span>' +
|
|
229
|
+
'</span>' +
|
|
230
|
+
'<span class="stt-stop-label">Stop</span>';
|
|
231
|
+
ctx.inputEl.setAttribute('placeholder', 'Listening...');
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.error('[STT] Failed to start:', err);
|
|
234
|
+
if (ctx.addSystemMessage) {
|
|
235
|
+
ctx.addSystemMessage('Failed to start voice input: ' + err.message, true);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function stopRecording() {
|
|
241
|
+
if (!recording) return;
|
|
242
|
+
recording = false;
|
|
243
|
+
|
|
244
|
+
if (recognition) {
|
|
245
|
+
try { recognition.stop(); } catch (e) { /* ignore */ }
|
|
246
|
+
recognition = null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
sttBtn.classList.remove('stt-active');
|
|
250
|
+
sttBtn.innerHTML = iconHtml('mic');
|
|
251
|
+
refreshIcons();
|
|
252
|
+
ctx.inputEl.setAttribute('placeholder', 'Message Claude Code...');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// --- External lang setter (used by profile module) ---
|
|
256
|
+
export function setSTTLang(code) {
|
|
257
|
+
selectedLang = code;
|
|
258
|
+
saveLang(code);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function getSTTLang() {
|
|
262
|
+
return selectedLang;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- Exports ---
|
|
266
|
+
export function isSTTRecording() {
|
|
267
|
+
return recording;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function isSTTInitializing() {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
@@ -239,6 +239,20 @@ function createXtermForTab(tab) {
|
|
|
239
239
|
}
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
+
// Ctrl+V paste for Firefox (Firefox blocks xterm.js clipboard access)
|
|
243
|
+
bodyEl.addEventListener("keydown", function (e) {
|
|
244
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
if (navigator.clipboard && navigator.clipboard.readText) {
|
|
247
|
+
navigator.clipboard.readText().then(function (text) {
|
|
248
|
+
if (text && ctx.ws && ctx.connected) {
|
|
249
|
+
ctx.ws.send(JSON.stringify({ type: "term_input", id: tab.id, data: text }));
|
|
250
|
+
}
|
|
251
|
+
}).catch(function () { /* permission denied or not available */ });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
242
256
|
// Right-click context menu
|
|
243
257
|
bodyEl.addEventListener("contextmenu", function (e) {
|
|
244
258
|
showTermCtxMenu(e, tab);
|
|
@@ -599,6 +613,24 @@ function showTermCtxMenu(e, tab) {
|
|
|
599
613
|
});
|
|
600
614
|
menu.appendChild(copyItem);
|
|
601
615
|
|
|
616
|
+
// Paste
|
|
617
|
+
var pasteItem = document.createElement("button");
|
|
618
|
+
pasteItem.className = "term-ctx-item";
|
|
619
|
+
pasteItem.innerHTML = iconHtml("clipboard-paste") + " <span>Paste</span>";
|
|
620
|
+
pasteItem.addEventListener("click", function (ev) {
|
|
621
|
+
ev.stopPropagation();
|
|
622
|
+
closeTermCtxMenu();
|
|
623
|
+
if (!tab.xterm) return;
|
|
624
|
+
if (navigator.clipboard && navigator.clipboard.readText) {
|
|
625
|
+
navigator.clipboard.readText().then(function (text) {
|
|
626
|
+
if (text && ctx.ws && ctx.connected) {
|
|
627
|
+
ctx.ws.send(JSON.stringify({ type: "term_input", id: tab.id, data: text }));
|
|
628
|
+
}
|
|
629
|
+
}).catch(function () { /* permission denied or not available */ });
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
menu.appendChild(pasteItem);
|
|
633
|
+
|
|
602
634
|
// Clear
|
|
603
635
|
var clearItem = document.createElement("button");
|
|
604
636
|
clearItem.className = "term-ctx-item";
|
|
@@ -370,17 +370,10 @@ export function applyTheme(themeId, fromPicker) {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
// ---
|
|
373
|
+
// --- Favicon update on theme change ---
|
|
374
374
|
function updateMascotSvgs(vars, isLight) {
|
|
375
|
-
var
|
|
376
|
-
|
|
377
|
-
var src = isLight ? lightSrc : darkSrc;
|
|
378
|
-
var mascots = document.querySelectorAll("img.footer-mascot");
|
|
379
|
-
for (var i = 0; i < mascots.length; i++) {
|
|
380
|
-
mascots[i].setAttribute("src", src);
|
|
381
|
-
}
|
|
382
|
-
var faviconEl = document.querySelector('link[rel="icon"][type="image/svg+xml"]');
|
|
383
|
-
if (faviconEl) faviconEl.setAttribute("href", src);
|
|
375
|
+
var faviconEl = document.querySelector('link[rel="icon"][type="image/png"]');
|
|
376
|
+
if (faviconEl) faviconEl.setAttribute("href", "favicon-banded.png");
|
|
384
377
|
}
|
|
385
378
|
|
|
386
379
|
// --- Theme loading from server ---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { escapeHtml, copyToClipboard } from './utils.js';
|
|
2
2
|
import { iconHtml, refreshIcons, randomThinkingVerb } from './icons.js';
|
|
3
|
-
import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
|
|
3
|
+
import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, parseEmojis } from './markdown.js';
|
|
4
4
|
import { renderUnifiedDiff, renderSplitDiff, renderPatchDiff } from './diff.js';
|
|
5
5
|
import { openFile } from './filebrowser.js';
|
|
6
6
|
|
|
@@ -744,6 +744,7 @@ export function renderPlanCard(content) {
|
|
|
744
744
|
body.innerHTML = renderMarkdown(content);
|
|
745
745
|
highlightCodeBlocks(body);
|
|
746
746
|
renderMermaidBlocks(body);
|
|
747
|
+
parseEmojis(body);
|
|
747
748
|
|
|
748
749
|
var copyBtn = header.querySelector(".plan-card-copy");
|
|
749
750
|
if (copyBtn) {
|
package/lib/public/style.css
CHANGED
package/lib/public/sw.js
CHANGED
|
@@ -1,11 +1,88 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
var CACHE_NAME = "clay-offline-v1";
|
|
2
|
+
|
|
3
|
+
self.addEventListener("install", function (event) {
|
|
4
|
+
event.waitUntil(self.skipWaiting());
|
|
3
5
|
});
|
|
4
6
|
|
|
5
7
|
self.addEventListener("activate", function (event) {
|
|
6
|
-
|
|
8
|
+
// Clean up old cache versions
|
|
9
|
+
event.waitUntil(
|
|
10
|
+
caches.keys().then(function (names) {
|
|
11
|
+
return Promise.all(
|
|
12
|
+
names.filter(function (n) { return n !== CACHE_NAME; })
|
|
13
|
+
.map(function (n) { return caches.delete(n); })
|
|
14
|
+
);
|
|
15
|
+
}).then(function () {
|
|
16
|
+
return self.clients.claim();
|
|
17
|
+
})
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// --- Offline cache: network-first, cache-fallback ---
|
|
22
|
+
|
|
23
|
+
function shouldCache(request, response) {
|
|
24
|
+
if (request.method !== "GET") return false;
|
|
25
|
+
if (!response || !response.ok) return false;
|
|
26
|
+
// Cache same-origin static assets and CDN resources (jsdelivr, fonts)
|
|
27
|
+
var url = new URL(request.url);
|
|
28
|
+
if (url.origin === self.location.origin) return true;
|
|
29
|
+
if (url.hostname === "cdn.jsdelivr.net") return true;
|
|
30
|
+
if (url.hostname === "fonts.googleapis.com") return true;
|
|
31
|
+
if (url.hostname === "fonts.gstatic.com") return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
self.addEventListener("fetch", function (event) {
|
|
36
|
+
var request = event.request;
|
|
37
|
+
|
|
38
|
+
// Only handle GET requests
|
|
39
|
+
if (request.method !== "GET") return;
|
|
40
|
+
|
|
41
|
+
// Skip WebSocket upgrade requests and API/data endpoints
|
|
42
|
+
var url = new URL(request.url);
|
|
43
|
+
if (url.pathname.indexOf("/ws") !== -1) return;
|
|
44
|
+
if (url.pathname.indexOf("/api/") !== -1) return;
|
|
45
|
+
|
|
46
|
+
event.respondWith(
|
|
47
|
+
fetch(request).then(function (response) {
|
|
48
|
+
// Network succeeded: cache a clone for offline use
|
|
49
|
+
if (shouldCache(request, response)) {
|
|
50
|
+
var clone = response.clone();
|
|
51
|
+
caches.open(CACHE_NAME).then(function (cache) {
|
|
52
|
+
cache.put(request, clone);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return response;
|
|
56
|
+
}).catch(function () {
|
|
57
|
+
// Network failed: serve from cache
|
|
58
|
+
return caches.match(request).then(function (cached) {
|
|
59
|
+
if (cached) return cached;
|
|
60
|
+
|
|
61
|
+
// For navigation requests, serve cached index.html as fallback
|
|
62
|
+
// (handles /p/slug/ routes that all serve the same SPA shell)
|
|
63
|
+
if (request.mode === "navigate") {
|
|
64
|
+
return caches.match("/index.html").then(function (indexCached) {
|
|
65
|
+
// If even the index page is not cached, show a minimal offline page
|
|
66
|
+
if (indexCached) return indexCached;
|
|
67
|
+
return new Response(
|
|
68
|
+
"<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Clay</title>" +
|
|
69
|
+
"<style>body{margin:0;background:#000;color:#888;display:flex;" +
|
|
70
|
+
"align-items:center;justify-content:center;height:100vh;" +
|
|
71
|
+
"font-family:monospace;font-size:1.2em}</style></head>" +
|
|
72
|
+
"<body><p>Waiting for server…</p></body></html>",
|
|
73
|
+
{ headers: { "Content-Type": "text/html" } }
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Non-navigation request with no cache: return network error
|
|
78
|
+
return new Response("", { status: 503, statusText: "Offline" });
|
|
79
|
+
});
|
|
80
|
+
})
|
|
81
|
+
);
|
|
7
82
|
});
|
|
8
83
|
|
|
84
|
+
// --- Push notifications ---
|
|
85
|
+
|
|
9
86
|
self.addEventListener("push", function (event) {
|
|
10
87
|
var data = {};
|
|
11
88
|
try { data = event.data.json(); } catch (e) { return; }
|
|
@@ -46,6 +123,8 @@ self.addEventListener("push", function (event) {
|
|
|
46
123
|
);
|
|
47
124
|
});
|
|
48
125
|
|
|
126
|
+
// --- Notification click ---
|
|
127
|
+
|
|
49
128
|
self.addEventListener("notificationclick", function (event) {
|
|
50
129
|
var data = event.notification.data || {};
|
|
51
130
|
event.notification.close();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/scheduler.js
CHANGED
|
@@ -143,6 +143,12 @@ function createLoopRegistry(opts) {
|
|
|
143
143
|
// Recalculate nextRunAt for scheduled records
|
|
144
144
|
if (rec.cron && rec.enabled) {
|
|
145
145
|
rec.nextRunAt = nextRunTime(rec.cron);
|
|
146
|
+
} else if (!rec.cron && rec.enabled && rec.date && rec.time && rec.source === "schedule") {
|
|
147
|
+
// One-off: recalculate from date+time
|
|
148
|
+
var dtP = rec.date.split("-");
|
|
149
|
+
var tmP = rec.time.split(":");
|
|
150
|
+
var runD = new Date(parseInt(dtP[0], 10), parseInt(dtP[1], 10) - 1, parseInt(dtP[2], 10), parseInt(tmP[0], 10), parseInt(tmP[1], 10), 0);
|
|
151
|
+
rec.nextRunAt = runD.getTime();
|
|
146
152
|
}
|
|
147
153
|
records.push(rec);
|
|
148
154
|
} catch (e) {
|
|
@@ -192,7 +198,6 @@ function createLoopRegistry(opts) {
|
|
|
192
198
|
|
|
193
199
|
for (var i = 0; i < records.length; i++) {
|
|
194
200
|
var rec = records[i];
|
|
195
|
-
if (!rec.cron) continue; // skip one-off
|
|
196
201
|
if (!rec.enabled) continue;
|
|
197
202
|
if (!rec.nextRunAt) continue;
|
|
198
203
|
if (rec.nextRunAt > now) continue;
|
|
@@ -214,8 +219,15 @@ function createLoopRegistry(opts) {
|
|
|
214
219
|
|
|
215
220
|
// Update nextRunAt
|
|
216
221
|
rec.lastRunAt = now;
|
|
217
|
-
|
|
222
|
+
if (rec.cron) {
|
|
223
|
+
rec.nextRunAt = nextRunTime(rec.cron, now);
|
|
224
|
+
} else {
|
|
225
|
+
// One-off schedule: disable after firing
|
|
226
|
+
rec.nextRunAt = null;
|
|
227
|
+
rec.enabled = false;
|
|
228
|
+
}
|
|
218
229
|
save();
|
|
230
|
+
if (onChange) onChange(records);
|
|
219
231
|
|
|
220
232
|
console.log("[loop-registry] Triggering scheduled loop: " + rec.name + " (" + rec.id + ")");
|
|
221
233
|
if (onTrigger) {
|
|
@@ -241,11 +253,26 @@ function createLoopRegistry(opts) {
|
|
|
241
253
|
lastRunAt: null,
|
|
242
254
|
lastRunResult: null,
|
|
243
255
|
nextRunAt: null,
|
|
256
|
+
description: data.description || "",
|
|
257
|
+
date: data.date || null,
|
|
258
|
+
time: data.time || null,
|
|
259
|
+
allDay: data.allDay !== undefined ? data.allDay : true,
|
|
260
|
+
linkedTaskId: data.linkedTaskId || null,
|
|
244
261
|
craftingSessionId: data.craftingSessionId || null,
|
|
262
|
+
source: data.source || null,
|
|
263
|
+
color: data.color || null,
|
|
264
|
+
recurrenceEnd: data.recurrenceEnd || null,
|
|
245
265
|
runs: [],
|
|
246
266
|
};
|
|
247
267
|
if (rec.cron && rec.enabled) {
|
|
248
268
|
rec.nextRunAt = nextRunTime(rec.cron);
|
|
269
|
+
} else if (!rec.cron && rec.date && rec.time && rec.source === "schedule") {
|
|
270
|
+
// One-off schedule: compute nextRunAt from date + time
|
|
271
|
+
var dtParts = rec.date.split("-");
|
|
272
|
+
var tmParts = rec.time.split(":");
|
|
273
|
+
var runDate = new Date(parseInt(dtParts[0], 10), parseInt(dtParts[1], 10) - 1, parseInt(dtParts[2], 10), parseInt(tmParts[0], 10), parseInt(tmParts[1], 10), 0);
|
|
274
|
+
rec.nextRunAt = runDate.getTime();
|
|
275
|
+
rec.enabled = true;
|
|
249
276
|
}
|
|
250
277
|
records.push(rec);
|
|
251
278
|
save();
|
|
@@ -261,8 +288,20 @@ function createLoopRegistry(opts) {
|
|
|
261
288
|
if (data.cron !== undefined) rec.cron = data.cron;
|
|
262
289
|
if (data.enabled !== undefined) rec.enabled = data.enabled;
|
|
263
290
|
if (data.maxIterations !== undefined) rec.maxIterations = data.maxIterations;
|
|
291
|
+
if (data.date !== undefined) rec.date = data.date;
|
|
292
|
+
if (data.recurrenceEnd !== undefined) rec.recurrenceEnd = data.recurrenceEnd;
|
|
264
293
|
rec.updatedAt = Date.now();
|
|
265
|
-
|
|
294
|
+
if (rec.cron && rec.enabled) {
|
|
295
|
+
rec.nextRunAt = nextRunTime(rec.cron);
|
|
296
|
+
} else if (!rec.cron && rec.date && rec.time && rec.source === "schedule") {
|
|
297
|
+
var dtP2 = rec.date.split("-");
|
|
298
|
+
var tmP2 = rec.time.split(":");
|
|
299
|
+
var runD2 = new Date(parseInt(dtP2[0], 10), parseInt(dtP2[1], 10) - 1, parseInt(dtP2[2], 10), parseInt(tmP2[0], 10), parseInt(tmP2[1], 10), 0);
|
|
300
|
+
rec.nextRunAt = runD2.getTime();
|
|
301
|
+
rec.enabled = true;
|
|
302
|
+
} else {
|
|
303
|
+
rec.nextRunAt = null;
|
|
304
|
+
}
|
|
266
305
|
|
|
267
306
|
save();
|
|
268
307
|
if (onChange) onChange(records);
|
|
@@ -277,6 +316,7 @@ function createLoopRegistry(opts) {
|
|
|
277
316
|
rec[keys[i]] = data[keys[i]];
|
|
278
317
|
}
|
|
279
318
|
save();
|
|
319
|
+
if (onChange) onChange(records);
|
|
280
320
|
return rec;
|
|
281
321
|
}
|
|
282
322
|
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -487,8 +487,9 @@ function createSDKBridge(opts) {
|
|
|
487
487
|
// --- SDK query lifecycle ---
|
|
488
488
|
|
|
489
489
|
function handleCanUseTool(session, toolName, input, opts) {
|
|
490
|
-
// Ralph Loop: auto-approve all tools, deny interactive ones
|
|
491
|
-
|
|
490
|
+
// Ralph Loop execution: auto-approve all tools, deny interactive ones.
|
|
491
|
+
// Crafting sessions are interactive — user and Claude collaborate to build PROMPT.md / JUDGE.md.
|
|
492
|
+
if (session.loop && session.loop.active && session.loop.role !== "crafting") {
|
|
492
493
|
if (toolName === "AskUserQuestion") {
|
|
493
494
|
return Promise.resolve({ behavior: "deny", message: "Autonomous mode. Make your own decision." });
|
|
494
495
|
}
|