;
@@ -30,10 +28,8 @@ interface ChapterSidePanelProps {
}
export function ChapterSidePanel({
- runId,
chapter,
chapterIndex,
- allChapters,
chapterEntries,
viewedChapterIds,
checkedKeyChangeIds,
@@ -97,10 +93,8 @@ export function ChapterSidePanel({
>
Date: Mon, 4 May 2026 10:24:46 -0700
Subject: [PATCH 4/8] feat: add line counts to chapter side panel header
Display +N/-N line change counts below the chapter title in the side
panel, sourced from ChapterContext.
---
.../src/components/chapter/chapter-side-panel.tsx | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/packages/web/src/components/chapter/chapter-side-panel.tsx b/packages/web/src/components/chapter/chapter-side-panel.tsx
index c023a32..2f23870 100644
--- a/packages/web/src/components/chapter/chapter-side-panel.tsx
+++ b/packages/web/src/components/chapter/chapter-side-panel.tsx
@@ -1,6 +1,8 @@
import type { Chapter } from "@stage-cli/types/chapters";
import { useCallback, useEffect, useRef, useState } from "react";
+import { LineCounts } from "@/components/shared/line-counts";
import { Markdown } from "@/components/ui/markdown";
+import { useChapterContext } from "@/lib/chapter-context";
import type { FileDiffEntry } from "@/lib/parse-diff";
import { ChapterFileList } from "./chapter-file-list";
import { ChapterNavigator } from "./chapter-navigator";
@@ -42,6 +44,8 @@ export function ChapterSidePanel({
onSelectFile,
onCopyChapter,
}: ChapterSidePanelProps) {
+ const { chapterLineCountsMap } = useChapterContext();
+ const lineCounts = chapterLineCountsMap.get(chapter.id);
const [width, setWidth] = useState(SSR_FALLBACK_WIDTH);
const cleanupRef = useRef<(() => void) | null>(null);
@@ -102,8 +106,17 @@ export function ChapterSidePanel({
+ {lineCounts ? (
+
+ ) : (
+
+ )}
Date: Mon, 4 May 2026 10:26:18 -0700
Subject: [PATCH 5/8] feat: add line counts to chapters index page
Each chapter entry in the index now shows +N/-N line change counts
floated to the right of the chapter title, sourced from ChapterContext.
---
packages/web/src/routes/chapters-index-page.tsx | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/packages/web/src/routes/chapters-index-page.tsx b/packages/web/src/routes/chapters-index-page.tsx
index 34bb7cf..41d2923 100644
--- a/packages/web/src/routes/chapters-index-page.tsx
+++ b/packages/web/src/routes/chapters-index-page.tsx
@@ -3,10 +3,12 @@ import { Link } from "@tanstack/react-router";
import { ChevronRight, Circle, CircleCheck } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { FileViewRow } from "@/components/chapter";
+import { LineCounts } from "@/components/shared/line-counts";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import { type ChapterLineCounts, useChapterContext } from "@/lib/chapter-context";
import { useViewState } from "@/lib/use-view-state";
import { cn } from "@/lib/utils";
@@ -65,6 +67,7 @@ interface ChapterEntryProps {
filePaths: string[];
isFilesOpen: boolean;
runId: string;
+ lineCounts: ChapterLineCounts | undefined;
onToggleViewed: () => void;
onToggleFiles: () => void;
onToggleAllFiles: () => void;
@@ -76,6 +79,7 @@ function ChapterEntry({
filePaths,
isFilesOpen,
runId,
+ lineCounts,
onToggleViewed,
onToggleFiles,
onToggleAllFiles,
@@ -115,6 +119,13 @@ function ChapterEntry({
backgroundPosition: "0 calc(100% - 0.35em)",
}}
>
+ {lineCounts && (
+
+ )}
>(() => new Set());
const [mounted, setMounted] = useState(false);
@@ -221,6 +233,7 @@ function ChaptersList({ chapters, runId, viewedCount }: ChaptersListProps) {
filePaths={filePathsByChapter.get(c.id) ?? []}
isFilesOpen={openFiles.has(c.id)}
runId={runId}
+ lineCounts={chapterLineCountsMap.get(c.id)}
onToggleViewed={() =>
isViewed ? view.unmarkChapterViewed(externalId) : view.markChapterViewed(externalId)
}
From 12820503043d49f831e801a3e5c820c3d5d36b10 Mon Sep 17 00:00:00 2001
From: Dean Stratakos <29683763+dastratakos@users.noreply.github.com>
Date: Mon, 4 May 2026 10:28:47 -0700
Subject: [PATCH 6/8] refactor: simplify ChapterDetailContent props via context
ChapterDetailContent now reads runId and allChapters from
ChapterContext instead of receiving them as props, reducing its
interface from 5 props to 3.
---
.../web/src/routes/chapter-detail-page.tsx | 22 ++++---------------
1 file changed, 4 insertions(+), 18 deletions(-)
diff --git a/packages/web/src/routes/chapter-detail-page.tsx b/packages/web/src/routes/chapter-detail-page.tsx
index daa6296..f827a67 100644
--- a/packages/web/src/routes/chapter-detail-page.tsx
+++ b/packages/web/src/routes/chapter-detail-page.tsx
@@ -11,6 +11,7 @@ import {
} from "@/components/files";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
+import { useChapterContext } from "@/lib/chapter-context";
import { FILE_STATUS } from "@/lib/diff-types";
import { filterFilesForChapter } from "@/lib/filter-files-for-chapter";
import { formatChapterAsMarkdown } from "@/lib/format-chapter-markdown";
@@ -67,32 +68,17 @@ export function ChapterDetailPage({ runId, chapterNumber }: ChapterDetailPagePro
return ;
}
- return (
-
- );
+ return ;
}
interface ChapterDetailContentProps {
- runId: string;
chapter: Chapter;
chapterIndex: number;
- allChapters: Chapter[];
patch: string;
}
-function ChapterDetailContent({
- runId,
- chapter,
- chapterIndex,
- allChapters,
- patch,
-}: ChapterDetailContentProps) {
+function ChapterDetailContent({ chapter, chapterIndex, patch }: ChapterDetailContentProps) {
+ const { runId, chapters: allChapters } = useChapterContext();
const view = useViewState(runId);
const [focusedKeyChangeId, setFocusedKeyChangeId] = useState(null);
From d619d79f66125a41a6f796159f57a1aaf87c5c14 Mon Sep 17 00:00:00 2001
From: Dean Stratakos <29683763+dastratakos@users.noreply.github.com>
Date: Mon, 4 May 2026 11:02:06 -0700
Subject: [PATCH 7/8] fix: handle zero line counts spacing in side panel header
When a chapter has zero additions and deletions, LineCounts returns
null but the fallback spacer was unreachable because the counts object
is always truthy. Check for non-zero counts before rendering.
---
packages/web/src/components/chapter/chapter-side-panel.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/web/src/components/chapter/chapter-side-panel.tsx b/packages/web/src/components/chapter/chapter-side-panel.tsx
index 2f23870..d6dcb88 100644
--- a/packages/web/src/components/chapter/chapter-side-panel.tsx
+++ b/packages/web/src/components/chapter/chapter-side-panel.tsx
@@ -108,7 +108,7 @@ export function ChapterSidePanel({
inheritSize
className="pb-1 pl-6 pr-4 font-semibold text-base leading-snug [&_.md-p]:my-0 lg:pl-8"
/>
- {lineCounts ? (
+ {lineCounts && (lineCounts.linesAdded > 0 || lineCounts.linesDeleted > 0) ? (
Date: Mon, 4 May 2026 11:02:11 -0700
Subject: [PATCH 8/8] fix: use context chapters instead of duplicated local
sort
ChapterDetailPage now reads the sorted chapters array from
ChapterContext instead of maintaining its own duplicate sort,
eliminating the fragile coupling between two independent sorts.
---
.../web/src/routes/chapter-detail-page.tsx | 18 ++++--------------
1 file changed, 4 insertions(+), 14 deletions(-)
diff --git a/packages/web/src/routes/chapter-detail-page.tsx b/packages/web/src/routes/chapter-detail-page.tsx
index f827a67..a25d5f4 100644
--- a/packages/web/src/routes/chapter-detail-page.tsx
+++ b/packages/web/src/routes/chapter-detail-page.tsx
@@ -36,23 +36,13 @@ interface ChapterDetailPageProps {
}
export function ChapterDetailPage({ runId, chapterNumber }: ChapterDetailPageProps) {
- const {
- data: chaptersData,
- isLoading: chaptersLoading,
- error: chaptersError,
- } = useChapters(runId);
+ const { chapters } = useChapterContext();
+ const { isLoading: chaptersLoading, error: chaptersError } = useChapters(runId);
const { data: patch, isLoading: patchLoading, error: patchError } = useDiffPatch(runId);
- const allChapters = useMemo(() => {
- if (!chaptersData?.chapters) return [];
- return [...chaptersData.chapters].sort((a, b) => a.order - b.order);
- }, [chaptersData?.chapters]);
- // Look up by `order` rather than indexing — the schema only requires
- // positive ints, so chapters can have gaps (1, 3, 5) and array position
- // won't match the URL chapter number.
const chapter =
- chapterNumber === null ? undefined : allChapters.find((c) => c.order === chapterNumber);
- const chapterIndex = chapter ? allChapters.indexOf(chapter) : -1;
+ chapterNumber === null ? undefined : chapters.find((c) => c.order === chapterNumber);
+ const chapterIndex = chapter ? chapters.indexOf(chapter) : -1;
const isLoading = chaptersLoading || patchLoading;
const error = chaptersError ?? patchError;