Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/app-bundle/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.cache/
.materialized/
21 changes: 17 additions & 4 deletions packages/app-bundle/drift-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
"tag": "v1.18.29",
"upstream_tree": "archive",
"counts": {
"overlay_total": 576,
"added": 298,
"modified": 278,
"unchanged": 0,
"overlay_total": 589,
"added": 305,
"modified": 283,
"unchanged": 1,
"deletions_active": 7,
"deletions_stale": 0
},
Expand Down Expand Up @@ -184,17 +184,21 @@
"packages/app/src/utils/amicode-route-info.ts": "added",
"packages/app/src/utils/amicode-workspace-projects.test.ts": "added",
"packages/app/src/utils/amicode-workspace-projects.ts": "added",
"packages/app/src/utils/boot-parity.ts": "added",
"packages/app/src/utils/chrome-dropdown.ts": "added",
"packages/app/src/utils/global-clipboard.test.ts": "added",
"packages/app/src/utils/global-clipboard.ts": "added",
"packages/app/src/utils/pane-bridge.ts": "added",
"packages/app/src/utils/path-key.ts": "unchanged",
"packages/app/src/utils/project-type-helpers.test.ts": "added",
"packages/app/src/utils/project-type-helpers.ts": "added",
"packages/app/src/utils/provider-disconnect.test.ts": "added",
"packages/app/src/utils/provider-disconnect.ts": "added",
"packages/app/src/utils/serialize-session.test.ts": "added",
"packages/app/src/utils/serialize-session.ts": "added",
"packages/app/src/utils/server-compat.ts": "modified",
"packages/app/src/utils/session-list-conformance.ts": "added",
"packages/app/src/utils/session-list-state.ts": "added",
"packages/app/src/utils/start-prompt.ts": "added",
"packages/app/src/utils/web-zoom.test.ts": "added",
"packages/app/src/utils/web-zoom.ts": "added",
Expand Down Expand Up @@ -489,11 +493,20 @@
"packages/app/src/context/global-sync/child-store.ts": "modified",
"packages/app/src/context/global-sync/event-reducer.test.ts": "modified",
"packages/app/src/context/global-sync/event-reducer.ts": "modified",
"packages/app/src/context/global-sync/h1-client-boot.ts": "added",
"packages/app/src/context/global-sync/home-session-index.ts": "modified",
"packages/app/src/context/global-sync/session-cache.test.ts": "modified",
"packages/app/src/context/global-sync/session-cache.ts": "modified",
"packages/app/src/context/global-sync/session-currency.ts": "added",
"packages/app/src/context/global-sync/session-snapshot.ts": "added",
"packages/app/src/context/global-sync/session-trim.ts": "modified",
"packages/app/src/context/global-sync/types.ts": "modified",
"packages/app/src/context/global-sync/utils.ts": "modified",
"packages/app/src/pages/home/home-projects-view.tsx": "modified",
"packages/app/src/pages/home/home-session-groups.ts": "added",
"packages/app/src/pages/home/home-sessions-controller.tsx": "modified",
"packages/app/src/pages/home/home-sessions-view.tsx": "modified",
"packages/app/src/pages/home/home-sessions.tsx": "modified",
"packages/app/src/pages/layout/helpers.test.ts": "modified",
"packages/app/src/pages/layout/helpers.ts": "modified",
"packages/app/src/pages/layout/project-avatar-state.ts": "modified",
Expand Down
86 changes: 56 additions & 30 deletions packages/app-bundle/manifest.json

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion packages/app-bundle/overlay/packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ import LegacyLayout from "@/pages/layout"
import NewLayout from "@/pages/layout-new"
import { ErrorPage } from "./pages/error"
import { useCheckServerHealth } from "./utils/server-health"
// D3 (issue #817): the boot parity record — the server-reported version is
// asserted against the release channel and the three-outcome record logged.
import { recordBootParity } from "./utils/boot-parity"
import { AmicodeSplash } from "@opencode-ai/ui/amicode-splash"
import { legacySessionHref, legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
import { createSessionLineage } from "@/pages/session/session-lineage"
Expand Down Expand Up @@ -551,6 +554,7 @@ export function AppBaseProviders(
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean; startup?: Promise<void> }>) {
const server = useServer()
const checkServerHealth = useCheckServerHealth()
const platform = usePlatform()

const [checkMode, setCheckMode] = createSignal<"blocking" | "background">("blocking")

Expand All @@ -565,7 +569,16 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean; start

while (true) {
const res = yield* Effect.promise(() => checkServerHealth(http))
if (res.healthy) return true
if (res.healthy) {
// D3 (issue #817): the boot parity assertion rides the healthy
// health check — the same probe that reported the server's
// version. Surfaced, never a gate; fails open as
// channel-unreachable when the release channel is unreachable.
yield* Effect.promise(() =>
recordBootParity({ serverVersion: res.version, fetcher: platform.fetch ?? globalThis.fetch }),
)
return true
}
if (checkMode() === "background" || type === "http") return false
}
}).pipe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { useServerSync } from "@/context/server-sync"
import { useGlobal } from "@/context/global"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { sessionListDirectories, sortedRootSessions } from "@/pages/layout/helpers"
import { sessionListState } from "@/utils/session-list-state"
import { useNavigate } from "@solidjs/router"
import type { Session } from "@opencode-ai/sdk/v2/client"

