@mcp-b/react-components 0.2.2 → 0.3.0

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 (51) hide show
  1. package/dist/{CodeBlock-BrCoypnc.js → CodeBlock-C2Xjxu9M.js} +1 -1
  2. package/dist/FileTree-D0c9Yj9U.js +310 -0
  3. package/dist/PromptInput-Dzn-fOPc.d.ts +444 -0
  4. package/dist/components/Activity.js +1 -1
  5. package/dist/components/AgentChat.d.ts +19 -9
  6. package/dist/components/AgentChat.js +27 -24
  7. package/dist/components/AgentConnection.js +2 -2
  8. package/dist/components/Artifact.js +1 -1
  9. package/dist/components/BrowserLiveView.d.ts +47 -0
  10. package/dist/components/BrowserLiveView.js +135 -0
  11. package/dist/components/BrowserReplay.d.ts +40 -0
  12. package/dist/components/BrowserReplay.js +187 -0
  13. package/dist/components/ChainOfThought.d.ts +1 -1
  14. package/dist/components/ChainOfThought.js +1 -1
  15. package/dist/components/CodeBlock.js +1 -1
  16. package/dist/components/Commit.js +1 -1
  17. package/dist/components/ContextMenu.js +1 -1
  18. package/dist/components/Conversation.js +1 -1
  19. package/dist/components/Dialog.js +1 -1
  20. package/dist/components/EnvironmentVariables.js +1 -1
  21. package/dist/components/FileTree.js +1 -309
  22. package/dist/components/Menubar.js +1 -1
  23. package/dist/components/Message.d.ts +2 -1
  24. package/dist/components/Message.js +1 -1
  25. package/dist/components/ModelSelector.js +1 -1
  26. package/dist/components/NavigationMenu.js +1 -1
  27. package/dist/components/PricingPage.js +1 -1
  28. package/dist/components/PromptInput.d.ts +2 -399
  29. package/dist/components/PromptInput.js +177 -2
  30. package/dist/components/Queue.js +1 -1
  31. package/dist/components/Reasoning.js +1 -1
  32. package/dist/components/Select.js +1 -1
  33. package/dist/components/Sources.js +1 -1
  34. package/dist/components/Stepper.js +1 -1
  35. package/dist/components/Task.js +1 -1
  36. package/dist/components/Terminal.js +2 -2
  37. package/dist/components/TestResults.js +1 -1
  38. package/dist/components/ThinkChat.d.ts +186 -0
  39. package/dist/components/ThinkChat.js +730 -0
  40. package/dist/components/Tool.d.ts +32 -28
  41. package/dist/components/Tool.js +29 -16
  42. package/dist/components/WebPreview.js +1 -1
  43. package/dist/components/WorkspaceExplorer.d.ts +102 -0
  44. package/dist/components/WorkspaceExplorer.js +352 -0
  45. package/dist/components/icons.d.ts +2 -2
  46. package/dist/components/icons.js +2 -2
  47. package/dist/{copy-button-DAPlB8Z0.js → copy-button-Dd6DEhOm.js} +1 -1
  48. package/dist/{icons-Bf7JMKk-.js → icons-DQC4Zw5R.js} +5 -1
  49. package/dist/{icons-BklFyZbx.d.ts → icons-ctdVxng0.d.ts} +2 -1
  50. package/dist/styles/prompt-input.css +149 -0
  51. package/package.json +26 -3
@@ -1,18 +1,20 @@
1
1
  import { CollapsiblePanelProps, CollapsibleRootProps, CollapsibleTriggerProps } from "./Collapsible.js";
2
2
  import * as React from "react";
3
+ import { DynamicToolUIPart, ToolUIPart } from "ai";
3
4
 
4
5
  //#region src/components/Tool.d.ts
5
- type ToolState = "input-streaming" | "input-available" | "approval-requested" | "approval-responded" | "output-available" | "output-error" | "output-denied";
6
- interface ToolPart {
7
- state: ToolState;
8
- input?: unknown;
9
- output?: unknown;
10
- errorText?: string;
11
- }
6
+ /** An AI SDK tool part — static (`tool-*`) or dynamic. */
7
+ type ToolPart = ToolUIPart | DynamicToolUIPart;
8
+ type ToolState = ToolPart["state"];
12
9
  declare function getStatusBadge(status: ToolState): React.JSX.Element;
