Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
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
104 changes: 65 additions & 39 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 @@ -540,6 +543,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
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 @@ -554,7 +558,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