Simplify mobile agent configuration (#3424)
This commit is contained in:
@@ -90,11 +90,7 @@ async function openTabSwitcher(page: Page) {
|
|||||||
async function openModelSelector(page: Page) {
|
async function openModelSelector(page: Page) {
|
||||||
await page.getByRole("button", { name: /Select model/ }).click();
|
await page.getByRole("button", { name: /Select model/ }).click();
|
||||||
await expectBottomSheetOpen(page);
|
await expectBottomSheetOpen(page);
|
||||||
await expect(
|
await expect(page.getByTestId("agent-controls-settings-list")).toBeVisible({ timeout: 10_000 });
|
||||||
page.getByLabel("Bottom Sheet", { exact: true }).getByText("Ten second stream", {
|
|
||||||
exact: true,
|
|
||||||
}),
|
|
||||||
).toBeVisible({ timeout: 10_000 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openAndCloseTabSwitcherTwice(page: Page) {
|
async function openAndCloseTabSwitcherTwice(page: Page) {
|
||||||
@@ -124,15 +120,23 @@ test.describe("mobile bottom sheet reopen", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("model selector closes after model selection", async ({ page }) => {
|
test("search selection returns to configuration options", async ({ page }) => {
|
||||||
await withMobileMockAgent(page, async () => {
|
await withMobileMockAgent(page, async () => {
|
||||||
await openModelSelector(page);
|
await openModelSelector(page);
|
||||||
const sheet = page.getByLabel("Bottom Sheet", { exact: true });
|
const sheet = page.getByTestId("agent-controls-model-sheet");
|
||||||
|
|
||||||
await sheet.getByText("Ten second stream", { exact: true }).click();
|
await page.getByTestId("model-search-all-input").click();
|
||||||
|
const model = page.getByRole("button", { name: /^Ten second stream/ });
|
||||||
|
await expect(model).toBeVisible({
|
||||||
|
timeout: 10_000,
|
||||||
|
});
|
||||||
|
|
||||||
await expect(sheet).not.toBeVisible({ timeout: 10_000 });
|
await model.click();
|
||||||
await expect(page.getByRole("button", { name: /Ten second stream/ })).toBeVisible();
|
|
||||||
|
await expect(sheet).toBeVisible();
|
||||||
|
await expect(page.getByTestId("agent-controls-settings-list")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("agent-controls-model")).toContainText("Ten second stream");
|
||||||
|
await expect(page.getByTestId("agent-controls-model-browser-sheet")).not.toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const STREAMED_MERMAID = [
|
|||||||
test("keeps a Mermaid diagram rendered while its message streams, completes, and reloads", async ({
|
test("keeps a Mermaid diagram rendered while its message streams, completes, and reloads", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
test.setTimeout(90_000);
|
test.setTimeout(120_000);
|
||||||
|
|
||||||
const agent = await seedMockAgentWorkspace({
|
const agent = await seedMockAgentWorkspace({
|
||||||
repoPrefix: "mermaid-streaming-",
|
repoPrefix: "mermaid-streaming-",
|
||||||
|
|||||||
@@ -21,21 +21,22 @@ async function openMockAgentAtMobileBreakpoint(page: Page) {
|
|||||||
|
|
||||||
async function openProviderSettingsFromModelSelector(page: Page) {
|
async function openProviderSettingsFromModelSelector(page: Page) {
|
||||||
await page.getByRole("button", { name: /Select model/ }).click();
|
await page.getByRole("button", { name: /Select model/ }).click();
|
||||||
await expect(page.getByLabel("Bottom Sheet", { exact: true })).toBeVisible({ timeout: 10_000 });
|
const configuration = page.getByTestId("agent-controls-model-sheet");
|
||||||
|
await expect(configuration).toBeVisible({ timeout: 10_000 });
|
||||||
|
await page.getByTestId("agent-controls-model").click();
|
||||||
|
|
||||||
const openCodeRow = page.getByText("OpenCode", { exact: true }).first();
|
const modelBrowser = page.getByTestId("agent-controls-model-browser-sheet");
|
||||||
if (await openCodeRow.isVisible().catch(() => false)) {
|
await expect(modelBrowser).toBeVisible({ timeout: 10_000 });
|
||||||
await openCodeRow.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: /Open .* settings/ }).click();
|
await page.getByRole("button", { name: /Open .* settings/ }).click();
|
||||||
await expect(page.getByTestId("provider-settings-sheet")).toBeVisible({ timeout: 10_000 });
|
await expect(page.getByTestId("provider-settings-sheet")).toBeVisible({ timeout: 10_000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function expectModelSelectorVisible(page: Page) {
|
async function expectModelBrowserVisible(page: Page) {
|
||||||
await expect(page.getByRole("button", { name: /Open .* settings/ })).toBeVisible({
|
await expect(page.getByTestId("agent-controls-model-browser-sheet")).toBeVisible({
|
||||||
timeout: 10_000,
|
timeout: 10_000,
|
||||||
});
|
});
|
||||||
|
await expect(page.getByRole("button", { name: /Open .* settings/ })).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function closeTopSheet(page: Page) {
|
async function closeTopSheet(page: Page) {
|
||||||
@@ -180,7 +181,7 @@ test.describe("provider settings overlay stack", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("provider settings and children close back through the model selector stack", async ({
|
test("provider settings and children close back through the model browser to configuration", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
test.setTimeout(180_000);
|
test.setTimeout(180_000);
|
||||||
@@ -192,13 +193,18 @@ test.describe("provider settings overlay stack", () => {
|
|||||||
await exerciseProviderSettingsStack(page);
|
await exerciseProviderSettingsStack(page);
|
||||||
await closeSheetByHeaderButton(page, "provider-settings-sheet");
|
await closeSheetByHeaderButton(page, "provider-settings-sheet");
|
||||||
|
|
||||||
await expectModelSelectorVisible(page);
|
await expectModelBrowserVisible(page);
|
||||||
await page.getByRole("button", { name: /Open .* settings/ }).click();
|
await page.getByRole("button", { name: /Open .* settings/ }).click();
|
||||||
await expect(page.getByTestId("provider-settings-sheet")).toBeVisible({ timeout: 10_000 });
|
await expect(page.getByTestId("provider-settings-sheet")).toBeVisible({ timeout: 10_000 });
|
||||||
await exerciseProviderSettingsStack(page);
|
await exerciseProviderSettingsStack(page);
|
||||||
await closeSheetByHeaderButton(page, "provider-settings-sheet");
|
await closeSheetByHeaderButton(page, "provider-settings-sheet");
|
||||||
|
|
||||||
await expectModelSelectorVisible(page);
|
await expectModelBrowserVisible(page);
|
||||||
|
await closeTopSheet(page);
|
||||||
|
await expect(page.getByTestId("agent-controls-model-browser-sheet")).not.toBeVisible({
|
||||||
|
timeout: 10_000,
|
||||||
|
});
|
||||||
|
await expect(page.getByTestId("agent-controls-settings-list")).toBeVisible();
|
||||||
await closeTopSheet(page);
|
await closeTopSheet(page);
|
||||||
} finally {
|
} finally {
|
||||||
await session.cleanup();
|
await session.cleanup();
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { getCompactSheetSafeAreaPadding } from "@/components/adaptive-modal-sheet-layout";
|
import {
|
||||||
|
getBottomSheetVisibleContentHeight,
|
||||||
|
getCompactSheetSafeAreaPadding,
|
||||||
|
} from "@/components/adaptive-modal-sheet-layout";
|
||||||
|
|
||||||
describe("getCompactSheetSafeAreaPadding", () => {
|
describe("getCompactSheetSafeAreaPadding", () => {
|
||||||
it("adds the bottom inset to compact sheet footers", () => {
|
it("adds the bottom inset to compact sheet footers", () => {
|
||||||
expect(
|
expect(
|
||||||
getCompactSheetSafeAreaPadding({
|
getCompactSheetSafeAreaPadding({
|
||||||
isCompact: true,
|
isCompact: true,
|
||||||
|
isKeyboardVisible: false,
|
||||||
hasFooter: true,
|
hasFooter: true,
|
||||||
baseContentPadding: 24,
|
baseContentPadding: 24,
|
||||||
baseFooterPadding: 12,
|
baseFooterPadding: 12,
|
||||||
@@ -18,6 +22,7 @@ describe("getCompactSheetSafeAreaPadding", () => {
|
|||||||
expect(
|
expect(
|
||||||
getCompactSheetSafeAreaPadding({
|
getCompactSheetSafeAreaPadding({
|
||||||
isCompact: true,
|
isCompact: true,
|
||||||
|
isKeyboardVisible: false,
|
||||||
hasFooter: false,
|
hasFooter: false,
|
||||||
baseContentPadding: 24,
|
baseContentPadding: 24,
|
||||||
baseFooterPadding: 12,
|
baseFooterPadding: 12,
|
||||||
@@ -26,10 +31,24 @@ describe("getCompactSheetSafeAreaPadding", () => {
|
|||||||
).toEqual({ contentPaddingBottom: 58 });
|
).toEqual({ contentPaddingBottom: 58 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not add a safe-area band above the compact keyboard", () => {
|
||||||
|
expect(
|
||||||
|
getCompactSheetSafeAreaPadding({
|
||||||
|
isCompact: true,
|
||||||
|
isKeyboardVisible: true,
|
||||||
|
hasFooter: false,
|
||||||
|
baseContentPadding: 24,
|
||||||
|
baseFooterPadding: 12,
|
||||||
|
safeAreaBottom: 34,
|
||||||
|
}),
|
||||||
|
).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
it("does not inset desktop sheets", () => {
|
it("does not inset desktop sheets", () => {
|
||||||
expect(
|
expect(
|
||||||
getCompactSheetSafeAreaPadding({
|
getCompactSheetSafeAreaPadding({
|
||||||
isCompact: false,
|
isCompact: false,
|
||||||
|
isKeyboardVisible: false,
|
||||||
hasFooter: false,
|
hasFooter: false,
|
||||||
baseContentPadding: 24,
|
baseContentPadding: 24,
|
||||||
baseFooterPadding: 12,
|
baseFooterPadding: 12,
|
||||||
@@ -38,3 +57,17 @@ describe("getCompactSheetSafeAreaPadding", () => {
|
|||||||
).toEqual({});
|
).toEqual({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getBottomSheetVisibleContentHeight", () => {
|
||||||
|
it("stops subtracting the retained keyboard height after the keyboard hides", () => {
|
||||||
|
const layout = {
|
||||||
|
containerHeight: 874,
|
||||||
|
contentPosition: 88,
|
||||||
|
handleHeight: 24,
|
||||||
|
keyboardHeight: 344,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getBottomSheetVisibleContentHeight({ ...layout, isKeyboardVisible: true })).toBe(418);
|
||||||
|
expect(getBottomSheetVisibleContentHeight({ ...layout, isKeyboardVisible: false })).toBe(762);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export interface CompactSheetSafeAreaPaddingInput {
|
export interface CompactSheetSafeAreaPaddingInput {
|
||||||
isCompact: boolean;
|
isCompact: boolean;
|
||||||
|
isKeyboardVisible: boolean;
|
||||||
hasFooter: boolean;
|
hasFooter: boolean;
|
||||||
baseContentPadding: number;
|
baseContentPadding: number;
|
||||||
baseFooterPadding: number;
|
baseFooterPadding: number;
|
||||||
@@ -11,14 +12,37 @@ export interface CompactSheetSafeAreaPadding {
|
|||||||
footerPaddingBottom?: number;
|
footerPaddingBottom?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BottomSheetVisibleContentHeightInput {
|
||||||
|
containerHeight: number;
|
||||||
|
contentPosition: number;
|
||||||
|
handleHeight: number;
|
||||||
|
keyboardHeight: number;
|
||||||
|
isKeyboardVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBottomSheetVisibleContentHeight({
|
||||||
|
containerHeight,
|
||||||
|
contentPosition,
|
||||||
|
handleHeight,
|
||||||
|
keyboardHeight,
|
||||||
|
isKeyboardVisible,
|
||||||
|
}: BottomSheetVisibleContentHeightInput): number {
|
||||||
|
"worklet";
|
||||||
|
return Math.max(
|
||||||
|
0,
|
||||||
|
containerHeight - contentPosition - handleHeight - (isKeyboardVisible ? keyboardHeight : 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getCompactSheetSafeAreaPadding({
|
export function getCompactSheetSafeAreaPadding({
|
||||||
isCompact,
|
isCompact,
|
||||||
|
isKeyboardVisible,
|
||||||
hasFooter,
|
hasFooter,
|
||||||
baseContentPadding,
|
baseContentPadding,
|
||||||
baseFooterPadding,
|
baseFooterPadding,
|
||||||
safeAreaBottom,
|
safeAreaBottom,
|
||||||
}: CompactSheetSafeAreaPaddingInput): CompactSheetSafeAreaPadding {
|
}: CompactSheetSafeAreaPaddingInput): CompactSheetSafeAreaPadding {
|
||||||
if (!isCompact || safeAreaBottom <= 0) {
|
if (!isCompact || isKeyboardVisible || safeAreaBottom <= 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
BottomSheetBackdrop,
|
BottomSheetBackdrop,
|
||||||
BottomSheetScrollView,
|
BottomSheetScrollView,
|
||||||
BottomSheetTextInput,
|
BottomSheetTextInput,
|
||||||
|
KEYBOARD_STATUS,
|
||||||
useBottomSheetInternal,
|
useBottomSheetInternal,
|
||||||
type BottomSheetBackgroundProps,
|
type BottomSheetBackgroundProps,
|
||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
@@ -25,9 +26,13 @@ import {
|
|||||||
IsolatedBottomSheetModal,
|
IsolatedBottomSheetModal,
|
||||||
useIsolatedBottomSheetVisibility,
|
useIsolatedBottomSheetVisibility,
|
||||||
} from "@/components/ui/isolated-bottom-sheet-modal";
|
} from "@/components/ui/isolated-bottom-sheet-modal";
|
||||||
import { getCompactSheetSafeAreaPadding } from "@/components/adaptive-modal-sheet-layout";
|
import {
|
||||||
|
getBottomSheetVisibleContentHeight,
|
||||||
|
getCompactSheetSafeAreaPadding,
|
||||||
|
} from "@/components/adaptive-modal-sheet-layout";
|
||||||
import { createControlGeometry } from "@/components/ui/control-geometry";
|
import { createControlGeometry } from "@/components/ui/control-geometry";
|
||||||
import { isNative, isWeb } from "@/constants/platform";
|
import { isNative, isWeb } from "@/constants/platform";
|
||||||
|
import { useKeyboardVisibility } from "@/hooks/use-keyboard-visibility";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|
||||||
// Horizontal indent token shared by the sheet header (title, back arrow,
|
// Horizontal indent token shared by the sheet header (title, back arrow,
|
||||||
@@ -38,6 +43,8 @@ export const SHEET_HORIZONTAL_PADDING_SCALE = 6;
|
|||||||
|
|
||||||
export interface SheetHeaderSearch {
|
export interface SheetHeaderSearch {
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
onFocus?: () => void;
|
||||||
|
onBlur?: () => void;
|
||||||
resetKey?: string | number;
|
resetKey?: string | number;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
@@ -262,14 +269,15 @@ function BottomSheetVisibleContent({ children }: { children: ReactNode }) {
|
|||||||
? animatedPosition.get()
|
? animatedPosition.get()
|
||||||
: Math.min(animatedPosition.get(), initialDetentPosition);
|
: Math.min(animatedPosition.get(), initialDetentPosition);
|
||||||
|
|
||||||
|
const keyboardState = animatedKeyboardState.get();
|
||||||
return {
|
return {
|
||||||
height: Math.max(
|
height: getBottomSheetVisibleContentHeight({
|
||||||
0,
|
containerHeight,
|
||||||
containerHeight -
|
contentPosition,
|
||||||
contentPosition -
|
handleHeight,
|
||||||
handleHeight -
|
keyboardHeight: keyboardState.heightWithinContainer,
|
||||||
animatedKeyboardState.get().heightWithinContainer,
|
isKeyboardVisible: keyboardState.status === KEYBOARD_STATUS.SHOWN,
|
||||||
),
|
}),
|
||||||
};
|
};
|
||||||
}, [animatedDetentsState, animatedKeyboardState, animatedLayoutState, animatedPosition]);
|
}, [animatedDetentsState, animatedKeyboardState, animatedLayoutState, animatedPosition]);
|
||||||
|
|
||||||
@@ -409,6 +417,8 @@ export function SheetHeaderView({
|
|||||||
placeholder={search.placeholder ?? t("common.actions.search")}
|
placeholder={search.placeholder ?? t("common.actions.search")}
|
||||||
resetKey={search.resetKey}
|
resetKey={search.resetKey}
|
||||||
onChangeText={handleSearchChange}
|
onChangeText={handleSearchChange}
|
||||||
|
onFocus={search.onFocus}
|
||||||
|
onBlur={search.onBlur}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
autoFocus={search.autoFocus}
|
autoFocus={search.autoFocus}
|
||||||
@@ -466,6 +476,8 @@ export function InlineHeaderView({ header }: { header: SheetHeader }) {
|
|||||||
placeholder={header.search.placeholder ?? t("common.actions.search")}
|
placeholder={header.search.placeholder ?? t("common.actions.search")}
|
||||||
resetKey={header.search.resetKey}
|
resetKey={header.search.resetKey}
|
||||||
onChangeText={header.search.onChange}
|
onChangeText={header.search.onChange}
|
||||||
|
onFocus={header.search.onFocus}
|
||||||
|
onBlur={header.search.onBlur}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
autoFocus={header.search.autoFocus}
|
autoFocus={header.search.autoFocus}
|
||||||
@@ -518,17 +530,19 @@ export function AdaptiveModalSheet({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isMobile = useIsCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const isKeyboardVisible = useKeyboardVisibility();
|
||||||
const resolvedSnapPoints = useMemo(() => snapPoints ?? ["65%", "90%"], [snapPoints]);
|
const resolvedSnapPoints = useMemo(() => snapPoints ?? ["65%", "90%"], [snapPoints]);
|
||||||
const compactSafeAreaPadding = useMemo(
|
const compactSafeAreaPadding = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getCompactSheetSafeAreaPadding({
|
getCompactSheetSafeAreaPadding({
|
||||||
isCompact: isMobile,
|
isCompact: isMobile,
|
||||||
|
isKeyboardVisible,
|
||||||
hasFooter: Boolean(footer),
|
hasFooter: Boolean(footer),
|
||||||
baseContentPadding: theme.spacing[SHEET_HORIZONTAL_PADDING_SCALE],
|
baseContentPadding: theme.spacing[SHEET_HORIZONTAL_PADDING_SCALE],
|
||||||
baseFooterPadding: theme.spacing[3],
|
baseFooterPadding: theme.spacing[3],
|
||||||
safeAreaBottom: insets.bottom,
|
safeAreaBottom: insets.bottom,
|
||||||
}),
|
}),
|
||||||
[footer, insets.bottom, isMobile, theme.spacing],
|
[footer, insets.bottom, isKeyboardVisible, isMobile, theme.spacing],
|
||||||
);
|
);
|
||||||
const compactContentStyle = useMemo(
|
const compactContentStyle = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
|||||||
@@ -105,13 +105,35 @@ describe("model browser all view", () => {
|
|||||||
const providers = [claude, copilot, codex];
|
const providers = [claude, copilot, codex];
|
||||||
|
|
||||||
it("browses providers while the query is empty", () => {
|
it("browses providers while the query is empty", () => {
|
||||||
expect(resolveModelBrowserAllView({ providers, normalizedQuery: "" })).toEqual({
|
expect(
|
||||||
|
resolveModelBrowserAllView({ providers, normalizedQuery: "", isSearchFocused: false }),
|
||||||
|
).toEqual({
|
||||||
kind: "browse",
|
kind: "browse",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows every searchable model as soon as empty search receives focus", () => {
|
||||||
|
const view = resolveModelBrowserAllView({
|
||||||
|
providers,
|
||||||
|
normalizedQuery: "",
|
||||||
|
isSearchFocused: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(view.kind).toBe("searchResults");
|
||||||
|
expect(view.kind === "searchResults" ? view.rows.map((row) => row.favoriteKey) : []).toEqual([
|
||||||
|
"claude:opus-5",
|
||||||
|
"claude:sonnet-4.6",
|
||||||
|
"copilot:claude-opus-5",
|
||||||
|
"codex:gpt-5.4",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("ranks the same model label across every provider that offers it", () => {
|
it("ranks the same model label across every provider that offers it", () => {
|
||||||
const view = resolveModelBrowserAllView({ providers, normalizedQuery: "opus" });
|
const view = resolveModelBrowserAllView({
|
||||||
|
providers,
|
||||||
|
normalizedQuery: "opus",
|
||||||
|
isSearchFocused: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(view.kind).toBe("searchResults");
|
expect(view.kind).toBe("searchResults");
|
||||||
expect(view.kind === "searchResults" ? view.rows.map((row) => row.favoriteKey) : []).toEqual([
|
expect(view.kind === "searchResults" ? view.rows.map((row) => row.favoriteKey) : []).toEqual([
|
||||||
@@ -121,7 +143,11 @@ describe("model browser all view", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("matches models by their provider label", () => {
|
it("matches models by their provider label", () => {
|
||||||
const view = resolveModelBrowserAllView({ providers, normalizedQuery: "codex" });
|
const view = resolveModelBrowserAllView({
|
||||||
|
providers,
|
||||||
|
normalizedQuery: "codex",
|
||||||
|
isSearchFocused: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(view.kind === "searchResults" ? view.rows.map((row) => row.modelId) : []).toEqual([
|
expect(view.kind === "searchResults" ? view.rows.map((row) => row.modelId) : []).toEqual([
|
||||||
"gpt-5.4",
|
"gpt-5.4",
|
||||||
@@ -129,9 +155,13 @@ describe("model browser all view", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("reports no matches instead of falling back to the provider list", () => {
|
it("reports no matches instead of falling back to the provider list", () => {
|
||||||
expect(resolveModelBrowserAllView({ providers, normalizedQuery: "zzzz" })).toEqual({
|
expect(
|
||||||
kind: "noSearchMatches",
|
resolveModelBrowserAllView({
|
||||||
});
|
providers,
|
||||||
|
normalizedQuery: "zzzz",
|
||||||
|
isSearchFocused: true,
|
||||||
|
}),
|
||||||
|
).toEqual({ kind: "noSearchMatches" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores providers that are still loading or errored", () => {
|
it("ignores providers that are still loading or errored", () => {
|
||||||
@@ -147,7 +177,11 @@ describe("model browser all view", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
resolveModelBrowserAllView({ providers: [loading, failed], normalizedQuery: "opus" }),
|
resolveModelBrowserAllView({
|
||||||
|
providers: [loading, failed],
|
||||||
|
normalizedQuery: "opus",
|
||||||
|
isSearchFocused: true,
|
||||||
|
}),
|
||||||
).toEqual({ kind: "noSearchMatches" });
|
).toEqual({ kind: "noSearchMatches" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,14 +18,17 @@ export type ModelBrowserAllView =
|
|||||||
export function resolveModelBrowserAllView({
|
export function resolveModelBrowserAllView({
|
||||||
providers,
|
providers,
|
||||||
normalizedQuery,
|
normalizedQuery,
|
||||||
|
isSearchFocused,
|
||||||
}: {
|
}: {
|
||||||
providers: ProviderSelectorProvider[];
|
providers: ProviderSelectorProvider[];
|
||||||
normalizedQuery: string;
|
normalizedQuery: string;
|
||||||
|
isSearchFocused: boolean;
|
||||||
}): ModelBrowserAllView {
|
}): ModelBrowserAllView {
|
||||||
if (!normalizedQuery) {
|
if (!normalizedQuery && !isSearchFocused) {
|
||||||
return { kind: "browse" };
|
return { kind: "browse" };
|
||||||
}
|
}
|
||||||
const rows = filterAndRankModelRows(getAllProviderModelRows(providers), normalizedQuery);
|
const allRows = getAllProviderModelRows(providers);
|
||||||
|
const rows = normalizedQuery ? filterAndRankModelRows(allRows, normalizedQuery) : allRows;
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return { kind: "noSearchMatches" };
|
return { kind: "noSearchMatches" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ interface ModelBrowserInput {
|
|||||||
selectedProvider: string;
|
selectedProvider: string;
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
autoFocusSearch?: boolean;
|
||||||
/** Pinned above the provider list on the root view. `null` hides the section. */
|
/** Pinned above the provider list on the root view. `null` hides the section. */
|
||||||
profiles?: AgentProfilePicker | null;
|
profiles?: AgentProfilePicker | null;
|
||||||
serverId?: string | null;
|
serverId?: string | null;
|
||||||
@@ -157,12 +158,14 @@ export interface ModelBrowserState {
|
|||||||
profiles: AgentProfilePicker | null;
|
profiles: AgentProfilePicker | null;
|
||||||
view: ModelBrowserView;
|
view: ModelBrowserView;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
|
isSearchFocused: boolean;
|
||||||
header: SheetHeader;
|
header: SheetHeader;
|
||||||
selectedModelLabel: string;
|
selectedModelLabel: string;
|
||||||
triggerLabel: string;
|
triggerLabel: string;
|
||||||
desktopFixedHeight: number | undefined;
|
desktopFixedHeight: number | undefined;
|
||||||
isProviderView: boolean;
|
isProviderView: boolean;
|
||||||
prepareToOpen: () => void;
|
prepareToOpen: () => void;
|
||||||
|
showAll: () => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
drillDown: (providerId: string, providerLabel: string) => void;
|
drillDown: (providerId: string, providerLabel: string) => void;
|
||||||
}
|
}
|
||||||
@@ -176,6 +179,10 @@ interface ModelBrowserProps {
|
|||||||
onRetryProvider?: (provider: AgentProvider) => void;
|
onRetryProvider?: (provider: AgentProvider) => void;
|
||||||
isRetryingProvider?: boolean;
|
isRetryingProvider?: boolean;
|
||||||
scrolling?: "sheet" | "independent";
|
scrolling?: "sheet" | "independent";
|
||||||
|
/** Empty focused search shows all model rows instead of the browse root. */
|
||||||
|
searchAllOnFocus?: boolean;
|
||||||
|
/** Replaces provider rows only while the all-provider search is empty. */
|
||||||
|
rootBrowseContent?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModelBrowserContentProps extends Omit<ModelBrowserProps, "state" | "scrolling"> {
|
interface ModelBrowserContentProps extends Omit<ModelBrowserProps, "state" | "scrolling"> {
|
||||||
@@ -184,9 +191,12 @@ interface ModelBrowserContentProps extends Omit<ModelBrowserProps, "state" | "sc
|
|||||||
selectedProvider: string;
|
selectedProvider: string;
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
|
isSearchFocused: boolean;
|
||||||
profiles: AgentProfilePicker | null;
|
profiles: AgentProfilePicker | null;
|
||||||
onDrillDown: (providerId: string, providerLabel: string) => void;
|
onDrillDown: (providerId: string, providerLabel: string) => void;
|
||||||
scrolling: "sheet" | "independent";
|
scrolling: "sheet" | "independent";
|
||||||
|
searchAllOnFocus: boolean;
|
||||||
|
rootBrowseContent?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderGlyphTone = "muted" | "foreground";
|
type ProviderGlyphTone = "muted" | "foreground";
|
||||||
@@ -246,12 +256,14 @@ export function useModelBrowser({
|
|||||||
selectedProvider,
|
selectedProvider,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
autoFocusSearch = isWeb,
|
||||||
profiles = null,
|
profiles = null,
|
||||||
serverId = null,
|
serverId = null,
|
||||||
}: ModelBrowserInput): ModelBrowserState {
|
}: ModelBrowserInput): ModelBrowserState {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [view, setView] = useState<ModelBrowserView>({ kind: "all" });
|
const [view, setView] = useState<ModelBrowserView>({ kind: "all" });
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
||||||
const [searchResetKey, bumpSearchResetKey] = useReducer((key: number) => key + 1, 0);
|
const [searchResetKey, bumpSearchResetKey] = useReducer((key: number) => key + 1, 0);
|
||||||
const hasProfiles = (profiles?.rows.length ?? 0) > 0;
|
const hasProfiles = (profiles?.rows.length ?? 0) > 0;
|
||||||
|
|
||||||
@@ -272,10 +284,11 @@ export function useModelBrowser({
|
|||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
|
setIsSearchFocused(false);
|
||||||
bumpSearchResetKey();
|
bumpSearchResetKey();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleBackToAll = useCallback(() => {
|
const showAll = useCallback(() => {
|
||||||
setView({ kind: "all" });
|
setView({ kind: "all" });
|
||||||
reset();
|
reset();
|
||||||
}, [reset]);
|
}, [reset]);
|
||||||
@@ -299,9 +312,11 @@ export function useModelBrowser({
|
|||||||
title: t("modelSelector.title"),
|
title: t("modelSelector.title"),
|
||||||
search: {
|
search: {
|
||||||
onChange: handleSearchQueryChange,
|
onChange: handleSearchQueryChange,
|
||||||
|
onFocus: () => setIsSearchFocused(true),
|
||||||
|
onBlur: () => setIsSearchFocused(false),
|
||||||
resetKey: `all:${searchResetKey}`,
|
resetKey: `all:${searchResetKey}`,
|
||||||
placeholder: t("modelSelector.searchAllPlaceholder"),
|
placeholder: t("modelSelector.searchAllPlaceholder"),
|
||||||
autoFocus: isWeb,
|
autoFocus: autoFocusSearch,
|
||||||
testID: "model-search-all-input",
|
testID: "model-search-all-input",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -311,7 +326,7 @@ export function useModelBrowser({
|
|||||||
leading: (
|
leading: (
|
||||||
<ModelProviderGlyph provider={view.providerId} size={ICON_SIZE.md} tone="foreground" />
|
<ModelProviderGlyph provider={view.providerId} size={ICON_SIZE.md} tone="foreground" />
|
||||||
),
|
),
|
||||||
back: singleProviderView ? undefined : { onPress: handleBackToAll },
|
back: singleProviderView ? undefined : { onPress: showAll },
|
||||||
actions: (
|
actions: (
|
||||||
<View style={styles.headerActionRow}>
|
<View style={styles.headerActionRow}>
|
||||||
<ProviderSettingsAction
|
<ProviderSettingsAction
|
||||||
@@ -325,18 +340,21 @@ export function useModelBrowser({
|
|||||||
),
|
),
|
||||||
search: {
|
search: {
|
||||||
onChange: handleSearchQueryChange,
|
onChange: handleSearchQueryChange,
|
||||||
|
onFocus: () => setIsSearchFocused(true),
|
||||||
|
onBlur: () => setIsSearchFocused(false),
|
||||||
resetKey: `${view.providerId}:${searchResetKey}`,
|
resetKey: `${view.providerId}:${searchResetKey}`,
|
||||||
placeholder: t("modelSelector.searchPlaceholder"),
|
placeholder: t("modelSelector.searchPlaceholder"),
|
||||||
autoFocus: isWeb,
|
autoFocus: autoFocusSearch,
|
||||||
testID: "model-search-input",
|
testID: "model-search-input",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
handleBackToAll,
|
autoFocusSearch,
|
||||||
handleSearchQueryChange,
|
handleSearchQueryChange,
|
||||||
searchResetKey,
|
searchResetKey,
|
||||||
serverId,
|
serverId,
|
||||||
singleProviderView,
|
singleProviderView,
|
||||||
|
showAll,
|
||||||
t,
|
t,
|
||||||
view,
|
view,
|
||||||
]);
|
]);
|
||||||
@@ -371,12 +389,14 @@ export function useModelBrowser({
|
|||||||
profiles,
|
profiles,
|
||||||
view,
|
view,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
isSearchFocused,
|
||||||
header,
|
header,
|
||||||
selectedModelLabel,
|
selectedModelLabel,
|
||||||
triggerLabel,
|
triggerLabel,
|
||||||
desktopFixedHeight,
|
desktopFixedHeight,
|
||||||
isProviderView: view.kind === "provider",
|
isProviderView: view.kind === "provider",
|
||||||
prepareToOpen,
|
prepareToOpen,
|
||||||
|
showAll,
|
||||||
reset,
|
reset,
|
||||||
drillDown,
|
drillDown,
|
||||||
};
|
};
|
||||||
@@ -856,6 +876,7 @@ function IndependentModelList({
|
|||||||
keyExtractor={getModelRowKey}
|
keyExtractor={getModelRowKey}
|
||||||
style={styles.virtualizedModelList}
|
style={styles.virtualizedModelList}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
|
keyboardDismissMode="on-drag"
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.virtualizedModelListContent}
|
contentContainerStyle={styles.virtualizedModelListContent}
|
||||||
nestedScrollEnabled
|
nestedScrollEnabled
|
||||||
@@ -879,6 +900,7 @@ function IndependentProviderList({ children }: { children: React.ReactNode }) {
|
|||||||
styles.virtualizedProviderListContent,
|
styles.virtualizedProviderListContent,
|
||||||
]}
|
]}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
|
keyboardDismissMode="on-drag"
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
nestedScrollEnabled
|
nestedScrollEnabled
|
||||||
testID="compact-provider-list"
|
testID="compact-provider-list"
|
||||||
@@ -1078,6 +1100,7 @@ function ModelBrowserContent({
|
|||||||
selectedProvider,
|
selectedProvider,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
isSearchFocused,
|
||||||
profiles,
|
profiles,
|
||||||
onSelect,
|
onSelect,
|
||||||
onApplyProfile,
|
onApplyProfile,
|
||||||
@@ -1086,6 +1109,8 @@ function ModelBrowserContent({
|
|||||||
onRetryProvider,
|
onRetryProvider,
|
||||||
isRetryingProvider = false,
|
isRetryingProvider = false,
|
||||||
scrolling,
|
scrolling,
|
||||||
|
searchAllOnFocus,
|
||||||
|
rootBrowseContent,
|
||||||
}: ModelBrowserContentProps) {
|
}: ModelBrowserContentProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const normalizedQuery = useMemo(() => normalizeSearchQuery(searchQuery), [searchQuery]);
|
const normalizedQuery = useMemo(() => normalizeSearchQuery(searchQuery), [searchQuery]);
|
||||||
@@ -1097,10 +1122,15 @@ function ModelBrowserContent({
|
|||||||
[providers, view],
|
[providers, view],
|
||||||
);
|
);
|
||||||
const allView = useMemo(
|
const allView = useMemo(
|
||||||
() => resolveModelBrowserAllView({ providers, normalizedQuery }),
|
() =>
|
||||||
[normalizedQuery, providers],
|
resolveModelBrowserAllView({
|
||||||
|
providers,
|
||||||
|
normalizedQuery,
|
||||||
|
isSearchFocused: searchAllOnFocus && isSearchFocused,
|
||||||
|
}),
|
||||||
|
[isSearchFocused, normalizedQuery, providers, searchAllOnFocus],
|
||||||
);
|
);
|
||||||
const hasResults = profiles !== null || providers.length > 0;
|
const hasResults = profiles !== null || providers.length > 0 || rootBrowseContent != null;
|
||||||
|
|
||||||
if (view.kind === "provider") {
|
if (view.kind === "provider") {
|
||||||
return (
|
return (
|
||||||
@@ -1154,7 +1184,8 @@ function ModelBrowserContent({
|
|||||||
onEditProfiles={onEditProfiles}
|
onEditProfiles={onEditProfiles}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{providers.length > 0 ? (
|
{rootBrowseContent ??
|
||||||
|
(providers.length > 0 ? (
|
||||||
<View>
|
<View>
|
||||||
{profiles ? (
|
{profiles ? (
|
||||||
<View style={styles.sectionHeading}>
|
<View style={styles.sectionHeading}>
|
||||||
@@ -1163,7 +1194,7 @@ function ModelBrowserContent({
|
|||||||
) : null}
|
) : null}
|
||||||
<GroupedProviderRows providers={providers} onDrillDown={onDrillDown} />
|
<GroupedProviderRows providers={providers} onDrillDown={onDrillDown} />
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null)}
|
||||||
{!hasResults ? <ModelSearchEmptyState /> : null}
|
{!hasResults ? <ModelSearchEmptyState /> : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -1183,6 +1214,8 @@ export function ModelBrowser({
|
|||||||
onRetryProvider,
|
onRetryProvider,
|
||||||
isRetryingProvider = false,
|
isRetryingProvider = false,
|
||||||
scrolling = "sheet",
|
scrolling = "sheet",
|
||||||
|
searchAllOnFocus = false,
|
||||||
|
rootBrowseContent,
|
||||||
}: ModelBrowserProps) {
|
}: ModelBrowserProps) {
|
||||||
return (
|
return (
|
||||||
<ModelBrowserContent
|
<ModelBrowserContent
|
||||||
@@ -1191,6 +1224,7 @@ export function ModelBrowser({
|
|||||||
selectedProvider={state.selectedProvider}
|
selectedProvider={state.selectedProvider}
|
||||||
selectedModel={state.selectedModel}
|
selectedModel={state.selectedModel}
|
||||||
searchQuery={state.searchQuery}
|
searchQuery={state.searchQuery}
|
||||||
|
isSearchFocused={state.isSearchFocused}
|
||||||
profiles={state.profiles}
|
profiles={state.profiles}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onApplyProfile={onApplyProfile}
|
onApplyProfile={onApplyProfile}
|
||||||
@@ -1199,6 +1233,8 @@ export function ModelBrowser({
|
|||||||
onRetryProvider={onRetryProvider}
|
onRetryProvider={onRetryProvider}
|
||||||
isRetryingProvider={isRetryingProvider}
|
isRetryingProvider={isRetryingProvider}
|
||||||
scrolling={scrolling}
|
scrolling={scrolling}
|
||||||
|
searchAllOnFocus={searchAllOnFocus}
|
||||||
|
rootBrowseContent={rootBrowseContent}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -772,6 +772,7 @@ function ControlledAgentControls({
|
|||||||
modeControl={modeControl}
|
modeControl={modeControl}
|
||||||
glyphSize={layoutContextValue.glyphSize}
|
glyphSize={layoutContextValue.glyphSize}
|
||||||
modelSelectorServerId={modelSelectorServerId}
|
modelSelectorServerId={modelSelectorServerId}
|
||||||
|
canSwitchProvider={Boolean(onSelectProviderAndModel)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -1086,6 +1087,7 @@ interface SheetAgentControlsContentProps {
|
|||||||
modeControl?: AgentModeControlValue | null;
|
modeControl?: AgentModeControlValue | null;
|
||||||
glyphSize: number;
|
glyphSize: number;
|
||||||
modelSelectorServerId: string | null;
|
modelSelectorServerId: string | null;
|
||||||
|
canSwitchProvider: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetAgentControlsContent(props: SheetAgentControlsContentProps) {
|
function SheetAgentControlsContent(props: SheetAgentControlsContentProps) {
|
||||||
@@ -1122,6 +1124,7 @@ function SheetAgentControlsContent(props: SheetAgentControlsContentProps) {
|
|||||||
modeControl,
|
modeControl,
|
||||||
glyphSize,
|
glyphSize,
|
||||||
modelSelectorServerId,
|
modelSelectorServerId,
|
||||||
|
canSwitchProvider,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const thinkingAnchorRef = useRef<View | null>(null);
|
const thinkingAnchorRef = useRef<View | null>(null);
|
||||||
@@ -1193,6 +1196,7 @@ function SheetAgentControlsContent(props: SheetAgentControlsContentProps) {
|
|||||||
providers={modelSelectorProviders}
|
providers={modelSelectorProviders}
|
||||||
selectedProvider={provider}
|
selectedProvider={provider}
|
||||||
selectedModel={selectedModelId ?? ""}
|
selectedModel={selectedModelId ?? ""}
|
||||||
|
thinkingLabel={hasThinking ? displayThinking : null}
|
||||||
onSelect={handleSheetModelSelect}
|
onSelect={handleSheetModelSelect}
|
||||||
profiles={agentProfiles}
|
profiles={agentProfiles}
|
||||||
onApplyProfile={onApplyAgentProfile}
|
onApplyProfile={onApplyAgentProfile}
|
||||||
@@ -1205,6 +1209,7 @@ function SheetAgentControlsContent(props: SheetAgentControlsContentProps) {
|
|||||||
isRetryingProvider={isRetryingModelProvider}
|
isRetryingProvider={isRetryingModelProvider}
|
||||||
serverId={modelSelectorServerId}
|
serverId={modelSelectorServerId}
|
||||||
glyphSize={glyphSize}
|
glyphSize={glyphSize}
|
||||||
|
canSwitchProvider={canSwitchProvider}
|
||||||
>
|
>
|
||||||
{sheetControls}
|
{sheetControls}
|
||||||
</CompactModelSheet>
|
</CompactModelSheet>
|
||||||
@@ -1357,30 +1362,28 @@ function SheetFeatureItem({
|
|||||||
);
|
);
|
||||||
const sheetHeader = useMemo<SheetHeader>(() => ({ title: feature.label }), [feature.label]);
|
const sheetHeader = useMemo<SheetHeader>(() => ({ title: feature.label }), [feature.label]);
|
||||||
|
|
||||||
const handleTogglePress = useCallback(() => {
|
|
||||||
if (feature.type === "toggle") {
|
|
||||||
onSetFeature?.(feature.id, !feature.value);
|
|
||||||
}
|
|
||||||
}, [feature, onSetFeature]);
|
|
||||||
|
|
||||||
const handleSelectOption = useCallback(
|
const handleSelectOption = useCallback(
|
||||||
(optionId: string) => {
|
(optionId: string) => {
|
||||||
onSetFeature?.(feature.id, optionId);
|
onSetFeature?.(feature.id, feature.type === "toggle" ? optionId === "true" : optionId);
|
||||||
},
|
},
|
||||||
[feature.id, onSetFeature],
|
[feature.id, feature.type, onSetFeature],
|
||||||
);
|
|
||||||
const comboboxOptions = useMemo<ComboboxOption[]>(
|
|
||||||
() =>
|
|
||||||
feature.type === "select"
|
|
||||||
? feature.options.map((option) => ({ id: option.id, label: option.label }))
|
|
||||||
: [],
|
|
||||||
[feature],
|
|
||||||
);
|
);
|
||||||
|
const comboboxOptions = useMemo<ComboboxOption[]>(() => {
|
||||||
|
if (feature.type === "select") {
|
||||||
|
return feature.options.map((option) => ({ id: option.id, label: option.label }));
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ id: "true", label: t("agentControls.features.on") },
|
||||||
|
{ id: "false", label: t("agentControls.features.off") },
|
||||||
|
];
|
||||||
|
}, [feature, t]);
|
||||||
|
|
||||||
if (feature.type === "toggle") {
|
if (feature.type === "toggle") {
|
||||||
const FeatureIcon = getAgentFeatureIcon(feature.icon);
|
const FeatureIcon = getAgentFeatureIcon(feature.icon);
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<AgentControlTrigger
|
<AgentControlTrigger
|
||||||
|
ref={featureAnchorRef}
|
||||||
icon={FeatureIcon}
|
icon={FeatureIcon}
|
||||||
iconColor={getFeatureIconColor(
|
iconColor={getFeatureIconColor(
|
||||||
feature.id,
|
feature.id,
|
||||||
@@ -1391,11 +1394,24 @@ function SheetFeatureItem({
|
|||||||
surface="sheet"
|
surface="sheet"
|
||||||
label={feature.label}
|
label={feature.label}
|
||||||
value={feature.value ? t("agentControls.features.on") : t("agentControls.features.off")}
|
value={feature.value ? t("agentControls.features.on") : t("agentControls.features.off")}
|
||||||
|
open={openSelector === featureSelector}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onPress={handleTogglePress}
|
onPress={handleSelectPress}
|
||||||
accessibilityLabel={getFeatureTooltip(feature)}
|
accessibilityLabel={getFeatureTooltip(feature)}
|
||||||
testID={`agent-feature-${feature.id}`}
|
testID={`agent-feature-${feature.id}`}
|
||||||
/>
|
/>
|
||||||
|
<Combobox
|
||||||
|
options={comboboxOptions}
|
||||||
|
value={String(feature.value)}
|
||||||
|
onSelect={handleSelectOption}
|
||||||
|
open={openSelector === featureSelector}
|
||||||
|
onOpenChange={handleFeatureOpenChange}
|
||||||
|
anchorRef={featureAnchorRef}
|
||||||
|
presentation="push"
|
||||||
|
searchable={false}
|
||||||
|
header={sheetHeader}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection";
|
||||||
|
import { resolveModelSheetOpening } from "./model-sheet-flow";
|
||||||
|
|
||||||
|
function provider(id: string, label: string): ProviderSelectorProvider {
|
||||||
|
return { id, label, modelSelection: { kind: "models", rows: [] } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const codex = provider("codex", "Codex");
|
||||||
|
const claude = provider("claude", "Claude Code");
|
||||||
|
|
||||||
|
describe("model sheet opening", () => {
|
||||||
|
it("starts drafts at providers even when only one provider is available", () => {
|
||||||
|
expect(
|
||||||
|
resolveModelSheetOpening({
|
||||||
|
canSwitchProvider: true,
|
||||||
|
providers: [codex],
|
||||||
|
selectedProvider: "codex",
|
||||||
|
}),
|
||||||
|
).toEqual({ kind: "all" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens a running agent directly at its fixed provider", () => {
|
||||||
|
expect(
|
||||||
|
resolveModelSheetOpening({
|
||||||
|
canSwitchProvider: false,
|
||||||
|
providers: [claude, codex],
|
||||||
|
selectedProvider: "codex",
|
||||||
|
}),
|
||||||
|
).toEqual({ kind: "provider", providerId: "codex", providerLabel: "Codex" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the only provider while live state catches up", () => {
|
||||||
|
expect(
|
||||||
|
resolveModelSheetOpening({
|
||||||
|
canSwitchProvider: false,
|
||||||
|
providers: [codex],
|
||||||
|
selectedProvider: "",
|
||||||
|
}),
|
||||||
|
).toEqual({ kind: "provider", providerId: "codex", providerLabel: "Codex" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { ModelBrowserView } from "@/components/model-browser-view";
|
||||||
|
import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection";
|
||||||
|
|
||||||
|
export function resolveModelSheetOpening({
|
||||||
|
canSwitchProvider,
|
||||||
|
providers,
|
||||||
|
selectedProvider,
|
||||||
|
}: {
|
||||||
|
canSwitchProvider: boolean;
|
||||||
|
providers: ProviderSelectorProvider[];
|
||||||
|
selectedProvider: string;
|
||||||
|
}): ModelBrowserView {
|
||||||
|
if (canSwitchProvider) return { kind: "all" };
|
||||||
|
|
||||||
|
const provider = providers.find((entry) => entry.id === selectedProvider) ?? providers[0] ?? null;
|
||||||
|
return provider
|
||||||
|
? { kind: "provider", providerId: provider.id, providerLabel: provider.label }
|
||||||
|
: { kind: "all" };
|
||||||
|
}
|
||||||
@@ -2,15 +2,19 @@ import { useCallback, useMemo, useState, type ReactNode } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Keyboard, ScrollView, Text, View, type PressableStateCallbackType } from "react-native";
|
import { Keyboard, ScrollView, Text, View, type PressableStateCallbackType } from "react-native";
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
|
import { Bot } from "lucide-react-native";
|
||||||
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
||||||
import type { AgentProfilePicker } from "@/agent-profiles";
|
import type { AgentProfilePicker } from "@/agent-profiles";
|
||||||
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
||||||
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
|
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
|
||||||
import { getProviderIcon } from "@/components/provider-icons";
|
import { getProviderIcon } from "@/components/provider-icons";
|
||||||
import { ModelBrowser, useModelBrowser } from "@/components/model-browser";
|
import { ModelBrowser, useModelBrowser } from "@/components/model-browser";
|
||||||
|
import { AgentControlTrigger } from "@/composer/agent-controls/control";
|
||||||
import { ComposerToolbarGlyph } from "@/composer/agent-controls/glyph";
|
import { ComposerToolbarGlyph } from "@/composer/agent-controls/glyph";
|
||||||
|
import { resolveModelSheetOpening } from "@/composer/agent-controls/model-sheet-flow";
|
||||||
import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection";
|
import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection";
|
||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
|
import { isWeb } from "@/constants/platform";
|
||||||
|
|
||||||
const SNAP_POINTS = ["80%", "90%"];
|
const SNAP_POINTS = ["80%", "90%"];
|
||||||
const MODEL_LIST_TOP_INSET = 4;
|
const MODEL_LIST_TOP_INSET = 4;
|
||||||
@@ -23,6 +27,7 @@ interface CompactModelSheetProps {
|
|||||||
providers: ProviderSelectorProvider[];
|
providers: ProviderSelectorProvider[];
|
||||||
selectedProvider: string;
|
selectedProvider: string;
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
|
thinkingLabel: string | null;
|
||||||
onSelect: (provider: string, modelId: string) => void;
|
onSelect: (provider: string, modelId: string) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
profiles?: AgentProfilePicker | null;
|
profiles?: AgentProfilePicker | null;
|
||||||
@@ -35,6 +40,7 @@ interface CompactModelSheetProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
serverId?: string | null;
|
serverId?: string | null;
|
||||||
glyphSize: number;
|
glyphSize: number;
|
||||||
|
canSwitchProvider: boolean;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +53,7 @@ export function CompactModelSheet({
|
|||||||
providers,
|
providers,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
|
thinkingLabel,
|
||||||
onSelect,
|
onSelect,
|
||||||
isLoading,
|
isLoading,
|
||||||
profiles = null,
|
profiles = null,
|
||||||
@@ -59,47 +66,107 @@ export function CompactModelSheet({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
serverId = null,
|
serverId = null,
|
||||||
glyphSize,
|
glyphSize,
|
||||||
|
canSwitchProvider,
|
||||||
children,
|
children,
|
||||||
}: CompactModelSheetProps) {
|
}: CompactModelSheetProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const usesBottomSheet = useIsCompactFormFactor();
|
const usesBottomSheet = useIsCompactFormFactor();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const browser = useModelBrowser({
|
const [isModelBrowserOpen, setIsModelBrowserOpen] = useState(false);
|
||||||
providers,
|
const availableProviders = useMemo(() => {
|
||||||
|
if (canSwitchProvider) return providers;
|
||||||
|
const fixedProvider =
|
||||||
|
providers.find((entry) => entry.id === selectedProvider) ?? providers[0] ?? null;
|
||||||
|
return fixedProvider ? [fixedProvider] : [];
|
||||||
|
}, [canSwitchProvider, providers, selectedProvider]);
|
||||||
|
const rootBrowser = useModelBrowser({
|
||||||
|
providers: availableProviders,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
autoFocusSearch: isWeb && !usesBottomSheet,
|
||||||
profiles,
|
profiles,
|
||||||
serverId,
|
serverId,
|
||||||
});
|
});
|
||||||
const { prepareToOpen, reset } = browser;
|
const modelBrowser = useModelBrowser({
|
||||||
|
providers: availableProviders,
|
||||||
|
selectedProvider,
|
||||||
|
selectedModel,
|
||||||
|
isLoading,
|
||||||
|
autoFocusSearch: isWeb && !usesBottomSheet,
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
const ProviderIcon =
|
const ProviderIcon =
|
||||||
selectedProvider.trim().length > 0 ? getProviderIcon(selectedProvider) : null;
|
selectedProvider.trim().length > 0 ? getProviderIcon(selectedProvider) : null;
|
||||||
const compactFooter = useMemo(
|
const ModelIcon = ProviderIcon ?? Bot;
|
||||||
() =>
|
const rootHeader = useMemo(
|
||||||
usesBottomSheet ? (
|
() => ({
|
||||||
<View style={styles.compactFooter} testID="agent-controls-settings-list">
|
...rootBrowser.header,
|
||||||
<View style={styles.modelViewportDivider} />
|
title: t("modelSelector.selectModel"),
|
||||||
<View style={[styles.controlsContent, styles.compactControlsContent]}>{children}</View>
|
search:
|
||||||
</View>
|
rootBrowser.header.search && !canSwitchProvider
|
||||||
) : undefined,
|
? {
|
||||||
[children, usesBottomSheet],
|
...rootBrowser.header.search,
|
||||||
|
placeholder: t("modelSelector.searchPlaceholder"),
|
||||||
|
}
|
||||||
|
: rootBrowser.header.search,
|
||||||
|
}),
|
||||||
|
[canSwitchProvider, rootBrowser.header, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const open = useCallback(() => {
|
const open = useCallback(() => {
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
prepareToOpen();
|
rootBrowser.showAll();
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
onOpen?.();
|
onOpen?.();
|
||||||
}, [onOpen, prepareToOpen]);
|
}, [onOpen, rootBrowser]);
|
||||||
|
|
||||||
const close = useCallback(() => {
|
const close = useCallback(() => {
|
||||||
|
setIsModelBrowserOpen(false);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
reset();
|
rootBrowser.reset();
|
||||||
|
modelBrowser.reset();
|
||||||
onClose?.();
|
onClose?.();
|
||||||
}, [onClose, reset]);
|
}, [modelBrowser, onClose, rootBrowser]);
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSearchSelect = useCallback(
|
||||||
|
(provider: string, modelId: string) => {
|
||||||
|
onSelect(provider, modelId);
|
||||||
|
Keyboard.dismiss();
|
||||||
|
rootBrowser.showAll();
|
||||||
|
},
|
||||||
|
[onSelect, rootBrowser],
|
||||||
|
);
|
||||||
|
|
||||||
|
const openModelBrowser = useCallback(() => {
|
||||||
|
Keyboard.dismiss();
|
||||||
|
const destination = resolveModelSheetOpening({
|
||||||
|
canSwitchProvider,
|
||||||
|
providers: availableProviders,
|
||||||
|
selectedProvider,
|
||||||
|
});
|
||||||
|
if (destination.kind === "all") {
|
||||||
|
modelBrowser.showAll();
|
||||||
|
} else {
|
||||||
|
modelBrowser.drillDown(destination.providerId, destination.providerLabel);
|
||||||
|
}
|
||||||
|
setIsModelBrowserOpen(true);
|
||||||
|
}, [availableProviders, canSwitchProvider, modelBrowser, selectedProvider]);
|
||||||
|
|
||||||
|
const closeModelBrowser = useCallback(() => {
|
||||||
|
setIsModelBrowserOpen(false);
|
||||||
|
modelBrowser.reset();
|
||||||
|
}, [modelBrowser]);
|
||||||
|
|
||||||
|
const handleBrowserSelect = useCallback(
|
||||||
|
(provider: string, modelId: string) => {
|
||||||
|
onSelect(provider, modelId);
|
||||||
|
closeModelBrowser();
|
||||||
|
},
|
||||||
|
[closeModelBrowser, onSelect],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDesktopSelect = useCallback(
|
||||||
(provider: string, modelId: string) => {
|
(provider: string, modelId: string) => {
|
||||||
onSelect(provider, modelId);
|
onSelect(provider, modelId);
|
||||||
close();
|
close();
|
||||||
@@ -137,6 +204,28 @@ export function CompactModelSheet({
|
|||||||
],
|
],
|
||||||
[disabled, isOpen],
|
[disabled, isOpen],
|
||||||
);
|
);
|
||||||
|
const mobileRootContent = useMemo(
|
||||||
|
() => (
|
||||||
|
<View style={styles.mobileRootContent} testID="agent-controls-settings-list">
|
||||||
|
<View style={styles.controlsContent}>
|
||||||
|
<AgentControlTrigger
|
||||||
|
icon={ModelIcon}
|
||||||
|
surface="sheet"
|
||||||
|
label={t("modelSelector.model")}
|
||||||
|
value={rootBrowser.selectedModelLabel}
|
||||||
|
disabled={disabled}
|
||||||
|
onPress={openModelBrowser}
|
||||||
|
accessibilityLabel={t("modelSelector.selectedModel", {
|
||||||
|
model: rootBrowser.selectedModelLabel,
|
||||||
|
})}
|
||||||
|
testID="agent-controls-model"
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
[ModelIcon, children, disabled, openModelBrowser, rootBrowser.selectedModelLabel, t],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -147,7 +236,7 @@ export function CompactModelSheet({
|
|||||||
style={triggerStyle}
|
style={triggerStyle}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={t("modelSelector.selectedModel", {
|
accessibilityLabel={t("modelSelector.selectedModel", {
|
||||||
model: browser.selectedModelLabel,
|
model: rootBrowser.selectedModelLabel,
|
||||||
})}
|
})}
|
||||||
testID="combined-model-selector"
|
testID="combined-model-selector"
|
||||||
chevron={null}
|
chevron={null}
|
||||||
@@ -157,20 +246,25 @@ export function CompactModelSheet({
|
|||||||
<ProviderIcon size={glyphSize} color={styles.providerIcon.color} />
|
<ProviderIcon size={glyphSize} color={styles.providerIcon.color} />
|
||||||
</ComposerToolbarGlyph>
|
</ComposerToolbarGlyph>
|
||||||
) : null}
|
) : null}
|
||||||
|
<View style={styles.triggerLabels}>
|
||||||
<Text style={styles.triggerText} numberOfLines={1}>
|
<Text style={styles.triggerText} numberOfLines={1}>
|
||||||
{shortModelLabel(browser.triggerLabel)}
|
{shortModelLabel(rootBrowser.triggerLabel)}
|
||||||
</Text>
|
</Text>
|
||||||
|
{thinkingLabel ? (
|
||||||
|
<Text style={styles.triggerThinking} numberOfLines={1}>
|
||||||
|
{thinkingLabel}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
</ComboboxTrigger>
|
</ComboboxTrigger>
|
||||||
|
|
||||||
<AdaptiveModalSheet
|
<AdaptiveModalSheet
|
||||||
header={browser.header}
|
header={rootHeader}
|
||||||
visible={isOpen}
|
visible={isOpen}
|
||||||
onClose={close}
|
onClose={close}
|
||||||
snapPoints={SNAP_POINTS}
|
snapPoints={SNAP_POINTS}
|
||||||
scrollable={false}
|
scrollable={false}
|
||||||
sizeContentToCurrentSnapPoint={usesBottomSheet}
|
sizeContentToCurrentSnapPoint={usesBottomSheet}
|
||||||
footer={compactFooter}
|
|
||||||
footerContainerStyle={usesBottomSheet ? styles.compactFooterContainer : undefined}
|
|
||||||
contentStyle={styles.sheetBody}
|
contentStyle={styles.sheetBody}
|
||||||
testID="agent-controls-model-sheet"
|
testID="agent-controls-model-sheet"
|
||||||
>
|
>
|
||||||
@@ -182,13 +276,15 @@ export function CompactModelSheet({
|
|||||||
testID="agent-controls-model-viewport"
|
testID="agent-controls-model-viewport"
|
||||||
>
|
>
|
||||||
<ModelBrowser
|
<ModelBrowser
|
||||||
state={browser}
|
state={rootBrowser}
|
||||||
onSelect={handleSelect}
|
onSelect={usesBottomSheet ? handleSearchSelect : handleDesktopSelect}
|
||||||
onApplyProfile={handleApplyProfile}
|
onApplyProfile={handleApplyProfile}
|
||||||
onEditProfiles={onEditProfiles ? handleEditProfiles : undefined}
|
onEditProfiles={onEditProfiles ? handleEditProfiles : undefined}
|
||||||
onRetryProvider={onRetryProvider}
|
onRetryProvider={onRetryProvider}
|
||||||
isRetryingProvider={isRetryingProvider}
|
isRetryingProvider={isRetryingProvider}
|
||||||
scrolling="independent"
|
scrolling="independent"
|
||||||
|
searchAllOnFocus={usesBottomSheet}
|
||||||
|
rootBrowseContent={usesBottomSheet ? mobileRootContent : undefined}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{!usesBottomSheet ? (
|
{!usesBottomSheet ? (
|
||||||
@@ -206,6 +302,34 @@ export function CompactModelSheet({
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</AdaptiveModalSheet>
|
</AdaptiveModalSheet>
|
||||||
|
|
||||||
|
{usesBottomSheet ? (
|
||||||
|
<AdaptiveModalSheet
|
||||||
|
header={modelBrowser.header}
|
||||||
|
visible={isModelBrowserOpen}
|
||||||
|
onClose={closeModelBrowser}
|
||||||
|
snapPoints={SNAP_POINTS}
|
||||||
|
scrollable={false}
|
||||||
|
sizeContentToCurrentSnapPoint
|
||||||
|
presentation="push"
|
||||||
|
contentStyle={styles.sheetBody}
|
||||||
|
testID="agent-controls-model-browser-sheet"
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[styles.modelViewport, styles.flexibleModelViewport]}
|
||||||
|
testID="agent-controls-model-browser-viewport"
|
||||||
|
>
|
||||||
|
<ModelBrowser
|
||||||
|
state={modelBrowser}
|
||||||
|
onSelect={handleBrowserSelect}
|
||||||
|
onRetryProvider={onRetryProvider}
|
||||||
|
isRetryingProvider={isRetryingProvider}
|
||||||
|
scrolling="independent"
|
||||||
|
searchAllOnFocus
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</AdaptiveModalSheet>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -238,6 +362,19 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
fontWeight: theme.fontWeight.normal,
|
fontWeight: theme.fontWeight.normal,
|
||||||
},
|
},
|
||||||
|
triggerLabels: {
|
||||||
|
minWidth: 0,
|
||||||
|
flexShrink: 1,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[1],
|
||||||
|
},
|
||||||
|
triggerThinking: {
|
||||||
|
flexShrink: 0,
|
||||||
|
color: theme.colors.foregroundExtraMuted,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
fontWeight: theme.fontWeight.normal,
|
||||||
|
},
|
||||||
providerIcon: {
|
providerIcon: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
},
|
},
|
||||||
@@ -268,20 +405,10 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
},
|
},
|
||||||
compactFooterContainer: {
|
mobileRootContent: {
|
||||||
flexDirection: "column",
|
backgroundColor: theme.colors.surfaceSidebar,
|
||||||
alignItems: "stretch",
|
borderTopWidth: 1,
|
||||||
justifyContent: "flex-start",
|
borderTopColor: theme.colors.border,
|
||||||
gap: 0,
|
|
||||||
paddingHorizontal: 0,
|
|
||||||
paddingTop: 0,
|
|
||||||
borderTopWidth: 0,
|
|
||||||
},
|
|
||||||
compactFooter: {
|
|
||||||
minWidth: 0,
|
|
||||||
},
|
|
||||||
compactControlsContent: {
|
|
||||||
paddingBottom: 0,
|
|
||||||
},
|
},
|
||||||
controlsContent: {
|
controlsContent: {
|
||||||
paddingHorizontal: theme.spacing[2],
|
paddingHorizontal: theme.spacing[2],
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { useKeyboardState } from "react-native-keyboard-controller";
|
||||||
|
|
||||||
|
export function useKeyboardVisibility(): boolean {
|
||||||
|
return useKeyboardState((state) => state.isVisible);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function useKeyboardVisibility(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -1411,6 +1411,7 @@ export const ar: TranslationResources = {
|
|||||||
defaultModel: "تقصير",
|
defaultModel: "تقصير",
|
||||||
profiles: "الملفات الشخصية",
|
profiles: "الملفات الشخصية",
|
||||||
providers: "المزودون",
|
providers: "المزودون",
|
||||||
|
model: "النموذج",
|
||||||
editProfiles: "تحرير",
|
editProfiles: "تحرير",
|
||||||
editProfilesLabel: "تحرير ملفات الوكيل",
|
editProfilesLabel: "تحرير ملفات الوكيل",
|
||||||
createProfile: "إنشاء ملف شخصي",
|
createProfile: "إنشاء ملف شخصي",
|
||||||
|
|||||||
@@ -1421,6 +1421,7 @@ export const en = {
|
|||||||
defaultModel: "Default",
|
defaultModel: "Default",
|
||||||
profiles: "Profiles",
|
profiles: "Profiles",
|
||||||
providers: "Providers",
|
providers: "Providers",
|
||||||
|
model: "Model",
|
||||||
editProfiles: "Edit",
|
editProfiles: "Edit",
|
||||||
editProfilesLabel: "Edit agent profiles",
|
editProfilesLabel: "Edit agent profiles",
|
||||||
createProfile: "Create profile",
|
createProfile: "Create profile",
|
||||||
|
|||||||
@@ -1452,6 +1452,7 @@ export const es: TranslationResources = {
|
|||||||
defaultModel: "Por defecto",
|
defaultModel: "Por defecto",
|
||||||
profiles: "Perfiles",
|
profiles: "Perfiles",
|
||||||
providers: "Proveedores",
|
providers: "Proveedores",
|
||||||
|
model: "Modelo",
|
||||||
editProfiles: "Editar",
|
editProfiles: "Editar",
|
||||||
editProfilesLabel: "Editar perfiles de agente",
|
editProfilesLabel: "Editar perfiles de agente",
|
||||||
createProfile: "Crear perfil",
|
createProfile: "Crear perfil",
|
||||||
|
|||||||
@@ -1456,6 +1456,7 @@ export const fr: TranslationResources = {
|
|||||||
defaultModel: "Défaut",
|
defaultModel: "Défaut",
|
||||||
profiles: "Profils",
|
profiles: "Profils",
|
||||||
providers: "Fournisseurs",
|
providers: "Fournisseurs",
|
||||||
|
model: "Modèle",
|
||||||
editProfiles: "Modifier",
|
editProfiles: "Modifier",
|
||||||
editProfilesLabel: "Modifier les profils d'agent",
|
editProfilesLabel: "Modifier les profils d'agent",
|
||||||
createProfile: "Créer un profil",
|
createProfile: "Créer un profil",
|
||||||
|
|||||||
@@ -1425,6 +1425,7 @@ export const ja: TranslationResources = {
|
|||||||
defaultModel: "デフォルト",
|
defaultModel: "デフォルト",
|
||||||
profiles: "プロファイル",
|
profiles: "プロファイル",
|
||||||
providers: "プロバイダー",
|
providers: "プロバイダー",
|
||||||
|
model: "モデル",
|
||||||
editProfiles: "編集",
|
editProfiles: "編集",
|
||||||
editProfilesLabel: "エージェントプロファイルを編集",
|
editProfilesLabel: "エージェントプロファイルを編集",
|
||||||
createProfile: "プロファイルを作成",
|
createProfile: "プロファイルを作成",
|
||||||
|
|||||||
@@ -1421,6 +1421,7 @@ export const ko: TranslationResources = {
|
|||||||
defaultModel: "기본값",
|
defaultModel: "기본값",
|
||||||
profiles: "프로필",
|
profiles: "프로필",
|
||||||
providers: "제공자",
|
providers: "제공자",
|
||||||
|
model: "모델",
|
||||||
editProfiles: "편집",
|
editProfiles: "편집",
|
||||||
editProfilesLabel: "에이전트 프로필 편집",
|
editProfilesLabel: "에이전트 프로필 편집",
|
||||||
createProfile: "프로필 만들기",
|
createProfile: "프로필 만들기",
|
||||||
|
|||||||
@@ -1438,6 +1438,7 @@ export const ptBR: TranslationResources = {
|
|||||||
defaultModel: "Padrão",
|
defaultModel: "Padrão",
|
||||||
profiles: "Perfis",
|
profiles: "Perfis",
|
||||||
providers: "Provedores",
|
providers: "Provedores",
|
||||||
|
model: "Modelo",
|
||||||
editProfiles: "Editar",
|
editProfiles: "Editar",
|
||||||
editProfilesLabel: "Editar perfis de agente",
|
editProfilesLabel: "Editar perfis de agente",
|
||||||
createProfile: "Criar perfil",
|
createProfile: "Criar perfil",
|
||||||
|
|||||||
@@ -1443,6 +1443,7 @@ export const ru: TranslationResources = {
|
|||||||
defaultModel: "По умолчанию",
|
defaultModel: "По умолчанию",
|
||||||
profiles: "Профили",
|
profiles: "Профили",
|
||||||
providers: "Провайдеры",
|
providers: "Провайдеры",
|
||||||
|
model: "Модель",
|
||||||
editProfiles: "Изменить",
|
editProfiles: "Изменить",
|
||||||
editProfilesLabel: "Изменить профили агентов",
|
editProfilesLabel: "Изменить профили агентов",
|
||||||
createProfile: "Создать профиль",
|
createProfile: "Создать профиль",
|
||||||
|
|||||||
@@ -1394,6 +1394,7 @@ export const zhCN: TranslationResources = {
|
|||||||
defaultModel: "默认",
|
defaultModel: "默认",
|
||||||
profiles: "配置文件",
|
profiles: "配置文件",
|
||||||
providers: "提供方",
|
providers: "提供方",
|
||||||
|
model: "模型",
|
||||||
editProfiles: "编辑",
|
editProfiles: "编辑",
|
||||||
editProfilesLabel: "编辑智能体配置文件",
|
editProfilesLabel: "编辑智能体配置文件",
|
||||||
createProfile: "创建配置文件",
|
createProfile: "创建配置文件",
|
||||||
|
|||||||
@@ -400,9 +400,11 @@ describe("createArchiveFinishedSubagents", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("dismisses finished provider rows locally without removing their descriptors", async () => {
|
it("dismisses finished provider rows locally without removing their descriptors", async () => {
|
||||||
|
const finished = provider("finished");
|
||||||
|
const running = provider("running", "running");
|
||||||
const descriptors = new Map([
|
const descriptors = new Map([
|
||||||
["finished", provider("finished")],
|
["finished", finished],
|
||||||
["running", provider("running", "running")],
|
["running", running],
|
||||||
]);
|
]);
|
||||||
const dismissed: string[][] = [];
|
const dismissed: string[][] = [];
|
||||||
const archive = createArchiveFinishedSubagents([...descriptors.values()], {
|
const archive = createArchiveFinishedSubagents([...descriptors.values()], {
|
||||||
@@ -420,7 +422,7 @@ describe("createArchiveFinishedSubagents", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(dismissed).toEqual([["finished"]]);
|
expect(dismissed).toEqual([["finished"]]);
|
||||||
expect(descriptors.get("finished")).toEqual(provider("finished"));
|
expect(descriptors.get("finished")).toBe(finished);
|
||||||
expect(descriptors.get("running")).toEqual(provider("running", "running"));
|
expect(descriptors.get("running")).toBe(running);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user