@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
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.106.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0d8118e: Allow review comment composers to render a custom agent action in the standard action row, choose the Enter-key submit target, and atomically compare-and-set application state for race-safe UI workflows.
|
|
8
|
+
|
|
3
9
|
## 0.106.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.106.
|
|
3
|
+
"version": "0.106.1",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -4,6 +4,7 @@ export {
|
|
|
4
4
|
appStateGetMany,
|
|
5
5
|
appStatePut,
|
|
6
6
|
appStateDelete,
|
|
7
|
+
appStateCompareAndSet,
|
|
7
8
|
appStateList,
|
|
8
9
|
appStateDeleteByPrefix,
|
|
9
10
|
} from "./store.js";
|
|
@@ -33,6 +34,7 @@ export {
|
|
|
33
34
|
readAppState,
|
|
34
35
|
writeAppState,
|
|
35
36
|
deleteAppState,
|
|
37
|
+
compareAndSetAppState,
|
|
36
38
|
listAppState,
|
|
37
39
|
deleteAppStateByPrefix,
|
|
38
40
|
readAppStateForCurrentTab,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
appStateGet,
|
|
18
18
|
appStatePut,
|
|
19
19
|
appStateDelete,
|
|
20
|
+
appStateCompareAndSet,
|
|
20
21
|
appStateList,
|
|
21
22
|
appStateDeleteByPrefix,
|
|
22
23
|
} from "./store.js";
|
|
@@ -71,6 +72,17 @@ export async function deleteAppState(key: string): Promise<boolean> {
|
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
export async function compareAndSetAppState(
|
|
76
|
+
key: string,
|
|
77
|
+
expectedValue: Record<string, unknown>,
|
|
78
|
+
nextValue: Record<string, unknown> | null,
|
|
79
|
+
): Promise<boolean> {
|
|
80
|
+
const sessionId = await resolveSessionId();
|
|
81
|
+
return appStateCompareAndSet(sessionId, key, expectedValue, nextValue, {
|
|
82
|
+
requestSource: "agent",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
export async function listAppState(
|
|
75
87
|
prefix: string,
|
|
76
88
|
): Promise<Array<{ key: string; value: Record<string, unknown> }>> {
|
|
@@ -191,6 +191,44 @@ export async function appStateDelete(
|
|
|
191
191
|
return deleted;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
export async function appStateCompareAndSet(
|
|
195
|
+
sessionId: string,
|
|
196
|
+
key: string,
|
|
197
|
+
expectedValue: Record<string, unknown>,
|
|
198
|
+
nextValue: Record<string, unknown> | null,
|
|
199
|
+
options?: StoreWriteOptions,
|
|
200
|
+
): Promise<boolean> {
|
|
201
|
+
await ensureTable();
|
|
202
|
+
const client = getDbExec();
|
|
203
|
+
const expected = JSON.stringify(expectedValue);
|
|
204
|
+
if (nextValue === null) {
|
|
205
|
+
const result = await client.execute({
|
|
206
|
+
sql: `DELETE FROM application_state WHERE session_id = ? AND key = ? AND value = ?`,
|
|
207
|
+
args: [sessionId, key, expected],
|
|
208
|
+
});
|
|
209
|
+
const changed = result.rowsAffected > 0;
|
|
210
|
+
if (changed) emitAppStateDelete(key, options?.requestSource, sessionId);
|
|
211
|
+
return changed;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const next = JSON.stringify(nextValue);
|
|
215
|
+
if (
|
|
216
|
+
!isLocalDatabase() &&
|
|
217
|
+
Buffer.byteLength(next, "utf8") > MAX_HOSTED_APP_STATE_VALUE_BYTES
|
|
218
|
+
) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`application_state value "${key}" is too large for hosted SQL storage. Store large files, base64, or blobs in file storage and write only a URL or handle.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const result = await client.execute({
|
|
224
|
+
sql: `UPDATE application_state SET value = ?, updated_at = ? WHERE session_id = ? AND key = ? AND value = ?`,
|
|
225
|
+
args: [next, Date.now(), sessionId, key, expected],
|
|
226
|
+
});
|
|
227
|
+
const changed = result.rowsAffected > 0;
|
|
228
|
+
if (changed) emitAppStateChange(key, options?.requestSource, sessionId);
|
|
229
|
+
return changed;
|
|
230
|
+
}
|
|
231
|
+
|
|
194
232
|
export async function appStateList(
|
|
195
233
|
sessionId: string,
|
|
196
234
|
keyPrefix: string,
|
|
@@ -2,6 +2,7 @@ import { Button } from "@agent-native/toolkit/ui/button";
|
|
|
2
2
|
import { Spinner } from "@agent-native/toolkit/ui/spinner";
|
|
3
3
|
import { Textarea } from "@agent-native/toolkit/ui/textarea";
|
|
4
4
|
import { IconFocus2, IconMessageCircle, IconSend } from "@tabler/icons-react";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
5
6
|
|
|
6
7
|
import type { ReviewResolutionTarget } from "../../review/types.js";
|
|
7
8
|
import { cn } from "../utils.js";
|
|
@@ -13,12 +14,14 @@ export interface ReviewCommentComposerProps {
|
|
|
13
14
|
submittingTarget?: ReviewResolutionTarget | null;
|
|
14
15
|
disabled?: boolean;
|
|
15
16
|
showAgentAction?: boolean;
|
|
17
|
+
agentAction?: ReactNode;
|
|
16
18
|
placeholder?: string;
|
|
17
19
|
commentLabel?: string;
|
|
18
20
|
agentLabel?: string;
|
|
19
21
|
contextLabel?: string;
|
|
20
22
|
autoFocus?: boolean;
|
|
21
23
|
submitOnEnter?: boolean;
|
|
24
|
+
enterSubmitTarget?: ReviewResolutionTarget;
|
|
22
25
|
onEscape?: () => void;
|
|
23
26
|
className?: string;
|
|
24
27
|
}
|
|
@@ -30,12 +33,14 @@ export function ReviewCommentComposer({
|
|
|
30
33
|
submittingTarget = null,
|
|
31
34
|
disabled = false,
|
|
32
35
|
showAgentAction = false,
|
|
36
|
+
agentAction,
|
|
33
37
|
placeholder = "Add a comment...",
|
|
34
38
|
commentLabel = "Comment",
|
|
35
39
|
agentLabel = "Send to agent",
|
|
36
40
|
contextLabel,
|
|
37
41
|
autoFocus = false,
|
|
38
42
|
submitOnEnter = false,
|
|
43
|
+
enterSubmitTarget = "human",
|
|
39
44
|
onEscape,
|
|
40
45
|
className,
|
|
41
46
|
}: ReviewCommentComposerProps) {
|
|
@@ -75,7 +80,7 @@ export function ReviewCommentComposer({
|
|
|
75
80
|
}
|
|
76
81
|
if (submitOnEnter && event.key === "Enter" && !event.shiftKey) {
|
|
77
82
|
event.preventDefault();
|
|
78
|
-
submit(
|
|
83
|
+
submit(enterSubmitTarget);
|
|
79
84
|
}
|
|
80
85
|
}}
|
|
81
86
|
/>
|
|
@@ -84,7 +89,7 @@ export function ReviewCommentComposer({
|
|
|
84
89
|
type="submit"
|
|
85
90
|
size="sm"
|
|
86
91
|
disabled={!canSubmit}
|
|
87
|
-
className="h-8 w-full
|
|
92
|
+
className="h-8 w-full gap-1.5 @2xs/review:w-auto @2xs/review:min-w-28 @2xs/review:shrink-0"
|
|
88
93
|
>
|
|
89
94
|
{submittingTarget === "human" ? (
|
|
90
95
|
<Spinner className="size-3.5" />
|
|
@@ -93,7 +98,9 @@ export function ReviewCommentComposer({
|
|
|
93
98
|
)}
|
|
94
99
|
<span className="truncate">{commentLabel}</span>
|
|
95
100
|
</Button>
|
|
96
|
-
{showAgentAction ? (
|
|
101
|
+
{showAgentAction && agentAction ? (
|
|
102
|
+
agentAction
|
|
103
|
+
) : showAgentAction ? (
|
|
97
104
|
<Button
|
|
98
105
|
type="button"
|
|
99
106
|
size="sm"
|
|
@@ -47,6 +47,28 @@ iframe-backed screens on the infinite canvas.
|
|
|
47
47
|
and create one screen per URL/path. Shorthand like
|
|
48
48
|
`localhost:1234/onboarding/1` means `http://localhost:1234/onboarding/1`.
|
|
49
49
|
|
|
50
|
+
## Select And Reprompt
|
|
51
|
+
|
|
52
|
+
When a chat message begins with `[Reprompt selection]`, the selected subtree is
|
|
53
|
+
a hard write boundary. The only mutation path is `propose-node-rewrite` with
|
|
54
|
+
the exact `repromptId`, target, and `baseVersionHash` captured in
|
|
55
|
+
`design-reprompt-pending:<designId>:<fileId>`. Never use `apply-visual-edit`,
|
|
56
|
+
`apply-source-edit`, `write-source`, `write-local-file`, `edit-design`, or any
|
|
57
|
+
other content-writing action for that request. Clarifying questions are allowed,
|
|
58
|
+
but a requested change must remain a proposal.
|
|
59
|
+
|
|
60
|
+
Produce one variant by default. Produce two or three only when the instruction
|
|
61
|
+
asks for options. A retry includes `priorProposalId`; keep the same target and
|
|
62
|
+
base version, incorporate the feedback, and call `propose-node-rewrite` again.
|
|
63
|
+
The UI previews the returned subtree without persisting it.
|
|
64
|
+
|
|
65
|
+
Use `resolve-node-rewrite` for the accept/reject lifecycle. Accept applies the
|
|
66
|
+
chosen variant as one version-checked inline/Yjs content transaction so one
|
|
67
|
+
undo restores the prior structure; reject clears the proposal without changing
|
|
68
|
+
content. For conversational resolution such as “apply the second one,” call
|
|
69
|
+
`view-screen`, read the active `design.reprompt.proposal`, and pass its
|
|
70
|
+
`proposalId` plus the zero-based `variantIndex` to `resolve-node-rewrite`.
|
|
71
|
+
|
|
50
72
|
## Review Quality
|
|
51
73
|
|
|
52
74
|
- Treat the running app as the truth. Preserve its component language, tokens,
|
|
@@ -16,6 +16,13 @@ patterns live in `.agents/skills/`.
|
|
|
16
16
|
- Never hardcode API keys, tokens, webhook URLs, signing secrets, private Builder/internal data, customer data, or credential-looking literals. Use secrets/OAuth/runtime configuration and obvious placeholders in examples.
|
|
17
17
|
- Use the app actions for designs, files, versions, design systems, variants,
|
|
18
18
|
export, and sharing. Do not write design rows directly with SQL.
|
|
19
|
+
- A message beginning with `[Reprompt selection]` is preview-only. Call
|
|
20
|
+
`propose-node-rewrite` with its exact `repromptId`, target, and base hash;
|
|
21
|
+
never call `edit-design`, `update-design`, `update-file`, `generate-design`,
|
|
22
|
+
`apply-visual-edit`, or another content writer. Only the frontend-only
|
|
23
|
+
`resolve-node-rewrite` action may persist an explicitly accepted proposal.
|
|
24
|
+
- A message beginning with `[Selection question]` is read-only. Answer about
|
|
25
|
+
the captured element and subtree without calling content-writing actions.
|
|
19
26
|
- When a user wants an established public system as a starting point, call
|
|
20
27
|
`create-design-system` with `templateId: material-3`, `carbon-white`, or
|
|
21
28
|
`primer-light`. These are source-linked, versioned token snapshots with
|
|
@@ -268,6 +275,15 @@ patterns live in `.agents/skills/`.
|
|
|
268
275
|
generation planning state created by `generate-screens` (canvas region
|
|
269
276
|
assignments and per-frame instructions consumed by `generate-design` and
|
|
270
277
|
`view-screen`; not rendered as canvas overlays).
|
|
278
|
+
- `design-reprompt-pending:<designId>:<fileId>` is the client-captured source
|
|
279
|
+
selection, instruction, base hash, and authoritative current request id for
|
|
280
|
+
a scoped regenerate request.
|
|
281
|
+
- `design-reprompt-proposal:<designId>:<fileId>:<repromptId>` is one
|
|
282
|
+
request-specific preview-only subtree proposal. Candidate payloads have a
|
|
283
|
+
256 KiB aggregate serialized limit. Resolution and cancellation use atomic
|
|
284
|
+
compare-and-set cleanup so an older request cannot erase a newer one.
|
|
285
|
+
`view-screen` lists only proposals paired to the current pending request as
|
|
286
|
+
`pendingCandidateReviews`.
|
|
271
287
|
- `show-design-questions` opens focused pre-generation questions in the main
|
|
272
288
|
design canvas (`show-questions` application state).
|
|
273
289
|
- `guided-questions` may contain a one-click chat choice for the current
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
compareAndSetAppState,
|
|
4
|
+
readAppState,
|
|
5
|
+
} from "@agent-native/core/application-state";
|
|
6
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
designRepromptPendingStateKey,
|
|
11
|
+
designRepromptProposalStateKey,
|
|
12
|
+
isNodeRewriteProposal,
|
|
13
|
+
isPendingDesignReprompt,
|
|
14
|
+
} from "../shared/node-rewrite.js";
|
|
15
|
+
|
|
16
|
+
export default defineAction({
|
|
17
|
+
agentTool: false,
|
|
18
|
+
description:
|
|
19
|
+
"Cancel one pending node rewrite only when it is still the current request.",
|
|
20
|
+
schema: z.object({
|
|
21
|
+
designId: z.string().min(1),
|
|
22
|
+
fileId: z.string().min(1),
|
|
23
|
+
repromptId: z.string().min(1),
|
|
24
|
+
}),
|
|
25
|
+
run: async ({ designId, fileId, repromptId }) => {
|
|
26
|
+
await assertAccess("design", designId, "editor");
|
|
27
|
+
const pendingKey = designRepromptPendingStateKey(designId, fileId);
|
|
28
|
+
const pending = await readAppState(pendingKey);
|
|
29
|
+
if (
|
|
30
|
+
!isPendingDesignReprompt(pending) ||
|
|
31
|
+
pending.repromptId !== repromptId
|
|
32
|
+
) {
|
|
33
|
+
return { cancelled: false, superseded: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const proposalKey = designRepromptProposalStateKey(
|
|
37
|
+
designId,
|
|
38
|
+
fileId,
|
|
39
|
+
repromptId,
|
|
40
|
+
);
|
|
41
|
+
const proposal = await readAppState(proposalKey);
|
|
42
|
+
const [pendingCancelled, proposalCancelled] = await Promise.all([
|
|
43
|
+
compareAndSetAppState(pendingKey, pending, null),
|
|
44
|
+
isNodeRewriteProposal(proposal)
|
|
45
|
+
? compareAndSetAppState(proposalKey, proposal, null)
|
|
46
|
+
: Promise.resolve(false),
|
|
47
|
+
]);
|
|
48
|
+
return {
|
|
49
|
+
cancelled: pendingCancelled,
|
|
50
|
+
proposalCancelled,
|
|
51
|
+
superseded: !pendingCancelled,
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -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
|
+
});
|