fix(app): improve tab handling (#30669)
This commit is contained in:
parent
3003867c25
commit
5426478e46
12 changed files with 238 additions and 203 deletions
|
|
@ -8,11 +8,11 @@ import {
|
|||
getSessionPrefetchPromise,
|
||||
setSessionPrefetch,
|
||||
} from "./global-sync/session-prefetch"
|
||||
import { createServerSyncContext } from "./server-sync"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
||||
import { diffs as list, message as clean } from "@/utils/diffs"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
import { createServerSdkContext, useServerSDK } from "./server-sdk"
|
||||
import { type createServerSyncContextInner } from "./server-sync"
|
||||
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
|
||||
|
|
@ -171,8 +171,11 @@ function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: Opti
|
|||
})
|
||||
}
|
||||
|
||||
export const createDirSyncContext = (directory: string, serverSync: ReturnType<typeof createServerSyncContext>) => {
|
||||
const serverSDK = useServerSDK()
|
||||
export const createDirSyncContext = (
|
||||
directory: string,
|
||||
serverSync: ReturnType<typeof createServerSyncContextInner>,
|
||||
serverSDK: ReturnType<typeof createServerSdkContext> = useServerSDK(),
|
||||
) => {
|
||||
const client = serverSDK.createClient({ directory, throwOnError: true })
|
||||
|
||||
type Child = ReturnType<(typeof serverSync)["child"]>
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
|
|||
servers: {
|
||||
list: allServers,
|
||||
health: serverHealth,
|
||||
default: () => allServers().find((s) => ServerConnection.key(s) === props.defaultServer) ?? allServers()[0]!,
|
||||
},
|
||||
settings: {
|
||||
server: {
|
||||
|
|
|
|||
38
packages/app/src/context/layout-helpers.ts
Normal file
38
packages/app/src/context/layout-helpers.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import type { Accessor } from "solid-js"
|
||||
|
||||
export function ensureSessionKey(key: string, touch: (key: string) => void, seed: (key: string) => void) {
|
||||
touch(key)
|
||||
seed(key)
|
||||
return key
|
||||
}
|
||||
|
||||
export function createSessionKeyReader(sessionKey: string | Accessor<string>, ensure: (key: string) => void) {
|
||||
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
|
||||
return () => {
|
||||
const value = key()
|
||||
ensure(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export function pruneSessionKeys(input: {
|
||||
keep?: string
|
||||
max: number
|
||||
used: Map<string, number>
|
||||
view: string[]
|
||||
tabs: string[]
|
||||
}) {
|
||||
if (!input.keep) return []
|
||||
|
||||
const keys = new Set<string>([...input.view, ...input.tabs])
|
||||
if (keys.size <= input.max) return []
|
||||
|
||||
const score = (key: string) => {
|
||||
if (key === input.keep) return Number.MAX_SAFE_INTEGER
|
||||
return input.used.get(key) ?? 0
|
||||
}
|
||||
|
||||
return Array.from(keys)
|
||||
.sort((a, b) => score(b) - score(a))
|
||||
.slice(input.max)
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { createRoot, createSignal } from "solid-js"
|
||||
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout"
|
||||
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
|
||||
|
||||
describe("layout session-key helpers", () => {
|
||||
test("couples touch and scroll seed in order", () => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { createStore, produce } from "solid-js/store"
|
||||
import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { useLocation } from "@solidjs/router"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { useServerSync } from "./server-sync"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
import { useServer } from "./server"
|
||||
import { ServerConnection, useServer } from "./server"
|
||||
import { usePlatform } from "./platform"
|
||||
import { Project } from "@opencode-ai/sdk/v2"
|
||||
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
||||
|
|
@ -13,6 +14,9 @@ import { same } from "@/utils/same"
|
|||
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
|
||||
import { createPathHelpers } from "./file/path"
|
||||
import type { ProjectAvatarVariant } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
|
||||
|
||||
export { createSessionKeyReader, ensureSessionKey, pruneSessionKeys }
|
||||
|
||||
export type { ProjectAvatarVariant }
|
||||
|
||||
|
|
@ -69,42 +73,10 @@ export type LocalProject = Partial<Project> & { worktree: string; expanded: bool
|
|||
|
||||
export type ReviewDiffStyle = "unified" | "split"
|
||||
|
||||
export function ensureSessionKey(key: string, touch: (key: string) => void, seed: (key: string) => void) {
|
||||
touch(key)
|
||||
seed(key)
|
||||
return key
|
||||
}
|
||||
|
||||
export function createSessionKeyReader(sessionKey: string | Accessor<string>, ensure: (key: string) => void) {
|
||||
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
|
||||
return () => {
|
||||
const value = key()
|
||||
ensure(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export function pruneSessionKeys(input: {
|
||||
keep?: string
|
||||
max: number
|
||||
used: Map<string, number>
|
||||
view: string[]
|
||||
tabs: string[]
|
||||
}) {
|
||||
if (!input.keep) return []
|
||||
|
||||
const keys = new Set<string>([...input.view, ...input.tabs])
|
||||
if (keys.size <= input.max) return []
|
||||
|
||||
const score = (key: string) => {
|
||||
if (key === input.keep) return Number.MAX_SAFE_INTEGER
|
||||
return input.used.get(key) ?? 0
|
||||
}
|
||||
|
||||
return Array.from(keys)
|
||||
.sort((a, b) => score(b) - score(a))
|
||||
.slice(input.max)
|
||||
}
|
||||
export type LayoutRoute =
|
||||
| { type: "home" }
|
||||
| { type: "dir-new-sesssion"; dir: string; dirBase64: string; server?: ServerConnection.Key }
|
||||
| { type: "session"; dir: string; dirBase64: string; sessionId: string; server?: ServerConnection.Key }
|
||||
|
||||
function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string): SessionTabs {
|
||||
const all = current?.all ?? []
|
||||
|
|
@ -146,6 +118,21 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
|
|||
}
|
||||
}
|
||||
|
||||
const currentRoute = (pathname: string): LayoutRoute => {
|
||||
const parts = pathname.split("/").filter(Boolean)
|
||||
if (parts.length === 0) return { type: "home" }
|
||||
|
||||
const dirBase64 = parts[0]
|
||||
const dir = decode64(dirBase64)
|
||||
if (!dir) return { type: "home" }
|
||||
|
||||
if (parts[1] !== "session") return { type: "home" }
|
||||
|
||||
const id = parts[2]
|
||||
if (id) return { type: "session", dir, dirBase64, sessionId: id }
|
||||
return { type: "dir-new-sesssion", dir, dirBase64 }
|
||||
}
|
||||
|
||||
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
|
||||
name: "Layout",
|
||||
init: () => {
|
||||
|
|
@ -153,6 +140,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||
const serverSync = useServerSync()
|
||||
const server = useServer()
|
||||
const platform = usePlatform()
|
||||
const location = useLocation()
|
||||
const route = createMemo(() => currentRoute(location.pathname))
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
|
|
@ -557,6 +546,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||
})
|
||||
|
||||
return {
|
||||
route,
|
||||
ready,
|
||||
handoff: {
|
||||
tabs: createMemo(() => store.handoff?.tabs),
|
||||
|
|
|
|||
|
|
@ -1,17 +1,7 @@
|
|||
import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import {
|
||||
batch,
|
||||
createContext,
|
||||
createEffect,
|
||||
getOwner,
|
||||
onCleanup,
|
||||
onMount,
|
||||
type ParentProps,
|
||||
untrack,
|
||||
useContext,
|
||||
} from "solid-js"
|
||||
import { batch, getOwner, onCleanup, onMount, untrack } from "solid-js"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import type { InitError } from "../pages/error"
|
||||
|
|
@ -86,7 +76,7 @@ function makeQueryOptionsApi(serverSDK: () => OpencodeClient, sdkFor: (dir: Path
|
|||
}
|
||||
export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
|
||||
|
||||
export function createServerSyncContext(_serverSDK?: ServerSDK) {
|
||||
export function createServerSyncContextInner(_serverSDK?: ServerSDK) {
|
||||
const serverSDK: ServerSDK = _serverSDK ?? useServerSDK()
|
||||
const language = useLanguage()
|
||||
const owner = getOwner()
|
||||
|
|
@ -476,6 +466,17 @@ export function createServerSyncContext(_serverSDK?: ServerSDK) {
|
|||
}
|
||||
}
|
||||
|
||||
export function createServerSyncContext(_serverSDK?: ServerSDK) {
|
||||
const inner = createServerSyncContextInner(_serverSDK)
|
||||
return Object.assign(inner, {
|
||||
createDirSyncContext: createRefCountMap(
|
||||
(dir) => createDirSyncContext(dir, inner, _serverSDK),
|
||||
(dir) => inner.disableMcp(dir),
|
||||
directoryKey,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({
|
||||
name: "ServerSync",
|
||||
init: (props: { server?: ServerConnection.Any }) => {
|
||||
|
|
@ -487,13 +488,7 @@ export const { use: useServerSync, provider: ServerSyncProvider } = createSimple
|
|||
if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable"))
|
||||
const ctx = global.createServerCtx(conn)
|
||||
|
||||
return Object.assign(ctx.sync, {
|
||||
createDirSyncContext: createRefCountMap(
|
||||
(dir) => createDirSyncContext(dir, ctx.sync),
|
||||
(dir) => ctx.sync.disableMcp(dir),
|
||||
directoryKey,
|
||||
),
|
||||
})
|
||||
return ctx.sync
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -159,10 +159,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||
init: () => {
|
||||
const [store, setStore, _, ready] = persisted("settings.v3", createStore<Settings>(defaultSettings))
|
||||
|
||||
createEffect(() => {
|
||||
console.log("settings", { ready: ready() })
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof document === "undefined") return
|
||||
const root = document.documentElement
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue