@agent-native/core 0.84.10 → 0.84.13

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.
Files changed (140) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +48 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +30 -0
  5. package/corpus/core/src/agent/production-agent.ts +34 -6
  6. package/corpus/core/src/agent/run-manager.ts +45 -2
  7. package/corpus/core/src/agent/run-store.ts +29 -6
  8. package/corpus/core/src/chat-threads/store.ts +2 -0
  9. package/corpus/core/src/cli/skills.ts +10 -6
  10. package/corpus/core/src/client/AssistantChat.tsx +40 -9
  11. package/corpus/core/src/client/agent-chat-adapter.ts +23 -2
  12. package/corpus/core/src/client/chat/run-recovery.tsx +52 -44
  13. package/corpus/core/src/client/chat/tool-call-display.tsx +7 -2
  14. package/corpus/core/src/client/require-session.tsx +20 -1
  15. package/corpus/core/src/client/sharing/ShareButton.tsx +9 -3
  16. package/corpus/core/src/collab/storage.ts +40 -5
  17. package/corpus/core/src/server/agent-chat-plugin.ts +1 -0
  18. package/corpus/core/src/server/auth.ts +6 -0
  19. package/corpus/core/src/shared/streaming-text-smoothing.ts +10 -5
  20. package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +7 -6
  21. package/corpus/templates/assets/AGENTS.md +15 -0
  22. package/corpus/templates/assets/actions/_helpers.ts +3 -1
  23. package/corpus/templates/assets/actions/create-generation-preset.ts +12 -1
  24. package/corpus/templates/assets/actions/generate-asset.ts +5 -1
  25. package/corpus/templates/assets/actions/generate-image-batch.ts +6 -1
  26. package/corpus/templates/assets/actions/generate-image.ts +18 -11
  27. package/corpus/templates/assets/actions/open-asset-picker.ts +4 -2
  28. package/corpus/templates/assets/actions/update-generation-preset.ts +16 -3
  29. package/corpus/templates/assets/app/i18n/zh-TW.ts +3 -0
  30. package/corpus/templates/assets/app/i18n-data.ts +30 -0
  31. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +42 -7
  32. package/corpus/templates/assets/app/routes/library.tsx +4 -2
  33. package/corpus/templates/assets/changelog/2026-07-01-generation-presets-can-now-composite-your-brand-s-canonical-.md +6 -0
  34. package/corpus/templates/assets/changelog/2026-07-01-tagging-a-preset-now-briefs-the-agent-on-that-preset-s-aesth.md +6 -0
  35. package/corpus/templates/assets/server/lib/preset-chat-context.ts +118 -0
  36. package/corpus/templates/assets/server/plugins/agent-chat.ts +5 -0
  37. package/corpus/templates/assets/shared/api.ts +1 -0
  38. package/corpus/templates/clips/app/components/library/recording-card.tsx +14 -4
  39. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +10 -12
  40. package/corpus/templates/clips/app/hooks/use-library.ts +7 -4
  41. package/corpus/templates/clips/app/lib/recording-status.ts +32 -0
  42. package/corpus/templates/clips/changelog/2026-07-01-dictation-keeps-listening-through-natural-pauses-and-gives-c.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-01-redoing-a-paused-desktop-recording-no-longer-leaves-the-recording-controls-disabled.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-01-stalled-clip-uploads-now-update-in-the-library-and-surface-as-failed.md +6 -0
  45. package/corpus/templates/clips/desktop/src/lib/recorder.ts +36 -18
  46. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +99 -27
  47. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +4 -43
  48. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +3 -0
  49. package/corpus/templates/clips/desktop/src/styles.css +5 -48
  50. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +4 -5
  51. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +15 -6
  52. package/corpus/templates/design/AGENTS.md +8 -4
  53. package/corpus/templates/design/actions/create-design.ts +1 -0
  54. package/corpus/templates/design/actions/duplicate-design.ts +3 -1
  55. package/corpus/templates/design/actions/edit-design.ts +43 -13
  56. package/corpus/templates/design/actions/export-html.ts +4 -2
  57. package/corpus/templates/design/actions/export-pdf.ts +3 -1
  58. package/corpus/templates/design/actions/export-svg.ts +4 -2
  59. package/corpus/templates/design/actions/export-zip.ts +10 -3
  60. package/corpus/templates/design/actions/generate-design.ts +5 -1
  61. package/corpus/templates/design/actions/present-design-variants.ts +17 -12
  62. package/corpus/templates/design/actions/update-file.ts +68 -17
  63. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +3 -2
  64. package/corpus/templates/design/app/components/design/EditPanel.tsx +35 -3
  65. package/corpus/templates/design/app/components/design/MotionDock.tsx +46 -11
  66. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +80 -30
  67. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +60 -10
  68. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +2 -0
  69. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +4 -0
  70. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  71. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +2 -2
  72. package/corpus/templates/design/app/i18n-data.ts +1 -1
  73. package/corpus/templates/design/app/pages/DesignEditor.tsx +493 -109
  74. package/corpus/templates/design/app/pages/VisualEdit.tsx +4 -16
  75. package/corpus/templates/design/changelog/2026-07-01-org-members-can-see-design-projects-created-in-their-workspace.md +6 -0
  76. package/corpus/templates/design/e2e/helpers.ts +2 -2
  77. package/corpus/templates/design/server/plugins/db.ts +9 -0
  78. package/dist/agent/durable-background.d.ts +3 -0
  79. package/dist/agent/durable-background.d.ts.map +1 -1
  80. package/dist/agent/durable-background.js +19 -0
  81. package/dist/agent/durable-background.js.map +1 -1
  82. package/dist/agent/production-agent.d.ts +1 -0
  83. package/dist/agent/production-agent.d.ts.map +1 -1
  84. package/dist/agent/production-agent.js +29 -6
  85. package/dist/agent/production-agent.js.map +1 -1
  86. package/dist/agent/run-manager.d.ts.map +1 -1
  87. package/dist/agent/run-manager.js +40 -2
  88. package/dist/agent/run-manager.js.map +1 -1
  89. package/dist/agent/run-store.d.ts.map +1 -1
  90. package/dist/agent/run-store.js +28 -6
  91. package/dist/agent/run-store.js.map +1 -1
  92. package/dist/chat-threads/store.d.ts +1 -0
  93. package/dist/chat-threads/store.d.ts.map +1 -1
  94. package/dist/chat-threads/store.js +2 -0
  95. package/dist/chat-threads/store.js.map +1 -1
  96. package/dist/cli/skills.d.ts.map +1 -1
  97. package/dist/cli/skills.js +10 -6
  98. package/dist/cli/skills.js.map +1 -1
  99. package/dist/client/AssistantChat.d.ts +10 -0
  100. package/dist/client/AssistantChat.d.ts.map +1 -1
  101. package/dist/client/AssistantChat.js +26 -8
  102. package/dist/client/AssistantChat.js.map +1 -1
  103. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  104. package/dist/client/agent-chat-adapter.js +21 -2
  105. package/dist/client/agent-chat-adapter.js.map +1 -1
  106. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  107. package/dist/client/chat/run-recovery.js +15 -10
  108. package/dist/client/chat/run-recovery.js.map +1 -1
  109. package/dist/client/chat/tool-call-display.d.ts +1 -0
  110. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  111. package/dist/client/chat/tool-call-display.js +3 -2
  112. package/dist/client/chat/tool-call-display.js.map +1 -1
  113. package/dist/client/require-session.d.ts +14 -0
  114. package/dist/client/require-session.d.ts.map +1 -1
  115. package/dist/client/require-session.js +19 -1
  116. package/dist/client/require-session.js.map +1 -1
  117. package/dist/client/sharing/ShareButton.d.ts +4 -2
  118. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  119. package/dist/client/sharing/ShareButton.js +3 -1
  120. package/dist/client/sharing/ShareButton.js.map +1 -1
  121. package/dist/collab/routes.d.ts +2 -2
  122. package/dist/collab/storage.d.ts.map +1 -1
  123. package/dist/collab/storage.js +31 -5
  124. package/dist/collab/storage.js.map +1 -1
  125. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  126. package/dist/notifications/routes.d.ts +2 -2
  127. package/dist/observability/routes.d.ts +5 -5
  128. package/dist/progress/routes.d.ts +1 -1
  129. package/dist/resources/handlers.d.ts +3 -3
  130. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  131. package/dist/server/agent-chat-plugin.js +1 -1
  132. package/dist/server/agent-chat-plugin.js.map +1 -1
  133. package/dist/server/auth.d.ts.map +1 -1
  134. package/dist/server/auth.js +7 -0
  135. package/dist/server/auth.js.map +1 -1
  136. package/dist/server/transcribe-voice.d.ts +1 -1
  137. package/dist/shared/streaming-text-smoothing.d.ts.map +1 -1
  138. package/dist/shared/streaming-text-smoothing.js +8 -5
  139. package/dist/shared/streaming-text-smoothing.js.map +1 -1
  140. package/package.json +1 -1
