@agent-native/core 0.190.1-nightly-20260926035143 → 0.191.0

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 (109) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +1 -0
  3. package/corpus/templates/calendar/CHANGELOG.md +2 -0
  4. package/corpus/templates/chat/app/root.tsx +1 -1
  5. package/corpus/templates/clips/AGENTS.md +7 -6
  6. package/corpus/templates/clips/CHANGELOG.md +7 -0
  7. package/corpus/templates/clips/actions/complete-workflow.ts +126 -0
  8. package/corpus/templates/clips/actions/generate-workflow.ts +72 -33
  9. package/corpus/templates/clips/actions/reconcile-workflow-generation.ts +19 -6
  10. package/corpus/templates/clips/actions/set-organization-branding.ts +5 -5
  11. package/corpus/templates/clips/app/components/clips-command-menu.tsx +9 -1
  12. package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +41 -27
  13. package/corpus/templates/clips/app/components/workspace/organization-identity-card.tsx +7 -2
  14. package/corpus/templates/clips/app/hooks/use-auto-title.ts +37 -16
  15. package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
  16. package/corpus/templates/clips/app/lib/organization-logo.ts +17 -0
  17. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +9 -1
  18. package/corpus/templates/clips/app/routes/share.$shareId.tsx +17 -26
  19. package/corpus/templates/clips/changelog/2026-09-25-clips-clears-a-stale-recording-overlay-after-an-interrupted-.md +6 -0
  20. package/corpus/templates/clips/changelog/2026-09-25-compact-no-comments-signup-prompt.md +6 -0
  21. package/corpus/templates/clips/changelog/2026-09-25-concurrent-workflow-requests-no-longer-replace-an-active-gen.md +6 -0
  22. package/corpus/templates/clips/changelog/2026-09-25-generated-workflows-stay-tied-to-the-request-that-created-th.md +6 -0
  23. package/corpus/templates/clips/changelog/2026-09-25-organization-logos-can-be-stored-privately-and-load-in-setti.md +6 -0
  24. package/corpus/templates/clips/changelog/2026-09-25-private-minio-and-s3-compatible-endpoints-now-work-with-clip.md +6 -0
  25. package/corpus/templates/clips/changelog/2026-09-25-workflow-drafts-are-saved-before-the-agent-reports-them-comp.md +6 -0
  26. package/corpus/templates/clips/chrome-extension/src/background.ts +45 -13
  27. package/corpus/templates/clips/chrome-extension/src/native-recording-state.ts +17 -0
  28. package/corpus/templates/clips/server/db/index.ts +7 -1
  29. package/corpus/templates/clips/server/jobs/transactional-emails.ts +8 -1
  30. package/corpus/templates/clips/server/lib/organization-logo.ts +57 -0
  31. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +240 -15
  32. package/corpus/templates/clips/server/plugins/agent-chat.ts +1 -0
  33. package/corpus/templates/clips/server/plugins/onboarding.ts +6 -1
  34. package/corpus/templates/clips/server/routes/api/media/index.post.ts +36 -16
  35. package/corpus/templates/clips/server/routes/api/media/organization-logo/[organizationId].get.ts +181 -0
  36. package/corpus/templates/clips/shared/organization-logo.ts +51 -0
  37. package/corpus/templates/clips/shared/workflow.ts +9 -0
  38. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +4 -2
  39. package/corpus/templates/design/CHANGELOG.md +6 -0
  40. package/corpus/templates/design/actions/export-html.ts +5 -1
  41. package/corpus/templates/design/actions/export-svg.ts +5 -1
  42. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +31 -6
  43. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +33 -31
  44. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +39 -19
  45. package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +2 -0
  46. package/corpus/templates/design/app/components/design/inspector/InspectorControlPopover.tsx +4 -1
  47. package/corpus/templates/design/app/pages/DesignSystems.tsx +10 -7
  48. package/corpus/templates/design/changelog/2026-09-25-builder-design-system-failures-preserve-readable-json-errors.md +6 -0
  49. package/corpus/templates/design/changelog/2026-09-25-design-html-export-stacks-screens.md +7 -0
  50. package/corpus/templates/design/changelog/2026-09-25-design-system-empty-states-show-one-creation-action.md +6 -0
  51. package/corpus/templates/design/changelog/2026-09-25-effect-values-stay-editable-after-blur-and-the-shader-picker.md +6 -0
  52. package/corpus/templates/design/changelog/2026-09-25-escape-cancels-unfinished-pen-paths-and-enter-selects-the-fi.md +6 -0
  53. package/corpus/templates/design/e2e/drag-and-drop.shared.ts +9 -12
  54. package/corpus/templates/design/e2e/helpers.ts +76 -23
  55. package/corpus/templates/design/server/handlers/design-system-decode-job-status.ts +2 -1
  56. package/corpus/templates/design/server/handlers/design-system-upload-start.ts +2 -1
  57. package/corpus/templates/design/server/handlers/index-design-system-sources.ts +3 -2
  58. package/corpus/templates/design/server/lib/design-export.ts +132 -13
  59. package/corpus/templates/slides/CHANGELOG.md +8 -0
  60. package/corpus/templates/slides/actions/get-deck.ts +149 -1
  61. package/corpus/templates/slides/actions/import-file.ts +29 -1
  62. package/corpus/templates/slides/actions/import-from-url.ts +7 -1
  63. package/corpus/templates/slides/actions/import-google-slides-reference.ts +7 -3
  64. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +5 -3
  65. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +451 -402
  66. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +6 -2
  67. package/corpus/templates/slides/app/components/editor/QuestionFlow.tsx +1 -1
  68. package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -0
  69. package/corpus/templates/slides/app/components/ui/progress.tsx +1 -0
  70. package/corpus/templates/slides/app/global.css +14 -0
  71. package/corpus/templates/slides/app/pages/DeckEditor.tsx +15 -0
  72. package/corpus/templates/slides/app/pages/Index.tsx +13 -9
  73. package/corpus/templates/slides/changelog/2026-09-25-agent-recognizes-blank-slides-and-uses-clear-prompts.md +6 -0
  74. package/corpus/templates/slides/changelog/2026-09-25-compact-deck-reads-handle-imported-slides-with-invalid-selec.md +6 -0
  75. package/corpus/templates/slides/changelog/2026-09-25-empty-decks-show-a-structured-slide-preview.md +6 -0
  76. package/corpus/templates/slides/changelog/2026-09-25-imports-handle-common-file-and-connection-errors.md +6 -0
  77. package/corpus/templates/slides/changelog/2026-09-25-keep-empty-deck-lists-clear-of-search-and-filter-controls.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-09-25-presentations-keep-slides-centered.md +6 -0
  79. package/corpus/templates/slides/changelog/2026-09-25-selected-design-system-source-forms-open-directly-beneath-th.md +6 -0
  80. package/corpus/templates/slides/changelog/2026-09-25-selected-presentation-options-keep-their-highlight-during-se.md +6 -0
  81. package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +2 -1
  82. package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +2 -1
  83. package/corpus/templates/slides/server/handlers/import/docx-parser.ts +30 -3
  84. package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +2 -1
  85. package/corpus/templates/slides/shared/deck-title.ts +14 -5
  86. package/dist/agent/production-agent.d.ts +3 -5
  87. package/dist/agent/production-agent.js +3 -5
  88. package/dist/client/chat/markdown-renderer.js +15 -1
  89. package/dist/collab/struct-routes.d.ts +1 -1
  90. package/dist/deploy/build.js +1 -1
  91. package/dist/extensions/url-safety.js +33 -2
  92. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  93. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  94. package/dist/notifications/routes.d.ts +6 -6
  95. package/dist/private-blob/index.d.ts +1 -1
  96. package/dist/private-blob/index.js +1 -1
  97. package/dist/private-blob/registry.d.ts +1 -0
  98. package/dist/private-blob/registry.js +22 -1
  99. package/dist/private-blob/types.d.ts +1 -0
  100. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  101. package/dist/server/core-routes-plugin.d.ts +1 -1
  102. package/dist/server/index.d.ts +1 -1
  103. package/dist/server/index.js +1 -1
  104. package/dist/server/workspace-provider-oauth.js +16 -2
  105. package/dist/templates/chat/app/root.tsx +1 -1
  106. package/dist/templates/default/app/root.tsx +1 -1
  107. package/package.json +4 -4
  108. package/src/templates/chat/app/root.tsx +1 -1
  109. package/src/templates/default/app/root.tsx +1 -1
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 10002
34
+ - template files: 10028
@@ -11,6 +11,7 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
11
11
 
