@agent-native/core 0.84.9 → 0.84.12
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 +43 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/run-store.ts +29 -6
- package/corpus/core/src/cli/skills.ts +6 -3
- package/corpus/core/src/client/agent-chat-adapter.ts +31 -11
- package/corpus/core/src/client/require-session.tsx +20 -1
- package/corpus/core/src/client/sharing/ShareButton.tsx +9 -3
- package/corpus/core/src/collab/storage.ts +40 -5
- package/corpus/core/src/server/auth.ts +6 -0
- package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +7 -6
- package/corpus/templates/assets/AGENTS.md +15 -0
- package/corpus/templates/assets/actions/_helpers.ts +3 -1
- package/corpus/templates/assets/actions/create-generation-preset.ts +12 -1
- package/corpus/templates/assets/actions/generate-asset.ts +5 -1
- package/corpus/templates/assets/actions/generate-image-batch.ts +6 -1
- package/corpus/templates/assets/actions/generate-image.ts +18 -11
- package/corpus/templates/assets/actions/open-asset-picker.ts +4 -2
- package/corpus/templates/assets/actions/update-generation-preset.ts +16 -3
- package/corpus/templates/assets/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/assets/app/i18n-data.ts +30 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +42 -7
- package/corpus/templates/assets/app/routes/library.tsx +4 -2
- package/corpus/templates/assets/changelog/2026-07-01-generation-presets-can-now-composite-your-brand-s-canonical-.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-01-tagging-a-preset-now-briefs-the-agent-on-that-preset-s-aesth.md +6 -0
- package/corpus/templates/assets/server/lib/preset-chat-context.ts +118 -0
- package/corpus/templates/assets/server/plugins/agent-chat.ts +5 -0
- package/corpus/templates/assets/shared/api.ts +1 -0
- package/corpus/templates/design/actions/export-html.ts +4 -2
- package/corpus/templates/design/actions/export-pdf.ts +3 -1
- package/corpus/templates/design/actions/export-svg.ts +4 -2
- package/corpus/templates/design/actions/export-zip.ts +10 -3
- package/corpus/templates/design/actions/get-design-snapshot.ts +41 -4
- package/corpus/templates/design/actions/present-design-variants.ts +21 -6
- package/corpus/templates/design/actions/update-file.ts +68 -17
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +3 -2
- package/corpus/templates/design/app/components/design/EditPanel.tsx +35 -3
- package/corpus/templates/design/app/components/design/MotionDock.tsx +46 -11
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +80 -30
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +60 -10
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +2 -0
- package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +4 -0
- package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
- package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +2 -2
- package/corpus/templates/design/app/i18n-data.ts +1 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +493 -109
- package/corpus/templates/design/app/pages/VisualEdit.tsx +4 -16
- package/corpus/templates/design/changelog/2026-07-01-improved-variant-pick-follow-ups-so-selected-directions-cont.md +6 -0
- package/corpus/templates/design/e2e/helpers.ts +2 -2
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +28 -6
- package/dist/agent/run-store.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +6 -3
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +26 -11
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/require-session.d.ts +14 -0
- package/dist/client/require-session.d.ts.map +1 -1
- package/dist/client/require-session.js +19 -1
- package/dist/client/require-session.js.map +1 -1
- package/dist/client/sharing/ShareButton.d.ts +4 -2
- package/dist/client/sharing/ShareButton.d.ts.map +1 -1
- package/dist/client/sharing/ShareButton.js +3 -1
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/storage.d.ts.map +1 -1
- package/dist/collab/storage.js +31 -5
- package/dist/collab/storage.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +6 -6
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +7 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.84.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9032acb: Don't reap a run that is actively making progress. The stale reapers
|
|
8
|
+
(`reapIfStale`, `reapAllStaleRuns`, and the heartbeat-stale path of
|
|
9
|
+
`cleanupOldRuns`) keyed liveness solely on `heartbeat_at` (the 1.5s
|
|
10
|
+
process-liveness timer) and ignored `last_progress_at` (bumped whenever the
|
|
11
|
+
agent emits an event, including a long-running tool's periodic activity
|
|
12
|
+
heartbeats — e.g. image generation streaming activity every 8s). When the
|
|
13
|
+
heartbeat write lagged while a multi-minute tool was in flight, the run was
|
|
14
|
+
flipped to `errored`, and the producing isolate's SQL-abort check then
|
|
15
|
+
self-aborted the in-flight tool with "Run aborted" — looping on the
|
|
16
|
+
durable-background self-chaining path and interrupting once inline. The reapers
|
|
17
|
+
now use the most recent of `heartbeat_at` and `last_progress_at` (falling back to
|
|
18
|
+
`started_at`) as their liveness basis, so a demonstrably-progressing run is never
|
|
19
|
+
reaped mid-tool. Portable across SQLite and Postgres; can only make reaping more
|
|
20
|
+
conservative (a dead producer emits neither signal).
|
|
21
|
+
- 9032acb: Add a `trigger="label-icon"` option to `ShareButton` that renders a leading
|
|
22
|
+
share glyph alongside the "Share" label, so the trigger matches adjacent
|
|
23
|
+
icon+label buttons (e.g. an Upload button). The default `"label"` trigger stays
|
|
24
|
+
text-only and `"icon"` stays icon-only.
|
|
25
|
+
- 351ee93: Fix an infinite sign-in redirect loop under base-path deploys. When the
|
|
26
|
+
authenticated app shell (wrapped in `RequireSession`) was served at the sign-in
|
|
27
|
+
path — e.g. `/<app>/_agent-native/sign-in` on a path-prefixed deploy — the gate
|
|
28
|
+
redirected to the sign-in page from the sign-in page, nesting and re-encoding the
|
|
29
|
+
current URL as a fresh `?return=` on every hop (`…sign-in?return=%252F…sign-in%253Freturn…`).
|
|
30
|
+
`RequireSession` now refuses to redirect when already on the sign-in entry point
|
|
31
|
+
(new exported `isOnSignInPage` helper), and `safeReturnPath` collapses any
|
|
32
|
+
`return` that resolves back to `…/_agent-native/sign-in` to `/`.
|
|
33
|
+
|
|
34
|
+
## 0.84.11
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- a2ce30e: Improve local SQLite collab write robustness for Design editor workflows.
|
|
39
|
+
|
|
40
|
+
## 0.84.10
|
|
41
|
+
|
|
42
|
+
### Patch Changes
|
|
43
|
+
|
|
44
|
+
- 720b8b0: Make agent chat recovery surface completed-tool timeouts more reliably and keep Design variant follow-ups bounded to the selected screen.
|
|
45
|
+
|
|
3
46
|
## 0.84.9
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.84.
|
|
3
|
+
"version": "0.84.12",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -428,6 +428,29 @@ function backgroundAwareStaleCutoffSql(): string {
|
|
|
428
428
|
return `(CAST(? AS BIGINT) - CASE WHEN dispatch_mode LIKE 'background%' THEN ${BACKGROUND_RUN_STALE_MS} ELSE ${RUN_STALE_MS} END)`;
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Liveness basis for the stale reapers: the MOST RECENT of `heartbeat_at`
|
|
433
|
+
* ("process is up", bumped on a 1.5s timer) and `last_progress_at` ("real work
|
|
434
|
+
* is happening", bumped whenever the agent emits an event — including a
|
|
435
|
+
* long-running tool's periodic activity heartbeats, e.g. image generation every
|
|
436
|
+
* 8s), falling back to `started_at`.
|
|
437
|
+
*
|
|
438
|
+
* The reapers previously keyed liveness on `heartbeat_at` alone, so a run that
|
|
439
|
+
* was demonstrably progressing got reaped ('running' → 'errored') the moment the
|
|
440
|
+
* process-liveness write lagged (DB latency, a brief event-loop stall). The
|
|
441
|
+
* producing isolate's SQL-abort check then self-aborted the in-flight action
|
|
442
|
+
* with "Run aborted"; on the durable-background self-chaining path this re-drove
|
|
443
|
+
* the turn in a loop. Honoring progress means a run doing real work is never
|
|
444
|
+
* reaped mid-tool. It can only make reaping MORE conservative — a genuinely dead
|
|
445
|
+
* producer emits neither signal — so a truly-dead run is still reaped.
|
|
446
|
+
*
|
|
447
|
+
* Portable across SQLite and Postgres (CASE + COALESCE only; no GREATEST or
|
|
448
|
+
* scalar MAX, which differ between engines).
|
|
449
|
+
*/
|
|
450
|
+
function livenessBasisSql(): string {
|
|
451
|
+
return `(CASE WHEN COALESCE(last_progress_at, started_at) > COALESCE(heartbeat_at, started_at) THEN COALESCE(last_progress_at, started_at) ELSE COALESCE(heartbeat_at, started_at) END)`;
|
|
452
|
+
}
|
|
453
|
+
|
|
431
454
|
/**
|
|
432
455
|
* Atomically claim a background-dispatched run for processing. The foreground
|
|
433
456
|
* POST inserts the run row with `dispatch_mode = 'background'`; the FIRST
|
|
@@ -733,8 +756,8 @@ export async function reapIfStale(
|
|
|
733
756
|
// override forces a flat window for callers that want one.
|
|
734
757
|
const staleClause =
|
|
735
758
|
typeof maxStaleMs === "number"
|
|
736
|
-
?
|
|
737
|
-
:
|
|
759
|
+
? `${livenessBasisSql()} < ?`
|
|
760
|
+
: `${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`;
|
|
738
761
|
const staleArgs =
|
|
739
762
|
typeof maxStaleMs === "number" ? [completedAt - maxStaleMs] : [completedAt];
|
|
740
763
|
const { rowsAffected } = await client.execute({
|
|
@@ -1151,7 +1174,7 @@ export async function reapAllStaleRuns(): Promise<number> {
|
|
|
1151
1174
|
const stale = await client.execute({
|
|
1152
1175
|
sql: `SELECT id FROM agent_runs
|
|
1153
1176
|
WHERE status = 'running'
|
|
1154
|
-
AND
|
|
1177
|
+
AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
|
|
1155
1178
|
args: [now],
|
|
1156
1179
|
});
|
|
1157
1180
|
const completedAt = Date.now();
|
|
@@ -1162,7 +1185,7 @@ export async function reapAllStaleRuns(): Promise<number> {
|
|
|
1162
1185
|
error_code = ?,
|
|
1163
1186
|
error_detail = ?
|
|
1164
1187
|
WHERE status = 'running'
|
|
1165
|
-
AND
|
|
1188
|
+
AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
|
|
1166
1189
|
args: [
|
|
1167
1190
|
completedAt,
|
|
1168
1191
|
STALE_RUN_ERROR_EVENT.errorCode,
|
|
@@ -1207,7 +1230,7 @@ export async function cleanupOldRuns(
|
|
|
1207
1230
|
sql: `SELECT id FROM agent_runs
|
|
1208
1231
|
WHERE status = 'running'
|
|
1209
1232
|
AND (
|
|
1210
|
-
|
|
1233
|
+
${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}
|
|
1211
1234
|
OR started_at < ?
|
|
1212
1235
|
)`,
|
|
1213
1236
|
args: [now, cutoff],
|
|
@@ -1236,7 +1259,7 @@ export async function cleanupOldRuns(
|
|
|
1236
1259
|
error_code = ?,
|
|
1237
1260
|
error_detail = ?
|
|
1238
1261
|
WHERE status = 'running'
|
|
1239
|
-
AND
|
|
1262
|
+
AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
|
|
1240
1263
|
args: [
|
|
1241
1264
|
completedAt,
|
|
1242
1265
|
STALE_RUN_ERROR_EVENT.errorCode,
|
|
@@ -328,7 +328,8 @@ iteration, or a human-in-the-loop choice among design directions.
|
|
|
328
328
|
full app per variant. Design saves every option as a normal screen on the
|
|
329
329
|
overview board and renders an inline chat choice with one button per screen
|
|
330
330
|
name. After the user picks, delete the unchosen variant screens and continue
|
|
331
|
-
from the kept screen
|
|
331
|
+
from the kept screen by first calling \`get-design-snapshot\` with that
|
|
332
|
+
screen's \`fileId\`, then making a bounded single-file pass.
|
|
332
333
|
- If the chat choice buttons are not available in the host, ask the user to
|
|
333
334
|
tell you the screen name they prefer. The variants are already real screens
|
|
334
335
|
on the board, so do not ask them to paste HTML or copy a generated handoff
|
|
@@ -351,8 +352,10 @@ iteration, or a human-in-the-loop choice among design directions.
|
|
|
351
352
|
and realistic controls over decorative mockups.
|
|
352
353
|
5. After \`present-design-variants\`, wait for the user's pick before
|
|
353
354
|
generating the next version. Keep the chosen screen, delete the other
|
|
354
|
-
variant screens,
|
|
355
|
-
|
|
355
|
+
variant screens, call \`get-design-snapshot\` with \`fileId\` for the kept
|
|
356
|
+
screen, then continue from that single file in a bounded pass. Prefer
|
|
357
|
+
\`edit-design\`; if \`generate-design\` is needed, update only the kept file
|
|
358
|
+
compactly and stop after the first successful save.
|
|
356
359
|
|
|
357
360
|
## Design Quality Bar
|
|
358
361
|
|
|
@@ -703,7 +703,26 @@ function hasContinuationProgress(content: ContentPart[]): boolean {
|
|
|
703
703
|
);
|
|
704
704
|
}
|
|
705
705
|
|
|
706
|
-
|
|
706
|
+
const COMPLETED_TOOL_TIMEOUT_NAME_RE =
|
|
707
|
+
/^(add|apply|archive|capture|create|delete|deploy|duplicate|edit|generate|grant|insert|migrate|move|present|publish|remove|rename|reorder|revoke|save|send|set|sync|trash|update|write)(-|$)/;
|
|
708
|
+
const COMPLETED_TOOL_TIMEOUT_NAME_ALLOWLIST = new Set([
|
|
709
|
+
"connect-assets-mcp",
|
|
710
|
+
"import-design-tokens",
|
|
711
|
+
]);
|
|
712
|
+
|
|
713
|
+
function isCompletedToolTimeoutCandidate(
|
|
714
|
+
part: Extract<ContentPart, { type: "tool-call" }>,
|
|
715
|
+
): boolean {
|
|
716
|
+
if (part.completedSideEffect === false) return false;
|
|
717
|
+
if (part.completedSideEffect === true) return true;
|
|
718
|
+
const toolName = part.toolName.toLowerCase();
|
|
719
|
+
return (
|
|
720
|
+
COMPLETED_TOOL_TIMEOUT_NAME_ALLOWLIST.has(toolName) ||
|
|
721
|
+
COMPLETED_TOOL_TIMEOUT_NAME_RE.test(toolName)
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function lastCompletedTimeoutCandidateTool(
|
|
707
726
|
content: ContentPart[],
|
|
708
727
|
): Extract<ContentPart, { type: "tool-call" }> | undefined {
|
|
709
728
|
for (let i = content.length - 1; i >= 0; i--) {
|
|
@@ -713,7 +732,7 @@ function lastCompletedSideEffectTool(
|
|
|
713
732
|
part.activity !== true &&
|
|
714
733
|
part.result !== undefined &&
|
|
715
734
|
part.isError !== true &&
|
|
716
|
-
part
|
|
735
|
+
isCompletedToolTimeoutCandidate(part)
|
|
717
736
|
) {
|
|
718
737
|
return part;
|
|
719
738
|
}
|
|
@@ -1866,7 +1885,7 @@ export function createAgentChatAdapter(
|
|
|
1866
1885
|
// before tool_start; treating it as progress caused silent retry
|
|
1867
1886
|
// loops when the LLM timed out while assembling a large tool input.
|
|
1868
1887
|
const hasInFlightTool = hasInFlightToolCall(visibleContent);
|
|
1869
|
-
const
|
|
1888
|
+
const completedTool = lastCompletedTimeoutCandidateTool(content);
|
|
1870
1889
|
// Either real output or an actively-running tool counts as progress
|
|
1871
1890
|
// for the stalled/empty caps.
|
|
1872
1891
|
const madeProgress = madeContentProgress || hasInFlightTool;
|
|
@@ -1963,22 +1982,23 @@ export function createAgentChatAdapter(
|
|
|
1963
1982
|
emptyTransientContinuationAttempts = 0;
|
|
1964
1983
|
} else {
|
|
1965
1984
|
totalTransientContinuationAttempts += 1;
|
|
1966
|
-
// If a
|
|
1967
|
-
//
|
|
1968
|
-
//
|
|
1969
|
-
//
|
|
1970
|
-
//
|
|
1971
|
-
// out" message.
|
|
1985
|
+
// If a tool already completed, do not turn a missing closing
|
|
1986
|
+
// sentence into a scary connection failure. Give the model one
|
|
1987
|
+
// continuation opportunity (the completed tool itself counts as
|
|
1988
|
+
// progress on the first timeout); if the follow-up produces no new
|
|
1989
|
+
// content, stop locally with a clear completed-tool warning.
|
|
1972
1990
|
if (
|
|
1973
|
-
|
|
1991
|
+
signal.reason === "run_timeout" &&
|
|
1992
|
+
completedTool &&
|
|
1974
1993
|
!hasInFlightToolCall(content) &&
|
|
1994
|
+
!currentPreparingToolName &&
|
|
1975
1995
|
!madeContentProgress &&
|
|
1976
1996
|
!hasInFlightTool
|
|
1977
1997
|
) {
|
|
1978
1998
|
return {
|
|
1979
1999
|
ok: false,
|
|
1980
2000
|
resetVisibleContent: false,
|
|
1981
|
-
completedToolName:
|
|
2001
|
+
completedToolName: completedTool.toolName,
|
|
1982
2002
|
};
|
|
1983
2003
|
}
|
|
1984
2004
|
// Bail when the same write tool is stuck in-flight across too many
|
|
@@ -70,6 +70,24 @@ export function buildSignInReturnHref(): string {
|
|
|
70
70
|
return `${base}?return=${encodeURIComponent(ret)}`;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* True when the browser is already sitting on the framework sign-in entry
|
|
75
|
+
* point. Redirecting to sign-in from here would append the current sign-in
|
|
76
|
+
* URL as a fresh `?return=`, re-encode it, and navigate again — an infinite
|
|
77
|
+
* loop of ever-growing `…sign-in?return=%252F…sign-in%253Freturn%253D…` URLs.
|
|
78
|
+
*
|
|
79
|
+
* This happens when the authenticated app shell (wrapped in `RequireSession`)
|
|
80
|
+
* is served at the sign-in path instead of the framework login HTML — e.g.
|
|
81
|
+
* under a base-path deploy where the SPA shell answers `/<app>/_agent-native/
|
|
82
|
+
* sign-in`. The sign-in page is the framework's job, not the gate's: never
|
|
83
|
+
* redirect to ourselves. Compared against `agentNativePath(...)` so it matches
|
|
84
|
+
* whether or not the app is mounted under a base path.
|
|
85
|
+
*/
|
|
86
|
+
export function isOnSignInPage(): boolean {
|
|
87
|
+
if (typeof window === "undefined") return false;
|
|
88
|
+
return window.location.pathname === agentNativePath("/_agent-native/sign-in");
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
export function RequireSession({
|
|
74
92
|
children,
|
|
75
93
|
fallback,
|
|
@@ -83,7 +101,8 @@ export function RequireSession({
|
|
|
83
101
|
// flight is harmless but noisy.
|
|
84
102
|
const redirectedRef = useRef(false);
|
|
85
103
|
|
|
86
|
-
const mustRedirect =
|
|
104
|
+
const mustRedirect =
|
|
105
|
+
!bypass && !isLoading && !session && redirect && !isOnSignInPage();
|
|
87
106
|
|
|
88
107
|
useEffect(() => {
|
|
89
108
|
if (!mustRedirect) return;
|
|
@@ -46,8 +46,10 @@ export interface ShareButtonProps {
|
|
|
46
46
|
/** @deprecated No longer affects rendering — trigger always says
|
|
47
47
|
* "Share". Kept for callsite compatibility. */
|
|
48
48
|
variant?: "compact" | "label";
|
|
49
|
-
/** Optional trigger style. Defaults to the Google-Docs-style "Share" label.
|
|
50
|
-
|
|
49
|
+
/** Optional trigger style. Defaults to the Google-Docs-style "Share" label.
|
|
50
|
+
* Use "label-icon" to render a leading share glyph alongside the label so
|
|
51
|
+
* the trigger matches adjacent icon+label buttons; "icon" is icon-only. */
|
|
52
|
+
trigger?: "label" | "icon" | "label-icon";
|
|
51
53
|
/** @deprecated Label triggers no longer render a visibility/share glyph. */
|
|
52
54
|
hideTriggerIcon?: boolean;
|
|
53
55
|
/** Optional className applied to the trigger button. */
|
|
@@ -355,7 +357,9 @@ export function ShareButton(props: ShareButtonProps) {
|
|
|
355
357
|
};
|
|
356
358
|
|
|
357
359
|
// The default trigger stays text-only; the icon trigger keeps the share glyph.
|
|
360
|
+
// "label-icon" renders the glyph alongside the label for icon+label parity.
|
|
358
361
|
const iconOnly = props.trigger === "icon";
|
|
362
|
+
const showLabelIcon = props.trigger === "label-icon";
|
|
359
363
|
|
|
360
364
|
return (
|
|
361
365
|
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
@@ -368,7 +372,9 @@ export function ShareButton(props: ShareButtonProps) {
|
|
|
368
372
|
)}
|
|
369
373
|
aria-label={iconOnly ? "Share" : undefined}
|
|
370
374
|
>
|
|
371
|
-
{iconOnly
|
|
375
|
+
{iconOnly || showLabelIcon ? (
|
|
376
|
+
<IconShare3 size={16} strokeWidth={1.75} />
|
|
377
|
+
) : null}
|
|
372
378
|
{!iconOnly && <span>Share</span>}
|
|
373
379
|
</button>
|
|
374
380
|
</PopoverTrigger>
|
|
@@ -10,6 +10,41 @@ import { ensureTableExists, ensureColumnExists } from "../db/ddl-guard.js";
|
|
|
10
10
|
|
|
11
11
|
let _initPromise: Promise<void> | undefined;
|
|
12
12
|
|
|
13
|
+
type DbExecClient = ReturnType<typeof getDbExec>;
|
|
14
|
+
type DbExecuteArg = Parameters<DbExecClient["execute"]>[0];
|
|
15
|
+
type DbExecuteResult = Awaited<ReturnType<DbExecClient["execute"]>>;
|
|
16
|
+
|
|
17
|
+
function isSqliteBusyError(error: unknown): boolean {
|
|
18
|
+
if (isPostgres()) return false;
|
|
19
|
+
const candidate = error as { code?: unknown; message?: unknown } | null;
|
|
20
|
+
return (
|
|
21
|
+
candidate?.code === "SQLITE_BUSY" ||
|
|
22
|
+
candidate?.code === "SQLITE_BUSY_RECOVERY" ||
|
|
23
|
+
(typeof candidate?.message === "string" &&
|
|
24
|
+
/database is locked|SQLITE_BUSY/i.test(candidate.message))
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sleep(ms: number): Promise<void> {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function executeWithSqliteRetry(
|
|
33
|
+
client: DbExecClient,
|
|
34
|
+
statement: DbExecuteArg,
|
|
35
|
+
): Promise<DbExecuteResult> {
|
|
36
|
+
const delays = [25, 75, 150, 300, 600];
|
|
37
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
38
|
+
try {
|
|
39
|
+
return await client.execute(statement);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const delay = delays[attempt];
|
|
42
|
+
if (delay === undefined || !isSqliteBusyError(error)) throw error;
|
|
43
|
+
await sleep(delay);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
13
48
|
async function ensureTable(): Promise<void> {
|
|
14
49
|
if (!_initPromise) {
|
|
15
50
|
_initPromise = (async () => {
|
|
@@ -97,7 +132,7 @@ export async function trySaveYDocState(
|
|
|
97
132
|
const b64 = uint8ArrayToBase64(state);
|
|
98
133
|
const nowExpr = isPostgres() ? "NOW()::text" : "datetime('now')";
|
|
99
134
|
if (expectedVersion === null) {
|
|
100
|
-
const result = await client
|
|
135
|
+
const result = await executeWithSqliteRetry(client, {
|
|
101
136
|
sql: isPostgres()
|
|
102
137
|
? `INSERT INTO _collab_docs (doc_id, yjs_state, text_snapshot, version, updated_at) VALUES (?, ?, ?, 0, ${nowExpr}) ON CONFLICT (doc_id) DO NOTHING`
|
|
103
138
|
: `INSERT OR IGNORE INTO _collab_docs (doc_id, yjs_state, text_snapshot, version, updated_at) VALUES (?, ?, ?, 0, ${nowExpr})`,
|
|
@@ -106,7 +141,7 @@ export async function trySaveYDocState(
|
|
|
106
141
|
return result.rowsAffected > 0;
|
|
107
142
|
}
|
|
108
143
|
|
|
109
|
-
const result = await client
|
|
144
|
+
const result = await executeWithSqliteRetry(client, {
|
|
110
145
|
sql: `UPDATE _collab_docs SET yjs_state = ?, text_snapshot = ?, version = version + 1, updated_at = ${nowExpr} WHERE doc_id = ? AND version = ?`,
|
|
111
146
|
args: [b64, textSnapshot, docId, expectedVersion],
|
|
112
147
|
});
|
|
@@ -123,13 +158,13 @@ export async function saveYDocState(
|
|
|
123
158
|
const client = getDbExec();
|
|
124
159
|
const b64 = uint8ArrayToBase64(state);
|
|
125
160
|
const nowExpr = isPostgres() ? "NOW()::text" : "datetime('now')";
|
|
126
|
-
const updated = await client
|
|
161
|
+
const updated = await executeWithSqliteRetry(client, {
|
|
127
162
|
sql: `UPDATE _collab_docs SET yjs_state = ?, text_snapshot = ?, version = version + 1, updated_at = ${nowExpr} WHERE doc_id = ?`,
|
|
128
163
|
args: [b64, textSnapshot, docId],
|
|
129
164
|
});
|
|
130
165
|
if (updated.rowsAffected > 0) return;
|
|
131
166
|
|
|
132
|
-
const inserted = await client
|
|
167
|
+
const inserted = await executeWithSqliteRetry(client, {
|
|
133
168
|
sql: isPostgres()
|
|
134
169
|
? `INSERT INTO _collab_docs (doc_id, yjs_state, text_snapshot, version, updated_at) VALUES (?, ?, ?, 0, ${nowExpr}) ON CONFLICT (doc_id) DO NOTHING`
|
|
135
170
|
: `INSERT OR IGNORE INTO _collab_docs (doc_id, yjs_state, text_snapshot, version, updated_at) VALUES (?, ?, ?, 0, ${nowExpr})`,
|
|
@@ -137,7 +172,7 @@ export async function saveYDocState(
|
|
|
137
172
|
});
|
|
138
173
|
if (inserted.rowsAffected > 0) return;
|
|
139
174
|
|
|
140
|
-
await client
|
|
175
|
+
await executeWithSqliteRetry(client, {
|
|
141
176
|
sql: `UPDATE _collab_docs SET yjs_state = ?, text_snapshot = ?, version = version + 1, updated_at = ${nowExpr} WHERE doc_id = ?`,
|
|
142
177
|
args: [b64, textSnapshot, docId],
|
|
143
178
|
});
|
|
@@ -542,6 +542,12 @@ export function safeReturnPath(raw: string | null | undefined): string {
|
|
|
542
542
|
try {
|
|
543
543
|
const parsed = new URL(raw, "http://safe-base.invalid");
|
|
544
544
|
if (parsed.origin !== "http://safe-base.invalid") return "/";
|
|
545
|
+
// Never return to the sign-in entry point itself. A `return` that resolves
|
|
546
|
+
// back to `…/_agent-native/sign-in` re-enters the redirect loop — each hop
|
|
547
|
+
// nests the prior sign-in URL as a fresh `?return=`. Collapse it to "/" so
|
|
548
|
+
// the post-sign-in 302 lands on the app, not another sign-in page. Matches
|
|
549
|
+
// with or without an app base-path prefix (`/<app>/_agent-native/sign-in`).
|
|
550
|
+
if (parsed.pathname.endsWith("/_agent-native/sign-in")) return "/";
|
|
545
551
|
return parsed.pathname + parsed.search + parsed.hash;
|
|
546
552
|
} catch {
|
|
547
553
|
return "/";
|
|
@@ -10,16 +10,17 @@ LLMs — including Gemini Pro — degrade complex logos. They smear gradients, d
|
|
|
10
10
|
## How it works
|
|
11
11
|
|
|
12
12
|
1. The library has a `canonicalLogoUrl` (set via `set-canonical-logo --libraryId --assetId`). The asset's role is `logo_reference`.
|
|
13
|
-
2. When `generate-image
|
|
13
|
+
2. **Logo compositing is a preset option.** A generation preset carries `includeLogo` (stored in the preset `settings` and surfaced as a first-class field). When a generation resolves to a preset with `includeLogo: true`, `generate-image` composites the logo. A generate call's own `includeLogo` arg, when passed, overrides the preset for that run; when omitted, the preset's value wins.
|
|
14
|
+
3. When logo compositing is on, the prompt envelope adds:
|
|
14
15
|
> Leave a clean uncluttered area in the upper-right for the real brand logo; do not draw or approximate the logo yourself.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
4. Gemini returns an image with empty space in that corner.
|
|
17
|
+
5. `compositeLogo()` from `server/lib/image-processing.ts` (Sharp) loads the canonical logo PNG / SVG, resizes it to ~16% of the image width with reasonable inset, and composites it onto the generated image.
|
|
18
|
+
6. Output: the image with the actual logo, pixel-perfect, vector-quality if the source is SVG.
|
|
18
19
|
|
|
19
20
|
## When to use it
|
|
20
21
|
|
|
21
|
-
- The user
|
|
22
|
-
- The agent infers the user wants the logo (e.g. "make a hero with our brand logo").
|
|
22
|
+
- The user turned on "Composite canonical logo" when creating a generation preset (the preset then stamps the logo on every image made with it).
|
|
23
|
+
- The agent infers the user wants the logo for a one-off (e.g. "make a hero with our brand logo") — pass `includeLogo: true` on that single generate call to override the preset.
|
|
23
24
|
- The image will appear in a customer-facing context where logo accuracy matters.
|
|
24
25
|
|
|
25
26
|
## When NOT to use it
|
|
@@ -47,10 +47,25 @@ prompt, aspectRatio }`.
|
|
|
47
47
|
composer is empty. The image model is the only remaining composer-side
|
|
48
48
|
default; the image-model picker writes `imageGenerationModel`, which image
|
|
49
49
|
generation actions may use when `model` is omitted.
|
|
50
|
+
- When a `preset` is tagged, the server embeds that preset's aesthetics and
|
|
51
|
+
creative philosophy (brand style brief, prompt template, text/logo policy,
|
|
52
|
+
output format) into your message inside a `<tagged-generation-presets>` block.
|
|
53
|
+
Study and internalize that brief before you generate — let it drive
|
|
54
|
+
composition, mood, lighting, and subject — then pass the `presetId` to
|
|
55
|
+
`generate-image` / `generate-image-batch` so the saved format/model/tier/logo
|
|
56
|
+
apply automatically. Do not restate those as ad-hoc args.
|
|
50
57
|
- For exact visible copy inside a generated image, pass `embeddedText` and
|
|
51
58
|
optional `textPlacement` to `generate-image` or each `generate-image-batch`
|
|
52
59
|
slot. Keep the general `prompt` for creative direction; the structured text
|
|
53
60
|
fields are what allow the pipeline to render copy instead of suppressing it.
|
|
61
|
+
- Logo compositing is a preset option: a generation preset carries
|
|
62
|
+
`includeLogo`, and `generate-image` / `generate-image-batch` stamp the
|
|
63
|
+
library's pixel-perfect canonical logo onto the result when the resolved
|
|
64
|
+
preset has it on (no-op if the library has no canonical logo). Set it via
|
|
65
|
+
`create-generation-preset` / `update-generation-preset` (`includeLogo`). The
|
|
66
|
+
model never draws the logo itself. Pass `includeLogo` on a generate call only
|
|
67
|
+
to override the preset for that one run; otherwise omit it. See the
|
|
68
|
+
`logo-composite` skill.
|
|
54
69
|
- `asset-variants` is the shared live generation tray state. New image
|
|
55
70
|
candidates should appear there through `generate-image` or
|
|
56
71
|
`generate-image-batch`; do not invent page-local progress surfaces.
|
|
@@ -155,6 +155,7 @@ export function serializeGenerationRun(row: any) {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
export function serializeGenerationPreset(row: any): GenerationPresetSummary {
|
|
158
|
+
const settings = parseJson<Record<string, unknown>>(row.settings, {});
|
|
158
159
|
return {
|
|
159
160
|
id: row.id,
|
|
160
161
|
libraryId: row.libraryId,
|
|
@@ -169,7 +170,8 @@ export function serializeGenerationPreset(row: any): GenerationPresetSummary {
|
|
|
169
170
|
model: row.model,
|
|
170
171
|
textPolicy: row.textPolicy ?? "",
|
|
171
172
|
referencePolicy: row.referencePolicy ?? "auto",
|
|
172
|
-
|
|
173
|
+
includeLogo: settings.includeLogo === true,
|
|
174
|
+
settings,
|
|
173
175
|
sortOrder: Number(row.sortOrder ?? 0),
|
|
174
176
|
createdAt: row.createdAt,
|
|
175
177
|
updatedAt: row.updatedAt,
|
|
@@ -32,6 +32,12 @@ export default defineAction({
|
|
|
32
32
|
referencePolicy: z
|
|
33
33
|
.enum(GENERATION_PRESET_REFERENCE_POLICIES)
|
|
34
34
|
.default("auto"),
|
|
35
|
+
includeLogo: z.coerce
|
|
36
|
+
.boolean()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe(
|
|
39
|
+
"When true, images generated with this preset composite the library's canonical logo (no-op if the library has no canonical logo).",
|
|
40
|
+
),
|
|
35
41
|
settings: z.record(z.string(), z.unknown()).optional(),
|
|
36
42
|
sortOrder: z.coerce.number().optional(),
|
|
37
43
|
}),
|
|
@@ -62,7 +68,12 @@ export default defineAction({
|
|
|
62
68
|
model: args.model,
|
|
63
69
|
textPolicy: args.textPolicy,
|
|
64
70
|
referencePolicy: args.referencePolicy,
|
|
65
|
-
settings: stringifyJson(
|
|
71
|
+
settings: stringifyJson({
|
|
72
|
+
...(args.settings ?? {}),
|
|
73
|
+
...(args.includeLogo !== undefined
|
|
74
|
+
? { includeLogo: args.includeLogo }
|
|
75
|
+
: {}),
|
|
76
|
+
}),
|
|
66
77
|
sortOrder: args.sortOrder ?? 100,
|
|
67
78
|
createdAt: now,
|
|
68
79
|
updatedAt: now,
|
|
@@ -48,7 +48,11 @@ const schema = z.object({
|
|
|
48
48
|
.describe("Number of image candidates to generate in the picker."),
|
|
49
49
|
tier: z.enum(IMAGE_QUALITY_TIERS).optional(),
|
|
50
50
|
styleStrength: z.enum(STYLE_STRENGTHS).default("balanced"),
|
|
51
|
-
includeLogo: booleanParam
|
|
51
|
+
includeLogo: booleanParam
|
|
52
|
+
.optional()
|
|
53
|
+
.describe(
|
|
54
|
+
"Override logo compositing for this run. When omitted, the selected preset's logo setting decides whether the library's canonical logo is composited.",
|
|
55
|
+
),
|
|
52
56
|
callerAppId: z
|
|
53
57
|
.string()
|
|
54
58
|
.optional()
|
|
@@ -86,7 +86,12 @@ export default defineAction({
|
|
|
86
86
|
tier: z.enum(IMAGE_QUALITY_TIERS).optional(),
|
|
87
87
|
intent: z.enum(GENERATION_INTENTS).default("generate"),
|
|
88
88
|
styleStrength: z.enum(STYLE_STRENGTHS).default("balanced"),
|
|
89
|
-
includeLogo: z.coerce
|
|
89
|
+
includeLogo: z.coerce
|
|
90
|
+
.boolean()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe(
|
|
93
|
+
"Composite the library's canonical logo onto every slot. When omitted, the selected preset's logo setting is used; pass an explicit value to override it. No-op if the library has no canonical logo.",
|
|
94
|
+
),
|
|
90
95
|
groundingMode: z.enum(["auto", "off", "google-search"]).default("auto"),
|
|
91
96
|
source: z.enum(["chat", "ui", "a2a"]).default("chat"),
|
|
92
97
|
callerAppId: z
|
|
@@ -103,7 +103,12 @@ export default defineAction({
|
|
|
103
103
|
.describe(
|
|
104
104
|
"Exact reference assets to use. When omitted, the server deterministically chooses a small relevant subset from the latest library references.",
|
|
105
105
|
),
|
|
106
|
-
includeLogo: z.coerce
|
|
106
|
+
includeLogo: z.coerce
|
|
107
|
+
.boolean()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe(
|
|
110
|
+
"Composite the library's pixel-perfect canonical logo onto the finished image. When omitted, the selected preset's logo setting is used; pass an explicit value to override it. No-op if the library has no canonical logo.",
|
|
111
|
+
),
|
|
107
112
|
slotId: z.string().optional(),
|
|
108
113
|
variantBatchId: z.string().optional(),
|
|
109
114
|
variantScopeId: z
|
|
@@ -259,11 +264,13 @@ export default defineAction({
|
|
|
259
264
|
preset?.imageSize ??
|
|
260
265
|
collection?.defaultImageSize ??
|
|
261
266
|
"2K") as (typeof IMAGE_SIZES)[number];
|
|
262
|
-
const presetSettings = parseJson<{
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
);
|
|
267
|
+
const presetSettings = parseJson<{
|
|
268
|
+
tier?: ImageQualityTier;
|
|
269
|
+
includeLogo?: boolean;
|
|
270
|
+
}>(preset?.settings, {});
|
|
266
271
|
const resolvedTier = args.tier ?? presetSettings.tier;
|
|
272
|
+
const resolvedIncludeLogo =
|
|
273
|
+
args.includeLogo ?? presetSettings.includeLogo ?? false;
|
|
267
274
|
const category = (args.categories?.[0] ??
|
|
268
275
|
preset?.category ??
|
|
269
276
|
collection?.category) as ImageCategory | undefined;
|
|
@@ -320,7 +327,7 @@ export default defineAction({
|
|
|
320
327
|
embeddedText: args.embeddedText,
|
|
321
328
|
textPlacement: args.textPlacement,
|
|
322
329
|
referenceCount: references.length,
|
|
323
|
-
includeLogo:
|
|
330
|
+
includeLogo: resolvedIncludeLogo,
|
|
324
331
|
aspectRatio: resolvedAspectRatio,
|
|
325
332
|
imageSize: resolvedImageSize,
|
|
326
333
|
category,
|
|
@@ -363,7 +370,7 @@ export default defineAction({
|
|
|
363
370
|
aspectRatio: resolvedAspectRatio,
|
|
364
371
|
imageSize: resolvedImageSize,
|
|
365
372
|
groundingMode: args.groundingMode,
|
|
366
|
-
includeLogo:
|
|
373
|
+
includeLogo: resolvedIncludeLogo,
|
|
367
374
|
categories: resolvedCategories ?? [],
|
|
368
375
|
collectionId: resolvedCollectionId ?? null,
|
|
369
376
|
presetId: preset?.id ?? null,
|
|
@@ -386,7 +393,7 @@ export default defineAction({
|
|
|
386
393
|
intent: args.intent,
|
|
387
394
|
styleStrength: args.styleStrength,
|
|
388
395
|
tier: resolvedTier,
|
|
389
|
-
includeLogo:
|
|
396
|
+
includeLogo: resolvedIncludeLogo,
|
|
390
397
|
categories: resolvedCategories ?? [],
|
|
391
398
|
presetId: preset?.id,
|
|
392
399
|
sessionId: session?.id,
|
|
@@ -463,7 +470,7 @@ export default defineAction({
|
|
|
463
470
|
await deleteAppState("image-generation-setup").catch(() => {});
|
|
464
471
|
let image = generated.image;
|
|
465
472
|
let mimeType = generated.mimeType;
|
|
466
|
-
if (
|
|
473
|
+
if (resolvedIncludeLogo && library.canonicalLogoAssetId) {
|
|
467
474
|
const [logo] = await db
|
|
468
475
|
.select()
|
|
469
476
|
.from(schema.assets)
|
|
@@ -539,7 +546,7 @@ export default defineAction({
|
|
|
539
546
|
intent: args.intent,
|
|
540
547
|
styleStrength: args.styleStrength,
|
|
541
548
|
tier: resolvedTier,
|
|
542
|
-
includeLogo:
|
|
549
|
+
includeLogo: resolvedIncludeLogo,
|
|
543
550
|
presetId: preset?.id,
|
|
544
551
|
sessionId: session?.id,
|
|
545
552
|
generated: true,
|
|
@@ -584,7 +591,7 @@ export default defineAction({
|
|
|
584
591
|
intent: args.intent,
|
|
585
592
|
styleStrength: args.styleStrength,
|
|
586
593
|
tier: resolvedTier,
|
|
587
|
-
includeLogo:
|
|
594
|
+
includeLogo: resolvedIncludeLogo,
|
|
588
595
|
categories: resolvedCategories ?? [],
|
|
589
596
|
referenceSelection,
|
|
590
597
|
settingsUsed,
|
|
@@ -72,8 +72,10 @@ const schema = z.object({
|
|
|
72
72
|
.default("balanced")
|
|
73
73
|
.describe("How strongly to follow the library style during generation."),
|
|
74
74
|
includeLogo: booleanParam
|
|
75
|
-
.
|
|
76
|
-
.describe(
|
|
75
|
+
.optional()
|
|
76
|
+
.describe(
|
|
77
|
+
"Override logo compositing for auto-generation. When omitted, the selected preset's logo setting decides whether the library's canonical logo is composited.",
|
|
78
|
+
),
|
|
77
79
|
callerAppId: z
|
|
78
80
|
.string()
|
|
79
81
|
.optional()
|