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
11 changes: 9 additions & 2 deletions packages/panel/src/ai-elements/fold.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,26 @@ import type { ComponentProps, MouseEvent, ReactNode } from "react";
* summary row is `Disclosure` itself, not this.
*/

/** Note that the element's own `open` is not a prop: the state is this component's, and a fold meant to start open says `defaultOpen`. */
/** Note that the element's own `open` is not a prop: a fold meant to start open says `defaultOpen`, and one whose holder keeps its state says `open` and hears `onOpenChange`, each press and each toggle a find makes. */
export type FoldProps = Omit<ComponentProps<"details">, "open"> & {
defaultOpen?: boolean;
open?: boolean | undefined;
onOpenChange?: ((open: boolean) => void) | undefined;
};

export function Fold({
className,
children,
defaultOpen = false,
open,
onOpenChange,
onToggle,
...props
}: FoldProps): ReactNode {
const { state, details } = useDisclosureFold(defaultOpen);
const { state, details } = useDisclosureFold(
defaultOpen,
open === undefined ? undefined : { open, onOpenChange: onOpenChange ?? (() => undefined) },
);
return (
<DisclosureScope state={state}>
<details
Expand Down
1 change: 1 addition & 0 deletions packages/panel/src/desktop/desktop-settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ test("Keyboard shortcuts lists Files changed's own keys under a group of their o
["Collapse or expand all files", "⇧;"],
["Clear file focus", "Esc"],
["Toggle viewed and go to next file", "V"],
["Minimize or show inline comments", "I"],
],
);
assert.deepEqual(foundUnder(page, "Collapse or expand all files"), ["Keyboard shortcuts"]);
Expand Down
70 changes: 70 additions & 0 deletions packages/panel/src/styles/tasks.css
Original file line number Diff line number Diff line change
Expand Up @@ -2465,6 +2465,76 @@
white-space: normal;
}

/* A line's review threads minimized to one chip (minimized-threads.tsx), as
stage-cli draws them: the chip, its fold's summary, floats up over the end
of the line in a row of no height while closed, so the diff reads as if the
line held no comments, and a note holding the fold alone keeps no padding
of its own. Open, the chip stands at the end of its own row over the
threads it shows; so does the chip of threads on no line, above the diff
or under a file's picture, which has no line to float over. */
.diff-note:has(> .diff-thread-chip-row:only-child:not([open], [data-above])) {
padding-block: 0;
}

.diff-thread-chip-row {
position: relative;
}

.diff-thread-chip-row:not([open], [data-above]) {
height: 0;
}

.diff-thread-chip {
display: flex;
align-items: center;
gap: var(--luke-icon-gap);
width: fit-content;
margin-inline-start: auto;
padding: var(--luke-space-2) var(--luke-space-6);
border: 1px solid var(--luke-color-border);
border-radius: var(--luke-control-radius);
background: var(--luke-color-surface);
color: var(--luke-color-text-muted);
font-size: var(--luke-type-caption-size);
}

.diff-thread-chip:hover {
color: var(--luke-color-text);
}

.diff-thread-chip-row:not([open], [data-above]) > .diff-thread-chip {
position: absolute;
right: 0;
bottom: var(--luke-space-2);
}

.diff-thread-chip-row[open] > .diff-thread-chip {
margin-bottom: var(--luke-space-4);
}

.diff-thread-chip > svg {
width: var(--luke-space-12);
height: var(--luke-space-12);
}

.diff-thread-chip-faces {
display: inline-flex;
}

.diff-thread-chip-faces > .actor-mark + .actor-mark {
margin-inline-start: calc(-1 * var(--luke-space-4));
}

.diff-thread-chip-resolved {
color: var(--luke-color-complete);
}

.diff-thread-chip-threads {
display: flex;
flex-direction: column;
gap: var(--luke-space-6);
}

