diff --git a/docs/custom-providers.md b/docs/custom-providers.md index 40933afc8..f7dd2745f 100644 --- a/docs/custom-providers.md +++ b/docs/custom-providers.md @@ -339,6 +339,8 @@ The [Agent Client Protocol (ACP)](https://agentclientprotocol.com) is an open st ACP agents communicate over JSON-RPC 2.0 on stdio. Paseo spawns the agent process and talks to it through stdin/stdout. +Paseo also ships an in-app ACP provider catalog for common agents, including Cursor, DeepAgents, DeepSeek TUI, DimCode, Gemini CLI, Hermes, Qwen Code, and Kimi Code. Catalog entries create the same `extends: "acp"` provider config shown below. + ### Adding a generic ACP provider Set `extends: "acp"` and provide a `command`: diff --git a/docs/product.md b/docs/product.md index ba1c531cf..57fa1808b 100644 --- a/docs/product.md +++ b/docs/product.md @@ -71,7 +71,7 @@ Anyone who builds software: - Desktop (Electron), mobile (iOS/Android), web, CLI - Built-in providers: Claude Code (Agent SDK), Codex (app-server), GitHub Copilot (ACP), OpenCode, Pi -- One-click ACP provider catalog: Cursor, Hermes, Qwen Coder, Kimi Code, and others — plus custom ACP providers +- One-click ACP provider catalog: Cursor, DeepSeek TUI, Hermes, Qwen Coder, Kimi Code, and others — plus custom ACP providers - Voice mode: dictate prompts or talk through problems hands-free - MCP server exposes the daemon to other agents (create_agent, send_agent_prompt, schedules, terminals, worktrees) - Scheduled agents (cron-style triggers) via app, CLI, and MCP diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx index cb4672b38..9dcc01202 100644 --- a/packages/app/src/components/agent-status-bar.tsx +++ b/packages/app/src/components/agent-status-bar.tsx @@ -33,9 +33,9 @@ import { import { getProviderIcon } from "@/components/provider-icons"; import { CombinedModelSelector } from "@/components/combined-model-selector"; import { - buildModelSelectorProviders, - type ModelSelectorProvider, -} from "@/components/combined-model-selector.utils"; + buildProviderSelectorProviders, + type ProviderSelectorProvider, +} from "@/provider-selection/provider-selection"; import { useSessionStore } from "@/stores/session-store"; import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; import { resolveProviderDefinition } from "@/utils/provider-definitions"; @@ -97,7 +97,7 @@ interface ControlledAgentStatusBarProps { disabled?: boolean; isModelLoading?: boolean; providerDefinitions: AgentProviderDefinition[]; - modelSelectorProviders?: ModelSelectorProvider[]; + modelSelectorProviders?: ProviderSelectorProvider[]; favoriteKeys?: Set; onToggleFavoriteModel?: (provider: string, modelId: string) => void; features?: AgentFeature[]; @@ -117,7 +117,7 @@ export interface DraftAgentStatusBarProps { selectedModel: string; onSelectModel: (modelId: string) => void; isModelLoading: boolean; - modelSelectorProviders: ModelSelectorProvider[]; + modelSelectorProviders: ProviderSelectorProvider[]; isAllModelsLoading: boolean; onSelectProviderAndModel: (provider: AgentProvider, modelId: string) => void; thinkingOptions: NonNullable; @@ -237,7 +237,7 @@ function toComboboxOptions(options: StatusOption[] | undefined): ComboboxOption[ function buildFallbackModelSelectorProviders( provider: string, modelOptions: StatusOption[] | undefined, -): ModelSelectorProvider[] { +): ProviderSelectorProvider[] { if (!modelOptions || modelOptions.length === 0) { return []; } @@ -245,13 +245,16 @@ function buildFallbackModelSelectorProviders( { id: provider, label: provider, - rows: modelOptions.map((option) => ({ - favoriteKey: buildFavoriteModelKey({ provider, modelId: option.id }), - provider, - providerLabel: provider, - modelId: option.id, - modelLabel: option.label, - })), + modelSelection: { + kind: "models", + rows: modelOptions.map((option) => ({ + favoriteKey: buildFavoriteModelKey({ provider, modelId: option.id }), + provider, + providerLabel: provider, + modelId: option.id, + modelLabel: option.label, + })), + }, }, ]; } @@ -794,7 +797,7 @@ interface DesktopStatusBarContentProps { canSelectMode: boolean; canSelectModel: boolean; canSelectThinking: boolean; - modelSelectorProviders: ModelSelectorProvider[]; + modelSelectorProviders: ProviderSelectorProvider[]; modelDisabled: boolean; comboboxProviderOptions: ComboboxOption[]; comboboxModeOptions: ComboboxOption[]; @@ -1058,7 +1061,7 @@ interface SheetStatusBarContentProps { canSelectMode: boolean; canSelectModel: boolean; canSelectThinking: boolean; - modelSelectorProviders: ModelSelectorProvider[]; + modelSelectorProviders: ProviderSelectorProvider[]; modelDisabled: boolean; comboboxModeOptions: ComboboxOption[]; comboboxThinkingOptions: ComboboxOption[]; @@ -1662,7 +1665,11 @@ export const AgentStatusBar = memo(function AgentStatusBar({ [agent?.provider, models], ); const agentModelSelectorProviders = useMemo( - () => buildModelSelectorProviders(agentProviderDefinitions, agentProviderModels), + () => + buildProviderSelectorProviders({ + providerDefinitions: agentProviderDefinitions, + modelsByProvider: agentProviderModels, + }), [agentProviderDefinitions, agentProviderModels], ); diff --git a/packages/app/src/components/combined-model-selector.tsx b/packages/app/src/components/combined-model-selector.tsx index 23ff82cd1..920a1f6d6 100644 --- a/packages/app/src/components/combined-model-selector.tsx +++ b/packages/app/src/components/combined-model-selector.tsx @@ -47,9 +47,13 @@ import { getProviderIcon } from "@/components/provider-icons"; import { buildSelectedTriggerLabel, filterAndRankModelRows, - type ModelSelectorProvider, - type SelectorModelRow, -} from "./combined-model-selector.utils"; + getAllProviderModelRows, + getProviderDefaultLabel, + getProviderModelRows, + resolveSelectedModelLabel, + type ProviderSelectionModelRow, + type ProviderSelectorProvider, +} from "@/provider-selection/provider-selection"; // TODO: this should be configured per provider in the provider manifest const PROVIDERS_WITH_MODEL_DESCRIPTIONS = new Set(["opencode", "pi"]); @@ -59,7 +63,7 @@ type SelectorView = | { kind: "provider"; providerId: string; providerLabel: string }; interface CombinedModelSelectorProps { - providers: ModelSelectorProvider[]; + providers: ProviderSelectorProvider[]; selectedProvider: string; selectedModel: string; onSelect: (provider: AgentProvider, modelId: string) => void; @@ -79,7 +83,7 @@ interface CombinedModelSelectorProps { interface SelectorContentProps { view: SelectorView; - providers: ModelSelectorProvider[]; + providers: ProviderSelectorProvider[]; selectedProvider: string; selectedModel: string; searchQuery: string; @@ -89,23 +93,16 @@ interface SelectorContentProps { onDrillDown: (providerId: string, providerLabel: string) => void; } -function resolveDefaultModelLabel(rows: SelectorModelRow[] | undefined): string { - if (!rows || rows.length === 0) { - return "Select model"; - } - return (rows.find((row) => row.isDefault) ?? rows[0])?.modelLabel ?? "Select model"; -} - function normalizeSearchQuery(value: string): string { return value.trim().toLowerCase(); } function sortFavoritesFirst( - rows: SelectorModelRow[], + rows: ProviderSelectionModelRow[], favoriteKeys: Set, -): SelectorModelRow[] { - const favorites: SelectorModelRow[] = []; - const rest: SelectorModelRow[] = []; +): ProviderSelectionModelRow[] { + const favorites: ProviderSelectionModelRow[] = []; + const rest: ProviderSelectionModelRow[] = []; for (const row of rows) { if (favoriteKeys.has(row.favoriteKey)) { favorites.push(row); @@ -124,7 +121,7 @@ function ModelRow({ onPress, onToggleFavorite, }: { - row: SelectorModelRow; + row: ProviderSelectionModelRow; isSelected: boolean; isFavorite: boolean; elevated?: boolean; @@ -200,7 +197,7 @@ function ModelRow({ } interface SelectableModelRowProps { - row: SelectorModelRow; + row: ProviderSelectionModelRow; isSelected: boolean; isFavorite: boolean; elevated?: boolean; @@ -239,7 +236,7 @@ function FavoritesSection({ onSelect, onToggleFavorite, }: { - favoriteRows: SelectorModelRow[]; + favoriteRows: ProviderSelectionModelRow[]; selectedProvider: string; selectedModel: string; favoriteKeys: Set; @@ -274,7 +271,7 @@ interface GroupProviderButtonProps { providerId: string; providerLabel: string; rowCount: number; - hasNoModels: boolean; + defaultLabel: string | null; onDrillDown: (providerId: string, providerLabel: string) => void; onSelectDefault: (providerId: string) => void; } @@ -283,28 +280,28 @@ function GroupProviderButton({ providerId, providerLabel, rowCount, - hasNoModels, + defaultLabel, onDrillDown, onSelectDefault, }: GroupProviderButtonProps) { const { theme } = useUnistyles(); const ProvIcon = getProviderIcon(providerId); const handlePress = useCallback(() => { - if (hasNoModels) { + if (defaultLabel) { onSelectDefault(providerId); return; } onDrillDown(providerId, providerLabel); - }, [hasNoModels, onDrillDown, onSelectDefault, providerId, providerLabel]); + }, [defaultLabel, onDrillDown, onSelectDefault, providerId, providerLabel]); return ( {providerLabel} - {hasNoModels ? "Default" : `${rowCount} ${rowCount === 1 ? "model" : "models"}`} + {defaultLabel ?? `${rowCount} ${rowCount === 1 ? "model" : "models"}`} - {hasNoModels ? null : ( + {defaultLabel ? null : ( )} @@ -317,22 +314,23 @@ function GroupedProviderRows({ onDrillDown, onSelectDefault, }: { - providers: ModelSelectorProvider[]; + providers: ProviderSelectorProvider[]; onDrillDown: (providerId: string, providerLabel: string) => void; onSelectDefault: (providerId: string) => void; }) { return ( {providers.map((provider, index) => { - const hasNoModels = provider.rows.length === 0; + const rows = getProviderModelRows(provider); + const defaultLabel = getProviderDefaultLabel(provider); return ( {index > 0 ? : null} @@ -381,7 +379,7 @@ function ProviderModelRows({ onToggleFavorite, normalizedQuery, }: { - rows: SelectorModelRow[]; + rows: ProviderSelectionModelRow[]; selectedProvider: string; selectedModel: string; favoriteKeys: Set; @@ -396,7 +394,7 @@ function ProviderModelRows({ [favoriteKeys, normalizedQuery, rows], ); const renderItem = useCallback( - ({ item }: { item: SelectorModelRow }) => ( + ({ item }: { item: ProviderSelectionModelRow }) => ( row.favoriteKey, []); + const keyExtractor = useCallback((row: ProviderSelectionModelRow) => row.favoriteKey, []); if (useVirtualizedList) { return ( @@ -455,15 +453,12 @@ function SelectorContent({ const visibleRows = useMemo( () => selectedViewProvider - ? filterAndRankModelRows(selectedViewProvider.rows, normalizedQuery) + ? filterAndRankModelRows(getProviderModelRows(selectedViewProvider), normalizedQuery) : [], [normalizedQuery, selectedViewProvider], ); const favoriteRows = useMemo( - () => - providers.flatMap((provider) => - provider.rows.filter((row) => favoriteKeys.has(row.favoriteKey)), - ), + () => getAllProviderModelRows(providers).filter((row) => favoriteKeys.has(row.favoriteKey)), [favoriteKeys, providers], ); const handleSelectDefaultProvider = useCallback( @@ -485,7 +480,7 @@ function SelectorContent({ return emptyState; } - if (selectedViewProvider.rows.length === 0 && !normalizedQuery) { + if (getProviderDefaultLabel(selectedViewProvider) && !normalizedQuery) { return ( { - if (!selectedModel) { - if (!hasSelectedProvider) { - return "Select model"; - } - const provider = providers.find((entry) => entry.id === selectedProvider); - if (provider?.rows.length === 0) { - return "Default"; - } - return isLoading ? "Loading..." : "Select model"; - } - const provider = providers.find((entry) => entry.id === selectedProvider); - if (!provider) { - return isLoading ? "Loading..." : "Select model"; - } - const model = provider.rows.find((entry) => entry.modelId === selectedModel); - return model?.modelLabel ?? resolveDefaultModelLabel(provider.rows); - }, [hasSelectedProvider, isLoading, providers, selectedModel, selectedProvider]); + return resolveSelectedModelLabel({ + providers, + selectedProvider, + selectedModel, + isLoading, + }); + }, [isLoading, providers, selectedModel, selectedProvider]); const desktopFixedHeight = useMemo(() => { if (view.kind !== "provider") { return undefined; } - const modelCount = - providers.find((provider) => provider.id === view.providerId)?.rows.length ?? 0; + const provider = providers.find((entry) => entry.id === view.providerId); + const modelCount = provider ? getProviderModelRows(provider).length : 0; return Math.min(80 + modelCount * 40, 400); }, [providers, view]); diff --git a/packages/app/src/components/combined-model-selector.utils.ts b/packages/app/src/components/combined-model-selector.utils.ts deleted file mode 100644 index b39324019..000000000 --- a/packages/app/src/components/combined-model-selector.utils.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { - AgentModelDefinition, - ProviderSnapshotEntry, -} from "@server/server/agent/agent-sdk-types"; -import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest"; -import { buildFavoriteModelKey, type FavoriteModelRow } from "@/hooks/use-form-preferences"; -import { compareMatchScores, scoreTextFields } from "@/utils/score-match"; - -export type SelectorModelRow = FavoriteModelRow & { isDefault?: boolean }; - -export interface ModelSelectorProvider { - id: string; - label: string; - rows: SelectorModelRow[]; -} - -export function buildSelectedTriggerLabel(modelLabel: string): string { - return modelLabel; -} - -export function buildModelSelectorProviders( - providerDefinitions: AgentProviderDefinition[], - allProviderModels: Map, -): ModelSelectorProvider[] { - return providerDefinitions.map((definition) => ({ - id: definition.id, - label: definition.label, - rows: (allProviderModels.get(definition.id) ?? []).map((model) => ({ - favoriteKey: buildFavoriteModelKey({ provider: definition.id, modelId: model.id }), - provider: definition.id, - providerLabel: definition.label, - modelId: model.id, - modelLabel: model.label, - description: model.description, - isDefault: model.isDefault, - })), - })); -} - -export function buildSelectableModelSelectorProviders( - entries: ProviderSnapshotEntry[] | undefined, -): ModelSelectorProvider[] { - return (entries ?? []) - .filter((entry) => entry.enabled && entry.status === "ready") - .map((entry) => ({ - id: entry.provider, - label: entry.label ?? entry.provider, - rows: (entry.models ?? []).map((model) => ({ - favoriteKey: buildFavoriteModelKey({ provider: entry.provider, modelId: model.id }), - provider: entry.provider, - providerLabel: entry.label ?? entry.provider, - modelId: model.id, - modelLabel: model.label, - description: model.description, - isDefault: model.isDefault, - })), - })); -} - -export function matchesSearch(row: SelectorModelRow, normalizedQuery: string): boolean { - return scoreModelRow(row, normalizedQuery) !== null; -} - -function getModelRowSearchFields(row: SelectorModelRow): string[] { - return [row.modelLabel, row.modelId, row.providerLabel, row.description ?? ""]; -} - -export function scoreModelRow(row: SelectorModelRow, normalizedQuery: string) { - return scoreTextFields(normalizedQuery, getModelRowSearchFields(row)); -} - -export function filterAndRankModelRows( - rows: SelectorModelRow[], - normalizedQuery: string, -): SelectorModelRow[] { - if (!normalizedQuery) return rows; - const scored = rows - .map((row) => ({ row, score: scoreModelRow(row, normalizedQuery) })) - .filter((entry): entry is { row: SelectorModelRow; score: NonNullable } => - Boolean(entry.score), - ); - - scored.sort((a, b) => { - const cmp = compareMatchScores(a.score, b.score); - if (cmp !== 0) return cmp; - return a.row.modelLabel.localeCompare(b.row.modelLabel); - }); - - return scored.map((entry) => entry.row); -} diff --git a/packages/app/src/hooks/use-agent-form-state.ts b/packages/app/src/hooks/use-agent-form-state.ts index fb579a53c..c99a34d7b 100644 --- a/packages/app/src/hooks/use-agent-form-state.ts +++ b/packages/app/src/hooks/use-agent-form-state.ts @@ -9,9 +9,9 @@ import type { import { useHosts } from "@/runtime/host-runtime"; import { buildProviderDefinitions } from "@/utils/provider-definitions"; import { - buildSelectableModelSelectorProviders, - type ModelSelectorProvider, -} from "@/components/combined-model-selector.utils"; + buildSelectableProviderSelectorProviders, + type ProviderSelectorProvider, +} from "@/provider-selection/provider-selection"; import { useProvidersSnapshot } from "./use-providers-snapshot"; import { useFormPreferences, @@ -32,9 +32,9 @@ import { SELECTABLE_PROVIDER_STATUSES, type FormInitialValues, type FormState, -} from "./resolve-agent-form"; +} from "@/provider-selection/resolve-agent-form"; -export type { FormInitialValues } from "./resolve-agent-form"; +export type { FormInitialValues } from "@/provider-selection/resolve-agent-form"; export interface UseAgentFormStateOptions { initialServerId?: string | null; @@ -67,7 +67,7 @@ export interface UseAgentFormStateResult { modeOptions: AgentMode[]; availableModels: AgentModelDefinition[]; allProviderModels: Map; - modelSelectorProviders: ModelSelectorProvider[]; + modelSelectorProviders: ProviderSelectorProvider[]; isAllModelsLoading: boolean; availableThinkingOptions: NonNullable; isModelLoading: boolean; @@ -243,7 +243,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg [snapshotEntries], ); const snapshotModelSelectorProviders = useMemo( - () => buildSelectableModelSelectorProviders(snapshotEntries), + () => buildSelectableProviderSelectorProviders(snapshotEntries), [snapshotEntries], ); const snapshotSelectedEntry = useMemo( diff --git a/packages/app/src/hooks/use-agent-input-draft-core.ts b/packages/app/src/hooks/use-agent-input-draft-core.ts index 91c16bbab..45d2b37fb 100644 --- a/packages/app/src/hooks/use-agent-input-draft-core.ts +++ b/packages/app/src/hooks/use-agent-input-draft-core.ts @@ -1,8 +1,6 @@ import type { UserComposerAttachment } from "@/attachments/types"; import type { DraftAgentStatusBarProps } from "@/components/agent-status-bar"; -import type { DraftCommandConfig } from "@/hooks/use-agent-commands-query"; import type { UseAgentFormStateResult } from "@/hooks/use-agent-form-state"; -import type { AgentModelDefinition, AgentProvider } from "@server/server/agent/agent-sdk-types"; export interface DraftKeyContext { selectedServerId: string | null; @@ -20,56 +18,6 @@ export function resolveDraftKey(input: { return input.draftKey; } -export function resolveEffectiveComposerModelId(input: { - selectedModel: string; - availableModels: AgentModelDefinition[]; -}): string { - return input.selectedModel.trim(); -} - -export function resolveEffectiveComposerThinkingOptionId(input: { - selectedThinkingOptionId: string; - availableModels: AgentModelDefinition[]; - effectiveModelId: string; -}): string { - const selectedThinkingOptionId = input.selectedThinkingOptionId.trim(); - if (selectedThinkingOptionId) { - return selectedThinkingOptionId; - } - - const selectedModelDefinition = - input.availableModels.find((model) => model.id === input.effectiveModelId) ?? null; - return selectedModelDefinition?.defaultThinkingOptionId ?? ""; -} - -export function buildDraftComposerCommandConfig(input: { - provider: AgentProvider | null; - cwd: string; - modeOptions: DraftAgentStatusBarProps["modeOptions"]; - selectedMode: string; - effectiveModelId: string; - effectiveThinkingOptionId: string; - featureValues?: Record; -}): DraftCommandConfig | undefined { - const cwd = input.cwd.trim(); - if (!input.provider || !cwd) { - return undefined; - } - - return { - provider: input.provider, - cwd, - ...(input.modeOptions.length > 0 && input.selectedMode !== "" - ? { modeId: input.selectedMode } - : {}), - ...(input.effectiveModelId ? { model: input.effectiveModelId } : {}), - ...(input.effectiveThinkingOptionId - ? { thinkingOptionId: input.effectiveThinkingOptionId } - : {}), - ...(input.featureValues ? { featureValues: input.featureValues } : {}), - }; -} - export function buildDraftStatusControls(input: { formState: UseAgentFormStateResult; features?: DraftAgentStatusBarProps["features"]; diff --git a/packages/app/src/hooks/use-agent-input-draft.live.test.tsx b/packages/app/src/hooks/use-agent-input-draft.live.test.tsx index 07cc2aed9..90c30756f 100644 --- a/packages/app/src/hooks/use-agent-input-draft.live.test.tsx +++ b/packages/app/src/hooks/use-agent-input-draft.live.test.tsx @@ -85,16 +85,19 @@ vi.mock("./use-agent-form-state", () => ({ { id: "codex", label: "Codex", - rows: [ - { - favoriteKey: "codex:gpt-5.4", - provider: "codex", - providerLabel: "Codex", - modelId: "gpt-5.4", - modelLabel: "gpt-5.4", - isDefault: true, - }, - ], + modelSelection: { + kind: "models", + rows: [ + { + favoriteKey: "codex:gpt-5.4", + provider: "codex", + providerLabel: "Codex", + modelId: "gpt-5.4", + modelLabel: "gpt-5.4", + isDefault: true, + }, + ], + }, }, ], isAllModelsLoading: false, diff --git a/packages/app/src/hooks/use-agent-input-draft.test.ts b/packages/app/src/hooks/use-agent-input-draft.test.ts index ee9cec7d7..f34c5ff2a 100644 --- a/packages/app/src/hooks/use-agent-input-draft.test.ts +++ b/packages/app/src/hooks/use-agent-input-draft.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "vitest"; +import { resolveDraftKey } from "./use-agent-input-draft-core"; import { - buildDraftComposerCommandConfig, - resolveDraftKey, + buildDraftCommandConfig, resolveEffectiveComposerModelId, resolveEffectiveComposerThinkingOptionId, -} from "./use-agent-input-draft-core"; +} from "@/provider-selection/provider-selection"; describe("resolveDraftKey", () => { it("returns a string draft key unchanged", () => { @@ -30,8 +30,12 @@ describe("resolveEffectiveComposerModelId", () => { it("returns the selected model trimmed", () => { expect( resolveEffectiveComposerModelId({ - selectedModel: " gpt-5.4-mini ", + provider: "codex", + modelId: " gpt-5.4-mini ", + modeId: "", + thinkingOptionId: "", availableModels: [], + modeOptions: [], }), ).toBe("gpt-5.4-mini"); }); @@ -39,8 +43,12 @@ describe("resolveEffectiveComposerModelId", () => { it("returns empty string when no model selected", () => { expect( resolveEffectiveComposerModelId({ - selectedModel: "", + provider: "codex", + modelId: "", + modeId: "", + thinkingOptionId: "", availableModels: [], + modeOptions: [], }), ).toBe(""); }); @@ -63,21 +71,33 @@ describe("resolveEffectiveComposerThinkingOptionId", () => { it("prefers the selected thinking option when present", () => { expect( - resolveEffectiveComposerThinkingOptionId({ - selectedThinkingOptionId: "medium", - availableModels: models, - effectiveModelId: "gpt-5.4", - }), + resolveEffectiveComposerThinkingOptionId( + { + provider: "codex", + modelId: "gpt-5.4", + modeId: "", + thinkingOptionId: "medium", + availableModels: models, + modeOptions: [], + }, + "gpt-5.4", + ), ).toBe("medium"); }); it("falls back to the model default thinking option", () => { expect( - resolveEffectiveComposerThinkingOptionId({ - selectedThinkingOptionId: "", - availableModels: models, - effectiveModelId: "gpt-5.4", - }), + resolveEffectiveComposerThinkingOptionId( + { + provider: "codex", + modelId: "gpt-5.4", + modeId: "", + thinkingOptionId: "", + availableModels: models, + modeOptions: [], + }, + "gpt-5.4", + ), ).toBe("high"); }); }); @@ -85,11 +105,16 @@ describe("resolveEffectiveComposerThinkingOptionId", () => { describe("buildDraftComposerCommandConfig", () => { it("returns undefined when cwd is empty", () => { expect( - buildDraftComposerCommandConfig({ - provider: "codex", + buildDraftCommandConfig({ + selection: { + provider: "codex", + modelId: "gpt-5.4", + modeId: "", + thinkingOptionId: "", + availableModels: [], + modeOptions: [], + }, cwd: " ", - modeOptions: [], - selectedMode: "", effectiveModelId: "gpt-5.4", effectiveThinkingOptionId: "high", }), @@ -98,11 +123,16 @@ describe("buildDraftComposerCommandConfig", () => { it("builds the draft command config from derived composer state", () => { expect( - buildDraftComposerCommandConfig({ - provider: "codex", + buildDraftCommandConfig({ + selection: { + provider: "codex", + modelId: "gpt-5.4", + modeId: "auto", + thinkingOptionId: "high", + availableModels: [], + modeOptions: [{ id: "auto", label: "Auto" }], + }, cwd: "/repo", - modeOptions: [{ id: "auto", label: "Auto" }], - selectedMode: "auto", effectiveModelId: "gpt-5.4", effectiveThinkingOptionId: "high", }), diff --git a/packages/app/src/hooks/use-agent-input-draft.ts b/packages/app/src/hooks/use-agent-input-draft.ts index 8e63f38ac..f8c468ae2 100644 --- a/packages/app/src/hooks/use-agent-input-draft.ts +++ b/packages/app/src/hooks/use-agent-input-draft.ts @@ -10,14 +10,17 @@ import { import { useDraftAgentFeatures } from "@/hooks/use-draft-agent-features"; import { areAttachmentsEqual, - buildDraftComposerCommandConfig, buildDraftStatusControls, hasDraftContent, resolveDraftKey, - resolveEffectiveComposerModelId, - resolveEffectiveComposerThinkingOptionId, type DraftKeyInput, } from "@/hooks/use-agent-input-draft-core"; +import { + buildDraftCommandConfig, + resolveEffectiveComposerModelId, + resolveEffectiveComposerThinkingOptionId, + type ProviderSelectionState, +} from "@/provider-selection/provider-selection"; import { useDraftStore } from "@/stores/draft-store"; type AttachmentUpdater = @@ -199,23 +202,33 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr formState.setWorkingDir(lockedWorkingDir); }, [composerOptions, formState, lockedWorkingDir]); + const providerSelection = useMemo( + () => ({ + provider: formState.selectedProvider, + modelId: formState.selectedModel, + modeId: formState.selectedMode, + thinkingOptionId: formState.selectedThinkingOptionId, + availableModels: formState.availableModels, + modeOptions: formState.modeOptions, + }), + [ + formState.availableModels, + formState.modeOptions, + formState.selectedMode, + formState.selectedModel, + formState.selectedProvider, + formState.selectedThinkingOptionId, + ], + ); + const effectiveModelId = useMemo( - () => - resolveEffectiveComposerModelId({ - selectedModel: formState.selectedModel, - availableModels: formState.availableModels, - }), - [formState.availableModels, formState.selectedModel], + () => resolveEffectiveComposerModelId(providerSelection), + [providerSelection], ); const effectiveThinkingOptionId = useMemo( - () => - resolveEffectiveComposerThinkingOptionId({ - selectedThinkingOptionId: formState.selectedThinkingOptionId, - availableModels: formState.availableModels, - effectiveModelId, - }), - [effectiveModelId, formState.availableModels, formState.selectedThinkingOptionId], + () => resolveEffectiveComposerThinkingOptionId(providerSelection, effectiveModelId), + [effectiveModelId, providerSelection], ); const workingDir = lockedWorkingDir || formState.workingDir; @@ -236,11 +249,9 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr const commandDraftConfig = useMemo( () => composerOptions - ? buildDraftComposerCommandConfig({ - provider: formState.selectedProvider, + ? buildDraftCommandConfig({ + selection: providerSelection, cwd: workingDir, - modeOptions: formState.modeOptions, - selectedMode: formState.selectedMode, effectiveModelId, effectiveThinkingOptionId, featureValues: draftFeatureValues, @@ -251,10 +262,8 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr effectiveModelId, effectiveThinkingOptionId, draftFeatureValues, + providerSelection, workingDir, - formState.modeOptions, - formState.selectedMode, - formState.selectedProvider, ], ); @@ -303,6 +312,7 @@ export const __private__ = { resolveDraftKey, resolveEffectiveComposerModelId, resolveEffectiveComposerThinkingOptionId, - buildDraftComposerCommandConfig, + buildDraftCommandConfig, + buildDraftComposerCommandConfig: buildDraftCommandConfig, buildDraftStatusControls, }; diff --git a/packages/app/src/components/combined-model-selector.test.ts b/packages/app/src/provider-selection/provider-selection.test.ts similarity index 53% rename from packages/app/src/components/combined-model-selector.test.ts rename to packages/app/src/provider-selection/provider-selection.test.ts index 5c892159d..8d79f7d3b 100644 --- a/packages/app/src/components/combined-model-selector.test.ts +++ b/packages/app/src/provider-selection/provider-selection.test.ts @@ -5,12 +5,14 @@ import type { } from "@server/server/agent/agent-sdk-types"; import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest"; import { - buildModelSelectorProviders, - buildSelectableModelSelectorProviders, + buildProviderSelectorProviders, + buildSelectableProviderSelectorProviders, buildSelectedTriggerLabel, filterAndRankModelRows, - matchesSearch, -} from "./combined-model-selector.utils"; + matchesModelSearch, + resolveSelectedModelLabel, + resolveSubmissionReadiness, +} from "./provider-selection"; describe("combined model selector data", () => { const codexModel: AgentModelDefinition = { @@ -37,7 +39,7 @@ describe("combined model selector data", () => { it("builds selector providers from ready enabled snapshot entries", () => { expect( - buildSelectableModelSelectorProviders([ + buildSelectableProviderSelectorProviders([ snapshotEntry({ provider: "codex", label: "Codex", @@ -48,24 +50,27 @@ describe("combined model selector data", () => { { id: "codex", label: "Codex", - rows: [ - { - favoriteKey: "codex:gpt-5.4", - provider: "codex", - providerLabel: "Codex", - modelId: "gpt-5.4", - modelLabel: "GPT-5.4", - description: undefined, - isDefault: undefined, - }, - ], + modelSelection: { + kind: "models", + rows: [ + { + favoriteKey: "codex:gpt-5.4", + provider: "codex", + providerLabel: "Codex", + modelId: "gpt-5.4", + modelLabel: "GPT-5.4", + description: undefined, + isDefault: undefined, + }, + ], + }, }, ]); }); - it("keeps ready enabled providers with no models as model-less providers", () => { + it("represents ready enabled providers without explicit models as provider-default selection", () => { expect( - buildSelectableModelSelectorProviders([ + buildSelectableProviderSelectorProviders([ snapshotEntry({ provider: "deepseek-tui", label: "DeepSeek TUI", @@ -76,14 +81,14 @@ describe("combined model selector data", () => { { id: "deepseek-tui", label: "DeepSeek TUI", - rows: [], + modelSelection: { kind: "providerDefault", label: "Default" }, }, ]); }); it("excludes disabled providers from selector data", () => { expect( - buildSelectableModelSelectorProviders([ + buildSelectableProviderSelectorProviders([ snapshotEntry({ provider: "deepseek-tui", label: "DeepSeek TUI", @@ -96,7 +101,7 @@ describe("combined model selector data", () => { it("excludes providers that are not ready", () => { expect( - buildSelectableModelSelectorProviders([ + buildSelectableProviderSelectorProviders([ snapshotEntry({ provider: "loading-provider", status: "loading", models: [] }), snapshotEntry({ provider: "error-provider", status: "error", models: [] }), snapshotEntry({ provider: "unavailable-provider", status: "unavailable", models: [] }), @@ -116,19 +121,25 @@ describe("combined model selector data", () => { ]; expect( - buildModelSelectorProviders(providerDefinitions, new Map([["codex", [codexModel]]])), + buildProviderSelectorProviders({ + providerDefinitions, + modelsByProvider: new Map([["codex", [codexModel]]]), + }), ).toEqual([ { id: "codex", label: "Codex", - rows: [ - expect.objectContaining({ - provider: "codex", - providerLabel: "Codex", - modelId: "gpt-5.4", - modelLabel: "GPT-5.4", - }), - ], + modelSelection: { + kind: "models", + rows: [ + expect.objectContaining({ + provider: "codex", + providerLabel: "Codex", + modelId: "gpt-5.4", + modelLabel: "GPT-5.4", + }), + ], + }, }, ]); }); @@ -143,10 +154,10 @@ describe("combined model selector data", () => { description: "OpenCode Zen - kimi", }; - expect(matchesSearch(row, "kimi zen")).toBe(true); - expect(matchesSearch(row, "zen kimi")).toBe(true); - expect(matchesSearch(row, "k2.5 zen")).toBe(true); - expect(matchesSearch(row, "kimi gemini")).toBe(false); + expect(matchesModelSearch(row, "kimi zen")).toBe(true); + expect(matchesModelSearch(row, "zen kimi")).toBe(true); + expect(matchesModelSearch(row, "k2.5 zen")).toBe(true); + expect(matchesModelSearch(row, "kimi gemini")).toBe(false); }); it("ranks model search results by fuzzy match quality", () => { @@ -180,4 +191,75 @@ describe("combined model selector data", () => { it("keeps the selected trigger label model-only", () => { expect(buildSelectedTriggerLabel("GPT-5.4")).toBe("GPT-5.4"); }); + + it("resolves selected labels from explicit provider model-selection state", () => { + const providers = buildSelectableProviderSelectorProviders([ + snapshotEntry({ + provider: "codex", + label: "Codex", + models: [codexModel], + }), + snapshotEntry({ + provider: "deepseek-tui", + label: "DeepSeek TUI", + models: [], + }), + ]); + + expect( + resolveSelectedModelLabel({ + providers, + selectedProvider: "codex", + selectedModel: "gpt-5.4", + isLoading: false, + }), + ).toBe("GPT-5.4"); + expect( + resolveSelectedModelLabel({ + providers, + selectedProvider: "deepseek-tui", + selectedModel: "", + isLoading: false, + }), + ).toBe("Default"); + }); + + it("returns observable submission readiness reasons", () => { + expect( + resolveSubmissionReadiness({ + text: "hello", + allowsEmptyAutoSubmit: false, + providerCount: 1, + selection: { + provider: "codex", + modelId: "", + availableModels: [codexModel], + isModelLoading: false, + }, + autoSubmitConfig: null, + workspaceDirectory: "/repo", + hasClient: true, + }), + ).toEqual({ + ok: false, + reason: "No model is available for the selected provider", + }); + + expect( + resolveSubmissionReadiness({ + text: "hello", + allowsEmptyAutoSubmit: false, + providerCount: 1, + selection: { + provider: "deepseek-tui", + modelId: "", + availableModels: [], + isModelLoading: false, + }, + autoSubmitConfig: null, + workspaceDirectory: "/repo", + hasClient: true, + }), + ).toEqual({ ok: true }); + }); }); diff --git a/packages/app/src/provider-selection/provider-selection.ts b/packages/app/src/provider-selection/provider-selection.ts new file mode 100644 index 000000000..871694045 --- /dev/null +++ b/packages/app/src/provider-selection/provider-selection.ts @@ -0,0 +1,284 @@ +import type { + AgentMode, + AgentModelDefinition, + AgentProvider, + ProviderSnapshotEntry, +} from "@server/server/agent/agent-sdk-types"; +import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest"; +import type { DraftCommandConfig } from "@/hooks/use-agent-commands-query"; +import { buildFavoriteModelKey, type FavoriteModelRow } from "@/hooks/use-form-preferences"; +import { compareMatchScores, scoreTextFields } from "@/utils/score-match"; + +export type ProviderSelectionModelRow = FavoriteModelRow & { isDefault?: boolean }; + +export type ProviderModelSelection = + | { kind: "models"; rows: ProviderSelectionModelRow[] } + | { kind: "providerDefault"; label: string } + | { kind: "loading" } + | { kind: "unavailable"; reason: string }; + +export interface ProviderSelectorProvider { + id: string; + label: string; + modelSelection: ProviderModelSelection; +} + +export interface ProviderSelectionState { + provider: AgentProvider | null; + modelId: string; + modeId: string; + thinkingOptionId: string; + availableModels: AgentModelDefinition[]; + modeOptions: AgentMode[]; +} + +export interface ProviderSelectionReadiness { + ok: boolean; + reason?: string; +} + +function buildModelRows( + provider: string, + providerLabel: string, + models: AgentModelDefinition[], +): ProviderSelectionModelRow[] { + return models.map((model) => ({ + favoriteKey: buildFavoriteModelKey({ provider, modelId: model.id }), + provider, + providerLabel, + modelId: model.id, + modelLabel: model.label, + description: model.description, + isDefault: model.isDefault, + })); +} + +function buildModelSelection( + provider: string, + providerLabel: string, + models: AgentModelDefinition[] | null, +): ProviderModelSelection { + if (models === null) { + return { kind: "loading" }; + } + if (models.length === 0) { + return { kind: "providerDefault", label: "Default" }; + } + return { kind: "models", rows: buildModelRows(provider, providerLabel, models) }; +} + +function isSelectableProvider(entry: ProviderSnapshotEntry): boolean { + return entry.enabled && entry.status === "ready"; +} + +export function buildProviderSelectorProviders(input: { + providerDefinitions: AgentProviderDefinition[]; + modelsByProvider: Map; +}): ProviderSelectorProvider[] { + return input.providerDefinitions.map((definition) => ({ + id: definition.id, + label: definition.label, + modelSelection: buildModelSelection( + definition.id, + definition.label, + input.modelsByProvider.has(definition.id) + ? (input.modelsByProvider.get(definition.id) ?? []) + : null, + ), + })); +} + +export function buildSelectableProviderSelectorProviders( + entries: ProviderSnapshotEntry[] | undefined, +): ProviderSelectorProvider[] { + return (entries ?? []).filter(isSelectableProvider).map((entry) => { + const label = entry.label ?? entry.provider; + return { + id: entry.provider, + label, + modelSelection: buildModelSelection(entry.provider, label, entry.models ?? []), + }; + }); +} + +export function getProviderModelRows( + provider: ProviderSelectorProvider, +): ProviderSelectionModelRow[] { + return provider.modelSelection.kind === "models" ? provider.modelSelection.rows : []; +} + +export function getAllProviderModelRows( + providers: ProviderSelectorProvider[], +): ProviderSelectionModelRow[] { + return providers.flatMap(getProviderModelRows); +} + +export function getProviderDefaultLabel(provider: ProviderSelectorProvider): string | null { + return provider.modelSelection.kind === "providerDefault" ? provider.modelSelection.label : null; +} + +export function resolveSelectedModelLabel(input: { + providers: ProviderSelectorProvider[]; + selectedProvider: string; + selectedModel: string; + isLoading: boolean; +}): string { + const selectedProvider = input.selectedProvider.trim(); + if (!selectedProvider) { + return "Select model"; + } + + const provider = input.providers.find((entry) => entry.id === selectedProvider); + if (!input.selectedModel) { + if (provider?.modelSelection.kind === "providerDefault") { + return provider.modelSelection.label; + } + if (provider?.modelSelection.kind === "loading") { + return "Loading..."; + } + return input.isLoading ? "Loading..." : "Select model"; + } + + if (!provider) { + return input.isLoading ? "Loading..." : "Select model"; + } + if (provider.modelSelection.kind !== "models") { + return provider.modelSelection.kind === "providerDefault" + ? provider.modelSelection.label + : "Select model"; + } + + const model = provider.modelSelection.rows.find((entry) => entry.modelId === input.selectedModel); + const defaultModel = provider.modelSelection.rows.find((row) => row.isDefault); + return ( + model?.modelLabel ?? + defaultModel?.modelLabel ?? + provider.modelSelection.rows[0]?.modelLabel ?? + "Select model" + ); +} + +export function buildSelectedTriggerLabel(modelLabel: string): string { + return modelLabel; +} + +export function matchesModelSearch( + row: ProviderSelectionModelRow, + normalizedQuery: string, +): boolean { + return scoreModelRow(row, normalizedQuery) !== null; +} + +function getModelRowSearchFields(row: ProviderSelectionModelRow): string[] { + return [row.modelLabel, row.modelId, row.providerLabel, row.description ?? ""]; +} + +export function scoreModelRow(row: ProviderSelectionModelRow, normalizedQuery: string) { + return scoreTextFields(normalizedQuery, getModelRowSearchFields(row)); +} + +export function filterAndRankModelRows( + rows: ProviderSelectionModelRow[], + normalizedQuery: string, +): ProviderSelectionModelRow[] { + if (!normalizedQuery) return rows; + const scored = rows + .map((row) => ({ row, score: scoreModelRow(row, normalizedQuery) })) + .filter( + ( + entry, + ): entry is { row: ProviderSelectionModelRow; score: NonNullable } => + Boolean(entry.score), + ); + + scored.sort((a, b) => { + const cmp = compareMatchScores(a.score, b.score); + if (cmp !== 0) return cmp; + return a.row.modelLabel.localeCompare(b.row.modelLabel); + }); + + return scored.map((entry) => entry.row); +} + +export function resolveEffectiveComposerModelId(selection: ProviderSelectionState): string { + return selection.modelId.trim(); +} + +export function resolveEffectiveComposerThinkingOptionId( + selection: ProviderSelectionState, + effectiveModelId: string, +): string { + const selectedThinkingOptionId = selection.thinkingOptionId.trim(); + if (selectedThinkingOptionId) { + return selectedThinkingOptionId; + } + + const selectedModelDefinition = + selection.availableModels.find((model) => model.id === effectiveModelId) ?? null; + return selectedModelDefinition?.defaultThinkingOptionId ?? ""; +} + +export function buildDraftCommandConfig(input: { + selection: ProviderSelectionState; + cwd: string; + effectiveModelId: string; + effectiveThinkingOptionId: string; + featureValues?: Record; +}): DraftCommandConfig | undefined { + const cwd = input.cwd.trim(); + if (!input.selection.provider || !cwd) { + return undefined; + } + + return { + provider: input.selection.provider, + cwd, + ...(input.selection.modeOptions.length > 0 && input.selection.modeId !== "" + ? { modeId: input.selection.modeId } + : {}), + ...(input.effectiveModelId ? { model: input.effectiveModelId } : {}), + ...(input.effectiveThinkingOptionId + ? { thinkingOptionId: input.effectiveThinkingOptionId } + : {}), + ...(input.featureValues ? { featureValues: input.featureValues } : {}), + }; +} + +export function resolveSubmissionReadiness(input: { + text: string; + allowsEmptyAutoSubmit: boolean; + providerCount: number; + selection: { + provider: AgentProvider | string | null; + modelId: string; + availableModels: readonly unknown[]; + isModelLoading: boolean; + }; + autoSubmitConfig: { provider: string; model: string | null } | null; + workspaceDirectory: string | null; + hasClient: boolean; +}): ProviderSelectionReadiness { + if (!input.allowsEmptyAutoSubmit && !input.text.trim()) { + return { ok: false, reason: "Initial prompt is required" }; + } + if (input.providerCount === 0) { + return { ok: false, reason: "No available providers on the selected host" }; + } + if (!(input.autoSubmitConfig?.provider ?? input.selection.provider)) { + return { ok: false, reason: "Select a model" }; + } + if (input.selection.isModelLoading) { + return { ok: false, reason: "Model defaults are still loading" }; + } + const hasSelectedModel = Boolean(input.autoSubmitConfig?.model ?? input.selection.modelId); + if (!hasSelectedModel && input.selection.availableModels.length > 0) { + return { ok: false, reason: "No model is available for the selected provider" }; + } + if (!input.workspaceDirectory) { + return { ok: false, reason: "Workspace directory not found" }; + } + if (!input.hasClient) { + return { ok: false, reason: "Host is not connected" }; + } + return { ok: true }; +} diff --git a/packages/app/src/hooks/resolve-agent-form.test.ts b/packages/app/src/provider-selection/resolve-agent-form.test.ts similarity index 100% rename from packages/app/src/hooks/resolve-agent-form.test.ts rename to packages/app/src/provider-selection/resolve-agent-form.test.ts diff --git a/packages/app/src/hooks/resolve-agent-form.ts b/packages/app/src/provider-selection/resolve-agent-form.ts similarity index 99% rename from packages/app/src/hooks/resolve-agent-form.ts rename to packages/app/src/provider-selection/resolve-agent-form.ts index a54f9ae8c..3763370e2 100644 --- a/packages/app/src/hooks/resolve-agent-form.ts +++ b/packages/app/src/provider-selection/resolve-agent-form.ts @@ -8,7 +8,7 @@ import { mergeProviderPreferences, type FormPreferences, type ProviderPreferences, -} from "./use-form-preferences"; +} from "@/hooks/use-form-preferences"; export interface FormInitialValues { serverId?: string | null; diff --git a/packages/app/src/screens/workspace/workspace-draft-agent-tab-core.ts b/packages/app/src/screens/workspace/workspace-draft-agent-tab-core.ts index c9d0d55e2..7e31b03a9 100644 --- a/packages/app/src/screens/workspace/workspace-draft-agent-tab-core.ts +++ b/packages/app/src/screens/workspace/workspace-draft-agent-tab-core.ts @@ -1,3 +1,5 @@ +import { resolveSubmissionReadiness } from "@/provider-selection/provider-selection"; + export interface WorkspaceDraftAutoSubmitConfig { provider: string; model: string | null; @@ -25,27 +27,19 @@ export function validateDraftSubmission(input: { workspaceDirectory, hasClient, } = input; - if (!allowsEmptyAutoSubmit && !text.trim()) { - return "Initial prompt is required"; - } - if (composerState.providerDefinitions.length === 0) { - return "No available providers on the selected host"; - } - if (!(autoSubmitConfig?.provider ?? composerState.selectedProvider)) { - return "Select a model"; - } - if (composerState.isModelLoading) { - return "Model defaults are still loading"; - } - const hasSelectedModel = Boolean(autoSubmitConfig?.model ?? composerState.effectiveModelId); - if (!hasSelectedModel && composerState.availableModels.length > 0) { - return "No model is available for the selected provider"; - } - if (!workspaceDirectory) { - return "Workspace directory not found"; - } - if (!hasClient) { - return "Host is not connected"; - } - return null; + const readiness = resolveSubmissionReadiness({ + text, + allowsEmptyAutoSubmit, + providerCount: composerState.providerDefinitions.length, + selection: { + provider: composerState.selectedProvider, + modelId: composerState.effectiveModelId ?? "", + availableModels: composerState.availableModels, + isModelLoading: composerState.isModelLoading, + }, + autoSubmitConfig, + workspaceDirectory, + hasClient, + }); + return readiness.ok ? null : (readiness.reason ?? null); } diff --git a/packages/website/src/data/agent-pages.ts b/packages/website/src/data/agent-pages.ts index 005bf1ee6..6091a3d10 100644 --- a/packages/website/src/data/agent-pages.ts +++ b/packages/website/src/data/agent-pages.ts @@ -187,6 +187,16 @@ export const AGENT_PAGES = [ metaDescription: "Run the LangChain DeepAgents coding agent from your phone. Launch sessions, monitor progress, and ship code from anywhere.", }, + { + slug: "deepseek-tui", + name: "DeepSeek TUI", + title: "DeepSeek TUI from your phone", + subtitle: + "Run the DeepSeek V4 terminal coding agent on your machine, then drive it from anywhere.", + metaTitle: "DeepSeek TUI Mobile App – Run DeepSeek TUI from anywhere | Paseo", + metaDescription: + "Drive DeepSeek TUI from your phone. Launch coding sessions on your machine, monitor progress, and ship from anywhere.", + }, { slug: "dimcode", name: "DimCode", diff --git a/packages/website/src/routeTree.gen.ts b/packages/website/src/routeTree.gen.ts index 94c697378..21fc1bdfc 100644 --- a/packages/website/src/routeTree.gen.ts +++ b/packages/website/src/routeTree.gen.ts @@ -34,6 +34,7 @@ import { Route as DownloadRouteImport } from "./routes/download"; import { Route as DocsRouteImport } from "./routes/docs"; import { Route as DiracRouteImport } from "./routes/dirac"; import { Route as DimcodeRouteImport } from "./routes/dimcode"; +import { Route as DeepseekTuiRouteImport } from "./routes/deepseek-tui"; import { Route as DeepagentsRouteImport } from "./routes/deepagents"; import { Route as CursorRouteImport } from "./routes/cursor"; import { Route as CrowRouteImport } from "./routes/crow"; @@ -183,6 +184,11 @@ const DimcodeRoute = DimcodeRouteImport.update({ path: "/dimcode", getParentRoute: () => rootRouteImport, } as any); +const DeepseekTuiRoute = DeepseekTuiRouteImport.update({ + id: "/deepseek-tui", + path: "/deepseek-tui", + getParentRoute: () => rootRouteImport, +} as any); const DeepagentsRoute = DeepagentsRouteImport.update({ id: "/deepagents", path: "/deepagents", @@ -319,6 +325,7 @@ export interface FileRoutesByFullPath { "/crow": typeof CrowRoute; "/cursor": typeof CursorRoute; "/deepagents": typeof DeepagentsRoute; + "/deepseek-tui": typeof DeepseekTuiRoute; "/dimcode": typeof DimcodeRoute; "/dirac": typeof DiracRoute; "/docs": typeof DocsRouteWithChildren; @@ -368,6 +375,7 @@ export interface FileRoutesByTo { "/crow": typeof CrowRoute; "/cursor": typeof CursorRoute; "/deepagents": typeof DeepagentsRoute; + "/deepseek-tui": typeof DeepseekTuiRoute; "/dimcode": typeof DimcodeRoute; "/dirac": typeof DiracRoute; "/download": typeof DownloadRoute; @@ -418,6 +426,7 @@ export interface FileRoutesById { "/crow": typeof CrowRoute; "/cursor": typeof CursorRoute; "/deepagents": typeof DeepagentsRoute; + "/deepseek-tui": typeof DeepseekTuiRoute; "/dimcode": typeof DimcodeRoute; "/dirac": typeof DiracRoute; "/docs": typeof DocsRouteWithChildren; @@ -470,6 +479,7 @@ export interface FileRouteTypes { | "/crow" | "/cursor" | "/deepagents" + | "/deepseek-tui" | "/dimcode" | "/dirac" | "/docs" @@ -519,6 +529,7 @@ export interface FileRouteTypes { | "/crow" | "/cursor" | "/deepagents" + | "/deepseek-tui" | "/dimcode" | "/dirac" | "/download" @@ -568,6 +579,7 @@ export interface FileRouteTypes { | "/crow" | "/cursor" | "/deepagents" + | "/deepseek-tui" | "/dimcode" | "/dirac" | "/docs" @@ -619,6 +631,7 @@ export interface RootRouteChildren { CrowRoute: typeof CrowRoute; CursorRoute: typeof CursorRoute; DeepagentsRoute: typeof DeepagentsRoute; + DeepseekTuiRoute: typeof DeepseekTuiRoute; DimcodeRoute: typeof DimcodeRoute; DiracRoute: typeof DiracRoute; DocsRoute: typeof DocsRouteWithChildren; @@ -823,6 +836,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof DimcodeRouteImport; parentRoute: typeof rootRouteImport; }; + "/deepseek-tui": { + id: "/deepseek-tui"; + path: "/deepseek-tui"; + fullPath: "/deepseek-tui"; + preLoaderRoute: typeof DeepseekTuiRouteImport; + parentRoute: typeof rootRouteImport; + }; "/deepagents": { id: "/deepagents"; path: "/deepagents"; @@ -1031,6 +1051,7 @@ const rootRouteChildren: RootRouteChildren = { CrowRoute: CrowRoute, CursorRoute: CursorRoute, DeepagentsRoute: DeepagentsRoute, + DeepseekTuiRoute: DeepseekTuiRoute, DimcodeRoute: DimcodeRoute, DiracRoute: DiracRoute, DocsRoute: DocsRouteWithChildren, diff --git a/packages/website/src/routes/agents.tsx b/packages/website/src/routes/agents.tsx index 976595904..05a4d67c8 100644 --- a/packages/website/src/routes/agents.tsx +++ b/packages/website/src/routes/agents.tsx @@ -10,7 +10,7 @@ export const Route = createFileRoute("/agents")({ head: () => ({ meta: pageMeta( "Supported agents – Every coding agent Paseo runs | Paseo", - "Run Claude Code, Codex, OpenCode, Cursor CLI, Gemini CLI, Hermes Agent, Qwen Code, Kimi Code, and 28 more coding agents from your phone. Self-hosted, your code stays on your machine.", + "Run Claude Code, Codex, OpenCode, Cursor CLI, Gemini CLI, Hermes Agent, Qwen Code, Kimi Code, DeepSeek TUI, and 28 more coding agents from your phone. Self-hosted, your code stays on your machine.", ), }), component: AgentsPage, diff --git a/packages/website/src/routes/deepseek-tui.tsx b/packages/website/src/routes/deepseek-tui.tsx new file mode 100644 index 000000000..b709ec6cf --- /dev/null +++ b/packages/website/src/routes/deepseek-tui.tsx @@ -0,0 +1,4 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { agentRouteOptions } from "~/components/agent-route"; + +export const Route = createFileRoute("/deepseek-tui")(agentRouteOptions("deepseek-tui"));