@agent-native/core 0.102.2 → 0.103.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 (206) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/docs/content/local-file-mode.mdx +99 -489
  4. package/corpus/core/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
  5. package/corpus/core/docs/content/locales/ar-SA/template-content.mdx +10 -24
  6. package/corpus/core/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
  7. package/corpus/core/docs/content/locales/de-DE/template-content.mdx +10 -24
  8. package/corpus/core/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
  9. package/corpus/core/docs/content/locales/es-ES/template-content.mdx +10 -24
  10. package/corpus/core/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
  11. package/corpus/core/docs/content/locales/fr-FR/template-content.mdx +10 -24
  12. package/corpus/core/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
  13. package/corpus/core/docs/content/locales/hi-IN/template-content.mdx +10 -24
  14. package/corpus/core/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
  15. package/corpus/core/docs/content/locales/ja-JP/template-content.mdx +10 -24
  16. package/corpus/core/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
  17. package/corpus/core/docs/content/locales/ko-KR/template-content.mdx +10 -24
  18. package/corpus/core/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
  19. package/corpus/core/docs/content/locales/pt-BR/template-content.mdx +10 -24
  20. package/corpus/core/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
  21. package/corpus/core/docs/content/locales/zh-CN/template-content.mdx +9 -24
  22. package/corpus/core/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
  23. package/corpus/core/docs/content/locales/zh-TW/template-content.mdx +9 -24
  24. package/corpus/core/docs/content/template-content.mdx +26 -42
  25. package/corpus/core/package.json +1 -1
  26. package/corpus/core/src/cli/content-local.ts +86 -18
  27. package/corpus/core/src/cli/index.ts +2 -2
  28. package/corpus/core/src/cli/skills-content/content-skill.ts +36 -25
  29. package/corpus/core/src/cli/skills.ts +47 -13
  30. package/corpus/core/src/local-artifacts/index.ts +65 -0
  31. package/corpus/templates/assets/app/components/library/GenerationPresetsPanel.tsx +325 -0
  32. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +12 -0
  33. package/corpus/templates/assets/app/i18n/zh-TW.ts +21 -0
  34. package/corpus/templates/assets/app/i18n-data.ts +215 -0
  35. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +19 -479
  36. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1 -1
  37. package/corpus/templates/assets/app/routes/brand-kits.$id_.settings.tsx +446 -0
  38. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +12 -2
  39. package/corpus/templates/clips/app/components/player/settings-panel.tsx +30 -12
  40. package/corpus/templates/clips/app/components/player/video-player.tsx +5 -1
  41. package/corpus/templates/clips/app/components/recorder/microphone-visualizer.tsx +22 -3
  42. package/corpus/templates/clips/app/i18n/ar-SA.ts +3 -2
  43. package/corpus/templates/clips/app/i18n/de-DE.ts +4 -2
  44. package/corpus/templates/clips/app/i18n/en-US.ts +3 -2
  45. package/corpus/templates/clips/app/i18n/es-ES.ts +4 -2
  46. package/corpus/templates/clips/app/i18n/fr-FR.ts +3 -2
  47. package/corpus/templates/clips/app/i18n/hi-IN.ts +3 -2
  48. package/corpus/templates/clips/app/i18n/ja-JP.ts +4 -1
  49. package/corpus/templates/clips/app/i18n/ko-KR.ts +3 -2
  50. package/corpus/templates/clips/app/i18n/pt-BR.ts +4 -2
  51. package/corpus/templates/clips/app/i18n/zh-CN.ts +3 -2
  52. package/corpus/templates/clips/app/i18n/zh-TW.ts +3 -2
  53. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +27 -1
  54. package/corpus/templates/clips/server/lib/share-password.ts +6 -3
  55. package/corpus/templates/content/.agents/skills/content/SKILL.md +26 -39
  56. package/corpus/templates/content/AGENTS.md +25 -30
  57. package/corpus/templates/content/actions/_builder-docs-client.ts +23 -0
  58. package/corpus/templates/content/actions/_content-database-source-adapters.ts +21 -3
  59. package/corpus/templates/content/actions/_content-files.ts +338 -0
  60. package/corpus/templates/content/actions/_content-space-access.ts +135 -0
  61. package/corpus/templates/content/actions/_content-spaces.ts +640 -0
  62. package/corpus/templates/content/actions/_database-source-utils.ts +90 -7
  63. package/corpus/templates/content/actions/_database-utils.ts +7 -1
  64. package/corpus/templates/content/actions/_local-file-documents.ts +0 -9
  65. package/corpus/templates/content/actions/_local-folder-source.ts +53 -0
  66. package/corpus/templates/content/actions/_property-utils.ts +9 -4
  67. package/corpus/templates/content/actions/add-database-item.ts +108 -65
  68. package/corpus/templates/content/actions/backfill-content-files.ts +22 -0
  69. package/corpus/templates/content/actions/connect-local-folder-source.ts +221 -0
  70. package/corpus/templates/content/actions/create-content-database.ts +129 -3
  71. package/corpus/templates/content/actions/create-document.ts +65 -55
  72. package/corpus/templates/content/actions/create-inline-content-database.ts +6 -1
  73. package/corpus/templates/content/actions/delete-content-database.ts +3 -0
  74. package/corpus/templates/content/actions/delete-document.ts +7 -11
  75. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +175 -0
  76. package/corpus/templates/content/actions/duplicate-database-item.ts +10 -0
  77. package/corpus/templates/content/actions/duplicate-database-items.ts +13 -0
  78. package/corpus/templates/content/actions/edit-document.ts +2 -28
  79. package/corpus/templates/content/actions/ensure-content-spaces.ts +26 -0
  80. package/corpus/templates/content/actions/export-content-source.ts +47 -9
  81. package/corpus/templates/content/actions/get-document.ts +0 -14
  82. package/corpus/templates/content/actions/import-content-source.ts +43 -3
  83. package/corpus/templates/content/actions/list-content-spaces.ts +103 -0
  84. package/corpus/templates/content/actions/list-documents.ts +12 -9
  85. package/corpus/templates/content/actions/move-document.ts +3 -14
  86. package/corpus/templates/content/actions/pull-document.ts +0 -23
  87. package/corpus/templates/content/actions/remove-local-file-source.ts +50 -29
  88. package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +442 -0
  89. package/corpus/templates/content/actions/reveal-local-source-file.ts +6 -17
  90. package/corpus/templates/content/actions/search-documents.ts +0 -34
  91. package/corpus/templates/content/actions/share-local-file-document.ts +25 -1
  92. package/corpus/templates/content/actions/submit-content-database-form.ts +12 -0
  93. package/corpus/templates/content/actions/sync-local-folder-source.ts +731 -0
  94. package/corpus/templates/content/actions/sync-manifest-local-folder-source.ts +123 -0
  95. package/corpus/templates/content/actions/update-content-database-view.ts +10 -1
  96. package/corpus/templates/content/actions/update-document.ts +0 -15
  97. package/corpus/templates/content/actions/view-screen.ts +2 -50
  98. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +76 -62
  99. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +70 -22
  100. package/corpus/templates/content/app/components/editor/database/grouping.ts +2 -1
  101. package/corpus/templates/content/app/components/editor/database/settings.tsx +23 -20
  102. package/corpus/templates/content/app/components/editor/database/shared.tsx +2 -0
  103. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +330 -0
  104. package/corpus/templates/content/app/components/editor/database/types.ts +1 -0
  105. package/corpus/templates/content/app/components/editor/database/view-config.ts +3 -1
  106. package/corpus/templates/content/app/components/layout/Layout.tsx +25 -1
  107. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +209 -113
  108. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +31 -0
  109. package/corpus/templates/content/app/hooks/use-content-database.ts +16 -0
  110. package/corpus/templates/content/app/hooks/use-content-spaces.ts +39 -0
  111. package/corpus/templates/content/app/i18n/zh-TW.ts +11 -0
  112. package/corpus/templates/content/app/i18n-data.ts +75 -0
  113. package/corpus/templates/content/app/routes/_app.local-files.tsx +174 -65
  114. package/corpus/templates/content/changelog/2026-07-16-content-callouts-can-now-be-reviewed-and-published-to-builder.md +6 -0
  115. package/corpus/templates/content/changelog/2026-07-16-heading-5-and-heading-6-are-available-from-the-editor-slash-.md +6 -0
  116. package/corpus/templates/content/changelog/2026-07-16-organize-every-workspace-through-a-customizable-files-databa.md +6 -0
  117. package/corpus/templates/content/parity/matrix.md +2 -1
  118. package/corpus/templates/content/parity/matrix.ts +37 -1
  119. package/corpus/templates/content/scripts/dev-database.mjs +7 -5
  120. package/corpus/templates/content/server/db/schema.ts +113 -40
  121. package/corpus/templates/content/server/lib/document-context.ts +15 -1
  122. package/corpus/templates/content/server/lib/notion-sync.ts +6 -0
  123. package/corpus/templates/content/server/plugins/db.ts +153 -0
  124. package/corpus/templates/content/shared/api.ts +17 -3
  125. package/corpus/templates/content/shared/builder-mdx.ts +163 -16
  126. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +59 -0
  127. package/corpus/templates/design/AGENTS.md +6 -1
  128. package/corpus/templates/design/actions/create-design-from-template.ts +19 -7
  129. package/corpus/templates/design/actions/list-design-templates.ts +69 -21
  130. package/corpus/templates/design/actions/navigate.ts +2 -2
  131. package/corpus/templates/design/actions/view-screen.ts +15 -2
  132. package/corpus/templates/design/agent-native.app-skill.json +5 -0
  133. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +281 -56
  134. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +14 -2
  135. package/corpus/templates/design/app/hooks/use-navigation-state.ts +3 -2
  136. package/corpus/templates/design/app/i18n/zh-TW.ts +3 -3
  137. package/corpus/templates/design/app/i18n-data.ts +21 -2
  138. package/corpus/templates/design/app/i18n-template-feature.ts +218 -10
  139. package/corpus/templates/design/app/pages/Index.tsx +195 -24
  140. package/corpus/templates/design/app/pages/Templates.tsx +144 -55
  141. package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +12 -1
  142. package/corpus/templates/design/changelog/2026-07-14-templates-now-appear-in-the-new-design-picker-with-previews-.md +6 -0
  143. package/corpus/templates/design/changelog/2026-07-15-templates-now-open-immediately-after-copying-even-while-the-.md +6 -0
  144. package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
  145. package/corpus/templates/mail/AGENTS.md +23 -18
  146. package/corpus/templates/mail/actions/bulk-archive.ts +32 -28
  147. package/corpus/templates/mail/actions/mark-read.ts +131 -20
  148. package/corpus/templates/mail/actions/mark-thread-read.ts +1 -1
  149. package/corpus/templates/mail/actions/move-email.ts +30 -24
  150. package/corpus/templates/mail/actions/send-email.ts +73 -68
  151. package/corpus/templates/mail/changelog/2026-07-15-unread-inbox-cleanup-now-completes-reliably.md +6 -0
  152. package/corpus/templates/mail/server/handlers/emails.ts +218 -209
  153. package/corpus/templates/mail/server/lib/bulk-mark-read.ts +30 -0
  154. package/corpus/templates/mail/server/lib/email-state.ts +198 -113
  155. package/corpus/templates/mail/server/lib/google-auth.ts +218 -25
  156. package/corpus/templates/mail/server/lib/jobs.ts +69 -74
  157. package/corpus/templates/mail/server/lib/local-email-store.ts +265 -0
  158. package/corpus/templates/mail/server/plugins/agent-chat.ts +6 -0
  159. package/dist/cli/content-local.d.ts +1 -0
  160. package/dist/cli/content-local.d.ts.map +1 -1
  161. package/dist/cli/content-local.js +75 -17
  162. package/dist/cli/content-local.js.map +1 -1
  163. package/dist/cli/index.js +2 -2
  164. package/dist/cli/index.js.map +1 -1
  165. package/dist/cli/skills-content/content-skill.d.ts +1 -1
  166. package/dist/cli/skills-content/content-skill.d.ts.map +1 -1
  167. package/dist/cli/skills-content/content-skill.js +36 -25
  168. package/dist/cli/skills-content/content-skill.js.map +1 -1
  169. package/dist/cli/skills.d.ts.map +1 -1
  170. package/dist/cli/skills.js +47 -14
  171. package/dist/cli/skills.js.map +1 -1
  172. package/dist/collab/awareness.d.ts +2 -2
  173. package/dist/collab/awareness.d.ts.map +1 -1
  174. package/dist/collab/struct-routes.d.ts +1 -1
  175. package/dist/local-artifacts/index.d.ts +10 -0
  176. package/dist/local-artifacts/index.d.ts.map +1 -1
  177. package/dist/local-artifacts/index.js +39 -0
  178. package/dist/local-artifacts/index.js.map +1 -1
  179. package/dist/notifications/routes.d.ts +1 -1
  180. package/dist/observability/routes.d.ts +5 -5
  181. package/dist/resources/handlers.d.ts +1 -1
  182. package/dist/secrets/routes.d.ts +9 -9
  183. package/docs/content/local-file-mode.mdx +99 -489
  184. package/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
  185. package/docs/content/locales/ar-SA/template-content.mdx +10 -24
  186. package/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
  187. package/docs/content/locales/de-DE/template-content.mdx +10 -24
  188. package/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
  189. package/docs/content/locales/es-ES/template-content.mdx +10 -24
  190. package/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
  191. package/docs/content/locales/fr-FR/template-content.mdx +10 -24
  192. package/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
  193. package/docs/content/locales/hi-IN/template-content.mdx +10 -24
  194. package/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
  195. package/docs/content/locales/ja-JP/template-content.mdx +10 -24
  196. package/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
  197. package/docs/content/locales/ko-KR/template-content.mdx +10 -24
  198. package/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
  199. package/docs/content/locales/pt-BR/template-content.mdx +10 -24
  200. package/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
  201. package/docs/content/locales/zh-CN/template-content.mdx +9 -24
  202. package/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
  203. package/docs/content/locales/zh-TW/template-content.mdx +9 -24
  204. package/docs/content/template-content.mdx +26 -42
  205. package/package.json +1 -1
  206. package/corpus/templates/assets/app/components/library/EditLibraryDialog.tsx +0 -126
