@mcp-b/react-components 0.3.0 → 0.4.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.
@@ -1,9 +1,10 @@
1
1
  import { t as cx } from "../class-names-9UtZwb0J.js";
2
- import { A as SpinnerIcon, C as MonitorIcon, D as PlusIcon, N as WarningIcon, T as PencilIcon, i as ArrowUpIcon, j as SquareIcon, v as ImageIcon } from "../icons-DQC4Zw5R.js";
2
+ import { A as SpinnerIcon, C as MonitorIcon, D as PlusIcon, F as XIcon, N as WarningIcon, T as PencilIcon, h as FileIcon, i as ArrowUpIcon, j as SquareIcon, s as ChevronDownIcon, v as ImageIcon } from "../icons-DQC4Zw5R.js";
3
3
  import { Button } from "./Button.js";
4
4
  import { Tooltip, TooltipPopup, TooltipPortal, TooltipPositioner, TooltipTrigger } from "./Tooltip.js";
5
+ import { t as AttachmentPreview } from "../attachment-preview-ChtC3HiU.js";
5
6
  import { Combobox, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxList, ComboboxSeparator } from "./Combobox.js";
6
- import { MenuItem, MenuPopup, MenuPortal, MenuPositioner, MenuRoot, MenuTrigger } from "./Menu.js";
7
+ import { MenuItem, MenuPopup, MenuPortal, MenuPositioner, MenuRadioGroup, MenuRadioItem, MenuRadioItemIndicator, MenuRoot, MenuTrigger } from "./Menu.js";
7
8
  import { Select, SelectContent, SelectItem, SelectPortal, SelectPositioner, SelectTrigger, SelectValue } from "./Select.js";
8
9
  import { Tab, TabPanel, TabsList } from "./Tabs.js";
9
10
  import * as React from "react";
@@ -339,6 +340,59 @@ function PromptInputHeader({ className, children, ref, ...props }) {
339
340
  children
340
341
  });
341
342
  }
343
+ /**
344
+ * One pending attachment chip: image files show a thumbnail, everything else
345
+ * a file icon and name. The corner button removes it from the pending list.
346
+ */
347
+ function PromptInputAttachment({ file, className, ref, ...props }) {
348
+ const attachments = usePromptInputAttachments();
349
+ const isImage = file.mediaType.startsWith("image/");
350
+ const name = file.filename ?? "attachment";
351
+ return /* @__PURE__ */ jsxs("div", {
352
+ ref,
353
+ className: cx("prompt-input__attachment", className),
354
+ "data-image": isImage || void 0,
355
+ ...props,
356
+ children: [isImage ? /* @__PURE__ */ jsx(AttachmentPreview, {
357
+ url: file.url,
358
+ filename: file.filename,
359
+ children: /* @__PURE__ */ jsx("button", {
360
+ type: "button",
361
+ className: "prompt-input__attachment-zoom",
362
+ "aria-label": `Preview ${name}`,
363
+ children: /* @__PURE__ */ jsx("img", {
364
+ className: "prompt-input__attachment-thumb",
365
+ src: file.url,
366
+ alt: ""
367
+ })
368
+ })
369
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FileIcon, { className: "prompt-input__attachment-icon" }), /* @__PURE__ */ jsx("span", {
370
+ className: "prompt-input__attachment-name",
371
+ children: name
372
+ })] }), /* @__PURE__ */ jsx("button", {
373
+ type: "button",
374
+ className: "prompt-input__attachment-remove",
375
+ "aria-label": `Remove ${name}`,
376
+ onClick: () => attachments.remove(file.id),
377
+ children: /* @__PURE__ */ jsx(XIcon, { size: 12 })
378
+ })]
379
+ });
380
+ }
381
+ /**
382
+ * The pending-attachment chip row. Renders nothing while the list is empty,
383
+ * so it can sit in the composer unconditionally — files added via the file
384
+ * dialog, paste, or drop appear here until submit sends and clears them.
385
+ */
386
+ function PromptInputAttachments({ className, children, ref, ...props }) {
387
+ const attachments = usePromptInputAttachments();
388
+ if (attachments.files.length === 0) return null;
389
+ return /* @__PURE__ */ jsx("div", {
390
+ ref,
391
+ className: cx("prompt-input__attachments", className),
392
+ ...props,
393
+ children: attachments.files.map((file) => /* @__PURE__ */ jsx(React.Fragment, { children: children ? children(file) : /* @__PURE__ */ jsx(PromptInputAttachment, { file }) }, file.id))
394
+ });
395
+ }
342
396
  function PromptInputBody({ className, children, ref, ...props }) {
343
397
  return /* @__PURE__ */ jsx("div", {
344
398
  ref,
@@ -525,6 +579,60 @@ function PromptInputActionAddScreenshot({ label = "Take screenshot", onClick, ch
525
579
  children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MonitorIcon, { className: "prompt-input__action-menu-icon" }), label] })
526
580
  });
527
581
  }
