| 1 | import { lazy, Suspense, type ComponentProps, type CSSProperties, type ReactNode } from "react"; |
| 2 | |
| 3 | import { Composer } from "../components/Composer"; |
| 4 | |
| 5 | const TodoPanel = lazy(() => import("../components/TodoPanel").then((module) => ({ default: module.TodoPanel }))); |
| 6 | const UndoRewindBanner = lazy(() => import("../components/UndoRewindBanner").then((module) => ({ default: module.UndoRewindBanner }))); |
| 7 | const ApprovalModal = lazy(() => import("../components/ApprovalModal").then((module) => ({ default: module.ApprovalModal }))); |
| 8 | const AskCard = lazy(() => import("../components/AskCard").then((module) => ({ default: module.AskCard }))); |
| 9 | const MCPInteractionCard = lazy(() => import("../components/MCPInteractionCard").then((module) => ({ default: module.MCPInteractionCard }))); |
| 10 | const ExtensionFormDialog = lazy(() => import("../components/ExtensionFormDialog").then((module) => ({ default: module.ExtensionFormDialog }))); |
| 11 | const RuntimeDecisionCard = lazy(() => import("../components/RuntimeDecisionCard").then((module) => ({ default: module.RuntimeDecisionCard }))); |
| 12 | const ClearContextCard = lazy(() => import("../components/ClearContextCard").then((module) => ({ default: module.ClearContextCard }))); |
| 13 | |
| 14 | export type ComposerProps = ComponentProps<(typeof import("../components/Composer"))["Composer"]>; |
| 15 | export type TodoProps = ComponentProps<(typeof import("../components/TodoPanel"))["TodoPanel"]>; |
| 16 | export type UndoProps = ComponentProps<(typeof import("../components/UndoRewindBanner"))["UndoRewindBanner"]>; |
| 17 | export type ApprovalProps = ComponentProps<(typeof import("../components/ApprovalModal"))["ApprovalModal"]>; |
| 18 | export type AskProps = ComponentProps<(typeof import("../components/AskCard"))["AskCard"]>; |
| 19 | export type McpProps = ComponentProps<(typeof import("../components/MCPInteractionCard"))["MCPInteractionCard"]>; |
| 20 | export type ExtensionProps = ComponentProps<(typeof import("../components/ExtensionFormDialog"))["ExtensionFormDialog"]>; |
| 21 | export type RuntimeDecisionProps = ComponentProps<(typeof import("../components/RuntimeDecisionCard"))["RuntimeDecisionCard"]>; |
| 22 | export type ClearContextProps = ComponentProps<(typeof import("../components/ClearContextCard"))["ClearContextCard"]>; |
| 23 | |
| 24 | export type DecisionFooterSurface = |
| 25 | | { kind: "approval"; identity: string; props: ApprovalProps } |
| 26 | | { kind: "ask"; identity: string; props: AskProps } |
| 27 | | { kind: "mcp"; identity: string; props: McpProps } |
| 28 | | { kind: "extension"; identity: string; props: ExtensionProps } |
| 29 | | { kind: "runtime"; identity: string; props: RuntimeDecisionProps } |
| 30 | | { kind: "clear-context"; identity: string; props: ClearContextProps }; |
| 31 | |
| 32 | /** Loading one decision cannot hide an already available sibling or its focus. */ |
| 33 | export function DecisionFooterSlots({ todo, undo, decision }: { todo: ReactNode; undo: ReactNode; decision: ReactNode }) { |
| 34 | return <> |
| 35 | <Suspense fallback={null}>{todo}</Suspense> |
| 36 | <Suspense fallback={null}>{undo}</Suspense> |
| 37 | <Suspense fallback={null}>{decision}</Suspense> |
| 38 | </>; |
| 39 | } |
| 40 | |
| 41 | export type DecisionFooterRegionProps = { |
| 42 | hidden: boolean; |
| 43 | className: string; |
| 44 | style?: CSSProperties; |
| 45 | footerRef: ComponentProps<"footer">["ref"]; |
| 46 | todo?: { identity: string; props: TodoProps }; |
| 47 | undo?: { identity: string; props: UndoProps }; |
| 48 | decision?: DecisionFooterSurface; |
| 49 | composer: { |
| 50 | hidden: boolean; |
| 51 | inert: boolean; |
| 52 | hero: boolean; |
| 53 | headline?: string; |
| 54 | hint?: string; |
| 55 | props: ComposerProps; |
| 56 | }; |
| 57 | }; |
| 58 | |
| 59 | function DecisionSurface({ surface }: { surface: DecisionFooterSurface }) { |
| 60 | switch (surface.kind) { |
| 61 | case "approval": |
| 62 | return <ApprovalModal key={surface.identity} {...surface.props} />; |
| 63 | case "ask": |
| 64 | return <AskCard key={surface.identity} {...surface.props} />; |
| 65 | case "mcp": |
| 66 | return <MCPInteractionCard key={surface.identity} {...surface.props} />; |
| 67 | case "extension": |
| 68 | return <ExtensionFormDialog key={surface.identity} {...surface.props} />; |
| 69 | case "runtime": |
| 70 | return <RuntimeDecisionCard key={surface.identity} {...surface.props} />; |
| 71 | case "clear-context": |
| 72 | return <ClearContextCard key={surface.identity} {...surface.props} />; |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | export function DecisionFooterRegion({ |
| 77 | hidden, |
| 78 | className, |
| 79 | style, |
| 80 | footerRef, |
| 81 | todo, |
| 82 | undo, |
| 83 | decision, |
| 84 | composer, |
| 85 | }: DecisionFooterRegionProps) { |
| 86 | if (hidden) return null; |
| 87 | |
| 88 | return ( |
| 89 | <footer className={className} ref={footerRef} style={style} inert={composer.inert || undefined} aria-hidden={composer.inert || undefined}> |
| 90 | <DecisionFooterSlots |
| 91 | todo={todo ? <TodoPanel key={todo.identity} {...todo.props} /> : null} |
| 92 | undo={undo ? <UndoRewindBanner key={undo.identity} {...undo.props} /> : null} |
| 93 | decision={decision ? <DecisionSurface surface={decision} /> : null} |
| 94 | /> |
| 95 | {/* Composer remains mounted while decisions are visible so session-scoped drafts survive. */} |
| 96 | <div |
| 97 | className={[ |
| 98 | "composer-decision-host", |
| 99 | composer.inert ? "composer-decision-host--footprint-hidden" : composer.hidden ? "composer-decision-host--hidden" : "", |
| 100 | composer.hero ? "composer-decision-host--creation-hero" : "", |
| 101 | ].filter(Boolean).join(" ")} |
| 102 | hidden={Boolean(decision) || undefined} |
| 103 | inert={composer.hidden ? true : undefined} |
| 104 | aria-hidden={composer.hidden ? true : undefined} |
| 105 | > |
| 106 | {composer.hero && composer.headline ? <h2 className="welcome-creation__headline">{composer.headline}</h2> : null} |
| 107 | <Composer {...composer.props} /> |
| 108 | {composer.hero && composer.hint ? <p className="composer-decision-host__hint">{composer.hint}</p> : null} |
| 109 | </div> |
| 110 | </footer> |
| 111 | ); |
| 112 | } |
| 113 |