@agent-native/core 0.106.0 → 0.106.2

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.
Files changed (104) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/artifact-response.ts +84 -0
  5. package/corpus/core/src/agent/thread-data-builder.ts +1 -1
  6. package/corpus/core/src/application-state/index.ts +2 -0
  7. package/corpus/core/src/application-state/script-helpers.ts +12 -0
  8. package/corpus/core/src/application-state/store.ts +38 -0
  9. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +10 -3
  10. package/corpus/core/src/integrations/webhook-handler.ts +41 -5
  11. package/corpus/core/src/server/agent-chat-plugin.ts +7 -2
  12. package/corpus/templates/clips/app/components/capture-install-options.tsx +60 -5
  13. package/corpus/templates/clips/app/components/library/library-layout.tsx +9 -1
  14. package/corpus/templates/clips/app/hooks/use-desktop-promo.ts +4 -4
  15. package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -0
  16. package/corpus/templates/clips/app/i18n/de-DE.ts +1 -0
  17. package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
  18. package/corpus/templates/clips/app/i18n/es-ES.ts +1 -0
  19. package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -0
  20. package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -0
  21. package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -0
  22. package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -0
  23. package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -0
  24. package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -0
  25. package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -0
  26. package/corpus/templates/clips/app/lib/capture-install-options.ts +89 -4
  27. package/corpus/templates/clips/app/routes/_app.dictate.tsx +10 -1
  28. package/corpus/templates/clips/app/routes/record.tsx +1 -0
  29. package/corpus/templates/clips/app/routes/share.$shareId.tsx +7 -0
  30. package/corpus/templates/clips/changelog/2026-07-16-the-desktop-app-button-now-shows-open-desktop-app-and-launch.md +6 -0
  31. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
  32. package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +1 -0
  33. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +41 -6
  34. package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +5 -0
  35. package/corpus/templates/content/.agents/skills/content/SKILL.md +31 -0
  36. package/corpus/templates/content/changelog/2026-07-16-slack-corrections-now-preserve-newer-content-values-unless-y.md +6 -0
  37. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +22 -0
  38. package/corpus/templates/design/AGENTS.md +16 -0
  39. package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +54 -0
  40. package/corpus/templates/design/actions/propose-node-rewrite.ts +290 -0
  41. package/corpus/templates/design/actions/resolve-node-rewrite.ts +257 -0
  42. package/corpus/templates/design/actions/view-screen.ts +98 -2
  43. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +71 -1
  44. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +19 -0
  45. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +33 -0
  46. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +194 -0
  47. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +21 -4
  48. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +18 -0
  49. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
  50. package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +638 -0
  51. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +535 -54
  52. package/corpus/templates/design/app/components/visual-editor/index.ts +5 -1
  53. package/corpus/templates/design/app/components/visual-editor/review-canvas-state.ts +21 -3
  54. package/corpus/templates/design/app/i18n/ar-SA.ts +35 -0
  55. package/corpus/templates/design/app/i18n/de-DE.ts +35 -0
  56. package/corpus/templates/design/app/i18n/es-ES.ts +35 -0
  57. package/corpus/templates/design/app/i18n/fr-FR.ts +35 -0
  58. package/corpus/templates/design/app/i18n/hi-IN.ts +35 -0
  59. package/corpus/templates/design/app/i18n/ja-JP.ts +35 -0
  60. package/corpus/templates/design/app/i18n/ko-KR.ts +35 -0
  61. package/corpus/templates/design/app/i18n/pt-BR.ts +35 -0
  62. package/corpus/templates/design/app/i18n/zh-CN.ts +35 -0
  63. package/corpus/templates/design/app/i18n/zh-TW.ts +32 -0
  64. package/corpus/templates/design/app/i18n-data.ts +32 -0
  65. package/corpus/templates/design/app/lib/node-reprompt.ts +110 -0
  66. package/corpus/templates/design/changelog/2026-07-16-design-editing-and-review-are-now-more-predictable.md +6 -0
  67. package/corpus/templates/design/server/lib/reprompt-action-guard.ts +107 -0
  68. package/corpus/templates/design/server/plugins/agent-chat.ts +9 -1
  69. package/corpus/templates/design/shared/code-layer.ts +15 -0
  70. package/corpus/templates/design/shared/node-rewrite.ts +213 -0
  71. package/corpus/templates/design/shared/review-anchor.ts +26 -3
  72. package/dist/a2a/artifact-response.d.ts +9 -0
  73. package/dist/a2a/artifact-response.d.ts.map +1 -1
  74. package/dist/a2a/artifact-response.js +68 -0
  75. package/dist/a2a/artifact-response.js.map +1 -1
  76. package/dist/agent/thread-data-builder.js +1 -1
  77. package/dist/agent/thread-data-builder.js.map +1 -1
  78. package/dist/application-state/index.d.ts +2 -2
  79. package/dist/application-state/index.d.ts.map +1 -1
  80. package/dist/application-state/index.js +2 -2
  81. package/dist/application-state/index.js.map +1 -1
  82. package/dist/application-state/script-helpers.d.ts +1 -0
  83. package/dist/application-state/script-helpers.d.ts.map +1 -1
  84. package/dist/application-state/script-helpers.js +7 -1
  85. package/dist/application-state/script-helpers.js.map +1 -1
  86. package/dist/application-state/store.d.ts +1 -0
  87. package/dist/application-state/store.d.ts.map +1 -1
  88. package/dist/application-state/store.js +28 -0
  89. package/dist/application-state/store.js.map +1 -1
  90. package/dist/client/review/ReviewCommentComposer.d.ts +4 -1
  91. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  92. package/dist/client/review/ReviewCommentComposer.js +3 -3
  93. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  94. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  95. package/dist/integrations/webhook-handler.js +24 -5
  96. package/dist/integrations/webhook-handler.js.map +1 -1
  97. package/dist/notifications/routes.d.ts +2 -2
  98. package/dist/resources/handlers.d.ts +1 -1
  99. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  100. package/dist/server/agent-chat-plugin.js +3 -1
  101. package/dist/server/agent-chat-plugin.js.map +1 -1
  102. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  103. package/dist/server/transcribe-voice.d.ts +1 -1
  104. package/package.json +1 -1
