@agent-native/core 0.106.0 → 0.106.1
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 +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/application-state/index.ts +2 -0
- package/corpus/core/src/application-state/script-helpers.ts +12 -0
- package/corpus/core/src/application-state/store.ts +38 -0
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +10 -3
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +22 -0
- package/corpus/templates/design/AGENTS.md +16 -0
- package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +54 -0
- package/corpus/templates/design/actions/propose-node-rewrite.ts +290 -0
- package/corpus/templates/design/actions/resolve-node-rewrite.ts +257 -0
- package/corpus/templates/design/actions/view-screen.ts +98 -2
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +71 -1
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +19 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +33 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +194 -0
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +21 -4
- package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +18 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
- package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +638 -0
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +535 -54
- package/corpus/templates/design/app/components/visual-editor/index.ts +5 -1
- package/corpus/templates/design/app/components/visual-editor/review-canvas-state.ts +21 -3
- package/corpus/templates/design/app/i18n/ar-SA.ts +35 -0
- package/corpus/templates/design/app/i18n/de-DE.ts +35 -0
- package/corpus/templates/design/app/i18n/es-ES.ts +35 -0
- package/corpus/templates/design/app/i18n/fr-FR.ts +35 -0
- package/corpus/templates/design/app/i18n/hi-IN.ts +35 -0
- package/corpus/templates/design/app/i18n/ja-JP.ts +35 -0
- package/corpus/templates/design/app/i18n/ko-KR.ts +35 -0
- package/corpus/templates/design/app/i18n/pt-BR.ts +35 -0
- package/corpus/templates/design/app/i18n/zh-CN.ts +35 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +32 -0
- package/corpus/templates/design/app/i18n-data.ts +32 -0
- package/corpus/templates/design/app/lib/node-reprompt.ts +110 -0
- package/corpus/templates/design/changelog/2026-07-16-design-editing-and-review-are-now-more-predictable.md +6 -0
- package/corpus/templates/design/server/lib/reprompt-action-guard.ts +107 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +9 -1
- package/corpus/templates/design/shared/code-layer.ts +15 -0
- package/corpus/templates/design/shared/node-rewrite.ts +213 -0
- package/corpus/templates/design/shared/review-anchor.ts +26 -3
- package/dist/application-state/index.d.ts +2 -2
- package/dist/application-state/index.d.ts.map +1 -1
- package/dist/application-state/index.js +2 -2
- package/dist/application-state/index.js.map +1 -1
- package/dist/application-state/script-helpers.d.ts +1 -0
- package/dist/application-state/script-helpers.d.ts.map +1 -1
- package/dist/application-state/script-helpers.js +7 -1
- package/dist/application-state/script-helpers.js.map +1 -1
- package/dist/application-state/store.d.ts +1 -0
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +28 -0
- package/dist/application-state/store.js.map +1 -1
- package/dist/client/review/ReviewCommentComposer.d.ts +4 -1
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
- package/dist/client/review/ReviewCommentComposer.js +3 -3
- package/dist/client/review/ReviewCommentComposer.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
compareAndSetAppState,
|
|
4
|
+
listAppState,
|
|
5
|
+
readAppState,
|
|
6
|
+
} from "@agent-native/core/application-state";
|
|
7
|
+
import { accessFilter, assertAccess } from "@agent-native/core/sharing";
|
|
8
|
+
import { and, eq } from "drizzle-orm";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
12
|
+
import {
|
|
13
|
+
readLiveSourceFile,
|
|
14
|
+
writeInlineSourceFile,
|
|
15
|
+
type SourceWorkspaceFile,
|
|
16
|
+
} from "../server/source-workspace.js";
|
|
17
|
+
import type { CodeLayerSource } from "../shared/code-layer.js";
|
|
18
|
+
import {
|
|
19
|
+
DESIGN_REPROMPT_PROPOSAL_STATE_PREFIX,
|
|
20
|
+
designRepromptPendingStateKey,
|
|
21
|
+
designRepromptProposalStateKey,
|
|
22
|
+
isPendingDesignReprompt,
|
|
23
|
+
spliceNodeRewriteVariant,
|
|
24
|
+
type NodeHtmlPreviewBridgeMessage,
|
|
25
|
+
type NodeRewriteProposal,
|
|
26
|
+
} from "../shared/node-rewrite.js";
|
|
27
|
+
import { designSourceTypeFromData } from "../shared/source-mode.js";
|
|
28
|
+
|
|
29
|
+
const targetSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
nodeId: z.string().min(1).optional(),
|
|
32
|
+
selector: z.string().min(1).optional(),
|
|
33
|
+
})
|
|
34
|
+
.superRefine((target, ctx) => {
|
|
35
|
+
if (!target.nodeId && !target.selector) {
|
|
36
|
+
ctx.addIssue({
|
|
37
|
+
code: z.ZodIssueCode.custom,
|
|
38
|
+
path: ["nodeId"],
|
|
39
|
+
message: "target.nodeId or target.selector is required",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const proposalSchema = z.object({
|
|
45
|
+
proposalId: z.string().min(1),
|
|
46
|
+
repromptId: z.string().min(1),
|
|
47
|
+
designId: z.string().min(1),
|
|
48
|
+
fileId: z.string().min(1),
|
|
49
|
+
filename: z.string().min(1),
|
|
50
|
+
target: targetSchema,
|
|
51
|
+
resolvedTarget: z.object({
|
|
52
|
+
nodeId: z.string().min(1),
|
|
53
|
+
selector: z.string().min(1),
|
|
54
|
+
}),
|
|
55
|
+
baseVersionHash: z.string().min(1),
|
|
56
|
+
variants: z
|
|
57
|
+
.array(
|
|
58
|
+
z.object({
|
|
59
|
+
html: z.string().min(1).max(250_000),
|
|
60
|
+
summary: z.string().min(1).max(240),
|
|
61
|
+
}),
|
|
62
|
+
)
|
|
63
|
+
.min(1)
|
|
64
|
+
.max(3),
|
|
65
|
+
chosenIndex: z.number().int().min(0),
|
|
66
|
+
createdAt: z.string(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
interface EditableDesignFile extends SourceWorkspaceFile {
|
|
70
|
+
designData: string | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function resolveProposal(proposalId: string): Promise<{
|
|
74
|
+
key: string;
|
|
75
|
+
proposal: NodeRewriteProposal;
|
|
76
|
+
}> {
|
|
77
|
+
const candidates = await listAppState(DESIGN_REPROMPT_PROPOSAL_STATE_PREFIX);
|
|
78
|
+
const matches = candidates.filter(
|
|
79
|
+
({ value }) => value.proposalId === proposalId,
|
|
80
|
+
);
|
|
81
|
+
if (matches.length !== 1) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
matches.length === 0
|
|
84
|
+
? "Node rewrite proposal not found or already resolved."
|
|
85
|
+
: "Multiple node rewrite proposals matched this id.",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const match = matches[0]!;
|
|
89
|
+
const proposal = proposalSchema.parse(match.value) as NodeRewriteProposal;
|
|
90
|
+
const expectedKey = designRepromptProposalStateKey(
|
|
91
|
+
proposal.designId,
|
|
92
|
+
proposal.fileId,
|
|
93
|
+
proposal.repromptId,
|
|
94
|
+
);
|
|
95
|
+
if (match.key !== expectedKey) {
|
|
96
|
+
throw new Error("Node rewrite proposal state is scoped to the wrong file.");
|
|
97
|
+
}
|
|
98
|
+
return { key: match.key, proposal };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function resolveEditableDesignFile(
|
|
102
|
+
proposal: NodeRewriteProposal,
|
|
103
|
+
): Promise<EditableDesignFile> {
|
|
104
|
+
const db = getDb();
|
|
105
|
+
const [file] = await db
|
|
106
|
+
.select({
|
|
107
|
+
id: schema.designFiles.id,
|
|
108
|
+
designId: schema.designFiles.designId,
|
|
109
|
+
filename: schema.designFiles.filename,
|
|
110
|
+
fileType: schema.designFiles.fileType,
|
|
111
|
+
content: schema.designFiles.content,
|
|
112
|
+
createdAt: schema.designFiles.createdAt,
|
|
113
|
+
updatedAt: schema.designFiles.updatedAt,
|
|
114
|
+
designData: schema.designs.data,
|
|
115
|
+
})
|
|
116
|
+
.from(schema.designFiles)
|
|
117
|
+
.innerJoin(
|
|
118
|
+
schema.designs,
|
|
119
|
+
eq(schema.designFiles.designId, schema.designs.id),
|
|
120
|
+
)
|
|
121
|
+
.where(
|
|
122
|
+
and(
|
|
123
|
+
accessFilter(schema.designs, schema.designShares),
|
|
124
|
+
eq(schema.designFiles.id, proposal.fileId),
|
|
125
|
+
eq(schema.designFiles.designId, proposal.designId),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
.limit(1);
|
|
129
|
+
|
|
130
|
+
if (!file) throw new Error("Design HTML file not found.");
|
|
131
|
+
if (file.filename !== proposal.filename || file.fileType !== "html") {
|
|
132
|
+
throw new Error("Node rewrite proposal no longer matches its HTML file.");
|
|
133
|
+
}
|
|
134
|
+
if (designSourceTypeFromData(file.designData) !== "inline") {
|
|
135
|
+
throw new Error("Node rewrites only support inline design screens.");
|
|
136
|
+
}
|
|
137
|
+
await assertAccess("design", proposal.designId, "editor");
|
|
138
|
+
return file;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function clearProposalState(
|
|
142
|
+
proposalKey: string,
|
|
143
|
+
proposal: NodeRewriteProposal,
|
|
144
|
+
pending: Record<string, unknown>,
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
const pendingKey = designRepromptPendingStateKey(
|
|
147
|
+
proposal.designId,
|
|
148
|
+
proposal.fileId,
|
|
149
|
+
);
|
|
150
|
+
const [proposalCleared] = await Promise.all([
|
|
151
|
+
compareAndSetAppState(
|
|
152
|
+
proposalKey,
|
|
153
|
+
proposal as unknown as Record<string, unknown>,
|
|
154
|
+
null,
|
|
155
|
+
),
|
|
156
|
+
compareAndSetAppState(pendingKey, pending, null),
|
|
157
|
+
]);
|
|
158
|
+
if (!proposalCleared) {
|
|
159
|
+
throw new Error("Node rewrite proposal changed while it was resolving.");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default defineAction({
|
|
164
|
+
agentTool: false,
|
|
165
|
+
description:
|
|
166
|
+
"Accept or reject a pending scoped Design node rewrite. Accept performs one " +
|
|
167
|
+
"version-checked inline/Yjs content transaction; reject changes no design content. " +
|
|
168
|
+
"Both resolutions clear the proposal and its matching paired pending reprompt.",
|
|
169
|
+
schema: z.object({
|
|
170
|
+
proposalId: z.string().min(1),
|
|
171
|
+
resolution: z.enum(["accept", "reject"]),
|
|
172
|
+
variantIndex: z.number().int().min(0).optional(),
|
|
173
|
+
}),
|
|
174
|
+
run: async ({ proposalId, resolution, variantIndex }) => {
|
|
175
|
+
const { key: proposalKey, proposal } = await resolveProposal(proposalId);
|
|
176
|
+
const pendingKey = designRepromptPendingStateKey(
|
|
177
|
+
proposal.designId,
|
|
178
|
+
proposal.fileId,
|
|
179
|
+
);
|
|
180
|
+
const pending = await readAppState(pendingKey);
|
|
181
|
+
if (
|
|
182
|
+
!isPendingDesignReprompt(pending) ||
|
|
183
|
+
pending.repromptId !== proposal.repromptId
|
|
184
|
+
) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
"A newer regeneration request replaced this proposal. Review the latest candidates instead.",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const restoreMessage: NodeHtmlPreviewBridgeMessage = {
|
|
190
|
+
type: "node-html-preview",
|
|
191
|
+
proposalId,
|
|
192
|
+
target: proposal.resolvedTarget,
|
|
193
|
+
operation: "restore",
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (resolution === "reject") {
|
|
197
|
+
await assertAccess("design", proposal.designId, "editor");
|
|
198
|
+
await clearProposalState(proposalKey, proposal, pending);
|
|
199
|
+
return {
|
|
200
|
+
proposalId,
|
|
201
|
+
repromptId: proposal.repromptId,
|
|
202
|
+
resolution,
|
|
203
|
+
changed: false,
|
|
204
|
+
bridgeMessages: [restoreMessage],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const file = await resolveEditableDesignFile(proposal);
|
|
209
|
+
const chosenIndex = variantIndex ?? proposal.chosenIndex;
|
|
210
|
+
const variant = proposal.variants[chosenIndex];
|
|
211
|
+
if (!variant) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Variant index ${chosenIndex} is out of range for this proposal.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const live = await readLiveSourceFile(file);
|
|
218
|
+
if (live.versionHash !== proposal.baseVersionHash) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
"Screen changed since proposal — regenerate before accepting.",
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const source: CodeLayerSource = {
|
|
224
|
+
kind: "design-file",
|
|
225
|
+
sourceType: "inline",
|
|
226
|
+
designId: file.designId,
|
|
227
|
+
fileId: file.id,
|
|
228
|
+
filename: file.filename,
|
|
229
|
+
};
|
|
230
|
+
const rewrite = spliceNodeRewriteVariant({
|
|
231
|
+
content: live.content,
|
|
232
|
+
target: proposal.resolvedTarget,
|
|
233
|
+
variant,
|
|
234
|
+
source,
|
|
235
|
+
fileType: file.fileType,
|
|
236
|
+
});
|
|
237
|
+
const write = await writeInlineSourceFile({
|
|
238
|
+
designId: file.designId,
|
|
239
|
+
file,
|
|
240
|
+
content: rewrite.content,
|
|
241
|
+
expectedVersionHash: proposal.baseVersionHash,
|
|
242
|
+
});
|
|
243
|
+
await clearProposalState(proposalKey, proposal, pending);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
proposalId,
|
|
247
|
+
repromptId: proposal.repromptId,
|
|
248
|
+
resolution,
|
|
249
|
+
variantIndex: chosenIndex,
|
|
250
|
+
summary: variant.summary,
|
|
251
|
+
changed: write.changed,
|
|
252
|
+
designId: file.designId,
|
|
253
|
+
fileId: file.id,
|
|
254
|
+
versionHash: write.versionHash,
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { defineAction } from "@agent-native/core";
|
|
11
11
|
import {
|
|
12
|
+
listAppState,
|
|
12
13
|
readAppState,
|
|
13
14
|
readAppStateForCurrentTab,
|
|
14
15
|
} from "@agent-native/core/application-state";
|
|
@@ -30,6 +31,14 @@ import { getDb, schema } from "../server/db/index.js";
|
|
|
30
31
|
import { parseCanvasFrameGeometryById } from "../shared/canvas-frames.js";
|
|
31
32
|
import { getDesignTemplatePreset } from "../shared/design-template-presets.js";
|
|
32
33
|
import { designGenerationSessionKey } from "../shared/generation-session.js";
|
|
34
|
+
import {
|
|
35
|
+
DESIGN_REPROMPT_PENDING_STATE_PREFIX,
|
|
36
|
+
DESIGN_REPROMPT_PROPOSAL_STATE_PREFIX,
|
|
37
|
+
designRepromptPendingStateKey,
|
|
38
|
+
designRepromptProposalStateKey,
|
|
39
|
+
isNodeRewriteProposal,
|
|
40
|
+
isPendingDesignReprompt,
|
|
41
|
+
} from "../shared/node-rewrite.js";
|
|
33
42
|
|
|
34
43
|
interface ReviewThreadSummary {
|
|
35
44
|
openCount: number;
|
|
@@ -309,14 +318,101 @@ export default defineAction({
|
|
|
309
318
|
data = {};
|
|
310
319
|
}
|
|
311
320
|
}
|
|
321
|
+
const activeScreen = resolveActiveScreen(
|
|
322
|
+
files,
|
|
323
|
+
navigation,
|
|
324
|
+
designSelection,
|
|
325
|
+
);
|
|
312
326
|
screen.design = {
|
|
313
327
|
id: designId,
|
|
314
328
|
title: (access.resource as { title?: unknown }).title ?? null,
|
|
315
329
|
screens: files,
|
|
316
|
-
activeScreen
|
|
330
|
+
activeScreen,
|
|
317
331
|
activeCodeFile: resolveActiveCodeFile(files, designSelection),
|
|
318
332
|
canvasFrames: parseCanvasFrameGeometryById(data.canvasFrames),
|
|
319
333
|
};
|
|
334
|
+
const proposalPrefix = `${DESIGN_REPROMPT_PROPOSAL_STATE_PREFIX}${designId}:`;
|
|
335
|
+
const pendingPrefix = `${DESIGN_REPROMPT_PENDING_STATE_PREFIX}${designId}:`;
|
|
336
|
+
const [proposalEntries, pendingEntries] = await Promise.all([
|
|
337
|
+
listAppState(proposalPrefix),
|
|
338
|
+
listAppState(pendingPrefix),
|
|
339
|
+
]);
|
|
340
|
+
const filesById = new Map(files.map((file) => [file.id, file]));
|
|
341
|
+
const pendingByFileId = new Map(
|
|
342
|
+
pendingEntries.flatMap(({ key, value }) => {
|
|
343
|
+
if (
|
|
344
|
+
!isPendingDesignReprompt(value) ||
|
|
345
|
+
value.designId !== designId ||
|
|
346
|
+
!filesById.has(value.fileId) ||
|
|
347
|
+
key !== designRepromptPendingStateKey(designId, value.fileId)
|
|
348
|
+
) {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
return [[value.fileId, value] as const];
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
const proposalsByReprompt = new Map(
|
|
355
|
+
proposalEntries.flatMap(({ key, value }) => {
|
|
356
|
+
if (
|
|
357
|
+
!isNodeRewriteProposal(value) ||
|
|
358
|
+
value.designId !== designId ||
|
|
359
|
+
!filesById.has(value.fileId) ||
|
|
360
|
+
key !==
|
|
361
|
+
designRepromptProposalStateKey(
|
|
362
|
+
designId,
|
|
363
|
+
value.fileId,
|
|
364
|
+
value.repromptId,
|
|
365
|
+
)
|
|
366
|
+
) {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
return [[`${value.fileId}:${value.repromptId}`, value] as const];
|
|
370
|
+
}),
|
|
371
|
+
);
|
|
372
|
+
const proposalsByFileId = new Map(
|
|
373
|
+
[...pendingByFileId.entries()].flatMap(([fileId, pending]) => {
|
|
374
|
+
const current = proposalsByReprompt.get(
|
|
375
|
+
`${fileId}:${pending.repromptId}`,
|
|
376
|
+
);
|
|
377
|
+
const prior = pending.priorRepromptId
|
|
378
|
+
? proposalsByReprompt.get(`${fileId}:${pending.priorRepromptId}`)
|
|
379
|
+
: undefined;
|
|
380
|
+
const proposal =
|
|
381
|
+
current ??
|
|
382
|
+
(prior?.proposalId === pending.priorProposalId
|
|
383
|
+
? prior
|
|
384
|
+
: undefined);
|
|
385
|
+
return proposal ? [[fileId, proposal] as const] : [];
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
const pendingCandidateReviews = [...proposalsByFileId.values()].map(
|
|
389
|
+
(proposal) => {
|
|
390
|
+
const file = filesById.get(proposal.fileId)!;
|
|
391
|
+
return {
|
|
392
|
+
proposalId: proposal.proposalId,
|
|
393
|
+
fileId: proposal.fileId,
|
|
394
|
+
filename: proposal.filename || file.filename,
|
|
395
|
+
candidateCount: proposal.variants.length,
|
|
396
|
+
chosenIndex: proposal.chosenIndex,
|
|
397
|
+
target: proposal.target,
|
|
398
|
+
createdAt: proposal.createdAt,
|
|
399
|
+
};
|
|
400
|
+
},
|
|
401
|
+
);
|
|
402
|
+
if (pendingCandidateReviews.length > 0) {
|
|
403
|
+
(screen.design as Record<string, unknown>).pendingCandidateReviews =
|
|
404
|
+
pendingCandidateReviews;
|
|
405
|
+
}
|
|
406
|
+
if (activeScreen?.id) {
|
|
407
|
+
const pendingReprompt = pendingByFileId.get(activeScreen.id);
|
|
408
|
+
const proposal = proposalsByFileId.get(activeScreen.id);
|
|
409
|
+
if (pendingReprompt || proposal) {
|
|
410
|
+
(screen.design as Record<string, unknown>).reprompt = {
|
|
411
|
+
pending: pendingReprompt ?? null,
|
|
412
|
+
proposal: proposal ?? null,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
320
416
|
const reviewContext = ctx as ReviewResourceContext | undefined;
|
|
321
417
|
const reviewScope = {
|
|
322
418
|
userEmail: reviewContext?.userEmail ?? null,
|
|
@@ -357,7 +453,7 @@ export default defineAction({
|
|
|
357
453
|
redactReviewIdentity
|
|
358
454
|
? redactPublicReviewStatusIdentity(reviewStatus)
|
|
359
455
|
: reviewStatus,
|
|
360
|
-
|
|
456
|
+
activeScreen?.id,
|
|
361
457
|
reviewSummary,
|
|
362
458
|
);
|
|
363
459
|
}
|
|
@@ -92,6 +92,7 @@ export type CanvasContextMenuAction =
|
|
|
92
92
|
| "add-auto-layout"
|
|
93
93
|
| "suggest-auto-layout"
|
|
94
94
|
| "create-component"
|
|
95
|
+
| "reprompt"
|
|
95
96
|
| "go-to-main-component"
|
|
96
97
|
| "swap-instance"
|
|
97
98
|
| "detach-instance"
|
|
@@ -135,6 +136,7 @@ export type CanvasContextMenuActionHandler = (
|
|
|
135
136
|
|
|
136
137
|
export interface CanvasContextMenuLabels {
|
|
137
138
|
selectLayer: string;
|
|
139
|
+
reprompt: string;
|
|
138
140
|
pasteHere: string;
|
|
139
141
|
selectAll: string;
|
|
140
142
|
zoomToFit: string;
|
|
@@ -231,6 +233,10 @@ export interface CanvasContextMenuProps {
|
|
|
231
233
|
selectedCount?: number;
|
|
232
234
|
layerCandidates?: readonly CanvasLayerHitCandidate[];
|
|
233
235
|
onSelectLayer?: (candidate: CanvasLayerHitCandidate) => void;
|
|
236
|
+
onRepromptLayer?: (
|
|
237
|
+
candidate: CanvasLayerHitCandidate,
|
|
238
|
+
details: CanvasContextMenuActionDetails,
|
|
239
|
+
) => void;
|
|
234
240
|
hasClipboard?: boolean;
|
|
235
241
|
hasPropsClipboard?: boolean;
|
|
236
242
|
hasAnimationClipboard?: boolean;
|
|
@@ -261,6 +267,7 @@ export interface CanvasContextMenuProps {
|
|
|
261
267
|
canAddAutoLayout?: boolean;
|
|
262
268
|
canSuggestAutoLayout?: boolean;
|
|
263
269
|
canCreateComponent?: boolean;
|
|
270
|
+
canReprompt?: boolean;
|
|
264
271
|
// Whether the current selection IS a component instance — gates the
|
|
265
272
|
// whole Go to main component / Swap instance / Detach instance cluster on
|
|
266
273
|
// (rather than showing them permanently disabled for non-instance
|
|
@@ -330,6 +337,7 @@ export interface CanvasContextMenuProps {
|
|
|
330
337
|
onAddAutoLayout?: CanvasContextMenuActionHandler;
|
|
331
338
|
onSuggestAutoLayout?: CanvasContextMenuActionHandler;
|
|
332
339
|
onCreateComponent?: CanvasContextMenuActionHandler;
|
|
340
|
+
onReprompt?: CanvasContextMenuActionHandler;
|
|
333
341
|
onGoToMainComponent?: CanvasContextMenuActionHandler;
|
|
334
342
|
onSwapInstance?: CanvasContextMenuActionHandler;
|
|
335
343
|
onDetachInstance?: CanvasContextMenuActionHandler;
|
|
@@ -361,6 +369,7 @@ export interface CanvasContextMenuProps {
|
|
|
361
369
|
|
|
362
370
|
const DEFAULT_LABELS: CanvasContextMenuLabels = {
|
|
363
371
|
selectLayer: "Select layer",
|
|
372
|
+
reprompt: "Regenerate…",
|
|
364
373
|
pasteHere: "Paste here",
|
|
365
374
|
selectAll: "Select all",
|
|
366
375
|
zoomToFit: "Zoom to fit",
|
|
@@ -481,6 +490,7 @@ export const CanvasContextMenu = forwardRef<
|
|
|
481
490
|
selectedCount = 0,
|
|
482
491
|
layerCandidates = [],
|
|
483
492
|
onSelectLayer,
|
|
493
|
+
onRepromptLayer,
|
|
484
494
|
hasClipboard = false,
|
|
485
495
|
hasPropsClipboard = false,
|
|
486
496
|
hasAnimationClipboard = false,
|
|
@@ -500,6 +510,7 @@ export const CanvasContextMenu = forwardRef<
|
|
|
500
510
|
canAddAutoLayout = selectedCount > 0,
|
|
501
511
|
canSuggestAutoLayout = false,
|
|
502
512
|
canCreateComponent = selectedCount > 0,
|
|
513
|
+
canReprompt = selectedCount > 0 || layerCandidates.length > 0,
|
|
503
514
|
isComponentInstance = false,
|
|
504
515
|
canGoToMainComponent = isComponentInstance,
|
|
505
516
|
canSwapInstance = isComponentInstance,
|
|
@@ -540,6 +551,7 @@ export const CanvasContextMenu = forwardRef<
|
|
|
540
551
|
onAddAutoLayout,
|
|
541
552
|
onSuggestAutoLayout,
|
|
542
553
|
onCreateComponent,
|
|
554
|
+
onReprompt,
|
|
543
555
|
onGoToMainComponent,
|
|
544
556
|
onSwapInstance,
|
|
545
557
|
onDetachInstance,
|
|
@@ -624,6 +636,7 @@ export const CanvasContextMenu = forwardRef<
|
|
|
624
636
|
"add-auto-layout": onAddAutoLayout,
|
|
625
637
|
"suggest-auto-layout": onSuggestAutoLayout,
|
|
626
638
|
"create-component": onCreateComponent,
|
|
639
|
+
reprompt: onReprompt,
|
|
627
640
|
"go-to-main-component": onGoToMainComponent,
|
|
628
641
|
"swap-instance": onSwapInstance,
|
|
629
642
|
"detach-instance": onDetachInstance,
|
|
@@ -654,6 +667,7 @@ export const CanvasContextMenu = forwardRef<
|
|
|
654
667
|
onCopyAsSvg,
|
|
655
668
|
onCopyProps,
|
|
656
669
|
onCreateComponent,
|
|
670
|
+
onReprompt,
|
|
657
671
|
onDetachInstance,
|
|
658
672
|
onFlipHorizontal,
|
|
659
673
|
onFlipVertical,
|
|
@@ -772,6 +786,62 @@ export const CanvasContextMenu = forwardRef<
|
|
|
772
786
|
<CanvasMenuSeparator />
|
|
773
787
|
</>
|
|
774
788
|
) : null}
|
|
789
|
+
{canReprompt && (onReprompt || onRepromptLayer) ? (
|
|
790
|
+
<>
|
|
791
|
+
<ContextMenuGroup>
|
|
792
|
+
{layerCandidates.length > 1 && onRepromptLayer ? (
|
|
793
|
+
<ContextMenuSub>
|
|
794
|
+
<ContextMenuSubTrigger className={MENU_SUB_TRIGGER_CLASS}>
|
|
795
|
+
{labels.reprompt}
|
|
796
|
+
</ContextMenuSubTrigger>
|
|
797
|
+
<ContextMenuSubContent
|
|
798
|
+
className={cn(MENU_CONTENT_CLASS, "w-56")}
|
|
799
|
+
>
|
|
800
|
+
{layerCandidates.map((candidate) => (
|
|
801
|
+
<CanvasLayerCandidateItem
|
|
802
|
+
key={`reprompt:${candidate.key}`}
|
|
803
|
+
candidate={candidate}
|
|
804
|
+
onSelect={(event) => {
|
|
805
|
+
onRepromptLayer(candidate, {
|
|
806
|
+
action: "reprompt",
|
|
807
|
+
point,
|
|
808
|
+
selectedCount,
|
|
809
|
+
originalEvent: event,
|
|
810
|
+
});
|
|
811
|
+
handleOpenChange(false);
|
|
812
|
+
}}
|
|
813
|
+
/>
|
|
814
|
+
))}
|
|
815
|
+
</ContextMenuSubContent>
|
|
816
|
+
</ContextMenuSub>
|
|
817
|
+
) : (
|
|
818
|
+
<CanvasMenuItem
|
|
819
|
+
hidden={isHiddenAction("reprompt")}
|
|
820
|
+
disabled={
|
|
821
|
+
!canReprompt ||
|
|
822
|
+
(!onReprompt &&
|
|
823
|
+
!(onRepromptLayer && layerCandidates.length === 1))
|
|
824
|
+
}
|
|
825
|
+
label={labels.reprompt}
|
|
826
|
+
onSelect={(event) => {
|
|
827
|
+
const candidate = layerCandidates[0];
|
|
828
|
+
if (!onReprompt && onRepromptLayer && candidate) {
|
|
829
|
+
onRepromptLayer(candidate, {
|
|
830
|
+
action: "reprompt",
|
|
831
|
+
point,
|
|
832
|
+
selectedCount,
|
|
833
|
+
originalEvent: event,
|
|
834
|
+
});
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
runAction("reprompt", event);
|
|
838
|
+
}}
|
|
839
|
+
/>
|
|
840
|
+
)}
|
|
841
|
+
</ContextMenuGroup>
|
|
842
|
+
<CanvasMenuSeparator />
|
|
843
|
+
</>
|
|
844
|
+
) : null}
|
|
775
845
|
{hasSelection ? (
|
|
776
846
|
<>
|
|
777
847
|
{/* LIVE-VERIFIED Figma "with selection" canvas menu. */}
|
|
@@ -1083,7 +1153,7 @@ function CanvasLayerCandidateItem({
|
|
|
1083
1153
|
onSelect,
|
|
1084
1154
|
}: {
|
|
1085
1155
|
candidate: CanvasLayerHitCandidate;
|
|
1086
|
-
onSelect: () => void;
|
|
1156
|
+
onSelect: (event: Event) => void;
|
|
1087
1157
|
}) {
|
|
1088
1158
|
const tag = candidate.info.tagName.toLowerCase();
|
|
1089
1159
|
const Icon = candidate.info.componentName
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type PenPath,
|
|
25
25
|
type PenPoint,
|
|
26
26
|
} from "@shared/pen-path";
|
|
27
|
+
import { sourceContentHash } from "@shared/source-workspace";
|
|
27
28
|
import { IconPlugConnectedX, IconRefresh } from "@tabler/icons-react";
|
|
28
29
|
import {
|
|
29
30
|
useCallback,
|
|
@@ -43,6 +44,7 @@ import { Button } from "@/components/ui/button";
|
|
|
43
44
|
import {
|
|
44
45
|
DrawOverlay as SharedDrawOverlay,
|
|
45
46
|
ReviewCanvasPins,
|
|
47
|
+
type RepromptDraftRequest,
|
|
46
48
|
type ReviewFocusRequest,
|
|
47
49
|
} from "@/components/visual-editor";
|
|
48
50
|
import { sendToDesignAgentChatAndConfirm } from "@/lib/agent-chat";
|
|
@@ -556,6 +558,11 @@ interface DesignCanvasProps {
|
|
|
556
558
|
previewFrameId?: string;
|
|
557
559
|
/** Human-readable label for comment-pin prompts. */
|
|
558
560
|
commentContextLabel?: string;
|
|
561
|
+
/** Opens an ephemeral, pre-anchored regenerate composer for this screen. */
|
|
562
|
+
repromptDraftRequest?: RepromptDraftRequest | null;
|
|
563
|
+
onRepromptDraftConsumed?: (nonce: number) => void;
|
|
564
|
+
/** Marks the one base canvas used for pending rewrite previews. */
|
|
565
|
+
nodeRewriteCanvasTarget?: boolean;
|
|
559
566
|
/**
|
|
560
567
|
* Called when a link inside the prototype points to another screen (a
|
|
561
568
|
* relative href or `data-screen`). Lets the editor switch the active screen
|
|
@@ -1053,6 +1060,9 @@ export function DesignCanvas({
|
|
|
1053
1060
|
commentContextId,
|
|
1054
1061
|
screenId,
|
|
1055
1062
|
previewFrameId,
|
|
1063
|
+
repromptDraftRequest,
|
|
1064
|
+
onRepromptDraftConsumed,
|
|
1065
|
+
nodeRewriteCanvasTarget = false,
|
|
1056
1066
|
onPrototypeNavigate,
|
|
1057
1067
|
motionTracks,
|
|
1058
1068
|
motionDefaultEase,
|
|
@@ -4027,6 +4037,9 @@ export function DesignCanvas({
|
|
|
4027
4037
|
const iframeElement = (
|
|
4028
4038
|
<div
|
|
4029
4039
|
data-review-canvas-id={reviewCanvasId}
|
|
4040
|
+
data-node-rewrite-canvas-target={
|
|
4041
|
+
nodeRewriteCanvasTarget ? "true" : undefined
|
|
4042
|
+
}
|
|
4030
4043
|
className="design-canvas-iframe-wrapper relative inline-block ring-1 ring-border/60 shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_8px_24px_-12px_rgba(0,0,0,0.45)]"
|
|
4031
4044
|
onDragEnter={handleWrapperDragEnter}
|
|
4032
4045
|
onDragOver={handleWrapperDragOver}
|
|
@@ -4548,6 +4561,12 @@ export function DesignCanvas({
|
|
|
4548
4561
|
onDispatchCommentToAgent={onDispatchCommentToAgent}
|
|
4549
4562
|
onSendThreadToAgent={onSendThreadToAgent}
|
|
4550
4563
|
sendingThreadId={reviewSendingThreadId}
|
|
4564
|
+
sourceType={
|
|
4565
|
+
sourceType ?? (externalPreviewUrl ? "localhost" : "inline")
|
|
4566
|
+
}
|
|
4567
|
+
sourceVersionHash={sourceContentHash(content)}
|
|
4568
|
+
repromptDraftRequest={repromptDraftRequest}
|
|
4569
|
+
onRepromptDraftConsumed={onRepromptDraftConsumed}
|
|
4551
4570
|
/>
|
|
4552
4571
|
) : null}
|
|
4553
4572
|
</div>
|
|
@@ -398,6 +398,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
398
398
|
hiddenScreenIds = EMPTY_SCREEN_IDS,
|
|
399
399
|
lockedScreenIds = EMPTY_SCREEN_IDS,
|
|
400
400
|
fullViewScreenIds,
|
|
401
|
+
pendingReviewScreenIds = EMPTY_SCREEN_IDS,
|
|
402
|
+
onReviewPendingScreen,
|
|
401
403
|
interactMode = false,
|
|
402
404
|
activeScreenHasHoveredChild = false,
|
|
403
405
|
hoveredChildScreenId,
|
|
@@ -525,6 +527,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
525
527
|
() => new Set(lockedScreenIds),
|
|
526
528
|
[lockedScreenIds],
|
|
527
529
|
);
|
|
530
|
+
const pendingReviewScreenIdSet = useMemo(
|
|
531
|
+
() => new Set(pendingReviewScreenIds),
|
|
532
|
+
[pendingReviewScreenIds],
|
|
533
|
+
);
|
|
528
534
|
const renderedScreens = useMemo(
|
|
529
535
|
() => screens.filter((screen) => !hiddenScreenIdSet.has(screen.id)),
|
|
530
536
|
[hiddenScreenIdSet, screens],
|
|
@@ -7609,6 +7615,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
7609
7615
|
!isBreakpointSelectionTarget(screen)
|
|
7610
7616
|
}
|
|
7611
7617
|
showFullView={fullViewIdSet.has(screen.id)}
|
|
7618
|
+
pendingReview={pendingReviewScreenIdSet.has(screen.id)}
|
|
7619
|
+
onReviewPendingScreen={onReviewPendingScreen}
|
|
7612
7620
|
interactMode={interactMode}
|
|
7613
7621
|
isDirectlyHovered={screen.id === directlyHoveredScreenId}
|
|
7614
7622
|
isFileDragOver={
|
|
@@ -8898,6 +8906,8 @@ interface ScreenProps {
|
|
|
8898
8906
|
isSelected: boolean;
|
|
8899
8907
|
isTopScreen: boolean;
|
|
8900
8908
|
showFullView: boolean;
|
|
8909
|
+
pendingReview: boolean;
|
|
8910
|
+
onReviewPendingScreen?: (screenId: string) => void;
|
|
8901
8911
|
interactMode: boolean;
|
|
8902
8912
|
isDirectlyHovered: boolean;
|
|
8903
8913
|
/** True while a native OS file drag is hovering this frame (Figma parity §1). */
|
|
@@ -8966,6 +8976,8 @@ const Screen = memo(function Screen({
|
|
|
8966
8976
|
isSelected,
|
|
8967
8977
|
isTopScreen,
|
|
8968
8978
|
showFullView,
|
|
8979
|
+
pendingReview,
|
|
8980
|
+
onReviewPendingScreen,
|
|
8969
8981
|
interactMode,
|
|
8970
8982
|
isDirectlyHovered,
|
|
8971
8983
|
isFileDragOver,
|
|
@@ -9174,6 +9186,27 @@ const Screen = memo(function Screen({
|
|
|
9174
9186
|
>
|
|
9175
9187
|
{display}
|
|
9176
9188
|
</span>
|
|
9189
|
+
{pendingReview && onReviewPendingScreen ? (
|
|
9190
|
+
<button
|
|
9191
|
+
type="button"
|
|
9192
|
+
data-node-rewrite-review-badge
|
|
9193
|
+
className="flex h-5 shrink-0 items-center gap-1 rounded-full border border-border bg-background/95 px-1.5 !text-[9px] font-medium text-foreground shadow-sm hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
9194
|
+
title={t("designEditor.nodeRewrite.reviewCandidate")}
|
|
9195
|
+
aria-label={t("designEditor.nodeRewrite.reviewCandidate")}
|
|
9196
|
+
onClick={(event) => {
|
|
9197
|
+
event.preventDefault();
|
|
9198
|
+
event.stopPropagation();
|
|
9199
|
+
onReviewPendingScreen(screen.id);
|
|
9200
|
+
}}
|
|
9201
|
+
onMouseDown={(event) => {
|
|
9202
|
+
event.preventDefault();
|
|
9203
|
+
event.stopPropagation();
|
|
9204
|
+
}}
|
|
9205
|
+
>
|
|
9206
|
+
<span className="size-1.5 rounded-full bg-primary" />
|
|
9207
|
+
{t("designEditor.nodeRewrite.reviewCandidate")}
|
|
9208
|
+
</button>
|
|
9209
|
+
) : null}
|
|
9177
9210
|
{metadata.source === "fusion" ? (
|
|
9178
9211
|
<span
|
|
9179
9212
|
data-frame-source-badge="fusion"
|