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