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