@backmatter/studio 0.0.14 → 0.0.16

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 (67) hide show
  1. package/dist/chunks/{UiKitApp-fYeVEf4-.js → UiKitApp-_EDqWID3.js} +2 -2
  2. package/dist/chunks/{WorkspaceRoutes-Dt7JvEWT.js → WorkspaceRoutes-B2PrFgpz.js} +29019 -27476
  3. package/dist/chunks/{dashboard-BuY2aGkO.js → dashboard-BnlnMnnr.js} +55 -54
  4. package/dist/chunks/{index-NWDqEZYW.js → index-D_BHF_8q.js} +1 -1
  5. package/dist/dashboard.js +2 -2
  6. package/dist/index.js +51 -52
  7. package/dist/site-widget.css +4 -0
  8. package/dist/site-widget.js +9419 -9156
  9. package/dist/studio.css +186 -12
  10. package/package.json +2 -2
  11. package/src/WorkspaceRoutes.tsx +5 -2
  12. package/src/api.ts +64 -11
  13. package/src/chat/ChangeCard.tsx +9 -15
  14. package/src/chat/ChatMessage.tsx +172 -103
  15. package/src/chat/ChatPanel.tsx +54 -34
  16. package/src/chat/ChatProvider.tsx +120 -24
  17. package/src/chat/Composer.tsx +48 -7
  18. package/src/chat/DocListCard.tsx +2 -16
  19. package/src/chat/MentionPopover.tsx +8 -9
  20. package/src/chat/PointerChip.tsx +9 -5
  21. package/src/chat/ThreadHistory.tsx +3 -3
  22. package/src/chat/chat-context.ts +74 -10
  23. package/src/chat/chat-helpers.ts +15 -8
  24. package/src/chat/chat-state.ts +11 -0
  25. package/src/chrome/AppShell.tsx +16 -5
  26. package/src/chrome/CommandPalette.tsx +14 -10
  27. package/src/chrome/ContentExplorerSidebar.tsx +23 -3
  28. package/src/chrome/UserMenu.tsx +34 -3
  29. package/src/chrome/WorkspaceStatusBar.tsx +215 -50
  30. package/src/chrome/content-api-url.ts +0 -78
  31. package/src/chrome/content-tree-visibility.ts +13 -3
  32. package/src/chrome/explorer-icons.tsx +5 -0
  33. package/src/code-session-entry.ts +9 -0
  34. package/src/components/WorkspaceSwitcher.tsx +2 -2
  35. package/src/element-origin.ts +149 -0
  36. package/src/host-pointer-bootstrap.ts +11 -0
  37. package/src/host-pointer.ts +147 -0
  38. package/src/install-preview-pointer.ts +19 -3
  39. package/src/pages/CodeSessionFiles.tsx +221 -0
  40. package/src/pages/CodeSessionPage.tsx +1188 -0
  41. package/src/pages/DocEditorPage.tsx +33 -15
  42. package/src/pages/EmbedChatPage.tsx +3 -3
  43. package/src/pages/EmbedPublishPage.tsx +2 -3
  44. package/src/pages/PreviewSize.tsx +181 -0
  45. package/src/pages/SettingsGeneralPage.tsx +67 -2
  46. package/src/pages/WorkspaceHomePage.tsx +313 -0
  47. package/src/pages/WorkspaceLayout.tsx +10 -1
  48. package/src/pages/code-session-boot.ts +43 -0
  49. package/src/pages/code-session-context.tsx +51 -0
  50. package/src/pages/code-session-files.ts +42 -0
  51. package/src/pages/code-session-frame.ts +52 -0
  52. package/src/pages/code-session-routes.ts +42 -0
  53. package/src/pages/home-rows.ts +140 -0
  54. package/src/pages/preview-size.ts +33 -0
  55. package/src/pages/session-preview.ts +109 -0
  56. package/src/pages/type-docs.ts +3 -6
  57. package/src/pages/use-session-preview.ts +70 -0
  58. package/src/replica/store.ts +13 -10
  59. package/src/replica/types.ts +7 -2
  60. package/src/site-pointer.ts +24 -5
  61. package/src/site-widget.tsx +2 -3
  62. package/src/studio-config.ts +1 -1
  63. package/src/ui/ListRow.tsx +12 -3
  64. package/src/ui/Page.tsx +12 -1
  65. package/src/workspace-path.ts +35 -3
  66. package/src/chrome/ContentApiPopover.tsx +0 -215
  67. package/src/pages/WorkspaceHomeRedirect.tsx +0 -17
