webui: Fix message bubbles and tool calls overflow issues in timeline
On the bright side, Sketch fixed this with repeated "hey, look at that
screenshot; there's horizontal truncation." On the downside, it got
really confused by shadow dom, and the solution doesn't look clean to
me, with lots of inline CSS.
~~~~~
Fixed various overflow issues in timeline component, particularly with
handling long bash commands in shadow DOM contexts:
1. Message Bubbles:
- Added overflow constraints to message-bubble-container
- Changed max-width from 80% to 100% for better containment
- Added text-overflow: ellipsis for clean truncation
2. Tool Cards:
- Implemented manual string truncation for bash commands
- Limited display to 80 chars with ellipsis for overflowing text
- Added CSS to constrain width in all contexts
3. Shadow DOM Challenges:
- Standard CSS inheritance doesn't penetrate shadow DOM boundaries
- Component-specific styles required direct modification in render methods
- Parallel CSS and JS truncation needed for reliable overflow handling
- Multiple nested web components required coordinated width constraints
4. Global Improvements:
- Added overflow-x: hidden to parent containers
- Used box-sizing: border-box for accurate sizing
- Fixed scroll container to prevent horizontal scrolling
The primary insight was that shadow DOM isolation prevented CSS-only
solutions from working reliably. Explicit string truncation in the
component code was needed alongside careful container width management.
This pattern may need to be applied to other web components that
display potentially lengthy content.
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: sc937c08ac1b7766fk
diff --git a/webui/src/web-components/demo/sketch-timeline.demo.html b/webui/src/web-components/demo/sketch-timeline.demo.html
index 9c817d8..b535fa1 100644
--- a/webui/src/web-components/demo/sketch-timeline.demo.html
+++ b/webui/src/web-components/demo/sketch-timeline.demo.html
@@ -6,6 +6,36 @@
<script>
const messages = [
{
+ type: "agent",
+ end_of_turn: false,
+ content:
+ "Now I'll create an empty commit with a very long commit message:",
+ tool_calls: [
+ {
+ name: "bash",
+ input:
+ '{"command":"git commit --allow-empty -m \\"chore: create empty commit with very long message\\\\n\\\\nThis is an extremely long commit message to demonstrate how Git handles verbose commit messages.\\\\nThis empty commit has no actual code changes, but contains a lengthy explanation.\\\\n\\\\nThe empty commit pattern can be useful in several scenarios:\\\\n1. Triggering CI/CD pipelines without modifying code\\\\n2. Marking significant project milestones or releases\\\\n3. Creating annotated reference points in the commit history\\\\n4. Documenting important project decisions\\\\n\\\\nEmpty commits are created using the --allow-empty flag, which instructs Git to \\\\ncreate a commit even when there are no changes staged for commit. This bypasses\\\\nGit\'s normal behavior of refusing to record a commit that has the exact same\\\\ntree as its parent.\\\\n\\\\nSome technical details about empty commits:\\\\n- They still receive a unique commit hash\\\\n- They appear in the git log like any other commit\\\\n- They can be referenced by other git commands\\\\n- They can be included in merges, rebases, and other git operations\\\\n- They take up minimal space in the repository\\\\n\\\\nThis commit message continues to be extremely verbose just to demonstrate how\\\\nGit handles very long commit messages and how they appear in various Git interfaces.\\\\nThe content of this message is not particularly meaningful but serves to\\\\ndemonstrate the ability to include detailed context when necessary.\\\\n\\\\nMore lines of explanation follow to make this commit message truly excessive...\\\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ultrices ligula\\\\nnon tellus tincidunt, in volutpat nisi venenatis. Phasellus convallis felis nec\\\\ndiam dignissim, vel fringilla odio ultricies. Morbi feugiat velit in nulla\\\\nconsequat, eget facilisis dui vehicula. Donec fermentum nisl vel justo ultricies,\\\\nut semper libero ultrices. \\\\n\\\\nCras sagittis libero vitae diam eleifend, vel viverra eros tempus.\\\\nSuspendisse potenti. Nullam ac pede. Curabitur blandit hendrerit nibh.\\\\nDonec quis augue ut diam lobortis venenatis. Quisque dapibus justo eget neque.\\\\nInteger sit amet ligula vitae arcu interdum ultrices. Nullam ornare, magna sed\\\\nvenenatis tincidunt, libero urna ullamcorper tortor, ac ultrices neque sem ut massa.\\" --trailer \'Co-Authored-By: sketch <hello@sketch.dev>\' --trailer \'Change-ID: s$(openssl rand -hex 8)k\'"}',
+ tool_call_id: "toolu_01X67pzGzW2NtTjZnxoXTMc7",
+ args: '{"command":"git commit --allow-empty -m \\"chore: create empty commit with very long message\\\\n\\\\nThis is an extremely long commit message to demonstrate how Git handles verbose commit messages..."}',
+ result:
+ "[detached HEAD abc1234] chore: create empty commit with very long message\n 1 file changed, 1 insertion(+), 1 deletion(-)",
+ },
+ ],
+ timestamp: "2025-05-11T21:44:48.760674089Z",
+ conversation_id: "3qc-ptm3",
+ usage: {
+ input_tokens: 6,
+ cache_creation_input_tokens: 77,
+ cache_read_input_tokens: 4230,
+ output_tokens: 668,
+ cost_usd: 0.01159575,
+ },
+ start_time: "2025-05-11T21:44:35.577868468Z",
+ end_time: "2025-05-11T21:44:48.760670506Z",
+ elapsed: 13182802037,
+ idx: 8,
+ },
+ {
type: "user",
content: "a user message",
timestamp: "2025-04-14T16:39:30.639533919Z",
@@ -206,6 +236,16 @@
});
</script>
<style>
+ /* Fix for bash command overflow */
+ sketch-timeline-message::part(message-content),
+ sketch-tool-calls::part(tool-call-card),
+ sketch-tool-card::part(summary) {
+ max-width: 100% !important;
+ overflow: hidden !important;
+ text-overflow: ellipsis !important;
+ white-space: nowrap !important;
+ }
+
.app-shell {
display: block;
font-family:
diff --git a/webui/src/web-components/sketch-timeline-message.ts b/webui/src/web-components/sketch-timeline-message.ts
index f28b1a3..1edaff2 100644
--- a/webui/src/web-components/sketch-timeline-message.ts
+++ b/webui/src/web-components/sketch-timeline-message.ts
@@ -60,6 +60,8 @@
flex: 1;
display: flex;
max-width: calc(100% - 160px);
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.user .message-bubble-container {
@@ -77,9 +79,11 @@
padding: 6px 10px;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
- max-width: 80%;
+ max-width: 100%;
width: fit-content;
min-width: min-content;
+ overflow-wrap: break-word;
+ word-break: break-word;
}
/* User message styling */
@@ -1098,6 +1102,28 @@
.floating-message.error {
background-color: rgba(220, 53, 69, 0.9);
}
+
+ /* Style for code, pre elements, and tool components to ensure proper wrapping/truncation */
+ pre, code, sketch-tool-calls, sketch-tool-card, sketch-tool-card-bash {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+ }
+
+ /* Special rule for the message content container */
+ .message-content {
+ max-width: 100% !important;
+ overflow: hidden !important;
+ }
+
+ /* Ensure tool call containers don't overflow */
+ ::slotted(sketch-tool-calls) {
+ max-width: 100%;
+ width: 100%;
+ overflow-wrap: break-word;
+ word-break: break-word;
+ }
`;
document.head.appendChild(floatingMessageStyles);
diff --git a/webui/src/web-components/sketch-timeline.ts b/webui/src/web-components/sketch-timeline.ts
index 64f3db0..3870e39 100644
--- a/webui/src/web-components/sketch-timeline.ts
+++ b/webui/src/web-components/sketch-timeline.ts
@@ -52,6 +52,7 @@
margin: 0 auto;
padding: 0 15px;
box-sizing: border-box;
+ overflow-x: hidden;
}
/* Chat-like timeline styles */
@@ -64,8 +65,10 @@
/* Remove the vertical timeline line */
#scroll-container {
- overflow: auto;
+ overflow-y: auto;
+ overflow-x: hidden;
padding-left: 1em;
+ max-width: 100%;
}
#jump-to-latest {
display: none;
diff --git a/webui/src/web-components/sketch-tool-calls.ts b/webui/src/web-components/sketch-tool-calls.ts
index 3245a45..5f416e1 100644
--- a/webui/src/web-components/sketch-tool-calls.ts
+++ b/webui/src/web-components/sketch-tool-calls.ts
@@ -21,6 +21,9 @@
.tool-calls-container {
margin-top: 8px;
padding-top: 4px;
+ max-width: 100%;
+ width: 100%;
+ box-sizing: border-box;
}
/* Card container */
@@ -34,6 +37,9 @@
cursor: pointer;
border-left: 2px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+ max-width: 100%;
+ overflow-wrap: break-word;
+ word-break: break-word;
}
/* Status indicators for tool calls */
diff --git a/webui/src/web-components/sketch-tool-card.ts b/webui/src/web-components/sketch-tool-card.ts
index b63e021..7b12eaa 100644
--- a/webui/src/web-components/sketch-tool-card.ts
+++ b/webui/src/web-components/sketch-tool-card.ts
@@ -20,6 +20,13 @@
// Common styles shared across all tool cards
const commonStyles = css`
+ :host {
+ display: block;
+ max-width: 100%;
+ width: 100%;
+ box-sizing: border-box;
+ overflow: hidden;
+ }
pre {
background: rgb(236, 236, 236);
color: black;
@@ -33,11 +40,13 @@
overflow-wrap: break-word;
}
.summary-text {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 100%;
+ overflow: hidden !important;
+ text-overflow: ellipsis !important;
+ white-space: nowrap !important;
+ max-width: 100% !important;
+ width: 100% !important;
font-family: monospace;
+ display: block;
}
`;
@@ -64,6 +73,7 @@
border-radius: 4px;
position: relative;
overflow: hidden;
+ flex-wrap: wrap;
}
.tool-row:hover {
background-color: rgba(0, 0, 0, 0.02);
@@ -94,9 +104,9 @@
font-size: 14px;
}
.summary-text {
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
+ white-space: normal;
+ overflow-wrap: break-word;
+ word-break: break-word;
flex-grow: 1;
flex-shrink: 1;
color: #444;
@@ -104,7 +114,7 @@
font-size: 12px;
padding: 0 4px;
min-width: 50px;
- max-width: calc(100% - 250px);
+ max-width: calc(100% - 150px);
display: inline-block;
}
.tool-status {
@@ -267,10 +277,23 @@
static styles = [
commonStyles,
css`
+ :host {
+ max-width: 100%;
+ display: block;
+ }
.input {
display: flex;
width: 100%;
+ max-width: 100%;
flex-direction: column;
+ overflow-wrap: break-word;
+ word-break: break-word;
+ }
+ .command-wrapper {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.input pre {
width: 100%;
@@ -319,13 +342,25 @@
const isBackground = inputData?.background === true;
const backgroundIcon = isBackground ? "🔄 " : "";
+ // Truncate the command if it's too long to display nicely
+ const command = inputData?.command || "";
+ const displayCommand =
+ command.length > 80 ? command.substring(0, 80) + "..." : command;
+
return html` <sketch-tool-card
.open=${this.open}
.toolCall=${this.toolCall}
>
- <span slot="summary" class="summary-text">
- <div class="command-wrapper">
- ${backgroundIcon}${inputData?.command}
+ <span
+ slot="summary"
+ class="summary-text"
+ style="display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
+ >
+ <div
+ class="command-wrapper"
+ style="max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
+ >
+ ${backgroundIcon}${displayCommand}
</div>
</span>
<div slot="input" class="input">
@@ -714,12 +749,26 @@
render() {
return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text">${this.toolCall?.input}</span>
- <div slot="input">
+ <span
+ slot="summary"
+ style="display: block; white-space: normal; word-break: break-word; overflow-wrap: break-word; max-width: 100%; width: 100%;"
+ >${this.toolCall?.input}</span
+ >
+ <div
+ slot="input"
+ style="max-width: 100%; overflow-wrap: break-word; word-break: break-word;"
+ >
Input:
- <pre>${this.toolCall?.input}</pre>
+ <pre
+ style="max-width: 100%; white-space: pre-wrap; overflow-wrap: break-word; word-break: break-word;"
+ >
+${this.toolCall?.input}</pre
+ >
</div>
- <div slot="result">
+ <div
+ slot="result"
+ style="max-width: 100%; overflow-wrap: break-word; word-break: break-word;"
+ >
Result:
${this.toolCall?.result_message?.tool_result
? html`<pre>${this.toolCall?.result_message.tool_result}</pre>`