@agent-native/core 0.80.9 → 0.80.11

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 (148) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +108 -15
  5. package/corpus/core/src/cli/design-connect.ts +28 -2
  6. package/corpus/core/src/cli/skills.ts +45 -24
  7. package/corpus/core/src/client/AssistantChat.tsx +123 -26
  8. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  9. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
  10. package/corpus/templates/assets/AGENTS.md +4 -0
  11. package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
  12. package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
  13. package/corpus/templates/assets/actions/generate-image.ts +118 -67
  14. package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
  15. package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
  16. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
  17. package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
  18. package/corpus/templates/assets/app/i18n-data.ts +61 -0
  19. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  20. package/corpus/templates/assets/app/routes/library.tsx +145 -38
  21. package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
  22. package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
  23. package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
  24. package/corpus/templates/assets/server/lib/generation.ts +135 -11
  25. package/corpus/templates/assets/shared/api.ts +4 -0
  26. package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
  27. package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
  28. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
  29. package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
  30. package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
  31. package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
  32. package/corpus/templates/design/actions/apply-component-prop-edit.ts +441 -0
  33. package/corpus/templates/design/actions/apply-design-state.ts +213 -0
  34. package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
  35. package/corpus/templates/design/actions/apply-motion-edit.ts +413 -0
  36. package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
  37. package/corpus/templates/design/actions/apply-visual-edit.ts +191 -5
  38. package/corpus/templates/design/actions/capture-design-state.ts +224 -0
  39. package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
  40. package/corpus/templates/design/actions/create-component.ts +447 -0
  41. package/corpus/templates/design/actions/create-design-branch.ts +263 -0
  42. package/corpus/templates/design/actions/create-design-state.ts +162 -0
  43. package/corpus/templates/design/actions/delete-design-state.ts +55 -0
  44. package/corpus/templates/design/actions/delete-design.ts +20 -0
  45. package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
  46. package/corpus/templates/design/actions/get-component-details.ts +251 -0
  47. package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
  48. package/corpus/templates/design/actions/get-design-review.ts +314 -0
  49. package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
  50. package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
  51. package/corpus/templates/design/actions/index-components.ts +258 -0
  52. package/corpus/templates/design/actions/index-design-tokens.ts +277 -0
  53. package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
  54. package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
  55. package/corpus/templates/design/actions/list-design-states.ts +90 -0
  56. package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
  57. package/corpus/templates/design/actions/open-component-source.ts +242 -0
  58. package/corpus/templates/design/actions/preview-component-prop-edit.ts +260 -0
  59. package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
  60. package/corpus/templates/design/actions/preview-shader-fill.ts +187 -0
  61. package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
  62. package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
  63. package/corpus/templates/design/actions/run-design-audit.ts +436 -0
  64. package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
  65. package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
  66. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1550 -82
  67. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +649 -43
  68. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
  69. package/corpus/templates/design/app/components/design/EditPanel.tsx +1788 -52
  70. package/corpus/templates/design/app/components/design/LayersPanel.tsx +114 -33
  71. package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
  72. package/corpus/templates/design/app/components/design/MotionDock.tsx +1071 -0
  73. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1053 -99
  74. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +766 -0
  75. package/corpus/templates/design/app/components/design/StatesPanel.tsx +581 -0
  76. package/corpus/templates/design/app/components/design/TokensPanel.tsx +578 -0
  77. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
  78. package/corpus/templates/design/app/components/design/index.ts +16 -0
  79. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
  80. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +66 -3
  81. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
  82. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
  83. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
  84. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
  85. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +3 -1
  86. package/corpus/templates/design/app/i18n/zh-TW.ts +32 -0
  87. package/corpus/templates/design/app/i18n-data.ts +280 -0
  88. package/corpus/templates/design/app/pages/DesignEditor.tsx +3087 -490
  89. package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
  90. package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
  91. package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
  92. package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
  93. package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
  94. package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
  95. package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
  96. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
  97. package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
  98. package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
  99. package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
  100. package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
  101. package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
  102. package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
  103. package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
  104. package/corpus/templates/design/e2e/global-setup.ts +94 -2
  105. package/corpus/templates/design/e2e/helpers.ts +10 -4
  106. package/corpus/templates/design/server/db/schema.ts +123 -0
  107. package/corpus/templates/design/server/plugins/db.ts +90 -0
  108. package/corpus/templates/design/shared/builder-app.ts +297 -0
  109. package/corpus/templates/design/shared/capability-resolver.ts +123 -0
  110. package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
  111. package/corpus/templates/design/shared/code-layer.ts +1016 -71
  112. package/corpus/templates/design/shared/component-model.ts +274 -0
  113. package/corpus/templates/design/shared/design-review.ts +275 -0
  114. package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
  115. package/corpus/templates/design/shared/design-state.ts +112 -0
  116. package/corpus/templates/design/shared/design-surface-index.ts +258 -0
  117. package/corpus/templates/design/shared/motion-compiler.ts +349 -0
  118. package/corpus/templates/design/shared/motion-timeline.ts +193 -0
  119. package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
  120. package/corpus/templates/design/shared/responsive-classes.ts +452 -0
  121. package/corpus/templates/design/shared/shader-fill.ts +323 -0
  122. package/corpus/templates/design/shared/source-mode.ts +245 -0
  123. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
  124. package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
  125. package/dist/agent/production-agent.d.ts.map +1 -1
  126. package/dist/agent/production-agent.js +94 -10
  127. package/dist/agent/production-agent.js.map +1 -1
  128. package/dist/cli/design-connect.d.ts.map +1 -1
  129. package/dist/cli/design-connect.js +28 -2
  130. package/dist/cli/design-connect.js.map +1 -1
  131. package/dist/cli/skills.d.ts.map +1 -1
  132. package/dist/cli/skills.js +37 -21
  133. package/dist/cli/skills.js.map +1 -1
  134. package/dist/client/AssistantChat.d.ts +14 -0
  135. package/dist/client/AssistantChat.d.ts.map +1 -1
  136. package/dist/client/AssistantChat.js +115 -22
  137. package/dist/client/AssistantChat.js.map +1 -1
  138. package/dist/client/sse-event-processor.d.ts.map +1 -1
  139. package/dist/client/sse-event-processor.js +10 -0
  140. package/dist/client/sse-event-processor.js.map +1 -1
  141. package/dist/collab/routes.d.ts +1 -1
  142. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  143. package/dist/notifications/routes.d.ts +2 -2
  144. package/dist/observability/routes.d.ts +8 -8
  145. package/dist/resources/handlers.d.ts +2 -2
  146. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  147. package/dist/server/transcribe-voice.d.ts +1 -1
  148. package/package.json +1 -1
