mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:10:43 +00:00
fix(control-ui): dismiss talk startup errors
This commit is contained in:
@@ -59,6 +59,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Control UI/Talk: make failed Talk startup errors dismissable and clear the stale Talk error state when dismissed, so missing realtime voice provider configuration does not leave a permanent chat banner. Fixes #77071. Thanks @ijoshdavis.
|
||||
- Web fetch: late-bind `web_fetch` config and provider fallback metadata from the active runtime snapshot, matching `web_search` so long-lived tools do not use stale fetch provider settings. Thanks @vincentkoc.
|
||||
- Discord: clear stale startup probe bot/application status when the async bot probe throws, not just when it returns a degraded probe result. Thanks @vincentkoc.
|
||||
- Web search: scope explicit bundled `web_search` provider runtime loading through manifest ownership, so selecting DuckDuckGo/Gemini/etc. does not import unrelated bundled providers or log their optional dependency failures. Thanks @vincentkoc.
|
||||
|
||||
@@ -1543,6 +1543,40 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.callout--dismissible {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.callout__content {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.callout__dismiss {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: currentColor;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.callout__dismiss:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.callout__dismiss svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.callout.danger {
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(239, 68, 68, 0.04) 100%);
|
||||
|
||||
@@ -36,6 +36,7 @@ vi.mock("./controllers/sessions.ts", () => ({
|
||||
|
||||
import {
|
||||
createChatSession,
|
||||
dismissChatError,
|
||||
isCronSessionKey,
|
||||
parseSessionKey,
|
||||
resolveAssistantAttachmentAuthToken,
|
||||
@@ -771,6 +772,7 @@ describe("switchChatSession", () => {
|
||||
chatRunId: "run-1",
|
||||
chatSideResultTerminalRuns: new Set(["btw-run-1"]),
|
||||
chatStreamStartedAt: 1,
|
||||
sessionsShowArchived: false,
|
||||
settings,
|
||||
applySettings(next: typeof settings) {
|
||||
state.settings = next;
|
||||
@@ -904,3 +906,25 @@ describe("switchChatSession", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("dismissChatError", () => {
|
||||
it("clears persistent Talk error state", () => {
|
||||
const state = {
|
||||
lastError: 'Realtime voice provider "openai" is not configured',
|
||||
lastErrorCode: "UNAVAILABLE",
|
||||
realtimeTalkActive: false,
|
||||
realtimeTalkStatus: "error",
|
||||
realtimeTalkDetail: 'Realtime voice provider "openai" is not configured',
|
||||
realtimeTalkTranscript: "partial transcript",
|
||||
} as AppViewState;
|
||||
|
||||
dismissChatError(state);
|
||||
|
||||
expect(state.lastError).toBeNull();
|
||||
expect(state.lastErrorCode).toBeNull();
|
||||
expect(state.realtimeTalkActive).toBe(false);
|
||||
expect(state.realtimeTalkStatus).toBe("idle");
|
||||
expect(state.realtimeTalkDetail).toBeNull();
|
||||
expect(state.realtimeTalkTranscript).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -619,6 +619,17 @@ export function switchChatSession(state: AppViewState, nextSessionKey: string) {
|
||||
void refreshSessionOptions(state);
|
||||
}
|
||||
|
||||
export function dismissChatError(state: AppViewState) {
|
||||
state.lastError = null;
|
||||
state.lastErrorCode = null;
|
||||
if (state.realtimeTalkStatus === "error") {
|
||||
state.realtimeTalkActive = false;
|
||||
state.realtimeTalkStatus = "idle";
|
||||
state.realtimeTalkDetail = null;
|
||||
state.realtimeTalkTranscript = null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createChatSession(state: AppViewState) {
|
||||
if (!state.client || !state.connected) {
|
||||
return;
|
||||
@@ -650,7 +661,7 @@ export async function createChatSession(state: AppViewState) {
|
||||
limit: 0,
|
||||
includeGlobal: true,
|
||||
includeUnknown: true,
|
||||
showArchived: Boolean(state.sessionsShowArchived),
|
||||
showArchived: state.sessionsShowArchived,
|
||||
},
|
||||
);
|
||||
if (
|
||||
@@ -681,7 +692,7 @@ async function refreshSessionOptions(state: AppViewState) {
|
||||
limit: 0,
|
||||
includeGlobal: true,
|
||||
includeUnknown: true,
|
||||
showArchived: Boolean(state.sessionsShowArchived),
|
||||
showArchived: state.sessionsShowArchived,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
renderSidebarConnectionStatus,
|
||||
renderTopbarThemeModeToggle,
|
||||
createChatSession,
|
||||
dismissChatError,
|
||||
switchChatSession,
|
||||
} from "./app-render.helpers.ts";
|
||||
import { warnQueryToken } from "./app-settings.ts";
|
||||
@@ -2364,6 +2365,7 @@ export function renderApp(state: AppViewState) {
|
||||
canSend: state.connected,
|
||||
disabledReason: chatDisabledReason,
|
||||
error: state.lastError,
|
||||
onDismissError: () => dismissChatError(state),
|
||||
sessions: state.sessionsResult,
|
||||
focusMode: chatFocus,
|
||||
autoExpandToolCalls: false,
|
||||
|
||||
@@ -77,7 +77,7 @@ async function refreshSessionOptions(state: AppViewState) {
|
||||
limit: 0,
|
||||
includeGlobal: true,
|
||||
includeUnknown: true,
|
||||
showArchived: Boolean(state.sessionsShowArchived),
|
||||
showArchived: state.sessionsShowArchived,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -389,6 +389,7 @@ function renderChatView(overrides: Partial<Parameters<typeof renderChat>[0]> = {
|
||||
onSend: () => undefined,
|
||||
onCompact: () => undefined,
|
||||
onToggleRealtimeTalk: () => undefined,
|
||||
onDismissError: () => undefined,
|
||||
onAbort: () => undefined,
|
||||
onQueueRemove: () => undefined,
|
||||
onQueueSteer: () => undefined,
|
||||
@@ -494,6 +495,24 @@ describe("chat voice controls", () => {
|
||||
expect(container.querySelector('[aria-label="Start Talk"]')).not.toBeNull();
|
||||
expect(container.querySelector('[aria-label="Voice input"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("lets users dismiss Talk start errors", () => {
|
||||
const onDismissError = vi.fn();
|
||||
const container = renderChatView({
|
||||
error: 'Realtime voice provider "openai" is not configured',
|
||||
realtimeTalkStatus: "error",
|
||||
realtimeTalkDetail: 'Realtime voice provider "openai" is not configured',
|
||||
onDismissError,
|
||||
});
|
||||
|
||||
expect(container.querySelector('[role="alert"]')?.textContent).toContain(
|
||||
'Realtime voice provider "openai" is not configured',
|
||||
);
|
||||
|
||||
container.querySelector<HTMLButtonElement>('[aria-label="Dismiss error"]')?.click();
|
||||
|
||||
expect(onDismissError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chat slash menu accessibility", () => {
|
||||
|
||||
@@ -111,6 +111,7 @@ export type ChatProps = {
|
||||
onCompact?: () => void | Promise<void>;
|
||||
onOpenSessionCheckpoints?: () => void | Promise<void>;
|
||||
onToggleRealtimeTalk?: () => void;
|
||||
onDismissError?: () => void;
|
||||
onAbort?: () => void;
|
||||
onQueueRemove: (id: string) => void;
|
||||
onQueueSteer?: (id: string) => void;
|
||||
@@ -1136,7 +1137,26 @@ export function renderChat(props: ChatProps) {
|
||||
@dragover=${(e: DragEvent) => e.preventDefault()}
|
||||
>
|
||||
${props.disabledReason ? html`<div class="callout">${props.disabledReason}</div>` : nothing}
|
||||
${props.error ? html`<div class="callout danger">${props.error}</div>` : nothing}
|
||||
${props.error
|
||||
? html`
|
||||
<div class="callout danger callout--dismissible" role="alert">
|
||||
<span class="callout__content">${props.error}</span>
|
||||
${props.onDismissError
|
||||
? html`
|
||||
<button
|
||||
class="callout__dismiss"
|
||||
type="button"
|
||||
@click=${props.onDismissError}
|
||||
aria-label="Dismiss error"
|
||||
title="Dismiss error"
|
||||
>
|
||||
${icons.x}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${props.focusMode
|
||||
? html`
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user