@@ -0,0 +1,446 @@
1
+ import {
2
+ sendToAgentChat,
3
+ useActionMutation,
4
+ useActionQuery,
5
+ useT,
6
+ } from "@agent-native/core/client";
7
+ import {
8
+ useSetHeaderActions,
9
+ useSetPageTitle,
10
+ } from "@agent-native/toolkit/app-shell";
11
+ import {
12
+ IconArrowLeft,
13
+ IconBulb,
14
+ IconChevronDown,
15
+ IconChevronRight,
16
+ IconListCheck,
17
+ IconPhoto,
18
+ IconTextCaption,
19
+ } from "@tabler/icons-react";
20
+ import { useCallback, useEffect, useMemo, useState } from "react";
21
+ import {
22
+ useBeforeUnload,
23
+ useBlocker,
24
+ useNavigate,
25
+ useParams,
26
+ } from "react-router";
27
+ import { toast } from "sonner";
28
+
29
+ import { GenerationPresetsPanel } from "@/components/library/GenerationPresetsPanel";
30
+ import { Badge } from "@/components/ui/badge";
31
+ import { Button } from "@/components/ui/button";
32
+ import {
33
+ Dialog,
34
+ DialogContent,
35
+ DialogDescription,
36
+ DialogFooter,
37
+ DialogHeader,
38
+ DialogTitle,
39
+ } from "@/components/ui/dialog";
40
+ import { Input } from "@/components/ui/input";
41
+ import { Label } from "@/components/ui/label";
42
+ import { Textarea } from "@/components/ui/textarea";
43
+ import { getLibraryCustomInstructions } from "@/lib/libraries";
44
+
45
+ function paletteDraftFromColors(colors: unknown): string {
46
+ return Array.isArray(colors)
47
+ ? colors
48
+ .filter((color) => typeof color === "string")
49
+ .map((color) => color.toLowerCase())
50
+ .join(", ")
51
+ : "";
52
+ }
53
+
54
+ function parsePaletteDraft(value: string): string[] {
55
+ const seen = new Set<string>();
56
+ const colors: string[] = [];
57
+ for (const raw of value.split(/[\s,]+/)) {
58
+ const trimmed = raw.trim();
59
+ if (!trimmed) continue;
60
+ const color = trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
61
+ if (!/^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(color)) continue;
62
+ const normalized = color.toLowerCase();
63
+ if (seen.has(normalized)) continue;
64
+ seen.add(normalized);
65
+ colors.push(normalized);
66
+ }
67
+ return colors;
68
+ }
69
+
70
+ export default function BrandKitSettingsRoute() {
71
+ const t = useT();
72
+ const navigate = useNavigate();
73
+ const { id } = useParams();
74
+ const libraryId = id ?? "";
75
+ const { data } = useActionQuery("get-library", { id: libraryId }) as any;
76
+ const { data: presetData } = useActionQuery("list-generation-presets", {
77
+ libraryId,
78
+ }) as any;
79
+ const updateLibrary = useActionMutation("update-library");
80
+
81
+ const library = data?.library;
82
+ const assets = (data?.assets ?? []) as any[];
83
+ const generationPresets = ((presetData as any)?.presets ?? []) as any[];
84
+
85
+ const [titleDraft, setTitleDraft] = useState("");
86
+ const [descriptionDraft, setDescriptionDraft] = useState("");
87
+ const [styleDescriptionDraft, setStyleDescriptionDraft] = useState("");
88
+ const [customInstructionsDraft, setCustomInstructionsDraft] = useState("");
89
+ const [paletteDraft, setPaletteDraft] = useState("");
90
+ const [detailsOpen, setDetailsOpen] = useState(false);
91
+ const [initializedLibraryId, setInitializedLibraryId] = useState<
92
+ string | null
93
+ >(null);
94
+
95
+ const isDirty = useMemo(() => {
96
+ if (!library) return false;
97
+ if (titleDraft.trim() !== (library.title ?? "").trim()) return true;
98
+ if (descriptionDraft.trim() !== (library.description ?? "").trim())
99
+ return true;
100
+ if (styleDescriptionDraft !== (library.styleBrief?.description ?? ""))
101
+ return true;
102
+ if (
103
+ customInstructionsDraft.trim() !==
104
+ (getLibraryCustomInstructions(library) ?? "")
105
+ )
106
+ return true;
107
+ if (
108
+ parsePaletteDraft(paletteDraft).join(", ") !==
109
+ paletteDraftFromColors(library.styleBrief?.palette)
110
+ )
111
+ return true;
112
+ return false;
113
+ }, [
114
+ library,
115
+ titleDraft,
116
+ descriptionDraft,
117
+ styleDescriptionDraft,
118
+ customInstructionsDraft,
119
+ paletteDraft,
120
+ ]);
121
+
122
+ useEffect(() => {
123
+ if (!library) return;
124
+ if (initializedLibraryId === library.id && isDirty) return;
125
+ setTitleDraft(library.title ?? "");
126
+ setDescriptionDraft(library.description ?? "");
127
+ setStyleDescriptionDraft(library.styleBrief?.description ?? "");
128
+ setCustomInstructionsDraft(getLibraryCustomInstructions(library) ?? "");
129
+ setPaletteDraft(paletteDraftFromColors(library.styleBrief?.palette));
130
+ if (initializedLibraryId !== library.id) {
131
+ const isNewLibrary =
132
+ !library.description &&
133
+ !getLibraryCustomInstructions(library) &&
134
+ !library.styleBrief?.description &&
135
+ !(library.styleBrief?.palette ?? []).length;
136
+ setDetailsOpen(isNewLibrary);
137
+ setInitializedLibraryId(library.id);
138
+ }
139
+ }, [library, initializedLibraryId, isDirty]);
140
+
141
+ const isTitleValid = titleDraft.trim().length > 0;
142
+
143
+ function saveAll() {
144
+ if (!library || !isDirty || !isTitleValid) return;
145
+ const trimmedTitle = titleDraft.trim();
146
+ const palette = parsePaletteDraft(paletteDraft);
147
+ updateLibrary.mutate(
148
+ {
149
+ id: library.id,
150
+ title: trimmedTitle,
151
+ description: descriptionDraft.trim() || null,
152
+ customInstructions: customInstructionsDraft.trim(),
153
+ styleBrief: {
154
+ ...library.styleBrief,
155
+ description: styleDescriptionDraft,
156
+ palette,
157
+ },
158
+ },
159
+ {
160
+ onSuccess: () => {
161
+ setPaletteDraft(palette.join(", "));
162
+ toast.success(t("brandKits.updated"));
163
+ },
164
+ onError: (error: Error) =>
165
+ toast.error(error.message || t("brandKits.updateFailed")),
166
+ },
167
+ );
168
+ }
169
+
170
+ const navigationBlocker = useBlocker(
171
+ useCallback(
172
+ ({ currentLocation, nextLocation }) =>
173
+ isDirty && currentLocation.pathname !== nextLocation.pathname,
174
+ [isDirty],
175
+ ),
176
+ );
177
+
178
+ useBeforeUnload(
179
+ useCallback(
180
+ (event: BeforeUnloadEvent) => {
181
+ if (!isDirty) return;
182
+ event.preventDefault();
183
+ event.returnValue = "";
184
+ },
185
+ [isDirty],
186
+ ),
187
+ );
188
+
189
+ function handleBack() {
190
+ navigate(`/library/${libraryId}`);
191
+ }
192
+
193
+ function keepEditing() {
194
+ if (navigationBlocker.state === "blocked") navigationBlocker.reset();
195
+ }
196
+
197
+ function discardAndLeave() {
198
+ if (navigationBlocker.state === "blocked") navigationBlocker.proceed();
199
+ }
200
+
201
+ function analyzeBrand() {
202
+ if (!library) return;
203
+ const referenceCount = assets.filter(
204
+ (asset) => asset.status === "reference",
205
+ ).length;
206
+ sendToAgentChat({
207
+ message: [
208
+ "Analyze this Assets library brand.",
209
+ `Call analyze-collection-style with libraryId: ${library.id}.`,
210
+ "Update the reusable style brief with palette and visual traits, then summarize what changed.",
211
+ ].join("\n"),
212
+ context: [
213
+ "## Assets library context",
214
+ `Library: ${library.title} (${library.id})`,
215
+ `Description: ${library.description || ""}`,
216
+ `Reference assets: ${referenceCount}`,
217
+ `Current style brief: ${JSON.stringify(library.styleBrief ?? {})}`,
218
+ getLibraryCustomInstructions(library)
219
+ ? `Custom instructions: ${getLibraryCustomInstructions(library)}`
220
+ : "Custom instructions: none",
221
+ ].join("\n"),
222
+ submit: true,
223
+ newTab: true,
224
+ });
225
+ }
226
+
227
+ useSetPageTitle(
228
+ <div className="flex min-w-0 items-center gap-2">
229
+ <Button
230
+ variant="ghost"
231
+ size="icon"
232
+ className="-ms-2 h-7 w-7 shrink-0"
233
+ onClick={handleBack}
234
+ aria-label={t("brandKitDetail.backToLibrary")}
235
+ >
236
+ <IconArrowLeft className="h-4 w-4" />
237
+ </Button>
238
+ <h1 className="truncate text-lg font-semibold tracking-tight">
239
+ {t("brandKitDetail.settingsTitle")}
240
+ </h1>
241
+ {library ? <Badge variant="outline">{library.title}</Badge> : null}
242
+ </div>,
243
+ );
244
+
245
+ useSetHeaderActions(
246
+ library ? (
247
+ <Button
248
+ size="sm"
249
+ onClick={saveAll}
250
+ disabled={!isDirty || !isTitleValid || updateLibrary.isPending}
251
+ >
252
+ {updateLibrary.isPending
253
+ ? t("brandKitDetail.saving")
254
+ : t("brandKitDetail.save")}
255
+ </Button>
256
+ ) : null,
257
+ );
258
+
259
+ if (!library) {
260
+ return (
261
+ <div className="p-6 text-sm text-muted-foreground">
262
+ {t("library.loadingBrandKit")}
263
+ </div>
264
+ );
265
+ }
266
+
267
+ return (
268
+ <div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-6">
269
+ <div className="flex flex-col gap-4">
270
+ <div className="space-y-4 rounded-lg border border-border p-4">
271
+ <Label htmlFor="brand-kit-title">{t("brandKitDetail.name")}</Label>
272
+ <Input
273
+ id="brand-kit-title"
274
+ value={titleDraft}
275
+ onChange={(event) => setTitleDraft(event.target.value)}
276
+ placeholder={t("brandKits.namePlaceholder")}
277
+ />
278
+ <Label htmlFor="brand-kit-description">
279
+ {t("assetDetail.description")}
280
+ </Label>
281
+ <Textarea
282
+ id="brand-kit-description"
283
+ value={descriptionDraft}
284
+ onChange={(event) => setDescriptionDraft(event.target.value)}
285
+ placeholder={t("brandKits.editDescriptionPlaceholder")}
286
+ />
287
+ <div>
288
+ <h3 className="text-sm font-semibold">
289
+ {t("brandKitDetail.agentUsage")}
290
+ </h3>
291
+ <p className="mt-2 text-sm text-muted-foreground">
292
+ {t("brandKitDetail.agentUsageDescription")}
293
+ </p>
294
+ <code className="mt-3 block rounded-md bg-muted p-3 text-xs">
295
+ {library.id}
296
+ </code>
297
+ </div>
298
+ </div>
299
+
300
+ <div className="rounded-lg border border-border p-4">
301
+ <button
302
+ type="button"
303
+ className="flex w-full items-center justify-between gap-2 text-left"
304
+ onClick={() => setDetailsOpen((open) => !open)}
305
+ aria-expanded={detailsOpen}
306
+ >
307
+ <div>
308
+ <h3 className="text-sm font-semibold">
309
+ {t("brandKitDetail.setupGuide")}
310
+ </h3>
311
+ <p className="mt-1 text-sm text-muted-foreground">
312
+ {t("brandKitDetail.setupGuideDescription")}
313
+ </p>
314
+ </div>
315
+ {detailsOpen ? (
316
+ <IconChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
317
+ ) : (
318
+ <IconChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
319
+ )}
320
+ </button>
321
+ {detailsOpen ? (
322
+ <ul className="mt-4 space-y-3">
323
+ <li className="flex gap-3">
324
+ <IconPhoto className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
325
+ <div>
326
+ <div className="text-sm font-medium">
327
+ {t("brandKitDetail.setupGuideReferences")}
328
+ </div>
329
+ <p className="text-sm text-muted-foreground">
330
+ {t("brandKitDetail.setupGuideReferencesHint")}
331
+ </p>
332
+ </div>
333
+ </li>
334
+ <li className="flex gap-3">
335
+ <IconTextCaption className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
336
+ <div>
337
+ <div className="text-sm font-medium">
338
+ {t("brandKitDetail.setupGuideStyleDescription")}
339
+ </div>
340
+ <p className="text-sm text-muted-foreground">
341
+ {t("brandKitDetail.setupGuideStyleDescriptionHint")}
342
+ </p>
343
+ </div>
344
+ </li>
345
+ <li className="flex gap-3">
346
+ <IconListCheck className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
347
+ <div>
348
+ <div className="text-sm font-medium">
349
+ {t("brandKitDetail.setupGuideInstructions")}
350
+ </div>
351
+ <p className="text-sm text-muted-foreground">
352
+ {t("brandKitDetail.setupGuideInstructionsHint")}
353
+ </p>
354
+ </div>
355
+ </li>
356
+ <li className="flex gap-3">
357
+ <IconBulb className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
358
+ <div>
359
+ <div className="text-sm font-medium">
360
+ {t("brandKitDetail.setupGuidePresets")}
361
+ </div>
362
+ <p className="text-sm text-muted-foreground">
363
+ {t("brandKitDetail.setupGuidePresetsHint")}
364
+ </p>
365
+ </div>
366
+ </li>
367
+ </ul>
368
+ ) : null}
369
+ </div>
370
+ </div>
371
+
372
+ <div className="space-y-4 rounded-lg border border-border p-4">
373
+ <Label>{t("brandKitDetail.styleDescription")}</Label>
374
+ <Textarea
375
+ value={styleDescriptionDraft}
376
+ onChange={(event) => setStyleDescriptionDraft(event.target.value)}
377
+ className="min-h-40"
378
+ />
379
+ <Label>{t("brandKitDetail.customInstructions")}</Label>
380
+ <Textarea
381
+ value={customInstructionsDraft}
382
+ onChange={(event) => setCustomInstructionsDraft(event.target.value)}
383
+ placeholder={t("brandKitDetail.customInstructionsPlaceholder")}
384
+ className="min-h-28"
385
+ />
386
+ <div className="flex items-center justify-between gap-3">
387
+ <div>
388
+ <div className="text-sm font-medium">
389
+ {t("brandKitDetail.palette")}
390
+ </div>
391
+ <div className="mt-2 flex flex-wrap gap-2">
392
+ {(library.styleBrief?.palette ?? []).map((color: string) => (
393
+ <span
394
+ key={color}
395
+ className="h-7 w-7 rounded-md border border-border"
396
+ style={{ backgroundColor: color }}
397
+ title={color}
398
+ />
399
+ ))}
400
+ </div>
401
+ <Input
402
+ value={paletteDraft}
403
+ onChange={(event) => setPaletteDraft(event.target.value)}
404
+ placeholder={"#111827, #f8fafc, #2563eb"}
405
+ className="mt-3 h-9 max-w-md text-xs"
406
+ />
407
+ </div>
408
+ <Button variant="outline" onClick={analyzeBrand}>
409
+ {library.settings?.brandAnalysis?.analyzedAt
410
+ ? t("brandKitDetail.refreshBrand")
411
+ : t("brandKitDetail.analyzeBrand")}
412
+ </Button>
413
+ </div>
414
+ </div>
415
+
416
+ <GenerationPresetsPanel
417
+ libraryId={libraryId}
418
+ presets={generationPresets}
419
+ />
420
+
421
+ <Dialog
422
+ open={navigationBlocker.state === "blocked"}
423
+ onOpenChange={(open) => {
424
+ if (!open) keepEditing();
425
+ }}
426
+ >
427
+ <DialogContent>
428
+ <DialogHeader>
429
+ <DialogTitle>{t("brandKitDetail.unsavedChangesTitle")}</DialogTitle>
430
+ <DialogDescription>
431
+ {t("brandKitDetail.unsavedChangesDescription")}
432
+ </DialogDescription>
433
+ </DialogHeader>
434
+ <DialogFooter>
435
+ <Button variant="outline" onClick={keepEditing}>
436
+ {t("brandKitDetail.keepEditing")}
437
+ </Button>
438
+ <Button variant="destructive" onClick={discardAndLeave}>
439
+ {t("brandKitDetail.discardChanges")}
440
+ </Button>
441
+ </DialogFooter>
442
+ </DialogContent>
443
+ </Dialog>
444
+ </div>
445
+ );
446
+ }
@@ -415,13 +415,20 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
415
415
  if (!recording?.id) return;
