Skip to content
84 linesCodeBlameRaw
1/**
2 * What a session recorded, step by step: prompts folded, messages in full,
3 * tool calls in a line each and their results folded. The pull request's
4 * Sessions page and a harness session's page both list them.
5 */
6import { MessageSquare, StickyNote, Terminal, Wrench } from "lucide-react";
7
8import type { SessionEntry } from "@g1t/contracts";
9
10import { TimeAgo } from "./ui";
11import { Card } from "./ui/card";
12import { Hint } from "./ui/hint";
13
14function Entry({ entry }: { entry: SessionEntry }) {
15 const at = (
16 <span className="shrink-0 text-xs text-faint">
17 <TimeAgo at={entry.at} />
18 </span>
19 );
20 if (entry.kind === "prompt") {
21 return (
22 <li className="px-4 py-3">
23 <details>
24 <summary className="flex cursor-pointer items-center gap-2 text-sm font-medium">
25 <Terminal size={14} className="text-accent" />
26 <span className="grow">Prompt</span>
27 {at}
28 </summary>
29 <pre className="mt-2 max-h-96 overflow-auto rounded-lg bg-bg p-3 text-xs whitespace-pre-wrap text-muted">{entry.text}</pre>
30 </details>
31 </li>
32 );
33 }
34 if (entry.kind === "message") {
35 return (
36 <li className="flex gap-2.5 px-4 py-3 text-sm">
37 <MessageSquare size={14} className="mt-0.5 shrink-0 text-faint" />
38 <p className="min-w-0 grow whitespace-pre-wrap wrap-break-word">{entry.text}</p>
39 {at}
40 </li>
41 );
42 }
43 if (entry.kind === "tool_call") {
44 return (
45 <li className="flex gap-2.5 px-4 py-2 text-xs">
46 <Wrench size={13} className="mt-0.5 shrink-0 text-faint" />
47 <span className="shrink-0 font-medium text-muted">{entry.tool}</span>
48 <Hint label={entry.text}>
49 <code className="min-w-0 grow truncate font-mono text-fg/85">{entry.text}</code>
50 </Hint>
51 </li>
52 );
53 }
54 if (entry.kind === "tool_result") {
55 return (
56 <li className="px-4 py-1.5 pl-10">
57 <details>
58 <summary className="cursor-pointer text-xs text-faint">Result</summary>
59 <pre className="mt-1.5 max-h-72 overflow-auto rounded-lg bg-bg p-3 text-xs text-muted">{entry.text}</pre>
60 </details>
61 </li>
62 );
63 }
64 return (
65 <li className="flex gap-2.5 px-4 py-2.5 text-xs text-muted">
66 <StickyNote size={13} className="mt-0.5 shrink-0 text-faint" />
67 <span className="min-w-0 grow">{entry.text}</span>
68 {at}
69 </li>
70 );
71}
72
73/** A session's entries in a card, oldest first. */
74export function SessionEntries({ entries }: { entries: SessionEntry[] }) {
75 return (
76 <Card asChild divided>
77 <ol>
78 {entries.map((entry) => (
79 <Entry key={entry.seq} entry={entry} />
80 ))}
81 </ol>
82 </Card>
83 );
84}