12
12
  ### Improved
13
13
 
14
+ - The chat composer keeps a consistent background while AI setup is open.
14
15
  - First-run onboarding records setup choices and Builder connection outcomes
15
16
  - Human review previews Analytics dashboards as real charts
16
17
  - Relevant metric definitions and saved dashboard examples are available with the first response so Analytics queries can reuse proven definitions and query shapes.
@@ -11,6 +11,8 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
11
11
 
12
12
  ### Fixed
13
13
 
14
+ - Booking links without a meeting type now show a not-found page.
15
+ - Personal booking pages use the host's saved timezone for availability.
14
16
  - Booking link previews now show their image in browser-based link preview tools
15
17
  - Calendar feeds with the same URL keep their own names and colors.
16
18
  - Calendar invitation rules surface expired connections and undo safely without duplicate RSVP updates
@@ -57,7 +57,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
57
57
  <meta charSet="utf-8" />
58
58
  <meta
59
59
  name="viewport"
60
- content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
60
+ content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, interactive-widget=resizes-content"
61
61
  />
62
62
  <script
63
63
  suppressHydrationWarning
@@ -76,22 +76,23 @@ video sharing app. The agent and the UI share the same SQL data and actions.
76
76
  | --- | --- |
77
77
  | `view-screen`, `navigate` | Read context; open a surface |
