@mcp-b/react-components 0.50.0 → 0.51.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.
@@ -252,7 +252,7 @@ function AgentChatActions({ children, ...props }) {
252
252
  const { messages, busy } = useAgentChatRuntime();
253
253
  const number = messages.slice(0, messageIndex + 1).filter((m) => m.role === message.role && !isCompactionMessage(m) && !getAgentEvent(m)).length;
254
254
  const unavailable = busy && (message.role === "user" ? message.id === messages.findLast((item) => item.role === "user" && !getAgentEvent(item))?.id : message.id === messages.at(-1)?.id);
255
- const actions = /* @__PURE__ */ jsx(MessageActions, {
255
+ return /* @__PURE__ */ jsx(MessageMeta, { children: /* @__PURE__ */ jsx(MessageActions, {
256
256
  reserveSpace: message.role === "assistant",
257
257
  "aria-label": `Actions for ${message.role === "assistant" ? "response" : "message"} ${number}`,
258
258
  "data-unavailable": unavailable ? true : void 0,
@@ -260,8 +260,7 @@ function AgentChatActions({ children, ...props }) {
260
260
  inert: unavailable ? true : void 0,
261
261
  ...props,
262
262
  children
263
- });
264
- return message.role === "user" ? /* @__PURE__ */ jsx(MessageMeta, { children: actions }) : actions;
263
+ }) });
265
264
  }
266
265
  function AgentChatCopyAction() {
267
266
  const { message, messageIndex } = useChatMessage();
@@ -14,6 +14,7 @@ type BrowserLiveScreenSignal = {
14
14
  type BrowserLiveScreenSignalPayload = {
15
15
  kind: "offer";
16
16
  sdp: string;
17
+ tabId: number;
17
18
  } | {
18
19
  kind: "answer";
19
20
  sdp: string;
@@ -70,9 +71,6 @@ type BrowserLiveScreenInput = {
70
71
  events: BrowserLiveScreenInputEvent[];
71
72
  };
72
73
  type BrowserLiveScreenCommand = {
73
- kind: "selectTab";
74
- tabId: number;
75
- } | {
76
74
  kind: "navigate";
77
75
  tabId: number;
78
76
  action: "back" | "reload";
@@ -111,14 +109,16 @@ interface BrowserLiveScreenProps {
111
109
  * from state, because Chrome's screencast carries only page pixels.
112
110
  */
113
111
  tabs?: readonly BrowserLiveScreenTab[];
114
- /** Claimed tab the host wants visible when the data channel becomes usable. */
115
- requestedTabId?: number;
112
+ /** The host owns this viewer's tab selection, typically in its route. */
113
+ tabId?: number;
114
+ /** Requests a new tab selection; the host commits it through `tabId`. */
115
+ onTabChange(this: void, tabId: number): void;
116
116
  }
117
117
  /**
118
118
  * A complete interactive viewer for an employee's claimed browser tabs.
119
119
  *
120
120
  * The host supplies only signaling transport, ICE configuration, and claimed
121
- * tab summaries. This component owns the peer connection, remote-input
121
+ * tab summaries and selection. This component owns the peer connection, remote-input
122
122
  * mapping, reconnect lifecycle, synthesized browser chrome, and cleanup.
123
123
  */
124
124
  declare function BrowserLiveScreen({
@@ -129,7 +129,8 @@ declare function BrowserLiveScreen({
129
129
  label,
130
130
  controlLabel,
131
131
  tabs,
132
- requestedTabId
132
+ tabId: selectedTabId,
133
+ onTabChange
133
134
  }: BrowserLiveScreenProps): import("react").JSX.Element;
134
135
  //#endregion
135
136
  export { BrowserLiveScreen, BrowserLiveScreenCommand, BrowserLiveScreenFrameMetadata, BrowserLiveScreenInput, BrowserLiveScreenInputEvent, BrowserLiveScreenProps, BrowserLiveScreenProtocol, BrowserLiveScreenSignal, BrowserLiveScreenSignalPayload, BrowserLiveScreenStatus, BrowserLiveScreenTab };
@@ -11,10 +11,10 @@ const BROWSER_LIVE_SCREEN_NEGOTIATION_TIMEOUT_MS = 1e4;
11
11
  * A complete interactive viewer for an employee's claimed browser tabs.
12
12
  *
13
13
  * The host supplies only signaling transport, ICE configuration, and claimed
14
- * tab summaries. This component owns the peer connection, remote-input
14
+ * tab summaries and selection. This component owns the peer connection, remote-input
15
15
  * mapping, reconnect lifecycle, synthesized browser chrome, and cleanup.
16
16
  */
17
- function BrowserLiveScreen({ sendSignal: sendHostSignal, subscribeSignals, protocol, getIceServers, label = "Employee browser", controlLabel = "Control employee browser", tabs = [], requestedTabId }) {
17
+ function BrowserLiveScreen({ sendSignal: sendHostSignal, subscribeSignals, protocol, getIceServers, label = "Employee browser", controlLabel = "Control employee browser", tabs = [], tabId: selectedTabId, onTabChange }) {
18
18
  const video = useRef(null);
19
19
  const input = useRef(null);
20
20
  const [inputElement, setInputElement] = useState(null);
@@ -59,13 +59,7 @@ function BrowserLiveScreen({ sendSignal: sendHostSignal, subscribeSignals, proto
59
59
  useLayoutEffect(() => {
60
60
  currentProtocol.current = protocol;
61
61
  }, [protocol]);
62
- const selectRequestedTab = useEffectEvent((data) => {
63
- if (requestedTabId === void 0 || data?.readyState !== "open") return;
64
- data.send(currentProtocol.current.serializeCommand({
65
- kind: "selectTab",
66
- tabId: requestedTabId
67
- }));
68
- });
62
+ const captureTabId = tabs.some((tab) => tab.tabId === selectedTabId) ? selectedTabId : void 0;
69
63
  const dataChannelLabel = protocol.dataChannelLabel;
70
64
  const handleWheel = useEffectEvent((event) => {
71
65
  event.preventDefault();
@@ -107,6 +101,13 @@ function BrowserLiveScreen({ sendSignal: sendHostSignal, subscribeSignals, proto
107
101
  }, [inputElement]);
108
102
  const [streamed, setStreamed] = useState(null);
109
103
  useEffect(() => {
104
+ if (captureTabId === void 0) {
105
+ setPhase("idle");
106
+ setStreamed(null);
107
+ return;
108
+ }
109
+ const tabId = captureTabId;
110
+ setPhase("connecting");
110
111
  let disposed = false;
111
112
  let connection;
112
113
  let viewerId = "";
@@ -220,8 +221,6 @@ function BrowserLiveScreen({ sendSignal: sendHostSignal, subscribeSignals, proto
220
221
  };
221
222
  const dataChannel = peer.createDataChannel(dataChannelLabel);
222
223
  channel.current = dataChannel;
223
- dataChannel.onopen = () => selectRequestedTab(dataChannel);
224
- selectRequestedTab(dataChannel);
225
224
  dataChannel.onmessage = (event) => {
226
225
  if (disposed || peer !== connection || channel.current !== dataChannel) return;
227
226
  const status = parseStatus(event.data);
@@ -236,6 +235,7 @@ function BrowserLiveScreen({ sendSignal: sendHostSignal, subscribeSignals, proto
236
235
  return;
237
236
  }
238
237
  if (status.kind === "streaming") {
238
+ if (status.tabId !== tabId) return;
239
239
  const previous = geometry.current;
240
240
  if (previous?.tabId !== status.tabId || previous.streamGeneration !== status.streamGeneration) cancelPointerState();
241
241
  geometry.current = status;
@@ -253,7 +253,8 @@ function BrowserLiveScreen({ sendSignal: sendHostSignal, subscribeSignals, proto
253
253
  if (disposed || peer !== connection) return;
254
254
  await sendSignal(currentViewerId, {
255
255
  kind: "offer",
256
- sdp: offer.sdp ?? ""
256
+ sdp: offer.sdp ?? "",
257
+ tabId
257
258
  });
258
259
  }
259
260
  function restart() {
@@ -270,10 +271,7 @@ function BrowserLiveScreen({ sendSignal: sendHostSignal, subscribeSignals, proto
270
271
  unsubscribe();
271
272
  closeCurrent(true);
272
273
  };
273
- }, [dataChannelLabel]);
274
- useEffect(() => {
275
- selectRequestedTab(channel.current);
276
- }, [requestedTabId]);
274
+ }, [dataChannelLabel, captureTabId]);
277
275
  function sendInput(events) {
278
276
  const open = channel.current;
279
277
  const status = geometry.current;
@@ -628,11 +626,8 @@ function BrowserLiveScreen({ sendSignal: sendHostSignal, subscribeSignals, proto
628
626
  title: tab.title || tab.url || `Tab ${String(tab.tabId)}`,
629
627
  ...tab.url === void 0 ? {} : { url: tab.url }
630
628
  })),
631
- selectedTabId: streamed ? String(streamed.tabId) : null,
632
- onSelectTab: (id) => sendCommand({
633
- kind: "selectTab",
634
- tabId: Number(id)
635
- }),
629
+ selectedTabId: captureTabId === void 0 ? null : String(captureTabId),
630
+ onSelectTab: (id) => onTabChange(Number(id)),
636
631
  url: displayUrl,
637
632
  onOpenUrl: (next) => {
638
633
  if (!streamed) return;
@@ -195,7 +195,7 @@ function SubagentRunPanelContent({ panel, renderRunLink }) {
195
195
  const { runs, active, completed, visible } = getSubagentRunRows(panel);
196
196
  const focus = useSubagentRunFocus(panel, visible);
197
197
  const stop = panel.commands["current-work"] ?? IDLE;
198
- if (panel.collection.status === "ready" && !runs.length && !panel.parentRunning) return null;
198
+ if (!runs.length) return null;
199
199
  return /* @__PURE__ */ jsx(AgentChatPromptTray, {
200
200
  "data-subagent-run-panel": "",
201
201
  children: /* @__PURE__ */ jsx(Queue, {
@@ -168,14 +168,17 @@
168
168
  the same quiet look without per-app classes. Hug the actions — a short
169
169
  viewport (Storybook addons, a short side panel) used to stretch this into
170
170
  a vacant slab. Space-between only when there are three slots to fill.
171
- Compact control tokens keep this strip on the same ramp as the composer. */
171
+ Compact control tokens keep this strip on the same ramp as the composer.
172
+ Count buttons only: opening a popup inserts non-layout focus guards. */
172
173
  .agent-chat-start-screen__composer-tray {
173
174
  align-self: start;
174
175
  justify-content: start;
175
176
  min-block-size: 0;
176
177
  }
177
178
 
178
- .agent-chat-start-screen__composer-tray:has(> :nth-child(3):last-child) {
179
+ .agent-chat-start-screen__composer-tray:has(
180
+ > :nth-child(3 of .button):nth-last-child(1 of .button)
181
+ ) {
179
182
  justify-content: space-between;
180
183
  }
181
184
 
@@ -561,12 +564,17 @@
561
564
  font-size: var(--sigvelo-text-lg);
562
565
  }
563
566
 
564
- .agent-chat-start-screen__composer-tray:has(> :nth-child(3):last-child) {
567
+ .agent-chat-start-screen__composer-tray:has(
568
+ > :nth-child(3 of .button):nth-last-child(1 of .button)
569
+ ) {
565
570
  display: grid;
566
571
  grid-template-columns: repeat(3, var(--sigvelo-layout-track-fill));
567
572
  }
568
573
 
569
- .agent-chat-start-screen__composer-tray:has(> :nth-child(3):last-child) > .button {
574
+ .agent-chat-start-screen__composer-tray:has(
575
+ > :nth-child(3 of .button):nth-last-child(1 of .button)
576
+ )
577
+ > .button {
570
578
  inline-size: 100%;
571
579
  justify-content: center;
572
580
  }
@@ -146,6 +146,10 @@
146
146
  white-space: nowrap;
147
147
  }
148
148
 
149
+ .message[data-from="assistant"] > .message__meta {
150
+ align-self: flex-start;
151
+ }
152
+
149
153
  .message__meta .message__actions[data-unavailable] {
150
154
  visibility: hidden;
151
155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-b/react-components",
3
- "version": "0.50.0",
3
+ "version": "0.51.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": {
@@ -54,7 +54,7 @@
54
54
  "streamdown": "^2.5.0",
55
55
  "unist-util-visit": "^5.1.0",
56
56
  "yet-another-react-lightbox": "^3.32.2",
57
- "@mcp-b/design-tokens": "0.50.0"
57
+ "@mcp-b/design-tokens": "0.51.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@ai-sdk/react": "^3.0.249",