summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/pages
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-03-19 11:15:07 -0500
committerGitHub <[email protected]>2026-03-19 11:15:07 -0500
commitaeece6166b7e728440f1a3c81aa7efcc32208f01 (patch)
treec5d2666b61e5fe79fc6739e10b873add67d39b02 /packages/app/src/pages
parent0d7e62a532ba9f2163f872414c52a213c517936a (diff)
downloadopencode-aeece6166b7e728440f1a3c81aa7efcc32208f01.tar.gz
opencode-aeece6166b7e728440f1a3c81aa7efcc32208f01.zip
ignore: revert 3 commits that broke dev branch (#18260)
Diffstat (limited to 'packages/app/src/pages')
-rw-r--r--packages/app/src/pages/layout.tsx6
-rw-r--r--packages/app/src/pages/layout/helpers.ts6
-rw-r--r--packages/app/src/pages/session.tsx228
-rw-r--r--packages/app/src/pages/session/session-side-panel.tsx58
-rw-r--r--packages/app/src/pages/session/use-session-commands.tsx6
5 files changed, 81 insertions, 223 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 52ac7c5f3..cca14fd50 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -633,8 +633,7 @@ export default function Layout(props: ParentProps) {
if (!expanded) continue
const key = workspaceKey(directory)
const project = projects.find(
- (item) =>
- workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key),
+ (item) => workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key),
)
if (!project) continue
if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue
@@ -1164,8 +1163,7 @@ export default function Layout(props: ParentProps) {
const project = layout.projects
.list()
.find(
- (item) =>
- workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key),
+ (item) => workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key),
)
if (project) return project.worktree
diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts
index 209cff8a7..886ffd26a 100644
--- a/packages/app/src/pages/layout/helpers.ts
+++ b/packages/app/src/pages/layout/helpers.ts
@@ -31,13 +31,11 @@ function sortSessions(now: number) {
const isRootVisibleSession = (session: Session, directory: string) =>
workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived
-const roots = (store: SessionStore) =>
- (store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
+const roots = (store: SessionStore) => (store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now))
-export const latestRootSession = (stores: SessionStore[], now: number) =>
- stores.flatMap(roots).sort(sortSessions(now))[0]
+export const latestRootSession = (stores: SessionStore[], now: number) => stores.flatMap(roots).sort(sortSessions(now))[0]
export function hasProjectPermissions<T>(
request: Record<string, T[] | undefined>,
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 970bc73b7..6d2917008 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -1,4 +1,4 @@
-import type { FileDiff, Project, UserMessage } from "@opencode-ai/sdk/v2"
+import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import {
batch,
@@ -57,9 +57,6 @@ import { formatServerError } from "@/utils/server-errors"
const emptyUserMessages: UserMessage[] = []
const emptyFollowups: (FollowupDraft & { id: string })[] = []
-type ChangeMode = "git" | "branch" | "session" | "turn"
-type VcsMode = "git" | "branch"
-
type SessionHistoryWindowInput = {
sessionID: () => string | undefined
messagesReady: () => boolean
@@ -418,16 +415,15 @@ export default function Page() {
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
- const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
- const hasSessionReview = createMemo(() => sessionCount() > 0)
- const canReview = createMemo(() => !!params.dir)
+ const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
+ const hasReview = createMemo(() => reviewCount() > 0)
const reviewTab = createMemo(() => isDesktop())
const tabState = createSessionTabs({
tabs,
pathFromTab: file.pathFromTab,
normalizeTab,
review: reviewTab,
- hasReview: canReview,
+ hasReview,
})
const contextOpen = tabState.contextOpen
const openedTabs = tabState.openedTabs
@@ -503,22 +499,11 @@ export default function Page() {
const [store, setStore] = createStore({
messageId: undefined as string | undefined,
mobileTab: "session" as "session" | "changes",
- changes: "git" as ChangeMode,
+ changes: "session" as "session" | "turn",
newSessionWorktree: "main",
deferRender: false,
})
- const [vcs, setVcs] = createStore({
- diff: {
- git: [] as FileDiff[],
- branch: [] as FileDiff[],
- },
- ready: {
- git: false,
- branch: false,
- },
- })
-
const [followup, setFollowup] = createStore({
items: {} as Record<string, (FollowupDraft & { id: string })[] | undefined>,
sending: {} as Record<string, string | undefined>,
@@ -546,40 +531,6 @@ export default function Page() {
let refreshTimer: number | undefined
let diffFrame: number | undefined
let diffTimer: number | undefined
- const vcsTask = new Map<VcsMode, Promise<void>>()
-
- const resetVcs = () => {
- vcsTask.clear()
- setVcs({
- diff: { git: [], branch: [] },
- ready: { git: false, branch: false },
- })
- }
-
- const loadVcs = (mode: VcsMode, force = false) => {
- if (sync.project?.vcs !== "git") return Promise.resolve()
- if (vcs.ready[mode] && !force) return Promise.resolve()
- const current = vcsTask.get(mode)
- if (current) return current
-
- const task = sdk.client.vcs
- .diff({ mode })
- .then((result) => {
- setVcs("diff", mode, result.data ?? [])
- setVcs("ready", mode, true)
- })
- .catch((error) => {
- console.debug("[session-review] failed to load vcs diff", { mode, error })
- setVcs("diff", mode, [])
- setVcs("ready", mode, true)
- })
- .finally(() => {
- vcsTask.delete(mode)
- })
-
- vcsTask.set(mode, task)
- return task
- }
createComputed((prev) => {
const open = desktopReviewOpen()
@@ -595,43 +546,7 @@ export default function Page() {
}, desktopReviewOpen())
const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
- const changesOptions = createMemo<ChangeMode[]>(() => {
- const list: ChangeMode[] = []
- const git = sync.project?.vcs === "git"
- if (git) list.push("git")
- if (
- git &&
- sync.data.vcs?.branch &&
- sync.data.vcs?.default_branch &&
- sync.data.vcs.branch !== sync.data.vcs.default_branch
- ) {
- list.push("branch")
- }
- list.push("session", "turn")
- return list
- })
- const vcsMode = createMemo<VcsMode | undefined>(() => {
- if (store.changes === "git" || store.changes === "branch") return store.changes
- })
- const reviewDiffs = createMemo(() => {
- if (store.changes === "git") return vcs.diff.git
- if (store.changes === "branch") return vcs.diff.branch
- if (store.changes === "session") return diffs()
- return turnDiffs()
- })
- const reviewCount = createMemo(() => {
- if (store.changes === "git") return vcs.diff.git.length
- if (store.changes === "branch") return vcs.diff.branch.length
- if (store.changes === "session") return sessionCount()
- return turnDiffs().length
- })
- const hasReview = createMemo(() => reviewCount() > 0)
- const reviewReady = createMemo(() => {
- if (store.changes === "git") return vcs.ready.git
- if (store.changes === "branch") return vcs.ready.branch
- if (store.changes === "session") return !hasSessionReview() || diffsReady()
- return true
- })
+ const reviewDiffs = createMemo(() => (store.changes === "session" ? diffs() : turnDiffs()))
const newSessionWorktree = createMemo(() => {
if (store.newSessionWorktree === "create") return "create"
@@ -700,10 +615,10 @@ export default function Page() {
const diffsReady = createMemo(() => {
const id = params.id
if (!id) return true
- if (!hasSessionReview()) return true
+ if (!hasReview()) return true
return sync.data.session_diff[id] !== undefined
})
- const sessionEmptyKey = createMemo(() => {
+ const reviewEmptyKey = createMemo(() => {
const project = sync.project
if (project && !project.vcs) return "session.review.noVcs"
if (sync.data.config.snapshot === false) return "session.review.noSnapshot"
@@ -826,7 +741,7 @@ export default function Page() {
sessionKey,
() => {
setStore("messageId", undefined)
- setStore("changes", "git")
+ setStore("changes", "session")
setUi("pendingMessage", undefined)
},
{ defer: true },
@@ -835,16 +750,6 @@ export default function Page() {
createEffect(
on(
- () => sdk.directory,
- () => {
- resetVcs()
- },
- { defer: true },
- ),
- )
-
- createEffect(
- on(
() => params.dir,
(dir) => {
if (!dir) return
@@ -965,40 +870,6 @@ export default function Page() {
}
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
- const wantsReview = createMemo(() =>
- isDesktop()
- ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
- : store.mobileTab === "changes",
- )
-
- createEffect(() => {
- const list = changesOptions()
- if (list.includes(store.changes)) return
- const next = list[0]
- if (!next) return
- setStore("changes", next)
- })
-
- createEffect(() => {
- const mode = vcsMode()
- if (!mode) return
- if (!wantsReview()) return
- void loadVcs(mode)
- })
-
- createEffect(
- on(
- () => sync.data.session_status[params.id ?? ""]?.type,
- (next, prev) => {
- const mode = vcsMode()
- if (!mode) return
- if (!wantsReview()) return
- if (next !== "idle" || prev === undefined || prev === "idle") return
- void loadVcs(mode, true)
- },
- { defer: true },
- ),
- )
const fileTreeTab = () => layout.fileTree.tab()
const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value)
@@ -1045,23 +916,21 @@ export default function Page() {
loadFile: file.load,
})
+ const changesOptions = ["session", "turn"] as const
+ const changesOptionsList = [...changesOptions]
+
const changesTitle = () => {
- if (!canReview()) {
+ if (!hasReview()) {
return null
}
- const label = (option: ChangeMode) => {
- if (option === "git") return language.t("ui.sessionReview.title.git")
- if (option === "branch") return language.t("ui.sessionReview.title.branch")
- if (option === "session") return language.t("ui.sessionReview.title")
- return language.t("ui.sessionReview.title.lastTurn")
- }
-
return (
<Select
- options={changesOptions()}
+ options={changesOptionsList}
current={store.changes}
- label={label}
+ label={(option) =>
+ option === "session" ? language.t("ui.sessionReview.title") : language.t("ui.sessionReview.title.lastTurn")
+ }
onSelect={(option) => option && setStore("changes", option)}
variant="ghost"
size="small"
@@ -1070,34 +939,20 @@ export default function Page() {
)
}
- const empty = (text: string) => (
+ const emptyTurn = () => (
<div class="h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6">
- <div class="text-14-regular text-text-weak max-w-56">{text}</div>
+ <div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.noChanges")}</div>
</div>
)
- const reviewEmptyText = createMemo(() => {
- if (store.changes === "git") return language.t("session.review.noUncommittedChanges")
- if (store.changes === "branch") return language.t("session.review.noBranchChanges")
- if (store.changes === "turn") return language.t("session.review.noChanges")
- return language.t(sessionEmptyKey())
- })
-
const reviewEmpty = (input: { loadingClass: string; emptyClass: string }) => {
- if (store.changes === "git" || store.changes === "branch") {
- if (!reviewReady()) return <div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>
- return empty(reviewEmptyText())
- }
-
- if (store.changes === "turn") {
- return empty(reviewEmptyText())
- }
+ if (store.changes === "turn") return emptyTurn()
- if (hasSessionReview() && !diffsReady()) {
+ if (hasReview() && !diffsReady()) {
return <div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>
}
- if (sessionEmptyKey() === "session.review.noVcs") {
+ if (reviewEmptyKey() === "session.review.noVcs") {
return (
<div class={input.emptyClass}>
<div class="flex flex-col gap-3">
@@ -1117,7 +972,7 @@ export default function Page() {
return (
<div class={input.emptyClass}>
- <div class="text-14-regular text-text-weak max-w-56">{reviewEmptyText()}</div>
+ <div class="text-14-regular text-text-weak max-w-56">{language.t(reviewEmptyKey())}</div>
</div>
)
}
@@ -1165,18 +1020,6 @@ export default function Page() {
</div>
)
- const mobileReview = () =>
- reviewContent({
- diffStyle: "unified",
- classes: {
- root: "pb-8",
- header: "px-4",
- container: "px-4",
- },
- loadingClass: "px-4 py-4 text-text-weak",
- emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6",
- })
-
createEffect(
on(
activeFileTab,
@@ -1233,7 +1076,7 @@ export default function Page() {
const pending = tree.pendingDiff
if (!pending) return
if (!tree.reviewScroll) return
- if (!reviewReady()) return
+ if (!diffsReady()) return
const attempt = (count: number) => {
if (tree.pendingDiff !== pending) return
@@ -1810,7 +1653,7 @@ export default function Page() {
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
<SessionHeader />
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
- <Show when={!isDesktop() && canReview()}>
+ <Show when={!isDesktop() && !!params.id}>
<Tabs value={store.mobileTab} class="h-auto">
<Tabs.List>
<Tabs.Trigger
@@ -1852,7 +1695,16 @@ export default function Page() {
<Show when={lastUserMessage()}>
<MessageTimeline
mobileChanges={mobileChanges()}
- mobileFallback={mobileReview()}
+ mobileFallback={reviewContent({
+ diffStyle: "unified",
+ classes: {
+ root: "pb-8",
+ header: "px-4",
+ container: "px-4",
+ },
+ loadingClass: "px-4 py-4 text-text-weak",
+ emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6",
+ })}
actions={actions}
scroll={ui.scroll}
onResumeScroll={resumeScroll}
@@ -1884,9 +1736,7 @@ export default function Page() {
</Show>
</Match>
<Match when={true}>
- <Show when={mobileChanges()} fallback={<NewSessionView worktree={newSessionWorktree()} />}>
- <div class="relative h-full overflow-hidden">{mobileReview()}</div>
- </Show>
+ <NewSessionView worktree={newSessionWorktree()} />
</Match>
</Switch>
</div>
@@ -1958,12 +1808,6 @@ export default function Page() {
</div>
<SessionSidePanel
- canReview={canReview}
- diffs={reviewDiffs}
- diffsReady={reviewReady}
- empty={reviewEmptyText}
- hasReview={hasReview}
- reviewCount={reviewCount}
reviewPanel={reviewPanel}
activeDiff={tree.activeDiff}
focusReviewDiff={focusReviewDiff}
diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx
index 3ba619736..3b8b0c96b 100644
--- a/packages/app/src/pages/session/session-side-panel.tsx
+++ b/packages/app/src/pages/session/session-side-panel.tsx
@@ -8,7 +8,6 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Mark } from "@opencode-ai/ui/logo"
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd"
-import type { FileDiff } from "@opencode-ai/sdk/v2"
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -20,6 +19,7 @@ import { useCommand } from "@/context/command"
import { useFile, type SelectedLineRange } from "@/context/file"
import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
+import { useSync } from "@/context/sync"
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
import { FileTabContent } from "@/pages/session/file-tabs"
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
@@ -27,12 +27,6 @@ import { setSessionHandoff } from "@/pages/session/handoff"
import { useSessionLayout } from "@/pages/session/session-layout"
export function SessionSidePanel(props: {
- canReview: () => boolean
- diffs: () => FileDiff[]
- diffsReady: () => boolean
- empty: () => string
- hasReview: () => boolean
- reviewCount: () => number
reviewPanel: () => JSX.Element
activeDiff?: string
focusReviewDiff: (path: string) => void
@@ -40,11 +34,12 @@ export function SessionSidePanel(props: {
size: Sizing
}) {
const layout = useLayout()
+ const sync = useSync()
const file = useFile()
const language = useLanguage()
const command = useCommand()
const dialog = useDialog()
- const { sessionKey, tabs, view } = useSessionLayout()
+ const { params, sessionKey, tabs, view } = useSessionLayout()
const isDesktop = createMediaQuery("(min-width: 768px)")
@@ -59,7 +54,24 @@ export function SessionSidePanel(props: {
})
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
- const diffFiles = createMemo(() => props.diffs().map((d) => d.file))
+ const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
+ const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
+ const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
+ const hasReview = createMemo(() => reviewCount() > 0)
+ const diffsReady = createMemo(() => {
+ const id = params.id
+ if (!id) return true
+ if (!hasReview()) return true
+ return sync.data.session_diff[id] !== undefined
+ })
+
+ const reviewEmptyKey = createMemo(() => {
+ if (sync.project && !sync.project.vcs) return "session.review.noVcs"
+ if (sync.data.config.snapshot === false) return "session.review.noSnapshot"
+ return "session.review.noChanges"
+ })
+
+ const diffFiles = createMemo(() => diffs().map((d) => d.file))
const kinds = createMemo(() => {
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
if (!a) return b
@@ -70,7 +82,7 @@ export function SessionSidePanel(props: {
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
const out = new Map<string, "add" | "del" | "mix">()
- for (const diff of props.diffs()) {
+ for (const diff of diffs()) {
const file = normalize(diff.file)
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
@@ -124,7 +136,7 @@ export function SessionSidePanel(props: {
pathFromTab: file.pathFromTab,
normalizeTab,
review: reviewTab,
- hasReview: props.canReview,
+ hasReview,
})
const contextOpen = tabState.contextOpen
const openedTabs = tabState.openedTabs
@@ -229,12 +241,12 @@ export function SessionSidePanel(props: {
onCleanup(stop)
}}
>
- <Show when={reviewTab() && props.canReview()}>
+ <Show when={reviewTab()}>
<Tabs.Trigger value="review">
<div class="flex items-center gap-1.5">
<div>{language.t("session.tab.review")}</div>
- <Show when={props.hasReview()}>
- <div>{props.reviewCount()}</div>
+ <Show when={hasReview()}>
+ <div>{reviewCount()}</div>
</Show>
</div>
</Tabs.Trigger>
@@ -291,7 +303,7 @@ export function SessionSidePanel(props: {
</Tabs.List>
</div>
- <Show when={reviewTab() && props.canReview()}>
+ <Show when={reviewTab()}>
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
</Tabs.Content>
@@ -365,10 +377,8 @@ export function SessionSidePanel(props: {
>
<Tabs.List>
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
- {props.reviewCount()}{" "}
- {language.t(
- props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
- )}
+ {reviewCount()}{" "}
+ {language.t(reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other")}
</Tabs.Trigger>
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
{language.t("session.files.all")}
@@ -376,9 +386,9 @@ export function SessionSidePanel(props: {
</Tabs.List>
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
<Switch>
- <Match when={props.hasReview() || !props.diffsReady()}>
+ <Match when={hasReview()}>
<Show
- when={props.diffsReady()}
+ when={diffsReady()}
fallback={
<div class="px-2 py-2 text-12-regular text-text-weak">
{language.t("common.loading")}
@@ -397,7 +407,11 @@ export function SessionSidePanel(props: {
/>
</Show>
</Match>
- <Match when={true}>{empty(props.empty())}</Match>
+ <Match when={true}>
+ {empty(
+ language.t(sync.project && !sync.project.vcs ? "session.review.noChanges" : reviewEmptyKey()),
+ )}
+ </Match>
</Switch>
</Tabs.Content>
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx
index f45374359..1a2e777f5 100644
--- a/packages/app/src/pages/session/use-session-commands.tsx
+++ b/packages/app/src/pages/session/use-session-commands.tsx
@@ -56,7 +56,11 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
if (!id) return
return sync.session.get(id)
}
- const hasReview = () => !!params.id
+ const hasReview = () => {
+ const id = params.id
+ if (!id) return false
+ return Math.max(info()?.summary?.files ?? 0, (sync.data.session_diff[id] ?? []).length) > 0
+ }
const normalizeTab = (tab: string) => {
if (!tab.startsWith("file://")) return tab
return file.tab(tab)