Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1718-think-row-clip.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +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)).
6 changes: 3 additions & 3 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -4677,11 +4677,11 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)}
`;
var CONVERSATION_CSS = `
.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)}
.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}
.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}
.bt-think summary::-webkit-details-marker{display:none}
.bt-think-caret{display:inline-block;transition:transform .15s;font-size:14px;line-height:1}
.bt-think[open] .bt-think-caret{transform:rotate(90deg)}
.bt-think-peek{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)}
.bt-think-peek{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)}
/* Opened, the body prints that same first line again, so the preview steps aside (#1636). */
.bt-think[open] .bt-think-peek{display:none}
.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))}
Expand All @@ -4690,7 +4690,7 @@ var CONVERSATION_CSS = `
/* A person's typed line keeps its line breaks, as on the sender's side (#455): Markdown would join them. */
.bt-person-msg .bt-bubble :is(p,li){white-space:pre-line}
.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))}
.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
.bt-think-text{white-space:pre-wrap;overflow-wrap:anywhere;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
.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}
.bt-event>span{padding:0 2px}
.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}
Expand Down
6 changes: 3 additions & 3 deletions core/src/client/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -884,11 +884,11 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)}

export const CONVERSATION_CSS = `
.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)}
.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}
.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}
.bt-think summary::-webkit-details-marker{display:none}
.bt-think-caret{display:inline-block;transition:transform .15s;font-size:14px;line-height:1}
.bt-think[open] .bt-think-caret{transform:rotate(90deg)}
.bt-think-peek{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)}
.bt-think-peek{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)}
/* Opened, the body prints that same first line again, so the preview steps aside (#1636). */
.bt-think[open] .bt-think-peek{display:none}
.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))}
Expand All @@ -897,7 +897,7 @@ export const CONVERSATION_CSS = `
/* A person's typed line keeps its line breaks, as on the sender's side (#455): Markdown would join them. */
.bt-person-msg .bt-bubble :is(p,li){white-space:pre-line}
.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))}
.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
.bt-think-text{white-space:pre-wrap;overflow-wrap:anywhere;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
.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}
.bt-event>span{padding:0 2px}
.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}
Expand Down
85 changes: 85 additions & 0 deletions core/test/thinking-row.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { runInNewContext } from 'node:vm'
import { build } from 'esbuild'
Expand Down Expand Up @@ -176,3 +178,86 @@ test('#1642: reasoning that is only markup gets a neutral summary, never a bare
assert.ok(peek, 'the folded row never reads as an empty frame')
assert.equal(text(peek), '2 lines of reasoning')
})

// #1718: the folded row must stay inside the column it sits in. jsdom lays no boxes out (every
// element is 0×0), so the unit suite cannot read scrollWidth/clientWidth here; what it can read is
// the cascade the app ships, mounted around this row. The pixel evidence, taken from the same
// render in headless Chromium at 900 px and 400 px, is in the pull request: before the fix the
// summary was 1308 px wide inside a 560 px / 312 px box.
const { JSDOM } = createRequire(join(process.env.DSH_HARNESS, 'package.json'))('jsdom')
const mountRow = (row, width) => {
const { window } = new JSDOM(`<!doctype html><html><head><style>${CONVERSATION_CSS}</style></head><body><div class="column" style="width:${width}px"></div></body></html>`)
const doc = window.document
const build = (node) => {
if (node === null || node === undefined || typeof node === 'boolean') return null
if (Array.isArray(node)) return node.map(build)
if (typeof node !== 'object') return doc.createTextNode(String(node))
if (node.type === Fragment) return build(node.props?.children)
const element = doc.createElement(node.type)
for (const [name, value] of Object.entries(node.props ?? {})) {
if (name === 'children' || name === 'key') continue
if (typeof value === 'string' || typeof value === 'number') element.setAttribute(name === 'className' ? 'class' : name, value)
}
for (const child of [build(node.props?.children ?? [])].flat(Infinity)) if (child) element.append(child)
return element
}
const rowEl = build(row)
doc.querySelector('.column').append(rowEl)
return { window, doc, style: (selector, property) => window.getComputedStyle(doc.querySelector(selector))[property] }
}
const LONG = 'x'.repeat(2000)
const longRow = () => {
const blocks = [think(LONG)]
return thinkingRow(render(step(1, blocks), [step(1, blocks)]))
}

test('#1718: a 2000-character line cannot widen the folded row past its box (900 px and 400 px)', () => {
const row = longRow()
assert.ok(row, 'the row renders for the long reasoning')
for (const width of [900, 400]) {
const { doc, style } = mountRow(row, width)
// The ceiling is still the details box, not the whole column.
assert.equal(style('.bt-think', 'maxWidth'), 'min(560px,78%)')
// An inline-flex box sizes to its content unless it is bounded; the bound is its own box.
assert.equal(style('.bt-think summary', 'maxWidth'), '100%', `summary is bounded by its box at ${width}px`)
assert.equal(style('.bt-think summary', 'minWidth'), '0px')
// One line: the label inside the summary must not wrap when the preview is squeezed.
assert.equal(style('.bt-think summary', 'whiteSpace'), 'nowrap', `the row stays one line at ${width}px`)
// The preview fills what is left and clips with an ellipsis, so the line cannot run wide.
assert.equal(style('.bt-think-peek', 'maxWidth'), '100%')
assert.equal(style('.bt-think-peek', 'minWidth'), '0px')
assert.equal(style('.bt-think-peek', 'overflow'), 'hidden')
assert.equal(style('.bt-think-peek', 'textOverflow'), 'ellipsis')
assert.equal(style('.bt-think-peek', 'whiteSpace'), 'nowrap')
// Opened, the same unbroken token wraps instead of running past the column.
assert.equal(style('.bt-think-text', 'overflowWrap'), 'anywhere')
assert.equal(style('.bt-think-text', 'whiteSpace'), 'pre-wrap')
// Folded, with the caret and the label it always showed.
const details = doc.querySelector('.bt-think')
assert.equal(details.open, false)
assert.equal(doc.querySelector('.bt-think-caret').textContent, '›')
assert.match(doc.querySelector('summary').textContent, /Thought it through/)
}
})

test('#1718: the fold stays the design — one line, no block summary, no hidden caret', () => {
const rules = [...CONVERSATION_CSS.matchAll(/\.bt-think[^{]*\{[^}]*\}/g)].map(match => match[0])
const summary = rules.find(rule => /^\.bt-think summary\{/.test(rule))
assert.ok(summary, 'the row has its summary rule')
assert.match(summary, /display:inline-flex/, 'the summary stays a one-line inline flex row')
assert.match(summary, /white-space:nowrap/, 'the label cannot wrap into a stacked block')
assert.doesNotMatch(summary, /display:(?:block|flex)/, 'the fold is not turned into a stacked block')
assert.doesNotMatch(summary, /overflow:hidden/, 'nothing clips the caret away')
// Opened, the body carries the whole 2000-character line and the preview steps aside (#1636).
const { doc } = mountRow(longRow(), 900)
doc.querySelector('details').open = true
assert.equal(doc.querySelector('.bt-think-text').textContent, LONG)
assert.equal(doc.querySelector('.bt-think-caret').textContent, '›')
})

test('#1718: the bundle the app loads carries the same containment rules', () => {
const bundle = readFileSync(new URL('../client.js', import.meta.url), 'utf8')
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')
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')
assert.ok(bundle.includes('.bt-think-text{white-space:pre-wrap;overflow-wrap:anywhere;'), 'body wraps in client.js')
})
13 changes: 9 additions & 4 deletions patches/ds-bot/0003-flybot-api-client.patch
Original file line number Diff line number Diff line change
Expand Up @@ -3035,13 +3035,17 @@ diff --git a/src/client/styles.js b/src/client/styles.js
.bt-mem-scrim{animation:bt-fade .2s ease-out}
.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}
.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}
@@ -782,6 +782,17 @@ export const CONVERSATION_CSS = `
@@ -780,9 +780,20 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)}

export const CONVERSATION_CSS = `
.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)}
.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}
-.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}
+.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}
.bt-think summary::-webkit-details-marker{display:none}
-.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
+.bt-think-caret{display:inline-block;transition:transform .15s;font-size:14px;line-height:1}
+.bt-think[open] .bt-think-caret{transform:rotate(90deg)}
+.bt-think-peek{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)}
+.bt-think-peek{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)}
+/* Opened, the body prints that same first line again, so the preview steps aside (#1636). */
+.bt-think[open] .bt-think-peek{display:none}
+.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))}
Expand All @@ -3050,9 +3054,10 @@ diff --git a/src/client/styles.js b/src/client/styles.js
+/* A person's typed line keeps its line breaks, as on the sender's side (#455): Markdown would join them. */
+.bt-person-msg .bt-bubble :is(p,li){white-space:pre-line}
+.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))}
.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
+.bt-think-text{white-space:pre-wrap;overflow-wrap:anywhere;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
.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}
.bt-event>span{padding:0 2px}
.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}
@@ -795,7 +795,6 @@ export const CONVERSATION_CSS = `
.bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.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}
Expand Down
Loading
Loading