@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backmatter/studio",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
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.34",
|
|
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/WorkspaceRoutes.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// a site Studio mounts the same tree under its base path.
|
|
4
4
|
import { Route } from "react-router";
|
|
5
5
|
import { WorkspaceLayout } from "./pages/WorkspaceLayout";
|
|
6
|
-
import {
|
|
6
|
+
import { WorkspaceHomePage } from "./pages/WorkspaceHomePage";
|
|
7
7
|
import { TypeDocsPage } from "./pages/TypeDocsPage";
|
|
8
8
|
import { SettingsGeneralPage } from "./pages/SettingsGeneralPage";
|
|
9
9
|
import { SettingsUsagePage } from "./pages/SettingsUsagePage";
|
|
@@ -15,6 +15,7 @@ import { SettingsUsersPage } from "./pages/SettingsUsersPage";
|
|
|
15
15
|
import { GithubConnectPage } from "./pages/GithubConnectPage";
|
|
16
16
|
import { DocEditorPage } from "./pages/DocEditorPage";
|
|
17
17
|
import { MediaGalleryPage } from "./pages/MediaGalleryPage";
|
|
18
|
+
import CodeSessionPage from "./pages/CodeSessionPage";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Returns `<Route>` elements. Spread the result inside a `<Routes>`; react-router
|
|
@@ -23,8 +24,10 @@ import { MediaGalleryPage } from "./pages/MediaGalleryPage";
|
|
|
23
24
|
export function workspaceRoutes() {
|
|
24
25
|
return (
|
|
25
26
|
<Route path="/:workspaceSlug" element={<WorkspaceLayout />}>
|
|
26
|
-
<Route index element={<
|
|
27
|
+
<Route index element={<WorkspaceHomePage />} />
|
|
28
|
+
<Route path="content" element={<TypeDocsPage />} />
|
|
27
29
|
<Route path="content/:type" element={<TypeDocsPage />} />
|
|
30
|
+
<Route path="code" element={<CodeSessionPage />} />
|
|
28
31
|
<Route element={<SettingsLayout />}>
|
|
29
32
|
<Route path="settings" element={<SettingsIndexRedirect />} />
|
|
30
33
|
<Route path="settings/general" element={<SettingsGeneralPage />} />
|
package/src/api.ts
CHANGED
|
@@ -23,6 +23,7 @@ async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
|
|
23
23
|
const body = (await res.json().catch(() => ({}))) as {
|
|
24
24
|
error?: string;
|
|
25
25
|
details?: string[];
|
|
26
|
+
paths?: string[];
|
|
26
27
|
};
|
|
27
28
|
if (res.status === 401 || res.status === 426) reportUnauthorized(res.status, body.error);
|
|
28
29
|
const code = body.error ?? `request failed: ${res.status}`;
|
|
@@ -33,7 +34,7 @@ async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
|
|
33
34
|
const err = new Error(`${code}${detailSuffix}`) as ApiError;
|
|
34
35
|
err.status = res.status;
|
|
35
36
|
err.code = body.error;
|
|
36
|
-
err.details = body.details;
|
|
37
|
+
err.details = body.details ?? body.paths;
|
|
37
38
|
throw err;
|
|
38
39
|
}
|
|
39
40
|
return res.json() as Promise<T>;
|
|
@@ -355,6 +356,11 @@ export type SchemaField = {
|
|
|
355
356
|
fields?: Record<string, SchemaField>;
|
|
356
357
|
/** Studio input name from `field.text({ input })`; see `defineStudio`. */
|
|
357
358
|
input?: string;
|
|
359
|
+
options?: string[];
|
|
360
|
+
min?: number;
|
|
361
|
+
max?: number;
|
|
362
|
+
step?: number;
|
|
363
|
+
where?: Record<string, string | number | boolean>;
|
|
358
364
|
};
|
|
359
365
|
|
|
360
366
|
export type BlockSnapshot = {
|
|
@@ -371,6 +377,8 @@ export type SchemaPayload = {
|
|
|
371
377
|
singleton?: boolean;
|
|
372
378
|
indexSlug?: string;
|
|
373
379
|
format?: "mdx" | "page";
|
|
380
|
+
external?: boolean;
|
|
381
|
+
template?: string;
|
|
374
382
|
fields: Record<string, SchemaField>;
|
|
375
383
|
}>;
|
|
376
384
|
blocks?: BlockSnapshot[];
|
|
@@ -400,10 +408,21 @@ export type GitChange = {
|
|
|
400
408
|
type: string;
|
|
401
409
|
slug: string;
|
|
402
410
|
status: ChangeStatus;
|
|
411
|
+
/** Working-tree edit time. Present on git status; absent when derived from the document list. */
|
|
412
|
+
updatedAt?: string;
|
|
403
413
|
};
|
|
404
414
|
|
|
405
415
|
export type GitHead = { sha: string; updatedAt: string };
|
|
406
416
|
|
|
417
|
+
/** A commit on the branch, newest first. `cms` means Studio wrote it. */
|
|
418
|
+
export type RecentCommit = {
|
|
419
|
+
sha: string;
|
|
420
|
+
message: string;
|
|
421
|
+
author: string;
|
|
422
|
+
committedAt: string;
|
|
423
|
+
cms: boolean;
|
|
424
|
+
};
|
|
425
|
+
|
|
407
426
|
/** `git status` for the current user on their checked-out branch. */
|
|
408
427
|
export type GitState = {
|
|
409
428
|
defaultBranch: string;
|
|
@@ -413,6 +432,10 @@ export type GitState = {
|
|
|
413
432
|
githubError?: "installation_missing" | "github_list_failed" | null;
|
|
414
433
|
head: GitHead | null;
|
|
415
434
|
changes: GitChange[];
|
|
435
|
+
/** Last git status list, including deletions and edit times. */
|
|
436
|
+
fetchedChanges?: GitChange[];
|
|
437
|
+
/** Latest commits on the branch, once git status has been fetched. */
|
|
438
|
+
recentCommits?: RecentCommit[];
|
|
416
439
|
};
|
|
417
440
|
|
|
418
441
|
export function countConflicts(state: Pick<GitState, "changes"> | null | undefined) {
|
|
@@ -466,6 +489,46 @@ export function commitWorkspace(workspaceId: string, message?: string) {
|
|
|
466
489
|
});
|
|
467
490
|
}
|
|
468
491
|
|
|
492
|
+
export type CodeSessionEditList = {
|
|
493
|
+
files: Array<{ path: string; deleted: boolean }>;
|
|
494
|
+
skipped: Array<{ path: string; reason: string }>;
|
|
495
|
+
foldPending: boolean;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
export function fetchCodeSessionEdits(workspaceId: string, sessionId: string) {
|
|
499
|
+
return api<CodeSessionEditList>(
|
|
500
|
+
`/workspaces/${workspaceId}/code-sessions/${sessionId}/edits`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function updateCodeSessionBranch(workspaceId: string, sessionId: string) {
|
|
505
|
+
return api<{ ok: true; baseSha: string }>(
|
|
506
|
+
`/workspaces/${workspaceId}/code-sessions/${sessionId}/update`,
|
|
507
|
+
{ method: "POST" },
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function clearCodeSessionEdits(workspaceId: string, sessionId: string) {
|
|
512
|
+
return api<{ ok: true; baseSha: string }>(
|
|
513
|
+
`/workspaces/${workspaceId}/code-sessions/${sessionId}/clear-edits`,
|
|
514
|
+
{ method: "POST" },
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export function commitCodeSessionEdits(
|
|
519
|
+
workspaceId: string,
|
|
520
|
+
sessionId: string,
|
|
521
|
+
message?: string,
|
|
522
|
+
) {
|
|
523
|
+
return api<{ ok: true; commitSha: string; foldErrors: string[] }>(
|
|
524
|
+
`/workspaces/${workspaceId}/code-sessions/${sessionId}/commit`,
|
|
525
|
+
{
|
|
526
|
+
method: "POST",
|
|
527
|
+
body: JSON.stringify(message?.trim() ? { message: message.trim() } : {}),
|
|
528
|
+
},
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
469
532
|
/** Default commit message the server uses when none is given (mirrored for the placeholder). */
|
|
470
533
|
export function defaultCommitMessage(changes: GitChange[]) {
|
|
471
534
|
if (changes.length === 1) {
|
|
@@ -701,6 +764,8 @@ export type ChatThread = {
|
|
|
701
764
|
workspaceId: string;
|
|
702
765
|
userId: string;
|
|
703
766
|
branch: string;
|
|
767
|
+
/** Null for an editor thread; a code session id for a session thread. */
|
|
768
|
+
codeSessionId?: string | null;
|
|
704
769
|
title: string;
|
|
705
770
|
createdAt: string;
|
|
706
771
|
updatedAt: string;
|
|
@@ -728,10 +793,11 @@ export type ChatChangeSummary = {
|
|
|
728
793
|
workspaceId: string;
|
|
729
794
|
userId: string;
|
|
730
795
|
branch: string;
|
|
731
|
-
resourceKind: "content";
|
|
796
|
+
resourceKind: "content" | "code";
|
|
797
|
+
path: string | null;
|
|
732
798
|
type: string;
|
|
733
799
|
slug: string;
|
|
734
|
-
op: "write" | "create";
|
|
800
|
+
op: "write" | "create" | "delete";
|
|
735
801
|
beforeSource: ContentSource | null;
|
|
736
802
|
diff: ChatChangeDiff | null;
|
|
737
803
|
status: ChatChangeStatus;
|
|
@@ -740,14 +806,23 @@ export type ChatChangeSummary = {
|
|
|
740
806
|
summary?: string;
|
|
741
807
|
};
|
|
742
808
|
|
|
743
|
-
export function fetchThreads(workspaceId: string) {
|
|
744
|
-
|
|
809
|
+
export function fetchThreads(workspaceId: string, codeSessionId?: string) {
|
|
810
|
+
const query = codeSessionId
|
|
811
|
+
? `?codeSessionId=${encodeURIComponent(codeSessionId)}`
|
|
812
|
+
: "";
|
|
813
|
+
return api<{ threads: ChatThread[] }>(`/workspaces/${workspaceId}/ai/threads${query}`);
|
|
745
814
|
}
|
|
746
815
|
|
|
747
|
-
export function createThread(
|
|
816
|
+
export function createThread(
|
|
817
|
+
workspaceId: string,
|
|
818
|
+
opts?: { title?: string; codeSessionId?: string },
|
|
819
|
+
) {
|
|
820
|
+
const body: { title?: string; codeSessionId?: string } = {};
|
|
821
|
+
if (opts?.title) body.title = opts.title;
|
|
822
|
+
if (opts?.codeSessionId) body.codeSessionId = opts.codeSessionId;
|
|
748
823
|
return api<{ thread: ChatThread }>(`/workspaces/${workspaceId}/ai/threads`, {
|
|
749
824
|
method: "POST",
|
|
750
|
-
body: JSON.stringify(
|
|
825
|
+
body: JSON.stringify(body),
|
|
751
826
|
});
|
|
752
827
|
}
|
|
753
828
|
|
package/src/chat/ChangeCard.tsx
CHANGED
|
@@ -28,8 +28,14 @@ export function ChangeCard({
|
|
|
28
28
|
}) {
|
|
29
29
|
const [expanded, setExpanded] = useState(false);
|
|
30
30
|
const nav = useWorkspaceNav();
|
|
31
|
-
const label =
|
|
32
|
-
|
|
31
|
+
const label =
|
|
32
|
+
change.resourceKind === "code"
|
|
33
|
+
? (change.path ?? "")
|
|
34
|
+
: `${change.type}/${change.slug}`;
|
|
35
|
+
const to =
|
|
36
|
+
change.resourceKind === "code" || !nav
|
|
37
|
+
? "#"
|
|
38
|
+
: contentDocPath(nav.workspaceSlug, change.type, change.slug);
|
|
33
39
|
const status = change.status;
|
|
34
40
|
|
|
35
41
|
return (
|
package/src/chat/ChatMessage.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// packages/studio/src/chat/ChatMessage.tsx
|
|
2
|
-
import { useState, type ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { useState, type ComponentPropsWithoutRef, type ReactNode } from "react";
|
|
3
3
|
import Markdown from "react-markdown";
|
|
4
4
|
import * as stylex from "@stylexjs/stylex";
|
|
5
5
|
import { getToolName, isToolUIPart, type UIMessage } from "ai";
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
changesInToolOutput,
|
|
23
23
|
type WriteToolOutput,
|
|
24
24
|
} from "./chat-helpers";
|
|
25
|
-
import { CheckIcon, EyeIcon, FileIcon, SearchIcon, UndoIcon } from "./icons";
|
|
25
|
+
import { CheckIcon, ChevronDownIcon, EyeIcon, FileIcon, SearchIcon, UndoIcon } from "./icons";
|
|
26
26
|
import { pointersFromMetadata } from "./chat-context";
|
|
27
27
|
import { PointerChip, pointerKey } from "./PointerChip";
|
|
28
28
|
import { Reveal } from "./Reveal";
|
|
@@ -79,6 +79,51 @@ function ToolIcon({ name }: { name: string }) {
|
|
|
79
79
|
return <FileIcon size={12} />;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
type TraceRow = {
|
|
83
|
+
key: string;
|
|
84
|
+
name: string;
|
|
85
|
+
label: string;
|
|
86
|
+
failed: boolean;
|
|
87
|
+
error: string | null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function ToolTrace({ rows }: { rows: TraceRow[] }) {
|
|
91
|
+
const [open, setOpen] = useState(false);
|
|
92
|
+
if (rows.length === 0) return null;
|
|
93
|
+
const list = rows.map((row) => (
|
|
94
|
+
<div key={row.key} {...stylex.props(styles.toolRow, row.failed && styles.toolFailed)}>
|
|
95
|
+
<ToolIcon name={row.name} />
|
|
96
|
+
<span>{row.label}</span>
|
|
97
|
+
{row.error ? <span {...stylex.props(styles.toolError)}>{row.error}</span> : null}
|
|
98
|
+
</div>
|
|
99
|
+
));
|
|
100
|
+
|
|
101
|
+
const failed = rows.filter((row) => row.failed).length;
|
|
102
|
+
const summary = rows.length === 1 ? "1 step" : `${rows.length} steps`;
|
|
103
|
+
return (
|
|
104
|
+
<div {...stylex.props(styles.toolBlock)}>
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
aria-expanded={open}
|
|
108
|
+
aria-label={open ? `Hide ${summary}` : `Show ${summary}`}
|
|
109
|
+
onClick={() => setOpen((value) => !value)}
|
|
110
|
+
{...stylex.props(styles.traceToggle)}
|
|
111
|
+
>
|
|
112
|
+
<span {...stylex.props(styles.traceChevron, open && styles.traceChevronOpen)}>
|
|
113
|
+
<ChevronDownIcon />
|
|
114
|
+
</span>
|
|
115
|
+
<span>{summary}</span>
|
|
116
|
+
{failed > 0 ? (
|
|
117
|
+
<span {...stylex.props(styles.toolFailed)}>
|
|
118
|
+
{failed === 1 ? "1 failed" : `${failed} failed`}
|
|
119
|
+
</span>
|
|
120
|
+
) : null}
|
|
121
|
+
</button>
|
|
122
|
+
{open ? list : null}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
82
127
|
const md = {
|
|
83
128
|
p: (p: ComponentPropsWithoutRef<"p">) => <p {...p} {...stylex.props(mdStyles.p)} />,
|
|
84
129
|
ul: (p: ComponentPropsWithoutRef<"ul">) => <ul {...p} {...stylex.props(mdStyles.list)} />,
|
|
@@ -213,108 +258,107 @@ export function ChatMessage({
|
|
|
213
258
|
);
|
|
214
259
|
}
|
|
215
260
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
<Reveal key={`${i}-${j}`} active={streaming} origin="start">
|
|
236
|
-
<div {...stylex.props(styles.markdown)}>
|
|
237
|
-
<Markdown components={md}>{seg.text}</Markdown>
|
|
238
|
-
</div>
|
|
239
|
-
</Reveal>
|
|
240
|
-
);
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
if (part.type === "reasoning") return null;
|
|
244
|
-
if (isToolUIPart(part)) {
|
|
245
|
-
const tp = part as ToolPart;
|
|
246
|
-
if (tp.state !== "output-available" && tp.state !== "output-error") return null;
|
|
247
|
-
const name = getToolName(tp);
|
|
248
|
-
const label = toolLabel(
|
|
249
|
-
name,
|
|
250
|
-
tp.input,
|
|
251
|
-
tp.state === "output-available" ? tp.output : undefined,
|
|
252
|
-
tp.state,
|
|
253
|
-
);
|
|
254
|
-
const write = isWriteTool(name);
|
|
255
|
-
const commit = isCommitTool(name);
|
|
256
|
-
const out = tp.state === "output-available" ? (tp.output as WriteToolOutput) : null;
|
|
257
|
-
const failed =
|
|
258
|
-
tp.state === "output-error" ||
|
|
259
|
-
(out !== null && (write || commit) && !out.ok);
|
|
260
|
-
const written = write && out && out.ok ? changesInToolOutput(out) : [];
|
|
261
|
-
// Doc lists placed inline via [[docs]] are not repeated here. While
|
|
262
|
-
// streaming, the placeholder usually arrives a moment after the tool
|
|
263
|
-
// result, so hold the fallback back until the stream has ended;
|
|
264
|
-
// otherwise the card would jump from here to its inline position.
|
|
265
|
-
const docList =
|
|
266
|
-
tp.state === "output-available" && !inlined.has(i) && !streaming
|
|
267
|
-
? docListFromTool(name, tp.output)
|
|
268
|
-
: 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
|
-
});
|
|
277
|
-
return (
|
|
278
|
-
<Reveal key={tp.toolCallId} active={streaming} origin="start">
|
|
279
|
-
<div {...stylex.props(styles.toolBlock)}>
|
|
280
|
-
<div {...stylex.props(styles.toolRow, failed && styles.toolFailed)}>
|
|
281
|
-
<ToolIcon name={name} />
|
|
282
|
-
<span>{label}</span>
|
|
283
|
-
{failed ? (
|
|
284
|
-
<span {...stylex.props(styles.toolError)}>
|
|
285
|
-
{tp.state === "output-error"
|
|
286
|
-
? tp.errorText
|
|
287
|
-
: (out as { message?: string } | null)?.message}
|
|
288
|
-
</span>
|
|
289
|
-
) : null}
|
|
290
|
-
</div>
|
|
291
|
-
{docList ? (
|
|
292
|
-
<DocListCard
|
|
293
|
-
items={docList.items}
|
|
294
|
-
total={docList.total}
|
|
295
|
-
workspaceId={workspaceId}
|
|
296
|
-
emptyLabel={name === "searchDocs" ? "No matches" : "No documents"}
|
|
297
|
-
/>
|
|
298
|
-
) : 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
|
-
)}
|
|
312
|
-
</div>
|
|
313
|
-
</Reveal>
|
|
261
|
+
const flow: ReactNode[] = [];
|
|
262
|
+
const traceRows: TraceRow[] = [];
|
|
263
|
+
let traceSlotted = false;
|
|
264
|
+
for (let i = 0; i < parts.length; i++) {
|
|
265
|
+
const part = parts[i];
|
|
266
|
+
if (part.type === "text") {
|
|
267
|
+
const text = streaming ? trimPartialPlaceholder(part.text) : part.text;
|
|
268
|
+
if (!text.trim()) continue;
|
|
269
|
+
const segments = splitDocsPlaceholders(text);
|
|
270
|
+
let n = 0;
|
|
271
|
+
for (let j = 0; j < segments.length; j++) {
|
|
272
|
+
const seg = segments[j];
|
|
273
|
+
if (seg.kind === "docs") {
|
|
274
|
+
const target = placeholderTarget.get(`${i}:${n++}`);
|
|
275
|
+
if (target === undefined) continue;
|
|
276
|
+
flow.push(
|
|
277
|
+
<Reveal key={`${i}-docs-${j}`} active={streaming} origin="start">
|
|
278
|
+
{renderDocCard(target, `${i}-docs-${j}`)}
|
|
279
|
+
</Reveal>,
|
|
314
280
|
);
|
|
281
|
+
continue;
|
|
315
282
|
}
|
|
316
|
-
|
|
317
|
-
|
|
283
|
+
flow.push(
|
|
284
|
+
<Reveal key={`${i}-${j}`} active={streaming} origin="start">
|
|
285
|
+
<div {...stylex.props(styles.markdown)}>
|
|
286
|
+
<Markdown components={md}>{seg.text}</Markdown>
|
|
287
|
+
</div>
|
|
288
|
+
</Reveal>,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (part.type === "reasoning" || !isToolUIPart(part)) continue;
|
|
294
|
+
const tp = part as ToolPart;
|
|
295
|
+
if (tp.state !== "output-available" && tp.state !== "output-error") continue;
|
|
296
|
+
const name = getToolName(tp);
|
|
297
|
+
const label = toolLabel(
|
|
298
|
+
name,
|
|
299
|
+
tp.input,
|
|
300
|
+
tp.state === "output-available" ? tp.output : undefined,
|
|
301
|
+
tp.state,
|
|
302
|
+
);
|
|
303
|
+
const write = isWriteTool(name);
|
|
304
|
+
const commit = isCommitTool(name);
|
|
305
|
+
const out = tp.state === "output-available" ? (tp.output as WriteToolOutput) : null;
|
|
306
|
+
const failed =
|
|
307
|
+
tp.state === "output-error" || (out !== null && (write || commit) && !out.ok);
|
|
308
|
+
const error = failed
|
|
309
|
+
? (tp.state === "output-error"
|
|
310
|
+
? tp.errorText
|
|
311
|
+
: (out as { message?: string } | null)?.message) ?? null
|
|
312
|
+
: null;
|
|
313
|
+
traceRows.push({ key: tp.toolCallId, name, label, failed, error });
|
|
314
|
+
if (!traceSlotted) {
|
|
315
|
+
traceSlotted = true;
|
|
316
|
+
flow.push(<ToolTrace key="trace" rows={traceRows} />);
|
|
317
|
+
}
|
|
318
|
+
const written = write && out && out.ok ? changesInToolOutput(out) : [];
|
|
319
|
+
// Doc lists placed inline via [[docs]] are not repeated here. While
|
|
320
|
+
// streaming, the placeholder usually arrives a moment after the tool
|
|
321
|
+
// result, so hold the fallback back until the stream has ended;
|
|
322
|
+
// otherwise the card would jump from here to its inline position.
|
|
323
|
+
const docList =
|
|
324
|
+
tp.state === "output-available" && !inlined.has(i) && !streaming
|
|
325
|
+
? docListFromTool(name, tp.output)
|
|
326
|
+
: null;
|
|
327
|
+
if (docList) {
|
|
328
|
+
flow.push(
|
|
329
|
+
<DocListCard
|
|
330
|
+
key={`${tp.toolCallId}-docs`}
|
|
331
|
+
items={docList.items}
|
|
332
|
+
total={docList.total}
|
|
333
|
+
workspaceId={workspaceId}
|
|
334
|
+
emptyLabel={name === "searchDocs" ? "No matches" : "No documents"}
|
|
335
|
+
/>,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
for (const raw of written) {
|
|
339
|
+
const change = {
|
|
340
|
+
...(session.changes[raw.id] ?? raw),
|
|
341
|
+
summary: session.changes[raw.id]?.summary ?? raw.summary,
|
|
342
|
+
};
|
|
343
|
+
const viewStatus = presentedChangeStatus(change, stack);
|
|
344
|
+
if (!viewStatus) continue;
|
|
345
|
+
flow.push(
|
|
346
|
+
<ChangeCard
|
|
347
|
+
key={change.id}
|
|
348
|
+
change={{ ...change, status: viewStatus }}
|
|
349
|
+
workspaceId={workspaceId}
|
|
350
|
+
busy={busyId === change.id}
|
|
351
|
+
canAct={canAct && !streaming}
|
|
352
|
+
onKeep={() => void act(change.id, () => session.keep(change.id))}
|
|
353
|
+
onUndo={() => void act(change.id, () => session.undo(change.id))}
|
|
354
|
+
/>,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<div {...stylex.props(styles.assistant)}>
|
|
361
|
+
{flow}
|
|
318
362
|
{applied.length > 1 && canAct && !streaming ? (
|
|
319
363
|
<div {...stylex.props(styles.bulk)}>
|
|
320
364
|
<span {...stylex.props(styles.bulkLabel)}>
|
|
@@ -389,6 +433,31 @@ const styles = stylex.create({
|
|
|
389
433
|
flexDirection: "column",
|
|
390
434
|
gap: space.sm,
|
|
391
435
|
},
|
|
436
|
+
traceToggle: {
|
|
437
|
+
display: "inline-flex",
|
|
438
|
+
alignItems: "center",
|
|
439
|
+
gap: space.sm,
|
|
440
|
+
padding: 0,
|
|
441
|
+
borderWidth: 0,
|
|
442
|
+
backgroundColor: "transparent",
|
|
443
|
+
font: "inherit",
|
|
444
|
+
fontSize: type.sm,
|
|
445
|
+
color: colors.textMuted,
|
|
446
|
+
cursor: "pointer",
|
|
447
|
+
textAlign: "left",
|
|
448
|
+
},
|
|
449
|
+
traceChevron: {
|
|
450
|
+
display: "inline-flex",
|
|
451
|
+
transform: "rotate(-90deg)",
|
|
452
|
+
transitionProperty: "transform",
|
|
453
|
+
transitionDuration: "120ms",
|
|
454
|
+
"@media (prefers-reduced-motion: reduce)": {
|
|
455
|
+
transitionDuration: "0ms",
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
traceChevronOpen: {
|
|
459
|
+
transform: "rotate(0deg)",
|
|
460
|
+
},
|
|
392
461
|
toolRow: {
|
|
393
462
|
display: "flex",
|
|
394
463
|
alignItems: "center",
|
package/src/chat/ChatPanel.tsx
CHANGED
|
@@ -32,6 +32,7 @@ export function ChatPanel({ fill = false, site = false }: { fill?: boolean; site
|
|
|
32
32
|
const session = useChatSession();
|
|
33
33
|
const visible = isPanelVisible(ui.panel.mode);
|
|
34
34
|
const fullscreen = ui.panel.mode === "fullscreen";
|
|
35
|
+
const docked = Boolean(ui.codeSessionId);
|
|
35
36
|
const threadId = session.thread?.id ?? null;
|
|
36
37
|
const knownIds = useRef<Set<string> | null>(null);
|
|
37
38
|
const seenThread = useRef<string | null | undefined>(undefined);
|
|
@@ -50,7 +51,9 @@ export function ChatPanel({ fill = false, site = false }: { fill?: boolean; site
|
|
|
50
51
|
resetKey: threadId,
|
|
51
52
|
pinKey: anchorId,
|
|
52
53
|
slotRef,
|
|
53
|
-
|
|
54
|
+
// The composer growing changes this viewport. That is not a new message,
|
|
55
|
+
// and following it scrolls the thread on every line the person types.
|
|
56
|
+
deps: [session.messages, session.status],
|
|
54
57
|
});
|
|
55
58
|
|
|
56
59
|
if (seenThread.current !== threadId) {
|
|
@@ -75,7 +78,7 @@ export function ChatPanel({ fill = false, site = false }: { fill?: boolean; site
|
|
|
75
78
|
|
|
76
79
|
const contextRows = Math.max(
|
|
77
80
|
1,
|
|
78
|
-
(ui.focus && ui.includeFocus ? 1 : 0) + ui.mentions.length,
|
|
81
|
+
(ui.focus && ui.includeFocus ? 1 : 0) + (ui.route ? 1 : 0) + ui.mentions.length,
|
|
79
82
|
);
|
|
80
83
|
const contextReserve = contextRows * CONTEXT_ROW_HEIGHT + CONTEXT_BAR_PADDING + 12;
|
|
81
84
|
|
|
@@ -153,7 +156,11 @@ export function ChatPanel({ fill = false, site = false }: { fill?: boolean; site
|
|
|
153
156
|
aria-label="AI chat"
|
|
154
157
|
data-backmatter-panel=""
|
|
155
158
|
{...stylex.props(
|
|
156
|
-
fill
|
|
159
|
+
fill
|
|
160
|
+
? ui.codeSessionId
|
|
161
|
+
? styles.panelColumnFill
|
|
162
|
+
: styles.panelFill
|
|
163
|
+
: styles.panel,
|
|
157
164
|
site && styles.panelSite,
|
|
158
165
|
!fill && fullscreen && styles.panelFull,
|
|
159
166
|
)}
|
|
@@ -193,37 +200,41 @@ export function ChatPanel({ fill = false, site = false }: { fill?: boolean; site
|
|
|
193
200
|
Back to chat
|
|
194
201
|
</button>
|
|
195
202
|
)}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
203
|
+
{docked ? null : (
|
|
204
|
+
<>
|
|
205
|
+
<span {...stylex.props(styles.divider)} />
|
|
206
|
+
<Tooltip label="Minimize (Esc)">
|
|
207
|
+
<button
|
|
208
|
+
type="button"
|
|
209
|
+
aria-label="Minimize"
|
|
210
|
+
onClick={() => ui.dispatch({ type: "minimize" })}
|
|
211
|
+
{...stylex.props(styles.iconBtn)}
|
|
212
|
+
>
|
|
213
|
+
<MinimizeIcon />
|
|
214
|
+
</button>
|
|
215
|
+
</Tooltip>
|
|
216
|
+
<Tooltip label={fullscreen ? "Exit fullscreen" : "Fullscreen"}>
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
aria-label={fullscreen ? "Exit fullscreen" : "Fullscreen"}
|
|
220
|
+
onClick={() => ui.dispatch({ type: "toggleFullscreen" })}
|
|
221
|
+
{...stylex.props(styles.iconBtn)}
|
|
222
|
+
>
|
|
223
|
+
{fullscreen ? <CollapseIcon /> : <ExpandIcon />}
|
|
224
|
+
</button>
|
|
225
|
+
</Tooltip>
|
|
226
|
+
<Tooltip label="Close">
|
|
227
|
+
<button
|
|
228
|
+
type="button"
|
|
229
|
+
aria-label="Close"
|
|
230
|
+
onClick={() => ui.dispatch({ type: "close" })}
|
|
231
|
+
{...stylex.props(styles.iconBtn)}
|
|
232
|
+
>
|
|
233
|
+
<CloseIcon />
|
|
234
|
+
</button>
|
|
235
|
+
</Tooltip>
|
|
236
|
+
</>
|
|
237
|
+
)}
|
|
227
238
|
</div>
|
|
228
239
|
</header>
|
|
229
240
|
|
|
@@ -353,6 +364,15 @@ const styles = stylex.create({
|
|
|
353
364
|
overflow: "hidden",
|
|
354
365
|
backgroundColor: colors.bgPanel,
|
|
355
366
|
},
|
|
367
|
+
panelColumnFill: {
|
|
368
|
+
position: "relative",
|
|
369
|
+
flex: 1,
|
|
370
|
+
minHeight: 0,
|
|
371
|
+
display: "flex",
|
|
372
|
+
flexDirection: "column",
|
|
373
|
+
overflow: "hidden",
|
|
374
|
+
backgroundColor: colors.bgPanel,
|
|
375
|
+
},
|
|
356
376
|
header: {
|
|
357
377
|
display: "flex",
|
|
358
378
|
alignItems: "center",
|