* Extract client SDK package * Polish SDK client identity defaults * Build client before dependent CI jobs * Restore daemon client server export * Extract protocol and client SDK packages * Fix provider override schema validation * Fix app test daemon client imports * Simplify workspace build targets * Fix CLI test server build bootstrap * Run SDK package tests in CI * Fix rebase package split drift * Restore lockfile registry metadata * Update SDK config test for prompt default * Move terminal stream router test to client package * Fix rebase drift for protocol imports * Fix SDK agent capability fixture * Restore legacy server client exports * Fix server export compatibility test * Advertise custom mode icon client capability * Remove server daemon-client exports * Format rebased mode control import * Fix rebase drift for protocol imports Files added by upstream PRs (#893, #1147, #1154) referenced the pre-split shared/ paths that this branch moves into @getpaseo/protocol. Redirect those imports to the protocol package so typecheck stays green after the rebase.
747 lines
24 KiB
TypeScript
747 lines
24 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
import { Keyboard, ScrollView, Text, View } from "react-native";
|
|
import { StyleSheet } from "react-native-unistyles";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import invariant from "tiny-invariant";
|
|
import { Composer } from "@/composer";
|
|
import { DraftAgentModeControl } from "@/composer/agent-controls/mode-control";
|
|
import { ComposerImportPill } from "@/composer/draft/import-pill";
|
|
import { FileDropZone } from "@/components/file-drop-zone";
|
|
import { AgentStreamView } from "@/agent-stream/view";
|
|
import { composerWorkspaceAttachment } from "@/composer/attachments/workspace";
|
|
import type { ImageAttachment } from "@/composer/types";
|
|
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 { useWorkspaceExecutionAuthority } from "@/stores/session-store-hooks";
|
|
import { useWorkspaceDraftSubmissionStore } from "@/stores/workspace-draft-submission-store";
|
|
import { encodeImages } from "@/utils/encode-images";
|
|
import type { WorkspaceFileOpenRequest } from "@/workspace/file-open";
|
|
import { shouldAutoFocusWorkspaceDraftComposer } from "@/screens/workspace/workspace-draft-pane-focus";
|
|
import { 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 {
|
|
useWorkspaceAttachments,
|
|
useWorkspaceAttachmentScopeKey,
|
|
} from "@/attachments/workspace-attachments-store";
|
|
import type { UserMessageImageAttachment } from "@/types/stream";
|
|
import { 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 ?? {},
|
|
};
|
|
}
|
|
|
|
function resolveDraftModeIdOverride(input: {
|
|
autoSubmitConfig: AutoSubmitConfig | null;
|
|
modeOptionsCount: number;
|
|
selectedMode: string;
|
|
}): { modeId: string } | Record<string, never> {
|
|
const { autoSubmitConfig, modeOptionsCount, selectedMode } = input;
|
|
if (autoSubmitConfig?.modeId) {
|
|
return { modeId: autoSubmitConfig.modeId };
|
|
}
|
|
if (modeOptionsCount > 0 && selectedMode !== "") {
|
|
return { modeId: selectedMode };
|
|
}
|
|
return {};
|
|
}
|
|
|
|
function resolveDraftModeId(input: {
|
|
autoSubmitConfig: AutoSubmitConfig | null;
|
|
modeOptionsCount: number;
|
|
selectedMode: string;
|
|
}): string | null {
|
|
const { autoSubmitConfig, modeOptionsCount, selectedMode } = input;
|
|
if (autoSubmitConfig?.modeId !== undefined) {
|
|
return autoSubmitConfig.modeId;
|
|
}
|
|
if (modeOptionsCount > 0 && selectedMode !== "") {
|
|
return selectedMode;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function submitDraftCreateRequest(input: {
|
|
attempt: { clientMessageId: string };
|
|
text: string;
|
|
images?: UserMessageImageAttachment[];
|
|
attachments?: unknown;
|
|
client: DaemonClient | null;
|
|
workspaceDirectory: string | null;
|
|
workspaceExecutionAuthority: { workspaceId: string } | null;
|
|
autoSubmitConfig: AutoSubmitConfig | null;
|
|
composerState: {
|
|
selectedProvider: string | null;
|
|
selectedMode: string;
|
|
modeOptions: unknown[];
|
|
effectiveModelId: string | null;
|
|
effectiveThinkingOptionId: string | null;
|
|
featureValues: Record<string, unknown> | undefined;
|
|
};
|
|
}): Promise<{ agentId: string | null; result: AgentSnapshotPayload }> {
|
|
const {
|
|
attempt,
|
|
text,
|
|
images,
|
|
attachments,
|
|
client,
|
|
workspaceDirectory,
|
|
workspaceExecutionAuthority,
|
|
autoSubmitConfig,
|
|
composerState,
|
|
} = input;
|
|
|
|
invariant(workspaceDirectory, "Workspace directory is required");
|
|
invariant(workspaceExecutionAuthority, "Workspace authority is required");
|
|
if (!client) {
|
|
throw new Error("Host is not connected");
|
|
}
|
|
|
|
const provider = autoSubmitConfig?.provider ?? composerState.selectedProvider;
|
|
if (!provider) {
|
|
throw new Error("Select a model");
|
|
}
|
|
const modeIdOverride = resolveDraftModeIdOverride({
|
|
autoSubmitConfig,
|
|
modeOptionsCount: composerState.modeOptions.length,
|
|
selectedMode: composerState.selectedMode,
|
|
});
|
|
const config = buildWorkspaceDraftAgentConfig({
|
|
provider,
|
|
cwd: workspaceDirectory,
|
|
...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: workspaceExecutionAuthority.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: unknown[];
|
|
selectedMode: string;
|
|
selectedProvider: string | null;
|
|
agentControls: { features?: Agent["features"] };
|
|
};
|
|
}): 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,
|
|
modeOptionsCount: composerState.modeOptions.length,
|
|
selectedMode: composerState.selectedMode,
|
|
});
|
|
const provider = autoSubmitConfig?.provider ?? composerState.selectedProvider;
|
|
if (!provider) {
|
|
throw new Error("Select a model");
|
|
}
|
|
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 insets = useSafeAreaInsets();
|
|
const client = useHostRuntimeClient(serverId);
|
|
const isConnected = useHostRuntimeIsConnected(serverId);
|
|
const workspaceAuthority = useWorkspaceExecutionAuthority(serverId, workspaceId);
|
|
const workspaceExecutionAuthority = workspaceAuthority?.ok ? workspaceAuthority.authority : null;
|
|
const workspaceDirectory = workspaceExecutionAuthority?.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 addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
|
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 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 isCompact = useIsCompactFormFactor();
|
|
const workspaceAttachmentScopeKey = useWorkspaceAttachmentScopeKey({
|
|
serverId,
|
|
cwd: composerState.workingDir,
|
|
workspaceId,
|
|
});
|
|
const workspaceAttachments = useWorkspaceAttachments(workspaceAttachmentScopeKey);
|
|
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,
|
|
});
|
|
setExplorerTabForCheckout({
|
|
...checkout,
|
|
tab: "changes",
|
|
});
|
|
},
|
|
[isCompact, openFileExplorerForCheckout, serverId, setExplorerTabForCheckout],
|
|
);
|
|
|
|
const {
|
|
formErrorMessage,
|
|
isSubmitting,
|
|
optimisticStreamItems,
|
|
draftAgent,
|
|
handleCreateFromInput,
|
|
continueCreateFromAttempt,
|
|
} = useDraftAgentCreateFlow<Agent, AgentSnapshotPayload>({
|
|
draftId,
|
|
getPendingServerId: () => serverId,
|
|
initialAttempt: initialCreateAttempt,
|
|
allowEmptyText: allowsEmptyAutoSubmit,
|
|
validateBeforeSubmit: ({ text }) =>
|
|
validateDraftSubmission({
|
|
text,
|
|
allowsEmptyAutoSubmit,
|
|
composerState,
|
|
autoSubmitConfig,
|
|
workspaceDirectory: draftWorkingDirectory,
|
|
hasClient: Boolean(client),
|
|
}),
|
|
onBeforeSubmit: () => {
|
|
void composerState.persistFormPreferences();
|
|
if (isWeb) {
|
|
(document.activeElement as HTMLElement | null)?.blur?.();
|
|
}
|
|
Keyboard.dismiss();
|
|
},
|
|
buildDraftAgent: (attempt) =>
|
|
buildDraftAgentSnapshot({
|
|
attempt,
|
|
serverId,
|
|
tabId,
|
|
workspaceDirectory: draftWorkingDirectory,
|
|
autoSubmitConfig,
|
|
composerState,
|
|
}),
|
|
createRequest: async ({ attempt, text, images, attachments }) =>
|
|
submitDraftCreateRequest({
|
|
attempt,
|
|
text,
|
|
images,
|
|
attachments,
|
|
client,
|
|
workspaceDirectory: draftWorkingDirectory,
|
|
workspaceExecutionAuthority,
|
|
autoSubmitConfig,
|
|
composerState,
|
|
}),
|
|
onCreateSuccess: ({ result }) => {
|
|
clearDraftInput("sent");
|
|
onCreated(result);
|
|
},
|
|
});
|
|
|
|
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 handleFilesDropped = useCallback((files: ImageAttachment[]) => {
|
|
addImagesRef.current?.(files);
|
|
}, []);
|
|
|
|
const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => {
|
|
addImagesRef.current = addImages;
|
|
}, []);
|
|
|
|
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 inputAreaWrapperStyle = useMemo(
|
|
() => [styles.inputAreaWrapper, { paddingBottom: insets.bottom }],
|
|
[insets.bottom],
|
|
);
|
|
|
|
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(
|
|
() =>
|
|
isCompact ? (
|
|
<DraftAgentModeControl placement="footer" {...composerAgentControls} />
|
|
) : undefined,
|
|
[isCompact, composerAgentControls],
|
|
);
|
|
|
|
return (
|
|
<FileDropZone onFilesDropped={handleFilesDropped}>
|
|
<View style={styles.container}>
|
|
<View style={styles.contentContainer}>
|
|
{isSubmitting && draftAgent ? (
|
|
<View style={styles.streamContainer}>
|
|
<AgentStreamView
|
|
agentId={tabId}
|
|
serverId={serverId}
|
|
agent={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>
|
|
|
|
<View style={inputAreaWrapperStyle}>
|
|
{importPillPress ? (
|
|
<View style={styles.importPillRow}>
|
|
<View style={styles.importPillContent}>
|
|
<ComposerImportPill onPress={importPillPress} />
|
|
</View>
|
|
</View>
|
|
) : null}
|
|
<Composer
|
|
agentId={tabId}
|
|
serverId={serverId}
|
|
isPaneFocused={isPaneFocused}
|
|
onSubmitMessage={handleCreateFromInput}
|
|
isSubmitLoading={isSubmitting}
|
|
blurOnSubmit={true}
|
|
value={draftInput.text}
|
|
onChangeText={draftInput.setText}
|
|
attachments={draftInput.attachments}
|
|
workspaceAttachments={workspaceAttachments}
|
|
onOpenWorkspaceAttachment={handleOpenWorkspaceAttachment}
|
|
onChangeAttachments={draftInput.setAttachments}
|
|
cwd={composerState.workingDir}
|
|
clearDraft={draftInput.clear}
|
|
autoFocus={shouldAutoFocusWorkspaceDraftComposer({ isPaneFocused, isSubmitting })}
|
|
onAddImages={handleAddImagesCallback}
|
|
onFocusInput={handleFocusInputCallback}
|
|
commandDraftConfig={composerState.commandDraftConfig}
|
|
agentControls={composerAgentControls}
|
|
footer={composerFooter}
|
|
/>
|
|
</View>
|
|
</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,
|
|
},
|
|
}));
|