@mcp-b/react-components 0.8.0 → 0.9.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.
@@ -17,7 +17,60 @@ interface AgentChatRenderMessageContext<MessageT extends UIMessage = UIMessage>
17
17
  }
18
18
  interface AgentChatProps<MessageT extends UIMessage = UIMessage> {
19
19
  className?: string;
20
- /** AI SDK `UIMessage`s to render, oldest first. */
20
+ /**
21
+ * AI SDK `UIMessage`s to render, oldest first.
22
+ *
23
+ * `AgentChat` intentionally renders the upstream message objects in place:
24
+ * it does not normalize them into a local transcript model. That keeps AI SDK
25
+ * parts such as `text`, `reasoning`, `file`, `tool-*`, `source-url`,
26
+ * `source-document`, and `step-start` aligned with the types exported from
27
+ * `ai`. Cloudflare Think also returns these same UI messages through
28
+ * `useAgentChat`, with Agents session storage applying compaction overlays to
29
+ * reads before they reach the hook.
30
+ *
31
+ * Backend setup for compaction is owned by Think/Agents, not this component:
32
+ * configure the agent session with `Session.create(this).onCompaction(...)
33
+ * .compactAfter(...)`. Agents' Sessions docs describe that `getHistory()`
34
+ * replaces a compacted range with a synthetic message whose id starts with
35
+ * `compaction_`; this component uses Agents' exported
36
+ * `isCompactionMessage` helper to render that synthetic message as a quiet
37
+ * separator. Think's context-overflow recovery is a separate opt-in layer:
38
+ * set `contextOverflow` and `classifyChatError` on the Think agent if the
39
+ * backend should compact and retry/proactively guard a too-large in-flight
40
+ * turn.
41
+ *
42
+ * @example Think session compaction setup
43
+ * ```ts
44
+ * import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";
45
+ * import { Session } from "agents/experimental/memory/session";
46
+ * import { createCompactFunction } from "agents/experimental/memory/utils";
47
+ * import { generateText } from "ai";
48
+ *
49
+ * export class MyAgent extends Think<Env> {
50
+ * override configureSession(session: Session) {
51
+ * return session
52
+ * .onCompaction(
53
+ * createCompactFunction({
54
+ * summarize: async (prompt) =>
55
+ * (await generateText({ model: this.env.MODEL, prompt })).text,
56
+ * protectHead: 3,
57
+ * tailTokenBudget: 20_000,
58
+ * }),
59
+ * )
60
+ * .compactAfter(100_000);
61
+ * }
62
+ *
63
+ * override contextOverflow = {
64
+ * reactive: true,
65
+ * proactive: { maxInputTokens: 200_000 },
66
+ * };
67
+ * override classifyChatError = defaultContextOverflowClassifier;
68
+ * }
69
+ * ```
70
+ *
71
+ * @see https://github.com/cloudflare/agents/blob/main/docs/agents/sessions.md#compaction
72
+ * @see https://github.com/cloudflare/agents/blob/main/docs/think/index.md#context-window-overflow-recovery
73
+ */
21
74
  messages: readonly MessageT[];
22
75
  /** Drives the composer's send/stop affordance and streaming indicators. */
23
76
  status?: ChatStatus;
@@ -64,7 +117,19 @@ interface AgentChatProps<MessageT extends UIMessage = UIMessage> {
64
117
  maxTokens: number;
65
118
  modelId?: string;
66
119
  };
67
- /** Override rendering per part; return `undefined` to fall back to the default part UI. */
120
+ /**
121
+ * Override rendering per part; return `undefined` to fall back to the default
122
+ * AI SDK/Think-aware part UI.
123
+ *
124
+ * The default renderer uses upstream helpers where available:
125
+ * `isTextUIPart`, `isReasoningUIPart`, `isFileUIPart`, `isToolUIPart`,
126
+ * `getToolName`, and Think's `getToolPartState`/input/output accessors. It
127
+ * also renders native AI SDK `source-url`/`source-document` parts as a single
128
+ * grouped `Sources` block when they appear consecutively, and native
129
+ * `step-start` parts as step separators. If you override source rendering,
130
+ * return a React node for every source part you want to show; returning
131
+ * `undefined` lets `AgentChat` keep the upstream-shaped source-run grouping.
132
+ */
68
133
  renderPart?: (context: AgentChatRenderPartContext<MessageT>) => React.ReactNode | undefined;
69
134
  /** Rendered after each message's parts — for actions, citations, and the like. */
70
135
  renderMessageAfter?: (context: AgentChatRenderMessageContext<MessageT>) => React.ReactNode;
@@ -1,4 +1,4 @@
1
- import { a as AgentChatRenderPartContext, r as AgentChatProps } from "./AgentChat-403mckDG.js";
1
+ import { a as AgentChatRenderPartContext, r as AgentChatProps } from "./AgentChat-B9LD_WF1.js";
2
2
  import { s as ThinkChatPart } from "./ThinkChatToolParts-DnUiZgCC.js";
3
3
  import * as React from "react";
4
4
  import { DynamicToolUIPart, ToolUIPart, UIMessage } from "ai";
@@ -1,2 +1,2 @@
1
- import { a as AgentChatRenderPartContext, i as AgentChatRenderMessageContext, n as AgentChatPart, r as AgentChatProps, t as AgentChat } from "../AgentChat-403mckDG.js";
1
+ import { a as AgentChatRenderPartContext, i as AgentChatRenderMessageContext, n as AgentChatPart, r as AgentChatProps, t as AgentChat } from "../AgentChat-B9LD_WF1.js";
2
2
  export { AgentChat, AgentChatPart, AgentChatProps, AgentChatRenderMessageContext, AgentChatRenderPartContext };
@@ -5,11 +5,14 @@ import { Context, ContextContent, ContextContentBody, ContextContentHeader, Cont
5
5
  import { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport } from "./MessageScroller.js";
6
6
  import { ConversationEmptyState } from "./Conversation.js";
7
7
  import { Message, MessageAction, MessageActions, MessageAttachment, MessageContent, MessageResponse } from "./Message.js";
8
+ import { Marker, MarkerContent } from "./Marker.js";
8
9
  import { MenuGroup, MenuGroupLabel } from "./Menu.js";
9
10
  import { PromptInput, PromptInputActionAddAttachments, PromptInputActionMenu, PromptInputActionMenuContent, PromptInputActionMenuTrigger, PromptInputApproval, PromptInputAttachments, PromptInputBody, PromptInputButton, PromptInputFooter, PromptInputPermissionSelector, PromptInputSubmit, PromptInputTextarea, PromptInputTools, getApprovalRequestId, getApprovalStateKey, getPendingToolApprovals, usePromptInputAttachments } from "./PromptInput.js";
10
11
  import { Reasoning, ReasoningContent, ReasoningTrigger } from "./Reasoning.js";
12
+ import { Source, Sources, SourcesContent, SourcesTrigger } from "./Sources.js";
11
13
  import * as React from "react";
12
14
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
+ import { isCompactionMessage } from "agents/experimental/memory/utils";
13
16
  import { getToolInput, getToolOutput, getToolPartState } from "@cloudflare/think/react";
14
17
  import { getToolName, isFileUIPart, isReasoningUIPart, isTextUIPart, isToolUIPart } from "ai";
15
18
  //#region src/components/AgentChat.tsx
@@ -33,6 +36,21 @@ function AgentChatAddMenu({ children }) {
33
36
  children: [/* @__PURE__ */ jsxs(MenuGroup, { children: [/* @__PURE__ */ jsx(MenuGroupLabel, { children: "Files" }), /* @__PURE__ */ jsx(PromptInputActionAddAttachments, {})] }), children]
34
37
  })] });
