Add DeepSeek TUI to the ACP catalog (#1096)

* Consolidate provider selection handling

* List DeepSeek TUI in docs and website
This commit is contained in:
Mohamed Boudra
2026-05-19 22:20:09 +08:00
committed by GitHub
parent 5dd6b030f2
commit da0e94fd18
19 changed files with 629 additions and 339 deletions

View File

@@ -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`:

View File

@@ -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

View File

@@ -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<string>;
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<AgentModelDefinition["thinkingOptions"]>;
@@ -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],
);

View File

@@ -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<string>,
): 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<string>;
@@ -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 (
<Pressable onPress={handlePress} style={drillDownRowStyle}>
<ProvIcon size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
<Text style={styles.drillDownText}>{providerLabel}</Text>
<View style={styles.drillDownTrailing}>
<Text style={styles.drillDownCount}>
{hasNoModels ? "Default" : `${rowCount} ${rowCount === 1 ? "model" : "models"}`}
{defaultLabel ?? `${rowCount} ${rowCount === 1 ? "model" : "models"}`}
</Text>
{hasNoModels ? null : (
{defaultLabel ? null : (
<ChevronRight size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
)}
</View>
@@ -317,22 +314,23 @@ function GroupedProviderRows({
onDrillDown,
onSelectDefault,
}: {
providers: ModelSelectorProvider[];
providers: ProviderSelectorProvider[];
onDrillDown: (providerId: string, providerLabel: string) => void;
onSelectDefault: (providerId: string) => void;
}) {
return (
<View>
{providers.map((provider, index) => {
const hasNoModels = provider.rows.length === 0;
const rows = getProviderModelRows(provider);
const defaultLabel = getProviderDefaultLabel(provider);
return (
<View key={provider.id}>
{index > 0 ? <View style={styles.separator} /> : null}
<GroupProviderButton
providerId={provider.id}
providerLabel={provider.label}
rowCount={provider.rows.length}
hasNoModels={hasNoModels}
rowCount={rows.length}
defaultLabel={defaultLabel}
onDrillDown={onDrillDown}
onSelectDefault={onSelectDefault}
/>
@@ -381,7 +379,7 @@ function ProviderModelRows({
onToggleFavorite,
normalizedQuery,
}: {
rows: SelectorModelRow[];
rows: ProviderSelectionModelRow[];
selectedProvider: string;
selectedModel: string;
favoriteKeys: Set<string>;
@@ -396,7 +394,7 @@ function ProviderModelRows({
[favoriteKeys, normalizedQuery, rows],
);
const renderItem = useCallback(
({ item }: { item: SelectorModelRow }) => (
({ item }: { item: ProviderSelectionModelRow }) => (
<SelectableModelRow
row={item}
isSelected={item.provider === selectedProvider && item.modelId === selectedModel}
@@ -407,7 +405,7 @@ function ProviderModelRows({
),
[favoriteKeys, onSelect, onToggleFavorite, selectedModel, selectedProvider],
);
const keyExtractor = useCallback((row: SelectorModelRow) => 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 (
<DefaultProviderRow
providerId={view.providerId}
@@ -607,30 +602,20 @@ export function CombinedModelSelector({
const ProviderIcon = hasSelectedProvider ? getProviderIcon(selectedProvider) : null;
const selectedModelLabel = useMemo(() => {
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]);

View File

@@ -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<string, AgentModelDefinition[]>,
): 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<typeof entry.score> } =>
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);
}

View File

@@ -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<string, AgentModelDefinition[]>;
modelSelectorProviders: ModelSelectorProvider[];
modelSelectorProviders: ProviderSelectorProvider[];
isAllModelsLoading: boolean;
availableThinkingOptions: NonNullable<AgentModelDefinition["thinkingOptions"]>;
isModelLoading: boolean;
@@ -243,7 +243,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
[snapshotEntries],
);
const snapshotModelSelectorProviders = useMemo(
() => buildSelectableModelSelectorProviders(snapshotEntries),
() => buildSelectableProviderSelectorProviders(snapshotEntries),
[snapshotEntries],
);
const snapshotSelectedEntry = useMemo(

View File

@@ -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<string, unknown>;
}): 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"];

View File

@@ -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,

View File

@@ -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",
}),

View File

@@ -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<ProviderSelectionState>(
() => ({
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,
};

View File

@@ -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 });
});
});

View File

@@ -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<string, AgentModelDefinition[]>;
}): 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<typeof entry.score> } =>
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<string, unknown>;
}): 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 };
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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",

View File

@@ -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,

View File

@@ -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,

View File

@@ -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"));