@agent-native/core 0.70.0 → 0.70.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 (167) hide show
  1. package/README.md +15 -15
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +61 -0
  4. package/corpus/core/docs/design/durable-agent-runs.md +217 -0
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/action.ts +89 -0
  7. package/corpus/core/src/agent/model-config.ts +0 -2
  8. package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
  9. package/corpus/core/src/cli/skills.ts +2 -2
  10. package/corpus/core/src/client/AgentPanel.tsx +5 -3
  11. package/corpus/core/src/client/AssistantChat.tsx +8 -0
  12. package/corpus/core/src/client/client-surface.ts +41 -0
  13. package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
  14. package/corpus/core/src/client/feedback-context.ts +4 -0
  15. package/corpus/core/src/client/index.ts +1 -0
  16. package/corpus/core/src/client/sse-event-processor.ts +7 -1
  17. package/corpus/core/src/mcp/build-server.ts +57 -1
  18. package/corpus/core/src/mcp/embed-app.ts +86 -2
  19. package/corpus/core/src/mcp/oauth-route.ts +109 -13
  20. package/corpus/core/src/server/core-routes-plugin.ts +78 -0
  21. package/corpus/core/src/server/onboarding-html.ts +35 -0
  22. package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  23. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
  24. package/corpus/templates/analytics/AGENTS.md +8 -1
  25. package/corpus/templates/analytics/actions/install-dashboard-template.ts +97 -2
  26. package/corpus/templates/analytics/actions/update-dashboard.ts +36 -7
  27. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
  28. package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
  29. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -4
  30. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -3
  31. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
  32. package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
  34. package/corpus/templates/assets/actions/generate-image.ts +24 -0
  35. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
  36. package/corpus/templates/assets/app/global.css +17 -1
  37. package/corpus/templates/assets/app/routes/_index.tsx +94 -14
  38. package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
  39. package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
  40. package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
  41. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
  42. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
  43. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
  44. package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
  50. package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
  51. package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
  52. package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
  53. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
  54. package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
  55. package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
  56. package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
  57. package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
  58. package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
  59. package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
  60. package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
  61. package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
  62. package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
  63. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
  64. package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
  65. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
  66. package/corpus/templates/clips/desktop/src/styles.css +70 -2
  67. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +73 -0
  68. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +34 -5
  69. package/corpus/templates/content/AGENTS.md +20 -3
  70. package/corpus/templates/content/actions/_database-utils.ts +15 -0
  71. package/corpus/templates/content/actions/_property-utils.ts +371 -4
  72. package/corpus/templates/content/actions/configure-document-property.ts +35 -1
  73. package/corpus/templates/content/actions/create-content-database.ts +5 -1
  74. package/corpus/templates/content/actions/delete-document-property.ts +52 -2
  75. package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
  76. package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
  77. package/corpus/templates/content/actions/set-document-property.ts +39 -0
  78. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
  79. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
  80. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
  81. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
  82. package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
  83. package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
  84. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
  85. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
  86. package/corpus/templates/content/app/global.css +9 -0
  87. package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
  88. package/corpus/templates/content/server/db/schema.ts +30 -0
  89. package/corpus/templates/content/server/plugins/db.ts +87 -0
  90. package/corpus/templates/content/shared/api.ts +7 -0
  91. package/corpus/templates/content/shared/properties.ts +109 -0
  92. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
  93. package/corpus/templates/forms/actions/export-responses.ts +6 -0
  94. package/corpus/templates/forms/actions/list-responses.ts +2 -0
  95. package/corpus/templates/forms/actions/response-insights.ts +2 -0
  96. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
  97. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
  98. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
  99. package/corpus/templates/forms/server/db/schema.ts +7 -0
  100. package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
  101. package/corpus/templates/forms/server/lib/integrations.ts +76 -2
  102. package/corpus/templates/forms/server/plugins/db.ts +20 -0
  103. package/corpus/templates/forms/shared/types.ts +12 -0
  104. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
  105. package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
  106. package/dist/action.js +87 -0
  107. package/dist/action.js.map +1 -1
  108. package/dist/agent/engine/builder-engine.d.ts +1 -1
  109. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  110. package/dist/agent/model-config.d.ts +2 -2
  111. package/dist/agent/model-config.d.ts.map +1 -1
  112. package/dist/agent/model-config.js +0 -2
  113. package/dist/agent/model-config.js.map +1 -1
  114. package/dist/agent/run-loop-with-resume.d.ts +13 -0
  115. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  116. package/dist/agent/run-loop-with-resume.js +44 -0
  117. package/dist/agent/run-loop-with-resume.js.map +1 -1
  118. package/dist/cli/skills.js +2 -2
  119. package/dist/cli/skills.js.map +1 -1
  120. package/dist/client/AgentPanel.d.ts.map +1 -1
  121. package/dist/client/AgentPanel.js +5 -3
  122. package/dist/client/AgentPanel.js.map +1 -1
  123. package/dist/client/AssistantChat.d.ts +6 -1
  124. package/dist/client/AssistantChat.d.ts.map +1 -1
  125. package/dist/client/AssistantChat.js +2 -2
  126. package/dist/client/AssistantChat.js.map +1 -1
  127. package/dist/client/client-surface.d.ts +17 -0
  128. package/dist/client/client-surface.d.ts.map +1 -0
  129. package/dist/client/client-surface.js +24 -0
  130. package/dist/client/client-surface.js.map +1 -0
  131. package/dist/client/composer/TiptapComposer.d.ts +26 -1
  132. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  133. package/dist/client/composer/TiptapComposer.js +27 -5
  134. package/dist/client/composer/TiptapComposer.js.map +1 -1
  135. package/dist/client/feedback-context.d.ts +3 -0
  136. package/dist/client/feedback-context.d.ts.map +1 -1
  137. package/dist/client/feedback-context.js +2 -0
  138. package/dist/client/feedback-context.js.map +1 -1
  139. package/dist/client/index.d.ts +1 -0
  140. package/dist/client/index.d.ts.map +1 -1
  141. package/dist/client/index.js +1 -0
  142. package/dist/client/index.js.map +1 -1
  143. package/dist/client/sse-event-processor.d.ts.map +1 -1
  144. package/dist/client/sse-event-processor.js +7 -1
  145. package/dist/client/sse-event-processor.js.map +1 -1
  146. package/dist/mcp/build-server.d.ts +21 -0
  147. package/dist/mcp/build-server.d.ts.map +1 -1
  148. package/dist/mcp/build-server.js +43 -1
  149. package/dist/mcp/build-server.js.map +1 -1
  150. package/dist/mcp/embed-app.d.ts.map +1 -1
  151. package/dist/mcp/embed-app.js +86 -2
  152. package/dist/mcp/embed-app.js.map +1 -1
  153. package/dist/mcp/oauth-route.d.ts.map +1 -1
  154. package/dist/mcp/oauth-route.js +95 -12
  155. package/dist/mcp/oauth-route.js.map +1 -1
  156. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  157. package/dist/server/core-routes-plugin.js +63 -0
  158. package/dist/server/core-routes-plugin.js.map +1 -1
  159. package/dist/server/onboarding-html.d.ts.map +1 -1
  160. package/dist/server/onboarding-html.js +35 -0
  161. package/dist/server/onboarding-html.js.map +1 -1
  162. package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  163. package/docs/design/durable-agent-runs.md +217 -0
  164. package/package.json +1 -1
  165. package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  166. package/corpus/templates/analytics/app/pages/About.tsx +0 -108
  167. package/corpus/templates/analytics/app/routes/about.tsx +0 -9
