feat: redesign persisted session summaries
This commit is contained in:
@@ -12,6 +12,7 @@ interface Props {
|
||||
session: SessionSummary;
|
||||
onClose: () => void;
|
||||
onResume: (id: string) => void;
|
||||
desktopSplit?: boolean;
|
||||
}
|
||||
|
||||
function statusLabel(s: SessionSummary): string {
|
||||
@@ -20,6 +21,25 @@ function statusLabel(s: SessionSummary): string {
|
||||
return `In progress · ${s.status}`;
|
||||
}
|
||||
|
||||
type MemoryItem = {
|
||||
status: "approved" | "declined";
|
||||
subject: string;
|
||||
detail: string;
|
||||
rationale: string;
|
||||
};
|
||||
|
||||
const HIDDEN_DECISION_TYPES = new Set([
|
||||
"concept_clarified",
|
||||
"memory_promoted",
|
||||
"memory_promotion_declined",
|
||||
"memory_rejected",
|
||||
"phase_approved",
|
||||
"phase_auto_approved",
|
||||
"table_approved",
|
||||
"table_promoted",
|
||||
"column_promoted",
|
||||
]);
|
||||
|
||||
function DocBody({ doc }: { doc: SessionDocument }) {
|
||||
if (doc.format === "sql") return <SqlViewer blocks={[{ name: doc.title, sql: doc.content }]} />;
|
||||
if (doc.format === "markdown") return <MarkdownView source={doc.content} />;
|
||||
@@ -31,6 +51,53 @@ function DocBody({ doc }: { doc: SessionDocument }) {
|
||||
return <pre className="whitespace-pre-wrap text-xs">{doc.content}</pre>;
|
||||
}
|
||||
}
|
||||
if (doc.format === "memories") {
|
||||
const raw = JSON.parse(doc.content) as unknown;
|
||||
const items = Array.isArray(raw)
|
||||
? raw.filter((item): item is MemoryItem => {
|
||||
if (!item || typeof item !== "object") return false;
|
||||
const candidate = item as Partial<MemoryItem>;
|
||||
return (
|
||||
(candidate.status === "approved" || candidate.status === "declined")
|
||||
&& typeof candidate.subject === "string"
|
||||
&& typeof candidate.detail === "string"
|
||||
&& typeof candidate.rationale === "string"
|
||||
);
|
||||
})
|
||||
: [];
|
||||
if (items.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">No memories recorded.</p>;
|
||||
}
|
||||
return (
|
||||
<ul aria-label="Memories" className="flex flex-col divide-y divide-border/50">
|
||||
{items.map((item, index) => (
|
||||
<li key={`${item.status}:${item.subject}:${index}`} className="py-4 first:pt-0">
|
||||
<span className={[
|
||||
"inline-flex rounded-md px-2 py-0.5 text-[0.62rem] font-semibold uppercase tracking-wide",
|
||||
item.status === "approved"
|
||||
? "bg-[oklch(var(--success)/0.15)] text-[oklch(0.47_0.12_165)]"
|
||||
: "bg-muted text-muted-foreground",
|
||||
].join(" ")}>
|
||||
{item.status === "approved" ? "Approved" : "Declined"}
|
||||
</span>
|
||||
<div className="mt-2 font-medium text-foreground">
|
||||
<MarkdownView source={item.subject} />
|
||||
</div>
|
||||
{item.detail && (
|
||||
<div className="mt-1 text-sm text-muted-foreground">
|
||||
<MarkdownView source={item.detail} />
|
||||
</div>
|
||||
)}
|
||||
{item.rationale && (
|
||||
<div className="mt-1 border-l-2 border-border pl-3 text-sm text-muted-foreground">
|
||||
<MarkdownView source={item.rationale} />
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
if (doc.format === "decisions") {
|
||||
const items = doc.content
|
||||
.split("\n")
|
||||
@@ -39,7 +106,9 @@ function DocBody({ doc }: { doc: SessionDocument }) {
|
||||
try { return JSON.parse(line) as { type?: string; subject?: string; detail?: string }; }
|
||||
catch { return null; }
|
||||
})
|
||||
.filter((d): d is { type?: string; subject?: string; detail?: string } => d !== null);
|
||||
.filter((d): d is { type?: string; subject?: string; detail?: string } => (
|
||||
d !== null && !HIDDEN_DECISION_TYPES.has(d.type ?? "")
|
||||
));
|
||||
if (items.length === 0) return <p className="text-sm text-muted-foreground">No decisions recorded.</p>;
|
||||
return (
|
||||
<ul className="flex flex-col divide-y divide-border/50">
|
||||
@@ -48,14 +117,22 @@ function DocBody({ doc }: { doc: SessionDocument }) {
|
||||
<span className={`inline-block rounded-md px-2 py-0.5 text-[0.62rem] font-semibold uppercase tracking-wide ${decisionChipClass(d.type ?? "")}`}>
|
||||
{(d.type ?? "decision").replace(/_/g, " ")}
|
||||
</span>
|
||||
{d.subject && <p className="mt-1.5 text-sm font-medium text-foreground">{d.subject}</p>}
|
||||
{d.detail && <p className="mt-0.5 text-sm leading-relaxed text-muted-foreground">{d.detail}</p>}
|
||||
{d.subject && (
|
||||
<div className="mt-1.5 text-sm font-medium text-foreground">
|
||||
<MarkdownView source={d.subject} />
|
||||
</div>
|
||||
)}
|
||||
{d.detail && (
|
||||
<div className="mt-0.5 text-sm leading-relaxed text-muted-foreground">
|
||||
<MarkdownView source={d.detail} />
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
return <p className="whitespace-pre-wrap text-sm leading-relaxed text-foreground/90">{doc.content}</p>;
|
||||
return <MarkdownView source={doc.content} />;
|
||||
}
|
||||
|
||||
/** Semantic tint for a decision-type chip: approvals/promotions read as progress
|
||||
@@ -69,7 +146,7 @@ function decisionChipClass(type: string): string {
|
||||
return "bg-[oklch(var(--primary)/0.1)] text-primary";
|
||||
}
|
||||
|
||||
export function SessionDocumentsPanel({ session, onClose, onResume }: Props) {
|
||||
export function SessionDocumentsPanel({ session, onClose, onResume, desktopSplit = false }: Props) {
|
||||
const { data: docs = [], isLoading } = useQuery<SessionDocument[]>({
|
||||
queryKey: ["session-documents", session.id],
|
||||
queryFn: () => getSessionDocuments(session.id),
|
||||
@@ -77,7 +154,15 @@ export function SessionDocumentsPanel({ session, onClose, onResume }: Props) {
|
||||
const resumable = session.status !== "finalized" && !session.archived;
|
||||
|
||||
return (
|
||||
<aside className="flex w-[30vw] max-w-[30vw] shrink-0 flex-col border-r border-border bg-sidebar">
|
||||
<aside
|
||||
aria-label="Session summary"
|
||||
className={[
|
||||
"flex shrink-0 flex-col border-r border-border bg-sidebar",
|
||||
desktopSplit
|
||||
? "w-[var(--session-panel-width)] max-w-[50%]"
|
||||
: "w-[30vw] max-w-[30vw]",
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 border-b border-border/60 px-3 py-3">
|
||||
<div className="min-w-0">
|
||||
<h2 className="truncate font-heading text-sm font-semibold text-foreground">
|
||||
|
||||
Reference in New Issue
Block a user