@agent-native/core 0.136.4 → 0.137.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 (170) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +53 -0
  3. package/corpus/templates/clips/actions/import-loom-recording.ts +7 -0
  4. package/corpus/templates/clips/actions/lib/loom-import-job.ts +24 -4
  5. package/corpus/templates/clips/app/components/import-menu.tsx +109 -0
  6. package/corpus/templates/clips/app/components/library/library-grid.tsx +8 -18
  7. package/corpus/templates/clips/app/components/library/library-layout.tsx +14 -50
  8. package/corpus/templates/clips/app/components/library/sort-menu.tsx +20 -6
  9. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +10 -34
  10. package/corpus/templates/clips/app/routes/_app.library._index.tsx +0 -2
  11. package/corpus/templates/clips/changelog/2026-08-03-restart-during-a-recording-now-immediately-starts-a-fresh-ta.md +6 -0
  12. package/corpus/templates/clips/changelog/2026-08-04-cleaner-library-controls-with-an-icon-only-sort-button.md +6 -0
  13. package/corpus/templates/clips/changelog/2026-08-04-upload-and-loom-imports-now-live-under-one-import-menu.md +6 -0
  14. package/corpus/templates/clips/desktop/src/app.tsx +81 -30
  15. package/corpus/templates/clips/desktop/src/lib/recorder.ts +436 -158
  16. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +14 -1
  17. package/corpus/templates/clips/server/plugins/auth.ts +9 -0
  18. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +13 -0
  19. package/corpus/templates/content/actions/_database-utils.ts +6 -0
  20. package/corpus/templates/content/actions/bind-content-database-source-field.ts +273 -97
  21. package/corpus/templates/content/actions/change-content-database-source-role.ts +65 -29
  22. package/corpus/templates/content/actions/configure-document-property.ts +113 -32
  23. package/corpus/templates/content/actions/delete-content-database.ts +5 -7
  24. package/corpus/templates/content/actions/delete-document-property.ts +109 -62
  25. package/corpus/templates/content/actions/delete-document.ts +178 -36
  26. package/corpus/templates/content/actions/duplicate-database-item.ts +27 -0
  27. package/corpus/templates/content/actions/duplicate-database-items.ts +31 -1
  28. package/corpus/templates/content/actions/permanently-delete-document.ts +4 -6
  29. package/corpus/templates/content/actions/remove-database-items.ts +13 -0
  30. package/corpus/templates/content/actions/set-document-property.ts +69 -1
  31. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +814 -0
  32. package/corpus/templates/content/parity/matrix.md +1 -1
  33. package/corpus/templates/content/parity/matrix.ts +1 -0
  34. package/corpus/templates/content/server/db/schema.ts +27 -0
  35. package/corpus/templates/content/server/plugins/db.ts +24 -0
  36. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +122 -114
  37. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
  38. package/corpus/templates/design/app/components/design/EditPanel.tsx +68 -34
  39. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +78 -4
  40. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +8 -0
  41. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +5 -2
  42. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +10 -9
  43. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +31 -69
  44. package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +80 -106
  45. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
  46. package/corpus/templates/design/app/hooks/use-editor-preferences.ts +75 -0
  47. package/corpus/templates/design/app/i18n-data.ts +8 -0
  48. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +61 -0
  49. package/corpus/templates/design/app/pages/design-editor/editor-preferences.ts +98 -0
  50. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +491 -0
  51. package/corpus/templates/design/app/pages/design-editor/paste-placement.ts +133 -0
  52. package/corpus/templates/design/app/pages/design-editor/png-export-render.ts +8 -0
  53. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -12
  54. package/corpus/templates/design/changelog/2026-08-03-arrow-keys-now-reorder-a-layer-inside-an-auto-layout-frame.md +6 -0
  55. package/corpus/templates/design/changelog/2026-08-03-auto-layout-flow-icons-show-the-direction-at-a-glance-instea.md +6 -0
  56. package/corpus/templates/design/changelog/2026-08-03-auto-layout-icons-share-one-outlined-square-style-so-flow-ga.md +6 -0
  57. package/corpus/templates/design/changelog/2026-08-03-export-has-a-named-preview-section-you-can-expand-replacing-.md +6 -0
  58. package/corpus/templates/design/changelog/2026-08-03-export-preview-shows-the-selected-element-instead-of-the-who.md +6 -0
  59. package/corpus/templates/design/changelog/2026-08-03-inspector-sections-show-an-expand-indicator-so-it-is-clear-w.md +6 -0
  60. package/corpus/templates/design/changelog/2026-08-03-paste-lands-inside-a-selected-frame-instead-of-beside-it.md +6 -0
  61. package/corpus/templates/design/changelog/2026-08-03-the-add-breakpoint-button-sits-outside-the-breakpoint-select.md +6 -0
  62. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-keep-space-below-the-continu.md +6 -0
  63. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-share-the-chat-panel-s-backg.md +6 -0
  64. package/corpus/templates/design/changelog/2026-08-03-the-export-panel-no-longer-stacks-a-duplicate-export-block-o.md +6 -0
  65. package/corpus/templates/design/changelog/2026-08-03-the-text-tool-uses-a-t-icon-matching-the-letter-it-inserts.md +6 -0
  66. package/corpus/templates/factory/.env.example +17 -0
  67. package/corpus/templates/factory/AGENTS.md +27 -17
  68. package/corpus/templates/factory/README.md +19 -9
  69. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +90 -0
  70. package/corpus/templates/factory/actions/get-triage-config.ts +14 -3
  71. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +386 -0
  72. package/corpus/templates/factory/actions/list-factory-automations.ts +53 -0
  73. package/corpus/templates/factory/actions/list-triage-items.ts +7 -2
  74. package/corpus/templates/factory/actions/poll-github-sources.ts +203 -0
  75. package/corpus/templates/factory/actions/poll-sentry-errors.ts +161 -0
  76. package/corpus/templates/factory/actions/poll-slack-channel.ts +4 -2
  77. package/corpus/templates/factory/actions/run-factory-automation.ts +42 -0
  78. package/corpus/templates/factory/actions/save-factory-automation.ts +69 -0
  79. package/corpus/templates/factory/actions/save-triage-config.ts +28 -2
  80. package/corpus/templates/factory/actions/start-builder-for-item.ts +411 -0
  81. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +14 -3
  82. package/corpus/templates/factory/app/components/ui/checkbox.tsx +1 -0
  83. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +7 -0
  84. package/corpus/templates/factory/app/i18n/en-US.ts +35 -1
  85. package/corpus/templates/factory/app/routes/factory.tsx +462 -9
  86. package/corpus/templates/factory/changelog/2026-08-04-added-organization-owned-factory-automations-for-slack-bug-t.md +6 -0
  87. package/corpus/templates/factory/changelog/2026-08-04-automation-editor-controls-and-model-picker.md +6 -0
  88. package/corpus/templates/factory/netlify.toml +2 -2
  89. package/corpus/templates/factory/server/connectors/credentials.ts +6 -5
  90. package/corpus/templates/factory/server/connectors/slack.ts +133 -5
  91. package/corpus/templates/factory/server/db/schema.ts +6 -0
  92. package/corpus/templates/factory/server/lib/require-factory-automation.ts +65 -0
  93. package/corpus/templates/factory/server/plugins/agent-chat.ts +29 -11
  94. package/corpus/templates/factory/server/plugins/auth.ts +1 -0
  95. package/corpus/templates/factory/server/plugins/factory-migrations.ts +12 -0
  96. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +218 -67
  97. package/corpus/templates/factory/server/triage/builder-executor.ts +4 -0
  98. package/corpus/templates/factory/server/triage/contracts.ts +14 -1
  99. package/corpus/templates/factory/server/triage/github-client.ts +404 -0
  100. package/corpus/templates/factory/server/triage/metadata.ts +44 -0
  101. package/corpus/templates/factory/server/triage/pr-babysit.ts +5 -5
  102. package/corpus/templates/factory/server/triage/pr-policy.ts +144 -0
  103. package/corpus/templates/factory/server/triage/sentry-client.ts +180 -0
  104. package/corpus/templates/factory/server/triage/slack-client.ts +43 -0
  105. package/corpus/templates/factory/server/triage/slack-poller.ts +9 -2
  106. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +27 -0
  107. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +14 -4
  108. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -0
  109. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -0
  110. package/corpus/templates/plan/actions/create-plan-design.ts +8 -6
  111. package/corpus/templates/plan/actions/create-prototype-plan.ts +8 -6
  112. package/corpus/templates/plan/actions/create-ui-plan.ts +7 -7
  113. package/corpus/templates/plan/actions/get-plan-blocks.ts +7 -1
  114. package/corpus/templates/plan/actions/validate-plan-input.ts +39 -0
  115. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +167 -11
  116. package/corpus/templates/plan/app/i18n/en-US.ts +1 -0
  117. package/corpus/templates/plan/changelog/2026-08-04-canvas-zoom-controls-now-explain-the-command-ctrl-plus-scrol.md +6 -0
  118. package/corpus/templates/plan/changelog/2026-08-04-pinch-to-zoom-is-now-supported-on-touchscreens-in-visual-pla.md +6 -0
  119. package/corpus/templates/plan/shared/plan-content.ts +17 -2
  120. package/dist/cli/skills-content/canvas.d.ts +3 -3
  121. package/dist/cli/skills-content/canvas.js +16 -5
  122. package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
  123. package/dist/cli/skills-content/visual-plan-skill.js +27 -0
  124. package/dist/cli/skills-content/wireframe.d.ts +2 -2
  125. package/dist/cli/skills-content/wireframe.js +7 -0
  126. package/dist/client/AgentPanel.js +23 -8
  127. package/dist/client/api-path.d.ts +6 -0
  128. package/dist/client/api-path.js +55 -0
  129. package/dist/client/onboarding/FirstRunOnboarding.d.ts +3 -0
  130. package/dist/client/onboarding/FirstRunOnboarding.js +93 -0
  131. package/dist/client/onboarding/first-run-enabled.d.ts +2 -0
  132. package/dist/client/onboarding/first-run-enabled.js +9 -0
  133. package/dist/client/onboarding/index.d.ts +2 -1
  134. package/dist/client/onboarding/index.js +1 -0
  135. package/dist/client/onboarding/use-onboarding.d.ts +6 -1
  136. package/dist/client/onboarding/use-onboarding.js +32 -1
  137. package/dist/client/route-state.js +10 -0
  138. package/dist/client/settings/SecretsSection.js +32 -8
  139. package/dist/collab/struct-routes.d.ts +1 -1
  140. package/dist/connections/catalog.d.ts +4 -4
  141. package/dist/connections/catalog.js +3 -3
  142. package/dist/db/index.d.ts +1 -1
  143. package/dist/db/index.js +1 -1
  144. package/dist/localization/default-messages.d.ts +5 -0
  145. package/dist/localization/default-messages.js +5 -0
  146. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  147. package/dist/observability/routes.d.ts +3 -3
  148. package/dist/onboarding/app-profile.d.ts +4 -0
  149. package/dist/onboarding/app-profile.js +378 -0
  150. package/dist/onboarding/index.d.ts +2 -1
  151. package/dist/onboarding/index.js +1 -0
  152. package/dist/onboarding/plugin.d.ts +2 -0
  153. package/dist/onboarding/plugin.js +21 -1
  154. package/dist/onboarding/types.d.ts +19 -0
  155. package/dist/progress/routes.d.ts +1 -1
  156. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  157. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  158. package/dist/secrets/routes.d.ts +2 -2
  159. package/dist/secrets/routes.js +41 -17
  160. package/dist/server/auth.js +21 -1
  161. package/dist/server/better-auth-instance.d.ts +3 -2
  162. package/dist/server/better-auth-instance.js +9 -7
  163. package/dist/server/email-markdown.js +3 -8
  164. package/dist/server/transcribe-voice.d.ts +1 -1
  165. package/dist/shared/first-run-onboarding.d.ts +3 -0
  166. package/dist/shared/first-run-onboarding.js +3 -0
  167. package/dist/triggers/index.d.ts +3 -0
  168. package/dist/triggers/index.js +5 -0
  169. package/docs/content/cross-app-sso.mdx +26 -0
  170. package/package.json +1 -1
