@agent-native/core 0.75.2 → 0.75.5
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 +86 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +57 -25
- package/corpus/core/src/agent/production-agent.ts +16 -13
- package/corpus/core/src/cli/skills.ts +22 -4
- package/corpus/core/src/cli/sync-builder-starter-manifest.ts +36 -12
- package/corpus/core/src/deploy/build.ts +79 -57
- package/corpus/core/src/deploy/workspace-deploy.ts +62 -30
- package/corpus/core/src/server/self-dispatch.ts +29 -2
- package/corpus/templates/clips/changelog/2026-06-24-chrome-extension-setup-now-shows-a-clear-all-done-confirmati.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/permission.html +60 -0
- package/corpus/templates/clips/chrome-extension/src/permission.ts +28 -15
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -20
- package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +22 -4
- package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +22 -4
- package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +45 -33
- package/corpus/templates/plan/changelog/2026-06-24-invalid-generated-recap-blocks-now-show-a-clear-warning-with.md +6 -0
- package/corpus/templates/plan/server/plan-content.ts +53 -3
- package/dist/agent/durable-background.d.ts +33 -24
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +56 -25
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +16 -13
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/skills.d.ts +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +22 -4
- package/dist/cli/skills.js.map +1 -1
- package/dist/cli/sync-builder-starter-manifest.d.ts.map +1 -1
- package/dist/cli/sync-builder-starter-manifest.js +21 -10
- package/dist/cli/sync-builder-starter-manifest.js.map +1 -1
- package/dist/deploy/build.d.ts +53 -33
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +79 -57
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.js +62 -30
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/server/self-dispatch.d.ts +0 -10
- package/dist/server/self-dispatch.d.ts.map +1 -1
- package/dist/server/self-dispatch.js +27 -2
- package/dist/server/self-dispatch.js.map +1 -1
- package/package.json +1 -1
|
@@ -696,24 +696,30 @@ function isDurableBackgroundDeployEnabled(): boolean {
|
|
|
696
696
|
/**
|
|
697
697
|
* Emit a SECOND Netlify function for `app` whose name ends in `-background`,
|
|
698
698
|
* re-exporting the SAME `main.mjs` handler bundle. Netlify invokes any function
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
699
|
+
* with `config.background: true` asynchronously (202 immediately, up to 15-min
|
|
700
|
+
* budget), which is exactly what the durable-background chat dispatch
|
|
701
|
+
* (`fireInternalDispatch` → the function's default url) needs.
|
|
702
702
|
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
703
|
+
* DOC-CORRECT DEFAULT-URL APPROACH (mirrors the single-template emit in
|
|
704
|
+
* deploy/build.ts): the function declares NO custom `config.path`, so it keeps
|
|
705
|
+
* its DEFAULT url `/.netlify/functions/<app>-agent-background`. The `<app>-server`
|
|
706
|
+
* function's catch-all already excludes `/.netlify/*`, so that default-url
|
|
707
|
+
* namespace is NEVER shadowed by the synchronous function — no overlapping
|
|
708
|
+
* `config.path` and no catch-all patch are needed. The foreground dispatches to
|
|
709
|
+
* that default url (`resolveAgentChatProcessRunDispatchPath` resolves the per-app
|
|
710
|
+
* name from `AGENT_NATIVE_WORKSPACE_APP_ID`); `fireInternalDispatch` strips the
|
|
711
|
+
* app base path for `/.netlify/*` targets so the request reaches the host-root
|
|
712
|
+
* function url. The entry then REWRITES the incoming pathname to the
|
|
713
|
+
* base-path-prefixed `_process-run` route before delegating to the Nitro router.
|
|
707
714
|
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
* win. See packages/core/docs/design/durable-agent-runs.md.
|
|
715
|
+
* It shares the same bundle (`includedFiles: ["**"]`) so `A2A_SECRET`, the DB
|
|
716
|
+
* URL, and the rest of the env/bundle are present. A prior attempt gave the
|
|
717
|
+
* function a custom `config.path` (the framework route) that overlapped the
|
|
718
|
+
* synchronous `<app>-server` catch-all; that path was not honored as a route in
|
|
719
|
+
* prod (probe → 404). The default url is the doc-correct fix.
|
|
720
|
+
*
|
|
721
|
+
* Safety net: if the dispatch fast-fails the foreground degrades to an inline
|
|
722
|
+
* 40s synchronous run (see production-agent.ts).
|
|
717
723
|
*/
|
|
718
724
|
function emitNetlifyBackgroundFunction(
|
|
719
725
|
workspaceRoot: string,
|
|
@@ -721,7 +727,9 @@ function emitNetlifyBackgroundFunction(
|
|
|
721
727
|
srcServerDir: string,
|
|
722
728
|
workspaceApps: WorkspaceAppManifestEntry[],
|
|
723
729
|
): void {
|
|
724
|
-
// Name MUST end in `-background`
|
|
730
|
+
// Name MUST end in `-background` (Netlify async convention + the runtime guard
|
|
731
|
+
// reads the -background Lambda-name suffix as a fallback). It is reached at its
|
|
732
|
+
// DEFAULT url /.netlify/functions/<app>-agent-background.
|
|
725
733
|
const backgroundName = `${app}-agent-background`;
|
|
726
734
|
const dest = path.join(netlifyFunctionsDir(workspaceRoot), backgroundName);
|
|
727
735
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
@@ -733,14 +741,20 @@ function emitNetlifyBackgroundFunction(
|
|
|
733
741
|
workspaceApps,
|
|
734
742
|
app,
|
|
735
743
|
);
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
// processor dispatch), so the route Netlify must map to this async function
|
|
740
|
-
// is `/<app>/_agent-native/agent-chat/_process-run`.
|
|
744
|
+
// The Nitro router for this app expects the base-path-prefixed framework route.
|
|
745
|
+
// The function is reached at its default url, so the entry rewrites the
|
|
746
|
+
// incoming pathname to `/<app>/_agent-native/agent-chat/_process-run`.
|
|
741
747
|
const processRunPath = `${basePath}${AGENT_CHAT_PROCESS_RUN_PATH}`;
|
|
742
|
-
const
|
|
743
|
-
|
|
748
|
+
const server = `// Mark this isolate as the durable background runtime BEFORE the handler bundle
|
|
749
|
+
// is imported, so isInBackgroundFunctionRuntime() reliably returns true in this
|
|
750
|
+
// function (the deployed Lambda name is not guaranteed to end in -background). A
|
|
751
|
+
// globalThis flag (NOT process.env) avoids the no-env-mutation guard and carries
|
|
752
|
+
// no cross-request state.
|
|
753
|
+
globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
|
|
754
|
+
|
|
755
|
+
const basePath = ${JSON.stringify(basePath)};
|
|
756
|
+
// The base-path-prefixed framework route the Nitro router dispatches to.
|
|
757
|
+
const PROCESS_RUN_PATH = ${JSON.stringify(processRunPath)};
|
|
744
758
|
|
|
745
759
|
function setBasePathEnv() {
|
|
746
760
|
const processRef = globalThis.process ??= { env: {} };
|
|
@@ -767,16 +781,33 @@ setBasePathEnv();
|
|
|
767
781
|
|
|
768
782
|
let cachedHandler;
|
|
769
783
|
|
|
770
|
-
|
|
784
|
+
// Reached at the DEFAULT url /.netlify/functions/${backgroundName}; REWRITE the
|
|
785
|
+
// incoming pathname to the base-path-prefixed _process-run route so the Nitro
|
|
786
|
+
// router runs the plugin. Method, ALL headers (the HMAC Authorization: Bearer
|
|
787
|
+
// MUST survive) and the body are preserved.
|
|
788
|
+
export default async function handler(request) {
|
|
771
789
|
setBasePathEnv();
|
|
772
790
|
cachedHandler ??= (await import("./main.mjs")).default;
|
|
773
|
-
|
|
791
|
+
const url = new URL(request.url);
|
|
792
|
+
url.pathname = PROCESS_RUN_PATH;
|
|
793
|
+
const method = request.method || "POST";
|
|
794
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
795
|
+
const body = hasBody ? await request.text() : undefined;
|
|
796
|
+
const rewritten = new Request(url.toString(), {
|
|
797
|
+
method,
|
|
798
|
+
headers: request.headers,
|
|
799
|
+
body,
|
|
800
|
+
});
|
|
801
|
+
return cachedHandler(rewritten);
|
|
774
802
|
}
|
|
775
803
|
|
|
776
804
|
export const config = {
|
|
777
805
|
name: ${JSON.stringify(`${app} agent background handler`)},
|
|
778
806
|
generator: "agent-native workspace deploy",
|
|
779
|
-
|
|
807
|
+
// background: true → async invoke (202, 15-min budget). NO custom path: the
|
|
808
|
+
// function keeps its default url /.netlify/functions/${backgroundName}, which
|
|
809
|
+
// the <app>-server catch-all never shadows (it excludes /.netlify/*).
|
|
810
|
+
background: true,
|
|
780
811
|
nodeBundler: "none",
|
|
781
812
|
includedFiles: ["**"],
|
|
782
813
|
preferStatic: false,
|
|
@@ -788,9 +819,10 @@ export const config = {
|
|
|
788
819
|
fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), server);
|
|
789
820
|
console.log(
|
|
790
821
|
`[workspace-deploy] Emitted durable-background function "${backgroundName}" ` +
|
|
791
|
-
`for app "${app}"
|
|
792
|
-
`
|
|
793
|
-
`
|
|
822
|
+
`for app "${app}" with config { background:true } and NO custom path — ` +
|
|
823
|
+
`reachable at its default url /.netlify/functions/${backgroundName} ` +
|
|
824
|
+
`(rewrites to ${processRunPath}). REQUIRES real-deploy verification of ` +
|
|
825
|
+
`Netlify async (202) invocation — see docs/design/durable-agent-runs.md.`,
|
|
794
826
|
);
|
|
795
827
|
}
|
|
796
828
|
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
* A2A, integration webhooks, and Agent Teams sub-agents share one tested
|
|
21
21
|
* implementation.
|
|
22
22
|
*/
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
getConfiguredAppBasePath,
|
|
25
|
+
withConfiguredAppBasePath,
|
|
26
|
+
} from "./app-base-path.js";
|
|
24
27
|
import { isLocalDatabase } from "../db/client.js";
|
|
25
28
|
import { signInternalToken } from "../integrations/internal-token.js";
|
|
26
29
|
|
|
@@ -119,11 +122,35 @@ async function dispatchResponseError(
|
|
|
119
122
|
* processor accepts unsigned dispatches in dev and relies on the SQL atomic
|
|
120
123
|
* claim for double-processing protection, mirroring the A2A/webhook flow.
|
|
121
124
|
*/
|
|
125
|
+
/**
|
|
126
|
+
* For host-root dispatch targets (`/.netlify/functions/*`), strip the configured
|
|
127
|
+
* app base path suffix from the resolved base url so the request reaches the
|
|
128
|
+
* function at the host root rather than under the workspace app base path. For
|
|
129
|
+
* every other (framework-route) path the base-path-prefixed base url is returned
|
|
130
|
+
* unchanged, preserving the existing self-dispatch behavior.
|
|
131
|
+
*/
|
|
132
|
+
function rootBaseUrlForPath(baseUrl: string, path: string): string {
|
|
133
|
+
if (!path.startsWith("/.netlify/")) return baseUrl;
|
|
134
|
+
const basePath = getConfiguredAppBasePath();
|
|
135
|
+
if (!basePath) return baseUrl;
|
|
136
|
+
const trimmed = baseUrl.replace(/\/$/, "");
|
|
137
|
+
if (trimmed.endsWith(basePath)) {
|
|
138
|
+
return trimmed.slice(0, trimmed.length - basePath.length);
|
|
139
|
+
}
|
|
140
|
+
return trimmed;
|
|
141
|
+
}
|
|
142
|
+
|
|
122
143
|
export async function fireInternalDispatch(
|
|
123
144
|
options: FireInternalDispatchOptions,
|
|
124
145
|
): Promise<void> {
|
|
125
146
|
const baseUrl = options.baseUrl ?? resolveSelfDispatchBaseUrl(options.event);
|
|
126
|
-
|
|
147
|
+
// Netlify function default urls (`/.netlify/functions/<name>`) live at the
|
|
148
|
+
// HOST ROOT, not under the workspace app base path. `resolveSelfDispatchBaseUrl`
|
|
149
|
+
// appends the configured base path (e.g. `https://host/starter`) so framework
|
|
150
|
+
// routes land on the right app; for a host-root function url we must dispatch
|
|
151
|
+
// to `https://host/.netlify/functions/<name>` instead. Strip the base path
|
|
152
|
+
// suffix from the resolved base url for `/.netlify/*` dispatch targets only.
|
|
153
|
+
const url = `${rootBaseUrlForPath(baseUrl, options.path)}${options.path}`;
|
|
127
154
|
const headers: Record<string, string> = {
|
|
128
155
|
"Content-Type": "application/json",
|
|
129
156
|
};
|
|
@@ -52,6 +52,9 @@
|
|
|
52
52
|
* {
|
|
53
53
|
box-sizing: border-box;
|
|
54
54
|
}
|
|
55
|
+
[hidden] {
|
|
56
|
+
display: none !important;
|
|
57
|
+
}
|
|
55
58
|
body {
|
|
56
59
|
margin: 0;
|
|
57
60
|
min-height: 100vh;
|
|
@@ -143,6 +146,45 @@
|
|
|
143
146
|
color: var(--fg-subtle);
|
|
144
147
|
font-size: 12px;
|
|
145
148
|
}
|
|
149
|
+
.success-state {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
align-items: center;
|
|
153
|
+
gap: 10px;
|
|
154
|
+
margin-top: -4px;
|
|
155
|
+
padding: 18px 16px;
|
|
156
|
+
border: 1px solid var(--status-ok);
|
|
157
|
+
border-radius: var(--radius-sm);
|
|
158
|
+
background: rgba(34, 197, 94, 0.1);
|
|
159
|
+
text-align: center;
|
|
160
|
+
}
|
|
161
|
+
.success-icon {
|
|
162
|
+
display: inline-flex;
|
|
163
|
+
width: 58px;
|
|
164
|
+
height: 58px;
|
|
165
|
+
align-items: center;
|
|
166
|
+
justify-content: center;
|
|
167
|
+
border: 2px solid var(--status-ok);
|
|
168
|
+
border-radius: 50%;
|
|
169
|
+
background: var(--bg);
|
|
170
|
+
color: var(--status-ok);
|
|
171
|
+
}
|
|
172
|
+
.success-icon svg {
|
|
173
|
+
width: 32px;
|
|
174
|
+
height: 32px;
|
|
175
|
+
}
|
|
176
|
+
.success-title {
|
|
177
|
+
margin: 0;
|
|
178
|
+
color: var(--fg);
|
|
179
|
+
font-size: 18px;
|
|
180
|
+
font-weight: 700;
|
|
181
|
+
line-height: 1.2;
|
|
182
|
+
}
|
|
183
|
+
.success-copy {
|
|
184
|
+
margin: 0;
|
|
185
|
+
color: var(--fg-muted);
|
|
186
|
+
font-size: 13.5px;
|
|
187
|
+
}
|
|
146
188
|
button {
|
|
147
189
|
font: inherit;
|
|
148
190
|
appearance: none;
|
|
@@ -219,6 +261,24 @@
|
|
|
219
261
|
>
|
|
220
262
|
</div>
|
|
221
263
|
</div>
|
|
264
|
+
<div class="success-state" id="success-state" role="status" hidden>
|
|
265
|
+
<div class="success-icon" aria-hidden="true">
|
|
266
|
+
<svg
|
|
267
|
+
viewBox="0 0 24 24"
|
|
268
|
+
fill="none"
|
|
269
|
+
stroke="currentColor"
|
|
270
|
+
stroke-width="2.4"
|
|
271
|
+
stroke-linecap="round"
|
|
272
|
+
stroke-linejoin="round"
|
|
273
|
+
>
|
|
274
|
+
<path d="M20 6 9 17l-5-5" />
|
|
275
|
+
</svg>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="success-title" id="success-title">You're all done</div>
|
|
278
|
+
<p class="success-copy" id="success-copy">
|
|
279
|
+
You can close this tab and start recording from the Clips icon.
|
|
280
|
+
</p>
|
|
281
|
+
</div>
|
|
222
282
|
<button id="enable">Enable camera & microphone</button>
|
|
223
283
|
<div class="status" id="status"></div>
|
|
224
284
|
</div>
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
const enableBtn = document.getElementById("enable") as HTMLButtonElement;
|
|
9
9
|
const statusEl = document.getElementById("status") as HTMLDivElement;
|
|
10
|
+
const successEl = document.getElementById("success-state") as HTMLDivElement;
|
|
11
|
+
const successTitle = document.getElementById("success-title") as HTMLDivElement;
|
|
12
|
+
const successCopy = document.getElementById(
|
|
13
|
+
"success-copy",
|
|
14
|
+
) as HTMLParagraphElement;
|
|
10
15
|
const rowMic = document.getElementById("row-mic") as HTMLDivElement;
|
|
11
16
|
const rowCam = document.getElementById("row-cam") as HTMLDivElement;
|
|
12
17
|
const checkMic = document.getElementById("check-mic") as HTMLSpanElement;
|
|
@@ -20,6 +25,25 @@ function setStatus(text: string, isError = false): void {
|
|
|
20
25
|
statusEl.classList.toggle("error", isError);
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
function showEnableButton(text: string, disabled: boolean): void {
|
|
29
|
+
enableBtn.hidden = false;
|
|
30
|
+
enableBtn.textContent = text;
|
|
31
|
+
enableBtn.disabled = disabled;
|
|
32
|
+
successEl.hidden = true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function showSuccess(title: string): void {
|
|
36
|
+
enableBtn.hidden = true;
|
|
37
|
+
enableBtn.disabled = true;
|
|
38
|
+
successTitle.textContent = title;
|
|
39
|
+
successCopy.textContent =
|
|
40
|
+
"You can close this tab and start recording from the Clips icon.";
|
|
41
|
+
successEl.hidden = false;
|
|
42
|
+
setStatus(
|
|
43
|
+
"Chrome still asks you what to share before each recording starts.",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
23
47
|
function markRow(kind: "mic" | "cam", granted: boolean): void {
|
|
24
48
|
const row = kind === "mic" ? rowMic : rowCam;
|
|
25
49
|
const check = kind === "mic" ? checkMic : checkCam;
|
|
@@ -59,16 +83,9 @@ function finish(camOk: boolean, micOk: boolean): void {
|
|
|
59
83
|
markRow("mic", micOk);
|
|
60
84
|
markRow("cam", camOk);
|
|
61
85
|
if (camOk || micOk) {
|
|
62
|
-
|
|
63
|
-
enableBtn.disabled = true;
|
|
64
|
-
setStatus(
|
|
65
|
-
camOk && micOk
|
|
66
|
-
? "Camera and microphone are ready. Click the Clips icon to record."
|
|
67
|
-
: "Saved. Click the Clips icon to record.",
|
|
68
|
-
);
|
|
86
|
+
showSuccess(camOk && micOk ? "You're all done" : "Saved");
|
|
69
87
|
} else {
|
|
70
|
-
|
|
71
|
-
enableBtn.textContent = "Try again";
|
|
88
|
+
showEnableButton("Try again", false);
|
|
72
89
|
setStatus(
|
|
73
90
|
"Access was blocked. Click the camera icon in Chrome's address bar to allow it, then try again.",
|
|
74
91
|
true,
|
|
@@ -77,7 +94,7 @@ function finish(camOk: boolean, micOk: boolean): void {
|
|
|
77
94
|
}
|
|
78
95
|
|
|
79
96
|
async function enable(): Promise<void> {
|
|
80
|
-
enableBtn.
|
|
97
|
+
showEnableButton(enableBtn.textContent ?? "Enable camera & microphone", true);
|
|
81
98
|
setStatus("Waiting for Chrome's permission prompt…");
|
|
82
99
|
// Request separately so a camera denial doesn't also block the microphone.
|
|
83
100
|
const micOk = await requestOne("mic");
|
|
@@ -96,10 +113,6 @@ void (async () => {
|
|
|
96
113
|
markRow("mic", mic === "granted");
|
|
97
114
|
markRow("cam", cam === "granted");
|
|
98
115
|
if (cam === "granted" && mic === "granted") {
|
|
99
|
-
|
|
100
|
-
enableBtn.disabled = true;
|
|
101
|
-
setStatus(
|
|
102
|
-
"Camera and microphone are ready. Click the Clips icon to record.",
|
|
103
|
-
);
|
|
116
|
+
showSuccess("You're all done");
|
|
104
117
|
}
|
|
105
118
|
})();
|
|
@@ -2349,7 +2349,6 @@ export function AddProperty({
|
|
|
2349
2349
|
},
|
|
2350
2350
|
});
|
|
2351
2351
|
const [open, setOpen] = useState(false);
|
|
2352
|
-
const [name, setName] = useState("");
|
|
2353
2352
|
const [typeQuery, setTypeQuery] = useState("");
|
|
2354
2353
|
const filteredPropertyTypes = filterDocumentPropertyTypes(typeQuery);
|
|
2355
2354
|
const firstFilteredPropertyType = filteredPropertyTypes[0] ?? null;
|
|
@@ -2377,19 +2376,18 @@ export function AddProperty({
|
|
|
2377
2376
|
}),
|
|
2378
2377
|
}))
|
|
2379
2378
|
.filter((group) => group.fields.length > 0);
|
|
2380
|
-
const
|
|
2379
|
+
const addPropertySearchInputRef = useRef<HTMLInputElement>(null);
|
|
2381
2380
|
|
|
2382
2381
|
useEffect(() => {
|
|
2383
2382
|
if (!open) return;
|
|
2384
2383
|
const frame = requestAnimationFrame(() => {
|
|
2385
|
-
|
|
2386
|
-
|
|
2384
|
+
addPropertySearchInputRef.current?.focus();
|
|
2385
|
+
addPropertySearchInputRef.current?.select();
|
|
2387
2386
|
});
|
|
2388
2387
|
return () => cancelAnimationFrame(frame);
|
|
2389
2388
|
}, [open]);
|
|
2390
2389
|
|
|
2391
2390
|
function closeAddPropertyPicker() {
|
|
2392
|
-
setName("");
|
|
2393
2391
|
setTypeQuery("");
|
|
2394
2392
|
setOpen(false);
|
|
2395
2393
|
}
|
|
@@ -2398,7 +2396,7 @@ export function AddProperty({
|
|
|
2398
2396
|
const label = DOCUMENT_PROPERTY_TYPE_LABELS[type];
|
|
2399
2397
|
await configure.mutateAsync({
|
|
2400
2398
|
documentId,
|
|
2401
|
-
name:
|
|
2399
|
+
name: label,
|
|
2402
2400
|
type,
|
|
2403
2401
|
options: defaultPropertyOptions(type),
|
|
2404
2402
|
});
|
|
@@ -2445,23 +2443,11 @@ export function AddProperty({
|
|
|
2445
2443
|
className="w-80 p-2"
|
|
2446
2444
|
>
|
|
2447
2445
|
<div className="grid gap-2">
|
|
2448
|
-
<Input
|
|
2449
|
-
ref={addPropertyNameInputRef}
|
|
2450
|
-
aria-label="New property name"
|
|
2451
|
-
autoFocus
|
|
2452
|
-
value={name}
|
|
2453
|
-
placeholder="Property name"
|
|
2454
|
-
onChange={(event) => setName(event.target.value)}
|
|
2455
|
-
onKeyDown={(event) => {
|
|
2456
|
-
if (event.key === "Escape") {
|
|
2457
|
-
event.preventDefault();
|
|
2458
|
-
closeAddPropertyPicker();
|
|
2459
|
-
}
|
|
2460
|
-
}}
|
|
2461
|
-
/>
|
|
2462
2446
|
<div className="flex h-8 items-center gap-1 rounded border border-border bg-background px-2">
|
|
2463
2447
|
<IconSearch className="size-3.5 shrink-0 text-muted-foreground" />
|
|
2464
2448
|
<Input
|
|
2449
|
+
ref={addPropertySearchInputRef}
|
|
2450
|
+
autoFocus
|
|
2465
2451
|
value={typeQuery}
|
|
2466
2452
|
placeholder="Search property types"
|
|
2467
2453
|
aria-label="Search property types"
|
|
@@ -64,6 +64,13 @@ themes. For any inline border, background, or text color, reference a token:
|
|
|
64
64
|
and `--wf-radius`. Never hard-code a hex color and never set `font-family` — the
|
|
65
65
|
renderer owns the sketch/clean font.
|
|
66
66
|
|
|
67
|
+
**Use literal CSS lengths for spacing.** The `--wf-*` tokens are for colors and
|
|
68
|
+
renderer-owned visual styling, not layout spacing. Do not use guessed spacing
|
|
69
|
+
tokens such as `var(--wf-space-4)`, Tailwind spacing classes, or theme spacing
|
|
70
|
+
variables inside wireframe HTML; if a token is unavailable in the Plan renderer,
|
|
71
|
+
padding collapses and content hugs the border. Use explicit CSS lengths for
|
|
72
|
+
layout: `padding:16px`, `gap:12px`, `margin-top:18px`, `minmax(0,1fr)`.
|
|
73
|
+
|
|
67
74
|
**Lay out with inline `style` flex/grid.** You write the real layout —
|
|
68
75
|
`display:flex; flex-direction:column; gap:10px; padding:16px` and so on — and the
|
|
69
76
|
renderer never repositions anything. Compose the actual product: reproduce the
|
|
@@ -148,10 +155,21 @@ check/serve or verify command for `<plan-dir>`.
|
|
|
148
155
|
**Treat the wireframe border as part of the visible design.** Always wrap HTML
|
|
149
156
|
wireframe content in a root container with real inner padding before drawing
|
|
150
157
|
cards, fields, pills, labels, or controls. Use at least 14-16px of padding,
|
|
151
|
-
`box-sizing: border-box`, `height: 100%`, and `gap` between child rows
|
|
152
|
-
first row never sits flush against the screen border.
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
`box-sizing: border-box`, `height: 100%`, and `gap` between child rows on the
|
|
159
|
+
root node itself so the first row never sits flush against the screen border. Do
|
|
160
|
+
not rely on padding on a nested page section as the first visible inset; the
|
|
161
|
+
outermost element must create the breathing room. Keep text away from borders:
|
|
162
|
+
every container, field, button, menu item, and annotation needs enough padding
|
|
163
|
+
and line-height to read cleanly in the rendered Plan view.
|
|
164
|
+
|
|
165
|
+
**For feature-cloud or abundance visuals, optimize the composition over line-by-line
|
|
166
|
+
reading.** Some marketing/product sections need to feel like a large surface area
|
|
167
|
+
of capability rather than a precise app workflow. In those cases, use one padded
|
|
168
|
+
root with a short headline and a dense, aesthetic cloud of short feature labels,
|
|
169
|
+
chips, rings, or columns. Vary scale and opacity with tokens, cluster by meaning,
|
|
170
|
+
and let many labels be glanceable rather than individually essential. Do not
|
|
171
|
+
force dozens of features into equal cards with long wrapped sentences; that
|
|
172
|
+
usually creates a messy unreadable mockup.
|
|
155
173
|
|
|
156
174
|
**Lay out children safely so they never collide.** Use HTML flex/grid with
|
|
157
175
|
`gap`, `min-width: 0`, and sensible overflow. Avoid negative margins, absolute
|
|
@@ -64,6 +64,13 @@ themes. For any inline border, background, or text color, reference a token:
|
|
|
64
64
|
and `--wf-radius`. Never hard-code a hex color and never set `font-family` — the
|
|
65
65
|
renderer owns the sketch/clean font.
|
|
66
66
|
|
|
67
|
+
**Use literal CSS lengths for spacing.** The `--wf-*` tokens are for colors and
|
|
68
|
+
renderer-owned visual styling, not layout spacing. Do not use guessed spacing
|
|
69
|
+
tokens such as `var(--wf-space-4)`, Tailwind spacing classes, or theme spacing
|
|
70
|
+
variables inside wireframe HTML; if a token is unavailable in the Plan renderer,
|
|
71
|
+
padding collapses and content hugs the border. Use explicit CSS lengths for
|
|
72
|
+
layout: `padding:16px`, `gap:12px`, `margin-top:18px`, `minmax(0,1fr)`.
|
|
73
|
+
|
|
67
74
|
**Lay out with inline `style` flex/grid.** You write the real layout —
|
|
68
75
|
`display:flex; flex-direction:column; gap:10px; padding:16px` and so on — and the
|
|
69
76
|
renderer never repositions anything. Compose the actual product: reproduce the
|
|
@@ -148,10 +155,21 @@ check/serve or verify command for `<plan-dir>`.
|
|
|
148
155
|
**Treat the wireframe border as part of the visible design.** Always wrap HTML
|
|
149
156
|
wireframe content in a root container with real inner padding before drawing
|
|
150
157
|
cards, fields, pills, labels, or controls. Use at least 14-16px of padding,
|
|
151
|
-
`box-sizing: border-box`, `height: 100%`, and `gap` between child rows
|
|
152
|
-
first row never sits flush against the screen border.
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
`box-sizing: border-box`, `height: 100%`, and `gap` between child rows on the
|
|
159
|
+
root node itself so the first row never sits flush against the screen border. Do
|
|
160
|
+
not rely on padding on a nested page section as the first visible inset; the
|
|
161
|
+
outermost element must create the breathing room. Keep text away from borders:
|
|
162
|
+
every container, field, button, menu item, and annotation needs enough padding
|
|
163
|
+
and line-height to read cleanly in the rendered Plan view.
|
|
164
|
+
|
|
165
|
+
**For feature-cloud or abundance visuals, optimize the composition over line-by-line
|
|
166
|
+
reading.** Some marketing/product sections need to feel like a large surface area
|
|
167
|
+
of capability rather than a precise app workflow. In those cases, use one padded
|
|
168
|
+
root with a short headline and a dense, aesthetic cloud of short feature labels,
|
|
169
|
+
chips, rings, or columns. Vary scale and opacity with tokens, cluster by meaning,
|
|
170
|
+
and let many labels be glanceable rather than individually essential. Do not
|
|
171
|
+
force dozens of features into equal cards with long wrapped sentences; that
|
|
172
|
+
usually creates a messy unreadable mockup.
|
|
155
173
|
|
|
156
174
|
**Lay out children safely so they never collide.** Use HTML flex/grid with
|
|
157
175
|
`gap`, `min-width: 0`, and sensible overflow. Avoid negative margins, absolute
|
|
@@ -43,8 +43,8 @@ const LazyPlanMarkdownEditor = lazy(() =>
|
|
|
43
43
|
/**
|
|
44
44
|
* Marker prefix embedded in salvaged "unknown-block" callout bodies by the
|
|
45
45
|
* server-side per-block salvage path in parsePlanContent. The renderer detects
|
|
46
|
-
* this prefix and shows
|
|
47
|
-
*
|
|
46
|
+
* this prefix and shows an invalid-block warning card rather than a generic
|
|
47
|
+
* callout.
|
|
48
48
|
*
|
|
49
49
|
* Format: `__unknown_block__:<originalType>\n<errorSummary>`
|
|
50
50
|
*/
|
|
@@ -87,7 +87,7 @@ class BlockErrorBoundary extends Component<
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/** Muted
|
|
90
|
+
/** Muted warning card for unknown/salvaged/errored blocks. */
|
|
91
91
|
function UnknownBlockPlaceholder({
|
|
92
92
|
blockId,
|
|
93
93
|
originalType,
|
|
@@ -103,26 +103,43 @@ function UnknownBlockPlaceholder({
|
|
|
103
103
|
data-block-id={blockId}
|
|
104
104
|
data-unknown-block-type={originalType}
|
|
105
105
|
>
|
|
106
|
-
<div className="flex items-start gap-
|
|
106
|
+
<div className="flex items-start gap-3 rounded-lg border border-amber-500/30 bg-amber-500/5 px-3.5 py-3 text-plan-muted">
|
|
107
107
|
<IconAlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-500" />
|
|
108
|
-
<
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
</
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
108
|
+
<div className="min-w-0 text-sm">
|
|
109
|
+
<div className="font-medium text-plan-text">
|
|
110
|
+
Invalid {originalType} block
|
|
111
|
+
</div>
|
|
112
|
+
<p className="mt-1 leading-5">
|
|
113
|
+
This generated block did not match the Plan schema, so it was left
|
|
114
|
+
out while the rest of the recap stayed visible.
|
|
115
|
+
</p>
|
|
116
|
+
{errorSummary && (
|
|
117
|
+
<details className="mt-2">
|
|
118
|
+
<summary className="cursor-pointer text-xs opacity-70 hover:opacity-90">
|
|
119
|
+
Validation details
|
|
120
|
+
</summary>
|
|
121
|
+
<pre className="mt-1 max-h-40 overflow-auto whitespace-pre-wrap break-words rounded border border-plan-line bg-plan-bg/60 p-2 font-mono text-xs opacity-80">
|
|
122
|
+
{errorSummary}
|
|
123
|
+
</pre>
|
|
124
|
+
</details>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
121
127
|
</div>
|
|
122
128
|
</section>
|
|
123
129
|
);
|
|
124
130
|
}
|
|
125
131
|
|
|
132
|
+
function parseUnknownBlockMarker(block: PlanBlock) {
|
|
133
|
+
if (block.type !== "callout") return null;
|
|
134
|
+
if (!block.data.body.startsWith(UNKNOWN_BLOCK_MARKER)) return null;
|
|
135
|
+
const rest = block.data.body.slice(UNKNOWN_BLOCK_MARKER.length);
|
|
136
|
+
const newline = rest.indexOf("\n");
|
|
137
|
+
return {
|
|
138
|
+
originalType: newline >= 0 ? rest.slice(0, newline) : rest,
|
|
139
|
+
errorSummary: newline >= 0 ? rest.slice(newline + 1) : "",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
126
143
|
type PlanBlockViewProps = {
|
|
127
144
|
block: PlanBlock;
|
|
128
145
|
onChange?: (block: PlanBlock) => Promise<void> | void;
|
|
@@ -163,6 +180,17 @@ function PlanBlockViewInner({
|
|
|
163
180
|
planId,
|
|
164
181
|
collabUser,
|
|
165
182
|
}: PlanBlockViewProps) {
|
|
183
|
+
const unknownBlock = parseUnknownBlockMarker(block);
|
|
184
|
+
if (unknownBlock) {
|
|
185
|
+
return (
|
|
186
|
+
<UnknownBlockPlaceholder
|
|
187
|
+
blockId={block.id}
|
|
188
|
+
originalType={unknownBlock.originalType}
|
|
189
|
+
errorSummary={unknownBlock.errorSummary}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
166
194
|
// Registry-first dispatch. If the block type is registered, render through the
|
|
167
195
|
// block registry (`BlockView` → spec `Read`, or in edit mode the spec `Edit`
|
|
168
196
|
// or the schema-driven auto-editor). Unregistered types fall through to the
|
|
@@ -230,22 +258,6 @@ function PlanBlockViewInner({
|
|
|
230
258
|
);
|
|
231
259
|
}
|
|
232
260
|
if (block.type === "callout") {
|
|
233
|
-
// Detect the server-side per-block salvage marker. The marker starts with a
|
|
234
|
-
// zero-width-space followed by `__unknown_block__:<type>` to avoid collision
|
|
235
|
-
// with real callout content (real callouts never start with a ZWSP).
|
|
236
|
-
if (block.data.body.startsWith(UNKNOWN_BLOCK_MARKER)) {
|
|
237
|
-
const rest = block.data.body.slice(UNKNOWN_BLOCK_MARKER.length);
|
|
238
|
-
const newline = rest.indexOf("\n");
|
|
239
|
-
const originalType = newline >= 0 ? rest.slice(0, newline) : rest;
|
|
240
|
-
const errorSummary = newline >= 0 ? rest.slice(newline + 1) : "";
|
|
241
|
-
return (
|
|
242
|
-
<UnknownBlockPlaceholder
|
|
243
|
-
blockId={block.id}
|
|
244
|
-
originalType={originalType}
|
|
245
|
-
errorSummary={errorSummary}
|
|
246
|
-
/>
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
261
|
return (
|
|
250
262
|
<section className="plan-block plan-callout" data-block-id={block.id}>
|
|
251
263
|
{block.title && <div className="plan-block-label">{block.title}</div>}
|