Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
6f5dc40
feat(service,features,desktop): comment on an agent's pull request fr…
dastratakos Oct 17, 2026
798c4cb
fix(desktop): keep a comment being written when a conversation read f…
dastratakos Oct 18, 2026
4bebc35
fix(desktop): draw the comment box with the Luke roles rather than th…
dastratakos Oct 18, 2026
de052fd
feat(service,features): comment on lines and discard a pending review…
dastratakos Oct 18, 2026
5420bd9
feat(desktop): comment on lines in Files changed, and count and disca…
dastratakos Oct 18, 2026
7abfe53
fix(service,desktop): start a review with its first comment, refuse o…
dastratakos Oct 18, 2026
eea78c0
Merge origin/main into feat/pull-request-comment
dastratakos Oct 18, 2026
ca69a7d
Merge feat/pull-request-comment into feat/pull-request-line-comments
dastratakos Oct 18, 2026
875cc8b
Merge origin/main into feat/pull-request-comment
dastratakos Oct 18, 2026
ea024af
Merge feat/pull-request-comment into feat/pull-request-line-comments
dastratakos Oct 18, 2026
ecaca5f
Merge remote-tracking branch 'origin/main' into feat/pull-request-com…
dastratakos Oct 18, 2026
be35595
fix(desktop): read the comment box's refusal as main now draws every one
dastratakos Oct 18, 2026
40a227f
Merge feat/pull-request-comment into feat/pull-request-line-comments
dastratakos Oct 18, 2026
290ea2a
Merge origin/main into feat/pull-request-comment
dastratakos Oct 18, 2026
44af117
Merge feat/pull-request-comment into feat/pull-request-line-comments
dastratakos Oct 18, 2026
60c3c04
Merge origin/main into feat/pull-request-line-comments
dastratakos Oct 18, 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
28 changes: 27 additions & 1 deletion apps/desktop/src/renderer/diff-view/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,18 +130,44 @@ export interface DiffListFile {
readonly lead: boolean;
}

/** Lines lit in one file of a list: the file, by its path, and the lines. */
export interface DiffListSelection {
readonly path: string;
readonly selection: DiffSelection;
}

/**
* What the view hands the bundle for a list of files: the files in order,
* how to draw them, the element each file's header is drawn in and each
* note, both kept by the view and filled by it, and the door told which file
* stands at the top of the list as it scrolls.
* stands at the top of the list as it scrolls. Where lines may be commented
* on, `onComment` is told the lines a press of the gutter's + picked, on a
* file that draws lines of its own, and `selected` lights the lines being
* commented on; with no `onComment` the gutter offers nothing.
*/
export interface DiffListProps {
readonly files: readonly DiffListFile[];
readonly look: DiffLook;
readonly renderHeader: (path: string) => HTMLElement;
readonly renderAnnotation: (path: string, key: string) => HTMLElement;
readonly onTopFile: (path: string) => void;
readonly onComment?: ((picked: DiffListSelection) => void) | undefined;
readonly selected?: DiffListSelection | undefined;
}

/** Whether two lists light the same lines of the same file, or both none. */
export function sameListSelection(
a: DiffListSelection | undefined,
b: DiffListSelection | undefined,
): boolean {
if (a === undefined || b === undefined) return a === b;
return (
a.path === b.path &&
a.selection.start === b.selection.start &&
a.selection.side === b.selection.side &&
a.selection.end === b.selection.end &&
a.selection.endSide === b.selection.endSide
);
}

/** The list as mounted: drawn again for new props, scrolled to a file's header, and taken down. */
Expand Down
57 changes: 54 additions & 3 deletions apps/desktop/src/renderer/diff-view/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@ import {
type File as PierreFile,
FileDiff as PierreFileDiff,
type PostRenderPhase,
type SelectedLineRange,
} from "@pierre/diffs";
import { FIND_SHADOW_ATTRIBUTE } from "../find-matches";
import {
DIFF_SELECTION_SIDE,
type DiffAnnotation,
type DiffListFile,
type DiffListHandle,
type DiffListProps,
type DiffListSelection,
type DiffLook,
type DiffSelection,
type DiffViewHandle,
type DiffViewModule,
type DiffViewProps,
sameListSelection,
sameLook,
sameNotes,
} from "./contract";
Expand Down Expand Up @@ -296,6 +301,36 @@ interface DrawnFile {
readonly item: CodeViewDiffItem<string>;
}

/** A range Pierre picked, in the contract's words: a side Pierre leaves unsaid is the new file's. */
function selectionOf(range: SelectedLineRange): DiffSelection {
const side = range.side ?? DIFF_SELECTION_SIDE.ADDITIONS;
return { start: range.start, side, end: range.end, endSide: range.endSide ?? side };
}

