@agent-native/core 0.105.0 → 0.106.1

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 (194) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +39 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/application-state/index.ts +2 -0
  5. package/corpus/core/src/application-state/script-helpers.ts +12 -0
  6. package/corpus/core/src/application-state/store.ts +38 -0
  7. package/corpus/core/src/client/AssistantChat.tsx +4 -0
  8. package/corpus/core/src/client/MultiTabAssistantChat.tsx +5 -1
  9. package/corpus/core/src/client/composer/PromptComposer.tsx +8 -0
  10. package/corpus/core/src/client/composer/TiptapComposer.tsx +38 -1
  11. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +107 -32
  12. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +33 -0
  13. package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
  14. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +10 -3
  15. package/corpus/core/src/client/use-chat-models.ts +10 -2
  16. package/corpus/core/src/connections/catalog.ts +107 -6
  17. package/corpus/core/src/localization/default-messages.ts +2 -0
  18. package/corpus/core/src/mcp/actions/call-mcp-tool.ts +18 -0
  19. package/corpus/core/src/mcp/actions/list-mcp-tools.ts +18 -0
  20. package/corpus/core/src/mcp-client/app-api.ts +136 -0
  21. package/corpus/core/src/mcp-client/index.ts +21 -0
  22. package/corpus/core/src/mcp-client/oauth-client.ts +462 -0
  23. package/corpus/core/src/mcp-client/oauth-routes.ts +307 -0
  24. package/corpus/core/src/mcp-client/remote-store.ts +93 -118
  25. package/corpus/core/src/mcp-client/remote-url.ts +97 -0
  26. package/corpus/core/src/mcp-client/routes.ts +11 -0
  27. package/corpus/core/src/oauth-tokens/store.ts +10 -4
  28. package/corpus/core/src/provider-api/index.ts +690 -55
  29. package/corpus/core/src/secrets/register-framework-secrets.ts +30 -0
  30. package/corpus/core/src/server/action-discovery.ts +2 -0
  31. package/corpus/core/src/server/core-routes-plugin.ts +9 -1
  32. package/corpus/core/src/server/workspace-provider-oauth.ts +448 -61
  33. package/corpus/core/src/templates/default/AGENTS.md +9 -7
  34. package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
  35. package/corpus/templates/analytics/app/lib/data-source-status.ts +2 -0
  36. package/corpus/templates/analytics/app/pages/DataSources.tsx +61 -0
  37. package/corpus/templates/analytics/server/handlers/jira.ts +7 -0
  38. package/corpus/templates/analytics/server/lib/jira.ts +14 -42
  39. package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +11 -0
  40. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +22 -0
  41. package/corpus/templates/design/AGENTS.md +16 -0
  42. package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +54 -0
  43. package/corpus/templates/design/actions/propose-node-rewrite.ts +290 -0
  44. package/corpus/templates/design/actions/resolve-node-rewrite.ts +257 -0
  45. package/corpus/templates/design/actions/view-screen.ts +98 -2
  46. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +71 -1
  47. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +19 -0
  48. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +33 -0
  49. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +194 -0
  50. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +21 -4
  51. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +18 -0
  52. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
  53. package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +638 -0
  54. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +535 -54
  55. package/corpus/templates/design/app/components/visual-editor/index.ts +5 -1
  56. package/corpus/templates/design/app/components/visual-editor/review-canvas-state.ts +21 -3
  57. package/corpus/templates/design/app/i18n/ar-SA.ts +35 -0
  58. package/corpus/templates/design/app/i18n/de-DE.ts +35 -0
  59. package/corpus/templates/design/app/i18n/es-ES.ts +35 -0
  60. package/corpus/templates/design/app/i18n/fr-FR.ts +35 -0
  61. package/corpus/templates/design/app/i18n/hi-IN.ts +35 -0
  62. package/corpus/templates/design/app/i18n/ja-JP.ts +35 -0
  63. package/corpus/templates/design/app/i18n/ko-KR.ts +35 -0
  64. package/corpus/templates/design/app/i18n/pt-BR.ts +35 -0
  65. package/corpus/templates/design/app/i18n/zh-CN.ts +35 -0
  66. package/corpus/templates/design/app/i18n/zh-TW.ts +32 -0
  67. package/corpus/templates/design/app/i18n-data.ts +32 -0
  68. package/corpus/templates/design/app/lib/node-reprompt.ts +110 -0
  69. package/corpus/templates/design/changelog/2026-07-16-design-editing-and-review-are-now-more-predictable.md +6 -0
  70. package/corpus/templates/design/server/lib/reprompt-action-guard.ts +107 -0
  71. package/corpus/templates/design/server/plugins/agent-chat.ts +9 -1
  72. package/corpus/templates/design/shared/code-layer.ts +15 -0
  73. package/corpus/templates/design/shared/node-rewrite.ts +213 -0
  74. package/corpus/templates/design/shared/review-anchor.ts +26 -3
  75. package/corpus/templates/dispatch/app/routes/integrations.tsx +57 -4
  76. package/dist/application-state/index.d.ts +2 -2
  77. package/dist/application-state/index.d.ts.map +1 -1
  78. package/dist/application-state/index.js +2 -2
  79. package/dist/application-state/index.js.map +1 -1
  80. package/dist/application-state/script-helpers.d.ts +1 -0
  81. package/dist/application-state/script-helpers.d.ts.map +1 -1
  82. package/dist/application-state/script-helpers.js +7 -1
  83. package/dist/application-state/script-helpers.js.map +1 -1
  84. package/dist/application-state/store.d.ts +1 -0
  85. package/dist/application-state/store.d.ts.map +1 -1
  86. package/dist/application-state/store.js +28 -0
  87. package/dist/application-state/store.js.map +1 -1
  88. package/dist/client/AssistantChat.d.ts +2 -0
  89. package/dist/client/AssistantChat.d.ts.map +1 -1
  90. package/dist/client/AssistantChat.js +2 -2
  91. package/dist/client/AssistantChat.js.map +1 -1
  92. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  93. package/dist/client/MultiTabAssistantChat.js +5 -2
  94. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  95. package/dist/client/composer/PromptComposer.d.ts +2 -0
  96. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  97. package/dist/client/composer/PromptComposer.js +5 -2
  98. package/dist/client/composer/PromptComposer.js.map +1 -1
  99. package/dist/client/composer/TiptapComposer.d.ts +4 -1
  100. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  101. package/dist/client/composer/TiptapComposer.js +13 -5
  102. package/dist/client/composer/TiptapComposer.js.map +1 -1
  103. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  104. package/dist/client/resources/McpIntegrationDialog.js +58 -14
  105. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  106. package/dist/client/resources/mcp-integration-catalog.d.ts +9 -0
  107. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  108. package/dist/client/resources/mcp-integration-catalog.js +13 -0
  109. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  110. package/dist/client/resources/use-mcp-servers.d.ts +1 -0
  111. package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
  112. package/dist/client/resources/use-mcp-servers.js.map +1 -1
  113. package/dist/client/review/ReviewCommentComposer.d.ts +4 -1
  114. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  115. package/dist/client/review/ReviewCommentComposer.js +3 -3
  116. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  117. package/dist/client/use-chat-models.d.ts +1 -0
  118. package/dist/client/use-chat-models.d.ts.map +1 -1
  119. package/dist/client/use-chat-models.js +8 -2
  120. package/dist/client/use-chat-models.js.map +1 -1
  121. package/dist/collab/struct-routes.d.ts +1 -1
  122. package/dist/connections/catalog.d.ts +78 -7
  123. package/dist/connections/catalog.d.ts.map +1 -1
  124. package/dist/connections/catalog.js +97 -6
  125. package/dist/connections/catalog.js.map +1 -1
  126. package/dist/localization/default-messages.d.ts +2 -0
  127. package/dist/localization/default-messages.d.ts.map +1 -1
  128. package/dist/localization/default-messages.js +2 -0
  129. package/dist/localization/default-messages.js.map +1 -1
  130. package/dist/mcp/actions/call-mcp-tool.d.ts +7 -0
  131. package/dist/mcp/actions/call-mcp-tool.d.ts.map +1 -0
  132. package/dist/mcp/actions/call-mcp-tool.js +15 -0
  133. package/dist/mcp/actions/call-mcp-tool.js.map +1 -0
  134. package/dist/mcp/actions/list-mcp-tools.d.ts +5 -0
  135. package/dist/mcp/actions/list-mcp-tools.d.ts.map +1 -0
  136. package/dist/mcp/actions/list-mcp-tools.js +16 -0
  137. package/dist/mcp/actions/list-mcp-tools.js.map +1 -0
  138. package/dist/mcp-client/app-api.d.ts +34 -0
  139. package/dist/mcp-client/app-api.d.ts.map +1 -0
  140. package/dist/mcp-client/app-api.js +94 -0
  141. package/dist/mcp-client/app-api.js.map +1 -0
  142. package/dist/mcp-client/index.d.ts +3 -1
  143. package/dist/mcp-client/index.d.ts.map +1 -1
  144. package/dist/mcp-client/index.js +3 -1
  145. package/dist/mcp-client/index.js.map +1 -1
  146. package/dist/mcp-client/oauth-client.d.ts +113 -0
  147. package/dist/mcp-client/oauth-client.d.ts.map +1 -0
  148. package/dist/mcp-client/oauth-client.js +316 -0
  149. package/dist/mcp-client/oauth-client.js.map +1 -0
  150. package/dist/mcp-client/oauth-routes.d.ts +25 -0
  151. package/dist/mcp-client/oauth-routes.d.ts.map +1 -0
  152. package/dist/mcp-client/oauth-routes.js +218 -0
  153. package/dist/mcp-client/oauth-routes.js.map +1 -0
  154. package/dist/mcp-client/remote-store.d.ts +21 -6
  155. package/dist/mcp-client/remote-store.d.ts.map +1 -1
  156. package/dist/mcp-client/remote-store.js +72 -124
  157. package/dist/mcp-client/remote-store.js.map +1 -1
  158. package/dist/mcp-client/remote-url.d.ts +8 -0
  159. package/dist/mcp-client/remote-url.d.ts.map +1 -0
  160. package/dist/mcp-client/remote-url.js +103 -0
  161. package/dist/mcp-client/remote-url.js.map +1 -0
  162. package/dist/mcp-client/routes.d.ts +1 -0
  163. package/dist/mcp-client/routes.d.ts.map +1 -1
  164. package/dist/mcp-client/routes.js +9 -0
  165. package/dist/mcp-client/routes.js.map +1 -1
  166. package/dist/notifications/routes.d.ts +2 -2
  167. package/dist/oauth-tokens/store.d.ts +2 -2
  168. package/dist/oauth-tokens/store.d.ts.map +1 -1
  169. package/dist/oauth-tokens/store.js +10 -6
  170. package/dist/oauth-tokens/store.js.map +1 -1
  171. package/dist/observability/routes.d.ts +5 -5
  172. package/dist/provider-api/index.d.ts +14 -0
  173. package/dist/provider-api/index.d.ts.map +1 -1
  174. package/dist/provider-api/index.js +499 -61
  175. package/dist/provider-api/index.js.map +1 -1
  176. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  177. package/dist/secrets/register-framework-secrets.js +28 -0
  178. package/dist/secrets/register-framework-secrets.js.map +1 -1
  179. package/dist/secrets/routes.d.ts +9 -9
  180. package/dist/server/action-discovery.d.ts.map +1 -1
  181. package/dist/server/action-discovery.js +2 -0
  182. package/dist/server/action-discovery.js.map +1 -1
  183. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  184. package/dist/server/core-routes-plugin.js +9 -1
  185. package/dist/server/core-routes-plugin.js.map +1 -1
  186. package/dist/server/workspace-provider-oauth.d.ts +10 -3
  187. package/dist/server/workspace-provider-oauth.d.ts.map +1 -1
  188. package/dist/server/workspace-provider-oauth.js +325 -43
  189. package/dist/server/workspace-provider-oauth.js.map +1 -1
  190. package/dist/templates/default/AGENTS.md +9 -7
  191. package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
  192. package/package.json +2 -2
  193. package/src/templates/default/AGENTS.md +9 -7
  194. package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
