claudeye 0.2.2 → 0.3.0

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.

Potentially problematic release.


This version of claudeye might be problematic. Click here for more details.

Files changed (191) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/standalone/.next/build-manifest.json +5 -5
  4. package/.next/standalone/.next/routes-manifest.json +9 -0
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  17. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  21. package/.next/standalone/.next/server/app/_not-found.rsc +5 -5
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +5 -5
  23. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  24. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +5 -5
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  27. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route/app-paths-manifest.json +3 -0
  29. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route/build-manifest.json +11 -0
  30. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route/server-reference-manifest.json +4 -0
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +6 -0
  32. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.map +5 -0
  33. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -0
  34. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route_client-reference-manifest.js +2 -0
  35. package/.next/standalone/.next/server/app/icon.png/route.js +2 -1
  36. package/.next/standalone/.next/server/app/icon.png/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  38. package/.next/standalone/.next/server/app/page.js +1 -1
  39. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  42. package/.next/standalone/.next/server/app/project/[name]/page.js +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +35 -5
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -3
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  52. package/.next/standalone/.next/server/chunks/[root-of-the-server]__f408c708._.js +21 -0
  53. package/.next/standalone/.next/server/chunks/[root-of-the-server]__fde83e67._.js +3 -0
  54. package/.next/standalone/.next/server/chunks/ce889_server_app_api_download_[project]_[session]_route_actions_bbdd823f.js +3 -0
  55. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_64175717.js +3 -0
  56. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__070e2009._.js +1 -1
  57. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0a745465._.js +3 -0
  58. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__bc37261c._.js → [root-of-the-server]__14f58da3._.js} +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__164d9311._.js +3 -0
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__2822fd21._.js +6 -0
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__31b4c2fd._.js +3 -0
  62. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__45656df2._.js → [root-of-the-server]__4e339665._.js} +2 -2
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__55018089._.js +3 -0
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__6313e929._.js +3 -0
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__7e21395a._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__ee388ee0._.js +3 -0
  67. package/.next/standalone/.next/server/chunks/ssr/_0b4924bd._.js +3 -0
  68. package/.next/standalone/.next/server/chunks/ssr/{node_modules_9e089768._.js → _1404b353._.js} +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/_3d21dde5._.js +8 -0
  70. package/.next/standalone/.next/server/chunks/ssr/_fd9b1ff7._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_7769b563._.js → node_modules_next_dist_esm_eedfc1fd._.js} +2 -2
  72. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  73. package/.next/standalone/.next/server/pages/404.html +2 -2
  74. package/.next/standalone/.next/server/pages/500.html +2 -2
  75. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/server-reference-manifest.json +35 -5
  77. package/.next/standalone/.next/static/chunks/0e266948a26a3cdf.js +1 -0
  78. package/.next/standalone/.next/static/chunks/2774382cf796393c.js +4 -0
  79. package/.next/standalone/.next/static/chunks/6189ca16caad4352.js +3 -0
  80. package/.next/standalone/.next/static/chunks/8111dbe882e31821.js +1 -0
  81. package/.next/standalone/.next/static/chunks/{5a424275276f2bb9.js → bdeaeb8c9876394b.js} +1 -1
  82. package/.next/standalone/.next/static/chunks/cdbb6932218650fd.js +1 -0
  83. package/.next/standalone/.next/static/chunks/ea03555bb726c073.css +1 -0
  84. package/.next/standalone/.next/static/chunks/f091501564eb2ea3.js +4 -0
  85. package/.next/standalone/.next/static/chunks/{turbopack-2315171089e56fad.js → turbopack-fc1f23734a087d36.js} +1 -1
  86. package/.next/standalone/README.md +528 -41
  87. package/.next/standalone/app/actions/run-enrichments.ts +26 -5
  88. package/.next/standalone/app/actions/run-evals.ts +26 -5
  89. package/.next/standalone/app/actions/run-subagent-enrichments.ts +89 -0
  90. package/.next/standalone/app/actions/run-subagent-evals.ts +88 -0
  91. package/.next/standalone/app/api/download/[project]/[session]/route.ts +49 -0
  92. package/.next/standalone/app/components/copy-button.tsx +37 -0
  93. package/.next/standalone/app/components/enrichment-results-panel.tsx +33 -13
  94. package/.next/standalone/app/components/eval-results-panel.tsx +33 -13
  95. package/.next/standalone/app/components/log-viewer/entry-row.tsx +43 -14
  96. package/.next/standalone/app/components/log-viewer/queue-divider.tsx +50 -7
  97. package/.next/standalone/app/components/log-viewer/tool-input-output.tsx +13 -3
  98. package/.next/standalone/app/components/project-list.tsx +11 -11
  99. package/.next/standalone/app/components/raw-log-viewer.tsx +80 -11
  100. package/.next/standalone/app/components/refresh-button.tsx +79 -0
  101. package/.next/standalone/app/components/sessions-list.tsx +23 -14
  102. package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +23 -12
  103. package/.next/standalone/bin/claudeye.mjs +112 -25
  104. package/.next/standalone/components/navbar.tsx +2 -0
  105. package/.next/standalone/dist/app.js +10 -4
  106. package/.next/standalone/dist/condition-registry.js +20 -0
  107. package/.next/standalone/dist/enrich-registry.js +26 -3
  108. package/.next/standalone/dist/enrich-runner.js +68 -13
  109. package/.next/standalone/dist/registry.js +26 -3
  110. package/.next/standalone/dist/runner.js +78 -20
  111. package/.next/standalone/dist/server-spawn.js +58 -34
  112. package/.next/standalone/lib/cache/hash.ts +67 -0
  113. package/.next/standalone/lib/cache/index.ts +9 -0
  114. package/.next/standalone/lib/cache/local-backend.ts +81 -0
  115. package/.next/standalone/lib/cache/manager.ts +127 -0
  116. package/.next/standalone/lib/cache/types.ts +19 -0
  117. package/.next/standalone/lib/evals/app.ts +30 -7
  118. package/.next/standalone/lib/evals/condition-registry.ts +26 -0
  119. package/.next/standalone/lib/evals/enrich-registry.ts +29 -3
  120. package/.next/standalone/lib/evals/enrich-runner.ts +68 -14
  121. package/.next/standalone/lib/evals/enrich-types.ts +6 -1
  122. package/.next/standalone/lib/evals/index.ts +3 -1
  123. package/.next/standalone/lib/evals/registry.ts +29 -4
  124. package/.next/standalone/lib/evals/runner.ts +77 -20
  125. package/.next/standalone/lib/evals/server-spawn.ts +67 -41
  126. package/.next/standalone/lib/evals/types.ts +16 -0
  127. package/.next/standalone/lib/log-format.ts +22 -1
  128. package/.next/standalone/package-lock.json +244 -308
  129. package/.next/standalone/package.json +1 -1
  130. package/.next/standalone/scripts/dev.ts +3 -1
  131. package/.next/standalone/scripts/parse-script-args.ts +30 -2
  132. package/.next/standalone/scripts/start.ts +3 -1
  133. package/.next/standalone/tsconfig.tsbuildinfo +1 -1
  134. package/README.md +528 -41
  135. package/bin/claudeye.mjs +112 -25
  136. package/dist/app.d.ts +17 -3
  137. package/dist/app.d.ts.map +1 -1
  138. package/dist/app.js +10 -4
  139. package/dist/app.js.map +1 -1
  140. package/dist/condition-registry.d.ts +9 -0
  141. package/dist/condition-registry.d.ts.map +1 -0
  142. package/dist/condition-registry.js +20 -0
  143. package/dist/condition-registry.js.map +1 -0
  144. package/dist/enrich-registry.d.ts +5 -1
  145. package/dist/enrich-registry.d.ts.map +1 -1
  146. package/dist/enrich-registry.js +26 -3
  147. package/dist/enrich-registry.js.map +1 -1
  148. package/dist/enrich-runner.d.ts +3 -3
  149. package/dist/enrich-runner.d.ts.map +1 -1
  150. package/dist/enrich-runner.js +68 -13
  151. package/dist/enrich-runner.js.map +1 -1
  152. package/dist/enrich-types.d.ts +6 -1
  153. package/dist/enrich-types.d.ts.map +1 -1
  154. package/dist/index.d.ts +2 -2
  155. package/dist/index.d.ts.map +1 -1
  156. package/dist/registry.d.ts +5 -2
  157. package/dist/registry.d.ts.map +1 -1
  158. package/dist/registry.js +26 -3
  159. package/dist/registry.js.map +1 -1
  160. package/dist/runner.d.ts +2 -2
  161. package/dist/runner.d.ts.map +1 -1
  162. package/dist/runner.js +78 -20
  163. package/dist/runner.js.map +1 -1
  164. package/dist/server-spawn.d.ts +2 -1
  165. package/dist/server-spawn.d.ts.map +1 -1
  166. package/dist/server-spawn.js +58 -34
  167. package/dist/server-spawn.js.map +1 -1
  168. package/dist/types.d.ts +14 -0
  169. package/dist/types.d.ts.map +1 -1
  170. package/package.json +1 -1
  171. package/.next/standalone/.next/server/chunks/[root-of-the-server]__24a1e50a._.js +0 -21
  172. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__32f115c9._.js +0 -6
  173. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__476a1712._.js +0 -3
  174. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__4ddcabf2._.js +0 -3
  175. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__ad593585._.js +0 -3
  176. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__afd8e13b._.js +0 -3
  177. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__dd7ee810._.js +0 -3
  178. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__ff3004de._.js +0 -3
  179. package/.next/standalone/.next/server/chunks/ssr/_53472598._.js +0 -3
  180. package/.next/standalone/.next/server/chunks/ssr/_863b6ca8._.js +0 -3
  181. package/.next/standalone/.next/server/chunks/ssr/_f7347c74._.js +0 -5
  182. package/.next/standalone/.next/static/chunks/2243ff2814e7a781.js +0 -3
  183. package/.next/standalone/.next/static/chunks/50531467396cea91.css +0 -1
  184. package/.next/standalone/.next/static/chunks/8f288c01f8d7ef2d.js +0 -1
  185. package/.next/standalone/.next/static/chunks/abab1b00b2788443.js +0 -4
  186. package/.next/standalone/.next/static/chunks/d250d7f6f0a8c325.js +0 -1
  187. package/.next/standalone/.next/static/chunks/d7a572a8b7eb1ec8.js +0 -1
  188. package/.next/standalone/.next/static/chunks/fb1b0b9da3f03023.js +0 -4
  189. /package/.next/standalone/.next/static/{LoGIEEP4cORCqcFv-Ywg0 → 5JsV7rfAEOIwNOQPaX3UP}/_buildManifest.js +0 -0
  190. /package/.next/standalone/.next/static/{LoGIEEP4cORCqcFv-Ywg0 → 5JsV7rfAEOIwNOQPaX3UP}/_clientMiddlewareManifest.json +0 -0
  191. /package/.next/standalone/.next/static/{LoGIEEP4cORCqcFv-Ywg0 → 5JsV7rfAEOIwNOQPaX3UP}/_ssgManifest.js +0 -0
