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( () => ( - - - - - - - { - const globalSync = useGlobalSync() - const slug = createMemo(() => base64Encode(globalSync.data.defaultProject!.worktree)) - return - }} - /> - - } /> + + + + + + + ( - - - - - - )} + path="/" + component={() => { + const globalSync = useGlobalSync() + const slug = createMemo(() => base64Encode(globalSync.data.defaultProject!.worktree)) + return + }} /> - - - - - - + + } /> + ( + + + + + + )} + /> + + + + + + + ), 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 ( -
+
@@ -438,7 +436,6 @@ export default function Page() { container: "px-6", }} diffs={session.diffs()} - diffComponent={Diff} actions={
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 ( - - {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) + + + {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 = () => ( -
-
-
- -
v{info().version}
-
-
- -
{model()?.name ?? modelID()}
-
-
- {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")} -
-
-
{info().title}
-
- ) - - const turns = () => ( -
-
{title()}
-
- - {(message) => ( - ( +
+
+
+ +
v{info().version}
+
+
+ - )} - -
-
- -
-
- ) - - const wide = createMemo(() => diffs().length === 0) - - return ( -
-
-
- - - +
{model()?.name ?? modelID()}
+
+
+ {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")} +
-
- - +
{info().title}
+
+ ) + + const turns = () => ( +
+
{title()}
+
+ + {(message) => ( + + )} +
- -
-
+
+ +
+
+ ) + + const wide = createMemo(() => diffs().length === 0) + + return ( +
+
+
+ + + +
+
+ + +
+
+
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()} -
-
- - 1 ? "pr-6 pl-18" : "px-6"), +
1, + "px-6": !wide() && messages().length === 1, }} - diffComponent={ClientOnlyDiff} > -
- -
- + {title()} +
+
+ + 1 ? "pr-6 pl-18" : "px-6"), + }} + > +
+ +
+
+
-
- 0}> -
- -
-
-
- - 0}> - - - - Session - - - 5 Files Changed - - - - {turns()} - -
-
- ) - })} - + ) + })} + + ) }} 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} /> )} @@ -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 ( - - {(part) => ( - - )} - + {(part) => } ) } @@ -98,13 +87,7 @@ export function Part(props: MessagePartProps) { const part = createMemo(() => sanitizePart(unwrap(props.part), props.sanitize)) return ( - + ) } @@ -113,7 +96,6 @@ export interface ToolProps { input: Record metadata: Record 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 (
AssistantMessageType[] - diffComponent: ValidComponent done?: boolean } @@ -172,12 +160,7 @@ export function MessageProgress(props: MessageProgressProps) { ) return (
- +
) }} 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 })[] - 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) => { , ) { 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(
- +
{/* Summary */} @@ -180,7 +170,7 @@ export function SessionTurn( - + @@ -241,18 +227,10 @@ export function SessionTurn( message={assistantMessage} parts={parts().filter((p) => p?.id !== last()?.id)} sanitize={sanitizer()} - diffComponent={props.diffComponent} /> ) } - return ( - - ) + return }}
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() + +export function DiffComponentProvider(props: ParentProps<{ component: ValidComponent }>) { + return {props.children} +} + +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"