summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/app/src/pages/session.tsx89
-rw-r--r--packages/ui/src/components/session-review.css6
-rw-r--r--packages/ui/src/components/session-review.tsx611
-rw-r--r--packages/ui/src/components/session-turn.tsx106
-rw-r--r--packages/ui/src/i18n/ar.ts1
-rw-r--r--packages/ui/src/i18n/br.ts1
-rw-r--r--packages/ui/src/i18n/da.ts1
-rw-r--r--packages/ui/src/i18n/de.ts1
-rw-r--r--packages/ui/src/i18n/en.ts1
-rw-r--r--packages/ui/src/i18n/es.ts1
-rw-r--r--packages/ui/src/i18n/fr.ts1
-rw-r--r--packages/ui/src/i18n/ja.ts1
-rw-r--r--packages/ui/src/i18n/ko.ts1
-rw-r--r--packages/ui/src/i18n/no.ts1
-rw-r--r--packages/ui/src/i18n/pl.ts1
-rw-r--r--packages/ui/src/i18n/ru.ts1
-rw-r--r--packages/ui/src/i18n/th.ts1
-rw-r--r--packages/ui/src/i18n/zh.ts1
-rw-r--r--packages/ui/src/i18n/zht.ts1
19 files changed, 415 insertions, 412 deletions
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 328e66a83..7f005c56e 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -28,6 +28,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
import { InlineInput } from "@opencode-ai/ui/inline-input"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Tabs } from "@opencode-ai/ui/tabs"
+import { Select } from "@opencode-ai/ui/select"
import { useCodeComponent } from "@opencode-ai/ui/context/code"
import { LineComment as LineCommentView, LineCommentEditor } from "@opencode-ai/ui/line-comment"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
@@ -54,7 +55,7 @@ import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import { useNavigate, useParams } from "@solidjs/router"
import { UserMessage } from "@opencode-ai/sdk/v2"
-import type { FileDiff } from "@opencode-ai/sdk/v2/client"
+import type { FileDiff } from "@opencode-ai/sdk/v2"
import { useSDK } from "@/context/sdk"
import { usePrompt } from "@/context/prompt"
import { useComments, type LineComment } from "@/context/comments"
@@ -104,6 +105,8 @@ const setSessionHandoff = (key: string, patch: Partial<HandoffSession>) => {
}
interface SessionReviewTabProps {
+ title?: JSX.Element
+ empty?: JSX.Element
diffs: () => FileDiff[]
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
diffStyle: DiffStyle
@@ -220,6 +223,8 @@ function SessionReviewTab(props: SessionReviewTabProps) {
return (
<SessionReview
+ title={props.title}
+ empty={props.empty}
scrollRef={(el) => {
scroll = el
props.onScrollRef?.(el)
@@ -709,10 +714,14 @@ export default function Page() {
messageId: undefined as string | undefined,
turnStart: 0,
mobileTab: "session" as "session" | "changes",
+ changes: "session" as "session" | "turn",
newSessionWorktree: "main",
promptHeight: 0,
})
+ const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
+ const reviewDiffs = createMemo(() => (store.changes === "session" ? diffs() : turnDiffs()))
+
const renderedUserMessages = createMemo(
() => {
const msgs = visibleUserMessages()
@@ -894,6 +903,7 @@ export default function Page() {
() => {
setStore("messageId", undefined)
setStore("expanded", {})
+ setStore("changes", "session")
setUi("autoCreated", false)
},
{ defer: true },
@@ -1428,17 +1438,64 @@ export default function Page() {
setFileTreeTab("all")
}
+ const changesOptions = ["session", "turn"] as const
+ const changesOptionsList = [...changesOptions]
+
+ const changesTitle = () => (
+ <Select
+ options={changesOptionsList}
+ current={store.changes}
+ label={(option) =>
+ option === "session" ? language.t("ui.sessionReview.title") : language.t("ui.sessionReview.title.lastTurn")
+ }
+ onSelect={(option) => option && setStore("changes", option)}
+ variant="ghost"
+ size="large"
+ triggerStyle={{ "font-size": "var(--font-size-large)" }}
+ />
+ )
+
+ const emptyTurn = () => (
+ <div class="h-full pb-30 flex flex-col items-center justify-center text-center gap-6">
+ <Mark class="w-14 opacity-10" />
+ <div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.noChanges")}</div>
+ </div>
+ )
+
const reviewPanel = () => (
<div class="flex flex-col h-full overflow-hidden bg-background-stronger contain-strict">
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<Switch>
+ <Match when={store.changes === "turn" && !!params.id}>
+ <SessionReviewTab
+ title={changesTitle()}
+ empty={emptyTurn()}
+ diffs={reviewDiffs}
+ view={view}
+ diffStyle={layout.review.diffStyle()}
+ onDiffStyleChange={layout.review.setDiffStyle}
+ onScrollRef={(el) => setTree("reviewScroll", el)}
+ focusedFile={tree.activeDiff}
+ onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
+ comments={comments.all()}
+ focusedComment={comments.focus()}
+ onFocusedCommentChange={comments.setFocus}
+ onViewFile={(path) => {
+ showAllFiles()
+ const value = file.tab(path)
+ tabs().open(value)
+ file.load(path)
+ }}
+ />
+ </Match>
<Match when={hasReview()}>
<Show
when={diffsReady()}
fallback={<div class="px-6 py-4 text-text-weak">{language.t("session.review.loadingChanges")}</div>}
>
<SessionReviewTab
- diffs={diffs}
+ title={changesTitle()}
+ diffs={reviewDiffs}
view={view}
diffStyle={layout.review.diffStyle()}
onDiffStyleChange={layout.review.setDiffStyle}
@@ -2138,6 +2195,31 @@ export default function Page() {
fallback={
<div class="relative h-full overflow-hidden">
<Switch>
+ <Match when={store.changes === "turn" && !!params.id}>
+ <SessionReviewTab
+ title={changesTitle()}
+ empty={emptyTurn()}
+ diffs={reviewDiffs}
+ view={view}
+ diffStyle="unified"
+ focusedFile={tree.activeDiff}
+ onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
+ comments={comments.all()}
+ focusedComment={comments.focus()}
+ onFocusedCommentChange={comments.setFocus}
+ onViewFile={(path) => {
+ showAllFiles()
+ const value = file.tab(path)
+ tabs().open(value)
+ file.load(path)
+ }}
+ classes={{
+ root: "pb-[calc(var(--prompt-height,8rem)+32px)]",
+ header: "px-4",
+ container: "px-4",
+ }}
+ />
+ </Match>
<Match when={hasReview()}>
<Show
when={diffsReady()}
@@ -2148,7 +2230,8 @@ export default function Page() {
}
>
<SessionReviewTab
- diffs={diffs}
+ title={changesTitle()}
+ diffs={reviewDiffs}
view={view}
diffStyle="unified"
focusedFile={tree.activeDiff}
diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css
index 363343f91..df6df4649 100644
--- a/packages/ui/src/components/session-review.css
+++ b/packages/ui/src/components/session-review.css
@@ -10,9 +10,9 @@
display: none;
}
- /* [data-slot="session-review-container"] { */
- /* height: 100%; */
- /* } */
+ [data-slot="session-review-container"] {
+ flex: 1 1 auto;
+ }
[data-slot="session-review-header"] {
position: sticky;
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx
index 84ec934e2..70d9fe802 100644
--- a/packages/ui/src/components/session-review.tsx
+++ b/packages/ui/src/components/session-review.tsx
@@ -36,6 +36,8 @@ export type SessionReviewLineComment = {
export type SessionReviewFocus = { file: string; id: string }
export interface SessionReviewProps {
+ title?: JSX.Element
+ empty?: JSX.Element
split?: boolean
diffStyle?: SessionReviewDiffStyle
onDiffStyleChange?: (diffStyle: SessionReviewDiffStyle) => void
@@ -184,6 +186,7 @@ export const SessionReview = (props: SessionReviewProps) => {
const open = () => props.open ?? store.open
const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified")
+ const hasDiffs = () => props.diffs.length > 0
const handleChange = (open: string[]) => {
props.onOpenChange?.(open)
@@ -287,9 +290,9 @@ export const SessionReview = (props: SessionReviewProps) => {
[props.classes?.header ?? ""]: !!props.classes?.header,
}}
>
- <div data-slot="session-review-title">{i18n.t("ui.sessionReview.title")}</div>
+ <div data-slot="session-review-title">{props.title ?? i18n.t("ui.sessionReview.title")}</div>
<div data-slot="session-review-actions">
- <Show when={props.onDiffStyleChange}>
+ <Show when={hasDiffs() && props.onDiffStyleChange}>
<RadioGroup
options={["unified", "split"] as const}
current={diffStyle()}
@@ -300,12 +303,14 @@ export const SessionReview = (props: SessionReviewProps) => {
onSelect={(style) => style && props.onDiffStyleChange?.(style)}
/>
</Show>
- <Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}>
- <Switch>
- <Match when={open().length > 0}>{i18n.t("ui.sessionReview.collapseAll")}</Match>
- <Match when={true}>{i18n.t("ui.sessionReview.expandAll")}</Match>
- </Switch>
- </Button>
+ <Show when={hasDiffs()}>
+ <Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}>
+ <Switch>
+ <Match when={open().length > 0}>{i18n.t("ui.sessionReview.collapseAll")}</Match>
+ <Match when={true}>{i18n.t("ui.sessionReview.expandAll")}</Match>
+ </Switch>
+ </Button>
+ </Show>
{props.actions}
</div>
</div>
@@ -315,322 +320,324 @@ export const SessionReview = (props: SessionReviewProps) => {
[props.classes?.container ?? ""]: !!props.classes?.container,
}}
>
- <Accordion multiple value={open()} onChange={handleChange}>
- <For each={props.diffs}>
- {(diff) => {
- let wrapper: HTMLDivElement | undefined
-
- const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
- const commentedLines = createMemo(() => comments().map((c) => c.selection))
-
- const beforeText = () => (typeof diff.before === "string" ? diff.before : "")
- const afterText = () => (typeof diff.after === "string" ? diff.after : "")
-
- const isAdded = () => beforeText().length === 0 && afterText().length > 0
- const isDeleted = () => afterText().length === 0 && beforeText().length > 0
- const isImage = () => isImageFile(diff.file)
- const isAudio = () => isAudioFile(diff.file)
-
- const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
- const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc)
- const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle")
-
- const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
- const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc)
- const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle")
- const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined)
-
- const selectedLines = createMemo(() => {
- const current = selection()
- if (!current || current.file !== diff.file) return null
- return current.range
- })
-
- const draftRange = createMemo(() => {
- const current = commenting()
- if (!current || current.file !== diff.file) return null
- return current.range
- })
-
- const [draft, setDraft] = createSignal("")
- const [positions, setPositions] = createSignal<Record<string, number>>({})
- const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
-
- const getRoot = () => {
- const el = wrapper
- if (!el) return
-
- const host = el.querySelector("diffs-container")
- if (!(host instanceof HTMLElement)) return
- return host.shadowRoot ?? undefined
- }
-
- const updateAnchors = () => {
- const el = wrapper
- if (!el) return
-
- const root = getRoot()
- if (!root) return
-
- const next: Record<string, number> = {}
- for (const item of comments()) {
- const marker = findMarker(root, item.selection)
- if (!marker) continue
- next[item.id] = markerTop(el, marker)
+ <Show when={hasDiffs()} fallback={props.empty}>
+ <Accordion multiple value={open()} onChange={handleChange}>
+ <For each={props.diffs}>
+ {(diff) => {
+ let wrapper: HTMLDivElement | undefined
+
+ const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
+ const commentedLines = createMemo(() => comments().map((c) => c.selection))
+
+ const beforeText = () => (typeof diff.before === "string" ? diff.before : "")
+ const afterText = () => (typeof diff.after === "string" ? diff.after : "")
+
+ const isAdded = () => beforeText().length === 0 && afterText().length > 0
+ const isDeleted = () => afterText().length === 0 && beforeText().length > 0
+ const isImage = () => isImageFile(diff.file)
+ const isAudio = () => isAudioFile(diff.file)
+
+ const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
+ const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc)
+ const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle")
+
+ const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
+ const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc)
+ const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle")
+ const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined)
+
+ const selectedLines = createMemo(() => {
+ const current = selection()
+ if (!current || current.file !== diff.file) return null
+ return current.range
+ })
+
+ const draftRange = createMemo(() => {
+ const current = commenting()
+ if (!current || current.file !== diff.file) return null
+ return current.range
+ })
+
+ const [draft, setDraft] = createSignal("")
+ const [positions, setPositions] = createSignal<Record<string, number>>({})
+ const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
+
+ const getRoot = () => {
+ const el = wrapper
+ if (!el) return
+
+ const host = el.querySelector("diffs-container")
+ if (!(host instanceof HTMLElement)) return
+ return host.shadowRoot ?? undefined
}
- setPositions(next)
- const range = draftRange()
- if (!range) {
- setDraftTop(undefined)
- return
+ const updateAnchors = () => {
+ const el = wrapper
+ if (!el) return
+
+ const root = getRoot()
+ if (!root) return
+
+ const next: Record<string, number> = {}
+ for (const item of comments()) {
+ const marker = findMarker(root, item.selection)
+ if (!marker) continue
+ next[item.id] = markerTop(el, marker)
+ }
+ setPositions(next)
+
+ const range = draftRange()
+ if (!range) {
+ setDraftTop(undefined)
+ return
+ }
+
+ const marker = findMarker(root, range)
+ if (!marker) {
+ setDraftTop(undefined)
+ return
+ }
+
+ setDraftTop(markerTop(el, marker))
}
- const marker = findMarker(root, range)
- if (!marker) {
- setDraftTop(undefined)
- return
+ const scheduleAnchors = () => {
+ requestAnimationFrame(updateAnchors)
}
- setDraftTop(markerTop(el, marker))
- }
-
- const scheduleAnchors = () => {
- requestAnimationFrame(updateAnchors)
- }
-
- createEffect(() => {
- comments()
- scheduleAnchors()
- })
-
- createEffect(() => {
- const range = draftRange()
- if (!range) return
- setDraft("")
- scheduleAnchors()
- })
-
- createEffect(() => {
- if (!open().includes(diff.file)) return
- if (!isImage()) return
- if (imageSrc()) return
- if (imageStatus() !== "idle") return
-
- const reader = props.readFile
- if (!reader) return
-
- setImageStatus("loading")
- reader(diff.file)
- .then((result) => {
- const src = dataUrl(result)
- if (!src) {
+ createEffect(() => {
+ comments()
+ scheduleAnchors()
+ })
+
+ createEffect(() => {
+ const range = draftRange()
+ if (!range) return
+ setDraft("")
+ scheduleAnchors()
+ })
+
+ createEffect(() => {
+ if (!open().includes(diff.file)) return
+ if (!isImage()) return
+ if (imageSrc()) return
+ if (imageStatus() !== "idle") return
+
+ const reader = props.readFile
+ if (!reader) return
+
+ setImageStatus("loading")
+ reader(diff.file)
+ .then((result) => {
+ const src = dataUrl(result)
+ if (!src) {
+ setImageStatus("error")
+ return
+ }
+ setImageSrc(src)
+ setImageStatus("idle")
+ })
+ .catch(() => {
setImageStatus("error")
- return
- }
- setImageSrc(src)
- setImageStatus("idle")
- })
- .catch(() => {
- setImageStatus("error")
- })
- })
-
- createEffect(() => {
- if (!open().includes(diff.file)) return
- if (!isAudio()) return
- if (audioSrc()) return
- if (audioStatus() !== "idle") return
-
- const reader = props.readFile
- if (!reader) return
-
- setAudioStatus("loading")
- reader(diff.file)
- .then((result) => {
- const src = dataUrl(result)
- if (!src) {
+ })
+ })
+
+ createEffect(() => {
+ if (!open().includes(diff.file)) return
+ if (!isAudio()) return
+ if (audioSrc()) return
+ if (audioStatus() !== "idle") return
+
+ const reader = props.readFile
+ if (!reader) return
+
+ setAudioStatus("loading")
+ reader(diff.file)
+ .then((result) => {
+ const src = dataUrl(result)
+ if (!src) {
+ setAudioStatus("error")
+ return
+ }
+ setAudioMime(normalizeMimeType(result?.mimeType))
+ setAudioSrc(src)
+ setAudioStatus("idle")
+ })
+ .catch(() => {
setAudioStatus("error")
- return
- }
- setAudioMime(normalizeMimeType(result?.mimeType))
- setAudioSrc(src)
- setAudioStatus("idle")
- })
- .catch(() => {
- setAudioStatus("error")
- })
- })
-
- const handleLineSelected = (range: SelectedLineRange | null) => {
- if (!props.onLineComment) return
-
- if (!range) {
- setSelection(null)
- return
- }
+ })
+ })
- setSelection({ file: diff.file, range })
- }
+ const handleLineSelected = (range: SelectedLineRange | null) => {
+ if (!props.onLineComment) return
- const handleLineSelectionEnd = (range: SelectedLineRange | null) => {
- if (!props.onLineComment) return
+ if (!range) {
+ setSelection(null)
+ return
+ }
- if (!range) {
- setCommenting(null)
- return
+ setSelection({ file: diff.file, range })
}
- setSelection({ file: diff.file, range })
- setCommenting({ file: diff.file, range })
- }
+ const handleLineSelectionEnd = (range: SelectedLineRange | null) => {
+ if (!props.onLineComment) return
- const openComment = (comment: SessionReviewComment) => {
- setOpened({ file: comment.file, id: comment.id })
- setSelection({ file: comment.file, range: comment.selection })
- }
+ if (!range) {
+ setCommenting(null)
+ return
+ }
- const isCommentOpen = (comment: SessionReviewComment) => {
- const current = opened()
- if (!current) return false
- return current.file === comment.file && current.id === comment.id
- }
+ setSelection({ file: diff.file, range })
+ setCommenting({ file: diff.file, range })
+ }
- return (
- <Accordion.Item
- value={diff.file}
- id={diffId(diff.file)}
- data-file={diff.file}
- data-slot="session-review-accordion-item"
- data-selected={props.focusedFile === diff.file ? "" : undefined}
- >
- <StickyAccordionHeader>
- <Accordion.Trigger>
- <div data-slot="session-review-trigger-content">
- <div data-slot="session-review-file-info">
- <FileIcon node={{ path: diff.file, type: "file" }} />
- <div data-slot="session-review-file-name-container">
- <Show when={diff.file.includes("/")}>
- <span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span>
- </Show>
- <span data-slot="session-review-filename">{getFilename(diff.file)}</span>
- <Show when={props.onViewFile}>
- <button
- data-slot="session-review-view-button"
- type="button"
- onClick={(e) => {
- e.stopPropagation()
- props.onViewFile?.(diff.file)
- }}
- >
- <Icon name="eye" size="small" />
- </button>
- </Show>
+ const openComment = (comment: SessionReviewComment) => {
+ setOpened({ file: comment.file, id: comment.id })
+ setSelection({ file: comment.file, range: comment.selection })
+ }
+
+ const isCommentOpen = (comment: SessionReviewComment) => {
+ const current = opened()
+ if (!current) return false
+ return current.file === comment.file && current.id === comment.id
+ }
+
+ return (
+ <Accordion.Item
+ value={diff.file}
+ id={diffId(diff.file)}
+ data-file={diff.file}
+ data-slot="session-review-accordion-item"
+ data-selected={props.focusedFile === diff.file ? "" : undefined}
+ >
+ <StickyAccordionHeader>
+ <Accordion.Trigger>
+ <div data-slot="session-review-trigger-content">
+ <div data-slot="session-review-file-info">
+ <FileIcon node={{ path: diff.file, type: "file" }} />
+ <div data-slot="session-review-file-name-container">
+ <Show when={diff.file.includes("/")}>
+ <span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span>
+ </Show>
+ <span data-slot="session-review-filename">{getFilename(diff.file)}</span>
+ <Show when={props.onViewFile}>
+ <button
+ data-slot="session-review-view-button"
+ type="button"
+ onClick={(e) => {
+ e.stopPropagation()
+ props.onViewFile?.(diff.file)
+ }}
+ >
+ <Icon name="eye" size="small" />
+ </button>
+ </Show>
+ </div>
+ </div>
+ <div data-slot="session-review-trigger-actions">
+ <Switch>
+ <Match when={isAdded()}>
+ <span data-slot="session-review-change" data-type="added">
+ {i18n.t("ui.sessionReview.change.added")}
+ </span>
+ </Match>
+ <Match when={isDeleted()}>
+ <span data-slot="session-review-change" data-type="removed">
+ {i18n.t("ui.sessionReview.change.removed")}
+ </span>
+ </Match>
+ <Match when={true}>
+ <DiffChanges changes={diff} />
+ </Match>
+ </Switch>
+ <Icon name="chevron-grabber-vertical" size="small" />
</div>
</div>
- <div data-slot="session-review-trigger-actions">
- <Switch>
- <Match when={isAdded()}>
- <span data-slot="session-review-change" data-type="added">
- {i18n.t("ui.sessionReview.change.added")}
- </span>
- </Match>
- <Match when={isDeleted()}>
- <span data-slot="session-review-change" data-type="removed">
- {i18n.t("ui.sessionReview.change.removed")}
- </span>
- </Match>
- <Match when={true}>
- <DiffChanges changes={diff} />
- </Match>
- </Switch>
- <Icon name="chevron-grabber-vertical" size="small" />
- </div>
- </div>
- </Accordion.Trigger>
- </StickyAccordionHeader>
- <Accordion.Content data-slot="session-review-accordion-content">
- <div
- data-slot="session-review-diff-wrapper"
- ref={(el) => {
- wrapper = el
- anchors.set(diff.file, el)
- scheduleAnchors()
- }}
- >
- <Dynamic
- component={diffComponent}
- preloadedDiff={diff.preloaded}
- diffStyle={diffStyle()}
- onRendered={() => {
- props.onDiffRendered?.()
+ </Accordion.Trigger>
+ </StickyAccordionHeader>
+ <Accordion.Content data-slot="session-review-accordion-content">
+ <div
+ data-slot="session-review-diff-wrapper"
+ ref={(el) => {
+ wrapper = el
+ anchors.set(diff.file, el)
scheduleAnchors()
}}
- enableLineSelection={props.onLineComment != null}
- onLineSelected={handleLineSelected}
- onLineSelectionEnd={handleLineSelectionEnd}
- selectedLines={selectedLines()}
- commentedLines={commentedLines()}
- before={{
- name: diff.file!,
- contents: typeof diff.before === "string" ? diff.before : "",
- }}
- after={{
- name: diff.file!,
- contents: typeof diff.after === "string" ? diff.after : "",
- }}
- />
-
- <For each={comments()}>
- {(comment) => (
- <LineComment
- id={comment.id}
- top={positions()[comment.id]}
- onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
- onClick={() => {
- if (isCommentOpen(comment)) {
- setOpened(null)
- return
- }
-
- openComment(comment)
- }}
- open={isCommentOpen(comment)}
- comment={comment.comment}
- selection={selectionLabel(comment.selection)}
- />
- )}
- </For>
-
- <Show when={draftRange()}>
- {(range) => (
- <Show when={draftTop() !== undefined}>
- <LineCommentEditor
- top={draftTop()}
- value={draft()}
- selection={selectionLabel(range())}
- onInput={setDraft}
- onCancel={() => setCommenting(null)}
- onSubmit={(comment) => {
- props.onLineComment?.({
- file: diff.file,
- selection: range(),
- comment,
- preview: selectionPreview(diff, range()),
- })
- setCommenting(null)
+ >
+ <Dynamic
+ component={diffComponent}
+ preloadedDiff={diff.preloaded}
+ diffStyle={diffStyle()}
+ onRendered={() => {
+ props.onDiffRendered?.()
+ scheduleAnchors()
+ }}
+ enableLineSelection={props.onLineComment != null}
+ onLineSelected={handleLineSelected}
+ onLineSelectionEnd={handleLineSelectionEnd}
+ selectedLines={selectedLines()}
+ commentedLines={commentedLines()}
+ before={{
+ name: diff.file!,
+ contents: typeof diff.before === "string" ? diff.before : "",
+ }}
+ after={{
+ name: diff.file!,
+ contents: typeof diff.after === "string" ? diff.after : "",
+ }}
+ />
+
+ <For each={comments()}>
+ {(comment) => (
+ <LineComment
+ id={comment.id}
+ top={positions()[comment.id]}
+ onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
+ onClick={() => {
+ if (isCommentOpen(comment)) {
+ setOpened(null)
+ return
+ }
+
+ openComment(comment)
}}
+ open={isCommentOpen(comment)}
+ comment={comment.comment}
+ selection={selectionLabel(comment.selection)}
/>
- </Show>
- )}
- </Show>
- </div>
- </Accordion.Content>
- </Accordion.Item>
- )
- }}
- </For>
- </Accordion>
+ )}
+ </For>
+
+ <Show when={draftRange()}>
+ {(range) => (
+ <Show when={draftTop() !== undefined}>
+ <LineCommentEditor
+ top={draftTop()}
+ value={draft()}
+ selection={selectionLabel(range())}
+ onInput={setDraft}
+ onCancel={() => setCommenting(null)}
+ onSubmit={(comment) => {
+ props.onLineComment?.({
+ file: diff.file,
+ selection: range(),
+ comment,
+ preview: selectionPreview(diff, range()),
+ })
+ setCommenting(null)
+ }}
+ />
+ </Show>
+ )}
+ </Show>
+ </div>
+ </Accordion.Content>
+ </Accordion.Item>
+ )
+ }}
+ </For>
+ </Accordion>
+ </Show>
</div>
</div>
)
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index d878bd245..7c5694ba5 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/packages/ui/src/components/session-turn.tsx
@@ -8,25 +8,16 @@ import {
TextPart,
ToolPart,
} from "@opencode-ai/sdk/v2/client"
-import { type FileDiff } from "@opencode-ai/sdk/v2"
import { useData } from "../context"
-import { useDiffComponent } from "../context/diff"
import { type UiI18nKey, type UiI18nParams, useI18n } from "../context/i18n"
import { findLast } from "@opencode-ai/util/array"
-import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { Binary } from "@opencode-ai/util/binary"
import { createEffect, createMemo, createSignal, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js"
-import { DiffChanges } from "./diff-changes"
import { Message, Part } from "./message-part"
import { Markdown } from "./markdown"
-import { Accordion } from "./accordion"
-import { StickyAccordionHeader } from "./sticky-accordion-header"
-import { FileIcon } from "./file-icon"
-import { Icon } from "./icon"
import { IconButton } from "./icon-button"
import { Card } from "./card"
-import { Dynamic } from "solid-js/web"
import { Button } from "./button"
import { Spinner } from "./spinner"
import { Tooltip } from "./tooltip"
@@ -143,7 +134,6 @@ export function SessionTurn(
) {
const i18n = useI18n()
const data = useData()
- const diffComponent = useDiffComponent()
const emptyMessages: MessageType[] = []
const emptyParts: PartType[] = []
@@ -153,7 +143,6 @@ export function SessionTurn(
const emptyPermissionParts: { part: ToolPart; message: AssistantMessage }[] = []
const emptyQuestions: QuestionRequest[] = []
const emptyQuestionParts: { part: ToolPart; message: AssistantMessage }[] = []
- const emptyDiffs: FileDiff[] = []
const idle = { type: "idle" as const }
const allMessages = createMemo(() => data.store.message[props.sessionID] ?? emptyMessages)
@@ -409,8 +398,7 @@ export function SessionTurn(
const response = createMemo(() => lastTextPart()?.text)
const responsePartId = createMemo(() => lastTextPart()?.id)
- const messageDiffs = createMemo(() => message()?.summary?.diffs ?? emptyDiffs)
- const hasDiffs = createMemo(() => messageDiffs().length > 0)
+ const hasDiffs = createMemo(() => (message()?.summary?.diffs?.length ?? 0) > 0)
const hideResponsePart = createMemo(() => !working() && !!responsePartId())
const [copied, setCopied] = createSignal(false)
@@ -476,28 +464,12 @@ export function SessionTurn(
updateStickyHeight(sticky.getBoundingClientRect().height)
})
- const diffInit = 20
- const diffBatch = 20
-
const [store, setStore] = createStore({
retrySeconds: 0,
- diffsOpen: [] as string[],
- diffLimit: diffInit,
status: rawStatus(),
duration: duration(),
})
- createEffect(
- on(
- () => message()?.id,
- () => {
- setStore("diffsOpen", [])
- setStore("diffLimit", diffInit)
- },
- { defer: true },
- ),
- )
-
createEffect(() => {
const r = retry()
if (!r) {
@@ -727,7 +699,7 @@ export function SessionTurn(
<div class="sr-only" aria-live="polite">
{!working() && response() ? response() : ""}
</div>
- <Show when={!working() && (response() || hasDiffs())}>
+ <Show when={!working() && response()}>
<div data-slot="session-turn-summary-section">
<div data-slot="session-turn-summary-header">
<h2 data-slot="session-turn-summary-title">{i18n.t("ui.sessionTurn.summary.response")}</h2>
@@ -760,80 +732,6 @@ export function SessionTurn(
</Show>
</div>
</div>
- <Accordion
- data-slot="session-turn-accordion"
- multiple
- value={store.diffsOpen}
- onChange={(value) => {
- if (!Array.isArray(value)) return
- setStore("diffsOpen", value)
- }}
- >
- <For each={messageDiffs().slice(0, store.diffLimit)}>
- {(diff) => (
- <Accordion.Item value={diff.file}>
- <StickyAccordionHeader>
- <Accordion.Trigger>
- <div data-slot="session-turn-accordion-trigger-content">
- <div data-slot="session-turn-file-info">
- <FileIcon
- node={{ path: diff.file, type: "file" }}
- data-slot="session-turn-file-icon"
- />
- <div data-slot="session-turn-file-path">
- <Show when={diff.file.includes("/")}>
- <span data-slot="session-turn-directory">
- {`\u202A${getDirectory(diff.file)}\u202C`}
- </span>
- </Show>
- <span data-slot="session-turn-filename">{getFilename(diff.file)}</span>
- </div>
- </div>
- <div data-slot="session-turn-accordion-actions">
- <DiffChanges changes={diff} />
- <Icon name="chevron-grabber-vertical" size="small" />
- </div>
- </div>
- </Accordion.Trigger>
- </StickyAccordionHeader>
- <Accordion.Content data-slot="session-turn-accordion-content">
- <Show when={store.diffsOpen.includes(diff.file!)}>
- <Dynamic
- component={diffComponent}
- before={{
- name: diff.file!,
- contents: diff.before!,
- }}
- after={{
- name: diff.file!,
- contents: diff.after!,
- }}
- />
- </Show>
- </Accordion.Content>
- </Accordion.Item>
- )}
- </For>
- </Accordion>
- <Show when={messageDiffs().length > store.diffLimit}>
- <Button
- data-slot="session-turn-accordion-more"
- variant="ghost"
- size="small"
- onClick={() => {
- const total = messageDiffs().length
- setStore("diffLimit", (limit) => {
- const next = limit + diffBatch
- if (next > total) return total
- return next
- })
- }}
- >
- {i18n.t("ui.sessionTurn.diff.showMore", {
- count: messageDiffs().length - store.diffLimit,
- })}
- </Button>
- </Show>
</div>
</Show>
<Show when={error() && !props.stepsExpanded}>
diff --git a/packages/ui/src/i18n/ar.ts b/packages/ui/src/i18n/ar.ts
index fc99bdacb..d1abf5de3 100644
--- a/packages/ui/src/i18n/ar.ts
+++ b/packages/ui/src/i18n/ar.ts
@@ -1,5 +1,6 @@
export const dict = {
"ui.sessionReview.title": "تغييرات الجلسة",
+ "ui.sessionReview.title.lastTurn": "تغييرات آخر دور",
"ui.sessionReview.diffStyle.unified": "موجد",
"ui.sessionReview.diffStyle.split": "منقسم",
"ui.sessionReview.expandAll": "توسيع الكل",
diff --git a/packages/ui/src/i18n/br.ts b/packages/ui/src/i18n/br.ts
index fdec9138a..36bef2650 100644
--- a/packages/ui/src/i18n/br.ts
+++ b/packages/ui/src/i18n/br.ts
@@ -1,5 +1,6 @@
export const dict = {
"ui.sessionReview.title": "Alterações da sessão",
+ "ui.sessionReview.title.lastTurn": "Alterações do último turno",
"ui.sessionReview.diffStyle.unified": "Unificado",
"ui.sessionReview.diffStyle.split": "Dividido",
"ui.sessionReview.expandAll": "Expandir tudo",
diff --git a/packages/ui/src/i18n/da.ts b/packages/ui/src/i18n/da.ts
index fbe34e03f..0142d161f 100644
--- a/packages/ui/src/i18n/da.ts
+++ b/packages/ui/src/i18n/da.ts
@@ -1,5 +1,6 @@
export const dict = {
"ui.sessionReview.title": "Sessionsændringer",
+ "ui.sessionReview.title.lastTurn": "Ændringer fra sidste tur",
"ui.sessionReview.diffStyle.unified": "Samlet",
"ui.sessionReview.diffStyle.split": "Opdelt",
"ui.sessionReview.expandAll": "Udvid alle",
diff --git a/packages/ui/src/i18n/de.ts b/packages/ui/src/i18n/de.ts
index d74cd5d22..e004233f6 100644
--- a/packages/ui/src/i18n/de.ts
+++ b/packages/ui/src/i18n/de.ts
@@ -4,6 +4,7 @@ type Keys = keyof typeof en
export const dict = {
"ui.sessionReview.title": "Sitzungsänderungen",
+ "ui.sessionReview.title.lastTurn": "Änderungen der letzten Runde",
"ui.sessionReview.diffStyle.unified": "Vereinheitlicht",
"ui.sessionReview.diffStyle.split": "Geteilt",
"ui.sessionReview.expandAll": "Alle erweitern",
diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts
index 8c0f09cb3..a92a498c9 100644
--- a/packages/ui/src/i18n/en.ts
+++ b/packages/ui/src/i18n/en.ts
@@ -1,5 +1,6 @@
export const dict = {
"ui.sessionReview.title": "Session changes",
+ "ui.sessionReview.title.lastTurn": "Last turn changes",
"ui.sessionReview.diffStyle.unified": "Unified",
"ui.sessionReview.diffStyle.split": "Split",
"ui.sessionReview.expandAll": "Expand all",
diff --git a/packages/ui/src/i18n/es.ts b/packages/ui/src/i18n/es.ts
index 3f71a98ac..283548bb3 100644
--- a/packages/ui/src/i18n/es.ts
+++ b/packages/ui/src/i18n/es.ts
@@ -1,5 +1,6 @@
export const dict = {
"ui.sessionReview.title": "Cambios de la sesión",
+ "ui.sessionReview.title.lastTurn": "Cambios del último turno",
"ui.sessionReview.diffStyle.unified": "Unificado",
"ui.sessionReview.diffStyle.split": "Dividido",
"ui.sessionReview.expandAll": "Expandir todo",
diff --git a/packages/ui/src/i18n/fr.ts b/packages/ui/src/i18n/fr.ts
index 0ec70509a..e9ab8bf8a 100644
--- a/packages/ui/src/i18n/fr.ts
+++ b/packages/ui/src/i18n/fr.ts
@@ -1,5 +1,6 @@
export const dict = {
"ui.sessionReview.title": "Modifications de la session",
+ "ui.sessionReview.title.lastTurn": "Modifications du dernier tour",
"ui.sessionReview.diffStyle.unified": "Unifié",
"ui.sessionReview.diffStyle.split": "Divisé",
"ui.sessionReview.expandAll": "Tout développer",
diff --git a/packages/ui/src/i18n/ja.ts b/packages/ui/src/i18n/ja.ts
index fd3f24ab3..4da2578d3 100644
--- a/packages/ui/src/i18n/ja.ts
+++ b/packages/ui/src/i18n/ja.ts
@@ -1,5 +1,6 @@
export const dict = {
"ui.sessionReview.title": "セッションの変更",
+ "ui.sessionReview.title.lastTurn": "前回ターンの変更",
"ui.sessionReview.diffStyle.unified": "Unified",
"ui.sessionReview.diffStyle.split": "Split",
"ui.sessionReview.expandAll": "すべて展開",
diff --git a/packages/ui/src/i18n/ko.ts b/packages/ui/src/i18n/ko.ts
index e419f730a..9fb120b5e 100644
--- a/packages/ui/src/i18n/ko.ts
+++ b/packages/ui/src/i18n/ko.ts
@@ -1,5 +1,6 @@
export const dict = {
"ui.sessionReview.title": "세션 변경 사항",
+ "ui.sessionReview.title.lastTurn": "마지막 턴 변경 사항",
"ui.sessionReview.diffStyle.unified": "통합 보기",
"ui.sessionReview.diffStyle.split": "분할 보기",
"ui.sessionReview.expandAll": "모두 펼치기",
diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts
index 4433b8114..e578e3cdf 100644
--- a/packages/ui/src/i18n/no.ts
+++ b/packages/ui/src/i18n/no.ts
@@ -3,6 +3,7 @@ type Keys = keyof typeof en
export const dict: Record<Keys, string> = {
"ui.sessionReview.title": "Sesjonsendringer",
+ "ui.sessionReview.title.lastTurn": "Endringer i siste tur",
"ui.sessionReview.diffStyle.unified": "Samlet",
"ui.sessionReview.diffStyle.split": "Delt",
"ui.sessionReview.expandAll": "Utvid alle",
diff --git a/packages/ui/src/i18n/pl.ts b/packages/ui/src/i18n/pl.ts
index efe4bf6cf..0690a7581 100644
--- a/packages/ui/src/i18n/pl.ts
+++ b/packages/ui/src/i18n/pl.ts
@@ -1,5 +1,6 @@
export const dict = {
"ui.sessionReview.title": "Zmiany w sesji",
+ "ui.sessionReview.title.lastTurn": "Zmiany z ostatniej tury",
"ui.sessionReview.diffStyle.unified": "Ujednolicony",
"ui.sessionReview.diffStyle.split": "Podzielony",
"ui.sessionReview.expandAll": "Rozwiń wszystko",
diff --git a/packages/ui/src/i18n/ru.ts b/packages/ui/src/i18n/ru.ts
index 60e63455d..d5a5b59fa 100644
--- a/packages/ui/src/i18n/ru.ts
+++ b/packages/ui/src/i18n/ru.ts
@@ -1,5 +1,6 @@
export const dict = {
"ui.sessionReview.title": "Изменения сессии",
+ "ui.sessionReview.title.lastTurn": "Изменения последнего хода",
"ui.sessionReview.diffStyle.unified": "Объединённый",
"ui.sessionReview.diffStyle.split": "Разделённый",
"ui.sessionReview.expandAll": "Развернуть всё",
diff --git a/packages/ui/src/i18n/th.ts b/packages/ui/src/i18n/th.ts
index 9c7e6fae5..097a2dab0 100644
--- a/packages/ui/src/i18n/th.ts
+++ b/packages/ui/src/i18n/th.ts
@@ -1,5 +1,6 @@
export const dict = {
"ui.sessionReview.title": "การเปลี่ยนแปลงเซสชัน",
+ "ui.sessionReview.title.lastTurn": "การเปลี่ยนแปลงของเทิร์นล่าสุด",
"ui.sessionReview.diffStyle.unified": "แบบรวม",
"ui.sessionReview.diffStyle.split": "แบบแยก",
"ui.sessionReview.expandAll": "ขยายทั้งหมด",
diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts
index 4ea477792..25f36b3cd 100644
--- a/packages/ui/src/i18n/zh.ts
+++ b/packages/ui/src/i18n/zh.ts
@@ -4,6 +4,7 @@ type Keys = keyof typeof en
export const dict = {
"ui.sessionReview.title": "会话变更",
+ "ui.sessionReview.title.lastTurn": "上一轮变更",
"ui.sessionReview.diffStyle.unified": "统一",
"ui.sessionReview.diffStyle.split": "拆分",
"ui.sessionReview.expandAll": "全部展开",
diff --git a/packages/ui/src/i18n/zht.ts b/packages/ui/src/i18n/zht.ts
index c5ef99cda..ebc096cbf 100644
--- a/packages/ui/src/i18n/zht.ts
+++ b/packages/ui/src/i18n/zht.ts
@@ -4,6 +4,7 @@ type Keys = keyof typeof en
export const dict = {
"ui.sessionReview.title": "工作階段變更",
+ "ui.sessionReview.title.lastTurn": "上一輪變更",
"ui.sessionReview.diffStyle.unified": "整合",
"ui.sessionReview.diffStyle.split": "拆分",
"ui.sessionReview.expandAll": "全部展開",