416
416
  const next = readPlaybackSpeedPreference(defaultPreviewSpeed);
417
417
  setPlaybackSpeed(next);
418
- if (videoRef.current) videoRef.current.playbackRate = next;
418
+ if (videoRef.current) {
419
+ videoRef.current.defaultPlaybackRate = next;
420
+ videoRef.current.playbackRate = next;
421
+ }
419
422
  }, [defaultPreviewSpeed, recording?.id]);
420
423
 
421
424
  // Keep the editor preview speed visible and in sync with the media element.
425
+ // `defaultPlaybackRate` is set too so a `videoUrl` source swap that resets
426
+ // `playbackRate` (some browsers do this on load) falls back to the chosen
427
+ // speed instead of 1x.
422
428
  useEffect(() => {
423
429
  const v = videoRef.current;
424
430
  if (!v) return;
431
+ v.defaultPlaybackRate = playbackSpeed;
425
432
  v.playbackRate = playbackSpeed;
426
433
  }, [playbackSpeed, videoUrl]);
427
434
 
@@ -429,7 +436,10 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
429
436
  const next = parsePlaybackSpeed(rate) ?? 1.2;
430
437
  setPlaybackSpeed(next);
431
438
  savePlaybackSpeedPreference(next);
432
- if (videoRef.current) videoRef.current.playbackRate = next;
439
+ if (videoRef.current) {
440
+ videoRef.current.defaultPlaybackRate = next;
441
+ videoRef.current.playbackRate = next;
442
+ }
433
443
  }, []);