@@ -0,0 +1,180 @@
1
+ import { resolveConnectorSecret } from "../connectors/credentials.js";
2
+
3
+ const DEFAULT_BASE_URL = "https://sentry.io/api/0";
4
+ const MAX_LIMIT = 100;
5
+
6
+ export interface SentryClientOptions {
7
+ ownerEmail: string;
8
+ orgId?: string | null;
9
+ orgSlug?: string;
10
+ baseUrl?: string;
11
+ fetchImpl?: typeof fetch;
12
+ }
13
+
14
+ export interface SentryIssue {
15
+ id: string;
16
+ shortId: string;
17
+ title: string;
18
+ culprit: string;
19
+ permalink: string;
20
+ level: string;
21
+ status: string;
22
+ projectSlug: string;
23
+ count: string;
24
+ firstSeen: string;
25
+ lastSeen: string;
26
+ }
27
+
28
+ export interface SentryEvent {
29
+ eventId: string;
30
+ title: string;
31
+ message: string;
32
+ dateCreated: string;
33
+ tags: readonly { key: string; value: string }[];
34
+ context: Readonly<Record<string, unknown>>;
35
+ }
36
+
37
+ interface JsonResponse {
38
+ ok: boolean;
39
+ status: number;
40
+ json(): Promise<unknown>;
41
+ text(): Promise<string>;
42
+ }
43
+
44
+ function object(value: unknown): Record<string, unknown> {
45
+ if (!value || typeof value !== "object" || Array.isArray(value))
46
+ throw new Error("Sentry response was not an object");
47
+ return value as Record<string, unknown>;
48
+ }
49
+
50
+ function string(value: unknown, field: string): string {
51
+ if (typeof value !== "string" || value.length === 0)
52
+ throw new Error(`Sentry response is missing ${field}`);
53
+ return value;
54
+ }
55
+
56
+ function limit(value?: number): number {
57
+ if (value === undefined) return MAX_LIMIT;
58
+ if (!Number.isInteger(value) || value < 1 || value > MAX_LIMIT)
59
+ throw new Error(`Sentry limit must be an integer from 1 to ${MAX_LIMIT}`);
60
+ return value;
61
+ }
62
+
63
+ function parseIssue(value: unknown): SentryIssue {
64
+ const item = object(value);
65
+ const project = object(item.project);
66
+ return {
67
+ id: string(item.id, "issue id"),
68
+ shortId: string(item.shortId, "issue short id"),
69
+ title: string(item.title, "issue title"),
70
+ culprit: string(item.culprit, "issue culprit"),
71
+ permalink: string(item.permalink, "issue permalink"),
72
+ level: string(item.level, "issue level"),
73
+ status: string(item.status, "issue status"),
74
+ projectSlug: string(project.slug, "issue project slug"),
75
+ count: string(item.count, "issue count"),
76
+ firstSeen: string(item.firstSeen, "issue first seen"),
77
+ lastSeen: string(item.lastSeen, "issue last seen"),
78
+ };
79
+ }
80
+
81
+ function parseEvent(value: unknown): SentryEvent {
82
+ const item = object(value);
83
+ const tags = item.tags;
84
+ if (!Array.isArray(tags))
85
+ throw new Error("Sentry event response is missing tags");
86
+ return {
87
+ eventId: string(item.eventID, "event id"),
88
+ title: string(item.title, "event title"),
89
+ message: string(item.message, "event message"),
90
+ dateCreated: string(item.dateCreated, "event created time"),
91
+ tags: tags.map((tag) => {
92
+ const value = object(tag);
93
+ return {
94
+ key: string(value.key, "event tag key"),
95
+ value: string(value.value, "event tag value"),
96
+ };
97
+ }),
98
+ context: object(item.context),
99
+ };
100
+ }
101
+
102
+ export function createSentryClient(options: SentryClientOptions) {
103
+ const fetchImpl = options.fetchImpl ?? fetch;
104
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
105
+
106
+ async function credential(
107
+ key: "SENTRY_SERVER_TOKEN" | "SENTRY_AUTH_TOKEN",
108
+ ): Promise<string | undefined> {
109
+ return resolveConnectorSecret(key, options.ownerEmail, {
110
+ orgId: options.orgId,
111
+ });
112
+ }
113
+
114
+ async function request<T>(path: string): Promise<T> {
115
+ const token =
116
+ (await credential("SENTRY_SERVER_TOKEN")) ??
117
+ (await credential("SENTRY_AUTH_TOKEN"));
118
+ if (!token)
119
+ throw new Error(
120
+ "SENTRY_SERVER_TOKEN or SENTRY_AUTH_TOKEN is not configured for this workspace",
121
+ );
122
+ const response = (await fetchImpl(`${baseUrl}${path}`, {
123
+ headers: { Authorization: `Bearer ${token}` },
124
+ })) as JsonResponse;
125
+ if (!response.ok) {
126
+ const detail = (await response.text()).slice(0, 500);
127
+ throw new Error(
128
+ `Sentry API request failed: HTTP ${response.status}${detail ? ` - ${detail}` : ""}`,
129
+ );
130
+ }
131
+ return (await response.json()) as T;
132
+ }
133
+
134
+ async function organization(): Promise<string> {
135
+ const value =
136
+ options.orgSlug?.trim() ||
137
+ (await resolveConnectorSecret("SENTRY_ORG_SLUG", options.ownerEmail, {
138
+ orgId: options.orgId,
139
+ }));
140
+ if (!value)
141
+ throw new Error("SENTRY_ORG_SLUG is not configured for this workspace");
142
+ return encodeURIComponent(value);
143
+ }
144
+
145
+ return {
146
+ async listIssues(
147
+ query?: string,
148
+ requestedLimit?: number,
149
+ ): Promise<readonly SentryIssue[]> {
150
+ const params = new URLSearchParams({
151
+ limit: String(limit(requestedLimit)),
152
+ sort: "freq",
153
+ });
154
+ if (query?.trim()) params.set("query", query.trim().slice(0, 500));
155
+ const value = await request<unknown>(
156
+ `/organizations/${await organization()}/issues/?${params}`,
157
+ );
158
+ if (!Array.isArray(value))
159
+ throw new Error("Sentry issue response was not an array");
160
+ return value.map(parseIssue);
161
+ },
162
+
163
+ async listEvents(
164
+ issueId: string,
165
+ requestedLimit?: number,
166
+ ): Promise<readonly SentryEvent[]> {
167
+ const id = issueId.trim();
168
+ if (!id) throw new Error("Sentry issue id is required");
169
+ const params = new URLSearchParams({
170
+ limit: String(limit(requestedLimit)),
171
+ });
172
+ const value = await request<unknown>(
173
+ `/organizations/${await organization()}/issues/${encodeURIComponent(id)}/events/?${params}`,
174
+ );
175
+ if (!Array.isArray(value))
176
+ throw new Error("Sentry event response was not an array");
177
+ return value.map(parseEvent);
178
+ },
179
+ };
180
+ }
@@ -1,10 +1,16 @@
1
1
  import { resolveConnectorSecret } from "../connectors/credentials.js";
