Skip to content

fix(gui): stop the log panel from jittering as rows scroll in - #3290

Merged
lidge-jun merged 3 commits into
devfrom
codex/260903-log-panel-jitter
Sep 2, 2026
Merged

fix(gui): stop the log panel from jittering as rows scroll in#3290
lidge-jun merged 3 commits into
devfrom
codex/260903-log-panel-jitter

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • The virtualized log table now uses table-layout: fixed with an explicit ten-column <colgroup>, so column widths no longer depend on which rows happen to be mounted.
  • estimateSize moves from 44px to the observed mean row height, and getItemKey keys measurements to the request rather than the row index, so a prepend does not shift them.
  • .logs-table-wrap disables scroll anchoring and reserves the scrollbar gutter.

Root cause: rows were measured dynamically from a 44px estimate while the multiline cells are roughly twice that, on a table left in automatic layout. Every changed mounted-row subset recomputed intrinsic column widths, model wrapping then changed row heights, and that fed another virtualizer measurement. Polling and scrollbar appearance amplified the loop rather than causing it.

UI change

Screenshot of the Logs & Debug panel, captured live against a running proxy with real request rows.

Before — dev at 3d3c4fe, automatic table layout:

Logs panel before

After — this branch, table-layout: fixed with the ten-column schema:

Logs panel after

The rendered content is deliberately the same; what changes is that column boundaries stop moving as rows scroll in. Verified in the running dashboard rather than only in tests — with this branch built and served on port 10877, the live DOM reports:

{"rows":26,"layout":"fixed","cols":10}

On dev the same query returns no table-layout declaration and no <colgroup> at all.

Verification

  • Red-first: bun test ./gui/tests/viewport-scroll-caps.test.ts failed with property not found: table.logs-table { table-layout }, and ./gui/tests/logs-auto-refresh.test.tsx failed with expect(colgroup).not.toBeNull() receiving null.
  • After the fix: 6 pass / 0 fail and 10 pass / 0 fail respectively.
  • bun run lint:gui — passed. bun run typecheck — passed. GUI production build — passed.
  • Live browser verification as shown above.
  • Per maintainer instruction for this campaign, the repository-wide suite was not run locally; CI is the full-suite gate.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed. This is a rendering fix with no user-facing contract change.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. Dashboard rendering only.

Closes #3152

Stacked on #3289; retarget to dev once that lands.

jun added 3 commits September 3, 2026 02:40
Roadmap unit for the September bug drawdown on dev: 000_plan.md locks a
ten-work-phase map from live gh evidence, and 010-090 carry per-item root
cause, file:line fix maps, and RED-before-fix assertions sourced from six
parallel read-only investigators.

Four decade docs cover the open bug-labelled PRs as adoption phases with
immutable base/head anchors. Five cover the bug-labelled issues, three of
which terminate as NEEDS_HUMAN with the ruled-out causes recorded rather
than a speculative patch.
A completed response mutates the continuation cache and calls schedulePersist,
and the process-level timer already coalesces those triggers. Under concurrent
completions, though, the revision kept changing during the async write, so the
bounded-retry loop rewrote the whole snapshot up to four times per background
tick. The existing test codified that as the contract.

Ordinary background persistence now gets a single attempt. When the snapshot is
unstable it schedules the existing delayed follow-up instead of rewriting
immediately, so an unstable revision costs one extra debounce interval rather
than three extra full atomic rewrites. Graceful shutdown keeps the bounded retry:
that path drains requests first and has to land.

Closes #3141
The virtualized log table measured rows dynamically from a 44px estimate while
its multiline cells are roughly twice that, and the table itself was on
automatic layout. Every changed mounted-row subset therefore recomputed
intrinsic column widths, model wrapping changed row heights, and that fed
another virtualizer measurement.

Fixed layout with an explicit ten-column schema removes the feedback loop:
column widths no longer depend on which rows happen to be mounted. The estimate
moves to the observed mean, getItemKey keys measurements to the request rather
than the index so a prepend does not shift them, and the wrapper disables scroll
anchoring and reserves the scrollbar gutter.

Closes #3152
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 2, 2026 17:48
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Team

Run ID: 4c7f1f12-d1fd-48ab-9b79-67798c03d321

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed.

@github-actions
github-actions Bot marked this pull request as draft September 2, 2026 17:49
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T17:53:04.481596Z 05f3ca7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05f3ca7998

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread gui/src/styles.css

