diff --git a/packages/app/src/context/tabs.tsx b/packages/app/src/context/tabs.tsx index 32c11466466d..b5840a733c05 100644 --- a/packages/app/src/context/tabs.tsx +++ b/packages/app/src/context/tabs.tsx @@ -1,4 +1,6 @@ +import type { Session } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" +import { base64Encode } from "@opencode-ai/core/util/encode" import { createStore, produce } from "solid-js/store" import { Persist, persisted } from "@/utils/persist" import { ServerConnection, useServer } from "./server" @@ -18,6 +20,17 @@ export type Tab = SessionTab export const tabHref = (tab: Tab) => `/${tab.dirBase64}/session/${tab.sessionId}` export const tabKey = (tab: Tab) => `${tab.server}\n${tabHref(tab)}` +export function sessionHasOpenTab(tabs: Tab[], server: ServerConnection.Key, session: Session) { + const dirBase64 = base64Encode(session.directory) + return tabs.some( + (tab) => + tab.type === "session" && + tab.server === server && + tab.dirBase64 === dirBase64 && + tab.sessionId === session.id, + ) +} + export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ name: "Tabs", gate: false, diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index b15e528a1ad9..de7edfa7bbf7 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -11,7 +11,6 @@ import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" -import { TabStateIndicator } from "@opencode-ai/ui/v2/tab-state-indicator" import { getProjectAvatarVariant, useLayout, type LocalProject } from "@/context/layout" import { useNavigate } from "@solidjs/router" import { base64Encode } from "@opencode-ai/core/util/encode" @@ -22,26 +21,24 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogSelectServer } from "@/components/dialog-select-server" import { ServerConnection, useServer } from "@/context/server" +import { sessionHasOpenTab, useTabs } from "@/context/tabs" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useNotification } from "@/context/notification" -import { usePermission } from "@/context/permission" import { closeHomeProject, displayName, getProjectAvatarSource, homeProjectDirectories, homeProjectNavigation, - homeSessionServerStatus, type HomeProjectSelection, projectForSession, sortedRootSessions, toggleHomeProjectSelection, } from "@/pages/layout/helpers" +import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state" import { sessionTitle } from "@/utils/session-title" import { pathKey } from "@/utils/path-key" -import { messageAgentColor } from "@/utils/agent" -import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree" import { useGlobal } from "@/context/global" import { useCommand } from "@/context/command" import { useSettings } from "@/context/settings" @@ -62,8 +59,6 @@ type HomeSessionRecord = { projectName: string } -type HomeSessionSync = Pick, "child"> - type HomeSessionGroup = { id: "today" | "yesterday" | "older" title: string @@ -110,53 +105,6 @@ function matchesHomeSessionSearch(record: HomeSessionRecord, query: string) { return `${record.session.title} ${record.projectName}`.toLowerCase().includes(query) } -function createHomeSessionStatus(input: { - record: () => HomeSessionRecord - sync: () => HomeSessionSync - activeServer: () => boolean -}) { - const notification = useNotification() - const permission = usePermission() - const sessionStore = createMemo(() => input.sync().child(input.record().session.directory, { bootstrap: false })[0]) - const unseenCount = createMemo(() => - input.activeServer() ? notification.session.unseenCount(input.record().session.id) : 0, - ) - const hasError = createMemo( - () => input.activeServer() && notification.session.unseenHasError(input.record().session.id), - ) - const hasPermissions = createMemo( - () => - input.activeServer() && - !!sessionPermissionRequest( - sessionStore().session, - sessionStore().permission, - input.record().session.id, - (item) => { - return !permission.autoResponds(item, input.record().session.directory) - }, - ), - ) - const serverStatus = createMemo(() => - homeSessionServerStatus(input.activeServer(), () => ({ - working: sessionStore().session_working(input.record().session.id), - tint: messageAgentColor(sessionStore().message[input.record().session.id], sessionStore().agent), - })), - ) - const isWorking = createMemo(() => { - if (hasPermissions()) return false - return serverStatus().working - }) - const tint = createMemo(() => serverStatus().tint) - return { - unseenCount, - hasError, - hasPermissions, - isWorking, - tint, - show: createMemo(() => isWorking() || hasPermissions() || hasError() || unseenCount() > 0), - } -} - function homeSessionSearchKey(record: HomeSessionRecord) { return `${pathKey(record.session.directory)}:${record.session.id}` } @@ -421,7 +369,7 @@ function HomeDesign() { open={searchOpen()} loading={sessionLoad.isLoading} results={searchResults()} - sync={focusedSync()} + server={state.selection.server} activeServer={state.selection.server === server.key} noResultsLabel={language.t("home.sessions.search.noResults", { query: search() })} bindFocus={(focus) => { @@ -461,7 +409,7 @@ function HomeDesign() { {(record) => ( @@ -705,13 +653,54 @@ function HomeProjectAvatar(props: { project: LocalProject }) { ) } +function HomeSessionAvatar(props: { project: LocalProject; session: Session; activeServer: boolean }) { + const directory = () => props.session.directory + const sessionId = () => props.session.id + const state = useSessionTabAvatarState(directory, sessionId, () => props.activeServer) + return ( + + ) +} + +function HomeSessionLeading(props: { + project: LocalProject + session: Session + server: ServerConnection.Key + activeServer: boolean +}) { + const tabs = useTabs() + const hasOpenTab = createMemo(() => sessionHasOpenTab(tabs.store, props.server, props.session)) + return ( +
+ + + +
+ ) +} + function HomeSessionSearch(props: { value: string placeholder: string open: boolean loading: boolean results: HomeSessionRecord[] - sync: HomeSessionSync + server: ServerConnection.Key activeServer: boolean noResultsLabel: string bindFocus: (focus: () => void) => void @@ -827,7 +816,7 @@ function HomeSessionSearch(props: { {(record) => ( setStore("active", homeSessionSearchKey(record))} @@ -913,17 +902,12 @@ function HomeSessionSearch(props: { function HomeSessionSearchResultRow(props: { record: HomeSessionRecord - sync: HomeSessionSync + server: ServerConnection.Key activeServer: boolean selected: boolean onHighlight: () => void onSelect: (session: Session) => void }) { - const status = createHomeSessionStatus({ - record: () => props.record, - sync: () => props.sync, - activeServer: () => props.activeServer, - }) const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) const key = () => homeSessionSearchKey(props.record) @@ -943,34 +927,12 @@ function HomeSessionSearchResultRow(props: { onMouseEnter={() => props.onHighlight()} onClick={() => props.onSelect(props.record.session)} > - - - - } - > -
- - - - - -
- - -
- - 0}> -
- - -
- +
voi function HomeSessionRow(props: { record: HomeSessionRecord - sync: HomeSessionSync + server: ServerConnection.Key activeServer: boolean openSession: (session: Session) => void }) { - const status = createHomeSessionStatus({ - record: () => props.record, - sync: () => props.sync, - activeServer: () => props.activeServer, - }) const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) return ( @@ -1028,34 +985,12 @@ function HomeSessionRow(props: { class={`${HOME_ROW} h-10 gap-2 px-6 py-3 pl-4`} onClick={() => props.openSession(props.record.session)} > - - -
- } - > -
- - - - - -
- - -
- - 0}> -
- - -
- + diff --git a/packages/ui/script/colors.txt b/packages/ui/script/colors.txt index 6f5585c7a4fa..4d077491e979 100644 --- a/packages/ui/script/colors.txt +++ b/packages/ui/script/colors.txt @@ -233,6 +233,7 @@ --v2-background-bg-layer-01: var(--v2-grey-200); --v2-background-bg-layer-02: var(--v2-grey-300); --v2-background-bg-layer-03: var(--v2-grey-400); +--v2-background-bg-layer-04: var(--v2-grey-600); --v2-background-bg-inverse: var(--v2-grey-1000); --v2-background-bg-contrast: var(--v2-grey-900); --v2-background-bg-button-neutral: var(--v2-grey-100); diff --git a/packages/ui/src/styles/tailwind/colors.css b/packages/ui/src/styles/tailwind/colors.css index a59f19e7814a..d5884a319fee 100644 --- a/packages/ui/src/styles/tailwind/colors.css +++ b/packages/ui/src/styles/tailwind/colors.css @@ -237,6 +237,7 @@ --color-v2-background-bg-layer-01: var(--v2-background-bg-layer-01); --color-v2-background-bg-layer-02: var(--v2-background-bg-layer-02); --color-v2-background-bg-layer-03: var(--v2-background-bg-layer-03); + --color-v2-background-bg-layer-04: var(--v2-background-bg-layer-04); --color-v2-background-bg-inverse: var(--v2-background-bg-inverse); --color-v2-background-bg-contrast: var(--v2-background-bg-contrast); --color-v2-background-bg-button-neutral: var(--v2-background-bg-button-neutral); @@ -281,4 +282,4 @@ --color-v2-state-bg-info: var(--v2-state-bg-info); --color-v2-state-fg-info: var(--v2-state-fg-info); --color-v2-state-border-info: var(--v2-state-border-info); -} +} \ No newline at end of file diff --git a/packages/ui/src/theme/themes/oc-2.json b/packages/ui/src/theme/themes/oc-2.json index c14799c0b78e..5ce07a72be16 100644 --- a/packages/ui/src/theme/themes/oc-2.json +++ b/packages/ui/src/theme/themes/oc-2.json @@ -157,6 +157,7 @@ "v2-background-bg-layer-01": "var(--v2-grey-200)", "v2-background-bg-layer-02": "var(--v2-grey-300)", "v2-background-bg-layer-03": "var(--v2-grey-400)", + "v2-background-bg-layer-04": "var(--v2-grey-400)", "v2-background-bg-inverse": "var(--v2-grey-1000)", "v2-background-bg-contrast": "var(--v2-grey-900)", "v2-background-bg-button-neutral": "var(--v2-grey-100)", @@ -386,6 +387,7 @@ "v2-background-bg-layer-01": "var(--v2-grey-900)", "v2-background-bg-layer-02": "var(--v2-grey-800)", "v2-background-bg-layer-03": "var(--v2-grey-700)", + "v2-background-bg-layer-04": "var(--v2-grey-700)", "v2-background-bg-inverse": "var(--v2-grey-100)", "v2-background-bg-contrast": "var(--v2-grey-700)", "v2-background-bg-button-neutral": "var(--v2-alpha-light-6)", diff --git a/packages/ui/src/theme/v2/mapping.ts b/packages/ui/src/theme/v2/mapping.ts index e998c432794c..bda42dc4276f 100644 --- a/packages/ui/src/theme/v2/mapping.ts +++ b/packages/ui/src/theme/v2/mapping.ts @@ -9,6 +9,7 @@ const light: Record = { "v2-background-bg-layer-01": ref("v2-grey-300"), "v2-background-bg-layer-02": ref("v2-grey-400"), "v2-background-bg-layer-03": ref("v2-grey-500"), + "v2-background-bg-layer-04": ref("v2-grey-600"), "v2-background-bg-inverse": ref("v2-grey-1000"), "v2-background-bg-contrast": ref("v2-grey-900"), "v2-background-bg-button-neutral": ref("v2-grey-100"), @@ -77,6 +78,7 @@ const dark: Record = { "v2-background-bg-layer-01": ref("v2-grey-800"), "v2-background-bg-layer-02": ref("v2-grey-600"), "v2-background-bg-layer-03": ref("v2-grey-500"), + "v2-background-bg-layer-04": ref("v2-grey-400"), "v2-background-bg-inverse": ref("v2-grey-100"), "v2-background-bg-contrast": ref("v2-grey-700"), "v2-background-bg-button-neutral": ref("v2-alpha-light-6"), diff --git a/packages/ui/src/v2/styles/theme.css b/packages/ui/src/v2/styles/theme.css index 5fd4194af847..3a588af2f38e 100644 --- a/packages/ui/src/v2/styles/theme.css +++ b/packages/ui/src/v2/styles/theme.css @@ -8,6 +8,7 @@ --v2-background-bg-layer-01: var(--v2-grey-200); --v2-background-bg-layer-02: var(--v2-grey-300); --v2-background-bg-layer-03: var(--v2-grey-400); + --v2-background-bg-layer-04: var(--v2-grey-600); --v2-background-bg-inverse: var(--v2-grey-1000); --v2-background-bg-contrast: var(--v2-grey-900); --v2-background-bg-button-neutral: var(--v2-grey-100); @@ -229,6 +230,7 @@ --v2-background-bg-layer-01: var(--v2-grey-200); --v2-background-bg-layer-02: var(--v2-grey-300); --v2-background-bg-layer-03: var(--v2-grey-400); + --v2-background-bg-layer-04: var(--v2-grey-600); --v2-background-bg-inverse: var(--v2-grey-1000); --v2-background-bg-contrast: var(--v2-grey-900); --v2-background-bg-button-neutral: var(--v2-grey-100); @@ -337,6 +339,7 @@ --v2-background-bg-layer-01: var(--v2-grey-900); --v2-background-bg-layer-02: var(--v2-grey-800); --v2-background-bg-layer-03: var(--v2-grey-700); + --v2-background-bg-layer-04: var(--v2-grey-600); --v2-background-bg-inverse: var(--v2-grey-100); --v2-background-bg-contrast: var(--v2-grey-700); --v2-background-bg-button-neutral: var(--v2-alpha-light-6);