@@ -24,6 +24,7 @@ import {
24
24
  createThread,
25
25
  fetchAiModels,
26
26
  fetchThread,
27
+ fetchThreads,
27
28
  keepAllChanges,
28
29
  keepChange,
29
30
  notifyHeadChanged,
@@ -48,6 +49,7 @@ import {
48
49
  addMention as addMentionTo,
49
50
  addPointer as addPointerTo,
50
51
  buildChatContext,
52
+ codeSessionChatContext,
51
53
  removeMention as removeMentionFrom,
52
54
  removePointer as removePointerFrom,
53
55
  type ChatPointer,
@@ -55,6 +57,7 @@ import {
55
57
  } from "./chat-context";
56
58
  import {
57
59
  changeIdsInMessage,
60
+ checkoutWriteReloads,
58
61
  isWriteTool,
59
62
  changesInToolOutput,
60
63
  detailForTreePatch,
@@ -62,6 +65,7 @@ import {
62
65
  type TreeStatusPatch,
63
66
  } from "./chat-helpers";
64
67
  import {
68
+ allowsPanelAction,
65
69
  initialPanelState,
66
70
  panelReducer,
67
71
  type PanelAction,
@@ -84,7 +88,8 @@ export type ChatUIValue = {
84
88
  model: string | null;
85
89
  setModel: (id: string) => void;
86
90
  threadId: string | null;
87
- openThread: (threadId: string) => void;
91
+ /** Refuses a thread that belongs to a different surface (editor vs a code session). */
92
+ openThread: (thread: { id: string; codeSessionId?: string | null }) => void;
88
93
  newThread: () => void;
89
94
  mentions: DocRef[];
90
95
  addMention: (ref: DocRef) => void;
@@ -105,6 +110,13 @@ export type ChatUIValue = {
105
110
  /** Bumped whenever a thread is created/renamed/deleted so history can refetch. */
106
111
  threadsVersion: number;
107
112
  bumpThreads: () => void;
113
+ /** Set when this provider is scoped to a code session checkout. */
114
+ codeSessionId: string | null;
115
+ /** Preview route included in chat context, or null when dismissed. */
116
+ route: string | null;
117
+ /** The document that route resolves to, when a model claims it. */
118
+ viewed: DocRef | null;
119
+ dismissRoute: () => void;
108
120
  };
109
121
 
110
122
  const ChatUIContext = createContext<ChatUIValue | null>(null);
@@ -155,6 +167,14 @@ export function useChatSession(): ChatSessionValue {
155
167
 
156
168
  const modelStorageKey = (ws: string) => `backmatter-chat-model:${ws}`;
157
169
 
170
+ function newestThread(threads: readonly { id: string; updatedAt: string }[]): string | null {
171
+ let best: { id: string; updatedAt: string } | null = null;
172
+ for (const thread of threads) {
173
+ if (!best || thread.updatedAt > best.updatedAt) best = thread;
174
+ }
175
+ return best?.id ?? null;
176
+ }
177
+
158
178
  /** Apply tool statuses locally so the top bar moves before the next git poll. */
159
179
  function publishTreePatches(workspaceId: string, patches: TreeStatusPatch[]) {
160
180
  let published = false;
@@ -163,7 +183,7 @@ function publishTreePatches(workspaceId: string, patches: TreeStatusPatch[]) {
163
183
  // A published file stays on screen; head-changed reloads it once. A file
164
184
  // that disappeared still needs the open editor to drop it.
165
185
  if (patch.absent || patch.status !== "clean") {
166
- notifyDocChanged({ locale: patch.locale, type: patch.type, slug: patch.slug });
186
+ notifyDocChanged({ type: patch.type, slug: patch.slug });
167
187
  }
168
188
  if (patch.status === "clean") published = true;
169
189
  }
@@ -175,26 +195,48 @@ function publishTreePatches(workspaceId: string, patches: TreeStatusPatch[]) {
175
195
 
176
196
  function refFromInput(input: unknown): DocRef | null {
177
197
  const ref = (input as { docRef?: Partial<DocRef> } | undefined)?.docRef;
178
- if (!ref || !ref.locale || !ref.type || !ref.slug) return null;
179
- return { locale: ref.locale, type: ref.type, slug: ref.slug };
198
+ if (!ref || !ref.type || !ref.slug) return null;
199
+ return { type: ref.type, slug: ref.slug };
180
200
  }
181
201
 
182
202
  export function ChatProvider({
183
203
  workspaceId,
184
204
  startOpen = false,
185
205
  pointer = false,
206
+ codeSessionId = null,
207
+ route = null,
208
+ viewed = null,
209
+ onRouteDismiss,
210
+ onCheckoutWrite,
211
+ onTurnSettled,
186
212
  children,
187
213
  }: {
188
214
  workspaceId: string;
189
215
  startOpen?: boolean;
190
216
  /** Dev bar chat can point at elements on the host page. */
191
217
  pointer?: boolean;
218
+ codeSessionId?: string | null;
219
+ route?: string | null;
220
+ /** Document for `route`, resolved by the caller. The agent still receives the URL. */
221
+ viewed?: DocRef | null;
222
+ onRouteDismiss?: (route: string) => void;
223
+ /** A file tool finished in the checkout. The preview does not hot-reload on its own. */
224
+ onCheckoutWrite?: () => void;
225
+ /** The reply finished, so a code session can read the edit set the turn just saved. */
226
+ onTurnSettled?: () => void;
192
227
  children: React.ReactNode;
193
228
  }) {
194
229
  const [panel, dispatch] = useReducer(
195
230
  panelReducer,
196
231
  startOpen ? { ...initialPanelState, mode: "open" as const } : initialPanelState,
197
232
  );
233
+ const dispatchPanel = useCallback(
234
+ (action: PanelAction) => {
235
+ if (!allowsPanelAction(action, Boolean(codeSessionId))) return;
236
+ dispatch(action);
237
+ },
238
+ [codeSessionId],
239
+ );
198
240
  const [aiAvailable, setAiAvailable] = useState<boolean | null>(null);
199
241
  const [models, setModels] = useState<AiModelInfo[]>([]);
200
242
  const [tier, setTier] = useState<AiTier>("ask");
@@ -206,8 +248,10 @@ export function ChatProvider({
206
248
  const [pointers, setPointers] = useState<ChatPointer[]>([]);
207
249
  const [previewPointer, setPreviewPointer] = useState(false);
208
250
  const [pointerHighlight, setPointerHighlight] = useState<"local" | "channel">("local");
209
- const [includeFocus, setIncludeFocus] = useState(true);
251
+ const [includeFocus, setIncludeFocus] = useState(!codeSessionId);
210
252
  const [threadsVersion, setThreadsVersion] = useState(0);
253
+ // Bumped when the person picks a chat, so a late session list cannot replace it.
254
+ const restore = useRef(0);
211
255
  const { focus } = useEditorFocus();
212
256
 
213
257
  // Models + availability.
@@ -234,15 +278,30 @@ export function ChatProvider({
234
278
  };
235
279
  }, [workspaceId]);
236
280
 
237
- // Reset per-workspace session state.
281
+ // Reset per-workspace session state. A code session reopens its newest chat.
238
282
  useEffect(() => {
239
283
  setThreadId(null);
240
284
  setFreshThread(false);
241
285
  setPendingText(null);
242
286
  setMentions([]);
243
287
  setPointers([]);
244
- setIncludeFocus(true);
245
- }, [workspaceId]);
288
+ setIncludeFocus(!codeSessionId);
289
+ if (!codeSessionId) return;
290
+ const token = ++restore.current;
291
+ fetchThreads(workspaceId, codeSessionId)
292
+ .then((res) => {
293
+ if (restore.current !== token) return;
294
+ const latest = newestThread(res.threads);
295
+ if (!latest) return;
296
+ setThreadId(latest);
297
+ })
298
+ .catch(() => {
299
+ // A missed list leaves the blank chat. Sending still starts one.
300
+ });
301
+ return () => {
302
+ restore.current += 1;
303
+ };
304
+ }, [workspaceId, codeSessionId]);
246
305
 
247
306
  const setModel = useCallback(
248
307
  (id: string) => {
@@ -252,14 +311,20 @@ export function ChatProvider({
252
311
  [workspaceId],
253
312
  );
254
313
 
255
- const openThread = useCallback((id: string) => {
256
- setThreadId(id);
257
- setFreshThread(false);
258
- setPendingText(null);
259
- dispatch({ type: "showChat" });
260
- }, []);
314
+ const openThread = useCallback(
315
+ (thread: { id: string; codeSessionId?: string | null }) => {
316
+ if ((thread.codeSessionId ?? null) !== (codeSessionId ?? null)) return;
317
+ restore.current += 1;
318
+ setThreadId(thread.id);
319
+ setFreshThread(false);
320
+ setPendingText(null);
321
+ dispatch({ type: "showChat" });
322
+ },
323
+ [codeSessionId],
324
+ );
261
325
 
262
326
  const newThread = useCallback(() => {
327
+ restore.current += 1;
263
328
  setThreadId(null);
264
329
  setFreshThread(false);
265
330
  setPendingText(null);
@@ -275,8 +340,8 @@ export function ChatProvider({
275
340
  setMentions((prev) => removeMentionFrom(prev, ref));
276
341
  }, []);
277
342
  const addPointer = useCallback((next: ChatPointer) => {
278
- setPointers((prev) => addPointerTo(prev, next));
279
- }, []);
343
+ setPointers((prev) => addPointerTo(prev, next, codeSessionId ? "replace" : "append"));
344
+ }, [codeSessionId]);
280
345
  const removePointer = useCallback((next: ChatPointer) => {
281
346
  setPointers((prev) => removePointerFrom(prev, next));
282
347
  }, []);
@@ -289,28 +354,40 @@ export function ChatProvider({
289
354
  selection: null,
290
355
  mentions: [],
291
356
  pointers: [],
357
+ route: null,
292
358
  }));
293
- contextRef.current = () => buildChatContext({ focus, includeFocus, mentions, pointers });
359
+ contextRef.current = () =>
360
+ codeSessionId
361
+ ? codeSessionChatContext({ pointers, route })
362
+ : buildChatContext({ focus, includeFocus, mentions, pointers });
294
363
  const modelRef = useRef<string | null>(model);
295
364
  modelRef.current = model;
296
365
 
297
366
  // Creating a thread lazily on first send.
298
367
  const startThread = useCallback(
299
368
  async (text: string) => {
300
- const res = await createThread(workspaceId);
369
+ restore.current += 1;
370
+ const res = await createThread(
371
+ workspaceId,
372
+ codeSessionId ? { codeSessionId } : undefined,
373
+ );
301
374
  setPendingText(text);
302
375
  setFreshThread(true);
303
376
  setThreadId(res.thread.id);
304
377
  bumpThreads();
305
378
  },
306
- [workspaceId, bumpThreads],
379
+ [workspaceId, bumpThreads, codeSessionId],
307
380
  );
308
381
 
382
+ const dismissRoute = useCallback(() => {
383
+ if (route && onRouteDismiss) onRouteDismiss(route);
384
+ }, [route, onRouteDismiss]);
385
+
309
386
  const ui = useMemo<ChatUIValue>(
310
387
  () => ({
311
388
  workspaceId,
312
389
  panel,
313
- dispatch,
390
+ dispatch: dispatchPanel,
314
391
  aiAvailable,
315
392
  models,
316
393
  tier,
@@ -335,10 +412,15 @@ export function ChatProvider({
335
412
  focus,
336
413
  threadsVersion,
337
414
  bumpThreads,
415
+ codeSessionId,
416
+ route,
417
+ viewed,
418
+ dismissRoute,
338
419
  }),
339
420
  [
340
421
  workspaceId,
341
422
  panel,
423
+ dispatchPanel,
342
424
  aiAvailable,
343
425
  models,
344
426
  tier,
@@ -361,6 +443,10 @@ export function ChatProvider({
361
443
  focus,
362
444
  threadsVersion,
363
445
  bumpThreads,
446
+ codeSessionId,
447
+ route,
448
+ viewed,
449
+ dismissRoute,
364
450
  ],
365
451
  );
366
452
 
@@ -380,13 +466,15 @@ export function ChatProvider({
380
466
  getContext={() => contextRef.current()}
381
467
  getModel={() => modelRef.current}
382
468
  onThreadTouched={bumpThreads}
469
+ onCheckoutWrite={onCheckoutWrite}
470
+ onTurnSettled={onTurnSettled}
383
471
  >
384
472
  {scoped}
385
473
  </ChatSession>
386
474
  ) : (
387
475
  <EmptySession onFirstSend={startThread}>{scoped}</EmptySession>
388
476
  ),
389
- [threadId, workspaceId, freshThread, pendingText, bumpThreads, startThread],
477
+ [threadId, workspaceId, freshThread, pendingText, bumpThreads, startThread, onCheckoutWrite, onTurnSettled],
390
478
  );
391
479
 
392
480
  return (
@@ -461,6 +549,8 @@ type SessionProps = {
461
549
  getContext: () => ChatRequestContext;
462
550
  getModel: () => string | null;
463
551
  onThreadTouched: () => void;
552
+ onCheckoutWrite?: () => void;
553
+ onTurnSettled?: () => void;
464
554
  children: React.ReactNode;
465
555
  };
466
556
 
@@ -539,6 +629,8 @@ function LiveSession({
539
629
  getContext,
540
630
  getModel,
541
631
  onThreadTouched,
632
+ onCheckoutWrite,
633
+ onTurnSettled,
542
634
  children,
543
635
  }: SessionProps & {
544
636
  thread: ChatThread | null;
@@ -574,7 +666,10 @@ function LiveSession({
574
666
  id: threadId,
575
667
  messages: initialMessages,
576
668
  transport,
577
- onFinish: () => onThreadTouched(),
669
+ onFinish: () => {
670
+ onThreadTouched();
671
+ onTurnSettled?.();
672
+ },
578
673
  });
579
674
 
580
675
  const { messages, status, sendMessage } = chat;
@@ -632,6 +727,7 @@ function LiveSession({
632
727
  if (streaming && write && ref) editingKey = docKey(ref);
633
728
  if (part.state === "output-available" && !seen.has(part.toolCallId)) {
634
729
  seen.add(part.toolCallId);
730
+ if (checkoutWriteReloads(name, part.output)) onCheckoutWrite?.();
635
731
  const patches = treeStatusPatches(part.output);
636
732
  if (patches.length > 0) publishTreePatches(workspaceId, patches);
637
733
  const found = write ? changesInToolOutput(part.output) : [];
@@ -650,14 +746,14 @@ function LiveSession({
650
746
  }
651
747
  }
652
748
  setAiEditingDocKey(streaming ? editingKey : null);
653
- }, [messages, status, setAiEditingDocKey, workspaceId]);
749
+ }, [messages, status, setAiEditingDocKey, workspaceId, onCheckoutWrite]);
654
750
 
655
751
  useEffect(() => () => setAiEditingDocKey(null), [setAiEditingDocKey]);
656
752
 
657
753
  const applyChangeResult = useCallback((change: ChatChangeSummary, notify: boolean) => {
658
754
  setChanges((prev) => ({ ...prev, [change.id]: { ...prev[change.id], ...change } }));
659
755
  if (notify) {
660
- notifyDocChanged({ locale: change.locale, type: change.type, slug: change.slug });
756
+ notifyDocChanged({ type: change.type, slug: change.slug });
661
757
  notifyTreeChanged();
662
758
  }
663
759
  }, []);
@@ -1,5 +1,5 @@
1
1
  // packages/studio/src/chat/Composer.tsx
2
- import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
3
  import * as stylex from "@stylexjs/stylex";
4
4
  import { docKey, type DocRef } from "../chrome/EditorFocusContext";
5
5
  import { colors, radius, space, type } from "../styles/tokens.stylex";
@@ -26,6 +26,27 @@ function isMentionKey(key: string): key is MentionKey {
26
26
 
27
27
  export { POINTER_ARM_EVENT } from "./pointer-arm";
28
28
 
29
+ /** The page open in the code session preview. Same row as the editor's open document. */
30
+ export function RouteChip({
31
+ route,
32
+ doc,
33
+ onDismiss,
34
+ }: {
35
+ route: string;
36
+ doc?: { type: string; slug: string } | null;
37
+ onDismiss: () => void;
38
+ }) {
39
+ return (
40
+ <ContextRow
41
+ icon={<FileIcon size={13} />}
42
+ {...(doc ? { type: doc.type, slug: doc.slug } : { label: route })}
43
+ meta="in preview"
44
+ removeLabel="Remove the page from context"
45
+ onRemove={onDismiss}
46
+ />
47
+ );
48
+ }
49
+
29
50
  function postPointer(on: boolean) {
30
51
  if (typeof window === "undefined") return;
31
52
  window.dispatchEvent(new CustomEvent(POINTER_ARM_EVENT, { detail: { on } }));
@@ -46,12 +67,21 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
46
67
  const busy = session.status === "streaming" || session.status === "submitted";
47
68
  const canSend = text.trim().length > 0 && !busy && ui.aiAvailable === true;
48
69
 
49
- // Auto-grow.
50
- useEffect(() => {
70
+ // Auto-grow. Measuring must not leave a 1px swing: that resizes the thread
71
+ // on every key, and the thread then tries to scroll to the bottom.
72
+ useLayoutEffect(() => {
51
73
  const el = ref.current;
52
74
  if (!el) return;
75
+ const previous = el.offsetHeight;
76
+ el.style.overflowY = "hidden";
53
77
  el.style.height = "0px";
54
- el.style.height = `${Math.min(el.scrollHeight, 180)}px`;
78
+ const next = Math.min(el.scrollHeight, 180);
79
+ if (previous > 0 && Math.abs(next - previous) <= 1) {
80
+ el.style.height = `${previous}px`;
81
+ return;
82
+ }
83
+ el.style.height = `${next}px`;
84
+ if (next >= 180) el.style.overflowY = "auto";
55
85
  }, [text]);
56
86
 
57
87
  useEffect(() => {
@@ -138,10 +168,12 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
138
168
  setMention(null);
139
169
  }
140
170
 
141
- const focusChip = ui.focus && ui.includeFocus ? ui.focus.docRef : null;
171
+ const sessionChat = Boolean(ui.codeSessionId);
172
+ const focusChip = !sessionChat && ui.focus && ui.includeFocus ? ui.focus.docRef : null;
173
+ const pageChip = sessionChat && ui.route ? ui.route : null;
142
174
  const selectionActive =
143
175
  ui.focus?.selection && ui.focus.selection.end > ui.focus.selection.start;
144
- const hasContext = Boolean(focusChip || ui.mentions.length > 0);
176
+ const hasContext = Boolean(focusChip || pageChip || ui.mentions.length > 0);
145
177
 
146
178
  return (
147
179
  <div {...stylex.props(styles.wrap)}>
@@ -168,6 +200,13 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
168
200
  onRemove={() => ui.setIncludeFocus(false)}
169
201
  />
170
202
  ) : null}
203
+ {pageChip ? (
204
+ <RouteChip
205
+ route={pageChip}
206
+ doc={ui.viewed}
207
+ onDismiss={() => ui.dismissRoute()}
208
+ />
209
+ ) : null}
171
210
  {ui.mentions.map((m) => (
172
211
  <ContextRow
173
212
  key={docKey(m)}
@@ -232,6 +271,7 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
232
271
  {...stylex.props(styles.textarea)}
233
272
  />
234
273
  <div {...stylex.props(styles.footer)}>
274
+ {!sessionChat ? (
235
275
  <Tooltip label="Mention a document (@)">
236
276
  <button
237
277
  type="button"
@@ -257,6 +297,7 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
257
297
  <AtIcon />
258
298
  </button>
259
299
  </Tooltip>
300
+ ) : null}
260
301
  <span {...stylex.props(styles.spacer)} />
261
302
  {ui.pointer ? (
262
303
  <Tooltip label={pointing ? "Stop pointing" : "Point at an element on the page"}>
@@ -276,7 +317,7 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
276
317
  </button>
277
318
  </Tooltip>
278
319
  ) : null}
279
- {ui.focus ? (
320
+ {!sessionChat && ui.focus ? (
280
321
  <Tooltip
281
322
  label={
282
323
  ui.includeFocus
@@ -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,20 +11,24 @@ 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.component ?? ""}/${pointer.file ?? ""}:${pointer.line ?? ""}/${pointer.quote}`;
15
15
  }
16
16
 
17
17
  function pointerLabel(pointer: ChatPointer): string {
18
18
  const quote = pointer.quote.replace(/\s+/g, " ").trim();
19
19
  const short = quote.length > 42 ? `${quote.slice(0, 41)}…` : quote;
20
- if (pointer.blockType && short) return `${pointer.blockType} · “${short}”`;
21
- if (pointer.blockType) return pointer.blockType;
22
- return short ? `“${short}”` : "element";
20
+ const kind = pointer.blockType || pointer.component;
21
+ const file = pointer.file
22
+ ? `${pointer.file}${pointer.line ? `:${pointer.line}` : ""}`
23
+ : "";
24
+ const parts = [kind && short ? `${kind} · “${short}”` : kind || (short ? `“${short}”` : "element")];
25
+ if (file) parts.push(file);
26
+ return parts.join(" · ");
23
27
  }
24
28
 
25
29
  function pointerChipLabel(pointer: ChatPointer): string {
26
30
  const quote = pointer.quote.replace(/\s+/g, " ").trim();
27
- const raw = quote || pointer.blockType || `${pointer.type}/${pointer.slug}`;
31
+ const raw = quote || pointer.component || pointer.blockType || `${pointer.type}/${pointer.slug}`;
28
32
  return raw.length > 32 ? `${raw.slice(0, 31)}…` : raw;
29
33
  }
30
34
 
@@ -21,7 +21,7 @@ export function ThreadHistory() {
21
21
 
22
22
  useEffect(() => {
23
23
  let cancelled = false;
24
- fetchThreads(ui.workspaceId)
24
+ fetchThreads(ui.workspaceId, ui.codeSessionId ?? undefined)
25
25
  .then((res) => {
26
26
  if (!cancelled) setThreads(res.threads);
27
27
  })
@@ -31,7 +31,7 @@ export function ThreadHistory() {
31
31
  return () => {
32
32
  cancelled = true;
33
33
  };
34
- }, [ui.workspaceId, ui.threadsVersion]);
34
+ }, [ui.workspaceId, ui.codeSessionId, ui.threadsVersion]);
35
35
 
36
36
  const filtered = useMemo(() => {
37
37
  const q = query.trim().toLowerCase();
@@ -110,7 +110,7 @@ export function ThreadHistory() {
110
110
  >
111
111
  <button
112
112
  type="button"
113
- onClick={() => ui.openThread(t.id)}
113
+ onClick={() => ui.openThread(t)}
114
114
  {...stylex.props(styles.rowBtn)}
115
115
  >
116
116
  <span {...stylex.props(styles.title)}>{t.title || "New chat"}</span>