import { getContext, setContext } from 'svelte' import type { AIMode, AIAutonomyMode } from './AIChatManager.svelte' import { getAiChatManager } from './aiChatManagerContext' import type { DisplayMessage, Tool } from './shared' import type { ContextElement } from './context' import type { AttachedImage } from './imageUtils' import type { AttachedTextFile } from './textFileUtils' import type { PasteAttachment } from './pasteTokens' import type { AttachedFilesStore } from './files/attachedFiles.svelte' import type { SessionArtifactsStore } from './artifacts/artifactsState.svelte' import type { ArtifactVersionTarget } from '$lib/components/sessions/previewRouter' import type { FlowAIChatHelpers } from './flow/core' import type { AppAIChatHelpers } from './app/core' import type AIChatInput from './AIChatInput.svelte' export type ChatSendRequestOptions = { instructions?: string pastes?: PasteAttachment[] images?: AttachedImage[] files?: AttachedTextFile[] /** Selected-context snapshot for this turn, in place of the live selection. Set * whenever a send settles its context ahead of the turn. A host with no context * of its own ignores it. */ contextOverride?: ContextElement[] /** Where `contextOverride` came from. 'pinned': chips picked for THIS message, so * they are consumed from the live selection on send. 'replay': an edit or retry * resending an older message's context, already consumed long ago. */ contextOverrideOrigin?: 'pinned' | 'replay' } /** * What the chat view components (AIChatDisplay and everything it renders) need * from whatever is driving the conversation. AIChatManager implements it for the * copilot's own LLM loop; FlowChatViewHost implements it over a flow run's * conversation so both chats render through the same components. * * Each affordance is gated by the field that answers for it — attachments by * `supportsMessageAttachments`, the model button by `supportsModelSettings`, and so on — * so a host turns on exactly what it can serve, and a new one is a matter of answering * these fields rather than of being a copilot. `mode` is the exception, still read * directly for chrome that only the copilot has. */ export interface ChatViewHost { // Transcript displayMessages: DisplayMessage[] /** API-level messages. Only the count is read (context usage visibility). */ messages: readonly unknown[] contextTokens: number /** The workspace a message's paths and jobs resolve against, which a fork session * pins away from the navigated one. */ readonly operatingWorkspace: string | undefined loading: boolean /** A turn this tab can neither follow nor stop, held by another tab on the same chat. */ readonly runHeldElsewhere: boolean loadingLabel: string | undefined compacting: boolean currentReply: string currentReasoning: string currentReasoningActive: boolean readonly reasoningHiddenIndicatorLabel: string | undefined readonly automaticScroll: boolean enableAutomaticScroll: () => void disableAutomaticScroll: () => void // Composer instructions: string readonly sendInFlight: boolean /** Resolves to whether the draft was consumed as a turn. */ sendRequest: (options?: ChatSendRequestOptions) => Promise cancel: (reason?: string) => void setAiChatInput: (aiChatInput: AIChatInput | null) => void readonly queuedMessage: string queuedContext: ContextElement[] | undefined readonly queuedImages: AttachedImage[] readonly queuedFiles: AttachedTextFile[] queueMessage: ( text: string, images?: AttachedImage[], context?: ContextElement[], files?: AttachedTextFile[] ) => void dequeueMessage: () => void setComposerStaged: (key: string, editingIndex: number | null, bytes: number) => void clearComposerStaged: (key: string) => void attachmentBytesExcluding: (selfKey: string) => number // Per-message actions storedImages: (displayMessageIndex: number) => AttachedImage[] | undefined retryRequest: (messageIndex: number) => void restartGeneration: ( displayMessageIndex: number, newContent?: string, pastes?: PasteAttachment[], images?: AttachedImage[], editedContext?: ContextElement[], files?: AttachedTextFile[] ) => void | Promise handleUserQuestionAnswer: (toolId: string, choices: string[]) => boolean handleToolConfirmation: (toolId: string, confirmed: boolean) => void /** A tool is waiting on a run form the user is filling in. Escape belongs to that form * then, not to the turn — see AIChatDisplay's window handler. */ readonly hasPendingRunForm: boolean isRunFormPending: (toolCallId: string) => boolean // Copilot-only surfaces. Left undefined/false by hosts that have no LLM loop // of their own; the chrome they drive hides itself. mode?: AIMode isSessionChat: boolean /** Model + reasoning picker. Off where the model is configured elsewhere. */ supportsModelSettings: boolean /** Click a user message to edit and resend it. Needs a host that can rewind * its own transcript, which a host replaying a server-side run cannot. */ supportsMessageEditing: boolean /** The `+` menu's file entry and drag-and-drop onto the panel. */ supportsMessageAttachments: boolean /** The turn needs text: attachments alone cannot be sent. True where the consumer * requires a message of its own — an AI agent step refuses a run with neither a * `user_message` nor manual memory. */ requiresMessageText: boolean /** The `+` menu's folder entries, backed by `attachedFiles`. A linked folder is a * live handle on the user's disk, so only a host reading files in the browser has one. */ supportsLinkedFolders: boolean /** `accept` for the file picker. */ attachmentAccept: string tools: Tool[] autonomyMode: AIAutonomyMode setAutonomyMode: (mode: AIAutonomyMode) => void readonly autoAcceptEditsActive: boolean readonly autoAcceptEditsAvailable: boolean readonly autoAcceptToolConfirmationsAvailable: boolean readonly planModeAvailable: boolean attachedFiles: AttachedFilesStore artifacts: SessionArtifactsStore openArtifact?: (artifactId: string, name: string, version?: ArtifactVersionTarget) => void flowAiChatHelpers?: FlowAIChatHelpers appAiChatHelpers?: AppAIChatHelpers } const CHAT_VIEW_HOST_CONTEXT_KEY = 'chatViewHost' export function setChatViewHost(host: ChatViewHost) { setContext(CHAT_VIEW_HOST_CONTEXT_KEY, host) } /** * Resolve the host driving the chat in this subtree. Falls back to the * AIChatManager (scoped instance or app-wide singleton) so every existing * copilot chat keeps working without setting anything. */ export function getChatViewHost(): ChatViewHost { return getContext(CHAT_VIEW_HOST_CONTEXT_KEY) ?? getAiChatManager() }