64017c6ca5
Run console:
- Picking a pipeline no longer pre-writes anything into the prompt box
(`promptPrefill` in `LaneConsolePane.tsx` is gone — it wrote the driving
slash command on a claude lane and a stage checklist on a codex one).
Whatever sits in that box is sent as a real prompt the moment the run
starts, so the box is the user's alone now.
- Resuming a session sends NO prompt at all (`handleStart` in `RunSetup.tsx`):
the resumed session carries its own transcript, and leftover text in the box
fired an extra turn into it.
Notifications:
- `client/src/lib/notify.ts` is now the ONE sender, used by both
`useNotifications` and Settings' test button, and it reports where the
notification landed: `POST /api/push/send` answering `{native:false,pushed:0}`
means it went NOWHERE, so the client falls back to a local notification
instead of counting the 200 as delivered.
- A notification carries the lane that raised it (`/workspace?lane=<id>`), and
Workspace selects that lane on arrival.
Profiles:
- `server/lib/cc-profiles.js` reads and writes the macOS login Keychain item
`Claude Code-credentials`, where the `claude` CLI actually keeps
`claudeAiOauth` on darwin — a switch used to rewrite a file nothing reads.
The file stays the fallback, is kept in step only when it already exists, and
`AGENTHUB_CC_KEYCHAIN=0` pins it (tests set it).
Tests: npm run test:server (1820 pass), client vitest (553 pass).
86 lines
3.2 KiB
TypeScript
86 lines
3.2 KiB
TypeScript
/**
|
|
* @file Shows a browser notification — the single `notify()` both
|
|
* `useNotifications` (live events) and Settings' "Send Test Notification"
|
|
* button call, so the test proves the real delivery path and can report
|
|
* where it actually landed instead of a bare fire-and-forget.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
export type NotifyVia = "native" | "push" | "local" | "blocked" | "none";
|
|
|
|
export interface NotifyError {
|
|
statusCode?: number;
|
|
message?: string;
|
|
}
|
|
|
|
export interface NotifyResult {
|
|
via: NotifyVia;
|
|
errors: NotifyError[];
|
|
}
|
|
|
|
/**
|
|
* Prefers a server-relayed push (so it can arrive even if this tab isn't the
|
|
* active one, or the browser is backgrounded) and falls back to a local
|
|
* service-worker/`Notification` call when the server didn't deliver.
|
|
* @param title Notification title.
|
|
* @param body Notification body text.
|
|
* @param url Same-origin path to focus/navigate to on click (e.g. the lane
|
|
* that raised this notification). Omit for notifications with no target.
|
|
*/
|
|
export async function notify(title: string, body: string, url?: string): Promise<NotifyResult> {
|
|
if (!("Notification" in window) || Notification.permission !== "granted") {
|
|
return { via: "blocked", errors: [] };
|
|
}
|
|
|
|
const errors: NotifyError[] = [];
|
|
|
|
// A 200 from /api/push/send means the request was ACCEPTED, not that anything
|
|
// reached the user: with no push subscription (no service worker, a
|
|
// non-secure origin, a browser that never subscribed) or a push service the
|
|
// server cannot reach, it answers `{ok:true, native:false, pushed:0}` and
|
|
// nobody is notified. Only `native` or `pushed > 0` is actual delivery —
|
|
// anything else falls through to the local notification below, which is why
|
|
// the fallback is keyed on the RESULT rather than on fetch throwing.
|
|
try {
|
|
const res = await fetch("/api/push/send", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ title, body, url }),
|
|
});
|
|
const result = res.ok ? await res.json().catch(() => null) : null;
|
|
if (result?.native) return { via: "native", errors };
|
|
if (result && result.pushed > 0) return { via: "push", errors };
|
|
if (!res.ok) errors.push({ statusCode: res.status });
|
|
} catch (e) {
|
|
errors.push({ message: e instanceof Error ? e.message : String(e) });
|
|
}
|
|
|
|
try {
|
|
// Vector, not `/favicon.ico` (32x32 raster) — crisp at whatever size the
|
|
// OS renders it, matching the icon server/lib/push.js already sends.
|
|
const data = url ? { url } : undefined;
|
|
if ("serviceWorker" in navigator) {
|
|
const registration = await navigator.serviceWorker.ready;
|
|
await registration.showNotification(title, {
|
|
body,
|
|
icon: "/favicon.svg",
|
|
silent: false,
|
|
data,
|
|
});
|
|
} else {
|
|
const n = new Notification(title, { body, icon: "/favicon.svg", data });
|
|
if (url) {
|
|
// No service worker to catch the click here — navigate this tab directly.
|
|
n.onclick = () => {
|
|
window.focus();
|
|
window.location.assign(url);
|
|
};
|
|
}
|
|
}
|
|
return { via: "local", errors };
|
|
} catch (e) {
|
|
errors.push({ message: e instanceof Error ? e.message : String(e) });
|
|
return { via: "none", errors };
|
|
}
|
|
}
|