@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.
- package/dist/{CodeBlock-BrCoypnc.js → CodeBlock-C2Xjxu9M.js} +1 -1
- package/dist/FileTree-D0c9Yj9U.js +310 -0
- package/dist/PromptInput-Dzn-fOPc.d.ts +444 -0
- package/dist/components/Activity.js +1 -1
- package/dist/components/AgentChat.d.ts +19 -9
- package/dist/components/AgentChat.js +27 -24
- package/dist/components/AgentConnection.js +2 -2
- package/dist/components/Artifact.js +1 -1
- package/dist/components/BrowserLiveView.d.ts +47 -0
- package/dist/components/BrowserLiveView.js +135 -0
- package/dist/components/BrowserReplay.d.ts +40 -0
- package/dist/components/BrowserReplay.js +187 -0
- package/dist/components/ChainOfThought.d.ts +1 -1
- package/dist/components/ChainOfThought.js +1 -1
- package/dist/components/CodeBlock.js +1 -1
- package/dist/components/Commit.js +1 -1
- package/dist/components/ContextMenu.js +1 -1
- package/dist/components/Conversation.js +1 -1
- package/dist/components/Dialog.js +1 -1
- package/dist/components/EnvironmentVariables.js +1 -1
- package/dist/components/FileTree.js +1 -309
- package/dist/components/Menubar.js +1 -1
- package/dist/components/Message.d.ts +2 -1
- package/dist/components/Message.js +1 -1
- package/dist/components/ModelSelector.js +1 -1
- package/dist/components/NavigationMenu.js +1 -1
- package/dist/components/PricingPage.js +1 -1
- package/dist/components/PromptInput.d.ts +2 -399
- package/dist/components/PromptInput.js +177 -2
- package/dist/components/Queue.js +1 -1
- package/dist/components/Reasoning.js +1 -1
- package/dist/components/Select.js +1 -1
- package/dist/components/Sources.js +1 -1
- package/dist/components/Stepper.js +1 -1
- package/dist/components/Task.js +1 -1
- package/dist/components/Terminal.js +2 -2
- package/dist/components/TestResults.js +1 -1
- package/dist/components/ThinkChat.d.ts +186 -0
- package/dist/components/ThinkChat.js +730 -0
- package/dist/components/Tool.d.ts +32 -28
- package/dist/components/Tool.js +29 -16
- package/dist/components/WebPreview.js +1 -1
- package/dist/components/WorkspaceExplorer.d.ts +102 -0
- package/dist/components/WorkspaceExplorer.js +352 -0
- package/dist/components/icons.d.ts +2 -2
- package/dist/components/icons.js +2 -2
- package/dist/{copy-button-DAPlB8Z0.js → copy-button-Dd6DEhOm.js} +1 -1
- package/dist/{icons-Bf7JMKk-.js → icons-DQC4Zw5R.js} +5 -1
- package/dist/{icons-BklFyZbx.d.ts → icons-ctdVxng0.d.ts} +2 -1
- package/dist/styles/prompt-input.css +149 -0
- 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
/**
|
|
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
|
|
27
|
-
* <ToolHeader
|
|
28
|
+
* <Tool part={part}>
|
|
29
|
+
* <ToolHeader />
|
|
28
30
|
* <ToolContent>
|
|
29
|
-
* <ToolInput
|
|
30
|
-
* <ToolOutput
|
|
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
|
-
|
|
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
|
|
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
|
|
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 & {
|
package/dist/components/Tool.js
CHANGED
|
@@ -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 {
|
|
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-
|
|
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
|
|
60
|
-
* <ToolHeader
|
|
60
|
+
* <Tool part={part}>
|
|
61
|
+
* <ToolHeader />
|
|
61
62
|
* <ToolContent>
|
|
62
|
-
* <ToolInput
|
|
63
|
-
* <ToolOutput
|
|
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(() => ({
|
|
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
|
|
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
|
|
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 {
|
|
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 };
|