feat(shell): 30/55/15 layout, formatted activity log, instant resume, stop confirmation
Left panels 30vw / central 55% / right rail 15vw, minimal padding. Model-activity log and documents render via .thot-prose; decisions become hairline rows with semantic type chips. Resume switches the view optimistically (instant feedback). The Stop button now asks for confirmation before interrupting. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -31,21 +31,41 @@ function DocBody({ doc }: { doc: SessionDocument }) {
|
||||
}
|
||||
}
|
||||
if (doc.format === "decisions") {
|
||||
const lines = doc.content.split("\n").filter(Boolean);
|
||||
const items = doc.content
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
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);
|
||||
if (items.length === 0) return <p className="text-sm text-muted-foreground">No decisions recorded.</p>;
|
||||
return (
|
||||
<details>
|
||||
<summary className="cursor-pointer text-sm text-muted-foreground">{`${lines.length} decisions`}</summary>
|
||||
<ul className="mt-2 flex flex-col gap-1 text-xs">
|
||||
{lines.map((line, i) => {
|
||||
let d: { type?: string; subject?: string; detail?: string };
|
||||
try { d = JSON.parse(line); } catch { d = {}; }
|
||||
return <li key={i} className="font-mono">[{d.type}] {d.subject}{d.detail ? ` — ${d.detail}` : ""}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</details>
|
||||
<ul className="flex flex-col divide-y divide-border/50">
|
||||
{items.map((d, i) => (
|
||||
<li key={i} className="py-3 first:pt-0">
|
||||
<span className={`inline-block rounded-full 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>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
return <p className="whitespace-pre-wrap text-sm">{doc.content}</p>;
|
||||
return <p className="whitespace-pre-wrap text-sm leading-relaxed text-foreground/90">{doc.content}</p>;
|
||||
}
|
||||
|
||||
/** Semantic tint for a decision-type chip: approvals/promotions read as progress
|
||||
* (green), exclusions as neutral, everything else carries the brand accent. */
|
||||
function decisionChipClass(type: string): string {
|
||||
const t = type.toLowerCase();
|
||||
if (t.includes("approved") || t.includes("promoted"))
|
||||
return "bg-[oklch(var(--success)/0.15)] text-[oklch(0.47_0.12_165)]";
|
||||
if (t.includes("excluded") || t.includes("rejected") || t.includes("reopened"))
|
||||
return "bg-muted text-muted-foreground";
|
||||
return "bg-[oklch(var(--primary)/0.1)] text-primary";
|
||||
}
|
||||
|
||||
export function SessionDocumentsPanel({ session, onClose, onResume }: Props) {
|
||||
@@ -56,8 +76,8 @@ export function SessionDocumentsPanel({ session, onClose, onResume }: Props) {
|
||||
const resumable = session.status !== "finalized" && !session.archived;
|
||||
|
||||
return (
|
||||
<aside className="flex w-[380px] shrink-0 flex-col border-r border-border bg-sidebar">
|
||||
<div className="flex items-start justify-between gap-2 border-b border-border/70 px-4 py-3">
|
||||
<aside className="flex w-[30vw] max-w-[30vw] shrink-0 flex-col border-r border-border bg-sidebar">
|
||||
<div className="flex items-start justify-between gap-2 border-b border-border/70 px-3 py-3">
|
||||
<div className="min-w-0">
|
||||
<h2 className="truncate font-heading text-sm font-semibold text-foreground">
|
||||
{session.name || session.question}
|
||||
@@ -73,16 +93,23 @@ export function SessionDocumentsPanel({ session, onClose, onResume }: Props) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||
<div className="flex-1 overflow-y-auto px-3 py-5">
|
||||
{isLoading ? (
|
||||
<p className="text-xs text-muted-foreground">Loading…</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-5">
|
||||
{docs.map((doc) => (
|
||||
<section key={doc.key}>
|
||||
<h3 className="mb-2 text-[0.65rem] font-bold uppercase tracking-wide text-primary">
|
||||
{doc.title}{doc.phase !== "—" ? ` · ${doc.phase}` : ""}
|
||||
</h3>
|
||||
<div className="flex max-w-[76ch] flex-col gap-7">
|
||||
{docs.map((doc, idx) => (
|
||||
<section key={doc.key} className={idx > 0 ? "border-t border-border/50 pt-7" : ""}>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<h3 className="text-[0.7rem] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
|
||||
{doc.title}
|
||||
</h3>
|
||||
{doc.phase !== "—" && (
|
||||
<span className="rounded-full bg-muted px-1.5 py-0.5 text-[0.6rem] font-medium text-muted-foreground">
|
||||
{doc.phase}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<DocBody doc={doc} />
|
||||
</section>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user