feat(run,notifications,profiles): stop sending prompts nobody typed, and fix notification + profile delivery
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).
This commit is contained in:
@@ -112,6 +112,7 @@ import { sagiPrefs } from "../components/Sagi/prefs";
|
||||
import { fmt, fmtCost, getCurrentLocale } from "../lib/format";
|
||||
import { subscribeToPush, unsubscribeFromPush } from "../lib/push";
|
||||
import { loadNotifPrefs, saveNotifPrefs, type NotifPrefs } from "../lib/notifPrefs";
|
||||
import { notify } from "../lib/notify";
|
||||
import { Tip } from "../components/Tip";
|
||||
import { ImportHistory } from "../components/ImportHistory";
|
||||
import { RemoteSources } from "../components/RemoteSources";
|
||||
@@ -569,7 +570,10 @@ export function Settings() {
|
||||
const perm = await Notification.requestPermission();
|
||||
if (perm === "granted") {
|
||||
updateNotifPrefs({ enabled: true });
|
||||
await subscribeToPush();
|
||||
// A browser with no service worker / on a non-secure origin rejects here;
|
||||
// notifications still work through the local fallback in useNotifications,
|
||||
// so this must not blow up the enable flow.
|
||||
await subscribeToPush().catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1554,14 +1558,6 @@ export function Settings() {
|
||||
label={t("notifications.awaitingInput")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 bg-surface-2 rounded-lg px-3.5 py-3">
|
||||
<MessageSquare className="w-4 h-4 text-accent flex-shrink-0" />
|
||||
<Toggle
|
||||
checked={notifPrefs.onTurnComplete}
|
||||
onChange={(v) => updateNotifPrefs({ onTurnComplete: v })}
|
||||
label={t("notifications.turnComplete")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 bg-surface-2 rounded-lg px-3.5 py-3">
|
||||
<Play className="w-4 h-4 text-status-success flex-shrink-0" />
|
||||
<Toggle
|
||||
@@ -1570,6 +1566,14 @@ export function Settings() {
|
||||
label={t("notifications.newSession")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 bg-surface-2 rounded-lg px-3.5 py-3">
|
||||
<MessageSquare className="w-4 h-4 text-accent flex-shrink-0" />
|
||||
<Toggle
|
||||
checked={notifPrefs.onTurnComplete}
|
||||
onChange={(v) => updateNotifPrefs({ onTurnComplete: v })}
|
||||
label={t("notifications.turnComplete")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 bg-surface-2 rounded-lg px-3.5 py-3">
|
||||
<CheckCircle className="w-4 h-4 text-violet-400 flex-shrink-0" />
|
||||
<Toggle
|
||||
@@ -1599,15 +1603,27 @@ export function Settings() {
|
||||
<div className="pt-3 border-t border-border">
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!("Notification" in window) || Notification.permission !== "granted")
|
||||
return;
|
||||
await fetch("/api/push/send", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
title: t("notifications.testTitle"),
|
||||
body: t("notifications.testBody"),
|
||||
}),
|
||||
// Same notify() the live events use, so the test proves the
|
||||
// real path — and reports WHERE it landed, since a push that
|
||||
// reaches nobody used to look exactly like success.
|
||||
const r = await notify(
|
||||
t("notifications.testTitle"),
|
||||
t("notifications.testBody")
|
||||
);
|
||||
const reason = r.errors
|
||||
.map((e) => [e.statusCode, e.message].filter(Boolean).join(" "))
|
||||
.join("; ");
|
||||
setActionResult({
|
||||
key: "notif-test",
|
||||
isError: r.via === "none" || r.via === "blocked",
|
||||
message:
|
||||
r.via === "push" || r.via === "native"
|
||||
? t("notifications.testSent")
|
||||
: r.via === "local"
|
||||
? t("notifications.testLocal")
|
||||
: r.via === "blocked"
|
||||
? t("notifications.testBlocked")
|
||||
: `${t("notifications.testFailed")}${reason ? ` (${reason})` : ""}`,
|
||||
});
|
||||
}}
|
||||
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md text-fg-secondary hover:text-fg-primary hover:bg-surface-4 border border-border transition-colors"
|
||||
@@ -1615,6 +1631,7 @@ export function Settings() {
|
||||
<Zap className="w-3 h-3" />
|
||||
{t("notifications.sendTest")}
|
||||
</button>
|
||||
<div className="mt-2">{actionBanner(["notif-test"])}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user