78
78
  | `list-recordings`, `search-recordings` | Library, trash, `--view=shared` |
79
- | `get-recording-player-data` | Full transcript, chapters, diagnostics |
79
+ | `get-recording-player-data` | Transcript, chapters, diagnostics |
80
80
  | `create-recording`, `finalize-recording` | Create row; finish upload |
81
- | `import-loom-recording` | Import Loom or direct MP4/WebM URL |
81
+ | `import-loom-recording` | Import Loom or video URLs |
82
82
  | `update-recording` | Title, password, expiry, visibility |
83
- | `move-recording` | Move `id` or `ids` to a folder or root |
83
+ | `move-recording` | Move recordings to folder/root |
84
84
  | `archive-`, `trash-`, `restore-recording` | Lifecycle |
85
85
  | `reprocess-recording` | Repair unseekable/frozen media |
86
- | `generate-filmstrip` | Editor timeline frame sprite; `all` backfills |
86
+ | `generate-filmstrip` | Timeline frames |
87
87
  | `request-transcript`, `cleanup-transcript` | Transcribe; `force`/`regenerate` |
88
88
  | `regenerate-title`, `-summary`, `-chapters` | AI metadata |
89
+ | `generate-workflow`, `complete-workflow` | Generate and save workflow |
89
90
  | `trim-`, `split-recording`, `remove-silences`, `remove-filler-words` | Edits |
90
91
  | `list-meetings`, `get-`, `update-`, `finalize-meeting` | Meetings |
91
- | `search-meetings` | Find a meeting by title, summary, notes, attendee, or transcript |
92
+ | `search-meetings` | Search meetings |
92
93
  | `list-dictations`, `cleanup-dictation` | Dictation history |
93
94
  | `add-comment`, `update-comment`, `create-folder`, `create-space` | Comments, folders |
94
- | `get-clips-notification-prefs`, `update-clips-notification-prefs` | Read or change optional email notifications |
95
+ | `get-clips-notification-prefs`, `update-clips-notification-prefs` | Read or update email alerts |
95
96
  | `share-resource`, `set-resource-visibility`, `build-embed-url` | Share, embed |
96
97
  | `create-recording-agent-link` | Two-hour `agent_access` share URL |
97
98
  | `prepare-crm-call-evidence` | Opaque clip id plus `/r/<id>` for CRM |
@@ -7,10 +7,17 @@ from the command menu (Cmd+K → "What's new") or from Settings.
7
7
 
8
8
  ### Improved
9
9
 
10
+ - The no-comments sidebar gives viewers a concise reason to try Clips and a clear path to sign up.
10
11
  - Shared clips remember your sidebar choice and help new viewers understand why to sign up.
11
12
 
12
13
  ### Fixed
13
14
 
15
+ - Clips clears a stale recording overlay after an interrupted save
16
+ - Concurrent workflow requests no longer replace an active generation.
17
+ - Generated workflows stay tied to the request that created them.
18
+ - Organization logos can be stored privately and load in settings, shared clips, and email.
19
+ - Private MinIO and S3-compatible endpoints now work with Clips.
20
+ - Workflow drafts are saved before the agent reports them complete
14
21
  - Signed-in viewers who hit an unavailable, expired, or private share link now land in their library instead of the public marketing page when they choose "Go home."
15
22
 
16
23
  ## 2026-09-24
