@agent-native/core 0.106.0 → 0.106.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/application-state/index.ts +2 -0
- package/corpus/core/src/application-state/script-helpers.ts +12 -0
- package/corpus/core/src/application-state/store.ts +38 -0
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +10 -3
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +22 -0
- package/corpus/templates/design/AGENTS.md +16 -0
- package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +54 -0
- package/corpus/templates/design/actions/propose-node-rewrite.ts +290 -0
- package/corpus/templates/design/actions/resolve-node-rewrite.ts +257 -0
- package/corpus/templates/design/actions/view-screen.ts +98 -2
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +71 -1
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +19 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +33 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +194 -0
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +21 -4
- package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +18 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
- package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +638 -0
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +535 -54
- package/corpus/templates/design/app/components/visual-editor/index.ts +5 -1
- package/corpus/templates/design/app/components/visual-editor/review-canvas-state.ts +21 -3
- package/corpus/templates/design/app/i18n/ar-SA.ts +35 -0
- package/corpus/templates/design/app/i18n/de-DE.ts +35 -0
- package/corpus/templates/design/app/i18n/es-ES.ts +35 -0
- package/corpus/templates/design/app/i18n/fr-FR.ts +35 -0
- package/corpus/templates/design/app/i18n/hi-IN.ts +35 -0
- package/corpus/templates/design/app/i18n/ja-JP.ts +35 -0
- package/corpus/templates/design/app/i18n/ko-KR.ts +35 -0
- package/corpus/templates/design/app/i18n/pt-BR.ts +35 -0
- package/corpus/templates/design/app/i18n/zh-CN.ts +35 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +32 -0
- package/corpus/templates/design/app/i18n-data.ts +32 -0
- package/corpus/templates/design/app/lib/node-reprompt.ts +110 -0
- package/corpus/templates/design/changelog/2026-07-16-design-editing-and-review-are-now-more-predictable.md +6 -0
- package/corpus/templates/design/server/lib/reprompt-action-guard.ts +107 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +9 -1
- package/corpus/templates/design/shared/code-layer.ts +15 -0
- package/corpus/templates/design/shared/node-rewrite.ts +213 -0
- package/corpus/templates/design/shared/review-anchor.ts +26 -3
- package/dist/application-state/index.d.ts +2 -2
- package/dist/application-state/index.d.ts.map +1 -1
- package/dist/application-state/index.js +2 -2
- package/dist/application-state/index.js.map +1 -1
- package/dist/application-state/script-helpers.d.ts +1 -0
- package/dist/application-state/script-helpers.d.ts.map +1 -1
- package/dist/application-state/script-helpers.js +7 -1
- package/dist/application-state/script-helpers.js.map +1 -1
- package/dist/application-state/store.d.ts +1 -0
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +28 -0
- package/dist/application-state/store.js.map +1 -1
- package/dist/client/review/ReviewCommentComposer.d.ts +4 -1
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
- package/dist/client/review/ReviewCommentComposer.js +3 -3
- package/dist/client/review/ReviewCommentComposer.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,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,5 +1,5 @@
|
|
|
1
|
-
export { appStateGet, appStateGetMany, appStatePut, appStateDelete, appStateList, appStateDeleteByPrefix, } from "./store.js";
|
|
1
|
+
export { appStateGet, appStateGetMany, appStatePut, appStateDelete, appStateCompareAndSet, appStateList, appStateDeleteByPrefix, } from "./store.js";
|
|
2
2
|
export { getAppStateEmitter, emitAppStateChange, emitAppStateDelete, type AppStateEvent, } from "./emitter.js";
|
|
3
3
|
export { getState, putState, deleteState, listComposeDrafts, getComposeDraft, putComposeDraft, deleteComposeDraft, deleteAllComposeDrafts, } from "./handlers.js";
|
|
4
|
-
export { readAppState, writeAppState, deleteAppState, listAppState, deleteAppStateByPrefix, readAppStateForCurrentTab, writeAppStateForCurrentTab, appStateKeyForBrowserTab, getCurrentRequestBrowserTabId, } from "./script-helpers.js";
|
|
4
|
+
export { readAppState, writeAppState, deleteAppState, compareAndSetAppState, listAppState, deleteAppStateByPrefix, readAppStateForCurrentTab, writeAppStateForCurrentTab, appStateKeyForBrowserTab, getCurrentRequestBrowserTabId, } from "./script-helpers.js";
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/application-state/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,eAAe,EACf,WAAW,EACX,cAAc,EACd,YAAY,EACZ,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,YAAY,EACZ,sBAAsB,EACtB,yBAAyB,EACzB,0BAA0B,EAC1B,wBAAwB,EACxB,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/application-state/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,eAAe,EACf,WAAW,EACX,cAAc,EACd,qBAAqB,EACrB,YAAY,EACZ,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,YAAY,EACZ,sBAAsB,EACtB,yBAAyB,EACzB,0BAA0B,EAC1B,wBAAwB,EACxB,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Store
|
|
2
|
-
export { appStateGet, appStateGetMany, appStatePut, appStateDelete, appStateList, appStateDeleteByPrefix, } from "./store.js";
|
|
2
|
+
export { appStateGet, appStateGetMany, appStatePut, appStateDelete, appStateCompareAndSet, appStateList, appStateDeleteByPrefix, } from "./store.js";
|
|
3
3
|
// Emitter (for SSE wiring)
|
|
4
4
|
export { getAppStateEmitter, emitAppStateChange, emitAppStateDelete, } from "./emitter.js";
|
|
5
5
|
// H3 route handlers (for templates)
|
|
6
6
|
export { getState, putState, deleteState, listComposeDrafts, getComposeDraft, putComposeDraft, deleteComposeDraft, deleteAllComposeDrafts, } from "./handlers.js";
|
|
7
7
|
// Script helpers
|
|
8
|
-
export { readAppState, writeAppState, deleteAppState, listAppState, deleteAppStateByPrefix, readAppStateForCurrentTab, writeAppStateForCurrentTab, appStateKeyForBrowserTab, getCurrentRequestBrowserTabId, } from "./script-helpers.js";
|
|
8
|
+
export { readAppState, writeAppState, deleteAppState, compareAndSetAppState, listAppState, deleteAppStateByPrefix, readAppStateForCurrentTab, writeAppStateForCurrentTab, appStateKeyForBrowserTab, getCurrentRequestBrowserTabId, } from "./script-helpers.js";
|
|
9
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/application-state/index.ts"],"names":[],"mappings":"AAAA,QAAQ;AACR,OAAO,EACL,WAAW,EACX,eAAe,EACf,WAAW,EACX,cAAc,EACd,YAAY,EACZ,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAEpB,2BAA2B;AAC3B,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GAEnB,MAAM,cAAc,CAAC;AAEtB,oCAAoC;AACpC,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAEvB,iBAAiB;AACjB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,YAAY,EACZ,sBAAsB,EACtB,yBAAyB,EACzB,0BAA0B,EAC1B,wBAAwB,EACxB,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC","sourcesContent":["// Store\nexport {\n appStateGet,\n appStateGetMany,\n appStatePut,\n appStateDelete,\n appStateList,\n appStateDeleteByPrefix,\n} from \"./store.js\";\n\n// Emitter (for SSE wiring)\nexport {\n getAppStateEmitter,\n emitAppStateChange,\n emitAppStateDelete,\n type AppStateEvent,\n} from \"./emitter.js\";\n\n// H3 route handlers (for templates)\nexport {\n getState,\n putState,\n deleteState,\n listComposeDrafts,\n getComposeDraft,\n putComposeDraft,\n deleteComposeDraft,\n deleteAllComposeDrafts,\n} from \"./handlers.js\";\n\n// Script helpers\nexport {\n readAppState,\n writeAppState,\n deleteAppState,\n listAppState,\n deleteAppStateByPrefix,\n readAppStateForCurrentTab,\n writeAppStateForCurrentTab,\n appStateKeyForBrowserTab,\n getCurrentRequestBrowserTabId,\n} from \"./script-helpers.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/application-state/index.ts"],"names":[],"mappings":"AAAA,QAAQ;AACR,OAAO,EACL,WAAW,EACX,eAAe,EACf,WAAW,EACX,cAAc,EACd,qBAAqB,EACrB,YAAY,EACZ,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAEpB,2BAA2B;AAC3B,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GAEnB,MAAM,cAAc,CAAC;AAEtB,oCAAoC;AACpC,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAEvB,iBAAiB;AACjB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,YAAY,EACZ,sBAAsB,EACtB,yBAAyB,EACzB,0BAA0B,EAC1B,wBAAwB,EACxB,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC","sourcesContent":["// Store\nexport {\n appStateGet,\n appStateGetMany,\n appStatePut,\n appStateDelete,\n appStateCompareAndSet,\n appStateList,\n appStateDeleteByPrefix,\n} from \"./store.js\";\n\n// Emitter (for SSE wiring)\nexport {\n getAppStateEmitter,\n emitAppStateChange,\n emitAppStateDelete,\n type AppStateEvent,\n} from \"./emitter.js\";\n\n// H3 route handlers (for templates)\nexport {\n getState,\n putState,\n deleteState,\n listComposeDrafts,\n getComposeDraft,\n putComposeDraft,\n deleteComposeDraft,\n deleteAllComposeDrafts,\n} from \"./handlers.js\";\n\n// Script helpers\nexport {\n readAppState,\n writeAppState,\n deleteAppState,\n compareAndSetAppState,\n listAppState,\n deleteAppStateByPrefix,\n readAppStateForCurrentTab,\n writeAppStateForCurrentTab,\n appStateKeyForBrowserTab,\n getCurrentRequestBrowserTabId,\n} from \"./script-helpers.js\";\n"]}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
export declare function readAppState(key: string): Promise<Record<string, unknown> | null>;
|
|
15
15
|
export declare function writeAppState(key: string, value: Record<string, unknown>): Promise<void>;
|
|
16
16
|
export declare function deleteAppState(key: string): Promise<boolean>;
|
|
17
|
+
export declare function compareAndSetAppState(key: string, expectedValue: Record<string, unknown>, nextValue: Record<string, unknown> | null): Promise<boolean>;
|
|
17
18
|
export declare function listAppState(prefix: string): Promise<Array<{
|
|
18
19
|
key: string;
|
|
19
20
|
value: Record<string, unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"script-helpers.d.ts","sourceRoot":"","sources":["../../src/application-state/script-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"script-helpers.d.ts","sourceRoot":"","sources":["../../src/application-state/script-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAsCH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAGzC;AAED,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAKlE;AAED,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GACxC,OAAO,CAAC,OAAO,CAAC,CAKlB;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAAC,CAGjE;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK5E;AAUD;;;;GAIG;AACH,wBAAgB,6BAA6B,IAAI,MAAM,GAAG,IAAI,CAM7D;AAED,uEAAuE;AACvE,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,OAAO,GACpB,MAAM,CAGR;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IAAE,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAE,GACvC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAQzC;AAED,oEAAoE;AACpE,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,IAAI,CAAC,CAGf"}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* context) should fall through to the env var.
|
|
13
13
|
*/
|
|
14
14
|
import { getRequestRunContext } from "../server/request-context.js";
|
|
15
|
-
import { appStateGet, appStatePut, appStateDelete, appStateList, appStateDeleteByPrefix, } from "./store.js";
|
|
15
|
+
import { appStateGet, appStatePut, appStateDelete, appStateCompareAndSet, appStateList, appStateDeleteByPrefix, } from "./store.js";
|
|
16
16
|
/**
|
|
17
17
|
* Resolve session ID for the current caller.
|
|
18
18
|
*
|
|
@@ -52,6 +52,12 @@ export async function deleteAppState(key) {
|
|
|
52
52
|
requestSource: "agent",
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
+
export async function compareAndSetAppState(key, expectedValue, nextValue) {
|
|
56
|
+
const sessionId = await resolveSessionId();
|
|
57
|
+
return appStateCompareAndSet(sessionId, key, expectedValue, nextValue, {
|
|
58
|
+
requestSource: "agent",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
55
61
|
export async function listAppState(prefix) {
|
|
56
62
|
const sessionId = await resolveSessionId();
|
|
57
63
|
return appStateList(sessionId, prefix);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"script-helpers.js","sourceRoot":"","sources":["../../src/application-state/script-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EACL,WAAW,EACX,WAAW,EACX,cAAc,EACd,YAAY,EACZ,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAEpB;;;;;;;GAOG;AACH,KAAK,UAAU,gBAAgB;IAC7B,IAAI,CAAC;QACH,MAAM,EAAE,mBAAmB,EAAE,GAC3B,MAAM,MAAM,CAAC,8BAA8B,CAAC,CAAC;QAC/C,MAAM,QAAQ,GAAG,mBAAmB,EAAE,CAAC;QACvC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,0DAA0D;IAC5D,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAC3C,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC;IAExB,MAAM,IAAI,KAAK,CACb,gGAAgG,CACjG,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAW;IAEX,MAAM,SAAS,GAAG,MAAM,gBAAgB,EAAE,CAAC;IAC3C,OAAO,WAAW,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,GAAW,EACX,KAA8B;IAE9B,MAAM,SAAS,GAAG,MAAM,gBAAgB,EAAE,CAAC;IAC3C,OAAO,WAAW,CAAC,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE;QACxC,aAAa,EAAE,OAAO;KACvB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAW;IAC9C,MAAM,SAAS,GAAG,MAAM,gBAAgB,EAAE,CAAC;IAC3C,OAAO,cAAc,CAAC,SAAS,EAAE,GAAG,EAAE;QACpC,aAAa,EAAE,OAAO;KACvB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAAc;IAEd,MAAM,SAAS,GAAG,MAAM,gBAAgB,EAAE,CAAC;IAC3C,OAAO,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,MAAc;IACzD,MAAM,SAAS,GAAG,MAAM,gBAAgB,EAAE,CAAC;IAC3C,OAAO,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE;QAC/C,aAAa,EAAE,OAAO;KACvB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,cAAc,GAAG,uBAAuB,CAAC;AAE/C,SAAS,qBAAqB,CAAC,KAAc;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B;IAC3C,IAAI,CAAC;QACH,OAAO,qBAAqB,CAAC,oBAAoB,EAAE,EAAE,YAAY,CAAC,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,wBAAwB,CACtC,GAAW,EACX,YAAqB;IAErB,MAAM,UAAU,GAAG,qBAAqB,CAAC,YAAY,CAAC,CAAC;IACvD,OAAO,UAAU,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;AACnD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,GAAW,EACX,OAAwC;IAExC,MAAM,MAAM,GAAG,wBAAwB,CAAC,GAAG,EAAE,6BAA6B,EAAE,CAAC,CAAC;IAC9E,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QAC5D,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAC1B,IAAI,OAAO,EAAE,gBAAgB,KAAK,KAAK;YAAE,OAAO,IAAI,CAAC;IACvD,CAAC;IACD,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED,oEAAoE;AACpE,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,GAAW,EACX,KAA8B;IAE9B,MAAM,MAAM,GAAG,wBAAwB,CAAC,GAAG,EAAE,6BAA6B,EAAE,CAAC,CAAC;IAC9E,OAAO,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AACtC,CAAC","sourcesContent":["/**\n * Application state helpers for use in scripts and actions.\n *\n * The session ID determines which user's application state is read/written.\n * Resolution order:\n * 1. Per-request context (AsyncLocalStorage) — set by the HTTP handler\n * 2. AGENT_USER_EMAIL env var — CLI scripts only\n *\n * The per-request context is critical in multi-user deployments: the env var\n * is process-global and gets overwritten by concurrent requests, so it cannot\n * reliably identify the caller. Only CLI scripts (single-user, no HTTP\n * context) should fall through to the env var.\n */\n\nimport { getRequestRunContext } from \"../server/request-context.js\";\nimport {\n appStateGet,\n appStatePut,\n appStateDelete,\n appStateList,\n appStateDeleteByPrefix,\n} from \"./store.js\";\n\n/**\n * Resolve session ID for the current caller.\n *\n * In an HTTP/action context, uses the per-request user email from\n * AsyncLocalStorage so concurrent users don't collide. In a CLI context\n * (no request), falls back to AGENT_USER_EMAIL. Throws when neither is\n * present — application state must be scoped to a real identity.\n */\nasync function resolveSessionId(): Promise<string> {\n try {\n const { getRequestUserEmail } =\n await import(\"../server/request-context.js\");\n const ctxEmail = getRequestUserEmail();\n if (ctxEmail) return ctxEmail;\n } catch {\n // request-context not available — fall through to env var\n }\n\n const email = process.env.AGENT_USER_EMAIL;\n if (email) return email;\n\n throw new Error(\n \"Application state access requires an authenticated request context or AGENT_USER_EMAIL env var\",\n );\n}\n\nexport async function readAppState(\n key: string,\n): Promise<Record<string, unknown> | null> {\n const sessionId = await resolveSessionId();\n return appStateGet(sessionId, key);\n}\n\nexport async function writeAppState(\n key: string,\n value: Record<string, unknown>,\n): Promise<void> {\n const sessionId = await resolveSessionId();\n return appStatePut(sessionId, key, value, {\n requestSource: \"agent\",\n });\n}\n\nexport async function deleteAppState(key: string): Promise<boolean> {\n const sessionId = await resolveSessionId();\n return appStateDelete(sessionId, key, {\n requestSource: \"agent\",\n });\n}\n\nexport async function listAppState(\n prefix: string,\n): Promise<Array<{ key: string; value: Record<string, unknown> }>> {\n const sessionId = await resolveSessionId();\n return appStateList(sessionId, prefix);\n}\n\nexport async function deleteAppStateByPrefix(prefix: string): Promise<number> {\n const sessionId = await resolveSessionId();\n return appStateDeleteByPrefix(sessionId, prefix, {\n requestSource: \"agent\",\n });\n}\n\nconst SAFE_TAB_ID_RE = /^[A-Za-z0-9_-]{1,96}$/;\n\nfunction normalizeBrowserTabId(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return SAFE_TAB_ID_RE.test(trimmed) ? trimmed : null;\n}\n\n/**\n * Browser tab id for the current request, if the client sent one. Used to\n * scope ambient UI state (navigation, selection, etc.) so a chat from one tab\n * reads that tab's state instead of whichever tab wrote the global key last.\n */\nexport function getCurrentRequestBrowserTabId(): string | null {\n try {\n return normalizeBrowserTabId(getRequestRunContext()?.browserTabId);\n } catch {\n return null;\n }\n}\n\n/** `key:<tabId>` when a browser tab id is present, otherwise `key`. */\nexport function appStateKeyForBrowserTab(\n key: string,\n browserTabId: unknown,\n): string {\n const normalized = normalizeBrowserTabId(browserTabId);\n return normalized ? `${key}:${normalized}` : key;\n}\n\n/**\n * Read application state scoped to the requesting browser tab. Reads the\n * tab-scoped key first and (by default) falls back to the global key so\n * CLI/external agents and pre-scoping clients keep working.\n */\nexport async function readAppStateForCurrentTab(\n key: string,\n options?: { fallbackToGlobal?: boolean },\n): Promise<Record<string, unknown> | null> {\n const tabKey = appStateKeyForBrowserTab(key, getCurrentRequestBrowserTabId());\n if (tabKey !== key) {\n const scoped = await readAppState(tabKey).catch(() => null);\n if (scoped) return scoped;\n if (options?.fallbackToGlobal === false) return null;\n }\n return readAppState(key);\n}\n\n/** Write application state scoped to the requesting browser tab. */\nexport async function writeAppStateForCurrentTab(\n key: string,\n value: Record<string, unknown>,\n): Promise<void> {\n const tabKey = appStateKeyForBrowserTab(key, getCurrentRequestBrowserTabId());\n return writeAppState(tabKey, value);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"script-helpers.js","sourceRoot":"","sources":["../../src/application-state/script-helpers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Application state helpers for use in scripts and actions.\n *\n * The session ID determines which user's application state is read/written.\n * Resolution order:\n * 1. Per-request context (AsyncLocalStorage) — set by the HTTP handler\n * 2. AGENT_USER_EMAIL env var — CLI scripts only\n *\n * The per-request context is critical in multi-user deployments: the env var\n * is process-global and gets overwritten by concurrent requests, so it cannot\n * reliably identify the caller. Only CLI scripts (single-user, no HTTP\n * context) should fall through to the env var.\n */\n\nimport { getRequestRunContext } from \"../server/request-context.js\";\nimport {\n appStateGet,\n appStatePut,\n appStateDelete,\n appStateCompareAndSet,\n appStateList,\n appStateDeleteByPrefix,\n} from \"./store.js\";\n\n/**\n * Resolve session ID for the current caller.\n *\n * In an HTTP/action context, uses the per-request user email from\n * AsyncLocalStorage so concurrent users don't collide. In a CLI context\n * (no request), falls back to AGENT_USER_EMAIL. Throws when neither is\n * present — application state must be scoped to a real identity.\n */\nasync function resolveSessionId(): Promise<string> {\n try {\n const { getRequestUserEmail } =\n await import(\"../server/request-context.js\");\n const ctxEmail = getRequestUserEmail();\n if (ctxEmail) return ctxEmail;\n } catch {\n // request-context not available — fall through to env var\n }\n\n const email = process.env.AGENT_USER_EMAIL;\n if (email) return email;\n\n throw new Error(\n \"Application state access requires an authenticated request context or AGENT_USER_EMAIL env var\",\n );\n}\n\nexport async function readAppState(\n key: string,\n): Promise<Record<string, unknown> | null> {\n const sessionId = await resolveSessionId();\n return appStateGet(sessionId, key);\n}\n\nexport async function writeAppState(\n key: string,\n value: Record<string, unknown>,\n): Promise<void> {\n const sessionId = await resolveSessionId();\n return appStatePut(sessionId, key, value, {\n requestSource: \"agent\",\n });\n}\n\nexport async function deleteAppState(key: string): Promise<boolean> {\n const sessionId = await resolveSessionId();\n return appStateDelete(sessionId, key, {\n requestSource: \"agent\",\n });\n}\n\nexport async function compareAndSetAppState(\n key: string,\n expectedValue: Record<string, unknown>,\n nextValue: Record<string, unknown> | null,\n): Promise<boolean> {\n const sessionId = await resolveSessionId();\n return appStateCompareAndSet(sessionId, key, expectedValue, nextValue, {\n requestSource: \"agent\",\n });\n}\n\nexport async function listAppState(\n prefix: string,\n): Promise<Array<{ key: string; value: Record<string, unknown> }>> {\n const sessionId = await resolveSessionId();\n return appStateList(sessionId, prefix);\n}\n\nexport async function deleteAppStateByPrefix(prefix: string): Promise<number> {\n const sessionId = await resolveSessionId();\n return appStateDeleteByPrefix(sessionId, prefix, {\n requestSource: \"agent\",\n });\n}\n\nconst SAFE_TAB_ID_RE = /^[A-Za-z0-9_-]{1,96}$/;\n\nfunction normalizeBrowserTabId(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return SAFE_TAB_ID_RE.test(trimmed) ? trimmed : null;\n}\n\n/**\n * Browser tab id for the current request, if the client sent one. Used to\n * scope ambient UI state (navigation, selection, etc.) so a chat from one tab\n * reads that tab's state instead of whichever tab wrote the global key last.\n */\nexport function getCurrentRequestBrowserTabId(): string | null {\n try {\n return normalizeBrowserTabId(getRequestRunContext()?.browserTabId);\n } catch {\n return null;\n }\n}\n\n/** `key:<tabId>` when a browser tab id is present, otherwise `key`. */\nexport function appStateKeyForBrowserTab(\n key: string,\n browserTabId: unknown,\n): string {\n const normalized = normalizeBrowserTabId(browserTabId);\n return normalized ? `${key}:${normalized}` : key;\n}\n\n/**\n * Read application state scoped to the requesting browser tab. Reads the\n * tab-scoped key first and (by default) falls back to the global key so\n * CLI/external agents and pre-scoping clients keep working.\n */\nexport async function readAppStateForCurrentTab(\n key: string,\n options?: { fallbackToGlobal?: boolean },\n): Promise<Record<string, unknown> | null> {\n const tabKey = appStateKeyForBrowserTab(key, getCurrentRequestBrowserTabId());\n if (tabKey !== key) {\n const scoped = await readAppState(tabKey).catch(() => null);\n if (scoped) return scoped;\n if (options?.fallbackToGlobal === false) return null;\n }\n return readAppState(key);\n}\n\n/** Write application state scoped to the requesting browser tab. */\nexport async function writeAppStateForCurrentTab(\n key: string,\n value: Record<string, unknown>,\n): Promise<void> {\n const tabKey = appStateKeyForBrowserTab(key, getCurrentRequestBrowserTabId());\n return writeAppState(tabKey, value);\n}\n"]}
|
|
@@ -8,6 +8,7 @@ export declare function appStateGet(sessionId: string, key: string): Promise<Rec
|
|
|
8
8
|
export declare function appStateGetMany(sessionId: string, keys: readonly string[]): Promise<Record<string, Record<string, unknown> | null>>;
|
|
9
9
|
export declare function appStatePut(sessionId: string, key: string, value: Record<string, unknown>, options?: StoreWriteOptions): Promise<void>;
|
|
10
10
|
export declare function appStateDelete(sessionId: string, key: string, options?: StoreWriteOptions): Promise<boolean>;
|
|
11
|
+
export declare function appStateCompareAndSet(sessionId: string, key: string, expectedValue: Record<string, unknown>, nextValue: Record<string, unknown> | null, options?: StoreWriteOptions): Promise<boolean>;
|
|
11
12
|
export declare function appStateList(sessionId: string, keyPrefix: string): Promise<Array<{
|
|
12
13
|
key: string;
|
|
13
14
|
value: Record<string, unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/application-state/store.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAyF9D,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAgBzC;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,CAsBzD;AAED,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAmBf;AAED,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAAC,CAWjE;AAED,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAsBjB"}
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/application-state/store.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAyF9D,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAgBzC;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,CAsBzD;AAED,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAmBf;AAED,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACzC,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,OAAO,CAAC,CA8BlB;AAED,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAAC,CAWjE;AAED,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAsBjB"}
|
|
@@ -157,6 +157,34 @@ export async function appStateDelete(sessionId, key, options) {
|
|
|
157
157
|
emitAppStateDelete(key, options?.requestSource, sessionId);
|
|
158
158
|
return deleted;
|
|
159
159
|
}
|
|
160
|
+
export async function appStateCompareAndSet(sessionId, key, expectedValue, nextValue, options) {
|
|
161
|
+
await ensureTable();
|
|
162
|
+
const client = getDbExec();
|
|
163
|
+
const expected = JSON.stringify(expectedValue);
|
|
164
|
+
if (nextValue === null) {
|
|
165
|
+
const result = await client.execute({
|
|
166
|
+
sql: `DELETE FROM application_state WHERE session_id = ? AND key = ? AND value = ?`,
|
|
167
|
+
args: [sessionId, key, expected],
|
|
168
|
+
});
|
|
169
|
+
const changed = result.rowsAffected > 0;
|
|
170
|
+
if (changed)
|
|
171
|
+
emitAppStateDelete(key, options?.requestSource, sessionId);
|
|
172
|
+
return changed;
|
|
173
|
+
}
|
|
174
|
+
const next = JSON.stringify(nextValue);
|
|
175
|
+
if (!isLocalDatabase() &&
|
|
176
|
+
Buffer.byteLength(next, "utf8") > MAX_HOSTED_APP_STATE_VALUE_BYTES) {
|
|
177
|
+
throw new Error(`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.`);
|
|
178
|
+
}
|
|
179
|
+
const result = await client.execute({
|
|
180
|
+
sql: `UPDATE application_state SET value = ?, updated_at = ? WHERE session_id = ? AND key = ? AND value = ?`,
|
|
181
|
+
args: [next, Date.now(), sessionId, key, expected],
|
|
182
|
+
});
|
|
183
|
+
const changed = result.rowsAffected > 0;
|
|
184
|
+
if (changed)
|
|
185
|
+
emitAppStateChange(key, options?.requestSource, sessionId);
|
|
186
|
+
return changed;
|
|
187
|
+
}
|
|
160
188
|
export async function appStateList(sessionId, keyPrefix) {
|
|
161
189
|
await ensureTable();
|
|
162
190
|
const client = getDbExec();
|