35
38
  }
39
+ /**
40
+ * Per-message row-size compaction marker from `AIChatAgent` persistence.
41
+ *
42
+ * This is distinct from Session history compaction overlays. When a single
43
+ * SQLite row approaches the Workers/SQLite size limit, Agents compacts large
44
+ * tool outputs and, if necessary, truncates text before persistence. The
45
+ * message remains in place and receives metadata keys documented by Agents:
46
+ * `compactedToolOutputs` lists tool-call ids whose outputs were summarized;
47
+ * `compactedTextParts` lists text-part indexes that were truncated. The UI
48
+ * reads those keys structurally because they are persisted metadata rather than
49
+ * exported AI SDK part variants, then adds a local visual note while preserving
50
+ * the original `UIMessage` and part shapes.
51
+ *
52
+ * @see https://github.com/cloudflare/agents/blob/main/docs/agents/chat-agents.md#row-size-protection
53
+ */
36
54
  function isCompactedPart(message, part, partIndex) {
37
55
  const metadata = message.metadata;
38
56
  if (isTextUIPart(part)) return Array.isArray(metadata?.compactedTextParts) && metadata.compactedTextParts.includes(partIndex);
@@ -60,6 +78,69 @@ function isToolPartFailed(part) {
60
78
  const state = getToolPartState(part);
61
79
  return state === "error" || state === "denied";
62
80
  }
81
+ /**
82
+ * AI SDK exports concrete source part types but no public source-part guard.
83
+ * Keep these local guards as exact discriminant checks, matching
84
+ * `ai/src/ui/ui-messages.ts` and `ai/src/ui/process-ui-message-stream.ts`.
85
+ * If AI SDK exports source or step guards later, replace these wrappers rather
86
+ * than widening the local transcript contract.
87
+ */
88
+ function isSourceUrlPart(part) {
89
+ return part?.type === "source-url";
90
+ }
91
+ function isSourceDocumentPart(part) {
92
+ return part?.type === "source-document";
93
+ }
94
+ /**
95
+ * AI SDK emits `step-start` as a native `StepStartUIPart`, but currently exports
96
+ * only the type, not an `isStepStartUIPart` helper. Keep this as the exact
97
+ * upstream discriminant so step separators do not become a local protocol.
98
+ */
99
+ function isStepStartPart(part) {
100
+ return part.type === "step-start";
101
+ }
102
+ /**
103
+ * Session compaction overlay marker from Agents Sessions.
104
+ *
105
+ * Agents exports `isCompactionMessage` from
106
+ * `agents/experimental/memory/utils`; it checks the canonical `compaction_`
107
+ * id prefix used when `Session.getHistory()` materializes a summary overlay.
108
+ * `UIMessage` is structurally compatible with the fields the helper reads, so
109
+ * keep this wrapper instead of duplicating the prefix test locally.
110
+ */
111
+ function isCompactionMessage$1(message) {
112
+ return isCompactionMessage(message);
113
+ }
114
+ function getMessageText(message) {
115
+ return message.parts.filter((part) => isTextUIPart(part) || isReasoningUIPart(part)).map((part) => part.text).join("");
116
+ }
117
+ function formatSourceTitle(part) {
118
+ if (part.title) return part.title;
119
+ if (part.type === "source-document") return part.filename ?? part.mediaType;
120
+ try {
121
+ return new URL(part.url).hostname.replace(/^www\./, "");
122
+ } catch {
123
+ return part.url;
124
+ }
125
+ }
126
+ function renderSources(parts) {
127
+ return /* @__PURE__ */ jsxs(Sources, { children: [/* @__PURE__ */ jsx(SourcesTrigger, { count: parts.length }), /* @__PURE__ */ jsx(SourcesContent, { children: parts.map((part) => part.type === "source-url" ? /* @__PURE__ */ jsx(Source, {
128
+ href: part.url,
129
+ title: formatSourceTitle(part)
130
+ }, part.sourceId) : /* @__PURE__ */ jsx(Source, {
131
+ title: formatSourceTitle(part),
132
+ description: part.filename ?? part.mediaType
133
+ }, part.sourceId)) })] });
134
+ }
135
+ function getSourceRun(parts, start) {
136
+ const sources = [];
137
+ for (let index = start; index < parts.length; index++) {
138
+ const candidate = parts[index];
139
+ if (!isSourceUrlPart(candidate) && !isSourceDocumentPart(candidate)) break;
140
+ sources.push(candidate);
141
+ }
142
+ return sources;
143
+ }
63
144
  function describeToolPart(part) {
64
145
  const name = getToolName(part);
65
146
  const verb = isToolPartFailed(part) ? `${name} failed` : isToolPartWorking(part) ? `Running ${name}` : `Ran ${name}`;
@@ -96,6 +177,10 @@ function renderDefaultPart(part) {
96
177
  children: [/* @__PURE__ */ jsx(ReasoningTrigger, {}), /* @__PURE__ */ jsx(ReasoningContent, { children: part.text })]
97
178
  });
98
179
  if (isToolUIPart(part)) return renderToolActivity(part);
180
+ if (isStepStartPart(part)) return /* @__PURE__ */ jsx(Marker, {
181
+ variant: "separator",
182
+ children: /* @__PURE__ */ jsx(MarkerContent, { children: "Next step" })
183
+ });
99
184
  return null;
100
185
  }
