feat(app): sessions list improvements (#30941)
This commit is contained in:
parent
9ed17da55a
commit
1fd9c77744
@ -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,
|
||||
|
||||
@ -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"
|
||||
@ -23,26 +22,24 @@ import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||
import { DialogSelectServer, useServerManagementController } from "@/components/dialog-select-server"
|
||||
import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2"
|
||||
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"
|
||||
@ -65,8 +62,6 @@ type HomeSessionRecord = {
|
||||
projectName: string
|
||||
}
|
||||
|
||||
type HomeSessionSync = Pick<ReturnType<typeof useServerSync>, "child">
|
||||
|
||||
type HomeSessionGroup = {
|
||||
id: "today" | "yesterday" | "older"
|
||||
title: string
|
||||
@ -113,53 +108,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}`
|
||||
}
|
||||
@ -424,7 +372,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) => {
|
||||
@ -464,7 +412,7 @@ function HomeDesign() {
|
||||
{(record) => (
|
||||
<HomeSessionRow
|
||||
record={record}
|
||||
sync={focusedSync()}
|
||||
server={state.selection.server}
|
||||
activeServer={state.selection.server === server.key}
|
||||
openSession={openSession}
|
||||
/>
|
||||
@ -748,13 +696,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
|
||||
@ -870,7 +859,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))}
|
||||
@ -956,17 +945,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)
|
||||
@ -986,34 +970,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]"}`}
|
||||
@ -1053,15 +1015,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 (
|
||||
@ -1071,34 +1028,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]"}`}
|
||||
>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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)",
|
||||
|
||||
@ -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"),
|
||||
@ -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"),
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user