@agent-native/core 0.154.5 → 0.155.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 (185) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/account-health/SKILL.md +76 -0
  3. package/corpus/templates/analytics/.agents/skills/bigquery/SKILL.md +3 -0
  4. package/corpus/templates/analytics/.agents/skills/cross-source-analysis/SKILL.md +2 -1
  5. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +6 -5
  6. package/corpus/templates/analytics/AGENTS.md +3 -0
  7. package/corpus/templates/analytics/actions/account-deep-dive.ts +3 -2
  8. package/corpus/templates/analytics/actions/bigquery.ts +1 -1
  9. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +4 -1
  10. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
  11. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyCombobox.tsx +12 -4
  12. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyValueCombobox.tsx +5 -2
  13. package/corpus/templates/analytics/app/pages/adhoc/explorer/property-label.ts +32 -0
  14. package/corpus/templates/analytics/changelog/2026-08-13-account-health-readouts-verify-customer-scope-and-contract-metrics.md +7 -0
  15. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +43 -17
  16. package/corpus/templates/analytics/server/lib/analytics-query-catalog.ts +17 -0
  17. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
  18. package/corpus/templates/calendar/AGENTS.md +2 -0
  19. package/corpus/templates/calendar/DEVELOPING.md +4 -4
  20. package/corpus/templates/calendar/actions/get-settings.ts +4 -11
  21. package/corpus/templates/calendar/actions/update-availability.ts +15 -4
  22. package/corpus/templates/calendar/actions/update-settings.ts +15 -4
  23. package/corpus/templates/calendar/app/components/booking/DatePicker.tsx +9 -3
  24. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +21 -19
  25. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +9 -6
  26. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +25 -3
  27. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +10 -3
  28. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +9 -9
  29. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -3
  30. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +10 -4
  31. package/corpus/templates/calendar/app/i18n-data.ts +30 -0
  32. package/corpus/templates/calendar/app/lib/availability-schedule.ts +53 -0
  33. package/corpus/templates/calendar/app/lib/event-form-utils.ts +5 -0
  34. package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +90 -28
  35. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +135 -61
  36. package/corpus/templates/calendar/app/pages/BookingPage.tsx +2 -0
  37. package/corpus/templates/calendar/app/pages/CalendarView.tsx +30 -15
  38. package/corpus/templates/calendar/app/pages/Settings.tsx +60 -1
  39. package/corpus/templates/calendar/changelog/2026-08-13-find-a-time-now-closes-from-a-clear-header-action.md +6 -0
  40. package/corpus/templates/calendar/server/handlers/bookings.ts +2 -1
  41. package/corpus/templates/calendar/server/handlers/settings.ts +12 -16
  42. package/corpus/templates/calendar/shared/api.ts +1 -0
  43. package/corpus/templates/calendar/shared/availability-schedule.ts +64 -0
  44. package/corpus/templates/calendar/shared/calendar-week.ts +21 -0
  45. package/corpus/templates/calendar/shared/settings.ts +44 -0
  46. package/corpus/templates/clips/AGENTS.md +2 -0
  47. package/corpus/templates/clips/actions/add-comment.ts +5 -2
  48. package/corpus/templates/clips/actions/finalize-recording.ts +31 -1
  49. package/corpus/templates/clips/actions/get-recording-player-data.ts +9 -0
  50. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +21 -1
  51. package/corpus/templates/clips/actions/reply-to-comment.ts +5 -2
  52. package/corpus/templates/clips/actions/update-comment.ts +9 -2
  53. package/corpus/templates/clips/app/components/library/search-bar.tsx +26 -13
  54. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +135 -3
  55. package/corpus/templates/clips/app/components/player/comments-panel.tsx +5 -99
  56. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +7 -3
  57. package/corpus/templates/clips/app/components/player/video-player.tsx +84 -24
  58. package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +11 -3
  59. package/corpus/templates/clips/app/i18n/en-US.ts +12 -0
  60. package/corpus/templates/clips/app/lib/public-meeting.ts +11 -3
  61. package/corpus/templates/clips/app/lib/search-focus.ts +15 -0
  62. package/corpus/templates/clips/app/root.tsx +2 -1
  63. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +94 -26
  64. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +24 -7
  65. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +8 -4
  66. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +14 -7
  67. package/corpus/templates/clips/app/routes/share.$shareId.tsx +8 -4
  68. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +72 -26
  69. package/corpus/templates/clips/changelog/2026-08-13-inline-markdown-comments.md +6 -0
  70. package/corpus/templates/clips/changelog/2026-08-13-private-meeting-agent-links.md +6 -0
  71. package/corpus/templates/clips/desktop/src/app.tsx +116 -19
  72. package/corpus/templates/clips/desktop/src/lib/recorder.ts +13 -29
  73. package/corpus/templates/clips/desktop/src/lib/recording-request.ts +39 -0
  74. package/corpus/templates/clips/desktop/src/lib/settings-navigation.ts +11 -0
  75. package/corpus/templates/clips/desktop/src/lib/whisper-model-picker.ts +7 -0
  76. package/corpus/templates/clips/desktop/src/styles.css +117 -0
  77. package/corpus/templates/clips/server/db/index.ts +10 -0
  78. package/corpus/templates/clips/server/db/schema.ts +1 -0
  79. package/corpus/templates/clips/server/lib/recordings.ts +3 -1
  80. package/corpus/templates/clips/server/lib/seekable-media-state.ts +90 -0
  81. package/corpus/templates/clips/server/plugins/db.ts +5 -0
  82. package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +37 -4
  83. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +16 -5
  84. package/corpus/templates/clips/shared/clips-ai-prefs.ts +3 -0
  85. package/corpus/templates/clips/shared/meeting-agent-access.ts +5 -0
  86. package/corpus/templates/content/actions/add-comment.ts +2 -1
  87. package/corpus/templates/content/actions/update-comment.ts +1 -1
  88. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +24 -15
  89. package/corpus/templates/design/actions/generate-design.ts +10 -7
  90. package/corpus/templates/design/actions/present-design-variants.ts +35 -6
  91. package/corpus/templates/design/actions/run-design-audit.ts +33 -0
  92. package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-catches-a-screen-that-renders-behind-an.md +6 -0
  93. package/corpus/templates/design/shared/design-review.ts +1 -0
  94. package/corpus/templates/design/shared/html-integrity.ts +66 -0
  95. package/corpus/templates/dispatch/.env.example +5 -0
  96. package/corpus/templates/dispatch/AGENTS.md +24 -19
  97. package/corpus/templates/dispatch/app/i18n-data.ts +20 -0
  98. package/corpus/templates/dispatch/server/lib/identity-sso.ts +21 -90
  99. package/corpus/templates/dispatch/server/plugins/identity-sso.ts +3 -12
  100. package/corpus/templates/dispatch/shared/feature-flags.ts +4 -0
  101. package/corpus/templates/factory/AGENTS.md +5 -1
  102. package/corpus/templates/factory/README.md +23 -0
  103. package/corpus/templates/factory/actions/view-screen.ts +17 -0
  104. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +239 -0
  105. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +2 -0
  106. package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +130 -7
  107. package/corpus/templates/factory/app/i18n/en-US.ts +20 -0
  108. package/corpus/templates/factory/app/routes/factory.tsx +41 -0
  109. package/corpus/templates/factory/package.json +1 -0
  110. package/corpus/templates/factory/server/factory-graph/contracts.ts +2 -0
  111. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -0
  112. package/corpus/templates/factory/server/plugins/dispatch-db.ts +1 -0
  113. package/corpus/templates/factory/server/plugins/setup-dispatch.ts +3 -0
  114. package/corpus/templates/mail/app/lib/native-mcp-exclusions.ts +3 -0
  115. package/corpus/templates/mail/vite.config.ts +7 -0
  116. package/corpus/templates/plan/actions/reply-to-plan-comment.ts +7 -2
  117. package/corpus/templates/plan/actions/resolve-plan-comment.ts +2 -2
  118. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  119. package/corpus/templates/plan/app/pages/PlansPage.tsx +29 -20
  120. package/corpus/templates/slides/actions/add-slide-comment.ts +1 -1
  121. package/corpus/templates/slides/actions/update-slide-comment.ts +1 -1
  122. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +9 -6
  123. package/dist/agent/production-agent.js +5 -1
  124. package/dist/agent/thread-data-builder.js +17 -1
  125. package/dist/cli/skills-content/help.js +6 -4
  126. package/dist/cli/skills.js +17 -2
  127. package/dist/cli/workspacify.d.ts +5 -0
  128. package/dist/cli/workspacify.js +65 -8
  129. package/dist/client/AgentPanel.d.ts +5 -0
  130. package/dist/client/AgentPanel.js +20 -4
  131. package/dist/client/AssistantChat.d.ts +21 -0
  132. package/dist/client/AssistantChat.js +66 -68
  133. package/dist/client/active-run-state.d.ts +14 -1
  134. package/dist/client/active-run-state.js +0 -0
  135. package/dist/client/agent-chat-adapter.js +107 -5
  136. package/dist/client/analytics.js +9 -2
  137. package/dist/client/chat/tool-call-display.js +4 -4
  138. package/dist/client/chat-first/apps-rail.js +9 -4
  139. package/dist/client/chat-first/primary-nav.d.ts +2 -1
  140. package/dist/client/chat-first/primary-nav.js +3 -3
  141. package/dist/client/chat-first/types.d.ts +5 -1
  142. package/dist/client/markdown/InlineMarkdown.d.ts +23 -0
  143. package/dist/client/markdown/InlineMarkdown.js +65 -0
  144. package/dist/client/markdown/index.d.ts +1 -0
  145. package/dist/client/markdown/index.js +1 -0
  146. package/dist/client/review/ReviewThreadPanel.js +2 -1
  147. package/dist/client/run-stream-ownership.d.ts +39 -0
  148. package/dist/client/run-stream-ownership.js +65 -0
  149. package/dist/collab/awareness.d.ts +2 -2
  150. package/dist/collab/struct-routes.d.ts +1 -1
  151. package/dist/feature-flags/index.d.ts +1 -1
  152. package/dist/feature-flags/index.js +1 -1
  153. package/dist/feature-flags/store.d.ts +10 -0
  154. package/dist/feature-flags/store.js +112 -1
  155. package/dist/guards/no-env-credentials.js +4 -0
  156. package/dist/index.d.ts +1 -1
  157. package/dist/index.js +1 -1
  158. package/dist/integrations/adapters/slack.js +5 -1
  159. package/dist/localization/default-messages.d.ts +1 -0
  160. package/dist/localization/default-messages.js +1 -0
  161. package/dist/observability/routes.d.ts +6 -6
  162. package/dist/progress/routes.d.ts +1 -1
  163. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  164. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  165. package/dist/resources/agents.js +40 -5
  166. package/dist/resources/handlers.d.ts +1 -1
  167. package/dist/resources/metadata.d.ts +10 -0
  168. package/dist/resources/metadata.js +7 -3
  169. package/dist/review/actions/create-review-comment.js +1 -1
  170. package/dist/review/actions/reply-review-comment.js +1 -1
  171. package/dist/review/actions/resolve-review-thread.js +1 -1
  172. package/dist/secrets/routes.d.ts +3 -3
  173. package/dist/server/action-routes.js +32 -6
  174. package/dist/server/agent-discovery.js +14 -3
  175. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  176. package/dist/server/core-routes-plugin.js +1 -1
  177. package/dist/server/identity-sso-migrations.d.ts +7 -0
  178. package/dist/server/identity-sso-migrations.js +36 -0
  179. package/dist/server/identity-sso-store.d.ts +3 -2
  180. package/dist/server/identity-sso-store.js +8 -3
  181. package/dist/server/realtime-token.d.ts +1 -1
  182. package/dist/server/release-migrations.js +4 -0
  183. package/dist/tracking/providers.js +13 -3
  184. package/dist/workspace-files/tool.js +1 -1
  185. package/package.json +3 -2
