diff --git a/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx b/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx
new file mode 100644
index 000000000000..dd030870a15b
--- /dev/null
+++ b/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx
@@ -0,0 +1,118 @@
+import { For } from "solid-js"
+import { createStore } from "solid-js/store"
+import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
+import { SettingsProvider, useSettings } from "@/context/settings"
+
+export default {
+ title: "Composer/Revert Dock",
+ id: "composer-revert-dock",
+ tags: ["autodocs"],
+ parameters: {
+ docs: {
+ description: {
+ component: `### Overview
+Real \`SessionRevertDock\` from app code, rendered above a mock composer card.
+
+### Source path
+- \`packages/app/src/pages/session/composer/session-revert-dock.tsx\`
+
+### Why the mock composer
+The live composer overlaps the dock's bottom by 18px (\`session-composer-region-controller.ts\` \`lift()\`).
+The card below reproduces that overlap so the collapsed/expanded cutoff behavior can be verified in isolation.
+
+### Layout split
+Use the **Layout** button to toggle \`newLayoutDesigns\` and preview both the v2 dock and the legacy (v1) \`DockTray\` fallback.
+
+### Notes
+- \`onRestore\` only mutates local story state, so nothing in the real session is affected.
+- Click the header to expand/collapse. Click "Restore message" to remove a row.`,
+ },
+ },
+ },
+}
+
+const messages = [
+ "update current branch with latest changes from dev and fix conflicts if any",
+ "investigate why the chat input loses focus after sending a message",
+ "Debug why streaming responses sometimes duplicate the last token",
+ "suggest a better title for this PR based on the diff",
+ "add a storybook story for the revert dock",
+ "reduce re-renders in the timeline component",
+]
+
+const btn = (accent?: boolean) =>
+ ({
+ padding: "6px 12px",
+ "border-radius": "6px",
+ border: "1px solid var(--v2-border-border-base, #0000001a)",
+ background: accent ? "var(--v2-background-bg-contrast, #242424)" : "var(--v2-background-bg-base, #fff)",
+ color: accent ? "var(--v2-text-text-contrast, #fafafa)" : "var(--v2-text-text-base, #161616)",
+ cursor: "pointer",
+ "font-size": "13px",
+ }) as const
+
+function Stage(props: { count: number }) {
+ const settings = useSettings()
+ const seed = () => messages.slice(0, props.count).map((text, index) => ({ id: `rolled-${index}`, text }))
+ const [store, setStore] = createStore({ items: seed() })
+
+ const v2 = () => settings.general.newLayoutDesigns()
+ const reset = () => setStore("items", seed())
+ const restore = (id: string) =>
+ setStore(
+ "items",
+ store.items.filter((item) => item.id !== id),
+ )
+
+ return (
+
+
+
+
+
+
+ {/* Reproduce the real composer stack: dock + card overlapping the dock's bottom by lift() = 18px */}
+
+
+
+ Ask anything...
+
+
+
+
+ Restored so far:{" "}
+
+ {(item) => {store.items.some((current) => current.id === item.id) ? "" : `“${item.text}” `}}
+
+
+
+ )
+}
+
+const story = (count: number) => () => (
+
+
+
+)
+
+export const OneMessage = {
+ name: "1 rolled back",
+ render: story(1),
+}
+
+export const ThreeMessages = {
+ name: "3 rolled back",
+ render: story(3),
+}
+
+export const ManyMessages = {
+ name: "6 rolled back (scrolls)",
+ render: story(6),
+}
diff --git a/packages/app/src/pages/session/composer/session-revert-dock.tsx b/packages/app/src/pages/session/composer/session-revert-dock.tsx
index 5a5b8a9cd457..d8f022a53780 100644
--- a/packages/app/src/pages/session/composer/session-revert-dock.tsx
+++ b/packages/app/src/pages/session/composer/session-revert-dock.tsx
@@ -3,7 +3,11 @@ import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
import { DockTray } from "@opencode-ai/ui/dock-surface"
import { IconButton } from "@opencode-ai/ui/icon-button"
+import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
+import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
+import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { useLanguage } from "@/context/language"
+import { useSettings } from "@/context/settings"
export function SessionRevertDock(props: {
items: { id: string; text: string }[]
@@ -12,6 +16,7 @@ export function SessionRevertDock(props: {
onRestore: (id: string) => void
}) {
const language = useLanguage()
+ const settings = useSettings()
const [store, setStore] = createStore({
collapsed: true,
})
@@ -31,69 +36,153 @@ export function SessionRevertDock(props: {
)
const preview = createMemo(() => props.items[0]?.text ?? "")
+ const onHeaderKeyDown = (event: KeyboardEvent) => {
+ if (event.key !== "Enter" && event.key !== " ") return
+ event.preventDefault()
+ toggle()
+ }
+
return (
-
+
+
+
{label()}
+
+ {preview()}
+
+
+ {
+ event.preventDefault()
+ event.stopPropagation()
+ }}
+ onClick={(event) => {
+ event.stopPropagation()
+ toggle()
+ }}
+ aria-label={
+ store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse")
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
{
- if (event.key !== "Enter" && event.key !== " ") return
- event.preventDefault()
- toggle()
- }}
+ data-component="session-revert-dock"
+ class="w-full overflow-hidden rounded-xl border-[0.5px] border-v2-border-border-base bg-v2-background-bg-layer-01"
>
-
{label()}
-
- {preview()}
-
-
-
{
- event.preventDefault()
- event.stopPropagation()
- }}
- onClick={(event) => {
- event.stopPropagation()
- toggle()
+
+
+
+ >
+ {label()}
+
+
+
+ {preview()}
+
+
+
+ }
+ size="large"
+ variant="ghost-muted"
+ style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
+ onMouseDown={(event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ }}
+ onClick={(event) => {
+ event.stopPropagation()
+ toggle()
+ }}
+ aria-label={
+ store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse")
+ }
+ />
+
-
-
-
-
+ {/* Sacrificial space the composer overlaps via its negative lift (18px), so the header stays fully visible */}
+
+
+
-
-
-
-
+
+ {/* Scroll viewport ends above the composer; the 18px sacrificial below is what the composer overlaps */}
+
+
+
+
+
)
}
diff --git a/packages/storybook/.storybook/mocks/app/context/language.ts b/packages/storybook/.storybook/mocks/app/context/language.ts
index df28d79fbdca..2510acc6f974 100644
--- a/packages/storybook/.storybook/mocks/app/context/language.ts
+++ b/packages/storybook/.storybook/mocks/app/context/language.ts
@@ -2,6 +2,11 @@ const dict: Record = {
"session.todo.title": "Todos",
"session.todo.collapse": "Collapse todos",
"session.todo.expand": "Expand todos",
+ "session.revertDock.summary.one": "{{count}} rolled back message",
+ "session.revertDock.summary.other": "{{count}} rolled back messages",
+ "session.revertDock.collapse": "Collapse rolled back messages",
+ "session.revertDock.expand": "Expand rolled back messages",
+ "session.revertDock.restore": "Restore message",
"prompt.loading": "Loading prompt...",
"prompt.placeholder.normal": "Ask anything...",
"prompt.placeholder.simple": "Ask anything...",
diff --git a/packages/ui/src/v2/components/icon.tsx b/packages/ui/src/v2/components/icon.tsx
index d9bd04e48fab..b0b70bf37f90 100644
--- a/packages/ui/src/v2/components/icon.tsx
+++ b/packages/ui/src/v2/components/icon.tsx
@@ -133,6 +133,10 @@ const icons = {
viewBox: "0 0 20 20",
body: ``,
},
+ "outline-reset": {
+ viewBox: "0 0 20 20",
+ body: ``,
+ },
archive: {
viewBox: "0 0 16 16",
body: ``,