Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 2026-10-27

- [chat] A ↑ recall or a `/` pick no longer moves the caret a frame later (cause of the intermittent `chat-history.spec.ts` CI failures, e.g. `second promptline one`): the frame could land between a select-all and the typing that replaces it, so the text was added instead; the caret is now placed in a layout effect right after the text is in the box (files: src/renderer/components/chat/Composer.tsx)
- [chat] A sideways swipe over the chat no longer pauses follow (the cause of the intermittent `chat.spec.ts:299` CI failure): Chromium sometimes scrolls the chat by the swipe's few px of vertical noise, and that upward move read as the reader going back; for 150 ms after a mostly sideways wheel the chat's own upward scroll is ignored (files: src/renderer/components/chat/ChatView.tsx)

## 2026-10-26

- [git] The Changes panel follows more edits to a file that is already changed (#139): git's status line stays ` M a.ts`, so the open panel now fetches the diff again every 5 s (not while the window is hidden, never two at once) and swaps it in only when patch or untracked list differ (`sameDiff`) (files: src/renderer/components/DiffPanel.tsx, src/renderer/lib/diff.ts, tests/unit/git.test.ts, tests/e2e/git-palette.spec.ts)
Expand Down
2 changes: 1 addition & 1 deletion docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Now
- #76 Rust host: phase 3 (macOS + Windows) and 4 (default on, drop node-pty) left; phase 5 arm64 binary never run on real arm64; old `~/.wraithgrid/bin/wraithgrid-host-*` versions on remote machines are never removed
- CI: chat.spec.ts:299 (scroll follow after a sideways wheel) fails intermittently on the Linux runner, also on main
- CI flakes fixed 2026-10-27 (PR #152): chat.spec.ts:299 (sideways-wheel noise paused follow), chat-history.spec.ts (caret placed a frame late collapsed a select-all). Still seen once locally: responsive.spec.ts:131 (Settings nav heading not hidden in a small window), passed 3/3 on rerun
- Audit (2026-10-26) bugs: #140 smaller bugs (checklist). #139 fixed on fix/stale-diff-20261026. #135–#138 fixed (PR #148; #138 needs a real cancelled pkexec prompt to see live)
- Audit (2026-10-26) perf: #141 done (PR #149); not measured live yet (a DevTools profile of a long streaming chat before/after would confirm)
- #41 (2026-10-22): agy in the chat UI (`src/main/chat/agy.ts`, `chat: true`); agy 1.4.4 has `-p --input-format/--output-format stream-json`, `--model/--effort/--mode`, `--continue/--conversation`; also set its `resumeArgs`
Expand Down
12 changes: 10 additions & 2 deletions src/renderer/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ const motionReduced = (): boolean =>
*/
/** How long text must stop growing before the working line shows under it again (#129). */
const TEXT_QUIET_MS = 1200
/** How long after a sideways wheel the chat's own upward scroll is taken as its noise. */
const SIDEWAYS_MS = 150

function usePacedText(text: string, streaming: boolean, visible: boolean): string {
const [paced] = useState(() => streaming && !motionReduced())
Expand Down Expand Up @@ -392,13 +394,18 @@ function ChatViewBody({ paneId, visible, focused }: Props) {
stick.current = false
setAtBottom(false)
}
// A sideways swipe (over a wide code block) still scrolls the chat by its few px of vertical
// noise, sometimes: that upward move is not the reader going back.
const sidewaysAt = useRef(0)
const onScroll = (): void => {
const el = scrollRef.current
if (!el) return
// Content growing can increase the bottom gap before its resize callback runs.
// Only upward movement pauses follow; a gap by itself is not user input.
const maxTop = el.scrollHeight - el.clientHeight
if (el.scrollTop < Math.min(lastScrollTop.current, maxTop) - 0.5) stick.current = false
const sideways = performance.now() - sidewaysAt.current < SIDEWAYS_MS
if (!sideways && el.scrollTop < Math.min(lastScrollTop.current, maxTop) - 0.5)
stick.current = false
lastScrollTop.current = el.scrollTop
// A small upward movement remains paused until the actual bottom is reached.
if (maxTop - el.scrollTop < 1) stick.current = true
Expand Down Expand Up @@ -463,7 +470,8 @@ function ChatViewBody({ paneId, visible, focused }: Props) {
ref={scrollRef}
onScroll={onScroll}
onWheelCapture={(e) => {
if (wheelScrollsUp(e.nativeEvent, scrollRef.current)) pauseFollow()
if (Math.abs(e.deltaY) < Math.abs(e.deltaX)) sidewaysAt.current = performance.now()
else if (wheelScrollsUp(e.nativeEvent, scrollRef.current)) pauseFollow()
}}
onTouchStartCapture={pauseFollow}
onPointerDown={(e) => {
Expand Down
26 changes: 15 additions & 11 deletions src/renderer/components/chat/Composer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
useEffect,
useId,
useLayoutEffect,
useRef,
useState,
type ClipboardEvent,
Expand Down Expand Up @@ -142,6 +143,18 @@ export function Composer({
setActive(0)
}
const syncCaret = (): void => setCaret(ref.current?.selectionStart ?? 0)
// Where the caret goes once a recalled or picked text is in the box: right after React put
// it there, in the same task as the key. A frame later could land between someone's select
// all and their typing, and collapse the selection so the text is added, not replaced.
const placeCaret = useRef<{ text: string; at: number; focus?: boolean } | null>(null)
useLayoutEffect(() => {
const p = placeCaret.current
const box = ref.current
placeCaret.current = null
if (!p || !box || box.value !== p.text) return
if (p.focus) box.focus()
box.setSelectionRange(p.at, p.at)
})
// ↑/↓ through sent prompts (#132); typing or sending ends browsing.
const nav = useRef<HistoryNav>(NOT_BROWSING)
const recall = (dir: -1 | 1, box: HTMLTextAreaElement): boolean => {
Expand All @@ -153,10 +166,7 @@ export function Composer({
setCaret(step.text.length)
// A recalled `/command` doesn't open the menu: ↑/↓ keep going through history.
setDismissed(step.text)
requestAnimationFrame(() => {
if (ref.current?.value === step.text)
ref.current.setSelectionRange(step.text.length, step.text.length)
})
placeCaret.current = { text: step.text, at: step.text.length }
return true
}

Expand All @@ -172,13 +182,7 @@ export function Composer({
const next = item.sigil === '$' ? head + text.slice(caret).trimStart() : head
setText(next)
setCaret(head.length)
requestAnimationFrame(() => {
const box = ref.current
if (!box) return
box.focus()
// Only if nothing was typed since: a fast typist's first letters must stay in order.
if (box.value === next) box.setSelectionRange(head.length, head.length)
})
placeCaret.current = { text: next, at: head.length, focus: true }
}

useEffect(() => {
Expand Down
Loading