@agent-native/core 0.164.15 → 0.164.17

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 (52) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/netlify.toml +4 -0
  3. package/corpus/templates/clips/actions/request-transcript.ts +10 -6
  4. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +298 -3
  5. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +3 -0
  6. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +18 -4
  7. package/corpus/templates/content/app/i18n-data.ts +160 -0
  8. package/corpus/templates/content/docs/solutions/2026-08-18-colored-text-highlighting-shape.md +693 -0
  9. package/corpus/templates/content/package.json +1 -0
  10. package/corpus/templates/content/shared/nfm.ts +14 -3
  11. package/corpus/templates/factory/AGENTS.md +24 -29
  12. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +8 -28
  13. package/corpus/templates/factory/actions/get-triage-config.ts +1 -0
  14. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +67 -59
  15. package/corpus/templates/factory/actions/list-triage-items.ts +6 -1
  16. package/corpus/templates/factory/actions/reconcile-triage-run.ts +7 -1
  17. package/corpus/templates/factory/actions/save-triage-config.ts +27 -7
  18. package/corpus/templates/factory/actions/start-builder-for-item.ts +113 -31
  19. package/corpus/templates/factory/actions/view-screen.ts +12 -0
  20. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +457 -0
  21. package/corpus/templates/factory/app/components/factory/FactoryWorkspaceActions.tsx +53 -0
  22. package/corpus/templates/factory/app/components/layout/Layout.tsx +2 -0
  23. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +3 -1
  24. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +10 -7
  25. package/corpus/templates/factory/app/i18n/en-US.ts +3 -0
  26. package/corpus/templates/factory/app/routes/agents.tsx +22 -1
  27. package/corpus/templates/factory/app/routes/factory-settings.tsx +34 -0
  28. package/corpus/templates/factory/app/routes/factory.tsx +33 -323
  29. package/corpus/templates/factory/server/connectors/slack.ts +62 -0
  30. package/corpus/templates/factory/server/db/schema.ts +1 -0
  31. package/corpus/templates/factory/server/lib/slack-feedback-prompt.ts +30 -0
  32. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -8
  33. package/corpus/templates/factory/server/plugins/factory-migrations.ts +7 -0
  34. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +43 -25
  35. package/corpus/templates/factory/server/triage/ai-services-git.ts +2 -0
  36. package/corpus/templates/factory/server/triage/pr-monitor.ts +2 -1
  37. package/corpus/templates/factory/server/triage/pr-policy.ts +197 -19
  38. package/corpus/templates/factory/server/triage/review-skill-alignment.ts +127 -0
  39. package/corpus/templates/factory/server/triage/slack-client.ts +79 -6
  40. package/dist/client/MultiTabAssistantChat.js +28 -1
  41. package/dist/client/use-chat-threads.d.ts +1 -0
  42. package/dist/client/use-chat-threads.js +13 -3
  43. package/dist/collab/awareness.d.ts +2 -2
  44. package/dist/notifications/routes.d.ts +3 -3
  45. package/dist/observability/routes.d.ts +5 -5
  46. package/dist/progress/routes.d.ts +1 -1
  47. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  48. package/dist/server/auth.js +2 -2
  49. package/dist/server/realtime-token.d.ts +1 -1
  50. package/dist/server/transcribe-voice.d.ts +1 -1
  51. package/dist/vite/client.js +2 -0
  52. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8478
34
+ - template files: 8484
@@ -22,6 +22,10 @@ AGENT_CHAT_DURABLE_BACKGROUND = "true"
22
22
  AGENT_NATIVE_HOSTED_HARNESS = "true"
23
23
  AGENT_NATIVE_ENABLE_KEEP_WARM = "1"
24
24
  AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUND = "1"
25
+ # Keep the public shell close to the current deploy. A stale shell can retain
26
+ # hashed modulepreload URLs after a Netlify asset publish and leave the app on
27
+ # its loading screen while those chunks return 404.
28
+ AGENT_NATIVE_SSR_CACHE = "30s"
25
29
  # Analytics owns its schema at release time (`migrate:production` above), so
26
30
  # production request functions skip migrations. Preview functions still run
27
31
  # request-path migrations because previews do not run the production release
