Commit Graph

279 Commits

Author SHA1 Message Date
creatixchu a1b0e97162 Merge remote-tracking branch 'origin/master' into worktree-session-hover-menu-fix 2026-07-31 17:32:52 +08:00
creatixchu c029f03516 fix(web): reveal the caret when a persisted draft arrives after mount
ConversationSession seeds a stored draft in its own mount effect, and a parent's mount effect runs after its children's — so the unlock effect measured an empty mirror and never ran again for the draft that then appeared, leaving a restored long draft showing its head with the caret at its end. The effect now depends on the draft being non-empty; clearing on send and typing the first character flip it too, where both the focus and the reveal are no-ops. Pre-existing (the old geometry did not scroll for a programmatic value change either), fixed here because the reveal now exists.

Also from review: the golden's paste goes back to a block NOT ending in a newline, so the collapsed branch keeps a real engine under it while the standalone case owns the after-newline branch; the shared line-height rule names the reveal as its third consumer.
2026-07-31 17:26:29 +08:00
Tianyi Cui ab6d77a12d Merge remote-tracking branch 'origin/master' into fix/web-transcript-projection
# Conflicts:
#	apps/web/tests/queue-actions.e2e.ts
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/src/client/chat/MessageItem.tsx
2026-07-31 17:17:00 +08:00
creatixchu 8b5c3946da docs: keep the approval-panel cap note current with the composer's scrollport
This PR moved the 14-line cap declaration off the auto-grow mirror and onto the box that scrolls the draft, and retargeted the approval scenario's cap probe from the textarea to that box. The sibling note stated both as they were; implemented notes have to state what shipped, in the change that ships it.
2026-07-31 17:15:47 +08:00
creatixchu 1008ca865b fix(web): reveal a caret that sits after a newline, where the engines disagree
A caret straight after a newline is on a line with nothing to measure — the shape a trailing-newline draft ends in. chromium returns no client rects at all for the collapsed position (an all-zero box, which sent the reveal upward instead), firefox reports the line above, WebKit the right one. Measure the newline the caret just left and step one line down: all three then land on 649 of 652 with the caret's line at 315 inside the 336px box. The browser case now pastes a newline-terminated block, and fails 'expected 0 to be greater than 0' without the rule.
2026-07-31 17:03:55 +08:00
creatixchu 0afa3af1d5 Merge remote-tracking branch 'origin/master' into worktree-session-hover-menu-fix 2026-07-31 16:51:10 +08:00
Turtle b7ee38fef0 feat(tui): open the /resume picker immediately with a loading state
The selector overlay opens as soon as the command dispatches: the
picker renders a loading placeholder over an undefined candidate set,
owns terminal input from its first frame, answers Enter with a
still-loading error, and cancels on Escape exactly like the loaded
list. The finished scan swaps rows in through setCandidates without
replacing the overlay; a scan failure closes it and keeps the existing
notice.
2026-07-31 16:39:33 +08:00
creatixchu 065ed6337e fix(web): reveal the caret in the composer's own scrollport on a session switch
preventScroll stopped the browser from revealing a focus nobody gestured for, and took the caret with it. The composer DOM is reused across sessions, so switching to a longer draft keeps the previous offset while the value swap puts the caret at the new draft's end: measured on all three engines, 940px below a box sitting at 0, where the old geometry reached the caret at 628. The unlock effect now suppresses the browser's walk and reveals the caret in its own scrollport, landing at 625 of 628. The focus-keeping mousedown gets preventScroll too and needs no reveal. revealCaret returns early when the box cannot scroll: the draft fits, so every caret is already in view.
2026-07-31 16:38:36 +08:00
creatixchu 1128b7071e docs: name the undo/redo caret gap in the composer scrollport note 2026-07-31 16:12:05 +08:00
Turtle 3c08ca3606 perf(tui): open the /resume selector from one batch projection
The selector called readSession per listed session under an unbounded
Promise.all: each call re-listed the whole persistence store (O(N^2)
listings), decompressed and parsed the complete log, replay-validated
every event, and deep-cloned it up to three times, only to derive one
row's title, activity time, turn label, route, and goal phase. On a
real 185-session / 87 MB store the selector took tens of seconds.

