feat(auth): add remembered local login to the frontend

This commit is contained in:
2026-08-17 04:52:39 +02:00
parent 8c67cb75dc
commit 202822f3ba
36 changed files with 2649 additions and 163 deletions
+26 -11
View File
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
import { CornerDownLeft } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { postSteer, createSession } from "../api/sessions";
import { ApiError } from "../api/client";
import { ApiError, apiErrorMessage } from "../api/client";
import { getSettings } from "../api/settings";
import { getWorkspace, listWorkspaces } from "../api/workspaces";
import { listModels } from "../api/models";
@@ -11,6 +11,7 @@ import { useSessionStore } from "../store/sessionStore";
import {
WorkspaceSelectionError, workspacePolicyGate, workspacePreferences, type WorkspacePreference,
} from "../workspaces/drafts";
import { captureAuthOperation, isAuthOperationCurrent } from "../auth/authOperation";
const THINKING_LEVELS = ["low", "medium", "high"] as const;
@@ -48,6 +49,8 @@ export function SteerInput({
const setLastUserEntry = useSessionStore((s) => s.setLastUserEntry);
const setPhase = useSessionStore((s) => s.setPhase);
const taRef = useRef<HTMLTextAreaElement | null>(null);
const operationEpochRef = useRef(0);
useEffect(() => () => { operationEpochRef.current += 1; }, []);
// Merge our own ref (for autosizing) with the forwarded one (parent focus).
function attachRef(el: HTMLTextAreaElement | null) {
@@ -68,14 +71,19 @@ export function SteerInput({
async function submit() {
const trimmed = text.trim();
if (!trimmed || busy) return;
const operation = captureAuthOperation({ sessionId, disposalEpoch: operationEpochRef.current });
if (!operation) return;
setBusy(true);
if (sessionId) {
try {
await postSteer(sessionId, trimmed);
if (!isAuthOperationCurrent(operation, { sessionId, disposalEpoch: operationEpochRef.current })) return;
setLastUserEntry({ kind: "input", text: trimmed });
setText("");
} finally {
setBusy(false);
if (isAuthOperationCurrent(operation, { sessionId, disposalEpoch: operationEpochRef.current })) {
setBusy(false);
}
}
return;
}
@@ -86,22 +94,29 @@ export function SteerInput({
setLastUserEntry({ kind: "input", text: trimmed });
onSessionCreating?.(trimmed);
try {
const { id } = await createSession({ question: trimmed });
const { id } = await createSession({ question: trimmed }, {
operation,
isCurrent: () => isAuthOperationCurrent(operation, {
sessionId: null,
disposalEpoch: operationEpochRef.current,
}),
});
if (!isAuthOperationCurrent(operation, { sessionId: null, disposalEpoch: operationEpochRef.current })) return;
onSessionCreated?.(id);
setText("");
} catch (error) {
if (!isAuthOperationCurrent(operation, { sessionId: null, disposalEpoch: operationEpochRef.current })) return;
// Keep the question in the composer so retrying does not require retyping.
// A DWH-unreachable precheck (local dev, VPN down) carries a specific code;
// surface its message as the alert instead of the generic retry hint.
const payload = error instanceof ApiError
? (error.payload as { code?: string; error?: string } | undefined)
: undefined;
const alert = payload?.code === "dwh_unreachable"
? payload.error
// ApiError messages are derived from the local status/code allowlist;
// never surface response-body text from the server.
const alert = error instanceof ApiError && error.code === "dwh_unreachable"
? apiErrorMessage(error)
: error instanceof WorkspaceSelectionError ? error.message : undefined;
onSessionCreateFailed?.(alert);
} finally {
setBusy(false);
if (isAuthOperationCurrent(operation, { sessionId: null, disposalEpoch: operationEpochRef.current })) {
setBusy(false);
}
}
}