@@ -1526,18 +1526,22 @@ const requestTranscriptAction = defineAction({
1526
1526
  transcribeWithBuilderModelFallback({
1527
1527
  audioBytes: audioMedia.audioBytes,
1528
1528
  mimeType: audioMedia.mimeType,
1529
- diarize: false,
1529
+ diarize: true,
1530
1530
  instructions: SPEECH_ONLY_TRANSCRIPTION_INSTRUCTIONS,
1531
1531
  timeoutMs: builderTranscriptionTimeoutMs(rec.durationMs),
1532
1532
  }),
1533
1533
  );
1534
1534
 
1535
1535
  const segments = (builderResult.segments ?? [])
1536
- .map((s) => ({
1537
- startMs: s.startMs,
1538
- endMs: s.endMs,
1539
- text: s.text.trim(),
1540
- }))
1536
+ .map((s) => {
1537
+ const speaker = s.speakerLabel?.trim();
1538
+ return {
1539
+ startMs: s.startMs,
1540
+ endMs: s.endMs,
1541
+ text: s.text.trim(),
1542
+ ...(speaker ? { speaker } : {}),
1543
+ };
1544
+ })
1541
1545
  .filter((segment) => segment.text);
1542
1546
  const normalizedTranscript = normalizeProviderTranscript(
1543
1547
  builderResult.text,
@@ -60,6 +60,109 @@ type SelectionFillRange = {
60
60
  };
61
61
 
62
62
  type TextStyle = "paragraph" | 1 | 2 | 3 | 4 | 5 | 6;
63
+ type ColorAttribute = "color" | "bgColor";
64
+ type ColorName =
65
+ | "gray"
66
+ | "brown"
67
+ | "orange"
68
+ | "yellow"
69
+ | "green"
70
+ | "blue"
71
+ | "purple"
72
+ | "pink"
73
+ | "red";
74
+
75
+ const COLOR_NAMES: ColorName[] = [
76
+ "gray",
77
+ "brown",
78
+ "orange",
79
+ "yellow",
80
+ "green",
81
+ "blue",
82
+ "purple",
83
+ "pink",
84
+ "red",
85
+ ];
86
+
87
+ export function getSelectionNotionSpanAttribute(
88
+ editor: Editor,
89
+ attribute: ColorAttribute,
90
+ ): string | null | "mixed" {
91
+ const { from, to } = editor.state.selection;
92
+ const markType = editor.state.schema.marks.notionSpan;
93
+ if (!markType) return null;
94
+
95
+ let observed: string | null = null;
96
+ let hasObserved = false;
97
+ let mixed = false;
98
+
99
+ editor.state.doc.nodesBetween(from, to, (node, _position, parent) => {
100
+ if (!node.isText || !parent?.type.allowsMarkType(markType)) return;
101
+ const mark = node.marks.find((candidate) => candidate.type === markType);
102
+ const value = (mark?.attrs[attribute] as string | null | undefined) ?? null;
103
+ if (!hasObserved) {
104
+ observed = value;
105
+ hasObserved = true;
106
+ } else if (observed !== value) {
107
+ mixed = true;
108
+ }
109
+ });
110
+
111
+ return mixed ? "mixed" : observed;
112
+ }
113
+
114
+ export function selectionHasColorableText(
115
+ state: EditorState,
116
+ from: number,
117
+ to: number,
118
+ ) {
119
+ const markType = state.schema.marks.notionSpan;
120
+ if (!markType) return false;
121
+
122
+ let hasText = false;
123
+ state.doc.nodesBetween(from, to, (node, _position, parent) => {
124
+ if (node.isText && parent?.type.allowsMarkType(markType)) hasText = true;
125
+ return !hasText;
126
+ });
127
+ return hasText;
128
+ }
129
+
130
+ export function setSelectionNotionSpanAttribute(
131
+ editor: Editor,
132
+ attribute: ColorAttribute,
133
+ value: string | null,
134
+ ) {
135
+ const { state } = editor;
136
+ const { from, to } = state.selection;
137
+ const markType = state.schema.marks.notionSpan;
138
+ if (!markType || from === to) return false;
139
+
140
+ const transaction = state.tr;
141
+ state.doc.nodesBetween(from, to, (node, position, parent) => {
142
+ if (!node.isText || !parent?.type.allowsMarkType(markType)) return;
143
+ const start = Math.max(from, position);
144
+ const end = Math.min(to, position + node.nodeSize);
145
+ if (start >= end) return;
146
+
147
+ const existing = node.marks.find((mark) => mark.type === markType);
148
+ const attrs = { ...existing?.attrs, [attribute]: value };
149
+ transaction.removeMark(start, end, markType);
150
+ if (
151
+ attrs.color ||
152
+ attrs.bgColor ||
153
+ attrs.underline ||
154
+ attrs.href ||
155
+ (attrs.attrsJson && attrs.attrsJson !== "{}")
156
+ ) {
157
+ transaction.addMark(start, end, markType.create(attrs));
158
+ }
159
+ });
160
+
161
+ if (!transaction.docChanged) return false;
162
+ editor.view.dispatch(transaction);
163
+ editor.commands.focus();
164
+ return true;
165
+ }
63
166
 
64
167
  function activeTextStyle(editor: Editor): TextStyle {
65
168
  for (const level of [1, 2, 3, 4, 5, 6] as const) {
@@ -119,8 +222,16 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
119
222
  const [showLinkInput, setShowLinkInput] = useState(false);
120
223
  const [linkUrl, setLinkUrl] = useState("");
121
224
  const [textStyleOpen, setTextStyleOpen] = useState(false);
225
+ const [colorOpen, setColorOpen] = useState(false);
226
+ const [colorRevision, setColorRevision] = useState(0);
227
+ const [recentColor, setRecentColor] = useState<{
228
+ attribute: ColorAttribute;
229
+ value: string;
230
+ } | null>(null);
122
231
  const textStyleSelection = useRef<{ from: number; to: number } | null>(null);
232
+ const colorSelection = useRef<{ from: number; to: number } | null>(null);
123
233
  const textStyleApplied = useRef(false);
234
+ const colorApplied = useRef(false);
124
235
  const restoreEditorFocusOnClose = useRef(false);
125
236
  const [textStyle, setTextStyle] = useState<TextStyle>(() =>
126
237
  activeTextStyle(editor),
@@ -129,8 +240,12 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
129
240
  useEffect(() => {
130
241
  const syncTextStyle = () => {
131
242
  setTextStyle(activeTextStyle(editor));
243
+ setColorRevision((revision) => revision + 1);
132
244
  const { from, to } = editor.state.selection;
133
- if (from !== to) textStyleSelection.current = { from, to };
245
+ if (from !== to) {
246
+ textStyleSelection.current = { from, to };
247
+ colorSelection.current = { from, to };
248
+ }
134
249
  };
135
250
  editor.on("selectionUpdate", syncTextStyle);
136
251
  editor.on("transaction", syncTextStyle);
@@ -199,6 +314,82 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
199
314
  setTextStyleOpen(false);
200
315
  };
201
316
 
317
+ const applyColor = (attribute: ColorAttribute, value: string | null) => {
318
+ if (colorSelection.current) {
319
+ editor.commands.setTextSelection(colorSelection.current);
320
+ }
321
+ if (!setSelectionNotionSpanAttribute(editor, attribute, value)) return;
322
+ if (value) setRecentColor({ attribute, value });
323
+ colorApplied.current = true;
324
+ setColorOpen(false);
325
+ };
326
+
327
+ const activeTextColor = getSelectionNotionSpanAttribute(editor, "color");
328
+ const activeBackgroundColor = getSelectionNotionSpanAttribute(
329
+ editor,
330
+ "bgColor",
331
+ );
332
+ void colorRevision;
333
+
334
+ const renderColorChoice = (
335
+ attribute: ColorAttribute,
336
+ value: string | null,
337
+ ) => {
338
+ const sectionLabel = t(
339
+ attribute === "color" ? "editor.textColor" : "editor.backgroundColor",
340
+ );
341
+ const colorName = value?.replace(/_bg$/, "") as ColorName | undefined;
342
+ const choiceLabel = colorName
343
+ ? t(`editor.color.${colorName}`)
344
+ : t("editor.defaultColor");
345
+ const activeValue =
346
+ attribute === "color" ? activeTextColor : activeBackgroundColor;
347
+ const isActive = activeValue !== "mixed" && activeValue === value;
348
+
349
+ return (
350
+ <button
351
+ key={`${attribute}-${value ?? "default"}`}
352
+ type="button"
353
+ role="menuitemradio"
354
+ aria-checked={isActive}
355
+ aria-label={`${sectionLabel}: ${choiceLabel}`}
356
+ onPointerDown={(event) => {
357
+ event.preventDefault();
358
+ event.stopPropagation();
359
+ applyColor(attribute, value);
360
+ }}
361
+ onClick={(event) => {
362
+ if (event.detail === 0) applyColor(attribute, value);
363
+ }}
364
+ className={cn(
365
+ "relative flex size-8 items-center justify-center rounded-md border border-border bg-background text-sm font-semibold hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
366
+ isActive && "ring-2 ring-foreground",
367
+ )}
368
+ >
369
+ {attribute === "color" ? (
370
+ <span className={colorName ? `notion-block-color--${colorName}` : ""}>
371
+ A
372
+ </span>
373
+ ) : (
374
+ <span
375
+ className={cn(
376
+ "size-5 rounded border border-border",
377
+ colorName && `notion-block-bg--${colorName}`,
378
+ )}
379
+ />
380
+ )}
381
+ {isActive ? (
382
+ <IconCheck
383
+ aria-hidden="true"
384
+ className="absolute -end-1 -top-1 rounded-full bg-foreground p-0.5 text-background"
385
+ size={12}
386
+ strokeWidth={3}
387
+ />
388
+ ) : null}
389
+ </button>
390
+ );
391
+ };
392
+
202
393
  const openLinkInput = useCallback(() => {
203
394
  setLinkUrl(editor.getAttributes("link").href || "");
204
395
  setShowLinkInput(true);
@@ -318,6 +509,15 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
318
509
  };
319
510
 
320
511
  const items = [
512
+ { type: "text-style" as const },
513
+ ...(selectionHasColorableText(
514
+ editor.state,
515
+ editor.state.selection.from,
516
+ editor.state.selection.to,
517
+ )
518
+ ? [{ type: "color" as const }]
519
+ : []),
520
+ { type: "divider" as const },
321
521
  {
322
522
  icon: IconBold,
323
523
  title: t("editor.bold"),
@@ -343,8 +543,6 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
343
543
  isActive: () => editor.isActive("code"),
344
544
  },
345
545
  { type: "divider" as const },
346
- { type: "text-style" as const },
347
- { type: "divider" as const },
348
546
  {
349
547
  icon: IconLink,
350
548
  title: t("editor.link"),
@@ -529,6 +727,103 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
529
727
  </Popover>
530
728
  );
531
729
  }
730
+ if ("type" in item && item.type === "color") {
731
+ return (
732
+ <Popover
733
+ key="color"
734
+ open={colorOpen}
735
+ onOpenChange={(open) => {
736
+ if (open) {
737
+ colorApplied.current = false;
738
+ restoreEditorFocusOnClose.current = false;
739
+ const { from, to } = editor.state.selection;
740
+ if (from !== to) colorSelection.current = { from, to };
741
+ }
742
+ setColorOpen(open);
743
+ }}
744
+ >
745
+ <PopoverTrigger asChild>
746
+ <button
747
+ type="button"
748
+ aria-label={t("editor.color.label")}
749
+ className={cn(
750
+ "flex size-8 items-center justify-center rounded text-sm font-semibold text-popover-foreground/85 hover:bg-accent hover:text-accent-foreground",
751
+ colorOpen && "bg-accent text-accent-foreground",
752
+ )}
753
+ >
754
+ A
755
+ </button>
756
+ </PopoverTrigger>
757
+ <PopoverContent
758
+ portalled={false}
759
+ align="start"
760
+ sideOffset={24}
761
+ className="w-52 p-2"
762
+ onEscapeKeyDown={() => {
763
+ restoreEditorFocusOnClose.current = true;
764
+ window.setTimeout(() => editor.commands.focus(), 0);
765
+ }}
766
+ onCloseAutoFocus={(event) => {
767
+ if (
768
+ colorApplied.current ||
769
+ restoreEditorFocusOnClose.current
770
+ ) {
771
+ event.preventDefault();
772
+ }
773
+ if (restoreEditorFocusOnClose.current) {
774
+ editor.commands.focus();
775
+ }
776
+ colorApplied.current = false;
777
+ restoreEditorFocusOnClose.current = false;
778
+ }}
779
+ >
780
+ {recentColor ? (
781
+ <div className="mb-2">
782
+ <div className="mb-1 text-xs font-medium text-muted-foreground">
783
+ {t("editor.color.recentlyUsed")}
784
+ </div>
785
+ <div role="menu">
786
+ {renderColorChoice(
787
+ recentColor.attribute,
788
+ recentColor.value,
789
+ )}
790
+ </div>
791
+ </div>
792
+ ) : null}
793
+ <div className="mb-2">
794
+ <div className="mb-1 text-xs font-medium text-muted-foreground">
795
+ {t("editor.textColor")}
796
+ </div>
797
+ <div
798
+ role="menu"
799
+ aria-label={t("editor.textColor")}
800
+ className="grid grid-cols-5 gap-1"
801
+ >
802
+ {renderColorChoice("color", null)}
803
+ {COLOR_NAMES.map((name) =>
804
+ renderColorChoice("color", name),
805
+ )}
806
+ </div>
807
+ </div>
808
+ <div>
809
+ <div className="mb-1 text-xs font-medium text-muted-foreground">
810
+ {t("editor.backgroundColor")}
811
+ </div>
812
+ <div
813
+ role="menu"
814
+ aria-label={t("editor.backgroundColor")}
815
+ className="grid grid-cols-5 gap-1"
816
+ >
817
+ {renderColorChoice("bgColor", null)}
818
+ {COLOR_NAMES.map((name) =>
819
+ renderColorChoice("bgColor", `${name}_bg`),
820
+ )}
821
+ </div>
822
+ </div>
823
+ </PopoverContent>
824
+ </Popover>
825
+ );
826
+ }
532
827
  const {
533
828
  icon: Icon,
534
829
  title,
@@ -76,6 +76,7 @@ import {
76
76
  LocalMdxComponentNode,
77
77
  } from "./extensions/LocalMdxComponentNode";
78
78
  import {
79
+ CompatibleCode,
79
80
  createNotionEditorExtensions,
80
81
  focusMostRecentEmptyToggleSummary,
81
82
  type NotionPageLink,
@@ -1645,6 +1646,7 @@ export function createVisualEditorExtensions({
1645
1646
  },
1646
1647
  starterKit: {
1647
1648
  blockquote: false,
1649
+ code: false,
1648
1650
  paragraph: false,
1649
1651
  heading: { levels: [1, 2, 3, 4, 5, 6] },
1650
1652
  horizontalRule: {},
@@ -1653,6 +1655,7 @@ export function createVisualEditorExtensions({
1653
1655
  collab:
1654
1656
  ydoc || localAwareness ? { ydoc, awareness: localAwareness, user } : null,
1655
1657
  extraExtensions: [
1658
+ CompatibleCode,
1656
1659
  EmptyLineParagraph,
1657
1660
  NotionBlockquote,
1658
1661
  CodeBlock,
@@ -1,4 +1,5 @@
1
1
  import { findTrailingPlainInlineMath } from "@shared/inline-math";
2
+ import { NFM_COLORS } from "@shared/nfm";
2
3
  import {
3
4
  escapeHtml,
4
5
  indentMarkdown,
@@ -12,6 +13,7 @@ import {
12
13
  IconFileText,
13
14
  } from "@tabler/icons-react";
14
15
  import { InputRule, type Editor } from "@tiptap/core";
16
+ import { Code } from "@tiptap/extension-code";
15
17
  import type { Fragment, Node as ProseMirrorNode } from "@tiptap/pm/model";
16
18
  import { Selection } from "@tiptap/pm/state";
17
19
  import {
@@ -745,12 +747,19 @@ export const NotionSpanMark = Mark.create({
745
747
  renderHTML({ HTMLAttributes }) {
746
748
  const attrs = parseAttrsJson(HTMLAttributes.attrsJson as string);
747
749
  const style: string[] = [];
750
+ const classes: string[] = [];
748
751
 
749
- if (HTMLAttributes.color) {
750
- style.push(`color: ${HTMLAttributes.color}`);
752
+ if (
753
+ NFM_COLORS.has(HTMLAttributes.color) &&
754
+ !HTMLAttributes.color.endsWith("_bg")
755
+ ) {
756
+ classes.push(`notion-block-color--${HTMLAttributes.color}`);
751
757
  }
752
- if (HTMLAttributes.bgColor) {
753
- style.push(`background-color: ${HTMLAttributes.bgColor}`);
758
+ if (
759
+ NFM_COLORS.has(HTMLAttributes.bgColor) &&
760
+ HTMLAttributes.bgColor.endsWith("_bg")
761
+ ) {
762
+ classes.push(`notion-block-bg--${HTMLAttributes.bgColor.slice(0, -3)}`);
754
763
  }
755
764
  if (HTMLAttributes.underline === "true") {
756
765
  style.push("text-decoration: underline");
@@ -764,6 +773,7 @@ export const NotionSpanMark = Mark.create({
764
773
  bg_color: HTMLAttributes.bgColor || undefined,
765
774
  underline: HTMLAttributes.underline || undefined,
766
775
  href: HTMLAttributes.href || undefined,
776
+ class: classes.length ? classes.join(" ") : undefined,
767
777
  style: style.length ? style.join("; ") : undefined,
768
778
  }),
769
779
  0,
@@ -787,6 +797,10 @@ export const NotionSpanMark = Mark.create({
787
797
  },
788
798
  });
789
799
 
800
+ export const CompatibleCode = Code.extend({
801
+ excludes: "",
802
+ });
803
+
790
804
  export const NotionToggle = Node.create({
791
805
  name: "notionToggle",
792
806
  priority: 1000,