Commit Graph
8 Commits
Author SHA1 Message Date
6ec1b83044 feat(web-shell): delete selected message groups from session history (#14297)
* feat(web-shell): add grouped message deletion with durable history updates

* fix(web-shell): repair message deletion CI contracts

* docs(serve): index message deletion protocol routes

* fix(web-shell): harden grouped deletion recovery and export build

* fix(web-shell): allow selecting all message groups for deletion

* test(cli): observe hook cancellation rejection before waiting

---------

Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-10-20 13:06:23 +08:00
易良 0a42064649 fix(web-shell): move the hover timestamp off the message text (#11380)
* fix(web-shell): stop the hover timestamp from garbling message text

`MessageTimestamp`'s hover chip is absolutely positioned at the row's
top-right with a 92%-opaque background, so it paints over the first line of
the message underneath and lets the glyphs bleed through it. The first line of
a wrapped message runs the full column width, so it is almost always the one
that gets hit: in the web-shell visual preview for #11267 the turn-error copy
"...before the answer finished." renders as "finisheu." beneath an 09:18:16
tip, which reads as a corrupted error message rather than as an overlay.

Anchor the chip at the bottom-right instead and make its background opaque.
The last line of a wrapped block is a ragged remainder, and rows that carry an
action bar - the turn-error Copy button is one - leave that corner empty
outright, so the common case no longer overlaps text at all; where it still
does, an opaque chip with a border reads as an overlay instead of as broken
characters. `.chatTip` and `.copyButton` share the same translucent
background over content that `.chatActions` pulls up by -24px, so they get the
same treatment.

This makes overlap rare, not impossible - a message ending in a full-width
code block or table still gets the chip in its corner. Reserving space in flow
was rejected on purpose: adding padding on hover reflows the transcript, and a
permanent float notch shrinks every code block that establishes its own
formatting context.

* test(web-shell): freeze the wall clock for visual captures

The visuals job renders the PR head, then reinstalls dependencies and renders
the merge-base, so the two passes photograph the same view minutes apart. Any
view showing a wall-clock time therefore differed on every run for no reason
other than when it was taken. On #11267 that was the entire preview: the only
view the compose step reported as CHANGED was
`terminal-turn-error-copy-narrow-dark` at exactly the 0.02% threshold, and the
whole diff was a 09:09:28 timestamp against a 09:18:16 one. The narrow
viewport is half the pixels of the wide one, which is why the same ~100
timestamp pixels cleared the threshold there and were skipped at 0.01%
everywhere else - so the noise also crowded out any real change the PR made.

Pin `Date.now()` / `new Date()` with `page.clock.setFixedTime` from
`gotoSession` and `gotoNewSession`, which every visuals spec navigates
through, including the Pixel 7 touch page. `setFixedTime` fakes only clock
readings and keeps timers running, so replay, streaming and
`freezeLoopingAnimations` are unaffected; elapsed-duration UI settles on a
constant instead of drifting, which is the point. This is the same class of
fix as `freezeLoopingAnimations`, and `harness.spec.ts` pins its contract the
same way.

`formatTimestamp` renders local time, so pin `timezoneId: 'UTC'` alongside
it. CI runners are already UTC, so this changes no baseline there; it is what
makes a local capture match them. Contexts created directly from the browser
do not inherit `use` from the config, so the flow-recording context and the
touch context repeat it.

* fix(web-shell): future-date the frozen capture clock

The first pass froze captures at 2026-01-01, which is BEFORE dates the
fixtures hardcode. `formatRelativeTime` measures `Date.now() - value`, so the
channel editor's pairing requests, dated 2026-07-28, came out negative and
collapsed to "just now" where the real clock rendered "7/28/2026" -- visible
as the only unexplained CHANGED pair in this PR's own preview
(`channel-editor-existing-{dark,light}`).

Move the constant to 2027-01-01. Future-dating is the safe direction: every
fixture then reads as older than now, which is what all of them mean, and the
relative formatter lands in the same branch the live clock did.

`git-branch-picker` seeded `computedAt` from `Date.now()`, which in a spec is
Node's real clock rather than the page's frozen one. The status therefore
looked newer than the branch listing and armed a reconcile re-fetch that no
real session performs; it now derives from the same constant. Both rules are
written into the constant's docstring for the next fixture.

* test(web-shell): make the turn-error fixture identify itself

The visual harness injects a fake `turn_error` so the error row's Copy button
can be photographed, but its copy was written to read like a real incident --
"The model provider closed the response stream before the answer finished.
Retry the request or copy these details when reporting the failure." One test
produces four screenshots (two themes x two viewports), so every web-shell
preview shows that sentence four times, and readers keep taking the preview
for a live failure rather than for mock data. It is the top question this
preview generates.

Nothing about the scenario needs realistic copy; it needs an error row. Say
what the row is instead, and follow the string in the two locators that filter
on it.

* revert(web-shell): drop the fixture recopy, the timezone pin and the tip move

Three of the changes on this branch were not root-caused before being made,
and this reverts them to what the reported symptom actually justifies.

The tip no longer moves to the bottom-right. The symptom -- "finished."
reading as "finisheu." -- comes entirely from the chip's 92%-opaque
background letting the glyph beneath bleed through it, so the fix is the one
word that makes it opaque. Repositioning was a second, bundled change: more
correct in that it stops covering a word at all, but not what was reported,
and it is what churned six screenshots. `.chatTip` and `.copyButton` carry the
identical translucent background over content, so they change with it.

The turn-error fixture keeps its original copy. Rewording it fixed no bug and
measurably backfired: the touch pair started differing BECAUSE of the edit, so
the preview went from four red screenshots to six, and the replacement text
was a line longer. The red rows are the subject of the only visual test that
hovers a message row -- the Copy affordance shipped in #10001 -- not evidence
of a failure, and no wording makes them look otherwise.

The `timezoneId: 'UTC'` pins go too. CI runners are already UTC, so they
changed no baseline and only served a local reproduction nobody had asked for.
The `harness.spec.ts` clock case goes with them: `freezeWallClock` is a
one-line wrapper, so the case asserted that Playwright's own `setFixedTime`
works. `freezeLoopingAnimations` keeps its case because that function has real
logic to pin.

What survives is what measurement supports: the opaque background, the frozen
clock with its future-dated constant, and the `git-branch-picker` fixture that
the freeze would otherwise desynchronise.

* test(web-shell): drop the touch turn-error captures, keep their assertions

The touch variant renders under `hover: none`, so `.row:hover > .tip` never
fires and the hover timestamp is absent from those two screenshots entirely.
A run that moved the chip and repainted its background scored both of them at
0% diff, which is direct evidence they carry no coverage of it -- they only
contributed two full-height red error images to every web-shell preview.

The behaviour they were meant to protect is the one #10001 shipped: on a touch
device the row actions stay visible with no hover to reveal them. That is
asserted directly (`toHaveCSS('opacity', '1')` plus a visible Copy button) and
those assertions stay. Only the captures go.

The four remaining turn-error captures stay for now: they are the only views
in the suite where the hover timestamp lands on real text, so they are the
sole visual evidence for the opacity fix on this branch.

* fix(web-shell): move the hover timestamp off the first line of the message

Reverting the chip to the top-right was wrong, and zooming into the captured
pixels is what showed it. The chip's lower edge cuts horizontally through the
first line of the message; it does not cover the last word so much as
decapitate it, removing a letter's ascender while the bowl stays visible
underneath. That is the whole of the reported symptom: "finished." reads as
"finisheu." because only the bowl of the d survives.

An opaque background therefore cannot fix it. The faint remnant of the
ascender currently showing through the 92% chip is the only hint the letter is
a d at all -- painting the chip solid erases that hint and leaves a cleaner
"u". The previous commit would have made the reported symptom slightly worse.

Anchoring at the bottom-right is what removes the overlap: the last line of a
wrapped block is a ragged remainder, and rows carrying an action bar leave
that corner empty outright. Opacity is kept for its own reason -- a glyph
bleeding through any overlay reads as broken text -- and the border makes the
chip legible as an overlay where it does still land on content.

* test(web-shell): stop capturing the turn-error scenario

This scenario is the only visual test that hovers a message row, which it must
do to reveal the Copy affordance shipped in #10001. The hover timestamp is
`opacity: 0` until `.row:hover`, so those captures were the only place in the
whole suite where a hover-timestamp change could appear -- and the six
screenshots they produced meant every web-shell preview led with full-height
red error images, whatever the PR had actually touched. Readers repeatedly
took the preview for a live failure.

They have now done the one job that justified keeping them: run 34231051803
proved the overlap fix at pixel level, with the d in "finished." regaining its
ascender at full h-height while the base panel still shows "finisheu.". That
evidence is recorded in the PR description, where it does not have to be
re-rendered on every unrelated push.

The behaviour stays pinned by assertion, not by image: seven opacity checks
across the default viewport, the 720px viewport and the Pixel 7 touch context
cover reveal on hover, reveal on focus, hide on blur, and always-visible
actions under `hover: none`. A capture never verified any of that; Playwright
does not diff these images, the preview bot does, and the preview is a review
aid rather than a test.

The scenario, the fixture and every assertion remain. Only the captures go.

* test(web-shell): fail when a fixture date outlives the frozen capture clock

Captures render at a fixed instant, so a fixture dated after it silently reads
as "just now": `formatRelativeTime` measures `Date.now() - value`, a future
value yields a negative age, and that lands in the `mins < 1` branch. This
already happened on this branch. The first frozen constant sat at 2026-01-01,
before the channel editor's 2026-07-28 pairing requests, and turned "7/28/2026"
into "just now" across four views. Nothing failed -- no assertion covers that
label -- and it was caught only by reading a preview diff by eye.

Scan the visuals specs for ISO date literals and require each to precede
FIXED_CAPTURE_TIME. The docstring already asked for this; a comment does not
fail a build. `harness.ts` is skipped because it is where the constant itself
is written, so its literal is the boundary rather than a fixture.

The scan reaches further than the fixture that caused the incident: it covers
eleven literals across the suite, eight of them a 2026-07-03 pair I had not
been aware of. Verified in both directions -- silent against the tree as it
stands, and flagging a planted 2027-06-01 literal.

Not a general date linter. It catches ISO strings in fixtures, which is the
shape the regression actually took; an epoch number is already opaque enough
that nobody reads it as a date.

* test(web-shell): fail when a fixture date outlives the frozen capture clock

Captures render at a fixed instant, so a fixture dated after it silently reads
as "just now": `formatRelativeTime` measures `Date.now() - value`, a future
value yields a negative age, and that lands in the `mins < 1` branch. This
already happened here. The first frozen constant sat at 2026-01-01, before the
channel editor's 2026-07-28 pairing requests, and turned "7/28/2026" into
"just now" across four views. Nothing failed -- no assertion covers that label
-- and it surfaced only by reading a preview diff by eye.

Scan the visuals specs for ISO date literals and require each to precede
FIXED_CAPTURE_TIME. The constant's docstring already asked for this, and a
docstring does not fail a build. `harness.ts` is skipped because it is where
the constant itself is written, so its literal is the boundary, not a fixture.

The scan reaches further than the fixture that caused the incident: eleven
literals across the suite, eight of them a 2026-07-03 group that was not on my
radar. Checked in both directions before committing -- silent against the tree
as it stands, and flagging a planted 2027-06-01 literal.

Not a general date linter. It catches ISO strings in fixtures, which is the
shape this regression actually took; an epoch number is already opaque enough
that nobody reads it as a date.

b162c19 was meant to carry this and landed empty: the scratch copy of the file
was overwritten by a later extraction step before it was hashed, so the commit
recorded no change while its message described one. History is append-only on
an open PR here, so it stays in the log; this commit is the content.

* fix(web-shell): correct three wrong comments and give the contracts a gating home

Review found eight issues on this branch. Three were comments asserting things
the code does not do, and comments that confidently state the opposite of the
behaviour are worse than no comment.

The CSS block kept its original opening sentence, "Anchored inside the
message's top-right corner", above a paragraph declaring the BOTTOM-right
anchor and a `bottom: 2px` rule. The retained sentence also carries the
overflow rationale that is still true, so it reads as the block's thesis --
someone "restoring" `top: 2px` to match it puts the chip back over the first
line and reintroduces the exact decapitation this branch exists to remove.
Only the corner is dropped; the overflow reason stays.

The `git-branch-picker` fixture comment claimed a Node-stamped `computedAt`
would look NEWER than the branch listing and arm a reconcile re-fetch. The
constant is future-dated, so a real-clock value sits months BELOW
`listingFetchedAt`, and the gate is `at <= listingFetchedAt` -- the branch was
already unreachable and the edit is inert today. The comment now says what the
edit is for (the harness rule about "now" values) and what would actually
exercise the reconcile path.

The `freezeWallClock` docstring said `setFixedTime` fakes clock readings and
not timers. It installs Playwright's full fake clock: timers, rAF, rIC,
`performance` and `Intl` are all replaced. Outcomes are unaffected today, but
`performance.mark`/`measure` are stubbed and every `Date.now()`-delta window
in the app is pinned permanently shut. Nothing seeds those states, so nothing
fails -- until a scenario needs one to elapse and hangs as a bare expect
timeout while the docstring insists timers behave normally.

`.toolTimeTip` in SubAgentPanel mirrors this chip over a sub-tool line, was
missed by a paragraph claiming to sweep the overlays, and was strictly worse:
same `top: 2px` geometry, no `background` at all, so two texts composited
directly. It gets the same treatment, and the paragraph now names it.

The remaining three findings share a root: nothing observes any of this.
`.tip` is `opacity: 0` until hover and no capture paints it any more, so
reverting the anchor or the background keeps every suite green.
`freezeWallClock` runs only implicitly and no assertion reads the page clock,
so dropping the call resumes the drift silently. And the date guard sat in
`harness.spec.ts`, which no gating config collects -- `vitest.config.ts`
excludes `e2e/**`, `playwright.config.ts` ignores `visuals/**`, and the one
workflow step that does run it is deliberately `continue-on-error: true` so a
failing scenario cannot discard the preview.

`visual-capture-contracts.test.ts` is a plain vitest file outside `e2e/`,
collected by `test:ci:workspaces`, pinning all three: fixture dates precede
the frozen clock, both navigation helpers freeze before they navigate, and
every hover chip stays bottom-anchored and opaque. The date scan also now
reads `e2e/utils`, where `mockDaemon.ts` holds the date the default
scenario's session rows actually render -- scanning only `visuals/` missed it,
which is what the review caught. Its reach is still ISO string literals; the
comment says so rather than implying the name's full promise.

`FIXED_CAPTURE_TIME` moves to `constants.ts` so the guard can import it
without pulling in `@playwright/test`.

Each guard was checked in both directions before committing: silent against
the tree as it stands, and firing on a planted 2027-06-01 date in
`mockDaemon.ts`, a `freezeWallClock` moved after `page.goto`, and a `.tip`
reverted to `top`.

* fix(web-shell): stop claiming the sub-tool chip moved off its row

Moving `.toolTimeTip` to the bottom of its row was inert, and the comment
saying it 'inherits its fix for the same reason' overstated it the same way
three comments this branch already had to correct.

The row has no vertical slack to move into. `.line` is `padding: 2px 0` around
13px text, so roughly 17-24px tall, and `.lineArg` is `white-space: nowrap` so
it never wraps to a second line. The chip is about 19px once padding and a
border are added -- as tall as the row it sits in. `bottom: 2px` therefore
lands where `top: 2px` already was, and the padding and border I added only
made the chip taller than the row on the narrow end of that range.

What was actually wrong there is that the chip declared no `background` at
all, so its digits overprinted the row's text rather than covering it. That is
the whole fix: one declaration, no geometry change. The text under the chip is
still covered -- cleanly now instead of overprinted -- and the comment says so
rather than implying the overlap is gone.

`.tip` keeps its bottom anchor, which does work: a message wraps, so its last
line is a ragged remainder and rows carrying an action bar leave that corner
empty outright.

The contract test splits along the same line: every chip must be opaque, only
`.tip` must be bottom-anchored. Asserting a bottom anchor for a row that has
nowhere to put one would have pinned a cosmetic no-op as if it were the fix.

* docs(web-shell): correct the claim that only turn-error paints the chip

The comment left where the captures were removed said this scenario's
screenshots were "the only place a hover-timestamp change could show up".
That is false, and this PR's own preview disproves it: the parallel agents
test leaves the cursor on the group header after `summary.click()`, so
`parallel-agents-expanded` paints the chip through residual hover -- and it is
the only view still reported as CHANGED here, at 0.08%, precisely because the
chip moved.

The distinction that survives is deliberate versus incidental. This test is
the only one that hovers on purpose; the parallel agents coverage is a side
effect of a click and would vanish the moment that test stopped clicking.
Neither is what pins the chip now -- visual-capture-contracts.test.ts is.
2026-09-09 08:51:02 +00:00
ytahdnand钉萁 f2de42dec4 feat(web-shell): improve compact tool activity (#8973)
* feat(web-shell): improve compact tool activity

* fix(web-shell): preserve tool timing metadata

* fix(web-shell): address compact activity review

* fix(web-shell): preserve customized tool metadata

* test(web-shell): cover completed agent tokens

---------

Co-authored-by: 钉萁 <[email protected]>
2026-08-12 12:48:08 +00:00
ytahdnandytahdn 4fd72b3ce3 feat(web-shell): polish chat UI and table rendering (#6099)
* feat(web-shell): polish chat UI and table rendering

* fix(web-shell): address review follow-ups

---------

Co-authored-by: ytahdn <[email protected]>
2026-07-01 16:20:19 +08:00
ytahdnandytahdn a384c4a21a feat(web-shell): polish chat UI (#5893)
* style(web-shell): polish chat UI

* fix(web-shell): address chat polish review feedback

---------

Co-authored-by: ytahdn <[email protected]>
2026-06-26 15:38:23 +00:00
ytahdnandytahdn 83e09f2b5b refactor(web-shell): restructure chat UI (#5775)
Co-authored-by: ytahdn <[email protected]>
2026-06-23 22:43:19 +08:00
ytahdnandytahdn 9641532a10 Polish web-shell execution display (#5190)
* fix(web-shell): correct prompt turn metrics

* feat(web-shell): clarify turn execution traces

* fix(web-shell): stabilize thinking trace display

* style(web-shell): polish execution display

* style(web-shell): refine command and tool displays

* fix(web-shell): address review feedback on execution display polish

- Remove unused trace field and withTrace() helper from DisplayItem
- Simplify getRowClassName to a single ternary, drop unused index param
- Restore !content guard on thinking preview tail-scroll
- Add CSS comment documenting margin-left cross-file coupling
- Rename isNonAssistantStep to isExecutionWorkStep for clarity
- Replace APL glyphs (⌄/⌃) with Geometric Shapes (▼/▲)
- Add keyboard accessibility tests for tool row expand (Enter/Space)

* fix(web-shell): address execution display review

---------

Co-authored-by: ytahdn <[email protected]>
2026-06-16 23:07:21 +08:00
Shaojin Wen 84d01e7070 feat(web-shell): show message time on hover (#5079)
* fix(acp-bridge): preserve original timestamp when replaying session history

History replay re-emits each persisted record with its original epoch-ms time nested in update._meta, but BridgeClient.sessionUpdate published the frame without lifting it to the envelope. EventBus.publish then stamped envelope _meta.serverTimestamp with publish-time Date.now(), which the client's extractServerTimestamp picks up at higher priority than the nested original — so a resumed session rendered every historical message at the resume moment instead of when it was sent.

Lift update._meta.timestamp (or serverTimestamp) to the envelope serverTimestamp so EventBus preserves it. Live updates without such a timestamp keep the Date.now() fallback unchanged.

* feat(web-shell): show each history message's time on hover

Carry each transcript block's wall-clock time (serverTimestamp ?? clientReceivedAt) onto every message and reveal it as a CSS-only hover tooltip in the message list. Same-day messages show HH:mm:ss; older ones show yyyy-MM-dd HH:mm:ss (local time, zero-padded).
2026-06-13 15:30:44 +08:00