@arcreflex/agent-transcripts 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,13 +11,15 @@
11
11
 
12
12
  import type { Transcript, Message, ToolCall } from "./types.ts";
13
13
  import { createHighlighter, type Highlighter } from "shiki";
14
- import {
15
- buildTree,
16
- findLatestLeaf,
17
- tracePath,
18
- getFirstLine,
19
- } from "./utils/tree.ts";
14
+ import { walkTranscriptTree } from "./utils/tree.ts";
20
15
  import { escapeHtml } from "./utils/html.ts";
16
+ import {
17
+ THEME_VARS,
18
+ BASE_RESET,
19
+ SCROLLBAR_STYLES,
20
+ accentBar,
21
+ responsiveBase,
22
+ } from "./utils/theme.ts";
21
23
 
22
24
  // Lazy-loaded shiki highlighter
23
25
  let highlighter: Highlighter | null = null;
@@ -39,32 +41,11 @@ async function getHighlighter(): Promise<Highlighter> {
39
41
  const STYLES = `
40
42
  /* ============================================================================
41
43
  Agent Transcripts - Terminal Chronicle Theme
42
- Inspired by the Claude Code TUI: dark, focused, monospace-forward
43
44
  ============================================================================ */
44
-
45
- @import url('https://fonts.googleapis.com/css2?family=Berkeley+Mono:wght@400;500&family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500&display=swap');
45
+ ${THEME_VARS}
46
46
 
47
47
  :root {
48
- /* Typography - Monospace primary, clean sans for body */
49
- --font-mono: 'Berkeley Mono', 'IBM Plex Mono', 'JetBrains Mono', 'SF Mono', Consolas, monospace;
50
- --font-body: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
51
-
52
- /* Dark theme - Terminal aesthetic */
53
- --bg: #0d0d0d;
54
- --bg-elevated: #141414;
55
- --bg-surface: #1a1a1a;
56
- --fg: #e4e4e4;
57
- --fg-secondary: #a3a3a3;
58
- --muted: #666666;
59
- --border: #2a2a2a;
60
- --border-subtle: #222222;
61
-
62
- /* Accent - Amber/Orange (Claude Code cursor vibe) */
63
- --accent: #f59e0b;
64
- --accent-dim: #b45309;
65
- --accent-glow: rgba(245, 158, 11, 0.15);
66
-
67
- /* Semantic colors */
48
+ /* Semantic colors (transcript-specific) */
68
49
  --user-accent: #3b82f6;
69
50
  --user-bg: rgba(59, 130, 246, 0.08);
70
51
  --user-border: rgba(59, 130, 246, 0.3);
@@ -87,32 +68,11 @@ const STYLES = `
87
68
  --thinking-border: #1f1f1f;
88
69
  --raw-bg: #0a0a0a;
89
70
 
90
- /* Links */
91
- --link: #60a5fa;
92
- --link-hover: #93c5fd;
93
-
94
- /* Shadows & effects */
95
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
96
- --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
97
71
  --glow: 0 0 20px var(--accent-glow);
98
72
  }
99
73
 