@@ -68,6 +68,52 @@ the real app focused on exactly what was produced. It reuses the existing
68
68
  `navigate` / `application_state` contract the UI already drains every 2s (see
69
69
  **context-awareness**) — we never invent a second navigation mechanism.
70
70
 
71
+ ## App-facing MCP connectors
72
+
73
+ Apps can also consume connected remote MCP servers as a server-side API. The
74
+ framework stores each connector's OAuth client registration, discovery metadata,
75
+ access token, and refresh token in the encrypted `oauth_tokens` store, scoped to
76
+ the user or organization. The browser and sandboxed MCP App iframes never
77
+ receive those credentials.
78
+
79
+ Use the hidden core actions `list-mcp-tools` and `call-mcp-tool` when an app UI
80
+ needs a request-scoped API surface. Server-side app actions can import
81
+ `listVisibleMcpTools` and `callMcpTool` from `@agent-native/core/mcp-client`.
82
+ Both paths require an authenticated request context and enforce user/org server
83
+ visibility plus MCP App visibility metadata. Pass the configured server id and
84
+ the server's original tool name; the framework resolves the internal prefixed
85
+ tool name and injects the connector's bearer token.
86
+
87
+ ```ts
88
+ import { callMcpTool } from "@agent-native/core/mcp-client";
89
+
90
+ const result = await callMcpTool("org_acme_linear", "list_issues", {
91
+ project: "<PROJECT_ID>",
92
+ });
93
+ ```
94
+
95
+ Use this for bounded product workflows (for example, syncing issues or
96
+ refreshing analytics). Do not expose a raw token, arbitrary server URL, or
97
+ unbounded proxy from an app action. Keep provider-specific schemas and access
98
+ checks in the app action, and use the connector catalog/OAuth button for setup.
99
+
100
+ ## Provider API connectors
101
+
102
+ The shared workspace connection catalog is also the setup surface for provider
103
+ APIs. When a provider advertises OAuth, show that connection as the primary
104
+ setup action and retain its API key or token as an explicit fallback. Current
105
+ shared OAuth providers include Figma, GitHub, Google Drive, HubSpot, Jira Cloud, Notion, and Sentry. Slack
106
+ uses its managed messaging-install OAuth flow. Gmail,
107
+ Calendar, and Slides may use template-owned Google OAuth flows with broader
108
+ scopes; do not replace those flows with the narrower Drive connection.
109
+
110
+ Provider API actions resolve credentials server-side. Pass a granted
111
+ `connectionId` when a request must use a particular OAuth account; never copy
112
+ the access token into a browser, extension, MCP App, prompt, or stored action
113
+ payload. Key-only providers should remain in the normal scoped secrets flow
114
+ until their upstream OAuth app, scopes, refresh behavior, and identity mapping
115
+ have been implemented and configured.
116
+
71
117
  ## How
