diff --git a/packages/session-ui/component-tests/session-tool-projection.spec.ts b/packages/session-ui/component-tests/session-tool-projection.spec.ts index 7b9e77bd52c..d0f0ad9f174 100644 --- a/packages/session-ui/component-tests/session-tool-projection.spec.ts +++ b/packages/session-ui/component-tests/session-tool-projection.spec.ts @@ -179,6 +179,35 @@ story("opens an execution's code and output from its View code action", async ({ await expect(dialog.getByRole("button", { name: "Copy output" })).toBeVisible() }) +story("wraps execute details code, pads it evenly, and starts line numbers near the block edge", async ({ mount }) => { + const timeline = await mount("current-session-terminal-work--execute-code") + const tool = timeline.locator('[data-timeline-part-id="tool_execute_long_call"]') + await tool.hover() + await tool.getByRole("button", { name: "View code" }).click() + const source = timeline.page().getByRole("dialog").locator('[data-slot="execute-details-source"]').first() + await expect(source.locator("[data-column-number]").first()).toBeVisible() + + const layout = await source.evaluate((element) => { + const root = element.querySelector("diffs-container")!.shadowRoot! + const code = root.querySelector("[data-code]")! + const number = root.querySelector("[data-line-number-content]")! + const lines = [...root.querySelectorAll("[data-line]")].map((line) => line.getBoundingClientRect()) + const box = element.getBoundingClientRect() + return { + paddingTop: lines[0].top - box.top, + paddingBottom: box.bottom - lines.at(-1)!.bottom, + // A horizontally scrolling block reserves a scrollbar track under a single line. + overflow: code.scrollWidth - code.clientWidth, + numberStart: number.getBoundingClientRect().left - box.left, + } + }) + + expect(layout.overflow).toBe(0) + expect(layout.numberStart).toBe(12) + expect(layout.paddingTop).toBe(4) + expect(layout.paddingBottom).toBe(4) +}) + story("reveals the View code action while hovering anywhere over the execution", async ({ mount }) => { const timeline = await mount("current-session-terminal-work--execute-code") const tool = timeline.locator('[data-timeline-part-id="tool_execute_long_call"]') diff --git a/packages/session-ui/src/components/message-part.css b/packages/session-ui/src/components/message-part.css index 02f1fcf79ea..3810108c459 100644 --- a/packages/session-ui/src/components/message-part.css +++ b/packages/session-ui/src/components/message-part.css @@ -964,6 +964,15 @@ overflow: auto; border-radius: 8px; box-shadow: 0 0 0 0.5px var(--v2-border-border-base); + + /* The file wrapper sets Pierre's defaults inline, so override them on the shadow host below that wrapper. */ + diffs-container { + --diffs-min-number-column-width: var(--execute-details-number-width, 1ch); + --opencode-diffs-number-inset: 12px; + --diffs-gap-block: 4px; + /* Wrapped blocks never scroll sideways; Pierre would otherwise subtract a scrollbar track from the bottom gap. */ + --diffs-scrollbar-gutter-override: 0px; + } } [data-slot="execute-details-error"] { diff --git a/packages/session-ui/src/pierre/index.ts b/packages/session-ui/src/pierre/index.ts index 315890d0672..a1607ed36a6 100644 --- a/packages/session-ui/src/pierre/index.ts +++ b/packages/session-ui/src/pierre/index.ts @@ -234,6 +234,11 @@ const unsafeCSS = ` ); } +/* Pierre insets line numbers by 2ch to leave room for diff change bars; compact file viewers can tighten it. */ +[data-file]:not([data-disable-line-numbers]) :is([data-column-number], [data-gutter-buffer]) { + padding-left: var(--opencode-diffs-number-inset, 2ch); +} + [data-diff-header], [data-diff], [data-file] { diff --git a/packages/session-ui/src/tools/tool-renderer.tsx b/packages/session-ui/src/tools/tool-renderer.tsx index 86ffc73a6cb..6381d13ab38 100644 --- a/packages/session-ui/src/tools/tool-renderer.tsx +++ b/packages/session-ui/src/tools/tool-renderer.tsx @@ -1976,7 +1976,11 @@ const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unkno function ExecuteDetails(props: { code: string; output: string; error?: string; calls: number; status?: string }) { const i18n = useI18n() const fileComponent = useFileComponent() - const lines = (text: string) => i18n.plural("ui.tool.execute.lines", text.split("\n").length) + const lineCount = (text: string) => text.split("\n").length + const lines = (text: string) => i18n.plural("ui.tool.execute.lines", lineCount(text)) + // Code and output share one number gutter, sized to the longer block, so their content starts on the same column. + const numberDigits = () => + String(Math.max(lineCount(props.code), props.output && !props.error ? lineCount(props.output) : 0)).length return ( @@ -1994,7 +1998,7 @@ function ExecuteDetails(props: { code: string; output: string; error?: string; c -
+
)}