100
- /* Light theme - Minimal, paper-like */
101
74
  @media (prefers-color-scheme: light) {
102
75
  :root {
103
- --bg: #fafafa;
104
- --bg-elevated: #ffffff;
105
- --bg-surface: #f5f5f5;
106
- --fg: #171717;
107
- --fg-secondary: #525252;
108
- --muted: #a3a3a3;
109
- --border: #e5e5e5;
110
- --border-subtle: #f0f0f0;
111
-
112
- --accent: #d97706;
113
- --accent-dim: #92400e;
114
- --accent-glow: rgba(217, 119, 6, 0.1);
115
-
116
76
  --user-accent: #2563eb;
117
77
  --user-bg: rgba(37, 99, 235, 0.04);
118
78
  --user-border: rgba(37, 99, 235, 0.2);
@@ -134,32 +94,10 @@ const STYLES = `
134
94
  --thinking-border: #e5e5e5;
135
95
  --raw-bg: #f0f0f0;
136
96
 
137
- --link: #2563eb;
138
- --link-hover: #1d4ed8;
139
-
140
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
141
- --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
142
97
  --glow: none;
143
98
  }
144
99
  }
145
-
146
- *, *::before, *::after { box-sizing: border-box; }
147
-
148
- html {
149
- font-size: 15px;
150
- -webkit-font-smoothing: antialiased;
151
- -moz-osx-font-smoothing: grayscale;
152
- }
153
-
154
- body {
155
- font-family: var(--font-body);
156
- background: var(--bg);
157
- color: var(--fg);
158
- line-height: 1.65;
159
- margin: 0;
160
- padding: 0;
161
- min-height: 100vh;
162
- }
100
+ ${BASE_RESET}
163
101
 
164
102
  /* Main container */
165
103
  .transcript-container {
@@ -168,35 +106,7 @@ body {
168
106
  padding: 2.5rem 2rem 4rem;
169
107
  position: relative;
170
108
  }
171
-
172
- /* Subtle left border accent */
173
- .transcript-container::before {
174
- content: '';
175
- position: fixed;
176
- left: 0;
177
- top: 0;
178
- bottom: 0;
179
- width: 2px;
180
- background: linear-gradient(
181
- 180deg,
182
- transparent 0%,
183
- var(--accent-dim) 15%,
184
- var(--accent) 50%,
185
- var(--accent-dim) 85%,
186
- transparent 100%
187
- );
188
- opacity: 0.6;
189
- }
190
-
191
- a {
192
- color: var(--link);
193
- text-decoration: none;
194
- transition: color 0.15s ease;
195
- }
196
-
197
- a:hover {
198
- color: var(--link-hover);
199
- }
109
+ ${accentBar("transcript-container")}
200
110
 
201
111
  /* ============================================================================
202
112
  Header - Terminal prompt style
@@ -678,45 +588,10 @@ details.tool-call[open] > .tool-call-header::before {
678
588
  display: none;
679
589
  }
680
590
 
681
- /* ============================================================================
682
- Scrollbar
683
- ============================================================================ */
684
-
685
- ::-webkit-scrollbar {
686
- width: 6px;
687
- height: 6px;
688
- }
689
-
690
- ::-webkit-scrollbar-track {
691
- background: var(--border-subtle);
692
- }
693
-
694
- ::-webkit-scrollbar-thumb {
695
- background: var(--muted);
696
- border-radius: 3px;
697
- }
698
-
699
- ::-webkit-scrollbar-thumb:hover {
700
- background: var(--fg-secondary);
701
- }
702
-
703
- /* ============================================================================
704
- Responsive
705
- ============================================================================ */
591
+ ${SCROLLBAR_STYLES}
592
+ ${responsiveBase("transcript-container")}
706
593
 
707
594
  @media (max-width: 640px) {
708
- html {
709
- font-size: 14px;
710
- }
711
-
712
- .transcript-container {
713
- padding: 1.5rem 1rem 3rem;
714
- }
715
-
716
- .transcript-container::before {
717
- display: none;
718
- }
719
-
720
595
  header h1 {
721
596
  font-size: 1rem;
722
597
  }
@@ -796,9 +671,6 @@ const SCRIPT = `
796
671
  // HTML Utilities
797
672
  // ============================================================================
798
673
 
799
- /**
800
- * Format JSON for display with indentation.
801
- */
802
674
  function formatJson(obj: unknown): string {
803
675
  try {
804
676
  return JSON.stringify(obj, null, 2);
@@ -865,9 +737,14 @@ async function renderMessage(
865
737
  msg: Message,
866
738
  ctx: RenderContext,
867
739
  ): Promise<string> {
868
- const rawJson = msg.rawJson
869
- ? escapeHtml(formatJson(JSON.parse(msg.rawJson)))
870
- : "";
740
+ let rawJson = "";
741
+ if (msg.rawJson) {
742
+ try {
743
+ rawJson = escapeHtml(formatJson(JSON.parse(msg.rawJson)));
744
+ } catch {
745
+ rawJson = escapeHtml(msg.rawJson);
746
+ }
747
+ }
871
748
 
872
749
  switch (msg.type) {
873
750
  case "user":
@@ -1000,77 +877,43 @@ export async function renderTranscriptHtml(
1000
877
 
1001
878
  // Build messages section
1002
879
  let messagesHtml = "";
880
+ let inAssistantTurn = false;
1003
881
 
1004
- if (transcript.messages.length === 0) {
1005
- messagesHtml = "<p><em>No messages in this transcript.</em></p>";
1006
- } else {
1007
- const { bySourceRef, children, parents } = buildTree(transcript.messages);
882
+ for (const event of walkTranscriptTree(transcript, { head })) {
883
+ switch (event.type) {
884
+ case "empty":
885
+ messagesHtml = "<p><em>No messages in this transcript.</em></p>";
886
+ break;
1008
887
 
1009
- let target: string | undefined;
1010
- if (head) {
1011
- if (!bySourceRef.has(head)) {
1012
- messagesHtml = `<p class="error">Message ID <code>${escapeHtml(head)}</code> not found</p>`;
1013
- } else {
1014
- target = head;
1015
- }
1016
- } else {
1017
- target = findLatestLeaf(bySourceRef, children);
1018
- }
888
+ case "head_not_found":
889
+ messagesHtml = `<p class="error">Message ID <code>${escapeHtml(event.head)}</code> not found</p>`;
890
+ break;
1019
891
 
1020
- if (target) {
1021
- const path = tracePath(target, parents);
1022
- const pathSet = new Set(path);
1023
- let inAssistantTurn = false;
1024
-
1025
- for (const sourceRef of path) {
1026
- const msgs = bySourceRef.get(sourceRef);
1027
- if (!msgs) continue;
1028
-
1029
- for (const msg of msgs) {
1030
- // Track when we enter/exit assistant turns
892
+ case "messages":
893
+ for (const msg of event.messages) {
1031
894
  const isAssistantContent =
1032
895
  msg.type === "assistant" || msg.type === "tool_calls";
1033
-
1034
- // Show header only at the START of an assistant turn (after user)
1035
896
  const showAssistantHeader = isAssistantContent && !inAssistantTurn;
1036
897
 
1037
898
  messagesHtml += await renderMessage(msg, { showAssistantHeader });
1038
899
 
1039
- // Update turn state
1040
900
  if (msg.type === "user") {
1041
901
  inAssistantTurn = false;
1042
902
  } else if (isAssistantContent) {
1043
903
  inAssistantTurn = true;
1044
904
  }
1045
905
  }
906
+ break;
1046
907
 
1047
- // Branch notes
1048
- if (!head) {
1049
- const childSet = children.get(sourceRef);
1050
- if (childSet && childSet.size > 1) {
1051
- const otherBranches = [...childSet].filter((c) => !pathSet.has(c));
1052
- if (otherBranches.length > 0) {
1053
- messagesHtml += `
908
+ case "branch_note":
909
+ messagesHtml += `
1054
910
  <div class="branch-note">
1055
911
  <strong>Other branches:</strong>
1056
912
  <ul>
1057
- ${otherBranches
1058
- .map((branchRef) => {
1059
- const branchMsgs = bySourceRef.get(branchRef);
1060
- if (branchMsgs && branchMsgs.length > 0) {
1061
- const firstLine = getFirstLine(branchMsgs[0]);
1062
- return `<li><code>${escapeHtml(branchRef)}</code> "${escapeHtml(firstLine)}"</li>`;
1063
- }
1064
- return "";
1065
- })
1066
- .filter(Boolean)
1067
- .join("\n ")}
913
+ ${event.branches.map((b) => `<li><code>${escapeHtml(b.sourceRef)}</code> "${escapeHtml(b.firstLine)}"</li>`).join("\n ")}
1068
914
  </ul>
1069
915
  </div>`;
1070
- }
1071
- }
1072
- }
1073
- }
916
+ break;
1074
917
  }
1075
918
  }
1076
919
 
@@ -7,8 +7,15 @@
7
7
  * - Data embedded inline (works with file://)
8
8
  */
9
9
 
10
- import type { TranscriptsIndex } from "./utils/provenance.ts";
11
10
  import { escapeHtml } from "./utils/html.ts";
11
+ import { truncate } from "./utils/text.ts";
12
+ import {
13
+ THEME_VARS,
14
+ BASE_RESET,
15
+ SCROLLBAR_STYLES,
16
+ accentBar,
17
+ responsiveBase,
18
+ } from "./utils/theme.ts";
12
19
 
13
20
  // ============================================================================
14
21
  // Styles - Terminal Chronicle Theme (Index)
@@ -18,98 +25,35 @@ const INDEX_STYLES = `
18
25
  /* ============================================================================
19
26
  Agent Transcripts Index - Terminal Chronicle Theme
20
27
  ============================================================================ */
21
-
22
- @import url('https://fonts.googleapis.com/css2?family=Berkeley+Mono:wght@400;500&family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500&display=swap');
28
+ ${THEME_VARS}
23
29
 
24
30
  :root {
25
- /* Typography */
26
- --font-mono: 'Berkeley Mono', 'IBM Plex Mono', 'JetBrains Mono', 'SF Mono', Consolas, monospace;
27
- --font-body: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
28
-
29
- /* Dark theme */
30
- --bg: #0d0d0d;
31
- --bg-elevated: #141414;
32
- --bg-surface: #1a1a1a;
33
- --fg: #e4e4e4;
34
- --fg-secondary: #a3a3a3;
35
- --muted: #666666;
36
- --border: #2a2a2a;
37
- --border-subtle: #222222;
38
-
39
- /* Accent */
40
- --accent: #f59e0b;
41
- --accent-dim: #b45309;
42
- --accent-glow: rgba(245, 158, 11, 0.15);
43
-
44
- /* Cards */
31
+ /* Index-specific tokens */
45
32
  --card-bg: var(--bg-elevated);
46
33
  --card-hover: var(--bg-surface);
47
34
  --card-border: var(--border);
48
35
  --card-border-hover: #3a3a3a;
49
36
 
50
- /* Links */
51
- --link: #60a5fa;
52
- --link-hover: #93c5fd;
53
-
54
- /* Input */
55
37
  --input-bg: var(--bg-elevated);
56
38
  --input-border: var(--border);
57
39
  --input-focus: var(--accent);
58
-
59
- /* Effects */
60
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
61
- --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
62
40
  }
63
41
 
64
42
  @media (prefers-color-scheme: light) {
65
43
  :root {
66
- --bg: #fafafa;
67
- --bg-elevated: #ffffff;
68
- --bg-surface: #f5f5f5;
69
- --fg: #171717;
70
- --fg-secondary: #525252;
71
- --muted: #a3a3a3;
72
- --border: #e5e5e5;
73
- --border-subtle: #f0f0f0;
74
-
75
- --accent: #d97706;
76
- --accent-dim: #92400e;
77
- --accent-glow: rgba(217, 119, 6, 0.1);
78
-
79
44
  --card-bg: var(--bg-elevated);
80
45
  --card-hover: var(--bg-surface);
81
46
  --card-border: var(--border);
82
47
  --card-border-hover: #d4d4d4;
83
48
 
84
- --link: #2563eb;
85
- --link-hover: #1d4ed8;
86
-
87
49
  --input-bg: var(--bg-elevated);
88
50
  --input-border: var(--border);
89
51
  --input-focus: var(--accent);
90
-
91
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
92
- --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
93
52
  }
94
53
  }
54
+ ${BASE_RESET}
95
55
 
96
- *, *::before, *::after { box-sizing: border-box; }
97
-
98
- html {
99
- font-size: 15px;
100
- -webkit-font-smoothing: antialiased;
101
- -moz-osx-font-smoothing: grayscale;
102
- }
103
-
104
- body {
105
- font-family: var(--font-body);
106
- background: var(--bg);
107
- color: var(--fg);
108
- line-height: 1.6;
109
- margin: 0;
110
- padding: 0;
111
- min-height: 100vh;
112
- }
56
+ body { line-height: 1.6; }
113
57
 
114
58
  .index-container {
115
59
  max-width: 54rem;
@@ -117,35 +61,7 @@ body {
117
61
  padding: 2.5rem 2rem 4rem;
118
62
  position: relative;
119
63
  }
120
-
121
- /* Subtle accent bar */
122
- .index-container::before {
123
- content: '';
124
- position: fixed;
125
- left: 0;
126
- top: 0;
127
- bottom: 0;
128
- width: 2px;
129
- background: linear-gradient(
130
- 180deg,
131
- transparent 0%,
132
- var(--accent-dim) 15%,
133
- var(--accent) 50%,
134
- var(--accent-dim) 85%,
135
- transparent 100%
136
- );
137
- opacity: 0.6;
138
- }
139
-
140
- a {
141
- color: var(--link);
142
- text-decoration: none;
143
- transition: color 0.15s ease;
144
- }
145
-
146
- a:hover {
147
- color: var(--link-hover);
148
- }
64
+ ${accentBar("index-container")}
149
65
 
150
66
  /* ============================================================================
151
67
  Header
@@ -314,45 +230,10 @@ header h1::before {
314
230
  margin: 0;
315
231
  }
316
232
 
317
- /* ============================================================================
318
- Scrollbar
319
- ============================================================================ */
320
-
321
- ::-webkit-scrollbar {
322
- width: 6px;
323
- height: 6px;
324
- }
325
-
326
- ::-webkit-scrollbar-track {
327
- background: var(--border-subtle);
328
- }
329
-
330
- ::-webkit-scrollbar-thumb {
331
- background: var(--muted);
332
- border-radius: 3px;
333
- }
334
-
335
- ::-webkit-scrollbar-thumb:hover {
336
- background: var(--fg-secondary);
337
- }
338
-
339
- /* ============================================================================
340
- Responsive
341
- ============================================================================ */
233
+ ${SCROLLBAR_STYLES}
234
+ ${responsiveBase("index-container")}
342
235
 
343
236
  @media (max-width: 640px) {
344
- html {
345
- font-size: 14px;
346
- }
347
-
348
- .index-container {
349
- padding: 1.5rem 1rem 3rem;
350
- }
351
-
352
- .index-container::before {
353
- display: none;
354
- }
355
-
356
237
  header h1 {
357
238
  font-size: 1.125rem;
358
239
  }
@@ -411,26 +292,6 @@ const INDEX_SCRIPT = `
411
292
  // Helpers
412
293
  // ============================================================================
413
294
 
414
- function truncate(text: string, maxLen: number): string {
415
- if (text.length <= maxLen) return text;
416
- return text.slice(0, maxLen).trim() + "...";
417
- }
418
-
419
- function formatDate(isoString: string): string {
420
- try {
421
- const date = new Date(isoString);
422
- return date.toLocaleDateString("en-US", {
423
- year: "numeric",
424
- month: "short",
425
- day: "numeric",
426
- hour: "2-digit",
427
- minute: "2-digit",
428
- });
429
- } catch {
430
- return isoString;
431
- }
432
- }
433
-
434
295
  /**
435
296
  * Format a date range compactly, e.g., "1/25 4:00-6:30"
436
297
  */
@@ -585,27 +446,3 @@ export function renderIndexFromSessions(
585
446
  </body>
586
447
  </html>`;
587
448
  }
588
-
589
- /**
590
- * Render index.html from transcripts.json data.
591
- * Convenience wrapper around renderIndexFromSessions.
592
- */
593
- export function renderIndex(
594
- index: TranscriptsIndex,
595
- options: RenderIndexOptions = {},
596
- ): string {
597
- const sessions: SessionEntry[] = Object.entries(index.entries)
598
- .filter(([filename]) => filename.endsWith(".html"))
599
- .map(([filename, entry]) => ({
600
- filename,
601
- title:
602
- entry.title || truncate(entry.firstUserMessage, 80) || entry.sessionId,
603
- firstUserMessage: entry.firstUserMessage,
604
- date: entry.startTime,
605
- endDate: entry.endTime,
606
- messageCount: entry.messageCount,
607
- cwd: entry.cwd,
608
- }));
609
-
610
- return renderIndexFromSessions(sessions, options);
611
- }