diff --git a/AGENTS.md b/AGENTS.md index ab2858bdb72ba..c26ffb7390eec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -215,6 +215,7 @@ NeoCode is a distinct product built from upstream T3 Code (`pingdotgg/t3code`), | `apps/web/src/components/chat/ChatComposer.tsx` | Optional provider-owned composer projection, editor action, command label, and skill insertion hooks | | `packages/contracts/src/{settings,model,providerRuntime,server,usage}.ts`, `packages/contracts/src/settings.test.ts` | Provider contracts and NeoPi's enabled-by-default configuration; optional automatic-executable provenance; neutral client-facing settings guidance; preserve explicit opt-out and optional-provider boundaries | | `apps/web/src/components/settings/ProviderSettingsPanel.tsx` | Mount the NeoPi-owned automatic-executable selection note beside the saved binary setting | +| `apps/web/src/components/settings/ProviderModelsSection.tsx`, `docs/user/composer.md` | Keep large provider catalogs responsive with the existing virtual-list component; preserve full-catalog filtering, model preferences and custom-model reveal | | `apps/server/src/orchestration/{ActivityPayloadProjection,ActivityPayloadProjection.test}.ts` | Provider-neutral opt-in bounded command output for expanded activity rows; preserve complete Unicode code points at the truncation boundary | | `packages/client-runtime/src/work-log/{presentation,presentation.test}.ts` | Provider-neutral opt-in expanded command output precedence shared by web, desktop, and mobile, with fallback regression coverage | | `apps/web/src/session-logic.command-output.test.ts` | Regression for expanded command output through the web work-log consumer | diff --git a/apps/web/src/components/settings/ProviderModelsSection.tsx b/apps/web/src/components/settings/ProviderModelsSection.tsx index f4b1bfd7439b9..e0aeaba373e01 100644 --- a/apps/web/src/components/settings/ProviderModelsSection.tsx +++ b/apps/web/src/components/settings/ProviderModelsSection.tsx @@ -17,6 +17,7 @@ import { Input } from "../ui/input"; import { Switch } from "../ui/switch"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { CustomModelEditor } from "./CustomModelEditor"; +import { LegendList, LegendListRef } from "@legendapp/list/react"; /** * Placeholder text for the "add a custom model" input, keyed by driver @@ -177,7 +178,7 @@ export function ProviderModelsSection({ const [error, setError] = useState(null); // Slug of the custom model whose inline editor is open, if any. const [editingSlug, setEditingSlug] = useState(null); - const listRef = useRef(null); + const listRef = useRef(null); // Slug of a just-added custom model, scrolled into view once its row exists. const scrollToSlugRef = useRef(null); const hiddenModelSet = useMemo(() => new Set(hiddenModels), [hiddenModels]); @@ -209,17 +210,15 @@ export function ProviderModelsSection({ ) : displayModels; - // The parent commits the new custom model and hands back an updated - // `models` list, so the row can only be scrolled to after that render. + // Reveal a just-added model by index: its row may not be mounted yet. useEffect(() => { const slug = scrollToSlugRef.current; if (slug === null) return; - const row = listRef.current?.querySelector( - `[data-model-slug="${CSS.escape(slug)}"]`, - ); - if (!row) return; + const index = displayModels.findIndex((model) => model.slug === slug); + if (index < 0 || !listRef.current) return; scrollToSlugRef.current = null; - row.scrollIntoView({ block: "nearest" }); + void listRef.current.scrollToIndex({ index, animated: false, viewPosition: 0.5 }); + listRef.current.getScrollableNode().scrollIntoView({ block: "nearest" }); }, [displayModels]); const handleAdd = () => { @@ -505,7 +504,10 @@ export function ProviderModelsSection({ {showFilter ? ( setFilter(event.target.value)} + onChange={(event) => { + setFilter(event.target.value); + void listRef.current?.scrollToOffset({ offset: 0, animated: false }); + }} placeholder="Filter models" size="sm" className="w-56 max-w-full" @@ -547,49 +549,65 @@ export function ProviderModelsSection({ ) : null} -
+
{visibleModels.length === 0 ? (

{isFiltering ? "No models match." : "No models reported for this provider yet."}

) : null} - {visibleModels.map((model, index) => { - const group = groupOf(model); - const previous = visibleModels[index - 1]; - const startsGroup = previous === undefined || groupOf(previous) !== group; - const editingEntry = - model.isCustom && editingSlug === model.slug - ? customModels.find((entry) => entry.slug === model.slug) - : undefined; - return ( -
- {startsGroup && favoriteCount > 0 && group === "favorite" - ? groupLabel("Favorites", index === 0) - : null} - {startsGroup && favoriteCount > 0 && group === "visible" - ? groupLabel("All", index === 0) - : null} - {startsGroup && group === "hidden" - ? groupLabel("Hidden from picker", index === 0) - : null} - {renderRow(model)} - {editingEntry ? ( - setEditingSlug(null)} - /> - ) : null} -
- ); - })} + model.slug} + estimatedItemSize={28} + drawDistance={160} + recycleItems={false} + maintainVisibleContentPosition={false} + style={{ + height: Math.min( + visibleModels.length * 28 + + (favoriteCount > 0 ? 80 : 0) + + (hiddenCount > 0 ? 40 : 0) + + (editingSlug !== null ? 320 : 0), + 256, + ), + }} + renderItem={({ item: model, index }) => { + const group = groupOf(model); + const previous = visibleModels[index - 1]; + const startsGroup = previous === undefined || groupOf(previous) !== group; + const editingEntry = + model.isCustom && editingSlug === model.slug + ? customModels.find((entry) => entry.slug === model.slug) + : undefined; + return ( +
+ {startsGroup && favoriteCount > 0 && group === "favorite" + ? groupLabel("Favorites", index === 0) + : null} + {startsGroup && favoriteCount > 0 && group === "visible" + ? groupLabel("All", index === 0) + : null} + {startsGroup && group === "hidden" + ? groupLabel("Hidden from picker", index === 0) + : null} + {renderRow(model)} + {editingEntry ? ( + setEditingSlug(null)} + /> + ) : null} +
+ ); + }} + />
{driverKind === "antigravity" ? null : isAdding ? ( diff --git a/docs/user/composer.md b/docs/user/composer.md index 7ee8edcbb1215..747bf965c8fbb 100644 --- a/docs/user/composer.md +++ b/docs/user/composer.md @@ -62,6 +62,10 @@ On web and desktop, use Settings → Providers → **Models** to add an unlisted name and options. Only options supported by the provider integration affect turns. Antigravity uses its account catalog and does not support custom models. +The filter searches the provider's full catalog, including models outside the +visible rows. Favorites, ordering and hidden-model preferences apply to the +whole catalog. Adding a custom model brings its row into view. + ## Model defaults T3 Code remembers your provider, model, and model options for new threads. A