@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +1 -0
- package/corpus/templates/calendar/CHANGELOG.md +2 -0
- package/corpus/templates/chat/app/root.tsx +1 -1
- package/corpus/templates/clips/AGENTS.md +7 -6
- package/corpus/templates/clips/CHANGELOG.md +7 -0
- package/corpus/templates/clips/actions/complete-workflow.ts +126 -0
- package/corpus/templates/clips/actions/generate-workflow.ts +72 -33
- package/corpus/templates/clips/actions/reconcile-workflow-generation.ts +19 -6
- package/corpus/templates/clips/actions/set-organization-branding.ts +5 -5
- package/corpus/templates/clips/app/components/clips-command-menu.tsx +9 -1
- package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +41 -27
- package/corpus/templates/clips/app/components/workspace/organization-identity-card.tsx +7 -2
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +37 -16
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
- package/corpus/templates/clips/app/lib/organization-logo.ts +17 -0
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +9 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +17 -26
- package/corpus/templates/clips/changelog/2026-09-25-clips-clears-a-stale-recording-overlay-after-an-interrupted-.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-compact-no-comments-signup-prompt.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-concurrent-workflow-requests-no-longer-replace-an-active-gen.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-generated-workflows-stay-tied-to-the-request-that-created-th.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-organization-logos-can-be-stored-privately-and-load-in-setti.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-private-minio-and-s3-compatible-endpoints-now-work-with-clip.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-workflow-drafts-are-saved-before-the-agent-reports-them-comp.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/background.ts +45 -13
- package/corpus/templates/clips/chrome-extension/src/native-recording-state.ts +17 -0
- package/corpus/templates/clips/server/db/index.ts +7 -1
- package/corpus/templates/clips/server/jobs/transactional-emails.ts +8 -1
- package/corpus/templates/clips/server/lib/organization-logo.ts +57 -0
- package/corpus/templates/clips/server/lib/s3-upload-provider.ts +240 -15
- package/corpus/templates/clips/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/clips/server/plugins/onboarding.ts +6 -1
- package/corpus/templates/clips/server/routes/api/media/index.post.ts +36 -16
- package/corpus/templates/clips/server/routes/api/media/organization-logo/[organizationId].get.ts +181 -0
- package/corpus/templates/clips/shared/organization-logo.ts +51 -0
- package/corpus/templates/clips/shared/workflow.ts +9 -0
- package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +4 -2
- package/corpus/templates/design/CHANGELOG.md +6 -0
- package/corpus/templates/design/actions/export-html.ts +5 -1
- package/corpus/templates/design/actions/export-svg.ts +5 -1
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +31 -6
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +33 -31
- package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +39 -19
- package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +2 -0
- package/corpus/templates/design/app/components/design/inspector/InspectorControlPopover.tsx +4 -1
- package/corpus/templates/design/app/pages/DesignSystems.tsx +10 -7
- package/corpus/templates/design/changelog/2026-09-25-builder-design-system-failures-preserve-readable-json-errors.md +6 -0
- package/corpus/templates/design/changelog/2026-09-25-design-html-export-stacks-screens.md +7 -0
- package/corpus/templates/design/changelog/2026-09-25-design-system-empty-states-show-one-creation-action.md +6 -0
- package/corpus/templates/design/changelog/2026-09-25-effect-values-stay-editable-after-blur-and-the-shader-picker.md +6 -0
- package/corpus/templates/design/changelog/2026-09-25-escape-cancels-unfinished-pen-paths-and-enter-selects-the-fi.md +6 -0
- package/corpus/templates/design/e2e/drag-and-drop.shared.ts +9 -12
- package/corpus/templates/design/e2e/helpers.ts +76 -23
- package/corpus/templates/design/server/handlers/design-system-decode-job-status.ts +2 -1
- package/corpus/templates/design/server/handlers/design-system-upload-start.ts +2 -1
- package/corpus/templates/design/server/handlers/index-design-system-sources.ts +3 -2
- package/corpus/templates/design/server/lib/design-export.ts +132 -13
- package/corpus/templates/slides/CHANGELOG.md +8 -0
- package/corpus/templates/slides/actions/get-deck.ts +149 -1
- package/corpus/templates/slides/actions/import-file.ts +29 -1
- package/corpus/templates/slides/actions/import-from-url.ts +7 -1
- package/corpus/templates/slides/actions/import-google-slides-reference.ts +7 -3
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +5 -3
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +451 -402
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +6 -2
- package/corpus/templates/slides/app/components/editor/QuestionFlow.tsx +1 -1
- package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/progress.tsx +1 -0
- package/corpus/templates/slides/app/global.css +14 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +15 -0
- package/corpus/templates/slides/app/pages/Index.tsx +13 -9
- package/corpus/templates/slides/changelog/2026-09-25-agent-recognizes-blank-slides-and-uses-clear-prompts.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-compact-deck-reads-handle-imported-slides-with-invalid-selec.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-empty-decks-show-a-structured-slide-preview.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-imports-handle-common-file-and-connection-errors.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-keep-empty-deck-lists-clear-of-search-and-filter-controls.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-presentations-keep-slides-centered.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-selected-design-system-source-forms-open-directly-beneath-th.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-selected-presentation-options-keep-their-highlight-during-se.md +6 -0
- package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +2 -1
- package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +2 -1
- package/corpus/templates/slides/server/handlers/import/docx-parser.ts +30 -3
- package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +2 -1
- package/corpus/templates/slides/shared/deck-title.ts +14 -5
- package/dist/agent/production-agent.d.ts +3 -5
- package/dist/agent/production-agent.js +3 -5
- package/dist/client/chat/markdown-renderer.js +15 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +1 -1
- package/dist/extensions/url-safety.js +33 -2
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/private-blob/index.d.ts +1 -1
- package/dist/private-blob/index.js +1 -1
- package/dist/private-blob/registry.d.ts +1 -0
- package/dist/private-blob/registry.js +22 -1
- package/dist/private-blob/types.d.ts +1 -0
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/workspace-provider-oauth.js +16 -2
- package/dist/templates/chat/app/root.tsx +1 -1
- package/dist/templates/default/app/root.tsx +1 -1
- package/package.json +4 -4
- package/src/templates/chat/app/root.tsx +1 -1
- package/src/templates/default/app/root.tsx +1 -1
package/corpus/README.md
CHANGED
|
@@ -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` |
|
|
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
|
|
81
|
+
| `import-loom-recording` | Import Loom or video URLs |
|
|
82
82
|
| `update-recording` | Title, password, expiry, visibility |
|
|
83
|
-
| `move-recording` | Move
|
|
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` |
|
|
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` |
|
|
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
|
|
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
|
|
11
|
-
*
|
|
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
|
-
//
|
|
59
|
-
//
|
|
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
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
`
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
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 {
|
|
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 (
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
40
|
+
.describe("Hex color"),
|
|
41
41
|
brandLogoUrl: z
|
|
42
42
|
.string()
|
|
43
43
|
.nullish()
|
|
44
|
-
.describe("
|
|
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 {
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 {
|
|
58
|
-
if (!json.
|
|
59
|
-
return json.
|
|
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
|
|
125
|
-
setBrandLogoUrl(
|
|
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:
|
|
252
|
+
background: logoUrl ? undefined : brandColor + "20",
|
|
238
253
|
}}
|
|
239
254
|
>
|
|
240
|
-
{
|
|
255
|
+
{logoUrl ? (
|
|
241
256
|
<img
|
|
242
|
-
src={
|
|
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={() =>
|
|
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
|
-
{
|
|
349
|
+
{logoUrl ? (
|
|
332
350
|
<img
|
|
333
|
-
src={
|
|
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
|
-
{
|
|
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]"
|