瀏覽代碼

fix: use diff context instead of prop drilling

Adam 2 月之前
父節點
當前提交
9a90939ac4

+ 34 - 30
packages/desktop/src/index.tsx

@@ -6,6 +6,8 @@ import { MetaProvider } from "@solidjs/meta"
 import { Font } from "@opencode-ai/ui/font"
 import { Favicon } from "@opencode-ai/ui/favicon"
 import { MarkedProvider } from "@opencode-ai/ui/context/marked"
+import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
+import { Diff } from "@opencode-ai/ui/diff"
 import { GlobalSyncProvider, useGlobalSync } from "./context/global-sync"
 import Layout from "@/pages/layout"
 import DirectoryLayout from "@/pages/directory-layout"
@@ -35,38 +37,40 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
 render(
   () => (
     <MarkedProvider>
-      <GlobalSDKProvider url={url}>
-        <GlobalSyncProvider>
-          <LayoutProvider>
-            <MetaProvider>
-              <Font />
-              <Router root={Layout}>
-                <Route
-                  path="/"
-                  component={() => {
-                    const globalSync = useGlobalSync()
-                    const slug = createMemo(() => base64Encode(globalSync.data.defaultProject!.worktree))
-                    return <Navigate href={`${slug()}/session`} />
-                  }}
-                />
-                <Route path="/:dir" component={DirectoryLayout}>
-                  <Route path="/" component={() => <Navigate href="session" />} />
+      <DiffComponentProvider component={Diff}>
+        <GlobalSDKProvider url={url}>
+          <GlobalSyncProvider>
+            <LayoutProvider>
+              <MetaProvider>
+                <Font />
+                <Router root={Layout}>
                   <Route
-                    path="/session/:id?"
-                    component={(p) => (
-                      <Show when={p.params.id || true} keyed>
-                        <SessionProvider>
-                          <Session />
-                        </SessionProvider>
-                      </Show>
-                    )}
+                    path="/"
+                    component={() => {
+                      const globalSync = useGlobalSync()
+                      const slug = createMemo(() => base64Encode(globalSync.data.defaultProject!.worktree))
+                      return <Navigate href={`${slug()}/session`} />
+                    }}
                   />
-                </Route>
-              </Router>
-            </MetaProvider>
-          </LayoutProvider>
-        </GlobalSyncProvider>
-      </GlobalSDKProvider>
+                  <Route path="/:dir" component={DirectoryLayout}>
+                    <Route path="/" component={() => <Navigate href="session" />} />
+                    <Route
+                      path="/session/:id?"
+                      component={(p) => (
+                        <Show when={p.params.id || true} keyed>
+                          <SessionProvider>
+                            <Session />
+                          </SessionProvider>
+                        </Show>
+                      )}
+                    />
+                  </Route>
+                </Router>
+              </MetaProvider>
+            </LayoutProvider>
+          </GlobalSyncProvider>
+        </GlobalSDKProvider>
+      </DiffComponentProvider>
     </MarkedProvider>
   ),
   root!,

+ 1 - 5
packages/desktop/src/pages/session.tsx

@@ -30,7 +30,6 @@ import { useSync } from "@/context/sync"
 import { useSession } from "@/context/session"
 import { useLayout } from "@/context/layout"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
-import { Diff } from "@opencode-ai/ui/diff"
 import { Terminal } from "@/components/terminal"
 
 export default function Page() {
@@ -282,7 +281,7 @@ export default function Page() {
   const wide = createMemo(() => layout.review.state() === "tab" || !session.diffs().length)
 
   return (
-    <div class="relative bg-background-base size-full overflow-x-hidden flex flex-col items-start">
+    <div class="relative bg-background-base size-full overflow-x-hidden flex flex-col">
       <div class="min-h-0 grow w-full">
         <DragDropProvider
           onDragStart={handleDragStart}
@@ -389,7 +388,6 @@ export default function Page() {
                                   ? "pr-6 pl-18"
                                   : "px-6"),
                           }}
-                          diffComponent={Diff}
                         />
                       </div>
                     </Match>
@@ -438,7 +436,6 @@ export default function Page() {
                         container: "px-6",
                       }}
                       diffs={session.diffs()}
-                      diffComponent={Diff}
                       actions={
                         <Tooltip value="Open in tab">
                           <IconButton
@@ -470,7 +467,6 @@ export default function Page() {
                       container: "px-6",
                     }}
                     diffs={session.diffs()}
-                    diffComponent={Diff}
                     split
                   />
                 </div>

+ 211 - 204
packages/enterprise/src/routes/share/[shareID].tsx

@@ -2,6 +2,7 @@ import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } f
 import { SessionTurn } from "@opencode-ai/ui/session-turn"
 import { SessionReview } from "@opencode-ai/ui/session-review"
 import { DataProvider } from "@opencode-ai/ui/context"
