From 8711c368337417f8f790826e495e3aec63f3c432 Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 16 Sep 2026 16:25:19 +0200 Subject: [PATCH] feat(card): progress narrative and humanised commit history --- .../[projectSlug]/c/[cardId]/page.tsx | 5 ++ src/components/card/card-narrative.tsx | 34 ++++++++ src/components/card/commit-history.tsx | 83 +++++++++++++++++++ src/lib/data/card.ts | 13 +++ 4 files changed, 135 insertions(+) create mode 100644 src/components/card/card-narrative.tsx create mode 100644 src/components/card/commit-history.tsx diff --git a/src/app/p/[clientSlug]/[projectSlug]/c/[cardId]/page.tsx b/src/app/p/[clientSlug]/[projectSlug]/c/[cardId]/page.tsx index cef8128..9a5ad43 100644 --- a/src/app/p/[clientSlug]/[projectSlug]/c/[cardId]/page.tsx +++ b/src/app/p/[clientSlug]/[projectSlug]/c/[cardId]/page.tsx @@ -6,6 +6,8 @@ import { AppShell } from "@/components/shell/app-shell"; import { TopBar } from "@/components/shell/top-bar"; import { CardHeader } from "@/components/card/card-header"; import { CardActions } from "@/components/card/card-actions"; +import { CardNarrative } from "@/components/card/card-narrative"; +import { CommitHistory } from "@/components/card/commit-history"; import { SubtaskList } from "@/components/card/subtask-list"; import { CommentThread } from "@/components/card/comment-thread"; import { ActivityTimeline } from "@/components/card/activity-timeline"; @@ -64,6 +66,9 @@ export default async function CardPage({ projectSlug={projectSlug} /> + + + +

+ Progress +

+ +
+ {narrative + .split(/\n{2,}/) + .map((paragraph) => paragraph.trim()) + .filter(Boolean) + .map((paragraph, index) => ( +

+ {paragraph} +

+ ))} +
+ +

+ Written automatically from the change history for this item. +

+ + ); +} diff --git a/src/components/card/commit-history.tsx b/src/components/card/commit-history.tsx new file mode 100644 index 0000000..5a0a6e1 --- /dev/null +++ b/src/components/card/commit-history.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useState } from "react"; +import { Code2 } from "lucide-react"; + +import { relativeTime } from "@/lib/time/relative"; +import type { CommitRecord } from "@/lib/pb/types"; + +/** + * A card's commits, in the client's language. + * + * Two rules, both from the spec: + * - A commit with no summary shows its raw subject. Never a gap — a card that + * renders blank because the provider was down looks like no work happened. + * - The raw commit is always one toggle away. The humanised text is a + * translation, not a replacement, and a client who wants to check it must be + * able to. + */ +export function CommitHistory({ commits }: { commits: CommitRecord[] }) { + const [showRaw, setShowRaw] = useState(false); + + if (commits.length === 0) return null; + + return ( +
+
+

+ Work done +

+ + {commits.length} {commits.length === 1 ? "change" : "changes"} + + +
+ +
    + {commits.map((commit) => ( +
  1. + +
    +

    + {/* Fall back to the raw subject — never render nothing. */} + {commit.summary || commit.subject} +

    +

    + {relativeTime(commit.authored_at)} +

    + + {showRaw && ( +

    + {commit.url ? ( + + {commit.short_sha} + + ) : ( + commit.short_sha + )}{" "} + {commit.subject} +

    + )} +
    +
  2. + ))} +
+
+ ); +} diff --git a/src/lib/data/card.ts b/src/lib/data/card.ts index 2db8dc4..4198d13 100644 --- a/src/lib/data/card.ts +++ b/src/lib/data/card.ts @@ -7,6 +7,7 @@ import type { ClientRecord, ColumnRecord, CommentRecord, + CommitRecord, ProjectRecord, SubtaskRecord, UserRecord, @@ -23,6 +24,10 @@ export interface CardDetail { activity: ActivityRecord[]; /** Display names by user id, for comments and the timeline. */ names: Record; + /** Non-merge commits linked to this card, oldest first, with AI summaries + * when present and the raw subject otherwise. Used by the Progress section + * and the humanised commit history on the card page. */ + commits: CommitRecord[]; } /** @@ -83,6 +88,13 @@ export async function loadCardDetail( sort: "-created", }); + // Linked commits, oldest first, in plain English where available. Used by + // the Progress section and the humanised commit history on the card page. + const commits = await pb.collection("commits").getFullList({ + filter: `card = "${cardId}" && is_merge = false`, + sort: "authored_at", + }); + const userIds = new Set(); for (const c of comments) if (c.author) userIds.add(c.author); for (const a of activity) if (a.actor) userIds.add(a.actor); @@ -107,6 +119,7 @@ export async function loadCardDetail( comments, activity, names, + commits, }; } catch { return null;