Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
67cafa4
test(next): cover transcript gap selection
teamleaderleo Oct 28, 2026
94752a3
fix(next): make transcript gaps selectable
teamleaderleo Oct 28, 2026
25fa50f
test(next): cover selection gaps across transcript row types
teamleaderleo Oct 28, 2026
36287f3
fix(next): resolve padding presses to visible text carets
teamleaderleo Oct 28, 2026
6aa9d71
test(next): typecheck gap selection matrix
teamleaderleo Oct 28, 2026
2904d9f
fix(next): preserve transcript control padding clicks
teamleaderleo Oct 28, 2026
6eb2194
fix(next): ignore invisible transcript text anchors
teamleaderleo Oct 28, 2026
f993adf
fix(next): honor selectable text beneath nonselectable rows
teamleaderleo Oct 28, 2026
dfbd608
test(next): aim gap drags clear of tool controls
teamleaderleo Oct 28, 2026
710ef50
test(next): verify the nearest gap anchor and native prompt text
teamleaderleo Oct 28, 2026
28a929e
test(next): cover measured inter-row selection midpoints
teamleaderleo Oct 28, 2026
565b534
Merge feat-cmux-next into transcript gap selection
teamleaderleo Oct 28, 2026
b467c56
test(chat): cover RTL gap selection endpoints
teamleaderleo Oct 28, 2026
c79669d
fix(chat): honor line direction when anchoring gap selections
teamleaderleo Oct 28, 2026
0cc3b24
test(transcript): protect sender chips from gap selection
teamleaderleo Oct 28, 2026
e1a4c0c
fix(transcript): mark non-button controls as interactive
teamleaderleo Oct 28, 2026
ab22cba
test(transcript): verify generic controls retain pointer ownership
teamleaderleo Oct 28, 2026
b42bd56
test(transcript): preserve native selection in shadow text
teamleaderleo Oct 28, 2026
e3c2338
fix(transcript): preserve native shadow text selection
teamleaderleo Oct 28, 2026
fa1c5d8
test(transcript): stabilize shadow text fixtures
teamleaderleo Oct 28, 2026
b6f10f5
test(transcript): keep shadow text presses inside the fixture
teamleaderleo Oct 28, 2026
6380187
test(transcript): use a plain shadow fixture host
teamleaderleo Oct 28, 2026
7fdf1c4
test(transcript): assert shadow text stays isolated
teamleaderleo Oct 28, 2026
1cac423
test(transcript): select actual Pierre code and diff text
teamleaderleo Oct 28, 2026
682f9e3
Merge feat-cmux-next into reconcile-26704
teamleaderleo Oct 29, 2026
1251af7
fix(chat): own transcript gap padding hits
teamleaderleo Oct 29, 2026
c2d5efa
fix(chat): anchor gap selection by row geometry
teamleaderleo Oct 29, 2026
504d3fc
test(chat): retain nearest text gap resolution
teamleaderleo Oct 29, 2026
67952da
fix(chat): anchor gap boundaries at text edges
teamleaderleo Oct 29, 2026
24d462e
fix(chat): select only explicit transcript gaps
teamleaderleo Oct 29, 2026
093474a
test(chat): give tool gap gallery an actual gap
teamleaderleo Oct 29, 2026
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
8 changes: 8 additions & 0 deletions tests/test-execution.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2175,6 +2175,14 @@ lane = "linux-guard"
path = "tests/test_zig_fetch_all.py"
lane = "linux-guard"

[[test]]
path = "tests/test_ci_runs_on_fallback.py"
lane = "linux-guard"

[[test]]
path = "tests/test_workflow_file_size.py"
lane = "linux-guard"