Candidate rows now come from one projectSessions batch over borrowed
logs; a rejected projection degrades to the same disabled unreadable
row. Preflight still replay-validates the single chosen session through
readSession, which is already live-preferred, so its redundant live
shortcut is gone.
2026-07-31 16:09:17 +08:00
creatixchu e465806120 fix(web): scroll to the caret after an edit the composer performs itself
Pasting a long block left the view where it was while the caret sat at the
end of what was pasted. Paste, ctrl/meta-Enter newline and cut all suppress
the native edit — the machine owns the draft and the undo log — and restore
the caret with `setSelectionRange`, which reveals nothing: measured in
chromium and WebKit, before this branch as well as on it. Firefox happened to
reveal it, in the old geometry only.

The three restores now share one helper that measures the caret against the
hidden mirror — same draft, same metrics, same wrap width, so a Range
collapsed at the caret's index reports where the caret is without a caret API
— and scrolls the scrollport the minimum that brings the line inside, which
is what the browser does for typing. One scrollport is what makes this
possible at all: the reveal is finally a single offset to move.

Also from review: the composer's own focus() on unlock and session switch
passes preventScroll, so a session switch cannot move the transcript through
the taller textarea's reveal chain.
2026-07-31 16:07:55 +08:00
Tianyi Cui 810a2a571c Merge pull request #1022 from deepseek-harness/codex/compact-english-checkpoint
fix(compact): stabilize checkpoint language
2026-07-31 15:57:49 +08:00
imccyu abc77e707e Merge remote-tracking branch 'origin/feat/manual-compact-single-lock' into feat/compaction-progress-visibility 2026-07-31 15:51:17 +08:00
imccyu 9ca02b4fc8 Merge remote-tracking branch 'origin/master' into fix/web-transcript-projection 2026-07-31 15:48:26 +08:00
Hypatia May b14c9daed2 Merge branch 'feat/manual-compact-single-lock' into feat/compaction-progress-visibility 2026-07-31 15:37:06 +08:00
Hypatia May d67de808b0 Merge remote-tracking branch 'origin/master' into fix/web-transcript-projection
# Conflicts:
#	packages/client/ui-conversation/README.i18n.yaml
2026-07-31 15:32:36 +08:00
kingwl 4dfbb52106 Merge remote-tracking branch 'origin/master' into codex/fix-tui-diff-context-counts
# Conflicts:
#	packages/ui/tui/src/components/transcript.ts
2026-07-31 15:27:24 +08:00
creatixchu 49f8cdd401 fix(web): bind the composer's caret to its glyphs with one scrollport
The composer paints its draft in two layers — the textarea owns the value,
the selection and the caret, the backdrop paints every visible glyph — and
they had one scroll offset each, kept equal by a `scroll` listener. That
holds at rest and not in motion: a wheel gesture scrolls the textarea on the
compositor, the listener runs afterwards, and for those frames the caret sits
at the new offset with the words at the old one. Measured on a harness of the
same geometry, a 200px offset change separates caret from glyphs by ~200px
(chromium 203, firefox 202, WebKit 203) until a later frame — the caret
flying out of its own text when a user swipes a long draft quickly.

