From b530418425586cdaf2f3a7a074b9d3ebb98ab0eb Mon Sep 17 00:00:00 2001 From: Hona <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 01:46:50 +0000 Subject: [PATCH] fix(session-ui): share timeline tool headers --- .../component-tests/tool-header.spec.ts | 126 ++++++++++++ .../session-ui/src/components/basic-tool.css | 42 ---- .../session-ui/src/components/basic-tool.tsx | 67 +------ .../src/components/message-part.css | 125 ------------ .../session-ui/src/components/tool-header.css | 87 +++++++++ .../session-ui/src/components/tool-header.tsx | 73 +++++++ .../src/message/message-content.tsx | 33 +--- packages/session-ui/src/styles/index.css | 1 + .../src/timeline/tool-headers.stories.tsx | 84 ++++++++ .../session-ui/src/tools/tool-renderer.tsx | 179 ++++++------------ 10 files changed, 447 insertions(+), 370 deletions(-) create mode 100644 packages/session-ui/component-tests/tool-header.spec.ts create mode 100644 packages/session-ui/src/components/tool-header.css create mode 100644 packages/session-ui/src/components/tool-header.tsx create mode 100644 packages/session-ui/src/timeline/tool-headers.stories.tsx diff --git a/packages/session-ui/component-tests/tool-header.spec.ts b/packages/session-ui/component-tests/tool-header.spec.ts new file mode 100644 index 000000000000..938c9c51b50f --- /dev/null +++ b/packages/session-ui/component-tests/tool-header.spec.ts @@ -0,0 +1,126 @@ +import { expect, story } from "../../storybook/playwright/story" + +const id = "current-session-tool-headers--shared-headers" + +story("shares compact title and detail metrics across tool families", async ({ mount }, info) => { + const root = await mount(id) + await root.getByRole("button", { name: /^Used 7 / }).click() + const headers = root.locator('[data-component="context-tool-group-list"] [data-component="tool-header"]') + await expect(headers).toHaveCount(7) + const titles = headers.locator('[data-slot="basic-tool-tool-title"]') + await expect(headers.locator('[data-component="text-shimmer"][aria-label="Write"]')).toBeVisible() + await expect(headers.locator('[data-component="text-shimmer"][aria-label="Edit"]')).toBeVisible() + for (const title of await titles.all()) { + await expect(title).toBeVisible() + await expect(title).toHaveCSS("font-family", /^Inter,/) + await expect(title).toHaveCSS("font-size", "13px") + await expect(title).toHaveCSS("line-height", "16px") + await expect(title).toHaveCSS("font-weight", "530") + await expect(title.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "false") + } + const details = headers.locator( + '[data-slot="basic-tool-tool-subtitle"], [data-slot="basic-tool-tool-arg"], [data-slot="tool-header-directory"]', + ) + expect(await details.count()).toBeGreaterThan(7) + for (const detail of await details.all()) { + await expect(detail).toHaveCSS("font-size", "13px") + await expect(detail).toHaveCSS("line-height", "16px") + await expect(detail).toHaveCSS("font-weight", "440") + } + await expect(headers.locator('[data-slot="basic-tool-tool-arg"]')).toHaveText([ + "offset=12", + "limit=40", + "pattern=header", + "include=*.tsx", + ]) + await root.locator('[data-component="session-timeline"]').screenshot({ path: info.outputPath("tool-headers.png") }) +}) + +story("keeps pending file titles active without showing unfinished paths", async ({ mount }) => { + const root = await mount(id, { args: { phase: "streaming", pathKnown: false } }) + await root + .locator( + '[data-component="collapsed-tool-group"] > [data-component="collapsible"] > [data-slot="collapsible-trigger"]', + ) + .click() + for (const action of [undefined, "Provide paths", "Run tools"]) { + if (action) await root.getByRole("button", { name: action, exact: true }).click() + for (const name of ["edit", "write"]) { + const header = root.locator(`[data-timeline-part-id="tool_header_${name}"] [data-component="tool-header"]`) + await expect(header).toBeVisible() + await expect(header.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true") + await expect(header.locator('[data-slot="basic-tool-tool-title"]')).toHaveCSS("line-height", "16px") + await expect( + header.locator('[data-slot="basic-tool-tool-subtitle"], [data-slot="tool-header-directory"]'), + ).toHaveCount(0) + } + } + await root.getByRole("button", { name: "Complete tools", exact: true }).click() + const group = root.getByRole("button", { name: /^Used 7 / }) + if ((await group.getAttribute("aria-expanded")) === "false") await group.click() + for (const name of ["edit", "write"]) { + const header = root.locator(`[data-timeline-part-id="tool_header_${name}"] [data-component="tool-header"]`) + await expect(header.locator('[data-slot="basic-tool-tool-subtitle"]')).toHaveText(`${name}.ts`) + await expect(header.locator('[data-slot="tool-header-directory"]')).toContainText("src/components") + await expect(header.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "false") + } +}) + +for (const theme of ["light", "dark"]) { + for (const width of [390, 1000]) { + story(`truncates long file headers at ${width}px in ${theme}`, async ({ mount, page }, info) => { + await page.setViewportSize({ width, height: 850 }) + const root = await mount(id, { args: { longPath: true }, globals: { theme } }) + await root.getByRole("button", { name: /^Used 7 / }).click() + for (const name of ["edit", "write"]) { + const header = root.locator(`[data-timeline-part-id="tool_header_${name}"] [data-component="tool-header"]`) + const filename = header.locator('[data-slot="basic-tool-tool-subtitle"]') + const directory = header.locator('[data-slot="tool-header-directory"] > span') + await expect(filename).toContainText(`${name}.ts`) + for (const text of [filename, directory]) { + await expect(text).toHaveCSS("text-overflow", "ellipsis") + await expect(text).toHaveCSS("white-space", "nowrap") + await expect(text).toHaveCSS("line-height", "16px") + } + expect(await filename.evaluate((node) => node.scrollWidth > node.clientWidth)).toBe(true) + const bounds = await header.boundingBox() + expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width) + } + expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true) + await root + .locator('[data-component="session-timeline"]') + .screenshot({ path: info.outputPath("long-headers.png") }) + }) + } +} + +story("preserves keyboard disclosures and the webfetch link", async ({ mount }) => { + const root = await mount(id) + await root.getByRole("button", { name: /^Used 7 / }).click() + for (const name of ["shell", "execute", "edit", "write"]) { + const row = root.locator(`[data-timeline-part-id="tool_header_${name}"]`) + const trigger = row.locator('[data-slot="collapsible-trigger"]').first() + const content = row.locator('[data-slot="collapsible-content"]').first() + await expect(trigger).toHaveAttribute("aria-expanded", "false") + await trigger.focus() + await trigger.press("Enter") + await expect(trigger).toHaveAttribute("aria-expanded", "true") + await expect(content).toBeVisible() + await expect(trigger).toBeFocused() + await trigger.press("Space") + await expect(trigger).toHaveAttribute("aria-expanded", "false") + await expect(content).toBeHidden() + await expect(trigger).toBeFocused() + } + const link = root.getByRole("link", { name: "https://example.com/docs" }) + await expect(link).toBeVisible() + await expect(link).toHaveAttribute("href", "https://example.com/docs") + await expect(link).toHaveAttribute("target", "_blank") + await expect(link).toHaveAttribute("rel", /noopener/) + await expect(link).toHaveCSS("font-size", "13px") + await expect(link).toHaveCSS("font-weight", "440") + await expect(link).toHaveCSS("line-height", "16px") + await expect(link).toHaveCSS("letter-spacing", "-0.04px") + await link.focus() + await expect(link).toBeFocused() +}) diff --git a/packages/session-ui/src/components/basic-tool.css b/packages/session-ui/src/components/basic-tool.css index 0fce2044a361..db43a5cd60d2 100644 --- a/packages/session-ui/src/components/basic-tool.css +++ b/packages/session-ui/src/components/basic-tool.css @@ -83,22 +83,11 @@ [data-slot="basic-tool-tool-title"] { flex-shrink: 0; - font-family: var(--font-family-sans); - font-size: 14px; - font-style: normal; - font-weight: var(--font-weight-medium); - line-height: var(--line-height-large); - letter-spacing: var(--letter-spacing-normal); color: var(--v2-text-text-base); &.capitalize { text-transform: capitalize; } - - &.agent-title { - color: var(--v2-text-text-base); - font-weight: var(--font-weight-medium); - } } [data-slot="basic-tool-tool-subtitle"] { @@ -107,13 +96,6 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-family: var(--font-family-sans); - font-variant-numeric: tabular-nums; - font-size: 14px; - font-style: normal; - font-weight: var(--font-weight-regular); - line-height: var(--line-height-large); - letter-spacing: var(--letter-spacing-normal); color: var(--v2-text-text-muted); &.clickable:not(.webfetch-link) { @@ -153,13 +135,6 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-family: var(--font-family-sans); - font-variant-numeric: tabular-nums; - font-size: 14px; - font-style: normal; - font-weight: var(--font-weight-regular); - line-height: var(--line-height-large); - letter-spacing: var(--letter-spacing-normal); color: var(--v2-text-text-muted); } @@ -177,23 +152,6 @@ [data-slot="basic-tool-tool-info-main"] { gap: 6px; } - - [data-slot="basic-tool-tool-title"] { - font-family: var(--v2-font-family-sans); - font-size: 13px; - font-weight: 530; - /* Keep compact text on the shared metric; solid 13px line boxes clip Inter descenders. */ - line-height: var(--line-height-compact); - letter-spacing: -0.04px; - } - - [data-slot="basic-tool-tool-subtitle"] { - font-family: var(--v2-font-family-sans); - font-size: 13px; - font-weight: 440; - line-height: var(--line-height-compact); - letter-spacing: -0.04px; - } } [data-component="task-tool-card"] { diff --git a/packages/session-ui/src/components/basic-tool.tsx b/packages/session-ui/src/components/basic-tool.tsx index 0026ecdc535a..1a096a772d26 100644 --- a/packages/session-ui/src/components/basic-tool.tsx +++ b/packages/session-ui/src/components/basic-tool.tsx @@ -1,7 +1,6 @@ import { createEffect, createMemo, - For, Match, on, onCleanup, @@ -16,17 +15,9 @@ import { useI18n } from "@opencode-ai/ui/context/i18n" import { createStore } from "solid-js/store" import { Collapsible } from "@opencode-ai/ui/collapsible" import type { IconProps } from "@opencode-ai/ui/icon" -import { TextShimmer } from "@opencode-ai/ui/text-shimmer" +import { ToolHeader, type ToolHeaderProps } from "./tool-header" -export type TriggerTitle = { - title: string - titleClass?: string - subtitle?: string - subtitleClass?: string - args?: string[] - argsClass?: string - action?: JSX.Element -} +export type TriggerTitle = Omit const isTriggerTitle = (val: unknown): val is TriggerTitle => { if (typeof val !== "object" || val === null) return false @@ -216,54 +207,12 @@ export function BasicTool(props: BasicToolProps) { {(title) => ( -
-
- - - - - - { - if (props.onSubtitleClick) { - e.stopPropagation() - props.onSubtitleClick() - } - }} - > - {title().subtitle} - - - - - {(arg) => ( - - {arg} - - )} - - - -
- - {title().action} - -
+ )}
{triggerContent() as JSX.Element} diff --git a/packages/session-ui/src/components/message-part.css b/packages/session-ui/src/components/message-part.css index 03472c2e2e58..bca60ce84d9b 100644 --- a/packages/session-ui/src/components/message-part.css +++ b/packages/session-ui/src/components/message-part.css @@ -446,102 +446,6 @@ --tool-content-gap: 6px; } -[data-component="edit-trigger"], -[data-component="write-trigger"] { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - width: 100%; - - [data-slot="message-part-title-area"] { - flex-grow: 1; - display: flex; - align-items: center; - gap: 8px; - min-width: 0; - } - - [data-slot="message-part-title"] { - flex: 1 1 auto; - display: flex; - align-items: center; - gap: 8px; - min-width: 0; - font-family: var(--font-family-sans); - font-size: 14px; - font-style: normal; - font-weight: var(--font-weight-medium); - line-height: var(--line-height-large); - letter-spacing: var(--letter-spacing-normal); - color: var(--v2-text-text-muted); - } - - [data-slot="message-part-title-spinner"] { - margin-left: 4px; - width: 16px; - height: 16px; - display: inline-flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - color: var(--v2-text-text-muted); - - [data-component="spinner"] { - width: 16px; - height: 16px; - } - } - - [data-slot="message-part-title-text"] { - flex-shrink: 0; - text-transform: capitalize; - color: var(--v2-text-text-base); - } - - [data-slot="message-part-title-filename"] { - /* No text-transform - preserve original filename casing */ - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-weight: var(--font-weight-regular); - direction: ltr; - unicode-bidi: isolate; - } - - [data-slot="message-part-path"] { - display: flex; - flex-grow: 1; - min-width: 0; - font-weight: var(--font-weight-regular); - direction: ltr; - unicode-bidi: isolate; - } - - [data-slot="message-part-directory"] { - color: var(--v2-text-text-muted); - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - direction: rtl; - text-align: left; - } - - [data-slot="message-part-filename"] { - color: var(--v2-text-text-base); - flex-shrink: 0; - } - - [data-slot="message-part-actions"] { - display: flex; - gap: 16px; - align-items: center; - justify-content: flex-end; - flex-shrink: 0; - } -} - [data-component="edit-content"] { border-radius: inherit; border-top: 0.5px solid var(--v2-border-border-muted); @@ -678,24 +582,6 @@ gap: 0px; cursor: default; - [data-slot="context-tool-group-title"] { - display: flex; - align-items: center; - gap: 8px; - flex-shrink: 1; - min-width: 0; - } - - [data-slot="context-tool-group-prefix"] { - flex-shrink: 0; - color: var(--v2-text-text-muted); - font-family: var(--v2-font-family-sans); - font-size: 13px; - font-weight: 440; - line-height: var(--line-height-compact); - letter-spacing: -0.04px; - } - [data-slot="basic-tool-tool-title"] { overflow: hidden; text-overflow: ellipsis; @@ -740,17 +626,6 @@ gap: 6px; } - [data-slot="basic-tool-tool-title"], - [data-slot="basic-tool-tool-subtitle"], - [data-slot="basic-tool-tool-arg"], - [data-slot="context-tool-group-matches"] { - font-family: var(--v2-font-family-sans); - font-size: 13px; - font-weight: 440; - line-height: var(--line-height-compact); - letter-spacing: -0.04px; - } - [data-slot="basic-tool-tool-subtitle"] { color: var(--v2-text-text-faint); } diff --git a/packages/session-ui/src/components/tool-header.css b/packages/session-ui/src/components/tool-header.css new file mode 100644 index 000000000000..b7b208ba6974 --- /dev/null +++ b/packages/session-ui/src/components/tool-header.css @@ -0,0 +1,87 @@ +[data-component="tool-header"] { + font-family: var(--v2-font-family-sans); + font-size: 13px; + font-style: normal; + font-weight: 440; + /* Truncated text still needs room for Inter descenders. */ + line-height: var(--line-height-compact); + letter-spacing: -0.04px; + color: var(--v2-text-text-muted); + + &[data-slot="basic-tool-tool-info-structured"] { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + max-width: 100%; + } + + [data-slot="basic-tool-tool-info-main"] { + display: flex; + align-items: baseline; + gap: 6px; + min-width: 0; + overflow: hidden; + } + + [data-slot="basic-tool-tool-title"] { + flex-shrink: 0; + font: inherit; + font-weight: 530; + color: var(--v2-text-text-base); + + &.capitalize { + text-transform: capitalize; + } + } + + [data-slot="tool-header-affix"] { + flex-shrink: 0; + } + + [data-slot="basic-tool-tool-subtitle"], + [data-slot="basic-tool-tool-arg"] { + flex-shrink: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font: inherit; + font-variant-numeric: tabular-nums; + + &[dir] { + unicode-bidi: isolate; + } + + &.clickable { + cursor: pointer; + text-decoration: underline; + } + } + + [data-slot="tool-header-directory"] { + display: flex; + min-width: 0; + unicode-bidi: isolate; + + > span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + direction: rtl; + text-align: left; + } + } + + [data-slot="basic-tool-tool-action"] { + display: inline-flex; + align-items: center; + gap: 6px; + flex-shrink: 0; + } + + .webfetch-link { + font: inherit; + letter-spacing: inherit; + } +} diff --git a/packages/session-ui/src/components/tool-header.tsx b/packages/session-ui/src/components/tool-header.tsx new file mode 100644 index 000000000000..d15a058b2f72 --- /dev/null +++ b/packages/session-ui/src/components/tool-header.tsx @@ -0,0 +1,73 @@ +import { children, For, Show, type JSX } from "solid-js" +import { TextShimmer } from "@opencode-ai/ui/text-shimmer" + +export type ToolHeaderProps = { + title: string + active?: boolean + titleClass?: string + prefix?: string + suffix?: string + subtitle?: JSX.Element + subtitleClass?: string + subtitleDir?: "ltr" | "rtl" + directory?: string + args?: string[] + argsClass?: string + action?: JSX.Element + onSubtitleClick?: () => void +} + +/** Shared presentation for tool rows; callers own values, status, and disclosure. */ +export function ToolHeader(props: ToolHeaderProps) { + const subtitle = children(() => props.subtitle) + const action = children(() => props.action) + return ( +
+
+ + {props.prefix} + + + + + + + + {props.suffix} + + + { + if (!props.onSubtitleClick) return + event.stopPropagation() + props.onSubtitleClick() + }} + > + {subtitle()} + + + + {(arg) => ( + + {arg} + + )} + +
+ + + {props.directory} + + + + {action()} + +
+ ) +} diff --git a/packages/session-ui/src/message/message-content.tsx b/packages/session-ui/src/message/message-content.tsx index 88bdc5f658eb..0b9fe653afe0 100644 --- a/packages/session-ui/src/message/message-content.tsx +++ b/packages/session-ui/src/message/message-content.tsx @@ -14,7 +14,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Icon } from "@opencode-ai/ui/icon" import { Button } from "@opencode-ai/ui/button" import { TextReveal } from "@opencode-ai/ui/text-reveal" -import { TextShimmer } from "@opencode-ai/ui/text-shimmer" import { BasicTool } from "../components/basic-tool" import { reasoningHeading } from "../timeline/projection" import { Card } from "@opencode-ai/ui/card" @@ -534,30 +533,14 @@ export function AssistantReasoningContent(props: { props.onOpenChange?.(value) props.onContentRendered?.() }} - trigger={ -
-
- - - - - {(value) => {value()}} - - } - > - - - - -
-
- } + trigger={{ + title: i18n.t(props.streaming ? "ui.sessionTurn.status.thinking" : "ui.message.thought"), + subtitle: ( + + + + ), + }} > diff --git a/packages/session-ui/src/styles/index.css b/packages/session-ui/src/styles/index.css index 24f78feb73f7..59b03dc5423b 100644 --- a/packages/session-ui/src/styles/index.css +++ b/packages/session-ui/src/styles/index.css @@ -1,6 +1,7 @@ @layer theme, base, components, utilities; @import "../components/basic-tool.css" layer(components); +@import "../components/tool-header.css" layer(components); @import "../components/file.css" layer(components); @import "../components/markdown.css" layer(components); @import "../components/message-part.css" layer(components); diff --git a/packages/session-ui/src/timeline/tool-headers.stories.tsx b/packages/session-ui/src/timeline/tool-headers.stories.tsx new file mode 100644 index 000000000000..60a871515165 --- /dev/null +++ b/packages/session-ui/src/timeline/tool-headers.stories.tsx @@ -0,0 +1,84 @@ +import { createMemo, createSignal } from "solid-js" +import { CurrentSessionProviders } from "../storybook/current-session-story" +import { storyDocument, storyTool } from "../storybook/current-session-scenarios" +import { SessionTimeline } from "./session-timeline" + +export default { + title: "OpenCode/Work/Tool headers", + id: "current-session-tool-headers", + component: SessionTimeline, + parameters: { layout: "fullscreen" }, +} + +export const SharedHeaders = { + args: { phase: "completed", pathKnown: true, longPath: false }, + argTypes: { phase: { control: "select", options: ["streaming", "running", "completed"] } }, + render: (args: { phase: "streaming" | "running" | "completed"; pathKnown: boolean; longPath: boolean }) => { + const [phase, setPhase] = createSignal(args.phase) + const [known, setKnown] = createSignal(args.pathKnown) + const path = (name: string) => + args.longPath + ? `src/components/session/timeline/tools/deeply/nested/directory/with/a/long/path/${"long-filename-".repeat(8)}${name}.ts` + : `src/components/${name}.ts` + const document = createMemo(() => + storyDocument( + [ + storyTool("tool_header_read", "read", phase(), { path: path("read"), offset: 12, limit: 40 }), + storyTool( + "tool_header_grep", + "grep", + phase(), + { path: "src/components", pattern: "header", include: "*.tsx" }, + { metadata: { matches: 3 } }, + ), + storyTool("tool_header_shell", "shell", phase(), { command: "printf checked" }, { output: "checked" }), + storyTool( + "tool_header_execute", + "execute", + phase(), + { code: 'console.log("checked")' }, + { output: "checked" }, + ), + storyTool("tool_header_webfetch", "webfetch", phase(), { url: "https://example.com/docs" }), + storyTool("tool_header_edit", "edit", phase(), { + ...(known() ? { path: path("edit") } : {}), + oldString: "export const before = true\n", + newString: "export const after = true\n", + }), + storyTool("tool_header_write", "write", phase(), { + ...(known() ? { path: path("write") } : {}), + content: "export const written = true\n", + }), + ], + phase() !== "completed", + ), + ) + return ( +
+
+ + + + +
+ + + +
+ ) + }, +} diff --git a/packages/session-ui/src/tools/tool-renderer.tsx b/packages/session-ui/src/tools/tool-renderer.tsx index dbf6e99d9f89..04ea7bccb47d 100644 --- a/packages/session-ui/src/tools/tool-renderer.tsx +++ b/packages/session-ui/src/tools/tool-renderer.tsx @@ -19,6 +19,7 @@ import { type SessionSummary, useData } from "../context" import { useFileComponent } from "@opencode-ai/ui/context/file" import { type UiI18n, useI18n } from "@opencode-ai/ui/context/i18n" import { BasicTool, GenericTool } from "../components/basic-tool" +import { ToolHeader } from "../components/tool-header" import { Accordion } from "@opencode-ai/ui/accordion" import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" import { Collapsible } from "@opencode-ai/ui/collapsible" @@ -560,15 +561,7 @@ export function CurrentContextToolGroup(props: { onOpenChange={change} trigger={
- - - {(before) => {before()}} - - {label().title} - - {(after) => {after()}} - - +
} > @@ -689,32 +682,22 @@ export function CurrentContextToolGroup(props: {
-
-
- - - - - {(subtitle) => {subtitle()}} + + {(matches) => ( + <> + + {matches()} + + )} - - {(arg) => {arg}} - -
- - {(matches) => ( - <> - - {matches()} - - )} - -
+ } + />
@@ -1209,28 +1192,23 @@ ToolRegistry.register({ {...props} hideDetails icon="window-cursor" - trigger={ -
-
- - - - - event.stopPropagation()} - > - {url()} - - - -
-
- } + trigger={{ + title: i18n.t("ui.tool.webfetch"), + subtitle: ( + + event.stopPropagation()} + > + {url()} + + + + ), + }} /> ) }, @@ -1443,16 +1421,15 @@ ToolRegistry.register({ compact allowOpenWhilePending trigger={(open) => ( -
-
- - - + -
-
+ } + /> )} > @@ -1532,25 +1509,20 @@ ToolRegistry.register({ compact allowOpenWhilePending trigger={(open) => ( -
-
- - - + - {i18n.t("ui.tool.shell.writingCommand")} - - } + fallback={{i18n.t("ui.tool.shell.writingCommand")}} > {(command) => } -
-
+ } + /> )} > @@ -1663,30 +1635,13 @@ ToolRegistry.register({ icon="code-lines" rail={false} defer={props.deferContent !== false} - trigger={ -
-
-
- - - - - {filename()} - -
- -
- {displayDirectory(inputPath())} -
-
-
-
- - {(diff) => } - -
-
- } + trigger={{ + title: i18n.t("ui.messagePart.title.edit"), + subtitle: !pending() ? filename() : undefined, + subtitleDir: "ltr", + directory: !pending() && inputPath().includes("/") ? displayDirectory(inputPath()) : undefined, + action: {(diff) => }, + }} > -
-
- - - - - {filename()} - -
- -
- {displayDirectory(path())} -
-
-
-
{/* */}
- - } + trigger={{ + title: i18n.t("ui.messagePart.title.write"), + subtitle: !pending() ? filename() : undefined, + subtitleDir: "ltr", + directory: !pending() && path().includes("/") ? displayDirectory(path()) : undefined, + }} >