434
444
 
435
445
  // Keep the playheadMs in sync with the element's currentTime.
@@ -156,7 +156,7 @@ export function SettingsPanel(props: SettingsPanelProps) {
156
156
  <Label className="text-xs">
157
157
  {t("playerSettings.passwordProtection")}
158
158
  </Label>
159
- <div className="flex gap-2 mt-1">
159
+ <div className="mt-1 space-y-2">
160
160
  <Input
161
161
  type="text"
162
162
  value={password}
@@ -166,18 +166,36 @@ export function SettingsPanel(props: SettingsPanelProps) {
166
166
  ? t("playerSettings.passwordSetPlaceholder")
167
167
  : t("playerSettings.noPasswordPlaceholder")
168
168
  }
169
- className="flex-1"
170
169
  />
171
- <Button
172
- variant="outline"
173
- onClick={() => {
174
- patch({ password: password || null });
175
- setPassword("");
176
- }}
177
- disabled={update.isPending}
178
- >
179
- {t("common.save")}
180
- </Button>
170
+ {password.length > 0 && !password.trim() ? (
171
+ <p className="text-xs text-muted-foreground">
172
+ {t("playerSettings.passwordWhitespaceOnly")}
173
+ </p>
174
+ ) : null}
175
+ <div className="flex gap-2">
176
+ <Button
177
+ variant="outline"
178
+ onClick={() => {
179
+ patch({ password: password.trim() });
180
+ setPassword("");
181
+ }}
182
+ disabled={update.isPending || !password.trim()}
183
+ >
184
+ {t("common.save")}
185
+ </Button>
186
+ {recording.hasPassword ? (
187
+ <Button
188
+ variant="outline"
189
+ onClick={() => {
190
+ patch({ password: null });
191
+ setPassword("");
192
+ }}
193
+ disabled={update.isPending}
194
+ >
195
+ {t("playerSettings.removePassword")}
196
+ </Button>
197
+ ) : null}
198
+ </div>
181
199
  </div>
182
200
  </div>
183
201
 
@@ -679,7 +679,10 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
679
679
  (isPlaying || playAttemptPendingRef.current || !v.paused),
680
680
  );
