Skip to content

Commit acb38f4

Browse files
committed
chat: keep the folded thinking row inside its column
The summary is an inline-flex box, so a long reasoning line without breaks sized it to its content: at 900 px it was 1308 px wide inside a 560 px details box, and the opened body ran past the column too. Bound the summary and the preview to their box and let the opened body break anywhere; the summary stays one line (white-space:nowrap keeps the label from wrapping into a stack), the caret and the peek stay as they are. Chromium measurement of the rendered cell at 900 px and 400 px: summary 1308 / 1308 px before, 560 / 312 px after, inside a 560 / 312 px details box; the peek fills what is left (423 / 175 px against a 1236 px line); the opened 2000-character line wraps instead of scrolling to 15397 px. Docs-not-needed: #1718 bounds three CSS rules of the thinking row; no architecture, interface or contract changes. Fixes #1718
1 parent f0159ea commit acb38f4

7 files changed

Lines changed: 378 additions & 203 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A folded thinking row stays inside the message column: a long reasoning line without a break is clipped at the row with an ellipsis instead of widening it past its column, and the opened row wraps that line instead of scrolling sideways ([#1718](https://github.com/ThinkFlowLab/FlyBot/issues/1718)).

‎core/client.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4677,11 +4677,11 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)}
46774677
`;
46784678
var CONVERSATION_CSS = `
46794679
.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)}
4680-
.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}
4680+
.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px;max-width:100%;min-width:0;white-space:nowrap}
46814681
.bt-think summary::-webkit-details-marker{display:none}
46824682
.bt-think-caret{display:inline-block;transition:transform .15s;font-size:14px;line-height:1}
46834683
.bt-think[open] .bt-think-caret{transform:rotate(90deg)}
4684-
.bt-think-peek{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)}
4684+
.bt-think-peek{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)}
46854685
/* Opened, the body prints that same first line again, so the preview steps aside (#1636). */
46864686
.bt-think[open] .bt-think-peek{display:none}
46874687
.bt-person-mark{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-ink-5,rgba(20,20,20,.08));color:var(--bt-ink-3,rgba(20,20,20,.45))}
@@ -4690,7 +4690,7 @@ var CONVERSATION_CSS = `
46904690
/* A person's typed line keeps its line breaks, as on the sender's side (#455): Markdown would join them. */
46914691
.bt-person-msg .bt-bubble :is(p,li){white-space:pre-line}
46924692
.bt-remote-handle{display:inline-flex;align-items:center;gap:3px;margin-left:6px;font-weight:400;color:var(--bt-ink-3,rgba(20,20,20,.36))}
4693-
.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
4693+
.bt-think-text{white-space:pre-wrap;overflow-wrap:anywhere;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
46944694
.bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap}
46954695
.bt-event>span{padding:0 2px}
46964696
.bt-event button{border:0;background:none;color:inherit;font:inherit;display:inline-flex;align-items:center;gap:4px;cursor:pointer;height:24px;padding:4px 6px 4px 4px;border-radius:999px;box-sizing:border-box;transition:background-color .12s ease}

‎core/src/client/styles.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -884,11 +884,11 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)}
884884

885885
export const CONVERSATION_CSS = `
886886
.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)}
887-
.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}
887+
.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px;max-width:100%;min-width:0;white-space:nowrap}
888888
.bt-think summary::-webkit-details-marker{display:none}
889889
.bt-think-caret{display:inline-block;transition:transform .15s;font-size:14px;line-height:1}
890890
.bt-think[open] .bt-think-caret{transform:rotate(90deg)}
891-
.bt-think-peek{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)}
891+
.bt-think-peek{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)}
892892
/* Opened, the body prints that same first line again, so the preview steps aside (#1636). */
893893
.bt-think[open] .bt-think-peek{display:none}
894894
.bt-person-mark{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-ink-5,rgba(20,20,20,.08));color:var(--bt-ink-3,rgba(20,20,20,.45))}
@@ -897,7 +897,7 @@ export const CONVERSATION_CSS = `
897897
/* A person's typed line keeps its line breaks, as on the sender's side (#455): Markdown would join them. */
898898
.bt-person-msg .bt-bubble :is(p,li){white-space:pre-line}
899899
.bt-remote-handle{display:inline-flex;align-items:center;gap:3px;margin-left:6px;font-weight:400;color:var(--bt-ink-3,rgba(20,20,20,.36))}
900-
.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
900+
.bt-think-text{white-space:pre-wrap;overflow-wrap:anywhere;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
901901
.bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap}
902902
.bt-event>span{padding:0 2px}
903903
.bt-event button{border:0;background:none;color:inherit;font:inherit;display:inline-flex;align-items:center;gap:4px;cursor:pointer;height:24px;padding:4px 6px 4px 4px;border-radius:999px;box-sizing:border-box;transition:background-color .12s ease}

‎core/test/thinking-row.test.js‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { test } from 'node:test'
22
import assert from 'node:assert/strict'
33
import { readFileSync } from 'node:fs'
4+
import { createRequire } from 'node:module'
5+
import { join } from 'node:path'
46
import { fileURLToPath } from 'node:url'
57
import { runInNewContext } from 'node:vm'
68
import { build } from 'esbuild'
@@ -176,3 +178,86 @@ test('#1642: reasoning that is only markup gets a neutral summary, never a bare
176178
assert.ok(peek, 'the folded row never reads as an empty frame')
177179
assert.equal(text(peek), '2 lines of reasoning')
178180
})
181+
182+
// #1718: the folded row must stay inside the column it sits in. jsdom lays no boxes out (every
183+
// element is 0×0), so the unit suite cannot read scrollWidth/clientWidth here; what it can read is
184+
// the cascade the app ships, mounted around this row. The pixel evidence, taken from the same
185+
// render in headless Chromium at 900 px and 400 px, is in the pull request: before the fix the
186+
// summary was 1308 px wide inside a 560 px / 312 px box.
187+
const { JSDOM } = createRequire(join(process.env.DSH_HARNESS, 'package.json'))('jsdom')
188+
const mountRow = (row, width) => {
189+
const { window } = new JSDOM(`<!doctype html><html><head><style>${CONVERSATION_CSS}</style></head><body><div class="column" style="width:${width}px"></div></body></html>`)
190+
const doc = window.document
191+
const build = (node) => {
192+
if (node === null || node === undefined || typeof node === 'boolean') return null
193+
if (Array.isArray(node)) return node.map(build)
194+
if (typeof node !== 'object') return doc.createTextNode(String(node))
195+
if (node.type === Fragment) return build(node.props?.children)
196+
const element = doc.createElement(node.type)
197+
for (const [name, value] of Object.entries(node.props ?? {})) {
198+
if (name === 'children' || name === 'key') continue
199+
if (typeof value === 'string' || typeof value === 'number') element.setAttribute(name === 'className' ? 'class' : name, value)
200+
}
201+
for (const child of [build(node.props?.children ?? [])].flat(Infinity)) if (child) element.append(child)
202+
return element
203+
}
204+
const rowEl = build(row)
205+
doc.querySelector('.column').append(rowEl)
206+
return { window, doc, style: (selector, property) => window.getComputedStyle(doc.querySelector(selector))[property] }
207+
}
208+
const LONG = 'x'.repeat(2000)
209+
const longRow = () => {
210+
const blocks = [think(LONG)]
211+
return thinkingRow(render(step(1, blocks), [step(1, blocks)]))
212+
}
213+
214+
test('#1718: a 2000-character line cannot widen the folded row past its box (900 px and 400 px)', () => {
215+
const row = longRow()
216+
assert.ok(row, 'the row renders for the long reasoning')
217+
for (const width of [900, 400]) {
218+
const { doc, style } = mountRow(row, width)
219+
// The ceiling is still the details box, not the whole column.
220+
assert.equal(style('.bt-think', 'maxWidth'), 'min(560px,78%)')
221+
// An inline-flex box sizes to its content unless it is bounded; the bound is its own box.
222+
assert.equal(style('.bt-think summary', 'maxWidth'), '100%', `summary is bounded by its box at ${width}px`)
223+
assert.equal(style('.bt-think summary', 'minWidth'), '0px')
224+
// One line: the label inside the summary must not wrap when the preview is squeezed.
225+
assert.equal(style('.bt-think summary', 'whiteSpace'), 'nowrap', `the row stays one line at ${width}px`)
226+
// The preview fills what is left and clips with an ellipsis, so the line cannot run wide.
227+
assert.equal(style('.bt-think-peek', 'maxWidth'), '100%')
228+
assert.equal(style('.bt-think-peek', 'minWidth'), '0px')
229+
assert.equal(style('.bt-think-peek', 'overflow'), 'hidden')
230+
assert.equal(style('.bt-think-peek', 'textOverflow'), 'ellipsis')
231+
assert.equal(style('.bt-think-peek', 'whiteSpace'), 'nowrap')
232+
// Opened, the same unbroken token wraps instead of running past the column.
233+
assert.equal(style('.bt-think-text', 'overflowWrap'), 'anywhere')
234+
assert.equal(style('.bt-think-text', 'whiteSpace'), 'pre-wrap')
235+
// Folded, with the caret and the label it always showed.
236+
const details = doc.querySelector('.bt-think')
237+
assert.equal(details.open, false)
238+
assert.equal(doc.querySelector('.bt-think-caret').textContent, '›')
239+
assert.match(doc.querySelector('summary').textContent, /Thought it through/)
240+
}
241+
})
242+
243+
test('#1718: the fold stays the design — one line, no block summary, no hidden caret', () => {
244+
const rules = [...CONVERSATION_CSS.matchAll(/\.bt-think[^{]*\{[^}]*\}/g)].map(match => match[0])
245+
const summary = rules.find(rule => /^\.bt-think summary\{/.test(rule))
246+
assert.ok(summary, 'the row has its summary rule')
247+
assert.match(summary, /display:inline-flex/, 'the summary stays a one-line inline flex row')
248+
assert.match(summary, /white-space:nowrap/, 'the label cannot wrap into a stacked block')
249+
assert.doesNotMatch(summary, /display:(?:block|flex)/, 'the fold is not turned into a stacked block')
250+
assert.doesNotMatch(summary, /overflow:hidden/, 'nothing clips the caret away')
251+
// Opened, the body carries the whole 2000-character line and the preview steps aside (#1636).
252+
const { doc } = mountRow(longRow(), 900)
253+
doc.querySelector('details').open = true
254+
assert.equal(doc.querySelector('.bt-think-text').textContent, LONG)
255+
assert.equal(doc.querySelector('.bt-think-caret').textContent, '›')
256+
})
257+
258+
test('#1718: the bundle the app loads carries the same containment rules', () => {
259+
const bundle = readFileSync(new URL('../client.js', import.meta.url), 'utf8')
260+
assert.ok(bundle.includes('.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px;max-width:100%;min-width:0;white-space:nowrap}'), 'summary bounded in client.js')
261+
assert.ok(bundle.includes('.bt-think-peek{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'), 'peek bounded in client.js')
262+
assert.ok(bundle.includes('.bt-think-text{white-space:pre-wrap;overflow-wrap:anywhere;'), 'body wraps in client.js')
263+
})

‎patches/ds-bot/0003-flybot-api-client.patch‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3035,13 +3035,17 @@ diff --git a/src/client/styles.js b/src/client/styles.js
30353035
.bt-mem-scrim{animation:bt-fade .2s ease-out}
30363036
.bt-mem{position:relative;width:min(680px,calc(100vw - 48px));height:min(760px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));background:var(--bt-main);border-radius:20px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;flex-direction:column;overflow:hidden;color:var(--bt-ink);animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
30373037
.bt-mem-head{position:relative;display:flex;align-items:center;gap:8px;height:60px;flex:none;padding:0 14px 0 24px;border-bottom:.5px solid var(--bt-line);box-sizing:border-box}
3038-
@@ -782,6 +782,17 @@ export const CONVERSATION_CSS = `
3038+
@@ -780,9 +780,20 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)}
3039+
3040+
export const CONVERSATION_CSS = `
30393041
.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)}
3040-
.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}
3042+
-.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}
3043+
+.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px;max-width:100%;min-width:0;white-space:nowrap}
30413044
.bt-think summary::-webkit-details-marker{display:none}
3045+
-.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
30423046
+.bt-think-caret{display:inline-block;transition:transform .15s;font-size:14px;line-height:1}
30433047
+.bt-think[open] .bt-think-caret{transform:rotate(90deg)}
3044-
+.bt-think-peek{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)}
3048+
+.bt-think-peek{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)}
30453049
+/* Opened, the body prints that same first line again, so the preview steps aside (#1636). */
30463050
+.bt-think[open] .bt-think-peek{display:none}
30473051
+.bt-person-mark{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-ink-5,rgba(20,20,20,.08));color:var(--bt-ink-3,rgba(20,20,20,.45))}
@@ -3050,9 +3054,10 @@ diff --git a/src/client/styles.js b/src/client/styles.js
30503054
+/* A person's typed line keeps its line breaks, as on the sender's side (#455): Markdown would join them. */
30513055
+.bt-person-msg .bt-bubble :is(p,li){white-space:pre-line}
30523056
+.bt-remote-handle{display:inline-flex;align-items:center;gap:3px;margin-left:6px;font-weight:400;color:var(--bt-ink-3,rgba(20,20,20,.36))}
3053-
.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
3057+
+.bt-think-text{white-space:pre-wrap;overflow-wrap:anywhere;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
30543058
.bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap}
30553059
.bt-event>span{padding:0 2px}
3060+
.bt-event button{border:0;background:none;color:inherit;font:inherit;display:inline-flex;align-items:center;gap:4px;cursor:pointer;height:24px;padding:4px 6px 4px 4px;border-radius:999px;box-sizing:border-box;transition:background-color .12s ease}
30563061
@@ -795,7 +795,6 @@ export const CONVERSATION_CSS = `
30573062
.bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
30583063
.bt-time-sep{display:flex;justify-content:center;align-items:center;height:28px;margin:14px 0 8px;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap}

0 commit comments

Comments
 (0)