Skip to content

Commit 091cf25

Browse files
fix(app): better review/filetree empty states (anomalyco#16221)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
1 parent 7a071ef commit 091cf25

7 files changed

Lines changed: 168 additions & 76 deletions

File tree

packages/app/src/i18n/en.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,11 +511,12 @@ export const dict = {
511511
"session.review.change.other": "Changes",
512512
"session.review.loadingChanges": "Loading changes...",
513513
"session.review.empty": "No changes in this session yet",
514-
"session.review.noVcs": "No git VCS detected, so session changes will not be detected",
514+
"session.review.noVcs": "No Git Version Control System detected, changes not displayed",
515515
"session.review.noChanges": "No changes",
516516

517517
"session.files.selectToOpen": "Select a file to open",
518518
"session.files.all": "All files",
519+
"session.files.empty": "No files",
519520
"session.files.binaryContent": "Binary file (content cannot be displayed)",
520521

521522
"session.messages.renderEarlier": "Render earlier messages",

packages/app/src/index.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,29 @@
11
@import "@opencode-ai/ui/styles/tailwind";
2+
3+
@layer components {
4+
[data-component="getting-started"] {
5+
container-type: inline-size;
6+
container-name: getting-started;
7+
}
8+
9+
[data-component="getting-started-actions"] {
10+
display: flex;
11+
flex-direction: column;
12+
gap: 0.75rem; /* gap-3 */
13+
}
14+
15+
[data-component="getting-started-actions"] > [data-component="button"] {
16+
width: 100%;
17+
}
18+
19+
@container getting-started (min-width: 17rem) {
20+
[data-component="getting-started-actions"] {
21+
flex-direction: row;
22+
align-items: center;
23+
}
24+
25+
[data-component="getting-started-actions"] > [data-component="button"] {
26+
width: auto;
27+
}
28+
}
29+
}

packages/app/src/pages/layout.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export default function Layout(props: ParentProps) {
9393
workspaceName: {} as Record<string, string>,
9494
workspaceBranchName: {} as Record<string, Record<string, string>>,
9595
workspaceExpanded: {} as Record<string, boolean>,
96+
gettingStartedDismissed: false,
9697
}),
9798
)
9899

