Repository navigation
Description
本仓库回归,由 #1762(Settings 图形 fit,merged_at 2026-10-30T03:18:40Z,merge commit a6ba3280)引入:fitScale 给缩放加了 MIN_FIT = 0.5 下限,而画布是「中心线缩放 + overflow: auto」,于是当最小缩放仍让图形宽于画布时,溢出对称落在两侧——左溢出不可滚,左侧的卡和外框被裁掉且滚不回去。
What happens
core/src/client/workflows.js:
:129const MIN_FIT = 0.5:142-147fitScale(nodes, width)=Math.min(1, Math.max(MIN_FIT, (width / 2) / reach)),reach = max(|node.x| + FRAME_SIDE):213-214stage 用transform: scale(...)缩放,画布overflow: auto:139-141的注释却声称「A canvas narrower than the graph shrinks the whole plane, so the frame, the cards and the wire into them stay inside it.」
当 reach * scale > width / 2 时,图形左右同时溢出;scrollLeft 最小为 0,所以右侧可以滚、左侧永远滚不到。设计面宽 960,编辑器允许把卡拖到中心线左侧(x = -400 合法),窄窗口下就必然触发。
Reproduction
探针(esbuild + vm 载入真实模块,非 jsdom),在 main 8252bce9 上:
fitScale([{ x: -400 }], 380) -> 0.500 # 被 MIN_FIT 钳住
卡片屏幕区间 [-52.0, 32.0]
外框屏幕区间 [-64.0, 44.0]
width=300 时更差
380px 正是 #1752 报告里自己写的画布宽度;MIN_FIT 之下的部分全部落在画布左侧之外且不可滚。
Expected
A(本单采用):给 stage 加一段左留白,宽度等于 (width / 2) - reach * scale(不小于 0),让所有溢出都落到右侧、始终可滚;缩放本身与 #1762 的 MIN_FIT 行为不变。
B(备选,不在本单):让下限只在「不裁切」时生效——会改变 #1762 已落地的缩放行为,需要单独决定。
Acceptance: a rendered-value assertion, not a scale( substring
现有 ok 13 - the Settings graph fits its plane into the canvas width 抓不住这个缺陷:把 fitScale 改成 return 1(等于停掉 #1762 的缩放)它依然绿——它只断言 transform 字符串里有 scale(,而 jsdom 里 canvasWidth = 0,fitScale 恒返回 1,于是 scale(1) 也满足。只有 helper 测试 14 会转红。
所以本单必须补一条对渲染值断言的用例:给定 width(如 380)与非零 node.x(如 -400),断言实际 scale 值与左留白/可滚边界(例如 offset ≥ 0,或断言 scale >= (2 * reach) / width 的取值),并在测试名里写明「a clamped scale still keeps the left card reachable」。改坏成 return 1 时这条必须转红。
Scope
core/src/client/workflows.js:左留白(修法 A)+ 注释与实现对齐core/src/client/styles.js:仅当左留白需要一条样式core/test/workflows-page.test.js:上面那条渲染值红先(另加一条fitScale边界用例,width=380、x=-400→ 断言留白能把卡推回可滚区)- 生成面:
core/client.js与patches/ds-bot/0008-client-bundle.patch由整链generate产出(0001/0014若只有纯 hunk 头漂移,还原成 base 字节并在正文写明,应用字节不变) - 不改变
#1762的MIN_FIT语义,不动 960 设计面与拖拽坐标
Risk surface (not verified)
overflow: auto 的滚动容器里加左留白后,overflowAnchor 的默认行为可能在窗口尺寸变化时改变 scrollLeft;本单不含该交互测试,若复现请在 PR 里补一条。
Duplicate check
在 live main 上按 settings graph clipped card、workflow graph keyboard、settings workflows accessibility 搜 issue,各 0 条;该面由 #1762 于 2026-10-30 引入,close 的 #1752 是它自己的来源单(其正文只报「卡片被压」、不含窄画布裁切)。
Where fix A applies (and where it must not)
Left padding is only for the case that actually overflows: reach * scale > width / 2. When the graph already fits, the stage must render exactly as #1762 does today — no padding, no coordinate shift. Otherwise every ordinary graph moves one slot and we would be changing shipped behaviour to fix an edge case.
Regression case that must exist alongside the fix: a normal width with node.x in the middle of the design plane must produce the same scale and the same offset as main 03c8c630 (the merge commit of #1745) — record the expected numbers from that tree in the test, so a future change to the padding cannot silently shift the normal case.
Acceptance, strengthened
The assertion must be able to catch the degradation someone actually made: turning fitScale into return 1. So it cannot look for the substring scale(; it must assert numbers and the scrollable boundary:
- the concrete
scalevalue for a givenwidthandnode.x(e.g.width=380,x=-400→ the value the fix produces, not just "> 0"); - the left padding / offset floor:
offset >= 0in the overflowing case, and0in the normal case; - a scrollable boundary, e.g.
scrollWidth - clientWidth(left overflow unreachable today means the leftmost card is at a negative screen x whilescrollLeftcannot go below 0); - the width must be injected explicitly (a prop or a
getBoundingClientRectstub). jsdom reports0, so any assertion that relies on the measured width is vacuous — that is exactly how#1762'sok 13passes withscale(1).
A test that still passes with fitScale = () => 1 does not count as coverage for this issue.
Attribution
该退化由今天合入的 #1762(fitScale + overflow:auto,merged_at 2026-10-30T03:18:40Z,merge commit a6ba3280)带来:其合并时新增的用例没有对渲染值断言(详情见 B-10)。我们自己发现、自己承接,不回头改 #1762 的评论或正文——向前修。
Side audit (bounded, not a survey)
既然已经确认「jsdom 里宽/高为 0 会让渲染断言空转」,同批排查 core/test/ 里依赖 clientWidth / canvasWidth / getBoundingClientRect 的用例:只要数量和不超过 10 条的清单(文件:行 + 一句“在零尺寸下它断言什么”),并入本单正文或 B-10,不逐条修。这条由 FlyBot Product 做,不改任何测试。
Side audit result (FlyBot Product, 2026-10-30)
Zero-size assertions in core/test/ — the count is 1, and it is the one this issue already owns:
core/test/workflows-page.test.js:378-383(ok 13) — jsdomgetBoundingClientRect().widthis0, so:164 measure()never sets a value,canvasWidthstays0,workflows.js:144returns1on!(width > 0), the renderedtransformis alwaysscale(1), and the assertion is only the substring/scale\(/. Deleting the whole measurement effect:161-172still leaves it green.
Registered, different mechanism — do not fix here:
core/test/new-chat.test.js:90— asserts the source string containsstyle: { left: paneLeft };paneLeftcomes fromoverlay-hooks.js:28 column.getBoundingClientRect().right, which is0under jsdom, so replacing the measurement with a constant0still passes.
Checked and explicitly not vacuous (do not list them as the same class): read-page.test.js:56-58 (stubs the prototype with width:100), bot-browsers.test.js:69,99 (BOX = {left:600,top:90,width:500,height:700}), exchange-read.test.js:110 (10×10 fake rect), thinking-row.test.js:182-186 (real JSDOM + factory CSS via getComputedStyle), workflows-page.test.js:385-399 (calls the pure fitScale with an explicit width=380 — this is the one leg that turns red under fitScale => 1), :356-376 (asserts authored coordinates, not measurements).
Side note worth keeping: nothing in core/test/ asserts clientWidth / clientHeight / offsetWidth / scrollWidth directly (the only hit is a comment at thinking-row.test.js:183), and the layout-heavy modules — browser-pane.js (11 reads), plugins/workflow/client.js (13), motion.js (5) — have no test under core/test/ that imports them. One vacuous assertion, but the layout surface is mostly unwatched; this issue does not widen to cover it.
Activity
Settings graph: a clamped fit scale still pushes the left card out of…
Settings graph: a clamped fit scale still pushes the left card out of…