Skip to content

Commit 5426478

Browse files
authored
fix(app): improve tab handling (anomalyco#30669)
1 parent 3003867 commit 5426478

12 files changed

Lines changed: 238 additions & 203 deletions

File tree

packages/app/src/components/titlebar.tsx

Lines changed: 128 additions & 118 deletions
Large diffs are not rendered by default.

packages/app/src/context/directory-sync.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import {
88
getSessionPrefetchPromise,
99
setSessionPrefetch,
1010
} from "./global-sync/session-prefetch"
11-
import { createServerSyncContext } from "./server-sync"
1211
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
1312
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
1413
import { diffs as list, message as clean } from "@/utils/diffs"
15-
import { useServerSDK } from "./server-sdk"
14+
import { createServerSdkContext, useServerSDK } from "./server-sdk"
15+
import { type createServerSyncContextInner } from "./server-sync"
1616

1717
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
1818

@@ -171,8 +171,11 @@ function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: Opti
171171
})
172172
}
173173

174-
export const createDirSyncContext = (directory: string, serverSync: ReturnType<typeof createServerSyncContext>) => {
175-
const serverSDK = useServerSDK()
174+
export const createDirSyncContext = (
175+
directory: string,
176+
serverSync: ReturnType<typeof createServerSyncContextInner>,
177+
serverSDK: ReturnType<typeof createServerSdkContext> = useServerSDK(),
178+
) => {
176179
const client = serverSDK.createClient({ directory, throwOnError: true })
177180

178181
type Child = ReturnType<(typeof serverSync)["child"]>

packages/app/src/context/global.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
7373
servers: {
7474
list: allServers,
7575
health: serverHealth,
76+
default: () => allServers().find((s) => ServerConnection.key(s) === props.defaultServer) ?? allServers()[0]!,
7677
},
7778
settings: {
7879
server: {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { Accessor } from "solid-js"
2+
3+
export function ensureSessionKey(key: string, touch: (key: string) => void, seed: (key: string) => void) {
4+
touch(key)
5+
seed(key)
6+
return key
7+
}
8+
9+
export function createSessionKeyReader(sessionKey: string | Accessor<string>, ensure: (key: string) => void) {
10+
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
11+
return () => {
12+
const value = key()
13+
ensure(value)
14+
return value
15+
}
16+
}
17+
18+
export function pruneSessionKeys(input: {
19+
keep?: string
20+
max: number
21+
used: Map<string, number>
22+
view: string[]
23+
tabs: string[]
24+
}) {
25+
if (!input.keep) return []
26+
27+
const keys = new Set<string>([...input.view, ...input.tabs])
28+
if (keys.size <= input.max) return []
29+
30+
const score = (key: string) => {
31+
if (key === input.keep) return Number.MAX_SAFE_INTEGER
32+
return input.used.get(key) ?? 0
33+
}
34+
35+
return Array.from(keys)
36+
.sort((a, b) => score(b) - score(a))
37+
.slice(input.max)
38+
}

packages/app/src/context/layout.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test } from "bun:test"
22
import { createRoot, createSignal } from "solid-js"
3-
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout"
3+
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
44

55
describe("layout session-key helpers", () => {
66
test("couples touch and scroll seed in order", () => {

packages/app/src/context/layout.tsx

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { createStore, produce } from "solid-js/store"
22
import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
3+
import { useLocation } from "@solidjs/router"
34
import { createSimpleContext } from "@opencode-ai/ui/context"
45
import { makeEventListener } from "@solid-primitives/event-listener"
56
import { useServerSync } from "./server-sync"
67
import { useServerSDK } from "./server-sdk"
7-
import { useServer } from "./server"
8+
import { ServerConnection, useServer } from "./server"
89
import { usePlatform } from "./platform"
910
import { Project } from "@opencode-ai/sdk/v2"
1011
import { Persist, persisted, removePersisted } from "@/utils/persist"
@@ -13,6 +14,9 @@ import { same } from "@/utils/same"
1314
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
1415
import { createPathHelpers } from "./file/path"
1516
import type { ProjectAvatarVariant } from "@opencode-ai/ui/v2/project-avatar-v2"
17+
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
18+
19+
export { createSessionKeyReader, ensureSessionKey, pruneSessionKeys }
1620

1721
export type { ProjectAvatarVariant }
1822

@@ -69,42 +73,10 @@ export type LocalProject = Partial<Project> & { worktree: string; expanded: bool
6973

7074
export type ReviewDiffStyle = "unified" | "split"
7175

72-
export function ensureSessionKey(key: string, touch: (key: string) => void, seed: (key: string) => void) {
73-
touch(key)
74-
seed(key)
75-
return key
76-
}
77-
78-
export function createSessionKeyReader(sessionKey: string | Accessor<string>, ensure: (key: string) => void) {
79-
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
80-
return () => {
81-
const value = key()
82-
ensure(value)
83-
return value
84-
}
85-
}
86-
87-
export function pruneSessionKeys(input: {
88-
keep?: string
89-
max: number
90-
used: Map<string, number>
91-
view: string[]
92-
tabs: string[]
93-
}) {
94-
if (!input.keep) return []
95-
96-
const keys = new Set<string>([...input.view, ...input.tabs])
97-
if (keys.size <= input.max) return []
98-
99-
const score = (key: string) => {
100-
if (key === input.keep) return Number.MAX_SAFE_INTEGER
101-
return input.used.get(key) ?? 0
102-
}
103-
104-
return Array.from(keys)
105-
.sort((a, b) => score(b) - score(a))
106-
.slice(input.max)
107-
}
76+
export type LayoutRoute =
77+
| { type: "home" }
78+
| { type: "dir-new-sesssion"; dir: string; dirBase64: string; server?: ServerConnection.Key }
79+
| { type: "session"; dir: string; dirBase64: string; sessionId: string; server?: ServerConnection.Key }
10880

10981
function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string): SessionTabs {
11082
const all = current?.all ?? []
@@ -146,13 +118,30 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
146118
}
147119
}
148120

121+
const currentRoute = (pathname: string): LayoutRoute => {
122+
const parts = pathname.split("/").filter(Boolean)
123+
if (parts.length === 0) return { type: "home" }
124+
125+
const dirBase64 = parts[0]
126+
const dir = decode64(dirBase64)
127+
if (!dir) return { type: "home" }
128+
129+
if (parts[1] !== "session") return { type: "home" }
130+
131+
const id = parts[2]
132+
if (id) return { type: "session", dir, dirBase64, sessionId: id }
133+
return { type: "dir-new-sesssion", dir, dirBase64 }
134+
}
135+
149136
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
150137
name: "Layout",
151138
init: () => {
152139
const globalSdk = useServerSDK()
153140
const serverSync = useServerSync()
154141
const server = useServer()
155142
const platform = usePlatform()
143+
const location = useLocation()
144+
const route = createMemo(() => currentRoute(location.pathname))
156145

157146
const isRecord = (value: unknown): value is Record<string, unknown> =>
158147
typeof value === "object" && value !== null && !Array.isArray(value)
@@ -557,6 +546,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
557546
})
558547

559548
return {
549+
route,
560550
ready,
561551
handoff: {
562552
tabs: createMemo(() => store.handoff?.tabs),

packages/app/src/context/server-sync.tsx

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
11
import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client"
22
import { showToast } from "@/utils/toast"
33
import { getFilename } from "@opencode-ai/core/util/path"
4-
import {
5-
batch,
6-
createContext,
7-
createEffect,
8-
getOwner,
9-
onCleanup,
10-
onMount,
11-
type ParentProps,
12-
untrack,
13-
useContext,
14-
} from "solid-js"
4+
import { batch, getOwner, onCleanup, onMount, untrack } from "solid-js"
155
import { createStore, produce, reconcile } from "solid-js/store"
166
import { useLanguage } from "@/context/language"
177
import type { InitError } from "../pages/error"
@@ -86,7 +76,7 @@ function makeQueryOptionsApi(serverSDK: () => OpencodeClient, sdkFor: (dir: Path
8676
}
8777
export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
8878

89-
export function createServerSyncContext(_serverSDK?: ServerSDK) {
79+
export function createServerSyncContextInner(_serverSDK?: ServerSDK) {
9080
const serverSDK: ServerSDK = _serverSDK ?? useServerSDK()
9181
const language = useLanguage()
9282
const owner = getOwner()
@@ -476,6 +466,17 @@ export function createServerSyncContext(_serverSDK?: ServerSDK) {
476466
}
477467
}
478468

469+
export function createServerSyncContext(_serverSDK?: ServerSDK) {
470+
const inner = createServerSyncContextInner(_serverSDK)
471+
return Object.assign(inner, {
472+
createDirSyncContext: createRefCountMap(
473+
(dir) => createDirSyncContext(dir, inner, _serverSDK),
474+
(dir) => inner.disableMcp(dir),
475+
directoryKey,
476+
),
477+
})
478+
}
479+
479480
export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({
480481
name: "ServerSync",
481482
init: (props: { server?: ServerConnection.Any }) => {
@@ -487,13 +488,7 @@ export const { use: useServerSync, provider: ServerSyncProvider } = createSimple
487488
if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable"))
488489
const ctx = global.createServerCtx(conn)
489490

490-
return Object.assign(ctx.sync, {
491-
createDirSyncContext: createRefCountMap(
492-
(dir) => createDirSyncContext(dir, ctx.sync),
493-
(dir) => ctx.sync.disableMcp(dir),
494-
directoryKey,
495-
),
496-
})
491+
return ctx.sync
497492
},
498493
})
499494

packages/app/src/context/settings.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
159159
init: () => {
160160
const [store, setStore, _, ready] = persisted("settings.v3", createStore<Settings>(defaultSettings))
161161

162-
createEffect(() => {
163-
console.log("settings", { ready: ready() })
164-
})
165-
166162
createEffect(() => {
167163
if (typeof document === "undefined") return
168164
const root = document.documentElement

packages/app/src/pages/directory-layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
2626

2727
createResource(
2828
() => params.id,
29-
(id) => sync.session.sync(id),
29+
(id) => sync.session.sync(id).catch(() => {}),
3030
)
3131

3232
return (

packages/app/src/pages/layout/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export function getProjectAvatarSource(id?: string, icon?: { color?: string; url
6767
export function projectForSession<T extends { id?: string; worktree: string; sandboxes?: string[] }>(
6868
session: Session,
6969
projects: T[],
70-
byID: Map<string, T>,
70+
byID: Map<string, T> = new Map(projects.flatMap((project) => (project.id ? [[project.id, project] as const] : []))),
7171
) {
7272
const direct = byID.get(session.projectID)
7373
if (direct) return direct

0 commit comments

Comments
 (0)