From e76c222626f2d4320edb475ec41b071c60148d14 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Wed, 28 Oct 2026 08:22:49 +0800 Subject: [PATCH] Render every Workflow stage chat line in full CHAT_PREVIEW (240) and chatPreview() clipped every stage chat message, so the owner's brief and each narrate line that quotes a Bot reply lost its tail and the rest was unreachable (regression from #1456). Render message.text in the system, user and Bot bubbles again, keep #1473's title and word-wrap, and keep the 240-character cap on taskSummary, which is the summary surface. Fixes #1460 Docs-not-needed: display-only restore of text that the stage chat already receives; docs/subsystems/workflow.md describes the message flow, not the truncation, and no flow or API changes. --- changelog.d/1460.fixed.md | 1 + plugins/workflow/client.js | 14 ++---- plugins/workflow/test/client.test.js | 70 ++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 changelog.d/1460.fixed.md diff --git a/changelog.d/1460.fixed.md b/changelog.d/1460.fixed.md new file mode 100644 index 000000000..f83113885 --- /dev/null +++ b/changelog.d/1460.fixed.md @@ -0,0 +1 @@ +The Workflow stage chat renders a message line in full instead of clipping it at 240 characters; the 240-character preview is now used by summaries only, so an owner brief and a Bot reply are readable to the end. ([#1460](https://github.com/ThinkFlowLab/FlyBot/issues/1460)) diff --git a/plugins/workflow/client.js b/plugins/workflow/client.js index 7e678c82e..fe7aa0254 100644 --- a/plugins/workflow/client.js +++ b/plugins/workflow/client.js @@ -190,7 +190,6 @@ window.__ModuleLoader__.load({ if (text.startsWith('task-')) return `#${text.slice(-6)}` return text } - const CHAT_PREVIEW = 240 function taskSummary(task, run) { if (run?.taskSummary) return run.taskSummary const id = shortId(task?.id ?? '') @@ -206,11 +205,6 @@ window.__ModuleLoader__.load({ const short = cleanTitle.length > room ? `${cleanTitle.slice(0, room - 1)}…` : cleanTitle return `${id} · ${short}` } - function chatPreview(text) { - const line = String(text ?? '').replace(/\s+/g, ' ').trim() - if (line.length <= CHAT_PREVIEW) return line - return `${line.slice(0, CHAT_PREVIEW - 1)}…` - } function sidebarPreview(item) { const busy = (item.activeCount ?? 0) > 0 && item.run?.live === true if (busy) return item.preview || tr('Waiting for a task') @@ -425,14 +419,14 @@ window.__ModuleLoader__.load({ function Bubble({ message, workflow, roster }) { if (message.kind === 'time') return h('div', { className: 'wf-ts' }, message.text) - if (message.kind === 'system') return h('div', { className: `wf-sys ${message.tone ?? ''}`, title: message.text }, chatPreview(message.text)) - if (message.kind === 'user') return h('div', { className: 'wf-user', title: message.text }, chatPreview(message.text)) + if (message.kind === 'system') return h('div', { className: `wf-sys ${message.tone ?? ''}`, title: message.text }, message.text) + if (message.kind === 'user') return h('div', { className: 'wf-user', title: message.text }, message.text) const node = nodeOf(workflow, message.botId) ?? { id: message.botId, name: message.botId } return h('div', { className: 'wf-msg' }, h(Mark, { node, roster, size: 28 }), h('div', null, h('div', { className: 'wf-who' }, node.name, message.step ? h('span', { className: 'wf-step' }, tr('Step {step}', { step: message.step })) : null), - h('div', { className: 'wf-bub', title: message.text }, chatPreview(message.text), + h('div', { className: 'wf-bub', title: message.text }, message.text, message.attachments?.length ? h('div', { className: 'wf-att' }, message.attachments.map(name => h('span', { key: name }, name))) : null))) } @@ -1534,7 +1528,7 @@ window.__ModuleLoader__.load({ return { name: 'flybot-workflow', inject: ['flybot'], apply, Section, WorkflowSettings, - tr, columnOf, shortId, stageMessages, taskSummary, chatPreview, wirePath, WIRE_SPAN, NODE_WIRE_OUT, + tr, columnOf, shortId, stageMessages, taskSummary, wirePath, WIRE_SPAN, NODE_WIRE_OUT, runHistoryRows, liveStageRuns, resolveFocusedRun, parallelFrameStyle, NODE_W, botPaletteSubtitle, botTint, } diff --git a/plugins/workflow/test/client.test.js b/plugins/workflow/test/client.test.js index 5cf701538..8dbd6b92a 100644 --- a/plugins/workflow/test/client.test.js +++ b/plugins/workflow/test/client.test.js @@ -525,6 +525,76 @@ test('stage chat hides workflow node Bot lines but keeps owner Bots and system l assert.deepEqual(visible.map(message => message.id), ['1', '3', '4']) }) +async function stageChatText(messages, roster) { + const graph = delivery() + graph.messages = messages + const call = async (name) => { + if (name === 'workflow/state') return { workflows: [graph], templates: [{ id: 'delivery', name: '发布交付流水线' }] } + return { ok: true, workflows: [graph] } + } + const view = mount(client.Section, { roster, call, actions: {} }) + await settle() + findAll(view.tree, node => node.type === 'button' && textOf(node).includes('发布交付流水线'))[0].props.onClick() + await settle() + const text = textOf(view.tree) + view.unmount() + return text +} + +function longLine(kind) { + const tail = kind === 'user' ? '让发布说明覆盖四个小节,并保留每条验收命令的原文。' + : kind === 'system' ? '系统提示的每一行都要完整显示,不能丢尾巴。' + : '这是 Chief 的原话,一个字都不能丢。' + const line = `${kind.toUpperCase()} ${tail.repeat(20)}END` + assert.ok(line.length > 240, `the fixture must exceed the 240-character preview cap, got ${line.length}`) + return line +} + +function assertFull(text, line) { + assert.equal(text.includes(line), true, `the stage chat dropped part of a ${line.length}-character line`) + assert.equal(text.includes(`${line.slice(0, 239)}…`), false, 'the stage chat still clipped a line at 240 characters') +} + +test('a long user line in the stage chat renders in full', async () => { + const long = longLine('user') + const text = await stageChatText([{ id: 'm1', kind: 'user', text: long }], { bots: [] }) + assertFull(text, long) +}) + +test('a long system line in the stage chat renders in full', async () => { + const long = longLine('system') + const text = await stageChatText([{ id: 'm1', kind: 'system', text: long }], { bots: [] }) + assertFull(text, long) +}) + +test('a long roster Bot line in the stage chat renders in full', async () => { + const long = longLine('bot') + const roster = { bots: [], byId: { 'bot-chief': { id: 'bot-chief', name: 'Chief' } } } + const text = await stageChatText([{ id: 'm1', kind: 'bot', botId: 'bot-chief', text: long }], roster) + assertFull(text, long) +}) + +test('the stage strip still clips a long task title as a summary', async () => { + const graph = delivery() + const longTitle = '让发布说明覆盖四个小节并保留每条验收命令的原文'.repeat(12) + graph.run.task = { id: '#1460', title: longTitle } + delete graph.run.taskSummary + const call = async (name) => { + if (name === 'workflow/state') return { workflows: [graph], templates: [{ id: 'delivery', name: '发布交付流水线' }] } + return { ok: true, workflows: [graph] } + } + const view = mount(client.Section, { roster: { bots: [] }, call, actions: {} }) + await settle() + findAll(view.tree, node => node.type === 'button' && textOf(node).includes('发布交付流水线'))[0].props.onClick() + await settle() + const text = textOf(view.tree) + const summary = client.taskSummary({ id: '#1460', title: longTitle }) + assert.equal(text.includes(summary), true, 'the stage strip lost its task summary') + assert.equal(text.includes(longTitle), false, 'the stage strip rendered a full task title instead of a summary') + assert.equal(summary.endsWith('…'), true) + view.unmount() +}) + test('the hand-a-task modal lists live Tasks board rows only', async () => { const graph = delivery() const call = async (name) => {