13
- interface ToolProps extends Omit<CollapsibleRootProps, "className" | "unstyled"> {
10
+ interface ToolProps extends Omit<CollapsibleRootProps, "className" | "part" | "unstyled"> {
14
11
  className?: string;
15
- /** Current state of the tool invocation. */
12
+ /**
13
+ * The AI SDK tool part, taken whole — ToolHeader/ToolInput/ToolOutput
14
+ * derive name, state, input, and output from it.
15
+ */
16
+ part?: ToolPart;
17
+ /** Current state of the tool invocation. Defaults to `part.state`. */
16
18
  state?: ToolState;
17
19
  }
18
20
  /**
@@ -23,11 +25,11 @@ interface ToolProps extends Omit<CollapsibleRootProps, "className" | "unstyled">
23
25
  *
24
26
  * @example
25
27
  * ```tsx
26
- * <Tool state="output-available">
27
- * <ToolHeader type="function" toolName="search_web" />
28
+ * <Tool part={part}>
29
+ * <ToolHeader />
28
30
  * <ToolContent>
29
- * <ToolInput input={{ query: "weather in Tokyo" }} />
30
- * <ToolOutput output={<pre>{JSON.stringify(result, null, 2)}</pre>} />
31
+ * <ToolInput />
32
+ * <ToolOutput />
31
33
  * </ToolContent>
32
34
  * </Tool>
33
35
  * ```
@@ -36,7 +38,8 @@ interface ToolProps extends Omit<CollapsibleRootProps, "className" | "unstyled">
36
38
  */
37
39
  declare function Tool({
38
40
  className,
39
- state,
41
+ part,
42
+ state: stateProp,
40
43
  defaultOpen,
41
44
  ref,
42
45
  ...props
@@ -45,9 +48,9 @@ declare function Tool({
45
48
  }): React.JSX.Element;
46
49
  interface ToolHeaderProps extends Omit<CollapsibleTriggerProps, "className" | "type" | "unstyled"> {
47
50
  className?: string;
48
- /** The tool "type" (e.g. "function", "retrieval"). */
49
- type: string;
50
- /** The specific tool invoked (e.g. "search_web"). */
51
+ /** The tool "type" (e.g. "function", "retrieval"). Derived from `<Tool part>` when omitted. */
52
+ type?: string;
53
+ /** The specific tool invoked (e.g. "search_web"). Derived from `<Tool part>` when omitted. */
51
54
  toolName?: string;
52
55
  /** Optional human-readable title shown alongside the tool name. */
53
56
  title?: string;
@@ -59,9 +62,9 @@ interface ToolHeaderProps extends Omit<CollapsibleTriggerProps, "className" | "t
59
62
  */
60
63
  declare function ToolHeader({
61
64
  className,
62
- type,
65
+ type: typeProp,
63
66
  toolName,
64
- title,
67
+ title: titleProp,
65
68
  state: stateProp,
66
69
  ref,
67
70
  ...props
@@ -83,24 +86,25 @@ declare function ToolContent({
83
86
  ref?: React.Ref<HTMLDivElement>;
84
87
  }): React.JSX.Element;
85
88
  interface ToolInputProps extends React.HTMLAttributes<HTMLDivElement> {
86
- /** The input passed to the tool (rendered as a JSON code block). */
87
- input: unknown;
89
+ /** The input passed to the tool (rendered as a JSON code block). Defaults to `part.input`. */
90
+ input?: unknown;
88
91
  }
89
92
  /**
90
- * Displays the tool input as a JSON code block.
93
+ * Displays the tool input as a JSON code block. Renders nothing while the
94
+ * input is still undefined.
91
95
  */
92
96
  declare function ToolInput({
93
97
  className,
94
- input,
98
+ input: inputProp,
95
99
  ref,
96
100
  ...props
97
101
  }: ToolInputProps & {
98
102
  ref?: React.Ref<HTMLDivElement>;
99
- }): React.JSX.Element;
103
+ }): React.JSX.Element | null;
100
104
  interface ToolOutputProps extends React.HTMLAttributes<HTMLDivElement> {
101
- /** The tool output (any React node). Mutually exclusive with errorText. */
105
+ /** The tool output (any React node). Defaults to `part.output`. Mutually exclusive with errorText. */
102
106
  output?: unknown;
103
- /** Error message, rendered with destructive styling. */
107
+ /** Error message, rendered with destructive styling. Defaults to `part.errorText`. */
104
108
  errorText?: string;
105
109
  }
106
110
  /**
@@ -108,8 +112,8 @@ interface ToolOutputProps extends React.HTMLAttributes<HTMLDivElement> {
108
112
  */
109
113
  declare function ToolOutput({
110
114
  className,
111
- output,
112
- errorText,
115
+ output: outputProp,
116
+ errorText: errorTextProp,
113
117
  ref,
114
118
  ...props
115
119
  }: ToolOutputProps & {
@@ -1,11 +1,12 @@
1
1
  import { CollapsiblePanel, CollapsibleRoot, CollapsibleTrigger } from "./Collapsible.js";
2
2
  import { t as cx } from "../class-names-9UtZwb0J.js";
3
- import { M as WarningIcon, N as WrenchIcon, P as XIcon, d as DotIcon, k as SpinnerIcon, l as ChevronRightIcon, o as CheckIcon } from "../icons-Bf7JMKk-.js";
3
+ import { A as SpinnerIcon, F as XIcon, N as WarningIcon, P as WrenchIcon, d as DotIcon, l as ChevronRightIcon, o as CheckIcon } from "../icons-DQC4Zw5R.js";
4
4
  import { Badge } from "./Badge.js";
5
- import { i as CodeBlockContent, r as CodeBlockContainer, t as CodeBlock } from "../CodeBlock-BrCoypnc.js";
5
+ import { i as CodeBlockContent, r as CodeBlockContainer, t as CodeBlock } from "../CodeBlock-C2Xjxu9M.js";
6
6
  import { formatStructuredCodeDisplay } from "../utils/structured-code.js";
7
7
  import * as React from "react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { getToolName } from "ai";
9
10
  //#region src/components/Tool.tsx
10
11
  const STATE_TO_COLOR = {
11
12
  "input-streaming": "primary",
@@ -56,20 +57,24 @@ function useToolContext() {
56
57
  *
57
58
  * @example
58
59
  * ```tsx
59
- * <Tool state="output-available">
60
- * <ToolHeader type="function" toolName="search_web" />
60
+ * <Tool part={part}>
61
+ * <ToolHeader />
61
62
  * <ToolContent>
62
- * <ToolInput input={{ query: "weather in Tokyo" }} />
63
- * <ToolOutput output={<pre>{JSON.stringify(result, null, 2)}</pre>} />
63
+ * <ToolInput />
64
+ * <ToolOutput />
64
65
  * </ToolContent>
65
66
  * </Tool>
66
67
  * ```
67
68
  *
68
69
  * @see {@link https://elements.ai-sdk.dev/components/tool | AI Elements Tool}
69
70
  */
70
- function Tool({ className, state, defaultOpen, ref, ...props }) {
71
+ function Tool({ className, part, state: stateProp, defaultOpen, ref, ...props }) {
72
+ const state = stateProp ?? part?.state;
71
73
  const autoOpen = state === "output-available" || state === "output-error" || state === "output-denied";
72
- const ctx = React.useMemo(() => ({ state }), [state]);
74
+ const ctx = React.useMemo(() => ({
75
+ state,
76
+ part
77
+ }), [state, part]);
73
78
  return /* @__PURE__ */ jsx(ToolContext.Provider, {
74
79
  value: ctx,
75
80
  children: /* @__PURE__ */ jsx(CollapsibleRoot, {
@@ -85,10 +90,12 @@ function Tool({ className, state, defaultOpen, ref, ...props }) {
85
90
  /**
86
91
  * Clickable header that shows the tool type, name, and state badge.
87
92
  */
88
- function ToolHeader({ className, type, toolName, title, state: stateProp, ref, ...props }) {
89
- const { state: contextState } = useToolContext();
93
+ function ToolHeader({ className, type: typeProp, toolName, title: titleProp, state: stateProp, ref, ...props }) {
94
+ const { state: contextState, part } = useToolContext();
90
95
  const state = stateProp ?? contextState ?? "input-available";
91
- const derivedName = type === "dynamic-tool" && toolName ? toolName : type.startsWith("tool-") ? type.split("-").slice(1).join("-") : toolName ?? type;
96
+ const type = typeProp ?? (part?.type === "dynamic-tool" ? "dynamic-tool" : "function");
97
+ const title = titleProp ?? part?.title;
98
+ const derivedName = part && typeProp === void 0 && toolName === void 0 ? getToolName(part) : type === "dynamic-tool" && toolName ? toolName : type.startsWith("tool-") ? type.split("-").slice(1).join("-") : toolName ?? type;
92
99
  return /* @__PURE__ */ jsxs(CollapsibleTrigger, {
93
100
  ref,
94
101
  className: cx("tool__header", className),
@@ -138,11 +145,14 @@ function ToolContent({ className, children, ref, ...props }) {
138
145
  });
139
146
  }
140
147
  /**
141
- * Displays the tool input as a JSON code block.
148
+ * Displays the tool input as a JSON code block. Renders nothing while the
149
+ * input is still undefined.
142
150
  */
143
- function ToolInput({ className, input, ref, ...props }) {
144
- const { state } = useToolContext();
145
- const formattedInput = React.useMemo(() => formatStructuredCodeDisplay(input), [input]);
151
+ function ToolInput({ className, input: inputProp, ref, ...props }) {
152
+ const { state, part } = useToolContext();
153
+ const input = inputProp !== void 0 ? inputProp : part?.input;
154
+ const formattedInput = React.useMemo(() => input === void 0 ? null : formatStructuredCodeDisplay(input), [input]);
155
+ if (formattedInput === null) return null;
146
156
  const isStreaming = state === "input-streaming";
147
157
  return /* @__PURE__ */ jsxs("div", {
148
158
  ref,
@@ -163,7 +173,10 @@ function ToolInput({ className, input, ref, ...props }) {
163
173
  /**
164
174
  * Displays the tool output or error text.
165
175
  */
166
- function ToolOutput({ className, output, errorText, ref, ...props }) {
176
+ function ToolOutput({ className, output: outputProp, errorText: errorTextProp, ref, ...props }) {
177
+ const { part } = useToolContext();
178
+ const output = outputProp !== void 0 ? outputProp : part && "output" in part ? part.output : void 0;
179
+ const errorText = errorTextProp !== void 0 ? errorTextProp : part && "errorText" in part ? part.errorText : void 0;
167
180
  const formattedOutput = React.useMemo(() => {
168
181
  if (output && typeof output === "object" && !React.isValidElement(output)) return formatStructuredCodeDisplay(output);
169
182
  if (typeof output === "string") return {
@@ -1,5 +1,5 @@
1
1
  import { t as cx } from "../class-names-9UtZwb0J.js";
2
- import { D as RefreshIcon, n as ArrowLeftIcon, r as ArrowRightIcon, s as ChevronDownIcon } from "../icons-Bf7JMKk-.js";
2
+ import { O as RefreshIcon, n as ArrowLeftIcon, r as ArrowRightIcon, s as ChevronDownIcon } from "../icons-DQC4Zw5R.js";
3
3
  import { Button } from "./Button.js";
4
4
  import { Tooltip, TooltipPopup, TooltipPortal, TooltipPositioner, TooltipTrigger } from "./Tooltip.js";
5
5
  import { Badge } from "./Badge.js";
@@ -0,0 +1,102 @@
1
+ import { A as CodeBlockLanguage } from "../CodeBlock-CxQXv3C4.js";
2
+ import * as React from "react";
3
+ import { FileInfo, Workspace } from "@cloudflare/shell";
4
+ import { WorkspaceLike } from "@cloudflare/think/tools/workspace";
5
+
6
+ //#region src/components/WorkspaceExplorer.d.ts
7
+ /** Workspace summary, as returned by `Workspace.getWorkspaceInfo()` upstream. */
8
+ type WorkspaceInfo = Awaited<ReturnType<Workspace["getWorkspaceInfo"]>>;
9
+ /**
10
+ * Upstream's workspace shapes, taken whole: pass a `Workspace` from
11
+ * `@cloudflare/shell` or anything satisfying Think's `WorkspaceLike` (e.g. a
12
+ * cross-DO RPC proxy). The explorer only calls `readDir` and `readFile` —
13
+ * plus `getWorkspaceInfo` for the header's file-count chip when given a full
14
+ * `Workspace` — and ignores the rest.
15
+ */
16
+ type WorkspaceSource = Workspace | WorkspaceLike;
17
+ interface UseWorkspaceOptions {
18
+ /** Must be referentially stable (memoize in the app) — a new source resets the workspace. */
19
+ source: WorkspaceSource;
20
+ /** Root directory to list first. Defaults to "/". */
21
+ root?: string;
22
+ }
23
+ interface WorkspaceFileState {
24
+ path: string;
25
+ name: string;
26
+ /** `null` means the file is missing or binary, matching upstream `readFile`. */
27
+ content: string | null;
28
+ error: string | null;
29
+ }
30
+ interface WorkspaceState {
31
+ root: string;
32
+ /** Loaded directory listings, keyed by directory path. */
33
+ entriesByDirectory: Readonly<Record<string, readonly FileInfo[]>>;
34
+ /** Every loaded entry, keyed by its path. */
35
+ entryByPath: ReadonlyMap<string, FileInfo>;
36
+ expandedPaths: ReadonlySet<string>;
37
+ /** Update expansion; newly expanded directories load lazily. */
38
+ setExpandedPaths: (next: Set<string>) => void;
39
+ selectedPath: string | null;
40
+ /** Select a file and load its content (cached after the first read). */
41
+ selectFile: (path: string | null) => void;
42
+ /** The selected file once its read settles; null while loading. */
43
+ selectedFile: WorkspaceFileState | null;
44
+ /** Root listing in flight. */
45
+ isLoading: boolean;
46
+ /** Directory and file paths with a request in flight. */
47
+ loadingPaths: ReadonlySet<string>;
48
+ /** Root/directory listing failure. Per-file read errors live on the file state. */
49
+ error: string | null;
50
+ /** Workspace summary, when the source provides `getWorkspaceInfo`. */
51
+ info: WorkspaceInfo | null;
52
+ /** Reload the root, every expanded directory, and the selected file. */
53
+ refresh: () => void;
54
+ }
55
+ /**
56
+ * Headless state for browsing a Think workspace: lazy directory loading,
57
+ * cached file reads, expansion/selection, and refresh. Feed the result to
58
+ * `WorkspaceExplorer`, or render your own UI from it.
59
+ */
60
+ declare function useWorkspace({
61
+ source,
62
+ root
63
+ }: UseWorkspaceOptions): WorkspaceState;
64
+ /** Best-effort syntax-highlighting language from a file path; unknown extensions render as plain text. */
65
+ declare function inferWorkspaceLanguage(path: string): CodeBlockLanguage;
66
+ interface WorkspaceExplorerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
67
+ workspace: WorkspaceState;
68
+ /** Header label. Defaults to "Workspace". */
69
+ title?: React.ReactNode;
70
+ /** Extra header actions, rendered next to the built-in refresh button. */
71
+ actions?: React.ReactNode;
72
+ /** Hide the file filter input. */
73
+ showFilter?: boolean;
74
+ filterPlaceholder?: string;
75
+ emptyState?: React.ReactNode;
76
+ }
77
+ /**
78
+ * A Think workspace file browser with a preview pane: lazy directory tree,
79
+ * filter, refresh, and syntax-highlighted file view with copy. Drive it with
80
+ * `useWorkspace(source)`.
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * const workspace = useWorkspace({ source: agent.workspace, root: "/workspace" });
85
+ * <WorkspaceExplorer workspace={workspace} />
86
+ * ```
87
+ */
88
+ declare function WorkspaceExplorer({
89
+ workspace,
90
+ className,
91
+ title,
92
+ actions,
93
+ showFilter,
94
+ filterPlaceholder,
95
+ emptyState,
96
+ ref,
97
+ ...props
98
+ }: WorkspaceExplorerProps & {
99
+ ref?: React.Ref<HTMLDivElement>;
100
+ }): React.JSX.Element;
101
+ //#endregion
102
+ export { type FileInfo, UseWorkspaceOptions, WorkspaceExplorer, WorkspaceExplorerProps, WorkspaceFileState, WorkspaceInfo, WorkspaceSource, WorkspaceState, inferWorkspaceLanguage, useWorkspace };
@@ -0,0 +1,352 @@
1
+ import { t as cx } from "../class-names-9UtZwb0J.js";
2
+ import { O as RefreshIcon, h as FileIcon, k as SearchIcon, n as ArrowLeftIcon } from "../icons-DQC4Zw5R.js";
3
+ import { Button } from "./Button.js";
4
+ import { a as CodeBlockCopyButton, i as CodeBlockContent, n as CodeBlockActions, o as CodeBlockFilename, p as CodeBlockTitle, r as CodeBlockContainer, s as CodeBlockHeader, t as CodeBlock } from "../CodeBlock-C2Xjxu9M.js";
5
+ import { i as FileTreeFolder, r as FileTreeFile, t as FileTree } from "../FileTree-D0c9Yj9U.js";
6
+ import * as React from "react";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/components/WorkspaceExplorer.tsx
9
+ function basename(path) {
10
+ return path.split("/").filter(Boolean).at(-1) ?? path;
11
+ }
12
+ function sortEntries(entries) {
13
+ return [...entries].sort((left, right) => {
14
+ if (left.type !== right.type) return left.type === "directory" ? -1 : 1;
15
+ return left.name.localeCompare(right.name);
16
+ });
17
+ }
18
+ function toErrorMessage(error) {
19
+ return error instanceof Error ? error.message : String(error);
20
+ }
21
+ /**
22
+ * Headless state for browsing a Think workspace: lazy directory loading,
23
+ * cached file reads, expansion/selection, and refresh. Feed the result to
24
+ * `WorkspaceExplorer`, or render your own UI from it.
25
+ */
26
+ function useWorkspace({ source, root = "/" }) {
27
+ const [entriesByDirectory, setEntriesByDirectory] = React.useState({});
28
+ const [filesByPath, setFilesByPath] = React.useState({});
29
+ const [expandedPaths, setExpandedPathsState] = React.useState(/* @__PURE__ */ new Set());
30
+ const [selectedPath, setSelectedPath] = React.useState(null);
31
+ const [isLoading, setIsLoading] = React.useState(false);
32
+ const [loadingPaths, setLoadingPaths] = React.useState(/* @__PURE__ */ new Set());
33
+ const [error, setError] = React.useState(null);
34
+ const [info, setInfo] = React.useState(null);
35
+ const loadedDirectoriesRef = React.useRef(/* @__PURE__ */ new Set());
36
+ const markLoading = React.useCallback((path, loading) => {
37
+ setLoadingPaths((current) => {
38
+ const next = new Set(current);
39
+ if (loading) next.add(path);
40
+ else next.delete(path);
41
+ return next;
42
+ });
43
+ }, []);
44
+ const loadDirectory = React.useCallback(async (path, { force = false } = {}) => {
45
+ if (!force && loadedDirectoriesRef.current.has(path)) return;
46
+ markLoading(path, true);
47
+ try {
48
+ const entries = sortEntries(await source.readDir(path));
49
+ loadedDirectoriesRef.current.add(path);
50
+ setEntriesByDirectory((current) => ({
51
+ ...current,
52
+ [path]: entries
53
+ }));
54
+ setError(null);
55
+ } catch (listError) {
56
+ setError(toErrorMessage(listError));
57
+ } finally {
58
+ markLoading(path, false);
59
+ }
60
+ }, [source, markLoading]);
61
+ const loadFile = React.useCallback(async (path, { force = false } = {}) => {
62
+ if (!force && Object.hasOwn(filesByPath, path)) return;
63
+ markLoading(path, true);
64
+ try {
65
+ const content = await source.readFile(path);
66
+ setFilesByPath((current) => ({
67
+ ...current,
68
+ [path]: {
69
+ path,
70
+ name: basename(path),
71
+ content,
72
+ error: null
73
+ }
74
+ }));
75
+ } catch (readError) {
76
+ setFilesByPath((current) => ({
77
+ ...current,
78
+ [path]: {
79
+ path,
80
+ name: basename(path),
81
+ content: null,
82
+ error: toErrorMessage(readError)
83
+ }
84
+ }));
85
+ } finally {
86
+ markLoading(path, false);
87
+ }
88
+ }, [
89
+ source,
90
+ filesByPath,
91
+ markLoading
92
+ ]);
93
+ const loadInfo = React.useCallback(async () => {
94
+ if (!("getWorkspaceInfo" in source)) return;
95
+ try {
96
+ setInfo(await source.getWorkspaceInfo());
97
+ } catch {
98
+ setInfo(null);
99
+ }
100
+ }, [source]);
101
+ React.useEffect(() => {
102
+ setEntriesByDirectory({});
103
+ setFilesByPath({});
104
+ setExpandedPathsState(/* @__PURE__ */ new Set());
105
+ setSelectedPath(null);
106
+ setError(null);
107
+ setInfo(null);
108
+ loadedDirectoriesRef.current = /* @__PURE__ */ new Set();
109
+ setIsLoading(true);
110
+ Promise.all([loadDirectory(root, { force: true }), loadInfo()]).finally(() => setIsLoading(false));
111
+ }, [
112
+ loadDirectory,
113
+ loadInfo,
114
+ root
115
+ ]);
116
+ const setExpandedPaths = React.useCallback((next) => {
117
+ setExpandedPathsState((current) => {
118
+ for (const path of next) if (!current.has(path)) loadDirectory(path);
119
+ return next;
120
+ });
121
+ }, [loadDirectory]);
122
+ const selectFile = React.useCallback((path) => {
123
+ setSelectedPath(path);
124
+ if (path !== null) loadFile(path);
125
+ }, [loadFile]);
126
+ const refresh = React.useCallback(() => {
127
+ (async () => {
128
+ setIsLoading(true);
129
+ try {
130
+ await Promise.all([...[root, ...expandedPaths].map((path) => loadDirectory(path, { force: true })), loadInfo()]);
131
+ if (selectedPath) await loadFile(selectedPath, { force: true });
132
+ } finally {
133
+ setIsLoading(false);
134
+ }
135
+ })();
136
+ }, [
137
+ expandedPaths,
138
+ loadDirectory,
139
+ loadFile,
140
+ loadInfo,
141
+ root,
142
+ selectedPath
143
+ ]);
144
+ return {
145
+ root,
146
+ entriesByDirectory,
147
+ entryByPath: React.useMemo(() => {
148
+ const map = /* @__PURE__ */ new Map();
149
+ for (const entries of Object.values(entriesByDirectory)) for (const entry of entries) map.set(entry.path, entry);
150
+ return map;
151
+ }, [entriesByDirectory]),
152
+ expandedPaths,
153
+ setExpandedPaths,
154
+ selectedPath,
155
+ selectFile,
156
+ selectedFile: selectedPath ? filesByPath[selectedPath] ?? null : null,
157
+ isLoading,
158
+ loadingPaths,
159
+ error,
160
+ info,
161
+ refresh
162
+ };
163
+ }
164
+ const LANGUAGE_BY_EXTENSION = {
165
+ ts: "ts",
166
+ mts: "ts",
167
+ cts: "ts",
168
+ tsx: "tsx",
169
+ js: "js",
170
+ mjs: "js",
171
+ cjs: "js",
172
+ jsx: "jsx",
173
+ json: "json",
174
+ jsonc: "jsonc",
175
+ css: "css",
176
+ html: "html",
177
+ md: "md",
178
+ mdx: "mdx",
179
+ sh: "bash",
180
+ bash: "bash",
181
+ zsh: "bash",
182
+ yml: "yaml",
183
+ yaml: "yaml",
184
+ toml: "toml",
185
+ py: "python",
186
+ rs: "rust",
187
+ go: "go",
188
+ sql: "sql",
189
+ xml: "xml",
190
+ svg: "xml",
191
+ vue: "vue",
192
+ svelte: "svelte"
193
+ };
194
+ /** Best-effort syntax-highlighting language from a file path; unknown extensions render as plain text. */
195
+ function inferWorkspaceLanguage(path) {
196
+ return LANGUAGE_BY_EXTENSION[basename(path).split(".").at(-1)?.toLowerCase() ?? ""] ?? "text";
197
+ }
198
+ /**
199
+ * A Think workspace file browser with a preview pane: lazy directory tree,
200
+ * filter, refresh, and syntax-highlighted file view with copy. Drive it with
201
+ * `useWorkspace(source)`.
202
+ *
203
+ * @example
204
+ * ```tsx
205
+ * const workspace = useWorkspace({ source: agent.workspace, root: "/workspace" });
206
+ * <WorkspaceExplorer workspace={workspace} />
207
+ * ```
208
+ */
209
+ function WorkspaceExplorer({ workspace, className, title = "Workspace", actions, showFilter = true, filterPlaceholder = "Filter files...", emptyState, ref, ...props }) {
210
+ const [view, setView] = React.useState("tree");
211
+ const [filter, setFilter] = React.useState("");
212
+ const filterQuery = filter.trim().toLowerCase();
213
+ const { root, entriesByDirectory, entryByPath, expandedPaths, setExpandedPaths, selectedPath, selectFile, selectedFile, isLoading, loadingPaths, error, info, refresh } = workspace;
214
+ const handleSelect = React.useCallback((path) => {
215
+ if (entryByPath.get(path)?.type === "directory") return;
216
+ selectFile(path);
217
+ setView("file");
218
+ }, [entryByPath, selectFile]);
219
+ const filteredFiles = React.useMemo(() => {
220
+ if (!filterQuery) return [];
221
+ return sortEntries([...entryByPath.values()].filter((entry) => entry.type !== "directory" && entry.path.toLowerCase().includes(filterQuery)));
222
+ }, [entryByPath, filterQuery]);
223
+ const renderEntry = (entry) => {
224
+ if (entry.type === "directory") return /* @__PURE__ */ jsx(FileTreeFolder, {
225
+ path: entry.path,
226
+ name: entry.name,
227
+ children: (entriesByDirectory[entry.path] ?? []).map(renderEntry)
228
+ }, entry.path);
229
+ return /* @__PURE__ */ jsx(FileTreeFile, {
230
+ path: entry.path,
231
+ name: entry.name
232
+ }, entry.path);
233
+ };
234
+ const rootEntries = entriesByDirectory[root] ?? [];
235
+ const showFileView = view === "file" && selectedPath !== null;
236
+ const selectedFileLoading = selectedPath !== null && loadingPaths.has(selectedPath);
237
+ return /* @__PURE__ */ jsxs("div", {
238
+ ref,
239
+ className: cx("workspace-explorer", className),
240
+ ...props,
241
+ children: [/* @__PURE__ */ jsxs("div", {
242
+ className: "workspace-explorer__header",
243
+ children: [showFileView ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
244
+ variant: "ghost",
245
+ color: "neutral",
246
+ size: "sm",
247
+ onClick: () => setView("tree"),
248
+ "aria-label": "Back to files",
249
+ children: /* @__PURE__ */ jsx(ArrowLeftIcon, { "aria-hidden": "true" })
250
+ }), /* @__PURE__ */ jsx("span", {
251
+ className: "workspace-explorer__path",
252
+ title: selectedPath,
253
+ children: selectedPath
254
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
255
+ className: "workspace-explorer__title",
256
+ children: title
257
+ }), info ? /* @__PURE__ */ jsxs("span", {
258
+ className: "workspace-explorer__count",
259
+ children: [info.fileCount.toLocaleString(), " files"]
260
+ }) : null] }), /* @__PURE__ */ jsxs("div", {
261
+ className: "workspace-explorer__actions",
262
+ children: [actions, /* @__PURE__ */ jsx(Button, {
263
+ variant: "ghost",
264
+ color: "neutral",
265
+ size: "sm",
266
+ onClick: refresh,
267
+ disabled: isLoading,
268
+ "aria-label": "Refresh workspace",
269
+ children: /* @__PURE__ */ jsx(RefreshIcon, { "aria-hidden": "true" })
270
+ })]
271
+ })]
272
+ }), showFileView ? /* @__PURE__ */ jsx(WorkspaceFileView, {
273
+ file: selectedFile,
274
+ isLoading: selectedFileLoading
275
+ }) : /* @__PURE__ */ jsxs("div", {
276
+ className: "workspace-explorer__body",
277
+ children: [showFilter ? /* @__PURE__ */ jsxs("label", {
278
+ className: "workspace-explorer__filter",
279
+ children: [/* @__PURE__ */ jsx(SearchIcon, { "aria-hidden": "true" }), /* @__PURE__ */ jsx("input", {
280
+ type: "search",
281
+ value: filter,
282
+ onChange: (event) => setFilter(event.currentTarget.value),
283
+ placeholder: filterPlaceholder,
284
+ "aria-label": "Filter workspace files",
285
+ autoComplete: "off"
286
+ })]
287
+ }) : null, error ? /* @__PURE__ */ jsx("div", {
288
+ className: "workspace-explorer__empty",
289
+ role: "alert",
290
+ children: error
291
+ }) : filterQuery ? filteredFiles.length > 0 ? /* @__PURE__ */ jsx(FileTree, {
292
+ className: "workspace-explorer__tree",
293
+ selectedPath,
294
+ onSelect: handleSelect,
295
+ "aria-label": "Workspace files",
296
+ children: filteredFiles.map((entry) => /* @__PURE__ */ jsx(FileTreeFile, {
297
+ path: entry.path,
298
+ name: entry.path
299
+ }, entry.path))
300
+ }) : /* @__PURE__ */ jsx("div", {
301
+ className: "workspace-explorer__empty",
302
+ children: "No matching files"
303
+ }) : rootEntries.length > 0 ? /* @__PURE__ */ jsx(FileTree, {
304
+ className: "workspace-explorer__tree",
305
+ expanded: new Set(expandedPaths),
306
+ onExpandedChange: setExpandedPaths,
307
+ selectedPath,
308
+ onSelect: handleSelect,
309
+ "aria-label": "Workspace files",
310
+ children: rootEntries.map(renderEntry)
311
+ }) : /* @__PURE__ */ jsx("div", {
312
+ className: "workspace-explorer__empty",
313
+ children: isLoading ? "Loading workspace..." : emptyState ?? "No workspace files"
314
+ })]
315
+ })]
316
+ });
317
+ }
318
+ function WorkspaceFileView({ file, isLoading }) {
319
+ if (isLoading) return /* @__PURE__ */ jsx("div", {
320
+ className: "workspace-explorer__empty",
321
+ children: "Loading file..."
322
+ });
323
+ if (!file) return /* @__PURE__ */ jsx("div", {
324
+ className: "workspace-explorer__empty",
325
+ children: "File preview is unavailable."
326
+ });
327
+ if (file.error) return /* @__PURE__ */ jsxs("div", {
328
+ className: "workspace-explorer__empty",
329
+ role: "alert",
330
+ children: ["Could not preview this file: ", file.error]
331
+ });
332
+ if (file.content === null) return /* @__PURE__ */ jsx("div", {
333
+ className: "workspace-explorer__empty",
334
+ children: "This file has no text preview."
335
+ });
336
+ if (file.content.length === 0) return /* @__PURE__ */ jsx("div", {
337
+ className: "workspace-explorer__empty",
338
+ children: "This file is empty."
339
+ });
340
+ return /* @__PURE__ */ jsx("div", {
341
+ className: "workspace-explorer__file",
342
+ children: /* @__PURE__ */ jsxs(CodeBlock, {
343
+ className: "workspace-explorer__code",
344
+ code: file.content,
345
+ language: inferWorkspaceLanguage(file.path),
346
+ showLineNumbers: true,
347
+ children: [/* @__PURE__ */ jsxs(CodeBlockHeader, { children: [/* @__PURE__ */ jsxs(CodeBlockTitle, { children: [/* @__PURE__ */ jsx(FileIcon, { "aria-hidden": "true" }), /* @__PURE__ */ jsx(CodeBlockFilename, { children: file.name })] }), /* @__PURE__ */ jsx(CodeBlockActions, { children: /* @__PURE__ */ jsx(CodeBlockCopyButton, {}) })] }), /* @__PURE__ */ jsx(CodeBlockContainer, { children: /* @__PURE__ */ jsx(CodeBlockContent, {}) })]
348
+ })
349
+ });
350
+ }
351
+ //#endregion
352
+ export { WorkspaceExplorer, inferWorkspaceLanguage, useWorkspace };