@agent-native/core 0.84.22 → 0.84.24

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 (97) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/model-config.ts +1 -1
  5. package/corpus/core/src/cli/skills.ts +17 -3
  6. package/corpus/core/src/client/AssistantChat.tsx +45 -12
  7. package/corpus/core/src/client/active-run-state.ts +22 -0
  8. package/corpus/core/src/client/chat/message-components.tsx +2 -1
  9. package/corpus/core/src/client/chat/tool-call-display.tsx +18 -2
  10. package/corpus/core/src/client/sse-event-processor.ts +157 -40
  11. package/corpus/core/src/notifications/index.ts +1 -0
  12. package/corpus/core/src/notifications/registry.ts +13 -1
  13. package/corpus/core/src/templates/default/.agents/skills/frontend-design/SKILL.md +2 -0
  14. package/corpus/core/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +2 -0
  15. package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +2 -0
  16. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +15 -13
  17. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +18 -1
  18. package/corpus/templates/analytics/server/db/schema.ts +1 -1
  19. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -1
  20. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +76 -7
  21. package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +2 -0
  22. package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +2 -0
  23. package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +2 -0
  24. package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +2 -0
  25. package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +2 -0
  26. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +30 -1
  27. package/corpus/templates/clips/actions/finalize-recording.ts +84 -4
  28. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +256 -0
  29. package/corpus/templates/clips/actions/reprocess-recording.ts +160 -0
  30. package/corpus/templates/clips/app/components/player/share-dialog.tsx +2 -2
  31. package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -0
  32. package/corpus/templates/clips/app/i18n/de-DE.ts +1 -0
  33. package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
  34. package/corpus/templates/clips/app/i18n/es-ES.ts +1 -0
  35. package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -0
  36. package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -0
  37. package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -0
  38. package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -0
  39. package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -0
  40. package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -0
  41. package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -0
  42. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +31 -3
  43. package/corpus/templates/clips/changelog/2026-07-01-clip-embeds-now-render-as-a-clean-video-only-player-without-.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-01-shared-clips-now-start-playing-and-scrub-instantly-instead-o.md +6 -0
  45. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  46. package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +2 -2
  47. package/corpus/templates/clips/chrome-extension/src/github-preview.ts +41 -31
  48. package/corpus/templates/clips/server/lib/video-remux.ts +222 -0
  49. package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +2 -0
  50. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +75 -19
  51. package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +2 -0
  52. package/corpus/templates/design/app/pages/DesignEditor.tsx +11 -1
  53. package/corpus/templates/design/changelog/2026-07-01-design-chat-no-longer-shows-an-empty-state-selection-chip-in-the-composer.md +6 -0
  54. package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +2 -0
  55. package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +2 -0
  56. package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +2 -0
  57. package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +2 -0
  58. package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +2 -0
  59. package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +2 -0
  60. package/corpus/templates/videos/.agents/skills/frontend-design/SKILL.md +2 -0
  61. package/dist/agent/model-config.js +1 -1
  62. package/dist/agent/model-config.js.map +1 -1
  63. package/dist/cli/skills.d.ts.map +1 -1
  64. package/dist/cli/skills.js +17 -3
  65. package/dist/cli/skills.js.map +1 -1
  66. package/dist/client/AssistantChat.d.ts +2 -1
  67. package/dist/client/AssistantChat.d.ts.map +1 -1
  68. package/dist/client/AssistantChat.js +37 -15
  69. package/dist/client/AssistantChat.js.map +1 -1
  70. package/dist/client/active-run-state.d.ts +2 -0
  71. package/dist/client/active-run-state.d.ts.map +1 -1
  72. package/dist/client/active-run-state.js +17 -0
  73. package/dist/client/active-run-state.js.map +1 -1
  74. package/dist/client/chat/message-components.d.ts.map +1 -1
  75. package/dist/client/chat/message-components.js +3 -1
  76. package/dist/client/chat/message-components.js.map +1 -1
  77. package/dist/client/chat/tool-call-display.d.ts +3 -1
  78. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  79. package/dist/client/chat/tool-call-display.js +8 -8
  80. package/dist/client/chat/tool-call-display.js.map +1 -1
  81. package/dist/client/sse-event-processor.d.ts +1 -0
  82. package/dist/client/sse-event-processor.d.ts.map +1 -1
  83. package/dist/client/sse-event-processor.js +136 -35
  84. package/dist/client/sse-event-processor.js.map +1 -1
  85. package/dist/notifications/index.d.ts +1 -1
  86. package/dist/notifications/index.d.ts.map +1 -1
  87. package/dist/notifications/index.js +1 -1
  88. package/dist/notifications/index.js.map +1 -1
  89. package/dist/notifications/registry.d.ts +5 -0
  90. package/dist/notifications/registry.d.ts.map +1 -1
  91. package/dist/notifications/registry.js +4 -1
  92. package/dist/notifications/registry.js.map +1 -1
  93. package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +2 -0
  94. package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +2 -0
  95. package/package.json +1 -1
  96. package/src/templates/default/.agents/skills/frontend-design/SKILL.md +2 -0
  97. package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +2 -0
