From 773af6f3f7f8c06c8f39b844fbb9d42c2091c6ea Mon Sep 17 00:00:00 2001 From: Day Dreamer <87640074+HugoXK@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:04:16 +0800 Subject: [PATCH 1/2] test(chat): cover an opened thinking row repeating its first line The folded row names its reasoning in the summary (#1624); opened, the body prints that same line again. Assert the open state hides the peek and that the shipped client bundle carries the rule, so #1636 cannot land half-generated. --- core/test/thinking-row.test.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/core/test/thinking-row.test.js b/core/test/thinking-row.test.js index e0d1ccf25..ef3971cc4 100644 --- a/core/test/thinking-row.test.js +++ b/core/test/thinking-row.test.js @@ -1,5 +1,6 @@ import { test } from 'node:test' import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { runInNewContext } from 'node:vm' import { build } from 'esbuild' @@ -8,7 +9,7 @@ import { build } from 'esbuild' // React whose components run as plain functions, as in tool-cells.test.js. const { outputFiles } = await build({ stdin: { - contents: "export { AssistantCell } from './src/client/cells.js'", + contents: "export { AssistantCell } from './src/client/cells.js';\nexport { CONVERSATION_CSS } from './src/client/styles.js'", resolveDir: fileURLToPath(new URL('..', import.meta.url)), }, bundle: true, format: 'cjs', platform: 'browser', write: false, logLevel: 'silent', @@ -36,7 +37,7 @@ runInNewContext(outputFiles[0].text, { throw new Error(`Unexpected platform module ${name}`) }, }) -const { AssistantCell } = module.exports +const { AssistantCell, CONVERSATION_CSS } = module.exports const expand = (node) => { if (node === null || node === undefined || typeof node === 'boolean') return null @@ -120,3 +121,18 @@ test('#1624: reasoning that is only whitespace draws no row at all', () => { const blocks = [think(' \n\t '), { kind: 'text', text: 'Answer' }] assert.equal(thinkingRow(render(step(1, blocks), [step(1, blocks)])), undefined) }) + +test('#1636: an opened row hides its preview, so its first line is not printed twice', () => { + // The folded row names its reasoning inside the summary (#1624). Opened, the body prints that + // same line again, so the open state drops the preview instead of repeating it. + const openRules = [...CONVERSATION_CSS.matchAll(/\.bt-think\[open\][^{]*\{[^}]*\}/g)].map(match => match[0]) + assert.ok( + openRules.some(rule => /\.bt-think-peek/.test(rule) && /display:\s*none/.test(rule)), + `the opened row hides .bt-think-peek; open-state rules: ${openRules.join(' ') || 'none'}`, + ) + // The app loads the committed bundle, not this source, so the bundle carries the rule too. + assert.match( + readFileSync(new URL('../client.js', import.meta.url), 'utf8'), + /\.bt-think\[open\] \.bt-think-peek\{display:none\}/, + ) +}) From 5d32f0e26a4c3b0a0e9c7870dda81672e732190d Mon Sep 17 00:00:00 2001 From: Day Dreamer <87640074+HugoXK@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:13:10 +0800 Subject: [PATCH 2/2] fix(chat): an opened thinking row hides its preview line The folded row names a line of its reasoning inside the summary (#1624) and the body below prints the whole text; with no open-state rule the truncated copy stayed above that same line, so an opened row read the sentence twice. One rule hides the preview once the details element is open, and the closed state renders exactly as before. core/client.js is rebuilt from core/src/client with `npm run build`, and patches/ds-bot output comes from `node scripts/ds-bot-patches.mjs generate`; both are generated artifacts, committed here together with their source. Docs-not-needed: one CSS rule in CONVERSATION_CSS hides the folded row's preview line while the row is opened; docs/subsystems/core-client.md describes the client's structure, not the thinking row's markup, which it did not cover before either. --- .../1636-thinking-row-duplicate-line.fixed.md | 1 + core/client.js | 2 ++ core/src/client/styles.js | 2 ++ patches/ds-bot/0003-flybot-api-client.patch | 4 +++- patches/ds-bot/0008-client-bundle.patch | 4 +++- .../ds-bot/files/test/thinking-row.test.js | 20 +++++++++++++++++-- 6 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 changelog.d/1636-thinking-row-duplicate-line.fixed.md diff --git a/changelog.d/1636-thinking-row-duplicate-line.fixed.md b/changelog.d/1636-thinking-row-duplicate-line.fixed.md new file mode 100644 index 000000000..92ea94050 --- /dev/null +++ b/changelog.d/1636-thinking-row-duplicate-line.fixed.md @@ -0,0 +1 @@ +An opened thinking row no longer prints its first line twice: the folded row still names a line of its reasoning next to 思考过程, and once you open it that preview steps aside so only the full text below shows the sentence ([#1636](https://github.com/ThinkFlowLab/FlyBot/issues/1636)). diff --git a/core/client.js b/core/client.js index d30bc5c69..3414e1f00 100644 --- a/core/client.js +++ b/core/client.js @@ -4673,6 +4673,8 @@ var CONVERSATION_CSS = ` .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)} +/* 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))} .bt-person-initial{color:#fff;font-weight:600;line-height:1;user-select:none} .bt-person-msg .bt-group-name{color:var(--bt-ink-2,rgba(20,20,20,.7))} diff --git a/core/src/client/styles.js b/core/src/client/styles.js index 9b0edec0e..9611d9a76 100644 --- a/core/src/client/styles.js +++ b/core/src/client/styles.js @@ -889,6 +889,8 @@ export const CONVERSATION_CSS = ` .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)} +/* 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))} .bt-person-initial{color:#fff;font-weight:600;line-height:1;user-select:none} .bt-person-msg .bt-group-name{color:var(--bt-ink-2,rgba(20,20,20,.7))} diff --git a/patches/ds-bot/0003-flybot-api-client.patch b/patches/ds-bot/0003-flybot-api-client.patch index a72056f0c..3f1a67494 100644 --- a/patches/ds-bot/0003-flybot-api-client.patch +++ b/patches/ds-bot/0003-flybot-api-client.patch @@ -3004,13 +3004,15 @@ 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,15 @@ export const CONVERSATION_CSS = ` +@@ -782,6 +782,17 @@ 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::-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)} ++/* 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))} +.bt-person-initial{color:#fff;font-weight:600;line-height:1;user-select:none} +.bt-person-msg .bt-group-name{color:var(--bt-ink-2,rgba(20,20,20,.7))} diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index 68d0ad7f5..ba448a312 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -3533,13 +3533,15 @@ diff --git a/client.js b/client.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} -@@ -2599,6 +2599,15 @@ var CONVERSATION_CSS = ` +@@ -2599,6 +2599,17 @@ 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::-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)} ++/* 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))} +.bt-person-initial{color:#fff;font-weight:600;line-height:1;user-select:none} +.bt-person-msg .bt-group-name{color:var(--bt-ink-2,rgba(20,20,20,.7))} diff --git a/patches/ds-bot/files/test/thinking-row.test.js b/patches/ds-bot/files/test/thinking-row.test.js index e0d1ccf25..ef3971cc4 100644 --- a/patches/ds-bot/files/test/thinking-row.test.js +++ b/patches/ds-bot/files/test/thinking-row.test.js @@ -1,5 +1,6 @@ import { test } from 'node:test' import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { runInNewContext } from 'node:vm' import { build } from 'esbuild' @@ -8,7 +9,7 @@ import { build } from 'esbuild' // React whose components run as plain functions, as in tool-cells.test.js. const { outputFiles } = await build({ stdin: { - contents: "export { AssistantCell } from './src/client/cells.js'", + contents: "export { AssistantCell } from './src/client/cells.js';\nexport { CONVERSATION_CSS } from './src/client/styles.js'", resolveDir: fileURLToPath(new URL('..', import.meta.url)), }, bundle: true, format: 'cjs', platform: 'browser', write: false, logLevel: 'silent', @@ -36,7 +37,7 @@ runInNewContext(outputFiles[0].text, { throw new Error(`Unexpected platform module ${name}`) }, }) -const { AssistantCell } = module.exports +const { AssistantCell, CONVERSATION_CSS } = module.exports const expand = (node) => { if (node === null || node === undefined || typeof node === 'boolean') return null @@ -120,3 +121,18 @@ test('#1624: reasoning that is only whitespace draws no row at all', () => { const blocks = [think(' \n\t '), { kind: 'text', text: 'Answer' }] assert.equal(thinkingRow(render(step(1, blocks), [step(1, blocks)])), undefined) }) + +test('#1636: an opened row hides its preview, so its first line is not printed twice', () => { + // The folded row names its reasoning inside the summary (#1624). Opened, the body prints that + // same line again, so the open state drops the preview instead of repeating it. + const openRules = [...CONVERSATION_CSS.matchAll(/\.bt-think\[open\][^{]*\{[^}]*\}/g)].map(match => match[0]) + assert.ok( + openRules.some(rule => /\.bt-think-peek/.test(rule) && /display:\s*none/.test(rule)), + `the opened row hides .bt-think-peek; open-state rules: ${openRules.join(' ') || 'none'}`, + ) + // The app loads the committed bundle, not this source, so the bundle carries the rule too. + assert.match( + readFileSync(new URL('../client.js', import.meta.url), 'utf8'), + /\.bt-think\[open\] \.bt-think-peek\{display:none\}/, + ) +})