{"version":3,"file":"extension-chrome.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/extension-chrome.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,KAAK,IAAI,EAAgB,KAAK,GAAG,EAAE,MAAM,0BAA0B,CAAC;AACxG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAE7E,OAAO,EAAE,KAAK,KAAK,EAAS,MAAM,kBAAkB,CAAC;AAKrD,KAAK,mBAAmB,GAAG,SAAS,GAAG;IAAE,OAAO,CAAC,IAAI,IAAI,CAAA;CAAE,CAAC;AAE5D,iEAAiE;AACjE,MAAM,WAAW,mBAAmB;IACnC,EAAE,EAAE,GAAG,CAAC;IACR,oBAAoB,EAAE,SAAS,CAAC;IAChC,oBAAoB,EAAE,SAAS,CAAC;IAChC,eAAe,EAAE,SAAS,CAAC;IAC3B,kFAAkF;IAClF,MAAM,EAAE,SAAS,CAAC;IAClB,sEAAsE;IACtE,UAAU,EAAE,IAAI,CAAC;IACjB,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,2EAA2E;IAC3E,gBAAgB,IAAI,SAAS,GAAG,SAAS,CAAC;IAC1C,oBAAoB,IAAI,OAAO,CAAC;CAChC;AAED,qBAAa,eAAe;IAQf,OAAO,CAAC,QAAQ,CAAC,IAAI;IAPjC,OAAO,CAAC,YAAY,CAA0C;IAC9D,OAAO,CAAC,YAAY,CAA0C;IAC9D,sEAAsE;IACtE,OAAO,CAAC,cAAc,CAA8C;IACpE,sEAAsE;IACtE,OAAO,CAAC,cAAc,CAA8C;IAEpE,YAA6B,IAAI,EAAE,mBAAmB,EAAI;IAE1D,sFAAsF;IACtF,IAAI,YAAY,IAAI,mBAAmB,GAAG,SAAS,CAElD;IAED,SAAS,CACR,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,KAAK,mBAAmB,CAAC,GAAG,SAAS,EAC/E,OAAO,CAAC,EAAE,sBAAsB,GAC9B,IAAI,CAoCN;IAED,YAAY,IAAI,IAAI,CAUnB;IAED;;OAEG;IACH,aAAa,IAAI,IAAI,CAKpB;IAED,OAAO,CAAC,qBAAqB;IAuB7B;;OAEG;IACH,SAAS,CACR,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,UAAU,EAAE,kBAAkB,KAAK,mBAAmB,CAAC,GAAG,SAAS,GAClG,IAAI,CAoBN;IAED;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,KAAK,mBAAmB,CAAC,GAAG,SAAS,GAAG,IAAI,CAwCpF;IAED,mEAAmE;IACnE,KAAK,IAAI,IAAI,CAIZ;CACD","sourcesContent":["/**\n * Extension chrome: the widget slots above/below the editor and the custom\n * footer/header overrides that extensions can install via the\n * ExtensionUIContext. Extracted from interactive-mode.ts.\n */\n\nimport { type Component, Container, type Slot, Spacer, Text, type TUI } from \"@kolisachint/hoocode-tui\";\nimport type { ExtensionWidgetOptions } from \"../../core/extensions/index.js\";\nimport type { FooterDataProvider } from \"../../core/footer-data-provider.js\";\nimport { isExpandable } from \"./resource-display.js\";\nimport { type Theme, theme } from \"./theme/theme.js\";\n\n// Maximum total widget lines to prevent viewport overflow\nconst MAX_WIDGET_LINES = 10;\n\ntype DisposableComponent = Component & { dispose?(): void };\n\n/** The slice of the interactive mode the chrome renders into. */\nexport interface ExtensionChromeDeps {\n\tui: TUI;\n\twidgetContainerAbove: Container;\n\twidgetContainerBelow: Container;\n\theaderContainer: Container;\n\t/** The built-in footer component (restored when the custom footer is removed). */\n\tfooter: Component;\n\t/** The root child the footer lives in; its occupant is what swaps. */\n\tfooterSlot: Slot;\n\tfooterDataProvider: FooterDataProvider;\n\t/** The built-in header, once init() created it (undefined before that). */\n\tgetBuiltInHeader(): Component | undefined;\n\tisToolOutputExpanded(): boolean;\n}\n\nexport class ExtensionChrome {\n\tprivate widgetsAbove = new Map<string, DisposableComponent>();\n\tprivate widgetsBelow = new Map<string, DisposableComponent>();\n\t/** Custom footer set by an extension; undefined = built-in footer. */\n\tprivate footerOverride: DisposableComponent | undefined = undefined;\n\t/** Custom header set by an extension; undefined = built-in header. */\n\tprivate headerOverride: DisposableComponent | undefined = undefined;\n\n\tconstructor(private readonly deps: ExtensionChromeDeps) {}\n\n\t/** The extension-installed header, if any (combined with the built-in by callers). */\n\tget customHeader(): DisposableComponent | undefined {\n\t\treturn this.headerOverride;\n\t}\n\n\tsetWidget(\n\t\tkey: string,\n\t\tcontent: string[] | ((tui: TUI, thm: Theme) => DisposableComponent) | undefined,\n\t\toptions?: ExtensionWidgetOptions,\n\t): void {\n\t\tconst placement = options?.placement ?? \"aboveEditor\";\n\t\tconst removeExisting = (map: Map<string, DisposableComponent>) => {\n\t\t\tconst existing = map.get(key);\n\t\t\tif (existing?.dispose) existing.dispose();\n\t\t\tmap.delete(key);\n\t\t};\n\n\t\tremoveExisting(this.widgetsAbove);\n\t\tremoveExisting(this.widgetsBelow);\n\n\t\tif (content === undefined) {\n\t\t\tthis.renderWidgets();\n\t\t\treturn;\n\t\t}\n\n\t\tlet component: DisposableComponent;\n\n\t\tif (Array.isArray(content)) {\n\t\t\t// Wrap string array in a Container with Text components\n\t\t\tconst container = new Container();\n\t\t\tfor (const line of content.slice(0, MAX_WIDGET_LINES)) {\n\t\t\t\tcontainer.addChild(new Text(line, 1, 0));\n\t\t\t}\n\t\t\tif (content.length > MAX_WIDGET_LINES) {\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"muted\", \"... (widget truncated)\"), 1, 0));\n\t\t\t}\n\t\t\tcomponent = container;\n\t\t} else {\n\t\t\t// Factory function - create component\n\t\t\tcomponent = content(this.deps.ui, theme);\n\t\t}\n\n\t\tconst targetMap = placement === \"belowEditor\" ? this.widgetsBelow : this.widgetsAbove;\n\t\ttargetMap.set(key, component);\n\t\tthis.renderWidgets();\n\t}\n\n\tclearWidgets(): void {\n\t\tfor (const widget of this.widgetsAbove.values()) {\n\t\t\twidget.dispose?.();\n\t\t}\n\t\tfor (const widget of this.widgetsBelow.values()) {\n\t\t\twidget.dispose?.();\n\t\t}\n\t\tthis.widgetsAbove.clear();\n\t\tthis.widgetsBelow.clear();\n\t\tthis.renderWidgets();\n\t}\n\n\t/**\n\t * Render all extension widgets to the widget container.\n\t */\n\trenderWidgets(): void {\n\t\tif (!this.deps.widgetContainerAbove || !this.deps.widgetContainerBelow) return;\n\t\tthis.renderWidgetContainer(this.deps.widgetContainerAbove, this.widgetsAbove, true, true);\n\t\tthis.renderWidgetContainer(this.deps.widgetContainerBelow, this.widgetsBelow, false, false);\n\t\tthis.deps.ui.requestRender();\n\t}\n\n\tprivate renderWidgetContainer(\n\t\tcontainer: Container,\n\t\twidgets: Map<string, DisposableComponent>,\n\t\tspacerWhenEmpty: boolean,\n\t\tleadingSpacer: boolean,\n\t): void {\n\t\tcontainer.clear();\n\n\t\tif (widgets.size === 0) {\n\t\t\tif (spacerWhenEmpty) {\n\t\t\t\tcontainer.addChild(new Spacer(1));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (leadingSpacer) {\n\t\t\tcontainer.addChild(new Spacer(1));\n\t\t}\n\t\tfor (const component of widgets.values()) {\n\t\t\tcontainer.addChild(component);\n\t\t}\n\t}\n\n\t/**\n\t * Set a custom footer component, or restore the built-in footer.\n\t */\n\tsetFooter(\n\t\tfactory: ((tui: TUI, thm: Theme, footerData: FooterDataProvider) => DisposableComponent) | undefined,\n\t): void {\n\t\t// Dispose existing custom footer\n\t\tif (this.footerOverride?.dispose) {\n\t\t\tthis.footerOverride.dispose();\n\t\t}\n\n\t\t// Swap the occupant of the footer slot rather than the root's children.\n\t\t// Removing one root child and appending another moved the footer to the\n\t\t// end of the tree, behind the widgets that are supposed to sit below it,\n\t\t// and handed the root's per-child cache a changed child list every time.\n\t\t// The slot is the stable root child; only what is inside it changes.\n\t\tif (factory) {\n\t\t\tthis.footerOverride = factory(this.deps.ui, theme, this.deps.footerDataProvider);\n\t\t\tthis.deps.footerSlot.setChild(this.footerOverride);\n\t\t} else {\n\t\t\tthis.footerOverride = undefined;\n\t\t\tthis.deps.footerSlot.setChild(this.deps.footer);\n\t\t}\n\n\t\tthis.deps.ui.requestRender();\n\t}\n\n\t/**\n\t * Set a custom header component, or restore the built-in header.\n\t */\n\tsetHeader(factory: ((tui: TUI, thm: Theme) => DisposableComponent) | undefined): void {\n\t\t// Header may not be initialized yet if called during early initialization\n\t\tconst builtInHeader = this.deps.getBuiltInHeader();\n\t\tif (!builtInHeader) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Dispose existing custom header\n\t\tif (this.headerOverride?.dispose) {\n\t\t\tthis.headerOverride.dispose();\n\t\t}\n\n\t\t// Find the index of the current header in the header container\n\t\tconst currentHeader = this.headerOverride || builtInHeader;\n\t\tconst index = this.deps.headerContainer.children.indexOf(currentHeader);\n\n\t\tif (factory) {\n\t\t\t// Create and add custom header\n\t\t\tthis.headerOverride = factory(this.deps.ui, theme);\n\t\t\tif (isExpandable(this.headerOverride)) {\n\t\t\t\tthis.headerOverride.setExpanded(this.deps.isToolOutputExpanded());\n\t\t\t}\n\t\t\tif (index !== -1) {\n\t\t\t\tthis.deps.headerContainer.children[index] = this.headerOverride;\n\t\t\t} else {\n\t\t\t\t// If not found (e.g. builtInHeader was never added), add at the top\n\t\t\t\tthis.deps.headerContainer.children.unshift(this.headerOverride);\n\t\t\t}\n\t\t} else {\n\t\t\t// Restore built-in header\n\t\t\tthis.headerOverride = undefined;\n\t\t\tif (isExpandable(builtInHeader)) {\n\t\t\t\tbuiltInHeader.setExpanded(this.deps.isToolOutputExpanded());\n\t\t\t}\n\t\t\tif (index !== -1) {\n\t\t\t\tthis.deps.headerContainer.children[index] = builtInHeader;\n\t\t\t}\n\t\t}\n\n\t\tthis.deps.ui.requestRender();\n\t}\n\n\t/** Remove custom footer/header and all widgets (session reset). */\n\treset(): void {\n\t\tthis.setFooter(undefined);\n\t\tthis.setHeader(undefined);\n\t\tthis.clearWidgets();\n\t}\n}\n"]}