@agent-native/toolkit 0.10.7 → 0.10.9
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/composer/TiptapComposer.d.ts +8 -0
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +47 -33
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/composer/TiptapComposer.spec.js +11 -1
- package/dist/composer/TiptapComposer.spec.js.map +1 -1
- package/dist/composer/runtime-adapters.d.ts +1 -0
- package/dist/composer/runtime-adapters.d.ts.map +1 -1
- package/dist/composer/runtime-adapters.js +1 -0
- package/dist/composer/runtime-adapters.js.map +1 -1
- package/dist/editor/useCollabReconcile.concurrent.spec.js +46 -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 +14 -2
- package/dist/editor/useCollabReconcile.js.map +1 -1
- package/package.json +1 -1
- package/src/composer/TiptapComposer.spec.ts +14 -0
- package/src/composer/TiptapComposer.tsx +143 -111
- package/src/composer/runtime-adapters.tsx +2 -0
- package/src/editor/useCollabReconcile.concurrent.spec.ts +51 -0
- package/src/editor/useCollabReconcile.ts +14 -1
|
@@ -839,6 +839,18 @@ export function shouldShowModelSelectorSkeleton(
|
|
|
839
839
|
return isLoading && engineCount === 0;
|
|
840
840
|
}
|
|
841
841
|
|
|
842
|
+
/**
|
|
843
|
+
* With nothing connected, every family is a dead "needs API key" row, so the
|
|
844
|
+
* picker shows only the connect CTAs. Never hide the list unless a CTA is
|
|
845
|
+
* there to replace it — an empty popover reads as more broken, not less.
|
|
846
|
+
*/
|
|
847
|
+
export function shouldShowOnlyConnectPath(
|
|
848
|
+
showBuilderCta: boolean,
|
|
849
|
+
groups: ReadonlyArray<{ configured: boolean }>,
|
|
850
|
+
): boolean {
|
|
851
|
+
return showBuilderCta && groups.every((group) => !group.configured);
|
|
852
|
+
}
|
|
853
|
+
|
|
842
854
|
function friendlyModelName(model: string): string {
|
|
843
855
|
if (FRIENDLY_MODEL_NAMES[model]) return FRIENDLY_MODEL_NAMES[model];
|
|
844
856
|
// Claude: claude-{tier}-{major}[-minor][-dateYYYYMMDD] → Tier Major[.Minor]
|
|
@@ -1081,6 +1093,10 @@ function ModelSelector({
|
|
|
1081
1093
|
!builderFlow.configured &&
|
|
1082
1094
|
!builderFlow.envManaged &&
|
|
1083
1095
|
!hasConfiguredBuilderModels;
|
|
1096
|
+
const onlyConnectPathAvailable = shouldShowOnlyConnectPath(
|
|
1097
|
+
showBuilderCta,
|
|
1098
|
+
providerGroups,
|
|
1099
|
+
);
|
|
1084
1100
|
const openLlmSettings = useCallback(() => {
|
|
1085
1101
|
try {
|
|
1086
1102
|
window.location.hash = "llm";
|
|
@@ -1161,6 +1177,14 @@ function ModelSelector({
|
|
|
1161
1177
|
</span>
|
|
1162
1178
|
</span>
|
|
1163
1179
|
</button>
|
|
1180
|
+
{!onConnectProvider && builderFlow.error && (
|
|
1181
|
+
<p
|
|
1182
|
+
role="alert"
|
|
1183
|
+
className="px-3 pb-2 ps-9 text-[11px] text-destructive"
|
|
1184
|
+
>
|
|
1185
|
+
{builderFlow.error}
|
|
1186
|
+
</p>
|
|
1187
|
+
)}
|
|
1164
1188
|
<button
|
|
1165
1189
|
type="button"
|
|
1166
1190
|
onClick={openLlmSettings}
|
|
@@ -1181,7 +1205,9 @@ function ModelSelector({
|
|
|
1181
1205
|
</span>
|
|
1182
1206
|
</span>
|
|
1183
1207
|
</button>
|
|
1184
|
-
|
|
1208
|
+
{!onlyConnectPathAvailable && (
|
|
1209
|
+
<div className="my-1 border-t border-border" />
|
|
1210
|
+
)}
|
|
1185
1211
|
</>
|
|
1186
1212
|
)}
|
|
1187
1213
|
{imageModel && imageModel.options.length > 0 && (
|
|
@@ -1231,7 +1257,7 @@ function ModelSelector({
|
|
|
1231
1257
|
</>
|
|
1232
1258
|
)}
|
|
1233
1259
|
{showModelListSkeleton && <ModelSelectorSkeleton />}
|
|
1234
|
-
{autoModelGroup && (
|
|
1260
|
+
{autoModelGroup && !onlyConnectPathAvailable && (
|
|
1235
1261
|
<button
|
|
1236
1262
|
type="button"
|
|
1237
1263
|
onClick={() => {
|
|
@@ -1248,136 +1274,142 @@ function ModelSelector({
|
|
|
1248
1274
|
)}
|
|
1249
1275
|
</button>
|
|
1250
1276
|
)}
|
|
1251
|
-
{autoModelGroup &&
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1277
|
+
{autoModelGroup &&
|
|
1278
|
+
providerGroups.length > 0 &&
|
|
1279
|
+
!onlyConnectPathAvailable && (
|
|
1280
|
+
<div className="my-1 border-t border-border" />
|
|
1281
|
+
)}
|
|
1282
|
+
{!onlyConnectPathAvailable &&
|
|
1283
|
+
providerGroups.map((group) => {
|
|
1284
|
+
const models = latestModelsOnly(group.models);
|
|
1285
|
+
const groupKey = `${group.engine}:${group.label}`;
|
|
1286
|
+
const isExpanded = expandedGroups.has(groupKey);
|
|
1287
|
+
const ChevronIcon = isExpanded ? IconChevronDown : IconChevronRight;
|
|
1288
|
+
return (
|
|
1289
|
+
<div key={groupKey}>
|
|
1290
|
+
<div className="flex items-center hover:bg-accent/30">
|
|
1291
|
+
<button
|
|
1292
|
+
type="button"
|
|
1293
|
+
aria-expanded={isExpanded}
|
|
1294
|
+
onClick={() => toggleGroup(groupKey)}
|
|
1295
|
+
className="flex flex-1 min-w-0 items-center gap-1.5 px-2 py-1.5 cursor-pointer text-start"
|
|
1296
|
+
>
|
|
1297
|
+
<ChevronIcon className="h-3 w-3 shrink-0 text-muted-foreground rtl:-scale-x-100" />
|
|
1298
|
+
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
|
|
1299
|
+
{group.label}
|
|
1300
|
+
</span>
|
|
1301
|
+
{!isExpanded && groupKey === selectedGroupKey && (
|
|
1302
|
+
<span className="text-[11px] text-muted-foreground/80 truncate">
|
|
1303
|
+
{friendlyModelName(model)}
|
|
1304
|
+
</span>
|
|
1305
|
+
)}
|
|
1306
|
+
</button>
|
|
1307
|
+
{!group.configured && (
|
|
1308
|
+
<button
|
|
1309
|
+
type="button"
|
|
1310
|
+
className="text-[10px] text-muted-foreground/60 hover:text-foreground cursor-pointer pe-3 py-1.5"
|
|
1311
|
+
onClick={() =>
|
|
1312
|
+
group.engine === "codex-cli" && onConnectLocalRuntime
|
|
1313
|
+
? connectLocalRuntime(group.engine)
|
|
1314
|
+
: openLlmSettings()
|
|
1315
|
+
}
|
|
1316
|
+
>
|
|
1317
|
+
{group.engine === "codex-cli" && onConnectLocalRuntime
|
|
1318
|
+
? "sign in"
|
|
1319
|
+
: "needs API key"}
|
|
1320
|
+
</button>
|
|
1321
|
+
)}
|
|
1322
|
+
</div>
|
|
1323
|
+
{isExpanded &&
|
|
1324
|
+
models.map((m) => (
|
|
1325
|
+
<button
|
|
1326
|
+
key={m}
|
|
1327
|
+
type="button"
|
|
1328
|
+
onClick={() => {
|
|
1329
|
+
if (!group.configured) {
|
|
1330
|
+
if (
|
|
1331
|
+
group.engine === "codex-cli" &&
|
|
1332
|
+
onConnectLocalRuntime
|
|
1333
|
+
) {
|
|
1334
|
+
connectLocalRuntime(group.engine);
|
|
1335
|
+
} else {
|
|
1336
|
+
openLlmSettings();
|
|
1337
|
+
}
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
onChange(m, group.engine);
|
|
1341
|
+
const nextOptions =
|
|
1342
|
+
getReasoningEffortOptionsForModel(m);
|
|
1343
|
+
if (
|
|
1344
|
+
nextOptions.length > 0 &&
|
|
1345
|
+
!nextOptions.includes(selectedEffort)
|
|
1346
|
+
) {
|
|
1347
|
+
onEffortChange?.(defaultEffort);
|
|
1348
|
+
}
|
|
1349
|
+
setOpen(false);
|
|
1350
|
+
}}
|
|
1351
|
+
className={`flex w-full items-center gap-3 ps-7 pe-3 py-1.5 text-start ${
|
|
1352
|
+
group.configured
|
|
1353
|
+
? "hover:bg-accent/50"
|
|
1354
|
+
: "opacity-40 cursor-default"
|
|
1355
|
+
}`}
|
|
1356
|
+
>
|
|
1357
|
+
<span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
|
|
1358
|
+
{friendlyModelName(m)}
|
|
1359
|
+
</span>
|
|
1360
|
+
{m === model && group.configured && (
|
|
1361
|
+
<IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
|
|
1362
|
+
)}
|
|
1363
|
+
</button>
|
|
1364
|
+
))}
|
|
1365
|
+
</div>
|
|
1366
|
+
);
|
|
1367
|
+
})}
|
|
1368
|
+
{!showModelListSkeleton &&
|
|
1369
|
+
!onlyConnectPathAvailable &&
|
|
1370
|
+
effortOptions.length > 0 && (
|
|
1371
|
+
<>
|
|
1372
|
+
<div className="my-1 border-t border-border" />
|
|
1261
1373
|
<div className="flex items-center hover:bg-accent/30">
|
|
1262
1374
|
<button
|
|
1263
1375
|
type="button"
|
|
1264
|
-
aria-expanded={
|
|
1265
|
-
onClick={() =>
|
|
1376
|
+
aria-expanded={reasoningExpanded}
|
|
1377
|
+
onClick={() => setReasoningExpanded((prev) => !prev)}
|
|
1266
1378
|
className="flex flex-1 min-w-0 items-center gap-1.5 px-2 py-1.5 cursor-pointer text-start"
|
|
1267
1379
|
>
|
|
1268
|
-
|
|
1380
|
+
{reasoningExpanded ? (
|
|
1381
|
+
<IconChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
|
|
1382
|
+
) : (
|
|
1383
|
+
<IconChevronRight className="h-3 w-3 shrink-0 text-muted-foreground rtl:-scale-x-100" />
|
|
1384
|
+
)}
|
|
1269
1385
|
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
|
|
1270
|
-
|
|
1386
|
+
Reasoning
|
|
1271
1387
|
</span>
|
|
1272
|
-
{!
|
|
1388
|
+
{!reasoningExpanded && (
|
|
1273
1389
|
<span className="text-[11px] text-muted-foreground/80 truncate">
|
|
1274
|
-
{
|
|
1390
|
+
{effortLabel(selectedEffort)}
|
|
1275
1391
|
</span>
|
|
1276
1392
|
)}
|
|
1277
1393
|
</button>
|
|
1278
|
-
{!group.configured && (
|
|
1279
|
-
<button
|
|
1280
|
-
type="button"
|
|
1281
|
-
className="text-[10px] text-muted-foreground/60 hover:text-foreground cursor-pointer pe-3 py-1.5"
|
|
1282
|
-
onClick={() =>
|
|
1283
|
-
group.engine === "codex-cli" && onConnectLocalRuntime
|
|
1284
|
-
? connectLocalRuntime(group.engine)
|
|
1285
|
-
: openLlmSettings()
|
|
1286
|
-
}
|
|
1287
|
-
>
|
|
1288
|
-
{group.engine === "codex-cli" && onConnectLocalRuntime
|
|
1289
|
-
? "sign in"
|
|
1290
|
-
: "needs API key"}
|
|
1291
|
-
</button>
|
|
1292
|
-
)}
|
|
1293
1394
|
</div>
|
|
1294
|
-
{
|
|
1295
|
-
|
|
1395
|
+
{reasoningExpanded &&
|
|
1396
|
+
effortOptions.map((option) => (
|
|
1296
1397
|
<button
|
|
1297
|
-
key={
|
|
1398
|
+
key={option}
|
|
1298
1399
|
type="button"
|
|
1299
|
-
onClick={() =>
|
|
1300
|
-
|
|
1301
|
-
if (
|
|
1302
|
-
group.engine === "codex-cli" &&
|
|
1303
|
-
onConnectLocalRuntime
|
|
1304
|
-
) {
|
|
1305
|
-
connectLocalRuntime(group.engine);
|
|
1306
|
-
} else {
|
|
1307
|
-
openLlmSettings();
|
|
1308
|
-
}
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
|
-
onChange(m, group.engine);
|
|
1312
|
-
const nextOptions = getReasoningEffortOptionsForModel(m);
|
|
1313
|
-
if (
|
|
1314
|
-
nextOptions.length > 0 &&
|
|
1315
|
-
!nextOptions.includes(selectedEffort)
|
|
1316
|
-
) {
|
|
1317
|
-
onEffortChange?.(defaultEffort);
|
|
1318
|
-
}
|
|
1319
|
-
setOpen(false);
|
|
1320
|
-
}}
|
|
1321
|
-
className={`flex w-full items-center gap-3 ps-7 pe-3 py-1.5 text-start ${
|
|
1322
|
-
group.configured
|
|
1323
|
-
? "hover:bg-accent/50"
|
|
1324
|
-
: "opacity-40 cursor-default"
|
|
1325
|
-
}`}
|
|
1400
|
+
onClick={() => onEffortChange?.(option)}
|
|
1401
|
+
className="flex w-full items-center gap-3 ps-7 pe-3 py-1.5 text-start hover:bg-accent/50"
|
|
1326
1402
|
>
|
|
1327
1403
|
<span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
|
|
1328
|
-
{
|
|
1404
|
+
{effortLabel(option)}
|
|
1329
1405
|
</span>
|
|
1330
|
-
{
|
|
1406
|
+
{option === selectedEffort && (
|
|
1331
1407
|
<IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
|
|
1332
1408
|
)}
|
|
1333
1409
|
</button>
|
|
1334
1410
|
))}
|
|
1335
|
-
|
|
1336
|
-
)
|
|
1337
|
-
})}
|
|
1338
|
-
{!showModelListSkeleton && effortOptions.length > 0 && (
|
|
1339
|
-
<>
|
|
1340
|
-
<div className="my-1 border-t border-border" />
|
|
1341
|
-
<div className="flex items-center hover:bg-accent/30">
|
|
1342
|
-
<button
|
|
1343
|
-
type="button"
|
|
1344
|
-
aria-expanded={reasoningExpanded}
|
|
1345
|
-
onClick={() => setReasoningExpanded((prev) => !prev)}
|
|
1346
|
-
className="flex flex-1 min-w-0 items-center gap-1.5 px-2 py-1.5 cursor-pointer text-start"
|
|
1347
|
-
>
|
|
1348
|
-
{reasoningExpanded ? (
|
|
1349
|
-
<IconChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
|
|
1350
|
-
) : (
|
|
1351
|
-
<IconChevronRight className="h-3 w-3 shrink-0 text-muted-foreground rtl:-scale-x-100" />
|
|
1352
|
-
)}
|
|
1353
|
-
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
|
|
1354
|
-
Reasoning
|
|
1355
|
-
</span>
|
|
1356
|
-
{!reasoningExpanded && (
|
|
1357
|
-
<span className="text-[11px] text-muted-foreground/80 truncate">
|
|
1358
|
-
{effortLabel(selectedEffort)}
|
|
1359
|
-
</span>
|
|
1360
|
-
)}
|
|
1361
|
-
</button>
|
|
1362
|
-
</div>
|
|
1363
|
-
{reasoningExpanded &&
|
|
1364
|
-
effortOptions.map((option) => (
|
|
1365
|
-
<button
|
|
1366
|
-
key={option}
|
|
1367
|
-
type="button"
|
|
1368
|
-
onClick={() => onEffortChange?.(option)}
|
|
1369
|
-
className="flex w-full items-center gap-3 ps-7 pe-3 py-1.5 text-start hover:bg-accent/50"
|
|
1370
|
-
>
|
|
1371
|
-
<span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
|
|
1372
|
-
{effortLabel(option)}
|
|
1373
|
-
</span>
|
|
1374
|
-
{option === selectedEffort && (
|
|
1375
|
-
<IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
|
|
1376
|
-
)}
|
|
1377
|
-
</button>
|
|
1378
|
-
))}
|
|
1379
|
-
</>
|
|
1380
|
-
)}
|
|
1411
|
+
</>
|
|
1412
|
+
)}
|
|
1381
1413
|
</PopoverContent>
|
|
1382
1414
|
</Popover>
|
|
1383
1415
|
);
|
|
@@ -43,6 +43,7 @@ export interface ComposerBuilderConnectFlow {
|
|
|
43
43
|
envManaged: boolean;
|
|
44
44
|
connecting: boolean;
|
|
45
45
|
statusResolved: boolean;
|
|
46
|
+
error: string | null;
|
|
46
47
|
start: () => void;
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -196,6 +197,7 @@ const fallbackBuilderFlow = {
|
|
|
196
197
|
envManaged: false,
|
|
197
198
|
connecting: false,
|
|
198
199
|
statusResolved: false,
|
|
200
|
+
error: null,
|
|
199
201
|
start: () => {},
|
|
200
202
|
};
|
|
201
203
|
|
|
@@ -183,6 +183,57 @@ function render(
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
describe("useCollabReconcile — concurrent edit / lost-update guards", () => {
|
|
186
|
+
it("persists the first local edit after a synced empty collaborative document", async () => {
|
|
187
|
+
const captured: Captured = {
|
|
188
|
+
editor: null,
|
|
189
|
+
emitted: [],
|
|
190
|
+
setContentCalls: 0,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
function EmptyDocumentHarness() {
|
|
194
|
+
const guardsRef = React.useRef<ReturnType<
|
|
195
|
+
typeof useCollabReconcile
|
|
196
|
+
> | null>(null);
|
|
197
|
+
const insertedRef = React.useRef(false);
|
|
198
|
+
const ydoc = React.useMemo(() => new Y.Doc(), []);
|
|
199
|
+
const editor = useEditor({
|
|
200
|
+
extensions: createRichMarkdownExtensions({ dialect: "gfm", ydoc }),
|
|
201
|
+
onUpdate: ({ editor, transaction }) => {
|
|
202
|
+
const guards = guardsRef.current;
|
|
203
|
+
if (!guards || guards.shouldIgnoreUpdate(transaction)) return;
|
|
204
|
+
const markdown = getEditorMarkdown(editor);
|
|
205
|
+
if (!guards.registerEmitted(markdown)) return;
|
|
206
|
+
captured.emitted.push(markdown);
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
captured.editor = editor;
|
|
210
|
+
guardsRef.current = useCollabReconcile({
|
|
211
|
+
editor,
|
|
212
|
+
ydoc,
|
|
213
|
+
collabSynced: true,
|
|
214
|
+
value: "",
|
|
215
|
+
contentUpdatedAt: "2024-01-01T00:00:01.000Z",
|
|
216
|
+
editable: true,
|
|
217
|
+
getMarkdown: getEditorMarkdown,
|
|
218
|
+
});
|
|
219
|
+
React.useLayoutEffect(() => {
|
|
220
|
+
if (!editor || insertedRef.current) return;
|
|
221
|
+
insertedRef.current = true;
|
|
222
|
+
editor.view.dispatch(
|
|
223
|
+
editor.state.tr
|
|
224
|
+
.insertText("First persisted edit")
|
|
225
|
+
.setMeta("uiEvent", "input"),
|
|
226
|
+
);
|
|
227
|
+
}, [editor]);
|
|
228
|
+
return React.createElement("div", null);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
act(() => root.render(React.createElement(EmptyDocumentHarness)));
|
|
232
|
+
await flush();
|
|
233
|
+
|
|
234
|
+
expect(captured.emitted).toContain("First persisted edit");
|
|
235
|
+
});
|
|
236
|
+
|
|
186
237
|
it("does not seed until initial collab sync has completed", async () => {
|
|
187
238
|
const { captured, Harness } = makeCollabSeedHarness();
|
|
188
239
|
|
|
@@ -626,7 +626,20 @@ export function useCollabReconcile({
|
|
|
626
626
|
// app's local mirror or autosave path (Content serializes an empty paragraph
|
|
627
627
|
// as `<empty-block/>`, which is non-empty text and bypasses the generic
|
|
628
628
|
// registerEmitted empty-string guard).
|
|
629
|
-
if (collab && !seededRef.current)
|
|
629
|
+
if (collab && !seededRef.current) {
|
|
630
|
+
// Passive effects normally mark a synced empty document initialized
|
|
631
|
+
// before a person can type. Keep the event boundary correct too: if a
|
|
632
|
+
// genuine local edit wins that tiny race, it is itself proof that the
|
|
633
|
+
// synced empty document is ready. Remote Yjs transactions stay ignored.
|
|
634
|
+
const firstSyncedEmptyUserEdit =
|
|
635
|
+
collabSynced &&
|
|
636
|
+
!value.trim() &&
|
|
637
|
+
transaction.docChanged &&
|
|
638
|
+
Boolean(transaction.getMeta("uiEvent")) &&
|
|
639
|
+
!isChangeOrigin(transaction);
|
|
640
|
+
if (!firstSyncedEmptyUserEdit) return true;
|
|
641
|
+
seededRef.current = true;
|
|
642
|
+
}
|
|
630
643
|
if (transaction.getMeta(RICH_MARKDOWN_PROGRAMMATIC_TRANSACTION)) {
|
|
631
644
|
return true;
|
|
632
645
|
}
|