mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 09:03:22 +00:00
fix(core): adopt pre-project sessions on directory resolution (#42100)
This commit is contained in:
parent
bc2251d6d8
commit
0d703d39e7
12 changed files with 290 additions and 24 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { Binary } from "@opencode-ai/core/util/binary"
|
||||
import { ProjectDirectories } from "@opencode-ai/schema/project-directories"
|
||||
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import type { Message, Part, Project, Todo } from "@/types"
|
||||
import type {
|
||||
|
|
@ -187,6 +188,18 @@ export function applyDirectoryEvent(input: {
|
|||
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||
break
|
||||
}
|
||||
case "project.directory.resolved": {
|
||||
const properties = event.properties as { projectID: string; directory: string; previous: string }
|
||||
input.store.session.forEach((session, index) => {
|
||||
const adopted = ProjectDirectories.adopt(
|
||||
{ projectID: session.projectID, directory: session.location.directory },
|
||||
properties,
|
||||
)
|
||||
if (!adopted) return
|
||||
input.setStore("session", index, (current) => ({ ...current, ...adopted }))
|
||||
})
|
||||
break
|
||||
}
|
||||
case "session.renamed": {
|
||||
const properties = event.properties as { sessionID: string; title: string }
|
||||
const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Binary } from "@opencode-ai/core/util/binary"
|
||||
import { ProjectDirectories } from "@opencode-ai/schema/project-directories"
|
||||
import { retry } from "@opencode-ai/core/util/retry"
|
||||
import type { OpenCodeEvent, SessionApi, SessionInfo, SessionMessageInfo } from "@opencode-ai/client/promise"
|
||||
import type { Message, Part, Todo } from "@/types"
|
||||
|
|
@ -894,6 +895,17 @@ export function createServerSession(
|
|||
}
|
||||
|
||||
const applyV2 = (event: OpenCodeEvent) => {
|
||||
if (event.type === "project.directory.resolved") {
|
||||
Object.values(data.info).forEach((info) => {
|
||||
if (!info) return
|
||||
const adopted = ProjectDirectories.adopt(
|
||||
{ projectID: info.projectID, directory: info.location.directory },
|
||||
event.data,
|
||||
)
|
||||
if (adopted) remember({ ...info, ...adopted })
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!("data" in event) || !("sessionID" in event.data) || typeof event.data.sessionID !== "string") return
|
||||
const sessionID = event.data.sessionID
|
||||
const reduction = v2.reduce(data.session_message[sessionID] ?? [], event)
|
||||
|
|
|
|||
|
|
@ -837,6 +837,16 @@ export type ProjectDirectoriesUpdated = {
|
|||
data: { projectID: string }
|
||||
}
|
||||
|
||||
export type ProjectDirectoryResolved = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "project.directory.resolved"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { projectID: string; directory: string; previous: string }
|
||||
}
|
||||
|
||||
export type CommandUpdated = {
|
||||
id: string
|
||||
created: number
|
||||
|
|
@ -2093,6 +2103,7 @@ export type V2Event =
|
|||
| PluginAdded
|
||||
| PluginUpdated
|
||||
| ProjectDirectoriesUpdated
|
||||
| ProjectDirectoryResolved
|
||||
| CommandUpdated
|
||||
| ConfigUpdated
|
||||
| SkillUpdated
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type { EventLog } from "@opencode-ai/schema/event-log"
|
|||
import { and, asc, eq, gt, lte, sql } from "drizzle-orm"
|
||||
import { Database } from "./database/database.js"
|
||||
import { EventSequenceTable, EventTable } from "./event/sql.js"
|
||||
import { Location } from "./location.js"
|
||||
import type { Location } from "@opencode-ai/schema/location"
|
||||
import { KeyedMutex } from "./effect/keyed-mutex.js"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { isDeepStrictEqual } from "node:util"
|
||||
|
|
@ -183,6 +183,9 @@ export function configured(options?: Options) {
|
|||
layer: Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
// Deferred import: a static one would close the module cycle
|
||||
// bus → location → project → bus and hit the node bindings in TDZ.
|
||||
const { Location } = yield* Effect.promise(() => import("./location.js"))
|
||||
const pubsub = {
|
||||
live: yield* PubSub.unbounded<Event.Payload>(),
|
||||
durable: new Map<string, Set<PubSub.PubSub<void>>>(),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import { ChildProcess } from "effect/unstable/process"
|
|||
import { asc, desc } from "drizzle-orm"
|
||||
import path from "path"
|
||||
import { AbsolutePath } from "./schema.js"
|
||||
import { Bus } from "./bus.js"
|
||||
import { Database } from "./database/database.js"
|
||||
import { Event } from "@opencode-ai/schema/project-directories"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Git } from "./git.js"
|
||||
import { AppProcess } from "@opencode-ai/util/process"
|
||||
|
|
@ -91,28 +93,40 @@ const layer = Layer.effect(
|
|||
const fs = yield* FSUtil.Service
|
||||
const git = yield* Git.Service
|
||||
const proc = yield* AppProcess.Service
|
||||
const bus = yield* Bus.Service
|
||||
const db = (yield* Database.Service).db
|
||||
const projectDirectories = yield* ProjectDirectories.Service
|
||||
|
||||
const announcing = new Set<string>()
|
||||
const persist = Effect.fnUntraced(function* (project: Resolved) {
|
||||
yield* db
|
||||
.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* upsertProject(tx, project)
|
||||
if (!project.vcs) return
|
||||
yield* projectDirectories.create({ projectID: project.id, directory: project.canonical }, tx)
|
||||
if (project.directory === project.canonical) return
|
||||
yield* projectDirectories.create(
|
||||
{
|
||||
projectID: project.id,
|
||||
directory: project.directory,
|
||||
strategy: project.vcs.type === "git" ? "git_worktree" : undefined,
|
||||
},
|
||||
tx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
yield* upsertProject(db, project).pipe(Effect.orDie)
|
||||
if (!project.vcs) return project
|
||||
const directories: ProjectDirectories.CreateInput[] = [{ projectID: project.id, directory: project.canonical }]
|
||||
if (project.directory !== project.canonical)
|
||||
directories.push({
|
||||
projectID: project.id,
|
||||
directory: project.directory,
|
||||
strategy: project.vcs.type === "git" ? "git_worktree" : undefined,
|
||||
})
|
||||
// A missing directory row means this directory's resolution is a new durable
|
||||
// fact (copy.ts registers copy directories directly; those never strand
|
||||
// sessions and never announce). The row insert commits atomically with the
|
||||
// event, so a crash between checks retries on the next resolve instead of
|
||||
// stranding the announcement. The in-flight set keeps concurrent resolves
|
||||
// from publishing the same fact twice.
|
||||
for (const item of directories) {
|
||||
const key = item.projectID + "\u0000" + item.directory
|
||||
if (announcing.has(key)) continue
|
||||
announcing.add(key)
|
||||
yield* Effect.gen(function* () {
|
||||
if (yield* projectDirectories.get({ projectID: item.projectID, directory: item.directory })) return
|
||||
yield* bus.publish(
|
||||
Event.Resolved,
|
||||
{ projectID: item.projectID, directory: item.directory, previous: project.previous ?? ID.global },
|
||||
{ commit: () => Effect.asVoid(projectDirectories.create(item)) },
|
||||
)
|
||||
}).pipe(Effect.ensuring(Effect.sync(() => announcing.delete(key))))
|
||||
}
|
||||
return project
|
||||
})
|
||||
|
||||
|
|
@ -250,5 +264,5 @@ const layer = Layer.effect(
|
|||
export const node = makeGlobalNode({
|
||||
service: Service,
|
||||
layer: layer,
|
||||
deps: [Database.node, FSUtil.node, Git.node, AppProcess.node, ProjectDirectories.node],
|
||||
deps: [Bus.node, Database.node, FSUtil.node, Git.node, AppProcess.node, ProjectDirectories.node],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
export * as SessionProjector from "./projector.js"
|
||||
|
||||
import { and, asc, desc, eq, gt, gte, lt, lte, sql } from "drizzle-orm"
|
||||
import { and, asc, desc, eq, gt, gte, inArray, lt, lte, sql } from "drizzle-orm"
|
||||
import { DateTime, Effect, Layer, Schema, Stream } from "effect"
|
||||
import path from "path"
|
||||
import { Database } from "../database/database.js"
|
||||
import { Bus } from "../bus.js"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
|
|
@ -15,7 +16,10 @@ import { Workspace } from "../workspace.js"
|
|||
import { InstructionState } from "./instruction-state.js"
|
||||
import { SessionInboxTable, SessionMessageTable, SessionTable } from "./sql.js"
|
||||
import { Slug } from "../util/slug.js"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { Event } from "@opencode-ai/schema/project-directories"
|
||||
import { Project } from "@opencode-ai/schema/project"
|
||||
import { AbsolutePath, RelativePath } from "../schema.js"
|
||||
import type { SessionSchema } from "./schema.js"
|
||||
|
||||
|
|
@ -435,6 +439,47 @@ const layer = Layer.effectDiscard(
|
|||
yield* InstructionState.reset(db, event.data.sessionID)
|
||||
}),
|
||||
)
|
||||
// Sessions whose ownership came from the directory's previous resolution
|
||||
// follow its new identity. Location, transcript, instructions, and recency
|
||||
// are untouched: the session did not move, its directory got identified.
|
||||
yield* bus.project(Event.Resolved, (event) =>
|
||||
Effect.gen(function* () {
|
||||
const stale = [event.data.previous, Project.ID.global].filter((id) => id !== event.data.projectID)
|
||||
if (stale.length === 0) return
|
||||
const rows = yield* db
|
||||
.select({ id: SessionTable.id, directory: SessionTable.directory })
|
||||
.from(SessionTable)
|
||||
.where(
|
||||
and(
|
||||
inArray(SessionTable.project_id, stale),
|
||||
// Lexicographic range narrows the scan to prefix neighbors without
|
||||
// LIKE escaping; FSUtil.contains below decides containment exactly.
|
||||
gte(SessionTable.directory, event.data.directory),
|
||||
lte(SessionTable.directory, AbsolutePath.make(event.data.directory + "\uffff")),
|
||||
),
|
||||
)
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
yield* Effect.forEach(
|
||||
rows,
|
||||
(row) => {
|
||||
if (!FSUtil.contains(event.data.directory, row.directory)) return Effect.void
|
||||
return db
|
||||
.update(SessionTable)
|
||||
.set({
|
||||
project_id: event.data.projectID,
|
||||
path: RelativePath.make(path.relative(event.data.directory, row.directory).replaceAll("\\", "/")),
|
||||
// Self-assignment suppresses the column's $onUpdate: adoption is not activity.
|
||||
time_updated: sql`${SessionTable.time_updated}`,
|
||||
})
|
||||
.where(eq(SessionTable.id, row.id))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
},
|
||||
{ discard: true },
|
||||
)
|
||||
}),
|
||||
)
|
||||
yield* bus.project(SessionEvent.Deleted, (event) =>
|
||||
db.delete(SessionTable).where(eq(SessionTable.id, event.data.sessionID)).run().pipe(Effect.orDie),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { DateTime, Effect, Layer, Stream } from "effect"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
|
|
@ -7,6 +9,7 @@ import { asc, eq } from "drizzle-orm"
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Hash } from "@opencode-ai/util/hash"
|
||||
import { Bus } from "@opencode-ai/core/bus"
|
||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
|
|
@ -53,6 +56,15 @@ const it = testEffect(
|
|||
],
|
||||
),
|
||||
)
|
||||
const liveIt = testEffect(
|
||||
AppNodeBuilder.build(
|
||||
LayerNode.group([Database.node, Bus.node, Project.node, SessionProjector.node, SessionStore.node, Session.node]),
|
||||
[
|
||||
[Bus.node, Bus.configured({ persist: true })],
|
||||
[SessionExecution.node, SessionExecution.noopLayer],
|
||||
],
|
||||
),
|
||||
)
|
||||
const location = Location.Ref.make({ directory: AbsolutePath.make("/project") })
|
||||
const id = Session.ID.create()
|
||||
|
||||
|
|
@ -78,6 +90,65 @@ function withTmp<A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) {
|
|||
}
|
||||
|
||||
describe("Session.create", () => {
|
||||
liveIt.live("follows the directory's project identity established after creation", () =>
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
const projects = yield* Project.Service
|
||||
const { db } = yield* Database.Service
|
||||
const ref = Location.Ref.make({ directory: AbsolutePath.make(directory) })
|
||||
const nested = Location.Ref.make({ directory: AbsolutePath.make(path.join(directory, "packages", "app")) })
|
||||
const created = yield* session.create({ location: ref, title: "Before git" })
|
||||
const child = yield* session.create({ location: nested, title: "Nested before git" })
|
||||
const originalUpdated = created.time.updated
|
||||
|
||||
yield* Effect.promise(async () => {
|
||||
await $`git init -q`.cwd(directory)
|
||||
await $`git config user.email test@example.com`.cwd(directory)
|
||||
await $`git config user.name Test`.cwd(directory)
|
||||
await fs.writeFile(path.join(directory, "README.md"), "test\n")
|
||||
await $`git add README.md`.cwd(directory)
|
||||
await $`git commit -qm initial`.cwd(directory)
|
||||
await $`git remote add origin git@github.com:owner/adopted.git`.cwd(directory)
|
||||
})
|
||||
|
||||
const project = yield* projects.resolve(ref.directory)
|
||||
const repeat = yield* projects.resolve(ref.directory)
|
||||
const adopted = yield* session.get(created.id)
|
||||
const nestedAdopted = yield* session.get(child.id)
|
||||
const page = yield* session.list({ project: project.id })
|
||||
const log = Array.from(yield* Stream.runCollect(logEvents(session, created.id)))
|
||||
|
||||
expect(created.projectID).toBe(Project.ID.global)
|
||||
expect(project.id).toBe(Project.ID.make(Hash.fast("git-remote:github.com/owner/adopted")))
|
||||
expect(repeat.id).toBe(project.id)
|
||||
expect(page.data.map((item) => item.id)).toEqual(expect.arrayContaining([created.id, child.id]))
|
||||
expect(adopted).toMatchObject({
|
||||
projectID: project.id,
|
||||
location: ref,
|
||||
subpath: undefined,
|
||||
time: { updated: originalUpdated },
|
||||
})
|
||||
expect(nestedAdopted).toMatchObject({
|
||||
projectID: project.id,
|
||||
location: nested,
|
||||
subpath: RelativePath.make("packages/app"),
|
||||
})
|
||||
// Adoption is a project-domain fact; the session log records nothing new.
|
||||
expect(log.map((event) => event.type)).toEqual(["session.created"])
|
||||
expect(yield* session.messages({ sessionID: created.id })).toEqual([])
|
||||
// Repeated resolution announces the directory's identity exactly once.
|
||||
const announced = yield* db
|
||||
.select({ type: EventTable.type })
|
||||
.from(EventTable)
|
||||
.where(eq(EventTable.aggregate_id, project.id))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
expect(announced.map((event) => event.type)).toEqual(["project.directory.resolved.1"])
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("persists a missing title until one is generated or supplied", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Event } from "@opencode-ai/schema/project-directories"
|
||||
import { Bus } from "@opencode-ai/core/bus"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
|
|
@ -8,6 +9,7 @@ import { Location } from "@opencode-ai/core/location"
|
|||
import { Project } from "@opencode-ai/core/project"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
|
|
@ -75,4 +77,41 @@ describe("Session.move", () => {
|
|||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("keeps a moved session out of its former directory's new identity", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
const bus = yield* Bus.Service
|
||||
const previous = AbsolutePath.make(path.join(tmp.path, "previous"))
|
||||
const destination = AbsolutePath.make(tmp.path)
|
||||
const created = yield* session.create({ location: Location.Ref.make({ directory: previous }) })
|
||||
|
||||
// Moves are admitted through the inbox and applied by the drain;
|
||||
// publish the applied move directly since execution is a no-op here.
|
||||
yield* bus.publish(SessionEvent.Moved, {
|
||||
sessionID: created.id,
|
||||
location: Location.Ref.make({ directory: destination }),
|
||||
projectID: Project.ID.global,
|
||||
})
|
||||
// The former directory becomes a project after the session left it.
|
||||
yield* bus.publish(Event.Resolved, {
|
||||
projectID: Project.ID.make("adopting"),
|
||||
directory: previous,
|
||||
previous: Project.ID.global,
|
||||
})
|
||||
|
||||
expect(yield* session.get(created.id)).toMatchObject({
|
||||
projectID: Project.ID.global,
|
||||
location: { directory: destination },
|
||||
subpath: undefined,
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export * as DurableEventManifest from "./durable-event-manifest.js"
|
||||
|
||||
import { Event } from "./event.js"
|
||||
import { ProjectDirectories } from "./project-directories.js"
|
||||
import { SessionEvent } from "./session-event.js"
|
||||
|
||||
export const SessionDurable = {
|
||||
|
|
@ -8,4 +9,4 @@ export const SessionDurable = {
|
|||
schema: SessionEvent.Durable,
|
||||
} as const
|
||||
|
||||
export const Durable = Event.durableMap(SessionEvent.DurableDefinitions)
|
||||
export const Durable = Event.durableMap([...SessionEvent.DurableDefinitions, ProjectDirectories.Event.Resolved])
|
||||
|
|
|
|||
|
|
@ -1,10 +1,53 @@
|
|||
export * as ProjectDirectories from "./project-directories.js"
|
||||
|
||||
import { ephemeral, inventory } from "./event.js"
|
||||
import { durable, ephemeral, inventory } from "./event.js"
|
||||
import { AbsolutePath } from "./schema.js"
|
||||
import { Project } from "./project.js"
|
||||
|
||||
const Updated = ephemeral({
|
||||
type: "project.directories.updated",
|
||||
schema: { projectID: Project.ID },
|
||||
})
|
||||
export const Event = { Updated, Definitions: inventory(Updated) }
|
||||
|
||||
/**
|
||||
* A directory's resolution changed: it now resolves to `projectID` where it
|
||||
* previously resolved to `previous` (`global` when the directory had no
|
||||
* stable identity yet, e.g. before `git init`). Sessions whose ownership
|
||||
* came from the previous resolution follow the new identity by projection.
|
||||
*/
|
||||
const Resolved = durable({
|
||||
type: "project.directory.resolved",
|
||||
durable: { aggregate: "projectID", version: 1 },
|
||||
schema: {
|
||||
projectID: Project.ID,
|
||||
directory: AbsolutePath,
|
||||
previous: Project.ID,
|
||||
},
|
||||
})
|
||||
export const Event = { Updated, Resolved, Definitions: inventory(Updated, Resolved) }
|
||||
|
||||
/**
|
||||
* Client-side mirror of the server's `project.directory.resolved` session fold.
|
||||
* Returns the ownership update for a cached session, or undefined when the
|
||||
* session does not follow the resolution. Plain strings: callers hold
|
||||
* generated client types, and the server projection remains authoritative.
|
||||
*/
|
||||
export function adopt(
|
||||
session: { readonly projectID: string; readonly directory: string },
|
||||
event: { readonly projectID: string; readonly directory: string; readonly previous: string },
|
||||
) {
|
||||
if (session.projectID !== event.previous && session.projectID !== Project.ID.global) return
|
||||
if (session.projectID === event.projectID) return
|
||||
const inside =
|
||||
session.directory === event.directory ||
|
||||
session.directory.startsWith(event.directory + "/") ||
|
||||
session.directory.startsWith(event.directory + "\\")
|
||||
if (!inside) return
|
||||
return {
|
||||
projectID: event.projectID,
|
||||
subpath:
|
||||
session.directory === event.directory
|
||||
? undefined
|
||||
: session.directory.slice(event.directory.length + 1).replaceAll("\\", "/"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ describe("public event manifest", () => {
|
|||
"session.revert.staged.1",
|
||||
"session.revert.cleared.1",
|
||||
"session.revert.committed.1",
|
||||
"project.directory.resolved.1",
|
||||
].toSorted(),
|
||||
)
|
||||
expect(SessionEvent.DurableDefinitions).toEqual([
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import { createSimpleContext } from "./helper"
|
|||
import { useClient } from "./client"
|
||||
import { nonEmptyToolContent } from "../util/tool-display"
|
||||
import type { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
import { ProjectDirectories } from "@opencode-ai/schema/project-directories"
|
||||
import { createEffect, createSignal, onCleanup } from "solid-js"
|
||||
|
||||
export type DataSessionStatus = "idle" | "running"
|
||||
|
|
@ -457,6 +458,18 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
}
|
||||
break
|
||||
}
|
||||
case "project.directory.resolved": {
|
||||
for (const [sessionID, info] of Object.entries(store.session.info)) {
|
||||
const adopted = ProjectDirectories.adopt(
|
||||
{ projectID: info.projectID, directory: info.location.directory },
|
||||
event.data,
|
||||
)
|
||||
if (!adopted) continue
|
||||
setStore("session", "info", sessionID, "projectID", adopted.projectID)
|
||||
setStore("session", "info", sessionID, "subpath", adopted.subpath)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "session.inbox.delivered": {
|
||||
const admitted = store.session.input[event.data.sessionID]?.includes(event.data.inboxID) ?? false
|
||||
removePending(event.data.sessionID, event.data.inboxID)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue