From b3da5f9af15c98696b4019759dbfbcca58e4e95b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Tue, 9 Jun 2026 16:42:05 +0100 Subject: [PATCH] feat(web): add session handoff via agent tab context menu (#1317) --- .../task/handoff-profile-menu-items.test.ts | 105 ++++ .../task/handoff-profile-menu-items.tsx | 146 ++++++ .../web/components/task/handoff-types.test.ts | 19 + apps/web/components/task/handoff-types.ts | 18 + .../task/mobile/mobile-sessions-section.tsx | 35 +- .../components/task/new-session-dialog.tsx | 488 +++++++++--------- .../task/new-session-form-actions.test.ts | 413 +++++++++++++++ .../task/new-session-form-actions.ts | 153 ++++++ .../task/new-session-form-prompt.tsx | 111 ++++ apps/web/components/task/session-tab.tsx | 39 ++ apps/web/e2e/pages/session-page.ts | 48 +- .../e2e/tests/session/mobile-handoff.spec.ts | 66 +++ .../e2e/tests/session/session-handoff.spec.ts | 107 ++++ .../session/use-task-executor-profile.test.ts | 157 ++++++ .../session/use-task-executor-profile.ts | 53 ++ apps/web/hooks/use-summarize-session.ts | 3 +- 16 files changed, 1720 insertions(+), 241 deletions(-) create mode 100644 apps/web/components/task/handoff-profile-menu-items.test.ts create mode 100644 apps/web/components/task/handoff-profile-menu-items.tsx create mode 100644 apps/web/components/task/handoff-types.test.ts create mode 100644 apps/web/components/task/handoff-types.ts create mode 100644 apps/web/components/task/new-session-form-actions.test.ts create mode 100644 apps/web/components/task/new-session-form-actions.ts create mode 100644 apps/web/components/task/new-session-form-prompt.tsx create mode 100644 apps/web/e2e/tests/session/mobile-handoff.spec.ts create mode 100644 apps/web/e2e/tests/session/session-handoff.spec.ts create mode 100644 apps/web/hooks/domains/session/use-task-executor-profile.test.ts create mode 100644 apps/web/hooks/domains/session/use-task-executor-profile.ts diff --git a/apps/web/components/task/handoff-profile-menu-items.test.ts b/apps/web/components/task/handoff-profile-menu-items.test.ts new file mode 100644 index 0000000000..c747a5ebcb --- /dev/null +++ b/apps/web/components/task/handoff-profile-menu-items.test.ts @@ -0,0 +1,105 @@ +import { renderHook } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { AgentProfileOption } from "@/lib/state/slices"; +import type { ExecutorProfile } from "@/lib/types/http"; + +const PROFILE_A: AgentProfileOption = { + id: "profile-a", + label: "Mock Agent \u2022 Fast", + agent_name: "mock", + agent_id: "agent-1", + cli_passthrough: false, +}; + +const PROFILE_B: AgentProfileOption = { + id: "profile-b", + label: "Mock Agent \u2022 Slow", + agent_name: "mock", + agent_id: "agent-1", + cli_passthrough: false, +}; + +let mockProfiles: AgentProfileOption[] = [PROFILE_A, PROFILE_B]; +let mockExecutorProfile: ExecutorProfile | null = null; +let mockAuthLoaded = true; +let mockAuthSpecs: Record = {}; +const mockUseTaskExecutorProfile = vi.fn( + (_taskId: string, _enabled?: boolean) => mockExecutorProfile, +); + +vi.mock("@/components/state-provider", () => ({ + useAppStore: (selector: (state: Record) => unknown) => + selector({ + agentProfiles: { items: mockProfiles }, + }), +})); + +vi.mock("@/hooks/domains/session/use-task-executor-profile", () => ({ + useTaskExecutorProfile: (taskId: string, enabled?: boolean) => + mockUseTaskExecutorProfile(taskId, enabled), +})); + +vi.mock("@/hooks/domains/settings/use-remote-auth-specs", () => ({ + useRemoteAuthSpecs: () => ({ specs: mockAuthSpecs, loaded: mockAuthLoaded }), +})); + +vi.mock("@/lib/agent-executor-compat", () => ({ + isAgentConfiguredOnExecutor: ( + profile: AgentProfileOption, + _executor: ExecutorProfile, + _specs: Record, + ) => profile.id === "profile-a", +})); + +import { useHandoffProfiles } from "./handoff-profile-menu-items"; + +describe("useHandoffProfiles", () => { + afterEach(() => { + mockProfiles = [PROFILE_A, PROFILE_B]; + mockExecutorProfile = null; + mockAuthLoaded = true; + mockAuthSpecs = {}; + mockUseTaskExecutorProfile.mockClear(); + }); + + it("returns all agent profiles with display labels", () => { + const { result } = renderHook(() => useHandoffProfiles("task-1")); + expect(result.current).toHaveLength(2); + expect(result.current[0]).toMatchObject({ + id: "profile-a", + label: "Fast", + agentName: "mock", + }); + expect(result.current[1]).toMatchObject({ + id: "profile-b", + label: "Slow", + }); + }); + + it("marks incompatible profiles disabled when executor profile is known", () => { + mockExecutorProfile = { + id: "exec-profile-1", + name: "Default", + executor_id: "executor-1", + executor_type: "local_pc", + prepare_script: "", + cleanup_script: "", + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + }; + const { result } = renderHook(() => useHandoffProfiles("task-1")); + expect(result.current.find((p) => p.id === "profile-a")?.disabled).toBe(false); + expect(result.current.find((p) => p.id === "profile-b")?.disabled).toBe(true); + }); + + it("returns empty list when no profiles configured", () => { + mockProfiles = []; + const { result } = renderHook(() => useHandoffProfiles("task-1")); + expect(result.current).toEqual([]); + }); + + it("passes the enabled flag to executor profile lookup", () => { + renderHook(() => useHandoffProfiles("task-1", false)); + expect(mockUseTaskExecutorProfile).toHaveBeenCalledWith("task-1", false); + }); +}); diff --git a/apps/web/components/task/handoff-profile-menu-items.tsx b/apps/web/components/task/handoff-profile-menu-items.tsx new file mode 100644 index 0000000000..91861f8b96 --- /dev/null +++ b/apps/web/components/task/handoff-profile-menu-items.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { AgentLogo } from "@/components/agent-logo"; +import { + ContextMenuItem, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, +} from "@kandev/ui/context-menu"; +import { + DropdownMenuItem, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, +} from "@kandev/ui/dropdown-menu"; +import { useAppStore } from "@/components/state-provider"; +import { useRemoteAuthSpecs } from "@/hooks/domains/settings/use-remote-auth-specs"; +import { useTaskExecutorProfile } from "@/hooks/domains/session/use-task-executor-profile"; +import { isAgentConfiguredOnExecutor } from "@/lib/agent-executor-compat"; +import type { AgentProfileOption } from "@/lib/state/slices"; + +export type HandoffProfile = { + id: string; + label: string; + agentName?: string; + disabled: boolean; +}; + +function profileDisplayLabel(profile: AgentProfileOption): { label: string; agentName: string } { + const parts = profile.label.split(" \u2022 "); + const agentLabel = + parts.length > 1 ? parts.slice(1).join(" \u2022 ") : (parts[0] ?? profile.label); + return { + label: agentLabel, + agentName: profile.agent_name, + }; +} + +export function useHandoffProfiles(taskId: string, enabled = true): HandoffProfile[] { + const agentProfiles = useAppStore((s) => s.agentProfiles.items); + const executorProfile = useTaskExecutorProfile(taskId, enabled); + const { specs: authSpecs, loaded: authLoaded } = useRemoteAuthSpecs(); + + return useMemo(() => { + return agentProfiles.map((profile) => { + const { label, agentName } = profileDisplayLabel(profile); + let disabled = false; + if (executorProfile && authLoaded) { + disabled = !isAgentConfiguredOnExecutor(profile, executorProfile, authSpecs); + } + return { id: profile.id, label, agentName, disabled }; + }); + }, [agentProfiles, executorProfile, authSpecs, authLoaded]); +} + +function HandoffProfileList({ + profiles, + onSelectProfile, + Item, +}: { + profiles: HandoffProfile[]; + onSelectProfile: (profileId: string) => void; + Item: typeof ContextMenuItem | typeof DropdownMenuItem; +}) { + if (profiles.length === 0) { + return ( + + No agent profiles configured + + ); + } + return profiles.map((profile) => ( + onSelectProfile(profile.id)} + > + + {profile.agentName && ( + + )} + {profile.label} + + + )); +} + +type HandoffMenuProps = { + taskId: string; + disabled?: boolean; + onSelectProfile: (profileId: string) => void; +}; + +export function HandoffContextMenuSub({ taskId, disabled, onSelectProfile }: HandoffMenuProps) { + const [submenuOpen, setSubmenuOpen] = useState(false); + const profiles = useHandoffProfiles(taskId, submenuOpen); + const submenuDisabled = disabled || (profiles.length > 0 && profiles.every((p) => p.disabled)); + + return ( + + + Handoff + + + + + + ); +} + +export function HandoffDropdownMenuSub({ taskId, disabled, onSelectProfile }: HandoffMenuProps) { + const [submenuOpen, setSubmenuOpen] = useState(false); + const profiles = useHandoffProfiles(taskId, submenuOpen); + const submenuDisabled = disabled || (profiles.length > 0 && profiles.every((p) => p.disabled)); + + return ( + + + Handoff + + + + + + ); +} diff --git a/apps/web/components/task/handoff-types.test.ts b/apps/web/components/task/handoff-types.test.ts new file mode 100644 index 0000000000..69eb59e4b5 --- /dev/null +++ b/apps/web/components/task/handoff-types.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { buildHandoffInitialState, summarizeContextValue } from "./handoff-types"; + +describe("handoff-types", () => { + it("buildHandoffInitialState selects target profile and summarize context", () => { + const result = buildHandoffInitialState({ + sourceSessionId: "session-a", + targetProfileId: "profile-b", + }); + expect(result).toEqual({ + selectedProfileId: "profile-b", + contextValue: "summarize:session-a", + }); + }); + + it("summarizeContextValue prefixes session id", () => { + expect(summarizeContextValue("session-123")).toBe("summarize:session-123"); + }); +}); diff --git a/apps/web/components/task/handoff-types.ts b/apps/web/components/task/handoff-types.ts new file mode 100644 index 0000000000..8336d82ed6 --- /dev/null +++ b/apps/web/components/task/handoff-types.ts @@ -0,0 +1,18 @@ +export type HandoffPreset = { + sourceSessionId: string; + targetProfileId: string; +}; + +export function buildHandoffInitialState(handoff: HandoffPreset): { + selectedProfileId: string; + contextValue: string; +} { + return { + selectedProfileId: handoff.targetProfileId, + contextValue: summarizeContextValue(handoff.sourceSessionId), + }; +} + +export function summarizeContextValue(sessionId: string): string { + return `summarize:${sessionId}`; +} diff --git a/apps/web/components/task/mobile/mobile-sessions-section.tsx b/apps/web/components/task/mobile/mobile-sessions-section.tsx index 439ea77ae9..232547a634 100644 --- a/apps/web/components/task/mobile/mobile-sessions-section.tsx +++ b/apps/web/components/task/mobile/mobile-sessions-section.tsx @@ -29,7 +29,8 @@ import { isSessionDeletable, isSessionResumable, } from "@/hooks/domains/session/use-session-actions"; -import { NewSessionDialog } from "../new-session-dialog"; +import { HandoffDropdownMenuSub } from "../handoff-profile-menu-items"; +import { NewSessionDialog, type HandoffPreset } from "../new-session-dialog"; import { MobilePillButton } from "./mobile-pill-button"; import { MobilePickerSheet } from "./mobile-picker-sheet"; import { formatTaskSessionStateLabel } from "@/lib/ui/state-labels"; @@ -87,20 +88,28 @@ function StateBadge({ state }: { state: TaskSessionState | null }) { } function SessionActionsMenu({ + taskId, state, isPrimary, onSetPrimary, onStop, onResume, onAskDelete, + onHandoffProfile, }: { + taskId: string; state: TaskSessionState | null; isPrimary: boolean; onSetPrimary: () => void; onStop: () => void; onResume: () => void; onAskDelete: () => void; + onHandoffProfile: (profileId: string) => void; }) { + const hasLifecycleAction = + !!state && + (isSessionStoppable(state) || isSessionResumable(state) || isSessionDeletable(state)); + return ( @@ -141,6 +150,8 @@ function SessionActionsMenu({ Delete )} + {hasLifecycleAction && } + ); @@ -209,9 +220,18 @@ function SessionRowItem({ onSelect: (sessionId: string) => void; }) { const [confirmDelete, setConfirmDelete] = useState(false); + const [handoffOpen, setHandoffOpen] = useState(false); + const [handoffPreset, setHandoffPreset] = useState(null); const actions = useSessionActions({ sessionId: row.id, taskId }); const isOnly = totalSessions === 1; const showBadges = totalSessions > 1; + const handleHandoffProfile = useCallback( + (profileId: string) => { + setHandoffPreset({ sourceSessionId: row.id, targetProfileId: profileId }); + setHandoffOpen(true); + }, + [row.id], + ); return ( <> @@ -242,14 +262,27 @@ function SessionRowItem({ {row.agentLabel} void actions.setPrimary()} onStop={() => void actions.stop()} onResume={() => void actions.resume()} onAskDelete={() => setConfirmDelete(true)} + onHandoffProfile={handleHandoffProfile} /> + {handoffPreset && ( + { + setHandoffOpen(open); + if (!open) setHandoffPreset(null); + }} + taskId={taskId} + handoff={handoffPreset} + /> + )} void; taskId: string; groupId?: string; + handoff?: HandoffPreset; }; -function useTaskExecutorProfile(taskId: string, open: boolean): ExecutorProfile | null { - const executors = useAppStore((state) => state.executors.items); - const [profile, setProfile] = useState(null); - - useEffect(() => { - if (!open) return; - let active = true; - void fetchTaskEnvironment(taskId) - .then((env) => { - if (!active || !env) return; - for (const executor of executors) { - const match = (executor.profiles ?? []).find((p) => p.id === env.executor_profile_id); - if (match) { - setProfile({ - ...match, - executor_type: match.executor_type ?? executor.type, - executor_name: match.executor_name ?? executor.name, - }); - return; - } - } - }) - .catch(() => {}); - return () => { - active = false; - }; - }, [open, taskId, executors]); - - return profile; +function agentProfileDisplayLabel(profile: AgentProfileOption): string { + const parts = profile.label.split(" \u2022 "); + return parts.length > 1 ? parts.slice(1).join(" \u2022 ") : (parts[0] ?? profile.label); } function useNewSessionDialogState(taskId: string) { @@ -151,8 +124,7 @@ function useSessionOptions(taskId: string) { ); return sorted.map((s, idx) => { const profile = agentProfiles.find((p: { id: string }) => p.id === s.agent_profile_id); - const parts = profile?.label.split(" \u2022 "); - const name = parts?.[1] || parts?.[0] || "Agent"; + const name = profile ? agentProfileDisplayLabel(profile) : "Agent"; return { id: s.id, label: name, index: idx + 1, agentName: profile?.agent_name }; }); }, [sessions, agentProfiles]); @@ -181,6 +153,51 @@ function useCompatibleAgentProfiles( }, [agentProfiles, executorProfile, authSpecs, authLoaded]); } +function useHandoffAutoSummarize( + handoff: HandoffPreset | undefined, + contextValue: string, + onContextChange: (value: string) => void, +) { + const started = useRef(false); + useEffect(() => { + if (!handoff || started.current) return; + started.current = true; + void onContextChange(contextValue); + }, [handoff, contextValue, onContextChange]); +} + +function useSessionPromptEnhancer( + promptRef: RefObject, + setHasPrompt: (value: boolean) => void, +) { + const { enhancePrompt, isEnhancingPrompt } = useUtilityAgentGenerator({ sessionId: null }); + + const handleEnhancePrompt = useCallback(() => { + const current = promptRef.current?.value?.trim(); + if (!current) return; + + enhancePrompt(current, (enhanced) => { + if (promptRef.current) { + promptRef.current.value = enhanced; + setHasPrompt(true); + } + }); + }, [enhancePrompt, promptRef, setHasPrompt]); + + return { handleEnhancePrompt, isEnhancingPrompt }; +} + +function shouldDisableSubmit( + isCreating: boolean, + isSummarizing: boolean, + hasPrompt: boolean, + hasProfiles: boolean, +) { + const isBusy = Number(isCreating) + Number(isSummarizing); + const submitPenalty = Number(hasPrompt === false) + Number(hasProfiles === false) + isBusy; + return submitPenalty > 0; +} + function useEnforceCompatibleProfile( hasExecutorProfile: boolean, compatible: AgentProfileOption[], @@ -194,6 +211,93 @@ function useEnforceCompatibleProfile( }, [hasExecutorProfile, compatible, selectedId, setSelected]); } +function useSessionProfileSelection({ + agentProfiles, + executorProfile, + defaultProfileId, + selectedProfileId, + setSelectedProfileId, +}: { + agentProfiles: AgentProfileOption[]; + executorProfile: ExecutorProfile | null; + defaultProfileId: string; + selectedProfileId: string; + setSelectedProfileId: (value: string) => void; +}) { + const compatibleAgentProfiles = useCompatibleAgentProfiles(agentProfiles, executorProfile); + useEnforceCompatibleProfile( + Boolean(executorProfile), + compatibleAgentProfiles, + selectedProfileId, + setSelectedProfileId, + ); + const profileOptions = useAgentProfileOptions(compatibleAgentProfiles); + const hasProfiles = profileOptions.length > 0; + const noCompatibleProfiles = isMissingCompatibleProfile( + executorProfile, + agentProfiles.length, + hasProfiles, + ); + const showAgentSelector = + hasProfiles && + (profileOptions.length > 1 || + (!!defaultProfileId && !profileOptions.find((o) => o.value === defaultProfileId))); + + return { + profileOptions, + hasProfiles, + noCompatibleProfiles, + showAgentSelector, + }; +} + +function SessionFormHeader({ + executorLabel, + worktreeBranch, + noCompatibleProfiles, + hasProfiles, + executorProfileName, + showAgentSelector, + profileOptions, + selectedProfileId, + isCreating, + onProfileChange, +}: { + executorLabel: string | null; + worktreeBranch: string | null; + noCompatibleProfiles: boolean; + hasProfiles: boolean; + executorProfileName: string | null; + showAgentSelector: boolean; + profileOptions: ReturnType; + selectedProfileId: string; + isCreating: boolean; + onProfileChange: (value: string) => void; +}) { + return ( + <> + + + {showAgentSelector && ( +
+ + +
+ )} + + ); +} + // eslint-disable-next-line max-lines-per-function function NewSessionForm({ taskId, @@ -206,6 +310,7 @@ function NewSessionForm({ initialPrompt, agentProfiles, groupId, + handoff, onClose, }: { taskId: string; @@ -218,16 +323,22 @@ function NewSessionForm({ initialPrompt: string | null; agentProfiles: AgentProfileOption[]; groupId?: string; + handoff?: HandoffPreset; onClose: () => void; }) { + const handoffInitial = handoff ? buildHandoffInitialState(handoff) : null; const { toast } = useToast(); const setActiveSession = useAppStore((state) => state.setActiveSession); const { summarize, isSummarizing } = useSummarizeSession(); - const [isCreating, setIsCreating] = useState(false); - const [contextValue, setContextValue] = useState("blank"); - const [selectedProfileId, setSelectedProfileId] = useState(initialProfileId ?? defaultProfileId); + const [contextValue, setContextValue] = useState(handoffInitial?.contextValue ?? "blank"); + const [selectedProfileId, setSelectedProfileId] = useState( + handoffInitial?.selectedProfileId ?? initialProfileId ?? defaultProfileId, + ); const [hasPrompt, setHasPrompt] = useState(false); + const [isCreating, setIsCreating] = useState(false); const promptRef = useRef(null); + const busySignal = Number(isCreating) + Number(isSummarizing); + const isBusyState = busySignal > 0; const { attachments, isDragging, @@ -239,146 +350,71 @@ function NewSessionForm({ handleDrop, handleAttachClick, handleFileInputChange, - } = useDialogAttachments(isCreating || isSummarizing); + } = useDialogAttachments(isBusyState); const contextItems = useMemo( () => toContextItems(attachments, handleRemoveAttachment), [attachments, handleRemoveAttachment], ); - const compatibleAgentProfiles = useCompatibleAgentProfiles(agentProfiles, executorProfile); - useEnforceCompatibleProfile( - Boolean(executorProfile), - compatibleAgentProfiles, - selectedProfileId, - setSelectedProfileId, - ); - const profileOptions = useAgentProfileOptions(compatibleAgentProfiles); const sessionOptions = useSessionOptions(taskId); const isUtilityConfigured = useIsUtilityConfigured(); - const { enhancePrompt, isEnhancingPrompt } = useUtilityAgentGenerator({ sessionId: null }); - const handleEnhancePrompt = useCallback(() => { - const current = promptRef.current?.value?.trim(); - if (!current) return; - enhancePrompt(current, (enhanced) => { - if (promptRef.current) { - promptRef.current.value = enhanced; - setHasPrompt(true); - } - }); - }, [enhancePrompt]); - const hasProfiles = profileOptions.length > 0; - const noCompatibleProfiles = isMissingCompatibleProfile( + const profileSelection = useSessionProfileSelection({ + agentProfiles, executorProfile, - agentProfiles.length, - hasProfiles, + defaultProfileId, + selectedProfileId, + setSelectedProfileId, + }); + const { handleEnhancePrompt, isEnhancingPrompt } = useSessionPromptEnhancer( + promptRef, + setHasPrompt, ); - const showAgentSelector = - hasProfiles && - (profileOptions.length > 1 || - (!!defaultProfileId && !profileOptions.find((o) => o.value === defaultProfileId))); - const isBusy = isCreating || isSummarizing; + const handleContextChange = useSessionContextChange({ + promptRef, + initialPrompt, + summarize, + toast, + setContextValue, + setHasPrompt, + }); + useHandoffAutoSummarize(handoff, handoffInitial?.contextValue ?? "blank", handleContextChange); - const handleContextChange = useCallback( - async (value: string) => { - if (!value) return; - setContextValue(value); - if (value === "copy_prompt" && initialPrompt && promptRef.current) { - promptRef.current.value = initialPrompt; - setHasPrompt(true); - } else if (value === "blank" && promptRef.current) { - promptRef.current.value = ""; - setHasPrompt(false); - } else if (value.startsWith("summarize:")) { - const sessionId = value.slice("summarize:".length); - const result = await summarize(sessionId); - if (result && promptRef.current) { - promptRef.current.value = result; - setHasPrompt(true); - } else { - setContextValue("blank"); - toast({ - title: "Summarize failed", - description: - "Could not generate a summary. Check that the summarize utility agent is configured and enabled in settings.", - variant: "error", - }); - } - } - }, - [initialPrompt, summarize, toast], - ); - - const handleSubmit = useCallback( - async (e: React.FormEvent) => { - e.preventDefault(); - const typed = promptRef.current?.value?.trim() ?? ""; - const prompt = - contextValue === "copy_prompt" && !typed && initialPrompt ? initialPrompt : typed; - if (!prompt) return; - setIsCreating(true); - try { - const { request } = buildStartRequest(taskId, selectedProfileId, { - executorId, - prompt, - attachments: toMessageAttachments(attachments), - }); - const response = await launchSession(request); - if (!response.session_id) { - throw new Error("Session created but no session ID returned"); - } - const profile = agentProfiles.find((p: AgentProfileOption) => p.id === selectedProfileId); - activateNewSession( - response.session_id, - taskId, - profile?.label ?? "Agent", - groupId, - setActiveSession, - ); - onClose(); - } catch (error) { - toast({ - title: "Failed to create session", - description: error instanceof Error ? error.message : "Unknown error", - variant: "error", - }); - } finally { - setIsCreating(false); - } - }, - [ - taskId, - selectedProfileId, - executorId, - contextValue, - initialPrompt, - agentProfiles, - groupId, - onClose, - toast, - setActiveSession, - attachments, - ], + const handleSubmit = useSessionLaunchSubmit({ + promptRef, + taskId, + selectedProfileId, + executorId, + contextValue, + initialPrompt, + agentProfiles, + groupId, + attachments, + onClose, + toast, + setActiveSession, + activateSession: activateNewSession, + setIsCreating, + }); + const isSubmitDisabled = shouldDisableSubmit( + isCreating, + isSummarizing, + hasPrompt, + profileSelection.hasProfiles, ); return (
- - - {showAgentSelector && ( -
- - -
- )} -
setHasPrompt(!!promptRef.current?.value?.trim())} + onPaste={handlePaste} + onSubmit={handleSubmit} + onAttachClick={handleAttachClick} + onEnhancePrompt={handleEnhancePrompt} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} - > -
- -