@@ -11,6 +11,7 @@ import type {
11
11
  GenerationIntent,
12
12
  ImageCategory,
13
13
  ImageModel,
14
+ ImageQualityTier,
14
15
  ImageSize,
15
16
  StyleStrength,
16
17
  StyleBrief,
@@ -564,7 +565,8 @@ export async function analyzeStyleWithGemini(input: {
564
565
  {
565
566
  text: [
566
567
  "Analyze these brand/style reference images for a reusable image generation style brief.",
567
- "Return only compact JSON with keys: description, medium, mood, subjectMatter, texture, composition, lighting, typographyPolicy, doNot.",
568
+ "Return only compact JSON with keys: description, medium, mood, subjectMatter, texture, composition, lighting, fontFamilies, fontWeights, letterforms, caseStyle, typographyPolicy, doNot.",
569
+ "For typography, describe specific letterforms and font traits, not just broad sans/serif categories; omit any typography field you cannot determine confidently.",
568
570
  "Use specific trait-locking phrases that can be reused across future image prompts.",
569
571
  "Do not name brands, artists, studios, franchises, or copyrighted works unless they appear as user-provided brand identity.",
570
572
  "Keep doNot as an array of short constraints. Omit uncertain fields instead of guessing.",
@@ -608,16 +610,24 @@ function extractJsonObject(text: string): string {
608
610
  return "{}";
609
611
  }
610
612
 
611
- function sanitizeStyleBrief(value: Record<string, unknown>): StyleBrief {
613
+ export function sanitizeStyleBrief(value: Record<string, unknown>): StyleBrief {
612
614
  const stringField = (key: string) => {
613
615
  const raw = value[key];
614
616
  return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
615
617
  };
616
- const doNot = Array.isArray(value.doNot)
617
- ? value.doNot.filter(
618
- (item): item is string => typeof item === "string" && !!item.trim(),
619
- )
620
- : undefined;
618
+ const stringArrayField = (key: string) => {
619
+ const raw = value[key];
620
+ return Array.isArray(raw)
621
+ ? raw
622
+ .filter(
623
+ (item): item is string => typeof item === "string" && !!item.trim(),
624
+ )
625
+ .map((item) => item.trim())
626
+ : undefined;
627
+ };
628
+ const fontFamilies = stringArrayField("fontFamilies");
629
+ const fontWeights = stringArrayField("fontWeights");
630
+ const doNot = stringArrayField("doNot");
621
631
  return {
622
632
  description: stringField("description"),
623
633
  medium: stringField("medium"),
@@ -626,8 +636,12 @@ function sanitizeStyleBrief(value: Record<string, unknown>): StyleBrief {
626
636
  texture: stringField("texture"),
627
637
  composition: stringField("composition"),
628
638
  lighting: stringField("lighting"),
639
+ fontFamilies: fontFamilies?.length ? fontFamilies : undefined,
640
+ fontWeights: fontWeights?.length ? fontWeights : undefined,
641
+ letterforms: stringField("letterforms"),
642
+ caseStyle: stringField("caseStyle"),
629
643
  typographyPolicy: stringField("typographyPolicy"),
630
- doNot: doNot?.length ? doNot.map((item) => item.trim()) : undefined,
644
+ doNot: doNot?.length ? doNot : undefined,
631
645
  };
632
646
  }
633
647
 
@@ -774,6 +788,66 @@ function toBuilderImageModel(model: ImageModel) {
774
788
  return model;
775
789
  }
776
790
 
791
+ function resolveModelForTier(
792
+ tier: ImageQualityTier | undefined,
793
+ category: ImageCategory | undefined,
794
+ ): ImageModel | undefined {
795
+ if (!tier) return undefined;
796
+ if (tier === "fast") return "gemini-3.1-flash-image";
797
+ if (tier === "best") return "gemini-3-pro-image";
798
+ return ["hero", "landing", "logo", "campaign"].includes(category ?? "")
799
+ ? "gemini-3-pro-image"
800
+ : "gemini-3.1-flash-image";
801
+ }
802
+
803
+ export function resolveImageModelForRequest(input: {
804
+ explicitModel?: ImageModel;
805
+ imageModelDefault?: ImageModel;
806
+ explicitTier?: ImageQualityTier;
807
+ resolvedTier?: ImageQualityTier;
808
+ category?: ImageCategory;
809
+ presetModel?: ImageModel | null;
810
+ embeddedText?: string | null;
811
+ }): ImageModel {
812
+ if (input.explicitModel) return input.explicitModel;
813
+
814
+ // Exact embedded text is materially better on Gemini Pro, so upgrade to it by
815
+ // default for text requests — but never override a model or tier the caller
816
+ // or a tagged preset already chose (presets "own" model/tier per the action
817
+ // docs). Only auto-upgrade when there is no tier (explicit or preset-derived)
818
+ // and no preset model in play; this still upgrades a bare text request over
819
+ // the composer default.
820
+ const textAccurateModel: ImageModel | undefined =
821
+ input.embeddedText?.trim() &&
822
+ !input.explicitTier &&
823
+ !input.resolvedTier &&
824
+ !input.presetModel
825
+ ? "gemini-3-pro-image"
826
+ : undefined;
827
+
828
+ // Precedence: explicit per-request model (handled above) > embedded-text
829
+ // upgrade > an EXPLICIT per-request tier > the preset's saved model > the
830
+ // preset-DERIVED tier mapping > the sticky composer default > the floor.
831
+ //
832
+ // Two subtleties:
833
+ // - An explicit per-request tier outranks the preset's saved model (the
834
+ // caller is deliberately overriding the preset this turn).
835
+ // - The preset's explicit saved model outranks its OWN derived tier: a
836
+ // preset's `model` column and `settings.tier` can drift out of sync
837
+ // (update-generation-preset can change one without the other), and the
838
+ // explicitly saved model is the authoritative choice.
839
+ // - The composer default ranks LAST of the real signals: it is a global
840
+ // stored preference and must not defeat a tagged preset or a tier request.
841
+ return (
842
+ textAccurateModel ??
843
+ resolveModelForTier(input.explicitTier, input.category) ??
844
+ input.presetModel ??
845
+ resolveModelForTier(input.resolvedTier, input.category) ??
846
+ input.imageModelDefault ??
847
+ "gemini-3.1-flash-image"
848
+ );
849
+ }
850
+
777
851
  function toBuilderReferenceRole(role: string) {
778
852
  switch (role) {
779
853
  case "style_reference":
@@ -799,6 +873,8 @@ export function compilePrompt(input: {
799
873
  styleBrief: StyleBrief;
800
874
  customInstructions?: string | null;
801
875
  prompt: string;
876
+ embeddedText?: string | null;
877
+ textPlacement?: string | null;
802
878
  referenceCount: number;
803
879
  includeLogo: boolean;
804
880
  aspectRatio?: AspectRatio;
@@ -815,6 +891,11 @@ export function compilePrompt(input: {
815
891
  const doNot = style.doNot?.length
816
892
  ? `\nAvoid: ${style.doNot.join("; ")}.`
817
893
  : "";
894
+ const typographyBlock = formatBrandTypography(style);
895
+ const requestedEmbeddedText = hasEmbeddedText(input.embeddedText);
896
+ const textInstruction = requestedEmbeddedText
897
+ ? formatEmbeddedTextInstruction(input.embeddedText, input.textPlacement)
898
+ : "Do not render headlines, body text, UI labels, or prompt wording inside the image unless the user explicitly asks for exact visible text.";
818
899
  const logoInstruction = input.includeLogo
819
900
  ? "\nLeave a clean uncluttered area in the upper-right for the real brand logo; do not draw or approximate the logo yourself."
820
901
  : "";
@@ -839,14 +920,24 @@ export function compilePrompt(input: {
839
920
  : "No reference images are attached for this run. Use the style brief and custom instructions as the source of truth.";
840
921
 
841
922
  if (intent === "edit") {
923
+ const editTypographyInstruction =
924
+ requestedEmbeddedText && typographyBlock ? `\n\n${typographyBlock}` : "";
925
+ const editTextInstruction = requestedEmbeddedText
926
+ ? `\n\n${formatEmbeddedTextInstruction(input.embeddedText, input.textPlacement)}`
927
+ : "";
928
+ const editConstraint = requestedEmbeddedText
929
+ ? "Do not reimagine the image, change its aspect ratio, or alter unrelated subjects. Do not add any other new text. Return the full image."
930
+ : "Do not reimagine the image, change its aspect ratio, add new text, or alter unrelated subjects. Return the full image.";
842
931
  return `Edit the attached image for the "${input.libraryTitle}" asset library.
843
932
 
844
933
  ${referenceInstruction}
934
+ ${editTypographyInstruction}
845
935
 
846
936
  Make only this change:
847
937
  ${input.prompt}
938
+ ${editTextInstruction}
848
939
 
849
- Do not reimagine the image, change its aspect ratio, add new text, or alter unrelated subjects. Return the full image.`;
940
+ ${editConstraint}`;
850
941
  }
851
942
 
852
943
  return `Create a brand-consistent image for the "${input.libraryTitle}" asset library.
@@ -861,15 +952,48 @@ ${style.subjectMatter ? `\nSubject matter: ${style.subjectMatter}.` : ""}
861
952
  ${style.texture ? `\nTexture/material treatment: ${style.texture}.` : ""}
862
953
  ${style.composition ? `\nComposition: ${style.composition}.` : ""}
863
954
  ${style.lighting ? `\nLighting: ${style.lighting}.` : ""}
864
- ${style.typographyPolicy ? `\nTypography policy: ${style.typographyPolicy}.` : ""}${frameInstruction}
955
+ ${typographyBlock ? `\n${typographyBlock}` : ""}${frameInstruction}
865
956
  ${doNot}${logoInstruction}${diagramInstruction}${customInstructions}
866
957
 
867
- Do not render headlines, body text, UI labels, or prompt wording inside the image unless the user explicitly asks for exact visible text.
958
+ ${textInstruction}
868
959
 
869
960
  User request:
870
961
  ${input.prompt}`;
871
962
  }
872
963
 
964
+ function hasEmbeddedText(value?: string | null): boolean {
965
+ return typeof value === "string" && !!value.trim();
966
+ }
967
+
968
+ function formatEmbeddedTextInstruction(
969
+ embeddedText?: string | null,
970
+ textPlacement?: string | null,
971
+ ): string {
972
+ const placement = textPlacement?.trim();
973
+ return [
974
+ `Render this text exactly, spelled correctly, inside the image: ${JSON.stringify(embeddedText ?? "")}.`,
975
+ placement ? `Placement: ${placement}.` : "",
976
+ "Match the brand typography described above where specified; keep it legible and well-kerned.",
977
+ ]
978
+ .filter(Boolean)
979
+ .join(" ");
980
+ }
981
+
982
+ function formatBrandTypography(style: StyleBrief): string {
983
+ const parts = [
984
+ style.fontFamilies?.length
985
+ ? `families ${style.fontFamilies.join(", ")}`
986
+ : "",
987
+ style.fontWeights?.length ? `weights ${style.fontWeights.join(", ")}` : "",
988
+ style.letterforms ? `letterforms: ${style.letterforms}` : "",
989
+ style.caseStyle ? `case: ${style.caseStyle}` : "",
990
+ style.typographyPolicy,
991
+ ].filter((part): part is string => typeof part === "string" && !!part.trim());
992
+
993
+ if (!parts.length) return "";
994
+ return `Brand typography: ${parts.join("; ")}. Match these for any rendered text.`;
995
+ }
996
+
873
997
  // A content-only reference is an image the user attached as subject/content for
874
998
  // a single request (not a reusable brand/style reference). It should never count
875
999
  // as a brand-kit style reference.
@@ -112,6 +112,10 @@ export interface StyleBrief {
112
112
  texture?: string;
113
113
  composition?: string;
114
114
  lighting?: string;
115
+ fontFamilies?: string[];
116
+ fontWeights?: string[];
117
+ letterforms?: string;
118
+ caseStyle?: string;
115
119
  typographyPolicy?: string;
116
120
  doNot?: string[];
117
121
  }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * List pending clips AI requests for the current user.
3
+ *
4
+ * Collapses the per-recording `clips-ai-request-<id>` polling done by the
5
+ * auto-title bridge into a single call. Application state is already
6
+ * session-scoped, and we additionally filter the referenced recordings through
7
+ * `accessFilter` so we never return a request for a recording the user can't
8
+ * access. Only requests for `status = "ready"` recordings are returned — the
9
+ * bridge ignores non-ready recordings, so excluding them avoids sending large
10
+ * transcript blobs for recordings the hook will skip this tick.
11
+ */
12
+
13
+ import { defineAction } from "@agent-native/core";
14
+ import { listAppState } from "@agent-native/core/application-state";
15
+ import { accessFilter } from "@agent-native/core/sharing";
16
+ import { and, eq, inArray } from "drizzle-orm";
17
+ import { z } from "zod";
18
+
19
+ import { getDb, schema } from "../server/db/index.js";
20
+
21
+ const REQUEST_PREFIX = "clips-ai-request-";
22
+
23
+ export default defineAction({
24
+ description:
25
+ "List pending clips AI requests (regenerate-title, summary, chapters, etc.) for recordings the current user can access.",
26
+ schema: z.object({}),
27
+ http: { method: "GET" },
28
+ run: async () => {
29
+ const entries = await listAppState(REQUEST_PREFIX);
30
+
31
+ const requests = entries
32
+ .map((e) => e.value as Record<string, unknown>)
33
+ .filter(
34
+ (v): v is Record<string, unknown> & { recordingId: string } =>
35
+ !!v && typeof v.recordingId === "string" && v.recordingId.length > 0,
36
+ );
37
+
38
+ if (requests.length === 0) return { requests: [] };
39
+
40
+ const recordingIds = [...new Set(requests.map((r) => r.recordingId))];
41
+
42
+ const db = getDb();
43
+ const accessible = await db
44
+ .select({ id: schema.recordings.id })
45
+ .from(schema.recordings)
46
+ .where(
47
+ and(
48
+ accessFilter(schema.recordings, schema.recordingShares),
49
+ inArray(schema.recordings.id, recordingIds),
50
+ eq(schema.recordings.status, "ready"),
51
+ ),
52
+ );
53
+
54
+ const accessibleIds = new Set(accessible.map((r) => r.id));
55
+
56
+ return {
57
+ requests: requests.filter((r) => accessibleIds.has(r.recordingId)),
58
+ };
59
+ },
60
+ });
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Clips AI request bridge
3
3
  *
4
- * Watches the `clips-ai-request-:id` application_state queue. The bridge sends
5
- * queued recording work to the agent chat exactly once per
4
+ * Polls the `list-ai-requests` action a single access-scoped call that returns
5
+ * every pending `clips-ai-request-*` entry for recordings the user can access.
6
+ * The bridge sends queued recording work to the agent chat exactly once per
6
7
  * (recordingId, kind, requestedAt).
7
8
  *
8
9
  * Once handled we DELETE the request entry so the next page load / tab switch
@@ -21,6 +22,7 @@ import { useRecordings, type RecordingSummary } from "./use-library";
21
22
 
22
23
  const DEFAULT_TITLE = "Untitled recording";
23
24
  const POLL_INTERVAL_MS = 3000;
25
+ const TWO_MINUTES_MS = 2 * 60 * 1000;
24
26
 
25
27
  /** True when `title` is blank or equal to the server-seeded default. */
26
28
  export function isDefaultTitle(title: string | null | undefined): boolean {
@@ -62,22 +64,21 @@ const DISPATCHABLE_REQUESTS = new Set([
62
64
  "generate-workflow",
63
65
  ]);
64
66
 
65
- async function readRequest(recordingId: string): Promise<AiRequest | null> {
66
- const url = agentNativePath(
67
- `/_agent-native/application-state/${encodeURIComponent(
68
- `clips-ai-request-${recordingId}`,
69
- )}`,
70
- );
67
+ async function listRequests(): Promise<Map<string, AiRequest>> {
71
68
  try {
72
- const res = await fetch(url);
73
- if (!res.ok) return null;
74
- const payload = await res.json().catch(() => null);
75
- if (!payload || typeof payload !== "object") return null;
76
- // The application-state endpoint wraps stored values under `.value`.
77
- const value = (payload as any).value ?? payload;
78
- return value as AiRequest;
69
+ const result = (await callAction("list-ai-requests", {} as any, {
70
+ method: "GET",
71
+ })) as { requests?: AiRequest[] } | null | undefined;
72
+ return new Map(
73
+ (result?.requests ?? [])
74
+ .filter(
75
+ (r): r is AiRequest & { recordingId: string } => !!r?.recordingId,
76
+ )
77
+ .map((r) => [r.recordingId, r]),
78
+ );
79
79
  } catch {
80
- return null;
80
+ // Swallow — the next tick retries.
81
+ return new Map();
81
82
  }
82
83
  }
83
84
 
@@ -120,16 +121,15 @@ export function useAutoTitleBridge(): void {
120
121
  if (cancelled || inflight.current) return;
121
122
  inflight.current = true;
122
123
  try {
124
+ const requestsById = await listRequests();
125
+ if (cancelled) return;
126
+
123
127
  for (const rec of readyRecordings) {
124
128
  if (cancelled) return;
125
129
 
126
- const request = await readRequest(rec.id);
130
+ const request = requestsById.get(rec.id) ?? null;
127
131
 
128
- if (
129
- request?.kind &&
130
- DISPATCHABLE_REQUESTS.has(request.kind) &&
131
- request.recordingId === rec.id
132
- ) {
132
+ if (request?.kind && DISPATCHABLE_REQUESTS.has(request.kind)) {
133
133
  // Server queued a delegation — use the full context it provided.
134
134
  // Key includes requestedAt so each distinct server request fires
135
135
  // exactly once, independent of any prior fallback dispatch.
@@ -164,9 +164,8 @@ export function useAutoTitleBridge(): void {
164
164
  continue;
165
165
  }
166
166
 
167
- const ageMs = Date.now() - new Date(rec.createdAt).getTime();
168
- const TWO_MINUTES_MS = 2 * 60 * 1000;
169
- if (ageMs < TWO_MINUTES_MS) continue;
167
+ if (Date.now() - new Date(rec.createdAt).getTime() < TWO_MINUTES_MS)
168
+ continue;
170
169
 
171
170
  // Use a dedicated key so a later server-queued request (e.g. from
172
171
  // a long transcription that finishes after the 2-min window) is