mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-18 18:16:38 +03:00
feat(webui): add native workspace folder picker
This commit is contained in:
@@ -217,6 +217,7 @@ interface ThreadComposerProps {
|
||||
workspaceControls?: WorkspacesPayload["controls"] | null;
|
||||
workspaceScopeDisabled?: boolean;
|
||||
workspaceError?: string | null;
|
||||
onPickWorkspaceFolder?: () => Promise<string | null>;
|
||||
onWorkspaceScopeChange?: (scope: WorkspaceScopePayload) => void;
|
||||
pendingQueueKey?: string | null;
|
||||
transcriptionProvider?: string | null;
|
||||
@@ -970,6 +971,7 @@ export function ThreadComposer({
|
||||
workspaceControls = null,
|
||||
workspaceScopeDisabled = false,
|
||||
workspaceError = null,
|
||||
onPickWorkspaceFolder,
|
||||
onWorkspaceScopeChange,
|
||||
pendingQueueKey = null,
|
||||
transcriptionProvider = null,
|
||||
@@ -2600,6 +2602,7 @@ export function ThreadComposer({
|
||||
defaultScope={workspaceDefaultScope}
|
||||
controls={workspaceControls}
|
||||
error={workspaceError}
|
||||
onPickFolder={onPickWorkspaceFolder}
|
||||
onChange={onWorkspaceScopeChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -661,6 +661,14 @@ export function ThreadShell({
|
||||
forkBoundaryMessageCount,
|
||||
} = useSessionHistory(historyKey);
|
||||
const { client, getToken, ingressLimits, modelName, token } = useClient();
|
||||
const pickWorkspaceFolder = useCallback(async (): Promise<string | null> => {
|
||||
const response = await client.requestMutation<{ path: unknown }>(
|
||||
"workspace.pick_folder",
|
||||
{},
|
||||
300_000,
|
||||
);
|
||||
return typeof response.path === "string" ? response.path : null;
|
||||
}, [client]);
|
||||
const [fallbackModelName, setFallbackModelName] = useState<string | null>(null);
|
||||
const [booting, setBooting] = useState(false);
|
||||
const [slashCommands, setSlashCommands] = useState<SlashCommand[]>([]);
|
||||
@@ -1458,6 +1466,9 @@ export function ThreadShell({
|
||||
workspaceControls={workspaceControls}
|
||||
workspaceScopeDisabled={workspaceScopeDisabled}
|
||||
workspaceError={workspaceError}
|
||||
onPickWorkspaceFolder={
|
||||
workspaceControls?.can_pick_folder ? pickWorkspaceFolder : undefined
|
||||
}
|
||||
onWorkspaceScopeChange={onWorkspaceScopeChange}
|
||||
pendingQueueKey={temporary ? null : chatId}
|
||||
transcriptionProvider={settingsSnapshot?.transcription?.provider}
|
||||
@@ -1503,6 +1514,9 @@ export function ThreadShell({
|
||||
workspaceControls={workspaceControls}
|
||||
workspaceScopeDisabled={workspaceScopeDisabled}
|
||||
workspaceError={workspaceError}
|
||||
onPickWorkspaceFolder={
|
||||
workspaceControls?.can_pick_folder ? pickWorkspaceFolder : undefined
|
||||
}
|
||||
onWorkspaceScopeChange={onWorkspaceScopeChange}
|
||||
transcriptionProvider={settingsSnapshot?.transcription?.provider}
|
||||
ingressLimits={ingressLimits}
|
||||
|
||||
@@ -51,6 +51,7 @@ export function WorkspaceProjectPicker({
|
||||
defaultScope,
|
||||
controls,
|
||||
error,
|
||||
onPickFolder,
|
||||
onChange,
|
||||
}: {
|
||||
isHero: boolean;
|
||||
@@ -61,6 +62,7 @@ export function WorkspaceProjectPicker({
|
||||
defaultScope: WorkspaceScopePayload | null;
|
||||
controls: WorkspacesPayload["controls"] | null;
|
||||
error?: string | null;
|
||||
onPickFolder?: () => Promise<string | null>;
|
||||
onChange?: (scope: WorkspaceScopePayload) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -79,7 +81,7 @@ export function WorkspaceProjectPicker({
|
||||
&& !!defaultScope
|
||||
&& !!onChange
|
||||
&& controls?.can_change_project !== false;
|
||||
const pickFolder = getRuntimeHost().pickFolder;
|
||||
const pickFolder = getRuntimeHost().pickFolder ?? onPickFolder;
|
||||
const nativeProjectPicker = !!pickFolder;
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -363,6 +363,7 @@ export interface WorkspacesPayload {
|
||||
controls: {
|
||||
can_change_project: boolean;
|
||||
can_use_full_access: boolean;
|
||||
can_pick_folder?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1260,6 +1260,45 @@ describe("ThreadComposer", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("uses the gateway folder picker for a locally hosted WebUI", async () => {
|
||||
const onWorkspaceScopeChange = vi.fn();
|
||||
const pickFolder = vi.fn().mockResolvedValue("/Users/test/gateway-project");
|
||||
const defaultScope = {
|
||||
project_path: "/Users/test/.nanobot/workspace",
|
||||
project_name: "workspace",
|
||||
access_mode: "full" as const,
|
||||
restrict_to_workspace: false,
|
||||
};
|
||||
|
||||
render(
|
||||
<ThreadComposer
|
||||
onSend={vi.fn()}
|
||||
placeholder="Ask anything..."
|
||||
variant="hero"
|
||||
workspaceScope={defaultScope}
|
||||
workspaceDefaultScope={defaultScope}
|
||||
workspaceControls={{
|
||||
can_change_project: true,
|
||||
can_use_full_access: true,
|
||||
can_pick_folder: true,
|
||||
}}
|
||||
onPickWorkspaceFolder={pickFolder}
|
||||
onWorkspaceScopeChange={onWorkspaceScopeChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Choose project" }));
|
||||
|
||||
await waitFor(() => expect(pickFolder).toHaveBeenCalled());
|
||||
expect(screen.queryByLabelText("Paste path")).not.toBeInTheDocument();
|
||||
expect(onWorkspaceScopeChange).toHaveBeenCalledWith(expect.objectContaining({
|
||||
project_path: "/Users/test/gateway-project",
|
||||
project_name: "gateway-project",
|
||||
access_mode: "full",
|
||||
restrict_to_workspace: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it("uses the web path menu when no native host picker is available", async () => {
|
||||
const user = userEvent.setup();
|
||||
const defaultScope = {
|
||||
|
||||
Reference in New Issue
Block a user