582
+ const PERMISSION_MODES = [{
583
+ value: "ask",
584
+ label: "Ask for approval",
585
+ description: "Pause and ask before sensitive actions run"
586
+ }, {
587
+ value: "bypass",
588
+ label: "Bypass permissions",
589
+ description: "Approve every request automatically"
590
+ }];
591
+ /**
592
+ * Footer control for choosing how approval requests are handled — ask each
593
+ * time, or bypass. Purely a picker: the auto-approve behavior lives with
594
+ * whoever owns the approvals (`AgentChat` wires both ends).
595
+ */
596
+ function PromptInputPermissionSelector({ value, onValueChange, question = "How should agent actions be approved?" }) {
597
+ return /* @__PURE__ */ jsxs(MenuRoot, { children: [/* @__PURE__ */ jsx(MenuTrigger, { render: /* @__PURE__ */ jsxs(PromptInputButton, {
598
+ size: "sm",
599
+ className: "prompt-input__permissions-trigger",
600
+ "data-mode": value,
601
+ "aria-label": (PERMISSION_MODES.find((mode) => mode.value === value) ?? PERMISSION_MODES[0]).label,
602
+ children: [/* @__PURE__ */ jsx("span", {
603
+ className: "prompt-input__permissions-current",
604
+ children: PERMISSION_MODES.map((mode) => /* @__PURE__ */ jsxs("span", {
605
+ className: "prompt-input__permissions-current-option",
606
+ "data-active": mode.value === value || void 0,
607
+ children: [mode.value === "bypass" ? /* @__PURE__ */ jsx(WarningIcon, { size: 14 }) : null, mode.label]
608
+ }, mode.value))
609
+ }), /* @__PURE__ */ jsx(ChevronDownIcon, { size: 12 })]
610
+ }) }), /* @__PURE__ */ jsx(MenuPortal, { children: /* @__PURE__ */ jsx(MenuPositioner, { children: /* @__PURE__ */ jsxs(MenuPopup, {
611
+ className: "prompt-input__permissions-menu",
612
+ children: [/* @__PURE__ */ jsx("div", {
613
+ className: "prompt-input__permissions-question",
614
+ children: question
615
+ }), /* @__PURE__ */ jsx(MenuRadioGroup, {
616
+ value,
617
+ onValueChange: (mode) => onValueChange(mode),
618
+ children: PERMISSION_MODES.map((mode) => /* @__PURE__ */ jsxs(MenuRadioItem, {
619
+ value: mode.value,
620
+ closeOnClick: true,
621
+ className: "prompt-input__permissions-option",
622
+ children: [/* @__PURE__ */ jsxs("span", {
623
+ className: "prompt-input__permissions-option-text",
624
+ children: [/* @__PURE__ */ jsx("span", {
625
+ className: "prompt-input__permissions-option-label",
626
+ children: mode.label
627
+ }), /* @__PURE__ */ jsx("span", {
628
+ className: "prompt-input__permissions-option-description",
629
+ children: mode.description
630
+ })]
631
+ }), /* @__PURE__ */ jsx(MenuRadioItemIndicator, { className: "prompt-input__permissions-check" })]
632
+ }, mode.value))
633
+ })]
634
+ }) }) })] });
635
+ }
528
636
  function PromptInputSubmit({ className, status = "ready", onStop, variant = "normal", color = "primary", size = "icon", onClick, children, label, ref, ...props }) {
529
637
  const isStopping = status === "submitted" || status === "streaming";
530
638
  const resolvedLabel = label ?? defaultSubmitLabel(status);
@@ -549,12 +657,22 @@ function PromptInputSubmit({ className, status = "ready", onStop, variant = "nor
549
657
  children: children ?? defaultSubmitIcon(status)
550
658
  });
551
659
  }
660
+ function getPausedExecution(part) {
661
+ if (part.state !== "output-available") return void 0;
662
+ const output = part.output;
663
+ return output?.status === "paused" && typeof output.executionId === "string" ? output : void 0;
664
+ }
665
+ /** The id a response must carry: the approval id, or the paused run's executionId. */
666
+ function getApprovalRequestId(part) {
667
+ if (part.state === "approval-requested") return part.approval.id;
668
+ return getPausedExecution(part)?.executionId;
669
+ }
552
670
  /** Derives pending tool approvals straight from the messages — no app-owned shapes. */
