Skip to content

Commit 53abaae

Browse files
garlicKim21peterj
andauthored
fix(ui): stop breaking words mid-word in chat messages (#2351)
### Summary Chat message bodies are rendered with `break-all`, which lets a line break inside a word even when the whole word would have fit on the next line. Ordinary prose comes out chopped — `ModelConfig` renders as `M / odelConfig`, `propagates` as `propaga / tes`. `overflow-wrap: anywhere` keeps the behaviour `break-all` was added for (long tool-call ids and URLs still wrap instead of overflowing the column) but only breaks a word that genuinely cannot fit on a line of its own. The difference is invisible in CJK text, where a line break is permitted between almost any two characters — which is likely why it went unnoticed. ### Before / after Rendered from the `MessageWithUnbreakableTokens` story added in this PR, at a 560px-wide column. | | | |---|---| | **Before** (`break-all`) | `propaga` / `tes`, `componen` / `ts/chat/...` | | **After** (`overflow-wrap: anywhere`) | words intact; the URL breaks after `github.com/kagent-`, the tool-call id still wraps | <img width="560" height="340" alt="wb-before" src="https://github.com/user-attachments/assets/890102ed-4a43-4734-98ff-b413a3d89a24" /> <img width="560" height="340" alt="wb-after" src="https://github.com/user-attachments/assets/9125fbb8-dcf5-441b-bf3e-b13503d478b7" /> ### Test plan - [x] `npm run lint` - [x] `npm run test` — 344 passed - [x] `npm run test:vitest` — 34 files / 141 tests passed - [x] New story `Chat/ChatMessage/MessageWithUnbreakableTokens` renders prose next to a long tool-call id and a long URL, so Chromatic catches a regression here. ### Notes for reviewers - Chromatic will report a visual diff on this story only. It is a new story, so there is no prior baseline to compare against. - This does not address the separate problem where one over-wide element in the transcript widens the whole conversation column (see #2032). That is a container-level issue and I will open it separately. Signed-off-by: Golden Garlic <148346166+garlicKim21@users.noreply.github.com> Co-authored-by: Peter Jausovec <peterj@users.noreply.github.com>
1 parent a2eedd6 commit 53abaae

2 files changed

Lines changed: 31 additions & 1 deletion

File tree

ui/src/components/chat/ChatMessage.stories.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,30 @@ export const AgentMessageWithTable: Story = {
207207
},
208208
};
209209

210+
/**
211+
* Prose alongside content that offers no break opportunities — a tool-call id
212+
* carrying a Gemini thought signature, and a long URL. The prose has to keep
213+
* its word boundaries while the two long tokens wrap.
214+
*/
215+
export const MessageWithUnbreakableTokens: Story = {
216+
args: {
217+
message: createMessage({
218+
role: "agent",
219+
parts: [
220+
{
221+
kind: "text",
222+
text: `The kagent controller reconciles ModelConfig resources and propagates configuration to the agent deployment automatically.
223+
224+
Tool call id: \`call_9f2a__thought__QmFzZTY0RW5jb2RlZFRob3VnaHRTaWduYXR1cmVCbG9iQmFzZTY0RW5jb2RlZFRob3VnaHRTaWduYXR1cmU\`
225+
226+
See https://github.com/kagent-dev/kagent/blob/main/ui/src/components/chat/ChatMessage.tsx for the renderer.`,
227+
},
228+
],
229+
}),
230+
allMessages: [],
231+
},
232+
};
233+
210234
export const MessageWithMultipleParts: Story = {
211235
args: {
212236
message: createMessage({

ui/src/components/chat/ChatMessage.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,13 @@ export default function ChatMessage({ message, allMessages, agentContext, onAppr
175175
<KagentLogo className="w-4 h-4" />
176176
<div className="text-xs font-bold">{displayName}</div>
177177
</div> : <div className="text-xs font-bold">{displayName}</div>}
178-
<TruncatableText content={String(content)} className="break-all text-primary-foreground" />
178+
{/*
179+
`break-all` breaks a line inside a word even when the word would have
180+
fit on the next line ("ModelConfig" renders as "M / odelConfig").
181+
`overflow-wrap: anywhere` only breaks a word that cannot fit on a line
182+
of its own, which is what long tool-call ids and URLs need.
183+
*/}
184+
<TruncatableText content={String(content)} className="[overflow-wrap:anywhere] text-primary-foreground" />
179185
{source !== "user" && (
180186
<div className="flex mt-2 justify-end items-center gap-2">
181187
{tokenStats && <TokenStatsTooltip stats={tokenStats} />}

0 commit comments

Comments
 (0)