/**
* Pierre's own picking of lines, where the view comments on them: a press
* of a line's number picks it and a ⇧-press or a drag picks a range, as
* GitHub's diff does, and the gutter's + on the line under the pointer, or
* on the range picked, tells the view, which opens its comment there. A file
* drawn as its notes alone has no line of its own to pick, so its pick is
* let go.
*/
function commentOptions(
latest: () => DiffListProps,
commentable: (path: string) => boolean,
): CodeViewOptions<string, undefined> {
if (latest().onComment === undefined) return {};
return {
enableLineSelection: true,
enableGutterUtility: true,
lineHoverHighlight: "number",
onGutterUtilityClick: (range: SelectedLineRange, context: { item: { id: string } }) => {
const path = context.item.id;
if (commentable(path)) latest().onComment?.({ path, selection: selectionOf(range) });
},
};
}

/**
* Pierre's options for a list: the look, each file's header and notes drawn
* in the elements the view keeps for them, read off the latest props, and
Expand All @@ -305,9 +340,11 @@ function listOptions(
drawn: DiffLook,
latest: () => DiffListProps,
dress: (element: HTMLElement, path: string) => void,
commentable: (path: string) => boolean,
): CodeViewOptions<string, undefined> {
return {
...sharedOptions(drawn),
...commentOptions(latest, commentable),
hunkSeparators: "line-info-basic",
stickyHeaders: true,
itemMetrics: {
Expand Down Expand Up @@ -378,7 +415,10 @@ function mountList(host: HTMLElement, props: DiffListProps): DiffListHandle {
return item;
});
typeset(host, props.look);
const view = new CodeView<string, undefined>(listOptions(props.look, () => latest, dress));
const commentable = (path: string) => drawnFiles.get(path)?.file.lead === false;
const optionsFor = (next: DiffListProps) =>
listOptions(next.look, () => latest, dress, commentable);
const view = new CodeView<string, undefined>(optionsFor(props));
view.setup(host);
view.setItems(itemsOf(props.files));
view.render(true);
Expand All @@ -395,16 +435,27 @@ function mountList(host: HTMLElement, props: DiffListProps): DiffListHandle {
};
const unsubscribe = view.subscribeToScroll((scrollTop) => tellTop(scrollTop));
tellTop(0);
// The lines being commented on, lit; Pierre's own pick stands while none is.
const light = (selected: DiffListSelection | undefined) => {
if (selected === undefined) return view.clearSelectedLines();
const { start, side, end, endSide } = selected.selection;
view.setSelectedLines({ id: selected.path, range: { start, side, end, endSide } });
};
if (props.selected !== undefined) light(props.selected);
return {
update: (next) => {
const was = latest;
latest = next;
typeset(host, next.look);
if (!sameLook(next.look, was.look))
view.setOptions(listOptions(next.look, () => latest, dress));
if (
!sameLook(next.look, was.look) ||
(next.onComment === undefined) !== (was.onComment === undefined)
)
view.setOptions(optionsFor(next));
view.setItems(itemsOf(next.files));
view.render();
tellTop(host.scrollTop);
if (!sameListSelection(next.selected, was.selected)) light(next.selected);
},
reveal: (path) =>
view.scrollTo({ type: "item", id: path, align: "start", behavior: "smooth-auto" }),
Expand Down
50 changes: 50 additions & 0 deletions apps/desktop/src/renderer/styles/tasks.css
Original file line number Diff line number Diff line change
Expand Up @@ -1948,6 +1948,21 @@
margin: 0;
}

/* The pending comments the member's review holds, counted on the bar's
Review press as stage-cli's badge counts them. */
.pull-request-review-count {
min-width: 16px;
margin-inline-start: var(--luke-space-4);
padding: 0 var(--luke-space-4);
border-radius: 999px;
background: var(--luke-color-fill-strong);
color: var(--luke-color-text);
font-size: var(--luke-type-caption-size);
font-variant-numeric: tabular-nums;
line-height: 16px;
text-align: center;
}

.pull-request-review-foot {
display: flex;
justify-content: flex-end;
Expand Down Expand Up @@ -2452,6 +2467,41 @@
background: var(--luke-color-surface);
}

/* A comment being written on lines of a file (`line-comment.tsx`), under
the last of them as a review thread stands: a card of the thread's own
face, its head naming the lines, the words with their preview, GitHub's
refusal, and what GitHub's own line comment offers at its foot. */
.line-comment {
display: flex;
flex-direction: column;
gap: var(--luke-space-8);
max-width: 720px;
padding: var(--luke-space-8) var(--luke-space-12) var(--luke-space-12);
border: 1px solid var(--luke-color-border);
border-radius: var(--luke-control-radius);
background: var(--luke-color-surface);
}

.line-comment-head {
margin: 0;
color: var(--luke-color-text-muted);
font-size: var(--luke-type-caption-size);
font-weight: 600;
}

.line-comment-note {
margin: 0;
color: var(--luke-color-text-muted);
font-size: var(--luke-type-caption-size);
}

.line-comment-foot {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: var(--luke-control-gap);
}

.diff-outdated-threads {
display: flex;
flex: none;
Expand Down
Loading
Loading