+import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
 import { createAsync, query, useParams } from "@solidjs/router"
 import { createEffect, createMemo, ErrorBoundary, For, Match, Show, Switch } from "solid-js"
 import { Share } from "~/core/share"
@@ -18,7 +19,7 @@ import z from "zod"
 import NotFound from "../[...404]"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
-import { Diff } from "@opencode-ai/ui/diff-ssr"
+import { Diff as SSRDiff } from "@opencode-ai/ui/diff-ssr"
 import { clientOnly } from "@solidjs/start"
 
 const ClientOnlyDiff = clientOnly(() => import("@opencode-ai/ui/diff").then((m) => ({ default: m.Diff })))
@@ -157,234 +158,240 @@ export default function () {
           const info = createMemo(() => data().session[match().index])
 
           return (
-            <DataProvider data={data()} directory={info().directory}>
-              {iife(() => {
-                const [store, setStore] = createStore({
-                  messageId: undefined as string | undefined,
-                })
-                const messages = createMemo(() =>
-                  data().sessionID
-                    ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
-                        (a, b) => b.time.created - a.time.created,
-                      )
-                    : [],
-                )
-                const firstUserMessage = createMemo(() => messages().at(0))
-                const activeMessage = createMemo(
-                  () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
-                )
-                function setActiveMessage(message: UserMessage | undefined) {
-                  if (message) {
-                    setStore("messageId", message.id)
-                  } else {
-                    setStore("messageId", undefined)
+            <DiffComponentProvider component={ClientOnlyDiff}>
+              <DataProvider data={data()} directory={info().directory}>
+                {iife(() => {
+                  const [store, setStore] = createStore({
+                    messageId: undefined as string | undefined,
+                  })
+                  const messages = createMemo(() =>
+                    data().sessionID
+                      ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
+                          (a, b) => b.time.created - a.time.created,
+                        )
+                      : [],
+                  )
+                  const firstUserMessage = createMemo(() => messages().at(0))
+                  const activeMessage = createMemo(
+                    () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
+                  )
+                  function setActiveMessage(message: UserMessage | undefined) {
+                    if (message) {
+                      setStore("messageId", message.id)
+                    } else {
+                      setStore("messageId", undefined)
+                    }
                   }
-                }
-                const provider = createMemo(() => activeMessage()?.model?.providerID)
-                const modelID = createMemo(() => activeMessage()?.model?.modelID)
-                const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
-                const diffs = createMemo(() => {
-                  const diffs = data().session_diff[data().sessionID] ?? []
-                  const preloaded = data().session_diff_preload[data().sessionID] ?? []
-                  return diffs.map((diff) => ({
-                    ...diff,
-                    preloaded: preloaded.find((d) => d.newFile.name === diff.file),
-                  }))
-                })
-                const splitDiffs = createMemo(() => {
-                  const diffs = data().session_diff[data().sessionID] ?? []
-                  const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
-                  return diffs.map((diff) => ({
-                    ...diff,
-                    preloaded: preloaded.find((d) => d.newFile.name === diff.file),
-                  }))
-                })
+                  const provider = createMemo(() => activeMessage()?.model?.providerID)
+                  const modelID = createMemo(() => activeMessage()?.model?.modelID)
+                  const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
+                  const diffs = createMemo(() => {
+                    const diffs = data().session_diff[data().sessionID] ?? []
+                    const preloaded = data().session_diff_preload[data().sessionID] ?? []
+                    return diffs.map((diff) => ({
+                      ...diff,
+                      preloaded: preloaded.find((d) => d.newFile.name === diff.file),
+                    }))
+                  })
+                  const splitDiffs = createMemo(() => {
+                    const diffs = data().session_diff[data().sessionID] ?? []
+                    const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
+                    return diffs.map((diff) => ({
+                      ...diff,
+                      preloaded: preloaded.find((d) => d.newFile.name === diff.file),
+                    }))
+                  })
 
-                const title = () => (
-                  <div class="flex flex-col gap-4">
-                    <div class="h-8 flex gap-4 items-center justify-start self-stretch">
-                      <div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
-                        <Mark class="shrink-0 w-3 my-0.5" />
-                        <div class="text-12-mono text-text-base">v{info().version}</div>
-                      </div>
-                      <div class="flex gap-2 items-center">
-                        <img src={`https://models.dev/logos/${provider()}.svg`} class="size-3.5 shrink-0 dark:invert" />
-                        <div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
-                      </div>
-                      <div class="text-12-regular text-text-weaker">
-                        {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
+                  const title = () => (
+                    <div class="flex flex-col gap-4">
+                      <div class="h-8 flex gap-4 items-center justify-start self-stretch">
+                        <div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
+                          <Mark class="shrink-0 w-3 my-0.5" />
+                          <div class="text-12-mono text-text-base">v{info().version}</div>
+                        </div>
+                        <div class="flex gap-2 items-center">
+                          <img
+                            src={`https://models.dev/logos/${provider()}.svg`}
+                            class="size-3.5 shrink-0 dark:invert"
+                          />
+                          <div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
+                        </div>
+                        <div class="text-12-regular text-text-weaker">
+                          {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
+                        </div>
                       </div>
+                      <div class="text-left text-16-medium text-text-strong">{info().title}</div>
                     </div>
-                    <div class="text-left text-16-medium text-text-strong">{info().title}</div>
-                  </div>
-                )
+                  )
 
-                const turns = () => (
-                  <div class="relative mt-2 pt-6 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
-                    <div class="px-4">{title()}</div>
-                    <div class="flex flex-col gap-15 items-start justify-start mt-4">
-                      <For each={messages()}>
-                        {(message) => (
-                          <SessionTurn
-                            sessionID={data().sessionID}
-                            messageID={message.id}
-                            classes={{
-                              root: "min-w-0 w-full relative",
-                              content:
-                                "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
-                              container: "px-4",
-                            }}
-                            diffComponent={ClientOnlyDiff}
-                          />
-                        )}
-                      </For>
-                    </div>
-                    <div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
-                      <Logo class="w-58.5 opacity-12" />
+                  const turns = () => (
+                    <div class="relative mt-2 pt-6 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
+                      <div class="px-4">{title()}</div>
+                      <div class="flex flex-col gap-15 items-start justify-start mt-4">
+                        <For each={messages()}>
+                          {(message) => (
+                            <SessionTurn
+                              sessionID={data().sessionID}
+                              messageID={message.id}
+                              classes={{
+                                root: "min-w-0 w-full relative",
+                                content:
+                                  "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
+                                container: "px-4",
+                              }}
+                            />
+                          )}
+                        </For>
+                      </div>
+                      <div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
+                        <Logo class="w-58.5 opacity-12" />
+                      </div>
                     </div>
-                  </div>
-                )
+                  )
 
-                const wide = createMemo(() => diffs().length === 0)
+                  const wide = createMemo(() => diffs().length === 0)
 
-                return (
-                  <div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
-                    <header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
-                      <div class="">
-                        <a href="https://opencode.ai">
-                          <Mark />
-                        </a>
-                      </div>
-                      <div class="flex gap-3 items-center">
-                        <IconButton
-                          as={"a"}
-                          href="https://github.com/sst/opencode"
-                          target="_blank"
-                          icon="github"
-                          variant="ghost"
-                        />
-                        <IconButton
-                          as={"a"}
-                          href="https://opencode.ai/discord"
-                          target="_blank"
-                          icon="discord"
-                          variant="ghost"
-                        />
-                      </div>
-                    </header>
-                    <div class="select-text flex flex-col flex-1 min-h-0">
-                      <div classList={{ "hidden w-full flex-1 min-h-0": true, "md:flex": wide(), "lg:flex": !wide() }}>
+                  return (
+                    <div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
+                      <header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
+                        <div class="">
+                          <a href="https://opencode.ai">
+                            <Mark />
+                          </a>
+                        </div>
+                        <div class="flex gap-3 items-center">
+                          <IconButton
+                            as={"a"}
+                            href="https://github.com/sst/opencode"
+                            target="_blank"
+                            icon="github"
+                            variant="ghost"
+                          />
+                          <IconButton
+                            as={"a"}
+                            href="https://opencode.ai/discord"
+                            target="_blank"
+                            icon="discord"
+                            variant="ghost"
+                          />
+                        </div>
+                      </header>
+                      <div class="select-text flex flex-col flex-1 min-h-0">
                         <div
-                          classList={{
-                            "@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
-                            "mx-auto max-w-146": !wide(),
-                          }}
+                          classList={{ "hidden w-full flex-1 min-h-0": true, "md:flex": wide(), "lg:flex": !wide() }}
                         >
                           <div
                             classList={{
-                              "w-full flex justify-start items-start min-w-0": true,
-                              "max-w-146 mx-auto px-6": wide(),
-                              "pr-6 pl-18": !wide() && messages().length > 1,
-                              "px-6": !wide() && messages().length === 1,
+                              "@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
+                              "mx-auto max-w-146": !wide(),
                             }}
                           >
-                            {title()}
-                          </div>
-                          <div class="flex items-start justify-start h-full min-h-0">
-                            <SessionMessageRail
-                              messages={messages()}
-                              current={activeMessage()}
-                              onMessageSelect={setActiveMessage}
-                              wide={wide()}
-                            />
-                            <SessionTurn
-                              sessionID={data().sessionID}
-                              messageID={store.messageId ?? firstUserMessage()!.id!}
-                              classes={{
-                                root: "grow",
-                                content: "flex flex-col justify-between items-start",
-                                container:
-                                  "w-full pb-20 " +
-                                  (wide() ? "max-w-146 mx-auto px-6" : messages().length > 1 ? "pr-6 pl-18" : "px-6"),
+                            <div
+                              classList={{
+                                "w-full flex justify-start items-start min-w-0": true,
+                                "max-w-146 mx-auto px-6": wide(),
+                                "pr-6 pl-18": !wide() && messages().length > 1,
+                                "px-6": !wide() && messages().length === 1,
                               }}
-                              diffComponent={ClientOnlyDiff}
                             >
-                              <div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
-                                <Logo class="w-58.5 opacity-12" />
-                              </div>
-                            </SessionTurn>
-                          </div>
-                        </div>
-                        <Show when={diffs().length > 0}>
-                          <div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
-                            <SessionReview
-                              class="@4xl:hidden"
-                              diffs={diffs()}
-                              diffComponent={Diff}
-                              classes={{
-                                root: "pb-20",
-                                header: "px-6",
-                                container: "px-6",
-                              }}
-                            />
-                            <SessionReview
-                              split
-                              class="hidden @4xl:flex"
-                              diffs={splitDiffs()}
-                              diffComponent={Diff}
-                              classes={{
-                                root: "pb-20",
-                                header: "px-6",
-                                container: "px-6",
-                              }}
-                            />
+                              {title()}
+                            </div>
+                            <div class="flex items-start justify-start h-full min-h-0">
+                              <SessionMessageRail
+                                messages={messages()}
+                                current={activeMessage()}
+                                onMessageSelect={setActiveMessage}
+                                wide={wide()}
+                              />
+                              <SessionTurn
+                                sessionID={data().sessionID}
+                                messageID={store.messageId ?? firstUserMessage()!.id!}
+                                classes={{
+                                  root: "grow",
+                                  content: "flex flex-col justify-between items-start",
+                                  container:
+                                    "w-full pb-20 " +
+                                    (wide() ? "max-w-146 mx-auto px-6" : messages().length > 1 ? "pr-6 pl-18" : "px-6"),
+                                }}
+                              >
+                                <div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
+                                  <Logo class="w-58.5 opacity-12" />
+                                </div>
+                              </SessionTurn>
+                            </div>
                           </div>
-                        </Show>
-                      </div>
-                      <Switch>
-                        <Match when={diffs().length > 0}>
-                          <Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
-                            <Tabs.List>
-                              <Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
-                                Session
-                              </Tabs.Trigger>
-                              <Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
-                                5 Files Changed
-                              </Tabs.Trigger>
-                            </Tabs.List>
-                            <Tabs.Content value="session" class="!overflow-hidden">
-                              {turns()}
-                            </Tabs.Content>
-                            <Tabs.Content
-                              forceMount
-                              value="review"
-                              class="!overflow-hidden hidden data-[selected]:block"
-                            >
-                              <div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
+                          <Show when={diffs().length > 0}>
+                            <DiffComponentProvider component={SSRDiff}>
+                              <div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
                                 <SessionReview
+                                  class="@4xl:hidden"
                                   diffs={diffs()}
-                                  diffComponent={Diff}
                                   classes={{
                                     root: "pb-20",
-                                    header: "px-4",
-                                    container: "px-4",
+                                    header: "px-6",
+                                    container: "px-6",
+                                  }}
+                                />
+                                <SessionReview
+                                  split
+                                  class="hidden @4xl:flex"
+                                  diffs={splitDiffs()}
+                                  classes={{
+                                    root: "pb-20",
+                                    header: "px-6",
+                                    container: "px-6",
                                   }}
                                 />
                               </div>
-                            </Tabs.Content>
-                          </Tabs>
-                        </Match>
-                        <Match when={true}>
-                          <div classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}>
-                            {turns()}
-                          </div>
-                        </Match>
-                      </Switch>
+                            </DiffComponentProvider>
+                          </Show>
+                        </div>
+                        <Switch>
+                          <Match when={diffs().length > 0}>
+                            <Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
+                              <Tabs.List>
+                                <Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
+                                  Session
+                                </Tabs.Trigger>
+                                <Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
+                                  5 Files Changed
+                                </Tabs.Trigger>
+                              </Tabs.List>
+                              <Tabs.Content value="session" class="!overflow-hidden">
+                                {turns()}
+                              </Tabs.Content>
+                              <Tabs.Content
+                                forceMount
+                                value="review"
+                                class="!overflow-hidden hidden data-[selected]:block"
+                              >
+                                <div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
+                                  <DiffComponentProvider component={SSRDiff}>
+                                    <SessionReview
+                                      diffs={diffs()}
+                                      classes={{
+                                        root: "pb-20",
+                                        header: "px-4",
+                                        container: "px-4",
+                                      }}
+                                    />
+                                  </DiffComponentProvider>
+                                </div>
+                              </Tabs.Content>
+                            </Tabs>
+                          </Match>
+                          <Match when={true}>
+                            <div classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}>
+                              {turns()}
+                            </div>
+                          </Match>
+                        </Switch>
+                      </div>
                     </div>
-                  </div>
-                )
-              })}
-            </DataProvider>
+                  )
+                })}
+              </DataProvider>
+            </DiffComponentProvider>
           )
         }}
       </Show>

