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, 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")} +
+
+
+ + +
-
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 = () => ( - + 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")}
) @@ -809,9 +859,23 @@ 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())}
) 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) => ( +
+
+
+
{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) @@ -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 @@ -176,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 @@ -354,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")} @@ -363,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" - > + - -
- {language.t("session.review.noChanges")} -
-
+ {empty(language.t("session.review.noChanges"))}
- (allEl = el)} - onScroll={(e: UIEvent & { currentTarget: HTMLDivElement }) => 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 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 (
-
{props.title ?? i18n.t("ui.sessionReview.title")}
+
+ {props.title === undefined ? i18n.t("ui.sessionReview.title") : props.title} +