Pre-send key guards: no orphan error-shell chats on first send

The owner's asymmetry find (interlude over the zero-keys wave): the
models-unconfigured case guards BEFORE send — no request, no chat
created, setup opens — but the key cases only reacted to the 503 after
POST /api/chats had already created the record, leaving orphan error-shell
chats in the sidebar. The guards now twin the models mechanism on BOTH
creation paths (home start(), /chat/new send()):

- Zero keys (useZeroKeys reuse): no provider key present -> skip creation
  and send entirely, open SetupDialog (stage 1 first-key screen).
- Strong-provider key missing (owner-approved extension): keys exist
  generally but the strong slot's pinned provider has none — the original
  deleted-key scenario. New pure strongKeyless(catalog) in setup-cta.tsx:
  slots.strong resolved pin + providers[] (present-key only, the catalog's
  own federation surface), false when unconfigured (the models guard owns
  that case) or catalog not loaded (the server 503 gate remains the belt).
  useStrongKeyless rides useModelCatalog, re-reading on the 'keys' AND
  'models' signals (a deleted key or a re-pinned slot flips it live).

Both guards run after the configured check, same
skip-everything-and-open-setup treatment; 'force' (setup's onDone replay)
bypasses both — completing setup implies a keyed provider. /chat/[id]
sessions keep the 503 revert-draft interception unchanged: those chats
legitimately exist.

Pins: strongKeyless fires on a keyless strong pin (keys elsewhere or
none), never fires with the pin's key present (incl. the keyPresent=false
belt), false on unset pin / unloaded catalog. Normal-path pins ride the
existing startDraftSession contract suite. Live-verified on :3001 under
LH_IGNORE_ENV_KEYS=1 with a file-key anthropic + openrouter-pinned strong:
both guards on both pages (setup opens stage-1 / stage-2 respectively,
draft preserved, 154 chats unchanged, zero sidebar entries), normal path
with a keyed strong provider still creates and lands the prompt redirect.
929/929, tsc/lint/build green.
This commit is contained in:
amosblomqvist
2026-09-25 00:46:10 +03:00
parent 1f777f770c
commit 66090ed769
4 changed files with 100 additions and 6 deletions
+16 -2
View File
@@ -8,7 +8,7 @@ import { DictationBar, useDictation } from "@/components/dictation";
import { useDefaultSettings } from "@/components/default-settings";
import { SidebarToggle } from "@/components/sidebar";
import { SetupDialog } from "@/components/setup-dialog";
import { useZeroKeys, ZeroKeysPill } from "@/components/setup-cta";
import { useStrongKeyless, useZeroKeys, ZeroKeysPill } from "@/components/setup-cta";
import { StatsBar } from "@/components/stats-bar";
import type { ChatSettings } from "@/src/core";
@@ -168,6 +168,10 @@ export function NewSession({
// The zero-keys truth (zero-keys wave): no provider key = the pill
// takes precedence over the models CTA (keys come first in the chain).
const zeroKeys = useZeroKeys();
// The strong slot's keyless truth (pre-send key guards, owner interlude
// 2026-09-25): keys may exist generally while the STRONG slot's pinned
// provider has none — the same skip-everything treatment on send.
const strongKeyless = useStrongKeyless();
const [setupOpen, setSetupOpen] = useState(false);
const send = useCallback((force = false) => {
@@ -178,6 +182,16 @@ export function NewSession({
setSetupOpen(true);
return;
}
// The pre-send KEY guards (owner interlude, no-orphan-chats): guard
// BEFORE startDraftSession creates the record — a refused first
// send would otherwise leave an orphan error-shell chat in the
// sidebar. Same skip-everything-and-open-setup treatment; `force`
// (setup's onDone replay) bypasses, and the server's 503 gate
// remains the belt for /chat/[id] sessions.
if (!force && (zeroKeys || strongKeyless)) {
setSetupOpen(true);
return;
}
setBusy(true);
void startDraftSession(text, settings, fetch)
.then((target) => {
@@ -185,7 +199,7 @@ export function NewSession({
else setBusy(false);
})
.catch(() => setBusy(false));
}, [text, busy, settings, router, configured]);
}, [text, busy, settings, router, configured, zeroKeys, strongKeyless]);
return (
<>