inspect-ai 0.3.108__py3-none-any.whl → 0.3.109__py3-none-any.whl

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.
Files changed (141) hide show
  1. inspect_ai/_eval/task/log.py +1 -1
  2. inspect_ai/_eval/task/run.py +1 -1
  3. inspect_ai/_util/dateutil.py +40 -0
  4. inspect_ai/_view/schema.py +11 -0
  5. inspect_ai/_view/www/CLAUDE.md +1 -1
  6. inspect_ai/_view/www/dist/assets/index.css +2068 -1796
  7. inspect_ai/_view/www/dist/assets/index.js +7951 -3643
  8. inspect_ai/_view/www/package.json +3 -2
  9. inspect_ai/_view/www/src/@types/log.d.ts +5 -5
  10. inspect_ai/_view/www/src/app/App.css +71 -4
  11. inspect_ai/_view/www/src/app/App.tsx +7 -0
  12. inspect_ai/_view/www/src/app/appearance/icons.ts +18 -2
  13. inspect_ai/_view/www/src/app/content/RenderedContent.tsx +7 -9
  14. inspect_ai/_view/www/src/app/log-list/LogItem.ts +18 -0
  15. inspect_ai/_view/www/src/app/log-list/LogListFooter.module.css +55 -0
  16. inspect_ai/_view/www/src/app/log-list/LogListFooter.tsx +67 -0
  17. inspect_ai/_view/www/src/app/log-list/LogPager.module.css +29 -0
  18. inspect_ai/_view/www/src/app/log-list/LogPager.tsx +134 -0
  19. inspect_ai/_view/www/src/app/log-list/LogsFilterInput.module.css +5 -0
  20. inspect_ai/_view/www/src/app/log-list/LogsFilterInput.tsx +31 -0
  21. inspect_ai/_view/www/src/app/log-list/LogsPanel.module.css +12 -0
  22. inspect_ai/_view/www/src/app/log-list/LogsPanel.tsx +178 -0
  23. inspect_ai/_view/www/src/app/log-list/grid/LogListGrid.module.css +115 -0
  24. inspect_ai/_view/www/src/app/log-list/grid/LogListGrid.tsx +304 -0
  25. inspect_ai/_view/www/src/app/log-list/grid/columns/CompletedDate.module.css +6 -0
  26. inspect_ai/_view/www/src/app/log-list/grid/columns/CompletedDate.tsx +64 -0
  27. inspect_ai/_view/www/src/app/log-list/grid/columns/EmptyCell.module.css +3 -0
  28. inspect_ai/_view/www/src/app/log-list/grid/columns/EmptyCell.tsx +7 -0
  29. inspect_ai/_view/www/src/app/log-list/grid/columns/FileName.module.css +20 -0
  30. inspect_ai/_view/www/src/app/log-list/grid/columns/FileName.tsx +52 -0
  31. inspect_ai/_view/www/src/app/log-list/grid/columns/Icon.module.css +11 -0
  32. inspect_ai/_view/www/src/app/log-list/grid/columns/Icon.tsx +35 -0
  33. inspect_ai/_view/www/src/app/log-list/grid/columns/Model.module.css +6 -0
  34. inspect_ai/_view/www/src/app/log-list/grid/columns/Model.tsx +34 -0
  35. inspect_ai/_view/www/src/app/log-list/grid/columns/Score.module.css +6 -0
  36. inspect_ai/_view/www/src/app/log-list/grid/columns/Score.tsx +61 -0
  37. inspect_ai/_view/www/src/app/log-list/grid/columns/Status.module.css +15 -0
  38. inspect_ai/_view/www/src/app/log-list/grid/columns/Status.tsx +95 -0
  39. inspect_ai/_view/www/src/app/log-list/grid/columns/Task.module.css +20 -0
  40. inspect_ai/_view/www/src/app/log-list/grid/columns/Task.tsx +50 -0
  41. inspect_ai/_view/www/src/app/log-list/grid/columns/columns.ts +27 -0
  42. inspect_ai/_view/www/src/app/log-view/LogView.tsx +2 -5
  43. inspect_ai/_view/www/src/app/log-view/LogViewContainer.tsx +4 -30
  44. inspect_ai/_view/www/src/app/log-view/LogViewLayout.tsx +5 -30
  45. inspect_ai/_view/www/src/app/log-view/tabs/TaskTab.tsx +4 -7
  46. inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/PrimaryBar.module.css +2 -0
  47. inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/PrimaryBar.tsx +3 -31
  48. inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ResultsPanel.tsx +7 -57
  49. inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ScoreGrid.tsx +2 -2
  50. inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/SecondaryBar.tsx +7 -1
  51. inspect_ai/_view/www/src/app/log-view/{navbar/Navbar.tsx → title-view/TitleView.tsx} +3 -6
  52. inspect_ai/_view/www/src/app/navbar/Navbar.module.css +57 -0
  53. inspect_ai/_view/www/src/app/navbar/Navbar.tsx +117 -0
  54. inspect_ai/_view/www/src/app/navbar/useBreadcrumbTruncation.ts +128 -0
  55. inspect_ai/_view/www/src/app/plan/DatasetDetailView.tsx +3 -3
  56. inspect_ai/_view/www/src/app/plan/DetailStep.tsx +6 -6
  57. inspect_ai/_view/www/src/app/plan/PlanDetailView.module.css +1 -0
  58. inspect_ai/_view/www/src/app/plan/ScorerDetailView.tsx +1 -1
  59. inspect_ai/_view/www/src/app/routing/AppRouter.tsx +28 -4
  60. inspect_ai/_view/www/src/app/routing/RouteDispatcher.tsx +28 -0
  61. inspect_ai/_view/www/src/app/routing/sampleNavigation.ts +76 -7
  62. inspect_ai/_view/www/src/app/routing/url.ts +193 -20
  63. inspect_ai/_view/www/src/app/samples/SampleDisplay.tsx +3 -17
  64. inspect_ai/_view/www/src/app/samples/descriptor/score/ScoreDescriptor.tsx +1 -1
  65. inspect_ai/_view/www/src/app/samples/transcript/SubtaskEventView.tsx +2 -2
  66. inspect_ai/_view/www/src/app/samples/transcript/TranscriptPanel.tsx +2 -2
  67. inspect_ai/_view/www/src/app/samples/transcript/outline/tree-visitors.ts +5 -0
  68. inspect_ai/_view/www/src/app/samples/transcript/transform/treeify.ts +26 -10
  69. inspect_ai/_view/www/src/app/types.ts +21 -1
  70. inspect_ai/_view/www/src/client/api/api-http.ts +2 -1
  71. inspect_ai/_view/www/src/client/api/api-shared.ts +0 -32
  72. inspect_ai/_view/www/src/client/api/client-api.ts +1 -1
  73. inspect_ai/_view/www/src/client/remote/remoteLogFile.ts +38 -6
  74. inspect_ai/_view/www/src/components/TextInput.module.css +45 -0
  75. inspect_ai/_view/www/src/components/TextInput.tsx +52 -0
  76. inspect_ai/_view/www/src/constants.ts +18 -0
  77. inspect_ai/_view/www/src/img/inspect-16.svg +10 -0
  78. inspect_ai/_view/www/src/img/inspect-back.svg +5 -0
  79. inspect_ai/_view/www/src/img/inspect-file.svg +26 -0
  80. inspect_ai/_view/www/src/img/inspect-forward.svg +7 -0
  81. inspect_ai/_view/www/src/img/inspect-home.svg +18 -0
  82. inspect_ai/_view/www/src/scoring/metrics.ts +75 -0
  83. inspect_ai/_view/www/src/scoring/scores.ts +19 -0
  84. inspect_ai/_view/www/src/scoring/types.ts +11 -0
  85. inspect_ai/_view/www/src/state/appSlice.ts +27 -7
  86. inspect_ai/_view/www/src/state/clientEvents.ts +73 -0
  87. inspect_ai/_view/www/src/state/clientEventsService.ts +105 -0
  88. inspect_ai/_view/www/src/state/hooks.ts +118 -1
  89. inspect_ai/_view/www/src/state/log.ts +19 -0
  90. inspect_ai/_view/www/src/state/logPolling.ts +3 -1
  91. inspect_ai/_view/www/src/state/logSlice.ts +9 -0
  92. inspect_ai/_view/www/src/state/logsSlice.ts +157 -15
  93. inspect_ai/_view/www/src/state/samplePolling.ts +4 -2
  94. inspect_ai/_view/www/src/tests/utils/path.test.ts +3 -3
  95. inspect_ai/_view/www/src/utils/evallog.ts +31 -0
  96. inspect_ai/_view/www/src/utils/path.ts +28 -0
  97. inspect_ai/_view/www/src/utils/uri.ts +49 -0
  98. inspect_ai/_view/www/yarn.lock +54 -17
  99. inspect_ai/analysis/beta/_dataframe/util.py +106 -10
  100. inspect_ai/log/_recorders/buffer/database.py +55 -16
  101. inspect_ai/model/_model.py +1 -1
  102. inspect_ai/model/_providers/providers.py +2 -2
  103. inspect_ai/model/_providers/vertex.py +3 -0
  104. inspect_ai/tool/_mcp/_mcp.py +6 -1
  105. inspect_ai/tool/_mcp/sampling.py +8 -1
  106. inspect_ai/tool/_tools/_bash_session.py +3 -6
  107. inspect_ai/tool/_tools/_web_browser/_web_browser.py +3 -8
  108. inspect_ai/util/_anyio.py +12 -3
  109. {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/METADATA +2 -2
  110. {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/RECORD +124 -94
  111. inspect_ai/_util/datetime.py +0 -10
  112. inspect_ai/_view/www/src/app/content/MetaDataView.module.css +0 -35
  113. inspect_ai/_view/www/src/app/content/MetaDataView.tsx +0 -101
  114. inspect_ai/_view/www/src/app/log-view/utils.ts +0 -34
  115. inspect_ai/_view/www/src/app/sidebar/EvalStatus.module.css +0 -15
  116. inspect_ai/_view/www/src/app/sidebar/EvalStatus.tsx +0 -72
  117. inspect_ai/_view/www/src/app/sidebar/LogDirectoryTitleView.module.css +0 -16
  118. inspect_ai/_view/www/src/app/sidebar/LogDirectoryTitleView.tsx +0 -70
  119. inspect_ai/_view/www/src/app/sidebar/Sidebar.module.css +0 -77
  120. inspect_ai/_view/www/src/app/sidebar/Sidebar.tsx +0 -119
  121. inspect_ai/_view/www/src/app/sidebar/SidebarLogEntry.module.css +0 -29
  122. inspect_ai/_view/www/src/app/sidebar/SidebarLogEntry.tsx +0 -96
  123. inspect_ai/_view/www/src/app/sidebar/SidebarScoreView.module.css +0 -23
  124. inspect_ai/_view/www/src/app/sidebar/SidebarScoreView.tsx +0 -44
  125. inspect_ai/_view/www/src/app/sidebar/SidebarScoresView.module.css +0 -35
  126. inspect_ai/_view/www/src/app/sidebar/SidebarScoresView.tsx +0 -63
  127. inspect_ai/_view/www/src/state/logsPolling.ts +0 -118
  128. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ModelRolesView.module.css +0 -0
  129. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ModelRolesView.tsx +0 -0
  130. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ResultsPanel.module.css +0 -0
  131. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/RunningStatusPanel.module.css +0 -0
  132. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/RunningStatusPanel.tsx +0 -0
  133. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ScoreGrid.module.css +0 -0
  134. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/SecondaryBar.module.css +0 -0
  135. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/StatusPanel.module.css +0 -0
  136. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/StatusPanel.tsx +0 -0
  137. /inspect_ai/_view/www/src/app/log-view/{navbar/Navbar.module.css → title-view/TitleView.module.css} +0 -0
  138. {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/WHEEL +0 -0
  139. {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/entry_points.txt +0 -0
  140. {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/licenses/LICENSE +0 -0
  141. {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,28 @@
1
+ import { FC } from "react";
2
+ import { LogsPanel } from "../log-list/LogsPanel";
3
+ import { LogViewContainer } from "../log-view/LogViewContainer";
4
+ import { useLogRouteParams } from "./url";
5
+
6
+ /**
7
+ * RouteDispatcher component that determines whether to show LogsView or LogViewContainer
8
+ * based on the logPath parameter. If logPath ends with .eval or .json, it shows the
9
+ * individual log view. Otherwise, it shows the logs directory view.
10
+ */
11
+ export const RouteDispatcher: FC = () => {
12
+ const { logPath } = useLogRouteParams();
13
+
14
+ // If no logPath is provided, show the logs directory view
15
+ if (!logPath) {
16
+ return <LogsPanel />;
17
+ }
18
+
19
+ // Check if the path ends with .eval or .json (indicating it's a log file)
20
+ const isLogFile = logPath.endsWith(".eval") || logPath.endsWith(".json");
21
+
22
+ // Route to the appropriate component
23
+ if (isLogFile) {
24
+ return <LogViewContainer />;
25
+ } else {
26
+ return <LogsPanel />;
27
+ }
28
+ };
@@ -1,9 +1,82 @@
1
1
  import { useCallback } from "react";
2
- import { useNavigate, useParams, useSearchParams } from "react-router-dom";
2
+ import { useNavigate, useSearchParams } from "react-router-dom";
3
3
  import { useFilteredSamples } from "../../state/hooks";
4
4
  import { useStore } from "../../state/store";
5
5
  import { directoryRelativeUrl } from "../../utils/uri";
6
- import { logUrlRaw, sampleUrl } from "./url";
6
+ import { logUrl, logUrlRaw, sampleUrl, useLogRouteParams } from "./url";
7
+
8
+ export const useLogNavigation = () => {
9
+ const navigate = useNavigate();
10
+ const { logPath } = useLogRouteParams();
11
+ const logs = useStore((state) => state.logs.logs);
12
+ const loadedLog = useStore((state) => state.log.loadedLog);
13
+
14
+ const selectTab = useCallback(
15
+ (tabId: string) => {
16
+ // Only update URL if we have a loaded log
17
+ if (loadedLog && logPath) {
18
+ // We already have the logPath from params, just navigate to the tab
19
+ const url = logUrlRaw(logPath, tabId);
20
+ navigate(url);
21
+ } else if (loadedLog) {
22
+ // Fallback to constructing the path if needed
23
+ const url = logUrl(loadedLog, logs.log_dir, tabId);
24
+ navigate(url);
25
+ }
26
+ },
27
+ [loadedLog, logPath, logs.log_dir, navigate],
28
+ );
29
+
30
+ return {
31
+ selectTab,
32
+ };
33
+ };
34
+
35
+ export const useSampleUrl = () => {
36
+ const { logPath, tabId, sampleTabId } = useLogRouteParams();
37
+
38
+ const logDirectory = useStore((state) => state.logs.logs.log_dir);
39
+
40
+ const selectedLogFile = useStore((state) => state.logs.selectedLogFile);
41
+
42
+ // Helper function to resolve the log path for URLs
43
+ const resolveLogPath = useCallback(() => {
44
+ // If we have a logPath from URL params, use that
45
+ if (logPath) {
46
+ return logPath;
47
+ }
48
+
49
+ if (selectedLogFile) {
50
+ return directoryRelativeUrl(selectedLogFile, logDirectory);
51
+ }
52
+
53
+ return undefined;
54
+ }, [logPath, selectedLogFile, logDirectory]);
55
+
56
+ // Get a sample URL for a specific sample
57
+ const getSampleUrl = useCallback(
58
+ (
59
+ sampleId: string | number,
60
+ epoch: number,
61
+ specificSampleTabId?: string,
62
+ ) => {
63
+ const resolvedPath = resolveLogPath();
64
+ if (resolvedPath) {
65
+ const currentSampleTabId = specificSampleTabId || sampleTabId;
66
+ const url = sampleUrl(
67
+ resolvedPath,
68
+ sampleId,
69
+ epoch,
70
+ currentSampleTabId,
71
+ );
72
+ return url;
73
+ }
74
+ return undefined;
75
+ },
76
+ [resolveLogPath, tabId, sampleTabId],
77
+ );
78
+ return getSampleUrl;
79
+ };
7
80
 
8
81
  /**
9
82
  * Hook that provides sample navigation utilities with proper URL handling
@@ -16,11 +89,7 @@ export const useSampleNavigation = () => {
16
89
  const logDirectory = useStore((state) => state.logs.logs.log_dir);
17
90
 
18
91
  // The log
19
- const { logPath, tabId, sampleTabId } = useParams<{
20
- logPath?: string;
21
- tabId?: string;
22
- sampleTabId?: string;
23
- }>();
92
+ const { logPath, tabId, sampleTabId } = useLogRouteParams();
24
93
 
25
94
  // Get the store access values directly in the hook
26
95
  const selectedLogFile = useStore((state) => state.logs.selectedLogFile);
@@ -1,12 +1,185 @@
1
1
  import { useMemo } from "react";
2
2
  import { useParams } from "react-router-dom";
3
- import { kSampleMessagesTabId, kSampleTranscriptTabId } from "../../constants";
3
+ import {
4
+ kSampleMessagesTabId,
5
+ kSampleTabIds,
6
+ kSampleTranscriptTabId,
7
+ kWorkspaceTabs,
8
+ } from "../../constants";
4
9
  import { useStore } from "../../state/store";
5
- import { directoryRelativeUrl } from "../../utils/uri";
10
+ import { directoryRelativeUrl, encodePathParts } from "../../utils/uri";
6
11
 
7
- export const kLogRouteUrlPattern = "/logs/:logPath/:tabId?/:sampleTabId?";
12
+ /**
13
+ * Decodes a URL parameter that may be URL-encoded.
14
+ * Safely handles already decoded strings.
15
+ */
16
+ export const decodeUrlParam = (
17
+ param: string | undefined,
18
+ ): string | undefined => {
19
+ if (!param) return param;
20
+ try {
21
+ return decodeURIComponent(param);
22
+ } catch {
23
+ // If decoding fails, return the original string
24
+ return param;
25
+ }
26
+ };
27
+
28
+ /**
29
+ * Hook that provides URL parameters with automatic decoding.
30
+ * Use this instead of useParams when you need the actual unencoded values.
31
+ */
32
+ export const useDecodedParams = <
33
+ T extends Record<string, string | undefined>,
34
+ >() => {
35
+ const params = useParams<T>();
36
+
37
+ const decodedParams = useMemo(() => {
38
+ const decoded = {} as T;
39
+ Object.entries(params).forEach(([key, value]) => {
40
+ (decoded as any)[key] = decodeUrlParam(value as string);
41
+ });
42
+ return decoded;
43
+ }, [params]);
44
+
45
+ return decodedParams;
46
+ };
47
+
48
+ /**
49
+ * Hook that parses log route parameters from the splat route.
50
+ * Handles nested paths properly by parsing the full path after /logs/
51
+ */
52
+ export const useLogRouteParams = () => {
53
+ const params = useParams<{
54
+ "*": string;
55
+ sampleId?: string;
56
+ epoch?: string;
57
+ sampleTabId?: string;
58
+ }>();
59
+
60
+ return useMemo(() => {
61
+ const splatPath = params["*"] || "";
62
+
63
+ // Check for full sample route pattern in splat path (when route params aren't populated)
64
+ // Pattern: logPath/samples/sample/sampleId/epoch/sampleTabId (with optional trailing slash)
65
+ const fullSampleUrlMatch = splatPath.match(
66
+ /^(.+?)\/samples\/sample\/([^/]+)(?:\/([^/]+)(?:\/(.+?))?)?\/?\s*$/,
67
+ );
68
+ if (fullSampleUrlMatch) {
69
+ const [, logPath, sampleId, epoch, sampleTabId] = fullSampleUrlMatch;
70
+ return {
71
+ logPath: decodeUrlParam(logPath),
72
+ tabId: undefined,
73
+ sampleTabId: decodeUrlParam(sampleTabId),
74
+ sampleId: decodeUrlParam(sampleId),
75
+ epoch: epoch ? decodeUrlParam(epoch) : undefined,
76
+ };
77
+ }
78
+
79
+ // Check for sample URLs that might not match the formal route pattern
80
+ // (this is the single sample case, where is there is now sampleid/epoch, just sampletabid)
81
+ // Pattern: /logs/*/samples/sampleId/epoch or /logs/*/samples/sampleId or /logs/*/samples/sampleTabId
82
+ const sampleUrlMatch = splatPath.match(
83
+ /^(.+?)\/samples(?:\/([^/]+)(?:\/([^/]+))?)?$/,
84
+ );
85
+ if (sampleUrlMatch) {
86
+ const [, logPath, firstSegment, secondSegment] = sampleUrlMatch;
87
+
88
+ if (firstSegment) {
89
+ // Define known sample tab IDs
90
+ const validSampleTabIds = new Set(kSampleTabIds);
91
+
92
+ if (validSampleTabIds.has(firstSegment) && !secondSegment) {
93
+ // This is /logs/*/samples/sampleTabId
94
+ return {
95
+ logPath: decodeUrlParam(logPath),
96
+ tabId: "samples",
97
+ sampleTabId: decodeUrlParam(firstSegment),
98
+ sampleId: undefined,
99
+ epoch: undefined,
100
+ };
101
+ } else {
102
+ // This is a sample URL with sampleId (and possibly epoch)
103
+ return {
104
+ logPath: decodeUrlParam(logPath),
105
+ tabId: undefined,
106
+ sampleTabId: undefined,
107
+ sampleId: decodeUrlParam(firstSegment),
108
+ epoch: secondSegment ? decodeUrlParam(secondSegment) : undefined,
109
+ };
110
+ }
111
+ } else {
112
+ // This is just /logs/*/samples (samples listing)
113
+ return {
114
+ logPath: decodeUrlParam(logPath),
115
+ tabId: "samples",
116
+ sampleTabId: undefined,
117
+ sampleId: undefined,
118
+ epoch: undefined,
119
+ };
120
+ }
121
+ }
122
+
123
+ // Regular log route pattern: /logs/path/to/file.eval/tabId?
124
+ // Split the path and check if the last segment might be a tabId
125
+ const pathSegments = splatPath.split("/").filter(Boolean);
126
+
127
+ if (pathSegments.length === 0) {
128
+ return {
129
+ logPath: undefined,
130
+ tabId: undefined,
131
+ sampleTabId: undefined,
132
+ sampleId: undefined,
133
+ epoch: undefined,
134
+ };
135
+ }
136
+
137
+ // Define valid tab IDs for log view
138
+ const validTabIds = new Set(kWorkspaceTabs);
139
+
140
+ // Look for the first valid tab ID from right to left
141
+ let tabIdIndex = -1;
142
+ let foundTabId: string | undefined = undefined;
143
+
144
+ for (let i = pathSegments.length - 1; i >= 0; i--) {
145
+ const segment = pathSegments[i];
146
+ const decodedSegment = decodeUrlParam(segment) || segment;
147
+
148
+ if (validTabIds.has(decodedSegment)) {
149
+ tabIdIndex = i;
150
+ foundTabId = decodedSegment;
151
+ break;
152
+ }
153
+ }
154
+
155
+ if (foundTabId && tabIdIndex > 0) {
156
+ // Found a valid tab ID, split the path there
157
+ const logPath = pathSegments.slice(0, tabIdIndex).join("/");
158
+
159
+ return {
160
+ logPath: decodeUrlParam(logPath),
161
+ tabId: foundTabId,
162
+ sampleTabId: undefined,
163
+ sampleId: undefined,
164
+ epoch: undefined,
165
+ };
166
+ } else {
167
+ // No valid tab ID found, the entire path is the logPath
168
+ return {
169
+ logPath: decodeUrlParam(splatPath),
170
+ tabId: undefined,
171
+ sampleTabId: undefined,
172
+ sampleId: undefined,
173
+ epoch: undefined,
174
+ };
175
+ }
176
+ }, [params]);
177
+ };
178
+
179
+ export const kLogsRoutUrlPattern = "/logs";
180
+ export const kLogRouteUrlPattern = "/logs/*";
8
181
  export const kSampleRouteUrlPattern =
9
- "/logs/:logPath/samples/sample/:sampleId/:epoch?/:sampleTabId?";
182
+ "/logs/*/samples/sample/:sampleId/:epoch?/:sampleTabId?";
10
183
 
11
184
  export const baseUrl = (
12
185
  logPath: string,
@@ -26,10 +199,17 @@ export const sampleUrl = (
26
199
  sampleEpoch?: string | number,
27
200
  sampleTabId?: string,
28
201
  ) => {
202
+ // Ensure logPath is decoded before encoding for URL construction
203
+ const decodedLogPath = decodeUrlParam(logPath) || logPath;
204
+
29
205
  if (sampleId !== undefined && sampleEpoch !== undefined) {
30
- return `/logs/${encodeURIComponent(logPath)}/samples/sample/${encodeURIComponent(sampleId)}/${sampleEpoch}/${sampleTabId || ""}`;
206
+ return encodePathParts(
207
+ `/logs/${decodedLogPath}/samples/sample/${sampleId}/${sampleEpoch}/${sampleTabId || ""}`,
208
+ );
31
209
  } else {
32
- return `/logs/${encodeURIComponent(logPath)}/samples/${sampleTabId || ""}`;
210
+ return encodePathParts(
211
+ `/logs/${decodedLogPath}/samples/${sampleTabId || ""}`,
212
+ );
33
213
  }
34
214
  };
35
215
 
@@ -58,12 +238,7 @@ export const useSampleMessageUrl = (
58
238
  logPath: urlLogPath,
59
239
  sampleId: urlSampleId,
60
240
  epoch: urlEpoch,
61
- } = useParams<{
62
- logPath?: string;
63
- tabId?: string;
64
- sampleId?: string;
65
- epoch?: string;
66
- }>();
241
+ } = useLogRouteParams();
67
242
 
68
243
  const log_file = useStore((state) => state.logs.selectedLogFile);
69
244
  const log_dir = useStore((state) => state.logs.logs.log_dir);
@@ -95,12 +270,7 @@ export const useSampleEventUrl = (
95
270
  logPath: urlLogPath,
96
271
  sampleId: urlSampleId,
97
272
  epoch: urlEpoch,
98
- } = useParams<{
99
- logPath?: string;
100
- tabId?: string;
101
- sampleId?: string;
102
- epoch?: string;
103
- }>();
273
+ } = useLogRouteParams();
104
274
 
105
275
  const log_file = useStore((state) => state.logs.selectedLogFile);
106
276
  const log_dir = useStore((state) => state.logs.logs.log_dir);
@@ -149,10 +319,13 @@ export const makeLogPath = (log_file: string, log_dir?: string) => {
149
319
  };
150
320
 
151
321
  export const logUrlRaw = (log_segment: string, tabId?: string) => {
322
+ // Ensure log_segment is decoded before encoding for URL construction
323
+ const decodedLogSegment = decodeUrlParam(log_segment) || log_segment;
324
+
152
325
  if (tabId) {
153
- return `/logs/${encodeURIComponent(log_segment)}/${tabId}`;
326
+ return encodePathParts(`/logs/${decodedLogSegment}/${tabId}`);
154
327
  } else {
155
- return `/logs/${encodeURIComponent(log_segment)}`;
328
+ return encodePathParts(`/logs/${decodedLogSegment}`);
156
329
  }
157
330
  };
158
331
 
@@ -38,7 +38,7 @@ import { estimateSize } from "../../utils/json";
38
38
  import { printHeadingHtml, printHtml } from "../../utils/print";
39
39
  import { RecordTree } from "../content/RecordTree";
40
40
  import { useSampleDetailNavigation } from "../routing/sampleNavigation";
41
- import { sampleUrl } from "../routing/url";
41
+ import { sampleUrl, useLogRouteParams } from "../routing/url";
42
42
  import { ModelTokenTable } from "../usage/ModelTokenTable";
43
43
  import { ChatViewVirtualList } from "./chat/ChatViewVirtualList";
44
44
  import { messagesFromEvents } from "./chat/messages";
@@ -113,15 +113,9 @@ export const SampleDisplay: FC<SampleDisplayProps> = ({ id, scrollRef }) => {
113
113
  // Get all URL parameters at component level
114
114
  const {
115
115
  logPath: urlLogPath,
116
- tabId: urlTabId,
117
116
  sampleId: urlSampleId,
118
117
  epoch: urlEpoch,
119
- } = useParams<{
120
- logPath?: string;
121
- tabId?: string;
122
- sampleId?: string;
123
- epoch?: string;
124
- }>();
118
+ } = useLogRouteParams();
125
119
 
126
120
  // Tab selection
127
121
  const onSelectedTab = useCallback(
@@ -136,15 +130,7 @@ export const SampleDisplay: FC<SampleDisplayProps> = ({ id, scrollRef }) => {
136
130
  navigate(url);
137
131
  }
138
132
  },
139
- [
140
- sampleTabId,
141
- urlLogPath,
142
- urlTabId,
143
- urlSampleId,
144
- urlEpoch,
145
- navigate,
146
- setSelectedTab,
147
- ],
133
+ [sampleTabId, urlLogPath, urlSampleId, urlEpoch, navigate, setSelectedTab],
148
134
  );
149
135
 
150
136
  const sampleMetadatas = metadataViewsForSample(
@@ -46,7 +46,7 @@ const scoreCategorizers: ScoreCategorizer[] = [
46
46
  return val === 1 || val === 0;
47
47
  })
48
48
  ) {
49
- return booleanScoreDescriptor();
49
+ return numericScoreDescriptor(values);
50
50
  }
51
51
  },
52
52
  },
@@ -2,7 +2,7 @@ import clsx from "clsx";
2
2
  import { FC, ReactNode } from "react";
3
3
  import { Input2, Input5, Result2, SubtaskEvent } from "../../../@types/log";
4
4
  import { ApplicationIcons } from "../../appearance/icons";
5
- import { MetaDataView } from "../../content/MetaDataView";
5
+ import { MetaDataGrid } from "../../content/MetaDataGrid";
6
6
  import { EventPanel } from "./event/EventPanel";
7
7
  import { formatTiming, formatTitle } from "./event/utils";
8
8
  import styles from "./SubtaskEventView.module.css";
@@ -106,7 +106,7 @@ const Rendered: FC<RenderedProps> = ({ values }) => {
106
106
  if (Object.keys(values).length === 0) {
107
107
  return <None />;
108
108
  } else {
109
- return <MetaDataView entries={values as Record<string, unknown>} />;
109
+ return <MetaDataGrid entries={values as Record<string, unknown>} />;
110
110
  }
111
111
  } else {
112
112
  return values;
@@ -1,10 +1,10 @@
1
1
  import clsx from "clsx";
2
2
  import { FC, memo, RefObject } from "react";
3
- import { useParams } from "react-router-dom";
4
3
  import { Events } from "../../../@types/log";
5
4
  import { StickyScroll } from "../../../components/StickyScroll";
6
5
  import { useCollapsedState } from "../../../state/hooks";
7
6
  import { ApplicationIcons } from "../../appearance/icons";
7
+ import { useLogRouteParams } from "../../routing/url";
8
8
  import { TranscriptOutline } from "./outline/TranscriptOutline";
9
9
  import styles from "./TranscriptPanel.module.css";
10
10
  import { TranscriptVirtualList } from "./TranscriptVirtualList";
@@ -29,7 +29,7 @@ export const TranscriptPanel: FC<TranscriptPanelProps> = memo((props) => {
29
29
  events,
30
30
  running === true,
31
31
  );
32
- const { logPath } = useParams<{ logPath: string }>();
32
+ const { logPath } = useLogRouteParams();
33
33
 
34
34
  const [collapsed, setCollapsed] = useCollapsedState(
35
35
  `transcript-panel-${logPath || "na"}`,
@@ -141,6 +141,11 @@ export const collapseTurns = (eventNodes: EventNode[]): EventNode[] => {
141
141
 
142
142
  for (const node of eventNodes) {
143
143
  if (node.event.event === "span_begin" && node.event.type === kTurnType) {
144
+ // Check depth to ensure we are collecting turns at the same level
145
+ if (collecting.length > 0 && collecting[0].depth !== node.depth) {
146
+ collect();
147
+ }
148
+
144
149
  collecting.push(node);
145
150
  } else {
146
151
  collect();
@@ -349,15 +349,31 @@ const transformers = () => {
349
349
  },
350
350
  {
351
351
  name: "unwrap_handoff",
352
- matches: (node) =>
353
- node.event.event === SPAN_BEGIN &&
354
- node.event["type"] === TYPE_HANDOFF &&
355
- node.children.length === 2 &&
356
- node.children[0].event.event === TOOL &&
357
- node.children[1].event.event === STORE &&
358
- node.children[0].children.length === 2 &&
359
- node.children[0].children[0].event.event === SPAN_BEGIN &&
360
- node.children[0].children[0].event.type === TYPE_AGENT,
352
+ matches: (node) => {
353
+ const isHandoffNode =
354
+ node.event.event === SPAN_BEGIN &&
355
+ node.event["type"] === TYPE_HANDOFF;
356
+
357
+ if (!isHandoffNode) {
358
+ return false;
359
+ }
360
+
361
+ if (node.children.length === 1) {
362
+ return (
363
+ node.children[0].event.event === TOOL &&
364
+ !!node.children[0].event.agent
365
+ );
366
+ } else {
367
+ return (
368
+ node.children.length === 2 &&
369
+ node.children[0].event.event === TOOL &&
370
+ node.children[1].event.event === STORE &&
371
+ node.children[0].children.length === 2 &&
372
+ node.children[0].children[0].event.event === SPAN_BEGIN &&
373
+ node.children[0].children[0].event.type === TYPE_AGENT
374
+ );
375
+ }
376
+ },
361
377
  process: (node) => skipThisNode(node),
362
378
  },
363
379
  {
@@ -423,7 +439,7 @@ const skipFirstChildNode = (node: EventNode): EventNode => {
423
439
  const skipThisNode = (node: EventNode): EventNode => {
424
440
  const newNode = { ...node.children[0] };
425
441
  newNode.depth = node.depth;
426
- newNode.children = reduceDepth(newNode.children[0].children, 2);
442
+ newNode.children = reduceDepth(newNode.children, 2);
427
443
  return newNode;
428
444
  };
429
445
 
@@ -1,3 +1,8 @@
1
+ import {
2
+ ColumnFiltersState,
3
+ ColumnResizeMode,
4
+ SortingState,
5
+ } from "@tanstack/react-table";
1
6
  import { StateSnapshot } from "react-virtuoso";
2
7
  import {
3
8
  ApprovalEvent,
@@ -22,6 +27,7 @@ import {
22
27
  EvalLogHeader,
23
28
  EvalSummary,
24
29
  EventData,
30
+ LogFile,
25
31
  LogFiles,
26
32
  PendingSamples,
27
33
  SampleSummary,
@@ -30,7 +36,6 @@ import { ScorerInfo } from "../state/scoring";
30
36
 
31
37
  export interface AppState {
32
38
  status: AppStatus;
33
- offcanvas: boolean;
34
39
  showFind: boolean;
35
40
  tabs: {
36
41
  workspace: string;
@@ -51,6 +56,8 @@ export interface AppState {
51
56
  sample_epoch?: string;
52
57
  };
53
58
  rehydrated?: boolean;
59
+ pagination: Record<string, { page: number; pageSize: number }>;
60
+ singleFileMode?: boolean;
54
61
  }
55
62
 
56
63
  export interface LogsState {
@@ -59,6 +66,19 @@ export interface LogsState {
59
66
  headersLoading: boolean;
60
67
  selectedLogIndex: number;
61
68
  selectedLogFile?: string;
69
+ listing: LogsListing;
70
+ loadingFiles: Set<string>;
71
+ pendingRequests: Map<string, Promise<EvalLogHeader | null>>;
72
+ }
73
+
74
+ export interface LogsListing {
75
+ sorting?: SortingState;
76
+ filtering?: ColumnFiltersState;
77
+ globalFilter?: string;
78
+ columnResizeMode?: ColumnResizeMode;
79
+ columnSizes?: Record<string, number>;
80
+ filteredCount?: number;
81
+ watchedLogs?: LogFile[];
62
82
  }
63
83
 
64
84
  export interface LogState {
@@ -1,7 +1,8 @@
1
1
  import { EvalLog } from "../../@types/log";
2
2
  import { asyncJsonParse } from "../../utils/json-worker";
3
+ import { encodePathParts } from "../../utils/uri";
3
4
  import { fetchRange, fetchSize } from "../remote/remoteZipFile";
4
- import { download_file, encodePathParts } from "./api-shared";
5
+ import { download_file } from "./api-shared";
5
6
  import {
6
7
  Capabilities,
7
8
  LogContents,
@@ -13,35 +13,3 @@ export async function download_file(
13
13
  link.click();
14
14
  document.body.removeChild(link);
15
15
  }
16
-
17
- /**
18
- * Encodes the path segments of a URL or relative path to ensure special characters
19
- * (like `+`, spaces, etc.) are properly encoded without affecting legal characters like `/`.
20
- *
21
- * This function will encode file names and path portions of both absolute URLs and
22
- * relative paths. It ensures that components of a full URL, such as the protocol and
23
- * query parameters, remain intact, while only encoding the path.
24
- */
25
- export function encodePathParts(url: string): string {
26
- if (!url) return url; // Handle empty strings
27
-
28
- try {
29
- // Parse a full Uri
30
- const fullUrl = new URL(url);
31
- fullUrl.pathname = fullUrl.pathname
32
- .split("/")
33
- .map((segment) =>
34
- segment ? encodeURIComponent(decodeURIComponent(segment)) : "",
35
- )
36
- .join("/");
37
- return fullUrl.toString();
38
- } catch {
39
- // This is a relative path that isn't parseable as Uri
40
- return url
41
- .split("/")
42
- .map((segment) =>
43
- segment ? encodeURIComponent(decodeURIComponent(segment)) : "",
44
- )
45
- .join("/");
46
- }
47
- }
@@ -1,11 +1,11 @@
1
1
  import { EvalLog, EvalSample } from "../../@types/log";
2
+ import { encodePathParts } from "../../utils/uri";
2
3
  import {
3
4
  openRemoteLogFile,
4
5
  RemoteLogFile,
5
6
  SampleNotFoundError,
6
7
  } from "../remote/remoteLogFile";
7
8
  import { FileSizeLimitError } from "../remote/remoteZipFile";
8
- import { encodePathParts } from "./api-shared";
9
9
  import {
10
10
  ClientAPI,
11
11
  EvalSummary,