@agent-native/core 0.80.6 → 0.80.8
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/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +70 -27
- package/corpus/core/src/client/analytics.ts +3 -1
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +45 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +42 -4
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -1
- package/corpus/templates/analytics/actions/update-dashboard.ts +23 -9
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +72 -1
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +34 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +88 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +7 -1
- package/corpus/templates/analytics/changelog/2026-06-29-dashboards-can-now-include-extension-panels-that-embed-a-san.md +6 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +69 -16
- package/corpus/templates/content/changelog/2026-06-29-read-only-pages-no-longer-try-to-autosave-or-open-editor-onl.md +6 -0
- package/corpus/templates/design/AGENTS.md +4 -2
- package/corpus/templates/design/actions/edit-design.ts +6 -3
- package/corpus/templates/design/actions/generate-screens.ts +5 -3
- package/corpus/templates/design/actions/update-design.ts +47 -10
- package/corpus/templates/design/actions/update-file.ts +33 -8
- package/corpus/templates/design/actions/view-screen.ts +14 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +75 -17
- package/corpus/templates/design/app/components/design/EditPanel.tsx +121 -22
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +51 -8
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +148 -33
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +11 -11
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +27 -13
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +3 -37
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +18 -2
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +15 -3
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +4 -2
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +18 -4
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +10 -4
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +448 -259
- package/corpus/templates/design/app/hooks/use-variant-flow.ts +176 -92
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +10 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +220 -40
- package/corpus/templates/design/changelog/2026-06-29-agent-edits-to-a-design-are-no-longer-occasionally-dropped-o.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-box-shadow-and-gradient-colors-keep-named-css-colors-like-re.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-cut-cmd-ctrl-x-now-removes-the-selected-element-to-the-clipb.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-deleting-with-multiple-layers-selected-in-the-layers-panel-n.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-duplicating-a-screen-now-gives-the-copy-its-own-layer-ids-so.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-hiding-and-re-showing-a-gradient-fill-now-preserves-each-sto.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-picking-a-design-direction-now-submits-to-the-agent-right-aw.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-style-changes-that-set-several-properties-at-once-fixed-size.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-editor-no-longer-crashes-when-a-screen-ends-up-with-dupl.md +6 -0
- package/corpus/templates/design/e2e/README.md +1 -0
- package/corpus/templates/design/e2e/global-setup.ts +13 -1
- package/corpus/templates/design/playwright.config.ts +10 -1
- package/corpus/templates/design/shared/color-utils.ts +159 -0
- package/corpus/templates/design/shared/generation-session.ts +1 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +54 -24
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +2 -1
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts +10 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +34 -3
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -392,6 +392,9 @@ const enUS = {
|
|
|
392
392
|
off: "Off",
|
|
393
393
|
since: "Since",
|
|
394
394
|
sectionOptions: "Section options",
|
|
395
|
+
extensionMissingId: "This extension panel has no extension selected.",
|
|
396
|
+
extensionUnavailable:
|
|
397
|
+
"This extension isn't shared with you, or it no longer exists.",
|
|
395
398
|
panelOptions: "Panel options",
|
|
396
399
|
fullScreen: "Full screen",
|
|
397
400
|
refresh: "Refresh",
|
|
@@ -5249,6 +5252,8 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
5249
5252
|
saveView: "保存视图",
|
|
5250
5253
|
savedViews: "已保存的视图",
|
|
5251
5254
|
sectionOptions: "部分选项",
|
|
5255
|
+
extensionMissingId: "此扩展面板未选择任何扩展。",
|
|
5256
|
+
extensionUnavailable: "此扩展未与您共享,或已不存在。",
|
|
5252
5257
|
sharedWithOrg: "与组织共享",
|
|
5253
5258
|
unhideFailed: "无法取消隐藏仪表板",
|
|
5254
5259
|
untitledDashboard: "无标题仪表板",
|
|
@@ -5424,6 +5429,10 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
5424
5429
|
saveView: "Guardar vista",
|
|
5425
5430
|
savedViews: "Vistas guardadas",
|
|
5426
5431
|
sectionOptions: "Opciones de sección",
|
|
5432
|
+
extensionMissingId:
|
|
5433
|
+
"Este panel de extensión no tiene ninguna extensión seleccionada.",
|
|
5434
|
+
extensionUnavailable:
|
|
5435
|
+
"Esta extensión no está compartida contigo o ya no existe.",
|
|
5427
5436
|
sharedWithOrg: "Compartido con la organización",
|
|
5428
5437
|
unhideFailed: "No se pudo mostrar el panel",
|
|
5429
5438
|
untitledDashboard: "Panel de control sin título",
|
|
@@ -5601,6 +5610,10 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
5601
5610
|
saveView: "Enregistrer la vue",
|
|
5602
5611
|
savedViews: "Vues enregistrées",
|
|
5603
5612
|
sectionOptions: "Options de sections",
|
|
5613
|
+
extensionMissingId:
|
|
5614
|
+
"Ce panneau d'extension n'a aucune extension sélectionnée.",
|
|
5615
|
+
extensionUnavailable:
|
|
5616
|
+
"Cette extension n'est pas partagée avec vous ou n'existe plus.",
|
|
5604
5617
|
sharedWithOrg: "Partagé avec l'organisation",
|
|
5605
5618
|
unhideFailed: "Impossible d'afficher le tableau de bord",
|
|
5606
5619
|
untitledDashboard: "Tableau de bord sans titre",
|
|
@@ -5779,6 +5792,10 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
5779
5792
|
saveView: "Ansicht speichern",
|
|
5780
5793
|
savedViews: "Gespeicherte Ansichten",
|
|
5781
5794
|
sectionOptions: "Abschnittsoptionen",
|
|
5795
|
+
extensionMissingId:
|
|
5796
|
+
"Für dieses Erweiterungs-Panel ist keine Erweiterung ausgewählt.",
|
|
5797
|
+
extensionUnavailable:
|
|
5798
|
+
"Diese Erweiterung ist nicht für Sie freigegeben oder existiert nicht mehr.",
|
|
5782
5799
|
sharedWithOrg: "Mit Org geteilt",
|
|
5783
5800
|
unhideFailed: "Das Dashboard konnte nicht eingeblendet werden",
|
|
5784
5801
|
untitledDashboard: "Unbenanntes Dashboard",
|
|
@@ -5952,6 +5969,10 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
5952
5969
|
saveView: "ビューを保存",
|
|
5953
5970
|
savedViews: "保存されたビュー",
|
|
5954
5971
|
sectionOptions: "セクションのオプション",
|
|
5972
|
+
extensionMissingId:
|
|
5973
|
+
"この拡張機能パネルには拡張機能が選択されていません。",
|
|
5974
|
+
extensionUnavailable:
|
|
5975
|
+
"この拡張機能はあなたと共有されていないか、存在しません。",
|
|
5955
5976
|
sharedWithOrg: "組織と共有",
|
|
5956
5977
|
unhideFailed: "ダッシュボードを再表示できませんでした",
|
|
5957
5978
|
untitledDashboard: "無題のダッシュボード",
|
|
@@ -6124,6 +6145,10 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
6124
6145
|
saveView: "보기 저장",
|
|
6125
6146
|
savedViews: "저장된 보기",
|
|
6126
6147
|
sectionOptions: "섹션 옵션",
|
|
6148
|
+
extensionMissingId:
|
|
6149
|
+
"이 확장 프로그램 패널에 선택된 확장 프로그램이 없습니다.",
|
|
6150
|
+
extensionUnavailable:
|
|
6151
|
+
"이 확장 프로그램이 공유되지 않았거나 더 이상 존재하지 않습니다.",
|
|
6127
6152
|
sharedWithOrg: "조직과 공유됨",
|
|
6128
6153
|
unhideFailed: "대시보드를 숨기기 해제할 수 없습니다.",
|
|
6129
6154
|
untitledDashboard: "제목 없는 대시보드",
|
|
@@ -6300,6 +6325,10 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
6300
6325
|
saveView: "Salvar visualização",
|
|
6301
6326
|
savedViews: "Visualizações salvas",
|
|
6302
6327
|
sectionOptions: "Opções de seção",
|
|
6328
|
+
extensionMissingId:
|
|
6329
|
+
"Este painel de extensão não tem nenhuma extensão selecionada.",
|
|
6330
|
+
extensionUnavailable:
|
|
6331
|
+
"Esta extensão não foi compartilhada com você ou não existe mais.",
|
|
6303
6332
|
sharedWithOrg: "Compartilhado com a organização",
|
|
6304
6333
|
unhideFailed: "Não foi possível exibir o painel",
|
|
6305
6334
|
untitledDashboard: "Painel sem título",
|
|
@@ -6472,6 +6501,9 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
6472
6501
|
saveView: "दृश्य सहेजें",
|
|
6473
6502
|
savedViews: "सहेजे गए दृश्य",
|
|
6474
6503
|
sectionOptions: "अनुभाग विकल्प",
|
|
6504
|
+
extensionMissingId: "इस एक्सटेंशन पैनल में कोई एक्सटेंशन चयनित नहीं है।",
|
|
6505
|
+
extensionUnavailable:
|
|
6506
|
+
"यह एक्सटेंशन आपके साथ साझा नहीं किया गया है, या अब मौजूद नहीं है।",
|
|
6475
6507
|
sharedWithOrg: "संगठन के साथ साझा किया गया",
|
|
6476
6508
|
unhideFailed: "डैशबोर्ड को उजागर नहीं किया जा सका",
|
|
6477
6509
|
untitledDashboard: "शीर्षक रहित डैशबोर्ड",
|
|
@@ -6644,6 +6676,8 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
6644
6676
|
saveView: "حفظ العرض",
|
|
6645
6677
|
savedViews: "المشاهدات المحفوظة",
|
|
6646
6678
|
sectionOptions: "خيارات القسم",
|
|
6679
|
+
extensionMissingId: "لم يتم تحديد أي إضافة في لوحة الإضافة هذه.",
|
|
6680
|
+
extensionUnavailable: "هذه الإضافة غير مشاركة معك، أو لم تعد موجودة.",
|
|
6647
6681
|
sharedWithOrg: "تمت المشاركة مع منظمة",
|
|
6648
6682
|
unhideFailed: "تعذر إظهار لوحة البيانات",
|
|
6649
6683
|
untitledDashboard: "لوحة تحكم بلا عنوان",
|
|
@@ -121,7 +121,9 @@ export function SqlChartCard({
|
|
|
121
121
|
const [expanded, setExpanded] = useState(false);
|
|
122
122
|
const [exportCsv, setExportCsv] = useState<(() => void) | null>(null);
|
|
123
123
|
const [shouldLoadData, setShouldLoadData] = useState(
|
|
124
|
-
eagerLoad ||
|
|
124
|
+
eagerLoad ||
|
|
125
|
+
panel.chartType === "section" ||
|
|
126
|
+
panel.chartType === "extension",
|
|
125
127
|
);
|
|
126
128
|
const cardRef = useRef<HTMLDivElement | null>(null);
|
|
127
129
|
const chartQueryKey = useMemo(
|
|
@@ -159,7 +161,9 @@ export function SqlChartCard({
|
|
|
159
161
|
setShouldLoadData(true);
|
|
160
162
|
return;
|
|
161
163
|
}
|
|
162
|
-
|
|
164
|
+
// Sections are layout-only and extensions render their own iframe — neither
|
|
165
|
+
// waits on the intersection observer that gates SQL panels.
|
|
166
|
+
if (panel.chartType === "section" || panel.chartType === "extension") {
|
|
163
167
|
setShouldLoadData(true);
|
|
164
168
|
return;
|
|
165
169
|
}
|
|
@@ -286,6 +290,88 @@ export function SqlChartCard({
|
|
|
286
290
|
);
|
|
287
291
|
}
|
|
288
292
|
|
|
293
|
+
// Extension panels render their sandboxed iframe full-bleed with no card chrome
|
|
294
|
+
// or title — the extension owns its own UI. Editable dashboards still get a
|
|
295
|
+
// hover overlay for delete/drag. Editing routes through the agent (the manual
|
|
296
|
+
// panel editor has no extension picker), so no inline edit action here.
|
|
297
|
+
if (panel.chartType === "extension") {
|
|
298
|
+
return (
|
|
299
|
+
<div
|
|
300
|
+
ref={setCardNodeRef}
|
|
301
|
+
style={isDragSource ? { zIndex: 50 } : undefined}
|
|
302
|
+
data-dragging={isDragSource ? "true" : undefined}
|
|
303
|
+
className="dashboard-extension-card group relative h-full"
|
|
304
|
+
>
|
|
305
|
+
<SqlChart panel={panel} resolvedSql={resolvedSql} loadData />
|
|
306
|
+
{editable ? (
|
|
307
|
+
<div className="absolute right-1 top-1 flex items-center gap-1 opacity-0 group-hover:opacity-100">
|
|
308
|
+
<DropdownMenu>
|
|
309
|
+
<Tooltip>
|
|
310
|
+
<TooltipTrigger asChild>
|
|
311
|
+
<DropdownMenuTrigger asChild>
|
|
312
|
+
<button
|
|
313
|
+
className="p-1 rounded bg-background/80 text-muted-foreground hover:text-foreground"
|
|
314
|
+
aria-label={t("sqlDashboard.panelOptions")}
|
|
315
|
+
>
|
|
316
|
+
<IconDotsVertical className="h-3.5 w-3.5" />
|
|
317
|
+
</button>
|
|
318
|
+
</DropdownMenuTrigger>
|
|
319
|
+
</TooltipTrigger>
|
|
320
|
+
<TooltipContent>
|
|
321
|
+
{t("sqlDashboard.panelOptions")}
|
|
322
|
+
</TooltipContent>
|
|
323
|
+
</Tooltip>
|
|
324
|
+
<DropdownMenuContent align="end" className="w-40">
|
|
325
|
+
<DropdownMenuItem
|
|
326
|
+
onSelect={(e) => {
|
|
327
|
+
e.preventDefault();
|
|
328
|
+
setConfirmOpen(true);
|
|
329
|
+
}}
|
|
330
|
+
>
|
|
331
|
+
<IconTrash className="h-4 w-4 mr-2" />
|
|
332
|
+
{t("sidebar.delete")}
|
|
333
|
+
</DropdownMenuItem>
|
|
334
|
+
</DropdownMenuContent>
|
|
335
|
+
</DropdownMenu>
|
|
336
|
+
<PanelDragHandle
|
|
337
|
+
panelId={panel.id}
|
|
338
|
+
label={t("sqlDashboard.dragToReorder")}
|
|
339
|
+
className="p-1 rounded bg-background/80 cursor-grab active:cursor-grabbing text-muted-foreground/50 hover:text-muted-foreground"
|
|
340
|
+
iconClassName="h-3.5 w-3.5"
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
343
|
+
) : null}
|
|
344
|
+
{editable ? (
|
|
345
|
+
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
|
346
|
+
<AlertDialogContent>
|
|
347
|
+
<AlertDialogHeader>
|
|
348
|
+
<AlertDialogTitle>
|
|
349
|
+
{t("sqlDashboard.deletePanelTitle")}
|
|
350
|
+
</AlertDialogTitle>
|
|
351
|
+
<AlertDialogDescription>
|
|
352
|
+
{t("sqlDashboard.deletePanelDescription", {
|
|
353
|
+
title: panel.title,
|
|
354
|
+
})}
|
|
355
|
+
</AlertDialogDescription>
|
|
356
|
+
</AlertDialogHeader>
|
|
357
|
+
<AlertDialogFooter>
|
|
358
|
+
<AlertDialogCancel>{t("sidebar.cancel")}</AlertDialogCancel>
|
|
359
|
+
<AlertDialogAction
|
|
360
|
+
onClick={() => {
|
|
361
|
+
setConfirmOpen(false);
|
|
362
|
+
onRemove();
|
|
363
|
+
}}
|
|
364
|
+
>
|
|
365
|
+
{t("sidebar.delete")}
|
|
366
|
+
</AlertDialogAction>
|
|
367
|
+
</AlertDialogFooter>
|
|
368
|
+
</AlertDialogContent>
|
|
369
|
+
</AlertDialog>
|
|
370
|
+
) : null}
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
289
375
|
// Every non-section panel exposes at least the Full screen view action, so the
|
|
290
376
|
// options menu always renders — including on read-only / shared dashboards.
|
|
291
377
|
const showPanelMenu = true;
|
|
@@ -15,7 +15,8 @@ export type ChartType =
|
|
|
15
15
|
| "pie"
|
|
16
16
|
| "section"
|
|
17
17
|
| "heatmap"
|
|
18
|
-
| "callout"
|
|
18
|
+
| "callout"
|
|
19
|
+
| "extension";
|
|
19
20
|
|
|
20
21
|
export type FilterType =
|
|
21
22
|
| "date"
|
|
@@ -79,6 +80,11 @@ export interface SqlPanelConfig {
|
|
|
79
80
|
sortable?: boolean;
|
|
80
81
|
columns?: TableColumnConfig[];
|
|
81
82
|
limit?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Extension panels only (`chartType: "extension"`): id of the extension to
|
|
85
|
+
* render inline as a sandboxed iframe instead of running the SQL pipeline.
|
|
86
|
+
*/
|
|
87
|
+
extensionId?: string;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
export interface SqlPanel {
|
|
@@ -157,10 +157,19 @@ interface DocumentEditorBodyProps {
|
|
|
157
157
|
type PendingDocumentSave = {
|
|
158
158
|
title: string;
|
|
159
159
|
content: string;
|
|
160
|
-
save: (
|
|
160
|
+
save: (
|
|
161
|
+
title: string,
|
|
162
|
+
content: string,
|
|
163
|
+
options?: DocumentSaveOptions,
|
|
164
|
+
) => unknown | Promise<unknown>;
|
|
165
|
+
canEditWhenQueued: boolean;
|
|
161
166
|
timeout: ReturnType<typeof setTimeout>;
|
|
162
167
|
};
|
|
163
168
|
|
|
169
|
+
type DocumentSaveOptions = {
|
|
170
|
+
allowQueuedSave?: boolean;
|
|
171
|
+
};
|
|
172
|
+
|
|
164
173
|
type DocumentSaveResult = {
|
|
165
174
|
contentPersisted: boolean;
|
|
166
175
|
};
|
|
@@ -274,6 +283,8 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
274
283
|
const updateDocument = useUpdateDocument();
|
|
275
284
|
const queryClient = useQueryClient();
|
|
276
285
|
const canEdit = document.canEdit ?? true;
|
|
286
|
+
const canEditRef = useRef(canEdit);
|
|
287
|
+
canEditRef.current = canEdit;
|
|
277
288
|
// The block render context (asset/upload resolvers, inline markdown reader,
|
|
278
289
|
// panel popover) is stable for the editor's lifetime. Created once here and
|
|
279
290
|
// provided alongside the content block registry so every registry block in the
|
|
@@ -324,6 +335,15 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
324
335
|
document.updatedAt ?? null,
|
|
325
336
|
);
|
|
326
337
|
documentUpdatedAtRef.current = document.updatedAt ?? null;
|
|
338
|
+
const handleBackgroundSaveError = useCallback(
|
|
339
|
+
(error: unknown) => {
|
|
340
|
+
toast.error(t("empty.genericError"), {
|
|
341
|
+
description:
|
|
342
|
+
error instanceof Error ? error.message : t("empty.genericError"),
|
|
343
|
+
});
|
|
344
|
+
},
|
|
345
|
+
[t],
|
|
346
|
+
);
|
|
327
347
|
const titleFocusedRef = useRef(false);
|
|
328
348
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
329
349
|
const titleInputRef = useRef<HTMLTextAreaElement>(null);
|
|
@@ -375,7 +395,8 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
375
395
|
}
|
|
376
396
|
: undefined;
|
|
377
397
|
|
|
378
|
-
// Collaborative editing
|
|
398
|
+
// Collaborative editing is write-only for now. Viewers get the SQL snapshot
|
|
399
|
+
// and skip collab endpoints that reject non-editor access.
|
|
379
400
|
const {
|
|
380
401
|
ydoc,
|
|
381
402
|
awareness,
|
|
@@ -384,7 +405,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
384
405
|
agentActive,
|
|
385
406
|
agentPresent,
|
|
386
407
|
} = useCollaborativeDoc({
|
|
387
|
-
docId: isLocalFileDocument ?
|
|
408
|
+
docId: canEdit && !isLocalFileDocument ? documentId : "",
|
|
388
409
|
requestSource: TAB_ID,
|
|
389
410
|
user: currentUser,
|
|
390
411
|
});
|
|
@@ -566,11 +587,16 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
566
587
|
}, [document, isLinkedLocalSourceDocument, localTitle, localContent]);
|
|
567
588
|
|
|
568
589
|
const persistDocumentUpdates = useCallback(
|
|
569
|
-
async (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
590
|
+
async (
|
|
591
|
+
updates: {
|
|
592
|
+
title?: string;
|
|
593
|
+
content?: string;
|
|
594
|
+
icon?: string | null;
|
|
595
|
+
},
|
|
596
|
+
options: DocumentSaveOptions = {},
|
|
597
|
+
): Promise<Document> => {
|
|
598
|
+
if (!options.allowQueuedSave && !canEditRef.current) return document;
|
|
599
|
+
|
|
574
600
|
const localSource = document.source;
|
|
575
601
|
const isLinkedLocalSource = canWriteLinkedLocalSource(
|
|
576
602
|
documentId,
|
|
@@ -629,7 +655,11 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
629
655
|
);
|
|
630
656
|
|
|
631
657
|
const saveDocumentImmediately = useCallback(
|
|
632
|
-
async (
|
|
658
|
+
async (
|
|
659
|
+
title: string,
|
|
660
|
+
content: string,
|
|
661
|
+
options: DocumentSaveOptions = {},
|
|
662
|
+
): Promise<DocumentSaveResult> => {
|
|
633
663
|
// Never clobber a newer server version (e.g. an agent edit we haven't
|
|
634
664
|
// reconciled into the editor yet) with the editor's current — possibly
|
|
635
665
|
// stale — content. Guard per-field using the field's own watermark.
|
|
@@ -653,7 +683,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
653
683
|
return { contentPersisted: !contentChanged };
|
|
654
684
|
}
|
|
655
685
|
|
|
656
|
-
const saved = await persistDocumentUpdates(updates);
|
|
686
|
+
const saved = await persistDocumentUpdates(updates, options);
|
|
657
687
|
// Adopt the server updatedAt per saved field.
|
|
658
688
|
const savedAt = saved?.updatedAt ?? new Date().toISOString();
|
|
659
689
|
if (updates.title !== undefined) {
|
|
@@ -700,25 +730,38 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
700
730
|
queryClient,
|
|
701
731
|
],
|
|
702
732
|
);
|
|
733
|
+
const flushPendingDocumentSave = useCallback(
|
|
734
|
+
(pending: PendingDocumentSave) => {
|
|
735
|
+
if (!pending.canEditWhenQueued) return;
|
|
736
|
+
void Promise.resolve(
|
|
737
|
+
pending.save(pending.title, pending.content, {
|
|
738
|
+
allowQueuedSave: true,
|
|
739
|
+
}),
|
|
740
|
+
).catch(handleBackgroundSaveError);
|
|
741
|
+
},
|
|
742
|
+
[handleBackgroundSaveError],
|
|
743
|
+
);
|
|
703
744
|
const debouncedSave = useCallback(
|
|
704
745
|
(title: string, content: string) => {
|
|
746
|
+
if (!canEditRef.current) return;
|
|
705
747
|
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
|
|
706
748
|
const pending: PendingDocumentSave = {
|
|
707
749
|
title,
|
|
708
750
|
content,
|
|
709
751
|
save: saveDocumentImmediately,
|
|
752
|
+
canEditWhenQueued: canEditRef.current,
|
|
710
753
|
timeout: setTimeout(() => {
|
|
711
754
|
if (pendingDocumentSaveRef.current === pending) {
|
|
712
755
|
pendingDocumentSaveRef.current = null;
|
|
713
756
|
}
|
|
714
757
|
saveTimeoutRef.current = null;
|
|
715
|
-
|
|
758
|
+
flushPendingDocumentSave(pending);
|
|
716
759
|
}, 500),
|
|
717
760
|
};
|
|
718
761
|
pendingDocumentSaveRef.current = pending;
|
|
719
762
|
saveTimeoutRef.current = pending.timeout;
|
|
720
763
|
},
|
|
721
|
-
[saveDocumentImmediately],
|
|
764
|
+
[flushPendingDocumentSave, saveDocumentImmediately],
|
|
722
765
|
);
|
|
723
766
|
|
|
724
767
|
useEffect(() => {
|
|
@@ -728,9 +771,19 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
728
771
|
clearTimeout(pending.timeout);
|
|
729
772
|
saveTimeoutRef.current = null;
|
|
730
773
|
pendingDocumentSaveRef.current = null;
|
|
731
|
-
|
|
774
|
+
flushPendingDocumentSave(pending);
|
|
732
775
|
};
|
|
733
|
-
}, [documentId]);
|
|
776
|
+
}, [documentId, flushPendingDocumentSave]);
|
|
777
|
+
|
|
778
|
+
useEffect(() => {
|
|
779
|
+
if (canEdit) return;
|
|
780
|
+
const pending = pendingDocumentSaveRef.current;
|
|
781
|
+
if (!pending) return;
|
|
782
|
+
clearTimeout(pending.timeout);
|
|
783
|
+
saveTimeoutRef.current = null;
|
|
784
|
+
pendingDocumentSaveRef.current = null;
|
|
785
|
+
flushPendingDocumentSave(pending);
|
|
786
|
+
}, [canEdit, documentId, flushPendingDocumentSave]);
|
|
734
787
|
|
|
735
788
|
// Collab-aware ingest flush: the `pull-document` action writes a one-shot
|
|
736
789
|
// `flush-request-<id>` app-state key when an external agent wants to ingest
|
|
@@ -846,7 +899,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
846
899
|
const [hoveredThreadId, setHoveredThreadId] = useState<string | null>(null);
|
|
847
900
|
const activeThreadId = hoveredThreadId ?? selectedThreadId;
|
|
848
901
|
const { data: threads, isLoading: commentsLoading } = useComments(
|
|
849
|
-
isLocalFileDocument ?
|
|
902
|
+
canEdit && !isLocalFileDocument ? documentId : null,
|
|
850
903
|
);
|
|
851
904
|
const hasComments =
|
|
852
905
|
!isLocalFileDocument &&
|
|
@@ -1061,7 +1114,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
1061
1114
|
onSelect={(emoji) => {
|
|
1062
1115
|
void (async () => {
|
|
1063
1116
|
await persistDocumentUpdates({ icon: emoji });
|
|
1064
|
-
})();
|
|
1117
|
+
})().catch(handleBackgroundSaveError);
|
|
1065
1118
|
}}
|
|
1066
1119
|
/>
|
|
1067
1120
|
) : document.icon ? (
|
|
@@ -127,8 +127,10 @@ patterns live in `.agents/skills/`.
|
|
|
127
127
|
- `navigate` moves the UI and is auto-deleted after the client consumes it.
|
|
128
128
|
- `design-selection` includes active screen, selected element, overview mode,
|
|
129
129
|
inspector tab, zoom, and screen list for the current tab.
|
|
130
|
-
- `design-generation-session:<designId>` contains
|
|
131
|
-
generation
|
|
130
|
+
- `design-generation-session:<designId>` contains agent-facing multi-screen
|
|
131
|
+
generation planning state created by `generate-screens` (canvas region
|
|
132
|
+
assignments and per-frame instructions consumed by `generate-design` and
|
|
133
|
+
`view-screen`; not rendered as canvas overlays).
|
|
132
134
|
- `show-design-questions` opens focused pre-generation questions in the main
|
|
133
135
|
design canvas (`show-questions` application state).
|
|
134
136
|
- `design-variants` contains in-progress candidates for the variant picker.
|
|
@@ -91,11 +91,14 @@ function findUniqueStableIdAgnosticSpan(
|
|
|
91
91
|
}
|
|
92
92
|
if (count !== 1) return null;
|
|
93
93
|
|
|
94
|
+
// Anchor `end` to one byte past the LAST matched stripped character rather than
|
|
95
|
+
// the mapped index of the NEXT character. Mapping the next index can land before
|
|
96
|
+
// a stripped node-id attribute that sits right after the match, so the splice
|
|
97
|
+
// would cross it and corrupt the file (e.g. duplicate/mangled tags).
|
|
98
|
+
const lastMatched = onlyIndex + strippedSearch.length - 1;
|
|
94
99
|
return {
|
|
95
100
|
start: strippedContent.indexMap[onlyIndex] ?? 0,
|
|
96
|
-
end:
|
|
97
|
-
strippedContent.indexMap[onlyIndex + strippedSearch.length] ??
|
|
98
|
-
content.length,
|
|
101
|
+
end: (strippedContent.indexMap[lastMatched] ?? content.length - 1) + 1,
|
|
99
102
|
};
|
|
100
103
|
}
|
|
101
104
|
|
|
@@ -66,10 +66,11 @@ const requestedScreenSchema = z
|
|
|
66
66
|
|
|
67
67
|
export default defineAction({
|
|
68
68
|
description:
|
|
69
|
-
"Start a
|
|
69
|
+
"Start a multi-screen generation session on the Design canvas. " +
|
|
70
70
|
"Use this before generating multiple screens or variations in parallel: it " +
|
|
71
|
-
"assigns non-overlapping canvas regions
|
|
72
|
-
"
|
|
71
|
+
"assigns non-overlapping canvas regions and returns per-frame generation " +
|
|
72
|
+
"instructions including canvasFrame placements. The session state is " +
|
|
73
|
+
"agent-facing planning state consumed by generate-design and view-screen. " +
|
|
73
74
|
"After this action, fan out calls to generate-design for each returned " +
|
|
74
75
|
"frame, passing the returned canvasFrame values to generate-design so " +
|
|
75
76
|
"screens appear in the infinite overview canvas.",
|
|
@@ -167,6 +168,7 @@ export default defineAction({
|
|
|
167
168
|
prompt,
|
|
168
169
|
contextRefs,
|
|
169
170
|
frames,
|
|
171
|
+
startedAt: new Date().toISOString(),
|
|
170
172
|
};
|
|
171
173
|
|
|
172
174
|
await writeAppState(
|
|
@@ -49,17 +49,54 @@ export default defineAction({
|
|
|
49
49
|
const db = getDb();
|
|
50
50
|
const now = new Date().toISOString();
|
|
51
51
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
const asRecord = (raw: string | null | undefined) => {
|
|
53
|
+
if (!raw) return {} as Record<string, unknown>;
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
57
|
+
? (parsed as Record<string, unknown>)
|
|
58
|
+
: ({} as Record<string, unknown>);
|
|
59
|
+
} catch {
|
|
60
|
+
return {} as Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Read-merge-write inside one transaction so a partial `data` update can't
|
|
65
|
+
// race a concurrent write and either destroy framework-owned keys (e.g.
|
|
66
|
+
// canvasFrames/tweaks) or overwrite newer server-side keys. Mirrors
|
|
67
|
+
// generate-design's transactional read-merge.
|
|
68
|
+
await db.transaction(async (tx) => {
|
|
69
|
+
const updates: Record<string, unknown> = { updatedAt: now };
|
|
70
|
+
if (title !== undefined) updates.title = title;
|
|
71
|
+
if (description !== undefined) updates.description = description;
|
|
72
|
+
if (data !== undefined) {
|
|
73
|
+
const [existing] = await tx
|
|
74
|
+
.select({ data: schema.designs.data })
|
|
75
|
+
.from(schema.designs)
|
|
76
|
+
.where(eq(schema.designs.id, id));
|
|
77
|
+
const incomingParsed = JSON.parse(data);
|
|
78
|
+
if (
|
|
79
|
+
incomingParsed &&
|
|
80
|
+
typeof incomingParsed === "object" &&
|
|
81
|
+
!Array.isArray(incomingParsed)
|
|
82
|
+
) {
|
|
83
|
+
updates.data = JSON.stringify({
|
|
84
|
+
...asRecord(existing?.data),
|
|
85
|
+
...(incomingParsed as Record<string, unknown>),
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
// Non-object JSON (array/primitive): keep the original verbatim write.
|
|
89
|
+
updates.data = data;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (projectType !== undefined) updates.projectType = projectType;
|
|
93
|
+
if (designSystemId !== undefined) updates.designSystemId = designSystemId;
|
|
58
94
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
95
|
+
await tx
|
|
96
|
+
.update(schema.designs)
|
|
97
|
+
.set(updates)
|
|
98
|
+
.where(eq(schema.designs.id, id));
|
|
99
|
+
});
|
|
63
100
|
|
|
64
101
|
return { id, updated: true };
|
|
65
102
|
},
|
|
@@ -70,15 +70,40 @@ export default defineAction({
|
|
|
70
70
|
|
|
71
71
|
await assertAccess("design", file.designId, "editor");
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
// Reject a rename that would collide with an existing filename in the same
|
|
74
|
+
// design. The collision check and the write run in one transaction so they
|
|
75
|
+
// can't be interleaved by a concurrent rename. (A DB-level UNIQUE index on
|
|
76
|
+
// (designId, filename) would be the strongest guarantee but is a non-additive
|
|
77
|
+
// schema change on existing data, so it's deferred.)
|
|
78
|
+
await db.transaction(async (tx) => {
|
|
79
|
+
if (filename !== undefined) {
|
|
80
|
+
const [collision] = await tx
|
|
81
|
+
.select({ id: schema.designFiles.id })
|
|
82
|
+
.from(schema.designFiles)
|
|
83
|
+
.where(
|
|
84
|
+
and(
|
|
85
|
+
eq(schema.designFiles.designId, file.designId),
|
|
86
|
+
eq(schema.designFiles.filename, filename),
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
.limit(1);
|
|
90
|
+
if (collision && collision.id !== id) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`File "${filename}" already exists in design ${file.designId}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
77
96
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
const updates: Record<string, unknown> = { updatedAt: now };
|
|
98
|
+
if (content !== undefined) updates.content = content;
|
|
99
|
+
if (filename !== undefined) updates.filename = filename;
|
|
100
|
+
if (fileType !== undefined) updates.fileType = fileType;
|
|
101
|
+
|
|
102
|
+
await tx
|
|
103
|
+
.update(schema.designFiles)
|
|
104
|
+
.set(updates)
|
|
105
|
+
.where(eq(schema.designFiles.id, id));
|
|
106
|
+
});
|
|
82
107
|
|
|
83
108
|
// Push content through the collab layer so live editors see the change
|
|
84
109
|
if (content !== undefined && syncCollab) {
|
|
@@ -120,7 +120,21 @@ export default defineAction({
|
|
|
120
120
|
'A variant picker is open. Wait for the user to choose a direction before generating further. In an inline MCP app their pick returns to you automatically; if it opened as a browser tab (a CLI or code editor), they paste an auto-copied summary or just tell you which one (e.g. "use variant A"). Once you know the choice, read the saved index.html with get-design-snapshot. Do not call generate-design while this picker is open.';
|
|
121
121
|
}
|
|
122
122
|
if (generationSession) {
|
|
123
|
+
const GENERATION_SESSION_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
124
|
+
const startedAt =
|
|
125
|
+
typeof (generationSession as { startedAt?: unknown }).startedAt ===
|
|
126
|
+
"string"
|
|
127
|
+
? new Date(
|
|
128
|
+
(generationSession as { startedAt: string }).startedAt,
|
|
129
|
+
).getTime()
|
|
130
|
+
: 0;
|
|
131
|
+
const isStale =
|
|
132
|
+
startedAt > 0 && Date.now() - startedAt > GENERATION_SESSION_TTL_MS;
|
|
123
133
|
screen.generationSession = generationSession;
|
|
134
|
+
if (isStale) {
|
|
135
|
+
screen.generationSessionNote =
|
|
136
|
+
"This generation session may be stale or abandoned (started more than 10 minutes ago). Verify saved screens via the design file list rather than assuming generation is still in progress.";
|
|
137
|
+
}
|
|
124
138
|
}
|
|
125
139
|
|
|
126
140
|
if (Object.keys(screen).length === 0) {
|