@@ -513,7 +513,9 @@ export function RunErrorRecoveryCard({
513
513
  onDismiss: () => void;
514
514
  }) {
515
515
  const [detailsOpen, setDetailsOpen] = useState(false);
516
- const [copied, setCopied] = useState(false);
516
+ const [copyState, setCopyState] = useState<"idle" | "copied" | "failed">(
517
+ "idle",
518
+ );
517
519
  const [forking, setForking] = useState(false);
518
520
  const [forkError, setForkError] = useState<string | null>(null);
519
521
  const builderReconnect = useBuilderConnectFlow({
@@ -538,11 +540,15 @@ export function RunErrorRecoveryCard({
538
540
  ]
539
541
  .filter(Boolean)
540
542
  .join("\n\n");
541
- void writeClipboardText(text).then((ok) => {
542
- if (!ok) return;
543
- setCopied(true);
544
- setTimeout(() => setCopied(false), 1200);
545
- });
543
+ void writeClipboardText(text)
544
+ .then((ok) => {
545
+ setCopyState(ok ? "copied" : "failed");
546
+ setTimeout(() => setCopyState("idle"), 1600);
547
+ })
548
+ .catch(() => {
549
+ setCopyState("failed");
550
+ setTimeout(() => setCopyState("idle"), 1600);
551
+ });
546
552
  }, [info]);
547
553
  const startNewChat = useCallback(() => {
548
554
  window.dispatchEvent(new CustomEvent("agent-chat:new-chat"));
@@ -705,8 +711,20 @@ export function RunErrorRecoveryCard({
705
711
  onClick={copyDetails}
706
712
  className="ml-auto inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs font-medium text-muted-foreground hover:bg-background/80 hover:text-foreground"
707
713
  >
708
- {copied ? <IconCheck size={13} /> : <IconCopy size={13} />}
709
- {copied ? "Copied" : copyLabel}
714
+ {copyState === "copied" ? (
715
+ <IconCheck size={13} />
716
+ ) : copyState === "failed" ? (
717
+ <IconX size={13} />
718
+ ) : (
719
+ <IconCopy size={13} />
720
+ )}
721
+ <span aria-live="polite">
722
+ {copyState === "copied"
723
+ ? "Copied"
724
+ : copyState === "failed"
725
+ ? "Copy failed"
726
+ : copyLabel}
727
+ </span>
710
728
  </button>
711
729
  </div>
712
730
  {shouldShowBuilderReconnect && builderReconnect.error && (
@@ -913,42 +931,32 @@ export function PlanModeCallout({
913
931
  onSwitchToAct: () => void;
914
932
  }) {
915
933
  return (
916
- <div className="shrink-0 px-3 pt-2">
917
- <div className="rounded-lg border border-blue-500/25 bg-blue-500/[0.06] px-3 py-2.5 shadow-sm">
918
- <div className="flex items-center gap-2.5">
919
- <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-blue-500/10 text-blue-600 dark:text-blue-300">
920
- <IconClipboardList size={15} />
921
- </span>
922
- <div className="min-w-0 flex-1">
923
- <p className="text-sm font-medium text-foreground">
924
- {canImplementPlan ? "Plan ready" : "Plan mode is on"}
925
- </p>
926
- <p className="mt-0.5 text-xs leading-relaxed text-muted-foreground">
927
- {canImplementPlan
928
- ? "Switch to Act and run the proposed plan."
929
- : "The next turn will stay read-only until you switch to Act."}
930
- </p>
931
- </div>
932
- {canImplementPlan ? (
933
- <button
934
- type="button"
935
- onClick={onImplementPlan}
936
- className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md bg-foreground px-3 text-xs font-medium text-background hover:opacity-90"
937
- >
938
- <IconPlayerPlay size={13} />
939
- Implement Plan
940
- </button>
941
- ) : (
942
- <button
943
- type="button"
944
- onClick={onSwitchToAct}
945
- className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md border border-border bg-background px-3 text-xs font-medium text-foreground hover:bg-accent"
946
- >
947
- Act
948
- <IconArrowRight size={13} />
949
- </button>
950
- )}
951
- </div>
934
+ <div className="shrink-0 px-3 pt-1">
935
+ <div className="ml-auto flex w-fit max-w-full items-center gap-2 rounded-full border border-border/70 bg-background/95 px-2 py-1.5 text-xs text-muted-foreground shadow-sm">
936
+ <IconClipboardList size={13} className="shrink-0" />
937
+ <span className="min-w-0 truncate">
938
+ {canImplementPlan ? "Plan ready" : "Plan mode"}
939
+ </span>
940
+ {canImplementPlan ? (
941
+ <button
942
+ type="button"
943
+ onClick={onImplementPlan}
944
+ className="inline-flex h-6 shrink-0 items-center gap-1 rounded-full bg-foreground px-2.5 text-[11px] font-medium text-background hover:opacity-90"
945
+ >
946
+ <IconPlayerPlay size={12} />
947
+ Implement
948
+ </button>
949
+ ) : (
950
+ <button
951
+ type="button"
952
+ onClick={onSwitchToAct}
953
+ className="inline-flex h-6 shrink-0 items-center gap-1 rounded-full border border-border bg-background px-2.5 text-[11px] font-medium text-foreground hover:bg-accent"
954
+ aria-label="Switch to Act mode"
955
+ >
956
+ Act
957
+ <IconArrowRight size={12} />
958
+ </button>
959
+ )}
952
960
  </div>
953
961
  </div>
954
962
  );
@@ -692,10 +692,12 @@ export function ToolCallFallback({
692
692
  mcpApp?: AgentMcpAppPayload;
693
693
  chatUI?: ActionChatUIConfig;
694
694
  structuredMeta?: Record<string, unknown>;
695
+ activity?: boolean;
695
696
  approval?: { approvalKey: string; dismissed?: boolean };
696
697
  }) {
697
698
  const chatRunning = React.useContext(ChatRunningContext);
698
- const isRunning = result === undefined && chatRunning;
699
+ const isRunning =
700
+ result === undefined && (chatRunning || rest.activity === true);
699
701
  return (
700
702
  <ToolCallDisplay
701
703
  toolName={toolName}
@@ -757,7 +759,10 @@ export function ReconnectStreamMessage({
757
759
  mcpApp={part.mcpApp}
758
760
  chatUI={part.chatUI}
759
761
  structuredMeta={part.structuredMeta}
760
- isRunning={part.result === undefined && chatRunning}
762
+ isRunning={
763
+ part.result === undefined &&
764
+ (chatRunning || part.activity === true)
765
+ }
761
766
  approval={part.approval}
762
767
  />
763
768
  );
@@ -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 = !bypass && !isLoading && !session && redirect;
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
- trigger?: "label" | "icon";
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 ? <IconShare3 size={16} strokeWidth={1.75} /> : null}
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.execute({
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.execute({
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.execute({
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.execute({
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.execute({
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
  });
@@ -7694,6 +7694,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
7694
7694
  nextTitle,
7695
7695
  nextPreview,
7696
7696
  newMessageCount,
7697
+ { ignoreConflicts: true },
7697
7698
  );
7698
7699
  // Scope updates piggyback on the PUT — the client uses this
7699
7700
  // path for both "detach" (scope: null) and "retag" flows.
@@ -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 "/";
@@ -1,6 +1,7 @@
1
1
  export const SMOOTH_STREAMING_COMMIT_INTERVAL_MS = 16;
2
2
  export const SMOOTH_STREAMING_LONG_TEXT_THRESHOLD_GRAPHEMES = 640;
3
3
  export const SMOOTH_STREAMING_LONG_TEXT_TAIL_GRAPHEMES = 180;
4
+ const SMOOTH_STREAMING_SPEED_MULTIPLIER = 2;
4
5
 
5
6
  type SegmenterInstance = {
6
7
  segment(input: string): Iterable<{ segment: string }>;
@@ -137,7 +138,7 @@ export function smoothStreamingRevealCount({
137
138
  return 0;
138
139
  }
139
140
 
140
- const charactersPerSecond = inputDone
141
+ const baseCharactersPerSecond = inputDone
141
142
  ? backlog > 800
142
143
  ? 900
143
144
  : 420
@@ -148,8 +149,12 @@ export function smoothStreamingRevealCount({
148
149
  : backlog > 180
149
150
  ? 190
150
151
  : 95;
152
+ const charactersPerSecond =
153
+ baseCharactersPerSecond * SMOOTH_STREAMING_SPEED_MULTIPLIER;
151
154
 
152
- const maxBurst = inputDone ? 160 : backlog > 1400 ? 120 : 72;
155
+ const maxBurst =
156
+ (inputDone ? 160 : backlog > 1400 ? 120 : 72) *
157
+ SMOOTH_STREAMING_SPEED_MULTIPLIER;
153
158
  const count = Math.floor((elapsedMs / 1000) * charactersPerSecond);
154
159
 
155
160
  return Math.min(backlog, Math.max(1, count), maxBurst);
@@ -164,15 +169,15 @@ export function smoothStreamingPunctuationDelayMs(
164
169
  }
165
170
 
166
171
  if (grapheme === "\n") {
167
- return 80;
172
+ return 80 / SMOOTH_STREAMING_SPEED_MULTIPLIER;
168
173
  }
169
174
 
170
175
  if (/[.!?)]/.test(grapheme)) {
171
- return 70;
176
+ return 70 / SMOOTH_STREAMING_SPEED_MULTIPLIER;
172
177
  }
173
178
 
174
179
  if (/[,;:]/.test(grapheme)) {
175
- return 35;
180
+ return 35 / SMOOTH_STREAMING_SPEED_MULTIPLIER;
176
181
  }
177
182
 
178
183
  return 0;
@@ -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 --includeLogo=true` runs, the prompt envelope adds:
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
- 3. Gemini returns an image with empty space in that corner.
16
- 4. `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.
17
- 5. Output: the image with the actual logo, pixel-perfect, vector-quality if the source is SVG.
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 explicitly toggled "Use logo" in the Generate popover.
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
- settings: parseJson<Record<string, unknown>>(row.settings, {}),
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(args.settings ?? {}),
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.default(false),
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.boolean().default(false),
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.boolean().default(false),
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<{ tier?: ImageQualityTier }>(
263
- preset?.settings,
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: args.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: args.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: args.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 (args.includeLogo && library.canonicalLogoAssetId) {
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: args.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: args.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
- .default(false)
76
- .describe("Whether auto-generation should include the library logo."),
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()
@@ -4,7 +4,7 @@ import { eq } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
- import { nowIso, stringifyJson } from "../server/lib/json.js";
7
+ import { nowIso, parseJson, stringifyJson } from "../server/lib/json.js";
8
8
  import {
9
9
  ASPECT_RATIOS,
10
10
  GENERATION_PRESET_REFERENCE_POLICIES,
@@ -29,6 +29,12 @@ export default defineAction({
29
29
  model: z.enum(IMAGE_MODELS).optional(),
30
30
  textPolicy: z.string().optional(),
31
31
  referencePolicy: z.enum(GENERATION_PRESET_REFERENCE_POLICIES).optional(),
32
+ includeLogo: z.coerce
33
+ .boolean()
34
+ .optional()
35
+ .describe(
36
+ "When true, images generated with this preset composite the library's canonical logo (no-op if the library has no canonical logo).",
37
+ ),
32
38
  settings: z.record(z.string(), z.unknown()).optional(),
33
39
  sortOrder: z.coerce.number().optional(),
34
40
  }),
@@ -67,8 +73,15 @@ export default defineAction({
67
73
  ] as const) {
68
74
  if (args[key] !== undefined) updates[key] = args[key];
69
75
  }
70
- if (args.settings !== undefined) {
71
- updates.settings = stringifyJson(args.settings);
76
+ if (args.settings !== undefined || args.includeLogo !== undefined) {
77
+ const nextSettings = {
78
+ ...parseJson<Record<string, unknown>>(preset.settings, {}),
79
+ ...(args.settings ?? {}),
80
+ };
81
+ if (args.includeLogo !== undefined) {
82
+ nextSettings.includeLogo = args.includeLogo;
83
+ }
84
+ updates.settings = stringifyJson(nextSettings);
72
85
  }
73
86
  await db
74
87
  .update(schema.assetGenerationPresets)