72
118
 
73
119
  ### 1. Connect to hosted apps
@@ -68,7 +68,9 @@ export interface SharedConnectionStatus {
68
68
  const dataSourceWorkspaceProviderIds: Record<string, string> = {
69
69
  github: "github",
70
70
  hubspot: "hubspot",
71
+ jira: "jira",
71
72
  notion: "notion",
73
+ sentry: "sentry",
72
74
  slack: "slack",
73
75
  };
74
76
 
@@ -26,6 +26,7 @@ import {
26
26
  IconCopy,
27
27
  IconDotsVertical,
28
28
  IconBrandGithub,
29
+ IconPlugConnected,
29
30
  } from "@tabler/icons-react";
30
31
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
31
32
  import { useEffect, useState } from "react";
@@ -427,6 +428,55 @@ function GitHubOAuthView({
427
428
  );
428
429
  }
429
430
 
431
+ function WorkspaceOAuthView({
432
+ provider,
433
+ label,
434
+ connected,
435
+ }: {
436
+ provider: string;
437
+ label: string;
438
+ connected: boolean;
439
+ }) {
440
+ const t = useT();
441
+ const connect = () => {
442
+ const params = new URLSearchParams({
443
+ appId: "analytics",
444
+ return: "/data-sources",
445
+ });
446
+ window.location.assign(
447
+ agentNativePath(
448
+ `/_agent-native/connections/oauth/${provider}/start?${params.toString()}`,
449
+ ),
450
+ );
451
+ };
452
+
453
+ return (
454
+ <div className="space-y-3 rounded-md border border-border/50 bg-muted/20 p-3">
455
+ <div className="flex items-start justify-between gap-3">
456
+ <div className="flex items-start gap-3">
457
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground">
458
+ <IconPlugConnected className="h-4 w-4" />
459
+ </div>
460
+ <div className="min-w-0 space-y-1">
461
+ <p className="text-xs font-medium text-foreground">{label}</p>
462
+ <p className="text-xs text-muted-foreground">
463
+ {t("dataSources.sharedIntegration")}
464
+ </p>
465
+ </div>
466
+ </div>
467
+ <Button
468
+ size="sm"
469
+ variant={connected ? "outline" : "default"}
470
+ onClick={connect}
471
+ className="shrink-0 text-xs"
472
+ >
473
+ {connected ? t("dataSources.reconnect") : t("dataSources.connect")}
474
+ </Button>
475
+ </div>
476
+ </div>
477
+ );
478
+ }
479
+
430
480
  function SharedConnectionBadge({ status }: { status: SharedConnectionStatus }) {
431
481
  const tone =
432
482
  status.kind === "ready"
@@ -1101,6 +1151,17 @@ function DataSourceCard({
1101
1151
  />
1102
1152
  </div>
1103
1153
  )}
1154
+ {(["notion", "hubspot", "jira", "sentry"] as const).includes(
1155
+ source.id as "notion" | "hubspot" | "jira" | "sentry",
1156
+ ) && (
1157
+ <div className="mb-4">
1158
+ <WorkspaceOAuthView
1159
+ provider={source.id}
1160
+ label={source.name}
1161
+ connected={readyViaWorkspace}
1162
+ />
1163
+ </div>
1164
+ )}
1104
1165
  {sharedConnectionStatus && (
1105
1166
  <SharedConnectionStatusRow status={sharedConnectionStatus} />
1106
1167
  )}
@@ -1,3 +1,4 @@
1
+ import { resolveWorkspaceConnectionForApp } from "@agent-native/core/workspace-connections";
1
2
  import { defineEventHandler, getQuery, setResponseStatus } from "h3";
2
3
 
3
4
  import {
@@ -17,6 +18,12 @@ import {
17
18
  async function requireJiraCredentials(
18
19
  event: Parameters<typeof requireCredential>[0],
19
20
  ) {
21
+ const workspaceConnection = await resolveWorkspaceConnectionForApp({
22
+ appId: "analytics",
23
+ provider: "jira",
24
+ requireConnected: true,
25
+ });
26
+ if (workspaceConnection.available) return null;
20
27
  return (
21
28
  (await requireCredential(event, "JIRA_BASE_URL", "Jira")) ||
22
29
  (await requireCredential(event, "JIRA_USER_EMAIL", "Jira")) ||
@@ -1,11 +1,7 @@
1
1
  // Jira Cloud REST API client
2
- // Uses Basic auth (email + API token)
3
2
 
4
- import { resolveCredential } from "./credentials";
5
- import {
6
- requireRequestCredentialContext,
7
- scopedCredentialCacheKey,
8
- } from "./credentials-context";
3
+ import { scopedCredentialCacheKey } from "./credentials-context";
4
+ import { executeProviderApiRequest } from "./provider-api";
9
5
 
10
6
  const API_V3 = "/rest/api/3";
11
7
  const API_AGILE = "/rest/agile/1.0";
@@ -15,29 +11,6 @@ const cache = new Map<string, { data: unknown; ts: number }>();
15
11
  const CACHE_TTL_MS = 10 * 60 * 1000;
16
12
  const MAX_CACHE = 100;
17
13
 
18
- async function getAuth(): Promise<{
19
- baseUrl: string;
20
- headers: Record<string, string>;
21
- }> {
22
- const ctx = requireRequestCredentialContext("JIRA_BASE_URL");
23
- const baseUrl = await resolveCredential("JIRA_BASE_URL", ctx);
24
- const email = await resolveCredential("JIRA_USER_EMAIL", ctx);
25
- const token = await resolveCredential("JIRA_API_TOKEN", ctx);
26
- if (!baseUrl || !email || !token) {
27
- throw new Error(
28
- "JIRA_BASE_URL, JIRA_USER_EMAIL, and JIRA_API_TOKEN are not configured",
29
- );
30
- }
31
- const encoded = Buffer.from(`${email}:${token}`).toString("base64");
32
- return {
33
- baseUrl: baseUrl.replace(/\/$/, ""),
34
- headers: {
35
- Authorization: `Basic ${encoded}`,
36
- Accept: "application/json",
37
- },
38
- };
39
- }
40
-
41
14
  async function jiraGet<T>(
42
15
  path: string,
43
16
  params?: Record<string, string>,
@@ -53,21 +26,20 @@ async function jiraGet<T>(
53
26
  return cached.data as T;
54
27
  }
55
28
 
56
- const { baseUrl, headers } = await getAuth();
57
- const url = new URL(path, baseUrl);
58
- if (params) {
59
- for (const [k, v] of Object.entries(params)) {
60
- url.searchParams.set(k, v);
61
- }
62
- }
63
-
64
- const res = await fetch(url.toString(), { headers });
65
- if (!res.ok) {
66
- const text = await res.text();
67
- throw new Error(`Jira API error ${res.status}: ${text}`);
29
+ const result = (await executeProviderApiRequest({
30
+ provider: "jira",
31
+ path,
32
+ query: params,
33
+ })) as {
34
+ response: { ok: boolean; status: number; json?: unknown; text?: string };
35
+ };
36
+ if (!result.response.ok) {
37
+ throw new Error(
38
+ `Jira API error ${result.response.status}: ${String(result.response.text ?? "")}`,
39
+ );
68
40
  }
69
41
 
70
- const data = await res.json();
42
+ const data = result.response.json;
71
43
  if (cache.size >= MAX_CACHE) {
72
44
  const oldest = cache.keys().next().value;
73
45
  if (oldest) cache.delete(oldest);
@@ -1,4 +1,5 @@
1
1
  import { readBody } from "@agent-native/core/server";
2
+ import { resolveWorkspaceConnectionForApp } from "@agent-native/core/workspace-connections";
2
3
  import { defineEventHandler } from "h3";
3
4
 
4
5
  import {
@@ -141,6 +142,16 @@ export default defineEventHandler(async (event) => {
141
142
  }
142
143
 
143
144
  case "jira": {
145
+ const workspaceConnection = await resolveWorkspaceConnectionForApp({
146
+ appId: "analytics",
147
+ provider: "jira",
148
+ requireConnected: true,
149
+ });
150
+ if (workspaceConnection.available) {
151
+ const { getProjects } = await import("../../lib/jira");
152
+ await getProjects();
153
+ return { ok: true };
154
+ }
144
155
  const baseUrl = await resolveCredential("JIRA_BASE_URL", ctx);
145
156
  const email = await resolveCredential("JIRA_USER_EMAIL", ctx);
146
157
  const token = await resolveCredential("JIRA_API_TOKEN", ctx);
@@ -47,6 +47,28 @@ iframe-backed screens on the infinite canvas.
47
47
  and create one screen per URL/path. Shorthand like
48
48
  `localhost:1234/onboarding/1` means `http://localhost:1234/onboarding/1`.
49
49
 
50
+ ## Select And Reprompt
51
+
52
+ When a chat message begins with `[Reprompt selection]`, the selected subtree is
53
+ a hard write boundary. The only mutation path is `propose-node-rewrite` with
54
+ the exact `repromptId`, target, and `baseVersionHash` captured in
55
+ `design-reprompt-pending:<designId>:<fileId>`. Never use `apply-visual-edit`,
56
+ `apply-source-edit`, `write-source`, `write-local-file`, `edit-design`, or any
57
+ other content-writing action for that request. Clarifying questions are allowed,
58
+ but a requested change must remain a proposal.
59
+
60
+ Produce one variant by default. Produce two or three only when the instruction
61
+ asks for options. A retry includes `priorProposalId`; keep the same target and
62
+ base version, incorporate the feedback, and call `propose-node-rewrite` again.
63
+ The UI previews the returned subtree without persisting it.
64
+
65
+ Use `resolve-node-rewrite` for the accept/reject lifecycle. Accept applies the
66
+ chosen variant as one version-checked inline/Yjs content transaction so one
67
+ undo restores the prior structure; reject clears the proposal without changing
68
+ content. For conversational resolution such as “apply the second one,” call
69
+ `view-screen`, read the active `design.reprompt.proposal`, and pass its
70
+ `proposalId` plus the zero-based `variantIndex` to `resolve-node-rewrite`.
71
+
50
72
  ## Review Quality
51
73
 
52
74
  - Treat the running app as the truth. Preserve its component language, tokens,
@@ -16,6 +16,13 @@ patterns live in `.agents/skills/`.
16
16
  - 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.
17
17
  - Use the app actions for designs, files, versions, design systems, variants,
18
18
  export, and sharing. Do not write design rows directly with SQL.
19
+ - A message beginning with `[Reprompt selection]` is preview-only. Call
20
+ `propose-node-rewrite` with its exact `repromptId`, target, and base hash;
21
+ never call `edit-design`, `update-design`, `update-file`, `generate-design`,
22
+ `apply-visual-edit`, or another content writer. Only the frontend-only
23
+ `resolve-node-rewrite` action may persist an explicitly accepted proposal.
24
+ - A message beginning with `[Selection question]` is read-only. Answer about
25
+ the captured element and subtree without calling content-writing actions.
19
26
  - When a user wants an established public system as a starting point, call
20
27
  `create-design-system` with `templateId: material-3`, `carbon-white`, or
21
28
  `primer-light`. These are source-linked, versioned token snapshots with
@@ -268,6 +275,15 @@ patterns live in `.agents/skills/`.
268
275
  generation planning state created by `generate-screens` (canvas region
269
276
  assignments and per-frame instructions consumed by `generate-design` and
270
277
  `view-screen`; not rendered as canvas overlays).
278
+ - `design-reprompt-pending:<designId>:<fileId>` is the client-captured source
279
+ selection, instruction, base hash, and authoritative current request id for
280
+ a scoped regenerate request.
281
+ - `design-reprompt-proposal:<designId>:<fileId>:<repromptId>` is one
282
+ request-specific preview-only subtree proposal. Candidate payloads have a
283
+ 256 KiB aggregate serialized limit. Resolution and cancellation use atomic
284
+ compare-and-set cleanup so an older request cannot erase a newer one.
285
+ `view-screen` lists only proposals paired to the current pending request as
286
+ `pendingCandidateReviews`.
271
287
  - `show-design-questions` opens focused pre-generation questions in the main
272
288
  design canvas (`show-questions` application state).
273
289
  - `guided-questions` may contain a one-click chat choice for the current
@@ -0,0 +1,54 @@
1
+ import { defineAction } from "@agent-native/core";
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
+ designRepromptPendingStateKey,
11
+ designRepromptProposalStateKey,
12
+ isNodeRewriteProposal,
13
+ isPendingDesignReprompt,
14
+ } from "../shared/node-rewrite.js";
15
+
16
+ export default defineAction({
17
+ agentTool: false,
18
+ description:
19
+ "Cancel one pending node rewrite only when it is still the current request.",
20
+ schema: z.object({
21
+ designId: z.string().min(1),
22
+ fileId: z.string().min(1),
23
+ repromptId: z.string().min(1),
24
+ }),
25
+ run: async ({ designId, fileId, repromptId }) => {
26
+ await assertAccess("design", designId, "editor");
27
+ const pendingKey = designRepromptPendingStateKey(designId, fileId);
28
+ const pending = await readAppState(pendingKey);
29
+ if (
30
+ !isPendingDesignReprompt(pending) ||
31
+ pending.repromptId !== repromptId
32
+ ) {
33
+ return { cancelled: false, superseded: true };
34
+ }
35
+
36
+ const proposalKey = designRepromptProposalStateKey(
37
+ designId,
38
+ fileId,
39
+ repromptId,
40
+ );
41
+ const proposal = await readAppState(proposalKey);
42
+ const [pendingCancelled, proposalCancelled] = await Promise.all([
43
+ compareAndSetAppState(pendingKey, pending, null),
44
+ isNodeRewriteProposal(proposal)
45
+ ? compareAndSetAppState(proposalKey, proposal, null)
46
+ : Promise.resolve(false),
47
+ ]);
48
+ return {
49
+ cancelled: pendingCancelled,
50
+ proposalCancelled,
51
+ superseded: !pendingCancelled,
52
+ };
53
+ },
54
+ });
@@ -0,0 +1,290 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ compareAndSetAppState,
4
+ readAppState,
5
+ writeAppState,
6
+ } from "@agent-native/core/application-state";
7
+ import { accessFilter, assertAccess } from "@agent-native/core/sharing";
8
+ import { and, eq } from "drizzle-orm";
9
+ import { nanoid } from "nanoid";
10
+ import { z } from "zod";
11
+
12
+ import { getDb, schema } from "../server/db/index.js";
13
+ import {
14
+ readLiveSourceFile,
15
+ type SourceWorkspaceFile,
16
+ } from "../server/source-workspace.js";
17
+ import type { CodeLayerSource } from "../shared/code-layer.js";
18
+ import {
19
+ designRepromptPendingStateKey,
20
+ designRepromptProposalStateKey,
21
+ MAX_NODE_REWRITE_PROPOSAL_BYTES,
22
+ resolveNodeRewriteTarget,
23
+ validateNodeRewriteVariant,
24
+ type NodeHtmlPreviewBridgeMessage,
25
+ type NodeRewriteProposal,
26
+ type NodeRewriteTarget,
27
+ } from "../shared/node-rewrite.js";
28
+ import { designSourceTypeFromData } from "../shared/source-mode.js";
29
+
30
+ const sourceSchema = z
31
+ .object({
32
+ designId: z.string().min(1).optional(),
33
+ fileId: z.string().min(1).optional(),
34
+ filename: z.string().min(1).optional(),
35
+ })
36
+ .superRefine((source, ctx) => {
37
+ if (!source.designId && !source.fileId) {
38
+ ctx.addIssue({
39
+ code: z.ZodIssueCode.custom,
40
+ path: ["designId"],
41
+ message: "source.designId or source.fileId is required",
42
+ });
43
+ }
44
+ });
45
+
46
+ const targetSchema = z
47
+ .object({
48
+ nodeId: z.string().min(1).optional(),
49
+ selector: z.string().min(1).optional(),
50
+ })
51
+ .superRefine((target, ctx) => {
52
+ if (!target.nodeId && !target.selector) {
53
+ ctx.addIssue({
54
+ code: z.ZodIssueCode.custom,
55
+ path: ["nodeId"],
56
+ message: "target.nodeId or target.selector is required",
57
+ });
58
+ }
59
+ });
60
+
61
+ const variantSchema = z.object({
62
+ html: z
63
+ .string()
64
+ .min(1)
65
+ .max(250_000)
66
+ .describe("Replacement outerHTML for the selected subtree"),
67
+ summary: z.string().trim().min(1).max(240),
68
+ });
69
+
70
+ const pendingRepromptSchema = z.object({
71
+ repromptId: z.string().min(1),
72
+ designId: z.string().min(1),
73
+ fileId: z.string().min(1),
74
+ target: targetSchema,
75
+ baseVersionHash: z.string().min(1),
76
+ instruction: z.string(),
77
+ createdAt: z.string(),
78
+ priorProposalId: z.string().min(1).optional(),
79
+ priorRepromptId: z.string().min(1).optional(),
80
+ });
81
+
82
+ interface EditableDesignFile extends SourceWorkspaceFile {
83
+ designData: string | null;
84
+ }
85
+
86
+ async function resolveEditableDesignFile(source: {
87
+ designId?: string;
88
+ fileId?: string;
89
+ filename?: string;
90
+ }): Promise<EditableDesignFile> {
91
+ const db = getDb();
92
+ const conditions = [
93
+ accessFilter(schema.designs, schema.designShares),
94
+ source.fileId
95
+ ? eq(schema.designFiles.id, source.fileId)
96
+ : eq(schema.designFiles.designId, source.designId ?? ""),
97
+ ];
98
+ if (!source.fileId) {
99
+ conditions.push(
100
+ eq(schema.designFiles.filename, source.filename ?? "index.html"),
101
+ );
102
+ }
103
+
104
+ const [file] = await db
105
+ .select({
106
+ id: schema.designFiles.id,
107
+ designId: schema.designFiles.designId,
108
+ filename: schema.designFiles.filename,
109
+ fileType: schema.designFiles.fileType,
110
+ content: schema.designFiles.content,
111
+ createdAt: schema.designFiles.createdAt,
112
+ updatedAt: schema.designFiles.updatedAt,
113
+ designData: schema.designs.data,
114
+ })
115
+ .from(schema.designFiles)
116
+ .innerJoin(
117
+ schema.designs,
118
+ eq(schema.designFiles.designId, schema.designs.id),
119
+ )
120
+ .where(and(...conditions))
121
+ .limit(1);
122
+
123
+ if (!file) throw new Error("Design HTML file not found.");
124
+ if (source.designId && source.designId !== file.designId) {
125
+ throw new Error(
126
+ `source.designId "${source.designId}" does not match file "${file.id}"`,
127
+ );
128
+ }
129
+ if (source.filename && source.filename !== file.filename) {
130
+ throw new Error(
131
+ `source.filename "${source.filename}" does not match file "${file.id}"`,
132
+ );
133
+ }
134
+ if (file.fileType !== "html") {
135
+ throw new Error("Node rewrites only support HTML design files.");
136
+ }
137
+ if (designSourceTypeFromData(file.designData) !== "inline") {
138
+ throw new Error("Node rewrites only support inline design screens.");
139
+ }
140
+ await assertAccess("design", file.designId, "editor");
141
+ return file;
142
+ }
143
+
144
+ function targetsMatch(a: NodeRewriteTarget, b: NodeRewriteTarget): boolean {
145
+ const nodeIdsConflict = a.nodeId && b.nodeId && a.nodeId !== b.nodeId;
146
+ const selectorsConflict =
147
+ a.selector && b.selector && a.selector !== b.selector;
148
+ if (nodeIdsConflict || selectorsConflict) return false;
149
+ return Boolean(
150
+ (a.nodeId && b.nodeId && a.nodeId === b.nodeId) ||
151
+ (a.selector && b.selector && a.selector === b.selector),
152
+ );
153
+ }
154
+
155
+ export default defineAction({
156
+ description:
157
+ "Propose one to three scoped HTML rewrites for a pending Design selection. " +
158
+ "This previews only: it stores proposal state and never changes the design file. " +
159
+ "The target must identify, and the reprompt id and base hash must exactly match, the pending client selection.",
160
+ schema: z.object({
161
+ source: sourceSchema,
162
+ target: targetSchema,
163
+ baseVersionHash: z.string().min(1),
164
+ variants: z.array(variantSchema).min(1).max(3),
165
+ repromptId: z.string().min(1),
166
+ }),
167
+ run: async ({ source, target, baseVersionHash, variants, repromptId }) => {
168
+ const file = await resolveEditableDesignFile(source);
169
+ const sourceDescriptor: CodeLayerSource = {
170
+ kind: "design-file",
171
+ sourceType: "inline",
172
+ designId: file.designId,
173
+ fileId: file.id,
174
+ filename: file.filename,
175
+ };
176
+ const live = await readLiveSourceFile(file);
177
+ if (live.versionHash !== baseVersionHash) {
178
+ throw new Error(
179
+ "Screen changed since the selection was captured — regenerate the proposal.",
180
+ );
181
+ }
182
+
183
+ const pendingKey = designRepromptPendingStateKey(file.designId, file.id);
184
+ const pending = pendingRepromptSchema.parse(await readAppState(pendingKey));
185
+ if (pending.repromptId !== repromptId) {
186
+ throw new Error("Pending reprompt does not match this request.");
187
+ }
188
+ if (
189
+ pending.designId !== file.designId ||
190
+ pending.fileId !== file.id ||
191
+ !targetsMatch(pending.target, target)
192
+ ) {
193
+ throw new Error(
194
+ "The proposed target does not match the pending selected subtree.",
195
+ );
196
+ }
197
+ if (pending.baseVersionHash !== baseVersionHash) {
198
+ throw new Error(
199
+ "The proposal base hash does not match the pending selection.",
200
+ );
201
+ }
202
+
203
+ const authoritativeTarget = pending.target;
204
+ const node = resolveNodeRewriteTarget(
205
+ live.content,
206
+ authoritativeTarget,
207
+ sourceDescriptor,
208
+ );
209
+ const validatedVariants = variants.map((variant) =>
210
+ validateNodeRewriteVariant(variant, sourceDescriptor),
211
+ );
212
+ const proposalId = `node-rewrite-${nanoid()}`;
213
+ const resolvedTarget = {
214
+ nodeId: node.id,
215
+ selector: node.selector,
216
+ };
217
+ const proposal: NodeRewriteProposal = {
218
+ proposalId,
219
+ repromptId,
220
+ designId: file.designId,
221
+ fileId: file.id,
222
+ filename: file.filename,
223
+ target: authoritativeTarget,
224
+ resolvedTarget,
225
+ baseVersionHash,
226
+ variants: validatedVariants,
227
+ chosenIndex: 0,
228
+ createdAt: new Date().toISOString(),
229
+ };
230
+ const serializedProposal = JSON.stringify(proposal);
231
+ if (
232
+ Buffer.byteLength(serializedProposal, "utf8") >
233
+ MAX_NODE_REWRITE_PROPOSAL_BYTES
234
+ ) {
235
+ throw new Error(
236
+ "Generated candidates are too large to preview safely. Use fewer variants or regenerate a smaller selected subtree.",
237
+ );
238
+ }
239
+ const proposalKey = designRepromptProposalStateKey(
240
+ file.designId,
241
+ file.id,
242
+ repromptId,
243
+ );
244
+ await writeAppState(
245
+ proposalKey,
246
+ proposal as unknown as Record<string, unknown>,
247
+ );
248
+ const currentPending = await readAppState(pendingKey);
249
+ if (currentPending?.repromptId !== repromptId) {
250
+ await compareAndSetAppState(
251
+ proposalKey,
252
+ proposal as unknown as Record<string, unknown>,
253
+ null,
254
+ );
255
+ throw new Error(
256
+ "This regeneration was superseded by a newer request before its candidates were published.",
257
+ );
258
+ }
259
+ if (pending.priorProposalId && pending.priorRepromptId) {
260
+ const priorProposalKey = designRepromptProposalStateKey(
261
+ file.designId,
262
+ file.id,
263
+ pending.priorRepromptId,
264
+ );
265
+ const priorProposal = await readAppState(priorProposalKey);
266
+ if (priorProposal?.proposalId === pending.priorProposalId) {
267
+ await compareAndSetAppState(priorProposalKey, priorProposal, null);
268
+ }
269
+ }
270
+
271
+ const bridgeMessages: NodeHtmlPreviewBridgeMessage[] = [
272
+ {
273
+ type: "node-html-preview",
274
+ proposalId,
275
+ target: resolvedTarget,
276
+ html: validatedVariants[0]!.html,
277
+ operation: "preview",
278
+ },
279
+ ];
280
+ return {
281
+ proposalId,
282
+ repromptId,
283
+ designId: file.designId,
284
+ fileId: file.id,
285
+ target: resolvedTarget,
286
+ variants: validatedVariants,
287
+ bridgeMessages,
288
+ };
289
+ },
290
+ });