553
- function getPendingToolApprovals(messages) {
671
+ function getPendingToolApprovals(messages, { pausedExecutions = false } = {}) {
554
672
  const pending = [];
555
673
  messages.forEach((message, messageIndex) => {
556
674
  message.parts.forEach((part, partIndex) => {
557
- if (isToolOrDynamicToolUIPart(part) && part.state === "approval-requested") pending.push({
675
+ if (isToolOrDynamicToolUIPart(part) && (part.state === "approval-requested" || pausedExecutions && getPausedExecution(part))) pending.push({
558
676
  message,
559
677
  messageIndex,
560
678
  part,
@@ -575,30 +693,33 @@ function formatApprovalValue(value) {
575
693
  * sends the text as the native approval `reason`. Submit (or Enter) sends the
576
694
  * current selection — approve by default.
577
695
  */
578
- function PromptInputApproval({ part, onRespond, onSkip, question, count, className, ref, ...props }) {
696
+ function PromptInputApproval({ part, onRespond, executionApprovals, onSkip, question, count, className, ref, ...props }) {
579
697
  const [selected, setSelected] = React.useState("approve");
580
698
  const [reason, setReason] = React.useState("");
581
699
  const [busy, setBusy] = React.useState(false);
582
700
  const yesRef = React.useRef(null);
583
701
  const reasonRef = React.useRef(null);
584
702
  const approval = part.state === "approval-requested" ? part.approval : void 0;
585
- const approvalId = approval?.id;
703
+ const paused = getPausedExecution(part);
704
+ const approvalId = approval?.id ?? paused?.executionId;
586
705
  React.useEffect(() => {
587
706
  setSelected("approve");
588
707
  setReason("");
589
708
  setBusy(false);
590
709
  }, [approvalId]);
591
- if (!approval) return null;
710
+ if (approval ? !onRespond : !(paused && executionApprovals)) return null;
592
711
  const toolName = getToolName(part);
593
712
  const respond = (approved) => {
594
713
  if (busy) return;
595
714
  setBusy(true);
596
715
  const trimmed = reason.trim();
597
- onRespond({
716
+ const denyReason = !approved && trimmed ? trimmed : void 0;
717
+ if (approval) onRespond?.({
598
718
  id: approval.id,
599
719
  approved,
600
- reason: !approved && trimmed ? trimmed : void 0
720
+ reason: denyReason
601
721
  });
722
+ else if (paused) approved ? executionApprovals?.approve(paused.executionId) : executionApprovals?.reject(paused.executionId, denyReason);
602
723
  };
603
724
  const submit = () => respond(selected === "approve");
604
725
  const input = "input" in part ? part.input : void 0;
@@ -618,16 +739,33 @@ function PromptInputApproval({ part, onRespond, onSkip, question, count, classNa
618
739
  children: [
619
740
  /* @__PURE__ */ jsxs("div", {
620
741
  className: "prompt-input__approval-question",
621
- children: [/* @__PURE__ */ jsx("span", { children: question ?? /* @__PURE__ */ jsxs(Fragment, { children: [
742
+ children: [/* @__PURE__ */ jsx("span", { children: question ?? (paused ? /* @__PURE__ */ jsx(Fragment, { children: "The agent's code is paused on these calls. Allow it to continue?" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
622
743
  "Do you want to allow the agent to run ",
623
744
  /* @__PURE__ */ jsx("code", { children: toolName }),
624
745
  "?"
625
- ] }) }), count && count > 1 ? /* @__PURE__ */ jsxs("span", {
746
+ ] })) }), count && count > 1 ? /* @__PURE__ */ jsxs("span", {
626
747
  className: "prompt-input__approval-count",
627
748
  children: ["1 of ", count]
628
749
  }) : null]
629
750
  }),
630
- input !== void 0 ? /* @__PURE__ */ jsx("div", {
751
+ paused ? /* @__PURE__ */ jsx("div", {
752
+ className: "prompt-input__approval-preview",
753
+ children: paused.pending.map((action) => /* @__PURE__ */ jsxs("div", {
754
+ className: "prompt-input__approval-preview-row",
755
+ children: [
756
+ /* @__PURE__ */ jsxs("span", {
757
+ className: "prompt-input__approval-preview-key",
758
+ children: [
759
+ action.connector,
760
+ ".",
761
+ action.method
762
+ ]
763
+ }),
764
+ " ",
765
+ action.args === void 0 ? null : formatApprovalValue(action.args)
766
+ ]
767
+ }, action.seq))
768
+ }) : input !== void 0 ? /* @__PURE__ */ jsx("div", {
631
769
  className: "prompt-input__approval-preview",
632
770
  children: inputEntries ? inputEntries.map(([key, value]) => /* @__PURE__ */ jsxs("div", {
633
771
  className: "prompt-input__approval-preview-row",
@@ -844,11 +982,11 @@ function fileToPart(file) {
844
982
  };
845
983
  }
846
984
  function revokeFileUrls(files) {
847
- for (const file of files) if (file.url?.startsWith("blob:")) URL.revokeObjectURL(file.url);
985
+ for (const file of files) if (file.url.startsWith("blob:")) URL.revokeObjectURL(file.url);
848
986
  }
849
987
  async function resolveSubmittedFile(file) {
850
988
  const { id: _id, ...part } = file;
851
- if (!part.url?.startsWith("blob:")) return part;
989
+ if (!part.url.startsWith("blob:")) return part;
852
990
  const dataUrl = await blobUrlToDataUrl(part.url);
853
991
  return {
854
992
  ...part,
@@ -909,4 +1047,4 @@ function createId() {
909
1047
  return globalThis.crypto?.randomUUID?.() ?? `prompt-input-${Date.now()}-${Math.random()}`;
910
1048
  }
911
1049
  //#endregion
912
- export { PromptInput, PromptInputActionAddAttachments, PromptInputActionAddScreenshot, PromptInputActionMenu, PromptInputActionMenuContent, PromptInputActionMenuItem, PromptInputActionMenuTrigger, PromptInputApproval, PromptInputBody, PromptInputButton, PromptInputCommand, PromptInputCommandEmpty, PromptInputCommandGroup, PromptInputCommandInput, PromptInputCommandItem, PromptInputCommandList, PromptInputCommandSeparator, PromptInputFooter, PromptInputHeader, PromptInputHoverCard, PromptInputHoverCardContent, PromptInputHoverCardTrigger, PromptInputProvider, PromptInputSelect, PromptInputSelectContent, PromptInputSelectItem, PromptInputSelectTrigger, PromptInputSelectValue, PromptInputSubmit, PromptInputTab, PromptInputTabBody, PromptInputTabItem, PromptInputTabLabel, PromptInputTabsList, PromptInputTextarea, PromptInputTools, getPendingToolApprovals, usePromptInputAttachments, usePromptInputController, usePromptInputReferencedSources, useProviderAttachments };
1050
+ export { PromptInput, PromptInputActionAddAttachments, PromptInputActionAddScreenshot, PromptInputActionMenu, PromptInputActionMenuContent, PromptInputActionMenuItem, PromptInputActionMenuTrigger, PromptInputApproval, PromptInputAttachment, PromptInputAttachments, PromptInputBody, PromptInputButton, PromptInputCommand, PromptInputCommandEmpty, PromptInputCommandGroup, PromptInputCommandInput, PromptInputCommandItem, PromptInputCommandList, PromptInputCommandSeparator, PromptInputFooter, PromptInputHeader, PromptInputHoverCard, PromptInputHoverCardContent, PromptInputHoverCardTrigger, PromptInputPermissionSelector, PromptInputProvider, PromptInputSelect, PromptInputSelectContent, PromptInputSelectItem, PromptInputSelectTrigger, PromptInputSelectValue, PromptInputSubmit, PromptInputTab, PromptInputTabBody, PromptInputTabItem, PromptInputTabLabel, PromptInputTabsList, PromptInputTextarea, PromptInputTools, getApprovalRequestId, getPendingToolApprovals, usePromptInputAttachments, usePromptInputController, usePromptInputReferencedSources, useProviderAttachments };
@@ -0,0 +1,43 @@
1
+ import { r as ButtonProps } from "../Button-Ct5I6aTu.js";
2
+ import * as React from "react";
3
+
4
+ //#region src/components/Suggestion.d.ts
5
+ interface SuggestionsProps extends React.HTMLAttributes<HTMLDivElement> {}
6
+ /**
7
+ * A horizontally scrollable row of prompt suggestions, typically shown above
8
+ * the composer or in the empty state. Children are {@link Suggestion} chips.
9
+ *
10
+ * Ported from AI Elements' Suggestions. Sigvelo changes: design-system Button
11
+ * chips and token-based styling; scrollbars hidden, scroll by wheel/touch.
12
+ */
13
+ declare function Suggestions({
14
+ className,
15
+ children,
16
+ ref,
17
+ ...props
18
+ }: SuggestionsProps & {
19
+ ref?: React.Ref<HTMLDivElement>;
20
+ }): React.JSX.Element;
21
+ interface SuggestionProps extends Omit<ButtonProps, "onClick" | "className"> {
22
+ className?: string;
23
+ /** The suggestion text — the chip label and the `onClick` payload. */
24
+ suggestion: string;
25
+ /** Called with the suggestion text; typically feeds `sendMessage`. */
26
+ onClick?: (suggestion: string) => void;
27
+ }
28
+ /** One pill-shaped suggestion chip. Renders `suggestion` unless children are given. */
29
+ declare function Suggestion({
30
+ className,
31
+ suggestion,
32
+ onClick,
33
+ variant,
34
+ color,
35
+ size,
36
+ children,
37
+ ref,
38
+ ...props
39
+ }: SuggestionProps & {
40
+ ref?: React.Ref<HTMLButtonElement>;
41
+ }): React.JSX.Element;
42
+ //#endregion
43
+ export { Suggestion, SuggestionProps, Suggestions, SuggestionsProps };
@@ -0,0 +1,39 @@
1
+ import { t as cx } from "../class-names-9UtZwb0J.js";
2
+ import { Button } from "./Button.js";
3
+ import "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/components/Suggestion.tsx
6
+ /**
7
+ * A horizontally scrollable row of prompt suggestions, typically shown above
8
+ * the composer or in the empty state. Children are {@link Suggestion} chips.
9
+ *
10
+ * Ported from AI Elements' Suggestions. Sigvelo changes: design-system Button
11
+ * chips and token-based styling; scrollbars hidden, scroll by wheel/touch.
12
+ */
13
+ function Suggestions({ className, children, ref, ...props }) {
14
+ return /* @__PURE__ */ jsx("div", {
15
+ ref,
16
+ className: cx("suggestions", className),
17
+ ...props,
18
+ children: /* @__PURE__ */ jsx("div", {
19
+ className: "suggestions__row",
20
+ children
21
+ })
22
+ });
23
+ }
24
+ /** One pill-shaped suggestion chip. Renders `suggestion` unless children are given. */
25
+ function Suggestion({ className, suggestion, onClick, variant = "outline", color = "neutral", size = "sm", children, ref, ...props }) {
26
+ return /* @__PURE__ */ jsx(Button, {
27
+ ref,
28
+ type: "button",
29
+ variant,
30
+ color,
31
+ size,
32
+ className: cx("suggestions__chip", className),
33
+ onClick: () => onClick?.(suggestion),
34
+ ...props,
35
+ children: children ?? suggestion
36
+ });
37
+ }
38
+ //#endregion
39
+ export { Suggestion, Suggestions };
@@ -1,3 +1,4 @@
1
+ import { ExecutionApprovals } from "./PromptInput.js";
1
2
  import { AgentChatProps, AgentChatRenderPartContext } from "./AgentChat.js";
2
3
  import { BrowserLiveView, BrowserLiveViewProps } from "./BrowserLiveView.js";
3
4
  import { BrowserReplay, BrowserReplayProps } from "./BrowserReplay.js";
@@ -34,8 +35,26 @@ interface ThinkChatMessagesProps extends ThinkChatActivityRendererOptions {
34
35
  placeholder?: string;
35
36
  /** Rendered after each message (e.g. per-message actions). */
36
37
  renderMessageAfter?: AgentChatProps["renderMessageAfter"];
38
+ /** Prompt suggestions shown above the composer; clicking one sends it. */
39
+ suggestions?: AgentChatProps["suggestions"];
40
+ /**
41
+ * Overrides the built-in connection notice (derived from the hook's
42
+ * `isRecovering`/`connectionError`) shown between transcript and composer.
43
+ */
44
+ notice?: AgentChatProps["notice"];
37
45
  /** Full replacement for part rendering; wins over excludeTools/toolRenderers. */
38
46
  renderPart?: AgentChatProps["renderPart"];
47
+ /**
48
+ * Surfaces paused codemode executions in the composer's approval UI —
49
+ * the same confirmation tool approvals use. See {@link ExecutionApprovals}.
50
+ */
51
+ executionApprovals?: ExecutionApprovals;
52
+ /**
53
+ * Shows a Context ring in the composer footer — how much of the model's
54
+ * window the conversation occupies, from `metadata.usage` on the newest
55
+ * message that reports it.
56
+ */
57
+ contextWindow?: AgentChatProps["contextWindow"];
39
58
  }
40
59
  /**
41
60
  * Bespoke rendering for one tool inside the activity log. Both hooks fall
@@ -98,6 +117,8 @@ declare function ThinkChatMessages({
98
117
  renderPart,
99
118
  excludeTools,
100
119
  toolRenderers,
120
+ executionApprovals,
121
+ notice,
101
122
  ...props
102
123
  }: ThinkChatMessagesProps): React.JSX.Element;
103
124
  /** Agent work (reasoning, tool calls, step boundaries) vs. content shown to the user. */
@@ -183,4 +204,4 @@ declare const ThinkChat: {
183
204
  createActivityRenderer: typeof createThinkChatActivityRenderer;
184
205
  };
185
206
  //#endregion
186
- export { BrowserLiveView, type BrowserLiveViewProps, BrowserReplay, type BrowserReplayProps, type FileInfo, ThinkChat, ThinkChatActivity, ThinkChatActivityProps, ThinkChatActivityRendererOptions, ThinkChatMessages, ThinkChatMessagesProps, ThinkChatProvider, ThinkChatProviderProps, ThinkChatRoot, ThinkChatRootProps, ThinkChatToolRenderer, ThinkChatToolRenderers, type UseWorkspaceOptions, WorkspaceExplorer, type WorkspaceExplorerProps, type WorkspaceFileState, type WorkspaceInfo, type WorkspaceSource, type WorkspaceState, createThinkChatActivityRenderer, describeThinkChatActivityPart, inferWorkspaceLanguage, isThinkChatActivityPart, renderThinkChatActivityPart, useThinkChat, useWorkspace };
207
+ export { BrowserLiveView, type BrowserLiveViewProps, BrowserReplay, type BrowserReplayProps, type ExecutionApprovals, type FileInfo, ThinkChat, ThinkChatActivity, ThinkChatActivityProps, ThinkChatActivityRendererOptions, ThinkChatMessages, ThinkChatMessagesProps, ThinkChatProvider, ThinkChatProviderProps, ThinkChatRoot, ThinkChatRootProps, ThinkChatToolRenderer, ThinkChatToolRenderers, type UseWorkspaceOptions, WorkspaceExplorer, type WorkspaceExplorerProps, type WorkspaceFileState, type WorkspaceInfo, type WorkspaceSource, type WorkspaceState, createThinkChatActivityRenderer, describeThinkChatActivityPart, inferWorkspaceLanguage, isThinkChatActivityPart, renderThinkChatActivityPart, useThinkChat, useWorkspace };
@@ -13,7 +13,8 @@ import { useAgent } from "agents/react";
13
13
  //#region src/components/ThinkChat.tsx
14
14
  /**
15
15
  * Think-specific chat rendering: interprets a Think agent's UIMessage stream
16
- * (reasoning + workspace/skill/fetch/browser tool parts) into Activity blocks.
16
+ * (reasoning + workspace/skill/fetch/browser/execute tool parts) into
17
+ * Activity blocks.
17
18
  *
18
19
  * Every shape here mirrors an upstream contract — see docs/think-chat.md
19
20
  * ("Upstream source map" and the re-alignment checklist) for where each comes from,
@@ -55,6 +56,9 @@ const BROWSER_TOOL_NAMES = new Set([
55
56
  function isBrowserToolPart(part) {
56
57
  return isToolUIPart(part) && BROWSER_TOOL_NAMES.has(getToolName(part));
57
58
  }
59
+ function isExecuteToolPart(part) {
60
+ return isToolUIPart(part) && getToolName(part) === "execute";
61
+ }
58
62
  const ThinkChatContext = React.createContext(null);
59
63
  function useThinkChatContext(component) {
60
64
  const context = React.useContext(ThinkChatContext);
@@ -109,13 +113,17 @@ function ThinkChatRoot({ agent: agentOption, name, children }) {
109
113
  * inside the log) or `excludeTools` + your own `renderPart` (standalone
110
114
  * card) — see README "App-specific tools" and the Custom Tool Panel story.
111
115
  */
112
- function ThinkChatMessages({ renderPart, excludeTools, toolRenderers, ...props }) {
116
+ function ThinkChatMessages({ renderPart, excludeTools, toolRenderers, executionApprovals, notice, ...props }) {
113
117
  const chat = useThinkChatContext("ThinkChat.Messages");
114
118
  const status = toAgentChatStatus(chat);
115
119
  const defaultRenderPart = React.useMemo(() => createThinkChatActivityRenderer({
116
120
  excludeTools,
117
121
  toolRenderers
118
122
  }), [excludeTools, toolRenderers]);
123
+ const connectionNotice = chat.isRecovering ? /* @__PURE__ */ jsx(Fragment, { children: "Reconnecting to the agent…" }) : chat.connectionError ? /* @__PURE__ */ jsxs("span", {
124
+ "data-tone": "danger",
125
+ children: ["Connection lost", chat.connectionError.reason ? ` — ${chat.connectionError.reason}` : ""]
126
+ }) : void 0;
119
127
  return /* @__PURE__ */ jsx(AgentChat, {
120
128
  ...props,
121
129
  renderPart: renderPart ?? defaultRenderPart,
@@ -123,9 +131,15 @@ function ThinkChatMessages({ renderPart, excludeTools, toolRenderers, ...props }
123
131
  status,
124
132
  onStop: chat.stop,
125
133
  onApprovalResponse: chat.addToolApprovalResponse,
126
- onSubmit: ({ text }) => {
134
+ executionApprovals,
135
+ notice: notice ?? connectionNotice,
136
+ onRegenerate: (messageId) => void chat.regenerate({ messageId }),
137
+ onSubmit: ({ text, files }) => {
127
138
  const trimmed = text.trim();
128
- if (trimmed) chat.sendMessage({ text: trimmed });
139
+ if (trimmed || files.length) chat.sendMessage({
140
+ text: trimmed,
141
+ files
142
+ });
129
143
  }
130
144
  });
131
145
  }
@@ -177,6 +191,7 @@ function describeThinkChatActivityPart(part, toolRenderers) {
177
191
  if (isSkillToolPart(part)) return describeSkillToolPart(part);
178
192
  if (isFetchToolPart(part)) return describeFetchToolPart(part);
179
193
  if (isBrowserToolPart(part)) return describeBrowserToolPart(part);
194
+ if (isExecuteToolPart(part)) return describeExecuteToolPart(part);
180
195
  if (isToolUIPart(part)) {
181
196
  const name = getToolName(part);
182
197
  const verb = isPartFailed(part) ? `${name} failed` : isPartWorking(part) ? `Running ${name}` : `Ran ${name}`;
@@ -251,6 +266,14 @@ function describeBrowserToolPart(part) {
251
266
  })();
252
267
  return failed ? `${label} · failed` : label;
253
268
  }
269
+ function describeExecuteToolPart(part) {
270
+ const output = part.state === "output-available" ? part.output : void 0;
271
+ if (isPartFailed(part) || output?.status === "error") return "Code failed";
272
+ if (output?.status === "paused") return "Code waiting for approval";
273
+ const label = isPartWorking(part) ? "Running code" : "Ran code";
274
+ const snippet = summarizeToolInput(part.input);
275
+ return snippet ? `${label} · ${snippet}` : label;
276
+ }
254
277
  function describeFetchToolPart(part) {
255
278
  const target = part.input?.url ?? part.input?.path ?? "";
256
279
  const output = part.state === "output-available" ? part.output : void 0;
@@ -266,6 +289,7 @@ function summarizeActivitySteps(parts) {
266
289
  let skillParts = 0;
267
290
  let fetches = 0;
268
291
  let browses = 0;
292
+ let codeRuns = 0;
269
293
  for (const part of parts) if (isReasoningUIPart(part)) thoughts++;
270
294
  else if (isWorkspaceToolPart(part)) {
271
295
  const name = getToolName(part);
@@ -275,6 +299,7 @@ function summarizeActivitySteps(parts) {
275
299
  if (typeof part.input?.name === "string") skillNames.add(part.input.name);
276
300
  } else if (isFetchToolPart(part)) fetches++;
277
301
  else if (isBrowserToolPart(part)) browses++;
302
+ else if (isExecuteToolPart(part)) codeRuns++;
278
303
  else if (isToolUIPart(part)) otherTools.add(getToolName(part));
279
304
  const count = (name) => byTool.get(name) ?? 0;
280
305
  const files = (n) => n === 1 ? "a file" : `${n} files`;
@@ -292,6 +317,7 @@ function summarizeActivitySteps(parts) {
292
317
  if (count("list") > 0) segments.push("listed files");
293
318
  if (fetches > 0) segments.push(fetches === 1 ? "fetched a URL" : `fetched ${fetches} URLs`);
294
319
  if (browses > 0) segments.push("used the browser");
320
+ if (codeRuns > 0) segments.push("ran code");
295
321
  if (commands > 0) segments.push(commands === 1 ? "ran a command" : `ran ${commands} commands`);
296
322
  for (const tool of otherTools) segments.push(`used ${tool}`);
297
323
  if (segments.length === 0) return "Worked";
@@ -359,6 +385,10 @@ function ThinkChatActivityStep({ text, part, toolRenderers }) {
359
385
  label: text,
360
386
  children: /* @__PURE__ */ jsx(ThinkChatBrowserPanel, { part })
361
387
  });
388
+ if (isExecuteToolPart(part)) return /* @__PURE__ */ jsx(ActivityItemGroup, {
389
+ label: text,
390
+ children: /* @__PURE__ */ jsx(ThinkChatExecutePanel, { part })
391
+ });
362
392
  return /* @__PURE__ */ jsx(ActivityItem, { children: text });
363
393
  }
364
394
  function renderDeniedPanel(label, reason) {
@@ -552,6 +582,50 @@ function findBrowserResultMedia(value, depth = 0) {
552
582
  visit(value, depth);
553
583
  return media;
554
584
  }
585
+ function ExecuteCodeView({ code, output, hideResult }) {
586
+ const sections = [
587
+ output && "logs" in output && output.logs?.length ? output.logs.join("\n") : "",
588
+ output?.status === "error" ? output.error : "",
589
+ output?.status === "paused" ? formatPendingActions(output.pending) : "",
590
+ output?.status === "completed" && output.result !== void 0 && !hideResult ? typeof output.result === "string" ? output.result : JSON.stringify(output.result, null, 2) : ""
591
+ ].filter(Boolean);
592
+ return /* @__PURE__ */ jsxs(Fragment, { children: [code ? /* @__PURE__ */ jsx(CodeBlock, {
593
+ className: "activity__code-block",
594
+ code,
595
+ language: "ts"
596
+ }) : null, sections.length ? /* @__PURE__ */ jsx(ActivityOutput, { children: sections.join("\n\n") }) : null] });
597
+ }
598
+ function formatPendingActions(pending) {
599
+ return pending.map((action) => {
600
+ const args = action.args === void 0 ? "" : JSON.stringify(action.args);
601
+ return `awaiting approval: ${action.connector}.${action.method}(${args})`;
602
+ }).join("\n");
603
+ }
604
+ function executeStatusText(output) {
605
+ if (!output) return void 0;
606
+ return output.status === "paused" ? "paused · needs approval" : output.status;
607
+ }
608
+ /** The codemode execute tool as an activity panel: highlighted TypeScript
609
+ plus logs, result, and the paused pending-approval list. */
610
+ function ThinkChatExecutePanel({ part }) {
611
+ if (part.state === "output-error") return /* @__PURE__ */ jsx(ActivityPanel, {
612
+ label: "Code",
613
+ text: part.errorText,
614
+ status: "error",
615
+ error: true
616
+ });
617
+ if (part.state === "output-denied") return renderDeniedPanel("Code", part.approval.reason);
618
+ const output = part.state === "output-available" ? part.output : void 0;
619
+ return /* @__PURE__ */ jsx(ActivityPanel, {
620
+ label: "Code",
621
+ status: executeStatusText(output),
622
+ error: output?.status === "error",
623
+ children: /* @__PURE__ */ jsx(ExecuteCodeView, {
624
+ code: part.input?.code ?? "",
625
+ output
626
+ })
627
+ });
628
+ }
555
629
  function ThinkChatBrowserPanel({ part }) {
556
630
  const [seenAt] = React.useState(() => Date.now());
557
631
  if (part.state === "output-error") return /* @__PURE__ */ jsx(ActivityPanel, {
@@ -566,12 +640,6 @@ function ThinkChatBrowserPanel({ part }) {
566
640
  const code = part.input?.code ?? "";
567
641
  const media = output?.status === "completed" ? findBrowserResultMedia(output.result) : {};
568
642
  const hasMedia = Boolean(media.liveViewUrl || media.screenshot);
569
- const sections = [
570
- output && "logs" in output && output.logs?.length ? output.logs.join("\n") : "",
571
- output?.status === "error" ? output.error : "",
572
- output?.status === "completed" && output.result !== void 0 && !hasMedia ? typeof output.result === "string" ? output.result : JSON.stringify(output.result, null, 2) : ""
573
- ].filter(Boolean);
574
- const status = !output ? void 0 : output.status === "paused" ? "paused · needs approval" : output.status === "error" ? "error" : "completed";
575
643
  const browserView = media.liveViewUrl ? /* @__PURE__ */ jsx(BrowserLiveView, {
576
644
  url: media.liveViewUrl,
577
645
  expiresAt: seenAt + (media.expiresInMs ?? 300 * 1e3),
@@ -584,16 +652,16 @@ function ThinkChatBrowserPanel({ part }) {
584
652
  src: media.screenshot,
585
653
  alt: "Browser screenshot"
586
654
  }) : null;
587
- const codeView = /* @__PURE__ */ jsxs(Fragment, { children: [code ? /* @__PURE__ */ jsx(CodeBlock, {
588
- className: "activity__code-block",
655
+ const codeView = /* @__PURE__ */ jsx(ExecuteCodeView, {
589
656
  code,
590
- language: "js"
591
- }) : null, sections.length ? /* @__PURE__ */ jsx(ActivityOutput, { children: sections.join("\n\n") }) : null] });
657
+ output,
658
+ hideResult: hasMedia
659
+ });
592
660
  return /* @__PURE__ */ jsx(ActivityPanel, {
593
661
  label: "Browser",
594
- status,
662
+ status: executeStatusText(output),
595
663
  error: output?.status === "error",
596
- children: browserView && (code || sections.length) ? /* @__PURE__ */ jsxs(Tabs, {
664
+ children: browserView && code ? /* @__PURE__ */ jsxs(Tabs, {
597
665
  className: "activity__browser-tabs",
598
666
  defaultValue: "browser",
599
667
  children: [
@@ -616,7 +684,7 @@ function ThinkChatBrowserPanel({ part }) {
616
684
  children: codeView
617
685
  })
618
686
  ]
619
- }) : /* @__PURE__ */ jsxs(Fragment, { children: [browserView, code || sections.length ? codeView : null] })
687
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [browserView, codeView] })
620
688
  });
621
689
  }
622
690
  const output = part.state === "output-available" ? part.output : void 0;
@@ -19,3 +19,22 @@
19
19
  padding: 0.75rem;
20
20
  background-color: var(--sigvelo-color-surface);
21
21
  }
22
+
23
+ /* Transient status pill between transcript and composer (recovery, errors). */
24
+ .agent-chat__notice {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 0.375rem;
28
+ margin-block-end: 0.5rem;
29
+ font-size: 0.75rem;
30
+ color: var(--sigvelo-color-text-muted);
31
+ }
32
+
33
+ .agent-chat__notice[data-tone="danger"],
34
+ .agent-chat__notice [data-tone="danger"] {
35
+ color: var(--sigvelo-color-danger-text);
36
+ }
37
+
38
+ .agent-chat__suggestions {
39
+ margin-block-end: 0.5rem;
40
+ }
@@ -0,0 +1,21 @@
1
+ /* Click-to-zoom lightbox for image attachments (composer chips + messages).
2
+ Scoped as .dialog__popup.attachment-preview so it beats the base dialog
3
+ sizing regardless of import order. */
4
+ .dialog__popup.attachment-preview {
5
+ width: fit-content;
6
+ max-width: min(90vw, 56rem);
7
+ }
8
+
9
+ .attachment-preview__image {
10
+ display: block;
11
+ max-width: 100%;
12
+ max-height: 70vh;
13
+ border-radius: var(--sigvelo-radius-md);
14
+ }
15
+
16
+ .attachment-preview__name {
17
+ margin-block-start: 0.5rem;
18
+ font-size: 0.75rem;
19
+ color: var(--sigvelo-color-text-muted);
20
+ text-align: center;
21
+ }