.diff-inline-thread {
--luke-focus-ring-offset: var(--luke-focus-ring-inset);
max-width: 720px;
Expand Down
88 changes: 86 additions & 2 deletions packages/panel/src/tasks/agent-changes-tab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
FILES_CHANGED_SHORTCUTS,
type ShortcutChord,
} from "@luke/settings/shortcuts";
import { DisclosureFind } from "@luke/ui";
import { Effect } from "effect";
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
import { act, createElement, type ReactElement } from "react";
Expand All @@ -40,6 +41,7 @@ import { useAppCommand, useAppKeymap } from "../app-commands.js";
import type { SideBundles } from "../bundle-loader.js";
import type { DiffListProps, DiffViewModule } from "../diff-view/contract.js";
import type { FileTreeModule } from "../file-tree/contract.js";
import { findRanges, reveal } from "../find-matches.js";
import { fakeAppBridge, fakeAppBridgeCalls } from "../testing/app-bridge.js";
import { handSettings } from "../testing/app-state.js";
import { onRegistry } from "../testing/copy-feedback.js";
Expand Down Expand Up @@ -244,6 +246,8 @@ interface DrawOptions {
readonly beside?: ReactElement;
readonly repository?: string;
readonly control?: typeof control;
/** Whether a find looks through the tab, which draws its folds' closed bodies hidden. */
readonly finding?: boolean;
/** Whether the view is the Pull request tab's chosen one; it is unless told. */
readonly shown?: boolean;
}
Expand All @@ -263,7 +267,13 @@ async function redraw(options: DrawOptions = {}): Promise<DiffListProps> {
root?.render(
onRegistry(
registry,
onSideBundles(bundles, createElement("div", null, tab, options.beside ?? null)),
onSideBundles(
bundles,
createElement(DisclosureFind, {
finding: options.finding ?? false,
children: createElement("div", null, tab, options.beside ?? null),
}),
),
),
),
);
Expand Down Expand Up @@ -682,7 +692,13 @@ test("? inside the view opens a sheet of its keys by group, which Escape or ? ag
["Clear file focus", "Esc"],
],
],
["Review", [["Toggle viewed and go to next file", "V"]]],
[
"Review",
[
["Toggle viewed and go to next file", "V"],
["Minimize or show inline comments", "I"],
],
],
]);

press(sheet, { key: "Escape" });
Expand Down Expand Up @@ -1480,6 +1496,74 @@ test("a refused mark leaves a fold chosen since, as Collapse all chose it", asyn
]);
});

test("I turns Minimize inline comments on and off inside Files changed, each line's threads then one chip that a find still reads and opens", async () => {
// Each thread drawn as a resolved one is, folded closed under its head.
const threads: ChangesThreads = {
list: [seatsThread(2, "why-42")],
draw: (thread) =>
createElement(
"details",
{ className: "drawn-card" },
createElement("summary", null, "head"),
createElement("p", { className: "drawn-thread" }, thread.comments[0]?.body),
),
};
await drawTab({ threads });
const note = () => lastDrawn().renderAnnotation("src/seats.ts", `${PAGE}#why-42`);
assert.equal(note().querySelector(".drawn-thread")?.textContent, "why-42");

press(listBox(), { key: "i" });
await flush();
const chip =
note().querySelector<HTMLElement>(".diff-thread-chip") ?? assert.fail("a chip stands");
assert.equal(chip.getAttribute("aria-label"), "1 comment by octo · 1 open, show");
assert.equal(note().querySelector(".drawn-thread"), null, "minimized, the thread is put away");
press(document.body, { key: "i" });
await flush();
assert.ok(note().querySelector(".diff-thread-chip"), "outside the view, I is not the view's");

// A find looking through the tab reads the put-away thread's words, and opens the line it brings into view.
await redraw({ threads, finding: true });
const slot = note();
document.body.append(slot);
const [match] = findRanges(slot, "why-42");
assert.ok(match, "the find reads the minimized thread");
// Note that jsdom lays nothing out, so a range is measured where the find scrolls it into view as standing nowhere.
Range.prototype.getBoundingClientRect = () => new DOMRect();
act(() => reveal(match, slot));
await flush();
assert.equal(
note().querySelector(".diff-thread-chip")?.getAttribute("aria-label"),
"1 comment by octo · 1 open, hide",
);
assert.equal(
note().querySelector<HTMLDetailsElement>(".drawn-card")?.open,
true,
"the found words' own fold stays open",
);

press(listBox(), { key: "i" });
await flush();
assert.equal(note().querySelector(".diff-thread-chip"), null, "I again draws every thread whole");
});

test("minimized, the threads on no line take a row of their own for their chip, which has no line to float over", async () => {
const threads: ChangesThreads = {
list: [seatsThread(2, "why-42"), seatsThread(null, "was-3")],
draw: (thread) => createElement("p", null, thread.comments[0]?.body),
};
await drawTab({ threads });
press(listBox(), { key: "i" });
await flush();
const [outdated, current] = lastDrawn().files[0]?.annotations ?? [];
const chipOf = (key: string | undefined) =>
lastDrawn()
.renderAnnotation("src/seats.ts", key ?? "")
.querySelector(".diff-thread-chip-row");
assert.equal(chipOf(outdated?.key)?.hasAttribute("data-above"), true, "above the diff");
assert.equal(chipOf(current?.key)?.hasAttribute("data-above"), false, "on its line");
});

/** The seats file at the pull request's head, ten lines, its fifth changed, and its patch as GitHub sends it. */
const HEAD_TEXT = `${["1", "2", "3", "4", "five", "6", "7", "8", "9", "10"].join("\n")}\n`;
const BASE_TEXT = HEAD_TEXT.replace("five", "5");
Expand Down
Loading
Loading