Expand Down Expand Up @@ -775,6 +776,31 @@ export function SessionChatsDropdown(props: { currentSessionID?: string } = {})
return [...openTabs, ...rest]
})

// D2 honest states (issue #817): "not yet fetched" (a completed list
// request is the only authority for "genuinely empty") vs "empty" vs ready
// — never render the empty state while no fetch has resolved (#293's
// invisible failure).
const activeListState = createMemo(() => {
if (!open()) return "ready" as const
try {
const conn = server.current
if (!conn) return "unfetched" as const
const ctx = globalCtx.ensureServerCtx(conn)
if (!ctx) return "unfetched" as const
const directories = sessionListDirectories(ctx.projects.list(), ctx.sync.data?.project ?? [])
let fetched = false
let count = 0
for (const dir of directories) {
const [store] = ctx.sync.child(dir, { bootstrap: false })
if (store.sessions_fetched) fetched = true
count += store.session?.length ?? 0
}
return sessionListState({ fetched, count, searching: !!searchQuery() })
} catch {
return "unfetched" as const
}
})

// Search filtering
const searchQuery = createMemo(() => search().trim().toLowerCase())
const filteredActiveSessions = createMemo(() => {
Expand Down Expand Up @@ -1071,7 +1097,11 @@ export function SessionChatsDropdown(props: { currentSessionID?: string } = {})
when={filteredActiveSessions().length > 0}
fallback={
<div class="pl-1.5 py-2 text-v2-text-text-faint" style={{ "font-size": "12px" }}>
{searchQuery() ? language.t("home.sessions.search.noResults", { query: search() }) : language.t("home.sessions.empty")}
{searchQuery()
? language.t("home.sessions.search.noResults", { query: search() })
: activeListState() === "unfetched"
? language.t("common.loading")
: language.t("home.sessions.empty")}
</div>
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import {
type IconCache,
type MetaCache,
type ProjectMeta,
type SessionSnapshotCache,
type State,
type VcsCache,
} from "./types"
import type { SessionSnapshot } from "./session-snapshot"
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
import { useQuery } from "@tanstack/solid-query"
import { QueryOptionsApi } from "../server-sync"
Expand All @@ -39,6 +41,7 @@ export function createChildStoreManager(input: {
const vcsCache = new Map<string, VcsCache>()
const metaCache = new Map<string, MetaCache>()
const iconCache = new Map<string, IconCache>()
const snapshotCache = new Map<string, SessionSnapshotCache>()
const lifecycle = new Map<string, DirState>()
const pins = new Map<string, number>()
const ownerPins = new WeakMap<object, Set<string>>()
Expand Down Expand Up @@ -117,6 +120,7 @@ export function createChildStoreManager(input: {
vcsCache.delete(key)
metaCache.delete(key)
iconCache.delete(key)
snapshotCache.delete(key)
lifecycle.delete(key)
mcpDirectories.delete(key)
mcpToggles.delete(key)
Expand Down Expand Up @@ -181,6 +185,23 @@ export function createChildStoreManager(input: {
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
iconCache.set(key, { store: icon[0], setStore: icon[1], ready: icon[3] })

// D2 (issue #817): the persisted session snapshot is a render
// accelerator, never an authority — hydrated only until the first real
// list fetch resolves, and verified against the client-derived currency
// token on every list response.
const sessionSnapshot = runWithOwner(input.owner, () =>
input.persist(
Persist.serverWorkspace(input.scope, directory, "session:snapshot"),
createStore({ value: undefined as SessionSnapshot | undefined }),
),
)
if (!sessionSnapshot) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
snapshotCache.set(key, {
store: sessionSnapshot[0],
setStore: sessionSnapshot[1],
ready: sessionSnapshot[3],
})

const init = () =>
createRoot((dispose) => {
const initialMeta = meta[0].value
Expand Down Expand Up @@ -228,6 +249,7 @@ export function createChildStoreManager(input: {
},
session: [],
sessionTotal: 0,
sessions_fetched: false,
session_status: {},
session_working(id: string) {
const type = this.session_status[id]?.type
Expand Down Expand Up @@ -288,6 +310,17 @@ export function createChildStoreManager(input: {
if (child[0].icon !== initialIcon) return
child[1]("icon", icon[0].value)
})

// D2 (issue #817): hydrate the persisted snapshot as a render
// accelerator — only until a real list fetch resolves
// (sessions_fetched flips true), never over a store a fetch
// already filled.
onPersistedInit(sessionSnapshot[2], () => {
if (child[0].sessions_fetched) return
const cached = sessionSnapshot[0].value
if (!cached || child[0].session.length > 0) return
child[1]("session", cached.sessions)
})
})

runWithOwner(input.owner, init)
Expand Down Expand Up @@ -393,5 +426,21 @@ export function createChildStoreManager(input: {
vcsCache,
metaCache,
iconCache,
// D2 (issue #817): the persisted snapshot's read/write seam — server-sync
// verifies the token on every list response and overwrites the snapshot;
// the panel reset invalidates all of them.
sessionSnapshot(directory: string) {
return snapshotCache.get(directoryKey(directory))?.store.value
},
writeSessionSnapshot(directory: string, next: SessionSnapshot) {
const cache = snapshotCache.get(directoryKey(directory))
if (!cache) return
cache.setStore("value", next)
},
resetSessionSnapshots() {
for (const cache of snapshotCache.values()) {
cache.setStore("value", undefined)
}
},
}
}
Loading
Loading