Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/app/src/context/tabs.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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,
Expand Down
187 changes: 61 additions & 126 deletions packages/app/src/pages/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -62,8 +59,6 @@ type HomeSessionRecord = {
projectName: string
}

type HomeSessionSync = Pick<ReturnType<typeof useServerSync>, "child">

type HomeSessionGroup = {
id: "today" | "yesterday" | "older"
title: string
Expand Down Expand Up @@ -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}`
}
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -461,7 +409,7 @@ function HomeDesign() {
{(record) => (
<HomeSessionRow
record={record}
sync={focusedSync()}
server={state.selection.server}
activeServer={state.selection.server === server.key}
openSession={openSession}
/>
Expand Down Expand Up @@ -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 (
<ProjectAvatar
fallback={displayName(props.project)}
src={getProjectAvatarSource(props.project.id, props.project.icon)}
variant={getProjectAvatarVariant(props.project.icon?.color)}
unread={state.unread()}
loading={state.loading()}
/>
)
}

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 (
<div class="relative shrink-0">
<Show when={hasOpenTab()}>
<span
aria-hidden="true"
class="pointer-events-none absolute top-1/2 h-[7px] w-[3px] -translate-y-1/2 rounded-[2px] bg-v2-background-bg-layer-04"
style={{ right: "calc(100% + 12px)" }}
/>
</Show>
<HomeSessionAvatar
project={props.project}
session={props.session}
activeServer={props.activeServer}
/>
</div>
)
}

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
Expand Down Expand Up @@ -827,7 +816,7 @@ function HomeSessionSearch(props: {
{(record) => (
<HomeSessionSearchResultRow
record={record}
sync={props.sync}
server={props.server}
activeServer={props.activeServer}
selected={store.active === homeSessionSearchKey(record)}
onHighlight={() => setStore("active", homeSessionSearchKey(record))}
Expand Down Expand Up @@ -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)
Expand All @@ -943,34 +927,12 @@ function HomeSessionSearchResultRow(props: {
onMouseEnter={() => props.onHighlight()}
onClick={() => props.onSelect(props.record.session)}
>
<Show
when={status.show()}
fallback={
<div class="flex size-4 shrink-0 items-center justify-center">
<TabStateIndicator />
</div>
}
>
<div
class="flex size-4 shrink-0 items-center justify-center"
style={{ color: status.tint() ?? "var(--icon-interactive-base)" }}
>
<Switch>
<Match when={status.isWorking()}>
<Spinner class="size-[15px]" />
</Match>
<Match when={status.hasPermissions()}>
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
</Match>
<Match when={status.hasError()}>
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
</Match>
<Match when={status.unseenCount() > 0}>
<div class="size-1.5 rounded-full bg-text-interactive-base" />
</Match>
</Switch>
</div>
</Show>
<HomeSessionLeading
project={props.record.project}
session={props.record.session}
server={props.server}
activeServer={props.activeServer}
/>
<div class="flex min-w-0 flex-1 items-center gap-1.5">
<span
class={`${HOME_SEARCH_RESULT_TITLE} ${props.record.projectName ? "max-w-[min(70%,480px)] flex-[0_1_auto]" : "flex-[1_1_auto]"}`}
Expand Down Expand Up @@ -1010,15 +972,10 @@ function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => 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 (
Expand All @@ -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)}
>
<Show
when={status.show()}
fallback={
<div class="flex size-4 shrink-0 items-center justify-center">
<TabStateIndicator />
</div>
}
>
<div
class="flex size-4 shrink-0 items-center justify-center"
style={{ color: status.tint() ?? "var(--icon-interactive-base)" }}
>
<Switch>
<Match when={status.isWorking()}>
<Spinner class="size-[15px]" />
</Match>
<Match when={status.hasPermissions()}>
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
</Match>
<Match when={status.hasError()}>
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
</Match>
<Match when={status.unseenCount() > 0}>
<div class="size-1.5 rounded-full bg-text-interactive-base" />
</Match>
</Switch>
</div>
</Show>
<HomeSessionLeading
project={props.record.project}
session={props.record.session}
server={props.server}
activeServer={props.activeServer}
/>
<span
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-v2-text-text-base [font-weight:530] ${props.record.projectName ? "max-w-[min(70%,480px)] flex-[0_1_auto]" : "flex-[1_1_auto]"}`}
>
Expand Down
1 change: 1 addition & 0 deletions packages/ui/script/colors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/src/styles/tailwind/colors.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
2 changes: 2 additions & 0 deletions packages/ui/src/theme/themes/oc-2.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This override is applied after the semantic mapping in resolveThemeVariantV2(), so OC-2 still resolves bg-layer-04 to the pre-tuning colors (grey-400 here and grey-700 in dark mode). Please regenerate oc-2.json with packages/ui/script/build-oc2-v2-overrides.ts after updating theme.css so the open-tab indicator receives the tuned values.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

grey-400 and gray-700 is correct as per Figma, I only changed the tokens for generated themes to bring their contrast closer to OC-2

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the other OC-2 tokens were outdated; the design system must've been updated... I made a quick PR to fix: #31071

"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)",
Expand Down Expand Up @@ -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)",
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/theme/v2/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const light: Record<string, V2ColorValue> = {
"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"),
Expand Down Expand Up @@ -77,6 +78,7 @@ const dark: Record<string, V2ColorValue> = {
"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"),
Expand Down
Loading
Loading