@@ -0,0 +1,126 @@
1
+ import { defineAction, fail } from "@agent-native/core/action";
2
+ import {
3
+ compareAndSetAppState,
4
+ readAppState,
5
+ } from "@agent-native/core/application-state";
6
+ import { assertAccess } from "@agent-native/core/sharing";
7
+ import { z } from "zod";
8
+
9
+ import {
10
+ matchesWorkflowRequest,
11
+ WorkflowKindSchema,
12
+ } from "../shared/workflow.js";
13
+
14
+ const WorkflowStateSchema = z
15
+ .object({
16
+ kind: WorkflowKindSchema.optional(),
17
+ status: z.string(),
18
+ recordingId: z.string().min(1),
19
+ requestedAt: z.string().min(1),
20
+ requestId: z.string().min(1).optional(),
21
+ })
22
+ .passthrough();
23
+
24
+ export default defineAction({
25
+ description:
26
+ "Save the final markdown for a Clips workflow request. Returns saved only after the matching request is persisted as ready.",
27
+ schema: z.object({
28
+ recordingId: z.string().min(1).describe("Recording that owns the workflow"),
29
+ requestedAt: z
30
+ .string()
31
+ .min(1)
32
+ .describe("Exact request timestamp from the generate-workflow context"),
33
+ requestId: z
34
+ .string()
35
+ .min(1)
36
+ .optional()
37
+ .describe(
38
+ "Exact workflow request ID when present in the request context",
39
+ ),
40
+ content: z
41
+ .string()
42
+ .trim()
43
+ .min(1)
44
+ .max(50_000)
45
+ .describe("Final workflow document in markdown"),
46
+ }),
47
+ run: async ({ recordingId, requestedAt, requestId, content }) => {
48
+ await assertAccess("recording", recordingId, "viewer");
49
+
50
+ const stateKey = `clips-workflow-${recordingId}`;
51
+ const current = await readAppState(stateKey);
52
+ if (current === null) {
53
+ fail("No active workflow request was found for this recording.", {
54
+ statusCode: 409,
55
+ errorCode: "workflow_not_active",
56
+ });
57
+ }
58
+
59
+ const parsed = WorkflowStateSchema.safeParse(current);
60
+ if (!parsed.success) {
61
+ throw new Error(`Invalid generated workflow state for ${recordingId}`);
62
+ }
63
+ if (parsed.data.recordingId !== recordingId) {
64
+ throw new Error(`Generated workflow state does not match ${recordingId}`);
65
+ }
66
+ const expected = { requestedAt, requestId };
67
+ if (!matchesWorkflowRequest(parsed.data, expected)) {
68
+ fail("This workflow request has been replaced by a newer request.", {
69
+ statusCode: 409,
70
+ errorCode: "stale_workflow_request",
71
+ });
72
+ }
73
+ const result = {
74
+ recordingId,
75
+ requestedAt,
76
+ ...(requestId ? { requestId } : {}),
77
+ };
78
+
79
+ if (parsed.data.status === "ready" && current.content === content) {
80
+ return { saved: true, ...result, alreadySaved: true };
81
+ }
82
+ if (parsed.data.status !== "generating") {
83
+ fail("This workflow request is no longer generating.", {
84
+ statusCode: 409,
85
+ errorCode: "workflow_not_generating",
86
+ });
87
+ }
88
+
89
+ const nextState = {
90
+ ...current,
91
+ status: "ready",
92
+ content,
93
+ completedAt: new Date().toISOString(),
94
+ };
95
+ const saved = await compareAndSetAppState(stateKey, current, nextState);
96
+ if (!saved) {
97
+ const latest = await readAppState(stateKey);
98
+ if (
99
+ latest?.status === "ready" &&
100
+ latest.recordingId === recordingId &&
101
+ matchesWorkflowRequest(latest, expected) &&
102
+ latest.content === content
103
+ ) {
104
+ return { saved: true, ...result, alreadySaved: true };
105
+ }
106
+ fail("The workflow changed before its result could be saved.", {
107
+ statusCode: 409,
108
+ errorCode: "workflow_changed",
109
+ });
110
+ }
111
+
112
+ const persisted = await readAppState(stateKey);
113
+ if (
114
+ persisted?.status !== "ready" ||
115
+ persisted.recordingId !== recordingId ||
116
+ !matchesWorkflowRequest(persisted, expected) ||
117
+ persisted.content !== content
118
+ ) {
119
+ throw new Error(
120
+ `Generated workflow was not persisted for ${recordingId}`,
121
+ );
122
+ }
123
+
124
+ return { saved: true, ...result };
125
+ },
126
+ });
@@ -7,15 +7,18 @@
7
7
  * - ticket — a bug/issue ticket
8
8
  * - email — a ready-to-send email
9
9
  *
10
- * The agent composes the document and stores it in application_state under
11
- * `clips-workflow-<recordingId>` so the UI can pick it up and display it.
10
+ * The agent composes the document and saves it with complete-workflow so the
11
+ * UI can pick it up and display it.
12
12
  *
13
13
  * Usage:
14
14
  * pnpm action generate-workflow --recordingId=<id> --kind=pr
15
15
  */
16
16
 
17
+ import { randomUUID } from "node:crypto";
18
+
17
19
  import { defineAction } from "@agent-native/core/action";
18
20
  import {
21
+ compareAndSetManyAppState,
19
22
  readAppState,
20
23
  writeAppState,
21
24
  } from "@agent-native/core/application-state";
@@ -55,14 +58,23 @@ Output markdown.`,
55
58
  Keep it concise, warm, and professional.`,
56
59
  } as const;
57
60
 