@@ -2006,25 +2007,31 @@ export default function Layout(props: ParentProps) {
20062007
</Show>
20072008

20082009
<div
2009-
class="shrink-0 px-2 py-3 border-t border-border-weak-base"
2010+
class="shrink-0 px-3 py-3"
20102011
classList={{
2011-
hidden: !(providers.all().length > 0 && providers.paid().length === 0),
2012+
hidden: store.gettingStartedDismissed || !(providers.all().length > 0 && providers.paid().length === 0),
20122013
}}
20132014
>
2014-
<div class="rounded-md bg-background-base shadow-xs-border-base">
2015-
<div class="p-3 flex flex-col gap-2">
2016-
<div class="text-12-medium text-text-strong">{language.t("sidebar.gettingStarted.title")}</div>
2017-
<div class="text-text-base">{language.t("sidebar.gettingStarted.line1")}</div>
2018-
<div class="text-text-base">{language.t("sidebar.gettingStarted.line2")}</div>
2015+
<div class="rounded-xl bg-background-base shadow-xs-border-base" data-component="getting-started">
2016+
<div class="p-3 flex flex-col gap-6">
2017+
<div class="flex flex-col gap-2">
2018+
<div class="text-14-medium text-text-strong">{language.t("sidebar.gettingStarted.title")}</div>
2019+
<div class="text-14-regular text-text-base" style={{ "line-height": "var(--line-height-normal)" }}>
2020+
{language.t("sidebar.gettingStarted.line1")}
2021+
</div>
2022+
<div class="text-14-regular text-text-base" style={{ "line-height": "var(--line-height-normal)" }}>
2023+
{language.t("sidebar.gettingStarted.line2")}
2024+
</div>
2025+
</div>
2026+
<div data-component="getting-started-actions">
2027+
<Button size="large" icon="plus-small" onClick={connectProvider}>
2028+
{language.t("command.provider.connect")}
2029+
</Button>
2030+
<Button size="large" variant="ghost" onClick={() => setStore("gettingStartedDismissed", true)}>
2031+
Not yet
2032+
</Button>
2033+
</div>
20192034
</div>
2020-
<Button
2021-
class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-md rounded-t-none shadow-none border-t border-border-weak-base px-3"
2022-
size="large"
2023-
icon="plus"
2024-
onClick={connectProvider}
2025-
>
2026-
{language.t("command.provider.connect")}
2027-
</Button>
20282035
</div>
20292036
</div>
20302037
</div>

packages/app/src/pages/session.tsx

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { UserMessage } from "@opencode-ai/sdk/v2"
1+
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
22
import { useDialog } from "@opencode-ai/ui/context/dialog"
33
import {
44
onCleanup,
@@ -20,11 +20,13 @@ import { createStore } from "solid-js/store"
2020
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
2121
import { Select } from "@opencode-ai/ui/select"
2222
import { createAutoScroll } from "@opencode-ai/ui/hooks"
23-
import { Mark } from "@opencode-ai/ui/logo"
23+
import { Button } from "@opencode-ai/ui/button"
24+
import { showToast } from "@opencode-ai/ui/toast"
2425
import { base64Encode, checksum } from "@opencode-ai/util/encode"
2526
import { useNavigate, useParams, useSearchParams } from "@solidjs/router"
2627
import { NewSessionView, SessionHeader } from "@/components/session"
2728
import { useComments } from "@/context/comments"
29+
import { useGlobalSync } from "@/context/global-sync"
2830
import { useLanguage } from "@/context/language"
2931
import { useLayout } from "@/context/layout"
3032
import { usePrompt } from "@/context/prompt"
@@ -41,6 +43,7 @@ import { TerminalPanel } from "@/pages/session/terminal-panel"
4143
import { useSessionCommands } from "@/pages/session/use-session-commands"
4244
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
4345
import { same } from "@/utils/same"
46+
import { formatServerError } from "@/utils/server-errors"
4447

4548
const emptyUserMessages: UserMessage[] = []
4649

@@ -252,6 +255,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
252255
}
253256

254257
export default function Page() {
258+
const globalSync = useGlobalSync()
255259
const layout = useLayout()
256260
const local = useLocal()
257261
const file = useFile()
@@ -278,6 +282,7 @@ export default function Page() {
278282
})
279283

280284
const [ui, setUi] = createStore({
285+
git: false,
281286
pendingMessage: undefined as string | undefined,
282287
scrollGesture: 0,
283288
scroll: {
@@ -494,6 +499,46 @@ export default function Page() {
494499
return "session.review.noVcs"
495500
})
496501

502+
function upsert(next: Project) {
503+
const list = globalSync.data.project
504+
sync.set("project", next.id)
505+
const idx = list.findIndex((item) => item.id === next.id)
506+
if (idx >= 0) {
507+
globalSync.set(
508+
"project",
509+
list.map((item, i) => (i === idx ? { ...item, ...next } : item)),
510+
)
511+
return
512+
}
513+
const at = list.findIndex((item) => item.id > next.id)
514+
if (at >= 0) {
515+
globalSync.set("project", [...list.slice(0, at), next, ...list.slice(at)])
516+
return
517+
}
518+
globalSync.set("project", [...list, next])
519+
}
520+
521+
function initGit() {
522+
if (ui.git) return
523+
setUi("git", true)
524+
void sdk.client.project
525+
.initGit()
526+
.then((x) => {
527+
if (!x.data) return
528+
upsert(x.data)
529+
})
530+
.catch((err) => {
531+
showToast({
532+
variant: "error",
533+
title: language.t("common.requestFailed"),
534+
description: formatServerError(err, language.t),
535+
})
536+
})
537+
.finally(() => {
538+
setUi("git", false)
539+
})
540+
}
541+
497542
let inputRef!: HTMLDivElement
498543
let promptDock: HTMLDivElement | undefined
499544
let dockHeight = 0
@@ -727,23 +772,28 @@ export default function Page() {
727772
const changesOptions = ["session", "turn"] as const
728773
const changesOptionsList = [...changesOptions]
729774

730-
const changesTitle = () => (
731-
<Select
732-
options={changesOptionsList}
733-
current={store.changes}
734-
label={(option) =>
735-
option === "session" ? language.t("ui.sessionReview.title") : language.t("ui.sessionReview.title.lastTurn")
736-
}
737-
onSelect={(option) => option && setStore("changes", option)}
738-
variant="ghost"
739-
size="small"
740-
valueClass="text-14-medium"
741-
/>
742-
)
775+
const changesTitle = () => {
776+
if (!hasReview()) {
777+
return null
778+
}
779+
780+
return (
781+
<Select
782+
options={changesOptionsList}
783+
current={store.changes}
784+
label={(option) =>
785+
option === "session" ? language.t("ui.sessionReview.title") : language.t("ui.sessionReview.title.lastTurn")
786+
}
787+
onSelect={(option) => option && setStore("changes", option)}
788+
variant="ghost"
789+
size="small"
790+
valueClass="text-14-medium"
791+
/>
792+
)
793+
}
743794

744795
const emptyTurn = () => (
745796
<div class="h-full pb-30 flex flex-col items-center justify-center text-center gap-6">
746-
<Mark class="w-14 opacity-10" />
747797
<div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.noChanges")}</div>
748798
</div>
749799
)
@@ -809,9 +859,23 @@ export default function Page() {
809859
empty={
810860
store.changes === "turn" ? (
811861
emptyTurn()
862+
) : reviewEmptyKey() === "session.review.noVcs" ? (
863+
<div class={input.emptyClass}>
864+
<div class="flex flex-col gap-3">
865+
<div class="text-14-medium text-text-strong">Create a Git repository</div>
866+
<div
867+
class="text-14-regular text-text-base max-w-md"
868+
style={{ "line-height": "var(--line-height-normal)" }}
869+
>
870+
Track, review, and undo changes in this project
871+
</div>
872+
</div>
873+
<Button size="large" disabled={ui.git} onClick={initGit}>
874+
{ui.git ? "Creating Git repository..." : "Create Git repository"}
875+
</Button>
876+
</div>
812877
) : (
813878
<div class={input.emptyClass}>
814-
<Mark class="w-14 opacity-10" />
815879
<div class="text-14-regular text-text-weak max-w-56">{language.t(reviewEmptyKey())}</div>
816880
</div>
817881
)

packages/app/src/pages/session/session-side-panel.tsx

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,21 @@ export function SessionSidePanel(props: {
8787
return out
8888
})
8989

90+
const empty = (msg: string) => (
91+
<div class="h-full flex flex-col">
92+
<div class="h-12 shrink-0" aria-hidden />
93+
<div class="flex-1 pb-30 flex items-center justify-center text-center">
94+
<div class="text-12-regular text-text-weak">{msg}</div>
95+
</div>
96+
</div>
97+
)
98+
99+
const nofiles = createMemo(() => {
100+
const state = file.tree.state("")
101+
if (!state?.loaded) return false
102+
return file.tree.children("").length === 0
103+
})
104+
90105
const normalizeTab = (tab: string) => {
91106
if (!tab.startsWith("file://")) return tab
92107
return file.tab(tab)
@@ -145,17 +160,8 @@ export function SessionSidePanel(props: {
145160

146161
const [store, setStore] = createStore({
147162
activeDraggable: undefined as string | undefined,
148-
fileTreeScrolled: false,
149163
})
150164

151-
let changesEl: HTMLDivElement | undefined
152-
let allEl: HTMLDivElement | undefined
153-
154-
const syncFileTreeScrolled = (el?: HTMLDivElement) => {
155-
const next = (el?.scrollTop ?? 0) > 0
156-
setStore("fileTreeScrolled", (current) => (current === next ? current : next))
157-
}
158-
159165
const handleDragStart = (event: unknown) => {
160166
const id = getDraggableId(event)
161167
if (!id) return
@@ -176,11 +182,6 @@ export function SessionSidePanel(props: {
176182
setStore("activeDraggable", undefined)
177183
}
178184

179-
createEffect(() => {
180-
if (!layout.fileTree.opened()) return
181-
syncFileTreeScrolled(fileTreeTab() === "changes" ? changesEl : allEl)
182-
})
183-
184185
createEffect(() => {
185186
if (!file.ready()) return
186187

@@ -354,7 +355,7 @@ export function SessionSidePanel(props: {
354355
class="h-full"
355356
data-scope="filetree"
356357
>
357-
<Tabs.List data-scrolled={store.fileTreeScrolled ? "" : undefined}>
358+
<Tabs.List>
358359
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
359360
{reviewCount()}{" "}
360361
{language.t(reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other")}
@@ -363,12 +364,7 @@ export function SessionSidePanel(props: {
363364
{language.t("session.files.all")}
364365
</Tabs.Trigger>
365366
</Tabs.List>
366-
<Tabs.Content
367-
value="changes"
368-
ref={(el: HTMLDivElement) => (changesEl = el)}
369-
onScroll={(e: UIEvent & { currentTarget: HTMLDivElement }) => syncFileTreeScrolled(e.currentTarget)}
370-
class="bg-background-stronger px-3 py-0"
371-
>
367+
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
372368
<Switch>
373369
<Match when={hasReview()}>
374370
<Show
@@ -382,6 +378,7 @@ export function SessionSidePanel(props: {
382378
>
383379
<FileTree
384380
path=""
381+
class="pt-3"
385382
allowed={diffFiles()}
386383
kinds={kinds()}
387384
draggable={false}
@@ -390,26 +387,23 @@ export function SessionSidePanel(props: {
390387
/>
391388
</Show>
392389
</Match>
390+
<Match when={true}>{empty(language.t("session.review.noChanges"))}</Match>
391+
</Switch>
392+
</Tabs.Content>
393+
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
394+
<Switch>
395+
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
393396
<Match when={true}>
394-
<div class="mt-8 text-center text-12-regular text-text-weak">
395-
{language.t("session.review.noChanges")}
396-
</div>
397+
<FileTree
398+
path=""
399+
class="pt-3"
400+
modified={diffFiles()}
401+
kinds={kinds()}
402+
onFileClick={(node) => openTab(file.tab(node.path))}
403+
/>
397404
</Match>
398405
</Switch>
399406
</Tabs.Content>
400-
<Tabs.Content
401-
value="all"
402-
ref={(el: HTMLDivElement) => (allEl = el)}
403-
onScroll={(e: UIEvent & { currentTarget: HTMLDivElement }) => syncFileTreeScrolled(e.currentTarget)}
404-
class="bg-background-stronger px-3 py-0"
405-
>
406-
<FileTree
407-
path=""
408-
modified={diffFiles()}
409-
kinds={kinds()}
410-
onFileClick={(node) => openTab(file.tab(node.path))}
411-
/>
412-
</Tabs.Content>
413407
</Tabs>
414408
</div>
415409
<ResizeHandle

packages/ui/src/components/session-review.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,9 @@ export const SessionReview = (props: SessionReviewProps) => {
554554
return (
555555
<div data-component="session-review" class={props.class} classList={props.classList}>
556556
<div data-slot="session-review-header" class={props.classes?.header}>
557-
<div data-slot="session-review-title">{props.title ?? i18n.t("ui.sessionReview.title")}</div>
557+
<div data-slot="session-review-title">
558+
{props.title === undefined ? i18n.t("ui.sessionReview.title") : props.title}
559+
</div>
558560
<div data-slot="session-review-actions">
559561
<Show when={hasDiffs() && props.onDiffStyleChange}>
560562
<RadioGroup

0 commit comments

Comments
 (0)