summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-07 21:21:13 -0600
committerAdam <[email protected]>2025-12-07 21:21:18 -0600
commit9a90939ac410e3ebffb741994383d7a434069682 (patch)
treef3621d491af6eb97e0bf9039c7f82f0451ebd9d7
parent123a136093968376935e7166efd26b17a3f68b39 (diff)
downloadopencode-9a90939ac410e3ebffb741994383d7a434069682.tar.gz
opencode-9a90939ac410e3ebffb741994383d7a434069682.zip
fix: use diff context instead of prop drilling
-rw-r--r--packages/desktop/src/index.tsx64
-rw-r--r--packages/desktop/src/pages/session.tsx6
-rw-r--r--packages/enterprise/src/routes/share/[shareID].tsx415
-rw-r--r--packages/ui/src/components/message-part.tsx32
-rw-r--r--packages/ui/src/components/message-progress.tsx21
-rw-r--r--packages/ui/src/components/session-review.tsx7
-rw-r--r--packages/ui/src/components/session-turn.tsx36
-rw-r--r--packages/ui/src/context/diff.tsx13
-rw-r--r--packages/ui/src/context/index.ts1
9 files changed, 280 insertions, 315 deletions
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx
index 149b907bc..3c21b9a37 100644
--- a/packages/desktop/src/index.tsx
+++ b/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!,
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx
index 8cd9e9d65..3cc6d307c 100644
--- a/packages/desktop/src/pages/session.tsx
+++ b/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>
diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx
index 5f6a09486..c3392489d 100644
--- a/packages/enterprise/src/routes/share/[shareID].tsx
+++ b/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>
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index a0e6e91b6..2da4387fd 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/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,
diff --git a/packages/ui/src/components/message-progress.tsx b/packages/ui/src/components/message-progress.tsx
index afd7f754a..bbcdd3096 100644
--- a/packages/ui/src/components/message-progress.tsx
+++ b/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>
)
}}
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx
index ea5871b95..6dbaaec18 100644
--- a/packages/ui/src/components/session-review.tsx
+++ b/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={{
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index 963713a22..44ecc3a14 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/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()}>
diff --git a/packages/ui/src/context/diff.tsx b/packages/ui/src/context/diff.tsx
new file mode 100644
index 000000000..630437de6
--- /dev/null
+++ b/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
+}
diff --git a/packages/ui/src/context/index.ts b/packages/ui/src/context/index.ts
index fdff32bf2..3e0f5de74 100644
--- a/packages/ui/src/context/index.ts
+++ b/packages/ui/src/context/index.ts
@@ -1,2 +1,3 @@
export * from "./helper"
export * from "./data"
+export * from "./diff"