mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(ui): surface command-palette chat search failures (#124648)
The palette's session search swallowed every gateway error and rendered the plain "No results" empty state — a failed search (e.g. sessions.list rejecting with a store-needs-doctor migration error) was indistinguishable from a successful search with zero matches. Silent failure on a default path is the worst bug class in this repo. Track a sessionSearchFailed flag (set only for the current request id while the palette is open, cleared on every new query/clear), and render "Chat search failed — check the gateway logs and retry" in the empty state instead. Navigation commands stay usable throughout.
This commit is contained in:
committed by
GitHub
parent
9873b0f6ad
commit
b90d7efddb
@@ -204,6 +204,32 @@ describe("CommandPalette lifecycle", () => {
|
||||
expect(palette.textContent).not.toContain("Stale chat");
|
||||
});
|
||||
|
||||
it("shows a search failure instead of a false empty result", async () => {
|
||||
const { gateway } = createGateway(true);
|
||||
const list = vi
|
||||
.fn<ApplicationContext<RouteId>["sessions"]["list"]>()
|
||||
.mockRejectedValueOnce(new Error("store needs doctor migration"))
|
||||
.mockResolvedValueOnce(createSessionResult("agent:main:zz", "Recovered chat"));
|
||||
const { palette } = await mountPalette(createContext(gateway, list));
|
||||
// The query matches no navigation item, so a swallowed search failure
|
||||
// would render the plain "No results" empty state.
|
||||
await enterQuery(palette, "zzz-unmatched");
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
await palette.updateComplete;
|
||||
|
||||
expect(list).toHaveBeenCalledOnce();
|
||||
expect(palette.textContent).toContain("Chat search failed");
|
||||
expect(palette.textContent).not.toContain("No results");
|
||||
|
||||
// A new keystroke clears the failure state and retries cleanly.
|
||||
await enterQuery(palette, "zz");
|
||||
await palette.updateComplete;
|
||||
expect(palette.textContent).not.toContain("Chat search failed");
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
await palette.updateComplete;
|
||||
expect(palette.textContent).toContain("Recovered chat");
|
||||
});
|
||||
|
||||
it("navigates to the plugin manager from search", async () => {
|
||||
const { gateway } = createGateway(true);
|
||||
const { palette } = await mountPalette(
|
||||
|
||||
@@ -124,6 +124,7 @@ type CommandPaletteProps = {
|
||||
query: string;
|
||||
activeIndex: number;
|
||||
sessionItems: readonly PaletteItem[];
|
||||
sessionSearchFailed: boolean;
|
||||
onToggle: () => void;
|
||||
onQueryChange: (query: string) => void;
|
||||
onActiveIndexChange: (index: number) => void;
|
||||
@@ -315,7 +316,11 @@ function renderCommandPalette(props: CommandPaletteProps) {
|
||||
<span class="nav-item__icon" style="opacity:0.3;width:20px;height:20px"
|
||||
>${icons.search}</span
|
||||
>
|
||||
<span>${t("palette.noResults")}</span>
|
||||
<span
|
||||
>${props.sessionSearchFailed
|
||||
? t("palette.searchFailed")
|
||||
: t("palette.noResults")}</span
|
||||
>
|
||||
</div>`
|
||||
: grouped.map(
|
||||
([category, groupedItems]) => html`
|
||||
@@ -369,6 +374,7 @@ export class CommandPalette extends OpenClawLightDomContentsElement {
|
||||
@state() private query = "";
|
||||
@state() private activeIndex = 0;
|
||||
@state() private sessionItems: readonly PaletteItem[] = [];
|
||||
@state() private sessionSearchFailed = false;
|
||||
|
||||
private readonly subscriptions = new SubscriptionsController(this);
|
||||
private sessionSearchTimer: ReturnType<typeof globalThis.setTimeout> | null = null;
|
||||
@@ -458,6 +464,7 @@ export class CommandPalette extends OpenClawLightDomContentsElement {
|
||||
}
|
||||
this.sessionSearchId += 1;
|
||||
this.sessionItems = [];
|
||||
this.sessionSearchFailed = false;
|
||||
}
|
||||
|
||||
private scheduleSessionSearch(query: string) {
|
||||
@@ -469,6 +476,7 @@ export class CommandPalette extends OpenClawLightDomContentsElement {
|
||||
// repopulate selectable stale rows during the debounce window.
|
||||
this.sessionSearchId += 1;
|
||||
this.sessionItems = [];
|
||||
this.sessionSearchFailed = false;
|
||||
const search = normalizeOptionalString(query);
|
||||
if (!this.open || !search || !this.onSelectSession) {
|
||||
return;
|
||||
@@ -551,7 +559,13 @@ export class CommandPalette extends OpenClawLightDomContentsElement {
|
||||
}));
|
||||
this.activeIndex = 0;
|
||||
} catch {
|
||||
// Session search is best-effort; navigation commands stay usable.
|
||||
// Session search is best-effort; navigation commands stay usable. But a
|
||||
// failed search must not render as "No results" — that reads as a
|
||||
// successful search with zero matches and hides gateway-side failures
|
||||
// (e.g. a store needing doctor migration) from the operator.
|
||||
if (requestId === this.sessionSearchId && this.open) {
|
||||
this.sessionSearchFailed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,6 +587,7 @@ export class CommandPalette extends OpenClawLightDomContentsElement {
|
||||
query: this.query,
|
||||
activeIndex: this.activeIndex,
|
||||
sessionItems: this.sessionItems,
|
||||
sessionSearchFailed: this.sessionSearchFailed,
|
||||
desktopAvailable: this.desktopAvailable,
|
||||
onToggle: this.togglePalette,
|
||||
onQueryChange: (query) => {
|
||||
|
||||
@@ -3857,6 +3857,7 @@ export const en: TranslationMap = {
|
||||
palette: {
|
||||
placeholder: "Search chats and commands…",
|
||||
noResults: "No results",
|
||||
searchFailed: "Chat search failed — check the gateway logs and retry",
|
||||
categories: {
|
||||
search: "Search",
|
||||
navigation: "Navigation",
|
||||
|
||||
Reference in New Issue
Block a user