@miragon/mcp-toolkit-ui 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/app/adapt-data-widget.d.ts +33 -0
- package/dist/app/adapt-data-widget.d.ts.map +1 -0
- package/dist/app/adapt-data-widget.js +35 -0
- package/dist/app/adapt-data-widget.js.map +1 -0
- package/dist/app/builder/CatalogueSheet.d.ts +13 -0
- package/dist/app/builder/CatalogueSheet.d.ts.map +1 -0
- package/dist/app/builder/CatalogueSheet.js +33 -0
- package/dist/app/builder/CatalogueSheet.js.map +1 -0
- package/dist/app/builder/PipelineStrip.d.ts +22 -0
- package/dist/app/builder/PipelineStrip.d.ts.map +1 -0
- package/dist/app/builder/PipelineStrip.js +31 -0
- package/dist/app/builder/PipelineStrip.js.map +1 -0
- package/dist/app/builder/PreviewPane.d.ts +13 -0
- package/dist/app/builder/PreviewPane.d.ts.map +1 -0
- package/dist/app/builder/PreviewPane.js +21 -0
- package/dist/app/builder/PreviewPane.js.map +1 -0
- package/dist/app/builder/SaveDialog.d.ts +14 -0
- package/dist/app/builder/SaveDialog.d.ts.map +1 -0
- package/dist/app/builder/SaveDialog.js +14 -0
- package/dist/app/builder/SaveDialog.js.map +1 -0
- package/dist/app/builder/Toolbar.d.ts +17 -0
- package/dist/app/builder/Toolbar.d.ts.map +1 -0
- package/dist/app/builder/Toolbar.js +15 -0
- package/dist/app/builder/Toolbar.js.map +1 -0
- package/dist/app/builder/Workspace.d.ts +28 -0
- package/dist/app/builder/Workspace.d.ts.map +1 -0
- package/dist/app/builder/Workspace.js +117 -0
- package/dist/app/builder/Workspace.js.map +1 -0
- package/dist/app/builder/builder-model.d.ts +86 -0
- package/dist/app/builder/builder-model.d.ts.map +1 -0
- package/dist/app/builder/builder-model.js +174 -0
- package/dist/app/builder/builder-model.js.map +1 -0
- package/dist/app/builder/builder-reducer.d.ts +71 -0
- package/dist/app/builder/builder-reducer.d.ts.map +1 -0
- package/dist/app/builder/builder-reducer.js +201 -0
- package/dist/app/builder/builder-reducer.js.map +1 -0
- package/dist/app/builder/labels.d.ts +78 -0
- package/dist/app/builder/labels.d.ts.map +1 -0
- package/dist/app/builder/labels.js +71 -0
- package/dist/app/builder/labels.js.map +1 -0
- package/dist/app/builder/status-chip.d.ts +10 -0
- package/dist/app/builder/status-chip.d.ts.map +1 -0
- package/dist/app/builder/status-chip.js +16 -0
- package/dist/app/builder/status-chip.js.map +1 -0
- package/dist/app/host-bridge.d.ts +211 -0
- package/dist/app/host-bridge.d.ts.map +1 -0
- package/dist/app/host-bridge.js +247 -0
- package/dist/app/host-bridge.js.map +1 -0
- package/dist/app/index.d.ts +16 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +18 -0
- package/dist/app/index.js.map +1 -0
- package/dist/app/layout-builder.d.ts +48 -0
- package/dist/app/layout-builder.d.ts.map +1 -0
- package/dist/app/layout-builder.js +418 -0
- package/dist/app/layout-builder.js.map +1 -0
- package/dist/app/mcp-app-view.d.ts +43 -0
- package/dist/app/mcp-app-view.d.ts.map +1 -0
- package/dist/app/mcp-app-view.js +169 -0
- package/dist/app/mcp-app-view.js.map +1 -0
- package/dist/app/mcp-toolkit-app.d.ts +18 -0
- package/dist/app/mcp-toolkit-app.d.ts.map +1 -0
- package/dist/app/mcp-toolkit-app.js +22 -0
- package/dist/app/mcp-toolkit-app.js.map +1 -0
- package/dist/app/use-host-actions.d.ts +57 -0
- package/dist/app/use-host-actions.d.ts.map +1 -0
- package/dist/app/use-host-actions.js +42 -0
- package/dist/app/use-host-actions.js.map +1 -0
- package/dist/app/widget-fixture.d.ts +180 -0
- package/dist/app/widget-fixture.d.ts.map +1 -0
- package/dist/app/widget-fixture.js +273 -0
- package/dist/app/widget-fixture.js.map +1 -0
- package/dist/app/widget-props-sheet.d.ts +20 -0
- package/dist/app/widget-props-sheet.d.ts.map +1 -0
- package/dist/app/widget-props-sheet.js +87 -0
- package/dist/app/widget-props-sheet.js.map +1 -0
- package/dist/app/widget-renderer.d.ts +87 -0
- package/dist/app/widget-renderer.d.ts.map +1 -0
- package/dist/app/widget-renderer.js +105 -0
- package/dist/app/widget-renderer.js.map +1 -0
- package/dist/components/DrillButton.d.ts +22 -0
- package/dist/components/DrillButton.d.ts.map +1 -0
- package/dist/components/DrillButton.js +13 -0
- package/dist/components/DrillButton.js.map +1 -0
- package/dist/components/FilterBar.d.ts +28 -0
- package/dist/components/FilterBar.d.ts.map +1 -0
- package/dist/components/FilterBar.js +12 -0
- package/dist/components/FilterBar.js.map +1 -0
- package/dist/components/GridLayout.d.ts +26 -0
- package/dist/components/GridLayout.d.ts.map +1 -0
- package/dist/components/GridLayout.js +31 -0
- package/dist/components/GridLayout.js.map +1 -0
- package/dist/components/GroupCard.d.ts +21 -0
- package/dist/components/GroupCard.d.ts.map +1 -0
- package/dist/components/GroupCard.js +24 -0
- package/dist/components/GroupCard.js.map +1 -0
- package/dist/components/KpiGrid.d.ts +43 -0
- package/dist/components/KpiGrid.d.ts.map +1 -0
- package/dist/components/KpiGrid.js +35 -0
- package/dist/components/KpiGrid.js.map +1 -0
- package/dist/components/ListFooter.d.ts +22 -0
- package/dist/components/ListFooter.d.ts.map +1 -0
- package/dist/components/ListFooter.js +13 -0
- package/dist/components/ListFooter.js.map +1 -0
- package/dist/components/Pills.d.ts +25 -0
- package/dist/components/Pills.d.ts.map +1 -0
- package/dist/components/Pills.js +17 -0
- package/dist/components/Pills.js.map +1 -0
- package/dist/components/SectionHeading.d.ts +16 -0
- package/dist/components/SectionHeading.d.ts.map +1 -0
- package/dist/components/SectionHeading.js +10 -0
- package/dist/components/SectionHeading.js.map +1 -0
- package/dist/components/WidgetHeader.d.ts +21 -0
- package/dist/components/WidgetHeader.d.ts.map +1 -0
- package/dist/components/WidgetHeader.js +11 -0
- package/dist/components/WidgetHeader.js.map +1 -0
- package/dist/hooks/index.d.ts +11 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/use-debounced-value.d.ts +7 -0
- package/dist/hooks/use-debounced-value.d.ts.map +1 -0
- package/dist/hooks/use-debounced-value.js +15 -0
- package/dist/hooks/use-debounced-value.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +6 -0
- package/dist/hooks/use-mobile.d.ts.map +1 -0
- package/dist/hooks/use-mobile.js +19 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/hooks/use-tool-query.d.ts +21 -0
- package/dist/hooks/use-tool-query.d.ts.map +1 -0
- package/dist/hooks/use-tool-query.js +47 -0
- package/dist/hooks/use-tool-query.js.map +1 -0
- package/dist/hooks/use-tool-result-recovery.d.ts +54 -0
- package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -0
- package/dist/hooks/use-tool-result-recovery.js +93 -0
- package/dist/hooks/use-tool-result-recovery.js.map +1 -0
- package/dist/hooks/use-view-data.d.ts +19 -0
- package/dist/hooks/use-view-data.d.ts.map +1 -0
- package/dist/hooks/use-view-data.js +18 -0
- package/dist/hooks/use-view-data.js.map +1 -0
- package/dist/hooks/use-view-tool-query.d.ts +18 -0
- package/dist/hooks/use-view-tool-query.d.ts.map +1 -0
- package/dist/hooks/use-view-tool-query.js +29 -0
- package/dist/hooks/use-view-tool-query.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/parse-tool-result.d.ts +39 -0
- package/dist/lib/parse-tool-result.d.ts.map +1 -0
- package/dist/lib/parse-tool-result.js +66 -0
- package/dist/lib/parse-tool-result.js.map +1 -0
- package/dist/lib/tone-utils.d.ts +18 -0
- package/dist/lib/tone-utils.d.ts.map +1 -0
- package/dist/lib/tone-utils.js +25 -0
- package/dist/lib/tone-utils.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/locale/index.d.ts +3 -0
- package/dist/locale/index.d.ts.map +1 -0
- package/dist/locale/index.js +7 -0
- package/dist/locale/index.js.map +1 -0
- package/dist/locale/locale-provider.d.ts +50 -0
- package/dist/locale/locale-provider.d.ts.map +1 -0
- package/dist/locale/locale-provider.js +47 -0
- package/dist/locale/locale-provider.js.map +1 -0
- package/dist/primitives/alert.d.ts +10 -0
- package/dist/primitives/alert.d.ts.map +1 -0
- package/dist/primitives/alert.js +25 -0
- package/dist/primitives/alert.js.map +1 -0
- package/dist/primitives/badge.d.ts +8 -0
- package/dist/primitives/badge.d.ts.map +1 -0
- package/dist/primitives/badge.js +34 -0
- package/dist/primitives/badge.js.map +1 -0
- package/dist/primitives/button.d.ts +11 -0
- package/dist/primitives/button.d.ts.map +1 -0
- package/dist/primitives/button.js +36 -0
- package/dist/primitives/button.js.map +1 -0
- package/dist/primitives/card.d.ts +12 -0
- package/dist/primitives/card.d.ts.map +1 -0
- package/dist/primitives/card.js +25 -0
- package/dist/primitives/card.js.map +1 -0
- package/dist/primitives/dialog.d.ts +18 -0
- package/dist/primitives/dialog.d.ts.map +1 -0
- package/dist/primitives/dialog.js +38 -0
- package/dist/primitives/dialog.js.map +1 -0
- package/dist/primitives/input.d.ts +4 -0
- package/dist/primitives/input.d.ts.map +1 -0
- package/dist/primitives/input.js +7 -0
- package/dist/primitives/input.js.map +1 -0
- package/dist/primitives/scroll-area.d.ts +6 -0
- package/dist/primitives/scroll-area.d.ts.map +1 -0
- package/dist/primitives/scroll-area.js +11 -0
- package/dist/primitives/scroll-area.js.map +1 -0
- package/dist/primitives/select.d.ts +16 -0
- package/dist/primitives/select.d.ts.map +1 -0
- package/dist/primitives/select.js +38 -0
- package/dist/primitives/select.js.map +1 -0
- package/dist/primitives/separator.d.ts +4 -0
- package/dist/primitives/separator.d.ts.map +1 -0
- package/dist/primitives/separator.js +9 -0
- package/dist/primitives/separator.js.map +1 -0
- package/dist/primitives/sheet.d.ts +15 -0
- package/dist/primitives/sheet.d.ts.map +1 -0
- package/dist/primitives/sheet.js +41 -0
- package/dist/primitives/sheet.js.map +1 -0
- package/dist/primitives/skeleton.d.ts +3 -0
- package/dist/primitives/skeleton.d.ts.map +1 -0
- package/dist/primitives/skeleton.js +7 -0
- package/dist/primitives/skeleton.js.map +1 -0
- package/dist/primitives/switch.d.ts +5 -0
- package/dist/primitives/switch.d.ts.map +1 -0
- package/dist/primitives/switch.js +8 -0
- package/dist/primitives/switch.js.map +1 -0
- package/dist/primitives/table.d.ts +11 -0
- package/dist/primitives/table.d.ts.map +1 -0
- package/dist/primitives/table.js +28 -0
- package/dist/primitives/table.js.map +1 -0
- package/dist/primitives/tabs.d.ts +12 -0
- package/dist/primitives/tabs.d.ts.map +1 -0
- package/dist/primitives/tabs.js +29 -0
- package/dist/primitives/tabs.js.map +1 -0
- package/dist/providers/query-provider.d.ts +11 -0
- package/dist/providers/query-provider.d.ts.map +1 -0
- package/dist/providers/query-provider.js +20 -0
- package/dist/providers/query-provider.js.map +1 -0
- package/dist/theme/create-theme.d.ts +130 -0
- package/dist/theme/create-theme.d.ts.map +1 -0
- package/dist/theme/create-theme.js +65 -0
- package/dist/theme/create-theme.js.map +1 -0
- package/dist/theme/index.d.ts +7 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +7 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/presets.d.ts +25 -0
- package/dist/theme/presets.d.ts.map +1 -0
- package/dist/theme/presets.js +68 -0
- package/dist/theme/presets.js.map +1 -0
- package/dist/theme/theme-provider.d.ts +69 -0
- package/dist/theme/theme-provider.d.ts.map +1 -0
- package/dist/theme/theme-provider.js +79 -0
- package/dist/theme/theme-provider.js.map +1 -0
- package/package.json +71 -0
- package/src/app/adapt-data-widget.tsx +51 -0
- package/src/app/builder/CatalogueSheet.tsx +230 -0
- package/src/app/builder/PipelineStrip.tsx +260 -0
- package/src/app/builder/PreviewPane.tsx +72 -0
- package/src/app/builder/SaveDialog.tsx +95 -0
- package/src/app/builder/Toolbar.tsx +95 -0
- package/src/app/builder/Workspace.tsx +483 -0
- package/src/app/builder/builder-model.test.ts +351 -0
- package/src/app/builder/builder-model.ts +213 -0
- package/src/app/builder/builder-reducer.test.ts +441 -0
- package/src/app/builder/builder-reducer.ts +289 -0
- package/src/app/builder/labels.ts +139 -0
- package/src/app/builder/status-chip.tsx +37 -0
- package/src/app/host-bridge.test.ts +206 -0
- package/src/app/host-bridge.tsx +371 -0
- package/src/app/index.ts +51 -0
- package/src/app/layout-builder.tsx +872 -0
- package/src/app/mcp-app-view.tsx +367 -0
- package/src/app/mcp-toolkit-app.tsx +25 -0
- package/src/app/use-host-actions.test.ts +16 -0
- package/src/app/use-host-actions.tsx +88 -0
- package/src/app/widget-fixture.test.ts +108 -0
- package/src/app/widget-fixture.tsx +441 -0
- package/src/app/widget-props-sheet.tsx +285 -0
- package/src/app/widget-renderer.test.ts +42 -0
- package/src/app/widget-renderer.tsx +200 -0
- package/src/components/DrillButton.tsx +37 -0
- package/src/components/FilterBar.tsx +68 -0
- package/src/components/GridLayout.tsx +49 -0
- package/src/components/GroupCard.tsx +52 -0
- package/src/components/KpiGrid.tsx +134 -0
- package/src/components/ListFooter.tsx +48 -0
- package/src/components/Pills.tsx +45 -0
- package/src/components/SectionHeading.tsx +26 -0
- package/src/components/WidgetHeader.tsx +51 -0
- package/src/globals.css +168 -0
- package/src/hooks/index.ts +17 -0
- package/src/hooks/use-debounced-value.ts +15 -0
- package/src/hooks/use-mobile.ts +24 -0
- package/src/hooks/use-tool-query.ts +63 -0
- package/src/hooks/use-tool-result-recovery.test.ts +65 -0
- package/src/hooks/use-tool-result-recovery.ts +144 -0
- package/src/hooks/use-view-data.ts +31 -0
- package/src/hooks/use-view-tool-query.ts +38 -0
- package/src/index.ts +134 -0
- package/src/lib/parse-tool-result.test.ts +139 -0
- package/src/lib/parse-tool-result.ts +89 -0
- package/src/lib/tone-utils.test.ts +35 -0
- package/src/lib/tone-utils.ts +38 -0
- package/src/lib/utils.ts +6 -0
- package/src/locale/index.ts +7 -0
- package/src/locale/locale-provider.tsx +81 -0
- package/src/primitives/alert.tsx +60 -0
- package/src/primitives/badge.tsx +49 -0
- package/src/primitives/button.tsx +62 -0
- package/src/primitives/card.tsx +92 -0
- package/src/primitives/dialog.tsx +144 -0
- package/src/primitives/input.tsx +21 -0
- package/src/primitives/scroll-area.tsx +54 -0
- package/src/primitives/select.tsx +173 -0
- package/src/primitives/separator.tsx +21 -0
- package/src/primitives/sheet.tsx +134 -0
- package/src/primitives/skeleton.tsx +13 -0
- package/src/primitives/switch.tsx +24 -0
- package/src/primitives/table.tsx +87 -0
- package/src/primitives/tabs.tsx +79 -0
- package/src/providers/query-provider.tsx +38 -0
- package/src/theme/create-theme.test.ts +108 -0
- package/src/theme/create-theme.ts +158 -0
- package/src/theme/index.ts +9 -0
- package/src/theme/presets.ts +82 -0
- package/src/theme/theme-provider.tsx +152 -0
- package/src/ui-catalog.test.ts +112 -0
- package/ui-catalog.json +1280 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure reducer for the LayoutBuilder draft state.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the `setDraft((prev) => …)` closures that previously lived in
|
|
5
|
+
* `layout-builder.tsx`, plus the `setState`-inside-an-updater anti-pattern
|
|
6
|
+
* those closures used to keep `activeTabIndex` / `focusedRowIndex` in sync
|
|
7
|
+
* with structural draft changes. Those index updates are now part of each
|
|
8
|
+
* action's pure result, so the whole thing is testable without React.
|
|
9
|
+
*/
|
|
10
|
+
import type { RowDef } from "@miragon/mcp-toolkit-core"
|
|
11
|
+
import { emptyRows, type DraftLayout, type DraftTab } from "./builder-model.js"
|
|
12
|
+
|
|
13
|
+
// -------------------------------------------------------------------------- //
|
|
14
|
+
// State
|
|
15
|
+
// -------------------------------------------------------------------------- //
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The draft plus the UI indices that are coupled to draft mutations.
|
|
19
|
+
* `editingTabIdx` is the inline-rename target; it is cleared whenever a
|
|
20
|
+
* structural tab change could invalidate it.
|
|
21
|
+
*/
|
|
22
|
+
export interface BuilderDraftState {
|
|
23
|
+
draft: DraftLayout
|
|
24
|
+
activeTabIndex: number
|
|
25
|
+
focusedRowIndex: number
|
|
26
|
+
editingTabIdx: number | null
|
|
27
|
+
/**
|
|
28
|
+
* Monotonic counter bumped whenever a `renameTab` action is rejected
|
|
29
|
+
* (duplicate label). The UI watches it to surface feedback; a counter
|
|
30
|
+
* rather than a boolean so two consecutive identical rejections still
|
|
31
|
+
* register as a change.
|
|
32
|
+
*/
|
|
33
|
+
renameRejectedAt: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function initBuilderState(draft: DraftLayout): BuilderDraftState {
|
|
37
|
+
return {
|
|
38
|
+
draft,
|
|
39
|
+
activeTabIndex: 0,
|
|
40
|
+
focusedRowIndex: 0,
|
|
41
|
+
editingTabIdx: null,
|
|
42
|
+
renameRejectedAt: 0,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// -------------------------------------------------------------------------- //
|
|
47
|
+
// Actions
|
|
48
|
+
// -------------------------------------------------------------------------- //
|
|
49
|
+
|
|
50
|
+
export type BuilderAction =
|
|
51
|
+
| { type: "addWidgetToRow"; widgetId: string; defaultSpan: number; targetRowIdx?: number }
|
|
52
|
+
| { type: "addRow" }
|
|
53
|
+
| { type: "removeRow"; rowIdx: number }
|
|
54
|
+
| { type: "moveRow"; rowIdx: number; direction: -1 | 1 }
|
|
55
|
+
| { type: "setCellSpan"; rowIdx: number; cellIdx: number; span: number }
|
|
56
|
+
| { type: "removeCell"; rowIdx: number; cellIdx: number }
|
|
57
|
+
| {
|
|
58
|
+
type: "setCellProps"
|
|
59
|
+
rowIdx: number
|
|
60
|
+
cellIdx: number
|
|
61
|
+
next: Record<string, unknown> | undefined
|
|
62
|
+
}
|
|
63
|
+
| { type: "setFocusedRow"; rowIdx: number }
|
|
64
|
+
| { type: "selectTab"; tabIdx: number }
|
|
65
|
+
| { type: "startEditingTab"; tabIdx: number }
|
|
66
|
+
| { type: "stopEditingTab" }
|
|
67
|
+
| { type: "addTab" }
|
|
68
|
+
| { type: "renameTab"; tabIdx: number; label: string }
|
|
69
|
+
| { type: "removeTab"; tabIdx: number }
|
|
70
|
+
|
|
71
|
+
// -------------------------------------------------------------------------- //
|
|
72
|
+
// Helpers
|
|
73
|
+
// -------------------------------------------------------------------------- //
|
|
74
|
+
|
|
75
|
+
/** The rows surface the active tab (or the flat draft) currently exposes. */
|
|
76
|
+
function activeRowsOf(draft: DraftLayout, activeTabIndex: number): RowDef[] {
|
|
77
|
+
return draft.kind === "tabs" ? (draft.tabs[activeTabIndex]?.rows ?? []) : draft.rows
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Clamp a focused-row index into the bounds of `rows`, never below 0. */
|
|
81
|
+
function clampRowIndex(rowIndex: number, rowCount: number): number {
|
|
82
|
+
return Math.max(0, Math.min(rowIndex, rowCount - 1))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Applies a pure rows-transform to whichever surface is active (the flat
|
|
87
|
+
* rows or the active tab's rows) and returns the new draft.
|
|
88
|
+
*/
|
|
89
|
+
function mapActiveRows(
|
|
90
|
+
draft: DraftLayout,
|
|
91
|
+
activeTabIndex: number,
|
|
92
|
+
fn: (rows: RowDef[]) => RowDef[],
|
|
93
|
+
): DraftLayout {
|
|
94
|
+
if (draft.kind === "rows") return { kind: "rows", rows: fn(draft.rows) }
|
|
95
|
+
const tabs = draft.tabs.map((t, idx) => (idx === activeTabIndex ? { ...t, rows: fn(t.rows) } : t))
|
|
96
|
+
return { kind: "tabs", tabs }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// -------------------------------------------------------------------------- //
|
|
100
|
+
// Reducer
|
|
101
|
+
// -------------------------------------------------------------------------- //
|
|
102
|
+
|
|
103
|
+
export function draftReducer(state: BuilderDraftState, action: BuilderAction): BuilderDraftState {
|
|
104
|
+
switch (action.type) {
|
|
105
|
+
case "addWidgetToRow": {
|
|
106
|
+
const rows = activeRowsOf(state.draft, state.activeTabIndex)
|
|
107
|
+
const safeLen = rows.length > 0 ? rows.length : 1
|
|
108
|
+
const idx = Math.min(Math.max(action.targetRowIdx ?? state.focusedRowIndex, 0), safeLen - 1)
|
|
109
|
+
const draft = mapActiveRows(state.draft, state.activeTabIndex, (rs) => {
|
|
110
|
+
const safeRows = rs.length > 0 ? rs : emptyRows()
|
|
111
|
+
return safeRows.map((r, i) =>
|
|
112
|
+
i === idx
|
|
113
|
+
? { row: [...r.row, { widget: action.widgetId, span: action.defaultSpan }] }
|
|
114
|
+
: r,
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
return { ...state, draft, focusedRowIndex: idx }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case "addRow": {
|
|
121
|
+
let newIndex = state.focusedRowIndex
|
|
122
|
+
const draft = mapActiveRows(state.draft, state.activeTabIndex, (rows) => {
|
|
123
|
+
const next = [...rows, { row: [] }]
|
|
124
|
+
newIndex = next.length - 1
|
|
125
|
+
return next
|
|
126
|
+
})
|
|
127
|
+
return { ...state, draft, focusedRowIndex: newIndex }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case "removeRow": {
|
|
131
|
+
const draft = mapActiveRows(state.draft, state.activeTabIndex, (rows) => {
|
|
132
|
+
const next = rows.filter((_, i) => i !== action.rowIdx)
|
|
133
|
+
return next.length > 0 ? next : emptyRows()
|
|
134
|
+
})
|
|
135
|
+
// Matches the previous behaviour: decrement, floored at 0. Finding [9]
|
|
136
|
+
// clamping is scoped to tab switch / removeTab, so this stays as-is.
|
|
137
|
+
return { ...state, draft, focusedRowIndex: Math.max(0, state.focusedRowIndex - 1) }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case "moveRow": {
|
|
141
|
+
const target = action.rowIdx + action.direction
|
|
142
|
+
const rows = activeRowsOf(state.draft, state.activeTabIndex)
|
|
143
|
+
if (target < 0 || target >= rows.length) return state
|
|
144
|
+
const draft = mapActiveRows(state.draft, state.activeTabIndex, (rs) => {
|
|
145
|
+
const next = [...rs]
|
|
146
|
+
const [moved] = next.splice(action.rowIdx, 1)
|
|
147
|
+
if (!moved) return rs
|
|
148
|
+
next.splice(target, 0, moved)
|
|
149
|
+
return next
|
|
150
|
+
})
|
|
151
|
+
return { ...state, draft, focusedRowIndex: target }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
case "setCellSpan": {
|
|
155
|
+
const clamped = Math.max(1, Math.min(12, action.span))
|
|
156
|
+
const draft = mapActiveRows(state.draft, state.activeTabIndex, (rows) =>
|
|
157
|
+
rows.map((r, i) =>
|
|
158
|
+
i === action.rowIdx
|
|
159
|
+
? { row: r.row.map((c, j) => (j === action.cellIdx ? { ...c, span: clamped } : c)) }
|
|
160
|
+
: r,
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
return { ...state, draft }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case "removeCell": {
|
|
167
|
+
const draft = mapActiveRows(state.draft, state.activeTabIndex, (rows) =>
|
|
168
|
+
rows.map((r, i) =>
|
|
169
|
+
i === action.rowIdx ? { row: r.row.filter((_, j) => j !== action.cellIdx) } : r,
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
return { ...state, draft }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case "setCellProps": {
|
|
176
|
+
const draft = mapActiveRows(state.draft, state.activeTabIndex, (rows) =>
|
|
177
|
+
rows.map((r, i) =>
|
|
178
|
+
i === action.rowIdx
|
|
179
|
+
? {
|
|
180
|
+
row: r.row.map((c, j) => {
|
|
181
|
+
if (j !== action.cellIdx) return c
|
|
182
|
+
if (action.next === undefined) {
|
|
183
|
+
const rest: typeof c = { widget: c.widget }
|
|
184
|
+
if (c.span !== undefined) rest.span = c.span
|
|
185
|
+
return rest
|
|
186
|
+
}
|
|
187
|
+
return { ...c, props: action.next }
|
|
188
|
+
}),
|
|
189
|
+
}
|
|
190
|
+
: r,
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
return { ...state, draft }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
case "setFocusedRow":
|
|
197
|
+
return { ...state, focusedRowIndex: Math.max(0, action.rowIdx) }
|
|
198
|
+
|
|
199
|
+
case "selectTab": {
|
|
200
|
+
if (state.draft.kind !== "tabs") return state
|
|
201
|
+
if (action.tabIdx < 0 || action.tabIdx >= state.draft.tabs.length) return state
|
|
202
|
+
const rows = state.draft.tabs[action.tabIdx]?.rows ?? []
|
|
203
|
+
return {
|
|
204
|
+
...state,
|
|
205
|
+
activeTabIndex: action.tabIdx,
|
|
206
|
+
focusedRowIndex: clampRowIndex(state.focusedRowIndex, rows.length),
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case "startEditingTab":
|
|
211
|
+
return { ...state, editingTabIdx: action.tabIdx }
|
|
212
|
+
|
|
213
|
+
case "stopEditingTab":
|
|
214
|
+
return { ...state, editingTabIdx: null }
|
|
215
|
+
|
|
216
|
+
case "addTab": {
|
|
217
|
+
if (state.draft.kind === "tabs") {
|
|
218
|
+
const label = `Tab ${state.draft.tabs.length + 1}`
|
|
219
|
+
const nextTabs: DraftTab[] = [...state.draft.tabs, { label, rows: emptyRows() }]
|
|
220
|
+
return {
|
|
221
|
+
...state,
|
|
222
|
+
draft: { kind: "tabs", tabs: nextTabs },
|
|
223
|
+
activeTabIndex: nextTabs.length - 1,
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const seededTabs: DraftTab[] = [
|
|
227
|
+
{ label: "Tab 1", rows: state.draft.rows },
|
|
228
|
+
{ label: "Tab 2", rows: emptyRows() },
|
|
229
|
+
]
|
|
230
|
+
return { ...state, draft: { kind: "tabs", tabs: seededTabs }, activeTabIndex: 1 }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case "renameTab": {
|
|
234
|
+
const trimmed = action.label.trim()
|
|
235
|
+
if (!trimmed) return state
|
|
236
|
+
if (state.draft.kind !== "tabs") return state
|
|
237
|
+
// Reject duplicates — Tabs keys by label, collisions break the
|
|
238
|
+
// component. Signal the rejection so the UI can give feedback; the
|
|
239
|
+
// old label stays put.
|
|
240
|
+
if (state.draft.tabs.some((t, i) => i !== action.tabIdx && t.label === trimmed)) {
|
|
241
|
+
return { ...state, renameRejectedAt: state.renameRejectedAt + 1 }
|
|
242
|
+
}
|
|
243
|
+
const tabs = state.draft.tabs.map((t, i) =>
|
|
244
|
+
i === action.tabIdx ? { ...t, label: trimmed } : t,
|
|
245
|
+
)
|
|
246
|
+
return { ...state, draft: { kind: "tabs", tabs } }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case "removeTab": {
|
|
250
|
+
if (state.draft.kind !== "tabs") return state
|
|
251
|
+
const tabs = state.draft.tabs.filter((_, i) => i !== action.tabIdx)
|
|
252
|
+
const activeTabIndex = clampRowIndex(state.activeTabIndex, tabs.length)
|
|
253
|
+
if (tabs.length === 0) {
|
|
254
|
+
const draft: DraftLayout = { kind: "rows", rows: emptyRows() }
|
|
255
|
+
return {
|
|
256
|
+
...state,
|
|
257
|
+
draft,
|
|
258
|
+
activeTabIndex: 0,
|
|
259
|
+
editingTabIdx: null,
|
|
260
|
+
focusedRowIndex: clampRowIndex(state.focusedRowIndex, draft.rows.length),
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (tabs.length === 1 && tabs[0]) {
|
|
264
|
+
const draft: DraftLayout = { kind: "rows", rows: tabs[0].rows }
|
|
265
|
+
return {
|
|
266
|
+
...state,
|
|
267
|
+
draft,
|
|
268
|
+
activeTabIndex: 0,
|
|
269
|
+
editingTabIdx: null,
|
|
270
|
+
focusedRowIndex: clampRowIndex(state.focusedRowIndex, draft.rows.length),
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const rows = tabs[activeTabIndex]?.rows ?? []
|
|
274
|
+
return {
|
|
275
|
+
...state,
|
|
276
|
+
draft: { kind: "tabs", tabs },
|
|
277
|
+
activeTabIndex,
|
|
278
|
+
editingTabIdx: null,
|
|
279
|
+
focusedRowIndex: clampRowIndex(state.focusedRowIndex, rows.length),
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
default: {
|
|
284
|
+
// Exhaustiveness guard: a new action type without a case is a type error.
|
|
285
|
+
const _never: never = action
|
|
286
|
+
return _never
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared label contract for the visual LayoutBuilder.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `layout-builder.tsx` so the presentational sub-components
|
|
5
|
+
* (Toolbar, PipelineStrip, Workspace, CatalogueSheet, SaveDialog, PreviewPane)
|
|
6
|
+
* can depend on the labels without importing the orchestrating container —
|
|
7
|
+
* which would create an import cycle. React-free and `mcp-use`-free.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface LayoutBuilderLabels {
|
|
11
|
+
title?: string
|
|
12
|
+
builderBadge?: string
|
|
13
|
+
previewBadge?: string
|
|
14
|
+
catalogue?: string
|
|
15
|
+
save?: string
|
|
16
|
+
done?: string
|
|
17
|
+
viewLayoutTab?: string
|
|
18
|
+
viewPipelineTab?: string
|
|
19
|
+
viewPreviewTab?: string
|
|
20
|
+
saveDialogTitle?: string
|
|
21
|
+
saveDialogDescription?: string
|
|
22
|
+
saveNameLabel?: string
|
|
23
|
+
saveDescriptionLabel?: string
|
|
24
|
+
saveConfirm?: string
|
|
25
|
+
cancel?: string
|
|
26
|
+
pipelineHeader?: string
|
|
27
|
+
keysHeader?: string
|
|
28
|
+
stepsHeader?: string
|
|
29
|
+
keyName?: string
|
|
30
|
+
keyValue?: string
|
|
31
|
+
stepContextId?: string
|
|
32
|
+
stepPickerLabel?: string
|
|
33
|
+
addKey?: string
|
|
34
|
+
addStep?: string
|
|
35
|
+
emptyKeys?: string
|
|
36
|
+
emptySteps?: string
|
|
37
|
+
statusUpToDate?: string
|
|
38
|
+
statusPending?: string
|
|
39
|
+
statusRefreshing?: string
|
|
40
|
+
paletteHeader?: string
|
|
41
|
+
paletteEmpty?: string
|
|
42
|
+
canvasEmpty?: string
|
|
43
|
+
canvasDropHint?: string
|
|
44
|
+
addRow?: string
|
|
45
|
+
addTab?: string
|
|
46
|
+
removeRow?: string
|
|
47
|
+
renameTab?: string
|
|
48
|
+
removeTab?: string
|
|
49
|
+
tabsLabel?: string
|
|
50
|
+
catalogueDescription?: string
|
|
51
|
+
catalogueKeys?: string
|
|
52
|
+
catalogueSteps?: string
|
|
53
|
+
catalogueUnreachable?: string
|
|
54
|
+
catalogueUnreachableEmpty?: string
|
|
55
|
+
catalogueProducedBy?: string
|
|
56
|
+
catalogueConsumedBy?: string
|
|
57
|
+
catalogueNeedsKeys?: string
|
|
58
|
+
cataloguePickKey?: string
|
|
59
|
+
catalogueAddProducingStep?: string
|
|
60
|
+
catalogueLive?: string
|
|
61
|
+
catalogueMissing?: string
|
|
62
|
+
/** Banner shown when the catalogue fetch fails (prefixes the reason). */
|
|
63
|
+
catalogueError?: string
|
|
64
|
+
/** Heading for the pipeline-validation warning banner. */
|
|
65
|
+
validationWarning?: string
|
|
66
|
+
/** Inline message when a save fails (prefixes the reason). */
|
|
67
|
+
saveError?: string
|
|
68
|
+
/** Inline message when a tab rename is rejected (duplicate label). */
|
|
69
|
+
renameDuplicate?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const DEFAULT_LABELS: Required<LayoutBuilderLabels> = {
|
|
73
|
+
title: "View Builder",
|
|
74
|
+
builderBadge: "Builder",
|
|
75
|
+
previewBadge: "Preview",
|
|
76
|
+
catalogue: "Catalogue",
|
|
77
|
+
save: "Save",
|
|
78
|
+
done: "Done",
|
|
79
|
+
viewLayoutTab: "Layout",
|
|
80
|
+
viewPipelineTab: "Pipeline",
|
|
81
|
+
viewPreviewTab: "Preview",
|
|
82
|
+
saveDialogTitle: "Save dashboard",
|
|
83
|
+
saveDialogDescription:
|
|
84
|
+
"Persists the current layout along with the keys and steps that populate it.",
|
|
85
|
+
saveNameLabel: "Name",
|
|
86
|
+
saveDescriptionLabel: "Description",
|
|
87
|
+
saveConfirm: "Save dashboard",
|
|
88
|
+
cancel: "Cancel",
|
|
89
|
+
pipelineHeader: "Pipeline",
|
|
90
|
+
keysHeader: "Keys",
|
|
91
|
+
stepsHeader: "Steps",
|
|
92
|
+
keyName: "key",
|
|
93
|
+
keyValue: 'value, "literal", true, {…}',
|
|
94
|
+
stepContextId: "ctx id",
|
|
95
|
+
stepPickerLabel: "pick a step",
|
|
96
|
+
addKey: "Add key",
|
|
97
|
+
addStep: "Add step",
|
|
98
|
+
emptyKeys: "No keys seeded.",
|
|
99
|
+
emptySteps: "No steps configured.",
|
|
100
|
+
statusUpToDate: "Up to date",
|
|
101
|
+
statusPending: "Applying soon…",
|
|
102
|
+
statusRefreshing: "Applying…",
|
|
103
|
+
paletteHeader: "Palette",
|
|
104
|
+
paletteEmpty: "No widgets reachable yet — seed a key or add a step.",
|
|
105
|
+
canvasEmpty: "Drag a widget here, or click one in the palette.",
|
|
106
|
+
canvasDropHint: "Drop to add",
|
|
107
|
+
addRow: "Add row",
|
|
108
|
+
addTab: "Add tab",
|
|
109
|
+
removeRow: "Remove row",
|
|
110
|
+
renameTab: "Rename tab",
|
|
111
|
+
removeTab: "Remove tab",
|
|
112
|
+
tabsLabel: "Tabs",
|
|
113
|
+
catalogueDescription:
|
|
114
|
+
"Every key and widget the framework knows about, whether or not it's reachable right now.",
|
|
115
|
+
catalogueKeys: "Keys",
|
|
116
|
+
catalogueSteps: "Steps",
|
|
117
|
+
catalogueUnreachable: "Unreachable widgets",
|
|
118
|
+
catalogueUnreachableEmpty: "Every registered widget is reachable.",
|
|
119
|
+
catalogueProducedBy: "produced by",
|
|
120
|
+
catalogueConsumedBy: "consumed by",
|
|
121
|
+
catalogueNeedsKeys: "needs",
|
|
122
|
+
cataloguePickKey: "Add as key",
|
|
123
|
+
catalogueAddProducingStep: "Add producing step",
|
|
124
|
+
catalogueLive: "live",
|
|
125
|
+
catalogueMissing: "missing",
|
|
126
|
+
catalogueError: "Couldn't refresh the builder catalogue",
|
|
127
|
+
validationWarning: "This pipeline won't render as configured",
|
|
128
|
+
saveError: "Couldn't save",
|
|
129
|
+
renameDuplicate: "A tab with that name already exists.",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* MIME type used to carry a widget id across the palette → canvas drag.
|
|
134
|
+
* Shared between the palette items and the canvas drop targets.
|
|
135
|
+
*/
|
|
136
|
+
export const WIDGET_DRAG_MIME = "application/x-mcp-widget-id"
|
|
137
|
+
|
|
138
|
+
/** Refresh status for the auto-apply chip and the pipeline strip. */
|
|
139
|
+
export type RefreshStatus = "idle" | "pending" | "refreshing"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CircleDot, Loader2 } from "lucide-react"
|
|
2
|
+
import type { LayoutBuilderLabels, RefreshStatus } from "./labels.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compact auto-apply status chip. Shared by the Toolbar and the
|
|
6
|
+
* PipelineStrip so both surfaces show the same refresh state.
|
|
7
|
+
*/
|
|
8
|
+
export function StatusChip({
|
|
9
|
+
status,
|
|
10
|
+
L,
|
|
11
|
+
}: {
|
|
12
|
+
status: RefreshStatus
|
|
13
|
+
L: Required<LayoutBuilderLabels>
|
|
14
|
+
}) {
|
|
15
|
+
if (status === "idle") {
|
|
16
|
+
return (
|
|
17
|
+
<span className="text-muted-foreground inline-flex items-center gap-1.5">
|
|
18
|
+
<CircleDot className="size-3" />
|
|
19
|
+
{L.statusUpToDate}
|
|
20
|
+
</span>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
if (status === "pending") {
|
|
24
|
+
return (
|
|
25
|
+
<span className="inline-flex items-center gap-1.5 text-amber-600 dark:text-amber-400">
|
|
26
|
+
<span className="size-1.5 animate-pulse rounded-full bg-amber-500" />
|
|
27
|
+
{L.statusPending}
|
|
28
|
+
</span>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
return (
|
|
32
|
+
<span className="text-primary inline-flex items-center gap-1.5">
|
|
33
|
+
<Loader2 className="size-3 animate-spin" />
|
|
34
|
+
{L.statusRefreshing}
|
|
35
|
+
</span>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
createChatGptHostBridge,
|
|
4
|
+
createStandaloneHostBridge,
|
|
5
|
+
type OpenAiAppsSdk,
|
|
6
|
+
} from "./host-bridge.js"
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe("createChatGptHostBridge — no window.openai (defensive guards)", () => {
|
|
13
|
+
// Passing `null` exercises the fully-absent SDK path without touching window.
|
|
14
|
+
it("rejects callTool with a clear error instead of throwing synchronously", async () => {
|
|
15
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
16
|
+
const bridge = createChatGptHostBridge(null)
|
|
17
|
+
|
|
18
|
+
await expect(bridge.callTool("do_thing", {})).rejects.toThrow(
|
|
19
|
+
/OpenAI Apps SDK callTool is unavailable.*do_thing/,
|
|
20
|
+
)
|
|
21
|
+
expect(warn).toHaveBeenCalled()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("sendFollowup is a logged no-op (no throw) when the SDK is absent", () => {
|
|
25
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
26
|
+
const bridge = createChatGptHostBridge(null)
|
|
27
|
+
|
|
28
|
+
expect(() => bridge.sendFollowup("hello")).not.toThrow()
|
|
29
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("sendFollowUpMessage unavailable"))
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("setModelContext is a logged no-op (no throw) when the SDK is absent", () => {
|
|
33
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
34
|
+
const bridge = createChatGptHostBridge(null)
|
|
35
|
+
|
|
36
|
+
expect(() => bridge.setModelContext?.("ctx")).not.toThrow()
|
|
37
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("setWidgetState unavailable"))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("getWidgetData returns null when the SDK is absent", () => {
|
|
41
|
+
const bridge = createChatGptHostBridge(null)
|
|
42
|
+
expect(bridge.getWidgetData()).toBeNull()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("leaves theme undefined when the SDK reports none", () => {
|
|
46
|
+
const bridge = createChatGptHostBridge(null)
|
|
47
|
+
expect(bridge.theme).toBeUndefined()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe("createChatGptHostBridge — with a stubbed SDK", () => {
|
|
52
|
+
it("routes callTool through window.openai.callTool", async () => {
|
|
53
|
+
const sdk: OpenAiAppsSdk = {
|
|
54
|
+
callTool: vi.fn().mockResolvedValue({ structuredContent: { ok: true } }),
|
|
55
|
+
}
|
|
56
|
+
const bridge = createChatGptHostBridge(sdk)
|
|
57
|
+
|
|
58
|
+
const res = await bridge.callTool("get_thing", { id: 1 })
|
|
59
|
+
|
|
60
|
+
expect(sdk.callTool).toHaveBeenCalledWith("get_thing", { id: 1 })
|
|
61
|
+
expect(res).toEqual({ structuredContent: { ok: true } })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("calls sendFollowUpMessage with the { prompt } shape", () => {
|
|
65
|
+
const sendFollowUpMessage = vi.fn().mockReturnValue(undefined)
|
|
66
|
+
const bridge = createChatGptHostBridge({ sendFollowUpMessage })
|
|
67
|
+
|
|
68
|
+
bridge.sendFollowup("show details")
|
|
69
|
+
|
|
70
|
+
expect(sendFollowUpMessage).toHaveBeenCalledWith({ prompt: "show details" })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("falls back to the bare-string signature when { prompt } throws", () => {
|
|
74
|
+
const calls: unknown[] = []
|
|
75
|
+
const sendFollowUpMessage = vi.fn((arg: { prompt: string } | string) => {
|
|
76
|
+
calls.push(arg)
|
|
77
|
+
// Reject the object form to force the string fallback.
|
|
78
|
+
if (typeof arg !== "string") throw new Error("object form unsupported")
|
|
79
|
+
})
|
|
80
|
+
const bridge = createChatGptHostBridge({ sendFollowUpMessage })
|
|
81
|
+
|
|
82
|
+
bridge.sendFollowup("hi")
|
|
83
|
+
|
|
84
|
+
expect(calls).toEqual([{ prompt: "hi" }, "hi"])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("accepts the alternate sendFollowupMessage spelling", () => {
|
|
88
|
+
const sendFollowupMessage = vi.fn()
|
|
89
|
+
const bridge = createChatGptHostBridge({ sendFollowupMessage })
|
|
90
|
+
|
|
91
|
+
bridge.sendFollowup("hi")
|
|
92
|
+
|
|
93
|
+
expect(sendFollowupMessage).toHaveBeenCalledWith({ prompt: "hi" })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("opens external links via openExternal with the { href } shape", () => {
|
|
97
|
+
const openExternal = vi.fn()
|
|
98
|
+
const bridge = createChatGptHostBridge({ openExternal })
|
|
99
|
+
|
|
100
|
+
bridge.openExternal("https://example.com")
|
|
101
|
+
|
|
102
|
+
expect(openExternal).toHaveBeenCalledWith({ href: "https://example.com" })
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("prefers toolOutput over toolResponseMetadata for widget data", () => {
|
|
106
|
+
const bridge = createChatGptHostBridge({
|
|
107
|
+
toolOutput: { a: 1 },
|
|
108
|
+
toolResponseMetadata: { b: 2 },
|
|
109
|
+
})
|
|
110
|
+
expect(bridge.getWidgetData()).toEqual({ a: 1 })
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("falls back to toolResponseMetadata when toolOutput is absent", () => {
|
|
114
|
+
const bridge = createChatGptHostBridge({ toolResponseMetadata: { b: 2 } })
|
|
115
|
+
expect(bridge.getWidgetData()).toEqual({ b: 2 })
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("normalizes the theme to light | dark | undefined", () => {
|
|
119
|
+
expect(createChatGptHostBridge({ theme: "dark" }).theme).toBe("dark")
|
|
120
|
+
expect(createChatGptHostBridge({ theme: "light" }).theme).toBe("light")
|
|
121
|
+
expect(createChatGptHostBridge({ theme: "sepia" }).theme).toBeUndefined()
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe("createStandaloneHostBridge", () => {
|
|
126
|
+
it("routes callTool through the injected function", async () => {
|
|
127
|
+
const callTool = vi.fn().mockResolvedValue({ structuredContent: { id: "x" } })
|
|
128
|
+
const bridge = createStandaloneHostBridge({ callTool })
|
|
129
|
+
|
|
130
|
+
const res = await bridge.callTool("find", { q: "a" })
|
|
131
|
+
|
|
132
|
+
expect(callTool).toHaveBeenCalledWith("find", { q: "a" })
|
|
133
|
+
expect(res).toEqual({ structuredContent: { id: "x" } })
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("reads static getData", () => {
|
|
137
|
+
const bridge = createStandaloneHostBridge({
|
|
138
|
+
callTool: vi.fn(),
|
|
139
|
+
getData: { name: "Acme" },
|
|
140
|
+
})
|
|
141
|
+
expect(bridge.getWidgetData()).toEqual({ name: "Acme" })
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it("re-reads getData when it is a getter", () => {
|
|
145
|
+
let n = 0
|
|
146
|
+
const bridge = createStandaloneHostBridge({
|
|
147
|
+
callTool: vi.fn(),
|
|
148
|
+
getData: () => ({ n: ++n }),
|
|
149
|
+
})
|
|
150
|
+
expect(bridge.getWidgetData()).toEqual({ n: 1 })
|
|
151
|
+
expect(bridge.getWidgetData()).toEqual({ n: 2 })
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it("returns null from getWidgetData when no getData is supplied", () => {
|
|
155
|
+
const bridge = createStandaloneHostBridge({ callTool: vi.fn() })
|
|
156
|
+
expect(bridge.getWidgetData()).toBeNull()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("invokes onFollowup when supplied", () => {
|
|
160
|
+
const onFollowup = vi.fn()
|
|
161
|
+
const bridge = createStandaloneHostBridge({ callTool: vi.fn(), onFollowup })
|
|
162
|
+
|
|
163
|
+
bridge.sendFollowup("analyse this")
|
|
164
|
+
|
|
165
|
+
expect(onFollowup).toHaveBeenCalledWith("analyse this")
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it("logs and drops the prompt when no onFollowup is supplied", () => {
|
|
169
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
170
|
+
const bridge = createStandaloneHostBridge({ callTool: vi.fn() })
|
|
171
|
+
|
|
172
|
+
expect(() => bridge.sendFollowup("dropped")).not.toThrow()
|
|
173
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("no onFollowup handler"))
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it("invokes onOpenExternal when supplied", () => {
|
|
177
|
+
const onOpenExternal = vi.fn()
|
|
178
|
+
const bridge = createStandaloneHostBridge({ callTool: vi.fn(), onOpenExternal })
|
|
179
|
+
|
|
180
|
+
bridge.openExternal("https://x.test")
|
|
181
|
+
|
|
182
|
+
expect(onOpenExternal).toHaveBeenCalledWith("https://x.test")
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it("routes setModelContext to onModelContext when supplied", () => {
|
|
186
|
+
const onModelContext = vi.fn()
|
|
187
|
+
const bridge = createStandaloneHostBridge({ callTool: vi.fn(), onModelContext })
|
|
188
|
+
|
|
189
|
+
bridge.setModelContext?.("selected row 3")
|
|
190
|
+
|
|
191
|
+
expect(onModelContext).toHaveBeenCalledWith("selected row 3")
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it("logs and drops model context when no onModelContext is supplied", () => {
|
|
195
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
196
|
+
const bridge = createStandaloneHostBridge({ callTool: vi.fn() })
|
|
197
|
+
|
|
198
|
+
expect(() => bridge.setModelContext?.("ctx")).not.toThrow()
|
|
199
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("no onModelContext handler"))
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it("passes through the configured theme", () => {
|
|
203
|
+
expect(createStandaloneHostBridge({ callTool: vi.fn(), theme: "dark" }).theme).toBe("dark")
|
|
204
|
+
expect(createStandaloneHostBridge({ callTool: vi.fn() }).theme).toBeUndefined()
|
|
205
|
+
})
|
|
206
|
+
})
|