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
4 changes: 2 additions & 2 deletions crates/compass-output/assets/viewer/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"sha256": "f992e86e7a4b04ba2729f0fb8be8a5e58ab9c317ec99c6b82d3d6f93036d6ae3"
},
"viewer.css": {
"bytes": 169307,
"sha256": "7cc964a595b64887e5ea57bf31d9cdfb43d2afed84250c8aa0d090ef3cd5af09"
"bytes": 196888,
"sha256": "0f0340291efa41910c4112c3b1684551a31c613aaeea93192ef73c438994c43a"
}
}
}
2 changes: 1 addition & 1 deletion crates/compass-output/assets/viewer/viewer.css

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion editors/vscode/src/webviews/history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ function render(): void {
enableState={enableState}
loadingMore={loadingMore}
loadMoreError={loadMoreError}
buildState={buildStates.get(selectedCommit)}
buildStates={buildStates}
operationError={operationErrors.get(selectedCommit)}
onSelectCommit={selectCommit}
graph={graph}
Expand Down
110 changes: 86 additions & 24 deletions packages/compass-viewer/src/history/CommitDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { GitCompareIcon, SearchIcon } from "lucide-react";
import { Badge } from "../components/ui/badge";
import { Button } from "../components/ui/button";
import type {
HistoryBuildState,
HistoryChangeCounts,
HistoryEntry,
HistoryOperationError
Expand All @@ -10,20 +11,58 @@ import type {
export function CommitDetails({
entry,
operationError,
availableCommits,
comparisonEntries,
comparisonCommit,
selectedBuildState,
comparisonBuildState,
hasMore,
onComparisonCommit,
onCompare,
onBuildRevision,
onQuery,
changeCounts
}: {
entry: HistoryEntry;
operationError?: HistoryOperationError | undefined;
availableCommits: ReadonlySet<string>;
onCompare(parent: string): void;
comparisonEntries: HistoryEntry[];
comparisonCommit: string;
selectedBuildState?: HistoryBuildState | undefined;
comparisonBuildState?: HistoryBuildState | undefined;
hasMore: boolean;
onComparisonCommit(commit: string): void;
onCompare(): void;
onBuildRevision(commit: string): void;
onQuery(): void;
changeCounts?: HistoryChangeCounts | undefined;
}) {
const unavailableParents = entry.parents.filter((parent) => !availableCommits.has(parent));
const comparisonUnavailable = !entry.presentationAvailable || unavailableParents.length > 0;
const comparisonEntry = comparisonEntries.find(
(candidate) => candidate.commit === comparisonCommit
);
const buildTarget = !entry.presentationAvailable
? { entry, state: selectedBuildState, label: "selected" }
: comparisonEntry && !comparisonEntry.presentationAvailable
? { entry: comparisonEntry, state: comparisonBuildState, label: "baseline" }
: undefined;
const buildInProgress = buildTarget?.state?.status === "requesting"
|| buildTarget?.state?.status === "running";
const actionLabel = buildTarget
? buildInProgress
? `Building ${buildTarget.label} graph…`
: buildTarget.state?.status === "failed"
? `Retry ${buildTarget.label} graph build`
: `Build ${buildTarget.label} graph`
: "Compare revisions";
const canAct = comparisonEntry !== undefined && !buildInProgress;

function performComparisonAction() {
if (!comparisonEntry) return;
if (buildTarget) {
onBuildRevision(buildTarget.entry.commit);
return;
}
onCompare();
}

return (
<section className="history-commit-details" aria-labelledby="history-selected-title">
<div className="history-commit-heading">
Expand Down Expand Up @@ -51,30 +90,53 @@ export function CommitDetails({
<SearchIcon /> Query this revision
</Button>
)}
{entry.parents.map((parent, index) => (
<div className="history-comparison-control">
<label htmlFor="history-comparison-revision">Compare against</label>
<select
id="history-comparison-revision"
aria-label="Comparison revision"
value={comparisonCommit}
disabled={comparisonEntries.length === 0 || buildInProgress}
onChange={(event) => onComparisonCommit(event.target.value)}
>
{comparisonEntries.length === 0 && (
<option value="">No other loaded revisions</option>
)}
{comparisonEntries.map((candidate) => (
<option key={candidate.commit} value={candidate.commit}>
{candidate.subject || "(no subject)"} · {candidate.commit.slice(0, 9)}
{candidate.presentationAvailable ? "" : " · graph not built"}
</option>
))}
</select>
<Button
key={parent}
size="sm"
variant="ghost"
disabled={!entry.presentationAvailable || !availableCommits.has(parent)}
title={!entry.presentationAvailable
? "Build this revision first"
: !availableCommits.has(parent)
? "Parent graph is not available"
: undefined}
onClick={() => onCompare(parent)}
variant="outline"
disabled={!canAct}
onClick={performComparisonAction}
>
<GitCompareIcon /> Compare parent {index + 1}
<GitCompareIcon /> {actionLabel}
</Button>
))}
</div>
</div>
{comparisonUnavailable && entry.parents.length > 0 && (
{comparisonEntries.length === 0 ? (
<p className="history-comparison-help">
Comparison unavailable: {!entry.presentationAvailable
? "build this revision first."
: "one or more parent graphs are not available."}
Load another revision to compare with this one.
</p>
)}
) : buildTarget?.state?.status === "failed" && buildTarget.label === "baseline" ? (
<p className="history-inline-error" role="alert">
Baseline graph build failed: {buildTarget.state.message}
</p>
) : buildTarget?.state?.status === "failed" ? null : buildTarget ? (
<p className="history-comparison-help">
Build the {buildTarget.label} revision graph, then compare without changing your
selection.
</p>
) : hasMore ? (
<p className="history-comparison-help">
Choose any loaded revision, or load more commits to reach older history.
</p>
) : null}
{operationError && (
<p className="history-inline-error" role="alert">
{operationError.operation}: {operationError.message}
Expand All @@ -89,8 +151,8 @@ export function CommitDetails({
</div>
{isEmptyChangeCounts(changeCounts) && (
<p className="history-change-counts-help">
No structural changes from the first parent. Source or configuration changes may
still exist; compare the revisions to inspect them.
No structural changes from the comparison baseline. Source or configuration
changes may still exist; compare the revisions to inspect them.
</p>
)}
</>
Expand Down
79 changes: 78 additions & 1 deletion packages/compass-viewer/src/history/HistoryWorkspace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,88 @@ describe("HistoryWorkspace", () => {
expect(container.textContent).toContain("No graph delta to draw");
expect(container.textContent).toContain(`Comparing ${commit.slice(0, 9)} to ${parent.slice(0, 9)}`);
expect(container.textContent).toContain(
"No structural changes from the first parent. Source or configuration changes may still exist"
"No structural changes from the comparison baseline. Source or configuration changes may still exist"
);
root.unmount();
});

it("compares the selected revision with any other loaded revision", () => {
const historyHost = host();
const container = document.createElement("div");
const root = createRoot(container);
const parent = "b".repeat(40);
const nonAdjacent = "c".repeat(40);
flushSync(() => root.render(
<HistoryWorkspace
timeline={{
schema: "compass.history.timeline/1",
repositoryId: "repo",
selectedHead: commit,
historyEnabled: true,
totalEntries: 3,
hasMore: false,
nextCursor: null,
entries: [{
commit,
parents: [parent],
authorName: "Compass",
authorEmail: "test@example.invalid",
authoredAtSeconds: 3,
subject: "Selected revision",
graphState: "graph_available",
presentationAvailable: true,
realization: "selected-realization",
fingerprint: "selected-fingerprint",
job: null
}, {
commit: parent,
parents: [nonAdjacent],
authorName: "Compass",
authorEmail: "test@example.invalid",
authoredAtSeconds: 2,
subject: "Parent revision",
graphState: "graph_available",
presentationAvailable: true,
realization: "parent-realization",
fingerprint: "parent-fingerprint",
job: null
}, {
commit: nonAdjacent,
parents: [],
authorName: "Compass",
authorEmail: "test@example.invalid",
authoredAtSeconds: 1,
subject: "Older revision",
graphState: "graph_available",
presentationAvailable: true,
realization: "older-realization",
fingerprint: "older-fingerprint",
job: null
}]
}}
selectedCommit={commit}
revisionLoadState="ready"
onSelectCommit={vi.fn()}
host={historyHost}
/>
));

const comparisonSelect = container.querySelector<HTMLSelectElement>(
'[aria-label="Comparison revision"]'
);
expect(comparisonSelect?.value).toBe(parent);
if (!comparisonSelect) throw new Error("comparison revision picker did not render");

comparisonSelect.value = nonAdjacent;
flushSync(() => comparisonSelect.dispatchEvent(new Event("change", { bubbles: true })));
const compareButton = Array.from(container.querySelectorAll("button"))
.find((candidate) => candidate.textContent?.includes("Compare revisions"));
flushSync(() => compareButton?.click());

expect(historyHost.compare).toHaveBeenCalledWith(commit, nonAdjacent);
root.unmount();
});

it("lets the user enable revision graphs from the disabled state", () => {
const historyHost = host();
const container = document.createElement("div");
Expand Down
61 changes: 47 additions & 14 deletions packages/compass-viewer/src/history/HistoryWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function HistoryWorkspace({
enableState,
loadingMore = false,
loadMoreError,
buildState,
buildStates,
operationError,
onSelectCommit,
host
Expand All @@ -84,13 +84,14 @@ export function HistoryWorkspace({
enableState?: HistoryBuildState | undefined;
loadingMore?: boolean | undefined;
loadMoreError?: string | undefined;
buildState?: HistoryBuildState | undefined;
buildStates?: ReadonlyMap<string, HistoryBuildState> | undefined;
operationError?: HistoryOperationError | undefined;
onSelectCommit(commit: string): void;
host: HistoryHost;
}) {
const [query, setQuery] = useState("");
const [comparisonTab, setComparisonTab] = useState<ComparisonTab>("source");
const [comparisonCommit, setComparisonCommit] = useState("");
useEffect(() => {
setComparisonTab("source");
}, [comparison?.parent, selectedCommit]);
Expand All @@ -103,12 +104,28 @@ export function HistoryWorkspace({
|| entry.graphState.replaceAll("_", " ").includes(normalizedQuery));
}, [query, timeline.entries]);
const selected = timeline.entries.find((entry) => entry.commit === selectedCommit);
const availableCommits = useMemo(
() => new Set(timeline.entries
.filter((entry) => entry.presentationAvailable)
.map((entry) => entry.commit)),
[timeline.entries]
const comparisonEntries = useMemo(
() => timeline.entries.filter((entry) => entry.commit !== selected?.commit),
[selected?.commit, timeline.entries]
);
useEffect(() => {
setComparisonCommit((current) => {
if (comparisonEntries.some((entry) => entry.commit === current)) return current;
const loadedParent = selected?.parents.find((parent) =>
comparisonEntries.some((entry) => entry.commit === parent)
);
const activeBaseline = comparisonEntries.some(
(entry) => entry.commit === comparison?.parent
)
? comparison?.parent
: undefined;
return loadedParent ?? activeBaseline ?? comparisonEntries[0]?.commit ?? "";
});
}, [comparison?.parent, comparisonEntries, selected?.parents]);
const comparisonEntry = comparisonEntries.find(
(entry) => entry.commit === comparisonCommit
);
const selectedBuildState = selected ? buildStates?.get(selected.commit) : undefined;
const visibleGraph = comparison?.graph
?? (graph && graphCommit === selected?.commit ? graph : undefined);
const loadedEntries = timeline.entries.length;
Expand Down Expand Up @@ -225,10 +242,26 @@ export function HistoryWorkspace({
<CommitDetails
entry={selected}
operationError={operationError?.operation === "Load graph" ? undefined : operationError}
availableCommits={availableCommits}
onCompare={(parent) => host.compare(selected.commit, parent)}
comparisonEntries={comparisonEntries}
comparisonCommit={comparisonCommit}
selectedBuildState={selectedBuildState}
comparisonBuildState={comparisonEntry
? buildStates?.get(comparisonEntry.commit)
: undefined}
hasMore={timeline.hasMore ?? false}
onComparisonCommit={(commit) => {
setComparisonCommit(commit);
if (comparison && comparison.parent !== commit) onExitComparison?.();
}}
onCompare={() => {
if (comparisonEntry) host.compare(selected.commit, comparisonEntry.commit);
}}
onBuildRevision={host.buildRevision}
onQuery={() => host.queryRevision(selected.commit)}
changeCounts={changeCounts?.commit === selected.commit ? changeCounts : undefined}
changeCounts={changeCounts?.commit === selected.commit
&& changeCounts.parent === comparisonCommit
? changeCounts
: undefined}
/>
{comparison && (
<ComparisonOverlay
Expand Down Expand Up @@ -394,23 +427,23 @@ export function HistoryWorkspace({
/>
</div>
</div>
) : buildState?.status === "requesting" ? (
) : selectedBuildState?.status === "requesting" ? (
<WorkspaceState
kind="running"
title="Choosing a build profile"
description="Select how Compass should materialize this revision graph."
/>
) : buildState?.status === "running" ? (
) : selectedBuildState?.status === "running" ? (
<WorkspaceState
kind="running"
title="Building revision graph"
description={`Compass is materializing ${selected.commit.slice(0, 9)}. You can cancel from the VS Code progress notification.`}
/>
) : buildState?.status === "failed" ? (
) : selectedBuildState?.status === "failed" ? (
<WorkspaceState
kind="error"
title="Revision build failed"
description={buildState.message}
description={selectedBuildState.message}
action={{
label: "Retry build",
onClick: () => host.buildRevision(selected.commit)
Expand Down
Loading
Loading