[[test]]
path = "tests/test_swift_linux_packages.py"
lane = "linux-guard"
4 changes: 4 additions & 0 deletions webviews/src/agent-session/acpmux/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1776,6 +1776,10 @@ const RowFrame = React.memo(function RowFrame({
/>
)}
<ReferenceScopeContext.Provider value={referenceScope ?? null}>{children}</ReferenceScopeContext.Provider>
{/* The row's own trailing padding is the only row-local empty surface owned by transcript
selection. Keeping it as a separate hit target means content controls (including native
media) never need to be enumerated by the gap handler. */}
<span className="acpmux-row__gap" data-transcript-gap="row" aria-hidden="true" />
</article>
);
}, sameRowFrame);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export function AgentMessageRow({
{/* The words toggle too, for the pointer; the toggle button is the control, and its label
reads the line. */}
{before.trim() && (
<span className="cv-agentmsg__words" aria-hidden onClick={onToggle}>
<span className="cv-agentmsg__words" data-interactive="" aria-hidden onClick={onToggle}>
{before.trim()}
</span>
)}
Expand All @@ -107,12 +107,18 @@ export function AgentMessageRow({
{name}
</button>
) : (
<span className="cv-agentmsg__name is-plain" aria-hidden title={nameTitle} onClick={onToggle}>
<span
className="cv-agentmsg__name is-plain"
data-interactive=""
aria-hidden
title={nameTitle}
onClick={onToggle}
>
{name}
</span>
)}
{after.trim() && (
<span className="cv-agentmsg__words" aria-hidden onClick={onToggle}>
<span className="cv-agentmsg__words" data-interactive="" aria-hidden onClick={onToggle}>
{after.trim()}
</span>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ function Card({
{/* oxlint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
className={single ? "acpmux-edited-title acpmux-edited-title--toggle" : "acpmux-edited-title"}
data-interactive={single ? "" : undefined}
onClick={single ? toggleDiff : undefined}
>
<div>{title}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ export function IncomingMessageLine({
id={bodyId}
className="cv-received__body selectable"
data-cut={cut || undefined}
data-interactive={cut ? "" : undefined}
onClick={cut ? openCut : undefined}
>
{/* The chips read whose files these are (chips/messageScope.ts). */}
Expand Down Expand Up @@ -385,7 +386,7 @@ export function IncomingMessageLine({
{from}
</button>
) : (
<span className="cv-received__name is-plain" aria-hidden onClick={toggle}>
<span className="cv-received__name is-plain" data-interactive="" aria-hidden onClick={toggle}>
{from}
</span>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function PreviewCard({ url, onOpen }: { url: string; onOpen: (url: string
</div>
)}
{!own && loaded && !live && (
<div className="acpmux-turn-preview-frame" aria-hidden="true" onClick={() => onOpen(frame)}>
<div className="acpmux-turn-preview-frame" data-interactive="" aria-hidden="true" onClick={() => onOpen(frame)}>
<iframe
src={frame}
title={address}
Expand Down
12 changes: 12 additions & 0 deletions webviews/src/agent-session/acpmux/conversation/conversation.css
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,18 @@ pre.cv-math-source {
left: var(--cv-inset);
right: var(--cv-inset);
}
.acpmux-row__gap {
position: absolute;
z-index: 0;
right: 0;
bottom: 0;
left: 0;
height: 0;
/* Inherit the actual row padding: step rows and live lines use smaller or zero gaps. */
padding-bottom: inherit;
pointer-events: auto;
background: transparent;
}
/* Keyboard transcript selection uses a roving article tab stop. The fill is deliberately on the
row rather than the prose so a range remains visible while its rows virtualize in and out. */
.acpmux-row[data-transcript-selected="true"] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { agentPaneEntry } from "../../../gallery/format";
import type { PlayContext } from "../../../gallery/play";
import { assistant, chat, summary, user } from "../../../gallery/fixtures/acpmux";
import { GAP_KINDS, gapPair } from "./selectionGaps.fixture";

const rows = [
user("How does the keeper start the night?", 30),
Expand Down Expand Up @@ -35,29 +36,87 @@ function answerLine(doc: Document, text: string) {
return undefined;
}

/// The blank vertical rhythm between the two public answer fixtures.
function answerGap(doc: Document, above: string, below: string) {
const first = answerLine(doc, above);
const second = answerLine(doc, below);
if (!first || !second) return undefined;
return { x: (first.left + first.right) / 2, y: (first.bottom + second.top) / 2 };
}

/// Presses at `from` in the margin, drags to `to` in steps a frame apart, and lifts.
async function drag(ctx: PlayContext, from: { x: number; y: number }, to: { x: number; y: number }) {
const doc = ctx.document;
const view = doc.defaultView!;
const scroller = doc.querySelector<HTMLElement>(".acpmux-scroll")!;
const init = (x: number, y: number, buttons: number): MouseEventInit => ({
const init = (x: number, y: number, buttons: number): PointerEventInit => ({
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
button: 0,
buttons,
isPrimary: true,
pointerId: 1,
pointerType: "mouse",
});
scroller.dispatchEvent(new MouseEvent("mousedown", init(from.x, from.y, 1)));
const target = doc.elementFromPoint(from.x, from.y) ?? scroller;
target.dispatchEvent(new PointerEvent("pointerdown", init(from.x, from.y, 1)));
for (let step = 1; step <= 6; step += 1) {
const at = (a: number, b: number) => a + ((b - a) * step) / 6;
view.dispatchEvent(new MouseEvent("mousemove", init(at(from.x, to.x), at(from.y, to.y), 1)));
view.dispatchEvent(new PointerEvent("pointermove", init(at(from.x, to.x), at(from.y, to.y), 1)));
await new Promise((resolve) => view.requestAnimationFrame(resolve));
}
view.dispatchEvent(new MouseEvent("mouseup", init(to.x, to.y, 0)));
view.dispatchEvent(new PointerEvent("pointerup", init(to.x, to.y, 0)));
await ctx.waitFor(() => !(view.getSelection()?.isCollapsed ?? true), { capMs: 1500 }).catch(() => undefined);
}

const gapVariants = Object.fromEntries(
GAP_KINDS.flatMap((above) =>
GAP_KINDS.map((below) => [
`${above}-to-${below}-gap`,
{
note: `Drag from the gap between ${above} and ${below} rows; the nearest visible text caret starts a new native selection.`,
snapshot: chat(gapPair(above, below), { title: "Select through transcript gaps" }),
play: async (ctx: PlayContext) => {
// Consecutive tools intentionally have no production row gap. This fixture supplies a
// visible gap for that pair, as the midpoint browser regression does; content stays native.
if (above === "tool" && below === "tool") {
const style = ctx.document.createElement("style");
style.textContent = '.acpmux-row[data-row-id="upper"] { padding-bottom: 32px; }';
ctx.document.head.append(style);
}
for (const id of ["upper", "lower"]) {
const selector = `[data-row-id="${id}"] .cv-tool.is-toggle`;
if (ctx.document.querySelector(selector)) await ctx.click({ selector });
}
const rects = (id: string) => {
const walker = ctx.document.createTreeWalker(ctx.document.querySelector(`[data-row-id="${id}"]`)!, 4);
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
if (!node.textContent?.startsWith(`${id} keeper`)) continue;
const range = ctx.document.createRange();
range.selectNodeContents(node);
const boxes = Array.from(range.getClientRects());
if (boxes.length) return boxes;
}
throw new Error(`No visible ${id} text`);
};
const after = rects("after")[0]!;
const upper = ctx.document.querySelector<HTMLElement>('[data-row-id="upper"]')!;
const column = upper.getBoundingClientRect();
const padding = Number.parseFloat(ctx.document.defaultView!.getComputedStyle(upper).paddingBottom);
await drag(
ctx,
{ x: (column.left + column.right) / 2, y: column.bottom - Math.max(1, padding / 2) },
{ x: after.left + 180, y: after.top + after.height / 2 },
);
if (ctx.document.getSelection()?.isCollapsed) throw new Error("Gap drag selected no text");
},
},
]),
),
);

export default agentPaneEntry({
id: "agent-pane.gutter-select",
title: "Drag-select from the margin",
Expand All @@ -67,6 +126,11 @@ export default agentPaneEntry({
anchors: [{ selector: ".acpmux-scroll" }],
covers: ["agent-session/acpmux/App.tsx#VirtualTranscript"],
variants: {
...gapVariants,
"gaps-ready": {
note: "Public agent message fixtures for the same before/after gap-selection capture; the pair variants also cover tools and prompts.",
snapshot: chat(gapPair("agent", "agent"), { title: "Select through transcript gaps" }),
},
"from-left-margin": {
note: "Pressed in the left margin beside the first answer and dragged down into the third: the selection runs from the start of the line beside the press through all three answers, as a document's margin selects.",
snapshot: chat(rows, { title: "Drag-select from the margin" }),
Expand All @@ -89,5 +153,15 @@ export default agentPaneEntry({
await drag(ctx, { x: (third.right + width - 16) / 2, y: third.bottom }, { x: first.left + 300, y: first.top });
},
},
"from-row-gap": {
note: "Pressed in the blank rhythm between the first two answers and dragged into the second: the nearest mounted caret starts the native range instead of leaving a dead zone.",
snapshot: chat(rows, { title: "Drag-select from a row gap" }),
play: async (ctx) => {
const gap = answerGap(ctx.document, "Alpha", "Bravo");
const second = answerLine(ctx.document, "Bravo");
if (!gap || !second) return;
await drag(ctx, gap, { x: second.left + 300, y: second.top + 4 });
},
},
},
});
Loading
Loading