@backmatter/studio 0.0.13 → 0.0.15

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 (41) hide show
  1. package/dist/chunks/{UiKitApp-ZZXQt3zy.js → UiKitApp-BDsrXN12.js} +323 -321
  2. package/dist/chunks/{WorkspaceRoutes-CCDPtLla.js → WorkspaceRoutes-BTZ1Ah4F.js} +33286 -33321
  3. package/dist/chunks/{dashboard-Dw0zfTXg.js → dashboard-FT7dm5wi.js} +2 -2
  4. package/dist/chunks/{index-DhZGCAru.js → index-DHJvh5Uy.js} +1 -1
  5. package/dist/dashboard.js +2 -2
  6. package/dist/index.js +51 -52
  7. package/dist/site-widget.js +9547 -10892
  8. package/package.json +2 -2
  9. package/src/api.ts +8 -26
  10. package/src/chat/ChangeCard.tsx +2 -10
  11. package/src/chat/ChatMessage.tsx +24 -19
  12. package/src/chat/ChatProvider.tsx +17 -15
  13. package/src/chat/DocListCard.tsx +2 -16
  14. package/src/chat/MentionPopover.tsx +8 -9
  15. package/src/chat/PointerChip.tsx +1 -1
  16. package/src/chat/chat-context.ts +7 -7
  17. package/src/chat/chat-helpers.ts +57 -14
  18. package/src/chrome/CommandPalette.tsx +2 -10
  19. package/src/chrome/ContentExplorerSidebar.tsx +51 -88
  20. package/src/chrome/EditorFocusContext.tsx +3 -3
  21. package/src/chrome/EditorStatusContext.tsx +0 -3
  22. package/src/chrome/WorkspaceNavContext.tsx +0 -2
  23. package/src/chrome/WorkspaceStatusBar.tsx +6 -23
  24. package/src/chrome/content-api-url.ts +3 -12
  25. package/src/chrome/content-repo-path.ts +8 -25
  26. package/src/chrome/content-type-groups.ts +0 -3
  27. package/src/chrome/folder-create-context.ts +15 -39
  28. package/src/pages/DocEditorPage.tsx +17 -33
  29. package/src/pages/EmbedChatPage.tsx +3 -3
  30. package/src/pages/EmbedPublishPage.tsx +2 -3
  31. package/src/pages/LegacyWorkspaceRedirect.tsx +2 -9
  32. package/src/pages/WorkspaceLayout.tsx +0 -5
  33. package/src/pages/preview-url.ts +6 -20
  34. package/src/pages/type-docs.ts +3 -6
  35. package/src/preview-frame.tsx +10 -4
  36. package/src/replica/store.ts +2 -10
  37. package/src/replica/types.ts +1 -2
  38. package/src/site-bar.tsx +3 -6
  39. package/src/site-dashboard-href.ts +1 -3
  40. package/src/site-widget.tsx +2 -3
  41. package/src/workspace-path.ts +3 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backmatter/studio",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "The Backmatter editing surface: document editor, AI chat, publish. Mounted on your site or hosted on the dashboard.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "@ai-sdk/react": "^4.0.100",
59
- "@backmatter/core": "^0.0.27",
59
+ "@backmatter/core": "^0.0.28",
60
60
  "@base-ui/react": "^1.6.0",
61
61
  "@central-icons-react/round-outlined-radius-2-stroke-1.5": "1.2.15",
62
62
  "@dnd-kit/abstract": "^0.5.0",
package/src/api.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  // packages/studio/src/api.ts
2
- import { encodeDocLocale } from "@backmatter/core";
3
2
  import { replicaQuery, type ReplicaCache } from "./replica/cache";
4
3
  import type { ReplicaBody } from "./replica/types";
5
4
  import { apiCredentials, apiHeaders, apiUrl, reportUnauthorized } from "./studio-config";