@@ -29,6 +29,39 @@ function valueAsString(val: unknown): string {
29
29
  return String(val);
30
30
  }
31
31
 
32
+ /** Drop the protocol for a cleaner table cell; the full URL stays the link href. */
33
+ function formatPageUrl(url: string): string {
34
+ return url.replace(/^https?:\/\//, "");
35
+ }
36
+
37
+ /**
38
+ * Only http(s) URLs are safe to use as an anchor href. Page URLs arrive from
39
+ * client `_meta` and could be spoofed by a direct POST, so reject other schemes
40
+ * (e.g. `javascript:`) to avoid a self-XSS when the owner clicks the cell.
41
+ */
42
+ function safeHttpUrl(value: string): string | null {
43
+ try {
44
+ const u = new URL(value);
45
+ return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ /** Friendly label for the client-surface token forwarded by feedback embeds. */
52
+ function formatClientSurface(surface: string): string {
53
+ switch (surface) {
54
+ case "electron":
55
+ return "Desktop (Electron)";
56
+ case "tauri":
57
+ return "Desktop (Tauri)";
58
+ case "web":
59
+ return "Web";
60
+ default:
61
+ return surface;
62
+ }
63
+ }
64
+
32
65
  function compareValues(a: unknown, b: unknown): number {
33
66
  // Empty values sort last regardless of direction.
34
67
  const aEmpty = a === undefined || a === null || a === "";
@@ -66,8 +99,21 @@ export function ResponsesPage() {
66
99
  () => responses.some((r: any) => valueAsString(r.submitterEmail).trim()),
67
100
  [responses],
68
101
  );
102
+ const hasPageUrl = useMemo(
103
+ () => responses.some((r: any) => valueAsString(r.pageUrl).trim()),
104
+ [responses],
105
+ );
106
+ const hasClientSurface = useMemo(
107
+ () => responses.some((r: any) => valueAsString(r.clientSurface).trim()),
108
+ [responses],
109
+ );
69
110
  const responseTableMinWidth =
70
- 64 + 160 + (hasSubmitterEmail ? 224 : 0) + Math.max(fields.length, 1) * 320;
111
+ 64 +
112
+ 160 +
113
+ (hasSubmitterEmail ? 224 : 0) +
114
+ (hasPageUrl ? 256 : 0) +
115
+ (hasClientSurface ? 160 : 0) +
116
+ Math.max(fields.length, 1) * 320;
71
117
 
72
118
  function toggleSort(key: SortKey) {
73
119
  if (sortKey === key) {
@@ -86,6 +132,16 @@ export function ResponsesPage() {
86
132
  if (valueAsString(r.submitterEmail).toLowerCase().includes(q)) {
87
133
  return true;
88
134
  }
135
+ if (valueAsString(r.pageUrl).toLowerCase().includes(q)) {
136
+ return true;
137
+ }
138
+ if (
139
+ formatClientSurface(valueAsString(r.clientSurface))
140
+ .toLowerCase()
141
+ .includes(q)
142
+ ) {
143
+ return true;
144
+ }
89
145
  for (const f of fields) {
90
146
  if (valueAsString(r.data[f.id]).toLowerCase().includes(q))
91
147
  return true;
@@ -100,6 +156,10 @@ export function ResponsesPage() {
100
156
  new Date(a.submittedAt).getTime() - new Date(b.submittedAt).getTime();
101
157
  } else if (sortKey === "_email") {
102
158
  cmp = compareValues(a.submitterEmail, b.submitterEmail);
159
+ } else if (sortKey === "_page") {
160
+ cmp = compareValues(a.pageUrl, b.pageUrl);
161
+ } else if (sortKey === "_source") {
162
+ cmp = compareValues(a.clientSurface, b.clientSurface);
103
163
  } else {
104
164
  cmp = compareValues(a.data[sortKey], b.data[sortKey]);
105
165
  }
@@ -113,11 +173,21 @@ export function ResponsesPage() {
113
173
  const headers = [
114
174
  "Submitted At",
115
175
  ...(hasSubmitterEmail ? ["Submitter Email"] : []),
176
+ ...(hasPageUrl ? ["Page URL"] : []),
177
+ ...(hasClientSurface ? ["Source"] : []),
116
178
  ...fields.map((f) => f.label),
117
179
  ];
118
180
  const rows = filteredSorted.map((r) => [
119
181
  r.submittedAt,
120
182
  ...(hasSubmitterEmail ? [valueAsString(r.submitterEmail)] : []),
183
+ ...(hasPageUrl ? [valueAsString(r.pageUrl)] : []),
184
+ ...(hasClientSurface
185
+ ? [
186
+ valueAsString(r.clientSurface)
187
+ ? formatClientSurface(valueAsString(r.clientSurface))
188
+ : "",
189
+ ]
190
+ : []),
121
191
  ...fields.map((f) => valueAsString(r.data[f.id])),
122
192
  ]);
123
193
 
@@ -282,6 +352,8 @@ export function ResponsesPage() {
282
352
  <col className="w-16" />
283
353
  <col className="w-40" />
284
354
  {hasSubmitterEmail ? <col className="w-56" /> : null}
355
+ {hasPageUrl ? <col className="w-64" /> : null}
356
+ {hasClientSurface ? <col className="w-40" /> : null}
285
357
  {fields.map((f) => (
286
358
  <col key={f.id} className="w-80" />
287
359
  ))}
@@ -308,6 +380,22 @@ export function ResponsesPage() {
308
380
  onClick={() => toggleSort("_email")}
309
381
  />
310
382
  ) : null}
383
+ {hasPageUrl ? (
384
+ <SortableHeader
385
+ label="Page"
386
+ active={sortKey === "_page"}
387
+ dir={sortDir}
388
+ onClick={() => toggleSort("_page")}
389
+ />
390
+ ) : null}
391
+ {hasClientSurface ? (
392
+ <SortableHeader
393
+ label="Source"
394
+ active={sortKey === "_source"}
395
+ dir={sortDir}
396
+ onClick={() => toggleSort("_source")}
397
+ />
398
+ ) : null}
311
399
  {fields.map((f) => (
312
400
  <SortableHeader
313
401
  key={f.id}
@@ -336,6 +424,55 @@ export function ResponsesPage() {
336
424
  {valueAsString(response.submitterEmail) || "-"}
337
425
  </td>
338
426
  ) : null}
427
+ {hasPageUrl ? (
428
+ <td className="w-64 px-4 py-3 align-top text-xs whitespace-normal break-words">
429
+ {(() => {
430
+ const raw = valueAsString(response.pageUrl);
431
+ if (!raw)
432
+ return (
433
+ <span className="text-muted-foreground">-</span>
434
+ );
435
+ const safe = safeHttpUrl(raw);
436
+ return safe ? (
437
+ <a
438
+ href={safe}
439
+ target="_blank"
440
+ rel="noopener noreferrer nofollow"
441
+ title={raw}
442
+ className="text-primary hover:underline"
443
+ >
444
+ {formatPageUrl(raw)}
445
+ </a>
446
+ ) : (
447
+ <span title={raw} className="text-muted-foreground">
448
+ {formatPageUrl(raw)}
449
+ </span>
450
+ );
451
+ })()}
452
+ </td>
453
+ ) : null}
454
+ {hasClientSurface ? (
455
+ <td className="w-40 px-4 py-3 align-top text-xs whitespace-nowrap">
456
+ {(() => {
457
+ const surface = valueAsString(response.clientSurface);
458
+ if (!surface)
459
+ return (
460
+ <span className="text-muted-foreground">-</span>
461
+ );
462
+ const label = formatClientSurface(surface);
463
+ // Make desktop submissions pop; web stays muted text.
464
+ return surface === "web" ? (
465
+ <span className="text-muted-foreground">
466
+ {label}
467
+ </span>
468
+ ) : (
469
+ <Badge variant="secondary" className="font-normal">
470
+ {label}
471
+ </Badge>
472
+ );
473
+ })()}
474
+ </td>
475
+ ) : null}
339
476
  {fields.map((f) => {
340
477
  const val = response.data[f.id];
341
478
  const display =
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Response tables now show the page each submission came from, passed through from embedded feedback widgets
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Response tables now show whether feedback came from the web, the Electron desktop app, or a Tauri app
@@ -32,6 +32,13 @@ export const responses = table("responses", {
32
32
  submittedAt: text("submitted_at").notNull(),
33
33
  ip: text("ip"),
34
34
  submitterEmail: text("submitter_email"),
35
+ // URL of the page the respondent was on, forwarded by trusted embeds (e.g.
36
+ // the framework FeedbackButton) as a hidden pass-through field. Client-scrubbed
37
+ // of sensitive query params. NULL for direct fills that send no page context.
38
+ pageUrl: text("page_url"),
39
+ // Runtime shell the feedback was sent from: "web", "electron", or "tauri".
40
+ // Hidden pass-through field forwarded by trusted embeds. NULL when unknown.
41
+ clientSurface: text("client_surface"),
35
42
  });
36
43
 
37
44
  export const formShares = createSharesTable("form_shares");
@@ -49,6 +49,15 @@ function cleanSubmitterEmail(value: unknown): string | null {
49
49
  return trimmed;
50
50
  }
51
51
 
52
+ // Allowlist the client-surface hint so only known values are stored. Anything
53
+ // else (including spoofed direct POSTs) is dropped to NULL.
54
+ const KNOWN_CLIENT_SURFACES = new Set(["web", "electron", "tauri"]);
55
+ function cleanClientSurface(value: unknown): string | null {
56
+ if (typeof value !== "string") return null;
57
+ const normalized = value.trim().toLowerCase();
58
+ return KNOWN_CLIENT_SURFACES.has(normalized) ? normalized : null;
59
+ }
60
+
52
61
  function cleanChatSessionIds(value: unknown): string[] {
53
62
  const ids: string[] = [];
54
63
  const visit = (item: unknown) => {
@@ -219,6 +228,7 @@ export const submitForm = defineEventHandler(async (event: H3Event) => {
219
228
  chatSessionIds?: unknown;
220
229
  activeRunId?: unknown;
221
230
  pageUrl?: unknown;
231
+ clientSurface?: unknown;
222
232
  })
223
233
  : null;
224
234
  const session = await getSession(event).catch(() => null);
@@ -231,6 +241,7 @@ export const submitForm = defineEventHandler(async (event: H3Event) => {
231
241
  ]);
232
242
  const activeRunId = cleanMetaText(meta?.activeRunId);
233
243
  const pageUrl = cleanMetaText(meta?.pageUrl);
244
+ const clientSurface = cleanClientSurface(meta?.clientSurface);
234
245
 
235
246
  await db.insert(schema.responses).values({
236
247
  id: responseId,
@@ -239,6 +250,8 @@ export const submitForm = defineEventHandler(async (event: H3Event) => {
239
250
  submittedAt: now,
240
251
  ip,
241
252
  submitterEmail,
253
+ pageUrl,
254
+ clientSurface,
242
255
  });
243
256
 
244
257
  // Write submission notification to application state (SQL-backed)
@@ -257,8 +270,9 @@ export const submitForm = defineEventHandler(async (event: H3Event) => {
257
270
  // Fire integrations best-effort and never fail the submission. Keep this
258
271
  // awaited: serverless hosts can freeze fire-and-forget work as soon as the
259
272
  // HTTP response returns, which silently drops Slack/webhook delivery.
260
- // Feedback debug context is intentionally integration-only: it helps triage
261
- // Slack submissions without retaining page URLs or debug breadcrumbs in SQL.
273
+ // pageUrl and clientSurface are persisted on the response (above) so owners
274
+ // can see which screen and which app feedback came from; chat session ids and
275
+ // run ids remain integration-only debug breadcrumbs we don't retain in SQL.
262
276
  try {
263
277
  const integrations: FormIntegration[] = settings.integrations ?? [];
264
278
  if (integrations.length > 0) {
@@ -273,6 +287,7 @@ export const submitForm = defineEventHandler(async (event: H3Event) => {
273
287
  chatSessionIds,
274
288
  activeRunId,
275
289
  pageUrl,
290
+ clientSurface,
276
291
  });
277
292
  }
278
293
  } catch {
@@ -326,6 +341,8 @@ export const listResponses = defineEventHandler(async (event: H3Event) => {
326
341
  data: JSON.parse(r.data),
327
342
  submittedAt: r.submittedAt,
328
343
  submitterEmail: r.submitterEmail,
344
+ pageUrl: r.pageUrl ?? null,
345
+ clientSurface: r.clientSurface ?? null,
329
346
  })) as FormResponse[],
330
347
  total: total?.count ?? 0,
331
348
  fields: JSON.parse(access.resource.fields),
@@ -48,6 +48,64 @@ interface SubmissionPayload {
48
48
  activeRunId?: string | null;
49
49
  /** Page URL where the feedback was submitted, when available. */
50
50
  pageUrl?: string | null;
51
+ /** Client surface (web/electron/tauri) the feedback came from, when known. */
52
+ clientSurface?: string | null;
53
+ }
54
+
55
+ /** Human-readable label for a client-surface token. */
56
+ function clientSurfaceLabel(surface: string): string {
57
+ switch (surface) {
58
+ case "electron":
59
+ return "Desktop (Electron)";
60
+ case "tauri":
61
+ return "Desktop (Tauri)";
62
+ case "web":
63
+ return "Web";
64
+ default:
65
+ return surface;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Friendly app name derived from a feedback page URL, so a reviewer can tell at
71
+ * a glance which app the feedback came from. `plan.agent-native.com` → "Plan",
72
+ * `analytics.agent-native.com` → "Analytics". Returns null when the host isn't a
73
+ * recognizable per-app subdomain (the full URL still carries the page).
74
+ */
75
+ function appLabelFromUrl(pageUrl: string): string | null {
76
+ try {
77
+ const { hostname } = new URL(pageUrl);
78
+ const match = hostname.match(/^([a-z0-9-]+)\.agent-native\.com$/i);
79
+ const sub = match?.[1];
80
+ if (!sub || sub === "www") return null;
81
+ return sub
82
+ .split("-")
83
+ .map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part))
84
+ .join(" ");
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Readable host+path label for a feedback page URL, used as the visible text of
92
+ * the Slack link so the app/page is legible inline instead of hidden behind a
93
+ * bare "open". The full (already client-scrubbed) URL stays the link target.
94
+ */
95
+ function pageLabelFromUrl(pageUrl: string): string {
96
+ let label = pageUrl;
97
+ try {
98
+ const url = new URL(pageUrl);
99
+ label = `${url.hostname}${url.pathname}`.replace(/\/$/, "") || url.hostname;
100
+ } catch {
101
+ // fall back to the raw string below
102
+ }
103
+ if (label.length > 80) label = `${label.slice(0, 79)}…`;
104
+ // Escape Slack mrkdwn link-text control characters.
105
+ return label
106
+ .replace(/&/g, "&amp;")
107
+ .replace(/</g, "&lt;")
108
+ .replace(/>/g, "&gt;");
51
109
  }
52
110
 
53
111
  // ---------------------------------------------------------------------------
@@ -82,13 +140,20 @@ function formatDebugContext(submission: SubmissionPayload): string[] {
82
140
  lines.push(`Run: \`${submission.activeRunId}\``);
83
141
  }
84
142
  if (submission.pageUrl) {
85
- lines.push(`Page: <${submission.pageUrl}|open>`);
143
+ const appLabel = appLabelFromUrl(submission.pageUrl);
144
+ if (appLabel) lines.push(`App: ${appLabel}`);
145
+ lines.push(
146
+ `Page: <${submission.pageUrl}|${pageLabelFromUrl(submission.pageUrl)}>`,
147
+ );
148
+ }
149
+ if (submission.clientSurface) {
150
+ lines.push(`Source: ${clientSurfaceLabel(submission.clientSurface)}`);
86
151
  }
87
152
  return lines;
88
153
  }
89
154
 
90
155
  /** Slack Block Kit message */
91
- function buildSlackPayload(submission: SubmissionPayload) {
156
+ export function buildSlackPayload(submission: SubmissionPayload) {
92
157
  const fieldLines = submission.fields
93
158
  .filter((f) => submission.data[f.id] !== undefined)
94
159
  .map((f) => {
@@ -170,6 +235,13 @@ function buildDiscordPayload(submission: SubmissionPayload) {
170
235
  inline: false,
171
236
  });
172
237
  }
238
+ if (submission.clientSurface) {
239
+ discordFields.push({
240
+ name: "Source",
241
+ value: clientSurfaceLabel(submission.clientSurface),
242
+ inline: true,
243
+ });
244
+ }
173
245
 
174
246
  return {
175
247
  embeds: [
@@ -192,6 +264,7 @@ function buildGoogleSheetsPayload(submission: SubmissionPayload) {
192
264
  chatSessionIds: (submission.chatSessionIds ?? []).join(", "),
193
265
  activeRunId: submission.activeRunId ?? "",
194
266
  pageUrl: submission.pageUrl ?? "",
267
+ clientSurface: submission.clientSurface ?? "",
195
268
  ...formatFields(submission.fields, submission.data),
196
269
  };
197
270
  }
@@ -208,6 +281,7 @@ function buildWebhookPayload(submission: SubmissionPayload) {
208
281
  chatSessionIds: submission.chatSessionIds ?? [],
209
282
  activeRunId: submission.activeRunId ?? null,
210
283
  pageUrl: submission.pageUrl ?? null,
284
+ clientSurface: submission.clientSurface ?? null,
211
285
  data: formatFields(submission.fields, submission.data),
212
286
  rawData: submission.data,
213
287
  };
@@ -102,6 +102,26 @@ CREATE TABLE IF NOT EXISTS form_shares (
102
102
  CREATE INDEX IF NOT EXISTS responses_form_id_idx ON responses (form_id, submitted_at);
103
103
  CREATE INDEX IF NOT EXISTS form_shares_resource_idx ON form_shares (resource_id, principal_type, principal_id)`,
104
104
  },
105
+ {
106
+ // Page URL the respondent was on, forwarded by trusted embeds (e.g. the
107
+ // framework FeedbackButton) as a hidden pass-through field so owners can
108
+ // see which screen feedback came from in the responses table.
109
+ version: 11,
110
+ sql: {
111
+ postgres: `ALTER TABLE responses ADD COLUMN IF NOT EXISTS page_url TEXT`,
112
+ sqlite: `ALTER TABLE responses ADD COLUMN IF NOT EXISTS page_url TEXT`,
113
+ },
114
+ },
115
+ {
116
+ // Client surface (web/electron/tauri) the respondent submitted from,
117
+ // forwarded by trusted embeds as a hidden pass-through field so owners can
118
+ // see whether feedback came from a desktop app or the browser.
119
+ version: 12,
120
+ sql: {
121
+ postgres: `ALTER TABLE responses ADD COLUMN IF NOT EXISTS client_surface TEXT`,
122
+ sqlite: `ALTER TABLE responses ADD COLUMN IF NOT EXISTS client_surface TEXT`,
123
+ },
124
+ },
105
125
  ],
106
126
  { table: "forms_migrations" },
107
127
  );
@@ -147,6 +147,18 @@ export interface FormResponse {
147
147
  submittedAt: string;
148
148
  /** Email of the submitter when known (claimed by the client; not verified). */
149
149
  submitterEmail?: string | null;
150
+ /**
151
+ * URL of the page the respondent was on, forwarded by trusted embeds (e.g.
152
+ * the framework FeedbackButton) as a hidden pass-through field. Null when the
153
+ * submission carried no page context (e.g. a direct fill on the public page).
154
+ */
155
+ pageUrl?: string | null;
156
+ /**
157
+ * Runtime shell the feedback was sent from — "web", "electron", or "tauri" —
158
+ * forwarded by trusted embeds as a hidden pass-through field. Null when
159
+ * unknown (e.g. a direct fill on the public page).
160
+ */
161
+ clientSurface?: string | null;
150
162
  }
151
163
 
152
164
  // ---------------------------------------------------------------------------
@@ -293,7 +293,9 @@
293
293
  --wf-font: var(--wf-font-clean);
294
294
 
295
295
  position: relative;
296
- overflow: hidden;
296
+ /* Matches core blocks.css `.plan-diagram-frame`: scroll oversized diagrams
297
+ instead of clipping them (Plan's copy had drifted to `hidden`). */
298
+ overflow: auto;
297
299
  width: 100%;
298
300
  border: 0;
299
301
  border-radius: 10px;
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Large diagrams in a plan now scroll within their block instead of being cut off
package/dist/action.js CHANGED
@@ -332,6 +332,92 @@ function zodDefToJsonSchema(def) {
332
332
  // ---------------------------------------------------------------------------
333
333
  // Runtime validation wrapper
334
334
  // ---------------------------------------------------------------------------
335
+ const NO_COERCE = Symbol("no-coerce");
336
+ /**
337
+ * Coerce a single stringified value to one of the JSON-schema types the field
338
+ * expects. Returns NO_COERCE when nothing safe applies, so the caller leaves
339
+ * the original value untouched and the normal validation error still surfaces.
340
+ */
341
+ function coerceStringToSchemaType(raw, types) {
342
+ const trimmed = raw.trim();
343
+ if (types.includes("boolean")) {
344
+ if (trimmed === "true")
345
+ return true;
346
+ if (trimmed === "false")
347
+ return false;
348
+ }
349
+ if ((types.includes("array") &&
350
+ trimmed.startsWith("[") &&
351
+ trimmed.endsWith("]")) ||
352
+ (types.includes("object") &&
353
+ trimmed.startsWith("{") &&
354
+ trimmed.endsWith("}"))) {
355
+ try {
356
+ const parsed = JSON.parse(trimmed);
357
+ if (types.includes("array") && Array.isArray(parsed))
358
+ return parsed;
359
+ if (types.includes("object") &&
360
+ parsed &&
361
+ typeof parsed === "object" &&
362
+ !Array.isArray(parsed)) {
363
+ return parsed;
364
+ }
365
+ }
366
+ catch {
367
+ // fall through — leave the original so validation reports the real error
368
+ }
369
+ }
370
+ if ((types.includes("number") || types.includes("integer")) &&
371
+ trimmed !== "" &&
372
+ Number.isFinite(Number(trimmed))) {
373
+ const n = Number(trimmed);
374
+ if (types.includes("number") || Number.isInteger(n))
375
+ return n;
376
+ }
377
+ return NO_COERCE;
378
+ }
379
+ /**
380
+ * Defensively coerce gateway-stringified tool arguments to the types the schema
381
+ * expects. Some model gateways (notably Builder's Gemini-backed gateway) hand
382
+ * back structured tool-call arguments as JSON strings — an array param arrives
383
+ * as `"[{...}]"`, a boolean as `"true"`. Standard Schema (zod) `validate` does
384
+ * not coerce, so these fail validation and the agent thrashes retrying shapes
385
+ * (and can hang). We only touch a string value when the schema expects a
386
+ * non-string type and the string parses cleanly to it; anything ambiguous
387
+ * (schema also allows string) or unparseable is left as-is. Operates on
388
+ * top-level properties only — once an array/object param is parsed, its nested
389
+ * members are already native and validate normally.
390
+ */
391
+ function coerceGatewayStringifiedArgs(args, parameters) {
392
+ const properties = parameters?.properties;
393
+ if (!properties)
394
+ return args;
395
+ if (!args || typeof args !== "object" || Array.isArray(args))
396
+ return args;
397
+ let out = null;
398
+ for (const [key, raw] of Object.entries(args)) {
399
+ if (typeof raw !== "string")
400
+ continue;
401
+ const spec = properties[key];
402
+ if (!spec)
403
+ continue;
404
+ const types = Array.isArray(spec.type)
405
+ ? spec.type
406
+ : spec.type
407
+ ? [spec.type]
408
+ : [];
409
+ // No declared type, or the field legitimately accepts a string → leave it.
410
+ if (types.length === 0 || types.includes("string"))
411
+ continue;
412
+ const coerced = coerceStringToSchemaType(raw, types);
413
+ if (coerced === NO_COERCE)
414
+ continue;
415
+ if (!out)
416
+ out = { ...args };
417
+ out[key] = coerced;
418
+ }
419
+ return out ?? args;
420
+ }
335
421
  /**
336
422
  * Wrap an action's run function with schema validation.
337
423
  * Invalid inputs get a clear error message (including what was actually passed)
@@ -339,6 +425,7 @@ function zodDefToJsonSchema(def) {
339
425
  */
340
426
  function wrapWithValidation(schema, run, toolParameters) {
341
427
  return async (args, ctx) => {
428
+ args = coerceGatewayStringifiedArgs(args, toolParameters);
342
429
  const result = await schema["~standard"].validate(args);
343
430
  if (result.issues) {
344
431
  // Split issues into "missing required field" vs other validation errors