@agent-native/core 0.161.9 → 0.161.12
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/corpus/README.md +1 -1
- package/corpus/templates/calendar/changelog/2026-08-18-calendar-event-time-editing-respects-the-event-timezone.md +6 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +1 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +1 -1
- package/corpus/templates/clips/changelog/2026-08-18-centered-clip-play-button-and-comment-panel-hint.md +6 -0
- package/corpus/templates/content/actions/export-document.ts +68 -3
- package/corpus/templates/content/changelog/2026-08-06-database-exports-now-include-their-pages.md +6 -0
- package/corpus/templates/content/shared/document-export.ts +25 -0
- package/corpus/templates/design/.agents/skills/creative-context/SKILL.md +3 -0
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +66 -0
- package/corpus/templates/design/app/pages/Index.tsx +45 -1
- package/corpus/templates/design/app/pages/design-editor/creative-context-precedent.ts +233 -0
- package/corpus/templates/design/changelog/2026-08-11-new-designs-now-skip-the-intake-questions-and-follow-your-ex.md +6 -0
- package/corpus/templates/factory/actions/start-builder-for-item.ts +241 -22
- package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -5
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +50 -14
- package/corpus/templates/factory/server/triage/builder-executor.ts +1 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +231 -9
- package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +56 -9
- package/corpus/templates/slides/app/context/DeckContext.tsx +99 -3
- package/corpus/templates/slides/app/i18n/en-US.ts +6 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +8 -2
- package/dist/action.js +19 -2
- package/dist/agent/chat-health-alert.d.ts +25 -0
- package/dist/agent/chat-health-alert.js +141 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.js +34 -4
- package/dist/client/embed-auth.js +40 -3
- package/dist/client/route-state.js +42 -5
- package/dist/client/use-change-version.js +71 -25
- package/dist/client/use-db-sync.d.ts +1 -0
- package/dist/client/use-db-sync.js +223 -45
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.js +37 -4
- package/dist/collab/client.js +17 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat-plugin.js +15 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/poll.d.ts +22 -14
- package/dist/server/poll.js +243 -67
- package/package.json +1 -1
|
@@ -662,9 +662,13 @@ interface SlideEditorProps {
|
|
|
662
662
|
* reconcile path knows not to replace the deck under an active in-progress
|
|
663
663
|
* edit, even before a `content` update has been flushed.
|
|
664
664
|
*/
|
|
665
|
-
onInlineEditStart?: () => void;
|
|
666
|
-
/** Called after inline edit mode exits and its draft has been handed off.
|
|
667
|
-
|
|
665
|
+
onInlineEditStart?: (slideId: string) => void;
|
|
666
|
+
/** Called after inline edit mode exits and its draft has been handed off.
|
|
667
|
+
* Always receives the slide that was actually being edited — the slide
|
|
668
|
+
* prop may have already advanced to a new slide by the time this fires
|
|
669
|
+
* (see the slide-switch effect), so callers must not substitute their own
|
|
670
|
+
* "current slide" state for this argument. */
|
|
671
|
+
onInlineEditEnd?: (slideId: string) => void;
|
|
668
672
|
/** Other users (besides the current user) currently viewing/editing THIS
|
|
669
673
|
* slide. Drives the soft same-slide-edit indicator on the canvas so a user
|
|
670
674
|
* knows before they clobber someone else's last-writer-wins text edit. */
|
|
@@ -1183,6 +1187,11 @@ export default function SlideEditor({
|
|
|
1183
1187
|
() => getCopiedElementStyle() !== null,
|
|
1184
1188
|
);
|
|
1185
1189
|
const contextMenuTargetRef = useRef<HTMLElement | null>(null);
|
|
1190
|
+
const contextMenuTableCellRef = useRef<HTMLTableCellElement | null>(null);
|
|
1191
|
+
const [contextMenuTableInfo, setContextMenuTableInfo] = useState<{
|
|
1192
|
+
rowCount: number;
|
|
1193
|
+
colCount: number;
|
|
1194
|
+
} | null>(null);
|
|
1186
1195
|
const selectionOverlayMeasurementKey = createSelectionOverlayMeasurementKey({
|
|
1187
1196
|
slideId: slide.id,
|
|
1188
1197
|
content,
|
|
@@ -1527,6 +1536,13 @@ export default function SlideEditor({
|
|
|
1527
1536
|
useEffect(() => {
|
|
1528
1537
|
onUpdateSlideRef.current = onUpdateSlide;
|
|
1529
1538
|
}, [onUpdateSlide]);
|
|
1539
|
+
/** Latest onInlineEditEnd in a ref so the unmount cleanup below (which must
|
|
1540
|
+
* run with a stable, empty dependency array) always calls the current
|
|
1541
|
+
* version instead of whatever was passed on the very first render. */
|
|
1542
|
+
const onInlineEditEndRef = useRef(onInlineEditEnd);
|
|
1543
|
+
useEffect(() => {
|
|
1544
|
+
onInlineEditEndRef.current = onInlineEditEnd;
|
|
1545
|
+
}, [onInlineEditEnd]);
|
|
1530
1546
|
const inlineEditDraftRef = useRef<InlineEditContentSnapshot | null>(null);
|
|
1531
1547
|
const inlineEditInitialContentRef = useRef<InlineEditContentSnapshot | null>(
|
|
1532
1548
|
null,
|
|
@@ -1870,7 +1886,7 @@ export default function SlideEditor({
|
|
|
1870
1886
|
) {
|
|
1871
1887
|
onUpdateSlideRef.current({ content: html });
|
|
1872
1888
|
}
|
|
1873
|
-
onInlineEditEnd?.();
|
|
1889
|
+
onInlineEditEnd?.(slide.id);
|
|
1874
1890
|
inlineEditDraftRef.current = null;
|
|
1875
1891
|
inlineEditInitialContentRef.current = null;
|
|
1876
1892
|
const escape = slidesCanvasInteractionCore.escape({
|
|
@@ -1910,7 +1926,7 @@ export default function SlideEditor({
|
|
|
1910
1926
|
// Mark the deck dirty immediately so SSE/poll refreshes do not replace
|
|
1911
1927
|
// the deck under an active contentEditable edit, even before the user
|
|
1912
1928
|
// types and triggers an onUpdateSlide flush.
|
|
1913
|
-
onInlineEditStart?.();
|
|
1929
|
+
onInlineEditStart?.(slide.id);
|
|
1914
1930
|
// Don't override the selection. The browser's native double-click
|
|
1915
1931
|
// word-select (or single-click caret) is already on the element from the
|
|
1916
1932
|
// user's gesture; re-selecting from JS clobbers it. focus() on an
|
|
@@ -1953,12 +1969,23 @@ export default function SlideEditor({
|
|
|
1953
1969
|
onUpdateSlideRef.current({ content: draft.content }, previousSlideId);
|
|
1954
1970
|
inlineEditDraftRef.current = null;
|
|
1955
1971
|
}
|
|
1956
|
-
if (editing || draft) onInlineEditEnd?.();
|
|
1972
|
+
if (editing || draft) onInlineEditEnd?.(previousSlideId);
|
|
1957
1973
|
inlineEditInitialContentRef.current = null;
|
|
1958
1974
|
|
|
1959
1975
|
previousSlideIdRef.current = slide.id;
|
|
1960
1976
|
}, [onInlineEditEnd, slide.id]);
|
|
1961
1977
|
|
|
1978
|
+
// Editor unmount (navigating away, closing the deck) skips the slide-switch
|
|
1979
|
+
// effect above entirely, so an active edit's "mid-edit" marker would
|
|
1980
|
+
// otherwise never clear and permanently block live sync for that slide.
|
|
1981
|
+
useEffect(() => {
|
|
1982
|
+
return () => {
|
|
1983
|
+
if (editingElRef.current || inlineEditDraftRef.current) {
|
|
1984
|
+
onInlineEditEndRef.current?.(previousSlideIdRef.current);
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
}, []);
|
|
1988
|
+
|
|
1962
1989
|
useEffect(() => {
|
|
1963
1990
|
if (!editingEl) return;
|
|
1964
1991
|
const editingSlideId = slide.id;
|
|
@@ -2608,6 +2635,107 @@ export default function SlideEditor({
|
|
|
2608
2635
|
resolveSelectedElement,
|
|
2609
2636
|
]);
|
|
2610
2637
|
|
|
2638
|
+
const commitTableMutation = useCallback(() => {
|
|
2639
|
+
const html = readCurrentSlideContentHtml();
|
|
2640
|
+
if (html !== null) onUpdateSlideRef.current({ content: html });
|
|
2641
|
+
}, [readCurrentSlideContentHtml]);
|
|
2642
|
+
|
|
2643
|
+
/** Row/column ops below index cells by position, which only holds for a
|
|
2644
|
+
* regular grid. `colspan`/`rowspan` are valid on imported tables
|
|
2645
|
+
* (sanitize-slide-html allows them), so every op bails here rather than
|
|
2646
|
+
* silently touching the wrong cell. */
|
|
2647
|
+
const tableHasMergedCells = useCallback((table: HTMLTableElement) => {
|
|
2648
|
+
return Array.from(table.rows).some((row) =>
|
|
2649
|
+
Array.from(row.cells).some((c) => c.colSpan > 1 || c.rowSpan > 1),
|
|
2650
|
+
);
|
|
2651
|
+
}, []);
|
|
2652
|
+
|
|
2653
|
+
/** Column ops change cell count per row but not col tracks. A colgroup
|
|
2654
|
+
* (imported decks can carry one — sanitize-slide-html allows it)
|
|
2655
|
+
* positionally maps each col element to a column, so an unsynced insert
|
|
2656
|
+
* or delete leaves too few/many tracks and visually misaligns columns.
|
|
2657
|
+
* Row ops don't touch columns, so this only guards column ops. */
|
|
2658
|
+
const tableHasColgroup = useCallback((table: HTMLTableElement) => {
|
|
2659
|
+
return table.querySelector("colgroup") !== null;
|
|
2660
|
+
}, []);
|
|
2661
|
+
|
|
2662
|
+
const insertTableRow = useCallback(
|
|
2663
|
+
(cell: HTMLTableCellElement, position: "above" | "below") => {
|
|
2664
|
+
const row = cell.closest("tr");
|
|
2665
|
+
const table = row?.closest("table");
|
|
2666
|
+
if (!row || !table || tableHasMergedCells(table)) return;
|
|
2667
|
+
const newRow = row.cloneNode(true) as HTMLTableRowElement;
|
|
2668
|
+
Array.from(newRow.cells).forEach((c) => {
|
|
2669
|
+
c.innerHTML = "";
|
|
2670
|
+
});
|
|
2671
|
+
row.insertAdjacentElement(
|
|
2672
|
+
position === "above" ? "beforebegin" : "afterend",
|
|
2673
|
+
newRow,
|
|
2674
|
+
);
|
|
2675
|
+
commitTableMutation();
|
|
2676
|
+
},
|
|
2677
|
+
[commitTableMutation, tableHasMergedCells],
|
|
2678
|
+
);
|
|
2679
|
+
|
|
2680
|
+
const deleteTableRow = useCallback(
|
|
2681
|
+
(cell: HTMLTableCellElement) => {
|
|
2682
|
+
const row = cell.closest("tr");
|
|
2683
|
+
const table = row?.closest("table");
|
|
2684
|
+
if (
|
|
2685
|
+
!row ||
|
|
2686
|
+
!table ||
|
|
2687
|
+
table.rows.length <= 1 ||
|
|
2688
|
+
tableHasMergedCells(table)
|
|
2689
|
+
) {
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
row.remove();
|
|
2693
|
+
commitTableMutation();
|
|
2694
|
+
},
|
|
2695
|
+
[commitTableMutation, tableHasMergedCells],
|
|
2696
|
+
);
|
|
2697
|
+
|
|
2698
|
+
const insertTableColumn = useCallback(
|
|
2699
|
+
(cell: HTMLTableCellElement, position: "left" | "right") => {
|
|
2700
|
+
const table = cell.closest("table");
|
|
2701
|
+
if (!table) return;
|
|
2702
|
+
const columnIndex = cell.cellIndex;
|
|
2703
|
+
Array.from(table.rows).forEach((row) => {
|
|
2704
|
+
const refCell = row.cells[columnIndex];
|
|
2705
|
+
if (!refCell) return;
|
|
2706
|
+
if (tableHasMergedCells(table) || tableHasColgroup(table)) return;
|
|
2707
|
+
const newCell = document.createElement(
|
|
2708
|
+
refCell.tagName,
|
|
2709
|
+
) as HTMLTableCellElement;
|
|
2710
|
+
newCell.className = refCell.className;
|
|
2711
|
+
const style = refCell.getAttribute("style");
|
|
2712
|
+
if (style) newCell.setAttribute("style", style);
|
|
2713
|
+
refCell.insertAdjacentElement(
|
|
2714
|
+
position === "left" ? "beforebegin" : "afterend",
|
|
2715
|
+
newCell,
|
|
2716
|
+
);
|
|
2717
|
+
});
|
|
2718
|
+
commitTableMutation();
|
|
2719
|
+
},
|
|
2720
|
+
[commitTableMutation, tableHasMergedCells, tableHasColgroup],
|
|
2721
|
+
);
|
|
2722
|
+
|
|
2723
|
+
const deleteTableColumn = useCallback(
|
|
2724
|
+
(cell: HTMLTableCellElement) => {
|
|
2725
|
+
const table = cell.closest("table");
|
|
2726
|
+
if (!table) return;
|
|
2727
|
+
const columnIndex = cell.cellIndex;
|
|
2728
|
+
const firstRow = table.rows[0];
|
|
2729
|
+
if (!firstRow || firstRow.cells.length <= 1) return;
|
|
2730
|
+
Array.from(table.rows).forEach((row) => {
|
|
2731
|
+
if (tableHasMergedCells(table) || tableHasColgroup(table)) return;
|
|
2732
|
+
row.cells[columnIndex]?.remove();
|
|
2733
|
+
});
|
|
2734
|
+
commitTableMutation();
|
|
2735
|
+
},
|
|
2736
|
+
[commitTableMutation, tableHasMergedCells, tableHasColgroup],
|
|
2737
|
+
);
|
|
2738
|
+
|
|
2611
2739
|
// Delete/Backspace removes the selected slide content (single or
|
|
2612
2740
|
// multi-select). Only active when something is selected for styling (not
|
|
2613
2741
|
// while inline-editing text, where Backspace should delete a character) and
|
|
@@ -2694,6 +2822,23 @@ export default function SlideEditor({
|
|
|
2694
2822
|
[],
|
|
2695
2823
|
);
|
|
2696
2824
|
|
|
2825
|
+
const findTableCell = useCallback(
|
|
2826
|
+
(
|
|
2827
|
+
target: HTMLElement,
|
|
2828
|
+
slideContent: HTMLElement,
|
|
2829
|
+
): HTMLTableCellElement | null => {
|
|
2830
|
+
let el: HTMLElement | null = target;
|
|
2831
|
+
while (el && slideContent.contains(el) && el !== slideContent) {
|
|
2832
|
+
if (el.tagName === "TD" || el.tagName === "TH") {
|
|
2833
|
+
return el as HTMLTableCellElement;
|
|
2834
|
+
}
|
|
2835
|
+
el = el.parentElement;
|
|
2836
|
+
}
|
|
2837
|
+
return null;
|
|
2838
|
+
},
|
|
2839
|
+
[],
|
|
2840
|
+
);
|
|
2841
|
+
|
|
2697
2842
|
/** True if the click is on "whitespace" inside the slide (not on any leaf) */
|
|
2698
2843
|
const isSlideWhitespaceTarget = useCallback(
|
|
2699
2844
|
(target: HTMLElement, slideContent: HTMLElement): boolean => {
|
|
@@ -4525,7 +4670,6 @@ export default function SlideEditor({
|
|
|
4525
4670
|
|
|
4526
4671
|
applyMultiSelection(hits);
|
|
4527
4672
|
};
|
|
4528
|
-
|
|
4529
4673
|
const onUp = () => finish(false);
|
|
4530
4674
|
const onCancel = () => finish(true);
|
|
4531
4675
|
window.addEventListener("pointermove", onMove);
|
|
@@ -4838,13 +4982,29 @@ export default function SlideEditor({
|
|
|
4838
4982
|
const slideContent = getSlideContent();
|
|
4839
4983
|
if (!slideContent) {
|
|
4840
4984
|
contextMenuTargetRef.current = null;
|
|
4985
|
+
contextMenuTableCellRef.current = null;
|
|
4986
|
+
setContextMenuTableInfo(null);
|
|
4841
4987
|
return;
|
|
4842
4988
|
}
|
|
4843
4989
|
|
|
4990
|
+
const tableCell = readOnly ? null : findTableCell(target, slideContent);
|
|
4991
|
+
contextMenuTableCellRef.current = tableCell;
|
|
4992
|
+
const table = tableCell?.closest("table") ?? null;
|
|
4993
|
+
setContextMenuTableInfo(
|
|
4994
|
+
table
|
|
4995
|
+
? {
|
|
4996
|
+
rowCount: table.rows.length,
|
|
4997
|
+
colCount: tableCell
|
|
4998
|
+
? (tableCell.parentElement?.childElementCount ?? 0)
|
|
4999
|
+
: 0,
|
|
5000
|
+
}
|
|
5001
|
+
: null,
|
|
5002
|
+
);
|
|
5003
|
+
|
|
4844
5004
|
const selectable = findSelectableElement(target, slideContent);
|
|
4845
5005
|
if (!selectable) {
|
|
4846
5006
|
contextMenuTargetRef.current = null;
|
|
4847
|
-
clearCanvasSelection();
|
|
5007
|
+
if (!tableCell) clearCanvasSelection();
|
|
4848
5008
|
return;
|
|
4849
5009
|
}
|
|
4850
5010
|
|
|
@@ -4866,8 +5026,10 @@ export default function SlideEditor({
|
|
|
4866
5026
|
clearCanvasSelection,
|
|
4867
5027
|
clearMultiSelection,
|
|
4868
5028
|
findSelectableElement,
|
|
5029
|
+
findTableCell,
|
|
4869
5030
|
getSlideContent,
|
|
4870
5031
|
multiSelection,
|
|
5032
|
+
readOnly,
|
|
4871
5033
|
selectElementForStyling,
|
|
4872
5034
|
],
|
|
4873
5035
|
);
|
|
@@ -5251,7 +5413,11 @@ export default function SlideEditor({
|
|
|
5251
5413
|
>
|
|
5252
5414
|
<ContextMenu
|
|
5253
5415
|
onOpenChange={(open) => {
|
|
5254
|
-
if (!open)
|
|
5416
|
+
if (!open) {
|
|
5417
|
+
contextMenuTargetRef.current = null;
|
|
5418
|
+
contextMenuTableCellRef.current = null;
|
|
5419
|
+
setContextMenuTableInfo(null);
|
|
5420
|
+
}
|
|
5255
5421
|
}}
|
|
5256
5422
|
>
|
|
5257
5423
|
<ContextMenuTrigger asChild disabled={readOnly}>
|
|
@@ -5341,6 +5507,62 @@ export default function SlideEditor({
|
|
|
5341
5507
|
</div>
|
|
5342
5508
|
</ContextMenuTrigger>
|
|
5343
5509
|
<ContextMenuContent>
|
|
5510
|
+
{contextMenuTableInfo && (
|
|
5511
|
+
<>
|
|
5512
|
+
<ContextMenuItem
|
|
5513
|
+
onSelect={() => {
|
|
5514
|
+
const cell = contextMenuTableCellRef.current;
|
|
5515
|
+
if (cell) insertTableRow(cell, "above");
|
|
5516
|
+
}}
|
|
5517
|
+
>
|
|
5518
|
+
{t("styleInspector.insertRowAbove")}
|
|
5519
|
+
</ContextMenuItem>
|
|
5520
|
+
<ContextMenuItem
|
|
5521
|
+
onSelect={() => {
|
|
5522
|
+
const cell = contextMenuTableCellRef.current;
|
|
5523
|
+
if (cell) insertTableRow(cell, "below");
|
|
5524
|
+
}}
|
|
5525
|
+
>
|
|
5526
|
+
{t("styleInspector.insertRowBelow")}
|
|
5527
|
+
</ContextMenuItem>
|
|
5528
|
+
<ContextMenuItem
|
|
5529
|
+
disabled={contextMenuTableInfo.rowCount <= 1}
|
|
5530
|
+
onSelect={() => {
|
|
5531
|
+
const cell = contextMenuTableCellRef.current;
|
|
5532
|
+
if (cell) deleteTableRow(cell);
|
|
5533
|
+
}}
|
|
5534
|
+
>
|
|
5535
|
+
{t("styleInspector.deleteRow")}
|
|
5536
|
+
</ContextMenuItem>
|
|
5537
|
+
<ContextMenuSeparator />
|
|
5538
|
+
<ContextMenuItem
|
|
5539
|
+
onSelect={() => {
|
|
5540
|
+
const cell = contextMenuTableCellRef.current;
|
|
5541
|
+
if (cell) insertTableColumn(cell, "left");
|
|
5542
|
+
}}
|
|
5543
|
+
>
|
|
5544
|
+
{t("styleInspector.insertColumnLeft")}
|
|
5545
|
+
</ContextMenuItem>
|
|
5546
|
+
<ContextMenuItem
|
|
5547
|
+
onSelect={() => {
|
|
5548
|
+
const cell = contextMenuTableCellRef.current;
|
|
5549
|
+
if (cell) insertTableColumn(cell, "right");
|
|
5550
|
+
}}
|
|
5551
|
+
>
|
|
5552
|
+
{t("styleInspector.insertColumnRight")}
|
|
5553
|
+
</ContextMenuItem>
|
|
5554
|
+
<ContextMenuItem
|
|
5555
|
+
disabled={contextMenuTableInfo.colCount <= 1}
|
|
5556
|
+
onSelect={() => {
|
|
5557
|
+
const cell = contextMenuTableCellRef.current;
|
|
5558
|
+
if (cell) deleteTableColumn(cell);
|
|
5559
|
+
}}
|
|
5560
|
+
>
|
|
5561
|
+
{t("styleInspector.deleteColumn")}
|
|
5562
|
+
</ContextMenuItem>
|
|
5563
|
+
<ContextMenuSeparator />
|
|
5564
|
+
</>
|
|
5565
|
+
)}
|
|
5344
5566
|
<ContextMenuItem
|
|
5345
5567
|
disabled={!hasObjectSelection}
|
|
5346
5568
|
onSelect={copySelectedObjects}
|
|
@@ -15,7 +15,25 @@ export function isAgentSidebarVisible() {
|
|
|
15
15
|
if (style.display === "none" || style.visibility === "hidden") return false;
|
|
16
16
|
|
|
17
17
|
const rect = panel.getBoundingClientRect();
|
|
18
|
-
|
|
18
|
+
if (rect.width <= 0 || rect.height <= 0) return false;
|
|
19
|
+
|
|
20
|
+
for (
|
|
21
|
+
let ancestor = panel.parentElement;
|
|
22
|
+
ancestor && ancestor !== document.body;
|
|
23
|
+
ancestor = ancestor.parentElement
|
|
24
|
+
) {
|
|
25
|
+
if (ancestor.getAttribute("aria-hidden") === "true") return false;
|
|
26
|
+
if (ancestor.inert) return false;
|
|
27
|
+
const ancestorStyle = window.getComputedStyle(ancestor);
|
|
28
|
+
if (
|
|
29
|
+
ancestorStyle.display === "none" ||
|
|
30
|
+
ancestorStyle.visibility === "hidden"
|
|
31
|
+
) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return true;
|
|
19
37
|
}
|
|
20
38
|
|
|
21
39
|
function useAgentSidebarVisible() {
|
|
@@ -25,20 +43,49 @@ function useAgentSidebarVisible() {
|
|
|
25
43
|
const update = () => setVisible(isAgentSidebarVisible());
|
|
26
44
|
update();
|
|
27
45
|
|
|
28
|
-
|
|
29
|
-
observer
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
// The panel is a portal and is normally a direct child of body. Discover
|
|
47
|
+
// portal mount/unmounts with a shallow child-list observer, then watch only
|
|
48
|
+
// the panel and its immediate parent. Observing every body attribute and
|
|
49
|
+
// descendant mutation made this tiny indicator run on every editor render.
|
|
50
|
+
let panelObserver: MutationObserver | null = null;
|
|
51
|
+
let parentObserver: MutationObserver | null = null;
|
|
52
|
+
|
|
53
|
+
const observePanel = () => {
|
|
54
|
+
panelObserver?.disconnect();
|
|
55
|
+
parentObserver?.disconnect();
|
|
56
|
+
const panel = document.querySelector<HTMLElement>(".agent-sidebar-panel");
|
|
57
|
+
if (!panel) return;
|
|
58
|
+
|
|
59
|
+
panelObserver = new MutationObserver(update);
|
|
60
|
+
panelObserver.observe(panel, {
|
|
61
|
+
attributes: true,
|
|
62
|
+
attributeFilter: ["aria-hidden", "class", "inert", "style"],
|
|
63
|
+
});
|
|
64
|
+
if (panel.parentElement && panel.parentElement !== document.body) {
|
|
65
|
+
parentObserver = new MutationObserver(update);
|
|
66
|
+
parentObserver.observe(panel.parentElement, {
|
|
67
|
+
attributes: true,
|
|
68
|
+
attributeFilter: ["aria-hidden", "class", "inert", "style"],
|
|
69
|
+
childList: true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const updateAndObserve = () => {
|
|
74
|
+
update();
|
|
75
|
+
observePanel();
|
|
76
|
+
};
|
|
77
|
+
const discovery = new MutationObserver(updateAndObserve);
|
|
78
|
+
discovery.observe(document.body, { childList: true });
|
|
79
|
+
updateAndObserve();
|
|
35
80
|
window.addEventListener("resize", update);
|
|
36
81
|
window.addEventListener("agent-panel:open", update);
|
|
37
82
|
window.addEventListener("agent-panel:toggle", update);
|
|
38
83
|
window.addEventListener("agent-panel:set-mode", update);
|
|
39
84
|
|
|
40
85
|
return () => {
|
|
41
|
-
|
|
86
|
+
discovery.disconnect();
|
|
87
|
+
panelObserver?.disconnect();
|
|
88
|
+
parentObserver?.disconnect();
|
|
42
89
|
window.removeEventListener("resize", update);
|
|
43
90
|
window.removeEventListener("agent-panel:open", update);
|
|
44
91
|
window.removeEventListener("agent-panel:toggle", update);
|
|
@@ -379,6 +379,35 @@ const DECK_SAVE_RETRY_BASE_MS = 250;
|
|
|
379
379
|
// Ops are appended by enqueueDeckOp and drained when the debounce fires.
|
|
380
380
|
const pendingOpsQueue = new Map<string, GranularOp[]>();
|
|
381
381
|
|
|
382
|
+
// The ops a deck's current in-flight save actually sent, so a slide-scoped
|
|
383
|
+
// check can tell "this slide's save is in flight" from "some other slide in
|
|
384
|
+
// this deck has a save in flight" — `inFlightSaves` alone can't, since it is
|
|
385
|
+
// deck-wide and previously made every slide in the deck look pending for the
|
|
386
|
+
// whole request duration, starving unrelated agent writes of live sync.
|
|
387
|
+
const inFlightOpSlides = new Map<string, GranularOp[]>();
|
|
388
|
+
|
|
389
|
+
// Slides currently mid inline-edit (contentEditable open, keystrokes not yet
|
|
390
|
+
// committed to a "patch-slide" op). `onInlineEditStart`/`onInlineEditEnd`
|
|
391
|
+
// keep this current; a slide only reaches `pendingOpsQueue` when editing
|
|
392
|
+
// ends, so this is the only signal that catches live, uncommitted typing.
|
|
393
|
+
const activeInlineEditSlides = new Map<string, Set<string>>();
|
|
394
|
+
|
|
395
|
+
/** Mark `slideId` as mid inline-edit so a concurrent agent write to the same
|
|
396
|
+
* slide is not adopted over the user's uncommitted keystrokes. */
|
|
397
|
+
export function markSlideEditingActive(deckId: string, slideId: string) {
|
|
398
|
+
const set = activeInlineEditSlides.get(deckId) ?? new Set<string>();
|
|
399
|
+
set.add(slideId);
|
|
400
|
+
activeInlineEditSlides.set(deckId, set);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Clear the mid-edit mark once inline editing ends (committed or discarded). */
|
|
404
|
+
export function clearSlideEditingActive(deckId: string, slideId: string) {
|
|
405
|
+
const set = activeInlineEditSlides.get(deckId);
|
|
406
|
+
if (!set) return;
|
|
407
|
+
set.delete(slideId);
|
|
408
|
+
if (set.size === 0) activeInlineEditSlides.delete(deckId);
|
|
409
|
+
}
|
|
410
|
+
|
|
382
411
|
// Cached snapshot for useSyncExternalStore. MUST be stable when either value
|
|
383
412
|
// is unchanged or React will infinite-loop (it compares snapshots with
|
|
384
413
|
// Object.is — a fresh object literal every call schedules a new update,
|
|
@@ -528,6 +557,7 @@ function drainPendingDeckOps(deckId: string): Promise<void> {
|
|
|
528
557
|
typeof AbortController === "undefined" ? null : new AbortController();
|
|
529
558
|
if (controller) inFlightSaveControllers.set(deckId, controller);
|
|
530
559
|
inFlightSaves.add(deckId);
|
|
560
|
+
inFlightOpSlides.set(deckId, ops);
|
|
531
561
|
const isCurrentGeneration = () =>
|
|
532
562
|
(deckSaveGenerations.get(deckId) ?? 0) === generation;
|
|
533
563
|
const next = persistDeckOps(deckId, ops, controller?.signal)
|
|
@@ -568,6 +598,7 @@ function drainPendingDeckOps(deckId: string): Promise<void> {
|
|
|
568
598
|
inFlightSaveControllers.delete(deckId);
|
|
569
599
|
}
|
|
570
600
|
inFlightSaves.delete(deckId);
|
|
601
|
+
inFlightOpSlides.delete(deckId);
|
|
571
602
|
const flushImmediately = immediateFlushRequests.delete(deckId);
|
|
572
603
|
notifySaveListeners();
|
|
573
604
|
if (flushImmediately) {
|
|
@@ -1250,6 +1281,61 @@ export function mergeServerAddedSlides(local: Deck, server: Deck): Deck {
|
|
|
1250
1281
|
return { ...local, slides: merged };
|
|
1251
1282
|
}
|
|
1252
1283
|
|
|
1284
|
+
/** True when `op` is a deck-wide write (touches every slide) or targets
|
|
1285
|
+
* `slideId` specifically. */
|
|
1286
|
+
function opTargetsSlide(op: GranularOp, slideId: string): boolean {
|
|
1287
|
+
return (
|
|
1288
|
+
op.op === "full-replace" ||
|
|
1289
|
+
op.op === "reorder-slides" ||
|
|
1290
|
+
("slideId" in op && op.slideId === slideId)
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* True when some queued-or-in-flight write for `deckId` could still touch
|
|
1296
|
+
* `slideId`, so adopting the server's copy of that slide would race a local
|
|
1297
|
+
* write instead of reflecting it. Checking the actual ops (queued or
|
|
1298
|
+
* in-flight) rather than a deck-wide "a save is running" flag matters: an
|
|
1299
|
+
* in-flight save for slide B must not block slide A's live update for the
|
|
1300
|
+
* whole request duration. The slide being mid inline-edit (typing not yet
|
|
1301
|
+
* committed to any op) also counts as a pending write.
|
|
1302
|
+
*/
|
|
1303
|
+
function hasPendingWriteForSlide(deckId: string, slideId: string): boolean {
|
|
1304
|
+
if (activeInlineEditSlides.get(deckId)?.has(slideId)) return true;
|
|
1305
|
+
const inFlightOps = inFlightOpSlides.get(deckId);
|
|
1306
|
+
if (inFlightOps?.some((op) => opTargetsSlide(op, slideId))) return true;
|
|
1307
|
+
const queue = pendingOpsQueue.get(deckId);
|
|
1308
|
+
if (!queue) return false;
|
|
1309
|
+
return queue.some((op) => opTargetsSlide(op, slideId));
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Same additive merge as `mergeServerAddedSlides`, but additionally adopts
|
|
1314
|
+
* the server's content for `changedSlideId` when no local write is pending
|
|
1315
|
+
* for that specific slide. This closes the gap where an agent edit to a
|
|
1316
|
+
* slide that already exists locally (e.g. removing a table row) was
|
|
1317
|
+
* otherwise invisible until the deck went fully clean or the page reloaded —
|
|
1318
|
+
* see the module doc on `refetchOpenDeckIfChanged`.
|
|
1319
|
+
*/
|
|
1320
|
+
export function mergeServerSlideUpdate(
|
|
1321
|
+
local: Deck,
|
|
1322
|
+
server: Deck,
|
|
1323
|
+
changedSlideId: string | undefined,
|
|
1324
|
+
deckId: string,
|
|
1325
|
+
): Deck {
|
|
1326
|
+
const merged = mergeServerAddedSlides(local, server);
|
|
1327
|
+
if (!changedSlideId || hasPendingWriteForSlide(deckId, changedSlideId)) {
|
|
1328
|
+
return merged;
|
|
1329
|
+
}
|
|
1330
|
+
const serverSlide = server.slides.find((s) => s.id === changedSlideId);
|
|
1331
|
+
if (!serverSlide) return merged;
|
|
1332
|
+
const idx = merged.slides.findIndex((s) => s.id === changedSlideId);
|
|
1333
|
+
if (idx < 0 || merged.slides[idx] === serverSlide) return merged;
|
|
1334
|
+
const nextSlides = [...merged.slides];
|
|
1335
|
+
nextSlides[idx] = serverSlide;
|
|
1336
|
+
return { ...merged, slides: nextSlides };
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1253
1339
|
export const defaultSlideContent: Record<SlideLayout, string> = {
|
|
1254
1340
|
title: `<div class="fmd-slide" style="padding: 80px 110px; justify-content: space-between;">
|
|
1255
1341
|
<div>
|
|
@@ -1557,7 +1643,7 @@ export function DeckProvider({ children }: { children: ReactNode }) {
|
|
|
1557
1643
|
const refetchOpenDeckIfChanged = useCallback(
|
|
1558
1644
|
async (
|
|
1559
1645
|
currentOpenId: string,
|
|
1560
|
-
options?: { clearPendingWrites?: boolean },
|
|
1646
|
+
options?: { clearPendingWrites?: boolean; changedSlideId?: string },
|
|
1561
1647
|
): Promise<Deck | null> => {
|
|
1562
1648
|
const requestId =
|
|
1563
1649
|
(openDeckRequestIdByDeckRef.current.get(currentOpenId) ?? 0) + 1;
|
|
@@ -1584,7 +1670,12 @@ export function DeckProvider({ children }: { children: ReactNode }) {
|
|
|
1584
1670
|
|
|
1585
1671
|
if (hasLocalEdits && clientDeck) {
|
|
1586
1672
|
// Content-preserving: only ADD server slides missing locally.
|
|
1587
|
-
const merged =
|
|
1673
|
+
const merged = mergeServerSlideUpdate(
|
|
1674
|
+
clientDeck,
|
|
1675
|
+
serverDeck,
|
|
1676
|
+
options?.changedSlideId,
|
|
1677
|
+
currentOpenId,
|
|
1678
|
+
);
|
|
1588
1679
|
if (merged === clientDeck) return serverDeck; // nothing new to surface
|
|
1589
1680
|
lastExternalUpdateRef.current = Date.now();
|
|
1590
1681
|
setDecks((prev) => {
|
|
@@ -1874,7 +1965,12 @@ export function DeckProvider({ children }: { children: ReactNode }) {
|
|
|
1874
1965
|
// write that arrived during the same local edit. The reconciler
|
|
1875
1966
|
// preserves local slide bodies and local-only slides while still
|
|
1876
1967
|
// surfacing server-added slides immediately.
|
|
1877
|
-
|
|
1968
|
+
const changedSlideId =
|
|
1969
|
+
typeof data.slideId === "string" ? data.slideId : undefined;
|
|
1970
|
+
const refetchPromise = refetchOpenDeckIfChanged(data.deckId, {
|
|
1971
|
+
changedSlideId,
|
|
1972
|
+
});
|
|
1973
|
+
void refetchPromise.catch((error) => {
|
|
1878
1974
|
console.error(
|
|
1879
1975
|
`Failed to refresh deck ${data.deckId} after sync event:`,
|
|
1880
1976
|
error,
|
|
@@ -444,6 +444,12 @@ const messages = {
|
|
|
444
444
|
pasteStyle: "Paste style",
|
|
445
445
|
copy: "Copy",
|
|
446
446
|
paste: "Paste",
|
|
447
|
+
insertRowAbove: "Insert row above",
|
|
448
|
+
insertRowBelow: "Insert row below",
|
|
449
|
+
deleteRow: "Delete row",
|
|
450
|
+
insertColumnLeft: "Insert column left",
|
|
451
|
+
insertColumnRight: "Insert column right",
|
|
452
|
+
deleteColumn: "Delete column",
|
|
447
453
|
},
|
|
448
454
|
animations: {
|
|
449
455
|
appear: "Appear",
|
|
@@ -58,8 +58,10 @@ import {
|
|
|
58
58
|
} from "@/components/ui/alert-dialog";
|
|
59
59
|
import { Button } from "@/components/ui/button";
|
|
60
60
|
import {
|
|
61
|
+
clearSlideEditingActive,
|
|
61
62
|
deckIdFromPathname,
|
|
62
63
|
hasUnsavedDeckChanges,
|
|
64
|
+
markSlideEditingActive,
|
|
63
65
|
type Slide,
|
|
64
66
|
useDecks,
|
|
65
67
|
useSaveState,
|
|
@@ -1561,11 +1563,15 @@ export default function DeckEditor() {
|
|
|
1561
1563
|
options,
|
|
1562
1564
|
)
|
|
1563
1565
|
}
|
|
1564
|
-
onInlineEditStart={() => {
|
|
1566
|
+
onInlineEditStart={(slideId) => {
|
|
1565
1567
|
setInlineEditActive(true);
|
|
1566
1568
|
markDeckDirty(id);
|
|
1569
|
+
if (id) markSlideEditingActive(id, slideId);
|
|
1570
|
+
}}
|
|
1571
|
+
onInlineEditEnd={(slideId) => {
|
|
1572
|
+
setInlineEditActive(false);
|
|
1573
|
+
if (id) clearSlideEditingActive(id, slideId);
|
|
1567
1574
|
}}
|
|
1568
|
-
onInlineEditEnd={() => setInlineEditActive(false)}
|
|
1569
1575
|
onGenerateImage={() => setImageGenOpen(true)}
|
|
1570
1576
|
onOpenAssetLibrary={(src) => {
|
|
1571
1577
|
setReplaceImageSrc(src);
|
package/dist/action.js
CHANGED
|
@@ -326,8 +326,8 @@ const SUBSCHEMA_MAP_KEYS = [
|
|
|
326
326
|
"dependentSchemas",
|
|
327
327
|
];
|
|
328
328
|
/**
|
|
329
|
-
* Remove JSON Schema keywords that some providers' function-calling
|
|
330
|
-
* validators reject. OpenAI (and Gemini via the Builder gateway) reject
|
|
329
|
+
* Remove or rewrite JSON Schema keywords that some providers' function-calling
|
|
330
|
+
* schema validators reject. OpenAI (and Gemini via the Builder gateway) reject
|
|
331
331
|
* `propertyNames` — which Zod v4 emits for `z.record(z.string(), …)` — with a
|
|
332
332
|
* `400 invalid_function_parameters` error, causing the model turn to produce no
|
|
333
333
|
* content (surfacing as an empty assistant response). Anthropic ignores the
|
|
@@ -335,6 +335,15 @@ const SUBSCHEMA_MAP_KEYS = [
|
|
|
335
335
|
* portable. `propertyNames` only constrained object *keys*; the value/shape of
|
|
336
336
|
* the object is unaffected by its removal.
|
|
337
337
|
*
|
|
338
|
+
* OpenAI rejects `oneOf` the same way — "Invalid schema for function 'x': …
|
|
339
|
+
* 'oneOf' is not permitted" — while accepting `anyOf`. Zod v4's `toJSONSchema`
|
|
340
|
+
* emits `oneOf` for every `z.discriminatedUnion`, so any action with one was
|
|
341
|
+
* 400'd for the whole request before a token streamed, and the gateway folded
|
|
342
|
+
* that 400 into a generic 500. Measured: 178k events / 786 users over seven
|
|
343
|
+
* weeks from a single action. For a discriminated union the two keywords accept
|
|
344
|
+
* the same documents, and the tool-input validator already handles both
|
|
345
|
+
* (see `narrowUnionBranchErrors`), so the rewrite is behaviour-preserving.
|
|
346
|
+
*
|
|
338
347
|
* Only descends through actual subschema positions (properties, items, union
|
|
339
348
|
* branches, definitions, etc.) — never through value-bearing keywords like
|
|
340
349
|
* `default`, `const`, `enum`, or `examples`, whose objects may legitimately
|
|
@@ -371,6 +380,14 @@ function stripUnsupportedSchemaKeywords(node) {
|
|
|
371
380
|
}
|
|
372
381
|
}
|
|
373
382
|
}
|
|
383
|
+
// After descending, so nested unions are rewritten too. Merged rather than
|
|
384
|
+
// overwritten on the pathological case where a schema carries both: dropping
|
|
385
|
+
// either arm would silently narrow what the tool accepts.
|
|
386
|
+
if (Array.isArray(obj.oneOf)) {
|
|
387
|
+
const existing = Array.isArray(obj.anyOf) ? obj.anyOf : [];
|
|
388
|
+
obj.anyOf = [...existing, ...obj.oneOf];
|
|
389
|
+
delete obj.oneOf;
|
|
390
|
+
}
|
|
374
391
|
return node;
|
|
375
392
|
}
|
|
376
393
|
/**
|