@@ -27,6 +27,7 @@ export type ContentPart =
27
27
  mcpApp?: AgentMcpAppPayload;
28
28
  chatUI?: ActionChatUIConfig;
29
29
  activity?: boolean;
30
+ repeatCount?: number;
30
31
  /**
31
32
  * Set when the server emitted an `approval_required` event for this tool
32
33
  * call (opt-in `needsApproval` actions). The action did NOT run; the UI
@@ -184,6 +185,10 @@ function isPreparingActionActivity(ev: SSEEvent): boolean {
184
185
  return label.startsWith("preparing ") && label.includes(" action");
185
186
  }
186
187
 
188
+ function isProgressEvent(ev: SSEEvent): boolean {
189
+ return ev.type !== "stream_keepalive";
190
+ }
191
+
187
192
  function baseActivityLabel(ev: SSEEvent, tool?: string): string {
188
193
  return humanizeToolLabelText(ev.label ?? "Working", tool);
189
194
  }
@@ -460,6 +465,81 @@ function pendingToolNames(content: ContentPart[]): {
460
465
  return { activity: [...activity], running: [...running] };
461
466
  }
462
467
 
468
+ function contentSnapshot(content: ContentPart[]): ContentPart[] {
469
+ return content.map((part) => {
470
+ if (part.type === "text") return { ...part };
471
+ return {
472
+ ...part,
473
+ args: { ...part.args },
474
+ ...(part.mcpApp ? { mcpApp: { ...part.mcpApp } } : {}),
475
+ ...(part.chatUI ? { chatUI: { ...part.chatUI } } : {}),
476
+ ...(part.approval ? { approval: { ...part.approval } } : {}),
477
+ ...(part.structuredMeta
478
+ ? { structuredMeta: { ...part.structuredMeta } }
479
+ : {}),
480
+ };
481
+ });
482
+ }
483
+
484
+ function repeatSignatureValue(value: unknown): string {
485
+ try {
486
+ return JSON.stringify(value);
487
+ } catch {
488
+ return String(value ?? "");
489
+ }
490
+ }
491
+
492
+ function completedToolRepeatSignature(
493
+ part: Extract<ContentPart, { type: "tool-call" }>,
494
+ ): string | null {
495
+ if (
496
+ part.result === undefined ||
497
+ part.activity === true ||
498
+ part.approval ||
499
+ part.mcpApp ||
500
+ part.chatUI ||
501
+ part.structuredMeta
502
+ ) {
503
+ return null;
504
+ }
505
+ return [
506
+ part.toolName,
507
+ part.argsText,
508
+ repeatSignatureValue(part.args),
509
+ part.result,
510
+ part.isError === true ? "error" : "",
511
+ part.completedSideEffect === true ? "side-effect" : "",
512
+ ].join("\u0000");
513
+ }
514
+
515
+ function coalesceCompletedToolRepeat(
516
+ content: ContentPart[],
517
+ completedIndex: number,
518
+ ): void {
519
+ const current = content[completedIndex];
520
+ const previous = content[completedIndex - 1];
521
+ if (
522
+ !current ||
523
+ !previous ||
524
+ current.type !== "tool-call" ||
525
+ previous.type !== "tool-call"
526
+ ) {
527
+ return;
528
+ }
529
+
530
+ const currentSignature = completedToolRepeatSignature(current);
531
+ if (
532
+ !currentSignature ||
533
+ currentSignature !== completedToolRepeatSignature(previous)
534
+ ) {
535
+ return;
536
+ }
537
+
538
+ previous.repeatCount =
539
+ (previous.repeatCount ?? 1) + (current.repeatCount ?? 1);
540
+ content.splice(completedIndex, 1);
541
+ }
542
+
463
543
  function formatToolNames(tools: string[]): string {
464
544
  const names = tools.map(humanizeToolName);
465
545
  if (names.length === 0) return "the promised action";
@@ -578,7 +658,7 @@ export function processEvent(
578
658
  }
579
659
  return {
580
660
  action: "yield",
581
- result: { content: [...content] } as ChatModelRunResult,
661
+ result: { content: contentSnapshot(content) } as ChatModelRunResult,
582
662
  };
583
663
  }
584
664
 
@@ -615,7 +695,7 @@ export function processEvent(
615
695
  }
616
696
  return {
617
697
  action: "yield",
618
- result: { content: [...content] } as ChatModelRunResult,
698
+ result: { content: contentSnapshot(content) } as ChatModelRunResult,
619
699
  };
620
700
  }
621
701
 
@@ -672,7 +752,7 @@ export function processEvent(
672
752
  }
673
753
  return {
674
754
  action: "yield",
675
- result: { content: [...content] } as ChatModelRunResult,
755
+ result: { content: contentSnapshot(content) } as ChatModelRunResult,
676
756
  };
677
757
  }
678
758
 
@@ -694,7 +774,7 @@ export function processEvent(
694
774
  }
695
775
  return {
696
776
  action: "yield",
697
- result: { content: [...content] } as ChatModelRunResult,
777
+ result: { content: contentSnapshot(content) } as ChatModelRunResult,
698
778
  };
699
779
  }
700
780
 
@@ -730,11 +810,12 @@ export function processEvent(
730
810
  if (part.activity !== true && part.isError !== true) {
731
811
  markCompletedToolAfterAssistantText(state, part.toolName);
732
812
  }
813
+ coalesceCompletedToolRepeat(content, doneIdx);
733
814
  }
734
815
  }
735
816
  return {
736
817
  action: "yield",
737
- result: { content: [...content] } as ChatModelRunResult,
818
+ result: { content: contentSnapshot(content) } as ChatModelRunResult,
738
819
  };
739
820
  }
740
821
 
@@ -764,7 +845,7 @@ export function processEvent(
764
845
  }
765
846
  return {
766
847
  action: "yield",
767
- result: { content: [...content] } as ChatModelRunResult,
848
+ result: { content: contentSnapshot(content) } as ChatModelRunResult,
768
849
  };
769
850
  }
770
851
 
@@ -784,7 +865,7 @@ export function processEvent(
784
865
  }
785
866
  return {
786
867
  action: "yield",
787
- result: { content: [...content] } as ChatModelRunResult,
868
+ result: { content: contentSnapshot(content) } as ChatModelRunResult,
788
869
  };
789
870
  }
790
871
 
@@ -826,7 +907,7 @@ export function processEvent(
826
907
  return {
827
908
  action: "missing_api_key",
828
909
  result: {
829
- content: [...content],
910
+ content: contentSnapshot(content),
830
911
  status: { type: "incomplete" as const, reason: "error" as const },
831
912
  metadata: { custom: { runError } },
832
913
  } as ChatModelRunResult,
@@ -938,7 +1019,7 @@ export function processEvent(
938
1019
  return {
939
1020
  action: "error",
940
1021
  result: {
941
- content: [...content],
1022
+ content: contentSnapshot(content),
942
1023
  status: { type: "incomplete" as const, reason: "error" as const },
943
1024
  metadata: { custom: { runError } },
944
1025
  } as ChatModelRunResult,
@@ -974,7 +1055,7 @@ export function processEvent(
974
1055
  return {
975
1056
  action: "error",
976
1057
  result: {
977
- content: [...content],
1058
+ content: contentSnapshot(content),
978
1059
  status: { type: "incomplete" as const, reason: "error" as const },
979
1060
  metadata: { custom: { runError } },
980
1061
  } as ChatModelRunResult,
@@ -993,7 +1074,7 @@ export function processEvent(
993
1074
  return {
994
1075
  action: "done",
995
1076
  result: {
996
- content: [...content],
1077
+ content: contentSnapshot(content),
997
1078
  status: { type: "complete" as const, reason: "stop" as const },
998
1079
  metadata: {
999
1080
  custom: {
@@ -1009,7 +1090,7 @@ export function processEvent(
1009
1090
  }
1010
1091
  return {
1011
1092
  action: "done",
1012
- result: { content: [...content] } as ChatModelRunResult,
1093
+ result: { content: contentSnapshot(content) } as ChatModelRunResult,
1013
1094
  };
1014
1095
  }
1015
1096
 
@@ -1075,16 +1156,30 @@ export async function* readSSEStream(
1075
1156
 
1076
1157
  try {
1077
1158
  while (true) {
1078
- const { done, value } = await readChunkWithProgressTimeout(
1079
- reader,
1080
- lastMeaningfulEventAt,
1081
- );
1159
+ let readResult: ReadableStreamReadResult<Uint8Array>;
1160
+ try {
1161
+ readResult = await readChunkWithProgressTimeout(
1162
+ reader,
1163
+ lastMeaningfulEventAt,
1164
+ );
1165
+ } catch (err) {
1166
+ if (err instanceof AgentAutoContinueSignal) {
1167
+ throw new AgentAutoContinueSignal({
1168
+ reason: err.reason,
1169
+ maxIterations: err.maxIterations,
1170
+ activityTrail: [...activityTrail],
1171
+ errorInfo: err.errorInfo,
1172
+ });
1173
+ }
1174
+ throw err;
1175
+ }
1176
+ const { done, value } = readResult;
1082
1177
  if (done) break;
1083
1178
 
1084
1179
  buf += decoder.decode(value, { stream: true });
1085
1180
  const lines = buf.split("\n");
1086
1181
  buf = lines.pop() ?? "";
1087
- let sawDataEvent = false;
1182
+ let sawProgressEvent = false;
1088
1183
 
1089
1184
  for (const line of lines) {
1090
1185
  if (!line.startsWith("data: ")) continue;
@@ -1097,9 +1192,11 @@ export async function* readSSEStream(
1097
1192
  } catch {
1098
1193
  continue;
1099
1194
  }
1100
- sawDataEvent = true;
1101
1195
  const now = Date.now();
1102
- lastMeaningfulEventAt = now;
1196
+ if (isProgressEvent(ev)) {
1197
+ sawProgressEvent = true;
1198
+ lastMeaningfulEventAt = now;
1199
+ }
1103
1200
  updatePreparingActionState(preparingActionState, ev, now);
1104
1201
 
1105
1202
  // Track sequence number for reconnection
@@ -1162,10 +1259,13 @@ export async function* readSSEStream(
1162
1259
  }
1163
1260
 
1164
1261
  if (
1165
- !sawDataEvent &&
1262
+ !sawProgressEvent &&
1166
1263
  Date.now() - lastMeaningfulEventAt >= SSE_NO_PROGRESS_TIMEOUT_MS
1167
1264
  ) {
1168
- throw new AgentAutoContinueSignal({ reason: "no_progress" });
1265
+ throw new AgentAutoContinueSignal({
1266
+ reason: "no_progress",
1267
+ activityTrail: [...activityTrail],
1268
+ });
1169
1269
  }
1170
1270
  }
1171
1271
  } finally {
@@ -1218,18 +1318,31 @@ export async function readSSEStreamRaw(
1218
1318
 
1219
1319
  try {
1220
1320
  while (true) {
1221
- const { done, value } = await readChunkWithProgressTimeout(
1222
- reader,
1223
- lastMeaningfulEventAt,
1224
- );
1321
+ let readResult: ReadableStreamReadResult<Uint8Array>;
1322
+ try {
1323
+ readResult = await readChunkWithProgressTimeout(
1324
+ reader,
1325
+ lastMeaningfulEventAt,
1326
+ );
1327
+ } catch (err) {
1328
+ if (err instanceof AgentAutoContinueSignal) {
1329
+ throw new AgentAutoContinueSignal({
1330
+ reason: err.reason,
1331
+ maxIterations: err.maxIterations,
1332
+ activityTrail: [...activityTrail],
1333
+ errorInfo: err.errorInfo,
1334
+ });
1335
+ }
1336
+ throw err;
1337
+ }
1338
+ const { done, value } = readResult;
1225
1339
  if (done) break;
1226
1340
 
1227
1341
  buf += decoder.decode(value, { stream: true });
1228
1342
  const lines = buf.split("\n");
1229
1343
  buf = lines.pop() ?? "";
1230
1344
 
1231
- let updated = false;
1232
- let sawDataEvent = false;
1345
+ let sawProgressEvent = false;
1233
1346
  for (const line of lines) {
1234
1347
  if (!line.startsWith("data: ")) continue;
1235
1348
  const raw = line.slice(6).trim();
@@ -1241,9 +1354,11 @@ export async function readSSEStreamRaw(
1241
1354
  } catch {
1242
1355
  continue;
1243
1356
  }
1244
- sawDataEvent = true;
1245
1357
  const now = Date.now();
1246
- lastMeaningfulEventAt = now;
1358
+ if (isProgressEvent(ev)) {
1359
+ sawProgressEvent = true;
1360
+ lastMeaningfulEventAt = now;
1361
+ }
1247
1362
  updatePreparingActionState(preparingActionState, ev, now);
1248
1363
 
1249
1364
  if (ev.seq !== undefined && onSeq) {
@@ -1287,10 +1402,12 @@ export async function readSSEStreamRaw(
1287
1402
  action === "error" ||
1288
1403
  action === "missing_api_key"
1289
1404
  ) {
1290
- updated = true;
1405
+ onUpdate(contentSnapshot(content));
1406
+ emittedLatestContent = true;
1291
1407
  }
1292
1408
  if (action === "auto_continue") {
1293
- onUpdate([...content]);
1409
+ onUpdate(contentSnapshot(content));
1410
+ emittedLatestContent = true;
1294
1411
  throw new AgentAutoContinueSignal(
1295
1412
  autoContinue
1296
1413
  ? { ...autoContinue, activityTrail: [...activityTrail] }
@@ -1298,7 +1415,7 @@ export async function readSSEStreamRaw(
1298
1415
  );
1299
1416
  }
1300
1417
  if (hasStalledPreparingAction(preparingActionState, Date.now())) {
1301
- onUpdate([...content]);
1418
+ onUpdate(contentSnapshot(content));
1302
1419
  throw new AgentAutoContinueSignal({
1303
1420
  reason: "no_progress",
1304
1421
  activityTrail: [...activityTrail],
@@ -1309,20 +1426,18 @@ export async function readSSEStreamRaw(
1309
1426
  action === "error" ||
1310
1427
  action === "missing_api_key"
1311
1428
  ) {
1312
- onUpdate([...content]);
1313
1429
  return;
1314
1430
  }
1315
1431
  }
1316
1432
 
1317
- if (updated) {
1318
- onUpdate([...content]);
1319
- emittedLatestContent = true;
1320
- }
1321
1433
  if (
1322
- !sawDataEvent &&
1434
+ !sawProgressEvent &&
1323
1435
  Date.now() - lastMeaningfulEventAt >= SSE_NO_PROGRESS_TIMEOUT_MS
1324
1436
  ) {
1325
- throw new AgentAutoContinueSignal({ reason: "no_progress" });
1437
+ throw new AgentAutoContinueSignal({
1438
+ reason: "no_progress",
1439
+ activityTrail: [...activityTrail],
1440
+ });
1326
1441
  }
1327
1442
  }
1328
1443
  } finally {
@@ -1332,6 +1447,8 @@ export async function readSSEStreamRaw(
1332
1447
  // See readSSEStream: cancellation may race lock release in browsers.
1333
1448
  }
1334
1449
  }
1335
- if (content.length > 0 && !emittedLatestContent) onUpdate([...content]);
1450
+ if (content.length > 0 && !emittedLatestContent) {
1451
+ onUpdate(contentSnapshot(content));
1452
+ }
1336
1453
  throw new AgentAutoContinueSignal({ reason: "stream_ended" });
1337
1454
  }
@@ -8,6 +8,7 @@ export type {
8
8
 
9
9
  export {
10
10
  notify,
11
+ notifyWithDelivery,
11
12
  registerNotificationChannel,
12
13
  unregisterNotificationChannel,
13
14
  listNotificationChannels,
@@ -13,6 +13,11 @@ import {
13
13
  type Notification,
14
14
  } from "./types.js";
15
15
 
16
+ export interface NotificationDeliveryResult {
17
+ notification?: Notification;
18
+ deliveredChannels: string[];
19
+ }
20
+
16
21
  registerEvent({
17
22
  name: "notification.sent",
18
23
  description:
@@ -84,6 +89,13 @@ export async function notify(
84
89
  input: NotificationInput,
85
90
  meta: NotificationMeta,
86
91
  ): Promise<Notification | undefined> {
92
+ return (await notifyWithDelivery(input, meta)).notification;
93
+ }
94
+
95
+ export async function notifyWithDelivery(
96
+ input: NotificationInput,
97
+ meta: NotificationMeta,
98
+ ): Promise<NotificationDeliveryResult> {
87
99
  if (!meta?.owner) {
88
100
  throw new Error("notify: meta.owner is required");
89
101
  }
@@ -166,7 +178,7 @@ export async function notify(
166
178
  }
167
179
  }
168
180
 
169
- return stored;
181
+ return { notification: stored, deliveredChannels: delivered };
170
182
  }
171
183
 
172
184
  function selectChannels(allowlist?: string[]): NotificationChannel[] {
@@ -53,6 +53,8 @@ Default to Apple/Linear-level restraint: make the primary workflow obvious, then
53
53
  - **Visual assets**: Websites, games, and object-focused pages need real or generated media when images help users understand the subject.
54
54
  - **Responsive fit**: Text must not overflow buttons, cards, tabs, sidebars, or fixed-format tools. Use stable dimensions for boards, grids, toolbars, and counters.
55
55
 
56
+ **Beat convergence, not just defaults.** You sample toward the "on-distribution" center, so naming what to avoid is not enough: every "don't" needs a "do", or you converge on the next safe option (ban Inter and you reach for Roboto; ban purple gradients and you reach for Space Grotesk + a teal accent on every screen). Commit to one named direction, pair any reference with the reason it fits ("Linear: the quiet confidence of its spacing" — a bare "Linear" collapses back to the average), and match implementation effort to the vision: maximalist wants elaborate motion and effects, minimal wants restraint and precise spacing. When building on an existing app, inspect its tokens/type/components first and treat any drift back to a default as a missing token to pin, not something to re-prompt.
57
+
56
58
  ## Agent-Native UI Rules
57
59
 
58
60
  - Agent-native apps use React, Vite, Tailwind CSS, shadcn/ui, and `@tabler/icons-react`.
@@ -53,6 +53,8 @@ Default to Apple/Linear-level restraint: make the primary workflow obvious, then
53
53
  - **Visual assets**: Websites, games, and object-focused pages need real or generated media when images help users understand the subject.
54
54
  - **Responsive fit**: Text must not overflow buttons, cards, tabs, sidebars, or fixed-format tools. Use stable dimensions for boards, grids, toolbars, and counters.
55
55
 
56
+ **Beat convergence, not just defaults.** You sample toward the "on-distribution" center, so naming what to avoid is not enough: every "don't" needs a "do", or you converge on the next safe option (ban Inter and you reach for Roboto; ban purple gradients and you reach for Space Grotesk + a teal accent on every screen). Commit to one named direction, pair any reference with the reason it fits ("Linear: the quiet confidence of its spacing" — a bare "Linear" collapses back to the average), and match implementation effort to the vision: maximalist wants elaborate motion and effects, minimal wants restraint and precise spacing. When building on an existing app, inspect its tokens/type/components first and treat any drift back to a default as a missing token to pin, not something to re-prompt.
57
+
56
58
  ## Agent-Native UI Rules
57
59
 
58
60
  - Agent-native apps use React, Vite, Tailwind CSS, shadcn/ui, and `@tabler/icons-react`.
@@ -53,6 +53,8 @@ Default to Apple/Linear-level restraint: make the primary workflow obvious, then
53
53
  - **Visual assets**: Websites, games, and object-focused pages need real or generated media when images help users understand the subject.
54
54
  - **Responsive fit**: Text must not overflow buttons, cards, tabs, sidebars, or fixed-format tools. Use stable dimensions for boards, grids, toolbars, and counters.
55
55
 
56
+ **Beat convergence, not just defaults.** You sample toward the "on-distribution" center, so naming what to avoid is not enough: every "don't" needs a "do", or you converge on the next safe option (ban Inter and you reach for Roboto; ban purple gradients and you reach for Space Grotesk + a teal accent on every screen). Commit to one named direction, pair any reference with the reason it fits ("Linear: the quiet confidence of its spacing" — a bare "Linear" collapses back to the average), and match implementation effort to the vision: maximalist wants elaborate motion and effects, minimal wants restraint and precise spacing. When building on an existing app, inspect its tokens/type/components first and treat any drift back to a default as a missing token to pin, not something to re-prompt.
57
+
56
58
  ## Agent-Native UI Rules
57
59
 
58
60
  - Agent-native apps use React, Vite, Tailwind CSS, shadcn/ui, and `@tabler/icons-react`.
@@ -466,6 +466,21 @@ export function SqlChartCard({
466
466
  <TooltipContent>{t("sqlDashboard.refreshing")}</TooltipContent>
467
467
  </Tooltip>
468
468
  ) : null}
469
+ {editable && onSaveSql ? (
470
+ <ViewSqlPopover
471
+ panel={panel}
472
+ resolvedSql={resolvedSql}
473
+ onSaveSql={onSaveSql}
474
+ >
475
+ <button
476
+ className="p-1 rounded text-muted-foreground hover:text-foreground"
477
+ aria-label={t("sqlDashboard.viewSql")}
478
+ title={t("sqlDashboard.viewSql")}
479
+ >
480
+ <IconCode className="h-3.5 w-3.5" />
481
+ </button>
482
+ </ViewSqlPopover>
483
+ ) : null}
469
484
  {showPanelMenu ? (
470
485
  <DropdownMenu>
471
486
  <Tooltip>
@@ -503,19 +518,6 @@ export function SqlChartCard({
503
518
  {editable && panel.chartType === "table" ? (
504
519
  <DropdownMenuSeparator />
505
520
  ) : null}
506
- {editable && onSaveSql ? (
507
- <ViewSqlPopover
508
- panel={panel}
509
- resolvedSql={resolvedSql}
510
- onSaveSql={onSaveSql}
511
- >
512
- <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
513
- <IconCode className="h-4 w-4 mr-2" />
514
- {t("sqlDashboard.viewSql")}
515
- </DropdownMenuItem>
516
- </ViewSqlPopover>
517
- ) : null}
518
- {editable ? <DropdownMenuSeparator /> : null}
519
521
  {editable && onEdit && (
520
522
  <DropdownMenuItem onSelect={() => onEdit()}>
521
523
  <IconPencil className="h-4 w-4 mr-2" />
@@ -135,7 +135,24 @@ function siteOrigin(request) {
135
135
  return url.origin;
136
136
  }
137
137
 
138
+ async function readScheduledInvocation(request) {
139
+ if (request.method !== "POST") return null;
140
+ let body;
141
+ try {
142
+ body = await request.json();
143
+ } catch {
144
+ return null;
145
+ }
146
+ const nextRun = typeof body?.next_run === "string" ? body.next_run : "";
147
+ return nextRun && Number.isFinite(Date.parse(nextRun))
148
+ ? { nextRun }
149
+ : null;
150
+ }
151
+
138
152
  export default async function handler(request) {
153
+ const scheduled = await readScheduledInvocation(request);
154
+ if (!scheduled) return new Response("Not Found", { status: 404 });
155
+
139
156
  const url = new URL(WORKER_PATH, siteOrigin(request));
140
157
  const response = await fetch(url.toString(), {
141
158
  method: "POST",
@@ -143,7 +160,7 @@ export default async function handler(request) {
143
160
  "content-type": "application/json",
144
161
  "x-agent-native-analytics-alert-cron": CRON_TOKEN,
145
162
  },
146
- body: JSON.stringify({ scheduled: true }),
163
+ body: JSON.stringify({ scheduled: true, next_run: scheduled.nextRun }),
147
164
  });
148
165
 
149
166
  if (!response.ok && response.status !== 202) {
@@ -207,7 +207,7 @@ export const analyticsAlertRules = table("analytics_alert_rules", {
207
207
  lastEvaluatedAt: text("last_evaluated_at"),
208
208
  lastTriggeredAt: text("last_triggered_at"),
209
209
  lastStatus: text("last_status", {
210
- enum: ["ok", "triggered", "cooldown", "error"],
210
+ enum: ["ok", "triggered", "cooldown", "error", "running"],
211
211
  }),
212
212
  lastError: text("last_error"),
213
213
  createdAt: text("created_at").notNull().default(now()),
@@ -1,4 +1,5 @@
1
1
  import {
2
+ claimAnalyticsAlertRuleEvaluation,
2
3
  evaluateAndNotifyAnalyticsAlertRule,
3
4
  listEnabledAnalyticsAlertRules,
4
5
  markAnalyticsAlertRuleError,
@@ -50,8 +51,10 @@ export async function runAnalyticsAlertsOnce(
50
51
  remaining = rules.length >= sweepLimit ? 1 : 0;
51
52
 
52
53
  for (const rule of rules) {
53
- processed++;
54
54
  try {
55
+ const claimed = await claimAnalyticsAlertRuleEvaluation(rule);
56
+ if (!claimed) continue;
57
+ processed++;
55
58
  const result = await evaluateAndNotifyAnalyticsAlertRule(rule);
56
59
  if (result.status === "triggered") triggered++;
57
60
  } catch (err) {