/** Where the panel lands. Generalises position, not composition. */ export type ShapeContainer = "corner" | "inline" | "edge" | "center" | "rail" | "fullpage" | "anchored"; /** * What the visitor finds on arrival. `collapsed` is a third state that the old * boolean could not express at all, expand-in-place is neither open nor closed, * it is a line of host text waiting to unfold. */ export type ShapeInitial = "open" | "closed" | "collapsed"; /** What summons it. `none` means it is simply present. */ export type ShapeTrigger = "launcher" | "host" | "keyboard" | "selection" | "none"; /** * Whether an operator can install this today. * * Copied from `surface-catalog.ts`: statuses live in data so promoting a shape * is a data change rather than a UI redesign, and nothing can offer a snippet * for a mount the runtime cannot honour. Every row below is real architecture; * `planned` rows are declared so the axes are exercised by the whole family * rather than fitted to the two that ship, but they must never reach an * operator-facing menu while they are planned. */ export type ShapeStatus = "live" | "planned"; /** * ── The anatomy axis ──────────────────────────────────────────────────────── * * The four axes above generalise POSITION. They do not generalise COMPOSITION, * and that omission is what makes a thirteen-shape family look like one chat * assistant being moved around a page. A bordered white card with its own radius, * shadow, avatar and empty message list is a foreign object in a pricing page * no matter which coordinates it occupies. * * The payoff is that decisions get located. "Should inline have a header?" is * an anatomy question, so it is answered once for all thirteen shapes rather * than thirteen times, thirteen chances to be inconsistent. */ /** Does the container draw its own border, shadow and background. */ export type AnatomyChrome = "none" | "minimal" | "full"; /** Identity, grounding badge, close. `onHistory` = only once there is history. */ export type AnatomyHeader = "none" | "onHistory" | "always"; /** * How an answer is composed. * * Not styling. Prose renders an answer as text in the host's own rhythm; * bubbles render a conversation. Chat bubbles inside a command palette are a * category error, and a lone bubble inside chrome-less inline looks more * bolted-on than the border it replaced. */ export type AnatomyRender = "prose" | "bubbles"; /** * Whether conversation history is shown. * * `onEscalate` is answer-first, chat-second: someone at a pricing table asks * one question and wants one answer, and a threaded log is the wrong * information architecture for that. The thread is the escalation, not the * opening move. */ export type AnatomyTranscript = "onEscalate" | "always"; /** * Whether the visitor can type. * * `onThread` is the anchored family: an affordance attached to a pricing tier * answers the question its element implies and then OFFERS to continue, rather * than assuming a conversation the visitor never asked to start. The composer * arrives when they take that offer. */ export type AnatomyComposer = "always" | "onThread"; /** * What the visitor sees before anything happens. * * This is the entire product for most of them, they never open it, and it is * the state that deserves the most design and usually gets the least. For half * the family it is a single line, not a panel. */ export type AnatomyRest = "none" | "line" | "field" | "affordance" | "open"; export type ShapeAnatomy = { chrome: AnatomyChrome; header: AnatomyHeader; render: AnatomyRender; transcript: AnatomyTranscript; rest: AnatomyRest; composer: AnatomyComposer; }; export type ShapeSpec = { container: ShapeContainer; initial: ShapeInitial; /** Dim + focus-trap + scroll-lock + Esc + return-focus, built once. */ modal: boolean; trigger: ShapeTrigger; /** * Default for `open({ send })` when this shape seeds a question. * * The seeding shapes genuinely disagree, which is why this is per-shape and * not a single global default: a starter chip carries a complete question and * should ask it, while select-to-ask carries a passage the visitor highlighted * and they may want to add "…in plain English" before sending. */ sendOnPrefill: boolean; /** * Whether the mount opens a session before the visitor has asked anything. * * ⚠️ This is the seventh concern the boolean conflated, and ADR 0202 lists * only six, it is also the one with a bill attached, so it is stated here * rather than derived from `initial`. * * Sessions are meant to mint lazily, on first ask. The inline mount already * breaks that: it calls `ensureSession()` at mount, because an always-open * panel that waits would show a transcript it cannot populate. That is * deliberate and preserved. * * **Decided 2026-08-05: no other shape gets it.** `fullpage` and `sidebar` * are `initial: "open"` and `eagerSession: false`, and that is the answer * rather than a placeholder. * * The reasoning is not the bill. Metering is per MESSAGE since ADR 0205, so * an empty session costs nothing to serve. The cost is to the record: a * session is a Conversation row and an impression event, so a docked sidebar * on a documentation site would make **every page view a conversation**. That * does not overcharge anyone, it destroys the conversation metric, which is * one of the few numbers an operator uses to judge whether this works, and it * grows storage in proportion to traffic rather than to interest. * * And the visitor gains nothing: `send()` mints on demand, so the only * difference is whether a session exists before anyone wanted one. * * `inline` keeps its eager session, and that is inconsistent on purpose. * Changing a shipping shape's behaviour on reasoning alone is how you find * out afterwards what depended on it, human takeover polls by session id, * for one. The follow-up is to establish whether inline still needs it and * make the rule uniform, not to guess now. */ eagerSession: boolean; /** * What renders, as opposed to where it lands. Every shape has exactly three * states, rest, answer, thread, so escalation works identically everywhere, * and that uniformity is what keeps thirteen anatomies from becoming thirteen * implementations. */ anatomy: ShapeAnatomy; /** * Whether opening this shape SHIFTS the host page rather than covering it. * * A pushing panel is not modal by definition, the page it moved is still * there to be read and clicked, so the two travel together: `modal: false` * plus `pushesContent: true`. Only meaningful for a panel pinned to a * viewport edge; a centred or corner panel has nothing to push away from. */ pushesContent?: boolean; /** * Whether the visitor may resize this panel by dragging its edges. * * Absent means yes, for every floating shape, that is the long-standing * behaviour and the common case, so the flag only ever appears on a shape * that opts OUT. A field rather than a container check because "edge" is a * place, not a policy: the mobile sheet lives at an edge too, and the next * edge shape should inherit nothing from this decision. * * `drawer` opts out (founder, 2026-08-18), "for now", so this is a one-word * flip back rather than code to reconstruct. */ resizable?: boolean; status: ShapeStatus; }; /** * The thirteen. Names match the shape ids already used by the marketing * gallery's i18n block, so one vocabulary spans the site, the docs and the * runtime. */ export type AssistantMode = "bubble" | "inline" | "drawer" | "commandk" | "navfield" | "askbar" | "expand" | "starters" | "mobile" | "fullpage" | "sidebar" | "selection" | "anchored" | "corner" | "center"; export declare const SHAPE_SPECS: Record; /** * ── Reach: additive triggers over one residence (ADR 0226) ───────────────── * * A page mounts ONE shape, the residence, where the conversation lives. * Reach names extra ways to summon it: highlight a passage, press ⌘K. Reach * adds entry points, never panels, the instance registry already guarantees * one panel per assistant, and reach is why that guarantee is a feature * rather than a limitation. A reach trigger opens the mounted residence; it * never conjures the shape it was named after: `selection` as reach seeds the * RESIDENCE's composer with the passage, not an anchored popover (that * graduation is ADR 0226's Rung 2, deliberately unbuilt until designed). * Reach installs listeners and nothing else at mount, `eagerSession` stays * the residence's own answer, and a reach trigger firing costs exactly what * the residence's own trigger costs: a session begins when the panel opens * (as with a launcher click), a conversation counts when a message is sent. */ export type ReachChannel = "selection" | "keyboard"; /** * The shape ids an embed may name as additive reach, and the trigger channel * each claims. Keyed by shape id, not channel name, because the operator met * these words in the gallery and the picker, asking them to learn that * commandk's channel is "keyboard" would be the runtime's internals leaking * into a pasted snippet. */ export declare const REACH_SHAPES: Partial>; /** * Resolve a comma-separated reach list against the residence. * * Unknown ids are reported, not swallowed, the same rule as * `resolveShapeSpec`'s `dropped`, and for the same reason: substituting * silence for a shape someone asked for is how they never learn they did not * get it. A reach naming the channel the residence already claims dedupes * silently, mounting `commandk` with `reach="commandk"` is idempotent, not * an error. Input is typed loosely (any string) for the reason the loader's * `mode` is: an old loader must be able to pass a newer bundle's vocabulary * through without owning a copy of the list. * * `incompatible` is the third bucket: ids that exist but cannot serve THIS * residence. Today that is `keyboard` reach over an anchored container, an * anchored panel positions FROM the element the call names, and a keystroke * names none, so honouring it would open a panel with no coordinates at all * (measured: it parks off-viewport). Selection reach is fine there, the * selection supplies its own element. Reported separately from `dropped` * because the operator's mistake is different: not a typo, a pairing. */ export declare function resolveReach(reach: string | null | undefined, residence: Pick): { channels: ReachChannel[]; dropped: string[]; incompatible: string[]; }; /** * ── Where a selection answers (ADR 0226 amendment) ────────────────────────── * * A selection-triggered open borrows the anchored container so the answer * arrives where the question was asked, that is the default and the designed * behaviour, and it stays the default. * * It is not the only defensible answer. A panel that flies to a paragraph is * motion the visitor did not ask for, and on a page whose assistant is a * fixture, a docked corner the visitor has already located, landing back in * that known place can beat landing beside the words. Operators who want that * say so; nobody has it imposed on them. * * Scoped deliberately to the SELECTION trigger. `open({ anchorPresentation })` * from host code is unaffected: a host that names an element is describing * where its own affordance sits, and has already made this choice by calling. */ export type SelectionAnswers = "beside" | "in-place"; export declare const DEFAULT_SELECTION_ANSWERS: SelectionAnswers; /** * Resolve the `selection-answers` attribute. * * Same discipline as `resolveReach`: an unrecognised value is REPORTED and the * default applied, never swallowed. Someone who wrote `in place` or `inplace` * has to be able to find out that the panel still flies, rather than * concluding the feature does not work. */ export declare function resolveSelectionAnswers(value: string | null | undefined): { answers: SelectionAnswers; dropped: string | null; }; /** What a mount is when nothing says otherwise. */ export declare const DEFAULT_MODE: AssistantMode; /** * The pre-ADR-0202 public API. Live customer snippets carry it, so it resolves * rather than warns: `floating` is the corner preset, `embedded` the inline one. */ export type Placement = "floating" | "embedded"; export declare function isAssistantMode(value: unknown): value is AssistantMode; /** * Resolve a mount to its spec. * * `mode` wins when it names a live shape. A planned shape is deliberately NOT * honoured, an embed that asks for one gets the default rather than a mount * the runtime cannot draw, which is the same rule that keeps `planned` cells * out of the dashboard's snippet generator. */ export declare function resolveShapeSpec(input: { mode?: string | null; placement?: Placement | string | null; }): { mode: AssistantMode; spec: ShapeSpec; dropped?: string; }; /** The shapes an operator may actually be offered. */ export declare const LIVE_MODES: readonly AssistantMode[];