diff options
| -rw-r--r-- | packages/app/src/i18n/en.ts | 3 | ||||
| -rw-r--r-- | packages/app/src/index.css | 28 | ||||
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 37 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 98 | ||||
| -rw-r--r-- | packages/app/src/pages/session/session-side-panel.tsx | 68 | ||||
| -rw-r--r-- | packages/ui/src/components/session-review.tsx | 4 | ||||
| -rw-r--r-- | packages/ui/src/components/tabs.css | 6 |
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"] { |
