diff --git a/apps/desktop/src/renderer/styles/workbar/inspector.css b/apps/desktop/src/renderer/styles/workbar/inspector.css index 6e5ad01d2b..d8d378d734 100644 --- a/apps/desktop/src/renderer/styles/workbar/inspector.css +++ b/apps/desktop/src/renderer/styles/workbar/inspector.css @@ -28,6 +28,7 @@ height: 100%; min-height: 0; overflow-y: auto; + container: maka-inspector / inline-size; } /* The live region is a wrapper only; its children carry their own spacing. */ @@ -125,7 +126,7 @@ font: var(--maka-text-label); color: var(--destructive-text); min-width: 0; - overflow-wrap: anywhere; + white-space: nowrap; } .maka-inspector-turn-meta { @@ -141,6 +142,37 @@ color: var(--foreground-secondary); } +/* The default 480px workbar does not leave enough room for the timestamp, + failure and measurement to remain one useful line in every locale. Reflow + from the inspector's own width rather than the window's: the workbar can be + resized independently, and the same panel can also live in the bottom + placement. The identity gets the first row; status and measurement become + a compact comparison row below it. Keeping the failure phrase atomic avoids + the character-by-character column that `overflow-wrap: anywhere` produced. */ +@container maka-inspector (max-width: 32rem) { + .maka-inspector-turn-head { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + row-gap: var(--space-1); + } + + .maka-inspector-turn-label { + grid-column: 1 / -1; + min-width: 0; + white-space: normal; + } + + .maka-inspector-turn-failure { + grid-column: 1; + } + + .maka-inspector-turn-meta { + grid-column: 2; + justify-self: end; + margin-left: 0; + } +} + .maka-inspector-steps { gap: var(--space-1); } diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx index 35f3fb080a..3dce3fcf81 100644 --- a/apps/desktop/stories/session-workbar.stories.tsx +++ b/apps/desktop/stories/session-workbar.stories.tsx @@ -19,6 +19,7 @@ import type { CSSProperties } from 'react'; import type { Decorator, Meta, StoryObj } from '@storybook/react-vite'; +import { expect, waitFor } from 'storybook/test'; import type { ArtifactRecord } from '@maka/core/artifacts'; import type { BrowserState } from '@maka/core/browser'; import type { GitReviewSnapshot } from '@maka/core/git-review'; @@ -433,6 +434,18 @@ const populatedTrace: SessionTrace = { }, }; +const narrowTrace: SessionTrace = { + ...populatedTrace, + turns: populatedTrace.turns.map((turn) => + turn.turnId === 'turn-2' + ? { + ...turn, + failure: { code: 'turn_aborted', message: 'turn was aborted' }, + } + : turn, + ), +}; + const populatedContext: ContextDiagnosticsResult = { status: 'available', providerId: 'zai', @@ -933,6 +946,39 @@ export const Trace: Story = { render: () => , }; +// Real path: resize the right workbar to its 320px floor while a failed turn +// is visible. The timestamp owns the first row; the failure and measurement +// share the second without squeezing the failure into a vertical word. +export const TraceMinimumWidth: Story = { + decorators: [bridge({ trace: narrowTrace, context: populatedContext })], + render: () => , + play: async ({ canvasElement }) => { + await waitFor(() => { + const failure = canvasElement.querySelector( + '[data-maka-contract="session-inspector-turn-failed"]', + ); + const turn = failure?.closest( + '[data-maka-contract="session-inspector-turn"]', + ); + const label = turn?.querySelector('.maka-inspector-turn-label'); + const meta = turn?.querySelector('.maka-inspector-turn-meta'); + expect(failure).not.toBeNull(); + expect(turn).not.toBeNull(); + expect(label).not.toBeNull(); + expect(meta).not.toBeNull(); + if (!failure || !turn || !label || !meta) return; + + const failureRect = failure.getBoundingClientRect(); + const labelRect = label.getBoundingClientRect(); + const metaRect = meta.getBoundingClientRect(); + expect(failureRect.top).toBeGreaterThan(labelRect.top); + expect(Math.abs(failureRect.top - metaRect.top)).toBeLessThanOrEqual(1); + expect(failureRect.height).toBeLessThanOrEqual(metaRect.height + 1); + expect(turn.scrollWidth).toBeLessThanOrEqual(turn.clientWidth); + }); + }, +}; + // Real path: the first bounded page of a longer trace. The continuation control // sits before the ascending timeline because earlier records are inserted at // that edge, not after the newest turn.