.logs-table-wrap {
overflow-y: auto;
overflow-anchor: none;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the reader's position when prepending logs

When auto-refresh returns a new request while the user is scrolled below the newest rows, Logs.tsx reverses the source array so that request is inserted above the viewport, but it contains no corresponding scrollTop adjustment. Disabling scroll anchoring here also prevents the browser from keeping the previously visible row in place, so the displayed records shift on every two-second refresh during active traffic. Leave scroll anchoring enabled or explicitly compensate for the prepended row height, and add a scrolled-refresh regression test rather than only asserting that this declaration exists.

AGENTS.md reference: gui/AGENTS.md:L46-L50

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 62 / 80

이 PR은 대시보드 로그 패널이 스크롤·폴링할 때마다 칸 너비가 흔들리던 문제(#3152)를 고칩니다. 지금 dev HEAD(3d3c4fe26, #3286 Gemini 3.8 Flash 롤아웃 직후)의 gui/src/pages/Logs.tsx를 보면 useVirtualizerestimateSize가 아직 44px이고, table.logs-table에는 table-layout이 없습니다. 멀티라인 칸이 실제로는 그 두 배쯤인데, 자동 레이아웃 테이블은 “지금 화면에 올라온 행 집합”으로 칸 너비를 다시 잡습니다. 그러면 모델 칸 줄바꿈이 바뀌고, 행 높이가 바뀌고, 가상 스크롤러가 다시 재고하는 피드백 루프가 돕니다. 이 PR은 그 루프의 원인(가변 칸 너비 + 낮은 추정 높이 + 인덱스 기반 측정 키)을 한꺼번에 끊습니다.

구체적으로는 세 갈래입니다. (1) gui/src/styles.css에서 table.logs-tabletable-layout: fixed를 주고, 열 열 너비를 퍼센트로 고정한 <colgroup>(합 100%)을 Logs.tsx에 넣습니다. 이제 어떤 행이 마운트돼도 칸 경계가 안 움직입니다. (2) estimateSize를 92로 올리고, getItemKey를 요청(requestId 또는 timestamp/model/provider) 기준으로 바꿔서, 위에 새 로그가 붙어도 측정값이 인덱스와 함께 밀리지 않게 합니다. 행 keyvirtualRow.key로 맞춥니다. (3) .logs-table-wrapoverflow-anchor: nonescrollbar-gutter: stable을 넣어 스크롤 앵커링·스크롤바 등장으로 생기는 부수 흔들림을 막습니다. 테스트는 viewport-scroll-caps.test.ts(CSS 고정 레이아웃·퍼센트 합·래퍼 속성)와 logs-auto-refresh.test.tsx(DOM에 열 개 col 순서)로 red-first 후 통과했다고 적혀 있고, lint/typecheck/GUI 빌드도 통과했다고 합니다. 캠페인 지시대로 전체 스위트는 CI에 맡긴 상태입니다.

현재 dev와의 관계는 깔끔합니다. 베이스는 codex/260903-bug-drawdown-plan(#3289 스택)이지만, #3289가 건드리는 파일은 src/responses/state.ts와 디스크 사용 문서·플랜뿐이고 이 PR의 GUI 네 파일과 겹치지 않습니다. 그래서 “코드 의존”이라기보다 bug-drawdown 캠페인 순서 스택에 가깝습니다. #3286이 방금 dev에 들어온 직후라 Gemini/모델 카탈로그 작업과도 충돌하지 않습니다. 이슈 #3152는 아직 OPEN이고 needs-info/gui/bug가 붙어 있는데, 이 PR이 재현·원인·수정·검증을 한 묶음으로 채워 주므로 머지 시 Close가 맞습니다.

라인 526 - dev의 estimateSize 44 → 이 PR의 92는 관측 평균으로 타당하지만, 여전히 상수라서 아주 긴 모델명·다줄 status가 많은 페이지에서는 첫 스크롤 전에 한 번 더 측정 보정이 필요합니다. measureElement가 보정하지만, 초기 jump가 남는지 실제 폴링 ON 상태에서 한 번 눈으로 확인하는 편이 좋습니다.
라인 getItemKey 폴백 - requestId가 없을 때 ${timestamp}:${model}:${provider}만 쓰므로, 동일 시각·동일 모델·동일 프로바이더의 두 행이 있으면 키가 충돌할 수 있습니다. 드물지만 가능하면 기존처럼 index나 다른 고유 조각을 폴백에 남기는 편이 안전합니다.
경로 gui/src/styles.css .log-col-rate / .log-col-cost - 여전히 min-width: 7ch / 10ch가 남아 있습니다. table-layout: fixed + 퍼센트 col 아래에서는 보통 col width가 이기지만, 좁은 뷰포트에서 ch 최소폭과 %가 어떻게 충돌하는지는 한 번만 확인하면 좋습니다.
경로 테스트 - 지오메트리/DOM 스키마 assertion이지 “스크롤 중 칸이 안 흔들린다”는 픽셀 테스트는 아닙니다. PR 본문도 인정한 한계라 차단 사유는 아니고, CI + 수동 스크롤로 충분합니다.

메인테이너의 판단이 필요한 지점

  • #3289가 아직 OPEN인데, 파일 겹침이 없으니 #3290을 지금 dev로 리타깃해서 먼저 넣을지, 캠페인 문서대로 fix(responses): stop the background write storm on responses-state.json #3289 머지 후 리타깃할지.
  • #3152의 needs-info를 머지와 함께 정리할지(이 PR이 이미 스펙·재현을 채움).
  • estimateSize 92를 상수로 둘지, 나중에 측정 평균을 코드 주석/상수명으로만 남길지.

너의 추천
#3289와 파일 충돌이 없으므로, 캠페인 순서를 굳이 지킬 이유가 없으면 base를 dev로 리타깃한 뒤 CI 그린 확인 후 머지하고 #3152를 닫으세요. 캠페인 순서를 유지하고 싶다면 #3289 머지 → 이 PR 리타깃 → 머지 순이면 됩니다. getItemKey 폴백 충돌만 신경 쓰이면 되고, types/config 스플릿과 무관한 독립 GUI 버그 픽스입니다.

이 댓글은 grok-bot이 작성했습니다

@github-actions
github-actions Bot marked this pull request as ready for review September 2, 2026 17:55
Base automatically changed from codex/260903-bug-drawdown-plan to dev September 2, 2026 18:04
@lidge-jun
lidge-jun merged commit fc08fc2 into dev Sep 2, 2026
29 of 30 checks passed
@lidge-jun
lidge-jun deleted the codex/260903-log-panel-jitter branch September 2, 2026 18:06
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 68 / 80

이 PR은 대시보드 로그 패널이 스크롤하거나 2초 폴링으로 새 행이 붙을 때마다 칸 너비가 흔들리던 문제(#3152)를 고칩니다. 지금 막 갱신한 dev HEAD는 34c9e9802이고, 바로 직전 머지가 #3289(responses-state.json 백그라운드 쓰기 폭주)입니다. 그 HEAD의 gui/src/pages/Logs.tsx를 보면 useVirtualizerestimateSize가 아직 44px이고, table.logs-table에는 table-layout이 없습니다. 멀티라인 칸(날짜·모델·effort·status)은 실제로 그 두 배쯤인데, 자동 레이아웃 테이블은 “지금 화면에 올라온 행 집합”으로 칸 너비를 다시 잡습니다. 그러면 모델 칸 줄바꿈이 바뀌고, 행 높이가 바뀌고, 가상 스크롤러가 다시 재고하는 피드백 루프가 돕니다. 폴링과 스크롤바 등장·사라짐은 그 루프를 증폭할 뿐, 원인은 아닙니다.

고치는 방법은 세 갈래입니다. 첫째, gui/src/styles.csstable.logs-tabletable-layout: fixed를 넣고, Logs.tsx에 열 개 <col>을 가진 <colgroup>을 둡니다. 너비는 12+9+7+8+15+9+13+8+11+8 = 100%라서, 어떤 행이 마운트돼도 칸 경계가 안 움직입니다. 둘째, estimateSize를 관측 평균인 92로 올리고, getItemKey를 요청 식별자(requestId, 없으면 timestamp/model/provider)로 바꿉니다. 위에 새 로그가 붙어도 측정값이 인덱스와 함께 밀리지 않습니다. 행 keyvirtualRow.key로 맞춰 두었습니다. 셋째, .logs-table-wrapoverflow-anchor: nonescrollbar-gutter: stable을 넣어 브라우저 스크롤 앵커링과 스크롤바 폭으로 생기는 부수 흔들림을 막습니다. 테스트는 gui/tests/viewport-scroll-caps.test.ts(고정 레이아웃·퍼센트 합 100·래퍼 속성)와 gui/tests/logs-auto-refresh.test.tsx(DOM에 열 개 col 순서)로 red-first 후 통과했다고 적혀 있고, lint/typecheck/GUI 빌드도 통과했다고 합니다. 캠페인 지시대로 전체 스위트는 CI에 맡긴 상태입니다.

이 리뷰 턴에서 dev938c0136a(#3246 write_stdin 브리지)에서 34c9e9802(#3289 스쿼시)로 올라갔습니다. 그래서 이전 grok-bot 댓글(62/80)이 말하던 “#3289가 아직 열려 있으니 기다리거나 리타깃하라”는 조건은 이미 끝났습니다. 이 브랜치가 들고 있는 #3289 파일(src/responses/state.ts, 테스트, 디스크 사용 문서, devlog/_plan/260903_bug_drawdown_bcda/)은 지금 dev 내용과 바이트가 같습니다. 진짜로 새로 들어오는 것은 GUI 네 파일뿐입니다. 다만 GitHub 삼점 디프는 머지 베이스가 스쿼시 이전(529639a57)이라 플랜·state.ts까지 17개로 보여 줍니다. 로컬 머지 트리는 충돌 없이 합쳐집니다. 파일 목록을 GUI만 남기려면 이 브랜치를 지금 dev 위에 리베이스하는 편이 더 안전합니다. 리베이스 없이 스쿼시해도 내용은 GUI만 남을 가능성이 높습니다. #3254/#3256/#3286/#3246은 이미 dev에 있고 이 PR의 고유 커밋과 겹치지 않습니다.

CI는 게이트·hygiene·테스트 샤드 4개·macos·npm-global까지 이 리뷰 시점에 전부 통과입니다. 이슈 #3152는 아직 OPEN이고 needs-info/gui/bug가 붙어 있습니다. 이 PR이 재현·원인·수정·라이브 DOM 확인(layout:fixed, col 10개)까지 채워 주므로 머지하면 Close가 맞습니다. needs-info는 이 PR이 스펙을 채웠으니 같이 떼면 됩니다. 캠페인 플랜 wp8이 바로 이 이슈입니다. types.ts/config.ts 분할과는 무관한 독립 GUI 버그 픽스라 닫지 말고 랜딩이 맞습니다.

트레이드오프는 작습니다. 고정 퍼센트 칸은 아주 긴 모델명에서 줄바꿈이 더 자주 생길 수 있지만, 그게 바로 흔들림을 없애는 대가입니다. 테스트는 CSS/DOM 스키마를 잠글 뿐 “스크롤 중 칸이 안 흔들린다”는 픽셀 테스트는 아닙니다. PR 본문도 라이브 대시보드로 확인했다고 했고, 그 한계는 차단 사유가 아닙니다. 참고로 #3289 본문의 Closes #3141은 기본 브랜치가 dev가 아니라서 이슈가 아직 열려 있습니다. 그건 이 PR과 별개로 랜딩 주석을 달고 닫으면 됩니다.

라인 526 - estimateSize 44→92는 관측 평균으로 타당하지만 상수라서, 아주 긴 모델명·다줄 status가 많은 페이지에서는 첫 스크롤 전에 measureElement 보정이 한 번 더 필요합니다.
라인 528 - requestId가 없을 때 폴백 키가 timestamp/model/provider 뿐이라, 같은 시각·같은 모델·같은 프로바이더의 두 행이 키 충돌할 수 있습니다. 예전 키는 인덱스를 넣어서 충돌이 없었습니다.
경로 gui/src/styles.css .log-col-rate / .log-col-cost - min-width: 7ch / 10ch가 남아 있습니다. table-layout: fixed + 퍼센트 col 아래에서는 보통 col width가 이기지만, 좁은 뷰포트에서 ch 최소폭과 %가 어떻게 만나는지는 한 번만 보면 됩니다.
경로 테스트 viewport-scroll-caps / logs-auto-refresh - 지오메트리·colgroup 순서 assertion이지 지터 픽셀 테스트는 아닙니다. CI + 수동 스크롤로 충분합니다.
경로 삼점 디프 - GitHub 파일 목록에 이미 dev에 있는 #3289 플랜/state.ts가 남아 있습니다. 리베이스하면 GUI 네 파일만 남습니다.

메인테이너의 판단이 필요한 지점

  • 지금 dev 위에 리베이스한 뒤 스쿼시할지, 충돌 없는 머지 트리를 믿고 그대로 스쿼시할지
  • #3152의 needs-info를 머지와 함께 떼고 이슈를 닫을지
  • #3289로 이미 고친 #3141을 이 턴에서 landed-via-maintainer로 닫을지

너의 추천
이 브랜치를 현재 dev(34c9e9802) 위에 리베이스해서 #3289 커밋을 걷어낸 다음, CI가 다시 초록이면 스쿼시 머지하고 #3152를 닫아라. getItemKey 폴백에 인덱스를 남기면 더 안전하지만 머지를 막을 정도는 아니다. types/config 분할과 무관하니 닫지 말고 랜딩이 맞다.

이 댓글은 grok-bot이 작성했습니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant