Skip to content

fix(chat): the Default chat width's ceiling is 60rem, never wider than Wide at another root font size - #474

Merged
devswha merged 8 commits into
mainfrom
fix/chat-width-ceiling
Oct 5, 2026
Merged

devswha merged 8 commits into
mainfrom
fix/chat-width-ceiling

Conversation

@devswha

@devswha devswha commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Change

승인된 채팅 모드 재설계의 한 조각입니다(앞서 #456 ~ #468이 머지되었고, 이번이 마지막 묶음입니다). 이 PR에 해당하는 목업 규칙은 없습니다. #463(960px 채팅 레인과 Chat width 설정) 리뷰에서 남은 후속 수정입니다.

Chat width가 Default일 때 레인의 최대 폭이 고정 960px이었습니다. Wide는 72rem이라서, 브라우저 글꼴 크기가 16px이 아니면 Default가 Wide보다 넓어질 수 있었습니다(13px 루트에서 Default 960px, Wide 936px).

이제 Default의 최대 폭은 60rem입니다.

  • 16px 루트: 지금과 같습니다(820px ~ 960px).
  • 20px 루트: 큰 pane에서 Default가 1200px까지 넓어집니다(Wide는 1440px).
  • 13px 루트: Default는 pane 폭과 상관없이 820px이고(Narrow와 같음), Wide(936px)가 다시 더 넓습니다.
  • 탭을 열어 둔 채 글꼴 크기를 바꿔도 pane 크기 변경이나 새로고침 없이 바로 적용됩니다.

구현: PaneTerminal이 --chat-w를 px 숫자가 아니라 CSS 길이 하나로 씁니다. min(max(820px, 60rem), <pane 몫>px) 형태이고 chatLaneLength가 만듭니다. 루트 글꼴 크기를 JS에서 읽지 않습니다. CHAT_LANE_MAX(960)는 CHAT_LANE_MAX_REM(60)으로 바뀌었고, chatLaneWidth(pane, root)는 같은 규칙의 숫자 형태로 남아 테스트에서만 씁니다. DESIGN.md와 src/styles.css의 주석을 함께 고쳤습니다. 새 문구, HTTP/WS 계약, 클래스 이름 변경은 없습니다.

목업과 다른 점

  • 대응하는 목업 규칙이 없어서 목업 렌더와 비교하지 않았습니다. 검증은 측정한 폭입니다.
  • 루트가 13.67px보다 작으면 60rem이 820px보다 작아지는데, 이때는 바닥값 820px이 이깁니다. Default가 Narrow보다 좁아지지 않게 하기 위해서입니다. 그래서 "Default ≤ Wide"는 루트 11.39px 이상에서 성립합니다. 그보다 작으면 Narrow의 820px 자체가 Wide의 72rem보다 넓은데, 이 PR은 그 부분을 바꾸지 않습니다.
  • chatLaneWidth는 rem 최대 폭을 정수 px로 반올림하지 않습니다(CSS가 계산하는 값과 같게). 루트가 소수 px이면 레인도 소수 px 폭이 될 수 있습니다.

위험과 리뷰할 곳

  • .terminal-stack의 --chat-w가 px 값이 아니라 min()/max() 식입니다. ChatView.css와 Composer.css(calc() 안에서 쓰는 곳 포함)에서 Chrome은 올바르게 계산했습니다. Chrome에서만 확인했습니다. src와 scripts에 이 인라인 값을 숫자로 파싱하는 코드는 없습니다.
  • 루트 글꼴이 13.67px보다 작고 모니터가 큰 사용자는 Default가 960px에서 820px으로 줄어듭니다. 의도한 수정이지만 눈에 보이는 변화입니다.
  • src/lib/settings.test.ts가 PaneTerminal이 setProperty("--chat-w", chatLaneLength(를 쓰고 chatLaneWidth(를 호출하지 않는지 소스 문자열로 확인합니다. JS에서 px로 계산한 최대 폭이 되돌아오는 것을 막기 위한 것입니다.

Validation

검증 환경: Linux, Bun 1.4.2, Chrome. origin/main(8378a95, #468 포함)을 머지한 뒤 실행했습니다.

  • bun run typecheck: 통과
  • bun run test:unit: 1,284건 통과, 4건 건너뜀, 실패 없음
  • bun run build: 통과(test:ui 안에서)
  • bun run test:ui(별도 세션 herdr-web-ui-test): PASS 36개 뒤 checkAlertSound의 .droplet-card 대기(scripts/alert-sound-regression.ts:79)에서 실패했습니다. 이 실패는 이 장비에서 origin/main도 전체 순서로 돌릴 때 똑같이 나는 것으로 알려져 있고, CI는 통과합니다. 그 앞에서 실패한 검사는 없습니다.
  • 같은 스크립트를 checkAlertSound와 checkUpdateNotice 두 호출만 뺀 임시 사본(커밋하지 않음)으로 다시 실행: PASS 64개, 종료 코드 0. 1920 창에서 레인이 960px, 1280 창에서 820px인 기존 검사가 여기에 들어 있고 그대로 통과합니다(검사 값은 바꾸지 않았고 주석만 고쳤습니다).
  • 레인 폭에 기대는 데모 스크립트: composer-fit-demo-regression(8), held-rows-demo-regression(10), prompt-dock-demo-regression(22), chat-greeting-demo-regression(10) 모두 통과.
  • 단위 테스트(src/lib/settings.test.ts): 루트 13, 14, 16, 17.6, 20, 24px에서 Default가 820px 이상 72rem 이하인지, 쓸 수 없는 루트 값(0, 음수, NaN, Infinity)이 16px로 처리되는지, chatLaneLength가 쓰는 문자열 형태, 그리고 그 길이가 각 루트에서 chatLaneWidth와 같은 값으로 계산되는지. 16px 기대값은 그대로입니다.
  • 빌더가 데모 전송 계층에 올린 실제 클라이언트를 Chrome에서 측정했습니다. 열린 페이지에서 pane 크기를 바꾸지 않고 루트 글꼴만 바꿨습니다: 2200x900 다크(pane 1880px)에서 16px 960 → 13px 820 → 20px 1200 → 16px 960, 1440x900 라이트에서 세 크기 모두 820, 390x844 터치에서 세 크기 모두 366. 대화 열과 입력 카드 폭이 모든 상태에서 같았고 가로 스크롤과 페이지 오류는 없었습니다.

하지 않은 것: test:integration, 계약 테스트, chat-history-browser-qa, file-viewer-regression, keyboard-viewport-regression, file-viewer-mobile-regression, droplet-demo-regression은 실행하지 않았습니다(CI가 실행합니다). Chrome 외 브라우저는 확인하지 않았습니다. 이번 단계에서 스크린샷을 새로 찍지 않았습니다.

Codex 적대적 리뷰: 작은 조각이라 실행하지 않았습니다. 자체 리뷰에서 막는 지적은 없었습니다.

열린 외부 PR과의 충돌

#436, #409, #438, #441, #421은 살펴보지 않았습니다. 이 PR은 settings.ts의 채팅 레인 함수, PaneTerminal.tsx의 effect 하나, 주석, DESIGN.md, CHANGELOG.md만 건드립니다. 겹칠 만한 곳은 CHANGELOG.md의 Unreleased → Fixed입니다.

🤖 Generated with Claude Code

devswha and others added 6 commits October 5, 2026 21:05
…n Wide at another root font size

The Default step's ceiling was a fixed 960px while Wide is 72rem, so at a 13px
root Wide (936px) was narrower than Default on a large pane. chatLaneWidth now
takes the root font size, which PaneTerminal reads with the pane's width, and
resolves a 60rem ceiling with it. The 820px floor stays in px (--content-w) and
wins where 60rem is under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is described

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a font size changed in an open tab applies at once

PaneTerminal read the root font size only when the pane was measured, so a
browser font size changed while the tab was open left Default at the old
ceiling (960px at a former 16px root) until the pane was next resized, wider
than Wide's 72rem in the meantime. --chat-w is now written as
min(max(820px, 60rem), <the pane's share>px): still one length with no
percentage, and the rem part follows the root font as Wide's does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: devswha/herdr-web-ui/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 21bda1b1-3c85-4b5e-a743-c15eb2f31a5a
📥 Commits

Reviewing files that changed from the base of the PR and between 7568745 and 441db55.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • scripts/ui-regression.ts
  • src/components/PaneTerminal.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • CHANGELOG.md
  • scripts/ui-regression.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Default chat width now scales with the browser’s font size, up to 60rem (960px at a 16px root size or 1200px at 20px) when space allows.
    • The chat lane follows the available pane width, stays at least 820px wide, and is capped by the Wide layout limit. If the rem-based maximum falls below 820px, the minimum width takes precedence.

Walkthrough

The default chat lane now uses a pane-relative CSS length with an 820px floor and a 60rem ceiling. Numeric width resolution accepts a root font size. PaneTerminal writes the CSS length for the default width.

Changes

Default chat lane sizing

Layer / File(s) Summary
Define and test lane sizing
src/lib/settings.ts, src/lib/settings.test.ts
The settings helpers generate a CSS length and resolve the lane width using the 820px floor, 60rem ceiling, and pane-derived value. Tests cover root sizes, invalid inputs, and the resulting bounds.
Apply and describe the lane
src/components/PaneTerminal.tsx, DESIGN.md, src/styles.css, scripts/ui-regression.ts, CHANGELOG.md
PaneTerminal writes the generated CSS length to --chat-w for the default width. Documentation, comments, and the changelog describe the sizing rule.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~12 minutes

Change: Bug fix

Suggested reviewers: haeminway1

Merge Risk: ⚪ Minimal · up to 441db

Default chat width now uses a pane-relative share with an 820px floor and a font-scaled ceiling; the transcript remains constrained to available width. No material merge-blocking risk is evident.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title identifies the main change: Default chat’s width ceiling now uses 60rem. Its claim about never exceeding Wide is broader than the documented behavior, which has an exception below an 11.39px…
Description check ✅ Passed The description includes both required sections. It explains the change and reports extensive validation results. It notes that no screenshot or recording was included, although the template requests …
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 4 files. (1 skipped: 1 u…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

devswha and others added 2 commits October 5, 2026 22:11
Under an 11.39px root the 820px floor wins on purpose and is wider than Wide's 72rem; the
changelog, DESIGN.md, a comment and a test title promised it without that limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@devswha
devswha merged commit ac5b066 into main Oct 5, 2026
4 checks passed
@devswha
devswha deleted the fix/chat-width-ceiling branch October 5, 2026 14:06
devswha added a commit that referenced this pull request Oct 5, 2026
… message box on a coarse-pointer device too (#475)

## Change

머지된 #468(입력 카드 위에 붙는 승인 카드)의 리뷰에서 나온 후속 수정입니다. #468은 아직 릴리스되지 않았습니다.

### 무엇이 잘못되었나

카드에서 답을 누르면 카드가 사라지므로, 탭이 아닌 경우 포커스를 메시지 상자로 넘깁니다. 카드는 클릭의
`pointerType`으로 탭을 가려냈지만, `PaneTerminal`이 기기가 `(pointer: coarse)`이면 넘기지
않는 조건을 하나 더 걸고 있었습니다. 원래는 클릭이 무엇으로 눌렸는지 알려 주지 않는 브라우저를 위한 대비책이었는데, 모든
누름에 적용되었습니다.

재현: 키보드를 붙인 태블릿(또는 터치 화면이 주 포인터인 노트북, 키보드를 연결한 폰)에서 Tab으로 선택지에 가서 Enter를
누릅니다. 답은 전달되고 카드가 사라지는데, 포커스는 메시지 상자로 가지 않고 `document.body`로 떨어집니다. 키보드
사용자는 자리를 잃고 Tab을 처음부터 다시 눌러야 합니다. 같은 기기에서 마우스로 눌러도 같았습니다.

### 무엇을 바꿨나

- 누름의 출처를 `keyboard` / `mouse` / `touch` / `pen` / `unknown` 다섯 가지로
구분합니다(`pressOrigin`, `src/lib/promptAnswer.ts`).
- 규칙(`focusFollowsAnswer`): 키보드와 마우스는 어떤 기기에서든 포커스를 넘깁니다. 터치와 펜은 넘기지
않습니다. `(pointer: coarse)`는 `unknown`일 때만 봅니다. 답이 돌아왔을 때 카드가 아직 있고 포커스가
카드 안이나 페이지에 있어야 한다는 기존 조건은 그대로입니다.
- `PaneTerminal`의 `!coarseRef.current` 조건을 뺐습니다. 판단은 이제 순수 함수 한 곳에 있습니다.
- 답 경로는 바뀌지 않았습니다. 누르지 않으면 아무것도 보내지 않고, 탭은 화면 키보드를 올리지 않습니다. CSS 변경은
없습니다.

### 누름의 출처를 알아내는 방법

클릭의 `pointerType`과, 그 버튼에서 직전에 있었던 pointerdown의 `pointerType`을 함께 봅니다. 둘
중 하나라도 `touch`나 `pen`이면 그것이고, 아니면 `mouse`입니다. pointerdown을 함께 보는 이유는 iOS
Safari가 탭의 클릭을 `mouse`로 보고한 적이 있기 때문입니다(WebKit bug 282988, CodeRabbit
지적). coarse 기기에서도 마우스가 포커스를 넘기게 되었으므로, 이를 믿으면 폰에서 탭이 화면 키보드를 올릴 수 있습니다.
클릭에 `pointerType`이 없는 브라우저도 pointerdown으로 구분됩니다. 포인터가 없으면 `detail ===
0`이고 **같은 버튼에서 Enter나 Space의 keydown이 먼저 있었을 때만** 키보드로 봅니다. 카드가 keydown을
기억해 두고, 포인터가 눌리거나 답이 나가면 지웁니다.

`detail === 0`만 쓰지 않은 이유: 스크립트의 `click()`과 보조 기술의 활성화도 `detail`이 0입니다.
폰의 화면 낭독기로 선택지를 활성화했을 때 키보드로 판정하면 메시지 상자에 포커스가 가서 화면 키보드가 올라옵니다.
keydown까지 요구하면 이런 클릭은 `unknown`이 되어 정밀 포인터에서는 넘기고 coarse에서는 넘기지 않습니다.
`pointerType === ""`만 보지 않은 이유: 클릭이 `MouseEvent`인 브라우저에는 이 값이 아예 없어서 탭과
키를 구분할 수 없습니다. 카드의 입력란에서 Enter로 보낸 답은 클릭이 없으므로 그대로 키보드입니다.

### 고치지 않은 것: 카드가 사라진 뒤 돌아온 실패

답이 돌아왔을 때 카드가 이미 없으면 성공이든 실패든 아무것도 알리지 않습니다(`PromptCard.tsx`의 `if
(!shown.current) return false`). 실패에 대해서는 알리지 않는 채로 두었습니다. 이유는 다음과 같습니다.

- 메시지 상자의 알림 줄(`composer-note`)은 `Composer` 안의 상태라서, 밖에서 쓰려면
`PromptCard` → `ChatView` → `PaneTerminal` → `Composer`로 새 prop을 이어야 하고
pane별 범위와 지우는 시점도 정해야 합니다. 문구는 기존 "Not sent: {message}"로 되지만 배선이 이 PR의
크기를 넘습니다.
- 카드가 사라지는 경우는 둘입니다. (1) 같은 pane에서 프롬프트가 바뀌었거나 없어진 경우: 서버는 대개 409
`prompt_changed`로 답하고, 화면에는 이미 지금의 프롬프트가 있습니다. 그 밖의 실패(네트워크 등)는 키가
전달되었는지 알 수 없어서 "보내지 못했다"는 말이 틀릴 수 있습니다. (2) 다른 pane으로 옮긴 경우: 지금 보는 메시지
상자에 알림을 띄우면 다른 pane의 이야기가 됩니다. 원래 pane은 답이 전달되지 않았다면 사이드바에 INPUT으로 남고,
돌아가면 카드가 다시 보입니다.

알려진 한계: (1)에서 409가 아닌 실패는 사용자에게 아무 말도 가지 않습니다.

### 그 밖의 한계

- 답이 가는 동안 카드가 다른 프롬프트로 바뀌면 키보드 사용자의 포커스는 여전히 페이지로 떨어집니다. 새 카드로 옮길지는 따로
정해야 해서 손대지 않았습니다.

## Validation

검증 환경: Linux, Bun 1.4.2, Chrome(`/usr/bin/google-chrome`).
`origin/main`(#474, ac5b066) 위에 있습니다.

- `bun run typecheck`: 통과
- `bun run test:unit`: 1,302건 통과, 실패 없음. `src/lib/promptAnswer.test.ts`에
`pressOrigin` 5건과, 다섯 출처 각각을 정밀 포인터와 coarse 포인터에서 본 10건을 넣었습니다.
- `bun run build`: 통과
- `scripts/prompt-dock-demo-regression.ts`: PASS 23개, 종료 코드 0(두 번). 새
블록은 `(pointer: coarse)`인 1024x768 터치 페이지에서 다음을 봅니다. 선택지에서 Enter와 Space,
카드 입력란에서 Enter, 400ms 걸리는 답, 마우스 클릭이 모두 메시지 상자로 포커스를 넘깁니다. 카드가 dock에서
빠지는 순간마다 포커스가 `document.body`가 아니었습니다. 탭, keydown 없는 `click()`,
pointerdown은 `touch`인데 클릭은 `mouse`라고 하는 누름은 넘기지 않습니다(마지막 것은 pointerdown을
보지 않으면 실패합니다). `PaneTerminal`의 예전 조건을 되돌리면 이 블록은 `actual: "page"`로
실패합니다.
- 같은 스크립트의 기존 폰 검사 하나는 Send를 Playwright의 `click()`(마우스)으로 누르고 포커스가 가지
않기를 기대했습니다. 새 규칙에서는 마우스가 포커스를 넘기므로 의도대로 `tap()`으로 바꿨습니다.
- `scripts/held-rows-demo-regression.ts`: PASS 10개, 종료 코드 0.
`scripts/composer-fit-demo-regression.ts`: PASS 8개, 종료 코드 0.
- `DESIGN.md`의 Prompt card 문장을 새 규칙으로 고쳤고, 변경 이력은 #468 항목의 문장에
덧붙였습니다(릴리스되지 않은 기능의 수정).

돌리지 않은 것: `bun run test:ui`, `bun run test:integration`, `*.contract`
테스트(CI에 맡김). 확인하지 못한 것: 실제 태블릿과 폰, Safari와 Firefox, 실제 화면 낭독기(TalkBack,
VoiceOver)의 활성화가 어떤 클릭을 만드는지. Chromium에서만 실행했습니다.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
devswha added a commit that referenced this pull request Oct 5, 2026
Bumps package.json and herdr-plugin.toml to 0.3.50 and moves the
Unreleased notes under [0.3.50] - 2026-10-06, with the compare link.
Adds the PR link and credit the #471 entry was missing; every other
entry already carries them. Raises the remote PC bundle to 17: #470
changes the prompt answer route on the bridge, so a remote PC on bundle
16 keeps the old behaviour until its bridge is updated. Push the
`remote-v17` tag and let the bundle workflow publish before running
release.yml.

Known limits carried into the notes: Japanese and Chinese text uses the
device's fonts beside Pretendard; #469 stays open for what #470 does not
cover (answers after the first committing key, identical twin prompts,
screens no reader knows).

Carries #422 #423 #424 #425 #426 #435 #437 #439 #446 #447 #448 #452 #454
#455 #456 #457 #458 #459 #460 #461 #462 #463 #465 #466 #467 #468 #471
#473 #474 #475

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
aNNdii added a commit to aNNdii/herdr-web-ui that referenced this pull request Oct 5, 2026
Resolves the conflicts with devswha#473 and devswha#474: the Markdown preview takes
--chat-fs-body too, and highlighted chat code blocks take main's padding,
size and line height.

Review follow-ups:
- Quotes nest at most 32 deep (MAX_QUOTE_DEPTH): 20 000 nested quotes
  overflowed the stack and blanked the app from the file viewer. The
  viewer's text view sits in a RenderBoundary with a Show source fallback.
- The text's size is read from the response (Content-Range, or the body of
  a 200); an unknown size counts as cut short. An empty file (416
  bytes */0) opens empty again instead of as an error.
- The Default chat lane is written on the document, so the Default
  Markdown preview is as wide as the chat.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant