From 63b2ffc6553cd1e6cea4edabc976341bef3b766a Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 5 Mar 2026 19:34:46 +0000 Subject: [PATCH 1/8] fix(app): polish review and filetree empty states Removes branding/title in review empty states, hides the changes selector when there are no diffs, aligns filetree empty messaging, and updates the no-git copy. --- packages/app/src/i18n/en.ts | 3 +- packages/app/src/pages/session.tsx | 35 +++++++++-------- .../src/pages/session/session-side-panel.tsx | 38 +++++++++++++------ packages/ui/src/components/session-review.tsx | 4 +- 4 files changed, 51 insertions(+), 29 deletions(-) diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 7e95fd739df7..8b672d437551 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -511,11 +511,12 @@ export const dict = { "session.review.change.other": "Changes", "session.review.loadingChanges": "Loading changes...", "session.review.empty": "No changes in this session yet", - "session.review.noVcs": "No git VCS detected, so session changes will not be detected", + "session.review.noVcs": "No Git Version Control System detected, changes not displayed", "session.review.noChanges": "No changes", "session.files.selectToOpen": "Select a file to open", "session.files.all": "All files", + "session.files.empty": "No files", "session.files.binaryContent": "Binary file (content cannot be displayed)", "session.messages.renderEarlier": "Render earlier messages", diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 1476e616e58c..c01ff8c301f4 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -20,7 +20,6 @@ import { createStore } from "solid-js/store" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Select } from "@opencode-ai/ui/select" import { createAutoScroll } from "@opencode-ai/ui/hooks" -import { Mark } from "@opencode-ai/ui/logo" import { base64Encode, checksum } from "@opencode-ai/util/encode" import { useNavigate, useParams, useSearchParams } from "@solidjs/router" import { NewSessionView, SessionHeader } from "@/components/session" @@ -727,23 +726,28 @@ export default function Page() { const changesOptions = ["session", "turn"] as const const changesOptionsList = [...changesOptions] - const changesTitle = () => ( - + option === "session" ? language.t("ui.sessionReview.title") : language.t("ui.sessionReview.title.lastTurn") + } + onSelect={(option) => option && setStore("changes", option)} + variant="ghost" + size="small" + valueClass="text-14-medium" + /> + ) + } const emptyTurn = () => (
-
{language.t("session.review.noChanges")}
) @@ -811,7 +815,6 @@ export default function Page() { emptyTurn() ) : (
-
{language.t(reviewEmptyKey())}
) diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index ad802d15d186..18e46e0091f0 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -87,6 +87,21 @@ export function SessionSidePanel(props: { return out }) + const empty = (msg: string) => ( +
+
+
+
{msg}
+
+
+ ) + + const nofiles = createMemo(() => { + const state = file.tree.state("") + if (!state?.loaded) return false + return file.tree.children("").length === 0 + }) + const normalizeTab = (tab: string) => { if (!tab.startsWith("file://")) return tab return file.tab(tab) @@ -390,11 +405,7 @@ export function SessionSidePanel(props: { /> - -
- {language.t("session.review.noChanges")} -
-
+ {empty(language.t("session.review.noChanges"))} syncFileTreeScrolled(e.currentTarget)} class="bg-background-stronger px-3 py-0" > - openTab(file.tab(node.path))} - /> + + {empty(language.t("session.files.empty"))} + + openTab(file.tab(node.path))} + /> + +
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 25a646acedd8..62c70e8647dd 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -554,7 +554,9 @@ export const SessionReview = (props: SessionReviewProps) => { return (
-
{props.title ?? i18n.t("ui.sessionReview.title")}
+
+ {props.title === undefined ? i18n.t("ui.sessionReview.title") : props.title} +
Date: Thu, 5 Mar 2026 19:38:19 +0000 Subject: [PATCH 2/8] fix(app): always show filetree tabs divider Removes scroll-based border toggling and keeps the bottom divider under filetree tabs visible at all times. --- .../src/pages/session/session-side-panel.tsx | 30 ++----------------- packages/ui/src/components/tabs.css | 6 +--- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 18e46e0091f0..f37020fe62bc 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -160,17 +160,8 @@ export function SessionSidePanel(props: { const [store, setStore] = createStore({ activeDraggable: undefined as string | undefined, - fileTreeScrolled: false, }) - let changesEl: HTMLDivElement | undefined - let allEl: HTMLDivElement | undefined - - const syncFileTreeScrolled = (el?: HTMLDivElement) => { - const next = (el?.scrollTop ?? 0) > 0 - setStore("fileTreeScrolled", (current) => (current === next ? current : next)) - } - const handleDragStart = (event: unknown) => { const id = getDraggableId(event) if (!id) return @@ -191,11 +182,6 @@ export function SessionSidePanel(props: { setStore("activeDraggable", undefined) } - createEffect(() => { - if (!layout.fileTree.opened()) return - syncFileTreeScrolled(fileTreeTab() === "changes" ? changesEl : allEl) - }) - createEffect(() => { if (!file.ready()) return @@ -369,7 +355,7 @@ export function SessionSidePanel(props: { class="h-full" data-scope="filetree" > - + {reviewCount()}{" "} {language.t(reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other")} @@ -378,12 +364,7 @@ export function SessionSidePanel(props: { {language.t("session.files.all")} - (changesEl = el)} - onScroll={(e: UIEvent & { currentTarget: HTMLDivElement }) => syncFileTreeScrolled(e.currentTarget)} - class="bg-background-stronger px-3 py-0" - > + {empty(language.t("session.review.noChanges"))} - (allEl = el)} - onScroll={(e: UIEvent & { currentTarget: HTMLDivElement }) => syncFileTreeScrolled(e.currentTarget)} - class="bg-background-stronger px-3 py-0" - > + {empty(language.t("session.files.empty"))} diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css index 51917489e23c..1bf00b785220 100644 --- a/packages/ui/src/components/tabs.css +++ b/packages/ui/src/components/tabs.css @@ -407,11 +407,7 @@ align-items: center; background-color: var(--background-stronger); box-sizing: border-box; - border-bottom: 1px solid transparent; - - &[data-scrolled] { - border-bottom-color: var(--border-weak-base); - } + border-bottom: 1px solid var(--border-weak-base); } [data-slot="tabs-trigger-wrapper"] { From 2bc7ba556a449862d75020436d8752c44b36609f Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 5 Mar 2026 19:56:02 +0000 Subject: [PATCH 3/8] tui: add top margin spacing to file tree for better visual separation --- packages/app/src/components/file-tree.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index 930832fb6555..7fbe7707c477 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -385,7 +385,7 @@ export default function FileTree(props: { }) return ( -
+
{(node) => { const expanded = () => file.tree.state(node.path)?.expanded ?? false From 3b9d96e7fd6ea1d95ba337fa87c8f0abfc8f4eee Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 5 Mar 2026 19:57:47 +0000 Subject: [PATCH 4/8] Revert "tui: add top margin spacing to file tree for better visual separation" This reverts commit fcea784a7d837627ca26588c865497579cb84ed6. --- packages/app/src/components/file-tree.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index 7fbe7707c477..930832fb6555 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -385,7 +385,7 @@ export default function FileTree(props: { }) return ( -
+
{(node) => { const expanded = () => file.tree.state(node.path)?.expanded ?? false From fb40a852f3391ed376cbdf55b0eb2a38bc78076d Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 5 Mar 2026 20:02:04 +0000 Subject: [PATCH 5/8] tui: add top padding to file tree panels to improve visual separation from headers --- packages/app/src/pages/session/session-side-panel.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index f37020fe62bc..66d4382c088e 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -378,6 +378,7 @@ export function SessionSidePanel(props: { > openTab(file.tab(node.path))} From f267d434ebff2ed82329b0e5b11da570f7a6d390 Mon Sep 17 00:00:00 2001 From: David Hill Date: Fri, 6 Mar 2026 00:09:28 +0000 Subject: [PATCH 6/8] fix(app): polish sidebar Getting started card Improves readability and spacing, adds a dismiss action, and makes the CTA layout responsive to sidebar width. --- packages/app/src/index.css | 28 +++++++++++++++++++++++ packages/app/src/pages/layout.tsx | 37 ++++++++++++++++++------------- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 4af87bca632a..9e231e2d2858 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -1 +1,29 @@ @import "@opencode-ai/ui/styles/tailwind"; + +@layer components { + [data-component="getting-started"] { + container-type: inline-size; + container-name: getting-started; + } + + [data-component="getting-started-actions"] { + display: flex; + flex-direction: column; + gap: 0.75rem; /* gap-3 */ + } + + [data-component="getting-started-actions"] > [data-component="button"] { + width: 100%; + } + + @container getting-started (min-width: 17rem) { + [data-component="getting-started-actions"] { + flex-direction: row; + align-items: center; + } + + [data-component="getting-started-actions"] > [data-component="button"] { + width: auto; + } + } +} diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index f6165461b1ee..cf2c3b6c4337 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -93,6 +93,7 @@ export default function Layout(props: ParentProps) { workspaceName: {} as Record, workspaceBranchName: {} as Record>, workspaceExpanded: {} as Record, + gettingStartedDismissed: false, }), ) @@ -2006,25 +2007,31 @@ export default function Layout(props: ParentProps) {
0 && providers.paid().length === 0), + hidden: store.gettingStartedDismissed || !(providers.all().length > 0 && providers.paid().length === 0), }} > -
-
-
{language.t("sidebar.gettingStarted.title")}
-
{language.t("sidebar.gettingStarted.line1")}
-
{language.t("sidebar.gettingStarted.line2")}
+
+
+
+
{language.t("sidebar.gettingStarted.title")}
+
+ {language.t("sidebar.gettingStarted.line1")} +
+
+ {language.t("sidebar.gettingStarted.line2")} +
+
+
+ + +
-
From 0b3702bab61773da8b3d333762c63fd5397d8a14 Mon Sep 17 00:00:00 2001 From: David Hill Date: Fri, 6 Mar 2026 00:25:37 +0000 Subject: [PATCH 7/8] tui: add empty state for sessions without version control When opening a session in a project without Git initialized, users now see clear guidance explaining that creating a repository enables tracking, reviewing, and undoing changes. Includes a button to initialize Git directly from the review panel. --- packages/app/src/pages/session.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index c01ff8c301f4..0a553c4b6a29 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -20,6 +20,7 @@ import { createStore } from "solid-js/store" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Select } from "@opencode-ai/ui/select" import { createAutoScroll } from "@opencode-ai/ui/hooks" +import { Button } from "@opencode-ai/ui/button" import { base64Encode, checksum } from "@opencode-ai/util/encode" import { useNavigate, useParams, useSearchParams } from "@solidjs/router" import { NewSessionView, SessionHeader } from "@/components/session" @@ -813,6 +814,21 @@ export default function Page() { empty={ store.changes === "turn" ? ( emptyTurn() + ) : reviewEmptyKey() === "session.review.noVcs" ? ( +
+
+
Create a Git repository
+
+ Track, review, and undo changes in this project +
+
+ +
) : (
{language.t(reviewEmptyKey())}
From 37739f46b451ddaaef5407c864ed7b2afb3fe61d Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:43:26 -0600 Subject: [PATCH 8/8] chore: wire create repo button --- packages/app/src/pages/session.tsx | 51 ++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 0a553c4b6a29..f6f6576c4dc2 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,4 +1,4 @@ -import type { UserMessage } from "@opencode-ai/sdk/v2" +import type { Project, UserMessage } from "@opencode-ai/sdk/v2" import { useDialog } from "@opencode-ai/ui/context/dialog" import { onCleanup, @@ -21,10 +21,12 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Select } from "@opencode-ai/ui/select" import { createAutoScroll } from "@opencode-ai/ui/hooks" import { Button } from "@opencode-ai/ui/button" +import { showToast } from "@opencode-ai/ui/toast" import { base64Encode, checksum } from "@opencode-ai/util/encode" import { useNavigate, useParams, useSearchParams } from "@solidjs/router" import { NewSessionView, SessionHeader } from "@/components/session" import { useComments } from "@/context/comments" +import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { usePrompt } from "@/context/prompt" @@ -41,6 +43,7 @@ import { TerminalPanel } from "@/pages/session/terminal-panel" import { useSessionCommands } from "@/pages/session/use-session-commands" import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" import { same } from "@/utils/same" +import { formatServerError } from "@/utils/server-errors" const emptyUserMessages: UserMessage[] = [] @@ -252,6 +255,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) { } export default function Page() { + const globalSync = useGlobalSync() const layout = useLayout() const local = useLocal() const file = useFile() @@ -278,6 +282,7 @@ export default function Page() { }) const [ui, setUi] = createStore({ + git: false, pendingMessage: undefined as string | undefined, scrollGesture: 0, scroll: { @@ -494,6 +499,46 @@ export default function Page() { return "session.review.noVcs" }) + 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]) + } + + function initGit() { + if (ui.git) return + setUi("git", true) + void sdk.client.project + .initGit() + .then((x) => { + if (!x.data) return + upsert(x.data) + }) + .catch((err) => { + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: formatServerError(err, language.t), + }) + }) + .finally(() => { + setUi("git", false) + }) + } + let inputRef!: HTMLDivElement let promptDock: HTMLDivElement | undefined let dockHeight = 0 @@ -825,8 +870,8 @@ export default function Page() { Track, review, and undo changes in this project
-
) : (