@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,107 @@
|
|
|
1
|
+
import { getThread, type ActionEntry } from "@agent-native/core/server";
|
|
2
|
+
|
|
3
|
+
const REPROMPT_MUTATION_ALLOWLIST = new Set(["propose-node-rewrite"]);
|
|
4
|
+
|
|
5
|
+
export function isRepromptSelectionMessage(value: string): boolean {
|
|
6
|
+
return /(?:^|\n)\[Reprompt selection\](?:\n|$)/.test(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isSelectionQuestionMessage(value: string): boolean {
|
|
10
|
+
return /(?:^|\n)\[Selection question\](?:\n|$)/.test(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function latestUserMessageText(threadData: string): string {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(threadData) as {
|
|
16
|
+
messages?: Array<{
|
|
17
|
+
message?: {
|
|
18
|
+
role?: unknown;
|
|
19
|
+
content?: unknown;
|
|
20
|
+
};
|
|
21
|
+
role?: unknown;
|
|
22
|
+
content?: unknown;
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
25
|
+
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
26
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
27
|
+
const entry = messages[index]!;
|
|
28
|
+
const message = entry.message ?? entry;
|
|
29
|
+
if (message.role !== "user") continue;
|
|
30
|
+
if (typeof message.content === "string") return message.content;
|
|
31
|
+
if (!Array.isArray(message.content)) return "";
|
|
32
|
+
return message.content
|
|
33
|
+
.filter((part): part is { type: "text"; text: string } =>
|
|
34
|
+
Boolean(
|
|
35
|
+
part &&
|
|
36
|
+
typeof part === "object" &&
|
|
37
|
+
(part as { type?: unknown }).type === "text" &&
|
|
38
|
+
typeof (part as { text?: unknown }).text === "string",
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
.map((part) => part.text)
|
|
42
|
+
.join("\n");
|
|
43
|
+
}
|
|
44
|
+
} catch {}
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function threadSelectionIntent(thread: {
|
|
49
|
+
preview: string;
|
|
50
|
+
threadData: string;
|
|
51
|
+
}): "reprompt" | "question" | null {
|
|
52
|
+
const latestMessage = latestUserMessageText(thread.threadData);
|
|
53
|
+
if (
|
|
54
|
+
isRepromptSelectionMessage(thread.preview) ||
|
|
55
|
+
isRepromptSelectionMessage(latestMessage)
|
|
56
|
+
) {
|
|
57
|
+
return "reprompt";
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
isSelectionQuestionMessage(thread.preview) ||
|
|
61
|
+
isSelectionQuestionMessage(latestMessage)
|
|
62
|
+
) {
|
|
63
|
+
return "question";
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function guardRepromptActionRegistry(
|
|
69
|
+
actions: Record<string, ActionEntry>,
|
|
70
|
+
): Record<string, ActionEntry> {
|
|
71
|
+
return Object.fromEntries(
|
|
72
|
+
Object.entries(actions).map(([name, entry]) => {
|
|
73
|
+
if (entry.readOnly === true) return [name, entry];
|
|
74
|
+
return [
|
|
75
|
+
name,
|
|
76
|
+
{
|
|
77
|
+
...entry,
|
|
78
|
+
run: async (args, context) => {
|
|
79
|
+
if (context?.caller === "tool" && context.threadId) {
|
|
80
|
+
const thread = await getThread(context.threadId);
|
|
81
|
+
if (!thread) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Cannot verify the agent thread for mutating action ${name}.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const intent = threadSelectionIntent(thread);
|
|
87
|
+
if (
|
|
88
|
+
intent === "reprompt" &&
|
|
89
|
+
!REPROMPT_MUTATION_ALLOWLIST.has(name)
|
|
90
|
+
) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`The current [Reprompt selection] turn is preview-only. Do not call ${name}; call propose-node-rewrite with the captured repromptId, target, and baseVersionHash instead.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (intent === "question") {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`The current [Selection question] turn is read-only. Answer the user's question without calling ${name} or changing the design.`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return entry.run(args, context);
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "@agent-native/core/server";
|
|
6
6
|
|
|
7
7
|
import actionsRegistry from "../../.generated/actions-registry.js";
|
|
8
|
+
import { guardRepromptActionRegistry } from "../lib/reprompt-action-guard.js";
|
|
8
9
|
import "../register-secrets.js";
|
|
9
10
|
|
|
10
11
|
const DESIGN_BACKGROUND_RUN_SOFT_TIMEOUT_MS = 13 * 60_000;
|
|
@@ -33,6 +34,7 @@ const INITIAL_TOOL_NAMES = [
|
|
|
33
34
|
"edit-design",
|
|
34
35
|
"generate-design",
|
|
35
36
|
"present-design-variants",
|
|
37
|
+
"propose-node-rewrite",
|
|
36
38
|
"insert-asset",
|
|
37
39
|
"connect-assets-mcp",
|
|
38
40
|
"apply-tweaks",
|
|
@@ -48,7 +50,9 @@ const INITIAL_TOOL_NAMES = [
|
|
|
48
50
|
|
|
49
51
|
export default createAgentChatPlugin({
|
|
50
52
|
appId: "design",
|
|
51
|
-
actions:
|
|
53
|
+
actions: guardRepromptActionRegistry(
|
|
54
|
+
loadActionsFromStaticRegistry(actionsRegistry),
|
|
55
|
+
),
|
|
52
56
|
initialToolNames: INITIAL_TOOL_NAMES,
|
|
53
57
|
// Enable sandboxed JavaScript execution so Design agents can fetch,
|
|
54
58
|
// paginate, and reduce provider data through providerFetch() without us
|
|
@@ -60,6 +64,10 @@ export default createAgentChatPlugin({
|
|
|
60
64
|
resolveOrgId: async (event) => (await getOrgContext(event)).orgId,
|
|
61
65
|
systemPrompt: `You are an AI prototyping assistant. You create and edit designs, files, design systems, variants, exports, sharing, and connected repository context through actions and shared application state.
|
|
62
66
|
|
|
67
|
+
When a user message begins with [Reprompt selection], the design must remain unchanged until the user accepts a preview. Call propose-node-rewrite with the exact repromptId, target, and baseVersionHash from the message. Never call edit-design, update-design, update-file, generate-design, apply-visual-edit, or any other content-writing action for that turn. The proposal action stores preview state only; the frontend-only resolve-node-rewrite action persists a chosen variant after the user presses Accept.
|
|
68
|
+
|
|
69
|
+
When a user message begins with [Selection question], answer about the captured element and selected subtree without changing the design. You may use read-only actions when more context is needed, but do not call any content-writing action. If the user actually intended an edit, explain that they can choose Preview change from the composer mode menu and resend.
|
|
70
|
+
|
|
63
71
|
When the user asks for a new design and the current navigation view is list, settings, design-systems, or otherwise has no designId, create a new design first. Do not reuse, delete screens from, or edit a previous design unless the user explicitly names that design or the current navigation state is an editor/present view with that designId.
|
|
64
72
|
|
|
65
73
|
Every web design must be responsive. Use mobile-first CSS, a viewport meta tag, and responsive layout changes for narrow widths; never ship a fixed-width desktop shell. Desktop is the default primary artboard: use a 1440×1024 canvas frame (or primaryViewport "desktop") unless the user explicitly asks for a mobile- or tablet-primary design. After generation, inspect desktop and mobile screenshots and correct overflow or broken reflow before reporting completion.
|
|
@@ -2709,6 +2709,21 @@ function resolveTarget(
|
|
|
2709
2709
|
};
|
|
2710
2710
|
}
|
|
2711
2711
|
|
|
2712
|
+
export function resolveCodeLayerTarget(
|
|
2713
|
+
html: string,
|
|
2714
|
+
target: EditIntentTarget,
|
|
2715
|
+
options: { source?: CodeLayerSource } = {},
|
|
2716
|
+
): { projection: CodeLayerProjection; resolution: EditIntentResolution } {
|
|
2717
|
+
const build = buildProjection(
|
|
2718
|
+
html,
|
|
2719
|
+
options.source ?? { kind: "inline-html" },
|
|
2720
|
+
);
|
|
2721
|
+
return {
|
|
2722
|
+
projection: build.projection,
|
|
2723
|
+
resolution: resolveTarget(build, target),
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2712
2727
|
function summarizeNode(node: CodeLayerNode): PatchNodeSummary {
|
|
2713
2728
|
return {
|
|
2714
2729
|
nodeId: node.id,
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CodeLayerNode,
|
|
3
|
+
CodeLayerSource,
|
|
4
|
+
EditIntentTarget,
|
|
5
|
+
} from "./code-layer.js";
|
|
6
|
+
import { resolveCodeLayerTarget } from "./code-layer.js";
|
|
7
|
+
import { assertDesignHtmlEditIntegrity } from "./html-integrity.js";
|
|
8
|
+
import { annotateScreenHtmlForPersist } from "./screen-annotation.js";
|
|
9
|
+
|
|
10
|
+
export const DESIGN_REPROMPT_PENDING_STATE_PREFIX = "design-reprompt-pending:";
|
|
11
|
+
export const DESIGN_REPROMPT_PROPOSAL_STATE_PREFIX =
|
|
12
|
+
"design-reprompt-proposal:";
|
|
13
|
+
export const MAX_NODE_REWRITE_PROPOSAL_BYTES = 256 * 1024;
|
|
14
|
+
|
|
15
|
+
export type NodeRewriteTarget = EditIntentTarget;
|
|
16
|
+
|
|
17
|
+
export interface PendingDesignReprompt {
|
|
18
|
+
repromptId: string;
|
|
19
|
+
designId: string;
|
|
20
|
+
fileId: string;
|
|
21
|
+
target: NodeRewriteTarget;
|
|
22
|
+
baseVersionHash: string;
|
|
23
|
+
instruction: string;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
priorProposalId?: string;
|
|
26
|
+
priorRepromptId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface NodeRewriteVariant {
|
|
30
|
+
html: string;
|
|
31
|
+
summary: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface NodeRewriteProposal {
|
|
35
|
+
proposalId: string;
|
|
36
|
+
repromptId: string;
|
|
37
|
+
designId: string;
|
|
38
|
+
fileId: string;
|
|
39
|
+
filename: string;
|
|
40
|
+
target: NodeRewriteTarget;
|
|
41
|
+
resolvedTarget: Required<NodeRewriteTarget>;
|
|
42
|
+
baseVersionHash: string;
|
|
43
|
+
variants: NodeRewriteVariant[];
|
|
44
|
+
chosenIndex: number;
|
|
45
|
+
createdAt: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isNodeRewriteProposal(
|
|
49
|
+
value: unknown,
|
|
50
|
+
): value is NodeRewriteProposal {
|
|
51
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
52
|
+
const proposal = value as Partial<NodeRewriteProposal>;
|
|
53
|
+
return (
|
|
54
|
+
typeof proposal.proposalId === "string" &&
|
|
55
|
+
typeof proposal.repromptId === "string" &&
|
|
56
|
+
typeof proposal.designId === "string" &&
|
|
57
|
+
typeof proposal.fileId === "string" &&
|
|
58
|
+
typeof proposal.baseVersionHash === "string" &&
|
|
59
|
+
Boolean(proposal.target) &&
|
|
60
|
+
Boolean(proposal.resolvedTarget) &&
|
|
61
|
+
Array.isArray(proposal.variants) &&
|
|
62
|
+
proposal.variants.length > 0
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isPendingDesignReprompt(
|
|
67
|
+
value: unknown,
|
|
68
|
+
): value is PendingDesignReprompt {
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
70
|
+
const pending = value as Partial<PendingDesignReprompt>;
|
|
71
|
+
return (
|
|
72
|
+
typeof pending.repromptId === "string" &&
|
|
73
|
+
typeof pending.designId === "string" &&
|
|
74
|
+
typeof pending.fileId === "string" &&
|
|
75
|
+
typeof pending.baseVersionHash === "string" &&
|
|
76
|
+
typeof pending.instruction === "string" &&
|
|
77
|
+
typeof pending.createdAt === "string" &&
|
|
78
|
+
Boolean(pending.target)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface NodeHtmlPreviewBridgeMessage {
|
|
83
|
+
type: "node-html-preview";
|
|
84
|
+
proposalId: string;
|
|
85
|
+
target: NodeRewriteTarget;
|
|
86
|
+
operation: "preview" | "restore";
|
|
87
|
+
html?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function designRepromptPendingStateKey(
|
|
91
|
+
designId: string,
|
|
92
|
+
fileId: string,
|
|
93
|
+
): string {
|
|
94
|
+
return `${DESIGN_REPROMPT_PENDING_STATE_PREFIX}${designId}:${fileId}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function designRepromptProposalStateKey(
|
|
98
|
+
designId: string,
|
|
99
|
+
fileId: string,
|
|
100
|
+
repromptId: string,
|
|
101
|
+
): string {
|
|
102
|
+
return `${DESIGN_REPROMPT_PROPOSAL_STATE_PREFIX}${designId}:${fileId}:${repromptId}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function designRepromptProposalStatePrefix(
|
|
106
|
+
designId: string,
|
|
107
|
+
fileId?: string,
|
|
108
|
+
): string {
|
|
109
|
+
return `${DESIGN_REPROMPT_PROPOSAL_STATE_PREFIX}${designId}:${fileId ? `${fileId}:` : ""}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function assertSingleElementFragment(
|
|
113
|
+
html: string,
|
|
114
|
+
source: CodeLayerSource,
|
|
115
|
+
): void {
|
|
116
|
+
if (!html.trim()) throw new Error("Variant HTML must not be empty.");
|
|
117
|
+
if (/<\/?(?:html|head|body)\b|<!doctype\b/i.test(html)) {
|
|
118
|
+
throw new Error("Variant HTML must be one subtree, not a full document.");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { projection } = resolveCodeLayerTarget(
|
|
122
|
+
html,
|
|
123
|
+
{ selector: "__node_rewrite_validation_target__" },
|
|
124
|
+
{ source },
|
|
125
|
+
);
|
|
126
|
+
if (projection.rootNodeIds.length !== 1) {
|
|
127
|
+
throw new Error("Variant HTML must contain exactly one root element.");
|
|
128
|
+
}
|
|
129
|
+
const root = projection.nodes.find(
|
|
130
|
+
(node) => node.id === projection.rootNodeIds[0],
|
|
131
|
+
);
|
|
132
|
+
if (!root?.source) {
|
|
133
|
+
throw new Error("Variant HTML could not be parsed as an element subtree.");
|
|
134
|
+
}
|
|
135
|
+
if (
|
|
136
|
+
html.slice(0, root.source.start).trim() ||
|
|
137
|
+
html.slice(root.source.end).trim()
|
|
138
|
+
) {
|
|
139
|
+
throw new Error("Variant HTML must contain exactly one root element.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const voidElements = new Set([
|
|
143
|
+
"area",
|
|
144
|
+
"base",
|
|
145
|
+
"br",
|
|
146
|
+
"col",
|
|
147
|
+
"embed",
|
|
148
|
+
"hr",
|
|
149
|
+
"img",
|
|
150
|
+
"input",
|
|
151
|
+
"link",
|
|
152
|
+
"meta",
|
|
153
|
+
"param",
|
|
154
|
+
"source",
|
|
155
|
+
"track",
|
|
156
|
+
"wbr",
|
|
157
|
+
]);
|
|
158
|
+
const unclosed = projection.nodes.find(
|
|
159
|
+
(node) =>
|
|
160
|
+
node.source &&
|
|
161
|
+
!voidElements.has(node.tag) &&
|
|
162
|
+
node.source.closeEnd === undefined,
|
|
163
|
+
);
|
|
164
|
+
if (unclosed) {
|
|
165
|
+
throw new Error(`Variant HTML has an unclosed <${unclosed.tag}> element.`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function validateNodeRewriteVariant(
|
|
170
|
+
variant: NodeRewriteVariant,
|
|
171
|
+
source: CodeLayerSource,
|
|
172
|
+
): NodeRewriteVariant {
|
|
173
|
+
const html = variant.html.trim();
|
|
174
|
+
assertSingleElementFragment(html, source);
|
|
175
|
+
return { html, summary: variant.summary.trim() };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function resolveNodeRewriteTarget(
|
|
179
|
+
content: string,
|
|
180
|
+
target: NodeRewriteTarget,
|
|
181
|
+
source: CodeLayerSource,
|
|
182
|
+
): CodeLayerNode {
|
|
183
|
+
const { resolution } = resolveCodeLayerTarget(content, target, { source });
|
|
184
|
+
if (resolution.status !== "resolved" || !resolution.node?.source) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Target missing — re-anchor the selection. ${resolution.message ?? "The selected node no longer exists."}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return resolution.node;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function spliceNodeRewriteVariant(args: {
|
|
193
|
+
content: string;
|
|
194
|
+
target: NodeRewriteTarget;
|
|
195
|
+
variant: NodeRewriteVariant;
|
|
196
|
+
source: CodeLayerSource;
|
|
197
|
+
fileType: string;
|
|
198
|
+
}): { content: string; node: CodeLayerNode; variant: NodeRewriteVariant } {
|
|
199
|
+
const node = resolveNodeRewriteTarget(args.content, args.target, args.source);
|
|
200
|
+
const variant = validateNodeRewriteVariant(args.variant, args.source);
|
|
201
|
+
const replacement = annotateScreenHtmlForPersist(variant.html, args.fileType);
|
|
202
|
+
const nextContent = `${args.content.slice(0, node.source!.start)}${replacement}${args.content.slice(node.source!.end)}`;
|
|
203
|
+
assertDesignHtmlEditIntegrity({
|
|
204
|
+
previousContent: args.content,
|
|
205
|
+
nextContent,
|
|
206
|
+
fileType: args.fileType,
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
content: nextContent,
|
|
210
|
+
node,
|
|
211
|
+
variant: { ...variant, html: replacement },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -5,22 +5,25 @@ export interface ReviewAnchorPoint {
|
|
|
5
5
|
|
|
6
6
|
export interface DesignReviewAnchor {
|
|
7
7
|
nodeId?: string;
|
|
8
|
+
selector?: string;
|
|
8
9
|
point: ReviewAnchorPoint;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export interface ResolvedReviewAnchor {
|
|
12
13
|
anchor: DesignReviewAnchor;
|
|
13
14
|
point: ReviewAnchorPoint;
|
|
14
|
-
source: "node" | "point";
|
|
15
|
+
source: "node" | "selector" | "point";
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function createElementReviewAnchor(input: {
|
|
18
19
|
nodeId?: string | null;
|
|
20
|
+
selector?: string | null;
|
|
19
21
|
rect?: { x: number; y: number; width: number; height: number } | null;
|
|
20
22
|
viewportWidth?: number | null;
|
|
21
23
|
viewportHeight?: number | null;
|
|
22
24
|
}): DesignReviewAnchor | null {
|
|
23
25
|
const nodeId = input.nodeId?.trim() ?? "";
|
|
26
|
+
const selector = input.selector?.trim() ?? "";
|
|
24
27
|
const rect = input.rect;
|
|
25
28
|
const hasRect = Boolean(
|
|
26
29
|
rect &&
|
|
@@ -37,7 +40,7 @@ export function createElementReviewAnchor(input: {
|
|
|
37
40
|
Number.isFinite(input.viewportHeight) &&
|
|
38
41
|
input.viewportHeight > 0,
|
|
39
42
|
);
|
|
40
|
-
if (!nodeId && !hasRect) return null;
|
|
43
|
+
if (!nodeId && !selector && !hasRect) return null;
|
|
41
44
|
|
|
42
45
|
const point = hasRect
|
|
43
46
|
? {
|
|
@@ -58,6 +61,7 @@ export function createElementReviewAnchor(input: {
|
|
|
58
61
|
|
|
59
62
|
return {
|
|
60
63
|
...(nodeId ? { nodeId } : {}),
|
|
64
|
+
...(selector ? { selector } : {}),
|
|
61
65
|
point,
|
|
62
66
|
};
|
|
63
67
|
}
|
|
@@ -100,16 +104,21 @@ export function parseReviewAnchor(value: unknown): DesignReviewAnchor | null {
|
|
|
100
104
|
if (xPct === null || yPct === null) return null;
|
|
101
105
|
|
|
102
106
|
const nodeId = typeof record.nodeId === "string" ? record.nodeId.trim() : "";
|
|
107
|
+
const selector =
|
|
108
|
+
typeof record.selector === "string" ? record.selector.trim() : "";
|
|
103
109
|
return {
|
|
104
110
|
...(nodeId ? { nodeId } : {}),
|
|
111
|
+
...(selector ? { selector } : {}),
|
|
105
112
|
point: { xPct, yPct },
|
|
106
113
|
};
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
/** Resolve a node
|
|
116
|
+
/** Resolve a node or selector position first, then degrade to the click point. */
|
|
110
117
|
export function resolveReviewAnchor(
|
|
111
118
|
value: unknown,
|
|
112
119
|
resolveNodePoint: (nodeId: string) => ReviewAnchorPoint | null,
|
|
120
|
+
resolveSelectorPoint: (selector: string) => ReviewAnchorPoint | null = () =>
|
|
121
|
+
null,
|
|
113
122
|
): ResolvedReviewAnchor | null {
|
|
114
123
|
const anchor = parseReviewAnchor(value);
|
|
115
124
|
if (!anchor) return null;
|
|
@@ -127,5 +136,19 @@ export function resolveReviewAnchor(
|
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
}
|
|
139
|
+
if (anchor.selector) {
|
|
140
|
+
const selectorPoint = resolveSelectorPoint(anchor.selector);
|
|
141
|
+
if (selectorPoint) {
|
|
142
|
+
const xPct = inBoundsPercentage(selectorPoint.xPct);
|
|
143
|
+
const yPct = inBoundsPercentage(selectorPoint.yPct);
|
|
144
|
+
if (xPct !== null && yPct !== null) {
|
|
145
|
+
return {
|
|
146
|
+
anchor,
|
|
147
|
+
point: { xPct, yPct },
|
|
148
|
+
source: "selector",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
130
153
|
return { anchor, point: anchor.point, source: "point" };
|
|
131
154
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface A2AToolResultSummary {
|
|
2
2
|
tool: string;
|
|
3
3
|
result: string;
|
|
4
|
+
isError?: boolean;
|
|
5
|
+
completedSideEffect?: boolean;
|
|
4
6
|
}
|
|
5
7
|
export interface A2AArtifactResponseOptions {
|
|
6
8
|
baseUrl?: string;
|
|
@@ -23,4 +25,11 @@ export declare function stripA2APersistedArtifactMarkers(text: string): string;
|
|
|
23
25
|
export declare function extractA2AArtifactIdentities(results: A2AToolResultSummary[]): A2AArtifactIdentity[];
|
|
24
26
|
export declare function appendA2AArtifactLinks(responseText: string, toolResults: A2AToolResultSummary[], options?: A2AArtifactResponseOptions): string;
|
|
25
27
|
export declare function buildA2ARecoverableArtifactMessage(toolResults: A2AToolResultSummary[], options?: A2AArtifactResponseOptions): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Build a bounded participant-facing receipt from authenticated artifact writes.
|
|
30
|
+
* Unlike generic artifact recovery, this only trusts identities extracted from
|
|
31
|
+
* successful write actions (or a signed downstream write ledger), so a read or
|
|
32
|
+
* an unverified URL cannot be rounded up to a successful mutation.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildA2AVerifiedMutationReceipt(toolResults: A2AToolResultSummary[], options?: A2AArtifactResponseOptions): string | null;
|
|
26
35
|
//# sourceMappingURL=artifact-response.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifact-response.d.ts","sourceRoot":"","sources":["../../src/a2a/artifact-response.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"artifact-response.d.ts","sourceRoot":"","sources":["../../src/a2a/artifact-response.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,8BAA8B,CAAC,EAAE,OAAO,CAAC;CAC1C;AAED,MAAM,WAAW,mBAAmB;IAClC,YAAY,EACR,UAAU,GACV,MAAM,GACN,WAAW,GACX,UAAU,GACV,OAAO,GACP,QAAQ,GACR,SAAS,GACT,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAiGD,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErE;AA4uBD;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,oBAAoB,EAAE,GAC9B,mBAAmB,EAAE,CAqGvB;AA2YD,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,oBAAoB,EAAE,EACnC,OAAO,GAAE,0BAA+B,GACvC,MAAM,CA8IR;AAED,wBAAgB,kCAAkC,CAChD,WAAW,EAAE,oBAAoB,EAAE,EACnC,OAAO,GAAE,0BAA+B,GACvC,MAAM,GAAG,IAAI,CA8Bf;AAmCD;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,WAAW,EAAE,oBAAoB,EAAE,EACnC,OAAO,GAAE,0BAA+B,GACvC,MAAM,GAAG,IAAI,CAqBf"}
|
|
@@ -6,6 +6,7 @@ const ARTIFACT_IDENTITY_WRITE_TOOLS = new Set([
|
|
|
6
6
|
"add-database-item",
|
|
7
7
|
"create-document",
|
|
8
8
|
"update-document",
|
|
9
|
+
"set-document-property",
|
|
9
10
|
"create-deck",
|
|
10
11
|
"duplicate-deck",
|
|
11
12
|
"add-slide",
|
|
@@ -367,6 +368,8 @@ function collectArtifacts(results) {
|
|
|
367
368
|
const monitors = new Map();
|
|
368
369
|
const forms = new Map();
|
|
369
370
|
for (const toolResult of results) {
|
|
371
|
+
if (toolResult.isError === true || toolResult.completedSideEffect === false)
|
|
372
|
+
continue;
|
|
370
373
|
if (toolResult.tool === "call-agent") {
|
|
371
374
|
for (const artifact of parseDownstreamArtifactBlock(toolResult.result)) {
|
|
372
375
|
if (artifact.kind === "deck") {
|
|
@@ -452,6 +455,8 @@ function collectArtifacts(results) {
|
|
|
452
455
|
}
|
|
453
456
|
if (toolResult.tool === "create-document" ||
|
|
454
457
|
toolResult.tool === "update-document") {
|
|
458
|
+
if (parsed.conflict === true)
|
|
459
|
+
continue;
|
|
455
460
|
const id = stringValue(parsed.id);
|
|
456
461
|
if (id) {
|
|
457
462
|
documents.set(id, {
|
|
@@ -462,6 +467,16 @@ function collectArtifacts(results) {
|
|
|
462
467
|
}
|
|
463
468
|
continue;
|
|
464
469
|
}
|
|
470
|
+
if (toolResult.tool === "set-document-property") {
|
|
471
|
+
const id = stringValue(parsed.documentId);
|
|
472
|
+
if (id) {
|
|
473
|
+
documents.set(id, {
|
|
474
|
+
id,
|
|
475
|
+
url: stringValue(parsed.url) ?? stringValue(parsed.urlPath),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
465
480
|
if (toolResult.tool === "get-document" ||
|
|
466
481
|
toolResult.tool === "get-content-document") {
|
|
467
482
|
const document = asRecord(parsed.document);
|
|
@@ -658,6 +673,8 @@ export function extractA2AArtifactIdentities(results) {
|
|
|
658
673
|
identities.set(`${identity.resourceType}:${identity.id}`, identity);
|
|
659
674
|
};
|
|
660
675
|
for (const result of results) {
|
|
676
|
+
if (result.isError === true || result.completedSideEffect === false)
|
|
677
|
+
continue;
|
|
661
678
|
if (result.tool === "call-agent") {
|
|
662
679
|
for (const identity of persistedArtifactIdentitiesFromMarker(result.result)) {
|
|
663
680
|
remember({ ...identity, sourceAction: "call-agent" });
|
|
@@ -1153,4 +1170,55 @@ export function buildA2ARecoverableArtifactMessage(toolResults, options = {}) {
|
|
|
1153
1170
|
...lines,
|
|
1154
1171
|
].join("\n");
|
|
1155
1172
|
}
|
|
1173
|
+
function mutationReceiptUrl(identity, baseUrl) {
|
|
1174
|
+
if (identity.sourceAction === "call-agent" &&
|
|
1175
|
+
(!identity.url || identity.url.startsWith("/"))) {
|
|
1176
|
+
return undefined;
|
|
1177
|
+
}
|
|
1178
|
+
if (identity.url) {
|
|
1179
|
+
return identity.url.startsWith("/")
|
|
1180
|
+
? artifactUrl(baseUrl, identity.url)
|
|
1181
|
+
: identity.url;
|
|
1182
|
+
}
|
|
1183
|
+
const path = identity.resourceType === "document"
|
|
1184
|
+
? `/page/${identity.id}`
|
|
1185
|
+
: identity.resourceType === "deck"
|
|
1186
|
+
? `/deck/${identity.id}`
|
|
1187
|
+
: identity.resourceType === "dashboard"
|
|
1188
|
+
? `/adhoc/${identity.id}`
|
|
1189
|
+
: identity.resourceType === "analysis"
|
|
1190
|
+
? `/analyses/${identity.id}`
|
|
1191
|
+
: identity.resourceType === "image"
|
|
1192
|
+
? `/image/${identity.id}`
|
|
1193
|
+
: identity.resourceType === "design"
|
|
1194
|
+
? `/design/${identity.id}`
|
|
1195
|
+
: undefined;
|
|
1196
|
+
return path ? artifactUrl(baseUrl, path) : undefined;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Build a bounded participant-facing receipt from authenticated artifact writes.
|
|
1200
|
+
* Unlike generic artifact recovery, this only trusts identities extracted from
|
|
1201
|
+
* successful write actions (or a signed downstream write ledger), so a read or
|
|
1202
|
+
* an unverified URL cannot be rounded up to a successful mutation.
|
|
1203
|
+
*/
|
|
1204
|
+
export function buildA2AVerifiedMutationReceipt(toolResults, options = {}) {
|
|
1205
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
1206
|
+
const identities = extractA2AArtifactIdentities(toolResults);
|
|
1207
|
+
if (identities.length === 0)
|
|
1208
|
+
return null;
|
|
1209
|
+
const lines = identities.map((identity) => {
|
|
1210
|
+
const label = identity.resourceType.charAt(0).toUpperCase() +
|
|
1211
|
+
identity.resourceType.slice(1);
|
|
1212
|
+
const url = mutationReceiptUrl(identity, baseUrl);
|
|
1213
|
+
return url
|
|
1214
|
+
? `- ${label}: ${url} (ID: ${identity.id})`
|
|
1215
|
+
: `- ${label} ID: ${identity.id}`;
|
|
1216
|
+
});
|
|
1217
|
+
return [
|
|
1218
|
+
"A verified change was saved, but I couldn't generate the detailed summary.",
|
|
1219
|
+
"",
|
|
1220
|
+
"Saved artifacts:",
|
|
1221
|
+
...lines,
|
|
1222
|
+
].join("\n");
|
|
1223
|
+
}
|
|
1156
1224
|
//# sourceMappingURL=artifact-response.js.map
|