feat: enhance chat history and error handling in UI
- Added a test to ensure chat.history preserves usage and cost metadata for assistant messages. - Updated chat message sanitization to retain usage and cost information for UI rendering. - Enhanced the AppViewState and UI components to include lastErrorCode for improved error handling. - Implemented new utility functions in overview hints to manage authentication and context errors. - Updated tests to cover new functionality and ensure correct behavior in various scenarios.
This commit is contained in:
parent
cfec9a268a
commit
1e440712fb
@ -180,6 +180,8 @@ function sanitizeChatHistoryMessage(message: unknown): { message: unknown; chang
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Keep usage/cost so the chat UI can render per-message token and cost badges.
|
||||
|
||||
if (typeof entry.content === "string") {
|
||||
const stripped = stripInlineDirectiveTagsForDisplay(entry.content);
|
||||
const res = truncateChatHistoryText(stripped.text);
|
||||
|
||||
@ -273,6 +273,37 @@ describe("gateway server chat", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("chat.history preserves usage and cost metadata for assistant messages", async () => {
|
||||
await withGatewayChatHarness(async ({ ws, createSessionDir }) => {
|
||||
await connectOk(ws);
|
||||
|
||||
const sessionDir = await createSessionDir();
|
||||
await writeMainSessionStore();
|
||||
|
||||
await writeMainSessionTranscript(sessionDir, [
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "assistant",
|
||||
timestamp: Date.now(),
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
usage: { input: 12, output: 5, totalTokens: 17 },
|
||||
cost: { total: 0.0123 },
|
||||
details: { debug: true },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const messages = await fetchHistoryMessages(ws);
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0]).toMatchObject({
|
||||
role: "assistant",
|
||||
usage: { input: 12, output: 5, totalTokens: 17 },
|
||||
cost: { total: 0.0123 },
|
||||
});
|
||||
expect(messages[0]).not.toHaveProperty("details");
|
||||
});
|
||||
});
|
||||
|
||||
test("chat.history strips inline directives from displayed message text", async () => {
|
||||
await withGatewayChatHarness(async ({ ws, createSessionDir }) => {
|
||||
await connectOk(ws);
|
||||
|
||||
@ -28,7 +28,11 @@ import {
|
||||
import { loadHealthState } from "./controllers/health.ts";
|
||||
import { loadNodes } from "./controllers/nodes.ts";
|
||||
import { loadSessions } from "./controllers/sessions.ts";
|
||||
import type { GatewayEventFrame, GatewayHelloOk } from "./gateway.ts";
|
||||
import {
|
||||
resolveGatewayErrorDetailCode,
|
||||
type GatewayEventFrame,
|
||||
type GatewayHelloOk,
|
||||
} from "./gateway.ts";
|
||||
import { GatewayBrowserClient } from "./gateway.ts";
|
||||
import type { Tab } from "./navigation.ts";
|
||||
import type { UiSettings } from "./storage.ts";
|
||||
@ -43,10 +47,12 @@ import type {
|
||||
type GatewayHost = {
|
||||
settings: UiSettings;
|
||||
password: string;
|
||||
clientInstanceId: string;
|
||||
client: GatewayBrowserClient | null;
|
||||
connected: boolean;
|
||||
hello: GatewayHelloOk | null;
|
||||
lastError: string | null;
|
||||
lastErrorCode: string | null;
|
||||
onboarding?: boolean;
|
||||
eventLogBuffer: EventLogEntry[];
|
||||
eventLog: EventLogEntry[];
|
||||
@ -161,6 +167,7 @@ function applySessionDefaults(host: GatewayHost, defaults?: SessionDefaultsSnaps
|
||||
|
||||
export function connectGateway(host: GatewayHost) {
|
||||
host.lastError = null;
|
||||
host.lastErrorCode = null;
|
||||
host.hello = null;
|
||||
host.connected = false;
|
||||
host.execApprovalQueue = [];
|
||||
@ -178,12 +185,14 @@ export function connectGateway(host: GatewayHost) {
|
||||
clientName: "openclaw-control-ui",
|
||||
clientVersion,
|
||||
mode: "webchat",
|
||||
instanceId: host.clientInstanceId,
|
||||
onHello: (hello) => {
|
||||
if (host.client !== client) {
|
||||
return;
|
||||
}
|
||||
host.connected = true;
|
||||
host.lastError = null;
|
||||
host.lastErrorCode = null;
|
||||
host.hello = hello;
|
||||
applySnapshot(host, hello);
|
||||
// Reset orphaned chat run state from before disconnect.
|
||||
@ -199,14 +208,24 @@ export function connectGateway(host: GatewayHost) {
|
||||
void loadDevices(host as unknown as OpenClawApp, { quiet: true });
|
||||
void refreshActiveTab(host as unknown as Parameters<typeof refreshActiveTab>[0]);
|
||||
},
|
||||
onClose: ({ code, reason }) => {
|
||||
onClose: ({ code, reason, error }) => {
|
||||
if (host.client !== client) {
|
||||
return;
|
||||
}
|
||||
host.connected = false;
|
||||
// Code 1012 = Service Restart (expected during config saves, don't show as error)
|
||||
host.lastErrorCode =
|
||||
resolveGatewayErrorDetailCode(error) ??
|
||||
(typeof error?.code === "string" ? error.code : null);
|
||||
if (code !== 1012) {
|
||||
if (error?.message) {
|
||||
host.lastError = error.message;
|
||||
return;
|
||||
}
|
||||
host.lastError = `disconnected (${code}): ${reason || "no reason"}`;
|
||||
} else {
|
||||
host.lastError = null;
|
||||
host.lastErrorCode = null;
|
||||
}
|
||||
},
|
||||
onEvent: (evt) => {
|
||||
@ -220,6 +239,7 @@ export function connectGateway(host: GatewayHost) {
|
||||
return;
|
||||
}
|
||||
host.lastError = `event gap detected (expected seq ${expected}, got ${received}); refresh recommended`;
|
||||
host.lastErrorCode = null;
|
||||
},
|
||||
});
|
||||
host.client = client;
|
||||
|
||||
@ -1,9 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.hoisted(() => {
|
||||
const storage = new Map<string, string>();
|
||||
Object.defineProperty(globalThis, "localStorage", {
|
||||
configurable: true,
|
||||
value: {
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => storage.set(key, value),
|
||||
removeItem: (key: string) => storage.delete(key),
|
||||
clear: () => storage.clear(),
|
||||
},
|
||||
});
|
||||
Object.defineProperty(globalThis, "navigator", {
|
||||
configurable: true,
|
||||
value: { language: "en-US" },
|
||||
});
|
||||
return {};
|
||||
});
|
||||
import {
|
||||
isCronSessionKey,
|
||||
parseSessionKey,
|
||||
resolveSessionOptionGroups,
|
||||
resolveSessionDisplayName,
|
||||
} from "./app-render.helpers.ts";
|
||||
import type { AppViewState } from "./app-view-state.ts";
|
||||
import type { SessionsListResult } from "./types.ts";
|
||||
|
||||
type SessionRow = SessionsListResult["sessions"][number];
|
||||
@ -12,6 +32,14 @@ function row(overrides: Partial<SessionRow> & { key: string }): SessionRow {
|
||||
return { kind: "direct", updatedAt: 0, ...overrides };
|
||||
}
|
||||
|
||||
function testState(overrides: Partial<AppViewState> = {}): AppViewState {
|
||||
return {
|
||||
agentsList: null,
|
||||
sessionsHideCron: true,
|
||||
...overrides,
|
||||
} as AppViewState;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* parseSessionKey – low-level key → type / fallback mapping
|
||||
* ================================================================ */
|
||||
@ -284,3 +312,40 @@ describe("isCronSessionKey", () => {
|
||||
expect(isCronSessionKey("agent:main:slack:cron:job:run:uuid")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSessionOptionGroups", () => {
|
||||
const sessions: SessionsListResult = {
|
||||
sessions: [
|
||||
row({ key: "agent:main:main" }),
|
||||
row({ key: "agent:main:cron:daily" }),
|
||||
row({ key: "agent:main:discord:direct:user-1" }),
|
||||
],
|
||||
};
|
||||
|
||||
it("filters cron sessions from options when the hide toggle is enabled", () => {
|
||||
const groups = resolveSessionOptionGroups(
|
||||
testState({ sessionsHideCron: true }),
|
||||
"agent:main:main",
|
||||
sessions,
|
||||
);
|
||||
|
||||
expect(groups.flatMap((group) => group.options.map((option) => option.key))).toEqual([
|
||||
"agent:main:main",
|
||||
"agent:main:discord:direct:user-1",
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains the active cron session even when cron sessions are hidden", () => {
|
||||
const groups = resolveSessionOptionGroups(
|
||||
testState({ sessionsHideCron: true }),
|
||||
"agent:main:cron:daily",
|
||||
sessions,
|
||||
);
|
||||
|
||||
expect(groups.flatMap((group) => group.options.map((option) => option.key))).toEqual([
|
||||
"agent:main:main",
|
||||
"agent:main:cron:daily",
|
||||
"agent:main:discord:direct:user-1",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -452,12 +452,13 @@ type SessionOptionGroup = {
|
||||
options: SessionOptionEntry[];
|
||||
};
|
||||
|
||||
function resolveSessionOptionGroups(
|
||||
export function resolveSessionOptionGroups(
|
||||
state: AppViewState,
|
||||
sessionKey: string,
|
||||
sessions: SessionsListResult | null,
|
||||
): SessionOptionGroup[] {
|
||||
const rows = sessions?.sessions ?? [];
|
||||
const hideCron = state.sessionsHideCron ?? true;
|
||||
const byKey = new Map<string, SessionsListResult["sessions"][number]>();
|
||||
for (const row of rows) {
|
||||
byKey.set(row.key, row);
|
||||
@ -501,6 +502,9 @@ function resolveSessionOptionGroups(
|
||||
};
|
||||
|
||||
for (const row of rows) {
|
||||
if (hideCron && row.key !== sessionKey && isCronSessionKey(row.key)) {
|
||||
continue;
|
||||
}
|
||||
addOption(row.key);
|
||||
}
|
||||
addOption(sessionKey);
|
||||
|
||||
@ -565,6 +565,7 @@ export function renderApp(state: AppViewState) {
|
||||
settings: state.settings,
|
||||
password: state.password,
|
||||
lastError: state.lastError,
|
||||
lastErrorCode: state.lastErrorCode,
|
||||
presenceCount,
|
||||
sessionsCount,
|
||||
cronEnabled: state.cronStatus?.enabled ?? null,
|
||||
|
||||
@ -51,6 +51,7 @@ export type AppViewState = {
|
||||
themeOrder: ThemeName[];
|
||||
hello: GatewayHelloOk | null;
|
||||
lastError: string | null;
|
||||
lastErrorCode: string | null;
|
||||
eventLog: EventLogEntry[];
|
||||
assistantName: string;
|
||||
assistantAvatar: string | null;
|
||||
|
||||
@ -153,6 +153,7 @@ export class OpenClawApp extends LitElement {
|
||||
@state() themeOrder: ThemeName[] = this.buildThemeOrder(this.theme);
|
||||
@state() hello: GatewayHelloOk | null = null;
|
||||
@state() lastError: string | null = null;
|
||||
@state() lastErrorCode: string | null = null;
|
||||
@state() eventLog: EventLogEntry[] = [];
|
||||
private eventLogBuffer: EventLogEntry[] = [];
|
||||
private toolStreamSyncTimer: number | null = null;
|
||||
|
||||
@ -413,7 +413,7 @@ export type {
|
||||
export type CronSchedule =
|
||||
| { kind: "at"; at: string }
|
||||
| { kind: "every"; everyMs: number; anchorMs?: number }
|
||||
| { kind: "cron"; expr: string; tz?: string };
|
||||
| { kind: "cron"; expr: string; tz?: string; staggerMs?: number };
|
||||
|
||||
export type CronSessionTarget = "main" | "isolated";
|
||||
export type CronWakeMode = "next-heartbeat" | "now";
|
||||
@ -423,6 +423,7 @@ export type CronPayload =
|
||||
| {
|
||||
kind: "agentTurn";
|
||||
message: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
timeoutSeconds?: number;
|
||||
lightContext?: boolean;
|
||||
|
||||
@ -1,5 +1,31 @@
|
||||
import { ConnectErrorDetailCodes } from "../../../../src/gateway/protocol/connect-error-details.js";
|
||||
|
||||
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 INSECURE_CONTEXT_CODES = new Set<string>([
|
||||
ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
|
||||
ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED,
|
||||
]);
|
||||
|
||||
/** Whether the overview should show device-pairing guidance for this error. */
|
||||
export function shouldShowPairingHint(
|
||||
connected: boolean,
|
||||
@ -14,3 +40,44 @@ export function shouldShowPairingHint(
|
||||
}
|
||||
return lastError.toLowerCase().includes("pairing required");
|
||||
}
|
||||
|
||||
export function shouldShowAuthHint(
|
||||
connected: boolean,
|
||||
lastError: string | null,
|
||||
lastErrorCode?: string | null,
|
||||
): boolean {
|
||||
if (connected || !lastError) {
|
||||
return false;
|
||||
}
|
||||
if (lastErrorCode) {
|
||||
return AUTH_FAILURE_CODES.has(lastErrorCode);
|
||||
}
|
||||
const lower = lastError.toLowerCase();
|
||||
return lower.includes("unauthorized") || lower.includes("connect failed");
|
||||
}
|
||||
|
||||
export function shouldShowAuthRequiredHint(
|
||||
hasToken: boolean,
|
||||
hasPassword: boolean,
|
||||
lastErrorCode?: string | null,
|
||||
): boolean {
|
||||
if (lastErrorCode) {
|
||||
return AUTH_REQUIRED_CODES.has(lastErrorCode);
|
||||
}
|
||||
return !hasToken && !hasPassword;
|
||||
}
|
||||
|
||||
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 = lastError.toLowerCase();
|
||||
return lower.includes("secure context") || lower.includes("device identity required");
|
||||
}
|
||||
|
||||
@ -1,39 +1,95 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ConnectErrorDetailCodes } from "../../../../src/gateway/protocol/connect-error-details.js";
|
||||
import { shouldShowPairingHint } from "./overview-hints.ts";
|
||||
import {
|
||||
shouldShowAuthHint,
|
||||
shouldShowAuthRequiredHint,
|
||||
shouldShowInsecureContextHint,
|
||||
shouldShowPairingHint,
|
||||
} from "./overview-hints.ts";
|
||||
|
||||
describe("shouldShowPairingHint", () => {
|
||||
it("returns true for 'pairing required' close reason", () => {
|
||||
expect(shouldShowPairingHint(false, "disconnected (1008): pairing required")).toBe(true);
|
||||
describe("overview hints", () => {
|
||||
describe("shouldShowPairingHint", () => {
|
||||
it("returns true for 'pairing required' close reason", () => {
|
||||
expect(shouldShowPairingHint(false, "disconnected (1008): pairing required")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches case-insensitively", () => {
|
||||
expect(shouldShowPairingHint(false, "Pairing Required")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when connected", () => {
|
||||
expect(shouldShowPairingHint(true, "disconnected (1008): pairing required")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when lastError is null", () => {
|
||||
expect(shouldShowPairingHint(false, null)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for unrelated errors", () => {
|
||||
expect(shouldShowPairingHint(false, "disconnected (1006): no reason")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for auth errors", () => {
|
||||
expect(shouldShowPairingHint(false, "disconnected (4008): unauthorized")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for structured pairing code", () => {
|
||||
expect(
|
||||
shouldShowPairingHint(
|
||||
false,
|
||||
"disconnected (4008): connect failed",
|
||||
ConnectErrorDetailCodes.PAIRING_REQUIRED,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("matches case-insensitively", () => {
|
||||
expect(shouldShowPairingHint(false, "Pairing Required")).toBe(true);
|
||||
describe("shouldShowAuthHint", () => {
|
||||
it("returns true for structured auth failures", () => {
|
||||
expect(
|
||||
shouldShowAuthHint(
|
||||
false,
|
||||
"disconnected (4008): connect failed",
|
||||
ConnectErrorDetailCodes.AUTH_TAILSCALE_IDENTITY_MISMATCH,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to legacy close text when no detail code is present", () => {
|
||||
expect(shouldShowAuthHint(false, "disconnected (4008): unauthorized")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for non-auth errors", () => {
|
||||
expect(shouldShowAuthHint(false, "disconnected (1006): no reason")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns false when connected", () => {
|
||||
expect(shouldShowPairingHint(true, "disconnected (1008): pairing required")).toBe(false);
|
||||
describe("shouldShowAuthRequiredHint", () => {
|
||||
it("returns true for structured auth-required codes", () => {
|
||||
expect(
|
||||
shouldShowAuthRequiredHint(true, true, ConnectErrorDetailCodes.AUTH_TOKEN_MISSING),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to missing credentials when detail code is absent", () => {
|
||||
expect(shouldShowAuthRequiredHint(false, false, null)).toBe(true);
|
||||
expect(shouldShowAuthRequiredHint(true, false, null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns false when lastError is null", () => {
|
||||
expect(shouldShowPairingHint(false, null)).toBe(false);
|
||||
});
|
||||
describe("shouldShowInsecureContextHint", () => {
|
||||
it("returns true for structured device identity errors", () => {
|
||||
expect(
|
||||
shouldShowInsecureContextHint(
|
||||
false,
|
||||
"disconnected (4008): connect failed",
|
||||
ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for unrelated errors", () => {
|
||||
expect(shouldShowPairingHint(false, "disconnected (1006): no reason")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for auth errors", () => {
|
||||
expect(shouldShowPairingHint(false, "disconnected (4008): unauthorized")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for structured pairing code", () => {
|
||||
expect(
|
||||
shouldShowPairingHint(
|
||||
false,
|
||||
"disconnected (4008): connect failed",
|
||||
ConnectErrorDetailCodes.PAIRING_REQUIRED,
|
||||
),
|
||||
).toBe(true);
|
||||
it("falls back to legacy close text when detail code is absent", () => {
|
||||
expect(shouldShowInsecureContextHint(false, "device identity required")).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { ConnectErrorDetailCodes } from "../../../../src/gateway/protocol/connect-error-details.js";
|
||||
import { t, i18n, SUPPORTED_LOCALES, type Locale } from "../../i18n/index.ts";
|
||||
import type { EventLogEntry } from "../app-events.ts";
|
||||
import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "../external-link.ts";
|
||||
@ -18,7 +17,12 @@ import type {
|
||||
import { renderOverviewAttention } from "./overview-attention.ts";
|
||||
import { renderOverviewCards } from "./overview-cards.ts";
|
||||
import { renderOverviewEventLog } from "./overview-event-log.ts";
|
||||
import { shouldShowPairingHint } from "./overview-hints.ts";
|
||||
import {
|
||||
shouldShowAuthHint,
|
||||
shouldShowAuthRequiredHint,
|
||||
shouldShowInsecureContextHint,
|
||||
shouldShowPairingHint,
|
||||
} from "./overview-hints.ts";
|
||||
import { renderOverviewLogTail } from "./overview-log-tail.ts";
|
||||
|
||||
export type OverviewProps = {
|
||||
@ -27,6 +31,7 @@ export type OverviewProps = {
|
||||
settings: UiSettings;
|
||||
password: string;
|
||||
lastError: string | null;
|
||||
lastErrorCode: string | null;
|
||||
presenceCount: number;
|
||||
sessionsCount: number | null;
|
||||
cronEnabled: boolean | null;
|
||||
@ -72,7 +77,7 @@ export function renderOverview(props: OverviewProps) {
|
||||
const isTrustedProxy = authMode === "trusted-proxy";
|
||||
|
||||
const pairingHint = (() => {
|
||||
if (!shouldShowPairingHint(props.connected, props.lastError)) {
|
||||
if (!shouldShowPairingHint(props.connected, props.lastError, props.lastErrorCode)) {
|
||||
return null;
|
||||
}
|
||||
return html`
|
||||
@ -103,14 +108,12 @@ export function renderOverview(props: OverviewProps) {
|
||||
if (props.connected || !props.lastError) {
|
||||
return null;
|
||||
}
|
||||
const lower = props.lastError.toLowerCase();
|
||||
const authFailed = lower.includes("unauthorized") || lower.includes("connect failed");
|
||||
if (!authFailed) {
|
||||
if (!shouldShowAuthHint(props.connected, props.lastError, props.lastErrorCode)) {
|
||||
return null;
|
||||
}
|
||||
const hasToken = Boolean(props.settings.token.trim());
|
||||
const hasPassword = Boolean(props.password.trim());
|
||||
if (!hasToken && !hasPassword) {
|
||||
if (shouldShowAuthRequiredHint(hasToken, hasPassword, props.lastErrorCode)) {
|
||||
return html`
|
||||
<div class="muted" style="margin-top: 8px">
|
||||
${t("overview.auth.required")}
|
||||
@ -156,8 +159,7 @@ export function renderOverview(props: OverviewProps) {
|
||||
if (isSecureContext) {
|
||||
return null;
|
||||
}
|
||||
const lower = props.lastError.toLowerCase();
|
||||
if (!lower.includes("secure context") && !lower.includes("device identity required")) {
|
||||
if (!shouldShowInsecureContextHint(props.connected, props.lastError, props.lastErrorCode)) {
|
||||
return null;
|
||||
}
|
||||
return html`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user