2
2
  import {
3
3
  getChannelHistory as readChannelHistory,
4
+ addEyesReaction as writeEyesReaction,
5
+ getThread as readThread,
4
6
  getTeamInfo as readTeamInfo,
7
+ postThreadReply as writeThreadReply,
5
8
  type ChannelHistoryResult,
9
+ type SlackPostMessageResult,
10
+ type SlackReactionResult,
6
11
  type SlackTeamInfo,
7
12
  type SlackTokenResolver,
13
+ type ThreadRepliesResult,
8
14
  type Workspace,
9
15
  } from "../connectors/slack.js";
10
16
 
@@ -51,5 +57,42 @@ export function createSlackReader(identity: SlackReaderIdentity) {
51
57
  getTeamInfo(workspace: Workspace): Promise<SlackTeamInfo> {
52
58
  return readTeamInfo(workspace, tokenResolver);
53
59
  },
60
+ getThread(
61
+ workspace: Workspace,
62
+ channelId: string,
63
+ threadTs: string,
64
+ limit?: number,
65
+ cursor?: string,
66
+ ): Promise<ThreadRepliesResult> {
67
+ return readThread(
68
+ workspace,
69
+ channelId,
70
+ threadTs,
71
+ limit,
72
+ cursor,
73
+ tokenResolver,
74
+ );
75
+ },
76
+ addEyesReaction(
77
+ workspace: Workspace,
78
+ channelId: string,
79
+ timestamp: string,
80
+ ): Promise<SlackReactionResult> {
81
+ return writeEyesReaction(workspace, channelId, timestamp, tokenResolver);
82
+ },
83
+ postThreadReply(
84
+ workspace: Workspace,
85
+ channelId: string,
86
+ threadTs: string,
87
+ text: string,
88
+ ): Promise<SlackPostMessageResult> {
89
+ return writeThreadReply(
90
+ workspace,
91
+ channelId,
92
+ threadTs,
93
+ text,
94
+ tokenResolver,
95
+ );
96
+ },
54
97
  };