@@ -7,9 +7,12 @@ import { ENTRY_BORDER_COLORS } from "./constants";
7
7
  import { TypeBadge } from "./type-badge";
8
8
  import { ToolInputOutput } from "./tool-input-output";
9
9
  import { StatsBar } from "./stats-bar";
10
+ import EvalResultsPanel from "@/app/components/eval-results-panel";
11
+ import EnrichmentResultsPanel from "@/app/components/enrichment-results-panel";
10
12
  import { QueueDivider } from "./queue-divider";
11
13
  import { UserContent, AssistantContent, GenericContent } from "./content-block-view";
12
- import { formatLocalTimestamp } from "@/lib/log-format";
14
+ import { formatLocalTimestamp, getEntryTextContent } from "@/lib/log-format";
15
+ import { CopyButton } from "@/app/components/copy-button";
13
16
 
14
17
  // ── Subagent Tool Card ──
15
18
 
@@ -108,7 +111,25 @@ export function SubagentToolCard({ block, projectName, sessionId }: SubagentTool
108
111
  <div className="text-xs text-muted-foreground py-2">No entries found in subagent log.</div>
109
112
  )}
110
113
  {entries && entries.length > 0 && (
111
- <StatsBar entries={entries} compact />
114
+ <>
115
+ <StatsBar entries={entries} compact />
116
+ <EvalResultsPanel
117
+ projectName={projectName}
118
+ sessionId={sessionId}
119
+ agentId={block.subagentId}
120
+ subagentType={block.subagentType}
121
+ subagentDescription={block.subagentDescription}
122
+ compact
123
+ />
124
+ <EnrichmentResultsPanel
125
+ projectName={projectName}
126
+ sessionId={sessionId}
127
+ agentId={block.subagentId}
128
+ subagentType={block.subagentType}
129
+ subagentDescription={block.subagentDescription}
130
+ compact
131
+ />
132
+ </>
112
133
  )}