58
- // Actions for the same recording can arrive concurrently when the player and
59
- // agent both retry a request. Keep the read-check-write/enqueue sequence
60
- // single-flight within this runtime so only one request can claim generation.
61
+ // Skip duplicate database work in this runtime; the multi-key CAS below also
62
+ // prevents separate runtimes from claiming the same generation.
61
63
  const workflowGenerationLocks = new Set<string>();
62
64
 
65
+ function isRecentGeneration(state: Record<string, unknown> | null): boolean {
66
+ if (state?.status !== "generating") return false;
67
+ const requestedAt = Date.parse(
68
+ typeof state.requestedAt === "string" ? state.requestedAt : "",
69
+ );
70
+ return (
71
+ !Number.isFinite(requestedAt) || Date.now() - requestedAt < 10 * 60_000
72
+ );
73
+ }
74
+
63
75
  export default defineAction({
64
76
  description:
65
- "Ask the agent to generate a structured workflow doc (pr/sop/ticket/email) from this recording's transcript (and the full video when Include full video is enabled). The agent writes the result to clips-workflow-<recordingId> in application_state.",
77
+ "Ask the agent to generate a structured workflow doc (pr/sop/ticket/email) from this recording's transcript (and the full video when Include full video is enabled). The agent saves the result with complete-workflow.",
66
78
  schema: z.object({
67
79
  recordingId: z.string().describe("Recording ID"),
68
80
  kind: WorkflowKindSchema.describe("Workflow kind"),
@@ -104,47 +116,44 @@ export default defineAction({
104
116
  const includeFullVideoInAi = await readIncludeFullVideoInAi();
105
117
 
106
118
  const existing = await readAppState(stateKey);
107
- if (existing?.status === "generating") {
108
- const requestedAt = Date.parse(
109
- typeof existing.requestedAt === "string" ? existing.requestedAt : "",
110
- );
111
- const isRecent =
112
- !Number.isFinite(requestedAt) ||
113
- Date.now() - requestedAt < 10 * 60_000;
114
- if (isRecent) {
115
- return {
116
- queued: false,
117
- duplicate: true,
118
- recordingId: args.recordingId,
119
- kind: existing.kind ?? args.kind,
120
- stateKey,
121
- };
122
- }
119
+ if (isRecentGeneration(existing)) {
120
+ return {
121
+ queued: false,
122
+ duplicate: true,
123
+ recordingId: args.recordingId,
124
+ kind: existing?.kind ?? args.kind,
125
+ stateKey,
126
+ };
123
127
  }
124
128
 
129
+ const requestKey = `clips-ai-request-${args.recordingId}`;
130
+ const existingRequest = await readAppState(requestKey);
125
131
  const requestedAt = new Date().toISOString();
126
- // Seed the output state with a "generating" placeholder so the UI can show
127
- // a loading state immediately.
128
- await writeAppState(stateKey, {
132
+ const requestId = randomUUID();
133
+ const workflowState = {
129
134
  kind: args.kind,
130
135
  status: "generating",
131
136
  recordingId: args.recordingId,
132
137
  requestedAt,
133
- } as any);
138
+ requestId,
139
+ };
134
140
 
135
141
  const baseMessage =
136
142
  `Generate a ${args.kind.toUpperCase()} workflow document from recording ${args.recordingId} ` +
137
143
  `(title: "${rec.title}"). Read the transcript from this request's context. ` +
138
144
  `${KIND_PROMPTS[args.kind]} ` +
139
- `Then write the final markdown to application_state key "${stateKey}" as ` +
140
- `\`{ kind: "${args.kind}", status: "ready", content: "...", recordingId: "${args.recordingId}" }\`. ` +
141
- `Finish by replying in chat with the same generated markdown so the user can read it immediately.`;
145
+ `Then call complete-workflow with recordingId "${args.recordingId}", ` +
146
+ `the exact requestedAt and requestId "${requestId}" from this request's context, ` +
147
+ `and the final markdown. ` +
148
+ `Do not report completion unless that action returns saved: true. ` +
149
+ `Finish by replying in chat with the same generated markdown.`;
142
150
 
143
151
  const request = {
144
152
  kind: "generate-workflow" as const,
145
153
  workflowKind: args.kind,
146
154
  recordingId: args.recordingId,
147
155
  requestedAt,
156
+ requestId,
148
157
  recordingTitle: rec.title,
149
158
  recordingDescription: rec.description,
150
159
  transcriptStatus: transcript?.status ?? "pending",
@@ -160,10 +169,40 @@ export default defineAction({
160
169
  ),
161
170
  };
162
171
 
163
- await writeAppState(
164
- `clips-ai-request-${args.recordingId}`,
165
- request as any,
166
- );
172
+ const claimed = await compareAndSetManyAppState([
173
+ {
174
+ key: stateKey,
175
+ expectedValue: existing,
176
+ nextValue: workflowState,
177
+ },
178
+ {
179
+ key: requestKey,
180
+ expectedValue: existingRequest,
181
+ nextValue: request,
182
+ },
183
+ ]);
184
+ if (!claimed) {
185
+ const current = await readAppState(stateKey);
186
+ if (isRecentGeneration(current)) {
187
+ return {
188
+ queued: false,
189
+ duplicate: true,
190
+ recordingId: args.recordingId,
191
+ kind: current?.kind ?? args.kind,
192
+ stateKey,
193
+ };
194
+ }
195
+ return {
196
+ queued: false,
197
+ duplicate: false,
198
+ retry: true,
199
+ reason: "claim-contended",
200
+ recordingId: args.recordingId,
201
+ kind: args.kind,
202
+ stateKey,
203
+ };
204
+ }
205
+
167
206
  await writeAppState("refresh-signal", { ts: Date.now() });
168
207
 
169
208
  console.log(
@@ -7,7 +7,10 @@ import {
7
7
  import { assertAccess } from "@agent-native/core/sharing";
8
8
  import { z } from "zod";
9
9
 
10
- import { WorkflowKindSchema } from "../shared/workflow.js";
10
+ import {
11
+ matchesWorkflowRequest,
12
+ WorkflowKindSchema,
13
+ } from "../shared/workflow.js";
11
14
 
12
15
  const WorkflowStateSchema = z
13
16
  .object({
@@ -16,6 +19,7 @@ const WorkflowStateSchema = z
16
19
  content: z.string().optional(),
17
20
  recordingId: z.string().optional(),
18
21
  requestedAt: z.string().optional(),
22
+ requestId: z.string().min(1).optional(),
19
23
  tabId: z.string().optional(),
20
24
  claimedAt: z.string().optional(),
21
25
  })
@@ -24,6 +28,7 @@ const WorkflowStateSchema = z
24
28
  const WorkflowRequestSchema = z
25
29
  .object({
26
30
  requestedAt: z.string(),
31
+ requestId: z.string().min(1).optional(),
27
32
  deliveredAt: z.string().optional(),
28
33
  deliveredTabId: z.string().optional(),
29
34
  })
@@ -45,9 +50,10 @@ export default defineAction({
45
50
  ]),
46
51
  recordingId: z.string().min(1),
47
52
  requestedAt: z.string().min(1),
53
+ requestId: z.string().min(1).optional(),
48
54
  tabId: z.string().min(1),
49
55
  }),
50
- run: async ({ operation, recordingId, requestedAt, tabId }) => {
56
+ run: async ({ operation, recordingId, requestedAt, requestId, tabId }) => {
51
57
  await assertAccess("recording", recordingId, "viewer");
52
58
 
53
59
  const requestKey = `clips-ai-request-${recordingId}`;
@@ -62,7 +68,9 @@ export default defineAction({
62
68
  if (!parsedRequest.success) {
63
69
  throw new Error(`Invalid workflow request state for ${recordingId}`);
64
70
  }
65
- if (parsedRequest.data.requestedAt !== requestedAt) {
71
+ if (
72
+ !matchesWorkflowRequest(parsedRequest.data, { requestedAt, requestId })
73
+ ) {
66
74
  return operation === "consume"
67
75
  ? {
68
76
  reconciled: false,
@@ -119,7 +127,10 @@ export default defineAction({
119
127
  };
120
128
  }
121
129
  if (
122
- parsedState.data.requestedAt !== requestedAt ||
130
+ !matchesWorkflowRequest(parsedState.data, {
131
+ requestedAt,
132
+ requestId,
133
+ }) ||
123
134
  parsedState.data.tabId !== tabId
124
135
  ) {
125
136
  return {
@@ -178,7 +189,7 @@ export default defineAction({
178
189
  if (state.status !== "generating") {
179
190
  return { reconciled: false, reason: "terminal" as const };
180
191
  }
181
- if (state.requestedAt !== requestedAt) {
192
+ if (!matchesWorkflowRequest(state, { requestedAt, requestId })) {
182
193
  return { reconciled: false, reason: "newer-request" as const };
183
194
  }
184
195
 
@@ -209,7 +220,9 @@ export default defineAction({
209
220
  if (!parsedRequest.success) {
210
221
  throw new Error(`Invalid workflow request state for ${recordingId}`);
211
222
  }
212
- if (parsedRequest.data.requestedAt !== requestedAt) {
223
+ if (
224
+ !matchesWorkflowRequest(parsedRequest.data, { requestedAt, requestId })
225
+ ) {
213
226
  return { reconciled: false, reason: "newer-request" as const };
214
227
  }
215
228
 
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Update organization branding — org name, brand color, brand logo URL,
2
+ * Update organization branding — org name, brand color, brand logo reference,
3
3
  * default visibility — by updating the framework `organizations` row for the
4
4
  * name and upserting the Clips-specific `organization_settings` sidecar row.
5
5
  *
6
6
  * Usage:
7
- * pnpm action set-organization-branding --brandColor="#18181B" --brandLogoUrl=/api/media/abc.png
7
+ * pnpm action set-organization-branding --brandColor=<hex-color> --brandLogoUrl=clips-org-logo:v1:<reference>
8
8
  */
9
9
 
10
10
  import { defineAction } from "@agent-native/core/action";
@@ -20,7 +20,7 @@ const VisibilityEnum = z.enum(["private", "org", "public"]);
20
20
 
21
21
  export default defineAction({
22
22
  description:
23
- "Update the active organization's Clips branding — brand color (e.g. #18181B), brand logo URL, and default recording visibility. Upserts the organization_settings sidecar row.",
23
+ "Update the active organization's Clips branding — brand color, brand logo reference, and default recording visibility. Upserts the organization_settings sidecar row.",
24
24
  schema: z.object({
25
25
  organizationId: z
26
26
  .string()
@@ -37,11 +37,11 @@ export default defineAction({
37
37
  .string()
38
38
  .regex(/^#[0-9a-fA-F]{3,8}$/)
39
39
  .optional()
40
- .describe("Hex color (e.g. #18181B)"),
40
+ .describe("Hex color"),
41
41
  brandLogoUrl: z
42
42
  .string()
43
43
  .nullish()
44
- .describe("URL of the logo image — pass null to clear"),
44
+ .describe("Stored logo reference or legacy URL — pass null to clear"),
45
45
  defaultVisibility: VisibilityEnum.optional().describe(
46
46
  "Default visibility for new recordings",
47
47
  ),
@@ -446,7 +446,15 @@ export function ClipsCommandMenu({
446
446
  <CommandMenu
447
447
  open={open}
448
448
  onOpenChange={onOpenChange}
449
- changelog={changelog}
449
+ changelog={changelog
450
+ .split(
451
+ "The no-comments sidebar gives viewers a concise reason to try Clips and a clear path to sign up.",
452
+ )
453
+ .join(t("settings.changelogCommentSignup"))
454
+ .split(
455
+ 'Signed-in viewers who hit an unavailable, expired, or private share link now land in their library instead of the public marketing page when they choose "Go home."',
456
+ )
457
+ .join(t("settings.changelogShareLink"))}
450
458
  changelogLabel={t("settings.whatsNew")}
451
459
  changelogKey="clips"
452
460
  renderResults={renderResults}
@@ -1,4 +1,4 @@
1
- import { appBasePath } from "@agent-native/core/client/api-path";
1
+ import { appApiPath } from "@agent-native/core/client/api-path";
2
2
  import { useActionMutation } from "@agent-native/core/client/hooks";
3
3
  import { useT } from "@agent-native/core/client/i18n";
4
4
  import { IconPalette, IconPhoto } from "@tabler/icons-react";
@@ -17,6 +17,7 @@ import {
17
17
  SelectTrigger,
18
18
  SelectValue,
19
19
  } from "@/components/ui/select";
20
+ import { organizationLogoUrl } from "@/lib/organization-logo";
20
21
 
21
22
  export type RecordingVisibility = "private" | "org" | "public";
22
23
 
@@ -41,22 +42,23 @@ const PRESETS = [
41
42
  "#111827",
42
43
  ];
43
44
 
44
- async function uploadLogo(file: File): Promise<string> {
45
+ async function uploadLogo(file: File, organizationId: string): Promise<string> {
45
46
  const body = await file.arrayBuffer();
46
- const res = await fetch(
47
- `${appBasePath()}/api/media?filename=${encodeURIComponent(file.name)}`,
48
- {
49
- method: "POST",
50
- body,
51
- headers: { "Content-Type": file.type || "application/octet-stream" },
52
- },
53
- );
47
+ const query = new URLSearchParams({
48
+ organizationId,
49
+ filename: file.name,
50
+ });
51
+ const res = await fetch(appApiPath(`/api/media?${query}`), {
52
+ method: "POST",
53
+ body,
54
+ headers: { "Content-Type": file.type || "application/octet-stream" },
55
+ });
54
56
  if (!res.ok) {
55
57
  throw new Error(`Upload failed (${res.status})`);
56
58
  }
57
- const json = (await res.json()) as { url?: string };
58
- if (!json.url) throw new Error("Upload returned no URL");
59
- return json.url;
59
+ const json = (await res.json()) as { reference?: string };
60
+ if (!json.reference) throw new Error("Upload returned no reference");
61
+ return json.reference;
60
62
  }
61
63
 
62
64
  export function BrandingEditor({
@@ -77,6 +79,14 @@ export function BrandingEditor({
77
79
  useState<RecordingVisibility>(initialDefaultVisibility);
78
80
  const [uploading, setUploading] = useState(false);
79
81
  const [dragging, setDragging] = useState(false);
82
+ const [uploadPreviewUrl, setUploadPreviewUrl] = useState<string | null>(null);
83
+
84
+ useEffect(
85
+ () => () => {
86
+ if (uploadPreviewUrl) URL.revokeObjectURL(uploadPreviewUrl);
87
+ },
88
+ [uploadPreviewUrl],
89
+ );
80
90
 
81
91
  // The last-saved values the Save button compares against to show the
82
92
  // dirty state. Re-seeded after every successful save so the button
@@ -101,6 +111,8 @@ export function BrandingEditor({
101
111
  brandColor !== savedState.brandColor ||
102
112
  brandLogoUrl !== savedState.brandLogoUrl ||
103
113
  defaultVisibility !== savedState.defaultVisibility;
114
+ const logoUrl =
115
+ uploadPreviewUrl ?? organizationLogoUrl(brandLogoUrl, organizationId);
104
116
 
105
117
  const qc = useQueryClient();
106
118
  const save = useActionMutation<
@@ -119,12 +131,14 @@ export function BrandingEditor({
119
131
  toast.error(t("brandingEditor.uploadImageFile"));
120
132
  return;
121
133
  }
134
+ setUploadPreviewUrl(URL.createObjectURL(file));
122
135
  try {
123
136
  setUploading(true);
124
- const url = await uploadLogo(file);
125
- setBrandLogoUrl(url);
137
+ const reference = await uploadLogo(file, organizationId);
138
+ setBrandLogoUrl(reference);
126
139
  toast.success(t("brandingEditor.logoUploaded"));
127
140
  } catch (err) {
141
+ setUploadPreviewUrl(null);
128
142
  toast.error(
129
143
  err instanceof Error ? err.message : t("brandingEditor.uploadFailed"),
130
144
  );
@@ -144,6 +158,7 @@ export function BrandingEditor({
144
158
  defaultVisibility,
145
159
  });
146
160
  setSavedState({ name, brandColor, brandLogoUrl, defaultVisibility });
161
+ setUploadPreviewUrl(null);
147
162
  toast.success(t("brandingEditor.brandingUpdated"));
148
163
  void qc.invalidateQueries({
149
164
  queryKey: ["action", "list-organization-state"],
@@ -234,12 +249,12 @@ export function BrandingEditor({
234
249
  <div
235
250
  className="h-14 w-14 rounded-md flex items-center justify-center border bg-muted/30"
236
251
  style={{
237
- background: brandLogoUrl ? undefined : brandColor + "20",
252
+ background: logoUrl ? undefined : brandColor + "20",
238
253
  }}
239
254
  >
240
- {brandLogoUrl ? (
255
+ {logoUrl ? (
241
256
  <img
242
- src={brandLogoUrl}
257
+ src={logoUrl}
243
258
  alt={t("brandingEditor.logoPreview")}
244
259
  className="max-h-12 max-w-12 object-contain"
245
260
  />
@@ -278,7 +293,10 @@ export function BrandingEditor({
278
293
  type="button"
279
294
  variant="ghost"
280
295
  size="sm"
281
- onClick={() => setBrandLogoUrl(null)}
296
+ onClick={() => {
297
+ setBrandLogoUrl(null);
298
+ setUploadPreviewUrl(null);
299
+ }}
282
300
  disabled={disabled}
283
301
  >
284
302
  {t("brandingEditor.remove")}
@@ -328,9 +346,9 @@ export function BrandingEditor({
328
346
  className="rounded-md p-3 flex items-center gap-3 text-white"
329
347
  style={{ background: brandColor }}
330
348
  >
331
- {brandLogoUrl ? (
349
+ {logoUrl ? (
332
350
  <img
333
- src={brandLogoUrl}
351
+ src={logoUrl}
334
352
  alt=""
335
353
  className="h-8 w-8 rounded bg-white/90 object-contain p-1"
336
354
  />
@@ -350,12 +368,8 @@ export function BrandingEditor({
350
368
  {t("brandingEditor.emailHeaderPreview")}
351
369
  </div>
352
370
  <div className="rounded-md border border-[#27272a] bg-[#0a0a0c] p-4 flex items-center justify-center gap-2">
353
- {brandLogoUrl ? (
354
- <img
355
- src={brandLogoUrl}
356
- alt=""
357
- className="h-7 w-7 object-contain"
358
- />
371
+ {logoUrl ? (
372
+ <img src={logoUrl} alt="" className="h-7 w-7 object-contain" />
359
373
  ) : (
360
374
  <div
361
375
  className="h-7 w-7 rounded bg-white/90 flex items-center justify-center font-semibold text-[12px]"