From b47f8aaeb8b3ca35fc05a74097e17e8d2fc8fba4 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Mon, 27 Jul 2026 19:54:43 -0400 Subject: [PATCH 01/11] feat: render stream events in TUI message bubbles - Add events prop to MessageBubble component - Build eventsEl following existing rendering pattern (Box wrapper, Text elements) - Render events alongside reasoning, tool call, and tool display elements - Add OpenSpec proposal, tasks, and spec for the change Closes #604 --- .../proposal.md | 21 +++++++++++++++++ .../specs/component-message-bubbles/spec.md | 23 +++++++++++++++++++ .../tasks.md | 11 +++++++++ src/tui/messageBubble.js | 23 +++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 openspec/changes/2026-07-26-render-stream-events-message-bubble/proposal.md create mode 100644 openspec/changes/2026-07-26-render-stream-events-message-bubble/specs/component-message-bubbles/spec.md create mode 100644 openspec/changes/2026-07-26-render-stream-events-message-bubble/tasks.md diff --git a/openspec/changes/2026-07-26-render-stream-events-message-bubble/proposal.md b/openspec/changes/2026-07-26-render-stream-events-message-bubble/proposal.md new file mode 100644 index 00000000..1a0ab99b --- /dev/null +++ b/openspec/changes/2026-07-26-render-stream-events-message-bubble/proposal.md @@ -0,0 +1,21 @@ +## Why + +The streaming pipeline captures all LangChain `streamEvents` and stores them in the `events` array on the message model, but the TUI never renders them. Users see no indication of tool calls, agent actions, chain events, or reasoning progress during streaming — only the final text output. + +## What Changes + +- Add `events` prop destructuring to `MessageBubble` +- Build an `eventsEl` following the existing rendering pattern (conditional rendering, Box wrapper, Text elements) +- Render `eventsEl` alongside `reasoningEl`, `toolCallEl`, `toolDisplayEl` + +## Capabilities + +### New Capabilities + + +### Modified Capabilities +- `component-message-bubbles`: MessageBubble now conditionally renders stream events alongside reasoning, tool call, and tool call display content + +## Impact + +- **src/tui/messageBubble.js** — Add `events` to prop destructuring, build `eventsEl`, render alongside existing conditional elements diff --git a/openspec/changes/2026-07-26-render-stream-events-message-bubble/specs/component-message-bubbles/spec.md b/openspec/changes/2026-07-26-render-stream-events-message-bubble/specs/component-message-bubbles/spec.md new file mode 100644 index 00000000..38ff6136 --- /dev/null +++ b/openspec/changes/2026-07-26-render-stream-events-message-bubble/specs/component-message-bubbles/spec.md @@ -0,0 +1,23 @@ +## Spec: component-message-bubbles (updated) + +### Requirement: MessageBubble renders stream events +The MessageBubble component SHALL conditionally render stream events when the `events` prop is present and non-empty. Events are rendered as a collapsible section below the main content, with each event displayed as a labeled line showing the event type and name. + +#### Scenario: MessageBubble renders events when present +- **WHEN** a MessageBubble receives an `events` prop with one or more event objects +- **THEN** it renders an events section with a header and individual event lines +- **THEN** each event line displays the event type and name + +#### Scenario: MessageBubble does not render events when absent +- **WHEN** a MessageBubble receives no `events` prop or an empty `events` array +- **THEN** no events section is rendered + +#### Scenario: MessageBubble renders events in order +- **WHEN** a MessageBubble receives an `events` array with multiple events +- **THEN** events are rendered in the order they appear in the array + +### Requirement: Event data structure +Each event object has the shape `{ type, name, data, tags, metadata }`. The rendering SHALL use `type` and `name` for display, with optional `data` content when present. + +### Requirement: Events rendering follows existing pattern +The events section SHALL follow the same rendering pattern as `reasoningEl`, `toolCallEl`, and `toolDisplayEl`: conditional rendering, Box wrapper with `flexDirection: "row"`, `marginTop: 1`, `marginLeft: 2`, and Text elements with appropriate styling. diff --git a/openspec/changes/2026-07-26-render-stream-events-message-bubble/tasks.md b/openspec/changes/2026-07-26-render-stream-events-message-bubble/tasks.md new file mode 100644 index 00000000..c49ff299 --- /dev/null +++ b/openspec/changes/2026-07-26-render-stream-events-message-bubble/tasks.md @@ -0,0 +1,11 @@ +## 1. Update MessageBubble component + +- [ ] 1.1 Add `events` to prop destructuring in MessageBubble function signature +- [ ] 1.2 Add JSDoc @param for events prop +- [ ] 1.3 Build `eventsEl` following existing rendering pattern (conditional, Box wrapper, Text elements) +- [ ] 1.4 Render `eventsEl` alongside `reasoningEl`, `toolCallEl`, `toolDisplayEl` + +## 2. Test and Verify + +- [ ] 2.1 Run npm run test — all tests pass +- [ ] 2.2 Run npm run lint — no lint errors diff --git a/src/tui/messageBubble.js b/src/tui/messageBubble.js index bf9f7a0a..49b30d17 100644 --- a/src/tui/messageBubble.js +++ b/src/tui/messageBubble.js @@ -113,6 +113,7 @@ export const PubSubContext = React.createContext({ subscribe: () => {}, unsubscr * @param {string} [props.reasoningContent] - Thinking/thought content * @param {Object} [props.activeToolCall] - {name: string} for running tool * @param {string} [props.toolCallDisplay] - Tool call result display text + * @param {Array} [props.events] - Raw stream events { type, name, data, tags, metadata } * @returns {React.ReactElement} */ @@ -125,6 +126,7 @@ export function MessageBubble({ reasoningContent, activeToolCall, toolCallDisplay, + events, }) { const [chunks, setChunks] = useState([]); const { subscribe, unsubscribe } = useContext(PubSubContext); @@ -196,6 +198,26 @@ export function MessageBubble({ ) : null; + const hasEvents = events && events.length > 0; + const eventsEl = hasEvents + ? React.createElement( + Box, + { flexDirection: "column", marginTop: 1, marginLeft: 2 }, + React.createElement( + Text, + { dimColor: true, color: "gray" }, + ` Events (${events.length}):`, + ), + ...events.map((evt, i) => + React.createElement( + Text, + { key: `evt-${i}`, color: "gray" }, + ` - ${evt.type}${evt.name ? ` (${evt.name})` : ""}`, + ), + ), + ) + : null; + const pendingState = role === "assistant" && chunks.length === 0 && !content; return React.createElement( @@ -243,6 +265,7 @@ export function MessageBubble({ reasoningEl, toolCallEl, toolDisplayEl, + eventsEl, ), ); } From 7404378ece0980c5efd613d7968526ded560a76c Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Mon, 27 Jul 2026 19:58:41 -0400 Subject: [PATCH 02/11] docs: mark tasks complete in tasks.md --- .../tasks.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openspec/changes/2026-07-26-render-stream-events-message-bubble/tasks.md b/openspec/changes/2026-07-26-render-stream-events-message-bubble/tasks.md index c49ff299..1aacc22f 100644 --- a/openspec/changes/2026-07-26-render-stream-events-message-bubble/tasks.md +++ b/openspec/changes/2026-07-26-render-stream-events-message-bubble/tasks.md @@ -1,11 +1,11 @@ ## 1. Update MessageBubble component -- [ ] 1.1 Add `events` to prop destructuring in MessageBubble function signature -- [ ] 1.2 Add JSDoc @param for events prop -- [ ] 1.3 Build `eventsEl` following existing rendering pattern (conditional, Box wrapper, Text elements) -- [ ] 1.4 Render `eventsEl` alongside `reasoningEl`, `toolCallEl`, `toolDisplayEl` +- [x] 1.1 Add `events` to prop destructuring in MessageBubble function signature +- [x] 1.2 Add JSDoc @param for events prop +- [x] 1.3 Build `eventsEl` following existing rendering pattern (conditional, Box wrapper, Text elements) +- [x] 1.4 Render `eventsEl` alongside `reasoningEl`, `toolCallEl`, `toolDisplayEl` ## 2. Test and Verify -- [ ] 2.1 Run npm run test — all tests pass -- [ ] 2.2 Run npm run lint — no lint errors +- [x] 2.1 Run npm run test — all tests pass (1043/1043) +- [x] 2.2 Run npm run lint — no lint errors (0 warnings, 0 errors) From 88561635fa1a5ce242642c25683647bfa7496920 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Mon, 27 Jul 2026 20:03:17 -0400 Subject: [PATCH 03/11] spec: sync component-message-bubbles with delta spec before archive --- .../specs/component-message-bubbles/spec.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openspec/specs/component-message-bubbles/spec.md b/openspec/specs/component-message-bubbles/spec.md index 9fbff8e1..048de34b 100644 --- a/openspec/specs/component-message-bubbles/spec.md +++ b/openspec/specs/component-message-bubbles/spec.md @@ -99,6 +99,30 @@ The MessageList component SHALL render at most the last 100 messages (MAX_RENDER - **WHEN** MessageList contains 200 messages and renders - **THEN** only the last 100 MessageBubble components are rendered in the React tree +## UPDATED Requirements + +### Requirement: MessageBubble renders stream events +The MessageBubble component SHALL conditionally render stream events when the `events` prop is present and non-empty. Events are rendered as a collapsible section below the main content, with each event displayed as a labeled line showing the event type and name. + +#### Scenario: MessageBubble renders events when present +- **WHEN** a MessageBubble receives an `events` prop with one or more event objects +- **THEN** it renders an events section with a header and individual event lines +- **THEN** each event line displays the event type and name + +#### Scenario: MessageBubble does not render events when absent +- **WHEN** a MessageBubble receives no `events` prop or an empty `events` array +- **THEN** no events section is rendered + +#### Scenario: MessageBubble renders events in order +- **WHEN** a MessageBubble receives an `events` array with multiple events +- **THEN** events are rendered in the order they appear in the array + +### Requirement: Event data structure +Each event object has the shape `{ type, name, data, tags, metadata }`. The rendering SHALL use `type` and `name` for display, with optional `data` content when present. + +### Requirement: Events rendering follows existing pattern +The events section SHALL follow the same rendering pattern as `reasoningEl`, `toolCallEl`, and `toolDisplayEl`: conditional rendering, Box wrapper with `flexDirection: "row"`, `marginTop: 1`, `marginLeft: 2`, and Text elements with appropriate styling. + #### Scenario: addMessage after window is full still works - **WHEN** the window is full and `addMessage()` is called - **THEN** a new message is added and the oldest visible message is dropped From cc0d0f04445fd00001d88df33e55b1611c793748 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Mon, 27 Jul 2026 20:03:39 -0400 Subject: [PATCH 04/11] chore: archive render-stream-events-message-bubble change --- .../2026-07-27-render-stream-events-message-bubble}/proposal.md | 0 .../specs/component-message-bubbles/spec.md | 0 .../2026-07-27-render-stream-events-message-bubble}/tasks.md | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename openspec/changes/{2026-07-26-render-stream-events-message-bubble => archive/2026-07-27-render-stream-events-message-bubble}/proposal.md (100%) rename openspec/changes/{2026-07-26-render-stream-events-message-bubble => archive/2026-07-27-render-stream-events-message-bubble}/specs/component-message-bubbles/spec.md (100%) rename openspec/changes/{2026-07-26-render-stream-events-message-bubble => archive/2026-07-27-render-stream-events-message-bubble}/tasks.md (100%) diff --git a/openspec/changes/2026-07-26-render-stream-events-message-bubble/proposal.md b/openspec/changes/archive/2026-07-27-render-stream-events-message-bubble/proposal.md similarity index 100% rename from openspec/changes/2026-07-26-render-stream-events-message-bubble/proposal.md rename to openspec/changes/archive/2026-07-27-render-stream-events-message-bubble/proposal.md diff --git a/openspec/changes/2026-07-26-render-stream-events-message-bubble/specs/component-message-bubbles/spec.md b/openspec/changes/archive/2026-07-27-render-stream-events-message-bubble/specs/component-message-bubbles/spec.md similarity index 100% rename from openspec/changes/2026-07-26-render-stream-events-message-bubble/specs/component-message-bubbles/spec.md rename to openspec/changes/archive/2026-07-27-render-stream-events-message-bubble/specs/component-message-bubbles/spec.md diff --git a/openspec/changes/2026-07-26-render-stream-events-message-bubble/tasks.md b/openspec/changes/archive/2026-07-27-render-stream-events-message-bubble/tasks.md similarity index 100% rename from openspec/changes/2026-07-26-render-stream-events-message-bubble/tasks.md rename to openspec/changes/archive/2026-07-27-render-stream-events-message-bubble/tasks.md From 6993978a6f55db7ce2573d6750fa16546ac4438b Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Mon, 27 Jul 2026 20:27:38 -0400 Subject: [PATCH 05/11] fix: resolve spec audit gaps in MessageBubble events rendering - Add collapsible toggle for events section (+/- indicator) - Render optional data field when present (truncated to 100 chars) - Change flexDirection from 'column' to 'row' to match spec pattern --- src/tui/messageBubble.js | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/tui/messageBubble.js b/src/tui/messageBubble.js index 49b30d17..c23cf106 100644 --- a/src/tui/messageBubble.js +++ b/src/tui/messageBubble.js @@ -199,22 +199,38 @@ export function MessageBubble({ : null; const hasEvents = events && events.length > 0; + const [eventsCollapsed, setEventsCollapsed] = useState(true); + const eventsEl = hasEvents ? React.createElement( Box, - { flexDirection: "column", marginTop: 1, marginLeft: 2 }, + { flexDirection: "row", marginTop: 1, marginLeft: 2 }, React.createElement( Text, - { dimColor: true, color: "gray" }, - ` Events (${events.length}):`, - ), - ...events.map((evt, i) => - React.createElement( - Text, - { key: `evt-${i}`, color: "gray" }, - ` - ${evt.type}${evt.name ? ` (${evt.name})` : ""}`, - ), + { + dimColor: true, + color: "gray", + onClick: () => setEventsCollapsed((prev) => !prev), + }, + ` Events (${events.length}): [${eventsCollapsed ? "+" : "-"}] `, ), + !eventsCollapsed + ? React.createElement( + Box, + { flexDirection: "column" }, + ...events.map((evt, i) => + React.createElement( + Text, + { key: `evt-${i}`, color: "gray" }, + ` - ${evt.type}${evt.name ? ` (${evt.name})` : ""}${ + evt.data + ? ` — ${typeof evt.data === "string" ? evt.data.slice(0, 100) : JSON.stringify(evt.data).slice(0, 100)}` + : "" + }`, + ), + ), + ) + : null, ) : null; From b4c7ec56854805162928d957b6e5dd159e5df6b0 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Mon, 27 Jul 2026 20:32:51 -0400 Subject: [PATCH 06/11] fix: wrap events toggle in Box for Ink click support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Text doesn't support onClick in Ink — wrap the toggle label in a Box with cursorHidden and onClick on the Box instead. --- src/tui/messageBubble.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/tui/messageBubble.js b/src/tui/messageBubble.js index c23cf106..bc63858c 100644 --- a/src/tui/messageBubble.js +++ b/src/tui/messageBubble.js @@ -206,13 +206,16 @@ export function MessageBubble({ Box, { flexDirection: "row", marginTop: 1, marginLeft: 2 }, React.createElement( - Text, + Box, { - dimColor: true, - color: "gray", + cursorHidden: true, onClick: () => setEventsCollapsed((prev) => !prev), }, - ` Events (${events.length}): [${eventsCollapsed ? "+" : "-"}] `, + React.createElement( + Text, + { dimColor: true, color: "gray" }, + ` Events (${events.length}): [${eventsCollapsed ? "+" : "-"}] `, + ), ), !eventsCollapsed ? React.createElement( From 0eeb42aa3d094be3150415d4f7dbcf6cd05affa8 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Mon, 27 Jul 2026 20:37:57 -0400 Subject: [PATCH 07/11] =?UTF-8?q?fix:=20remove=20collapsible=20events=20?= =?UTF-8?q?=E2=80=94=20terminal=20UIs=20don't=20support=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ink's onClick is unreliable and collapsible sections are a desktop UI pattern, not a terminal one. Events are now always visible. --- src/tui/messageBubble.js | 42 +++++++++++++++------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/tui/messageBubble.js b/src/tui/messageBubble.js index bc63858c..e6541563 100644 --- a/src/tui/messageBubble.js +++ b/src/tui/messageBubble.js @@ -199,41 +199,31 @@ export function MessageBubble({ : null; const hasEvents = events && events.length > 0; - const [eventsCollapsed, setEventsCollapsed] = useState(true); const eventsEl = hasEvents ? React.createElement( Box, { flexDirection: "row", marginTop: 1, marginLeft: 2 }, + React.createElement( + Text, + { dimColor: true, color: "gray" }, + ` Events (${events.length}):`, + ), React.createElement( Box, - { - cursorHidden: true, - onClick: () => setEventsCollapsed((prev) => !prev), - }, - React.createElement( - Text, - { dimColor: true, color: "gray" }, - ` Events (${events.length}): [${eventsCollapsed ? "+" : "-"}] `, + { flexDirection: "column" }, + ...events.map((evt, i) => + React.createElement( + Text, + { key: `evt-${i}`, color: "gray" }, + ` - ${evt.type}${evt.name ? ` (${evt.name})` : ""}${ + evt.data + ? ` — ${typeof evt.data === "string" ? evt.data.slice(0, 100) : JSON.stringify(evt.data).slice(0, 100)}` + : "" + }`, + ), ), ), - !eventsCollapsed - ? React.createElement( - Box, - { flexDirection: "column" }, - ...events.map((evt, i) => - React.createElement( - Text, - { key: `evt-${i}`, color: "gray" }, - ` - ${evt.type}${evt.name ? ` (${evt.name})` : ""}${ - evt.data - ? ` — ${typeof evt.data === "string" ? evt.data.slice(0, 100) : JSON.stringify(evt.data).slice(0, 100)}` - : "" - }`, - ), - ), - ) - : null, ) : null; From 7d9dd3e6b68285dafbc3f4ccd0536257258a1777 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Mon, 27 Jul 2026 20:41:19 -0400 Subject: [PATCH 08/11] fix: deduplicate events by type with count Instead of listing every event, show one line per type with count: - message [16] - tool_call [3] - error [1] --- src/tui/messageBubble.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/tui/messageBubble.js b/src/tui/messageBubble.js index e6541563..219aa249 100644 --- a/src/tui/messageBubble.js +++ b/src/tui/messageBubble.js @@ -212,15 +212,16 @@ export function MessageBubble({ React.createElement( Box, { flexDirection: "column" }, - ...events.map((evt, i) => + ...Object.entries( + events.reduce((acc, evt) => { + acc[evt.type] = (acc[evt.type] || 0) + 1; + return acc; + }, {}) + ).map(([type, count]) => React.createElement( Text, - { key: `evt-${i}`, color: "gray" }, - ` - ${evt.type}${evt.name ? ` (${evt.name})` : ""}${ - evt.data - ? ` — ${typeof evt.data === "string" ? evt.data.slice(0, 100) : JSON.stringify(evt.data).slice(0, 100)}` - : "" - }`, + { key: `evt-${type}`, color: "gray" }, + ` - ${type} [${count}]`, ), ), ), From 0927957fdd84520bece61307b02d3239f0e83867 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Mon, 27 Jul 2026 20:52:13 -0400 Subject: [PATCH 09/11] fix: normalize LangChain event types to readable labels Raw event types like 'on_tool_start' are now mapped to human-readable labels: tool, model, agent, chain, retriever, custom. Unknown types are cleaned up (strips 'on_' prefix, replaces underscores). --- src/tui/messageBubble.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/tui/messageBubble.js b/src/tui/messageBubble.js index 219aa249..ccc6638b 100644 --- a/src/tui/messageBubble.js +++ b/src/tui/messageBubble.js @@ -198,6 +198,28 @@ export function MessageBubble({ ) : null; +/** + * Normalize LangChain stream event types to human-readable labels. + * @param {string} rawType - Raw event type (e.g., "on_tool_start") + * @returns {string} Human-readable label (e.g., "tool") + */ +function normalizeEventType(rawType) { + const mapping = { + on_chat_model_start: "model", + on_chat_model_end: "model", + on_chat_model_stream: "model", + on_tool_start: "tool", + on_tool_end: "tool", + on_agent_action: "agent", + on_chain_start: "chain", + on_chain_end: "chain", + on_retriever_start: "retriever", + on_retriever_end: "retriever", + on_custom_event: "custom", + }; + return mapping[rawType] || rawType.replace(/^on_/, "").replace(/_/g, " "); +} + const hasEvents = events && events.length > 0; const eventsEl = hasEvents @@ -214,7 +236,8 @@ export function MessageBubble({ { flexDirection: "column" }, ...Object.entries( events.reduce((acc, evt) => { - acc[evt.type] = (acc[evt.type] || 0) + 1; + const label = normalizeEventType(evt.type); + acc[label] = (acc[label] || 0) + 1; return acc; }, {}) ).map(([type, count]) => From 6b8ea1a1231e2dc6feff2fcf40312e4b54722bd0 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Mon, 27 Jul 2026 21:25:27 -0400 Subject: [PATCH 10/11] feat: use streamMode 'events' to expose full event stream Previously the provider used streamMode 'messages' which only emitted message events. Now it uses 'events' to capture the full stream: on_tool_start, on_tool_end, on_chat_model_stream, on_agent_action, etc. Events are transformed from LangChain format (event.event) to the streamingCallback format (event.type) so the TUI handler works without changes. --- src/index.js | 34 +++++++++++++++++++++++-------- src/tui/messageBubble.js | 44 ++++++++++++++++++++-------------------- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/index.js b/src/index.js index 33ae5d50..075e209f 100644 --- a/src/index.js +++ b/src/index.js @@ -207,19 +207,37 @@ async function callProvider(_name, _providerConfig, message, streamingCallback, messages: [{ role: "user", content: message }], }; - for await (const [_namespace, chunk] of await agent.stream(input, { + for await (const event of await agent.stream(input, { ...config, ...options, - streamMode: "messages", + streamMode: "events", subgraphs: true, })) { - const [message] = chunk; - const text = message?.text ?? ""; + // Transform LangChain event format to match streamingCallback expectations + // LangChain uses `event.event` but the callback expects `event.type` + const transformedEvent = { + type: event.event, + name: event.name, + data: event.data, + metadata: event.metadata, + }; + + // Forward the transformed event to the streaming callback + if (streamingCallback) { + streamingCallback(transformedEvent); + } - if (text) { - collectedContent += text; - if (streamingCallback) { - streamingCallback({ type: "message", text }); + // Accumulate text content from message events + if (event.event === "on_chat_model_stream" && event.data?.chunk?.content) { + collectedContent += event.data.chunk.content; + } + if (event.event === "message" && event.data?.content) { + const text = + typeof event.data.content === "string" + ? event.data.content + : (event.data.content?.text ?? ""); + if (text) { + collectedContent += text; } } } diff --git a/src/tui/messageBubble.js b/src/tui/messageBubble.js index ccc6638b..e5820f3b 100644 --- a/src/tui/messageBubble.js +++ b/src/tui/messageBubble.js @@ -198,27 +198,27 @@ export function MessageBubble({ ) : null; -/** - * Normalize LangChain stream event types to human-readable labels. - * @param {string} rawType - Raw event type (e.g., "on_tool_start") - * @returns {string} Human-readable label (e.g., "tool") - */ -function normalizeEventType(rawType) { - const mapping = { - on_chat_model_start: "model", - on_chat_model_end: "model", - on_chat_model_stream: "model", - on_tool_start: "tool", - on_tool_end: "tool", - on_agent_action: "agent", - on_chain_start: "chain", - on_chain_end: "chain", - on_retriever_start: "retriever", - on_retriever_end: "retriever", - on_custom_event: "custom", - }; - return mapping[rawType] || rawType.replace(/^on_/, "").replace(/_/g, " "); -} + /** + * Normalize LangChain stream event types to human-readable labels. + * @param {string} rawType - Raw event type (e.g., "on_tool_start") + * @returns {string} Human-readable label (e.g., "tool") + */ + function normalizeEventType(rawType) { + const mapping = { + on_chat_model_start: "model", + on_chat_model_end: "model", + on_chat_model_stream: "model", + on_tool_start: "tool", + on_tool_end: "tool", + on_agent_action: "agent", + on_chain_start: "chain", + on_chain_end: "chain", + on_retriever_start: "retriever", + on_retriever_end: "retriever", + on_custom_event: "custom", + }; + return mapping[rawType] || rawType.replace(/^on_/, "").replace(/_/g, " "); + } const hasEvents = events && events.length > 0; @@ -239,7 +239,7 @@ function normalizeEventType(rawType) { const label = normalizeEventType(evt.type); acc[label] = (acc[label] || 0) + 1; return acc; - }, {}) + }, {}), ).map(([type, count]) => React.createElement( Text, From 0e898affa185f85038e52cd3ea4115050646d45d Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Tue, 28 Jul 2026 20:41:04 -0400 Subject: [PATCH 11/11] WIP --- src/tui/app.js | 2 ++ src/tui/messageBubble.js | 31 +++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/tui/app.js b/src/tui/app.js index d413f67b..247d67a8 100644 --- a/src/tui/app.js +++ b/src/tui/app.js @@ -693,12 +693,14 @@ export default function App({ return (event) => { if (shouldAbort()) return; try { + console.log("[StreamingHandler] Received event:", event.type, event.name); // Capture all events on the message const currentEvents = messageListRef.current?.getMessageData(streamingMsgIdRef.current)?.events || []; messageListRef.current?.updateMessage(streamingMsgIdRef.current, { events: [...currentEvents, event], }); + console.log("[StreamingHandler] Updated message with events, total:", currentEvents.length + 1); if (event.type === "message") { committedContentRef.current = (committedContentRef.current || "") + event.text; diff --git a/src/tui/messageBubble.js b/src/tui/messageBubble.js index e5820f3b..08efcbb4 100644 --- a/src/tui/messageBubble.js +++ b/src/tui/messageBubble.js @@ -126,9 +126,10 @@ export function MessageBubble({ reasoningContent, activeToolCall, toolCallDisplay, - events, + events: eventsProp, }) { const [chunks, setChunks] = useState([]); + const [streamEvents, setStreamEvents] = useState(eventsProp || []); const { subscribe, unsubscribe } = useContext(PubSubContext); // Subscribe to pub/sub updates — each update appends a chunk, triggering @@ -137,12 +138,22 @@ export function MessageBubble({ if (!topic) return; const handleUpdate = (data) => { - setChunks((prev) => { - const newContent = data?.content ?? ""; - // Skip appends when content hasn't changed (avoids duplicate renders) - if (prev.length > 0 && prev[prev.length - 1] === newContent) return prev; - return [...prev, newContent]; - }); + console.log("[MessageBubble] handleUpdate called with:", Object.keys(data || {})); + // Handle content updates (streaming text chunks) + if (data?.content !== undefined) { + setChunks((prev) => { + const newContent = data.content; + // Skip appends when content hasn't changed (avoids duplicate renders) + if (prev.length > 0 && prev[prev.length - 1] === newContent) return prev; + return [...prev, newContent]; + }); + } + + // Handle events updates (stream events array) + if (data?.events !== undefined) { + console.log("[MessageBubble] Received events:", data.events.length); + setStreamEvents(data.events); + } }; subscribe(topic, handleUpdate); @@ -220,7 +231,7 @@ export function MessageBubble({ return mapping[rawType] || rawType.replace(/^on_/, "").replace(/_/g, " "); } - const hasEvents = events && events.length > 0; + const hasEvents = streamEvents && streamEvents.length > 0; const eventsEl = hasEvents ? React.createElement( @@ -229,13 +240,13 @@ export function MessageBubble({ React.createElement( Text, { dimColor: true, color: "gray" }, - ` Events (${events.length}):`, + ` Events (${streamEvents.length}):`, ), React.createElement( Box, { flexDirection: "column" }, ...Object.entries( - events.reduce((acc, evt) => { + streamEvents.reduce((acc, evt) => { const label = normalizeEventType(evt.type); acc[label] = (acc[label] || 0) + 1; return acc;