Both layers now ride one scrollport: `[data-input-scroll]` carries the
14-line cap, the auto-grow stack inside it is as tall as the whole draft, and
the textarea holds no scrollable overflow of its own. The browser applies one
offset to both layers in the same frame, so the coupling is structural rather
than maintained. The backdrop's trailing-line sentinel and the cross-engine
wrap-width premise go with the mirror: the layers now share a containing
block, which closes the WebKit 768-against-776 gap by construction.
2026-07-31 15:25:29 +08:00
Tianyi Cui 81e5ee7e3e Merge branch 'master' into pr/adapter-registration-race 2026-07-31 15:22:35 +08:00
creatixchu 6ca3b78e55 Merge remote-tracking branch 'origin/master' into worktree-session-hover-menu-fix
# Conflicts:
#	apps/web/tests/workspace-management.e2e.ts
2026-07-31 15:14:05 +08:00
Turtle 732c9e6d95 fix(tui): release the adapter-registration listener on channel detach
Review follow-up: the llm/adapters-updated listener's disposer was
discarded, leaving it firing (harmlessly, behind isDisposed()) between
TUI shutdown and fiber disposal, asymmetric with the sibling channel
listeners. The controller now exposes detach(), and the channel's
detachListeners() calls it on both the dispose() and startup-failure
paths.
2026-07-31 15:11:24 +08:00
creatixchu 51fd1d2b41 fix(web): only anchor presses dismiss the hover card
The card is a React child of the wrapper, so capture-phase presses on it
reached the wrapper's dismissal handler: the first pointerdown of a text
selection closed the card, contradicting its JSDoc contract. Restrict the
immediate close to presses outside the card, keeping it mounted under a
held press (and the browser click with it), and align onPointerLeave's
grace arming with Menu (only while open).

Pin both new behaviors in hover-card.spec and update the bilingual Agent
Note.
2026-07-31 15:06:27 +08:00
creatixchu 23c4745560 fix(web): assert the wrap-width premise instead of reserving a gutter
Review flagged that "equal by construction" rested on an engine behaviour I
had not measured: `scrollbar-gutter: stable` only equalizes the layers if the
engine applies it to `overflow: hidden` the way it does to `overflow-y: auto`.
Measured it on the running app across the three engines Playwright ships,
and the property does not hold up.

  engine    .input / .backdrop / .mirror wrap width
  chromium  776 / 776 / 776   (768 / 768 / 768 with the declaration)
  firefox   776 / 776 / 776   (unchanged by it — overlay scrollbar)
  WebKit    768 / 776 / 776   (unchanged by it)

WebKit reserves for `overflow-y: auto` and not for `overflow: hidden`, so the
declaration left .input at 768 against 776 — exactly the gap it was meant to
close — on the one engine where that gap is observable at all, while costing
every chromium user 8px of text column unconditionally. Reverted: the
composer's metrics are now the same as before this PR.

The WebKit gap predates this change and is not closed here. It is recorded in
the Agent Note with the numbers, and the browser scenario asserts the equality
on the lane's engine so a regression into that state fails loudly. The mirror
is unaffected on WebKit for the drafts measured — the extents still agree — but
a draft whose wrapping turns on those 8px would clamp it.

The review's monotonicity concern resolves the same way: the declaration was
never worse than master, because WebKit already measured 768 against 776
without it. It simply was not better.

