blob: 47d715a31027dedd440af306df3c7a25132cdbd9 [file]
/**
* Composer draft-persistence hook (issue #1044).
*
* Owns the per-session unsent-draft store that used to live inline in
* `composer.tsx`: a bounded Map keyed by `draftKey` and the active key.
* The pure store operations
* (remember / read, with the 120k-char and 32-entry bounds) stay in
* `composer-helpers.ts` — this hook is the React seam that wires them to the
* input's `ComposerTextPort`.
*
* The swap effect preserves the exact pre-extraction semantics: when the host
* switches `draftKey` (e.g. the first send in the home composer creates a
* session and the surface re-keys), the current text is remembered under the
* OLD key and the draft for the NEW key is swapped in. `onDraftKeyChange` lets
* the composer reset sibling state machines (prompt-history navigation) at the
* same moment without this hook depending on them.
*/
import { useEffect, useRef } from 'react';
import type { ComposerTextPort } from './chat-input-behavior.js';
import {
appendPromptContextDraft,
readComposerDraft,
rememberComposerDraft,
} from './composer-helpers.js';
export interface ComposerDraftApi {
/** Persist the current (or given) input value under the active draft key. */
saveCurrentDraft(value?: string): void;
/**
* Clear the draft stored under an explicit key. A successful send clears
* the key it was submitted from (which may no longer be the active key
* after a new-session swap) in addition to the active draft.
*/
clearDraft(key: string | undefined): void;
/** Persist text under an explicit session key before the host switches it. */
setDraft(key: string | undefined, value: string): void;
/** Append text under an explicit session key without overwriting its draft. */
appendDraft(key: string | undefined, value: string): string;
/** The key the current input content is persisted under. */
activeDraftKey(): string | undefined;
}
export interface ComposerDraftPersistence {
read(key: string | undefined): string | undefined;
write(key: string | undefined, value: string): void;
}
export function useComposerDraft(input: {
text: ComposerTextPort;
/** Runtime-only key used to keep unsent drafts isolated per session. */
draftKey: string | undefined;
/** Fired after the active key swaps so sibling state machines can reset. */
onDraftKeyChange(): void;
/** Optional host persistence for drafts that must survive renderer replacement. */
persistence?: ComposerDraftPersistence;
}): ComposerDraftApi {
const draftStoreRef = useRef<Map<string, string>>(new Map());
const activeDraftKeyRef = useRef<string | undefined>(input.draftKey);
function saveCurrentDraft(value?: string) {
const nextValue = value ?? input.text.getValue();
rememberComposerDraft(draftStoreRef.current, activeDraftKeyRef.current, nextValue);
input.persistence?.write(activeDraftKeyRef.current, nextValue);
}
function clearDraft(key: string | undefined) {
rememberComposerDraft(draftStoreRef.current, key, '');
input.persistence?.write(key, '');
}
function setDraft(key: string | undefined, value: string) {
rememberComposerDraft(draftStoreRef.current, key, value);
input.persistence?.write(key, value);
}
function appendDraft(key: string | undefined, value: string) {
const current =
activeDraftKeyRef.current === key
? input.text.getValue()
: readComposerDraft(draftStoreRef.current, key);
const next = appendPromptContextDraft(current, value);
setDraft(key, next);
return next;
}
function activeDraftKey() {
return activeDraftKeyRef.current;
}
useEffect(() => {
const previousKey = activeDraftKeyRef.current;
const nextKey = input.draftKey;
if (previousKey === nextKey) return;
rememberComposerDraft(draftStoreRef.current, previousKey, input.text.getValue());
activeDraftKeyRef.current = nextKey;
input.onDraftKeyChange();
const nextDraft = readComposerDraft(draftStoreRef.current, nextKey);
input.text.setValue(nextDraft);
}, [input.draftKey]);
useEffect(() => {
const key = activeDraftKeyRef.current;
const persisted = input.persistence?.read(key);
if (!persisted) return;
rememberComposerDraft(draftStoreRef.current, key, persisted);
input.text.setValue(persisted);
}, []);
return {
saveCurrentDraft,
clearDraft,
setDraft,
appendDraft,
activeDraftKey,
};
}