+ 7 - 25
packages/ui/src/components/message-part.tsx

@@ -1,4 +1,4 @@
-import { Component, createMemo, For, Match, Show, Switch, ValidComponent } from "solid-js"
+import { Component, createMemo, For, Match, Show, Switch } from "solid-js"
 import { Dynamic } from "solid-js/web"
 import {
   AssistantMessage,
@@ -8,6 +8,7 @@ import {
   ToolPart,
   UserMessage,
 } from "@opencode-ai/sdk"
+import { useDiffComponent } from "../context/diff"
 import { BasicTool } from "./basic-tool"
 import { GenericTool } from "./basic-tool"
 import { Card } from "./card"
@@ -22,14 +23,12 @@ import { unwrap } from "solid-js/store"
 export interface MessageProps {
   message: MessageType
   parts: PartType[]
-  diffComponent: ValidComponent
   sanitize?: RegExp
 }
 
 export interface MessagePartProps {
   part: PartType
   message: MessageType
-  diffComponent: ValidComponent
   hideDetails?: boolean
   sanitize?: RegExp
 }
@@ -54,7 +53,6 @@ export function Message(props: MessageProps) {
             message={assistantMessage() as AssistantMessage}
             parts={props.parts}
             sanitize={props.sanitize}
-            diffComponent={props.diffComponent}
           />
         )}
       </Match>