Also from this round: the wrap-width assertion now covers .mirror as well as
the two glyph layers — it is the height authority, so a mirror alone wrapping
wider would measure the box short and clip content below the 14-line cap with
every other assertion green. Plus `renderGeometry`'s missing `@param
trailingNewline`, and both e2e tsconfig lists restored to alphabetical order.
2026-07-31 13:13:50 +08:00
kingwl 8d36353157 docs: archive superseded TUI path note 2026-07-31 12:58:46 +08:00
kingwl 81ff2894ca fix(tui): bound diff rendering work 2026-07-31 12:58:30 +08:00
creatixchu 2143361195 fix(web): reserve one scrollbar gutter across the composer's text layers
Second review round escalated the wrap-width divergence from a separate
concern to a defect in this fix's own premise, and it is right.

Only .input scrolls, so only .input loses content width to a scrollbar that
consumes layout space — what Windows and Firefox draw, and what the theme's
global `::-webkit-scrollbar` width makes chromium treat as occupying space.
A narrower .input wraps a long soft-wrapped draft onto more lines, so it
grows taller, its scroll maximum exceeds the backdrop's, and the mirrored
offset clamps below the caret. That is the same failure the trailing-line
sentinel fixes, in the same direction, so deferring it would have shipped a
fix that does not hold where users run a classic scrollbar.

My first attempt to reproduce it found nothing and was wrong: the probe
content was not wrap-sensitive. With varied-length words the effect is
plain — the same draft laid out at 8px-apart widths differs by 2 to 5
lines, while at equal widths a textarea and a div agree exactly.

The three layers now reserve the gutter together, in the shared metrics
block that already exists to keep them symmetric. `overflow: hidden` is
still a scroll container, so the non-scrolling layers honour it: 8px is
reserved on each, measured. The cost is a text column 8px narrower on every
platform, which is the price of one geometry rather than a per-platform one.

The browser scenario asserts the premise directly — equal wrap widths, and a
reserved band greater than zero on each layer. The band is what stops the
assertion being vacuous: the widths would also match with no reservation at
all on this engine's overlay scrollbar, and it is the reservation, not the
match, that carries the guarantee to a platform whose scrollbar takes real
width. Removing the declaration fails it with `expected 0 to be greater
than 0`, and fails the golden with it.

Also from the same round, three comment corrections: the e2e file header no
longer describes the deleted layout effect, the measurement guard no longer
claims the backdrop holds exactly one text node (the sentinel makes a
second), and the sentinel comment now carries the one-sidedness argument
that also settles the ghost hint — the mirror only fails when the backdrop
is SHORTER, and the hint can only add content, never remove a line box.
2026-07-31 12:39:52 +08:00
creatixchu f8ef2cf36b fix(web): give the backdrop the trailing-line sentinel so the layers share one extent
Review caught a real divergence the earlier measurements missed: mirroring
an offset is only correct while both layers can reach it, and for a draft
ending in a newline the backdrop could not.

A textarea reserves a line box for the caret after a final newline.
`white-space: pre-wrap` collapses a text node's trailing newline and
generates none. So a draft ending in a newline made the backdrop exactly one
line shorter than the textarea — measured 628 against 652 — and the mirrored
assignment clamped, leaving the glyphs one line behind the caret at the very
bottom of the draft.

The backdrop now carries the same trailing-line sentinel the mirror div has
carried all along: its content is the decoration walk plus one newline. The
same pre-wrap collapse absorbs it when the draft does not end in a newline,
so it costs no height in the ordinary case, and it supplies the missing line
box when it does. Verified in isolation first: a bare pre-wrap div measures
180/180/198 against a textarea's 180/198/216 for zero, one and two trailing
newlines, and 180/198/216 with the sentinel.

Coverage for the shape that exposed it: the browser scenario asserts the two
extents are equal before asserting the glyphs reach the end, observing each
layer's maximum by asking for an impossible offset and reading back the
clamp rather than computing it from scrollHeight, and the golden records the
relation. The unit spec pins the backdrop's text as the draft plus exactly
one newline. Removing the sentinel fails both, the e2e with the same 628
against 652.

The scrollbar-gutter half of the same review point does not reproduce here:
both layers measure clientWidth 776 against a border box of 776 while the
draft overflows, so this engine's textarea scrollbar is an overlay and takes
no width out of the wrap.
2026-07-31 12:12:54 +08:00
creatixchu 07448093c5 fix(web): drop the redundant second mirror
The first version coupled the layers from two places: a `scroll` listener
and a layout effect keyed on the committed draft. Mutation-testing each
hook alone against the built client shows the effect never fires the only
assignment that matters — with just the layout effect disabled the browser
scenario stays green, while disabling just the listener fails it.

Both premises behind the effect were wrong. Typing scrolls the caret into
view, which is an ordinary `scroll`. A draft that shrinks past the current
offset clamps both layers to the same maximum, because their extents are
equal — measured in chromium at 964/964, 1012/1012, 844/844 and 820/820 for
plain, soft-wrapped, unbreakable-run and highlighted drafts — and the
textarea's clamp fires `scroll` too.

The hazard the effect was imagined to cover does not exist either: React
replacing every child of the backdrop when the decoration set changes shape
preserves `scrollTop` (measured: 300 stays 300 through a full child
replacement), and the only replacement that zeroes it shrinks the content
below the offset, which is the clamp case already covered.

The e2e's edit case survives, retitled to say what it actually pins: that
typing is not a separate case needing its own mirror. The unit spec now
asserts the backdrop tracks a second move back to the top, which a one-shot
mirror would fail.
2026-07-31 12:02:38 +08:00
creatixchu a7b7066267 fix(web): scroll the composer's glyph layer with its textarea
A composer draft past the 14-line cap could not be scrolled: the caret and
the selection moved, but the words stayed frozen at line 1, so the tail of
anything longer than the cap was unreachable while writing it.

The composer paints its text in two stacked layers. The textarea owns the
value, the selection and the caret but renders its own glyphs transparent;
every visible character is painted by the decoration backdrop beneath it,
which also carries the claim-token highlight, the chips and the ghost hint.
The backdrop is `inset: 0; overflow: hidden` — clipped, not scrolled — and
nothing linked its offset to the textarea's. Below the cap both layers rest
at 0, which is why the defect hid behind every short-draft screenshot and
fixture.

InputBar now mirrors the textarea's scrollTop onto the backdrop, from a
`scroll` listener (every gesture and every caret-driven scroll) and from a
layout effect keyed on the committed draft (an edit reflows both layers
without necessarily firing a scroll event).

Scrolling is layout, so jsdom cannot show this: the unit spec stubs both
offsets and proves the mirroring paths run, while a new browser scenario
measures the user-visible fact against the built client with a DOM Range
over the backdrop's own text — after a wheel gesture over a 40-line draft
the last line is on screen and the first has scrolled out. Confirmed both
directions: with the mirroring reverted and the packages rebuilt, the
golden reads `last draft line is on screen: false` while `textarea moved:
true`.
2026-07-31 11:53:04 +08:00
kingwl e98cd522ee fix TUI diff context line accounting 2026-07-31 11:41:16 +08:00
Turtle 899d25dfb3 fix(tui): defer model-context resolution on the adapter-registration race
Loader activation is service-driven, so the TUI can mount before a
configured adapter plugin registers its provider routes; every fresh
session then printed 'Could not resolve model context: no adapter
registered for provider …' for a working configuration.

The model controller now treats a NO_ADAPTER rejection of the
context-window resolution as transient: it parks the resolution
silently and re-resolves on the next llm/adapters-updated commit. A
commit that still lacks the route parks the wait again; any target
change clears it; all other resolution errors still surface. A wrong
provider name keeps failing loudly at dispatch, where it is actionable.
2026-07-31 11:10:05 +08:00
Hypatia May afe8d49691 Merge finalized manual compaction parent 2026-07-31 10:11:54 +08:00
Hypatia May cc0d13872f docs(client): correct checkpoint pin contract 2026-07-31 09:29:19 +08:00
fz ed49f1d985 fix(compact): use English checkpoints 2026-07-31 01:36:20 +08:00
Hypatia May ceb37f3377 Merge final master into Web transcript 2026-07-31 00:27:16 +08:00
imccyu c6c0f5de6a Merge branch 'master' into worktree/approval-panel-max-height 2026-07-30 23:49:19 +08:00
Hypatia May bbff2c58de feat(tui): show live compaction progress 2026-07-30 21:59:33 +08:00
Tianyi Cui 99db7e8fc3 Merge remote-tracking branch 'origin/master' into feature/directory-listing-tool 2026-07-30 21:35:08 +08:00
Tianyi Cui 6777e4fab9 fix(fs-search): make glob sampling an explicit choice 2026-07-30 21:14:03 +08:00
Hypatia May 5ac766fd08 Merge latest human transcript base into web transcript
Build-review stack propagation round 1.
2026-07-30 21:04:33 +08:00
Tianyi Cui 21e8af2b3a Merge remote-tracking branch 'origin/master' into feature/directory-listing-tool
# Conflicts:
#	packages/fs/tool-fs-search/README.i18n.yaml
#	packages/fs/tool-fs-search/README.zh.md
2026-07-30 20:56:27 +08:00
Hypatia May acca880015 Merge latest origin/master into fix/human-transcript-projection
Build-review integration round 2.
2026-07-30 20:51:45 +08:00
creatixchu 2cac565383 fix(web): let the pointer reach hover cards and row menus
The workspace browser's two hover-raised popups both died on the way to
them. HoverCard closed on the first pointerleave and rendered its card
pointer-events:none, but the card sits 8px off the anchor, so every path
to it crossed ground belonging to neither. The row action menus put
closeOnPointerLeave's handler on the portaled list, so aiming back at the
... trigger that opened it, or overshooting a list edge, closed it with no
window to come back.

usePointerGrace owns one cancelable delayed close (200ms) shared by both
atoms: leaving arms it, returning cancels it. The hover card becomes
hit-testable so resting on it holds it open, and Menu moves pointer-leave
dismissal to the wrapper span, where React's enter/leave traversal makes
trigger and portaled list one region.

Both gestures are pinned in the real browser lane; each fails without the
corresponding fix.
2026-07-30 20:25:02 +08:00
Hypatia May c2337bfa10 Merge remote-tracking branch 'origin/fix/human-transcript-projection' into fix/web-transcript-projection
# Conflicts:
#	apps/web/tests/scaffold.ts
#	apps/web/tests/seeded-history.e2e.ts
#	apps/web/tests/snapshots/seeded-history/ui.expected.md
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/src/client/chat/MessageItem.tsx
2026-07-30 20:02:32 +08:00
Hypatia May fe97451604 Merge remote-tracking branch 'origin/master' into fix/human-transcript-projection
# Conflicts:
#	.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml
#	docs/cordis-catalog/services.md
#	packages/host/apiproxy/README.i18n.yaml
#	packages/ui/tui/README.i18n.yaml
2026-07-30 19:57:49 +08:00
Yichen Jiang e76b15e8ad Merge remote-tracking branch 'origin/master' into worktree/fix-multi-select-custom-answer
# Conflicts:
#	apps/cli/tests/fixtures/tui-scripted-llm.ts
#	packages/host/apiproxy/README.i18n.yaml
#	packages/ui/tui/README.i18n.yaml
2026-07-30 19:46:51 +08:00
creatixchu f5ab6d94a8 Merge remote-tracking branch 'origin/master' into worktree/approval-panel-max-height 2026-07-30 18:59:06 +08:00
creatixchu 12b55adf6b fix(web): keyboard reach and resting metrics on the approval scroll region
Review follow-ups on the approval takeover:

The scroll region is now a tab stop (tabIndex + named role="group"). The
question composer's scroll body needs none — its option rows are focusable and
pull the container along — but this one holds nothing but text, so a
keyboard-only user could reach the buttons and never the command's tail, and
approve what they could not finish reading.

The action row's padding reproduces the 14px gap it had inside the body: the
flex gap of 6 plus its 8px top margin, neither of which reaches it now that the
row sits outside the scroll region. The resting card is unchanged again.
2026-07-30 18:35:20 +08:00
creatixchu 14482fcef1 fix(web): keep the approval scenario's goldens platform-neutral
The answered-transcript golden captured the OS's own refusal of the denied
first attempt — "bash: notes.txt: Operation not permitted" on macOS against
"bash: line 1: notes.txt: Read-only file system" on Linux — so it passed
locally and failed the Linux snapshot lane.

The scenario now keeps one golden (the waiting panel, platform-neutral) and
asserts the answered state on the world instead: the decided outcome, the file
the escalated command actually wrote, DONE, the panel gone, and the composer
re-enabled. The file assertion is stronger evidence than the transcript dump
it replaces — it proves the grant reached the executor.
2026-07-30 18:11:05 +08:00
creatixchu e080ff2c34 fix(directory-picker-browse): resolve quiet-navigation review 2026-07-30 18:08:22 +08:00