55
98
  }
@@ -60,7 +60,7 @@ function toEnvelope(
60
60
 
61
61
  return {
62
62
  source: "slack",
63
- externalId: `${channelId}:${message.ts}`,
63
+ externalId: `${channelId}:${threadTs}`,
64
64
  receivedAt: new Date().toISOString(),
65
65
  ...(sourceUrl ? { sourceUrl } : {}),
66
66
  title: messageLabel(message),
@@ -133,6 +133,13 @@ export async function pollSlackChannel({
133
133
  .map((message) => ({ message, ts: numericTs(message.ts) }))
134
134
  .filter(({ ts }) => ts > priorTs)
135
135
  .sort((a, b) => a.ts - b.ts);
136
+ const seenThreads = new Set<string>();
137
+ const uniqueNewMessages = newMessages.filter(({ message }) => {
138
+ const threadTs = message.thread_ts ?? message.ts;
139
+ if (seenThreads.has(threadTs)) return false;
140
+ seenThreads.add(threadTs);
141
+ return true;
142
+ });
136
143
  const maxSeen = messages.reduce(
137
144
  (max, message) => {
138
145
  const ts = numericTs(message.ts);
@@ -142,7 +149,7 @@ export async function pollSlackChannel({
142
149
  );
143
150
 
144
151
  return {
145
- envelopes: newMessages.map(({ message }) =>
152
+ envelopes: uniqueNewMessages.map(({ message }) =>
146
153
  toEnvelope(channelId, message, teamDomain),
147
154
  ),
148
155
  nextLastSlackTs: maxSeen.raw,
@@ -292,6 +292,11 @@ folding framework chrome into the product UI.
292
292
  `content.prototype`, and rely on the top visual tabs to switch between them.
293
293
  When both surfaces are present, open the Wireframes tab by default; the
294
294
  prototype remains available as the interactive follow-up view.
295
+ - **Default to wireframes.** A clean, minimal UI, a high UX bar, or references
296
+ to Linear/Vercel describe the content and density bar; they do not request
297
+ full-fidelity design mode. Use renderer-owned wireframes unless the user
298
+ explicitly asks for branded, pixel-accurate, production-like, or full visual
299
+ design. This keeps every canvas screen inspectable and its full content visible.
295
300
  - **Prototype-first** when the user asks to operate the UI or when interaction is
296
301
  the main question. Use `create-prototype-plan`, which still preserves static
297
302
  mocks where useful.
@@ -354,6 +359,28 @@ For a worked example of the bar — a great UI-first plan and `/visual-plan`, pl
354
359
  the anti-patterns to avoid — READ `references/exemplar.md` in this skill
355
360
  directory before authoring a plan.
356
361
 
362
+ ## Authoring invariants
363
+
364
+ Treat these as data-integrity checks, not optional polish:
365
+
366
+ - `content` is a complete replacement. Pass either `content` or the mode's
367
+ convenience arrays (`screens`/`transitions` or `states`/`components`),
368
+ never both. The create actions reject mixed sources so a second payload cannot
369
+ silently discard CSS, frames, or document blocks.
370
+ - A design screen's scoped `css` is part of the artifact. Keep it on both the
371
+ prototype screen and its matching canvas frame, and use renderer-owned
372
+ `--wf-*` tokens for portable color and typography.
373
+ - Rich-text `data.markdown` must contain actual runtime line breaks. Do not
374
+ hand a plan a one-line Markdown value containing literal `\n` escape text,
375
+ which renders the whole section as one heading. Escaped newlines are fine in
376
+ code examples when the surrounding Markdown still has real line breaks.
377
+ - Canvas artboards do not scroll. Keep wireframe HTML in natural flow and set a
378
+ larger frame `height` when a screen exceeds the surface preset; preserve the
379
+ surface width and inspect the bottom edge at default zoom before handoff.
380
+ - After every hosted write, re-read the structured content and inspect the live
381
+ Plan surface. A valid JSON payload is not proof that CSS loaded or Markdown
382
+ rendered into the intended heading, paragraph, and list structure.
383
+
357
384
  ## Tool Guidance
358
385
 
359
386
  - `create-visual-plan`: start one structured visual plan per agent task/run, or
@@ -7,10 +7,12 @@ canvas layouts from memory or paraphrase these rules per mode.
7
7
 
8
8
  <!-- SHARED-CORE:canvas-surface START -->
9
9
 
10
- **The coordinate rule.** The `surface` locks each artboard's footprint and
11
- aspect — never set artboard width/height and never use coordinates inside the
12
- wireframe HTML; board-level artboard `x`/`y` IS allowed when it creates clear
13
- lanes. Let canvas auto-placement handle simple one-row boards.
10
+ **The coordinate rule.** The `surface` sets each artboard's default footprint
11
+ and width — never set width or use coordinates inside the wireframe HTML.
12
+ Board-level artboard `x`/`y` IS allowed when it creates clear lanes. A
13
+ larger explicit artboard `height` is allowed when the screen's content needs
14
+ more vertical room; canvas frames do not scroll, so reserve enough height to
15
+ show the entire UI. Let canvas auto-placement handle simple one-row boards.
14
16
 
15
17
  **Lay out mixed canvases in lanes.** When a canvas contains broad browser /
16
18
  desktop frames plus compact `mobile`, `popover`, or `panel` surfaces, do not put
@@ -34,6 +36,14 @@ and move any frame whose label, connector, or annotation crosses another frame.
34
36
 
35
37
  When in doubt, use larger values — the canvas auto-zooms to fit everything.
36
38
 
39
+ **Full-content artboards.** Canvas frames are pan/zoom surfaces, not scroll
40
+ containers. Keep wireframe HTML in natural document flow without an inner
41
+ scroll region or fixed child height. If the screen is taller than the default
42
+ surface preset, set the artboard's `height` to the measured content height plus
43
+ breathing room; keep the surface width unchanged. Before handoff, inspect the
44
+ bottom edge of every artboard at default zoom and confirm no control, row, or
45
+ footer is clipped.
46
+
37
47
  **Canvas annotations are designer notes on the artboard.** When a top canvas is
38
48
  present, sprinkle design-review notes near the frames they explain: a short
39
49
  heading, supporting text, and bullets — plain text layers, never bordered or
@@ -192,6 +192,13 @@ create the breathing room. Keep text away from borders: every container, field,
192
192
  button, menu item, and annotation needs enough padding and line-height to read
193
193
  cleanly in the rendered Plan view.
194
194
 
195
+ **Center page-like content inside broad surfaces.** For browser or desktop
196
+ screens, let the outer root provide the full-width frame and padding, then keep
197
+ the page body in a centered wrapper such as
198
+ `width:100%;max-width:720px;margin-inline:auto`. Full-width app bars may span
199
+ the root, but onboarding, auth, settings, and other page-like content should
200
+ not hug the left border unless the real product does.
201
+
195
202
  **For feature-cloud or abundance visuals, optimize the composition over line-by-line
196
203
  reading.** Some marketing/product sections need to feel like a large surface area
197
204
  of capability rather than a precise app workflow. In those cases, use one padded
@@ -192,6 +192,13 @@ create the breathing room. Keep text away from borders: every container, field,
192
192
  button, menu item, and annotation needs enough padding and line-height to read
193
193
  cleanly in the rendered Plan view.
194
194
 
195
+ **Center page-like content inside broad surfaces.** For browser or desktop
196
+ screens, let the outer root provide the full-width frame and padding, then keep
197
+ the page body in a centered wrapper such as
198
+ `width:100%;max-width:720px;margin-inline:auto`. Full-width app bars may span
199
+ the root, but onboarding, auth, settings, and other page-like content should
200
+ not hug the left border unless the real product does.
201
+
195
202
  **For feature-cloud or abundance visuals, optimize the composition over line-by-line
196
203
  reading.** Some marketing/product sections need to feel like a large surface area
197
204
  of capability rather than a precise app workflow. In those cases, use one padded
@@ -38,6 +38,7 @@ import {
38
38
  agentPlanContentSchema,
39
39
  planContentSchema,
40
40
  } from "../shared/plan-content.js";
41
+ import { rejectMixedPlanSources } from "./validate-plan-input.js";
41
42
 
42
43
  const designSurfaceSchema = z.enum([
43
44
  "desktop",
@@ -104,7 +105,7 @@ const designContextRecordSchema = z.record(z.string(), z.unknown()).refine(
104
105
  );
105
106
 
106
107
  const CONTENT_DESCRIPTION =
107
- "Full structured content when the caller has already authored the design/prototype. Prefer screens/transitions for normal /plan-design creation.";
108
+ "Full structured content when the caller has already authored the design/prototype. This is a complete replacement: do not pass screens or transitions alongside it, or screen CSS and other content will be rejected as conflicting input. Prefer screens/transitions for normal /plan-design creation.";
108
109
 
109
110
  // Named (and un-refined) so `agentInputSchema` below can `.extend()` it with
110
111
  // a compact `content` field instead of duplicating every other key. The
@@ -180,11 +181,12 @@ const createPlanDesignSchema = z.object({
180
181
 
181
182
  export default defineAction({
182
183
  description:
183
- "Create a full-fidelity branded design plan with a Design tab (editable design canvas) and optional Prototype tab. For a document-first plan use create-visual-plan; for a wireframe-canvas plan use create-ui-plan; for a recap of an existing diff use create-visual-recap; for a functional prototype use create-prototype-plan. Use design.md, .fig brand kits, and codebase CSS/Tailwind/token evidence when available. Design screens are bounded HTML/CSS fragments with data-design-id targets. Publish via this tool; never deliver the plan as inline chat text.",
184
- schema: createPlanDesignSchema.refine(
185
- (args) => Boolean(args.brief || args.goal),
186
- { message: "Either brief or goal is required." },
187
- ),
184
+ "Create a full-fidelity branded design plan with a Design tab (editable design canvas) and optional Prototype tab. For a document-first plan use create-visual-plan; for a wireframe-canvas plan use create-ui-plan; for a recap of an existing diff use create-visual-recap; for a functional prototype use create-prototype-plan. Use design.md, .fig brand kits, and codebase CSS/Tailwind/token evidence when available. Design screens are bounded HTML/CSS fragments with data-design-id targets. Pass either screens/transitions or one complete content payload, never both. Publish via this tool; never deliver the plan as inline chat text.",
185
+ schema: createPlanDesignSchema
186
+ .superRefine(rejectMixedPlanSources)
187
+ .refine((args) => Boolean(args.brief || args.goal), {
188
+ message: "Either brief or goal is required.",
189
+ }),
188
190
  // ADVERTISED-ONLY: same top-level shape, but `content` swaps the deep
189
191
  // per-block-type union for a compact `type`-enum stand-in. Runtime
190
192
  // validation always runs the full schema above — see the `actions` skill.
@@ -38,6 +38,7 @@ import {
38
38
  agentPlanContentSchema,
39
39
  planContentSchema,
40
40
  } from "../shared/plan-content.js";
41
+ import { rejectMixedPlanSources } from "./validate-plan-input.js";
41
42
 
42
43
  const prototypeSurfaceSchema = z.enum([
43
44
  "desktop",
@@ -108,7 +109,7 @@ const prototypeTransitionSchema = z.object({
108
109
  });
109
110
 
110
111
  const CONTENT_DESCRIPTION =
111
- "Full structured content when the caller has already authored a prototype. Prefer screens/transitions unless replacing the whole document.";
112
+ "Full structured content when the caller has already authored a prototype. This is a complete replacement: do not pass screens or transitions alongside it, or prototype CSS and other content will be rejected as conflicting input. Prefer screens/transitions unless replacing the whole document.";
112
113
 
113
114
  // Named (and un-refined) so `agentInputSchema` below can `.extend()` it with
114
115
  // a compact `content` field instead of duplicating every other key. The
@@ -167,11 +168,12 @@ const createPrototypePlanSchema = z.object({
167
168
 
168
169
  export default defineAction({
169
170
  description:
170
- 'Create a plan whose primary review surface is a running interactive prototype. For a document-first plan use create-visual-plan; for a UI-first wireframe canvas use create-ui-plan; for a recap of an existing diff use create-visual-recap; for full-fidelity branded design use create-plan-design. Prototype screen HTML uses safe Alpine-like directives for local state and data-goto for screen navigation only. If a functional prototype must also be high fidelity, set renderMode to "design" and put scoped styles in css; never embed style tags in html because they are rejected. Publish via this tool; never deliver the plan as inline chat text.',
171
- schema: createPrototypePlanSchema.refine(
172
- (args) => Boolean(args.brief || args.goal),
173
- { message: "Either brief or goal is required." },
174
- ),
171
+ 'Create a plan whose primary review surface is a running interactive prototype. For a document-first plan use create-visual-plan; for a UI-first wireframe canvas use create-ui-plan; for a recap of an existing diff use create-visual-recap; for full-fidelity branded design use create-plan-design. Prototype screen HTML uses safe Alpine-like directives for local state and data-goto for screen navigation only. If a functional prototype must also be high fidelity, set renderMode to "design" and put scoped styles in css; never embed style tags in html because they are rejected. Pass either screens/transitions or one complete content payload, never both. Publish via this tool; never deliver the plan as inline chat text.',
172
+ schema: createPrototypePlanSchema
173
+ .superRefine(rejectMixedPlanSources)
174
+ .refine((args) => Boolean(args.brief || args.goal), {
175
+ message: "Either brief or goal is required.",
176
+ }),
175
177
  // ADVERTISED-ONLY: same top-level shape, but `content` swaps the deep
176
178
  // per-block-type union for a compact `type`-enum stand-in. Runtime
177
179
  // validation always runs the full schema above — see the `actions` skill.
@@ -38,6 +38,7 @@ import {
38
38
  agentPlanContentSchema,
39
39
  planContentSchema,
40
40
  } from "../shared/plan-content.js";
41
+ import { rejectMixedPlanSources } from "./validate-plan-input.js";
41
42
 
42
43
  const uiPlanStateSchema = z.object({
43
44
  name: z.string().min(1).describe("State or screen name"),
@@ -56,7 +57,7 @@ const uiPlanComponentSchema = z.object({
56
57
  });
57
58
 
58
59
  const CONTENT_DESCRIPTION =
59
- 'Structured editable UI plan content. Prefer this for app-owned top canvas wireframes (HTML mockups: set the wireframe\'s data.html to a semantic HTML fragment of the screen and pick a surface — the renderer owns the theme, footprint/aspect, hand-drawn font, and sketch overlay; use --wf-* CSS tokens for any custom color, never hex). Call get-plan-blocks first for visual frame guidance before choosing frame: "show" or frame: "hide". Do not use legacy kit-tree screen arrays or nested FrameScreen/Card/Row/Btn-style children for new canvas artboards. Use sketch diagrams, rich text, code blocks (grouped in a vertical tabs block for a file map), annotated code for key files, validation checklists, and bounded custom HTML fragments. Diagram data.html/data.css should use renderer-owned .diagram-* primitives plus --wf-* tokens, not custom fonts or hard-coded hex/rgb/hsl colors, so light/dark and sketchy Excalifont/rough.js modes remain correct. The canvas should carry Claude-style flex/grid wireframe artboards and designer annotations; the document should add implementation substance instead of duplicating the same wireframes. The renderer owns all visual styling; emit lean content, not pixels.';
60
+ 'Structured editable UI plan content. This is a complete replacement: do not pass states or components alongside it, or canvas content will be rejected as conflicting input. Prefer this for app-owned top canvas wireframes (HTML mockups: set the wireframe\'s data.html to a semantic HTML fragment of the screen and pick a surface — the renderer owns the theme, footprint/aspect, hand-drawn font, and sketch overlay; use --wf-* CSS tokens for any custom color, never hex). Call get-plan-blocks first for visual frame guidance before choosing frame: "show" or frame: "hide". Do not use legacy kit-tree screen arrays or nested FrameScreen/Card/Row/Btn-style children for new canvas artboards. Use sketch diagrams, rich text, code blocks (grouped in a vertical tabs block for a file map), annotated code for key files, validation checklists, and bounded custom HTML fragments. Diagram data.html/data.css should use renderer-owned .diagram-* primitives plus --wf-* tokens, not custom fonts or hard-coded hex/rgb/hsl colors, so light/dark and sketchy Excalifont/rough.js modes remain correct. The canvas should carry Claude-style flex/grid wireframe artboards and designer annotations; the document should add implementation substance instead of duplicating the same wireframes. The renderer owns all visual styling; emit lean content, not pixels.';
60
61
 
61
62
  // Named (and un-refined) so `agentInputSchema` below can `.extend()` it with
62
63
  // a compact `content` field instead of duplicating every other key. The
@@ -112,13 +113,12 @@ const createUiPlanSchema = z.object({
112
113
 
113
114
  export default defineAction({
114
115
  description:
115
- "Create a UI-first plan whose centerpiece is wireframed screens/states on a canvas. For a document-first plan use create-visual-plan; for a recap of an existing diff use create-visual-recap; for a running interactive prototype use create-prototype-plan; for a full-fidelity branded design use create-plan-design. Publish via this tool; never deliver the plan as inline chat text.",
116
- schema: createUiPlanSchema.refine(
117
- (args) => Boolean(args.brief || args.goal),
118
- {
116
+ "Create a UI-first plan whose centerpiece is wireframed screens/states on a canvas. For a document-first plan use create-visual-plan; for a recap of an existing diff use create-visual-recap; for a running interactive prototype use create-prototype-plan; for a full-fidelity branded design use create-plan-design. Pass either states/components or one complete content payload, never both. Publish via this tool; never deliver the plan as inline chat text.",
117
+ schema: createUiPlanSchema
118
+ .superRefine(rejectMixedPlanSources)
119
+ .refine((args) => Boolean(args.brief || args.goal), {
119
120
  message: "Either brief or goal is required.",
120
- },
121
- ),
121
+ }),
122
122
  // ADVERTISED-ONLY: same top-level shape, but `content` swaps the deep
123
123
  // per-block-type union for a compact `type`-enum stand-in. Runtime
124
124
  // validation always runs the full schema above — see the `actions` skill.
@@ -33,6 +33,8 @@ const AUTHORING_RULES_NOTE = `
33
33
 
34
34
  **Wireframes**: set \`data.html\` to a semantic HTML fragment; pick a surface (desktop/mobile/popover/panel/browser). The renderer owns theme, footprint/aspect, Excalifont, and rough.js sketch overlay. Use \`--wf-*\` CSS tokens for any custom color (never hex). Prototype screens use semantic HTML with \`data-goto\` attributes for navigation.
35
35
 
36
+ **Page alignment**: for browser or desktop page-like screens, give the outer root explicit padding and keep the body in a centered wrapper such as \`width:100%;max-width:720px;margin-inline:auto\`. Full-width app bars may span the root, but onboarding, auth, settings, and other page content should not hug the artboard edge unless the real product does.
37
+
36
38
  **Visual frames**: \`wireframe\` and \`diagram\` data accept \`frame: "auto" | "show" | "hide"\`. Leave it unset/\`auto\` when the host context should decide: Plan and recap surfaces default to framed; docs default to unframed. Use \`frame: "show"\` for standalone product screens, before/after recap comparisons, screenshot-like artifacts, and visuals that need containment from surrounding prose. Use \`frame: "hide"\` when a docs page, tab, column, card, canvas artboard, or the visual's own internal chrome already supplies the boundary. Hiding the outer frame must not remove inner padding, meaningful card/field/button borders, or the visual's readable structure.
37
39
 
38
40
  **Canvas storyboards**: if the user asks for a canvas, storyboard, wireframe, light storyboard, UI flow, screen flow, product flow, mockup, or visual comparison, the primary artifact must be \`content.canvas\` / \`canvas.mdx\` with \`DesignBoard\` artboards containing \`Screen\` HTML wireframes. Each canvas \`Screen\` must carry \`html\` / \`data.html\`; never author fresh nested kit-tree children such as \`<FrameScreen>\`, \`<Card>\`, \`<Row>\`, or \`<Btn>\` inside canvas \`<Screen>\` tags. Kit trees are old-plan compatibility only and often render worse on the pan/zoom canvas than HTML wireframes. Do not use document-body \`diagram\` blocks for the primary UI story. Use \`diagram\` only for architecture, data flow, or implementation mechanics below the canvas, and only after the UI storyboard exists.
@@ -49,7 +51,11 @@ const AUTHORING_RULES_NOTE = `
49
51
 
50
52
  **API endpoints**: keep \`api-endpoint\` and \`openapi-spec\` blocks in normal single-column document flow. Use \`columns\` only for an explicit before/after contract comparison.
51
53
 
52
- **Visual fidelity and renderMode**: leave \`renderMode\` unset or set it to \`wireframe\` for normal wireframes. “Higher fidelity,” “pixel-accurate,” “polished mockup,” “production-like,” “real design,” or “not a sketch/wireframe” requires \`renderMode: "design"\`, substantial branded HTML/CSS grounded in the real app, and stable \`data-design-id\` targets. Put scoped styles in the wireframe/prototype \`css\` field — never in a \`<style>\` tag. On an existing plan, update the same plan id with \`set-visual-render-mode\` plus the upgraded HTML/CSS; do not create a duplicate. The viewer-local Clean toggle is not a fidelity upgrade.\``;
54
+ **Visual fidelity and renderMode**: leave \`renderMode\` unset or set it to \`wireframe\` for normal wireframes. “Clean,” “minimal,” “high UX bar,” or Linear/Vercel references set the content and density bar, not full-fidelity mode. Only explicit “higher fidelity,” “pixel-accurate,” “polished mockup,” “production-like,” “real design,” or “not a sketch/wireframe” requires \`renderMode: "design"\`, substantial branded HTML/CSS grounded in the real app, and stable \`data-design-id\` targets. Put scoped styles in the wireframe/prototype \`css\` field — never in a \`<style>\` tag. On an existing plan, update the same plan id with \`set-visual-render-mode\` plus the upgraded HTML/CSS; do not create a duplicate. The viewer-local Clean toggle is not a fidelity upgrade.
55
+
56
+ **Canvas fit**: canvas artboards are pan/zoom surfaces and do not scroll. Keep wireframe HTML in natural flow without an inner scroll region. If a screen is taller than the surface preset, set the artboard's explicit \`height\` larger than the default while keeping its surface width unchanged, then inspect the bottom edge at default zoom so no row, control, or footer is clipped.
57
+
58
+ **Payload integrity**: \`content\` is a complete replacement. Pass either \`content\` or the mode-specific convenience arrays (\`screens\`/\`transitions\` or \`states\`/\`components\`), never both; mixed inputs are rejected so CSS and blocks cannot disappear silently. For design content, keep the same HTML/CSS in the prototype screens and canvas frames. Rich-text \`data.markdown\` must contain real runtime line breaks; a one-line value containing literal \`\\n\` escape text will render as one giant heading and is rejected. After writing, re-read the structured plan and inspect the actual Plan surface before handoff.\``;
53
59
 
54
60
  /**
55
61
  * Expose the live plan block vocabulary to the agent. The list is generated from
@@ -0,0 +1,39 @@
1
+ import type { RefinementCtx } from "zod";
2
+
3
+ type PlanInputSources = {
4
+ content?: unknown;
5
+ screens?: readonly unknown[];
6
+ transitions?: readonly unknown[];
7
+ states?: readonly unknown[];
8
+ components?: readonly unknown[];
9
+ };
10
+
11
+ /**
12
+ * A full content payload is a replacement, not a second source to merge with
13
+ * the convenience arrays. Rejecting mixed inputs keeps screen CSS and blocks
14
+ * from being silently discarded at the action boundary.
15
+ */
16
+ export function rejectMixedPlanSources(
17
+ value: PlanInputSources,
18
+ ctx: RefinementCtx,
19
+ ): void {
20
+ if (!value.content) return;
21
+
22
+ const conflictingFields = [
23
+ ["screens", value.screens],
24
+ ["transitions", value.transitions],
25
+ ["states", value.states],
26
+ ["components", value.components],
27
+ ] as const;
28
+
29
+ for (const [field, entries] of conflictingFields) {
30
+ if (!entries || entries.length === 0) continue;
31
+ ctx.addIssue({
32
+ code: "custom",
33
+ path: [field],
34
+ message:
35
+ `content is a complete visual-plan replacement; do not pass ${field} ` +
36
+ "alongside it. Remove content or provide the full content payload.",
37
+ });
38
+ }
39
+ }