mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-13 14:23:58 +00:00
fix(cli): refresh installed apps after settings changes
This commit is contained in:
parent
7a6cc657db
commit
9efdce276f
@ -69,6 +69,7 @@ import {
|
|||||||
updateSettings,
|
updateSettings,
|
||||||
updateWebSearchSettings,
|
updateWebSearchSettings,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
|
import { notifyCliAppsChanged } from "@/lib/cli-app-events";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useClient } from "@/providers/ClientProvider";
|
import { useClient } from "@/providers/ClientProvider";
|
||||||
import type {
|
import type {
|
||||||
@ -626,6 +627,9 @@ export function SettingsView({
|
|||||||
try {
|
try {
|
||||||
const payload = await runCliAppAction(token, action, name);
|
const payload = await runCliAppAction(token, action, name);
|
||||||
setCliApps(payload);
|
setCliApps(payload);
|
||||||
|
if (action !== "test") {
|
||||||
|
notifyCliAppsChanged(payload);
|
||||||
|
}
|
||||||
setCliAppsMessage(payload.last_action?.message ?? null);
|
setCliAppsMessage(payload.last_action?.message ?? null);
|
||||||
setCliAppsFocusName(action === "uninstall" ? null : name);
|
setCliAppsFocusName(action === "uninstall" ? null : name);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -20,6 +20,11 @@ import { ThreadViewport } from "@/components/thread/ThreadViewport";
|
|||||||
import { useNanobotStream, type SendImage, type SendOptions } from "@/hooks/useNanobotStream";
|
import { useNanobotStream, type SendImage, type SendOptions } from "@/hooks/useNanobotStream";
|
||||||
import { useSessionHistory } from "@/hooks/useSessions";
|
import { useSessionHistory } from "@/hooks/useSessions";
|
||||||
import { fetchCliApps, listSlashCommands } from "@/lib/api";
|
import { fetchCliApps, listSlashCommands } from "@/lib/api";
|
||||||
|
import {
|
||||||
|
CLI_APPS_CHANGED_EVENT,
|
||||||
|
installedCliAppsFromPayload,
|
||||||
|
isCliAppsPayload,
|
||||||
|
} from "@/lib/cli-app-events";
|
||||||
import type { ChatSummary, CliAppInfo, SlashCommand, UIMessage } from "@/lib/types";
|
import type { ChatSummary, CliAppInfo, SlashCommand, UIMessage } from "@/lib/types";
|
||||||
import { normalizeLegacyLongTaskMessages } from "@/lib/thread-display-compat";
|
import { normalizeLegacyLongTaskMessages } from "@/lib/thread-display-compat";
|
||||||
import { scrubSubagentUiMessages } from "@/lib/subagent-channel-display";
|
import { scrubSubagentUiMessages } from "@/lib/subagent-channel-display";
|
||||||
@ -251,7 +256,7 @@ export function ThreadShell({
|
|||||||
const refreshCliApps = useCallback(async () => {
|
const refreshCliApps = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const payload = await fetchCliApps(token);
|
const payload = await fetchCliApps(token);
|
||||||
setCliApps(payload.apps.filter((app) => app.installed));
|
setCliApps(installedCliAppsFromPayload(payload));
|
||||||
} catch {
|
} catch {
|
||||||
setCliApps([]);
|
setCliApps([]);
|
||||||
}
|
}
|
||||||
@ -262,7 +267,7 @@ export function ThreadShell({
|
|||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
const payload = await fetchCliApps(token);
|
const payload = await fetchCliApps(token);
|
||||||
if (!cancelled) setCliApps(payload.apps.filter((app) => app.installed));
|
if (!cancelled) setCliApps(installedCliAppsFromPayload(payload));
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setCliApps([]);
|
if (!cancelled) setCliApps([]);
|
||||||
}
|
}
|
||||||
@ -275,10 +280,20 @@ export function ThreadShell({
|
|||||||
};
|
};
|
||||||
window.addEventListener("focus", refreshOnFocus);
|
window.addEventListener("focus", refreshOnFocus);
|
||||||
document.addEventListener("visibilitychange", refreshOnFocus);
|
document.addEventListener("visibilitychange", refreshOnFocus);
|
||||||
|
const refreshOnCliAppsChanged = (event: Event) => {
|
||||||
|
const payload = (event as CustomEvent<unknown>).detail;
|
||||||
|
if (isCliAppsPayload(payload)) {
|
||||||
|
setCliApps(installedCliAppsFromPayload(payload));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void refreshCliApps();
|
||||||
|
};
|
||||||
|
window.addEventListener(CLI_APPS_CHANGED_EVENT, refreshOnCliAppsChanged);
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
window.removeEventListener("focus", refreshOnFocus);
|
window.removeEventListener("focus", refreshOnFocus);
|
||||||
document.removeEventListener("visibilitychange", refreshOnFocus);
|
document.removeEventListener("visibilitychange", refreshOnFocus);
|
||||||
|
window.removeEventListener(CLI_APPS_CHANGED_EVENT, refreshOnCliAppsChanged);
|
||||||
};
|
};
|
||||||
}, [refreshCliApps, token]);
|
}, [refreshCliApps, token]);
|
||||||
|
|
||||||
|
|||||||
22
webui/src/lib/cli-app-events.ts
Normal file
22
webui/src/lib/cli-app-events.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import type { CliAppInfo, CliAppsPayload } from "@/lib/types";
|
||||||
|
|
||||||
|
export const CLI_APPS_CHANGED_EVENT = "nanobot:cli-apps-changed";
|
||||||
|
|
||||||
|
export function isCliAppsPayload(value: unknown): value is CliAppsPayload {
|
||||||
|
return (
|
||||||
|
!!value &&
|
||||||
|
typeof value === "object" &&
|
||||||
|
Array.isArray((value as { apps?: unknown }).apps)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installedCliAppsFromPayload(payload: CliAppsPayload): CliAppInfo[] {
|
||||||
|
return payload.apps.filter((app) => app.installed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notifyCliAppsChanged(payload: CliAppsPayload): void {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
window.dispatchEvent(new CustomEvent<CliAppsPayload>(CLI_APPS_CHANGED_EVENT, {
|
||||||
|
detail: payload,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@ -3,8 +3,9 @@ import type { ReactNode } from "react";
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { ThreadShell } from "@/components/thread/ThreadShell";
|
import { ThreadShell } from "@/components/thread/ThreadShell";
|
||||||
|
import { CLI_APPS_CHANGED_EVENT } from "@/lib/cli-app-events";
|
||||||
import { ClientProvider } from "@/providers/ClientProvider";
|
import { ClientProvider } from "@/providers/ClientProvider";
|
||||||
import type { UIMessage } from "@/lib/types";
|
import type { CliAppsPayload, UIMessage } from "@/lib/types";
|
||||||
function makeClient() {
|
function makeClient() {
|
||||||
const errorHandlers = new Set<(err: { kind: string }) => void>();
|
const errorHandlers = new Set<(err: { kind: string }) => void>();
|
||||||
const chatHandlers = new Map<string, Set<(ev: import("@/lib/types").InboundEvent) => void>>();
|
const chatHandlers = new Map<string, Set<(ev: import("@/lib/types").InboundEvent) => void>>();
|
||||||
@ -994,4 +995,50 @@ describe("ThreadShell", () => {
|
|||||||
await waitFor(() => expect(screen.getByText("from chat b")).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText("from chat b")).toBeInTheDocument());
|
||||||
expect(screen.queryByText("from chat a")).not.toBeInTheDocument();
|
expect(screen.queryByText("from chat a")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updates @ CLI app suggestions when settings broadcasts an install", async () => {
|
||||||
|
const client = makeClient();
|
||||||
|
render(wrap(
|
||||||
|
client,
|
||||||
|
<ThreadShell
|
||||||
|
session={session("chat-cli-apps")}
|
||||||
|
title="Chat chat-cli-apps"
|
||||||
|
onToggleSidebar={() => {}}
|
||||||
|
onGoHome={() => {}}
|
||||||
|
onNewChat={() => {}}
|
||||||
|
/>,
|
||||||
|
));
|
||||||
|
|
||||||
|
const input = await screen.findByLabelText("Message input");
|
||||||
|
expect(screen.queryByRole("listbox", { name: "CLI Apps" })).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
const payload: CliAppsPayload = {
|
||||||
|
apps: [{
|
||||||
|
name: "gimp",
|
||||||
|
display_name: "GIMP",
|
||||||
|
category: "image",
|
||||||
|
description: "Image editing",
|
||||||
|
requires: "",
|
||||||
|
source: "harness",
|
||||||
|
entry_point: "cli-anything-gimp",
|
||||||
|
install_supported: true,
|
||||||
|
installed: true,
|
||||||
|
available: true,
|
||||||
|
status: "installed",
|
||||||
|
logo_url: null,
|
||||||
|
brand_color: "#5C5543",
|
||||||
|
skill_installed: true,
|
||||||
|
}],
|
||||||
|
installed_count: 1,
|
||||||
|
catalog_updated_at: "2026-04-18",
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
window.dispatchEvent(new CustomEvent(CLI_APPS_CHANGED_EVENT, { detail: payload }));
|
||||||
|
});
|
||||||
|
fireEvent.change(input, { target: { value: "@", selectionStart: 1 } });
|
||||||
|
|
||||||
|
expect(screen.getByRole("listbox", { name: "CLI Apps" })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("option", { name: /@gimp/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user