113
134
  {entries && entries.map((entry) =>
114
135
  entry.type === "queue-operation" ? (
@@ -142,27 +163,35 @@ interface EntryRowProps {
142
163
  sessionId: string;
143
164
  }
144
165
 
166
+ function EntryContent({ entry, projectName, sessionId }: EntryRowProps): React.ReactNode {
167
+ switch (entry.type) {
168
+ case "user":
169
+ return <UserContent entry={entry} />;
170
+ case "assistant":
171
+ return <AssistantContent entry={entry} projectName={projectName} sessionId={sessionId} />;
172
+ case "file-history-snapshot":
173
+ case "progress":
174
+ case "system":
175
+ return <GenericContent entry={entry} />;
176
+ }
177
+ }
178
+
145
179
  export const EntryRow = React.memo(function EntryRow({ entry, projectName, sessionId }: EntryRowProps) {
146
180
  return (
147
181
  <div
148
182
  className={`border-l-4 ${ENTRY_BORDER_COLORS[entry.type]} bg-card/50 rounded-r-lg mb-2 hover:bg-muted/30 transition-colors`}
149
183
  >
150
- {/* Header row */}
151
184
  <div className="flex items-center gap-2 px-4 py-2 border-b border-border/30">
152
185
  <TypeBadge type={entry.type} />
153
- <span className="text-xs font-mono text-muted-foreground ml-auto whitespace-nowrap">
154
- {formatLocalTimestamp(entry.timestampMs)}
155
- </span>
186
+ <div className="flex items-center gap-1 ml-auto">
187
+ <CopyButton text={getEntryTextContent(entry)} />
188
+ <span className="text-xs font-mono text-muted-foreground whitespace-nowrap">
189
+ {formatLocalTimestamp(entry.timestampMs)}
190
+ </span>
191
+ </div>
156
192
  </div>
157
- {/* Content area */}
158
193
  <div className="px-4 py-3">
159
- {entry.type === "user" && <UserContent entry={entry} />}
160
- {entry.type === "assistant" && (
161
- <AssistantContent entry={entry} projectName={projectName} sessionId={sessionId} />
162
- )}
163
- {(entry.type === "file-history-snapshot" ||
164
- entry.type === "progress" ||
165
- entry.type === "system") && <GenericContent entry={entry} />}
194
+ <EntryContent entry={entry} projectName={projectName} sessionId={sessionId} />
166
195
  </div>
167
196
  </div>
168
197
  );
@@ -1,17 +1,60 @@
1
1
  import React from "react";
2
- import { Play } from "lucide-react";
2
+ import { Play, ChevronRight } from "lucide-react";
3
3
  import type { QueueOperationEntry } from "@/lib/log-entries";
4
4
  import { formatLocalTimestamp } from "@/lib/log-format";
5
5
 
6
- export const QueueDivider = React.memo(function QueueDivider({ entry }: { entry: QueueOperationEntry }) {
6
+ interface QueueDividerProps {
7
+ entry: QueueOperationEntry;
8
+ isCollapsed?: boolean;
9
+ entryCount?: number;
10
+ onToggle?: () => void;
11
+ }
12
+
13
+ export const QueueDivider = React.memo(function QueueDivider({
14
+ entry,
15
+ isCollapsed,
16
+ entryCount,
17
+ onToggle,
18
+ }: QueueDividerProps) {
19
+ const interactive = typeof onToggle === "function";
20
+
21
+ const pill = (
22
+ <div className={`flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-medium${interactive ? " group-hover:bg-primary/20 transition-colors" : ""}`}>
23
+ {interactive ? (
24
+ <ChevronRight
25
+ className={`w-3 h-3 transition-transform ${isCollapsed ? "" : "rotate-90"}`}
26
+ />
27
+ ) : (
28
+ <Play className="w-3 h-3" />
29
+ )}
30
+ <span>{entry.label}</span>
31
+ <span className="text-muted-foreground">{formatLocalTimestamp(entry.timestampMs)}</span>
32
+ {interactive && isCollapsed && typeof entryCount === "number" && (
33
+ <span className="ml-1 px-1.5 py-0.5 rounded bg-primary/20 text-[10px] leading-none">
34
+ {entryCount} {entryCount === 1 ? "entry" : "entries"}
35
+ </span>
36
+ )}
37
+ </div>
38
+ );
39
+
40
+ if (interactive) {
41
+ return (
42
+ <button
43
+ type="button"
44
+ onClick={onToggle}
45
+ className="group flex items-center gap-3 py-3 px-4 w-full cursor-pointer"
46
+ >
47
+ <div className="flex-1 h-px bg-primary/30" />
48
+ {pill}
49
+ <div className="flex-1 h-px bg-primary/30" />
50
+ </button>
51
+ );
52
+ }
53
+
7
54
  return (
8
55
  <div className="flex items-center gap-3 py-3 px-4">
9
56
  <div className="flex-1 h-px bg-primary/30" />
10
- <div className="flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-medium">
11
- <Play className="w-3 h-3" />
12
- <span>{entry.label}</span>
13
- <span className="text-muted-foreground">{formatLocalTimestamp(entry.timestampMs)}</span>
14
- </div>
57
+ {pill}
15
58
  <div className="flex-1 h-px bg-primary/30" />
16
59
  </div>
17
60
  );
@@ -1,15 +1,22 @@
1
+ import { useMemo } from "react";
1
2
  import type { ToolUseBlock } from "@/lib/log-entries";
2
3
  import { formatInput, formatLocalTimestamp } from "@/lib/log-format";
4
+ import { CopyButton } from "@/app/components/copy-button";
3
5
 
4
6
  export function ToolInputOutput({ block }: { block: ToolUseBlock }) {
7
+ const inputText = useMemo(() => formatInput(block), [block]);
8
+
5
9
  return (
6
10
  <div className="space-y-2">
7
11
  <details open>
8
12
  <summary className="text-xs text-muted-foreground cursor-pointer hover:text-foreground transition-colors">
9
- Input
13
+ <span className="inline-flex items-center gap-1">
14
+ Input
15
+ <CopyButton text={inputText} />
16
+ </span>
10
17
  </summary>
11
18
  <pre className="mt-1 p-2 bg-muted/50 rounded text-xs whitespace-pre-wrap break-words max-h-64 overflow-y-auto">
12
- {formatInput(block)}
19
+ {inputText}
13
20
  </pre>
14
21
  </details>
15
22
  {block.result ? (
@@ -25,7 +32,10 @@ export function ToolInputOutput({ block }: { block: ToolUseBlock }) {
25
32
  {block.result.content && (
26
33
  <details open>
27
34
  <summary className="text-xs text-muted-foreground cursor-pointer hover:text-foreground transition-colors">
28
- Output
35
+ <span className="inline-flex items-center gap-1">
36
+ Output
37
+ <CopyButton text={block.result.content} />
38
+ </span>
29
39
  </summary>
30
40
  <pre className="mt-1 p-2 bg-muted/50 rounded text-xs whitespace-pre-wrap break-words max-h-48 overflow-y-auto">
31
41
  {block.result.content}
@@ -20,6 +20,7 @@ import Link from "next/link";
20
20
  import PaginationControls from "./pagination-controls";
21
21
  import DatePickerInput from "./date-picker-input";
22
22
 
23
+
23
24
  interface ProjectListProps {
24
25
  folders: ProjectFolder[];
25
26
  }
@@ -93,7 +94,7 @@ export default function ProjectList({ folders }: ProjectListProps) {
93
94
  {/* Filter Bar */}
94
95
  <div className="bg-card border border-border rounded-lg p-4">
95
96
  <div className="flex flex-col gap-4">
96
- {/* Preset Filters */}
97
+ {/* Preset Filters + Refresh */}
97
98
  <div className="flex flex-wrap items-center gap-2">
98
99
  <span className="text-sm font-medium text-foreground">Filter by:</span>
99
100
  {FILTER_PRESETS.map((preset) => (
@@ -109,6 +110,7 @@ export default function ProjectList({ folders }: ProjectListProps) {
109
110
  {preset.label}
110
111
  </button>
111
112
  ))}
113
+
112
114
  </div>
113
115
 
114
116
  {/* Keyword Search */}
@@ -160,15 +162,13 @@ export default function ProjectList({ folders }: ProjectListProps) {
160
162
  </button>
161
163
  </div>
162
164
  ))}
163
- {keywords.length > 0 && (
164
- <button
165
- onClick={clearKeywords}
166
- className="px-2 py-1.5 text-xs bg-muted text-muted-foreground hover:bg-muted/80 rounded-md transition-colors"
167
- aria-label="Clear all keywords"
168
- >
169
- Clear all
170
- </button>
171
- )}
165
+ <button
166
+ onClick={clearKeywords}
167
+ className="px-2 py-1.5 text-xs bg-muted text-muted-foreground hover:bg-muted/80 rounded-md transition-colors"
168
+ aria-label="Clear all keywords"
169
+ >
170
+ Clear all
171
+ </button>
172
172
  </div>
173
173
  )}
174
174
  </div>
@@ -192,7 +192,7 @@ export default function ProjectList({ folders }: ProjectListProps) {
192
192
  aria-label="Filter to date"
193
193
  />
194
194
  </div>
195
- {(filterPreset !== "all" || dateRange.from || dateRange.to || keywords.length > 0) && (
195
+ {(filterPreset !== "all" || dateRange.from !== null || dateRange.to !== null || keywords.length > 0) && (
196
196
  <button
197
197
  onClick={clearFilters}
198
198
  className="px-3 py-2 text-sm bg-muted text-muted-foreground hover:bg-muted/80 rounded-md transition-colors"
@@ -5,7 +5,7 @@
5
5
  */
6
6
  "use client";
7
7
 
8
- import React, { useRef } from "react";
8
+ import React, { useCallback, useMemo, useRef, useState } from "react";
9
9
  import { useWindowVirtualizer } from "@tanstack/react-virtual";
10
10
  import type { LogEntry } from "@/lib/log-entries";
11
11
  import { StatsBar } from "@/app/components/log-viewer/stats-bar";
@@ -14,6 +14,7 @@ import { EntryRow } from "@/app/components/log-viewer/entry-row";
14
14
  import EvalResultsPanel from "@/app/components/eval-results-panel";
15
15
  import EnrichmentResultsPanel from "@/app/components/enrichment-results-panel";
16
16
 
17
+
17
18
  // ── Virtualized Entry List ──
18
19
 
19
20
  interface VirtualizedEntryListProps {
@@ -27,20 +28,83 @@ interface VirtualizedEntryListProps {
27
28
  // after render and self-corrects, so these just need to be close enough to
28
29
  // avoid large layout jumps on first paint.
29
30
  function estimateSize(entry: LogEntry): number {
30
- if (entry.type === "queue-operation") return 48;
31
- if (entry.type === "user") return 90;
32
- if (entry.type === "assistant") {
33
- return 80 + entry.message.content.length * 120;
31
+ switch (entry.type) {
32
+ case "queue-operation":
33
+ return 48;
34
+ case "user":
35
+ return 90;
36
+ case "assistant":
37
+ return 80 + entry.message.content.length * 120;
38
+ default:
39
+ return 100;
40
+ }
41
+ }
42
+
43
+ type QueueOperationEntry = Extract<LogEntry, { type: "queue-operation" }>;
44
+
45
+ function getSegmentId(entry: QueueOperationEntry): string {
46
+ return `${entry.uuid}-${entry.timestampMs}`;
47
+ }
48
+
49
+ /**
50
+ * Walks the entries array and returns a Map from each queue-operation segment
51
+ * to the count of non-queue-operation entries in its segment (entries after
52
+ * it until the next queue-operation or end of list).
53
+ */
54
+ function computeSegments(entries: LogEntry[]): Map<string, number> {
55
+ const segments = new Map<string, number>();
56
+ let currentId: string | null = null;
57
+ let count = 0;
58
+
59
+ for (const entry of entries) {
60
+ if (entry.type === "queue-operation") {
61
+ if (currentId !== null) {
62
+ segments.set(currentId, count);
63
+ }
64
+ currentId = getSegmentId(entry);
65
+ count = 0;
66
+ } else if (currentId !== null) {
67
+ count++;
68
+ }
34
69
  }
35
- return 100;
70
+ if (currentId !== null) {
71
+ segments.set(currentId, count);
72
+ }
73
+ return segments;
36
74
  }
37
75
 
38
76
  function VirtualizedEntryList({ entries, projectName, sessionId }: VirtualizedEntryListProps) {
39
77
  const listRef = useRef<HTMLDivElement>(null);
78
+ const [collapsedSessions, setCollapsedSessions] = useState<Set<string>>(new Set());
79
+
80
+ const segments = useMemo(() => computeSegments(entries), [entries]);
81
+
82
+ const visibleEntries = useMemo(() => {
83
+ let currentCollapsed = false;
84
+ return entries.filter((entry) => {
85
+ if (entry.type === "queue-operation") {
86
+ currentCollapsed = collapsedSessions.has(getSegmentId(entry));
87
+ return true; // dividers are always visible
88
+ }
89
+ return !currentCollapsed;
90
+ });
91
+ }, [entries, collapsedSessions]);
92
+
93
+ const handleToggleSegment = useCallback((uuid: string) => {
94
+ setCollapsedSessions((prev) => {
95
+ const next = new Set(prev);
96
+ if (next.has(uuid)) {
97
+ next.delete(uuid);
98
+ } else {
99
+ next.add(uuid);
100
+ }
101
+ return next;
102
+ });
103
+ }, []);
40
104
 
41
105
  const virtualizer = useWindowVirtualizer({
42
- count: entries.length,
43
- estimateSize: (index) => estimateSize(entries[index]),
106
+ count: visibleEntries.length,
107
+ estimateSize: (index) => estimateSize(visibleEntries[index]),
44
108
  overscan: 5,
45
109
  scrollMargin: listRef.current?.offsetTop ?? 0,
46
110
  });
@@ -55,10 +119,10 @@ function VirtualizedEntryList({ entries, projectName, sessionId }: VirtualizedEn
55
119
  }}
56
120
  >
57
121
  {virtualizer.getVirtualItems().map((virtualRow) => {
58
- const entry = entries[virtualRow.index];
122
+ const entry = visibleEntries[virtualRow.index];
59
123
  return (
60
124
  <div
61
- key={entry.uuid || entry.timestamp}
125
+ key={entry.type === "queue-operation" ? getSegmentId(entry) : (entry.uuid || entry.timestamp)}
62
126
  data-index={virtualRow.index}
63
127
  ref={virtualizer.measureElement}
64
128
  style={{
@@ -70,7 +134,12 @@ function VirtualizedEntryList({ entries, projectName, sessionId }: VirtualizedEn
70
134
  }}
71
135
  >
72
136
  {entry.type === "queue-operation" ? (
73
- <QueueDivider entry={entry} />
137
+ <QueueDivider
138
+ entry={entry}
139
+ isCollapsed={collapsedSessions.has(getSegmentId(entry))}
140
+ entryCount={segments.get(getSegmentId(entry)) ?? 0}
141
+ onToggle={() => handleToggleSegment(getSegmentId(entry))}
142
+ />
74
143
  ) : (
75
144
  <EntryRow
76
145
  entry={entry}
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import { useState, useEffect, useTransition } from "react";
5
+ import { RefreshCw } from "lucide-react";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const AUTO_REFRESH_OPTIONS = [
9
+ { label: "Off", value: 0 },
10
+ { label: "5s", value: 5 },
11
+ { label: "10s", value: 10 },
12
+ { label: "30s", value: 30 },
13
+ ] as const;
14
+
15
+ interface RefreshButtonProps {
16
+ className?: string;
17
+ }
18
+
19
+ export function RefreshButton({ className }: RefreshButtonProps) {
20
+ const router = useRouter();
21
+ const [isPending, startTransition] = useTransition();
22
+ const [autoInterval, setAutoInterval] = useState(0);
23
+
24
+ function handleRefresh(): void {
25
+ startTransition(() => router.refresh());
26
+ }
27
+
28
+ useEffect(() => {
29
+ if (autoInterval <= 0) return;
30
+ const id = setInterval(handleRefresh, autoInterval * 1000);
31
+ return () => clearInterval(id);
32
+ }, [autoInterval, router]);
33
+
34
+ const isAutoActive = autoInterval > 0;
35
+
36
+ return (
37
+ <div
38
+ className={cn(
39
+ "inline-flex items-center rounded-lg border border-border bg-muted/50 p-0.5 gap-0.5",
40
+ className,
41
+ )}
42
+ >
43
+ <button
44
+ onClick={handleRefresh}
45
+ title="Refresh"
46
+ className={cn(
47
+ "inline-flex items-center justify-center rounded-md p-1.5 transition-colors",
48
+ isAutoActive
49
+ ? "text-primary hover:bg-primary/10"
50
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
51
+ )}
52
+ >
53
+ <RefreshCw
54
+ className={cn("w-3.5 h-3.5", isPending && "animate-spin")}
55
+ />
56
+ </button>
57
+
58
+ <div className="w-px h-4 bg-border" />
59
+
60
+ <div className="inline-flex items-center gap-0.5" role="group" aria-label="Auto-refresh interval">
61
+ {AUTO_REFRESH_OPTIONS.map((opt) => (
62
+ <button
63
+ key={opt.value}
64
+ onClick={() => setAutoInterval(opt.value)}
65
+ aria-pressed={autoInterval === opt.value}
66
+ className={cn(
67
+ "px-2 py-1 text-[11px] font-medium rounded-md transition-colors",
68
+ autoInterval === opt.value
69
+ ? "bg-background text-foreground shadow-sm"
70
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
71
+ )}
72
+ >
73
+ {opt.label}
74
+ </button>
75
+ ))}
76
+ </div>
77
+ </div>
78
+ );
79
+ }
@@ -18,6 +18,8 @@ import { File, Search } from "lucide-react";
18
18
  import Link from "next/link";
19
19
  import PaginationControls from "./pagination-controls";
20
20
  import DatePickerInput from "./date-picker-input";
21
+ import { CopyButton } from "./copy-button";
22
+
21
23
 
22
24
  interface SessionsListProps {
23
25
  files: SessionFile[];
@@ -56,13 +58,14 @@ export default function SessionsList({ files, projectName }: SessionsListProps)
56
58
  const endIndex = Math.min(startIndex + ITEMS_PER_PAGE, filteredFiles.length);
57
59
  const paginatedFiles = filteredFiles.slice(startIndex, endIndex);
58
60
 
59
- const hasActiveFilters = filterPreset !== "all" || dateRange.from || dateRange.to || sessionIdFilter;
61
+ const hasActiveFilters =
62
+ filterPreset !== "all" || dateRange.from !== null || dateRange.to !== null || sessionIdFilter !== "";
60
63
 
61
64
  return (
62
65
  <div className="space-y-4">
63
66
  {/* Filters */}
64
67
  <div className="bg-card border border-border rounded-lg p-4 flex flex-col gap-4">
65
- {/* Preset Filters */}
68
+ {/* Preset Filters + Refresh */}
66
69
  <div className="flex flex-wrap items-center gap-2">
67
70
  <span className="text-sm font-medium text-foreground">Filter by:</span>
68
71
  {FILTER_PRESETS.map((preset) => (
@@ -78,6 +81,7 @@ export default function SessionsList({ files, projectName }: SessionsListProps)
78
81
  {preset.label}
79
82
  </button>
80
83
  ))}
84
+
81
85
  </div>
82
86
 
83
87
  {/* Custom Date Range */}
@@ -154,18 +158,23 @@ export default function SessionsList({ files, projectName }: SessionsListProps)
154
158
  <File className="w-5 h-5 text-primary" />
155
159
  </td>
156
160
  <td className="px-4 py-3 max-w-md">
157
- {file.sessionId ? (
158
- <Link
159
- href={`/project/${encodeURIComponent(projectName)}/session/${encodeURIComponent(file.sessionId)}`}
160
- className="font-semibold text-foreground hover:text-primary transition-colors break-words break-all inline-block max-w-full"
161
- >
162
- {file.name.replace(/\.jsonl$/, "")}
163
- </Link>
164
- ) : (
165
- <span className="font-semibold text-foreground break-words break-all inline-block max-w-full">
166
- {file.name.replace(/\.jsonl$/, "")}
167
- </span>
168
- )}
161
+ <div className="flex items-center gap-1">
162
+ {file.sessionId ? (
163
+ <>
164
+ <Link
165
+ href={`/project/${encodeURIComponent(projectName)}/session/${encodeURIComponent(file.sessionId)}`}
166
+ className="font-semibold text-foreground hover:text-primary transition-colors break-words break-all inline-block max-w-full"
167
+ >
168
+ {file.name.replace(/\.jsonl$/, "")}
169
+ </Link>
170
+ <CopyButton text={file.sessionId} />
171
+ </>
172
+ ) : (
173
+ <span className="font-semibold text-foreground break-words break-all inline-block max-w-full">
174
+ {file.name.replace(/\.jsonl$/, "")}
175
+ </span>
176
+ )}
177
+ </div>
169
178
  </td>
170
179
  <td className="px-4 py-3 text-sm text-muted-foreground">
171
180
  {file.lastModifiedFormatted || formatDate(file.lastModified)}
@@ -1,9 +1,10 @@
1
1
  /** Session page — parses and displays a single session's JSONL log via the Raw Log Viewer. */
2
2
  import Link from "next/link";
3
- import { ArrowLeft } from "lucide-react";
3
+ import { ArrowLeft, Download } from "lucide-react";
4
4
  import { getCachedSessionLog } from "@/lib/log-entries";
5
5
  import { decodeFolderName } from "@/lib/paths";
6
6
  import LazyLogViewer from "@/app/components/lazy-log-viewer";
7
+ import { CopyButton } from "@/app/components/copy-button";
7
8
 
8
9
  export const dynamic = "force-dynamic";
9
10
 
@@ -25,10 +26,8 @@ export default async function SessionPage({ params }: SessionPageProps) {
25
26
  try {
26
27
  entries = await getCachedSessionLog(decodedName, decodedSessionId);
27
28
  } catch (e) {
28
- error =
29
- e instanceof Error && "code" in e && (e as NodeJS.ErrnoException).code === "ENOENT"
30
- ? "Session log file not found."
31
- : "Failed to read session log.";
29
+ const isNotFound = (e as NodeJS.ErrnoException).code === "ENOENT";
30
+ error = isNotFound ? "Session log file not found." : "Failed to read session log.";
32
31
  }
33
32
 
34
33
  return (
@@ -51,24 +50,36 @@ export default async function SessionPage({ params }: SessionPageProps) {
51
50
  <span className="font-medium">Project:</span>{" "}
52
51
  {decodeFolderName(decodedName)}
53
52
  </p>
54
- <p className="text-muted-foreground break-words break-all">
53
+ <p className="text-muted-foreground break-words break-all inline-flex items-center gap-1">
55
54
  <span className="font-medium">Session:</span> {decodedSessionId}
55
+ <CopyButton text={decodedSessionId} />
56
56
  </p>
57
57
  {entries && (
58
- <p className="text-muted-foreground">
59
- <span className="font-medium">{entries.length}</span> log entries
60
- </p>
58
+ <div className="flex items-center gap-4">
59
+ <p className="text-muted-foreground">
60
+ <span className="font-medium">{entries.length}</span> log entries
61
+ </p>
62
+ <a
63
+ href={`/api/download/${encodeURIComponent(decodedName)}/${encodeURIComponent(decodedSessionId)}`}
64
+ download
65
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground transition-colors"
66
+ >
67
+ <Download className="w-4 h-4" />
68
+ Download JSONL
69
+ </a>
70
+ </div>
61
71
  )}
62
72
  </div>
63
73
  </div>
64
74
 
65
- {error ? (
75
+ {error && (
66
76
  <div className="bg-card text-card-foreground rounded-lg border border-destructive/50 p-6 shadow-sm">
67
77
  <p className="text-destructive text-center py-8">{error}</p>
68
78
  </div>
69
- ) : entries ? (
79
+ )}
80
+ {!error && entries && (
70
81
  <LazyLogViewer entries={entries} projectName={decodedName} sessionId={decodedSessionId} />
71
- ) : null}
82
+ )}
72
83
  </div>
73
84
  </main>
74
85
  );