@@ -62,12 +60,7 @@ export function Message(props: MessageProps) {
   )
 }
 
-export function AssistantMessageDisplay(props: {
-  message: AssistantMessage
-  parts: PartType[]
-  sanitize?: RegExp
-  diffComponent: ValidComponent
-}) {
+export function AssistantMessageDisplay(props: { message: AssistantMessage; parts: PartType[]; sanitize?: RegExp }) {
   const filteredParts = createMemo(() => {
     return props.parts?.filter((x) => {
       if (x.type === "reasoning") return false
@@ -75,11 +68,7 @@ export function AssistantMessageDisplay(props: {
     })
   })
   return (
-    <For each={filteredParts()}>
-      {(part) => (
-        <Part part={part} message={props.message} sanitize={props.sanitize} diffComponent={props.diffComponent} />
-      )}
-    </For>
+    <For each={filteredParts()}>{(part) => <Part part={part} message={props.message} sanitize={props.sanitize} />}</For>
   )
 }
 
@@ -98,13 +87,7 @@ export function Part(props: MessagePartProps) {
   const part = createMemo(() => sanitizePart(unwrap(props.part), props.sanitize))
   return (
     <Show when={component()}>
-      <Dynamic
-        component={component()}
-        part={part()}
-        message={props.message}
-        diffComponent={props.diffComponent}
-        hideDetails={props.hideDetails}
-      />
+      <Dynamic component={component()} part={part()} message={props.message} hideDetails={props.hideDetails} />
     </Show>
   )
 }
@@ -113,7 +96,6 @@ export interface ToolProps {
   input: Record<string, any>
   metadata: Record<string, any>
   tool: string
-  diffComponent: ValidComponent
   output?: string
   hideDetails?: boolean
 }
@@ -180,7 +162,6 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
             component={render}
             input={input}
             tool={part.tool}
-            diffComponent={props.diffComponent}
             metadata={metadata}
             output={part.state.status === "completed" ? part.state.output : undefined}
             hideDetails={props.hideDetails}
@@ -356,6 +337,7 @@ ToolRegistry.register({
 ToolRegistry.register({
   name: "edit",
   render(props) {
+    const diffComponent = useDiffComponent()
     return (
       <BasicTool
         icon="code-lines"
@@ -381,7 +363,7 @@ ToolRegistry.register({
         <Show when={props.metadata.filediff}>
           <div data-component="edit-content">
             <Dynamic
-              component={props.diffComponent}
+              component={diffComponent}
               before={{
                 name: getFilename(props.metadata.filediff.path),
                 contents: props.metadata.filediff.before,

+ 2 - 19
packages/ui/src/components/message-progress.tsx

@@ -1,15 +1,4 @@
-import {
-  For,
-  JSXElement,
-  Match,
-  Show,
-  Switch,
-  ValidComponent,
-  createEffect,
-  createMemo,
-  createSignal,
-  onCleanup,
-} from "solid-js"
+import { For, JSXElement, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
 import { Part } from "./message-part"
 import { Spinner } from "./spinner"
 import { useData } from "../context/data"
@@ -17,7 +6,6 @@ import type { AssistantMessage as AssistantMessageType, ToolPart } from "@openco
 
 export interface MessageProgressProps {
   assistantMessages: () => AssistantMessageType[]
-  diffComponent: ValidComponent
   done?: boolean
 }
 
@@ -172,12 +160,7 @@ export function MessageProgress(props: MessageProgressProps) {
                       )
                       return (
                         <div data-slot="message-progress-item">
-                          <Part
-                            message={message()!}
-                            part={part}
-                            sanitize={sanitizer()}
-                            diffComponent={props.diffComponent}
-                          />
+                          <Part message={message()!} part={part} sanitize={sanitizer()} />
                         </div>
                       )
                     }}

+ 4 - 3
packages/ui/src/components/session-review.tsx

@@ -4,8 +4,9 @@ import { DiffChanges } from "./diff-changes"
 import { FileIcon } from "./file-icon"
 import { Icon } from "./icon"
 import { StickyAccordionHeader } from "./sticky-accordion-header"
+import { useDiffComponent } from "../context/diff"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
-import { For, Match, Show, Switch, ValidComponent, type JSX } from "solid-js"
+import { For, Match, Show, Switch, type JSX } from "solid-js"
 import { createStore } from "solid-js/store"
 import { type FileDiff } from "@opencode-ai/sdk"
 import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
@@ -18,10 +19,10 @@ export interface SessionReviewProps {
   classes?: { root?: string; header?: string; container?: string }
   actions?: JSX.Element
   diffs: (FileDiff & { preloaded?: PreloadMultiFileDiffResult<any> })[]
-  diffComponent: ValidComponent
 }
 
 export const SessionReview = (props: SessionReviewProps) => {
+  const diffComponent = useDiffComponent()
   const [store, setStore] = createStore({
     open: props.diffs.map((d) => d.file),
   })
@@ -98,7 +99,7 @@ export const SessionReview = (props: SessionReviewProps) => {
                 </StickyAccordionHeader>
                 <Accordion.Content data-slot="session-review-accordion-content">
                   <Dynamic
-                    component={props.diffComponent}
+                    component={diffComponent}
                     preloadedDiff={diff.preloaded}
                     diffStyle={props.split ? "split" : "unified"}
                     before={{

+ 7 - 29
packages/ui/src/components/session-turn.tsx

@@ -1,19 +1,9 @@
 import { AssistantMessage } from "@opencode-ai/sdk"
 import { useData } from "../context"
+import { useDiffComponent } from "../context/diff"
 import { Binary } from "@opencode-ai/util/binary"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
-import {
-  createEffect,
-  createMemo,
-  createSignal,
-  For,
-  Match,
-  onMount,
-  ParentProps,
-  Show,
-  Switch,
-  ValidComponent,
-} from "solid-js"
+import { createEffect, createMemo, createSignal, For, Match, onMount, ParentProps, Show, Switch } from "solid-js"
 import { DiffChanges } from "./diff-changes"
 import { Typewriter } from "./typewriter"
 import { Message } from "./message-part"
@@ -36,10 +26,10 @@ export function SessionTurn(
       content?: string
       container?: string
     }
-    diffComponent: ValidComponent
   }>,
 ) {
   const data = useData()
+  const diffComponent = useDiffComponent()
   const match = Binary.search(data.store.session, props.sessionID, (s) => s.id)
   if (!match.found) throw new Error(`Session ${props.sessionID} not found`)
 
@@ -129,7 +119,7 @@ export function SessionTurn(
                   </div>
                 </div>
                 <div data-slot="session-turn-message-content">
-                  <Message message={msg()} parts={parts()} sanitize={sanitizer()} diffComponent={props.diffComponent} />
+                  <Message message={msg()} parts={parts()} sanitize={sanitizer()} />
                 </div>
                 {/* Summary */}
                 <Show when={completed()}>
@@ -180,7 +170,7 @@ export function SessionTurn(
                             </StickyAccordionHeader>
                             <Accordion.Content data-slot="session-turn-accordion-content">
                               <Dynamic
-                                component={props.diffComponent}
+                                component={diffComponent}
                                 before={{
                                   name: diff.file!,
                                   contents: diff.before!,
@@ -206,11 +196,7 @@ export function SessionTurn(
                 <div data-slot="session-turn-response-section">
                   <Switch>
                     <Match when={!completed()}>
-                      <MessageProgress
-                        assistantMessages={assistantMessages}
-                        done={!messageWorking()}
-                        diffComponent={props.diffComponent}
-                      />
+                      <MessageProgress assistantMessages={assistantMessages} done={!messageWorking()} />
                     </Match>
                     <Match when={completed() && hasToolPart()}>
                       <Collapsible variant="ghost" open={detailsExpanded()} onOpenChange={setDetailsExpanded}>
@@ -241,18 +227,10 @@ export function SessionTurn(
                                       message={assistantMessage}
                                       parts={parts().filter((p) => p?.id !== last()?.id)}
                                       sanitize={sanitizer()}
-                                      diffComponent={props.diffComponent}
                                     />
                                   )
                                 }
-                                return (
-                                  <Message
-                                    message={assistantMessage}
-                                    parts={parts()}
-                                    sanitize={sanitizer()}
-                                    diffComponent={props.diffComponent}
-                                  />
-                                )
+                                return <Message message={assistantMessage} parts={parts()} sanitize={sanitizer()} />
                               }}
                             </For>
                             <Show when={error()}>

+ 13 - 0
packages/ui/src/context/diff.tsx

@@ -0,0 +1,13 @@
+import { createContext, useContext, type ParentProps, type ValidComponent } from "solid-js"
+
+const DiffComponentContext = createContext<ValidComponent>()
+
+export function DiffComponentProvider(props: ParentProps<{ component: ValidComponent }>) {
+  return <DiffComponentContext.Provider value={props.component}>{props.children}</DiffComponentContext.Provider>
+}
+
+export function useDiffComponent() {
+  const component = useContext(DiffComponentContext)
+  if (!component) throw new Error("DiffComponentProvider must be used to provide a diff component")
+  return component
+}

+ 1 - 0
packages/ui/src/context/index.ts

@@ -1,2 +1,3 @@
 export * from "./helper"
 export * from "./data"
+export * from "./diff"