@@ -160,7 +159,6 @@ export type WorkspaceDetail = {
160
159
  githubDefaultBranch: string | null;
161
160
  contentRoot: string | null;
162
161
  githubDirectory: string | null;
163
- locales: string[];
164
162
  siteUrls: string[];
165
163
  /** Last `@backmatter/studio` version each site reported, keyed by origin. */
166
164
  studioVersions?: Record<string, { version: string; seenAt: string }>;
@@ -399,7 +397,6 @@ export type ChangeStatus = "modified" | "added" | "conflict" | "deleted";
399
397
  export type DocStatus = "clean" | ChangeStatus;
400
398
 
401
399
  export type GitChange = {
402
- locale: string;
403
400
  type: string;
404
401
  slug: string;
405
402
  status: ChangeStatus;
@@ -454,7 +451,6 @@ export type CommitResult = {
454
451
  message: string;
455
452
  branch: string;
456
453
  files: Array<{
457
- locale: string;
458
454
  type: string;
459
455
  slug: string;
460
456
  githubPath: string;
@@ -474,7 +470,7 @@ export function commitWorkspace(workspaceId: string, message?: string) {
474
470
  export function defaultCommitMessage(changes: GitChange[]) {
475
471
  if (changes.length === 1) {
476
472
  const f = changes[0]!;
477
- return `backmatter: update ${f.type}/${f.slug} (${f.locale})`;
473
+ return `backmatter: update ${f.type}/${f.slug}`;
478
474
  }
479
475
  return `backmatter: update ${changes.length} files`;
480
476
  }
@@ -486,7 +482,6 @@ export function notifyWorkspaceChanged() {
486
482
 
487
483
  export type TreeChangeDetail = {
488
484
  workspaceId: string;
489
- locale: string;
490
485
  type: string;
491
486
  slug: string;
492
487
  status?: DocStatus;
@@ -512,12 +507,11 @@ export function notifyHeadChanged() {
512
507
  */
513
508
  export function publishCommittedFiles(
514
509
  workspaceId: string,
515
- files: Array<{ locale: string; type: string; slug: string; githubSha: string }>,
510
+ files: Array<{ type: string; slug: string; githubSha: string }>,
516
511
  ) {
517
512
  for (const file of files) {
518
513
  notifyTreeChanged({
519
514
  workspaceId,
520
- locale: file.locale,
521
515
  type: file.type,
522
516
  slug: file.slug,
523
517
  ...(file.githubSha ? { status: "clean" } : { remove: true }),
@@ -561,22 +555,18 @@ export async function importContent(workspaceId: string) {
561
555
  export const pullFromGithub = importContent;
562
556
 
563
557
  export type DocListItem = {
564
- locale: string;
565
558
  type: string;
566
559
  slug: string;
567
560
  status: DocStatus;
568
561
  };
569
562
 
570
- export { encodeDocLocale };
571
-
572
563
  export function docApiPath(
573
564
  workspaceId: string,
574
- locale: string,
575
565
  type: string,
576
566
  slug: string,
577
567
  suffix = "",
578
568
  ) {
579
- return `/workspaces/${workspaceId}/docs/${encodeURIComponent(encodeDocLocale(locale))}/${encodeURIComponent(type)}/${encodeURIComponent(slug)}${suffix}`;
569
+ return `/workspaces/${workspaceId}/docs/${encodeURIComponent(type)}/${encodeURIComponent(slug)}${suffix}`;
580
570
  }
581
571
 
582
572
  export function fetchDocs(workspaceId: string) {
@@ -597,7 +587,7 @@ export function fetchDocNav(
597
587
  q?: string;
598
588
  limit?: number;
599
589
  offset?: number;
600
- current?: { locale: string; type: string; slug: string };
590
+ current?: { type: string; slug: string };
601
591
  } = {},
602
592
  ) {
603
593
  const qs = new URLSearchParams({ nav: "1" });
@@ -605,7 +595,6 @@ export function fetchDocNav(
605
595
  if (input.limit !== undefined) qs.set("limit", String(input.limit));
606
596
  if (input.offset !== undefined) qs.set("offset", String(input.offset));
607
597
  if (input.current) {
608
- qs.set("locale", input.current.locale);
609
598
  qs.set("type", input.current.type);
610
599
  qs.set("slug", input.current.slug);
611
600
  }
@@ -614,13 +603,11 @@ export function fetchDocNav(
614
603
 
615
604
  export function fetchDoc(
616
605
  workspaceId: string,
617
- locale: string,
618
606
  type: string,
619
607
  slug: string,
620
608
  ) {
621
609
  return api<{
622
610
  doc: {
623
- locale: string;
624
611
  type: string;
625
612
  slug: string;
626
613
  branch: string;
@@ -643,7 +630,7 @@ export function fetchDoc(
643
630
  } | null;
644
631
  parseError?: string | null;
645
632
  };
646
- }>(docApiPath(workspaceId, locale, type, slug));
633
+ }>(docApiPath(workspaceId, type, slug));
647
634
  }
648
635
 
649
636
  /** `head` = committed on the branch; `tree` = my uncommitted working copy. */
@@ -652,14 +639,12 @@ export type ContentSource = "head" | "tree";
652
639
  /** Save into my working tree (autosave). */
653
640
  export function saveDoc(
654
641
  workspaceId: string,
655
- locale: string,
656
642
  type: string,
657
643
  slug: string,
658
644
  input: { frontmatter: Record<string, unknown>; body: string },
659
645
  ) {
660
646
  return api<{
661
647
  doc: {
662
- locale: string;
663
648
  type: string;
664
649
  slug: string;
665
650
  source: "tree";
@@ -667,7 +652,7 @@ export function saveDoc(
667
652
  headSha: string | null;
668
653
  raw: string;
669
654
  };
670
- }>(docApiPath(workspaceId, locale, type, slug), {
655
+ }>(docApiPath(workspaceId, type, slug), {
671
656
  method: "PUT",
672
657
  body: JSON.stringify(input),
673
658
  });
@@ -676,12 +661,11 @@ export function saveDoc(
676
661
  /** Drop my uncommitted copy; the doc reads from HEAD again. */
677
662
  export function revertDoc(
678
663
  workspaceId: string,
679
- locale: string,
680
664
  type: string,
681
665
  slug: string,
682
666
  ) {
683
667
  return api<{ ok: true }>(
684
- docApiPath(workspaceId, locale, type, slug, "/tree"),
668
+ docApiPath(workspaceId, type, slug, "/tree"),
685
669
  { method: "DELETE" },
686
670
  );
687
671
  }
@@ -689,13 +673,12 @@ export function revertDoc(
689
673
  /** Resolve a conflict: keep mine (rebase onto the new HEAD) or take theirs (revert). */
690
674
  export function resolveDoc(
691
675
  workspaceId: string,
692
- locale: string,
693
676
  type: string,
694
677
  slug: string,
695
678
  strategy: "mine" | "theirs",
696
679
  ) {
697
680
  return api<{ ok: true; status: DocStatus; headSha: string | null }>(
698
- docApiPath(workspaceId, locale, type, slug, "/resolve"),
681
+ docApiPath(workspaceId, type, slug, "/resolve"),
699
682
  { method: "POST", body: JSON.stringify({ strategy }) },
700
683
  );
701
684
  }
@@ -746,7 +729,6 @@ export type ChatChangeSummary = {
746
729
  userId: string;
747
730
  branch: string;
748
731
  resourceKind: "content";
749
- locale: string;
750
732
  type: string;
751
733
  slug: string;
752
734
  op: "write" | "create";
@@ -29,15 +29,7 @@ export function ChangeCard({
29
29
  const [expanded, setExpanded] = useState(false);
30
30
  const nav = useWorkspaceNav();
31
31
  const label = `${change.type}/${change.slug}`;
32
- const to = nav
33
- ? contentDocPath(
34
- nav.workspaceSlug,
35
- change.type,
36
- change.slug,
37
- change.locale,
38
- nav.defaultLocale,
39
- )
40
- : "#";
32
+ const to = nav ? contentDocPath(nav.workspaceSlug, change.type, change.slug) : "#";
41
33
  const status = change.status;
42
34
 
43
35
  return (
@@ -69,7 +61,7 @@ export function ChangeCard({
69
61
  )}
70
62
  {change.diff?.renamedFrom ? (
71
63
  <span {...stylex.props(styles.badge)}>
72
- {change.diff.renamedFrom} → {change.slug}
64
+ {`${change.diff.renamedFrom} → ${change.slug}`}
73
65
  </span>
74
66
  ) : change.op === "create" ? (
75
67
  <span {...stylex.props(styles.badge)}>new</span>
@@ -19,6 +19,7 @@ import {
19
19
  stackChanges,
20
20
  toolLabel,
21
21
  trimPartialPlaceholder,
22
+ changesInToolOutput,
22
23
  type WriteToolOutput,
23
24
  } from "./chat-helpers";
24
25
  import { CheckIcon, EyeIcon, FileIcon, SearchIcon, UndoIcon } from "./icons";
@@ -33,7 +34,7 @@ type ToolPart = Extract<UIMessage["parts"][number], { toolCallId: string }>;
33
34
  type ListDocsOutput = { docs: DocCardItem[]; total?: number };
34
35
  type SearchDocsOutput = {
35
36
  matches: Array<{
36
- docRef: { locale: string; type: string; slug: string };
37
+ docRef: { type: string; slug: string };
37
38
  source?: string;
38
39
  excerpt?: string;
39
40
  title?: string | null;
@@ -256,13 +257,7 @@ export function ChatMessage({
256
257
  const failed =
257
258
  tp.state === "output-error" ||
258
259
  (out !== null && (write || commit) && !out.ok);
259
- const outChange = write && out && out.ok ? out.change : null;
260
- const change = outChange
261
- ? {
262
- ...(session.changes[outChange.id] ?? outChange),
263
- summary: session.changes[outChange.id]?.summary ?? outChange.summary,
264
- }
265
- : null;
260
+ const written = write && out && out.ok ? changesInToolOutput(out) : [];
266
261
  // Doc lists placed inline via [[docs]] are not repeated here. While
267
262
  // streaming, the placeholder usually arrives a moment after the tool
268
263
  // result, so hold the fallback back until the stream has ended;
@@ -271,7 +266,14 @@ export function ChatMessage({
271
266
  tp.state === "output-available" && !inlined.has(i) && !streaming
272
267
  ? docListFromTool(name, tp.output)
273
268
  : null;
274
- const viewStatus = change ? presentedChangeStatus(change, stack) : null;
269
+ const cards = written.map((raw) => {
270
+ const change = {
271
+ ...(session.changes[raw.id] ?? raw),
272
+ summary: session.changes[raw.id]?.summary ?? raw.summary,
273
+ };
274
+ const viewStatus = presentedChangeStatus(change, stack);
275
+ return { change, viewStatus };
276
+ });
275
277
  return (
276
278
  <Reveal key={tp.toolCallId} active={streaming} origin="start">
277
279
  <div {...stylex.props(styles.toolBlock)}>
@@ -294,16 +296,19 @@ export function ChatMessage({
294
296
  emptyLabel={name === "searchDocs" ? "No matches" : "No documents"}
295
297
  />
296
298
  ) : null}
297
- {change && viewStatus ? (
298
- <ChangeCard
299
- change={{ ...change, status: viewStatus }}
300
- workspaceId={workspaceId}
301
- busy={busyId === change.id}
302
- canAct={canAct && !streaming}
303
- onKeep={() => void act(change.id, () => session.keep(change.id))}
304
- onUndo={() => void act(change.id, () => session.undo(change.id))}
305
- />
306
- ) : null}
299
+ {cards.map(({ change, viewStatus }) =>
300
+ viewStatus ? (
301
+ <ChangeCard
302
+ key={change.id}
303
+ change={{ ...change, status: viewStatus }}
304
+ workspaceId={workspaceId}
305
+ busy={busyId === change.id}
306
+ canAct={canAct && !streaming}
307
+ onKeep={() => void act(change.id, () => session.keep(change.id))}
308
+ onUndo={() => void act(change.id, () => session.undo(change.id))}
309
+ />
310
+ ) : null,
311
+ )}
307
312
  </div>
308
313
  </Reveal>
309
314
  );
@@ -56,9 +56,10 @@ import {
56
56
  import {
57
57
  changeIdsInMessage,
58
58
  isWriteTool,
59
+ changesInToolOutput,
60
+ detailForTreePatch,
59
61
  treeStatusPatches,
60
62
  type TreeStatusPatch,
61
- type WriteToolOutput,
62
63
  } from "./chat-helpers";
63
64
  import {
64
65
  initialPanelState,
@@ -158,27 +159,24 @@ const modelStorageKey = (ws: string) => `backmatter-chat-model:${ws}`;
158
159
  function publishTreePatches(workspaceId: string, patches: TreeStatusPatch[]) {
159
160
  let published = false;
160
161
  for (const patch of patches) {
161
- notifyTreeChanged({
162
- workspaceId,
163
- locale: patch.locale,
164
- type: patch.type,
165
- slug: patch.slug,
166
- ...(patch.absent ? { remove: true } : { status: patch.status }),
167
- });
162
+ notifyTreeChanged(detailForTreePatch(workspaceId, patch));
168
163
  // A published file stays on screen; head-changed reloads it once. A file
169
164
  // that disappeared still needs the open editor to drop it.
170
165
  if (patch.absent || patch.status !== "clean") {
171
- notifyDocChanged({ locale: patch.locale, type: patch.type, slug: patch.slug });
166
+ notifyDocChanged({ type: patch.type, slug: patch.slug });
172
167
  }
173
168
  if (patch.status === "clean") published = true;
174
169
  }
175
170
  if (published) notifyHeadChanged();
171
+ // The per-file patch updates the tree immediately. A refetch picks up a
172
+ // file the patch missed, and drops one the server no longer lists.
173
+ notifyTreeChanged();
176
174
  }
177
175
 
178
176
  function refFromInput(input: unknown): DocRef | null {
179
177
  const ref = (input as { docRef?: Partial<DocRef> } | undefined)?.docRef;
180
- if (!ref || !ref.locale || !ref.type || !ref.slug) return null;
181
- return { locale: ref.locale, type: ref.type, slug: ref.slug };
178
+ if (!ref || !ref.type || !ref.slug) return null;
179
+ return { type: ref.type, slug: ref.slug };
182
180
  }
183
181
 
184
182
  export function ChatProvider({
@@ -636,9 +634,13 @@ function LiveSession({
636
634
  seen.add(part.toolCallId);
637
635
  const patches = treeStatusPatches(part.output);
638
636
  if (patches.length > 0) publishTreePatches(workspaceId, patches);
639
- const out = part.output as WriteToolOutput | undefined;
640
- if (write && out?.ok) {
641
- setChanges((prev) => ({ ...prev, [out.change.id]: out.change }));
637
+ const found = write ? changesInToolOutput(part.output) : [];
638
+ if (found.length > 0) {
639
+ setChanges((prev) => {
640
+ const next = { ...prev };
641
+ for (const change of found) next[change.id] = change;
642
+ return next;
643
+ });
642
644
  if (patches.length === 0) {
643
645
  if (ref) notifyDocChanged(ref);
644
646
  notifyTreeChanged();
@@ -655,7 +657,7 @@ function LiveSession({
655
657
  const applyChangeResult = useCallback((change: ChatChangeSummary, notify: boolean) => {
656
658
  setChanges((prev) => ({ ...prev, [change.id]: { ...prev[change.id], ...change } }));
657
659
  if (notify) {
658
- notifyDocChanged({ locale: change.locale, type: change.type, slug: change.slug });
660
+ notifyDocChanged({ type: change.type, slug: change.slug });
659
661
  notifyTreeChanged();
660
662
  }
661
663
  }, []);
@@ -14,7 +14,6 @@ import { ChevronDownIcon, FileIcon } from "./icons";
14
14
  const hover = "@media (hover: hover)";
15
15
 
16
16
  export type DocCardItem = {
17
- locale: string;
18
17
  type: string;
19
18
  slug: string;
20
19
  title?: string | null;
@@ -57,7 +56,6 @@ export function DocListCard({
57
56
 
58
57
  const groups = groupByType(items);
59
58
  const multiType = groups.length > 1;
60
- const multiLocale = new Set(items.map((d) => d.locale)).size > 1;
61
59
  const hidden = expanded ? 0 : Math.max(0, items.length - COLLAPSED_ROWS);
62
60
  const truncated = total !== undefined && total > items.length ? total - items.length : 0;
63
61
 
@@ -78,10 +76,9 @@ export function DocListCard({
78
76
  ) : null}
79
77
  {shown.map((doc) => (
80
78
  <DocRow
81
- key={`${doc.locale}/${doc.type}/${doc.slug}`}
79
+ key={`${doc.type}/${doc.slug}`}
82
80
  doc={doc}
83
81
  workspaceId={workspaceId}
84
- showLocale={multiLocale}
85
82
  />
86
83
  ))}
87
84
  </div>
@@ -109,22 +106,12 @@ export function DocListCard({
109
106
  function DocRow({
110
107
  doc,
111
108
  workspaceId,
112
- showLocale,
113
109
  }: {
114
110
  doc: DocCardItem;
115
111
  workspaceId: string;
116
- showLocale: boolean;
117
112
  }) {
118
113
  const nav = useWorkspaceNav();
119
- const to = nav
120
- ? contentDocPath(
121
- nav.workspaceSlug,
122
- doc.type,
123
- doc.slug,
124
- doc.locale,
125
- nav.defaultLocale,
126
- )
127
- : "#";
114
+ const to = nav ? contentDocPath(nav.workspaceSlug, doc.type, doc.slug) : "#";
128
115
  const title = doc.title?.trim() || doc.slug;
129
116
  const status = docCardBadge(doc);
130
117
  const secondary = doc.path ?? `${doc.type}/${doc.slug}`;
@@ -136,7 +123,6 @@ function DocRow({
136
123
  <FileIcon size={13} />
137
124
  </span>
138
125
  <span {...stylex.props(styles.title)}>{title}</span>
139
- {showLocale ? <span {...stylex.props(styles.locale)}>{doc.locale}</span> : null}
140
126
  {status ? (
141
127
  <span
142
128
  {...stylex.props(
@@ -3,7 +3,7 @@ import { useEffect, useMemo, useState, type RefObject } from "react";
3
3
  import { Popover } from "@base-ui/react/popover";
4
4
  import * as stylex from "@stylexjs/stylex";
5
5
  import { fetchDocs } from "../api";
6
- import type { DocRef } from "../chrome/EditorFocusContext";
6
+ import { docKey, type DocRef } from "../chrome/EditorFocusContext";
7
7
  import { colors, type } from "../styles/tokens.stylex";
8
8
  import { cx, popup, popupClassName } from "../ui/popup";
9
9
  import { FileIcon } from "./icons";
@@ -52,9 +52,9 @@ export function MentionPopover({
52
52
  }, [workspaceId]);
53
53
 
54
54
  const items = useMemo(() => {
55
- const excluded = new Set(exclude.map((r) => `${r.locale}/${r.type}/${r.slug}`));
55
+ const excluded = new Set(exclude.map((r) => docKey(r)));
56
56
  return filterDocs(
57
- docs.filter((d) => !excluded.has(`${d.locale}/${d.type}/${d.slug}`)),
57
+ docs.filter((d) => !excluded.has(docKey(d))),
58
58
  query,
59
59
  );
60
60
  }, [docs, query, exclude]);
@@ -69,7 +69,7 @@ export function MentionPopover({
69
69
  else if (key === "ArrowUp") setActive((i) => Math.max(i - 1, 0));
70
70
  else if (key === "Enter" || key === "Tab") {
71
71
  const item = items[active];
72
- if (item) onSelect({ locale: item.locale, type: item.type, slug: item.slug });
72
+ if (item) onSelect({ type: item.type, slug: item.slug });
73
73
  else onClose();
74
74
  } else if (key === "Escape") onClose();
75
75
  };
@@ -79,7 +79,7 @@ export function MentionPopover({
79
79
  }, [items, active, onSelect, onClose, keyHandlerRef]);
80
80
 
81
81
  const activeId = items[active]
82
- ? `mention-${items[active].locale}-${items[active].type}-${items[active].slug}`
82
+ ? `mention-${items[active].type}-${items[active].slug}`
83
83
  : undefined;
84
84
 
85
85
  return (
@@ -113,13 +113,13 @@ export function MentionPopover({
113
113
  ) : (
114
114
  items.map((item, i) => (
115
115
  <div
116
- key={`${item.locale}/${item.type}/${item.slug}`}
117
- id={`mention-${item.locale}-${item.type}-${item.slug}`}
116
+ key={docKey(item)}
117
+ id={`mention-${item.type}-${item.slug}`}
118
118
  role="option"
119
119
  aria-selected={i === active}
120
120
  onMouseEnter={() => setActive(i)}
121
121
  onMouseDown={(e) => e.preventDefault()}
122
- onClick={() => onSelect({ locale: item.locale, type: item.type, slug: item.slug })}
122
+ onClick={() => onSelect({ type: item.type, slug: item.slug })}
123
123
  {...stylex.props(popup.item, styles.item, i === active && popup.itemHighlighted)}
124
124
  >
125
125
  <FileIcon size={12} />
@@ -127,7 +127,6 @@ export function MentionPopover({
127
127
  {item.type}/{item.slug}
128
128
  </span>
129
129
  <span {...stylex.props(popup.itemMeta)}>
130
- {item.locale}
131
130
  {item.status === "added"
132
131
  ? " · new"
133
132
  : item.status !== "clean"
@@ -11,7 +11,7 @@ import { CloseIcon, PointerIcon } from "./icons";
11
11
  const hover = "@media (hover: hover)";
12
12
 
13
13
  export function pointerKey(pointer: ChatPointer): string {
14
- return `${pointer.locale}/${pointer.type}/${pointer.slug}/${pointer.blockId ?? ""}/${pointer.quote}`;
14
+ return `${pointer.type}/${pointer.slug}/${pointer.blockId ?? ""}/${pointer.quote}`;
15
15
  }
16
16
 
17
17
  function pointerLabel(pointer: ChatPointer): string {
@@ -1,5 +1,6 @@
1
1
  // packages/studio/src/chat/chat-context.ts
2
2
  import {
3
+ docKey,
3
4
  sameDocRef,
4
5
  type DocRef,
5
6
  type EditorFocus,
@@ -7,7 +8,6 @@ import {
7
8
 
8
9
  /** An element the user pointed at on the site. The agent edits that block, or the quote. */
9
10
  export type ChatPointer = {
10
- locale: string;
11
11
  type: string;
12
12
  slug: string;
13
13
  blockId: string | null;
@@ -28,7 +28,6 @@ export type ChatRequestContext = {
28
28
 
29
29
  function samePointer(a: ChatPointer, b: ChatPointer): boolean {
30
30
  return (
31
- a.locale === b.locale &&
32
31
  a.type === b.type &&
33
32
  a.slug === b.slug &&
34
33
  a.blockId === b.blockId &&
@@ -61,13 +60,13 @@ export function removePointer(list: ChatPointer[], next: ChatPointer): ChatPoint
61
60
  }
62
61
 
63
62
  export function chatPointerFromGrab(
64
- doc: { locale?: unknown; type?: unknown; slug?: unknown },
63
+ doc: { type?: unknown; slug?: unknown },
65
64
  grab: unknown,
66
65
  ): ChatPointer | null {
67
- if (!doc || typeof doc.locale !== "string" || typeof doc.type !== "string" || typeof doc.slug !== "string") {
66
+ if (!doc || typeof doc.type !== "string" || typeof doc.slug !== "string") {
68
67
  return null;
69
68
  }
70
- if (!doc.locale || !doc.type || !doc.slug) return null;
69
+ if (!doc.type || !doc.slug) return null;
71
70
  if (!grab || typeof grab !== "object") return null;
72
71
  const value = grab as Record<string, unknown>;
73
72
  if (value.blockId !== null && typeof value.blockId !== "string") return null;
@@ -75,7 +74,6 @@ export function chatPointerFromGrab(
75
74
  if (value.blockIndex !== null && typeof value.blockIndex !== "number") return null;
76
75
  if (typeof value.quote !== "string") return null;
77
76
  return {
78
- locale: doc.locale,
79
77
  type: doc.type,
80
78
  slug: doc.slug,
81
79
  blockId: value.blockId,
@@ -100,7 +98,7 @@ export function buildChatContext(input: {
100
98
  for (const m of input.mentions) {
101
99
  if (sameDocRef(m, focusDoc)) continue;
102
100
  if (mentions.some((x) => sameDocRef(x, m))) continue;
103
- mentions.push({ locale: m.locale, type: m.type, slug: m.slug });
101
+ mentions.push({ type: m.type, slug: m.slug });
104
102
  }
105
103
  const pointers: ChatPointer[] = [];
106
104
  for (const pointer of input.pointers) {
@@ -118,3 +116,5 @@ export function addMention(list: DocRef[], ref: DocRef): DocRef[] {
118
116
  export function removeMention(list: DocRef[], ref: DocRef): DocRef[] {
119
117
  return list.filter((x) => !sameDocRef(x, ref));
120
118
  }
119
+
120
+ export { docKey };