@agent-native/toolkit 0.10.9 → 0.10.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/chat-history.css +1 -2
- package/dist/composer/TiptapComposer.d.ts +1 -0
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +32 -1
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/composer/TiptapComposer.spec.js +21 -1
- package/dist/composer/TiptapComposer.spec.js.map +1 -1
- package/dist/editor/DragHandle.d.ts +6 -0
- package/dist/editor/DragHandle.d.ts.map +1 -1
- package/dist/editor/DragHandle.js +41 -6
- package/dist/editor/DragHandle.js.map +1 -1
- package/dist/editor/DragHandle.spec.js +90 -2
- package/dist/editor/DragHandle.spec.js.map +1 -1
- package/package.json +1 -1
- package/src/chat-history.css +1 -2
- package/src/composer/TiptapComposer.spec.ts +23 -0
- package/src/composer/TiptapComposer.tsx +45 -0
- package/src/editor/DragHandle.spec.ts +108 -2
- package/src/editor/DragHandle.ts +63 -9
|
@@ -948,6 +948,50 @@ function latestModelsOnly(models: string[]): string[] {
|
|
|
948
948
|
});
|
|
949
949
|
}
|
|
950
950
|
|
|
951
|
+
/**
|
|
952
|
+
* Coarse relative cost per model, rendered as a quiet `$`…`$$$` suffix.
|
|
953
|
+
*
|
|
954
|
+
* Tokens and their order mirror `MODEL_COST_ORDER` in `@agent-native/core`'s
|
|
955
|
+
* chat-model-groups, which sorts these same rows — the toolkit cannot import
|
|
956
|
+
* from core, so a new model family has to be added in both places. Tiers are
|
|
957
|
+
* each provider's own entry/mid/flagship ladder, not a cross-provider price
|
|
958
|
+
* claim; anything unlisted has no tier rather than a guessed one.
|
|
959
|
+
*/
|
|
960
|
+
const MODEL_COST_TIERS: ReadonlyArray<readonly [string, 1 | 2 | 3]> = [
|
|
961
|
+
["luna", 1],
|
|
962
|
+
["terra", 2],
|
|
963
|
+
["sol", 3],
|
|
964
|
+
["haiku", 1],
|
|
965
|
+
["sonnet", 2],
|
|
966
|
+
["opus", 3],
|
|
967
|
+
["fable", 3],
|
|
968
|
+
["flash", 1],
|
|
969
|
+
["pro", 3],
|
|
970
|
+
];
|
|
971
|
+
|
|
972
|
+
const COST_TIER_LABELS = { 1: "Lower", 2: "Medium", 3: "Higher" } as const;
|
|
973
|
+
|
|
974
|
+
export function composerModelCostTier(model: string): 1 | 2 | 3 | undefined {
|
|
975
|
+
const normalized = model.toLowerCase();
|
|
976
|
+
return MODEL_COST_TIERS.find(([token]) => normalized.includes(token))?.[1];
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function ModelCostTier({ model }: { model: string }) {
|
|
980
|
+
const tier = composerModelCostTier(model);
|
|
981
|
+
if (!tier) return null;
|
|
982
|
+
return (
|
|
983
|
+
<>
|
|
984
|
+
<span
|
|
985
|
+
aria-hidden="true"
|
|
986
|
+
className="shrink-0 text-[11px] tabular-nums text-muted-foreground/60"
|
|
987
|
+
>
|
|
988
|
+
{"$".repeat(tier)}
|
|
989
|
+
</span>
|
|
990
|
+
<span className="sr-only">{COST_TIER_LABELS[tier]} cost</span>
|
|
991
|
+
</>
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
|
|
951
995
|
/**
|
|
952
996
|
* Optional secondary model menu for apps that drive a separate generation model
|
|
953
997
|
* alongside the chat LLM (e.g. the Assets app's image-generation model). When
|
|
@@ -1357,6 +1401,7 @@ function ModelSelector({
|
|
|
1357
1401
|
<span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
|
|
1358
1402
|
{friendlyModelName(m)}
|
|
1359
1403
|
</span>
|
|
1404
|
+
<ModelCostTier model={m} />
|
|
1360
1405
|
{m === model && group.configured && (
|
|
1361
1406
|
<IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
|
|
1362
1407
|
)}
|
|
@@ -4,7 +4,24 @@ import { Editor } from "@tiptap/core";
|
|
|
4
4
|
import StarterKit from "@tiptap/starter-kit";
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
DragHandle,
|
|
9
|
+
dragPreviewTransform,
|
|
10
|
+
type DragHandleOptions,
|
|
11
|
+
} from "./DragHandle.js";
|
|
12
|
+
|
|
13
|
+
describe("dragPreviewTransform", () => {
|
|
14
|
+
it("keeps the grabbed point under the pointer", () => {
|
|
15
|
+
expect(
|
|
16
|
+
dragPreviewTransform({
|
|
17
|
+
clientX: 180,
|
|
18
|
+
clientY: 240,
|
|
19
|
+
pointerOffsetX: 32,
|
|
20
|
+
pointerOffsetY: 9,
|
|
21
|
+
}),
|
|
22
|
+
).toBe("translate3d(148px, 231px, 0)");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
8
25
|
|
|
9
26
|
function makeRect({
|
|
10
27
|
left,
|
|
@@ -146,6 +163,21 @@ function childText(editor: Editor, index: number): string {
|
|
|
146
163
|
return editor.state.doc.child(index).textContent;
|
|
147
164
|
}
|
|
148
165
|
|
|
166
|
+
function focusEditorWithNativeSelection(editor: Editor): void {
|
|
167
|
+
editor.view.dom.focus();
|
|
168
|
+
editor.commands.setTextSelection({ from: 1, to: 3 });
|
|
169
|
+
const textNode = editor.view.dom.querySelector("p")?.firstChild;
|
|
170
|
+
if (!(textNode instanceof Text)) {
|
|
171
|
+
throw new Error("Expected a paragraph text node");
|
|
172
|
+
}
|
|
173
|
+
const range = document.createRange();
|
|
174
|
+
range.setStart(textNode, 0);
|
|
175
|
+
range.setEnd(textNode, Math.min(2, textNode.length));
|
|
176
|
+
const selection = window.getSelection();
|
|
177
|
+
selection?.removeAllRanges();
|
|
178
|
+
selection?.addRange(range);
|
|
179
|
+
}
|
|
180
|
+
|
|
149
181
|
afterEach(() => {
|
|
150
182
|
document.body.innerHTML = "";
|
|
151
183
|
document
|
|
@@ -160,11 +192,13 @@ describe("DragHandle menu", () => {
|
|
|
160
192
|
try {
|
|
161
193
|
clickHandle(handle);
|
|
162
194
|
|
|
163
|
-
|
|
195
|
+
const items = getMenuItems();
|
|
196
|
+
expect(items.map((item) => item.textContent)).toEqual([
|
|
164
197
|
"Duplicate",
|
|
165
198
|
"Delete",
|
|
166
199
|
"Insert block below",
|
|
167
200
|
]);
|
|
201
|
+
expect(document.activeElement).toBe(items[0]);
|
|
168
202
|
} finally {
|
|
169
203
|
editor.destroy();
|
|
170
204
|
}
|
|
@@ -222,6 +256,9 @@ describe("DragHandle menu", () => {
|
|
|
222
256
|
const { editor, handle } = mountEditor("<p>First</p><p>Second</p>");
|
|
223
257
|
|
|
224
258
|
try {
|
|
259
|
+
focusEditorWithNativeSelection(editor);
|
|
260
|
+
expect(document.activeElement).toBe(editor.view.dom);
|
|
261
|
+
expect(window.getSelection()?.toString()).toBe("Fi");
|
|
225
262
|
handle.dispatchEvent(
|
|
226
263
|
new MouseEvent("mousedown", {
|
|
227
264
|
bubbles: true,
|
|
@@ -250,6 +287,63 @@ describe("DragHandle menu", () => {
|
|
|
250
287
|
expect(editor.state.doc.childCount).toBe(2);
|
|
251
288
|
expect(childText(editor, 0)).toBe("Second");
|
|
252
289
|
expect(childText(editor, 1)).toBe("First");
|
|
290
|
+
expect(editor.state.selection.empty).toBe(true);
|
|
291
|
+
expect(editor.isFocused).toBe(false);
|
|
292
|
+
expect(window.getSelection()?.toString()).toBe("");
|
|
293
|
+
|
|
294
|
+
expect(editor.commands.undo()).toBe(true);
|
|
295
|
+
expect(childText(editor, 0)).toBe("First");
|
|
296
|
+
expect(childText(editor, 1)).toBe("Second");
|
|
297
|
+
} finally {
|
|
298
|
+
editor.destroy();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("preserves native selections outside the editor after a drop", () => {
|
|
303
|
+
const outside = document.createElement("p");
|
|
304
|
+
outside.textContent = "Keep this selection";
|
|
305
|
+
document.body.append(outside);
|
|
306
|
+
const { editor, handle } = mountEditor("<p>First</p><p>Second</p>");
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const textNode = outside.firstChild;
|
|
310
|
+
if (!(textNode instanceof Text)) {
|
|
311
|
+
throw new Error("Expected outside text");
|
|
312
|
+
}
|
|
313
|
+
const range = document.createRange();
|
|
314
|
+
range.setStart(textNode, 0);
|
|
315
|
+
range.setEnd(textNode, 4);
|
|
316
|
+
const selection = window.getSelection();
|
|
317
|
+
selection?.removeAllRanges();
|
|
318
|
+
selection?.addRange(range);
|
|
319
|
+
|
|
320
|
+
handle.dispatchEvent(
|
|
321
|
+
new MouseEvent("mousedown", {
|
|
322
|
+
bubbles: true,
|
|
323
|
+
button: 0,
|
|
324
|
+
clientX: 12,
|
|
325
|
+
clientY: 12,
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
document.dispatchEvent(
|
|
329
|
+
new MouseEvent("mousemove", {
|
|
330
|
+
bubbles: true,
|
|
331
|
+
clientX: 12,
|
|
332
|
+
clientY: 56,
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
document.dispatchEvent(
|
|
336
|
+
new MouseEvent("mouseup", {
|
|
337
|
+
bubbles: true,
|
|
338
|
+
button: 0,
|
|
339
|
+
clientX: 12,
|
|
340
|
+
clientY: 56,
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
expect(childText(editor, 0)).toBe("Second");
|
|
345
|
+
expect(childText(editor, 1)).toBe("First");
|
|
346
|
+
expect(window.getSelection()?.toString()).toBe("Keep");
|
|
253
347
|
} finally {
|
|
254
348
|
editor.destroy();
|
|
255
349
|
}
|
|
@@ -271,6 +365,7 @@ describe("DragHandle menu", () => {
|
|
|
271
365
|
);
|
|
272
366
|
|
|
273
367
|
try {
|
|
368
|
+
focusEditorWithNativeSelection(source.editor);
|
|
274
369
|
hoverAt(32, 12);
|
|
275
370
|
source.handle.dispatchEvent(
|
|
276
371
|
new MouseEvent("mousedown", {
|
|
@@ -315,6 +410,11 @@ describe("DragHandle menu", () => {
|
|
|
315
410
|
expect(childText(target.editor, 0)).toBe("Move me");
|
|
316
411
|
expect(childText(target.editor, 1)).toBe("Target first");
|
|
317
412
|
expect(childText(target.editor, 2)).toBe("Target second");
|
|
413
|
+
expect(source.editor.state.selection.empty).toBe(true);
|
|
414
|
+
expect(target.editor.state.selection.empty).toBe(true);
|
|
415
|
+
expect(source.editor.isFocused).toBe(false);
|
|
416
|
+
expect(target.editor.isFocused).toBe(false);
|
|
417
|
+
expect(window.getSelection()?.toString()).toBe("");
|
|
318
418
|
} finally {
|
|
319
419
|
source.editor.destroy();
|
|
320
420
|
target.editor.destroy();
|
|
@@ -337,6 +437,7 @@ describe("DragHandle menu", () => {
|
|
|
337
437
|
);
|
|
338
438
|
|
|
339
439
|
try {
|
|
440
|
+
focusEditorWithNativeSelection(source.editor);
|
|
340
441
|
hoverAt(32, 12);
|
|
341
442
|
source.handle.dispatchEvent(
|
|
342
443
|
new MouseEvent("mousedown", {
|
|
@@ -381,6 +482,11 @@ describe("DragHandle menu", () => {
|
|
|
381
482
|
expect(target.editor.state.doc.childCount).toBe(2);
|
|
382
483
|
expect(childText(target.editor, 0)).toBe("Target first");
|
|
383
484
|
expect(childText(target.editor, 1)).toBe("Target second");
|
|
485
|
+
expect(source.editor.state.selection.empty).toBe(true);
|
|
486
|
+
expect(target.editor.state.selection.empty).toBe(true);
|
|
487
|
+
expect(source.editor.isFocused).toBe(false);
|
|
488
|
+
expect(target.editor.isFocused).toBe(false);
|
|
489
|
+
expect(window.getSelection()?.toString()).toBe("");
|
|
384
490
|
} finally {
|
|
385
491
|
source.editor.destroy();
|
|
386
492
|
target.editor.destroy();
|
package/src/editor/DragHandle.ts
CHANGED
|
@@ -148,6 +148,8 @@ type DragSession = {
|
|
|
148
148
|
sourceNodeSize: number;
|
|
149
149
|
startX: number;
|
|
150
150
|
startY: number;
|
|
151
|
+
pointerOffsetX: number;
|
|
152
|
+
pointerOffsetY: number;
|
|
151
153
|
dragging: boolean;
|
|
152
154
|
preview: HTMLElement | null;
|
|
153
155
|
dropLine: HTMLElement | null;
|
|
@@ -194,6 +196,47 @@ let activeHoverRegistration: DragHandleRegistration | null = null;
|
|
|
194
196
|
const clamp = (value: number, min: number, max: number) =>
|
|
195
197
|
Math.min(Math.max(value, min), max);
|
|
196
198
|
|
|
199
|
+
export const dragPreviewTransform = ({
|
|
200
|
+
clientX,
|
|
201
|
+
clientY,
|
|
202
|
+
pointerOffsetX,
|
|
203
|
+
pointerOffsetY,
|
|
204
|
+
}: {
|
|
205
|
+
clientX: number;
|
|
206
|
+
clientY: number;
|
|
207
|
+
pointerOffsetX: number;
|
|
208
|
+
pointerOffsetY: number;
|
|
209
|
+
}) =>
|
|
210
|
+
`translate3d(${clientX - pointerOffsetX}px, ${clientY - pointerOffsetY}px, 0)`;
|
|
211
|
+
|
|
212
|
+
const collapseAndBlurEditorAfterDrop = (
|
|
213
|
+
view: EditorView,
|
|
214
|
+
preferredPos: number,
|
|
215
|
+
) => {
|
|
216
|
+
const pos = clamp(preferredPos, 0, view.state.doc.content.size);
|
|
217
|
+
const selection = TextSelection.near(view.state.doc.resolve(pos), 1);
|
|
218
|
+
if (!view.state.selection.eq(selection)) {
|
|
219
|
+
view.dispatch(view.state.tr.setSelection(selection));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const nativeSelection = window.getSelection();
|
|
223
|
+
const selectionBelongsToEditor = [
|
|
224
|
+
nativeSelection?.anchorNode,
|
|
225
|
+
nativeSelection?.focusNode,
|
|
226
|
+
].some((node) => node && (node === view.dom || view.dom.contains(node)));
|
|
227
|
+
if (selectionBelongsToEditor) {
|
|
228
|
+
nativeSelection?.removeAllRanges();
|
|
229
|
+
}
|
|
230
|
+
const activeElement = document.activeElement;
|
|
231
|
+
if (
|
|
232
|
+
activeElement instanceof HTMLElement &&
|
|
233
|
+
(activeElement === view.dom || view.dom.contains(activeElement))
|
|
234
|
+
) {
|
|
235
|
+
activeElement.blur();
|
|
236
|
+
}
|
|
237
|
+
view.dom.blur();
|
|
238
|
+
};
|
|
239
|
+
|
|
197
240
|
const editorArea = (registration: DragHandleRegistration) => {
|
|
198
241
|
const rect = registration.view.dom.getBoundingClientRect();
|
|
199
242
|
return rect.width * rect.height;
|
|
@@ -977,7 +1020,12 @@ export const DragHandle = Extension.create<DragHandleOptions>({
|
|
|
977
1020
|
) => {
|
|
978
1021
|
if (!session.preview) return;
|
|
979
1022
|
|
|
980
|
-
session.preview.style.transform =
|
|
1023
|
+
session.preview.style.transform = dragPreviewTransform({
|
|
1024
|
+
clientX,
|
|
1025
|
+
clientY,
|
|
1026
|
+
pointerOffsetX: session.pointerOffsetX,
|
|
1027
|
+
pointerOffsetY: session.pointerOffsetY,
|
|
1028
|
+
});
|
|
981
1029
|
};
|
|
982
1030
|
|
|
983
1031
|
const updateDropLine = (
|
|
@@ -1174,6 +1222,7 @@ export const DragHandle = Extension.create<DragHandleOptions>({
|
|
|
1174
1222
|
) {
|
|
1175
1223
|
const sourceNode = session.view.state.doc.nodeAt(sourceStart);
|
|
1176
1224
|
if (sourceNode) {
|
|
1225
|
+
let dropCommitted = false;
|
|
1177
1226
|
const sourceRegistration = registrationForView(session.view);
|
|
1178
1227
|
const transferData = sourceRegistration?.getDragTransferData?.({
|
|
1179
1228
|
view: session.view,
|
|
@@ -1198,7 +1247,7 @@ export const DragHandle = Extension.create<DragHandleOptions>({
|
|
|
1198
1247
|
false);
|
|
1199
1248
|
|
|
1200
1249
|
if (handled) {
|
|
1201
|
-
|
|
1250
|
+
dropCommitted = true;
|
|
1202
1251
|
} else if (target.view === session.view) {
|
|
1203
1252
|
const insertPos =
|
|
1204
1253
|
dropPos > sourceStart ? dropPos - sourceNode.nodeSize : dropPos;
|
|
@@ -1206,10 +1255,8 @@ export const DragHandle = Extension.create<DragHandleOptions>({
|
|
|
1206
1255
|
.delete(sourceStart, sourceEnd)
|
|
1207
1256
|
.insert(insertPos, sourceNode);
|
|
1208
1257
|
|
|
1209
|
-
tr.setSelection(NodeSelection.create(tr.doc, insertPos));
|
|
1210
|
-
|
|
1211
1258
|
session.view.dispatch(tr.scrollIntoView());
|
|
1212
|
-
|
|
1259
|
+
dropCommitted = true;
|
|
1213
1260
|
} else {
|
|
1214
1261
|
try {
|
|
1215
1262
|
const targetNode = target.view.state.schema.nodeFromJSON(
|
|
@@ -1225,9 +1272,6 @@ export const DragHandle = Extension.create<DragHandleOptions>({
|
|
|
1225
1272
|
dropPos,
|
|
1226
1273
|
targetNode,
|
|
1227
1274
|
);
|
|
1228
|
-
insertTr.setSelection(
|
|
1229
|
-
NodeSelection.create(insertTr.doc, dropPos),
|
|
1230
|
-
);
|
|
1231
1275
|
target.view.dispatch(insertTr.scrollIntoView());
|
|
1232
1276
|
|
|
1233
1277
|
const deleteTr = session.view.state.tr.delete(
|
|
@@ -1235,12 +1279,19 @@ export const DragHandle = Extension.create<DragHandleOptions>({
|
|
|
1235
1279
|
sourceEnd,
|
|
1236
1280
|
);
|
|
1237
1281
|
session.view.dispatch(deleteTr);
|
|
1238
|
-
|
|
1282
|
+
dropCommitted = true;
|
|
1239
1283
|
} catch {
|
|
1240
1284
|
// If the target schema cannot accept this node, leave the
|
|
1241
1285
|
// source document untouched.
|
|
1242
1286
|
}
|
|
1243
1287
|
}
|
|
1288
|
+
|
|
1289
|
+
if (dropCommitted) {
|
|
1290
|
+
collapseAndBlurEditorAfterDrop(target.view, dropPos);
|
|
1291
|
+
if (target.view !== session.view) {
|
|
1292
|
+
collapseAndBlurEditorAfterDrop(session.view, sourceStart);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1244
1295
|
}
|
|
1245
1296
|
}
|
|
1246
1297
|
} else if (commit && !session.dragging && event) {
|
|
@@ -1356,6 +1407,7 @@ export const DragHandle = Extension.create<DragHandleOptions>({
|
|
|
1356
1407
|
if (!sourceNode) return;
|
|
1357
1408
|
|
|
1358
1409
|
e.preventDefault();
|
|
1410
|
+
const sourceRect = currentBlock.getBoundingClientRect();
|
|
1359
1411
|
dragSession = {
|
|
1360
1412
|
view: editorView,
|
|
1361
1413
|
sourceBlock: currentBlock,
|
|
@@ -1363,6 +1415,8 @@ export const DragHandle = Extension.create<DragHandleOptions>({
|
|
|
1363
1415
|
sourceNodeSize: sourceNode.nodeSize,
|
|
1364
1416
|
startX: e.clientX,
|
|
1365
1417
|
startY: e.clientY,
|
|
1418
|
+
pointerOffsetX: e.clientX - sourceRect.left,
|
|
1419
|
+
pointerOffsetY: e.clientY - sourceRect.top,
|
|
1366
1420
|
dragging: false,
|
|
1367
1421
|
preview: null,
|
|
1368
1422
|
dropLine: null,
|