diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index b24897f9c..3fe68ea01 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -595,7 +595,13 @@ export const dict = { "session.review.noSnapshot": "Snapshot tracking is disabled in config, so session changes are unavailable", "session.review.noChanges": "No changes", "session.review.noUncommittedChanges": "No uncommitted changes yet", + "session.review.noUnstagedChanges": "No unstaged changes yet", + "session.review.noStagedChanges": "No staged changes yet", "session.review.noBranchChanges": "No branch changes yet", + "ui.sessionReview.title.unstaged": "Unstaged", + "ui.sessionReview.title.staged": "Staged", + "ui.sessionReview.title.branch": "Branch", + "ui.sessionReview.title.lastTurn": "Last Turn", "session.files.selectToOpen": "Select a file to open", "session.files.all": "All files", diff --git a/packages/app/src/i18n/parity.test.ts b/packages/app/src/i18n/parity.test.ts index 9b15490b5..519b1d76c 100644 --- a/packages/app/src/i18n/parity.test.ts +++ b/packages/app/src/i18n/parity.test.ts @@ -18,6 +18,13 @@ const keys = [ "session.panel.utility", "session.panel.files", "session.panel.changes", + "session.review.noUnstagedChanges", + "session.review.noStagedChanges", + "session.review.noBranchChanges", + "ui.sessionReview.title.unstaged", + "ui.sessionReview.title.staged", + "ui.sessionReview.title.branch", + "ui.sessionReview.title.lastTurn", ] as const describe("i18n parity", () => { diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 9edcf834e..491744590 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -559,6 +559,13 @@ export const dict = { "session.review.noVcs": "未检测到 Git 版本控制系统,无法显示更改", "session.review.noSnapshot": "配置中已禁用快照跟踪,因此会话更改不可用", "session.review.noChanges": "无更改", + "session.review.noUnstagedChanges": "暂无未暂存变更", + "session.review.noStagedChanges": "暂无已暂存变更", + "session.review.noBranchChanges": "暂无分支变更", + "ui.sessionReview.title.unstaged": "未暂存变更", + "ui.sessionReview.title.staged": "已暂存变更", + "ui.sessionReview.title.branch": "分支变更", + "ui.sessionReview.title.lastTurn": "上轮变更", "session.files.selectToOpen": "选择要打开的文件", "session.files.all": "所有文件", "session.files.empty": "无文件", diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index d747bc77e..af06314ea 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,4 +1,4 @@ -import type { Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useMutation } from "@tanstack/solid-query" import { @@ -26,7 +26,6 @@ import { Select } from "@opencode-ai/ui/select" import { Tabs } from "@opencode-ai/ui/tabs" import { createAutoScroll } from "@opencode-ai/ui/hooks" import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" -import { Button } from "@opencode-ai/ui/button" import { showToast } from "@opencode-ai/ui/toast" import { checksum } from "@opencode-ai/util/encode" import { useLocation, useSearchParams } from "@solidjs/router" @@ -54,6 +53,17 @@ import { } from "@/pages/session/helpers" import { MessageTimeline } from "@/pages/session/message-timeline" import { SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab" +import { + coerceReviewChangeMode, + DEFAULT_REVIEW_CHANGE_MODE, + isVcsReviewMode, + nextReviewModeForSessionChange, + reviewChangeOptions, + reviewDiffsForMode, + reviewModeLabelKey, + type ReviewChangeMode, + type VcsReviewMode, +} from "@/pages/session/review-change-mode" import { useSessionLayout } from "@/pages/session/session-layout" import { emptyMessages, @@ -80,9 +90,6 @@ type FollowupItem = FollowupDraft & { id: string } type FollowupEdit = Pick const emptyFollowups: FollowupItem[] = [] -type ChangeMode = "git" | "branch" | "turn" -type VcsMode = "git" | "branch" - type SessionHistoryWindowInput = { sessionID: () => string | undefined messagesReady: () => boolean @@ -614,27 +621,23 @@ export default function Page() { const [store, setStore] = createStore({ messageId: undefined as string | undefined, mobileTab: "session" as "session" | "changes", - changes: "git" as ChangeMode, + changes: DEFAULT_REVIEW_CHANGE_MODE as ReviewChangeMode, newSessionWorktree: "main", deferRender: false, }) const [vcs, setVcs] = createStore<{ - diff: { - git: VcsFileDiff[] - branch: VcsFileDiff[] - } - ready: { - git: boolean - branch: boolean - } + diff: Record + ready: Record }>({ diff: { - git: [] as VcsFileDiff[], + unstaged: [] as VcsFileDiff[], + staged: [] as VcsFileDiff[], branch: [] as VcsFileDiff[], }, ready: { - git: false, + unstaged: false, + staged: false, branch: false, }, }) @@ -671,18 +674,18 @@ export default function Page() { let todoTimer: number | undefined let diffFrame: number | undefined let diffTimer: number | undefined - const vcsTask = new Map>() - const vcsRun = new Map() + const vcsTask = new Map>() + const vcsRun = new Map() - const bumpVcs = (mode: VcsMode) => { + const bumpVcs = (mode: VcsReviewMode) => { const next = (vcsRun.get(mode) ?? 0) + 1 vcsRun.set(mode, next) return next } - const resetVcs = (mode?: VcsMode) => { - const list = mode ? [mode] : (["git", "branch"] as const) - list.forEach((item) => { + const resetVcs = (mode?: VcsReviewMode) => { + const modes = mode ? [mode] : (["unstaged", "staged", "branch"] as const) + modes.forEach((item) => { bumpVcs(item) vcsTask.delete(item) setVcs("diff", item, []) @@ -690,7 +693,7 @@ export default function Page() { }) } - const loadVcs = (mode: VcsMode, force = false) => { + const loadVcs = (mode: VcsReviewMode, force = false) => { if (sync.project?.vcs !== "git") return Promise.resolve() if (!force && vcs.ready[mode]) return Promise.resolve() @@ -761,34 +764,24 @@ export default function Page() { }), ) }) - const nogit = createMemo(() => !!sync.project && sync.project.vcs !== "git") - const changesOptions = createMemo(() => { - const list: ChangeMode[] = [] - if (sync.project?.vcs === "git") list.push("git") - if ( - sync.project?.vcs === "git" && - sync.data.vcs?.branch && - sync.data.vcs?.default_branch && - sync.data.vcs.branch !== sync.data.vcs.default_branch - ) { - list.push("branch") - } - list.push("turn") - return list - }) - const vcsMode = createMemo(() => { - if (store.changes === "git" || store.changes === "branch") return store.changes + const changesOptions = createMemo(() => + reviewChangeOptions({ isGit: sync.project?.vcs === "git" }), + ) + const vcsMode = createMemo(() => { + if (isVcsReviewMode(store.changes)) return store.changes }) const reviewDiffs = createMemo(() => { - if (store.changes === "git") return list(vcs.diff.git) - if (store.changes === "branch") return list(vcs.diff.branch) - return turnDiffs() + return list( + reviewDiffsForMode(store.changes, { + turn: turnDiffs(), + vcs: vcs.diff, + }), + ) }) const reviewCount = createMemo(() => reviewDiffs().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 (isVcsReviewMode(store.changes)) return vcs.ready[store.changes] return true }) @@ -856,45 +849,6 @@ export default function Page() { scrollToMessage(msgs[targetIndex], "auto") } - function upsert(next: Project) { - const list = globalSync.data.project - sync.set("project", next.id) - const idx = list.findIndex((item) => item.id === next.id) - if (idx >= 0) { - globalSync.set( - "project", - list.map((item, i) => (i === idx ? { ...item, ...next } : item)), - ) - return - } - const at = list.findIndex((item) => item.id > next.id) - if (at >= 0) { - globalSync.set("project", [...list.slice(0, at), next, ...list.slice(at)]) - return - } - globalSync.set("project", [...list, next]) - } - - const gitMutation = useMutation(() => ({ - mutationFn: () => sdk.client.project.initGit(), - onSuccess: (x) => { - if (!x.data) return - upsert(x.data) - }, - onError: (err) => { - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: formatServerError(err, language.t), - }) - }, - })) - - function initGit() { - if (gitMutation.isPending) return - gitMutation.mutate() - } - let inputRef!: HTMLDivElement let promptDock: HTMLDivElement | undefined let dockHeight = 0 @@ -1003,7 +957,7 @@ export default function Page() { sessionKey, () => { setStore("messageId", undefined) - setStore("changes", "git") + setStore("changes", nextReviewModeForSessionChange()) setUi("pendingMessage", undefined) }, { defer: true }, @@ -1207,9 +1161,8 @@ export default function Page() { createEffect(() => { const list = changesOptions() - if (list.includes(store.changes)) return - const next = list[0] - if (!next) return + const next = coerceReviewChangeMode(store.changes, list) + if (next === store.changes) return setStore("changes", next) }) @@ -1287,11 +1240,7 @@ export default function Page() { 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") - return language.t("ui.sessionReview.title.lastTurn") - } + const label = (option: ReviewChangeMode) => language.t(reviewModeLabelKey(option)) return (