681
681
 
682
- if (v) v.playbackRate = nextSpeed;
682
+ if (v) {
683
+ v.defaultPlaybackRate = nextSpeed;
684
+ v.playbackRate = nextSpeed;
685
+ }
683
686
  setSpeed(nextSpeed);
684
687
  savePlaybackSpeedPreference(nextSpeed);
685
688
  onSpeedChange?.(nextSpeed);
@@ -778,6 +781,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
778
781
  const v = videoRef.current;
779
782
  if (!v) return;
780
783
  const initialSpeed = readPlaybackSpeedPreference(defaultSpeed);
784
+ v.defaultPlaybackRate = initialSpeed;
781
785
  v.playbackRate = initialSpeed;
782
786
  setSpeed(initialSpeed);
783
787
  onSpeedChange?.(initialSpeed);
@@ -41,6 +41,26 @@ function stopStream(stream: MediaStream | null): void {
41
41
 
42
42
  type MicrophonePermissionState = PermissionState | "unknown";
43
43
 
44
+ function isDesktopShell(): boolean {
45
+ if (typeof window === "undefined") return false;
46
+ const w = window as typeof window & {
47
+ electronAPI?: unknown;
48
+ __TAURI_INTERNALS__?: unknown;
49
+ __TAURI__?: unknown;
50
+ };
51
+ if (w.electronAPI || w.__TAURI_INTERNALS__ || w.__TAURI__) return true;
52
+ return (
53
+ typeof navigator !== "undefined" && /Electron/i.test(navigator.userAgent)
54
+ );
55
+ }
56
+
57
+ function micBlockedMessage(): string {
58
+ if (isDesktopShell()) {
59
+ return "Microphone access is blocked for this app. Enable the microphone for the app in your system Privacy settings, then reopen the recorder.";
60
+ }
61
+ return "Your browser has blocked microphone access for this site, so it won't prompt. Allow the microphone in this site's settings, then reload.";
62
+ }
63
+
44
64
  function isMicrophoneBlockedByPolicy(): boolean {
45
65
  const policy =
46
66
  (
@@ -96,7 +116,7 @@ export async function friendlyMicError(err: unknown): Promise<string> {
96
116
  return "Microphone prompts require HTTPS or localhost. Open this app on localhost or an HTTPS URL, then try again.";
97
117
  }
98
118
  if (permissionState === "denied") {
99
- return "This site is blocked from using the microphone. Open the browser site settings, set Microphone to Allow, then reload.";
119
+ return micBlockedMessage();
100
120
  }
101
121
  if (/NotAllowedError|Permission denied|denied|blocked/i.test(combined)) {
102
122
  return "The browser or operating system denied microphone access. Check this site's microphone setting and your system privacy settings for this browser, then reload.";
@@ -328,8 +348,7 @@ export function MicrophoneVisualizer({
328
348
  const permissionState = await getMicrophonePermissionState();
329
349
  if (runIdRef.current !== runId) return;
330
350
  if (permissionState === "denied") {
331
- const message =
332
- "Brave already has Microphone set to Block for this site, so it will not show the popup. Click the lock/tune icon in the address bar → Site settings → Microphone → Allow, then reload.";
351
+ const message = micBlockedMessage();
333
352
  setError(message);
334
353
  setStatus("error");
335
354
  onStatusChange?.("error", { error: message });
@@ -1227,9 +1227,10 @@ const messages = {
1227
1227
  visibilityOrg: "Organization (مترجم)",
1228
1228
  visibilityPublic: "Public (مترجم)",
1229
1229
  passwordProtection: "Password protection (مترجم)",
1230
- passwordSetPlaceholder:
1231
- "Password is set — type to replace, leave empty + Save to clear (مترجم)",
1230
+ passwordSetPlaceholder: "Password is set — type to replace (مترجم)",
1232
1231
  noPasswordPlaceholder: "No password (مترجم)",
1232
+ passwordWhitespaceOnly: "Spaces alone aren't a valid password. (مترجم)",
1233
+ removePassword: "Remove (مترجم)",
1233
1234
  expiry: "Expiry (مترجم)",
1234
1235
  viewerOptions: "Viewer options (مترجم)",
1235
1236
  comments: "Comments (مترجم)",
@@ -1253,9 +1253,11 @@ Alle sichtbaren Änderungen für Clips-Nutzer werden hier dokumentiert. Du kanns
1253
1253
  visibilityOrg: "Organization (Lokalisiert)",
1254
1254
  visibilityPublic: "Public (Lokalisiert)",
1255
1255
  passwordProtection: "Password protection (Lokalisiert)",
1256
- passwordSetPlaceholder:
1257
- "Password is set — type to replace, leave empty + Save to clear (Lokalisiert)",
1256
+ passwordSetPlaceholder: "Password is set — type to replace (Lokalisiert)",
1258
1257
  noPasswordPlaceholder: "No password (Lokalisiert)",
1258
+ passwordWhitespaceOnly:
1259
+ "Spaces alone aren't a valid password. (Lokalisiert)",
1260
+ removePassword: "Remove (Lokalisiert)",
1259
1261
  expiry: "Expiry (Lokalisiert)",
1260
1262
  viewerOptions: "Viewer options (Lokalisiert)",
1261
1263
  comments: "Comments (Lokalisiert)",
@@ -1221,9 +1221,10 @@ All notable user-facing changes to Clips are documented here. Open it any time f
1221
1221
  visibilityOrg: "Organization",
1222
1222
  visibilityPublic: "Public",
1223
1223
  passwordProtection: "Password protection",
1224
- passwordSetPlaceholder:
1225
- "Password is set — type to replace, leave empty + Save to clear",
1224
+ passwordSetPlaceholder: "Password is set — type to replace",
1226
1225
  noPasswordPlaceholder: "No password",
1226
+ passwordWhitespaceOnly: "Spaces alone aren't a valid password.",
1227
+ removePassword: "Remove",
1227
1228
  expiry: "Expiry",
1228
1229
  viewerOptions: "Viewer options",
1229
1230
  comments: "Comments",
@@ -1246,9 +1246,11 @@ Todos los cambios visibles para los usuarios de Clips se documentan aquí. Puede
1246
1246
  visibilityOrg: "Organization (Localizado)",
1247
1247
  visibilityPublic: "Public (Localizado)",
1248
1248
  passwordProtection: "Password protection (Localizado)",
1249
- passwordSetPlaceholder:
1250
- "Password is set — type to replace, leave empty + Save to clear (Localizado)",
1249
+ passwordSetPlaceholder: "Password is set — type to replace (Localizado)",
1251
1250
  noPasswordPlaceholder: "No password (Localizado)",
1251
+ passwordWhitespaceOnly:
1252
+ "Spaces alone aren't a valid password. (Localizado)",
1253
+ removePassword: "Remove (Localizado)",
1252
1254
  expiry: "Expiry (Localizado)",
1253
1255
  viewerOptions: "Viewer options (Localizado)",
1254
1256
  comments: "Comments (Localizado)",
@@ -1248,9 +1248,10 @@ Tous les changements visibles par les utilisateurs de Clips sont documentés ici
1248
1248
  visibilityOrg: "Organization (Localisé)",
1249
1249
  visibilityPublic: "Public (Localisé)",
1250
1250
  passwordProtection: "Password protection (Localisé)",
1251
- passwordSetPlaceholder:
1252
- "Password is set — type to replace, leave empty + Save to clear (Localisé)",
1251
+ passwordSetPlaceholder: "Password is set — type to replace (Localisé)",
1253
1252
  noPasswordPlaceholder: "No password (Localisé)",
1253
+ passwordWhitespaceOnly: "Spaces alone aren't a valid password. (Localisé)",
1254
+ removePassword: "Remove (Localisé)",
1254
1255
  expiry: "Expiry (Localisé)",
1255
1256
  viewerOptions: "Viewer options (Localisé)",
1256
1257
  comments: "Comments (Localisé)",