Repository navigation
feat(chat): the prompt card docks over the message box, outside the transcript - #468
Conversation
…ranscript
An agent's question, approval or plan was the sticky last element of the
transcript, with the popover shadow. It now sits on the composer's column,
directly above the input card: transcript, held messages (folded to their
caption), prompt card, input card. PaneTerminal owns that order
(.prompt-dock) and ChatView, which still owns the prompt, its poll and the
answer, renders the card into it through a portal. No answer path changes.
This reverses debate ruling R1-D3 (the card stays in the transcript); the
maintainer approved the move on 2026-10-05.
- Look: --bg-elevated, a --border-strong hairline, no shadow. The title is
the card's one red; the "input needed" badge is read, not drawn. Options
are flat rows with a keycap number, all of equal weight (the option's
name keeps "1."). A command stays in the body's mono box, where the
parsers already put it; the demo's approval fixture takes that shape.
- Height: the larger of 60% of the app height and six touch rows. Only the
reference text gives way (six lines, two at the least); a card still too
tall scrolls as a whole with a fade, the confirm row pinned.
- The composer keeps the grip's hit strip for itself under the card.
- Placeholders lose "Answer above:" ("Type 1–3 to choose…").
- A blocked agent's open work block reads "Needs you" behind a still red
dot, by the pane's status.
- The dock is a polite live region; after an answer pressed in the card,
focus goes to the message box (not on a coarse pointer).
- The transcript's box is clipped under the chat lens, so its scroller's
padding cannot paint over the card when the stack is short.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and puts a phone's keyboard away Review findings on the prompt dock: - The empty dock was display: none, so the live region appeared together with its card and a screen reader announced nothing. It stays rendered at zero height; its two margins apply only with a card in it. - Outside the transcript the card had lost Settings -> Chat font size and Chat font. The dock scales the type tokens and the card uses them; the reference text and keycaps stay mono. The tall form is checked again at 24px. - On a short phone the card stands where the transcript was, and the keyboard could not be put away. A tap on the card's text, or a drag down it once it is at its top, blurs the message box as on the transcript (dismissKeyboardOn takes atTopOnly). - Focus after an answer is decided when the answer comes back: not if the user moved it meanwhile or the card is gone (focusFollowsAnswer). - The card contains its overscroll, so a drag past its top is not Android's pull-to-refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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
📒 Files selected for processing (11)
🚧 Files skipped from review as they are similar to previous changes (3)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review. 📝 SummarySummary by CodeRabbit
WalkthroughThe chat prompt card now renders in a dock above the composer and outside the transcript. The update changes card layout, answer focus, phone keyboard dismissal, and blocked-work status display. It also adds browser regression checks and updates prompt translations. ChangesChat prompt dock and interactions
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Feature Suggested reviewers: Merge Risk: ⚪ Minimal · up to Prompt cards now appear in a dock above the message box. Their layout, focus, and phone keyboard behavior are updated, and the answer API is unchanged. No concrete defects were found in the change. Mergeability depends mainly on browser CI passing, including the new prompt-dock regression check. Security Architecture ReviewSecurity architecture risk: ⚪ Minimal · up to The change rearranges approval controls and tightens delayed-response isolation. The reviewed behavior does not introduce new privileges, weaken approval checks, or broaden access. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 78.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 16 files. (5 skipped: 5 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
…ped pick alone Each prompt that appears gets a card of its own (a key per occurrence, told by the prompt object the read keeps, since the id is a hash of the text). A card that is gone reports nothing when its answer comes back, the chat clears only the prompt the card was for, and a typed pick is cleared by its pane and prompt. The new card also opens at its top, with no scroll left over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…buttons on one line The row is pinned over the options, and an option's label can run to pages: its question now scrolls in itself, and Confirm and Cancel are one group that never breaks, beside the question when there is room and under it when not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The handoff asked the device's pointer, so a touch-screen laptop (a fine pointer) raised its on-screen keyboard after a tap. The press itself now says what made it; a coarse pointer still rules the handoff out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN.md no longer calls the input box the only card. The regression's prompts are per pane, and it covers a late answer (same pane and across a pane switch), a replaced card's scroll, a 2,166-character label in the Confirm row, its buttons on one line, and a tap on a fine-pointer screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n Wide at another root font size (#474) ## 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](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… reads a step larger (#473) ## Change 승인하신 채팅 모드 목업을 순서대로 올리는 작업의 마지막 묶음 중 하나입니다. 앞선 조각은 #456 ~ #468이고, 이 PR은 목업의 G 규칙(`mockups/chat-mode/variants/g.css`: 서체와 읽기 크기)만 담습니다. 다른 조각은 건드리지 않습니다. **사용자가 보는 것** - 앱이 서체를 직접 가져옵니다. 화면과 채팅은 Pretendard, 채팅과 화면의 코드는 JetBrains Mono로 그립니다. 서체가 설치되지 않은 휴대폰이나 PC에서도 같은 글자가 나옵니다. - 인터넷에서 받아 오는 것은 없습니다. 파일은 앱과 함께 사용자 PC에서 나갑니다. Pretendard는 92개 조각으로 나뉘어 있어서 페이지는 그 글에 필요한 조각만 받습니다(영어 채팅은 3개 조각과 JetBrains Mono, 한국어 UI는 9개 조각과 JetBrains Mono). 한 번 받은 조각은 서비스 워커의 기존 `/assets/` 규칙이 보관합니다. - 채팅에서 답변, 내 메시지 말풍선, 작업 중 나오는 말은 15px에 줄 높이 1.65입니다. 굵은 글씨는 semibold(600), 인라인 코드는 0.87em, 코드 블록은 13px에 줄 높이 1.6, 빈 채팅의 인사말은 22px semibold입니다. - 메시지 입력란은 마우스에서는 15px, 터치에서는 그대로 16px입니다(iOS 확대 방지). - 도구 행, 시각, 작은 라벨은 크기가 그대로입니다. - 터미널은 바뀌지 않습니다. 터미널 글꼴, **Terminal font** 설정, 격자가 그대로이고, 직접 고른 **Chat font**가 여전히 맨 앞에 옵니다. - 느린 연결에서 서체가 늦게 도착해도, 채팅 맨 끝을 보던 사람은 맨 끝에 그대로 있습니다. 입력 카드는 서체가 하나 도착할 때마다 다시 맞춥니다. **코드** - `src/fonts/`: Pretendard 스타일시트와 92개 woff2 조각(패키지 그대로), `JetBrainsMono-Regular.woff2`, `fonts.css`. `src/main.tsx`가 불러오고 Vite가 `dist/assets/`에 지문을 붙여 넣습니다. - `src/styles.css`와 `DESIGN.md`: 새 토큰 `--fs-chat`(15 / compact 14), `--fs-display`(22 / compact 21), `--lh-prose` 1.65, `--lh-code` 1.6, `--tracking-display` -0.02em, 그리고 `--font-sans`, `--font-mono` 스택. 두 파일은 함께 바꿨습니다. - `src/lib/fontFaces.ts`(새 파일): 서체 도착 횟수를 세는 모듈 저장소와 `useFacesArrived` 훅. `ChatView.tsx`는 `stickToBottom`일 때 도착마다 끝으로 스크롤하고, `Composer.tsx`는 기존 `loadingdone` 효과 대신 이 훅으로 다시 맞춥니다. - `public/sw.js`: `CACHE_NAME`을 올렸습니다. - `THIRD_PARTY_NOTICES.md`: 두 서체의 OFL 전문. - 새 `t("…")` 문구, HTTP/WS 계약 변경은 없습니다. ## Why 지금은 기기에 깔린 서체에 기대기 때문에 기기마다 글자 모양과 폭이 다릅니다. 입력 카드의 모델 라벨 맞춤(#465, #467)처럼 폭을 재는 코드는 서체가 같아야 결과가 같습니다. 그리고 채팅 본문은 14px에 좁은 줄 간격이라 긴 답변을 읽기에 빽빽했습니다. 목업에서 승인하신 크기가 15px / 1.65입니다. ## 목업과 다른 점 - 서체는 의존성으로 추가하지 않고 `src/fonts/`에 넣었습니다(기존 Nerd Font와 같은 방식). `pretendard` 패키지는 풀면 97 MB에 파일 1,826개이고, `build-remote-bundle.ts`가 `node_modules`를 원격 번들마다 복사합니다. - JetBrains Mono는 `"JetBrains Mono Web"`이라는 이름으로 등록했습니다(설치된 사본이 있으면 `local()`로 먼저 씁니다). g.css는 `"JetBrains Mono"`인데, `lib/fontFamily.ts`의 터미널 스택이 그 이름을 쓰고 있어서 같은 이름이면 xterm이 웹 서체로 칸 크기를 재기 시작합니다. `--font-mono`의 맨 앞은 여전히 `"Symbols Nerd Font Mono"`입니다. - G6(pane 제목)은 #461에서, G5(조용한 행과 상태 줄의 작은 크기)는 이미 main에 있어서 바꾸지 않았습니다. - G2의 본문 자간 -0.003em은 넣지 않았습니다(토큰이 없고 눈에 보이지 않습니다). 문단 `margin-block` 규칙도 넣지 않았습니다. 기존 아래 여백으로 같은 간격이 나옵니다. `li + li` 간격은 넣었습니다. - Markdown의 h3 ~ h6을 `--fs-md`에서 본문 크기로 올렸습니다. 그대로 두면 제목이 본문보다 작아집니다. g.css에는 없는 변경입니다. - 압축 / 중단된 턴 요약(`.chat-compact-text`)은 이전의 14px / 1.5로 고정했습니다. 완료된 뒤 접힌 말(settled narration)도 13px 그대로입니다. 크기를 올리는 대상은 본문, 말풍선, 작업 중인 말뿐입니다. - 목업에 없는 제품 코드: `src/lib/fontFaces.ts`. `font-display: swap`으로 서체가 바뀔 때 스크롤 위치와 입력 카드 맞춤이 어긋나는 것을 막습니다. ## 리뷰할 때 볼 것 - **용량**: 저장소와 릴리스, 원격 번들이 약 3.1 MB 늘어납니다(woff2 93개). `dist/`는 4.57 MB에서 7.68 MB가 됩니다. - **설정 표기**: Settings는 기본값을 "Chat font size 14px"로 보여 주는데 본문은 15px로 그려집니다. 설정은 기준 크기이고 본문은 그보다 한 단계 위이며, 설정을 바꾸면 여전히 함께 커집니다. 스테퍼가 15로 읽히길 원하시면 `lib/settings.ts`의 `CHAT_BASE_FONT`를 바꿔야 하는데, 여기서는 하지 않았습니다. - **입력란 15px 조건**: `(hover: hover) and (pointer: fine)`입니다(g.css 그대로). 트랙패드를 붙인 iPad도 여기에 들어갑니다. - **다시 그리기**: 서체가 하나 도착할 때마다 입력 카드와 열려 있는 ChatView가 한 번씩 다시 그려집니다. 한국어 페이지 첫 방문에 열 번 남짓입니다. 훅은 93개 face의 `loaded` 약속을 지켜보지만 내려받기를 시작하지는 않습니다. - **끝으로 스크롤**: 서체가 도착하면 끝에서 48px 안에 있던 사람만 끝으로 옮깁니다. resize observer가 쓰는 기존 기준과 같습니다. - **`CACHE_NAME`**: 올렸기 때문에 기기마다 기존 캐시가 활성화 때 한 번 비워집니다. - **코드 안의 한글**: JetBrains Mono에 한글이 없어 시스템 고정폭 서체로 넘어갑니다. regular 굵기만 넣었으므로 굵은 코드는 합성됩니다. - `scripts/film/capture.ts`는 여전히 `"JetBrains Mono"`라는 이름을 찾아 기존 경고를 냅니다. 이 조각 밖이라 두었습니다. ## Validation 검증 환경: Linux, herdr 0.9.3, Bun 1.4.2, Chromium. 브랜치는 `origin/main`(8378a95, #467과 #468 포함)을 병합한 상태이고 뒤처진 커밋이 없습니다. - `bun run typecheck`: 통과 - `bun run test:unit`: 1,286건 통과, 4건 건너뜀, 실패 없음 - `bun run build`: 통과 - `bun run test:ui`(별도 세션 `herdr-web-ui-test`), 첫 실행: `checkDroplet`의 "no in-app alert for the pane already open"에서 멈췄습니다(PASS 31개 뒤). - 같은 `ui-regression.ts`를 `checkAlertSound`와 `checkUpdateNotice` 두 호출만 뺀 임시 사본(커밋하지 않음)으로 다시 실행: PASS 64개, 종료 코드 0. `checkDroplet`도 통과했으므로 첫 실행의 실패는 타이밍 flake로 적습니다. 뺀 두 검사는 이 기기에서 `origin/main`도 전체 순서 안에서만 실패하는 것으로 알려진 것이고 CI에서는 통과합니다. 이 PR에서는 그 둘을 이 기기에서 실행하지 않았습니다. - CI의 나머지 브라우저 스크립트: `chat-history-browser-qa` 5/5, `file-viewer-regression` 6/6, `keyboard-viewport-regression` 1/1, `file-viewer-mobile-regression` 16/16, `droplet-demo-regression` 5/5, `chat-greeting-demo-regression` 10/10, `composer-fit-demo-regression` 8/8, `held-rows-demo-regression` 10/10, `prompt-dock-demo-regression` 22/22 - CI에 없는 스크립트: `font-swap-demo-regression`(새 파일) 3/3, `chat-browser-qa` 7/7, `skill-activity-browser-qa` 2/2, `output-browser-qa` 종료 코드 0 **테스트 변경** - `src/lib/fontFaces.test.ts`(새 파일, 4건): 불러오는 중이거나 아직 요청되지 않은 face만 따라감, 여러 번 훑어도 한 번만 따라감, 다른 face가 아직 오는 중이어도 각자 도착을 알림, 실패한 로드는 도착도 아니고 처리되지 않은 거절도 아님. - `src/lib/fontFamily.test.ts`: 번들 서체 이름, 그리고 터미널 스택이 그 이름을 쓰지 않는다는 것을 고정. - `scripts/font-swap-demo-regression.ts`(새 파일, 390px 터치, woff2 응답을 1.5초 늦춤): 끝을 보던 사람은 서체가 바뀌어도 끝에 있음, JetBrains Mono 파일이 끝내 오지 않아도 같음, 위로 올려 보던 사람은 끌려 내려가지 않음. 첫 검사는 ChatView 효과가 없으면 실패합니다(간격 18px). - `scripts/app-faces.ts`: 두 서체가 로드될 때까지 기다리는 도우미. `composer-fit`, `chat-greeting`, `held-rows`, `prompt-dock` 데모 스크립트가 측정 전에 씁니다. - 기대값을 바꾸거나 지우거나 느슨하게 한 것은 없습니다. 고정된 폭, 높이, 한 줄 검사는 서체가 로드된 상태에서 그대로 통과합니다. **눈으로 본 것**: 데모 transport의 실제 클라이언트를 1440과 390 터치, 다크와 라이트, 영어와 한국어 UI에서 봤습니다. 본문과 말풍선 15px / 24.75px, strong 600, 인라인 코드 13.05px, 코드 블록 13px / 20.8px, 도구 행 12px, 제목 14px / 600, 입력란은 마우스 15px / 24px, 터치 16px / 24px, 가로 스크롤 없음. Chat font size 20 + compact에서도 본문이 따라 커집니다(21.5px). g.css와 맞습니다. **하지 않은 것** - `bun run test:integration`, `*.contract` 테스트, `bun run build:site`. - 서비스 워커가 조각을 오프라인에서 돌려주는지는 실행해 보지 않았습니다. 바뀌지 않은 `/assets/` cache-first 규칙을 읽고 판단했습니다. - 늦게 온 고정폭 서체로 모델 라벨을 다시 맞추는 경우(Windows / Consolas)는 단위 테스트로만 덮었고, 브라우저에서 라벨 폭을 확인하지는 않았습니다. - herdr를 쓰는 브라우저 스크립트(`ui-regression` 등)는 서체 로드를 기다리지 않습니다. 폭을 한 번만 읽는 검사가 서체 교체와 겹칠 수 있습니다. CI에서 흔들리면 `appFaces`를 넣으면 됩니다. **Codex 적대적 리뷰**: gpt-6.1-sol xhigh가 끝까지 돌았고 지적은 하나였습니다(Composer가 `loadingdone`에서만 다시 맞춤). 브라우저에서 재현한 뒤 `useFacesArrived`로 고쳤고, 두 OFL 전문이 원본과 같다는 것도 확인받았습니다. ## 다른 PR과의 충돌 열려 있는 외부 PR과 겹칠 수 있습니다. #436(상태 줄: `Composer.css`, `Composer.tsx`), #409와 #438(`ChatView.css`의 `.markdown` 규칙, `ChatView.tsx`), 그리고 어느 쪽이든 `CHANGELOG.md`와 `DESIGN.md`. #441과 #421(사이드바)은 겹치지 않을 것으로 봅니다. 그 PR들은 건드리지 않았습니다. ## PR을 연 뒤의 변경 적대적 리뷰의 지적을 하나씩 확인하고 고쳤습니다(`62cf511`..`c7ed562`). - **서비스 워커 (`public/sw.js`)**: `CACHE_NAME`이 바뀌면 새 워커가 켜지는 순간 옛 캐시를 지웠고, 미리 담아 두는 파일이 없어서 그 뒤 온라인으로 한 번 더 열기 전에는 오프라인 새로고침이 브라우저 오류 화면이었습니다. 이제 켜질 때는 아무것도 지우지 않고, 서버에 닿은 탐색이 새 캐시에 셸(`/`)을 넣은 다음에 다른 이름의 캐시를 지웁니다. 옮겨 담는 방식 대신 이쪽을 고른 이유: 켜지는 시점에는 새 셸이 어느 캐시에도 없어서 "새 셸이 가리키는 파일"을 알 수 없고, 옛 캐시에는 그 페이지를 띄운 셸, 번들, 이미 받은 서체 조각이 그대로 있으며, 아무것도 복사하지 않으니 옛 빌드의 해시 파일이 첫 온라인 탐색 뒤까지 남지 않습니다. 서체 조각은 여전히 받을 때만 캐시합니다. 대가: 옛 캐시에 있던 서체 조각은 그 탐색 뒤 한 번 다시 받습니다. - 검증: `src/pwa.test.ts`에 실제 `sw.js`를 가짜 CacheStorage 위에서 돌리는 테스트 4개(고치기 전 워커에서는 2개 실패). 실제 Chrome에서도 한 번 확인했습니다(main의 v3 워커로 연 뒤 새 워커로 교체하고 서버를 내린 채 새로고침: 고치기 전 `net::ERR_FAILED`, 고친 뒤 앱이 그려짐. 다음 온라인 탐색 뒤 v4만 남고 다시 오프라인으로 열림). 이 Chrome 확인은 일회용 스크립트였고 커밋하지 않았습니다. - **압축 요약 (`ChatView.css`)**: `.chat-compact-text` 안의 h3~h6은 요약 글자 크기를 물려받고, 코드는 `--fs-xs` / `--lh-base`로 되돌렸습니다(버려진 턴의 요약도 같은 상자라 함께 적용). 빌드한 CSS로 잰 값(기본 크기): main h3 14px, 인라인 코드 12px, 코드 블록 12px/1.5 → 고치기 전 15px, 12.18px(제목 안 13.05px), 13px/1.6 → 고친 뒤 14px, 12px, 12px/1.5. 채팅 글자 크기 1.25배에서도 main과 같고, 요약 밖의 답변은 그대로입니다. - **문서 (CHANGELOG, DESIGN.md)**: "어느 기기에서나 같은 글자"를 라틴 문자와 한글로 한정했습니다. 리뷰는 한자가 전혀 없다고 했지만 조각의 `unicode-range`를 세어 보니 한글 11,172자, 히라가나 83, 가타카나 88, 한자 435자가 들어 있습니다. 그래서 "가나와 수백 자의 한자만 있고, 나머지 한자와 중국어는 기기의 서체로 그려지며 일본어 한 줄에는 두 서체가 섞인다"고 적었습니다. README, docs, site에는 같은 주장이 없었습니다. 오프라인 수정은 Fixed 항목으로 추가했습니다. - **서체 도착 횟수 (`src/lib/fontFaces.ts`)**: 서체 하나의 도착이 두 번(자기 promise, 이어서 `loadingdone`) 세어졌고 전부 실패한 묶음도 한 번으로 세어졌습니다. 서체마다 한 번만 세고 실패는 세지 않습니다. 단위 테스트 7개. - **#467 항목 (CHANGELOG)**: 모델 알약이 비켜나는 조건의 괄호를 "390px 휴대폰에서 확인"으로 고쳤습니다. 검증하지 못한 것: - 서비스 워커 교체를 Safari와 Firefox에서, 그리고 설치된 앱의 실제 업데이트 경로에서 확인하지 않았습니다. - `mobile-tabs-regression.ts`(서체 도착을 쓰는 탭 줄)와 `test:ui`, `test:integration`, contract 테스트는 공유 herdr 세션이 쓰이는 중이라 돌리지 않았습니다. CI에 맡깁니다. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… 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>
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>
Change
승인하신 채팅 모드 재설계(목업
mockups/chat-mode)의 조각 하나입니다. 앞선 조각은 #456 ~ #466으로 머지되었습니다. 이번 세 번째 묶음은 모델 표시(pill)와 이 PR, 곧 입력 카드 위에 붙는 승인 카드입니다. 마지막 묶음은 글자 크기 체계와 글꼴입니다.이 PR은 목업 규칙 D1(approval dock)과 D2(waiting head)를 옮깁니다(
variants/d.css,d.js).토론 판정 R1-D3(카드는 대화 안에 둔다)를 뒤집습니다. 2026-10-05에 승인하신 내용이며 첫 커밋 본문에도 적었습니다.
사용자가 보는 것
--border-strong테두리에--bg-elevated바탕입니다. 빨간색은 제목 하나뿐입니다. "INPUT NEEDED" 배지는 그리지 않지만 스크린 리더는 읽습니다.POST /api/pane/prompt/answer)와 HTTP/WS 계약은 바뀌지 않습니다.구현
PaneTerminal이.prompt-dock요소로 순서를 정하고, 프롬프트와 폴링, 답을 여전히 가진ChatView가 포털로 카드를 그 안에 그립니다. 새 문구 4개는 ko, ja, zh에 모두 넣었습니다. 변경 이력은 Unreleased에 적었습니다.목업과 다른 점
.composer의 자식이 아니라, 그 앞 형제인.prompt-dock안에 있습니다(보류 목록과 같은 방식). dock이 포털 대상이자aria-live="polite"영역이고, 카드는 "Message composer" 그룹 밖에 남습니다. 새 카드가 읽히도록 dock은 비어 있을 때도 높이 0으로 렌더링됩니다.is-command클래스와 질문의 고정폭 상자는 만들지 않았습니다. 서버 파서(Codex, omp, Claude 승인)가 이미 명령을body에 넣고, 그것이 카드의 고정폭 상자입니다. pi의 confirm은 메시지를question에 넣지만 명령임을 알리는 신호가 없습니다. 데모 fixture는 Codex의 실제 형태(title = question, body = 명령)로 바꿨습니다.--space-2가 아니라--composer-grip-h(12px, 터치 20px)입니다. 보류 목록 아래와 같은 크기 조절 손잡이 영역을 그대로 둡니다.--content-w가 아니라 #463의--chat-w입니다.--space-3이 아니라--space-2입니다..terminal-stack.is-chat > .terminal-surface { overflow: hidden }을 추가했습니다. 390x500에서 대화 스크롤 영역이 카드 윗부분을 덮어 그렸기 때문입니다.focusFollowsAnswer).dismissKeyboardOn의atTopOnly). 버튼, 라벨, 입력란은 예외입니다.리뷰하실 때 볼 곳
overflow: hidden은 공용 요소(.terminal-surface)에 채팅 렌즈일 때만 거는 새 클립입니다. 인사말과 보류 줄 회귀는 통과합니다..prompt-card가.chat-view안에 있다고 가정하는 외부 스크립트나 PR은 깨집니다. 저장소 안의scripts/film,scripts/readme-media,site/demo는 선택자가 그대로 맞습니다.docs/guide.md의media/look-prompt.png와site/index.html의 정적 목업은 예전 카드를 보여 줍니다. 릴리스 때 다시 찍어야 합니다. 바뀐 fixture가 이미 새 모습을 만듭니다.열려 있는 외부 PR과의 충돌
#436(
Composer.css,PaneTerminal.tsx), #409와 #438(ChatView.tsx,ChatView.css,PromptCard)과 충돌이 예상됩니다. #441과 #421(사이드바)은 겹치지 않을 것으로 봅니다.Validation
검증 환경: Linux, herdr 0.9.3, Bun 1.4.2, Chrome(
/usr/bin/google-chrome).origin/main은 #466(9153e60)이며 브랜치가 그 위에 있어 병합할 것이 없었습니다.bun run typecheck: 통과bun run test:unit: 1,272건 통과, 4건 건너뜀, 실패 없음(116개 파일)bun run build: 통과bun run test:ui(별도 세션herdr-web-ui-test) 1회: PASS 36개 뒤checkAlertSound의.droplet-card대기(scripts/alert-sound-regression.ts:79)에서 30초 타임아웃으로 종료 코드 1. 이 장비에서는origin/main에서도 전체 순서로 돌릴 때 여기서 실패하는 것으로 알려져 있으며(CI는 통과), 이번에main에서 다시 확인하지는 않았습니다. 그 앞에서 실패한 검사는 없습니다.checkAlertSound와checkUpdateNotice두 호출만 뺀ui-regression.ts임시 사본(커밋하지 않고 지움)으로 나머지를 돌렸습니다: PASS 64개, 종료 코드 0. 카드가.chat-view밖에 있고 dock의 다음 형제가.composer이며 카드가 입력 카드 위에서 끝나는지 보는 새 단언이 들어간 "startup prompt appears and accepts an answer while the agent status is idle"을 포함합니다.chat-browser-qa.tsPASS 7개,file-viewer-regression.tsPASS 6개,directory-groups-regression.tsPASS 7개. 모두 종료 코드 0.prompt-dock-demo-regression.ts(이 PR의 새 스크립트,ci-browser.sh에 등록) PASS 15개,held-rows-demo-regression.ts10개,composer-fit-demo-regression.ts5개,chat-greeting-demo-regression.ts10개,droplet-demo-regression.ts5개,keyboard-viewport-demo-regression.ts1개,keyboard-viewport-regression.ts1개,chat-history-browser-qa.ts5개,skill-activity-browser-qa.ts2개,file-viewer-mobile-regression.ts16개,pi-image-browser-qa.ts("browser OK" 한 줄).고친 테스트:
promptAnswer.test.ts(안내 문구 기대값,focusFollowsAnswer),keyboard.test.ts(atTopOnly,scrolledDown),workBlocks.test.ts(isWaitingWorkTurn),ui-regression.ts(카드 위치 단언). 지우거나 느슨하게 한 테스트는 없습니다.Codex 적대적 리뷰(gpt-6.1-sol, xhigh, 읽기 전용): 지적 4건(빈 라이브 영역이 숨겨짐, 채팅 글자 설정을 잃음, 늦은 답이 포커스를 가져감, 카드의 overscroll)을 모두 확인했고, 별도 리뷰에서 나온 폰 키보드 내리기 1건과 함께 202e9da에서 고쳤습니다.
확인하지 못한 것
navigator.virtualKeyboard를 씁니다.bun run test:integration과*.contract.test.ts. 서버와 계약은 건드리지 않았습니다.checkAlertSound,checkUpdateNotice(위 설명).PR을 연 뒤의 변경
리뷰에서 나온 Medium 네 건과 문서 한 건을 고쳤고, 그 사이 움직인 main을 merge로 합쳤습니다(rebase, force push 없음).
(pointer: coarse)확인도 그대로 두었습니다. 그래서 coarse 포인터 기기에서는 마우스로 눌러도 이전처럼 포커스가 넘어가지 않습니다. Composer의 Queue는 touch만 보지만 여기서는 pen도 탭으로 봅니다.확인하지 못한 것:
bun run test:ui,test:integration, contract 테스트는 돌리지 않았습니다.🤖 Generated with Claude Code