diff --git a/packages/ui/client/src/components/CaseMetricsPanel.vue b/packages/ui/client/src/components/CaseMetricsPanel.vue
index edee06f..bed388d 100644
--- a/packages/ui/client/src/components/CaseMetricsPanel.vue
+++ b/packages/ui/client/src/components/CaseMetricsPanel.vue
@@ -521,10 +521,11 @@ const formatOf = (name: string, value: number) => formatColumnValue(value, metri
every case against its recorded metrics
-
diff --git a/packages/ui/client/src/components/ProgressPanel.vue b/packages/ui/client/src/components/ProgressPanel.vue
index c80f079..c2d1f89 100644
--- a/packages/ui/client/src/components/ProgressPanel.vue
+++ b/packages/ui/client/src/components/ProgressPanel.vue
@@ -5,6 +5,7 @@ import type { LiveConnection } from '../composables/useRunLive'
import { useIntervalFn, useTimestamp } from '@vueuse/core'
import { computed } from 'vue'
+import InfoButton from './InfoButton.vue'
import ProgressBar from './ProgressBar.vue'
import ProgressChart from './ProgressChart.vue'
import StateBadge from './StateBadge.vue'
@@ -40,6 +41,8 @@ const basisText = computed(() => ({
'ended': 'run ended',
'unknown': 'no case finished yet: no estimate',
})[progress.value.basis])
+/** How the remaining time is estimated: printed under the numbers on desktop, behind an info button on phones. */
+const etaBasis = computed(() => `ETA basis: ${basisText.value}${live.value && progress.value.lowerBound ? '; auto attempts were seen, so more rounds may follow (lower bound)' : ''}. Rule: elapsed ÷ settled cases × estimated cases − elapsed, and at least as long as the longest running case has run.`)
const connectionText = computed(() => ({
closed: 'not connected',
connecting: 'connecting…',
@@ -122,8 +125,9 @@ const connectionText = computed(() => ({
-
+
{{ live ? 'Estimated remaining' : 'Total duration' }}
+
- {{ `ETA basis: ${basisText}${live && progress.lowerBound ? '; auto attempts were seen, so more rounds may follow (lower bound)' : ''}. Rule: elapsed ÷ settled cases × estimated cases − elapsed, and at least as long as the longest running case has run.` }}
+
+ {{ etaBasis }}
diff --git a/packages/ui/client/src/components/RunDetails.test.ts b/packages/ui/client/src/components/RunDetails.test.ts
index 2099173..f53f484 100644
--- a/packages/ui/client/src/components/RunDetails.test.ts
+++ b/packages/ui/client/src/components/RunDetails.test.ts
@@ -8,12 +8,12 @@ describe('runDetails', () => {
* @example
* expect(screen.getByText('Report root')).toBeVisible()
*/
- it('keeps the raw run identifiers collapsed until "Details" is opened', async () => {
+ it('keeps the raw run identifiers behind the "Run details" info button', async () => {
const screen = await render(RunDetails, { props: { reportPath: 'default-workspace/osworld/exp/attempt-1/run-1', reportRoot: '/data/.vieval/reports', runId: 'run-1' } })
expect(screen.getByTestId('run-details').query()).toBeNull()
- await screen.getByRole('button', { name: 'Details' }).click()
+ await screen.getByRole('button', { name: 'Run details' }).click()
await expect.element(screen.getByText('Report root')).toBeVisible()
await expect.element(screen.getByText('/data/.vieval/reports')).toBeVisible()
@@ -28,7 +28,7 @@ describe('runDetails', () => {
it('leaves out identifiers a run without a report does not have', async () => {
const screen = await render(RunDetails, { props: { reportPath: null, reportRoot: null, runId: 'run-2' } })
- await screen.getByRole('button', { name: 'Details' }).click()
+ await screen.getByRole('button', { name: 'Run details' }).click()
await expect.element(screen.getByText('run-2')).toBeVisible()
expect(screen.getByText('Report path').query()).toBeNull()
diff --git a/packages/ui/client/src/components/RunDetails.vue b/packages/ui/client/src/components/RunDetails.vue
index 9e46195..dfbabbe 100644
--- a/packages/ui/client/src/components/RunDetails.vue
+++ b/packages/ui/client/src/components/RunDetails.vue
@@ -1,11 +1,12 @@
-
-
- Details
-
-
-
-
-
- {{ row.label }}
-
-
- {{ row.value }}
-
-
-
-
-
-
-
-
+
+
+
+
+ {{ row.label }}
+
+
+ {{ row.value }}
+
+
+
+
+
+
+
diff --git a/packages/ui/client/src/components/ScoresPanel.vue b/packages/ui/client/src/components/ScoresPanel.vue
index c4edf53..14b1b0d 100644
--- a/packages/ui/client/src/components/ScoresPanel.vue
+++ b/packages/ui/client/src/components/ScoresPanel.vue
@@ -48,6 +48,17 @@ watch(() => props.refreshKey, () => {
})
const matrixKeys = computed(() => [...new Set(props.snapshot.tasks.flatMap(task => Object.keys({ ...task.matrix?.run, ...task.matrix?.eval })))])
+/**
+ * Normalizes a task's display name for the matrix rows table, whose axis columns already show the matrix values that
+ * `vieval run` appends in brackets (`formatTaskDisplayName` in `packages/vieval/src/cli/run.ts`).
+ *
+ * Before:
+ * - "en [tts=official, asr=gemini-3.8-flash]"
+ *
+ * After:
+ * - "en"
+ */
+const taskName = (displayName: string) => displayName.replace(/ \[[^\]]*\]$/, '')
const scoreDefinitions = computed(() => scores.data.value?.definitions ?? props.snapshot.scoreDefinitions)
/** Every score of the run in the order the summary lists them; the series of the bar charts and the table columns. */
const scoreNames = computed(() => (scores.data.value?.overall.scores ?? []).map(summary => summary.name))
@@ -176,7 +187,7 @@ const numeric = computed(() => (metrics.data.value?.numeric ?? []).map(metric =>
@@ -355,9 +366,10 @@ const numeric = computed(() => (metrics.data.value?.numeric ?? []).map(metric =>
-
- Inference: {{ metrics.data.value.inference.requests }} requests · {{ metrics.data.value.inference.responses }} responses · {{ metrics.data.value.inference.failed }} failed
- {{ key }}={{ value }}
+
+
+ Inference: {{ metrics.data.value.inference.requests }} requests · {{ metrics.data.value.inference.responses }} responses · {{ metrics.data.value.inference.failed }} failed
+ {{ key }}={{ value }}
diff --git a/packages/ui/client/src/pages/ExperimentPage.vue b/packages/ui/client/src/pages/ExperimentPage.vue
index 68e4c0b..8b86efe 100644
--- a/packages/ui/client/src/pages/ExperimentPage.vue
+++ b/packages/ui/client/src/pages/ExperimentPage.vue
@@ -78,14 +78,16 @@ const expectedCounts = (run: (typeof runs.value)[number]) => ({ ...run.counts, t
const isPartial = (run: (typeof runs.value)[number]) => run.state !== 'running' && run.caseTotal != null && run.counts.total < run.caseTotal
/**
- * Grid of a run row. Wide screens: one line with select, attempt, run, duration, cases, headline score, menu (the
- * header uses the same columns). Narrow screens: select, attempt, score, and menu on the first line, the run and its
- * duration below the attempt, then the cases. The score column fits a typical headline (`100.0% Correct and natural`);
+ * Grid of a run row. Wide lists: one line with select, attempt, run, duration, cases, headline score, menu (the
+ * header uses the same columns). Narrow lists: select, attempt, and menu on the first line, the run and its
+ * duration below the attempt, then the cases, and the score below them, so the attempt keeps the width of the row.
+ * "Wide" is the width of the list card (a container query), not of the window: with the sidebar open, a window past
+ * the `md` breakpoint can still leave the card narrower than the wide columns need (62rem). The score column fits a typical headline (`100.0% Correct and natural`);
* a longer label truncates inside it instead of spilling over the case bar.
*/
const rowGrid = [
'grid items-center gap-x-3 gap-y-1.5 grid-cols-[1rem_minmax(0,1fr)_auto_1.75rem]',
- 'md:(gap-x-4 grid-cols-[1rem_minmax(14rem,1.6fr)_minmax(9rem,1fr)_5rem_minmax(12rem,2fr)_minmax(8rem,12rem)_1.75rem])',
+ '@[62rem]:(gap-x-4 grid-cols-[1rem_minmax(14rem,1.6fr)_minmax(11rem,1fr)_5rem_minmax(12rem,2fr)_minmax(8rem,12rem)_1.75rem])',
]
const selected = shallowRef([])
function duration(run: { endedAt?: string, startedAt?: string }): number | undefined {
@@ -127,11 +129,14 @@ watch([() => props.attempt, () => attempts.value.length], async ([attempt]) => {
Compare {{ selected.length }} runs
-
-
-
+
@@ -161,20 +166,20 @@ watch([() => props.attempt, () => attempts.value.length], async ([attempt]) => {
- another run of this attempt
-
+ another run of this attempt
+ {{ shortRunHash(run.runKey) }}{{ shortRunHash(run.runKey) }}
- {{ formatDuration(duration(run)) }}
-
+ {{ formatDuration(duration(run)) }}
+ {{ run.counts.total }} / {{ run.caseTotal }}
-
+
-
+
diff --git a/packages/ui/client/src/pages/ProjectPage.vue b/packages/ui/client/src/pages/ProjectPage.vue
index c2122af..c552e94 100644
--- a/packages/ui/client/src/pages/ProjectPage.vue
+++ b/packages/ui/client/src/pages/ProjectPage.vue
@@ -57,13 +57,14 @@ function toRow(experiment: ExperimentEntry) {
}
const rows = computed(() => (project.value?.experiments ?? []).map(toRow))
/**
- * Grid of a row. Wide screens: one line with name, latest run, trend, cases, headline score, attempts (the header
- * uses the same columns). Narrow screens: the name on top, the latest run with the score below it, then the cases;
- * the trend and the attempt count are left out (both are on the experiment page).
+ * Grid of a row. Wide lists: one line with name, latest run, trend, cases, headline score, attempts (the header
+ * uses the same columns). Narrow lists: the name on top, the latest run below it, then the cases with the score
+ * below them; the trend and the attempt count are left out (both are on the experiment page). "Wide" is the width of
+ * the list card (a container query, 65rem), not of the window, so an open sidebar cannot push the columns out of it.
*/
const rowGrid = [
- 'grid items-center gap-x-4 gap-y-1.5 grid-cols-[minmax(0,1fr)_auto]',
- 'md:grid-cols-[minmax(14rem,2fr)_minmax(12rem,1.2fr)_4.5rem_minmax(16rem,1.8fr)_8rem_4rem]',
+ 'grid items-center gap-x-4 gap-y-1.5 grid-cols-[minmax(0,1fr)]',
+ '@[65rem]:grid-cols-[minmax(14rem,2fr)_minmax(12rem,1.2fr)_4.5rem_minmax(16rem,1.8fr)_8rem_4rem]',
]
@@ -76,22 +77,25 @@ const rowGrid = [
-
-