@@ -6,7 +6,11 @@
6
6
  * of that share and is omitted unless the meeting owner enables it.
7
7
  */
8
8
 
9
- import { getSession, runWithRequestContext } from "@agent-native/core/server";
9
+ import {
10
+ getSession,
11
+ runWithRequestContext,
12
+ verifyScopedAgentAccessToken,
13
+ } from "@agent-native/core/server";
10
14
  import { resolveAccess } from "@agent-native/core/sharing";
11
15
  import { eq } from "drizzle-orm";
12
16
  import {
@@ -16,6 +20,10 @@ import {
16
20
  setResponseStatus,
17
21
  } from "h3";
18
22
 
23
+ import {
24
+ CLIPS_MEETING_AGENT_ACCESS_PARAM,
25
+ CLIPS_MEETING_AGENT_RESOURCE_KIND,
26
+ } from "../../../shared/meeting-agent-access.js";
19
27
  import {
20
28
  normalizeTranscriptSegments,
21
29
  parseTranscriptSegments,
@@ -43,17 +51,31 @@ function parseBullets(raw: string | null | undefined): Bullet[] {
43
51
  }
44
52
  }
45
53
 
54
+ function queryString(value: unknown): string {
55
+ if (typeof value === "string") return value;
56
+ if (Array.isArray(value) && typeof value[0] === "string") return value[0];
57
+ return "";
58
+ }
59
+
46
60
  export default defineEventHandler(async (event) => {
47
61
  setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
48
62
  setResponseHeader(event, "Referrer-Policy", "no-referrer");
49
63
 
50
64
  const query = getQuery(event);
51
- const meetingId = typeof query.id === "string" ? query.id : "";
65
+ const meetingId = queryString(query.id);
52
66
  if (!meetingId) {
53
67
  setResponseStatus(event, 400);
54
68
  return { error: "id is required" };
55
69
  }
56
70
 
71
+ const agentAccessToken = queryString(query[CLIPS_MEETING_AGENT_ACCESS_PARAM]);
72
+ const tokenAccess = agentAccessToken
73
+ ? verifyScopedAgentAccessToken(agentAccessToken, {
74
+ resourceKind: CLIPS_MEETING_AGENT_RESOURCE_KIND,
75
+ resourceId: meetingId,
76
+ })
77
+ : null;
78
+
57
79
  const session = await getSession(event).catch(() => null);
58
80
  const accessContext = {
59
81
  userEmail: session?.email,
@@ -61,14 +83,25 @@ export default defineEventHandler(async (event) => {
61
83
  };
62
84
 
63
85
  return runWithRequestContext(accessContext, async () => {
64
- const access = await resolveAccess("meeting", meetingId, accessContext);
86
+ let db: ReturnType<typeof getDb> | undefined;
87
+ const access = tokenAccess?.ok
88
+ ? await (async () => {
89
+ db = getDb();
90
+ const [resource] = await db
91
+ .select()
92
+ .from(schema.meetings)
93
+ .where(eq(schema.meetings.id, meetingId))
94
+ .limit(1);
95
+ return resource ? { role: "viewer" as const, resource } : null;
96
+ })()
97
+ : await resolveAccess("meeting", meetingId, accessContext);
65
98
  const meeting = access?.resource;
66
99
  if (!meeting || meeting.trashedAt) {
67
100
  setResponseStatus(event, 404);
68
101
  return { error: "Not found" };
69
102
  }
70
103
 
71
- const db = getDb();
104
+ db ??= getDb();
72
105
  const [participants, actionItems, transcriptRows] = await Promise.all([
73
106
  db
74
107
  .select({
@@ -58,6 +58,7 @@ import {
58
58
  parseSpaceIds,
59
59
  type RecordingVisibility,
60
60
  } from "../../lib/recordings.js";
61
+ import { isSeekableRepairPending } from "../../lib/seekable-media-state.js";
61
62
  import { verifySharePassword } from "../../lib/share-password.js";
62
63
 
63
64
  function appPath(path: string): string {
@@ -406,11 +407,19 @@ export default defineEventHandler(async (event) => {
406
407
  // Referer of any outbound link the share page renders.
407
408
  setResponseHeader(event, "Referrer-Policy", "no-referrer");
408
409
  const transcriptPresentation = resolveTranscriptPresentation(transcript);
409
- const verificationPending = await isMediaVerificationPending({
410
- ownerEmail: rec.ownerEmail,
411
- recordingId,
412
- recordingStatus: rec.status,
413
- });
410
+ const [verificationPending, seekableRepairPending] = await Promise.all([
411
+ isMediaVerificationPending({
412
+ ownerEmail: rec.ownerEmail,
413
+ recordingId,
414
+ recordingStatus: rec.status,
415
+ }),
416
+ isSeekableRepairPending({
417
+ ownerEmail: rec.ownerEmail,
418
+ recordingId,
419
+ recordingStatus: rec.status,
420
+ videoUrl: rec.videoUrl,
421
+ }),
422
+ ]);
414
423
 
415
424
  const [viewCount, agentViewCount] = await Promise.all([
416
425
  countRecordingViews(recordingId),
@@ -454,12 +463,14 @@ export default defineEventHandler(async (event) => {
454
463
  videoUrl: playbackVideoUrl,
455
464
  videoFormat: rec.videoFormat,
456
465
  videoSizeBytes: rec.videoSizeBytes ?? null,
466
+ mediaUpdatedAt: rec.mediaUpdatedAt,
457
467
  width: rec.width,
458
468
  height: rec.height,
459
469
  hasAudio: Boolean(rec.hasAudio),
460
470
  hasCamera: Boolean(rec.hasCamera),
461
471
  status: rec.status,
462
472
  verificationPending,
473
+ seekableRepairPending,
463
474
  uploadProgress: rec.uploadProgress,
464
475
  failureReason: rec.failureReason,
465
476
  // Don't leak the password to clients; just indicate whether one was set.
@@ -37,6 +37,9 @@ export type ClipsUserPrefs = ClipsAiPrefs & {
37
37
  /** Visibility applied to new recordings unless the creator picks another. */
38
38
  export type ClipsDefaultVisibility = "private" | "org" | "public";
39
39
 
40
+ export const DEFAULT_CLIPS_RECORDING_VISIBILITY: ClipsDefaultVisibility =
41
+ "public";
42
+
40
43
  export function isIncludeFullVideoInAiEnabled(
41
44
  prefs: ClipsAiPrefs | Record<string, unknown> | null | undefined,
42
45
  ): boolean {
@@ -0,0 +1,5 @@
1
+ import { AGENT_ACCESS_PARAM } from "@agent-native/core/shared";
2
+
3
+ export const CLIPS_MEETING_AGENT_RESOURCE_KIND = "clips:meeting";
4
+ export const CLIPS_MEETING_AGENT_CONTEXT_ENDPOINT = "/api/public-meeting";
5
+ export const CLIPS_MEETING_AGENT_ACCESS_PARAM = AGENT_ACCESS_PARAM;
@@ -44,7 +44,8 @@ function displayNameFromEmail(email: string): string {
44
44
  }
45
45
 
46
46
  export default defineAction({
47
- description: "Add a comment to a document. For new threads, omit threadId.",
47
+ description:
48
+ "Add a comment to a document. Comment text supports inline Markdown for emphasis, inline code, links, and line breaks; headings are flattened. For new threads, omit threadId.",
48
49
  schema: z.object({
49
50
  documentId: z.string().optional().describe("Document ID (required)"),
50
51
  content: z.string().optional().describe("Comment text (required)"),
@@ -9,7 +9,7 @@ import { getDb, schema } from "../server/db/index.js";
9
9
 
10
10
  export default defineAction({
11
11
  description:
12
- "Update a document comment. Resolving or reopening a comment applies to the full thread.",
12
+ "Update a document comment. Comment text supports inline Markdown without headings. Resolving or reopening a comment applies to the full thread.",
13
13
  schema: z.object({
14
14
  id: z.string().describe("Comment ID"),
15
15
  documentId: z.string().optional().describe("Document ID"),
@@ -1,6 +1,10 @@
1
1
  import { sendToAgentChat } from "@agent-native/core/client/agent-chat";
2
2
  import { emailToName } from "@agent-native/core/client/collab";
3
3
  import { useT } from "@agent-native/core/client/i18n";
4
+ import {
5
+ InlineMarkdown,
6
+ type InlineMarkdownProtectedSpan,
7
+ } from "@agent-native/core/client/markdown";
4
8
  import {
5
9
  IconCheck,
6
10
  IconMessageCircle,
@@ -40,23 +44,28 @@ import { CommentComposer, type MentionEntry } from "./CommentComposer";
40
44
 
41
45
  /**
42
46
  * Render a comment body, styling any `@mention` tokens that match the comment's
43
- * stored mentions. Plain text otherwise — no HTML is interpreted.
47
+ * stored mentions. Raw HTML is never interpreted.
44
48
  */
45
- function renderCommentBody(content: string, mentions: CommentMention[]) {
49
+ function commentMentionSpans(
50
+ mentions: CommentMention[],
51
+ ): InlineMarkdownProtectedSpan[] {
46
52
  const labels = Array.from(
47
53
  new Set(mentions.map((m) => m.name).filter((n): n is string => !!n)),
48
54
  ).sort((a, b) => b.length - a.length);
49
- if (labels.length === 0) return content;
50
- const escaped = labels.map((l) => l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
51
- const re = new RegExp(`(@(?:${escaped.join("|")}))`, "g");
52
- return content.split(re).map((seg, i) =>
53
- seg.startsWith("@") && labels.includes(seg.slice(1)) ? (
54
- <span key={i} className="comment-mention">
55
- {seg}
56
- </span>
57
- ) : (
58
- seg
59
- ),
55
+ return labels.map((label) => ({
56
+ source: `@${label}`,
57
+ label: `@${label}`,
58
+ className: "comment-mention",
59
+ }));
60
+ }
61
+
62
+ function renderCommentBody(content: string, mentions: CommentMention[]) {
63
+ return (
64
+ <InlineMarkdown
65
+ content={content}
66
+ inline
67
+ protectedSpans={commentMentionSpans(mentions)}
68
+ />
60
69
  );
61
70
  }
62
71
 
@@ -942,9 +951,9 @@ function ThreadView({
942
951
  {formatDate(c.created_at)}
943
952
  </span>
944
953
  </div>
945
- <p className="text-[13px] text-foreground/90 pl-8 leading-relaxed whitespace-pre-wrap">
954
+ <div className="text-[13px] text-foreground/90 pl-8 leading-relaxed">
946
955
  {renderCommentBody(c.content, c.mentions)}
947
- </p>
956
+ </div>
948
957
  </div>
949
958
  ))}
950
959
  </div>
@@ -171,12 +171,15 @@ const reuseLabelSchema = z
171
171
  }
172
172
  });
173
173
 
174
- function hasBreakpointSet(value: unknown): boolean {
175
- if (!value || typeof value !== "object" || Array.isArray(value)) {
176
- return false;
177
- }
174
+ function classifyBreakpointSet(
175
+ value: unknown,
176
+ ): "absent" | "malformed" | "present" {
177
+ if (value === null || value === undefined) return "absent";
178
+ if (typeof value !== "object" || Array.isArray(value)) return "malformed";
178
179
  const breakpoints = (value as { breakpoints?: unknown }).breakpoints;
179
- return Array.isArray(breakpoints) && breakpoints.length > 0;
180
+ if (breakpoints === undefined) return "malformed";
181
+ if (!Array.isArray(breakpoints)) return "malformed";
182
+ return breakpoints.length > 0 ? "present" : "malformed";
180
183
  }
181
184
 
182
185
  function jsonValuesEqual(left: unknown, right: unknown): boolean {
@@ -1137,7 +1140,7 @@ const generateDesignAction = defineAction({
1137
1140
  mergedData.breakpointSetUpdatedAt = updatedAt;
1138
1141
  } else if (
1139
1142
  generatedBreakpointSet.length > 0 &&
1140
- !hasBreakpointSet(mergedData.breakpointSet)
1143
+ classifyBreakpointSet(mergedData.breakpointSet) === "absent"
1141
1144
  ) {
1142
1145
  mergedData.breakpointSet = {
1143
1146
  id: "generated-responsive",
@@ -1200,7 +1203,7 @@ const generateDesignAction = defineAction({
1200
1203
  devices && devices.length > 0
1201
1204
  ? jsonValuesEqual(currentBreakpointWidths, expectedBreakpointWidths)
1202
1205
  : generatedBreakpointSet.length === 0 ||
1203
- hasBreakpointSet(current.breakpointSet);
1206
+ classifyBreakpointSet(current.breakpointSet) !== "absent";
1204
1207
  return framesApplied && breakpointSetApplied;
1205
1208
  },
1206
1209
  });
@@ -89,12 +89,25 @@ async function hasLinkedDesignSystem(designId: string): Promise<boolean> {
89
89
  return Boolean(design?.designSystemId?.trim());
90
90
  }
91
91
 
92
- function hasBreakpointSet(value: unknown): boolean {
93
- if (!value || typeof value !== "object" || Array.isArray(value)) {
94
- return false;
95
- }
92
+ /**
93
+ * Absent, or present but unreadable — the caller must not treat these alike.
94
+ * A malformed set is still the user's data: overwriting it with the generated
95
+ * default silently discards breakpoints they configured, so it reports
96
+ * `"malformed"` and the install is skipped rather than clobbering it.
97
+ */
98
+ function classifyBreakpointSet(
99
+ value: unknown,
100
+ ): "absent" | "malformed" | "present" {
101
+ if (value === null || value === undefined) return "absent";
102
+ if (typeof value !== "object" || Array.isArray(value)) return "malformed";
96
103
  const breakpoints = (value as { breakpoints?: unknown }).breakpoints;
97
- return Array.isArray(breakpoints) && breakpoints.length > 0;
104
+ if (breakpoints === undefined) return "malformed";
105
+ if (!Array.isArray(breakpoints)) return "malformed";
106
+ return breakpoints.length > 0 ? "present" : "malformed";
107
+ }
108
+
109
+ function hasBreakpointSet(value: unknown): boolean {
110
+ return classifyBreakpointSet(value) === "present";
98
111
  }
99
112
 
100
113
  function designDeepLink(designId: string): string {
@@ -1042,9 +1055,16 @@ export default defineAction({
1042
1055
  });
1043
1056
  }
1044
1057
 
1058
+ // Presenting options should not silently reconfigure the design. When it
1059
+ // does, the overview paints an extra preview beside EVERY primary frame
1060
+ // from then on — including screens created later — so the caller is told.
1061
+ let installedBreakpointSet = false;
1062
+
1045
1063
  await mutateDesignData({
1046
1064
  designId,
1047
1065
  mutate: (current, { updatedAt }) => {
1066
+ installedBreakpointSet =
1067
+ classifyBreakpointSet(current.breakpointSet) === "absent";
1048
1068
  // Placement depends on the breakpoint set in effect after this call, so
1049
1069
  // it is resolved here (inside the compare-and-set body) rather than
1050
1070
  // against a design snapshot that a concurrent write may have moved on
@@ -1106,7 +1126,9 @@ export default defineAction({
1106
1126
  canvasFrames: mergedFrames.canvasFrames,
1107
1127
  screenMetadata: previousMetadata,
1108
1128
  designVariantSets: previousVariantSets,
1109
- ...(hasBreakpointSet(current.breakpointSet)
1129
+ // Only when genuinely absent. A malformed set is the user's data in
1130
+ // a shape this action cannot read, not a blank slate to overwrite.
1131
+ ...(classifyBreakpointSet(current.breakpointSet) !== "absent"
1110
1132
  ? {}
1111
1133
  : {
1112
1134
  breakpointSet: {
@@ -1227,6 +1249,13 @@ export default defineAction({
1227
1249
  embed: true,
1228
1250
  cleanedUpPreviousVariantScreens: variantSetCleanup.removedFileIds.length,
1229
1251
  deletedSupersededSetIds: variantSetCleanup.removedSetIds,
1252
+ ...(installedBreakpointSet
1253
+ ? {
1254
+ installedBreakpointSet: DEFAULT_RESPONSIVE_BREAKPOINTS.map(
1255
+ (breakpoint) => breakpoint.widthPx,
1256
+ ),
1257
+ }
1258
+ : {}),
1230
1259
  fallbackInstructions: FALLBACK_INSTRUCTIONS,
1231
1260
  nextRequiredAction:
1232
1261
  'Wait for the user to pick a screen in chat. Then delete each unchosen variant screen with delete-file at most once, call get-design-snapshot exactly once with fileId for the chosen screen, and call edit-design with that same fileId in a bounded pass. Use mode "replace-file" to replace the representative direction screen with a complete but compact requested app/product UI in the chosen visual style. Prioritize the primary workflow and render secondary details as visible controls, states, or affordances if the full feature list is too large for one reliable edit. Do not leave a direction board, variant brief, or summary card as the final result. Do not repeat delete/snapshot cycles. Do not call generate-design after a variant pick. Stop after the first successful edit-design save.',
@@ -25,6 +25,10 @@ import type {
25
25
  A11yFindingCategory,
26
26
  A11ySeverity,
27
27
  } from "../shared/design-review.js";
28
+ import {
29
+ describeDesignHtmlIntegrityIssue,
30
+ inspectDesignHtmlDocumentIntegrity,
31
+ } from "../shared/html-integrity.js";
28
32
 
29
33
  // ---------------------------------------------------------------------------
30
34
  // HTML helpers (static analysis — no DOM runtime available server-side)
@@ -259,6 +263,34 @@ export function checkTapTargets(html: string): A11yFinding[] {
259
263
  return findings;
260
264
  }
261
265
 
266
+ /**
267
+ * Screens that render with an Alpine-controlled element covering them. Every
268
+ * other check here is a regex over the raw string; this one delegates to the
269
+ * save-time integrity parser so the audit and the write gate cannot drift into
270
+ * disagreeing about the same document. It is the only check that can see the
271
+ * failure a screenshot cannot: `take-design-screenshot` waits for Alpine to
272
+ * settle, so a pre-Alpine cover never appears in the agent's own capture.
273
+ */
274
+ export function checkRenderBlockingOverlays(html: string): A11yFinding[] {
275
+ const result = inspectDesignHtmlDocumentIntegrity(html);
276
+ const issues = [...(result.detail ?? []), ...(result.advisory ?? [])].filter(
277
+ (issue) =>
278
+ issue.issue === "runtime-overlay-unhidden" ||
279
+ issue.issue === "runtime-cloak-missing" ||
280
+ issue.issue === "runtime-alpine-missing",
281
+ );
282
+
283
+ return issues.map((issue, index) => ({
284
+ id: `render-blocking-overlay:${issue.issue}-${index}`,
285
+ severity: "error" as A11ySeverity,
286
+ category: "render-blocking-overlay" as A11yFindingCategory,
287
+ message:
288
+ "An Alpine-controlled element can cover the screen before Alpine starts.",
289
+ detail: describeDesignHtmlIntegrityIssue(issue),
290
+ fixAvailable: false,
291
+ }));
292
+ }
293
+
262
294
  /** Check for animations/transitions without a prefers-reduced-motion guard. */
263
295
  function checkReducedMotion(html: string): A11yFinding[] {
264
296
  const findings: A11yFinding[] = [];
@@ -848,6 +880,7 @@ export default defineAction({
848
880
  ...checkReducedMotion(html),
849
881
  ...checkFocusVisibility(html),
850
882
  ...checkContrastHint(html),
883
+ ...checkRenderBlockingOverlays(html),
851
884
  ...tokenDriftFindings,
852
885
  ...designSystemFindings,
853
886
  ];
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-08-13
4
+ ---
5
+
6
+ The design audit now catches a screen that renders behind a full-frame Alpine overlay — the failure a screenshot cannot show, because the capture waits for Alpine to settle and the overlay is gone by then.
@@ -25,6 +25,7 @@ export const A11Y_FINDING_CATEGORIES = [
25
25
  "role",
26
26
  "token-drift",
27
27
  "design-system-drift",
28
+ "render-blocking-overlay",
28
29
  "other",
29
30
  ] as const;
30
31
 
@@ -34,6 +34,7 @@ export type DesignHtmlIntegrityIssue =
34
34
  | "runtime-missing"
35
35
  | "runtime-alpine-missing"
36
36
  | "runtime-cloak-missing"
37
+ | "runtime-overlay-unhidden"
37
38
  | "url-backed-screen-replaced";
38
39
 
39
40
  /**
@@ -153,6 +154,14 @@ export function describeDesignHtmlIntegrityIssue(
153
154
  `but no script source contains Alpine.js. The element will remain ` +
154
155
  `hidden forever; load Alpine.js before relying on x-cloak.`
155
156
  );
157
+ case "runtime-overlay-unhidden":
158
+ return (
159
+ `<${detail.tag ?? "element"}> at ${at} covers the whole frame and is ` +
160
+ `shown by Alpine (${detail.attribute ?? "x-show"}), but nothing hides ` +
161
+ `it before Alpine starts. It paints over the design on first render. ` +
162
+ `Add x-cloak plus [x-cloak] { display: none !important; }, or an ` +
163
+ `inline style="display:none".`
164
+ );
156
165
  case "runtime-cloak-missing":
157
166
  return (
158
167
  `the document uses x-cloak on <${detail.tag ?? "element"}> at ${at}, ` +
@@ -1175,6 +1184,53 @@ function collectInteractiveRuntimeIssues(
1175
1184
  ];
1176
1185
  }
1177
1186
 
1187
+ /**
1188
+ * Covers the frame when it is taken out of flow AND stretched to every edge.
1189
+ * `fixed` alone is a toolbar; `inset-0` alone is a normal filled parent.
1190
+ */
1191
+ function coversViewport(className: string): boolean {
1192
+ const classes = new Set(className.split(/\s+/));
1193
+ const positioned = classes.has("fixed") || classes.has("absolute");
1194
+ const fullBleed =
1195
+ classes.has("inset-0") ||
1196
+ (classes.has("w-screen") && classes.has("h-screen"));
1197
+ return positioned && fullBleed;
1198
+ }
1199
+
1200
+ /**
1201
+ * The shape the `x-cloak` checks above cannot see. An overlay toggled by
1202
+ * `x-show` with no `x-cloak` at all paints over the design until Alpine
1203
+ * evaluates the expression — the same symptom, minus the attribute that makes
1204
+ * it detectable. Advisory rather than blocking: with Alpine healthy this is one
1205
+ * frame, and a genuinely broken runtime is already caught above.
1206
+ *
1207
+ * `<template x-if>` is deliberately not checked: template content is inert
1208
+ * until Alpine clones it, so it cannot paint early.
1209
+ */
1210
+ function collectOverlayAdvisory(
1211
+ parsed: ParsedDocument,
1212
+ locate: Locator,
1213
+ ): DesignHtmlIntegrityIssueDetail[] {
1214
+ for (const element of parsed.elements) {
1215
+ const names = new Set(
1216
+ element.attrs.map((attribute) => attribute.name.toLowerCase()),
1217
+ );
1218
+ if (names.has("x-cloak") || !names.has("x-show")) continue;
1219
+ if (!coversViewport(attributeOf(element, "class") ?? "")) continue;
1220
+ if (INLINE_PRE_HIDE.test(attributeOf(element, "style") ?? "")) continue;
1221
+
1222
+ return [
1223
+ {
1224
+ issue: "runtime-overlay-unhidden",
1225
+ ...locate(locationOf(element)?.startOffset ?? 0),
1226
+ tag: element.tagName,
1227
+ attribute: "x-show",
1228
+ },
1229
+ ];
1230
+ }
1231
+ return [];
1232
+ }
1233
+
1178
1234
  /**
1179
1235
  * Reported, never enforced: legitimate fragments and token-only screens carry no
1180
1236
  * runtime of their own, so blocking here would reject valid work.
@@ -1182,6 +1238,16 @@ function collectInteractiveRuntimeIssues(
1182
1238
  function collectAdvisoryIssues(
1183
1239
  parsed: ParsedDocument,
1184
1240
  locate: Locator,
1241
+ ): DesignHtmlIntegrityIssueDetail[] {
1242
+ return [
1243
+ ...collectOverlayAdvisory(parsed, locate),
1244
+ ...collectTailwindRuntimeAdvisory(parsed, locate),
1245
+ ];
1246
+ }
1247
+
1248
+ function collectTailwindRuntimeAdvisory(
1249
+ parsed: ParsedDocument,
1250
+ locate: Locator,
1185
1251
  ): DesignHtmlIntegrityIssueDetail[] {
1186
1252
  // Only documents that actually depend on utility classes can be broken by a
1187
1253
  // missing runtime. A screen styled entirely through its own CSS needs no
@@ -43,6 +43,11 @@
43
43
  # App URL (used for webhook callback construction)
44
44
  # APP_URL=https://your-app.example.com
45
45
 
46
+ # Optional exact registry for custom workspace apps that opt into Desktop SSO.
47
+ # Each entry must declare appId, clientId, an exact HTTPS origin, callbackPath
48
+ # "/_agent-native/identity/callback", and the "identity-sso" capability.
49
+ # IDENTITY_SSO_APP_REGISTRY_JSON=
50
+
46
51
  # Builder-backed app creation in production
47
52
  # DISPATCH_BUILDER_PROJECT_ID= # Default Builder project for new app branches
48
53
  # BUILDER_PUBLIC_KEY= # Builder space/public key
@@ -13,10 +13,9 @@ Read the relevant skill before deeper work:
13
13
 
14
14
  ## Core Rules
15
15
 
16
- - Store large file/blob payloads in configured file/blob storage, not SQL: no
17
- base64, `data:` URLs, images, video/audio, PDFs, ZIPs, screenshots,
18
- thumbnails, or replay chunks in app tables, `application_state`, `settings`,
19
- or `resources`; persist URLs, ids, or handles instead.
16
+ - Store large file/blob payloads in configured file/blob storage, not SQL:
17
+ persist URLs, ids, or handles instead of base64, media, documents, archives,
18
+ screenshots, thumbnails, or replay chunks.
20
19
  - Never hardcode API keys, tokens, webhook URLs, signing secrets, private Builder/internal data, customer data, or credential-looking literals. Use secrets/OAuth/runtime configuration and obvious placeholders in examples.
21
20
  - Treat Dispatch as workspace infrastructure. Prefer actions over raw SQL for
22
21
  vault, integrations, resource grants, messaging, routing, and approvals.
@@ -32,13 +31,22 @@ Read the relevant skill before deeper work:
32
31
  values or silently widen app access while doing this.
33
32
  - Use `view-screen` when the current integration, resource, approval, route, or
34
33
  setup item is unclear.
35
- - Dispatch keeps its primary navigation focused on Overview, Chat, Apps, and
36
- the workspace app rail. Workspace management and operator tools live under
37
- the top-level `/admin` control plane, which uses grouped navigation for
38
- `/admin/operations`, `/admin/metrics`, `/admin/integrations`, `/admin/vault`,
39
- `/admin/automations`, `/admin/approvals`, `/admin/destinations`,
40
- `/admin/agents`, `/admin/workspace`, `/admin/messaging`, `/admin/identities`,
41
- `/admin/audit`, `/admin/dreams`, and `/admin/thread-debug`.
34
+ - Use `import-agent` to normalize safe Claude Markdown or JSON into an
35
+ `agents/<slug>.md` profile; it skips credentials, hooks, shell, and local env.
36
+ - Use `import-agent-pack` for a Claude Project/Cowork-style folder. It keeps the
37
+ runnable profile at `agents/<slug>.md` and stores text references, context,
38
+ and agent-owned skills below `agents/<slug>/`. Use `list-agent-pack` to
39
+ inspect the files and the shared agent pack UI to edit them.
40
+ - Use `connect-external-agent` for public HTTP/A2A metadata; authenticate through
41
+ the normal connection flow.
42
+ - Dispatch's primary nav is Overview, Chat, Apps, Agents, and the app rail.
43
+ `/agents` creates/imports reusable profiles, opens per-agent chat, and can
44
+ hand a profile off to app creation; `/admin/agents` manages technical MCP/A2A
45
+ connections; other workspace/operator tools live under `/admin`.
46
+ - Agent profiles and agentic apps are also managed from Factory's top-level
47
+ Agents tab. Factory embeds the same agent actions and pack editor and reads
48
+ mounted app metadata through the shared Dispatch database; it does not create
49
+ a second agent registry.
42
50
  - Keep approval and routing behavior explicit. Never silently widen access to
43
51
  secrets, apps, integrations, or workspace resources.
44
52
  - Curated workspace templates are private app sources. Use
@@ -65,15 +73,12 @@ Read the relevant skill before deeper work:
65
73
  preserve the returned per-source health instead of treating an unavailable
66
74
  source as zero failures.
67
75
  - For a Slack-linked issue, call `read-slack-thread-context` with the exact
68
- permalink before diagnosing it. It resolves child links to the parent thread,
69
- preserves attachments and related URLs, and reports whether pagination is
70
- complete. Never treat an unreadable Slack thread as an empty one.
76
+ permalink before diagnosing it; preserve its pagination and readability
77
+ status instead of treating an unreadable thread as empty.
71
78
  - For usage investigations, use `list-dispatch-usage-metrics` with the smallest
72
- useful scope (`me` or `workspace`), lookback, and optional `userEmail` filter.
73
- Treat prompt attribution as evidence with gaps: `not-captured` and
74
- `unavailable` are not zero usage or a reason to guess. Use `view-screen` on
75
- `/admin/metrics` to keep the agent's analysis aligned with the visible scope
76
- and selected user.
79
+ useful scope and lookback. Treat `not-captured` and `unavailable` attribution
80
+ as gaps, not zero usage; use `view-screen` on `/admin/metrics` to align with
81
+ the visible scope and selected user.
77
82
 
78
83
  ## Application State
79
84