- Claude Code as an engine: chats on a "claude-code/<alias>" model run through the claude CLI (stream-json); tool approvals go through our permission system. Sign in from the web UI (URL + pasted code), CLI version and update from Settings; the Docker image contains the CLI. - Small models (8k context) for side tasks: input is cut to the model's context window; one-line chat status, "What does this do?" on approvals, "Plan as automation" (job draft from a chat). - Mobile: top bar no longer widens the page, image viewer (zoom, swipe, mark up, use in message), jump to previous own message, message menu, time under every message. - Approvals: push actions (allow/reject), "always allow in this project" (never for dangerous commands) with revoke. - Per turn: changed files with diffs and rollback; project check command with a green/red badge and "let the agent fix it". - Colour tags for chats in the colours of the active theme. - Project defaults for new chats, project page (timeline), offline queue. Tested in a throwaway container with a mock model (8k small model): 26 API checks passed (permissions, diffs, checks, defaults, job draft, side-task budget, Claude Code status/login URL, unsigned-in chat). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
70 lines
3.1 KiB
JavaScript
70 lines
3.1 KiB
JavaScript
// Web Push handling, imported into the generated service worker (workbox importScripts).
|
|
self.addEventListener("push", (event) => {
|
|
let data = {};
|
|
try {
|
|
data = event.data ? event.data.json() : {};
|
|
} catch {
|
|
data = { body: event.data ? event.data.text() : "" };
|
|
}
|
|
const title = data.title || "CodeCraft";
|
|
const de = String((self.navigator && self.navigator.language) || "").toLowerCase().startsWith("de");
|
|
// Approvals can be answered right from the notification.
|
|
const actions = data.kind === "permission" && data.permission_id ? [
|
|
{ action: "allow", title: de ? "Erlauben" : "Allow" },
|
|
{ action: "reject", title: de ? "Ablehnen" : "Reject" },
|
|
] : [];
|
|
const prefix = { permission: "🔐 ", question: "❓ ", done: "✅ " }[data.kind] || "";
|
|
event.waitUntil(
|
|
self.registration.showNotification(prefix + title, {
|
|
body: data.kind === "done" ? data.body || "Finished" : data.body || "",
|
|
tag: data.tag || "codecraft",
|
|
renotify: true,
|
|
icon: "/icon-192.png",
|
|
badge: "/icon-192.png",
|
|
actions,
|
|
requireInteraction: data.kind === "permission",
|
|
data: { url: data.url || "/", permission_id: data.permission_id || null },
|
|
}),
|
|
);
|
|
});
|
|
|
|
self.addEventListener("notificationclick", (event) => {
|
|
event.notification.close();
|
|
const info = event.notification.data || {};
|
|
if ((event.action === "allow" || event.action === "reject") && info.permission_id) {
|
|
const de = String((self.navigator && self.navigator.language) || "").toLowerCase().startsWith("de");
|
|
const target = new URL(info.url || "/", self.location.origin).href;
|
|
event.waitUntil(
|
|
fetch("/api/permissions/" + encodeURIComponent(info.permission_id), {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ reply: event.action === "allow" ? "once" : "reject" }),
|
|
})
|
|
.then((r) => {
|
|
if (!r.ok) throw new Error(String(r.status));
|
|
// Short confirmation, so the tap does not feel lost.
|
|
return self.registration.showNotification(event.action === "allow" ? (de ? "Erlaubt" : "Allowed") : (de ? "Abgelehnt" : "Rejected"), {
|
|
tag: "perm-" + info.permission_id,
|
|
icon: "/icon-192.png",
|
|
silent: true,
|
|
data: { url: info.url },
|
|
}).then(() => new Promise((res) => setTimeout(res, 2500))).then(() => self.registration.getNotifications({ tag: "perm-" + info.permission_id })).then((l) => l.forEach((n) => n.close()));
|
|
})
|
|
.catch(() => self.clients.openWindow(target)), // expired, answered elsewhere or signed out: open the app
|
|
);
|
|
return;
|
|
}
|
|
const url = new URL((event.notification.data && event.notification.data.url) || "/", self.location.origin).href;
|
|
event.waitUntil(
|
|
self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((list) => {
|
|
for (const client of list) {
|
|
if (new URL(client.url).origin === self.location.origin) {
|
|
return client.focus().then((c) => (c && "navigate" in c ? c.navigate(url) : c));
|
|
}
|
|
}
|
|
return self.clients.openWindow(url);
|
|
}),
|
|
);
|
|
});
|