fix: use diff context instead of prop drilling

This commit is contained in:
Adam
2025-12-07 21:21:13 -06:00
parent 123a136093
commit 9a90939ac4
9 changed files with 283 additions and 318 deletions

View File

@@ -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,6 +37,7 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
render(
() => (
<MarkedProvider>
<DiffComponentProvider component={Diff}>
<GlobalSDKProvider url={url}>
<GlobalSyncProvider>
<LayoutProvider>
@@ -67,6 +70,7 @@ render(
</LayoutProvider>
</GlobalSyncProvider>
</GlobalSDKProvider>
</DiffComponentProvider>
</MarkedProvider>
),
root!,

View File

@@ -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>

View File

@@ -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,6 +158,7 @@ export default function () {
const info = createMemo(() => data().session[match().index])
return (
<DiffComponentProvider component={ClientOnlyDiff}>
<DataProvider data={data()} directory={info().directory}>
{iife(() => {
const [store, setStore] = createStore({
@@ -208,7 +210,10 @@ export default function () {
<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" />
<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">
@@ -234,7 +239,6 @@ export default function () {
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
container: "px-4",
}}
diffComponent={ClientOnlyDiff}
/>
)}
</For>
@@ -273,7 +277,9 @@ export default function () {
</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() }}>
<div
classList={{ "hidden w-full flex-1 min-h-0": true, "md:flex": wide(), "lg:flex": !wide() }}
>
<div
classList={{
"@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
@@ -307,7 +313,6 @@ export default function () {
"w-full pb-20 " +
(wide() ? "max-w-146 mx-auto px-6" : messages().length > 1 ? "pr-6 pl-18" : "px-6"),
}}
diffComponent={ClientOnlyDiff}
>
<div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
<Logo class="w-58.5 opacity-12" />
@@ -316,11 +321,11 @@ export default function () {
</div>
</div>
<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-6",
@@ -331,7 +336,6 @@ export default function () {
split
class="hidden @4xl:flex"
diffs={splitDiffs()}
diffComponent={Diff}
classes={{
root: "pb-20",
header: "px-6",
@@ -339,6 +343,7 @@ export default function () {
}}
/>
</div>
</DiffComponentProvider>
</Show>
</div>
<Switch>
@@ -361,15 +366,16 @@ export default function () {
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()}
diffComponent={Diff}
classes={{
root: "pb-20",
header: "px-4",
container: "px-4",
}}
/>
</DiffComponentProvider>
</div>
</Tabs.Content>
</Tabs>
@@ -385,6 +391,7 @@ export default function () {
)
})}
</DataProvider>
</DiffComponentProvider>
)
}}
</Show>

View File

@@ -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,

View File

@@ -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>
)
}}

View File

@@ -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={{

View File

@@ -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()}>

View File

@@ -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
}

View File

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