Skip to content

Commit cac07c0

Browse files
ymansurozerclaude
andauthored
feat: add walkthrough sidebar tab and overview file list (#13)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 459d771 commit cac07c0

14 files changed

Lines changed: 633 additions & 117 deletions

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ Immediate to-dos, in rough priority order.
7878
- [ ] **Anchor repair instead of stale-flagging**: when re-diffing on `reload`, try to re-anchor each guide entry to its nearest surviving section/line before declaring the guide stale, so a guide degrades gracefully across rounds (the agent edits between rounds, Galley's hot path) instead of invalidating wholesale on any drift. Keep clean "vanished → stale" semantics for anchors that genuinely no longer resolve.
7979
- [ ] **Command palette**: add a discoverable Cmd/Ctrl+Shift+P palette for common review actions: file filter, find in diffs, next/previous file or change, accept/reject/request change, approve file, toggle layout/settings/sidebar, open in editor, reload, and Send to Agent. Keep keyboard shortcuts as the fast path, but make every major action searchable.
8080
- [ ] **Commit/range/branch review modes**: expand beyond working/staged/file/PR branch reviews with `galley commit <ref>`, `galley range <base>..<head>` / `<base>...<head>`, and `galley branch <base>` so Galley can review historical or comparison diffs without requiring a dirty working tree.
81-
- [x] **Open file in editor**: add a configurable editor command and UI/shortcut action to open the selected file at the current line from the review desk. Support placeholders like `{repo}`, `{file}`, and `{line}` and keep it safe for repo-relative paths.
8281
- [ ] **Lazy diff/content loading + large/binary-file guards**: today every changed file's full contents are read and shipped up front; the only large-file handling is client-side render deferral. Move the guard to the data layer: classify each file by byte size and ship lightweight patch data first, hydrating full contents, highlighting, and rendered markdown on demand when a file is opened. Per-file `loadState` (`ready | deferred | too-large | binary | error`) with two byte tiers — an *eager* limit (~1 MiB, loaded up front) and a *manual* limit (~2 MiB, deferred until opened); over that is `too-large` (skipped with a summary + explicit load-anyway action), plus an image byte cap. Add **binary detection** (NUL-byte scan) so binaries are skipped rather than read as UTF-8 and handed to @pierre.
8382

8483
## License

assets/screenshot.png

6.02 KB
Loading

scripts/fetch-icons.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const ICON_MAP = {
2424
"gly-arrow-right": "lucide:arrow-right",
2525
"gly-warn": "lucide:triangle-alert",
2626
"gly-open-editor": "lucide:square-arrow-out-up-right",
27+
// Walkthrough per-file status trio (gly-check doubles as the approved state).
28+
"gly-circle": "lucide:circle",
29+
"gly-circle-alert": "lucide:circle-alert",
2730
};
2831

2932
// Hand-drawn glyphs with no good library equivalent (status primitives). 24-unit viewBox to

src/ui/guide.ts

Lines changed: 52 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { S, $, esc } from "./store";
22
import { fileReviewState } from "./changes";
3-
import { renderMarkdown } from "./markdown";
3+
import { renderMarkdown, renderMarkdownInline } from "./markdown";
4+
import { lineStats, walkthroughGroups, walkRows } from "./walkthrough";
5+
import type { WalkGroup, WalkRow, WalkFile } from "./walkthrough";
46

57
// Whether the current review carries an agent-attached guide with at least one file.
68
export function hasGuide(): boolean {
@@ -48,14 +50,27 @@ export function prevFileIndex(cur: number): number | null {
4850
// finishing a big file advances the bar more than a tiny one. Min 1 so every file counts.
4951
function locByPath(): Map<string, number> {
5052
const m = new Map<string, number>();
51-
for (const f of S.state?.files ?? []) {
52-
let n = 0;
53-
for (const h of f.hunks ?? []) for (const l of h.lines) if (l.kind !== "context") n++;
54-
m.set(f.path, Math.max(n, 1));
55-
}
53+
for (const [path, s] of lineStats(S.state?.files ?? []))
54+
m.set(path, Math.max(s.added + s.removed, 1));
5655
return m;
5756
}
5857

58+
// Guide categories + their files (plus the trailing "Other" group of unlisted diff files) —
59+
// the data behind the Walkthrough sidebar tab and the Overview file list.
60+
export function walkGroups(): WalkGroup[] {
61+
if (!hasGuide()) return [];
62+
return walkthroughGroups(S.state.guide!.files, S.state.files ?? [], fileReviewState);
63+
}
64+
65+
// Flat rows for the Walkthrough tab's x-for. Same active-path rule as the tree: nothing is
66+
// active on the Overview; a previewed file wins over the indexed review file.
67+
export function walkthroughRows(): WalkRow[] {
68+
const activePath = S.overviewOpen
69+
? null
70+
: (S.preview?.path ?? S.state?.files?.[S.fileIndex]?.path ?? null);
71+
return walkRows(walkGroups(), activePath);
72+
}
73+
5974
// Overall review progress for the guide-bar indicator, weighted by changed lines (LOC) rather
6075
// than file count: "done" sums the LOC of files the reviewer finished (approved OR
6176
// changes-requested), "approved" the clean-signoff LOC.
@@ -75,48 +90,6 @@ export function guideProgress(): { done: number; approved: number; total: number
7590
return { done, approved, total, pct: total ? Math.round((done / total) * 100) : 0 };
7691
}
7792

78-
export type CategoryStep = {
79-
category: string;
80-
total: number;
81-
done: number;
82-
pct: number;
83-
critical: boolean;
84-
active: boolean;
85-
};
86-
87-
// Per-category macro-progress for the stepper: distinct categories in guide order, each with
88-
// done/total **changed lines** (count + fill) and whether it holds the current file / a critical.
89-
export function categorySteps(): CategoryStep[] {
90-
if (!hasGuide()) return [];
91-
const byLines = S.settings.progressBy !== "files";
92-
const loc = byLines ? locByPath() : null;
93-
const curCat = !S.overviewOpen ? currentGuideEntry()?.category : undefined;
94-
const out: CategoryStep[] = [];
95-
const at = new Map<string, number>();
96-
for (const g of S.state.guide!.files) {
97-
let i = at.get(g.category);
98-
if (i === undefined) {
99-
i = out.length;
100-
at.set(g.category, i);
101-
out.push({
102-
category: g.category,
103-
total: 0,
104-
done: 0,
105-
pct: 0,
106-
critical: false,
107-
active: g.category === curCat,
108-
});
109-
}
110-
const step = out[i]!;
111-
const w = byLines ? (loc!.get(g.path) ?? 1) : 1;
112-
step.total += w;
113-
if (fileReviewState(g.path) !== "pending") step.done += w;
114-
if (g.critical) step.critical = true;
115-
}
116-
for (const s of out) s.pct = s.total ? Math.round((s.done / s.total) * 100) : 0;
117-
return out;
118-
}
119-
12093
// Jump target for a category click: its first not-yet-finished file (guide order), else its first.
12194
export function firstFileOfCategory(category: string): number | null {
12295
if (!hasGuide()) return null;
@@ -154,20 +127,37 @@ export function guideStale(): boolean {
154127
);
155128
}
156129

157-
// Render the Overview page into #diff: overview → optional PR description → the category
158-
// plan as a count+fill progress list → Start. Called by render() when overviewOpen &&
159-
// hasGuide(). Binds the Start button + per-category jumps.
130+
// One file row in the Overview list: path (dimmed dir, bright basename) + critical flag +
131+
// ±counts + a read-only review-state badge on top, the guide's one-line summary beneath.
132+
// Read-only by design — decisions stay where the diff is visible; clicking opens the file.
133+
function overviewFileRow(f: WalkFile): string {
134+
const badge =
135+
f.state === "approved"
136+
? `<svg class="ic badge approved" title="Approved"><use href="#gly-check"></use></svg>`
137+
: f.state === "changes-requested"
138+
? `<svg class="ic badge changes" title="Changes requested"><use href="#gly-flag"></use></svg>`
139+
: `<svg class="ic badge pending" title="Pending review"><use href="#gly-dot"></use></svg>`;
140+
return `<button class="go-file" data-i="${f.fileIndex}">
141+
<span class="go-file-top">
142+
<span class="go-file-path"><span class="fdir">${esc(f.dir)}</span><span class="fname">${esc(f.name)}</span></span>
143+
${f.critical ? `<svg class="ic crit" title="Critical"><use href="#gly-flag"></use></svg>` : ""}
144+
<span class="go-file-stats">${f.added ? `<i class="add">+${f.added}</i>` : ""}${f.removed ? `<i class="del">−${f.removed}</i>` : ""}${badge}</span>
145+
</span>
146+
${f.summary ? `<span class="go-file-sum">${renderMarkdownInline(f.summary)}</span>` : ""}
147+
</button>`;
148+
}
149+
150+
// Render the Overview page into #diff: overview → optional PR description → the per-file
151+
// list grouped by category → Start. Called by render() when overviewOpen && hasGuide().
152+
// Binds the Start button + per-file jumps.
160153
export function renderOverview() {
161154
const g = S.state.guide!;
162-
// Each category segment grows in proportion to how many files it holds (flex-grow = total).
163-
const plan = categorySteps()
155+
const fileList = walkGroups()
164156
.map(
165-
(
166-
c,
167-
) => `<button class="go-cat${c.critical ? " crit" : ""}${c.done === c.total ? " done" : ""}" data-cat="${esc(c.category)}" title="Jump to ${esc(c.category)} (${c.total} file${c.total === 1 ? "" : "s"})" style="flex-grow:${c.total}">
168-
<span class="go-cat-top"><span class="go-cat-lab">${c.critical ? `<svg class="ic"><use href="#gly-flag"></use></svg> ` : ""}${esc(c.category)}</span><span class="go-cat-cnt">${c.done}/${c.total}</span></span>
169-
<span class="go-cat-bar"><i style="width:${c.pct}%"></i></span>
170-
</button>`,
157+
(grp) => `<div class="go-grp">
158+
<div class="go-grp-h"><span class="go-grp-name">${esc(grp.category)}</span><span class="go-grp-meta">${grp.total} file${grp.total === 1 ? "" : "s"}${grp.added ? ` · <i class="add">+${grp.added}</i>` : ""}${grp.removed ? ` <i class="del">−${grp.removed}</i>` : ""}</span></div>
159+
${grp.files.map(overviewFileRow).join("")}
160+
</div>`,
171161
)
172162
.join("");
173163
const title = g.title || S.state.target || "Review";
@@ -177,14 +167,14 @@ export function renderOverview() {
177167
${guideStale() ? `<div class="go-stale"><svg class="ic"><use href="#gly-warn"></use></svg> This guide was generated for an earlier version of the diff. Regenerate it and restart the desk with <code>--guide</code> to refresh.</div>` : ""}
178168
<div class="go-overview md">${renderMarkdown(g.overview)}</div>
179169
${g.prDescription ? `<div class="go-pr"><b>PR description</b><div class="md">${renderMarkdown(g.prDescription)}</div></div>` : ""}
180-
${plan ? `<div class="go-plan">${plan}</div>` : ""}
170+
${fileList ? `<div class="label go-files-h">Files in this review</div><div class="go-files">${fileList}</div>` : ""}
181171
<div class="go-actions"><button class="btn primary" id="guideStart">Start Review <kbd>↵</kbd></button></div>
182172
</div></div>`;
183173
const start = $("diff").querySelector("#guideStart") as HTMLButtonElement | null;
184174
if (start) start.onclick = () => S.startGuided?.();
185175
$("diff")
186-
.querySelectorAll<HTMLElement>(".go-cat")
176+
.querySelectorAll<HTMLElement>(".go-file")
187177
.forEach((el) => {
188-
el.onclick = () => S.jumpToCategory?.(el.dataset.cat!);
178+
el.onclick = () => S.selectFile?.(Number(el.dataset.i));
189179
});
190180
}

src/ui/icon-data.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,13 @@ export const ICON_DATA: Record<string, { vb: string; body: string }> = {
5656
"gly-open-editor": {
5757
"vb": "0 0 24 24",
5858
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6m10 0l-9 9m3-9h6v6\"/>"
59+
},
60+
"gly-circle": {
61+
"vb": "0 0 24 24",
62+
"body": "<circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"/>"
63+
},
64+
"gly-circle-alert": {
65+
"vb": "0 0 24 24",
66+
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 8v4m0 4h.01\"/></g>"
5967
}
6068
};

0 commit comments

Comments
 (0)