@agent-native/toolkit 0.16.10 → 0.16.11
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/dist/collab-ui/lead-client.d.ts.map +1 -1
- package/dist/collab-ui/lead-client.js +11 -0
- package/dist/collab-ui/lead-client.js.map +1 -1
- package/dist/collab-ui/lead-client.spec.d.ts +2 -0
- package/dist/collab-ui/lead-client.spec.d.ts.map +1 -0
- package/dist/collab-ui/lead-client.spec.js +16 -0
- package/dist/collab-ui/lead-client.spec.js.map +1 -0
- package/dist/composer/TiptapComposer.d.ts +12 -0
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +59 -18
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/composer/TiptapComposer.spec.js +31 -1
- package/dist/composer/TiptapComposer.spec.js.map +1 -1
- package/dist/design-tweaks/scrub-input.d.ts.map +1 -1
- package/dist/design-tweaks/scrub-input.js +3 -2
- package/dist/design-tweaks/scrub-input.js.map +1 -1
- package/dist/design-tweaks/visual-style-controls.spec.js +19 -0
- package/dist/design-tweaks/visual-style-controls.spec.js.map +1 -1
- package/dist/editor/RegistryBlockContext.d.ts +4 -0
- package/dist/editor/RegistryBlockContext.d.ts.map +1 -1
- package/dist/editor/RegistryBlockContext.js.map +1 -1
- package/dist/editor/RegistryBlockNode.d.ts +13 -0
- package/dist/editor/RegistryBlockNode.d.ts.map +1 -1
- package/dist/editor/RegistryBlockNode.js +47 -20
- package/dist/editor/RegistryBlockNode.js.map +1 -1
- package/dist/editor/RegistryBlockNode.spec.js +61 -1
- package/dist/editor/RegistryBlockNode.spec.js.map +1 -1
- package/dist/editor/useCollabReconcile.concurrent.spec.js +63 -0
- package/dist/editor/useCollabReconcile.concurrent.spec.js.map +1 -1
- package/dist/editor/useCollabReconcile.d.ts.map +1 -1
- package/dist/editor/useCollabReconcile.js +15 -2
- package/dist/editor/useCollabReconcile.js.map +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/pointer-lock.d.ts +2 -0
- package/dist/ui/pointer-lock.d.ts.map +1 -0
- package/dist/ui/pointer-lock.js +22 -0
- package/dist/ui/pointer-lock.js.map +1 -0
- package/dist/ui/tooltip.js +1 -1
- package/package.json +1 -1
- package/src/collab-ui/lead-client.spec.ts +20 -0
- package/src/collab-ui/lead-client.ts +17 -1
- package/src/composer/TiptapComposer.spec.ts +40 -0
- package/src/composer/TiptapComposer.tsx +182 -75
- package/src/design-tweaks/scrub-input.tsx +3 -2
- package/src/design-tweaks/visual-style-controls.spec.tsx +37 -0
- package/src/editor/RegistryBlockContext.tsx +4 -0
- package/src/editor/RegistryBlockNode.spec.ts +78 -0
- package/src/editor/RegistryBlockNode.tsx +106 -19
- package/src/editor/useCollabReconcile.concurrent.spec.ts +75 -0
- package/src/editor/useCollabReconcile.ts +19 -2
- package/src/ui/index.ts +1 -0
- package/src/ui/pointer-lock.ts +21 -0
- package/src/ui/tooltip.tsx +1 -1
|
@@ -1024,6 +1024,14 @@ const LOCAL_RUNTIME_ENGINES = new Set([
|
|
|
1024
1024
|
"opencode-cli",
|
|
1025
1025
|
]);
|
|
1026
1026
|
|
|
1027
|
+
export function hasConfiguredCloudProvider(
|
|
1028
|
+
groups: ReadonlyArray<{ engine: string; configured: boolean }>,
|
|
1029
|
+
): boolean {
|
|
1030
|
+
return groups.some(
|
|
1031
|
+
(group) => group.configured && !LOCAL_RUNTIME_ENGINES.has(group.engine),
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1027
1035
|
function isOpenAiModelId(model: string): boolean {
|
|
1028
1036
|
const normalizedModel = model.toLowerCase();
|
|
1029
1037
|
return (
|
|
@@ -1082,6 +1090,22 @@ export function shouldShowOnlyConnectPath(
|
|
|
1082
1090
|
return showBuilderCta && groups.every((group) => !group.configured);
|
|
1083
1091
|
}
|
|
1084
1092
|
|
|
1093
|
+
/**
|
|
1094
|
+
* When nothing is routable yet, the model hook resolves `selectedModel` to
|
|
1095
|
+
* `""` rather than pre-selecting something unusable — that reflects "nothing
|
|
1096
|
+
* chosen," not "nothing to show." The picker itself still has a job to do in
|
|
1097
|
+
* that state (its connect-provider CTAs), so gate on there being engines to
|
|
1098
|
+
* list and a way to change the selection, not on a model already being set.
|
|
1099
|
+
*/
|
|
1100
|
+
export function shouldRenderModelSelector(
|
|
1101
|
+
availableModels: ReadonlyArray<unknown> | undefined,
|
|
1102
|
+
onModelChange: unknown,
|
|
1103
|
+
): boolean {
|
|
1104
|
+
return Boolean(
|
|
1105
|
+
availableModels && availableModels.length > 0 && onModelChange,
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1085
1109
|
function friendlyModelName(model: string, t?: ComposerTranslate): string {
|
|
1086
1110
|
if (model === "auto") {
|
|
1087
1111
|
return (
|
|
@@ -1320,6 +1344,7 @@ function ModelSelector({
|
|
|
1320
1344
|
providerConnectStatusEnabled = true,
|
|
1321
1345
|
onConnectProvider,
|
|
1322
1346
|
onConnectLocalRuntime,
|
|
1347
|
+
terminalModeControl,
|
|
1323
1348
|
imageModel,
|
|
1324
1349
|
}: {
|
|
1325
1350
|
model: string;
|
|
@@ -1344,6 +1369,7 @@ function ModelSelector({
|
|
|
1344
1369
|
providerConnectStatusEnabled?: boolean;
|
|
1345
1370
|
onConnectProvider?: () => void;
|
|
1346
1371
|
onConnectLocalRuntime?: (engine: string) => void;
|
|
1372
|
+
terminalModeControl?: ComposerTerminalModeControl;
|
|
1347
1373
|
onModelSelectorOpenChange?: (open: boolean) => void;
|
|
1348
1374
|
imageModel?: ComposerImageModelMenu;
|
|
1349
1375
|
open?: boolean;
|
|
@@ -1447,7 +1473,7 @@ function ModelSelector({
|
|
|
1447
1473
|
const selectedModelLabel = friendlyModelName(model, t).replace(/^GPT-/, "");
|
|
1448
1474
|
|
|
1449
1475
|
const [detailSection, setDetailSection] = useState<
|
|
1450
|
-
"agent" | "model" | "effort" | null
|
|
1476
|
+
"agent" | "model" | "effort" | "mode" | null
|
|
1451
1477
|
>(null);
|
|
1452
1478
|
const resolvedSection = detailSection ?? (agentOnly ? "agent" : "model");
|
|
1453
1479
|
|
|
@@ -1466,9 +1492,8 @@ function ModelSelector({
|
|
|
1466
1492
|
engines.length,
|
|
1467
1493
|
);
|
|
1468
1494
|
|
|
1469
|
-
// Keep setup actions
|
|
1470
|
-
//
|
|
1471
|
-
// what becomes available after connecting or adding a key.
|
|
1495
|
+
// Keep setup actions visible, but do not show unusable model rows until one
|
|
1496
|
+
// provider or local agent is ready.
|
|
1472
1497
|
const builderFlow = adapters.builder!.useConnectFlow!({
|
|
1473
1498
|
enabled: providerConnectStatusEnabled,
|
|
1474
1499
|
trackingSource: "composer_builder_cta",
|
|
@@ -1476,25 +1501,37 @@ function ModelSelector({
|
|
|
1476
1501
|
const hasConfiguredBuilderModels = providerGroups.some(
|
|
1477
1502
|
(group) => group.engine === "builder" && group.configured,
|
|
1478
1503
|
);
|
|
1504
|
+
const hasConfiguredCloudProviderReady =
|
|
1505
|
+
hasConfiguredCloudProvider(providerGroups);
|
|
1479
1506
|
const hasConnectedSubscription = providerGroups.some(
|
|
1480
|
-
(group) =>
|
|
1507
|
+
(group) =>
|
|
1508
|
+
hasConfiguredCloudProviderReady &&
|
|
1509
|
+
group.configured &&
|
|
1510
|
+
group.isSubscription &&
|
|
1511
|
+
!LOCAL_RUNTIME_ENGINES.has(group.engine),
|
|
1481
1512
|
);
|
|
1482
1513
|
const hasUnconfiguredVisibleModels = modelProviderGroups.some(
|
|
1483
1514
|
(group) => !group.configured,
|
|
1484
1515
|
);
|
|
1516
|
+
const hasConfiguredProvider = providerGroups.some(
|
|
1517
|
+
(group) => group.configured,
|
|
1518
|
+
);
|
|
1485
1519
|
const showBuilderAction =
|
|
1486
|
-
|
|
1520
|
+
!hasConfiguredCloudProviderReady &&
|
|
1487
1521
|
(Boolean(onConnectProvider) ||
|
|
1488
1522
|
(providerConnectStatusEnabled &&
|
|
1489
1523
|
!builderFlow.configured &&
|
|
1490
1524
|
!builderFlow.envManaged &&
|
|
1491
1525
|
!hasConfiguredBuilderModels &&
|
|
1492
1526
|
!hasConnectedSubscription));
|
|
1493
|
-
const showAddKeysAction =
|
|
1527
|
+
const showAddKeysAction =
|
|
1528
|
+
!hasConfiguredCloudProviderReady &&
|
|
1529
|
+
(hasUnconfiguredVisibleModels || showBuilderAction);
|
|
1494
1530
|
const showProviderActions = showBuilderAction || showAddKeysAction;
|
|
1495
|
-
const onlyConnectPathAvailable =
|
|
1496
|
-
|
|
1497
|
-
|
|
1531
|
+
const onlyConnectPathAvailable = shouldShowOnlyConnectPath(
|
|
1532
|
+
showProviderActions,
|
|
1533
|
+
providerGroups,
|
|
1534
|
+
);
|
|
1498
1535
|
const openLlmSettings = useCallback(() => {
|
|
1499
1536
|
try {
|
|
1500
1537
|
window.location.hash = "llm";
|
|
@@ -1601,7 +1638,7 @@ function ModelSelector({
|
|
|
1601
1638
|
/>
|
|
1602
1639
|
</span>
|
|
1603
1640
|
</TooltipTrigger>
|
|
1604
|
-
<TooltipContent side="right" className="
|
|
1641
|
+
<TooltipContent side="right" className="max-w-xs">
|
|
1605
1642
|
<span className="block">
|
|
1606
1643
|
{hostedHarness
|
|
1607
1644
|
? t("agentChat.composer.hostedHarnessDescription", {
|
|
@@ -1678,6 +1715,30 @@ function ModelSelector({
|
|
|
1678
1715
|
<IconChevronRight className="h-3 w-3 shrink-0 opacity-60 rtl:-scale-x-100" />
|
|
1679
1716
|
</button>
|
|
1680
1717
|
)}
|
|
1718
|
+
{terminalModeControl && (
|
|
1719
|
+
<button
|
|
1720
|
+
type="button"
|
|
1721
|
+
role="tab"
|
|
1722
|
+
aria-selected={resolvedSection === "mode"}
|
|
1723
|
+
onClick={() => setDetailSection("mode")}
|
|
1724
|
+
onMouseEnter={() => setDetailSection("mode")}
|
|
1725
|
+
className={`flex w-full min-w-0 items-center gap-1 rounded-md px-2 py-2 text-start transition-colors ${
|
|
1726
|
+
resolvedSection === "mode"
|
|
1727
|
+
? "bg-accent text-foreground"
|
|
1728
|
+
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
|
|
1729
|
+
}`}
|
|
1730
|
+
>
|
|
1731
|
+
<span className="shrink-0 text-[12px] font-medium">
|
|
1732
|
+
{t("agentPanel.mode", { defaultValue: "Mode" })}
|
|
1733
|
+
</span>
|
|
1734
|
+
<span className="ms-auto min-w-0 max-w-[6rem] truncate text-end text-[11px] text-muted-foreground/80">
|
|
1735
|
+
{terminalModeControl.enabled
|
|
1736
|
+
? t("agentPanel.cli", { defaultValue: "CLI" })
|
|
1737
|
+
: t("agentPanel.uiMode", { defaultValue: "UI" })}
|
|
1738
|
+
</span>
|
|
1739
|
+
<IconChevronRight className="h-3 w-3 shrink-0 opacity-60 rtl:-scale-x-100" />
|
|
1740
|
+
</button>
|
|
1741
|
+
)}
|
|
1681
1742
|
</div>
|
|
1682
1743
|
</PopoverAnchor>
|
|
1683
1744
|
<PopoverContent
|
|
@@ -1761,7 +1822,7 @@ function ModelSelector({
|
|
|
1761
1822
|
</TooltipTrigger>
|
|
1762
1823
|
<TooltipContent
|
|
1763
1824
|
side="left"
|
|
1764
|
-
className="
|
|
1825
|
+
className="max-w-xs"
|
|
1765
1826
|
>
|
|
1766
1827
|
{agent.description}
|
|
1767
1828
|
</TooltipContent>
|
|
@@ -1865,42 +1926,47 @@ function ModelSelector({
|
|
|
1865
1926
|
)}
|
|
1866
1927
|
</>
|
|
1867
1928
|
)}
|
|
1868
|
-
{
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
className={`min-w-0 flex-1 truncate text-[12px] ${
|
|
1888
|
-
option.value === imageModel.value
|
|
1889
|
-
? "text-foreground"
|
|
1890
|
-
: "text-muted-foreground"
|
|
1891
|
-
}`}
|
|
1929
|
+
{hasConfiguredCloudProviderReady &&
|
|
1930
|
+
imageModel &&
|
|
1931
|
+
imageModel.options.length > 0 && (
|
|
1932
|
+
<div className="mt-2 pt-1">
|
|
1933
|
+
<div className="px-2 py-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
1934
|
+
{imageModel.label ??
|
|
1935
|
+
t("agentChat.composer.imageModel", {
|
|
1936
|
+
defaultValue: "Image model",
|
|
1937
|
+
})}
|
|
1938
|
+
</div>
|
|
1939
|
+
{imageModel.options.map((option) => (
|
|
1940
|
+
<button
|
|
1941
|
+
key={option.value}
|
|
1942
|
+
type="button"
|
|
1943
|
+
onClick={() => {
|
|
1944
|
+
imageModel.onChange(option.value);
|
|
1945
|
+
setPickerOpen(false);
|
|
1946
|
+
}}
|
|
1947
|
+
className="flex w-full items-center gap-3 rounded-md px-2 py-2 text-start hover:bg-accent/50"
|
|
1892
1948
|
>
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1949
|
+
<span
|
|
1950
|
+
className={`min-w-0 flex-1 truncate text-[12px] ${
|
|
1951
|
+
option.value === imageModel.value
|
|
1952
|
+
? "text-foreground"
|
|
1953
|
+
: "text-muted-foreground"
|
|
1954
|
+
}`}
|
|
1955
|
+
>
|
|
1956
|
+
{option.label}
|
|
1957
|
+
</span>
|
|
1958
|
+
{option.value === imageModel.value && (
|
|
1959
|
+
<IconCheck className="size-4 shrink-0 text-primary" />
|
|
1960
|
+
)}
|
|
1961
|
+
</button>
|
|
1962
|
+
))}
|
|
1963
|
+
</div>
|
|
1964
|
+
)}
|
|
1965
|
+
{hasConfiguredProvider && showModelListSkeleton && (
|
|
1966
|
+
<ModelSelectorSkeleton />
|
|
1901
1967
|
)}
|
|
1902
|
-
{
|
|
1903
|
-
|
|
1968
|
+
{hasConfiguredProvider &&
|
|
1969
|
+
isCodexAgent &&
|
|
1904
1970
|
!showModelListSkeleton &&
|
|
1905
1971
|
!onlyConnectPathAvailable &&
|
|
1906
1972
|
modelProviderGroups.length === 0 && (
|
|
@@ -1919,32 +1985,35 @@ function ModelSelector({
|
|
|
1919
1985
|
</span>
|
|
1920
1986
|
</button>
|
|
1921
1987
|
)}
|
|
1922
|
-
{
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
className={`min-w-0 flex-1 truncate text-[13px] ${
|
|
1933
|
-
model === "auto"
|
|
1934
|
-
? "text-foreground"
|
|
1935
|
-
: "text-muted-foreground"
|
|
1936
|
-
}`}
|
|
1988
|
+
{hasConfiguredProvider &&
|
|
1989
|
+
autoModelGroup &&
|
|
1990
|
+
!onlyConnectPathAvailable && (
|
|
1991
|
+
<button
|
|
1992
|
+
type="button"
|
|
1993
|
+
onClick={() => {
|
|
1994
|
+
onChange("auto", autoModelGroup.engine);
|
|
1995
|
+
setPickerOpen(false);
|
|
1996
|
+
}}
|
|
1997
|
+
className="mt-1 flex w-full items-center gap-3 rounded-md px-2 py-1.5 text-start hover:bg-accent/50"
|
|
1937
1998
|
>
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1999
|
+
<span
|
|
2000
|
+
className={`min-w-0 flex-1 truncate text-[13px] ${
|
|
2001
|
+
model === "auto"
|
|
2002
|
+
? "text-foreground"
|
|
2003
|
+
: "text-muted-foreground"
|
|
2004
|
+
}`}
|
|
2005
|
+
>
|
|
2006
|
+
{t("agentChat.composer.auto", {
|
|
2007
|
+
defaultValue: "Auto",
|
|
2008
|
+
})}
|
|
2009
|
+
</span>
|
|
2010
|
+
{model === "auto" && (
|
|
2011
|
+
<IconCheck className="size-4 shrink-0 text-primary" />
|
|
2012
|
+
)}
|
|
2013
|
+
</button>
|
|
2014
|
+
)}
|
|
2015
|
+
{hasConfiguredProvider &&
|
|
2016
|
+
!onlyConnectPathAvailable &&
|
|
1948
2017
|
visibleProviderGroups.map((group, groupIndex) => {
|
|
1949
2018
|
const models = latestModelsOnly(group.models);
|
|
1950
2019
|
const showProviderLabels =
|
|
@@ -2084,6 +2153,43 @@ function ModelSelector({
|
|
|
2084
2153
|
))}
|
|
2085
2154
|
</div>
|
|
2086
2155
|
)}
|
|
2156
|
+
{resolvedSection === "mode" && terminalModeControl && (
|
|
2157
|
+
<div className="flex flex-col">
|
|
2158
|
+
{[
|
|
2159
|
+
{
|
|
2160
|
+
enabled: false,
|
|
2161
|
+
label: t("agentPanel.uiMode", { defaultValue: "UI" }),
|
|
2162
|
+
},
|
|
2163
|
+
{
|
|
2164
|
+
enabled: true,
|
|
2165
|
+
label: t("agentPanel.cli", { defaultValue: "CLI" }),
|
|
2166
|
+
},
|
|
2167
|
+
].map((option) => (
|
|
2168
|
+
<button
|
|
2169
|
+
key={option.label}
|
|
2170
|
+
type="button"
|
|
2171
|
+
onClick={() => {
|
|
2172
|
+
terminalModeControl.onChange(option.enabled);
|
|
2173
|
+
setPickerOpen(false);
|
|
2174
|
+
}}
|
|
2175
|
+
className="flex w-full items-center gap-3 rounded-md px-2 py-2 text-start hover:bg-accent/50"
|
|
2176
|
+
>
|
|
2177
|
+
<span
|
|
2178
|
+
className={`min-w-0 flex-1 truncate text-[12px] ${
|
|
2179
|
+
option.enabled === terminalModeControl.enabled
|
|
2180
|
+
? "text-foreground"
|
|
2181
|
+
: "text-muted-foreground"
|
|
2182
|
+
}`}
|
|
2183
|
+
>
|
|
2184
|
+
{option.label}
|
|
2185
|
+
</span>
|
|
2186
|
+
{option.enabled === terminalModeControl.enabled && (
|
|
2187
|
+
<IconCheck className="size-4 shrink-0 text-primary" />
|
|
2188
|
+
)}
|
|
2189
|
+
</button>
|
|
2190
|
+
))}
|
|
2191
|
+
</div>
|
|
2192
|
+
)}
|
|
2087
2193
|
</div>
|
|
2088
2194
|
</div>
|
|
2089
2195
|
</PopoverContent>
|
|
@@ -3679,25 +3785,26 @@ export function TiptapComposer({
|
|
|
3679
3785
|
))}
|
|
3680
3786
|
{toolbarSlot ?? modeControl}
|
|
3681
3787
|
<div data-agent-composer-slot="toolbar-spacer" className="flex-1" />
|
|
3682
|
-
{
|
|
3788
|
+
{shouldRenderModelSelector(availableModels, onModelChange) && (
|
|
3683
3789
|
<ModelSelector
|
|
3684
|
-
model={selectedModel}
|
|
3790
|
+
model={selectedModel ?? ""}
|
|
3685
3791
|
open={modelSelectorOpen}
|
|
3686
3792
|
effort={selectedEffort}
|
|
3687
|
-
engines={availableModels}
|
|
3793
|
+
engines={availableModels!}
|
|
3688
3794
|
agents={availableAgents}
|
|
3689
3795
|
selectedAgent={selectedAgent}
|
|
3690
3796
|
agentOnly={agentOnly}
|
|
3691
3797
|
hostedHarness={hostedHarness}
|
|
3692
3798
|
showAutoModelOption={showAutoModelOption}
|
|
3693
3799
|
modelListLoading={modelListLoading}
|
|
3694
|
-
onChange={onModelChange}
|
|
3800
|
+
onChange={onModelChange!}
|
|
3695
3801
|
onEffortChange={onEffortChange}
|
|
3696
3802
|
onAgentChange={onAgentChange}
|
|
3697
3803
|
onModelSelectorOpenChange={onModelSelectorOpenChange}
|
|
3698
3804
|
providerConnectStatusEnabled={providerConnectStatusEnabled}
|
|
3699
3805
|
onConnectProvider={onConnectProvider}
|
|
3700
3806
|
onConnectLocalRuntime={onConnectLocalRuntime}
|
|
3807
|
+
terminalModeControl={terminalModeControl}
|
|
3701
3808
|
imageModel={imageModelMenu}
|
|
3702
3809
|
/>
|
|
3703
3810
|
)}
|
|
@@ -272,8 +272,9 @@ export function VisualScrubInput({
|
|
|
272
272
|
if (event.key === "Enter") {
|
|
273
273
|
event.preventDefault();
|
|
274
274
|
commitDraft();
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
// Blurring here would hand the next keystroke to whatever global
|
|
276
|
+
// shortcut owns that key while the user is still typing at the field.
|
|
277
|
+
event.currentTarget.select();
|
|
277
278
|
return;
|
|
278
279
|
}
|
|
279
280
|
|
|
@@ -91,6 +91,43 @@ describe("visual style controls", () => {
|
|
|
91
91
|
expect(input?.value).toBe("24px");
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
it("commits on Enter and keeps focus so the next keystroke stays in the field", () => {
|
|
95
|
+
const onChange = vi.fn();
|
|
96
|
+
act(() => {
|
|
97
|
+
root.render(
|
|
98
|
+
<VisualScrubInput
|
|
99
|
+
label="Weight"
|
|
100
|
+
value={1}
|
|
101
|
+
unit="px"
|
|
102
|
+
onChange={onChange}
|
|
103
|
+
/>,
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const input = container.querySelector<HTMLInputElement>("input")!;
|
|
108
|
+
act(() => input.focus());
|
|
109
|
+
act(() => {
|
|
110
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
111
|
+
HTMLInputElement.prototype,
|
|
112
|
+
"value",
|
|
113
|
+
)!.set!;
|
|
114
|
+
setter.call(input, "2");
|
|
115
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
116
|
+
});
|
|
117
|
+
act(() => {
|
|
118
|
+
input.dispatchEvent(
|
|
119
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(onChange).toHaveBeenCalledWith(
|
|
124
|
+
2,
|
|
125
|
+
expect.objectContaining({ source: "commit" }),
|
|
126
|
+
);
|
|
127
|
+
// Blurring here would hand the next keystroke to a global shortcut.
|
|
128
|
+
expect(document.activeElement).toBe(input);
|
|
129
|
+
});
|
|
130
|
+
|
|
94
131
|
it("supports a compact icon prefix without removing the accessible label", () => {
|
|
95
132
|
const TestIcon = ({
|
|
96
133
|
className,
|
|
@@ -6,14 +6,38 @@ import { NodeSelection } from "@tiptap/pm/state";
|
|
|
6
6
|
import StarterKit from "@tiptap/starter-kit";
|
|
7
7
|
import React, { act } from "react";
|
|
8
8
|
import { createRoot, type Root } from "react-dom/client";
|
|
9
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
9
10
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
11
|
|
|
11
12
|
import {
|
|
12
13
|
createRegistryBlockNode,
|
|
13
14
|
LegacyJsonEditSurface,
|
|
15
|
+
RegistryBlockLoadErrorView,
|
|
16
|
+
selectRegistryBlockNode,
|
|
14
17
|
type RegistryBlockSideMapBlock,
|
|
15
18
|
} from "./RegistryBlockNode.js";
|
|
16
19
|
|
|
20
|
+
describe("RegistryBlockLoadErrorView", () => {
|
|
21
|
+
it("preserves unreadable source and reports a terminal load error", () => {
|
|
22
|
+
const rawSource = '<Mermaid id="broken" source={nope} />';
|
|
23
|
+
const markup = renderToStaticMarkup(
|
|
24
|
+
React.createElement(RegistryBlockLoadErrorView, {
|
|
25
|
+
message:
|
|
26
|
+
"Could not load mermaid block: Persisted block source is unreadable.",
|
|
27
|
+
rawSource,
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(markup).toContain(
|
|
32
|
+
"<Mermaid id="broken" source={nope} />",
|
|
33
|
+
);
|
|
34
|
+
expect(markup).toContain(
|
|
35
|
+
"Could not load mermaid block: Persisted block source is unreadable.",
|
|
36
|
+
);
|
|
37
|
+
expect(markup).not.toContain("Loading mermaid block");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
17
41
|
const PlanBlockNode = createRegistryBlockNode({
|
|
18
42
|
nodeName: "planBlock",
|
|
19
43
|
dataTag: "data-plan-block",
|
|
@@ -95,6 +119,60 @@ afterEach(() => {
|
|
|
95
119
|
});
|
|
96
120
|
|
|
97
121
|
describe("RegistryBlockNode keyboard guard", () => {
|
|
122
|
+
it("selects and removes an errored registry atom from its interactive error surface", () => {
|
|
123
|
+
const editor = createEditor();
|
|
124
|
+
const target = document.createElement("div");
|
|
125
|
+
target.setAttribute("data-plan-interactive", "true");
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
expect(
|
|
129
|
+
selectRegistryBlockNode({
|
|
130
|
+
editable: true,
|
|
131
|
+
allowInteractiveChild: true,
|
|
132
|
+
target,
|
|
133
|
+
getPos: () => findPlanBlockPos(editor),
|
|
134
|
+
view: editor.view,
|
|
135
|
+
preventDefault: vi.fn(),
|
|
136
|
+
stopPropagation: vi.fn(),
|
|
137
|
+
}),
|
|
138
|
+
).toBe(true);
|
|
139
|
+
expect(editor.state.selection).toBeInstanceOf(NodeSelection);
|
|
140
|
+
|
|
141
|
+
expect(editor.commands.deleteSelection()).toBe(true);
|
|
142
|
+
expect(() => findPlanBlockPos(editor)).toThrow("Expected planBlock node");
|
|
143
|
+
} finally {
|
|
144
|
+
editor.destroy();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("preserves native raw-source selection inside an errored registry atom", () => {
|
|
149
|
+
const editor = createEditor();
|
|
150
|
+
const errorSurface = document.createElement("div");
|
|
151
|
+
const rawSource = document.createElement("pre");
|
|
152
|
+
errorSurface.appendChild(rawSource);
|
|
153
|
+
const preventDefault = vi.fn();
|
|
154
|
+
const stopPropagation = vi.fn();
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
expect(
|
|
158
|
+
selectRegistryBlockNode({
|
|
159
|
+
editable: true,
|
|
160
|
+
allowInteractiveChild: true,
|
|
161
|
+
target: rawSource,
|
|
162
|
+
getPos: () => findPlanBlockPos(editor),
|
|
163
|
+
view: editor.view,
|
|
164
|
+
preventDefault,
|
|
165
|
+
stopPropagation,
|
|
166
|
+
}),
|
|
167
|
+
).toBe(false);
|
|
168
|
+
expect(preventDefault).not.toHaveBeenCalled();
|
|
169
|
+
expect(stopPropagation).not.toHaveBeenCalled();
|
|
170
|
+
expect(editor.state.selection).not.toBeInstanceOf(NodeSelection);
|
|
171
|
+
} finally {
|
|
172
|
+
editor.destroy();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
98
176
|
it("keeps selected registry atoms immutable for text entry and preserves undo history", () => {
|
|
99
177
|
const editor = createEditor();
|
|
100
178
|
|