import { type ComposerDraft, type DraftTimelineAnnotation, type EffectiveControlResumeOption, type EffectiveSessionControl, type LatencyMode, type ReasoningEffort, type ResourceRef, type SendMessageInput, type SessionEvent } from "@opengeni/sdk"; import { type EmbeddedSessionClientOverride } from "../session-context.js"; import { type SessionEventFeedOptions } from "./internal.js"; import { type OpenGeniText } from "../messages/runtime.js"; export type ComposerPolicy = { model: string; reasoningEffort: ReasoningEffort; latencyMode: LatencyMode; }; export type ComposerSendExtras = Omit; /** Unsent local content handed off from a new-conversation composer. */ export type InitialComposerDraft = { text: string; resources?: ResourceRef[] | undefined; annotations?: DraftTimelineAnnotation[] | undefined; policy?: ComposerPolicy | undefined; }; /** * What an ordinary Send does while the agent is working: `queue` runs it after * the current turn, `steer` has the agent read it as soon as it can, usually * within seconds. */ export type ComposerBusySend = "queue" | "steer"; export type UseComposerOptions = EmbeddedSessionClientOverride & SessionEventFeedOptions & { /** Called synchronously after local submission. Observer errors never change delivery. */ onSubmitted?: ((text: string, input: SendMessageInput) => void) | undefined; /** Called with the accepted wire input. Observer errors never turn acceptance into failure. */ onSent?: ((text: string, input: SendMessageInput) => void) | undefined; /** Called with the exact wire input after a delivery failure. */ onDeliveryError?: ((error: Error, input: SendMessageInput, delivery: "send" | "steer") => void) | undefined; /** * Non-policy message fields merged into every send. Durable policy belongs * to this composer draft; attachments and credentials may remain host-owned. */ sendExtras?: ComposerSendExtras | (() => ComposerSendExtras) | undefined; /** * Fail-closed delivery guard evaluated at send time. Attachment hosts use * this to preserve unresolved upload cards until the operator waits, * retries, or removes them; direct hook callers cannot bypass the UI gate. */ sendBlocked?: (() => boolean) | undefined; /** Latest server-derived workstream control; bound into Send/Steer OCC. */ effectiveControl?: EffectiveSessionControl | null | undefined; /** Immediate user-visible destination for an ordinary Send before admission replies. */ sendDestination?: (() => "chat" | "queue") | undefined; /** * What an ordinary Send does while the agent is working (while * `sendDestination` reports `queue`). `queue` (default) runs the message * after the current turn. `steer` delivers it as a Steer instead: the * agent reads it as soon as it can, usually within seconds (a reply still * being written stops; running tools are not cut off). `sendToQueue` * and `steer` stay explicit either way. * * Under `steer` the person has one Send and nothing to learn: no Queue * choice beside it, no Queue or Steer words (`singleSend`). Set * `offerQueue` to also offer Queue beside Send. */ busySend?: ComposerBusySend | undefined; /** * Under `busySend: "steer"`, also offer Queue beside Send while the agent * works (and on Cmd/Ctrl+Enter), for people who line up follow-ups. Off * by default; under `busySend: "queue"` Steer is always offered. */ offerQueue?: boolean | undefined; /** Disable remote composer-draft reads and writes for embedded hosts. */ draftPersistence?: "durable" | "disabled" | undefined; /** Required explicit authority when durable draft persistence is disabled. */ initialPolicy?: ComposerPolicy | undefined; /** * Seed local unsent content once when this session is mounted/selected. * The real server draft still supplies the revision used for persistence. * A retained uncertain submission takes precedence over this handoff. * An empty seed does not erase an existing server draft. */ initialDraft?: InitialComposerDraft | undefined; }; export type ComposerOptimisticMessage = { clientEventId: string; delivery: "send" | "steer"; destination: "chat" | "queue"; text: string; annotations: DraftTimelineAnnotation[]; resources: ResourceRef[]; occurredAt: string; state: "sending" | "queued" | "failed"; turnId?: string | null | undefined; triggerEventId?: string | null | undefined; appliedQueueVersion?: number | null | undefined; error?: string | undefined; /** Bounded support reference retained independently of display copy across remounts. */ correlationId?: string | undefined; /** False for a definitive refusal that requires changing payment or allowance settings. */ retryable?: boolean | undefined; outcomeUnknown?: boolean | undefined; }; export type ComposerState = { value: string; setValue: (value: string) => void; annotations?: DraftTimelineAnnotation[] | undefined; addAnnotation?: ((annotation: DraftTimelineAnnotation) => void) | undefined; updateAnnotation?: ((id: string, note: string) => void) | undefined; removeAnnotation?: ((id: string) => void) | undefined; /** Newly captured annotation the review surface should focus. */ annotationReviewTargetId?: string | null | undefined; clearAnnotationReviewTarget?: (() => void) | undefined; /** Focus an annotation, or the first incomplete note when omitted. */ requestAnnotationReview?: ((id?: string) => void) | undefined; /** Read the current draft synchronously before a destructive replacement. */ hasDraftContent: () => boolean; /** * The ordinary Send. While the agent is working it follows `busySend`: * queue behind the current turn (default) or Steer. */ send: (text?: string) => Promise; /** Append the draft behind prompts already visible in the queue, whatever `busySend` says. */ sendToQueue?: ((text?: string) => Promise) | undefined; /** What an ordinary Send does while the agent is working. */ busySend?: ComposerBusySend | undefined; /** * One Send and nothing to choose (`busySend: "steer"` without * `offerQueue`): no alternate beside Send, Cmd/Ctrl+Enter sends normally, * and stock surfaces avoid Queue and Steer words. */ singleSend?: boolean | undefined; /** * The other way to send right now, or `null` when Send is not taking its * busy path (nothing running or waiting, or the workstream is paused): * `steer` when Send queues, `queue` when Send steers. Hosts offer it next to * Send exactly when it is not `null`. */ alternateSend?: ComposerBusySend | null | undefined; /** Send the draft the other way: Steer when Send queues, queue when Send steers. */ sendAlternate?: ((text?: string) => Promise) | undefined; /** Locally acknowledged ordinary sends awaiting durable timeline reconciliation. */ optimisticMessages?: ComposerOptimisticMessage[] | undefined; retryOptimisticMessage?: ((clientEventId: string) => void) | undefined; /** Move a definitively refused message into an empty composer without resending it. */ restoreOptimisticMessage?: ((clientEventId: string) => void) | undefined; removeOptimisticMessage?: ((clientEventId: string) => void) | undefined; /** Supersede current direction with the draft. */ steer: (text?: string) => Promise; /** Optimistic-to-durable projection for a Steer that has not started yet. */ steering?: ComposerSteeringState | null | undefined; /** Immediate mutation-receipt projection while physical cancellation settles. */ stoppingAttempt?: "current" | "previous" | null | undefined; sending: boolean; canSend: boolean; /** Pause the session without deleting its prompt queue. */ pause: (reason?: string) => Promise; pausing: boolean; resume: (reason?: string) => Promise; resumeScope: (option: EffectiveControlResumeOption) => Promise; resuming: boolean; /** Mutation-confirmed control while the streamed queue projection catches up. */ effectiveControl?: EffectiveSessionControl | null | undefined; draft: ComposerDraft | null; draftRevision: number; draftLoading: boolean; draftSaving: boolean; draftConflict: Error | null; /** Exact policy owned by this actor/session composer. Null while hydrating. */ policy?: ComposerPolicy | null | undefined; setModel?: ((model: string) => void) | undefined; setReasoningEffort?: ((effort: ReasoningEffort) => void) | undefined; setLatencyMode?: ((mode: LatencyMode) => void) | undefined; /** Whether this controller owns a durable server-side draft. */ draftPersistence?: "durable" | "disabled" | undefined; /** Apply an atomic queue Edit checkout without a second read. */ applyDraft: (draft: ComposerDraft) => void; reloadDraft: () => Promise; resolveDraftConflict: (choice: "keep_mine" | "use_remote") => Promise; restoredResources: ResourceRef[]; removeRestoredResource: (index: number) => void; error: Error | null; clearError: () => void; }; export type ComposerControllerState = ComposerState & { policy: ComposerPolicy | null; setModel: (model: string) => void; setReasoningEffort: (effort: ReasoningEffort) => void; setLatencyMode: (mode: LatencyMode) => void; }; export type ComposerSteeringState = { phase: "submitting" | "accepted" | "failed"; text: string; clientEventId: string | null; triggerEventId: string | null; turnId: string | null; /** True only when the accepted Steer durably interrupted a live attempt. */ stoppingPreviousAttempt?: boolean | undefined; error?: string | undefined; outcomeUnknown?: boolean | undefined; }; /** * Draft + send + Pause/Resume state for the chat composer — the only * human-to-agent input surface. The draft survives a failed send (nothing is * more hostile than losing a typed message); each send carries a generated * `clientEventId` so retries stay idempotent server-side. */ export declare function useComposer(sessionId: string | null | undefined, options?: UseComposerOptions): ComposerControllerState; /** Events that can atomically replace or clear this subject's durable draft. */ export declare function isComposerDraftEvent(event: Pick): boolean; /** * Default text for a resource-only message (attachments present, no typed * draft). Kept non-empty so the wire contract (`text: z.string().min(1)`) and * the worker's non-whitespace guard accept it. The export name is retained for * compatibility now that repositories can ride beside files in `resources`. */ export declare const FILE_ONLY_MESSAGE_TEXT: (t: OpenGeniText) => string; /** Resolve possibly-deferred extras to a concrete bag (function evaluated now). */ export declare function resolveSendExtras(extras: ComposerSendExtras | (() => ComposerSendExtras) | undefined): ComposerSendExtras; /** * Merge the draft text + idempotency key with caller-provided extras. The * text and clientEventId always win over extras. Exported for tests. */ export declare function composeSendInput(text: string, clientEventId: string, extras: ComposerSendExtras | (() => ComposerSendExtras) | undefined, bound?: Partial): SendMessageInput; /** Submit on plain Enter; Shift+Enter inserts a newline. Exported for tests. */ export declare function shouldSubmitOnKey(event: { key: string; shiftKey: boolean; metaKey?: boolean; ctrlKey?: boolean; nativeEvent?: { isComposing?: boolean; }; }): boolean; /** Cmd/Ctrl+Enter steers; ordinary Enter appends to the queue. */ export declare function shouldSteerOnKey(event: { metaKey?: boolean; ctrlKey?: boolean; }): boolean;