# Conflicts: # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/src/client/input/facade.ts # packages/client/ui-conversation/src/client/input/hub.ts
400 lines
18 KiB
TypeScript
400 lines
18 KiB
TypeScript
/**
|
|
* Frozen input-machine contract. Types
|
|
* only. Three-tier visibility: business packages see InputState via the
|
|
* InputZone currency; the scoped input events carry the mutation verbs; the
|
|
* conversation wiring layer alone sees the full SessionInput. The draft text
|
|
* and its reference chips live in the shell's Lexical editor; the machine
|
|
* here is the submit plane (phase, claim, attempt) alone.
|
|
*/
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
import type { SessionBinding } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
import type { InboxState } from '@deepseek-ai/dsh-agent/types'
|
|
import type { ObservableSnapshot, SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
import type { Branded } from '@deepseek-ai/dsh-brand'
|
|
import type { ArbitrateKey, ArbitrateOutcome, DraftQuote, Occurrence, ReferenceInsert, TokenSpan } from './draft-editor.ts'
|
|
import type { InputSubmitMode, MessageSubmission } from './composer-submission.ts'
|
|
|
|
/** Attachment payload passed to a claimed command submission. */
|
|
export type SubmitAttachment =
|
|
| {
|
|
readonly type: 'image'
|
|
readonly mediaType: 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
|
|
readonly data: string
|
|
readonly name?: string
|
|
}
|
|
| { readonly type: 'file'; readonly receiptId: string }
|
|
|
|
/** Command serialization result for one ordered attachment draft. */
|
|
export interface DraftAttachmentSerializationResult {
|
|
readonly attachments: readonly SubmitAttachment[]
|
|
}
|
|
|
|
/** Settled result of a command or default composer submission. */
|
|
export interface SubmitOutcome {
|
|
readonly kind: 'success' | 'error'
|
|
readonly text?: string
|
|
}
|
|
|
|
/** Command-mode credential supplied by one input-trigger source. */
|
|
export interface CommandClaim {
|
|
/** Catalog command name without the leading slash (the key of per-command composer copy such as `hint.*`). */
|
|
readonly name: string
|
|
/** Command spelling followed by its argument separator; the composer shows the spelling as a chip and inserts the separator after it. */
|
|
readonly token: string
|
|
readonly hint?: string
|
|
readonly attachments?: boolean
|
|
/**
|
|
* Submit the claimed command.
|
|
* @param args - command text after the claimed token.
|
|
* @param actx - current Session scope.
|
|
* @param attachments - serialized draft attachments accepted by the claim.
|
|
* @returns command settlement.
|
|
*/
|
|
submit(args: string, actx: Context, attachments: readonly SubmitAttachment[]): Promise<SubmitOutcome>
|
|
}
|
|
|
|
/** Result of trigger-source adjudication. */
|
|
export type PickOutcome =
|
|
| { readonly claim: CommandClaim }
|
|
| { readonly insert: ReferenceInsert }
|
|
| { readonly text: string; readonly continue?: boolean }
|
|
| 'handled'
|
|
| undefined
|
|
|
|
/** Scoped request to enter command mode. */
|
|
export interface BeginCommandRequest {
|
|
readonly claim: CommandClaim
|
|
readonly span: TokenSpan
|
|
}
|
|
|
|
/** Scoped request to insert a structured reference. */
|
|
export interface InsertReferenceRequest {
|
|
readonly reference: ReferenceInsert
|
|
readonly span: TokenSpan
|
|
}
|
|
|
|
/** Scoped request to consume a command token after business settlement. */
|
|
export interface ConsumeTokenRequest {
|
|
readonly guard:
|
|
| { readonly kind: 'span'; readonly span: TokenSpan }
|
|
| { readonly kind: 'bare-token'; readonly token: string }
|
|
}
|
|
|
|
/** Scoped request to insert ordinary completion text. */
|
|
export interface InsertTextRequest {
|
|
readonly text: string
|
|
readonly span: TokenSpan
|
|
readonly continue?: boolean
|
|
}
|
|
|
|
/** Trigger hit used to open one source programmatically. */
|
|
export interface InputTriggerHit {
|
|
readonly trigger: '/' | '@'
|
|
readonly query: string
|
|
readonly quoted: boolean
|
|
readonly position: 'leading' | 'inline'
|
|
readonly span: TokenSpan
|
|
}
|
|
|
|
/** Structural per-Session trigger provider consumed by the input shell. */
|
|
export interface InputTriggerController {
|
|
readonly launcher: ObservableSnapshot<string | null>
|
|
readonly lexicon: ObservableSnapshot<ReadonlyMap<'/' | '@', readonly string[]>>
|
|
/** @param draft - current draft. @param caret - caret offset. @param guard - availability tier. @param draftRev - input revision. */
|
|
track(
|
|
draft: string,
|
|
caret: number,
|
|
guard: { readonly tier: 'plain' | 'claimed' | 'frozen' },
|
|
draftRev: number,
|
|
): void
|
|
/** @param key - intercepted key. @param composing - whether IME composition is active. @returns routing result. */
|
|
arbitrate(key: ArbitrateKey, composing: boolean): ArbitrateOutcome
|
|
/** @returns whether Space applied a trigger result. */
|
|
onSpace(): boolean
|
|
/** @param source - reference source. @param ref - source-local id. @param signal - submit cancellation. @returns model text. */
|
|
serializeReference(source: string, ref: string, signal: AbortSignal): Promise<string>
|
|
/** @param line - trimmed draft. @param signal - submit cancellation. @param envelope - attachment count. @returns winning result. */
|
|
adjudicate(
|
|
line: string,
|
|
signal: AbortSignal,
|
|
envelope: { readonly attachments: number },
|
|
): Promise<PickOutcome>
|
|
/**
|
|
* Resolve the claim a persisted command chip names without acting on it.
|
|
* @param token - command spelling without its argument separator, e.g. '/goal'.
|
|
* @param signal - restore-attempt abort.
|
|
* @returns the claim, or undefined when no source claims the token; rejects when catalog warmup fails.
|
|
*/
|
|
resolveClaim(token: string, signal: AbortSignal): Promise<CommandClaim | undefined>
|
|
/**
|
|
* @param source - chip owner, or undefined for a text reference.
|
|
* @param reference - source id and glyph.
|
|
* @returns whether a preview opened.
|
|
*/
|
|
openReference(source: string | undefined, reference: Pick<ReferenceInsert, 'ref' | 'appearance'>): boolean
|
|
/** @param source - source name. @param hit - synthetic trigger hit. */
|
|
toggleSource(source: string, hit: InputTriggerHit): void
|
|
}
|
|
|
|
declare module '@deepseek-ai/cordis' {
|
|
interface Events {
|
|
/**
|
|
* Claim a command token for the scoped input machine.
|
|
* @param request - command claim and span.
|
|
* @mode bail
|
|
*/
|
|
'slash/input-begin-command'(request: BeginCommandRequest): true | undefined
|
|
/**
|
|
* Insert a structured reference into the scoped input machine.
|
|
* @param request - reference and span.
|
|
* @mode bail
|
|
*/
|
|
'slash/input-insert-reference'(request: InsertReferenceRequest): true | undefined
|
|
/**
|
|
* Consume a trigger token without inserting replacement content.
|
|
* @param request - token guard.
|
|
* @mode bail
|
|
*/
|
|
'slash/input-consume-token'(request: ConsumeTokenRequest): true | undefined
|
|
/**
|
|
* Insert plain text into the scoped input machine.
|
|
* @param request - plain text and span.
|
|
* @mode bail
|
|
*/
|
|
'slash/input-insert-text'(request: InsertTextRequest): true | undefined
|
|
}
|
|
}
|
|
|
|
/** Browser-runtime identity of one unsent attachment draft. */
|
|
export type DraftAttachmentId = Branded<'DraftAttachmentId'>
|
|
|
|
/**
|
|
* The scoped-event application verbs: the hub's bail listeners call these,
|
|
* and the boolean answer IS the event's bail value (true ⟺ the editor
|
|
* applied the edit after phase and span guards).
|
|
*/
|
|
export interface InputTarget {
|
|
/** Replace the leading trigger span with a command chip and enter claimed (span-CAS'd). */
|
|
beginCommand(claim: CommandClaim, span: TokenSpan): boolean
|
|
/** Replace the trigger span with one reference chip (span-CAS'd). */
|
|
insertReference(ref: ReferenceInsert, span: TokenSpan): boolean
|
|
}
|
|
|
|
/** Per-session input facade owned by the conversation wiring layer. */
|
|
export interface SessionInput extends InputTarget {
|
|
/** Insert selected transcript text as an atomic quote. @param text - nonblank selection. @returns whether editing accepted it. */
|
|
insertQuote(text: string): boolean
|
|
|
|
/** Replace the whole draft with plain text, removing inline reference chips. */
|
|
setDraft(text: string): void
|
|
/** Append ordered browser-owned attachment ids; busy admission phases refuse. */
|
|
addAttachments(ids: readonly DraftAttachmentId[]): boolean
|
|
/** Remove one browser-owned attachment id; busy admission phases refuse. @returns whether the id was removed. */
|
|
removeAttachment(id: DraftAttachmentId): boolean
|
|
/** Drop ids whose browser-owned objects no longer exist. */
|
|
pruneAttachments(ids: readonly DraftAttachmentId[]): void
|
|
/**
|
|
* THE complexity sink: enter adjudication, submit transaction, and the default sink live inside.
|
|
* @param mode - delivery intent retained through asynchronous adjudication and serialization.
|
|
*/
|
|
submit(mode?: InputSubmitMode, source?: 'click' | 'enter'): void
|
|
/**
|
|
* Surface a notice outside the machine's own effect stream: detached
|
|
* command results and business notifications render through here.
|
|
* Session-routed — resolving the facade via SessionInputResolver.for(actx) lands
|
|
* the notice on that session's composer, so a result arriving after a
|
|
* session switch still reaches its own session.
|
|
* @param level - severity tier.
|
|
* @param text - notice body.
|
|
*/
|
|
notify(level: 'info' | 'error', text: string): void
|
|
|
|
/**
|
|
* Return the keyboard to the composer with the caret it last held, for
|
|
* callers that took focus away from it (an overlay that held its own).
|
|
*/
|
|
focus(): void
|
|
/** Input state store (InputZone currency + decorations read here). */
|
|
readonly state: SnapshotStore<InputState>
|
|
}
|
|
|
|
/** Session-addressed access to the per-session input facade. */
|
|
export interface SessionInputResolver {
|
|
/** Resolve the facade for one session-scope ctx. */
|
|
for(actx: Context): SessionInput
|
|
/**
|
|
* Prepare the exact retained Session before navigation publishes it.
|
|
* @param binding - target Session generation, already retained by the caller.
|
|
* @param options - content to adopt and the handling of an existing draft.
|
|
* @returns applied, preserved, or blocked by a pending submission or disposal.
|
|
* @throws when binding is not the currently retained Session generation.
|
|
*/
|
|
requestDraftInitialization(binding: SessionBinding, options: DraftInitializationOptions): DraftInitializationResult
|
|
}
|
|
|
|
/** Initial text for a target Session; attachments remain under their existing owner. */
|
|
export interface DraftInitializationOptions {
|
|
readonly prompt?: string
|
|
/** `true` acts as `whenDraftExists: 'replace'`; ignored when `whenDraftExists` is set. */
|
|
readonly clearPreviousDraft?: boolean
|
|
/**
|
|
* Handling of a target that already has text or attachments: `replace` swaps the text and
|
|
* inline references for the prompt (or clears them without one), `append` adds the prompt
|
|
* as new paragraphs after the existing text, and `keep` leaves the draft unchanged. Omitted,
|
|
* it is `replace` when `clearPreviousDraft` is true and `keep` otherwise.
|
|
*/
|
|
readonly whenDraftExists?: 'replace' | 'append' | 'keep'
|
|
}
|
|
|
|
/** An initialization request never waits for a React mount or a remote catalog. */
|
|
export type DraftInitializationResult = 'applied' | 'preserved' | 'blocked'
|
|
|
|
/**
|
|
* The public input action face provided to every session-scope slot
|
|
* component: stable-identity callbacks, mirroring the
|
|
* useStore+actions convention. Command-style handles (arbitrate/space/
|
|
* paste/…) stay InputBar-private and never ride this face.
|
|
*/
|
|
export interface InputActions {
|
|
/** Insert one nonblank transcript quote. @param text - selected text. @returns whether editing accepted it. */
|
|
insertQuote(text: string): boolean
|
|
/** @returns a revision-guarded copy of the current editor selection. */
|
|
captureInsertion(): TokenSpan
|
|
/**
|
|
* Insert asynchronous text without replacing subsequent edits or reference chips.
|
|
* @param text - plain text to insert in one undo step.
|
|
* @param span - captured selection and draft revision.
|
|
* @returns false when the draft changed or submission locked the editor.
|
|
*/
|
|
insertText(text: string, span: TokenSpan): boolean
|
|
/** Replace the whole draft with plain text, removing inline reference chips. */
|
|
setDraft(text: string): void
|
|
/** Write the current semantic document through the mounted persistence callback. */
|
|
persistDraft(): void
|
|
/** Append ordered browser-owned attachment ids; busy admission phases refuse. */
|
|
addAttachments(ids: readonly DraftAttachmentId[]): boolean
|
|
/** Remove one browser-owned attachment id; busy admission phases refuse. */
|
|
removeAttachment(id: DraftAttachmentId): void
|
|
/** Drop ids whose browser-owned objects no longer exist. */
|
|
pruneAttachments(ids: readonly DraftAttachmentId[]): void
|
|
/** Enter submission (adjudication / claim transaction / default sink inside). */
|
|
submit(): void
|
|
}
|
|
|
|
/** One surfaced notice (command results, adjudication failures). seq keys re-render of repeats. */
|
|
export interface InputNotice {
|
|
readonly level: 'info' | 'error'
|
|
readonly text: string
|
|
readonly seq: number
|
|
}
|
|
|
|
/** Guard union of the scoped consume-token event, checked by the shell. */
|
|
export type ConsumeTokenGuard = ConsumeTokenRequest['guard']
|
|
|
|
/** Published input state (the currency; per-session). */
|
|
export interface InputState {
|
|
/** Clipboard-text projection of the editor document (chips expanded to their clipboard form). */
|
|
readonly draft: string
|
|
/** Ordered runtime-only attachment ids; browser objects stay in ConversationController. */
|
|
readonly attachmentIds: readonly DraftAttachmentId[]
|
|
/** Monotonic editor revision (span CAS compares against this). */
|
|
readonly draftRev: number
|
|
/**
|
|
* Submit plane: `adjudicating` while a `/` line is checked against the
|
|
* commands, `submitting` while a claimed command runs, and `sending` while
|
|
* an ordinary message waits for the Host to accept it. The editor and the
|
|
* attachments are read-only in all three.
|
|
*/
|
|
readonly phase: 'plain' | 'adjudicating' | 'claimed' | 'submitting' | 'sending'
|
|
/** Present exactly while claimed/submitting (claim snapshot during flight; submit closure withheld). */
|
|
readonly claim?: { readonly name: string; readonly token: string; readonly hint?: string; readonly attachments?: boolean }
|
|
/** Reference occurrence view of the editor's chips, sorted by offset. */
|
|
readonly occurrences: readonly Occurrence[]
|
|
/** Messages still waiting for their own turn. */
|
|
readonly queue: InboxState['next-turn']
|
|
}
|
|
|
|
/**
|
|
* One in-flight submission attempt: the ONLY id concept in the submit plane.
|
|
* Created on enter; carried by adjudicated/submit-settled/sink-settled
|
|
* events; stale attempts are dropped (anti-backwash). Command and message
|
|
* attempts hold the single frozen in-flight slot, so the draft they carry is
|
|
* the one the user sent. release/session teardown aborts the attempt, keeping
|
|
* every promise bounded.
|
|
*/
|
|
export interface SubmitAttempt {
|
|
readonly seq: number
|
|
readonly signal: AbortSignal
|
|
/** Clipboard-projection draft captured when the user sent. */
|
|
readonly draftSnapshot: string
|
|
/** Reference chips of the sent draft, captured with it. */
|
|
readonly occurrences: readonly Occurrence[]
|
|
/** Quotes of the sent draft, captured with it. */
|
|
readonly quotes: readonly DraftQuote[]
|
|
/** Attachments of the sent draft, captured with it, in order. */
|
|
readonly attachmentIds: readonly DraftAttachmentId[]
|
|
/** Default-message delivery intent retained while slash adjudication is pending. */
|
|
readonly mode: InputSubmitMode
|
|
/** Original message occurrence, retained across command arbitration. */
|
|
readonly submission?: MessageSubmission
|
|
}
|
|
|
|
/**
|
|
* Submit-machine input events (the machine's single write path). Text
|
|
* mutation lives in the editor; the machine only observes the draft through
|
|
* event payloads (leading command chip, enter snapshots, settlement decisions).
|
|
*/
|
|
export type InputEvent =
|
|
/** Editor content changed; carries the claim of the leading command chip, if any (zero effects). */
|
|
| { readonly type: 'draft-changed'; readonly command: CommandClaim | undefined }
|
|
/** The editor inserted a command chip: enter claimed. */
|
|
| { readonly type: 'claim'; readonly claim: CommandClaim }
|
|
/** Enter submission with the current clipboard projection, its reference chips, its quotes, and its attachments. */
|
|
| {
|
|
readonly type: 'enter'
|
|
readonly mode: InputSubmitMode
|
|
readonly draft: string
|
|
readonly occurrences: readonly Occurrence[]
|
|
readonly quotes: readonly DraftQuote[]
|
|
readonly attachmentIds: readonly DraftAttachmentId[]
|
|
readonly submission?: MessageSubmission
|
|
}
|
|
| { readonly type: 'adjudicated'; readonly attempt: SubmitAttempt; readonly outcome: PickOutcome }
|
|
| { readonly type: 'adjudication-failed'; readonly attempt: SubmitAttempt; readonly message: string }
|
|
/** Settlement carries the leading command chip's claim, which the claim follows after a failure. */
|
|
| {
|
|
readonly type: 'submit-settled'
|
|
readonly attempt: SubmitAttempt
|
|
readonly ok: boolean
|
|
readonly command: CommandClaim | undefined
|
|
readonly outcome?: SubmitOutcome
|
|
readonly message?: string
|
|
}
|
|
/** Settlement of the message being sent: the Host accepted it, or refused it or failed. */
|
|
| { readonly type: 'sink-settled'; readonly attempt: SubmitAttempt; readonly ok: boolean; readonly outcome?: SubmitOutcome; readonly message?: string }
|
|
| { readonly type: 'release' }
|
|
|
|
/**
|
|
* Submit-machine output effects (executed by the SessionInput shell; the
|
|
* machine stays pure).
|
|
*/
|
|
export type InputEffect =
|
|
| { readonly type: 'adjudicate'; readonly attempt: SubmitAttempt; readonly draft: string }
|
|
| { readonly type: 'begin-submit'; readonly attempt: SubmitAttempt; readonly claim: CommandClaim; readonly args: string }
|
|
/** Send the attempt's frozen draft, references, and attachments as one ordinary message. */
|
|
| {
|
|
readonly type: 'default-sink'
|
|
readonly attempt: SubmitAttempt
|
|
readonly draft: string
|
|
readonly mode: InputSubmitMode
|
|
}
|
|
| { readonly type: 'notice'; readonly level: 'info' | 'error'; readonly text: string }
|
|
/**
|
|
* Clear the committed draft in the editor and cut undo history. A string
|
|
* snapshot keeps a pure suffix typed during the Host round-trip (content
|
|
* appended after the sent snapshot survives; interleaved edits cannot be
|
|
* separated and clear whole); null clears unconditionally (an accepted
|
|
* message froze the editor, so nothing was typed after it).
|
|
*/
|
|
| { readonly type: 'commit-draft'; readonly retainSuffixOf: string | null }
|