@backmatter/studio 0.0.15 → 0.0.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.
- package/dist/chunks/{UiKitApp-BDsrXN12.js → UiKitApp-D7vllRQE.js} +2 -2
- package/dist/chunks/{WorkspaceRoutes-BTZ1Ah4F.js → WorkspaceRoutes-DH1LD-2Q.js} +30013 -27965
- package/dist/chunks/{dashboard-FT7dm5wi.js → dashboard-Dsaeud2x.js} +55 -54
- package/dist/chunks/{define-studio-BYtYDGLX.js → define-studio-DlYjgrwr.js} +12 -16
- package/dist/chunks/{index-DHJvh5Uy.js → index-CGMqw2NH.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/define.js +1 -1
- package/dist/index.js +4 -4
- package/dist/site-widget.css +49 -1
- package/dist/site-widget.js +8341 -8058
- package/dist/studio.css +279 -13
- package/package.json +2 -2
- package/src/WorkspaceRoutes.tsx +5 -2
- package/src/api.ts +82 -7
- package/src/chat/ChangeCard.tsx +8 -2
- package/src/chat/ChatMessage.tsx +171 -102
- package/src/chat/ChatPanel.tsx +54 -34
- package/src/chat/ChatProvider.tsx +122 -20
- package/src/chat/Composer.tsx +51 -8
- package/src/chat/PointerChip.tsx +9 -5
- package/src/chat/ThreadHistory.tsx +3 -3
- package/src/chat/chat-context.ts +67 -3
- package/src/chat/chat-helpers.ts +12 -0
- package/src/chat/chat-state.ts +11 -0
- package/src/chrome/AppShell.tsx +16 -5
- package/src/chrome/CommandPalette.tsx +12 -0
- package/src/chrome/ContentExplorerSidebar.tsx +23 -3
- package/src/chrome/Toast.tsx +115 -0
- package/src/chrome/UserMenu.tsx +34 -3
- package/src/chrome/WorkspaceStatusBar.tsx +221 -30
- package/src/chrome/commit-error.ts +13 -0
- package/src/chrome/content-api-url.ts +0 -78
- package/src/chrome/content-tree-visibility.ts +13 -3
- package/src/chrome/explorer-icons.tsx +5 -0
- package/src/chrome/pull-failure.ts +61 -0
- package/src/code-session-entry.ts +9 -0
- package/src/components/WorkspaceSwitcher.tsx +2 -2
- package/src/element-origin.ts +149 -0
- package/src/fields/FieldControl.tsx +748 -0
- package/src/fields/choose-field-input.ts +64 -0
- package/src/fields/load-relation-documents.ts +35 -0
- package/src/fields/omit-field.ts +12 -0
- package/src/fields/relation-targets.ts +44 -0
- package/src/fields/resolve-object-fields.ts +21 -0
- package/src/host-pointer-bootstrap.ts +11 -0
- package/src/host-pointer.ts +147 -0
- package/src/install-preview-pointer.ts +19 -3
- package/src/pages/BlockSettingsDialog.tsx +19 -115
- package/src/pages/BlocksList.tsx +43 -35
- package/src/pages/BodyProse.tsx +16 -1
- package/src/pages/CodeSessionFiles.tsx +221 -0
- package/src/pages/CodeSessionPage.tsx +1361 -0
- package/src/pages/DocEditorPage.tsx +120 -107
- package/src/pages/PreviewSize.tsx +181 -0
- package/src/pages/RenderedBody.tsx +17 -2
- package/src/pages/SettingsGeneralPage.tsx +67 -2
- package/src/pages/WorkspaceHomePage.tsx +313 -0
- package/src/pages/WorkspaceLayout.tsx +10 -1
- package/src/pages/branch-notice.ts +16 -0
- package/src/pages/code-session-boot.ts +43 -0
- package/src/pages/code-session-context.tsx +53 -0
- package/src/pages/code-session-files.ts +42 -0
- package/src/pages/code-session-frame.ts +52 -0
- package/src/pages/code-session-routes.ts +42 -0
- package/src/pages/home-rows.ts +140 -0
- package/src/pages/preview-size.ts +33 -0
- package/src/pages/session-preview.ts +109 -0
- package/src/pages/use-session-preview.ts +70 -0
- package/src/replica/store.ts +11 -0
- package/src/replica/types.ts +6 -0
- package/src/shell-resets.css +15 -0
- package/src/site-pointer.ts +24 -5
- package/src/studio-config.ts +1 -1
- package/src/ui/Input.tsx +178 -20
- package/src/ui/ListRow.tsx +12 -3
- package/src/ui/Page.tsx +12 -1
- package/src/ui/popup.ts +11 -2
- package/src/workspace-path.ts +35 -3
- package/src/chrome/ContentApiPopover.tsx +0 -215
- 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
|
-
|
|
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);
|
|
@@ -151,10 +163,24 @@ export function useChatSession(): ChatSessionValue {
|
|
|
151
163
|
return ctx;
|
|
152
164
|
}
|
|
153
165
|
|
|
166
|
+
/** False outside a chat provider. True while a turn is submitted or streaming. */
|
|
167
|
+
export function useChatBusy(): boolean {
|
|
168
|
+
const ctx = useContext(ChatSessionContext);
|
|
169
|
+
return ctx?.status === "streaming" || ctx?.status === "submitted";
|
|
170
|
+
}
|
|
171
|
+
|
|
154
172
|
// ---------------------------------------------------------------------------
|
|
155
173
|
|
|
156
174
|
const modelStorageKey = (ws: string) => `backmatter-chat-model:${ws}`;
|
|
157
175
|
|
|
176
|
+
function newestThread(threads: readonly { id: string; updatedAt: string }[]): string | null {
|
|
177
|
+
let best: { id: string; updatedAt: string } | null = null;
|
|
178
|
+
for (const thread of threads) {
|
|
179
|
+
if (!best || thread.updatedAt > best.updatedAt) best = thread;
|
|
180
|
+
}
|
|
181
|
+
return best?.id ?? null;
|
|
182
|
+
}
|
|
183
|
+
|
|
158
184
|
/** Apply tool statuses locally so the top bar moves before the next git poll. */
|
|
159
185
|
function publishTreePatches(workspaceId: string, patches: TreeStatusPatch[]) {
|
|
160
186
|
let published = false;
|
|
@@ -183,18 +209,40 @@ export function ChatProvider({
|
|
|
183
209
|
workspaceId,
|
|
184
210
|
startOpen = false,
|
|
185
211
|
pointer = false,
|
|
212
|
+
codeSessionId = null,
|
|
213
|
+
route = null,
|
|
214
|
+
viewed = null,
|
|
215
|
+
onRouteDismiss,
|
|
216
|
+
onCheckoutWrite,
|
|
217
|
+
onTurnSettled,
|
|
186
218
|
children,
|
|
187
219
|
}: {
|
|
188
220
|
workspaceId: string;
|
|
189
221
|
startOpen?: boolean;
|
|
190
222
|
/** Dev bar chat can point at elements on the host page. */
|
|
191
223
|
pointer?: boolean;
|
|
224
|
+
codeSessionId?: string | null;
|
|
225
|
+
route?: string | null;
|
|
226
|
+
/** Document for `route`, resolved by the caller. The agent still receives the URL. */
|
|
227
|
+
viewed?: DocRef | null;
|
|
228
|
+
onRouteDismiss?: (route: string) => void;
|
|
229
|
+
/** A file tool finished in the checkout. The preview does not hot-reload on its own. */
|
|
230
|
+
onCheckoutWrite?: () => void;
|
|
231
|
+
/** The reply finished, so a code session can read the edit set the turn just saved. */
|
|
232
|
+
onTurnSettled?: () => void;
|
|
192
233
|
children: React.ReactNode;
|
|
193
234
|
}) {
|
|
194
235
|
const [panel, dispatch] = useReducer(
|
|
195
236
|
panelReducer,
|
|
196
237
|
startOpen ? { ...initialPanelState, mode: "open" as const } : initialPanelState,
|
|
197
238
|
);
|
|
239
|
+
const dispatchPanel = useCallback(
|
|
240
|
+
(action: PanelAction) => {
|
|
241
|
+
if (!allowsPanelAction(action, Boolean(codeSessionId))) return;
|
|
242
|
+
dispatch(action);
|
|
243
|
+
},
|
|
244
|
+
[codeSessionId],
|
|
245
|
+
);
|
|
198
246
|
const [aiAvailable, setAiAvailable] = useState<boolean | null>(null);
|
|
199
247
|
const [models, setModels] = useState<AiModelInfo[]>([]);
|
|
200
248
|
const [tier, setTier] = useState<AiTier>("ask");
|
|
@@ -206,8 +254,10 @@ export function ChatProvider({
|
|
|
206
254
|
const [pointers, setPointers] = useState<ChatPointer[]>([]);
|
|
207
255
|
const [previewPointer, setPreviewPointer] = useState(false);
|
|
208
256
|
const [pointerHighlight, setPointerHighlight] = useState<"local" | "channel">("local");
|
|
209
|
-
const [includeFocus, setIncludeFocus] = useState(
|
|
257
|
+
const [includeFocus, setIncludeFocus] = useState(!codeSessionId);
|
|
210
258
|
const [threadsVersion, setThreadsVersion] = useState(0);
|
|
259
|
+
// Bumped when the person picks a chat, so a late session list cannot replace it.
|
|
260
|
+
const restore = useRef(0);
|
|
211
261
|
const { focus } = useEditorFocus();
|
|
212
262
|
|
|
213
263
|
// Models + availability.
|
|
@@ -234,15 +284,30 @@ export function ChatProvider({
|
|
|
234
284
|
};
|
|
235
285
|
}, [workspaceId]);
|
|
236
286
|
|
|
237
|
-
// Reset per-workspace session state.
|
|
287
|
+
// Reset per-workspace session state. A code session reopens its newest chat.
|
|
238
288
|
useEffect(() => {
|
|
239
289
|
setThreadId(null);
|
|
240
290
|
setFreshThread(false);
|
|
241
291
|
setPendingText(null);
|
|
242
292
|
setMentions([]);
|
|
243
293
|
setPointers([]);
|
|
244
|
-
setIncludeFocus(
|
|
245
|
-
|
|
294
|
+
setIncludeFocus(!codeSessionId);
|
|
295
|
+
if (!codeSessionId) return;
|
|
296
|
+
const token = ++restore.current;
|
|
297
|
+
fetchThreads(workspaceId, codeSessionId)
|
|
298
|
+
.then((res) => {
|
|
299
|
+
if (restore.current !== token) return;
|
|
300
|
+
const latest = newestThread(res.threads);
|
|
301
|
+
if (!latest) return;
|
|
302
|
+
setThreadId(latest);
|
|
303
|
+
})
|
|
304
|
+
.catch(() => {
|
|
305
|
+
// A missed list leaves the blank chat. Sending still starts one.
|
|
306
|
+
});
|
|
307
|
+
return () => {
|
|
308
|
+
restore.current += 1;
|
|
309
|
+
};
|
|
310
|
+
}, [workspaceId, codeSessionId]);
|
|
246
311
|
|
|
247
312
|
const setModel = useCallback(
|
|
248
313
|
(id: string) => {
|
|
@@ -252,14 +317,20 @@ export function ChatProvider({
|
|
|
252
317
|
[workspaceId],
|
|
253
318
|
);
|
|
254
319
|
|
|
255
|
-
const openThread = useCallback(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
320
|
+
const openThread = useCallback(
|
|
321
|
+
(thread: { id: string; codeSessionId?: string | null }) => {
|
|
322
|
+
if ((thread.codeSessionId ?? null) !== (codeSessionId ?? null)) return;
|
|
323
|
+
restore.current += 1;
|
|
324
|
+
setThreadId(thread.id);
|
|
325
|
+
setFreshThread(false);
|
|
326
|
+
setPendingText(null);
|
|
327
|
+
dispatch({ type: "showChat" });
|
|
328
|
+
},
|
|
329
|
+
[codeSessionId],
|
|
330
|
+
);
|
|
261
331
|
|
|
262
332
|
const newThread = useCallback(() => {
|
|
333
|
+
restore.current += 1;
|
|
263
334
|
setThreadId(null);
|
|
264
335
|
setFreshThread(false);
|
|
265
336
|
setPendingText(null);
|
|
@@ -275,8 +346,8 @@ export function ChatProvider({
|
|
|
275
346
|
setMentions((prev) => removeMentionFrom(prev, ref));
|
|
276
347
|
}, []);
|
|
277
348
|
const addPointer = useCallback((next: ChatPointer) => {
|
|
278
|
-
setPointers((prev) => addPointerTo(prev, next));
|
|
279
|
-
}, []);
|
|
349
|
+
setPointers((prev) => addPointerTo(prev, next, codeSessionId ? "replace" : "append"));
|
|
350
|
+
}, [codeSessionId]);
|
|
280
351
|
const removePointer = useCallback((next: ChatPointer) => {
|
|
281
352
|
setPointers((prev) => removePointerFrom(prev, next));
|
|
282
353
|
}, []);
|
|
@@ -289,28 +360,40 @@ export function ChatProvider({
|
|
|
289
360
|
selection: null,
|
|
290
361
|
mentions: [],
|
|
291
362
|
pointers: [],
|
|
363
|
+
route: null,
|
|
292
364
|
}));
|
|
293
|
-
contextRef.current = () =>
|
|
365
|
+
contextRef.current = () =>
|
|
366
|
+
codeSessionId
|
|
367
|
+
? codeSessionChatContext({ pointers, route })
|
|
368
|
+
: buildChatContext({ focus, includeFocus, mentions, pointers });
|
|
294
369
|
const modelRef = useRef<string | null>(model);
|
|
295
370
|
modelRef.current = model;
|
|
296
371
|
|
|
297
372
|
// Creating a thread lazily on first send.
|
|
298
373
|
const startThread = useCallback(
|
|
299
374
|
async (text: string) => {
|
|
300
|
-
|
|
375
|
+
restore.current += 1;
|
|
376
|
+
const res = await createThread(
|
|
377
|
+
workspaceId,
|
|
378
|
+
codeSessionId ? { codeSessionId } : undefined,
|
|
379
|
+
);
|
|
301
380
|
setPendingText(text);
|
|
302
381
|
setFreshThread(true);
|
|
303
382
|
setThreadId(res.thread.id);
|
|
304
383
|
bumpThreads();
|
|
305
384
|
},
|
|
306
|
-
[workspaceId, bumpThreads],
|
|
385
|
+
[workspaceId, bumpThreads, codeSessionId],
|
|
307
386
|
);
|
|
308
387
|
|
|
388
|
+
const dismissRoute = useCallback(() => {
|
|
389
|
+
if (route && onRouteDismiss) onRouteDismiss(route);
|
|
390
|
+
}, [route, onRouteDismiss]);
|
|
391
|
+
|
|
309
392
|
const ui = useMemo<ChatUIValue>(
|
|
310
393
|
() => ({
|
|
311
394
|
workspaceId,
|
|
312
395
|
panel,
|
|
313
|
-
dispatch,
|
|
396
|
+
dispatch: dispatchPanel,
|
|
314
397
|
aiAvailable,
|
|
315
398
|
models,
|
|
316
399
|
tier,
|
|
@@ -335,10 +418,15 @@ export function ChatProvider({
|
|
|
335
418
|
focus,
|
|
336
419
|
threadsVersion,
|
|
337
420
|
bumpThreads,
|
|
421
|
+
codeSessionId,
|
|
422
|
+
route,
|
|
423
|
+
viewed,
|
|
424
|
+
dismissRoute,
|
|
338
425
|
}),
|
|
339
426
|
[
|
|
340
427
|
workspaceId,
|
|
341
428
|
panel,
|
|
429
|
+
dispatchPanel,
|
|
342
430
|
aiAvailable,
|
|
343
431
|
models,
|
|
344
432
|
tier,
|
|
@@ -361,6 +449,10 @@ export function ChatProvider({
|
|
|
361
449
|
focus,
|
|
362
450
|
threadsVersion,
|
|
363
451
|
bumpThreads,
|
|
452
|
+
codeSessionId,
|
|
453
|
+
route,
|
|
454
|
+
viewed,
|
|
455
|
+
dismissRoute,
|
|
364
456
|
],
|
|
365
457
|
);
|
|
366
458
|
|
|
@@ -380,13 +472,15 @@ export function ChatProvider({
|
|
|
380
472
|
getContext={() => contextRef.current()}
|
|
381
473
|
getModel={() => modelRef.current}
|
|
382
474
|
onThreadTouched={bumpThreads}
|
|
475
|
+
onCheckoutWrite={onCheckoutWrite}
|
|
476
|
+
onTurnSettled={onTurnSettled}
|
|
383
477
|
>
|
|
384
478
|
{scoped}
|
|
385
479
|
</ChatSession>
|
|
386
480
|
) : (
|
|
387
481
|
<EmptySession onFirstSend={startThread}>{scoped}</EmptySession>
|
|
388
482
|
),
|
|
389
|
-
[threadId, workspaceId, freshThread, pendingText, bumpThreads, startThread],
|
|
483
|
+
[threadId, workspaceId, freshThread, pendingText, bumpThreads, startThread, onCheckoutWrite, onTurnSettled],
|
|
390
484
|
);
|
|
391
485
|
|
|
392
486
|
return (
|
|
@@ -461,6 +555,8 @@ type SessionProps = {
|
|
|
461
555
|
getContext: () => ChatRequestContext;
|
|
462
556
|
getModel: () => string | null;
|
|
463
557
|
onThreadTouched: () => void;
|
|
558
|
+
onCheckoutWrite?: () => void;
|
|
559
|
+
onTurnSettled?: () => void;
|
|
464
560
|
children: React.ReactNode;
|
|
465
561
|
};
|
|
466
562
|
|
|
@@ -539,6 +635,8 @@ function LiveSession({
|
|
|
539
635
|
getContext,
|
|
540
636
|
getModel,
|
|
541
637
|
onThreadTouched,
|
|
638
|
+
onCheckoutWrite,
|
|
639
|
+
onTurnSettled,
|
|
542
640
|
children,
|
|
543
641
|
}: SessionProps & {
|
|
544
642
|
thread: ChatThread | null;
|
|
@@ -574,7 +672,10 @@ function LiveSession({
|
|
|
574
672
|
id: threadId,
|
|
575
673
|
messages: initialMessages,
|
|
576
674
|
transport,
|
|
577
|
-
onFinish: () =>
|
|
675
|
+
onFinish: () => {
|
|
676
|
+
onThreadTouched();
|
|
677
|
+
onTurnSettled?.();
|
|
678
|
+
},
|
|
578
679
|
});
|
|
579
680
|
|
|
580
681
|
const { messages, status, sendMessage } = chat;
|
|
@@ -632,6 +733,7 @@ function LiveSession({
|
|
|
632
733
|
if (streaming && write && ref) editingKey = docKey(ref);
|
|
633
734
|
if (part.state === "output-available" && !seen.has(part.toolCallId)) {
|
|
634
735
|
seen.add(part.toolCallId);
|
|
736
|
+
if (checkoutWriteReloads(name, part.output)) onCheckoutWrite?.();
|
|
635
737
|
const patches = treeStatusPatches(part.output);
|
|
636
738
|
if (patches.length > 0) publishTreePatches(workspaceId, patches);
|
|
637
739
|
const found = write ? changesInToolOutput(part.output) : [];
|
|
@@ -650,7 +752,7 @@ function LiveSession({
|
|
|
650
752
|
}
|
|
651
753
|
}
|
|
652
754
|
setAiEditingDocKey(streaming ? editingKey : null);
|
|
653
|
-
}, [messages, status, setAiEditingDocKey, workspaceId]);
|
|
755
|
+
}, [messages, status, setAiEditingDocKey, workspaceId, onCheckoutWrite]);
|
|
654
756
|
|
|
655
757
|
useEffect(() => () => setAiEditingDocKey(null), [setAiEditingDocKey]);
|
|
656
758
|
|
package/src/chat/Composer.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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";
|
|
6
|
+
import { useCodeSessionContext } from "../pages/code-session-context";
|
|
6
7
|
import { Tooltip } from "../ui/Tooltip";
|
|
7
8
|
import { useChatSession, useChatUI } from "./ChatProvider";
|
|
8
9
|
import { MentionPopover, type MentionKey } from "./MentionPopover";
|
|
@@ -26,6 +27,27 @@ function isMentionKey(key: string): key is MentionKey {
|
|
|
26
27
|
|
|
27
28
|
export { POINTER_ARM_EVENT } from "./pointer-arm";
|
|
28
29
|
|
|
30
|
+
/** The page open in the code session preview. Same row as the editor's open document. */
|
|
31
|
+
export function RouteChip({
|
|
32
|
+
route,
|
|
33
|
+
doc,
|
|
34
|
+
onDismiss,
|
|
35
|
+
}: {
|
|
36
|
+
route: string;
|
|
37
|
+
doc?: { type: string; slug: string } | null;
|
|
38
|
+
onDismiss: () => void;
|
|
39
|
+
}) {
|
|
40
|
+
return (
|
|
41
|
+
<ContextRow
|
|
42
|
+
icon={<FileIcon size={13} />}
|
|
43
|
+
{...(doc ? { type: doc.type, slug: doc.slug } : { label: route })}
|
|
44
|
+
meta="in preview"
|
|
45
|
+
removeLabel="Remove the page from context"
|
|
46
|
+
onRemove={onDismiss}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
function postPointer(on: boolean) {
|
|
30
52
|
if (typeof window === "undefined") return;
|
|
31
53
|
window.dispatchEvent(new CustomEvent(POINTER_ARM_EVENT, { detail: { on } }));
|
|
@@ -43,15 +65,25 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
|
|
|
43
65
|
const ref = useRef<HTMLTextAreaElement>(null);
|
|
44
66
|
const boxRef = useRef<HTMLDivElement>(null);
|
|
45
67
|
const mentionKeys = useRef<((key: MentionKey) => void) | null>(null);
|
|
46
|
-
const
|
|
68
|
+
const updating = useCodeSessionContext()?.updating === true;
|
|
69
|
+
const busy = session.status === "streaming" || session.status === "submitted" || updating;
|
|
47
70
|
const canSend = text.trim().length > 0 && !busy && ui.aiAvailable === true;
|
|
48
71
|
|
|
49
|
-
// Auto-grow.
|
|
50
|
-
|
|
72
|
+
// Auto-grow. Measuring must not leave a 1px swing: that resizes the thread
|
|
73
|
+
// on every key, and the thread then tries to scroll to the bottom.
|
|
74
|
+
useLayoutEffect(() => {
|
|
51
75
|
const el = ref.current;
|
|
52
76
|
if (!el) return;
|
|
77
|
+
const previous = el.offsetHeight;
|
|
78
|
+
el.style.overflowY = "hidden";
|
|
53
79
|
el.style.height = "0px";
|
|
54
|
-
|
|
80
|
+
const next = Math.min(el.scrollHeight, 180);
|
|
81
|
+
if (previous > 0 && Math.abs(next - previous) <= 1) {
|
|
82
|
+
el.style.height = `${previous}px`;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
el.style.height = `${next}px`;
|
|
86
|
+
if (next >= 180) el.style.overflowY = "auto";
|
|
55
87
|
}, [text]);
|
|
56
88
|
|
|
57
89
|
useEffect(() => {
|
|
@@ -138,10 +170,12 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
|
|
|
138
170
|
setMention(null);
|
|
139
171
|
}
|
|
140
172
|
|
|
141
|
-
const
|
|
173
|
+
const sessionChat = Boolean(ui.codeSessionId);
|
|
174
|
+
const focusChip = !sessionChat && ui.focus && ui.includeFocus ? ui.focus.docRef : null;
|
|
175
|
+
const pageChip = sessionChat && ui.route ? ui.route : null;
|
|
142
176
|
const selectionActive =
|
|
143
177
|
ui.focus?.selection && ui.focus.selection.end > ui.focus.selection.start;
|
|
144
|
-
const hasContext = Boolean(focusChip || ui.mentions.length > 0);
|
|
178
|
+
const hasContext = Boolean(focusChip || pageChip || ui.mentions.length > 0);
|
|
145
179
|
|
|
146
180
|
return (
|
|
147
181
|
<div {...stylex.props(styles.wrap)}>
|
|
@@ -168,6 +202,13 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
|
|
|
168
202
|
onRemove={() => ui.setIncludeFocus(false)}
|
|
169
203
|
/>
|
|
170
204
|
) : null}
|
|
205
|
+
{pageChip ? (
|
|
206
|
+
<RouteChip
|
|
207
|
+
route={pageChip}
|
|
208
|
+
doc={ui.viewed}
|
|
209
|
+
onDismiss={() => ui.dismissRoute()}
|
|
210
|
+
/>
|
|
211
|
+
) : null}
|
|
171
212
|
{ui.mentions.map((m) => (
|
|
172
213
|
<ContextRow
|
|
173
214
|
key={docKey(m)}
|
|
@@ -232,6 +273,7 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
|
|
|
232
273
|
{...stylex.props(styles.textarea)}
|
|
233
274
|
/>
|
|
234
275
|
<div {...stylex.props(styles.footer)}>
|
|
276
|
+
{!sessionChat ? (
|
|
235
277
|
<Tooltip label="Mention a document (@)">
|
|
236
278
|
<button
|
|
237
279
|
type="button"
|
|
@@ -257,6 +299,7 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
|
|
|
257
299
|
<AtIcon />
|
|
258
300
|
</button>
|
|
259
301
|
</Tooltip>
|
|
302
|
+
) : null}
|
|
260
303
|
<span {...stylex.props(styles.spacer)} />
|
|
261
304
|
{ui.pointer ? (
|
|
262
305
|
<Tooltip label={pointing ? "Stop pointing" : "Point at an element on the page"}>
|
|
@@ -276,7 +319,7 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
|
|
|
276
319
|
</button>
|
|
277
320
|
</Tooltip>
|
|
278
321
|
) : null}
|
|
279
|
-
{ui.focus ? (
|
|
322
|
+
{!sessionChat && ui.focus ? (
|
|
280
323
|
<Tooltip
|
|
281
324
|
label={
|
|
282
325
|
ui.includeFocus
|
package/src/chat/PointerChip.tsx
CHANGED
|
@@ -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.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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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>
|
package/src/chat/chat-context.ts
CHANGED
|
@@ -14,6 +14,14 @@ export type ChatPointer = {
|
|
|
14
14
|
blockType: string | null;
|
|
15
15
|
blockIndex: number | null;
|
|
16
16
|
quote: string;
|
|
17
|
+
/** React component that rendered the element, when the dev server exposes it. */
|
|
18
|
+
component?: string | null;
|
|
19
|
+
/** Checkout-relative source path. */
|
|
20
|
+
file?: string | null;
|
|
21
|
+
line?: number | null;
|
|
22
|
+
column?: number | null;
|
|
23
|
+
/** Component names from the element toward the layout. */
|
|
24
|
+
stack?: string[];
|
|
17
25
|
};
|
|
18
26
|
|
|
19
27
|
export const POINTER_CAP = 8;
|
|
@@ -24,14 +32,23 @@ export type ChatRequestContext = {
|
|
|
24
32
|
selection: { start: number; end: number } | null;
|
|
25
33
|
mentions: DocRef[];
|
|
26
34
|
pointers: ChatPointer[];
|
|
35
|
+
route: string | null;
|
|
27
36
|
};
|
|
28
37
|
|
|
38
|
+
function parseChatRoute(raw: unknown): string | null {
|
|
39
|
+
if (typeof raw !== "string") return null;
|
|
40
|
+
return raw.startsWith("/") && !raw.startsWith("//") ? raw : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
function samePointer(a: ChatPointer, b: ChatPointer): boolean {
|
|
30
44
|
return (
|
|
31
45
|
a.type === b.type &&
|
|
32
46
|
a.slug === b.slug &&
|
|
33
47
|
a.blockId === b.blockId &&
|
|
34
|
-
a.quote === b.quote
|
|
48
|
+
a.quote === b.quote &&
|
|
49
|
+
(a.component ?? null) === (b.component ?? null) &&
|
|
50
|
+
(a.file ?? null) === (b.file ?? null) &&
|
|
51
|
+
(a.line ?? null) === (b.line ?? null)
|
|
35
52
|
);
|
|
36
53
|
}
|
|
37
54
|
|
|
@@ -49,7 +66,12 @@ export function pointersFromMetadata(metadata: unknown): ChatPointer[] {
|
|
|
49
66
|
return pointers.slice(0, POINTER_CAP);
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
export function addPointer(
|
|
69
|
+
export function addPointer(
|
|
70
|
+
list: ChatPointer[],
|
|
71
|
+
next: ChatPointer,
|
|
72
|
+
mode: "append" | "replace" = "append",
|
|
73
|
+
): ChatPointer[] {
|
|
74
|
+
if (mode === "replace") return [next];
|
|
53
75
|
if (list.some((item) => samePointer(item, next))) return list;
|
|
54
76
|
const added = [...list, next];
|
|
55
77
|
return added.length > POINTER_CAP ? added.slice(added.length - POINTER_CAP) : added;
|
|
@@ -80,14 +102,50 @@ export function chatPointerFromGrab(
|
|
|
80
102
|
blockType: value.blockType,
|
|
81
103
|
blockIndex: value.blockIndex,
|
|
82
104
|
quote: value.quote.replace(/\s+/g, " ").trim().slice(0, 240),
|
|
105
|
+
...placeFrom(value),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function placeFrom(value: Record<string, unknown>): Pick<ChatPointer, "component" | "file" | "line" | "column" | "stack"> {
|
|
110
|
+
const component = typeof value.component === "string" && value.component ? value.component.slice(0, 80) : null;
|
|
111
|
+
const file =
|
|
112
|
+
typeof value.file === "string" && /^[\w./@+-]+\.(?:tsx|ts|jsx|js|mdx)$/.test(value.file) && !value.file.includes("..")
|
|
113
|
+
? value.file.slice(0, 240)
|
|
114
|
+
: null;
|
|
115
|
+
const line = typeof value.line === "number" && Number.isInteger(value.line) && value.line > 0 ? value.line : null;
|
|
116
|
+
const column = typeof value.column === "number" && Number.isInteger(value.column) && value.column > 0 ? value.column : null;
|
|
117
|
+
const stack = Array.isArray(value.stack)
|
|
118
|
+
? value.stack.filter((item): item is string => typeof item === "string" && item.length > 0).slice(0, 4)
|
|
119
|
+
: [];
|
|
120
|
+
return {
|
|
121
|
+
...(component ? { component } : {}),
|
|
122
|
+
...(file ? { file } : {}),
|
|
123
|
+
...(line ? { line } : {}),
|
|
124
|
+
...(column ? { column } : {}),
|
|
125
|
+
...(stack.length > 0 ? { stack } : {}),
|
|
83
126
|
};
|
|
84
127
|
}
|
|
85
128
|
|
|
129
|
+
/** A code session sends the route and the pointed blocks, not the open document. */
|
|
130
|
+
export function codeSessionChatContext(input: {
|
|
131
|
+
pointers: ChatPointer[];
|
|
132
|
+
route?: string | null;
|
|
133
|
+
}): ChatRequestContext {
|
|
134
|
+
return buildChatContext({
|
|
135
|
+
focus: null,
|
|
136
|
+
includeFocus: false,
|
|
137
|
+
mentions: [],
|
|
138
|
+
pointers: input.pointers,
|
|
139
|
+
route: input.route,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
86
143
|
export function buildChatContext(input: {
|
|
87
144
|
focus: EditorFocus | null;
|
|
88
145
|
includeFocus: boolean;
|
|
89
146
|
mentions: DocRef[];
|
|
90
147
|
pointers: ChatPointer[];
|
|
148
|
+
route?: string | null;
|
|
91
149
|
}): ChatRequestContext {
|
|
92
150
|
const focusDoc = input.includeFocus && input.focus ? input.focus.docRef : null;
|
|
93
151
|
const selection =
|
|
@@ -105,7 +163,13 @@ export function buildChatContext(input: {
|
|
|
105
163
|
if (pointers.some((item) => samePointer(item, pointer))) continue;
|
|
106
164
|
pointers.push(pointer);
|
|
107
165
|
}
|
|
108
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
focusDoc,
|
|
168
|
+
selection,
|
|
169
|
+
mentions,
|
|
170
|
+
pointers: pointers.slice(-POINTER_CAP),
|
|
171
|
+
route: parseChatRoute(input.route),
|
|
172
|
+
};
|
|
109
173
|
}
|
|
110
174
|
|
|
111
175
|
export function addMention(list: DocRef[], ref: DocRef): DocRef[] {
|
package/src/chat/chat-helpers.ts
CHANGED
|
@@ -10,6 +10,18 @@ export type WriteToolOutput =
|
|
|
10
10
|
| { ok: true; change?: ChatChangeSummary; changes?: ChatChangeSummary[] }
|
|
11
11
|
| { ok: false; error: string; message: string };
|
|
12
12
|
|
|
13
|
+
export function isCheckoutWriteTool(name: string) {
|
|
14
|
+
return name === "writeFile" || name === "editFile" || name === "deleteFile";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A finished checkout tool changed the host, so the preview should load again. */
|
|
18
|
+
export function checkoutWriteReloads(name: string, output: unknown): boolean {
|
|
19
|
+
const body = output as { ok?: boolean } | undefined;
|
|
20
|
+
if (isCheckoutWriteTool(name)) return !body || body.ok !== false;
|
|
21
|
+
if (name === "runCommand") return body?.ok === true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
export function isWriteTool(name: string) {
|
|
14
26
|
return (
|
|
15
27
|
name === "writeDoc" ||
|
package/src/chat/chat-state.ts
CHANGED
|
@@ -57,3 +57,14 @@ export function panelReducer(state: PanelState, action: PanelAction): PanelState
|
|
|
57
57
|
export function isPanelVisible(mode: PanelMode): boolean {
|
|
58
58
|
return mode === "open" || mode === "fullscreen";
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
/** A docked column has no window chrome: it cannot close, minimize, or go fullscreen. */
|
|
62
|
+
export function allowsPanelAction(action: PanelAction, docked: boolean): boolean {
|
|
63
|
+
if (!docked) return true;
|
|
64
|
+
return (
|
|
65
|
+
action.type !== "toggle" &&
|
|
66
|
+
action.type !== "close" &&
|
|
67
|
+
action.type !== "minimize" &&
|
|
68
|
+
action.type !== "toggleFullscreen"
|
|
69
|
+
);
|
|
70
|
+
}
|