@@ -0,0 +1,290 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ compareAndSetAppState,
4
+ readAppState,
5
+ writeAppState,
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 { nanoid } from "nanoid";
10
+ import { z } from "zod";
11
+
12
+ import { getDb, schema } from "../server/db/index.js";
13
+ import {
14
+ readLiveSourceFile,
15
+ type SourceWorkspaceFile,
16
+ } from "../server/source-workspace.js";
17
+ import type { CodeLayerSource } from "../shared/code-layer.js";
18
+ import {
19
+ designRepromptPendingStateKey,
20
+ designRepromptProposalStateKey,
21
+ MAX_NODE_REWRITE_PROPOSAL_BYTES,
22
+ resolveNodeRewriteTarget,
23
+ validateNodeRewriteVariant,
24
+ type NodeHtmlPreviewBridgeMessage,
25
+ type NodeRewriteProposal,
26
+ type NodeRewriteTarget,
27
+ } from "../shared/node-rewrite.js";
28
+ import { designSourceTypeFromData } from "../shared/source-mode.js";
29
+
30
+ const sourceSchema = z
31
+ .object({
32
+ designId: z.string().min(1).optional(),
33
+ fileId: z.string().min(1).optional(),
34
+ filename: z.string().min(1).optional(),
35
+ })
36
+ .superRefine((source, ctx) => {
37
+ if (!source.designId && !source.fileId) {
38
+ ctx.addIssue({
39
+ code: z.ZodIssueCode.custom,
40
+ path: ["designId"],
41
+ message: "source.designId or source.fileId is required",
42
+ });
43
+ }
44
+ });
45
+
46
+ const targetSchema = z
47
+ .object({
48
+ nodeId: z.string().min(1).optional(),
49
+ selector: z.string().min(1).optional(),
50
+ })
51
+ .superRefine((target, ctx) => {
52
+ if (!target.nodeId && !target.selector) {
53
+ ctx.addIssue({
54
+ code: z.ZodIssueCode.custom,
55
+ path: ["nodeId"],
56
+ message: "target.nodeId or target.selector is required",
57
+ });
58
+ }
59
+ });
60
+
61
+ const variantSchema = z.object({
62
+ html: z
63
+ .string()
64
+ .min(1)
65
+ .max(250_000)
66
+ .describe("Replacement outerHTML for the selected subtree"),
67
+ summary: z.string().trim().min(1).max(240),
68
+ });
69
+
70
+ const pendingRepromptSchema = z.object({
71
+ repromptId: z.string().min(1),
72
+ designId: z.string().min(1),
73
+ fileId: z.string().min(1),
74
+ target: targetSchema,
75
+ baseVersionHash: z.string().min(1),
76
+ instruction: z.string(),
77
+ createdAt: z.string(),
78
+ priorProposalId: z.string().min(1).optional(),
79
+ priorRepromptId: z.string().min(1).optional(),
80
+ });
81
+
82
+ interface EditableDesignFile extends SourceWorkspaceFile {
83
+ designData: string | null;
84
+ }
85
+
86
+ async function resolveEditableDesignFile(source: {
87
+ designId?: string;
88
+ fileId?: string;
89
+ filename?: string;
90
+ }): Promise<EditableDesignFile> {
91
+ const db = getDb();
92
+ const conditions = [
93
+ accessFilter(schema.designs, schema.designShares),
94
+ source.fileId
95
+ ? eq(schema.designFiles.id, source.fileId)
96
+ : eq(schema.designFiles.designId, source.designId ?? ""),
97
+ ];
98
+ if (!source.fileId) {
99
+ conditions.push(
100
+ eq(schema.designFiles.filename, source.filename ?? "index.html"),
101
+ );
102
+ }
103
+
104
+ const [file] = await db
105
+ .select({
106
+ id: schema.designFiles.id,
107
+ designId: schema.designFiles.designId,
108
+ filename: schema.designFiles.filename,
109
+ fileType: schema.designFiles.fileType,
110
+ content: schema.designFiles.content,
111
+ createdAt: schema.designFiles.createdAt,
112
+ updatedAt: schema.designFiles.updatedAt,
113
+ designData: schema.designs.data,
114
+ })
115
+ .from(schema.designFiles)
116
+ .innerJoin(
117
+ schema.designs,
118
+ eq(schema.designFiles.designId, schema.designs.id),
119
+ )
120
+ .where(and(...conditions))
121
+ .limit(1);
122
+
123
+ if (!file) throw new Error("Design HTML file not found.");
124
+ if (source.designId && source.designId !== file.designId) {
125
+ throw new Error(
126
+ `source.designId "${source.designId}" does not match file "${file.id}"`,
127
+ );
128
+ }
129
+ if (source.filename && source.filename !== file.filename) {
130
+ throw new Error(
131
+ `source.filename "${source.filename}" does not match file "${file.id}"`,
132
+ );
133
+ }
134
+ if (file.fileType !== "html") {
135
+ throw new Error("Node rewrites only support HTML design files.");
136
+ }
137
+ if (designSourceTypeFromData(file.designData) !== "inline") {
138
+ throw new Error("Node rewrites only support inline design screens.");
139
+ }
140
+ await assertAccess("design", file.designId, "editor");
141
+ return file;
142
+ }
143
+
144
+ function targetsMatch(a: NodeRewriteTarget, b: NodeRewriteTarget): boolean {
145
+ const nodeIdsConflict = a.nodeId && b.nodeId && a.nodeId !== b.nodeId;
146
+ const selectorsConflict =
147
+ a.selector && b.selector && a.selector !== b.selector;
148
+ if (nodeIdsConflict || selectorsConflict) return false;
149
+ return Boolean(
150
+ (a.nodeId && b.nodeId && a.nodeId === b.nodeId) ||
151
+ (a.selector && b.selector && a.selector === b.selector),
152
+ );
153
+ }
154
+
155
+ export default defineAction({
156
+ description:
157
+ "Propose one to three scoped HTML rewrites for a pending Design selection. " +
158
+ "This previews only: it stores proposal state and never changes the design file. " +
159
+ "The target must identify, and the reprompt id and base hash must exactly match, the pending client selection.",
160
+ schema: z.object({
161
+ source: sourceSchema,
162
+ target: targetSchema,
163
+ baseVersionHash: z.string().min(1),
164
+ variants: z.array(variantSchema).min(1).max(3),
165
+ repromptId: z.string().min(1),
166
+ }),
167
+ run: async ({ source, target, baseVersionHash, variants, repromptId }) => {
168
+ const file = await resolveEditableDesignFile(source);
169
+ const sourceDescriptor: CodeLayerSource = {
170
+ kind: "design-file",
171
+ sourceType: "inline",
172
+ designId: file.designId,
173
+ fileId: file.id,
174
+ filename: file.filename,
175
+ };
176
+ const live = await readLiveSourceFile(file);
177
+ if (live.versionHash !== baseVersionHash) {
178
+ throw new Error(
179
+ "Screen changed since the selection was captured — regenerate the proposal.",
180
+ );
181
+ }
182
+
183
+ const pendingKey = designRepromptPendingStateKey(file.designId, file.id);
184
+ const pending = pendingRepromptSchema.parse(await readAppState(pendingKey));
185
+ if (pending.repromptId !== repromptId) {
186
+ throw new Error("Pending reprompt does not match this request.");
187
+ }
188
+ if (
189
+ pending.designId !== file.designId ||
190
+ pending.fileId !== file.id ||
191
+ !targetsMatch(pending.target, target)
192
+ ) {
193
+ throw new Error(
194
+ "The proposed target does not match the pending selected subtree.",
195
+ );
196
+ }
197
+ if (pending.baseVersionHash !== baseVersionHash) {
198
+ throw new Error(
199
+ "The proposal base hash does not match the pending selection.",
200
+ );
201
+ }
202
+
203
+ const authoritativeTarget = pending.target;
204
+ const node = resolveNodeRewriteTarget(
205
+ live.content,
206
+ authoritativeTarget,
207
+ sourceDescriptor,
208
+ );
209
+ const validatedVariants = variants.map((variant) =>
210
+ validateNodeRewriteVariant(variant, sourceDescriptor),
211
+ );
212
+ const proposalId = `node-rewrite-${nanoid()}`;
213
+ const resolvedTarget = {
214
+ nodeId: node.id,
215
+ selector: node.selector,
216
+ };
217
+ const proposal: NodeRewriteProposal = {
218
+ proposalId,
219
+ repromptId,
220
+ designId: file.designId,
221
+ fileId: file.id,
222
+ filename: file.filename,
223
+ target: authoritativeTarget,
224
+ resolvedTarget,
225
+ baseVersionHash,
226
+ variants: validatedVariants,
227
+ chosenIndex: 0,
228
+ createdAt: new Date().toISOString(),
229
+ };
230
+ const serializedProposal = JSON.stringify(proposal);
231
+ if (
232
+ Buffer.byteLength(serializedProposal, "utf8") >
233
+ MAX_NODE_REWRITE_PROPOSAL_BYTES
234
+ ) {
235
+ throw new Error(
236
+ "Generated candidates are too large to preview safely. Use fewer variants or regenerate a smaller selected subtree.",
237
+ );
238
+ }
239
+ const proposalKey = designRepromptProposalStateKey(
240
+ file.designId,
241
+ file.id,
242
+ repromptId,
243
+ );
244
+ await writeAppState(
245
+ proposalKey,
246
+ proposal as unknown as Record<string, unknown>,
247
+ );
248
+ const currentPending = await readAppState(pendingKey);
249
+ if (currentPending?.repromptId !== repromptId) {
250
+ await compareAndSetAppState(
251
+ proposalKey,
252
+ proposal as unknown as Record<string, unknown>,
253
+ null,
254
+ );
255
+ throw new Error(
256
+ "This regeneration was superseded by a newer request before its candidates were published.",
257
+ );
258
+ }
259
+ if (pending.priorProposalId && pending.priorRepromptId) {
260
+ const priorProposalKey = designRepromptProposalStateKey(
261
+ file.designId,
262
+ file.id,
263
+ pending.priorRepromptId,
264
+ );
265
+ const priorProposal = await readAppState(priorProposalKey);
266
+ if (priorProposal?.proposalId === pending.priorProposalId) {
267
+ await compareAndSetAppState(priorProposalKey, priorProposal, null);
268
+ }
269
+ }
270
+
271
+ const bridgeMessages: NodeHtmlPreviewBridgeMessage[] = [
272
+ {
273
+ type: "node-html-preview",
274
+ proposalId,
275
+ target: resolvedTarget,
276
+ html: validatedVariants[0]!.html,
277
+ operation: "preview",
278
+ },
279
+ ];
280
+ return {
281
+ proposalId,
282
+ repromptId,
283
+ designId: file.designId,
284
+ fileId: file.id,
285
+ target: resolvedTarget,
286
+ variants: validatedVariants,
287
+ bridgeMessages,
288
+ };
289
+ },
290
+ });
@@ -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: resolveActiveScreen(files, navigation, designSelection),
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
- resolveActiveScreen(files, navigation, designSelection)?.id,
456
+ activeScreen?.id,
361
457
  reviewSummary,
362
458
  );
363
459
  }