Files
openclaw/ui/src/lib/connection-hints.ts
T
Peter Steinberger 2829fe107c improve(ui): retire the Overview page for a chat-first start screen (#104819)
* feat(ui): replace Overview page with Connection settings and sidebar attention chips

* feat(ui): open new-session drafts on the chat start screen hero

* refactor(ui): drop old overview-hints paths after rename

* chore(i18n): sync control-ui locale bundles for connection/palette/attention keys

* test(ui): expect attention slot above the sidebar update card

* chore(i18n): re-sync locale bundles after rebase onto main

* test(ui): stub sidebar-attention RPCs in app-sidebar unit tests

* test(ui): use a non-secret-shaped token fixture in connection view test

* refactor(ui): destructure gateway connection in connection settings draft

* refactor(ui): keep connection settings code out of secret-scanner shapes

* test(ui): expect Connection in the settings Connections group

* improve(ui): idle-refresh sidebar attention chips for always-visible windows

* chore(i18n): refresh raw-copy baseline after rebase

* docs(ui): note the unknown-route chat fallback covers retired paths
2026-07-11 18:11:14 -07:00

121 lines
3.7 KiB
TypeScript

// Connection-failure hint classification shared by the login gate.
import {
ConnectErrorDetailCodes,
readConnectPairingRequiredMessage,
} from "../../../packages/gateway-protocol/src/connect-error-details.js";
import { normalizeLowercaseStringOrEmpty } from "../lib/string-coerce.ts";
const AUTH_REQUIRED_CODES = new Set<string>([
ConnectErrorDetailCodes.AUTH_REQUIRED,
ConnectErrorDetailCodes.AUTH_TOKEN_MISSING,
ConnectErrorDetailCodes.AUTH_PASSWORD_MISSING,
ConnectErrorDetailCodes.AUTH_TOKEN_NOT_CONFIGURED,
ConnectErrorDetailCodes.AUTH_PASSWORD_NOT_CONFIGURED,
]);
const AUTH_FAILURE_CODES = new Set<string>([
...AUTH_REQUIRED_CODES,
ConnectErrorDetailCodes.AUTH_UNAUTHORIZED,
ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH,
ConnectErrorDetailCodes.AUTH_PASSWORD_MISMATCH,
ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH,
ConnectErrorDetailCodes.AUTH_RATE_LIMITED,
ConnectErrorDetailCodes.AUTH_TAILSCALE_IDENTITY_MISSING,
ConnectErrorDetailCodes.AUTH_TAILSCALE_PROXY_MISSING,
ConnectErrorDetailCodes.AUTH_TAILSCALE_WHOIS_FAILED,
ConnectErrorDetailCodes.AUTH_TAILSCALE_IDENTITY_MISMATCH,
]);
const BROWSER_WEBSOCKET_SECURITY_ERROR_CODE = "BROWSER_WEBSOCKET_SECURITY_ERROR";
const INSECURE_CONTEXT_CODES = new Set<string>([
BROWSER_WEBSOCKET_SECURITY_ERROR_CODE,
ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED,
]);
type AuthHintKind = "required" | "failed";
type PairingHint =
| {
kind: "pairing-required";
requestId: string | null;
}
| {
kind: "scope-upgrade-pending" | "role-upgrade-pending" | "metadata-upgrade-pending";
requestId: string | null;
};
export function resolvePairingHint(
connected: boolean,
lastError: string | null,
lastErrorCode?: string | null,
): PairingHint | null {
if (connected || !lastError) {
return null;
}
const pairing = readConnectPairingRequiredMessage(lastError);
if (pairing) {
return {
kind:
pairing.reason === "scope-upgrade"
? "scope-upgrade-pending"
: pairing.reason === "role-upgrade"
? "role-upgrade-pending"
: pairing.reason === "metadata-upgrade"
? "metadata-upgrade-pending"
: "pairing-required",
requestId: pairing.requestId ?? null,
};
}
if (lastErrorCode === ConnectErrorDetailCodes.PAIRING_REQUIRED) {
return { kind: "pairing-required", requestId: null };
}
return null;
}
/**
* Return the connection auth hint to show, if any.
*
* Keep fallback string matching narrow so generic "connect failed" close reasons
* do not get misclassified as token/password problems.
*/
export function resolveAuthHintKind(params: {
connected: boolean;
lastError: string | null;
lastErrorCode?: string | null;
hasToken: boolean;
hasPassword: boolean;
}): AuthHintKind | null {
if (params.connected || !params.lastError) {
return null;
}
if (params.lastErrorCode) {
if (!AUTH_FAILURE_CODES.has(params.lastErrorCode)) {
return null;
}
return AUTH_REQUIRED_CODES.has(params.lastErrorCode) ? "required" : "failed";
}
const lower = normalizeLowercaseStringOrEmpty(params.lastError);
if (!lower.includes("unauthorized")) {
return null;
}
return !params.hasToken && !params.hasPassword ? "required" : "failed";
}
export function shouldShowInsecureContextHint(
connected: boolean,
lastError: string | null,
lastErrorCode?: string | null,
): boolean {
if (connected || !lastError) {
return false;
}
if (lastErrorCode) {
return INSECURE_CONTEXT_CODES.has(lastErrorCode);
}
const lower = normalizeLowercaseStringOrEmpty(lastError);
return lower.includes("secure context") || lower.includes("device identity required");
}