@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.
- package/dist/chunks/{UiKitApp-ZZXQt3zy.js → UiKitApp-BDsrXN12.js} +323 -321
- package/dist/chunks/{WorkspaceRoutes-CCDPtLla.js → WorkspaceRoutes-BTZ1Ah4F.js} +33286 -33321
- package/dist/chunks/{dashboard-Dw0zfTXg.js → dashboard-FT7dm5wi.js} +2 -2
- package/dist/chunks/{index-DhZGCAru.js → index-DHJvh5Uy.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/index.js +51 -52
- package/dist/site-widget.js +9547 -10892
- package/package.json +2 -2
- package/src/api.ts +8 -26
- package/src/chat/ChangeCard.tsx +2 -10
- package/src/chat/ChatMessage.tsx +24 -19
- package/src/chat/ChatProvider.tsx +17 -15
- package/src/chat/DocListCard.tsx +2 -16
- package/src/chat/MentionPopover.tsx +8 -9
- package/src/chat/PointerChip.tsx +1 -1
- package/src/chat/chat-context.ts +7 -7
- package/src/chat/chat-helpers.ts +57 -14
- package/src/chrome/CommandPalette.tsx +2 -10
- package/src/chrome/ContentExplorerSidebar.tsx +51 -88
- package/src/chrome/EditorFocusContext.tsx +3 -3
- package/src/chrome/EditorStatusContext.tsx +0 -3
- package/src/chrome/WorkspaceNavContext.tsx +0 -2
- package/src/chrome/WorkspaceStatusBar.tsx +6 -23
- package/src/chrome/content-api-url.ts +3 -12
- package/src/chrome/content-repo-path.ts +8 -25
- package/src/chrome/content-type-groups.ts +0 -3
- package/src/chrome/folder-create-context.ts +15 -39
- package/src/pages/DocEditorPage.tsx +17 -33
- package/src/pages/EmbedChatPage.tsx +3 -3
- package/src/pages/EmbedPublishPage.tsx +2 -3
- package/src/pages/LegacyWorkspaceRedirect.tsx +2 -9
- package/src/pages/WorkspaceLayout.tsx +0 -5
- package/src/pages/preview-url.ts +6 -20
- package/src/pages/type-docs.ts +3 -6
- package/src/preview-frame.tsx +10 -4
- package/src/replica/store.ts +2 -10
- package/src/replica/types.ts +1 -2
- package/src/site-bar.tsx +3 -6
- package/src/site-dashboard-href.ts +1 -3
- package/src/site-widget.tsx +2 -3
- 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.
|
|
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.
|
|
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}
|
|
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<{
|
|
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(
|
|
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?: {
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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";
|
package/src/chat/ChangeCard.tsx
CHANGED
|
@@ -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>
|
package/src/chat/ChatMessage.tsx
CHANGED
|
@@ -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: {
|
|
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
|
|
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
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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({
|
|
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.
|
|
181
|
-
return {
|
|
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
|
|
640
|
-
if (
|
|
641
|
-
setChanges((prev) =>
|
|
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({
|
|
660
|
+
notifyDocChanged({ type: change.type, slug: change.slug });
|
|
659
661
|
notifyTreeChanged();
|
|
660
662
|
}
|
|
661
663
|
}, []);
|
package/src/chat/DocListCard.tsx
CHANGED
|
@@ -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.
|
|
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
|
|
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) =>
|
|
55
|
+
const excluded = new Set(exclude.map((r) => docKey(r)));
|
|
56
56
|
return filterDocs(
|
|
57
|
-
docs.filter((d) => !excluded.has(
|
|
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({
|
|
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].
|
|
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={
|
|
117
|
-
id={`mention-${item.
|
|
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({
|
|
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"
|
package/src/chat/PointerChip.tsx
CHANGED
|
@@ -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.
|
|
14
|
+
return `${pointer.type}/${pointer.slug}/${pointer.blockId ?? ""}/${pointer.quote}`;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function pointerLabel(pointer: ChatPointer): string {
|
package/src/chat/chat-context.ts
CHANGED
|
@@ -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: {
|
|
63
|
+
doc: { type?: unknown; slug?: unknown },
|
|
65
64
|
grab: unknown,
|
|
66
65
|
): ChatPointer | null {
|
|
67
|
-
if (!doc || typeof doc.
|
|
66
|
+
if (!doc || typeof doc.type !== "string" || typeof doc.slug !== "string") {
|
|
68
67
|
return null;
|
|
69
68
|
}
|
|
70
|
-
if (!doc.
|
|
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({
|
|
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 };
|