101
186
  /**
@@ -168,7 +253,10 @@ function AgentChat({ className, messages, status = "ready", onSubmit, onStop, pl
168
253
  }) : null, messages.map((message, messageIndex) => /* @__PURE__ */ jsx(MessageScrollerItem, {
169
254
  messageId: message.id,
170
255
  scrollAnchor: message.role === "user",
171
- children: /* @__PURE__ */ jsxs(Message, {
256
+ children: isCompactionMessage$1(message) ? /* @__PURE__ */ jsx(Marker, {
257
+ variant: "separator",
258
+ children: /* @__PURE__ */ jsx(MarkerContent, { children: getMessageText(message) || "Conversation compacted" })
259
+ }) : /* @__PURE__ */ jsxs(Message, {
172
260
  from: message.role,
173
261
  children: [
174
262
  message.parts.map((part, partIndex) => {
@@ -179,6 +267,12 @@ function AgentChat({ className, messages, status = "ready", onSubmit, onStop, pl
179
267
  partIndex,
180
268
  defaultRender
181
269
  });
270
+ if (isSourceUrlPart(part) || isSourceDocumentPart(part)) {
271
+ if (custom !== void 0) return custom ? /* @__PURE__ */ jsx(MessageContent, { children: custom }, getPartKey(part, partIndex)) : null;
272
+ const previous = message.parts[partIndex - 1];
273
+ if (isSourceUrlPart(previous) || isSourceDocumentPart(previous)) return null;
274
+ return /* @__PURE__ */ jsx(MessageContent, { children: renderSources(getSourceRun(message.parts, partIndex)) }, getPartKey(part, partIndex));
275
+ }
182
276
  const rendered = custom === void 0 ? defaultRender() : custom;
183
277
  return rendered ? /* @__PURE__ */ jsxs(MessageContent, { children: [rendered, isCompactedPart(message, part, partIndex) ? /* @__PURE__ */ jsx("div", {
184
278
  className: "message__compacted",
@@ -1,9 +1,9 @@
1
1
  import { n as ExecutionApprovals } from "../PromptInput-D6-G24p7.js";
2
- import { a as AgentChatRenderPartContext, r as AgentChatProps } from "../AgentChat-403mckDG.js";
2
+ import { a as AgentChatRenderPartContext, r as AgentChatProps } from "../AgentChat-B9LD_WF1.js";
3
3
  import { n as BrowserLiveViewProps, t as BrowserLiveView } from "../BrowserLiveView-ZktFZiMD.js";
4
4
  import { n as BrowserReplayProps, t as BrowserReplay } from "../BrowserReplay-6G-mnwcn.js";
5
5
  import { _ as isThinkChatActivityPart } from "../ThinkChatToolParts-DnUiZgCC.js";
6
- import { a as ThinkChatToolRenderers, c as renderThinkChatActivityPart, i as ThinkChatToolRenderer, n as ThinkChatActivityProps, o as createThinkChatActivityRenderer, r as ThinkChatActivityRendererOptions, s as describeThinkChatActivityPart, t as ThinkChatActivity } from "../ThinkChatActivity-CeHzk6-K.js";
6
+ import { a as ThinkChatToolRenderers, c as renderThinkChatActivityPart, i as ThinkChatToolRenderer, n as ThinkChatActivityProps, o as createThinkChatActivityRenderer, r as ThinkChatActivityRendererOptions, s as describeThinkChatActivityPart, t as ThinkChatActivity } from "../ThinkChatActivity-B2UD95My.js";
7
7
  import { n as ThinkChatValue } from "../ThinkChatContext-BLAAb8J5.js";
8
8
  import { FileInfo, UseWorkspaceOptions, WorkspaceExplorer, WorkspaceExplorerProps, WorkspaceFileState, WorkspaceInfo, WorkspaceSource, WorkspaceState, inferWorkspaceLanguage, useWorkspace } from "./WorkspaceExplorer.js";
9
9
  import * as React from "react";
@@ -1,3 +1,3 @@
1
1
  import { _ as isThinkChatActivityPart } from "../ThinkChatToolParts-DnUiZgCC.js";
2
- import { a as ThinkChatToolRenderers, c as renderThinkChatActivityPart, i as ThinkChatToolRenderer, n as ThinkChatActivityProps, o as createThinkChatActivityRenderer, r as ThinkChatActivityRendererOptions, s as describeThinkChatActivityPart, t as ThinkChatActivity } from "../ThinkChatActivity-CeHzk6-K.js";
2
+ import { a as ThinkChatToolRenderers, c as renderThinkChatActivityPart, i as ThinkChatToolRenderer, n as ThinkChatActivityProps, o as createThinkChatActivityRenderer, r as ThinkChatActivityRendererOptions, s as describeThinkChatActivityPart, t as ThinkChatActivity } from "../ThinkChatActivity-B2UD95My.js";
3
3
  export { ThinkChatActivity, ThinkChatActivityProps, ThinkChatActivityRendererOptions, ThinkChatToolRenderer, ThinkChatToolRenderers, createThinkChatActivityRenderer, describeThinkChatActivityPart, isThinkChatActivityPart, renderThinkChatActivityPart };
@@ -9,7 +9,20 @@ import { Think } from "@cloudflare/think";
9
9
 
10
10
  //#region src/components/ThinkFullApp.d.ts
11
11
  type ThinkFullAppAgent<State = unknown> = Think<object, State>;
12
- type ThinkFullAppWorkbenchView = "files" | "execution";
12
+ type ThinkFullAppWorkbenchView = "files" | "execution" | (string & {});
13
+ interface ThinkFullAppPanelContext<AgentT extends ThinkFullAppAgent<State>, State = unknown> {
14
+ agent: ReturnType<typeof useAgent<AgentT, State>>;
15
+ chat: ReturnType<typeof useAgentChat<State, UIMessage>>;
16
+ workspace?: WorkspaceSource;
17
+ workspaceRoot?: string;
18
+ }
19
+ interface ThinkFullAppPanel<AgentT extends ThinkFullAppAgent<State>, State = unknown> {
20
+ /** Unique panel id used by `workbenchView`; custom panels should not reuse built-in ids. */
21
+ id: ThinkFullAppWorkbenchView;
22
+ label: string;
23
+ icon: React.ReactNode;
24
+ render: (context: ThinkFullAppPanelContext<AgentT, State>) => React.ReactNode;
25
+ }
13
26
  interface ThinkFullAppProps<AgentT extends ThinkFullAppAgent<State>, State = unknown> extends React.HTMLAttributes<HTMLDivElement> {
14
27
  /** The `useAgent` value from `agents/react`; its stub drives approvals and execution records. */
15
28
  agent: ReturnType<typeof useAgent<AgentT, State>>;
@@ -21,6 +34,8 @@ interface ThinkFullAppProps<AgentT extends ThinkFullAppAgent<State>, State = unk
21
34
  workspaceRoot?: string;
22
35
  /** Message rendering labels and approval overrides for ThinkChat. */
23
36
  messages?: ThinkChatMessagesProps;
37
+ /** Extra workbench panels appended after the built-in Files and Execution panels. */
38
+ workbenchPanels?: readonly ThinkFullAppPanel<AgentT, State>[];
24
39
  /** Optional parent-owned thread picker/sidebar rendered beside the selected child app. */
25
40
  threadSidebar?: React.ReactNode;
26
41
  /** Whether the thread sidebar is open. Omit for internal state. */
@@ -50,6 +65,7 @@ declare function ThinkFullApp<AgentT extends ThinkFullAppAgent<State>, State = u
50
65
  style,
51
66
  threadSidebar,
52
67
  threadSidebarOpen,
68
+ workbenchPanels,
53
69
  workbenchView,
54
70
  workspace,
55
71
  workspaceRoot,
@@ -58,4 +74,4 @@ declare function ThinkFullApp<AgentT extends ThinkFullAppAgent<State>, State = u
58
74
  ref?: React.Ref<HTMLDivElement>;
59
75
  }): React.JSX.Element;
60
76
  //#endregion
61
- export { type ThinkExecutionAgent, ThinkExecutionWorkbench, type ThinkExecutionWorkbenchProps, type ThinkExecutionWorkbenchSource, ThinkFullApp, ThinkFullAppAgent, ThinkFullAppProps, ThinkFullAppWorkbenchView };
77
+ export { type ThinkExecutionAgent, ThinkExecutionWorkbench, type ThinkExecutionWorkbenchProps, type ThinkExecutionWorkbenchSource, ThinkFullApp, ThinkFullAppAgent, ThinkFullAppPanel, ThinkFullAppPanelContext, ThinkFullAppProps, ThinkFullAppWorkbenchView };
@@ -68,8 +68,8 @@ function WorkbenchViewItem({ children, label, value }) {
68
68
  children: /* @__PURE__ */ jsx(TooltipPopup, { children: label })
69
69
  }) })] });
70
70
  }
71
- function WorkbenchViewSwitch({ value, onValueChange }) {
72
- return /* @__PURE__ */ jsxs(ToggleGroup, {
71
+ function WorkbenchViewSwitch({ panels, value, onValueChange }) {
72
+ return /* @__PURE__ */ jsx(ToggleGroup, {
73
73
  className: "think-full-app__workbench-view-switch",
74
74
  size: "sm",
75
75
  value: [value],
@@ -78,36 +78,15 @@ function WorkbenchViewSwitch({ value, onValueChange }) {
78
78
  const [next] = nextValue;
79
79
  if (next) onValueChange(next);
80
80
  },
81
- children: [/* @__PURE__ */ jsx(WorkbenchViewItem, {
82
- value: "files",
83
- label: "Files",
84
- children: /* @__PURE__ */ jsx(FileIcon, {})
85
- }), /* @__PURE__ */ jsx(WorkbenchViewItem, {
86
- value: "execution",
87
- label: "Execution",
88
- children: /* @__PURE__ */ jsx(TerminalIcon, {})
89
- })]
90
- });
91
- }
92
- function WorkbenchPanel({ executionSource, view, workspace, workspaceRoot }) {
93
- if (workspace) return /* @__PURE__ */ jsx("div", {
94
- className: "think-full-app__workbench-panel",
95
- "data-view": view,
96
- children: view === "files" ? /* @__PURE__ */ jsx(WorkspacePanel, {
97
- source: workspace,
98
- root: workspaceRoot
99
- }) : /* @__PURE__ */ jsx(ThinkExecutionWorkbench, {
100
- source: executionSource,
101
- title: "Execution"
102
- })
103
- });
104
- return /* @__PURE__ */ jsx(ThinkExecutionWorkbench, {
105
- source: executionSource,
106
- title: "Execution"
81
+ children: panels.map((panel) => /* @__PURE__ */ jsx(WorkbenchViewItem, {
82
+ value: panel.id,
83
+ label: panel.label,
84
+ children: panel.icon
85
+ }, panel.id))
107
86
  });
108
87
  }
109
88
  /** Combines ThinkChat with an optional resizable workbench shell. */
110
- function ThinkFullApp({ agent, chat, className, defaultWorkbenchView, defaultThreadSidebarOpen, messages, onThreadSidebarOpenChange, onWorkbenchViewChange, ref, style, threadSidebar, threadSidebarOpen, workbenchView, workspace, workspaceRoot, ...props }) {
89
+ function ThinkFullApp({ agent, chat, className, defaultWorkbenchView, defaultThreadSidebarOpen, messages, onThreadSidebarOpenChange, onWorkbenchViewChange, ref, style, threadSidebar, threadSidebarOpen, workbenchPanels, workbenchView, workspace, workspaceRoot, ...props }) {
111
90
  const approvals = useThinkPendingApprovals(agent.stub, `${chat.messages.length}:${chat.status}`);
112
91
  const resolvedMessages = React.useMemo(() => ({
113
92
  ...messages,
@@ -118,14 +97,55 @@ function ThinkFullApp({ agent, chat, className, defaultWorkbenchView, defaultThr
118
97
  approvals.pendingApprovals,
119
98
  messages
120
99
  ]);
121
- const hasWorkspace = workspace !== void 0;
122
100
  const [mobileView, setMobileView] = React.useState("chat");
123
101
  const [innerWorkbenchView, setInnerWorkbenchView] = React.useState(defaultWorkbenchView ?? "files");
124
102
  const [workbenchPanelWidth, setWorkbenchPanelWidth] = React.useState(workbenchPanelDefaultWidth);
103
+ const hasWorkspace = workspace !== void 0;
104
+ const panelContext = React.useMemo(() => ({
105
+ agent,
106
+ chat,
107
+ workspace,
108
+ workspaceRoot
109
+ }), [
110
+ agent,
111
+ chat,
112
+ workspace,
113
+ workspaceRoot
114
+ ]);
115
+ const availableWorkbenchPanels = React.useMemo(() => {
116
+ const builtInPanels = [];
117
+ if (workspace !== void 0) builtInPanels.push({
118
+ id: "files",
119
+ label: "Files",
120
+ icon: /* @__PURE__ */ jsx(FileIcon, {}),
121
+ render: () => /* @__PURE__ */ jsx(WorkspacePanel, {
122
+ source: workspace,
123
+ root: workspaceRoot
124
+ })
125
+ });
126
+ builtInPanels.push({
127
+ id: "execution",
128
+ label: "Execution",
129
+ icon: /* @__PURE__ */ jsx(TerminalIcon, {}),
130
+ render: ({ agent }) => /* @__PURE__ */ jsx(ThinkExecutionWorkbench, {
131
+ source: agent.stub,
132
+ title: "Execution"
133
+ })
134
+ });
135
+ return workbenchPanels ? [...builtInPanels, ...workbenchPanels] : builtInPanels;
136
+ }, [
137
+ workbenchPanels,
138
+ workspace,
139
+ workspaceRoot
140
+ ]);
125
141
  const activeMobileView = mobileView === "workbench" ? "workbench" : "chat";
126
142
  const isMobileWorkbenchView = activeMobileView === "workbench";
127
- const activeWorkbenchView = hasWorkspace ? workbenchView ?? innerWorkbenchView : "execution";
128
- const showShellTopbar = hasWorkspace || isMobileWorkbenchView;
143
+ const requestedWorkbenchView = workbenchView ?? innerWorkbenchView;
144
+ const activePanel = availableWorkbenchPanels.find((panel) => panel.id === requestedWorkbenchView) ?? availableWorkbenchPanels[0];
145
+ const activeWorkbenchView = activePanel?.id ?? "execution";
146
+ const mobileWorkbenchLabel = hasWorkspace && activeWorkbenchView === "files" ? "Show workbench" : `Show ${activePanel?.label.toLowerCase() ?? "workbench"}`;
147
+ const showWorkbenchViewSwitch = availableWorkbenchPanels.length > 1;
148
+ const showShellTopbar = showWorkbenchViewSwitch || isMobileWorkbenchView;
129
149
  const rootStyle = {
130
150
  ...style,
131
151
  "--think-full-app-workbench-w": `${workbenchPanelWidth}px`
@@ -134,21 +154,16 @@ function ThinkFullApp({ agent, chat, className, defaultWorkbenchView, defaultThr
134
154
  setWorkbenchPanelWidth((current) => clampWorkbenchPanelWidth(current + delta));
135
155
  };
136
156
  const setWorkbenchView = React.useCallback((next) => {
137
- if (!hasWorkspace) return;
138
157
  if (workbenchView === void 0) setInnerWorkbenchView(next);
139
158
  onWorkbenchViewChange?.(next);
140
- }, [
141
- hasWorkspace,
142
- onWorkbenchViewChange,
143
- workbenchView
144
- ]);
159
+ }, [onWorkbenchViewChange, workbenchView]);
145
160
  const app = /* @__PURE__ */ jsxs("div", {
146
161
  ref,
147
162
  className: cx("think-full-app", className),
148
163
  "data-has-files": hasWorkspace ? "true" : "false",
149
164
  "data-has-thread-sidebar": threadSidebar ? "true" : "false",
150
165
  "data-has-workbench": "true",
151
- "data-has-workbench-topbar": hasWorkspace ? "true" : "false",
166
+ "data-has-workbench-topbar": showWorkbenchViewSwitch ? "true" : "false",
152
167
  "data-mobile-view": activeMobileView,
153
168
  style: rootStyle,
154
169
  ...props,
@@ -157,8 +172,9 @@ function ThinkFullApp({ agent, chat, className, defaultWorkbenchView, defaultThr
157
172
  className: "think-full-app__topbar",
158
173
  role: "toolbar",
159
174
  "aria-label": "Workbench controls",
160
- "data-mobile-only": hasWorkspace ? void 0 : "true",
161
- children: [/* @__PURE__ */ jsx(WorkbenchBackButton, { onBack: () => setMobileView("chat") }), hasWorkspace ? /* @__PURE__ */ jsx(WorkbenchViewSwitch, {
175
+ "data-mobile-only": showWorkbenchViewSwitch ? void 0 : "true",
176
+ children: [/* @__PURE__ */ jsx(WorkbenchBackButton, { onBack: () => setMobileView("chat") }), showWorkbenchViewSwitch ? /* @__PURE__ */ jsx(WorkbenchViewSwitch, {
177
+ panels: availableWorkbenchPanels,
162
178
  value: activeWorkbenchView,
163
179
  onValueChange: setWorkbenchView
164
180
  }) : null]
@@ -173,9 +189,9 @@ function ThinkFullApp({ agent, chat, className, defaultWorkbenchView, defaultThr
173
189
  color: "neutral",
174
190
  size: "icon-sm",
175
191
  className: "think-full-app__mobile-action",
176
- "aria-label": hasWorkspace ? "Show workbench" : "Show execution",
192
+ "aria-label": mobileWorkbenchLabel,
177
193
  onClick: () => setMobileView("workbench"),
178
- children: hasWorkspace ? /* @__PURE__ */ jsx(FileIcon, {}) : /* @__PURE__ */ jsx(TerminalIcon, {})
194
+ children: activePanel?.icon ?? /* @__PURE__ */ jsx(TerminalIcon, {})
179
195
  })
180
196
  }) : null,
181
197
  /* @__PURE__ */ jsx("section", {
@@ -235,11 +251,10 @@ function ThinkFullApp({ agent, chat, className, defaultWorkbenchView, defaultThr
235
251
  "aria-label": "Workbench",
236
252
  children: /* @__PURE__ */ jsx("div", {
237
253
  className: "think-full-app__workbench-slot",
238
- children: /* @__PURE__ */ jsx(WorkbenchPanel, {
239
- executionSource: agent.stub,
240
- workspace,
241
- workspaceRoot,
242
- view: activeWorkbenchView
254
+ children: /* @__PURE__ */ jsx("div", {
255
+ className: "think-full-app__workbench-panel",
256
+ "data-view": activeWorkbenchView,
257
+ children: activePanel?.render(panelContext)
243
258
  })
244
259
  })
245
260
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-b/react-components",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "MCP-B React components built on Base UI and shared design tokens, including Cloudflare Think chat primitives.",
5
5
  "homepage": "https://design-system.sigvelo.com",
6
6
  "bugs": {
@@ -50,7 +50,7 @@
50
50
  "anser": "^2.3.5",
51
51
  "shiki": "^3.23.0",
52
52
  "streamdown": "^2.5.0",
53
- "@mcp-b/design-tokens": "0.8.0"
53
+ "@mcp-b/design-tokens": "0.9.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@ai-sdk/react": "^3.0.222",