Files
paseo/packages/app/src/composer/draft/workspace-tab.tsx
T
Michał KędrzyńskiGitHubClaude Opus 4.8greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>Mohamed Boudra
45bbb973a3 Switch models from the Command Center (#2147)
* Switch models from the Command Center

Add a model switcher to the Command-K Command Center. Typing surfaces a
flat, filterable list of "Model › Provider › Name" breadcrumb rows with
provider icons:

- Running agent: its own provider's models (a live agent can't change
  provider); selecting calls setAgentModel.
- New draft tab: every available provider's models in one flat list;
  selecting sets provider + model on the draft via a focused-draft
  controller published to a global store (the draft form state is local
  to the composer subtree and otherwise unreachable from the global
  Command Center).

Models only appear once the user starts typing, so the default palette
view is unchanged. Reuses useProvidersSnapshot and the existing
setAgentModel RPC — no protocol changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update packages/app/src/components/command-center.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update packages/app/src/hooks/use-command-center.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* refactor(app): make Command Center extensible

Let focused features register stable actions while the palette owns search, selection, and a single virtualized result projection.

* fix(app): save model preference after agent switch

Persist the shared model choice only after the daemon confirms the live agent switched successfully.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-18 16:54:41 +02:00

827 lines
27 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef } from "react";
import { Keyboard, ScrollView, Text, View } from "react-native";
import { useTranslation } from "react-i18next";
import ReanimatedAnimated from "react-native-reanimated";
import { StyleSheet } from "react-native-unistyles";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { useContainerWidthBelow } from "@/hooks/use-container-width";
import invariant from "tiny-invariant";
import { Composer } from "@/composer";
import { FileDropZone } from "@/components/file-drop/file-drop-zone";
import { DraftAgentModeControl } from "@/composer/agent-controls/mode-control";
import { ComposerImportPill } from "@/composer/draft/import-pill";
import { AgentStreamView } from "@/agent-stream/view";
import { composerWorkspaceAttachment } from "@/composer/attachments/workspace";
import { useAgentInputDraft } from "@/composer/draft/input-draft";
import type { CreateAgentInitialValues } from "@/hooks/use-agent-form-state";
import { useDraftAgentCreateFlow, type DraftCreateAttempt } from "@/composer/draft/create-flow";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { buildWorkspaceDraftAgentConfig } from "@/screens/workspace/workspace-draft-agent-config";
import { buildDraftStoreKey } from "@/stores/draft-keys";
import { usePanelStore } from "@/stores/panel-store";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import type { Agent } from "@/stores/session-store";
import { useWorkspaceFields } from "@/stores/session-store-hooks";
import { useWorkspaceDraftSubmissionStore } from "@/stores/workspace-draft-submission-store";
import { useCommandCenterActions } from "@/command-center/provider";
import { buildModelChoiceContributions } from "@/command-center/model-contributions";
import { getCommandCenterProviderIcon } from "@/command-center/provider-icon";
import { encodeImages } from "@/utils/encode-images";
import type { WorkspaceFileOpenRequest } from "@/workspace/file-open";
import { shouldAutoFocusWorkspaceDraftComposer } from "@/screens/workspace/workspace-draft-pane-focus";
import {
shouldAllowEmptyDraftText,
validateDraftSubmission,
} from "@/composer/draft/workspace-tab-core";
import type { AgentCapabilityFlags } from "@getpaseo/protocol/agent-types";
import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import type { WorkspaceComposerAttachment } from "@/attachments/types";
import {
useDraftWorkspaceAttachmentScopeKey,
useWorkspaceAttachmentScopeKey,
useWorkspaceAttachmentsStore,
} from "@/attachments/workspace-attachments-store";
import type { UserMessageImageAttachment } from "@/types/stream";
import {
COMPACT_FORM_FACTOR_WIDTH,
MAX_CONTENT_WIDTH,
useIsCompactFormFactor,
} from "@/constants/layout";
import { isWeb } from "@/constants/platform";
import type { WorkspaceDraftTabSetup } from "@/stores/workspace-tabs-store";
const EMPTY_PENDING_PERMISSIONS = new Map();
const EMPTY_ONLINE_SERVER_IDS: string[] = [];
const DRAFT_CAPABILITIES: AgentCapabilityFlags = {
supportsStreaming: true,
supportsSessionPersistence: false,
supportsDynamicModes: false,
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: false,
};
interface AutoSubmitConfig {
provider: string;
modeId: string | null;
model: string | null;
thinkingOptionId: string | null;
featureValues: Record<string, unknown>;
}
function resolveAutoSubmitConfig(
pending: {
provider: string;
modeId?: string | null;
model?: string | null;
thinkingOptionId?: string | null;
featureValues?: Record<string, unknown>;
} | null,
): AutoSubmitConfig | null {
if (!pending) return null;
return {
provider: pending.provider,
modeId: pending.modeId ?? null,
model: pending.model ?? null,
thinkingOptionId: pending.thinkingOptionId ?? null,
featureValues: pending.featureValues ?? {},
};
}
// Reconcile the form's selected mode against the currently discovered modes.
// The mode picker displays modeOptions[0] when the stored mode isn't in the
// list (e.g. a globally-remembered "plan" that this workspace's OpenCode config
// no longer defines), so the submitted mode must match that display — otherwise
// we'd send a stale mode the provider rejects while the UI showed a valid one.
function reconcileSelectedMode(modeOptionIds: readonly string[], selectedMode: string): string {
if (modeOptionIds.length === 0) {
return "";
}
return modeOptionIds.includes(selectedMode) ? selectedMode : (modeOptionIds[0] ?? "");
}
function resolveDraftModeIdOverride(input: {
autoSubmitConfig: AutoSubmitConfig | null;
modeOptionIds: readonly string[];
selectedMode: string;
}): { modeId: string } | Record<string, never> {
const { autoSubmitConfig, modeOptionIds, selectedMode } = input;
if (autoSubmitConfig?.modeId) {
return { modeId: autoSubmitConfig.modeId };
}
const reconciled = reconcileSelectedMode(modeOptionIds, selectedMode);
if (reconciled !== "") {
return { modeId: reconciled };
}
return {};
}
function resolveDraftModeId(input: {
autoSubmitConfig: AutoSubmitConfig | null;
modeOptionIds: readonly string[];
selectedMode: string;
}): string | null {
const { autoSubmitConfig, modeOptionIds, selectedMode } = input;
if (autoSubmitConfig?.modeId !== undefined) {
return autoSubmitConfig.modeId;
}
const reconciled = reconcileSelectedMode(modeOptionIds, selectedMode);
if (reconciled !== "") {
return reconciled;
}
return null;
}
async function submitDraftCreateRequest(input: {
attempt: { clientMessageId: string };
text: string;
images?: UserMessageImageAttachment[];
attachments?: unknown;
cwd: string;
client: DaemonClient | null;
workspaceDirectory: string | null;
workspaceId: string | null;
autoSubmitConfig: AutoSubmitConfig | null;
composerState: {
selectedProvider: string | null;
selectedMode: string;
modeOptions: readonly { id: string }[];
effectiveModelId: string | null;
effectiveThinkingOptionId: string | null;
featureValues: Record<string, unknown> | undefined;
};
hostDisconnectedMessage: string;
selectModelMessage: string;
}): Promise<{ agentId: string | null; result: AgentSnapshotPayload }> {
const {
attempt,
text,
images,
attachments,
cwd,
client,
workspaceDirectory,
workspaceId,
autoSubmitConfig,
composerState,
} = input;
invariant(workspaceDirectory, "Workspace directory is required");
invariant(workspaceId, "Workspace id is required");
if (!client) {
throw new Error(input.hostDisconnectedMessage);
}
const provider = autoSubmitConfig?.provider ?? composerState.selectedProvider;
if (!provider) {
throw new Error(input.selectModelMessage);
}
const modeIdOverride = resolveDraftModeIdOverride({
autoSubmitConfig,
modeOptionIds: composerState.modeOptions.map((mode) => mode.id),
selectedMode: composerState.selectedMode,
});
const config = buildWorkspaceDraftAgentConfig({
provider,
cwd,
...modeIdOverride,
model: autoSubmitConfig?.model ?? (composerState.effectiveModelId || undefined),
thinkingOptionId:
autoSubmitConfig?.thinkingOptionId ?? (composerState.effectiveThinkingOptionId || undefined),
featureValues: autoSubmitConfig?.featureValues ?? composerState.featureValues,
});
const imagesData = await encodeImages(images);
const attachmentsArray = Array.isArray(attachments) ? attachments : undefined;
const result = await client.createAgent({
config,
workspaceId,
...(text ? { initialPrompt: text } : {}),
clientMessageId: attempt.clientMessageId,
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
...(attachmentsArray && attachmentsArray.length > 0 ? { attachments: attachmentsArray } : {}),
});
return {
agentId: result.id,
result,
};
}
function buildDraftAgentSnapshot(input: {
attempt: { timestamp: Date };
serverId: string;
tabId: string;
workspaceDirectory: string | null;
autoSubmitConfig: AutoSubmitConfig | null;
composerState: {
effectiveModelId: string | null;
effectiveThinkingOptionId: string | null;
modeOptions: readonly { id: string }[];
selectedMode: string;
selectedProvider: string | null;
agentControls: { features?: Agent["features"] };
};
selectModelMessage: string;
}): Agent {
const { attempt, serverId, tabId, workspaceDirectory, autoSubmitConfig, composerState } = input;
invariant(workspaceDirectory, "Workspace directory is required");
const now = attempt.timestamp;
const model = autoSubmitConfig?.model ?? (composerState.effectiveModelId || null);
const thinkingOptionId =
autoSubmitConfig?.thinkingOptionId ?? (composerState.effectiveThinkingOptionId || null);
const modeId = resolveDraftModeId({
autoSubmitConfig,
modeOptionIds: composerState.modeOptions.map((mode) => mode.id),
selectedMode: composerState.selectedMode,
});
const provider = autoSubmitConfig?.provider ?? composerState.selectedProvider;
if (!provider) {
throw new Error(input.selectModelMessage);
}
return {
serverId,
id: tabId,
provider,
status: "running",
createdAt: now,
updatedAt: now,
lastUserMessageAt: now,
lastActivityAt: now,
capabilities: DRAFT_CAPABILITIES,
currentModeId: modeId,
availableModes: [],
pendingPermissions: [],
persistence: null,
runtimeInfo: { provider, sessionId: null, model, modeId },
title: "Agent",
cwd: workspaceDirectory,
model,
features: composerState.agentControls.features,
thinkingOptionId,
parentAgentId: null,
labels: {},
};
}
function buildDraftInitialValues(input: {
workingDir: string | null;
initialSetup: WorkspaceDraftTabSetup | null;
}): CreateAgentInitialValues | undefined {
if (!input.workingDir) {
return undefined;
}
if (!input.initialSetup) {
return { workingDir: input.workingDir };
}
return {
workingDir: input.workingDir,
provider: input.initialSetup.provider,
modeId: input.initialSetup.modeId,
model: input.initialSetup.model,
thinkingOptionId: input.initialSetup.thinkingOptionId,
};
}
function resolveDraftWorkingDirectory(input: {
workspaceDirectory: string | null;
initialSetup: WorkspaceDraftTabSetup | null;
}): string | null {
if (input.initialSetup) {
return input.initialSetup.cwd;
}
return input.workspaceDirectory;
}
function resolveOnlineServerIds(input: { isConnected: boolean; serverId: string }): string[] {
if (!input.isConnected) {
return EMPTY_ONLINE_SERVER_IDS;
}
return [input.serverId];
}
interface WorkspaceDraftAgentTabProps {
serverId: string;
workspaceId: string;
tabId: string;
draftId: string;
initialSetup?: WorkspaceDraftTabSetup;
isPaneFocused: boolean;
onCreated: (snapshot: AgentSnapshotPayload) => void;
onOpenWorkspaceFile: (request: WorkspaceFileOpenRequest) => void;
onOpenImportSheet?: () => void;
}
function resolveImportPillPress(
onOpenImportSheet: (() => void) | undefined,
isSubmitting: boolean,
): (() => void) | null {
if (isSubmitting) {
return null;
}
return onOpenImportSheet ?? null;
}
export function WorkspaceDraftAgentTab({
serverId,
workspaceId,
tabId,
draftId,
initialSetup = undefined,
isPaneFocused,
onCreated,
onOpenWorkspaceFile,
onOpenImportSheet,
}: WorkspaceDraftAgentTabProps) {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const workspaceFields = useWorkspaceFields(serverId, workspaceId, (w) => ({
workspaceDirectory: w.workspaceDirectory,
id: w.id,
}));
const workspaceDirectory = workspaceFields?.workspaceDirectory || null;
const draftSetup = initialSetup ?? null;
const draftWorkingDirectory = resolveDraftWorkingDirectory({
workspaceDirectory,
initialSetup: draftSetup,
});
const draftInitialValues = buildDraftInitialValues({
workingDir: draftWorkingDirectory,
initialSetup: draftSetup,
});
const onlineServerIds = resolveOnlineServerIds({ isConnected, serverId });
const draftStoreKey = useMemo(
() =>
buildDraftStoreKey({
serverId,
agentId: tabId,
draftId,
}),
[draftId, serverId, tabId],
);
const draftInput = useAgentInputDraft({
draftKey: draftStoreKey,
composer: {
initialServerId: serverId,
initialValues: draftInitialValues,
initialFeatureValues: draftSetup?.featureValues,
isVisible: true,
onlineServerIds,
lockedWorkingDir: draftWorkingDirectory ?? undefined,
},
});
const composerState = draftInput.composerState;
if (!composerState) {
throw new Error("Workspace draft composer state is required");
}
const draftModelActions = useMemo(
() =>
buildModelChoiceContributions({
serverId,
providers: composerState.modelSelectorProviders,
selectedProvider: composerState.selectedProvider,
selectedModelId: composerState.effectiveModelId || null,
groupLabel: t("shell.commandCenter.modelGroupLabel"),
searchKeywords: t("shell.commandCenter.modelSearchKeywords"),
getIcon: getCommandCenterProviderIcon,
select: composerState.setProviderAndModelFromUser,
}),
[
composerState.effectiveModelId,
composerState.modelSelectorProviders,
composerState.selectedProvider,
composerState.setProviderAndModelFromUser,
serverId,
t,
],
);
const clearDraftInput = draftInput.clear;
const setDraftText = draftInput.setText;
const setDraftAttachments = draftInput.setAttachments;
const pendingAutoSubmit = useWorkspaceDraftSubmissionStore((state) => {
const pending = state.pendingByDraftId[draftId] ?? null;
return pending?.serverId === serverId && pending.workspaceId === workspaceId ? pending : null;
});
const pendingCreateAttempt = useCreateFlowStore((state) => {
const pending = state.pendingByDraftId[draftId] ?? null;
return pending?.serverId === serverId && pending.lifecycle === "active" ? pending : null;
});
const consumePendingAutoSubmit = useWorkspaceDraftSubmissionStore(
(state) => state.consumePending,
);
const autoSubmitConfig = resolveAutoSubmitConfig(pendingAutoSubmit);
const initialCreateAttempt = useMemo<DraftCreateAttempt | null>(() => {
if (!pendingAutoSubmit || !pendingCreateAttempt) {
return null;
}
if (pendingAutoSubmit.clientMessageId !== pendingCreateAttempt.clientMessageId) {
return null;
}
return {
clientMessageId: pendingCreateAttempt.clientMessageId,
text: pendingCreateAttempt.text,
timestamp: new Date(pendingCreateAttempt.timestamp),
...(pendingCreateAttempt.images && pendingCreateAttempt.images.length > 0
? { images: pendingCreateAttempt.images }
: {}),
...(pendingCreateAttempt.attachments && pendingCreateAttempt.attachments.length > 0
? { attachments: pendingCreateAttempt.attachments }
: {}),
};
}, [pendingAutoSubmit, pendingCreateAttempt]);
const allowsEmptyAutoSubmit = pendingAutoSubmit?.allowEmptyText === true;
const isCompactFormFactor = useIsCompactFormFactor();
const { onLayout: onInputAreaLayout, isBelow: isCompactComposerLayout } = useContainerWidthBelow(
COMPACT_FORM_FACTOR_WIDTH,
{ initialIsBelow: isCompactFormFactor },
);
const workspaceAttachmentScopeKey = useWorkspaceAttachmentScopeKey({
serverId,
cwd: composerState.workingDir,
workspaceId,
});
const draftAttachmentScopeKey = useDraftWorkspaceAttachmentScopeKey(draftId);
const attachmentScopeKeys = useMemo(
() => [draftAttachmentScopeKey, workspaceAttachmentScopeKey].filter(Boolean),
[draftAttachmentScopeKey, workspaceAttachmentScopeKey],
);
const clearWorkspaceAttachments = useWorkspaceAttachmentsStore(
(state) => state.clearWorkspaceAttachments,
);
const openFileExplorerForCheckout = usePanelStore((state) => state.openFileExplorerForCheckout);
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
const handleOpenWorkspaceAttachment = useCallback(
(attachment: WorkspaceComposerAttachment) => {
if (attachment.kind !== "review") {
return;
}
const checkout = {
serverId,
cwd: attachment.attachment.cwd,
isGit: true,
};
openFileExplorerForCheckout({
checkout,
isCompact: isCompactFormFactor,
});
setExplorerTabForCheckout({
...checkout,
tab: "changes",
});
},
[isCompactFormFactor, openFileExplorerForCheckout, serverId, setExplorerTabForCheckout],
);
const {
formErrorMessage,
isSubmitting,
optimisticStreamItems,
draftAgent,
handleCreateFromInput,
continueCreateFromAttempt,
} = useDraftAgentCreateFlow<Agent, AgentSnapshotPayload>({
draftId,
getPendingServerId: () => serverId,
initialAttempt: initialCreateAttempt,
allowEmptyText: allowsEmptyAutoSubmit,
validateBeforeSubmit: ({ text, attachments }) => {
const allowsEmptyDraftText = shouldAllowEmptyDraftText({
allowsEmptyAutoSubmit,
attachments,
});
return validateDraftSubmission({
text,
allowsEmptyAutoSubmit: allowsEmptyDraftText,
composerState,
autoSubmitConfig,
workspaceDirectory: draftWorkingDirectory,
hasClient: Boolean(client),
});
},
onBeforeSubmit: async () => {
await composerState.persistFormPreferences();
if (isWeb) {
(document.activeElement as HTMLElement | null)?.blur?.();
}
Keyboard.dismiss();
},
buildDraftAgent: (attempt) =>
buildDraftAgentSnapshot({
attempt,
serverId,
tabId,
workspaceDirectory: draftWorkingDirectory,
autoSubmitConfig,
composerState,
selectModelMessage: t("workspaceSetup.errors.selectModel"),
}),
createRequest: async ({ attempt, text, images, attachments, cwd }) =>
submitDraftCreateRequest({
attempt,
text,
images,
attachments,
cwd,
client,
workspaceDirectory: draftWorkingDirectory,
workspaceId: workspaceFields?.id ?? null,
autoSubmitConfig,
composerState,
hostDisconnectedMessage: t("workspace.terminal.hostDisconnected"),
selectModelMessage: t("workspaceSetup.errors.selectModel"),
}),
onCreateSuccess: ({ result }) => {
clearDraftInput("sent");
clearWorkspaceAttachments({ scopeKey: draftAttachmentScopeKey });
useWorkspaceDraftSubmissionStore.getState().clearDraftSetup({ draftId });
onCreated(result);
},
});
useCommandCenterActions({
sourceId: `draft:${serverId}:${tabId}`,
enabled: isPaneFocused && !isSubmitting,
actions: draftModelActions,
});
const isReadyForPendingAutoSubmit = Boolean(
pendingAutoSubmit &&
draftInput.isHydrated &&
draftWorkingDirectory &&
client &&
!composerState.isModelLoading,
);
const autoSubmitKeyRef = useRef<string | null>(null);
useEffect(() => {
if (!isReadyForPendingAutoSubmit) {
return;
}
const submitKey = `${serverId}:${workspaceId}:${draftId}`;
if (autoSubmitKeyRef.current === submitKey) {
return;
}
const submission = consumePendingAutoSubmit({ serverId, workspaceId, draftId });
if (!submission) {
return;
}
autoSubmitKeyRef.current = submitKey;
setDraftText("");
setDraftAttachments([]);
const preparedAttempt =
initialCreateAttempt?.clientMessageId === submission.clientMessageId
? initialCreateAttempt
: null;
const createPromise = preparedAttempt
? continueCreateFromAttempt({
attempt: preparedAttempt,
cwd: submission.cwd,
})
: handleCreateFromInput({
text: submission.text,
attachments: submission.attachments,
cwd: submission.cwd,
});
void createPromise.catch(() => {
setDraftText(submission.text);
setDraftAttachments(composerWorkspaceAttachment.userAttachmentsOnly(submission.attachments));
autoSubmitKeyRef.current = null;
});
}, [
continueCreateFromAttempt,
consumePendingAutoSubmit,
draftId,
handleCreateFromInput,
initialCreateAttempt,
isReadyForPendingAutoSubmit,
serverId,
setDraftAttachments,
setDraftText,
workspaceId,
]);
const focusInputRef = useRef<(() => void) | null>(null);
const handleFocusInputCallback = useCallback((focus: () => void) => {
focusInputRef.current = focus;
}, []);
const handleProviderSelectWithFocus = useCallback(
(provider: Parameters<typeof composerState.setProviderFromUser>[0]) => {
composerState.setProviderFromUser(provider);
focusInputRef.current?.();
},
[composerState],
);
const handleModeSelectWithFocus = useCallback(
(modeId: string) => {
composerState.setModeFromUser(modeId);
focusInputRef.current?.();
},
[composerState],
);
const handleModelSelectWithFocus = useCallback(
(modelId: string) => {
composerState.setModelFromUser(modelId);
focusInputRef.current?.();
},
[composerState],
);
const handleProviderAndModelSelectWithFocus = useCallback(
(
provider: Parameters<typeof composerState.setProviderAndModelFromUser>[0],
modelId: string,
) => {
composerState.setProviderAndModelFromUser(provider, modelId);
focusInputRef.current?.();
},
[composerState],
);
const handleThinkingOptionSelectWithFocus = useCallback(
(optionId: string) => {
composerState.setThinkingOptionFromUser(optionId);
focusInputRef.current?.();
},
[composerState],
);
const handleSetFeatureWithFocus = useCallback(
(featureId: string, value: unknown) => {
composerState.agentControls.onSetFeature?.(featureId, value);
focusInputRef.current?.();
},
[composerState],
);
const { style: composerKeyboardStyle } = useKeyboardShiftStyle({
mode: "translate",
});
const inputAreaWrapperStyle = useMemo(
() => [styles.inputAreaWrapper, { paddingBottom: insets.bottom }, composerKeyboardStyle],
[insets.bottom, composerKeyboardStyle],
);
const handleDropdownCloseFocus = useCallback(() => {
focusInputRef.current?.();
}, []);
const importPillPress = resolveImportPillPress(onOpenImportSheet, isSubmitting);
const composerAgentControls = useMemo(
() => ({
...composerState.agentControls,
onSelectProvider: handleProviderSelectWithFocus,
onSelectMode: handleModeSelectWithFocus,
onSelectModel: handleModelSelectWithFocus,
onSelectProviderAndModel: handleProviderAndModelSelectWithFocus,
onSelectThinkingOption: handleThinkingOptionSelectWithFocus,
onSetFeature: handleSetFeatureWithFocus,
onDropdownClose: handleDropdownCloseFocus,
disabled: isSubmitting,
}),
[
composerState.agentControls,
handleProviderSelectWithFocus,
handleModeSelectWithFocus,
handleModelSelectWithFocus,
handleProviderAndModelSelectWithFocus,
handleThinkingOptionSelectWithFocus,
handleSetFeatureWithFocus,
handleDropdownCloseFocus,
isSubmitting,
],
);
const composerFooter = useMemo(
() =>
isCompactComposerLayout ? (
<DraftAgentModeControl
placement="footer"
{...composerAgentControls}
isCompactLayout={isCompactComposerLayout}
/>
) : undefined,
[isCompactComposerLayout, composerAgentControls],
);
return (
<FileDropZone style={styles.container}>
<View style={styles.contentContainer}>
{isSubmitting && draftAgent ? (
<View style={styles.streamContainer}>
<AgentStreamView
agentId={tabId}
serverId={serverId}
context={draftAgent}
streamItems={optimisticStreamItems}
pendingPermissions={EMPTY_PENDING_PERMISSIONS}
onOpenWorkspaceFile={onOpenWorkspaceFile}
/>
</View>
) : (
<ScrollView style={styles.scrollView} contentContainerStyle={styles.configScrollContent}>
<View style={styles.configSection}>
{formErrorMessage ? (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{formErrorMessage}</Text>
</View>
) : null}
</View>
</ScrollView>
)}
</View>
<ReanimatedAnimated.View style={inputAreaWrapperStyle} onLayout={onInputAreaLayout}>
{importPillPress ? (
<View style={styles.importPillRow}>
<View style={styles.importPillContent}>
<ComposerImportPill onPress={importPillPress} />
</View>
</View>
) : null}
<Composer
agentId={tabId}
serverId={serverId}
externalKeyboardShift
isPaneFocused={isPaneFocused}
onSubmitMessage={handleCreateFromInput}
isSubmitLoading={isSubmitting}
blurOnSubmit={true}
value={draftInput.text}
onChangeText={draftInput.setText}
attachments={draftInput.attachments}
attachmentScopeKeys={attachmentScopeKeys}
onOpenWorkspaceAttachment={handleOpenWorkspaceAttachment}
onChangeAttachments={draftInput.setAttachments}
cwd={composerState.workingDir}
clearDraft={draftInput.clear}
autoFocus={shouldAutoFocusWorkspaceDraftComposer({ isPaneFocused, isSubmitting })}
onFocusInput={handleFocusInputCallback}
commandDraftConfig={composerState.commandDraftConfig}
agentControls={composerAgentControls}
footer={composerFooter}
isCompactLayout={isCompactComposerLayout}
/>
</ReanimatedAnimated.View>
</FileDropZone>
);
}
const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,
width: "100%",
backgroundColor: theme.colors.surface0,
},
contentContainer: {
flex: 1,
},
streamContainer: {
flex: 1,
},
scrollView: {
flex: 1,
},
configScrollContent: {
paddingHorizontal: theme.spacing[4],
paddingTop: theme.spacing[4],
paddingBottom: theme.spacing[6],
},
configSection: {
gap: theme.spacing[3],
},
inputAreaWrapper: {
width: "100%",
backgroundColor: theme.colors.surface0,
},
importPillRow: {
width: "100%",
paddingHorizontal: theme.spacing[4],
paddingTop: theme.spacing[3],
paddingBottom: theme.spacing[3],
alignItems: "center",
},
importPillContent: {
width: "100%",
maxWidth: MAX_CONTENT_WIDTH,
flexDirection: "row",
},
errorContainer: {
marginTop: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
borderRadius: theme.borderRadius.md,
backgroundColor: theme.colors.surface2,
borderWidth: 1,
borderColor: theme.colors.destructive,
},
errorText: {
color: theme.colors.destructive,
},
}));