summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/app/src/i18n/en.ts3
-rw-r--r--packages/app/src/index.css28
-rw-r--r--packages/app/src/pages/layout.tsx37
-rw-r--r--packages/app/src/pages/session.tsx98
-rw-r--r--packages/app/src/pages/session/session-side-panel.tsx68
-rw-r--r--packages/ui/src/components/session-review.tsx4
-rw-r--r--packages/ui/src/components/tabs.css6
7 files changed, 168 insertions, 76 deletions
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 7e95fd739..8b672d437 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/index.css b/packages/app/src/index.css
index 4af87bca6..9e231e2d2 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 f6165461b..cf2c3b6c4 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<string, string>,
workspaceBranchName: {} as Record<string, Record<string, string>>,
workspaceExpanded: {} as Record<string, boolean>,
+ gettingStartedDismissed: false,
}),
)
@@ -2006,25 +2007,31 @@ export default function Layout(props: ParentProps) {
</Show>
<div
- class="shrink-0 px-2 py-3 border-t border-border-weak-base"
+ class="shrink-0 px-3 py-3"
classList={{
- hidden: !(providers.all().length > 0 && providers.paid().length === 0),
+ hidden: store.gettingStartedDismissed || !(providers.all().length > 0 && providers.paid().length === 0),
}}
>
- <div class="rounded-md bg-background-base shadow-xs-border-base">
- <div class="p-3 flex flex-col gap-2">
- <div class="text-12-medium text-text-strong">{language.t("sidebar.gettingStarted.title")}</div>
- <div class="text-text-base">{language.t("sidebar.gettingStarted.line1")}</div>
- <div class="text-text-base">{language.t("sidebar.gettingStarted.line2")}</div>
+ <div class="rounded-xl bg-background-base shadow-xs-border-base" data-component="getting-started">
+ <div class="p-3 flex flex-col gap-6">
+ <div class="flex flex-col gap-2">
+ <div class="text-14-medium text-text-strong">{language.t("sidebar.gettingStarted.title")}</div>
+ <div class="text-14-regular text-text-base" style={{ "line-height": "var(--line-height-normal)" }}>
+ {language.t("sidebar.gettingStarted.line1")}
+ </div>
+ <div class="text-14-regular text-text-base" style={{ "line-height": "var(--line-height-normal)" }}>
+ {language.t("sidebar.gettingStarted.line2")}
+ </div>
+ </div>
+ <div data-component="getting-started-actions">
+ <Button size="large" icon="plus-small" onClick={connectProvider}>
+ {language.t("command.provider.connect")}
+ </Button>
+ <Button size="large" variant="ghost" onClick={() => setStore("gettingStartedDismissed", true)}>
+ Not yet
+ </Button>
+ </div>
</div>
- <Button
- 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"
- size="large"
- icon="plus"
- onClick={connectProvider}
- >
- {language.t("command.provider.connect")}
- </Button>
</div>
</div>
</div>
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 1476e616e..f6f6576c4 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,
@@ -20,11 +20,13 @@ 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 { 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
@@ -727,23 +772,28 @@ export default function Page() {
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="small"
- valueClass="text-14-medium"
- />
- )
+ const changesTitle = () => {
+ if (!hasReview()) {
+ return null
+ }
+
+ return (
+ <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="small"
+ valueClass="text-14-medium"
+ />
+ )
+ }
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>
)
@@ -809,9 +859,23 @@ export default function Page() {
empty={
store.changes === "turn" ? (
emptyTurn()
+ ) : reviewEmptyKey() === "session.review.noVcs" ? (
+ <div class={input.emptyClass}>
+ <div class="flex flex-col gap-3">
+ <div class="text-14-medium text-text-strong">Create a Git repository</div>
+ <div
+ class="text-14-regular text-text-base max-w-md"
+ style={{ "line-height": "var(--line-height-normal)" }}
+ >
+ Track, review, and undo changes in this project
+ </div>
+ </div>
+ <Button size="large" disabled={ui.git} onClick={initGit}>
+ {ui.git ? "Creating Git repository..." : "Create Git repository"}
+ </Button>
+ </div>
) : (
<div class={input.emptyClass}>
- <Mark class="w-14 opacity-10" />
<div class="text-14-regular text-text-weak max-w-56">{language.t(reviewEmptyKey())}</div>
</div>
)
diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx
index ad802d15d..66d4382c0 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) => (
+ <div class="h-full flex flex-col">
+ <div class="h-12 shrink-0" aria-hidden />
+ <div class="flex-1 pb-30 flex items-center justify-center text-center">
+ <div class="text-12-regular text-text-weak">{msg}</div>
+ </div>
+ </div>
+ )
+
+ 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)
@@ -145,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
@@ -177,11 +183,6 @@ export function SessionSidePanel(props: {
}
createEffect(() => {
- if (!layout.fileTree.opened()) return
- syncFileTreeScrolled(fileTreeTab() === "changes" ? changesEl : allEl)
- })
-
- createEffect(() => {
if (!file.ready()) return
setSessionHandoff(sessionKey(), {
@@ -354,7 +355,7 @@ export function SessionSidePanel(props: {
class="h-full"
data-scope="filetree"
>
- <Tabs.List data-scrolled={store.fileTreeScrolled ? "" : undefined}>
+ <Tabs.List>
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
{reviewCount()}{" "}
{language.t(reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other")}
@@ -363,12 +364,7 @@ export function SessionSidePanel(props: {
{language.t("session.files.all")}
</Tabs.Trigger>
</Tabs.List>
- <Tabs.Content
- value="changes"
- ref={(el: HTMLDivElement) => (changesEl = el)}
- onScroll={(e: UIEvent & { currentTarget: HTMLDivElement }) => syncFileTreeScrolled(e.currentTarget)}
- class="bg-background-stronger px-3 py-0"
- >
+ <Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
<Switch>
<Match when={hasReview()}>
<Show
@@ -382,6 +378,7 @@ export function SessionSidePanel(props: {
>
<FileTree
path=""
+ class="pt-3"
allowed={diffFiles()}
kinds={kinds()}
draggable={false}
@@ -390,26 +387,23 @@ export function SessionSidePanel(props: {
/>
</Show>
</Match>
+ <Match when={true}>{empty(language.t("session.review.noChanges"))}</Match>
+ </Switch>
+ </Tabs.Content>
+ <Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
+ <Switch>
+ <Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
<Match when={true}>
- <div class="mt-8 text-center text-12-regular text-text-weak">
- {language.t("session.review.noChanges")}
- </div>
+ <FileTree
+ path=""
+ class="pt-3"
+ modified={diffFiles()}
+ kinds={kinds()}
+ onFileClick={(node) => openTab(file.tab(node.path))}
+ />
</Match>
</Switch>
</Tabs.Content>
- <Tabs.Content
- value="all"
- ref={(el: HTMLDivElement) => (allEl = el)}
- onScroll={(e: UIEvent & { currentTarget: HTMLDivElement }) => syncFileTreeScrolled(e.currentTarget)}
- class="bg-background-stronger px-3 py-0"
- >
- <FileTree
- path=""
- modified={diffFiles()}
- kinds={kinds()}
- onFileClick={(node) => openTab(file.tab(node.path))}
- />
- </Tabs.Content>
</Tabs>
</div>
<ResizeHandle
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx
index 25a646ace..62c70e864 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 (
<div data-component="session-review" class={props.class} classList={props.classList}>
<div data-slot="session-review-header" class={props.classes?.header}>
- <div data-slot="session-review-title">{props.title ?? i18n.t("ui.sessionReview.title")}</div>
+ <div data-slot="session-review-title">
+ {props.title === undefined ? i18n.t("ui.sessionReview.title") : props.title}
+ </div>
<div data-slot="session-review-actions">
<Show when={hasDiffs() && props.onDiffStyleChange}>
<RadioGroup
diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css
index 51917489e..1bf00b785 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"] {