@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/artifact-response.ts +84 -0
- package/corpus/core/src/agent/thread-data-builder.ts +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/core/src/integrations/webhook-handler.ts +41 -5
- package/corpus/core/src/server/agent-chat-plugin.ts +7 -2
- package/corpus/templates/clips/app/components/capture-install-options.tsx +60 -5
- package/corpus/templates/clips/app/components/library/library-layout.tsx +9 -1
- package/corpus/templates/clips/app/hooks/use-desktop-promo.ts +4 -4
- package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/clips/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
- package/corpus/templates/clips/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/clips/app/lib/capture-install-options.ts +89 -4
- package/corpus/templates/clips/app/routes/_app.dictate.tsx +10 -1
- package/corpus/templates/clips/app/routes/record.tsx +1 -0
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +7 -0
- package/corpus/templates/clips/changelog/2026-07-16-the-desktop-app-button-now-shows-open-desktop-app-and-launch.md +6 -0
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
- package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +41 -6
- package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +5 -0
- package/corpus/templates/content/.agents/skills/content/SKILL.md +31 -0
- package/corpus/templates/content/changelog/2026-07-16-slack-corrections-now-preserve-newer-content-values-unless-y.md +6 -0
- 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/a2a/artifact-response.d.ts +9 -0
- package/dist/a2a/artifact-response.d.ts.map +1 -1
- package/dist/a2a/artifact-response.js +68 -0
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/agent/thread-data-builder.js +1 -1
- package/dist/agent/thread-data-builder.js.map +1 -1
- 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/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +24 -5
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +3 -1
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- 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
|
|
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
|
}
|