Skip to content
Merged
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
27 changes: 16 additions & 11 deletions packages/app-bundle/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"packages/app/public/amico.svg": "7c64b145bdd063bacd53318336a423f0654ec702de2b4e0c4a4a384694052804",
"packages/app/public/oc-theme-preload.js": "27227e802b3494e7c545da903e679efdb30ccc754cd4eb5cdf08005a40d560b6",
"packages/app/src/app-desktop-commands.test.ts": "1d2057f238dc14e4407ff5c4e53afe091784790bc0fd0dbfde0eacd91e2bd968",
"packages/app/src/app.tsx": "25d290c6f669a9a0ce79bb0fb3b3cdcf5251c59b316e379551b64aea0f7f1c95",
"packages/app/src/app.tsx": "a55585b8142c13c127d33fbe807488de12a528156e498fc9f532b0a036ce3b25",
"packages/app/src/design-polish.css": "6b860316d2d57a4a9a89c1ff2772de45eca0d0cef2bd3e2ea8572d817e1d1910",
"packages/app/src/desktop-menu.ts": "55481604f4fd5dcb74b0678347076c42fda7f928590d90bc2c3d1633bea8bc36",
"packages/app/src/entry.tsx": "f35e1017f4c9d478d254b2a38043e5750064b6bef25169c3e07ae9f72ff1049c",
Expand Down Expand Up @@ -149,7 +149,8 @@
"packages/app/src/components/windows-app-menu.tsx": "460669a129e9be61ab2d5f51fe6ecad73579a395295d5f724f235b988cdc667e",
"packages/app/src/components/workbench-panel.tsx": "d16f477044098842a214a2e16f9be719e7688a04bcb575889ca544988f23c690",
"packages/app/src/context/command.tsx": "388d6cb12b6dc43ecb0827e3fb918370cb9d16d96161a5990dec10fff05af172",
"packages/app/src/context/directory-sync.ts": "df5ef26146788551c04f93556f2046c898d011652a504a09402b3468d9c380d3",
"packages/app/src/context/directory-sync.test.ts": "c03c7bcd95ac47b3de5e1e9719b6eccdb3569063026fe993b448453c818c7b48",
"packages/app/src/context/directory-sync.ts": "8515e0f8a5bdf0bf6243a0c2dfe64f234f5292074464e38c68da8287235acc3e",
"packages/app/src/context/language.tsx": "cb545e04c128bf981b2b72ac407f9220cf9e80337fe8c87482ffe85407e0cf9a",
"packages/app/src/context/layout-side-panel-tabs.test.ts": "47425bbed0ada18d3fcb67a76b63de3b9e1f827cd63a61ffe4eb45430fc5814e",
"packages/app/src/context/layout-side-panel-tabs.ts": "e8426fadbdf9e7c49bf457e00ec92793475b44bcc11548e8b5ef9a9fa461be76",
Expand All @@ -163,10 +164,14 @@
"packages/app/src/context/platform.tsx": "edd9b1773feb5c084b922a57b6914f6a473eed558b44f9de0eeb1c4d4e2b49da",
"packages/app/src/context/server-sdk.test.ts": "5cd22962c8ea1e4374915508a765281ceb9f808ad8bbf241c06b00b48621c816",
"packages/app/src/context/server-sdk.tsx": "7e1b71538136237481f1f5ec05680a81b01c82f24ecf5819c430d317c4a72f17",
"packages/app/src/context/server-session.ts": "a9cb767f29cd169e3edddcc8fb1e7f8c8b8af8344a0ae1f36fc8f5568ab124af",
"packages/app/src/context/server-session.test.ts": "a76d24b0d36d7c6c9ce187d2eb5bba78c90774184adc0398559ee1a417674516",
"packages/app/src/context/server-session.ts": "4d85011c9ff81cf0470633f6f27cd9f11c234294c31e2c77a990aad135d317ab",
"packages/app/src/context/server-sync.tsx": "14396d7402d697ab9a10744c9e428b96e483f3a9620a0415246fa840596fd8ed",
"packages/app/src/context/server.tsx": "08e978e4b4b3094221a2a0b92e069f39cbedb6d6cf95935881e21cde52265ee2",
"packages/app/src/context/session-mirror.ts": "3354c805d6c16bbc69d264b67638ee30187700e8389b8d0f97897d7a04638144",
"packages/app/src/context/session-mirror.test.ts": "f995b8877132c11ddaa4243edf126377ae2edcd1d3f0b66df29e6e51f0a32d29",
"packages/app/src/context/session-mirror.ts": "f017fe4a997149172449d67b462e3a912e368412434f7f0d4cf2bbdb6ec17187",
"packages/app/src/context/session-warm.test.ts": "0ce4d0e637c9ce7ce5c7d69415a3e1b7d0b6950d6be63450dae7c5b7aaf45dd9",
"packages/app/src/context/session-warm.ts": "31837e183a33718493170587587a4c610c9853140de22d41110687395ab3debc",
"packages/app/src/context/settings.tsx": "19e078e81620ffa21a92797335009b84d4311485a37c5ede2f76906b734292ad",
"packages/app/src/context/split.tsx": "891aeb290b369b327a63ce548dcaab1daca08e41fab430648787be7f84e53ec0",
"packages/app/src/context/stream-gap.test.ts": "b7203445564f44fd1d966ef684a11cb9b5ec567d9ab54f1b7617726fdf44d3f5",
Expand Down Expand Up @@ -652,13 +657,13 @@
"packages/app/src/components/settings-v2/skills-controller.ts": "2c7344d144da0eec1c25083ead45f5afb2a8c6fd759541ad719cae9a09fcd19b",
"packages/app/src/components/settings-v2/skills.tsx": "42deb0c11fcba158abad857b9644ffb551aed5ba9e00dbfd1584a7ffaaba5aa0",
"packages/app/src/components/ui/drawer.tsx": "76574ca4c6997209f37a766a61000d707332a9a857f6ea8100a3cee1be58d65c",
"packages/app/src/context/global-sync/bootstrap.test.ts": "41dc3f7726d382829c76ff0f9b8bf4525411c5287e529ddda909ee8b789630a7",
"packages/app/src/context/global-sync/child-store.ts": "972315dcc47a0fa2828d045127d5e6d667f98b8342f19884043af736a1b6705e",
"packages/app/src/context/global-sync/event-reducer.test.ts": "d59d8cdcc9303af95227e36a644d7fa70be8747f209ea6b047c3b22f2bf937ba",
"packages/app/src/context/global-sync/event-reducer.ts": "7259b95c2b282791b3548bc2291b78e0e955a6bfde0dda420fbc6af31c47f0e8",
"packages/app/src/context/global-sync/session-cache.test.ts": "8441b26707fe0d231e24f3cfbd88edfa13c6c6ad9f01c18a5df8d749beb5c071",
"packages/app/src/context/global-sync/session-cache.ts": "8389f86293ce914455057b1ad555df8f9a245ec8e33529cc131de9b2b05d14a2",
"packages/app/src/context/global-sync/types.ts": "d290ae16d257273d831f4ed23dd32f622d73ede54371f7c85a7e218ac155fcf9",
"packages/app/src/context/global-sync/bootstrap.test.ts": "ed66b4c0427d51c015fd286dd63c2cc24a25391320366d75e28634334d70e366",
"packages/app/src/context/global-sync/child-store.ts": "3fde298a6cda8e96efc22bccee4ef124e139abd6ec28510363c78f702bf70db9",
"packages/app/src/context/global-sync/event-reducer.test.ts": "0528ed45952107b3ca174e72fd0b920191dbbd49eb795c4dff63cf89c2c91c0e",
"packages/app/src/context/global-sync/event-reducer.ts": "9c26e9e83709e56c57b228a03186df6bf1a52a073c4b7e721853956ee0da6142",
"packages/app/src/context/global-sync/session-cache.test.ts": "e3c66ef76ff20a416f3b2d72041acd66c2a65da57ded92cbcb381ab71474285f",
"packages/app/src/context/global-sync/session-cache.ts": "70f60c37ab9ffcb5ac0778f4962d88f0e7c6fb61fc39a18bda28942322605316",
"packages/app/src/context/global-sync/types.ts": "76af58cc126d49bbcb6ffc7ea4d6bce72dc9a714f9ee05de576d823f92b27963",
"packages/app/src/pages/home/home-projects-view.tsx": "074f1cdd7926db9574164824666cd63f1f5bd0f8e8dfb65453f6fe8f2dca0ec8",
"packages/app/src/pages/home/home-sessions-view.tsx": "5b73b18e4349a628a15e09a3810ecf2319bde222e18fe8de5d08373ac91fb37c",
"packages/app/src/pages/layout/helpers.test.ts": "3a77cfe018ce2867cccded2c726f556e53a9895a77baa944a4878714b9b2b59e",
Expand Down
50 changes: 27 additions & 23 deletions packages/app-bundle/overlay/packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ import { SDKProvider, useSDK } from "@/context/sdk"
import { resolveLandingDirectory } from "@/pages/new-session-landing"
import { authTokenFromCredentials } from "@/utils/server"
import { normalizeSessionInfo } from "@/utils/session"
import {
BULK_WARM_MESSAGES,
createSessionWarmScheduler,
sessionWarmSchedulerKey,
warmBulkSession,
warmOpenSessionTab,
} from "@/context/session-warm"
import type { SessionV2Info } from "@opencode-ai/sdk/v2/client"
import type { SessionInfo } from "@opencode-ai/client/promise"
import { WslServersProvider } from "@/wsl/context"
Expand Down Expand Up @@ -813,6 +820,7 @@ ${text.slice(0, 12000)}` }).catch(() => {}) // #1294: 2400 truncated snapshots
function SessionLineagePrewarmer() {
const global = useGlobal()
const tabs = useTabs()
const warmScheduler = createSessionWarmScheduler()
// #1294: pin OPEN TABS for their lifetime — the route-level pin unpins
// the session the moment you switch away, and without a pin the cache
// evictor can drop an idle tab's message data between warm passes
Expand Down Expand Up @@ -885,10 +893,7 @@ function SessionLineagePrewarmer() {
// (measured: the paint ring's 11,949ms / 8,034ms sessions). The
// 20-message page lands in one round trip and satisfies the
// timeline; the 60-deep pass continues behind it and fills history.
void session
.prefetch(tab.sessionId, 20)
.then(() => session.prefetch(tab.sessionId, 60))
.catch(() => {})
void warmOpenSessionTab(tab.sessionId, (sessionID, limit) => session.prefetch(sessionID, limit)).catch(() => {})
}
}
})
Expand All @@ -899,7 +904,6 @@ function SessionLineagePrewarmer() {
// recency-ordered list page per pass (the server sorts); the
// lineage-peek + shouldPrefetch guards make repeat passes free.
const BULK_WARM_SESSIONS = 30
const BULK_WARM_MESSAGES = 20
const bulkWarm = async () => {
for (const conn of global.servers.list()) {
// #1290: same ctx fix — conn.sync is undefined on raw list entries.
Expand Down Expand Up @@ -927,24 +931,24 @@ function SessionLineagePrewarmer() {
;(globalThis as { __amicodePrewarmErr?: string }).__amicodePrewarmErr = String(e).slice(0, 90)
continue
}
for (const info of recent) {
// #1294c: seed data.info from the list payload — zero wire cost.
// The v2 list objects carry location:{directory} with NO
// top-level directory/slug/path — normalizeSessionInfo maps them
// (every other consumer normalizes at the boundary; the raw
// object crashed the tab strip's render on the real hub).
try {
sync.session.remember(normalizeSessionInfo(info as SessionInfo))
} catch {
/* best-effort */
}
if (sync.session.lineage && !sync.session.lineage.peek(info.id)) {
void sync.session.lineage.resolve(info.id).catch(() => {})
}
if (sync.session.prefetch && sync.session.shouldPrefetch(info.id, BULK_WARM_MESSAGES)) {
void sync.session.prefetch(info.id, BULK_WARM_MESSAGES).catch(() => {})
}
}
const origin = sessionWarmSchedulerKey(conn.http.url)
await warmScheduler.warm(
origin,
recent.map((info) => async () => {
// #1294c: seed data.info from the list payload — zero wire cost.
// The v2 list objects carry location:{directory} with NO
// top-level directory/slug/path — normalizeSessionInfo maps them
// (every other consumer normalizes at the boundary; the raw
// object crashed the tab strip's render on the real hub).
await warmBulkSession({
remember: () => sync.session.remember(normalizeSessionInfo(info as SessionInfo)),
hasLineage: () => !sync.session.lineage || !!sync.session.lineage.peek(info.id),
resolveLineage: () => sync.session.lineage?.resolve(info.id) ?? Promise.resolve(),
shouldPrefetch: () => !!sync.session.prefetch && sync.session.shouldPrefetch(info.id, BULK_WARM_MESSAGES),
prefetch: () => sync.session.prefetch?.(info.id, BULK_WARM_MESSAGES) ?? Promise.resolve(),
})
}),
)
}
}
void bulkWarm()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, test } from "bun:test"
import { createRoot } from "solid-js"
import { createStore } from "solid-js/store"
import type { State } from "./global-sync/types"
import { createDirSyncContext } from "./directory-sync"

describe("directory diff-version delegation", () => {
test("reads and writes the shared ServerSession diff version without creating a directory copy", () => {
const [directory, setDirectory] = createStore({ session: [] } as unknown as State)
const [serverSession, setServerSession] = createStore({ diff_version: { tab: 0 } })
let dispose: (() => void) | undefined
let sync: ReturnType<typeof createDirSyncContext> | undefined

createRoot((cleanup) => {
dispose = cleanup
sync = createDirSyncContext(
"/project",
{
child: () => [directory, setDirectory],
session: { data: serverSession, set: setServerSession },
} as never,
{ createClient: () => ({}) } as never,
)
})

expect(sync?.data.diff_version).toBe(serverSession.diff_version)
;(sync?.set as unknown as (...args: unknown[]) => void)("diff_version", "tab", (version: number | undefined) =>
(version ?? 0) + 1,
)

expect(serverSession.diff_version.tab).toBe(1)
expect((directory as { diff_version?: unknown }).diff_version).toBeUndefined()
dispose?.()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,9 @@ const sessionFields = new Set([
"session_status",
"session_working",
"session_diff",
// amicode#hotfix: diff_version must NOT ride sessionFields. The shared
// ServerSession store never initializes the key, so a proxied read returns
// undefined and `data.diff_version[sessionID]` throws on every fresh load —
// the throw gets swallowed by the route error boundary, the query-options
// memo stays undefined, and TanStack Query surfaces the misleading
// "Cannot read properties of undefined (reading '_defaulted')". The child
// store owns diff_version (initialized + written by the event reducer).
// Diff versions are owned by ServerSession so every directory observes the
// same live file-edit invalidation state.
"diff_version",
"todo",
"permission",
"question",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ function directoryState() {
return this.session_status[id]?.type !== "idle"
},
session_diff: {},
diff_version: {},
parent_of: {},
todo: {},
permission: {},
question: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,6 @@ export function createChildStoreManager(input: {
return (type ?? "idle") !== "idle"
},
session_diff: {},
diff_version: {},
parent_of: {},
todo: {},
permission: {},
question: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ const baseState = (input: Partial<State> = {}) =>
sessionTotal: 0,
session_status: {},
session_diff: {},
diff_version: {},
parent_of: {},
todo: {},
permission: {},
question: {},
Expand Down Expand Up @@ -616,78 +614,4 @@ describe("applyDirectoryEvent", () => {
expect(lspLoads).toBe(1)
})

test("message.part.updated with filediff bumps diff_version of parent session too", () => {
const [store, setStore] = createStore<State>({
...baseState(),
session: [rootSession({ id: "parent" }), rootSession({ id: "child", parentID: "parent" })],
parent_of: { child: "parent" },
diff_version: {},
})

applyDirectoryEvent({
event: {
type: "message.part.updated",
properties: {
part: {
id: "prt_1",
sessionID: "child",
messageID: "msg_1",
type: "tool",
tool: "edit",
callID: "call_1",
state: {
status: "completed",
input: {},
output: "",
title: "edit",
time: { start: 1, end: 2 },
metadata: { filediff: { file: "test.ts", status: "modified" } },
},
} as Part,
},
},
store,
setStore,
push: () => {},
directory: "/tmp",
loadLsp: () => {},
})

expect(store.diff_version.child).toBe(1)
expect(store.diff_version.parent).toBe(1)
})

test("session.created with parentID populates parent_of mapping", () => {
const [store, setStore] = createStore<State>(baseState())

applyDirectoryEvent({
event: { type: "session.created", properties: { info: rootSession({ id: "child", parentID: "parent" }) } },
store,
setStore,
push: () => {},
directory: "/tmp",
loadLsp: () => {},
})

expect(store.parent_of.child).toBe("parent")
})

test("session.deleted cleans up parent_of mapping", () => {
const [store, setStore] = createStore<State>({
...baseState(),
session: [rootSession({ id: "parent" }), rootSession({ id: "child", parentID: "parent" })],
parent_of: { child: "parent" },
})

applyDirectoryEvent({
event: { type: "session.deleted", properties: { info: rootSession({ id: "child", parentID: "parent" }) } },
store,
setStore,
push: () => {},
directory: "/tmp",
loadLsp: () => {},
})

expect(store.parent_of.child).toBeUndefined()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { dropSessionCaches } from "./session-cache"
import { diffs as list, message as clean } from "@/utils/diffs"

const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
const EDIT_TOOLS = new Set(["edit", "write", "patch", "apply_patch"])
const SESSION_CONTENT_EVENTS = new Set([
"session.diff",
"todo.updated",
Expand Down Expand Up @@ -141,7 +140,6 @@ export function applyDirectoryEvent(input: {
input.setStore("session", reconcile(trimmed, { key: "id" }))
cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo)
if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1)
if (info.parentID) input.setStore("parent_of", info.id, info.parentID)
break
}
case "session.updated": {
Expand Down Expand Up @@ -187,12 +185,6 @@ export function applyDirectoryEvent(input: {
)
}
cleanupSessionCaches(input.setStore, sessionID, input.setSessionTodo)
input.setStore(
"parent_of",
produce((draft) => {
delete draft[sessionID]
}),
)
if (info?.parentID) break
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
break
Expand Down Expand Up @@ -320,21 +312,6 @@ export function applyDirectoryEvent(input: {
case "message.part.updated": {
const part = (event.properties as { part: Part }).part
if (SKIP_PARTS.has(part.type)) break
// Bump diff_version when a file-editing tool completes so the diff query refetches mid-turn
if (
part.type === "tool" &&
EDIT_TOOLS.has(part.tool) &&
part.state.status === "completed" &&
(part.state as { metadata?: Record<string, unknown> }).metadata?.filediff
) {
input.setStore("diff_version", part.sessionID, (v) => (v ?? 0) + 1)
// Propagate up the parent chain so parent sessions refetch their diff
let ancestor = input.store.parent_of[part.sessionID]
while (ancestor) {
input.setStore("diff_version", ancestor, (v) => (v ?? 0) + 1)
ancestor = input.store.parent_of[ancestor]
}
}
input.setStore(
produce((draft) => {
delete draft.part_text_accum_delta[part.id]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ describe("app session cache", () => {
const store: {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
diff_version: Record<string, number | undefined>
parent_of: Record<string, string | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
session_message: Record<string, never[] | undefined>
Expand All @@ -39,8 +37,6 @@ describe("app session cache", () => {
} = {
session_status: { ses_1: { type: "busy" } as SessionStatus },
session_diff: { ses_1: [] },
diff_version: { ses_1: 0 },
parent_of: {},
todo: { ses_1: [] as Todo[] },
message: {},
session_message: {},
Expand All @@ -67,8 +63,6 @@ describe("app session cache", () => {
const store: {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
diff_version: Record<string, number | undefined>
parent_of: Record<string, string | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
session_message: Record<string, never[] | undefined>
Expand All @@ -79,8 +73,6 @@ describe("app session cache", () => {
} = {
session_status: {},
session_diff: {},
diff_version: {},
parent_of: {},
todo: {},
message: { ses_1: [m] },
session_message: {},
Expand Down
Loading
Loading