From 0e204b6f1609b190e3e3577b1d8603f72e25ad9b Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sat, 16 May 2026 14:19:09 +0800 Subject: [PATCH 1/3] refactor(ui): extract session turn diffs --- .../ui/src/components/session-turn-diffs.tsx | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 packages/ui/src/components/session-turn-diffs.tsx diff --git a/packages/ui/src/components/session-turn-diffs.tsx b/packages/ui/src/components/session-turn-diffs.tsx new file mode 100644 index 000000000..636d0c463 --- /dev/null +++ b/packages/ui/src/components/session-turn-diffs.tsx @@ -0,0 +1,129 @@ +import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2/client" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" +import { createEffect, createMemo, createSignal, For, on, Show } from "solid-js" +import { Dynamic } from "solid-js/web" +import { createStore } from "solid-js/store" +import { useFileComponent } from "../context/file" +import { useI18n } from "../context/i18n" +import { Accordion } from "./accordion" +import { DiffChanges } from "./diff-changes" +import { Icon } from "./icon" +import { normalize } from "./session-diff" +import { StickyAccordionHeader } from "./sticky-accordion-header" + +const MAX_FILES = 10 + +export function SessionTurnDiffs(props: { + diffs: SnapshotFileDiff[] + onShowAllToggle?: () => void +}) { + const i18n = useI18n() + const fileComponent = useFileComponent() + const [state, setState] = createStore({ + showAll: false, + expanded: [] as string[], + }) + const showAll = () => state.showAll + const expanded = () => state.expanded + const edited = createMemo(() => props.diffs.length) + const overflow = createMemo(() => Math.max(0, edited() - MAX_FILES)) + const visible = createMemo(() => (showAll() ? props.diffs : props.diffs.slice(0, MAX_FILES))) + const toggleAll = () => { + props.onShowAllToggle?.() + setState("showAll", !showAll()) + } + + return ( +
+
+ + {edited()} {i18n.t("ui.sessionTurn.diffs.changed")}{" "} + {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")} + + + 0}> + + {showAll() ? i18n.t("ui.sessionTurn.diffs.showLess") : i18n.t("ui.sessionTurn.diffs.showAll")} + + +
+
+ setState("expanded", Array.isArray(value) ? value : value ? [value] : [])} + > + + {(diff) => { + const view = normalize(diff) + const active = createMemo(() => expanded().includes(diff.file)) + const [shown, setShown] = createSignal(false) + + createEffect( + on( + active, + (value) => { + if (!value) { + setShown(false) + return + } + + requestAnimationFrame(() => { + if (!active()) return + setShown(true) + }) + }, + { defer: true }, + ), + ) + + return ( + + + +
+ + + + {`\u202A${getDirectory(diff.file)}\u202C`} + + + {getFilename(diff.file)} + +
+ + + + + + +
+
+
+
+ + +
+ +
+
+
+
+ ) + }} +
+
+ 0}> +
+ {i18n.t("ui.sessionTurn.diffs.more", { count: String(overflow()) })} +
+
+
+
+ ) +} From 4a72d6fbe26f05aa7479f247941951647b31d454 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sat, 16 May 2026 22:32:54 +0800 Subject: [PATCH 2/3] fix(ui): wire accessible session turn diffs --- .../ui/src/components/session-turn-diffs.tsx | 10 +- .../components/session-turn-parent.test.ts | 2 +- packages/ui/src/components/session-turn.css | 7 + packages/ui/src/components/session-turn.tsx | 120 +----------------- 4 files changed, 17 insertions(+), 122 deletions(-) diff --git a/packages/ui/src/components/session-turn-diffs.tsx b/packages/ui/src/components/session-turn-diffs.tsx index 636d0c463..33896e01b 100644 --- a/packages/ui/src/components/session-turn-diffs.tsx +++ b/packages/ui/src/components/session-turn-diffs.tsx @@ -46,9 +46,9 @@ export function SessionTurnDiffs(props: { 0}> - +
@@ -119,9 +119,9 @@ export function SessionTurnDiffs(props: { 0}> -
- {i18n.t("ui.sessionTurn.diffs.more", { count: String(overflow()) })} -
+
diff --git a/packages/ui/src/components/session-turn-parent.test.ts b/packages/ui/src/components/session-turn-parent.test.ts index 2c2cb5a3c..0bdefc954 100644 --- a/packages/ui/src/components/session-turn-parent.test.ts +++ b/packages/ui/src/components/session-turn-parent.test.ts @@ -16,7 +16,7 @@ test("session turn collects assistant messages by parent id across the full mess test("legacy diff fallback is gated by visible turn-change data", () => { const source = readFileSync(new URL("./session-turn.tsx", import.meta.url), "utf8") - expect(source).toContain("!hasVisibleTurnChanges(turnChange()) && edited() > 0 && !working()") + expect(source).toContain("!hasVisibleTurnChanges(turnChange()) && diffs().length > 0 && !working()") expect(source).not.toContain("props.turnChanges === undefined &&") }) diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index db3ec1906..bcbf2a76f 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -290,6 +290,9 @@ } [data-slot="session-turn-diffs-toggle"] { + appearance: none; + border: 0; + background: none; color: var(--brand-primary); font-family: var(--font-family-sans); font-size: var(--font-size-body); @@ -299,6 +302,7 @@ opacity: 0; transition: opacity 0.15s ease; margin-left: 4px; + padding: 0; } [data-component="session-turn-diffs-group"]:hover [data-slot="session-turn-diffs-toggle"] { @@ -310,6 +314,9 @@ } [data-slot="session-turn-diffs-more"] { + appearance: none; + border: 0; + background: none; color: var(--fg-weak); font-family: var(--font-family-sans); font-size: var(--font-size-caption); diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 5a061efbc..460f39fe9 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -7,27 +7,20 @@ import { } from "@opencode-ai/sdk/v2/client" import type { SessionStatus } from "@opencode-ai/sdk/v2" import { useData } from "../context" -import { useFileComponent } from "../context/file" import { Binary } from "@opencode-ai/core/util/binary" -import { getDirectory, getFilename } from "@opencode-ai/core/util/path" -import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js" -import { createStore } from "solid-js/store" -import { Dynamic } from "solid-js/web" +import { createEffect, createMemo, createSignal, ParentProps, Show } from "solid-js" import { AssistantParts, Message, MessageDivider, PART_MAPPING, type UserActions } from "./message-part" import { Card } from "./card" -import { Accordion } from "./accordion" -import { StickyAccordionHeader } from "./sticky-accordion-header" -import { DiffChanges } from "./diff-changes" import { Icon } from "./icon" import { TextShimmer } from "./text-shimmer" import { SessionRetry } from "./session-retry" import { TextReveal } from "./text-reveal" import { createAutoScroll } from "../hooks" import { useI18n } from "../context/i18n" -import { normalize } from "./session-diff" import { hasVisibleTurnChanges, type TurnChangeActions, type TurnChangeDisplay } from "./session-turn-changes" import { SessionTurnChangesPanel } from "./session-turn-changes-panel" +import { SessionTurnDiffs } from "./session-turn-diffs" function record(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value) @@ -171,7 +164,6 @@ export function SessionTurn( ) { const data = useData() const i18n = useI18n() - const fileComponent = useFileComponent() const emptyMessages: MessageType[] = [] const emptyParts: PartType[] = [] @@ -254,20 +246,6 @@ export function SessionTurn( }, []) .reverse() }) - const MAX_FILES = 10 - const edited = createMemo(() => diffs().length) - const [state, setState] = createStore({ - showAll: false, - expanded: [] as string[], - }) - const showAll = () => state.showAll - const expanded = () => state.expanded - const overflow = createMemo(() => Math.max(0, edited() - MAX_FILES)) - const visible = createMemo(() => (showAll() ? diffs() : diffs().slice(0, MAX_FILES))) - const toggleAll = () => { - autoScroll.pause() - setState("showAll", !showAll()) - } const assistantMessages = createMemo( () => { @@ -454,98 +432,8 @@ export function SessionTurn( /> )} - 0 && !working()}> -
-
- - {edited()} {i18n.t("ui.sessionTurn.diffs.changed")}{" "} - {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")} - - - 0}> - - {showAll() ? i18n.t("ui.sessionTurn.diffs.showLess") : i18n.t("ui.sessionTurn.diffs.showAll")} - - -
-
- setState("expanded", Array.isArray(value) ? value : value ? [value] : [])} - > - - {(diff) => { - const view = normalize(diff) - const active = createMemo(() => expanded().includes(diff.file)) - const [shown, setShown] = createSignal(false) - - createEffect( - on( - active, - (value) => { - if (!value) { - setShown(false) - return - } - - requestAnimationFrame(() => { - if (!active()) return - setShown(true) - }) - }, - { defer: true }, - ), - ) - - return ( - - - -
- - - - {`\u202A${getDirectory(diff.file)}\u202C`} - - - {getFilename(diff.file)} - -
- - - - - - -
-
-
-
- - -
- -
-
-
-
- ) - }} -
-
- 0}> -
- {i18n.t("ui.sessionTurn.diffs.more", { count: String(overflow()) })} -
-
-
-
+ 0 && !working()}> + autoScroll.pause()} /> From 0cfe2542cc58b28eb5acf7580a9ef563f5e88961 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sat, 16 May 2026 22:40:59 +0800 Subject: [PATCH 3/3] fix(ui): reveal diff toggle on keyboard focus --- packages/ui/src/components/session-turn.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index bcbf2a76f..7e9e01d43 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -313,6 +313,12 @@ opacity: 1; } + [data-slot="session-turn-diffs-toggle"]:focus-visible { + opacity: 1; + outline: 1px solid var(--border-active); + outline-offset: 2px; + } + [data-slot="session-turn-diffs-more"] { appearance: none; border: 0; @@ -324,6 +330,7 @@ margin-top: 12px; padding: 0 0 6px; cursor: pointer; + text-align: left; transition: color 0.15s ease; &:hover {