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
49 changes: 44 additions & 5 deletions packages/panel/src/diff-view/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,13 @@ const LOOK_CSS = `
[data-gutter] {
scrollbar-width: thin;
}
/* Note that a line's code scrolls sideways alone, so it keeps no room for a
scroll bar up and down, which a system that draws its bars in the
layout would otherwise leave as a strip at the end of every line, its
rows and its gaps stopping short of the file's header. */
[data-code] {
scrollbar-gutter: auto;
}
/* A file drawn as its notes alone, laid out as blocks: its one empty line, the old side, the numbers, and the hatching beside a side's missing lines are hidden, so its notes run across its whole width, as tall as Pierre measured them. */
:host([${LEAD_ATTRIBUTE}]) pre,
:host([${LEAD_ATTRIBUTE}]) [data-code],
Expand Down Expand Up @@ -210,19 +217,50 @@ const FILL_CSS = `
}
`;

/** How tall a list's file header stands, in pixels, which Pierre reserves for it before it is measured: the view's `.diff-list-head` (`styles/tasks.css`). */
const LIST_HEADER_HEIGHT = 34;
/** How tall a list's file header stands, in pixels, which Pierre reserves for it before it is measured: the view's `.diff-list-head` (`styles/tasks.css`), its row and its rule. */
const LIST_HEADER_HEIGHT = 37;

/** How tall a band between two hunks of a list's file stands, in pixels, which Pierre lays the list out by (`LIST_CSS`). */
const LIST_GAP_HEIGHT = 24;

/** The room between two files of a list, and above the first, in pixels: the panel's ground between their cards. */
const LIST_FILE_GAP = 12;

/** The room below a list's last file, in pixels, so its last lines can be scrolled up off the foot of the pane. */
const LIST_FOOT_ROOM = 48;

/**
* A file in a list: a card, its edge and its corners the panel's, holding
* its header and its lines, each gap between hunks a quiet band of its own
* height (`LIST_GAP_HEIGHT`). Note that the card's corners are clipped at
* the card's own edge, so its header, stuck at the top of the list while
* its lines scroll by, stands square there, the rounded top scrolled away.
*/
const LIST_CSS = `
@layer unsafe {
:host {
box-sizing: border-box;
overflow: clip;
border: 1px solid var(--luke-color-border);
border-radius: var(--luke-control-radius);
--diffs-bg-separator-override: var(--luke-color-fill-hover);
}
[data-separator="line-info-basic"] {
height: ${LIST_GAP_HEIGHT}px;
color: var(--luke-color-text-subtle);
font-family: var(--luke-font-sans);
font-size: var(--luke-type-caption-size);
}
}
`;

let sheets: { look: CSSStyleSheet; fill: CSSStyleSheet } | undefined;

/** The look's sheets, made once and adopted by every diff's shadow root: the look every diff in a list wears, and the same with the fill one diff alone wears. */
/** The look's sheets, made once and adopted by every diff's shadow root: the look with the card every file of a list wears, and the look with the fill one diff alone wears. */
function lookSheets(): { look: CSSStyleSheet; fill: CSSStyleSheet } {
if (sheets !== undefined) return sheets;
const look = new CSSStyleSheet();
look.replaceSync(LOOK_CSS);
look.replaceSync(`${LOOK_CSS}${LIST_CSS}`);
const fill = new CSSStyleSheet();
fill.replaceSync(`${LOOK_CSS}${FILL_CSS}`);
sheets = { look, fill };
Expand Down Expand Up @@ -476,8 +514,9 @@ function listOptions(
itemMetrics: {
lineHeight: Number(drawn.fontSize) * Number(LINE_HEIGHT_RATIO[drawn.lineHeight]),
diffHeaderHeight: LIST_HEADER_HEIGHT,
hunkSeparatorHeight: LIST_GAP_HEIGHT,
},
layout: { paddingTop: 0, paddingBottom: LIST_FOOT_ROOM, gap: 0 },
layout: { paddingTop: LIST_FILE_GAP, paddingBottom: LIST_FOOT_ROOM, gap: LIST_FILE_GAP },
renderCustomHeader: (
_drawn: FileDiffMetadata | FileContents,
context: { item: { id: string } },
Expand Down
57 changes: 49 additions & 8 deletions packages/panel/src/styles/tasks.css
Original file line number Diff line number Diff line change
Expand Up @@ -1497,12 +1497,19 @@
user-select: text;
}

/* The list fills the File tab's place, its scroll area the whole of it. */
/* The list fills the File tab's place, its scroll area the whole of it, its
files cards on the panel's ground, inset from its edges by the room between
two of them (diff-view/index.ts's LIST_FILE_GAP). */
.diff-list {
display: flex;
min-height: 0;
flex: 1;
flex-direction: column;
background: var(--luke-color-ground);
}

.diff-list > .luke-scroll-area > :first-child {
padding-inline: var(--luke-space-12);
}

/* The sheet of Files changed's keys (files-changed-keys.tsx): its groups
Expand All @@ -1520,21 +1527,20 @@
bundle reserves for it (diff-view/index.ts's LIST_HEADER_HEIGHT). The file
stage-cli's keys stand on is ringed as a focused control is. */
.diff-list-head {
border-top: 1px solid var(--luke-color-border);
background: var(--code-ground);
background: var(--luke-color-surface);
color: var(--luke-color-text);
font-family: var(--luke-font-sans);
cursor: default;
}

.diff-list-head-row {
display: flex;
box-sizing: border-box;
height: 33px;
height: 36px;
min-width: 0;
align-items: center;
gap: var(--luke-space-8);
padding: 0 var(--luke-space-12) 0 var(--luke-space-6);
background: var(--luke-color-surface);
padding: 0 var(--luke-space-12) 0 var(--luke-space-4);
font-size: var(--luke-type-caption-size);
white-space: nowrap;
}
Expand All @@ -1557,14 +1563,43 @@
transform: none;
}

/* A file's path: its folder quiet and cut first where the row runs short, so
its name, in the full ink, stands whole as long as it can. */
.diff-list-path {
display: flex;
min-width: 0;
flex: 0 1 auto;
overflow: hidden;
font-family: var(--luke-font-mono);
}

.diff-list-folder {
min-width: 0;
overflow: hidden;
color: var(--luke-color-text-muted);
text-overflow: ellipsis;
}

.diff-list-name {
flex: none;
max-width: 100%;
overflow: hidden;
font-weight: var(--luke-weight-medium);
text-overflow: ellipsis;
}

/* A file's letter in its header stands on a tint of its own colour, the
letter itself what says what became of it. */
.diff-list-head .file-view-status {
display: inline-flex;
min-width: var(--luke-space-16);
height: var(--luke-space-16);
align-items: center;
justify-content: center;
border-radius: var(--luke-radius-6);
background: color-mix(in srgb, currentcolor 14%, transparent);
font-size: var(--luke-type-caption-size);
}

/* A file's Viewed box, GitHub's own (viewed-files.ts), at the end of its
header's row. */
.diff-list-viewed {
Expand Down Expand Up @@ -1695,6 +1730,12 @@
flex: 1;
}

/* Beside Files changed's cards, the search stands where the first card does,
the room between files above it, so the tree and the list share a top. */
.file-tab:has(> .diff-list) + .file-tree-pane .file-tree-search {
padding-top: var(--luke-space-12);
}

.file-tree-notice {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -2460,7 +2501,7 @@
so the window's own type and card draw it, inset from the code's edges
as GitHub's are; one the diff draws no row for stands in a list above it. */
.diff-note {
padding: var(--luke-space-6) var(--luke-space-12);
padding: var(--luke-space-8) var(--luke-space-12);
font-family: var(--luke-font-sans);
white-space: normal;
}
Expand Down
16 changes: 11 additions & 5 deletions packages/panel/src/tasks/agent-changes-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
FOLD_CHORD,
LIST_WORDS,
useChangesList,
VIEWED_CHORD,
} from "./changes-list.js";
import {
blobShaOf,
Expand Down Expand Up @@ -732,11 +733,16 @@ function ViewedBox({
onToggle: () => void;
}): React.JSX.Element {
return (
<span className="diff-list-viewed">
<Checkbox checked={viewed} onChange={onToggle}>
{LIST_WORDS.VIEWED}
</Checkbox>
</span>
<Tooltip
label={viewed ? LIST_WORDS.UNMARK_VIEWED : LIST_WORDS.MARK_VIEWED}
chord={VIEWED_CHORD}
>
Comment on lines +736 to +739

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Viewed hint lacks a behavior test

The new Tooltip changes Viewed's interaction without a test that fails when the hint is absent. Existing Files changed tests cover marking and V separately, but not this hint.

Learn more

The Viewed checkbox gains a hover and focus hint with a label that depends on its checked state. No test was added in this PR, and Files changed tests verify the checkbox and the V action separately without checking the new hint. The testing rule in AGENTS.md requires a test that fails without every behavior change.

Example: Removing this tooltip leaves the existing tests green, even though a file no longer displays “Mark as viewed V” on hover.

Recommended fix: Add a focused panel test that hovers or focuses the Viewed control, verifies the hint and its V chord, and checks that marking the file changes the hint to “Mark as not viewed.”

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

<span className="diff-list-viewed">
<Checkbox checked={viewed} onChange={onToggle}>
{LIST_WORDS.VIEWED}
</Checkbox>
</span>
Comment on lines +740 to +744

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Viewed shortcut missing from checkbox

When the Viewed checkbox takes focus, Tooltip places its shortcut on the surrounding span. The nested Checkbox input receives focus instead, so its shortcut is not announced.

Learn more

A tooltip attaches aria-keyshortcuts and its description to the element it wraps. Here that element is a span, but the interactive Viewed control is the nested input rendered by Checkbox. Keyboard focus reaches the input, which has neither attribute. The shortcut remains visible in the pill but is absent from the focused control's accessibility metadata.

Example: A screen-reader user tabs to a file's Viewed checkbox. The tooltip advertises V visually, while the focused input announces only “Viewed, checkbox.”

Recommended fix: Attach the shortcut to the checkbox input, either by extending Checkbox to forward the needed attributes and tooltip events or by passing the shortcut through explicitly. Add an accessibility assertion for the focused input.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

</Tooltip>
);
}

Expand Down
33 changes: 28 additions & 5 deletions packages/panel/src/tasks/changes-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
filesChangedKeyFor,
shortcutAria,
} from "@luke/settings/shortcuts";
import { IconButton } from "@luke/ui";
import { BUTTON_SIZE, IconButton } from "@luke/ui";
import { ChevronRightIcon } from "lucide-react";
import {
type ReactNode,
Expand Down Expand Up @@ -66,6 +66,9 @@ export const FOLD_CHORD = {
ALL: FILES_CHANGED_SHORTCUTS[FILES_CHANGED_KEY.TOGGLE_ALL_FILES].chord,
} as const;

/** The key that marks the file stood on viewed, or unmarks it, as GitHub's own V does. */
export const VIEWED_CHORD = FILES_CHANGED_SHORTCUTS[FILES_CHANGED_KEY.MARK_VIEWED].chord;

/** What the list says where a filter keeps no file. */
export const LIST_WORDS = {
NONE_KEPT: "No changed file's path holds that.",
Expand All @@ -76,6 +79,8 @@ export const LIST_WORDS = {
SHOW_SOURCE: "Show source",
SHOW_IMAGE: "Show image",
VIEWED: "Viewed",
MARK_VIEWED: "Mark as viewed",
UNMARK_VIEWED: "Mark as not viewed",
/** How far through the files the member is, as GitHub counts it. */
viewedOf: (viewed: number, files: number) => `${viewed} / ${files} viewed`,
} as const;
Expand Down Expand Up @@ -348,6 +353,23 @@ export function useChangesList(
};
}

/** A path as a header names it: its folder, quiet, then its name. */
function PathParts({ path }: { path: string }): React.JSX.Element {
const cut = path.lastIndexOf("/") + 1;
return (
<>
{cut === 0 ? null : (
<span className="diff-list-folder">
<bdi>{path.slice(0, cut)}</bdi>
</span>
)}
<span className="diff-list-name">
<bdi>{path.slice(cut)}</bdi>
</span>
</>
);
}

/** A pointer arriving over a control, as the window says it. */
function pointerOver(): PointerEvent {
return new PointerEvent("pointerover", { bubbles: true, pointerType: "mouse" });
Expand Down Expand Up @@ -403,6 +425,7 @@ export function FileHead({
ref={foldRef}
icon={<ChevronRightIcon className="diff-list-fold" aria-hidden="true" />}
label={label}
size={BUTTON_SIZE.COMPACT}
aria-expanded={!collapsed}
aria-keyshortcuts={shortcutAria(FOLD_CHORD.ONE)}
onClick={onToggle}
Expand Down Expand Up @@ -452,11 +475,11 @@ export function FileHead({
{near ? <Tooltip label={STATUS_WORDS[file.status]}>{letter}</Tooltip> : letter}
<span className="diff-list-path">
{file.previousPath === null ? null : (
<>
<bdi>{file.previousPath}</bdi> →{" "}
</>
<span className="diff-list-folder">
<bdi>{file.previousPath}</bdi> →{"\u00a0"}
</span>
)}
<bdi>{file.path}</bdi>
<PathParts path={file.path} />
</span>
<LinesChanged additions={file.additions} deletions={file.deletions} />
{controls}
Expand Down
Loading