feat(worktree): add managed workspace cloning (#30117)

This commit is contained in:
Dax
2026-05-31 11:57:44 -04:00
committed by GitHub
parent 331bed2469
commit 5661af2034
23 changed files with 2374 additions and 28 deletions

View File

@@ -122,6 +122,7 @@ export interface Interface {
readonly remove: (id: ID) => Effect.Effect<void, Error>
readonly activate: (id: ID) => Effect.Effect<void, Error>
readonly active: (serviceID: ServiceID) => Effect.Effect<Info | undefined, Error>
readonly activeAll: () => Effect.Effect<Map<ServiceID, Info>, Error>
readonly forService: (serviceID: ServiceID) => Effect.Effect<Info[], Error>
}
@@ -216,6 +217,19 @@ export const layer = Layer.effect(
)
}),
activeAll: Effect.fn("Auth.activeAll")(function* () {
const data = yield* SynchronizedRef.get(state)
const result = new Map<ServiceID, Info>()
for (const account of Object.values(data.accounts)) {
if (!result.has(account.serviceID)) result.set(account.serviceID, account)
}
for (const [serviceID, id] of Object.entries(data.active)) {
const account = data.accounts[id]
if (account) result.set(ServiceID.make(serviceID), account)
}
return result
}),
forService: Effect.fn("Auth.list")(function* (serviceID) {
return Object.values((yield* SynchronizedRef.get(state)).accounts).filter((a) => a.serviceID === serviceID)
}),

View File

@@ -166,8 +166,14 @@ export const layer = Layer.effect(
model: {
get: (providerID, modelID) => draft.providers.get(providerID)?.models.get(modelID),
update: (providerID, modelID, fn) => {
result.provider.update(providerID, () => {})
const record = draft.providers.get(providerID)!
let record = draft.providers.get(providerID)
if (!record) {
record = castDraft({
provider: ProviderV2.Info.empty(providerID),
models: new Map<ModelV2.ID, ModelV2.Info>(),
})
draft.providers.set(providerID, record)
}
const model = record.models.get(modelID) ?? castDraft(ModelV2.Info.empty(providerID, modelID))
if (!record.models.has(modelID)) record.models.set(modelID, model)
fn(model)
@@ -190,6 +196,7 @@ export const layer = Layer.effect(
},
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog, reason) {
if (reason !== "plugin.added") yield* plugin.trigger("catalog.transform", catalog, {}).pipe(Effect.asVoid)
if (!policy.hasStatements()) return
for (const record of [...catalog.provider.list()]) {
if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
catalog.provider.remove(record.provider.id)

View File

@@ -68,8 +68,8 @@ export class Info extends Schema.Class<Info>("ModelV2.Info")({
output: Schema.Int,
}),
}) {
static empty(providerID: ProviderV2.ID, modelID: ID) {
return new Info({
static empty(providerID: ProviderV2.ID, modelID: ID): Info {
return {
id: modelID,
apiID: modelID,
providerID,
@@ -101,7 +101,7 @@ export class Info extends Schema.Class<Info>("ModelV2.Info")({
context: 0,
output: 0,
},
})
}
}
}

View File

@@ -111,7 +111,14 @@ export const layer = Layer.effect(
const existing = hooks.find((item) => item.id === input.id)
if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore)
const scope = yield* Scope.make()
const result = yield* input.effect.pipe(Scope.provide(scope))
const result = yield* input.effect.pipe(
Scope.provide(scope),
Effect.withSpan("Plugin.load", {
attributes: {
"plugin.id": input.id,
},
}),
)
hooks = [
...hooks.filter((item) => item.id !== input.id),
{

View File

@@ -21,8 +21,10 @@ export const AccountPlugin = PluginV2.define({
return {
"catalog.transform": Effect.fn(function* (evt) {
const active = yield* accounts.activeAll().pipe(Effect.orDie)
if (active.size === 0) return
for (const item of evt.provider.list()) {
const account = yield* accounts.active(Auth.ServiceID.make(item.provider.id)).pipe(Effect.orDie)
const account = active.get(Auth.ServiceID.make(item.provider.id))
if (!account) continue
evt.provider.update(item.provider.id, (provider) => {
provider.enabled = {

View File

@@ -16,6 +16,7 @@ export class Info extends Schema.Class<Info>("Policy.Info")({
export interface Interface {
readonly load: (statements: Info[]) => EffectRuntime.Effect<void>
readonly evaluate: (action: string, resource: string, fallback: Effect) => EffectRuntime.Effect<Effect>
readonly hasStatements: () => boolean
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Policy") {}
@@ -30,6 +31,7 @@ export const layer = Layer.effect(
load: EffectRuntime.fn("Policy.load")(function* (input) {
statements = input
}),
hasStatements: () => statements.length > 0,
evaluate: EffectRuntime.fn("Policy.evaluate")(function* (action, resource, fallback) {
return (
statements.findLast(

View File

@@ -101,8 +101,8 @@ export class Info extends Schema.Class<Info>("ProviderV2.Info")({
endpoint: Endpoint,
options: Options,
}) {
static empty(providerID: ID) {
return new Info({
static empty(providerID: ID): Info {
return {
id: providerID,
name: providerID,
enabled: false,
@@ -118,6 +118,6 @@ export class Info extends Schema.Class<Info>("ProviderV2.Info")({
request: {},
},
},
})
}
}
}

View File

@@ -1,7 +1,7 @@
export * as State from "./state"
import { Effect, Scope, Semaphore } from "effect"
import { createDraft, finishDraft, type Draft, type Objectish } from "immer"
import type { Draft, Objectish } from "immer"
export type Transform<Editor> = (editor: Editor) => void
export type MakeEditor<State extends Objectish, Editor> = (draft: Draft<State>) => Editor
@@ -24,17 +24,18 @@ export function create<State extends Objectish, Editor>(options: Options<State,
let transforms: { update: Transform<Editor> }[] = []
const semaphore = Semaphore.makeUnsafe(1)
const commit = Effect.fn("State.commit")(function* (draft: Draft<State>, reason?: string) {
const api = options.editor(draft)
const commit = Effect.fn("State.commit")(function* (next: State, reason?: string) {
const api = options.editor(next as Draft<State>)
if (options.finalize) yield* options.finalize(api, reason)
state = finishDraft(draft) as State
state = next
})
const rebuild = Effect.fn("State.rebuild")(function* () {
const draft = createDraft(options.initial())
const api = options.editor(draft)
for (const transform of transforms) transform.update(api)
yield* commit(draft)
const next = options.initial()
const api = options.editor(next as Draft<State>)
for (const transform of transforms)
yield* Effect.sync(() => transform.update(api)).pipe(Effect.withSpan("State.rebuild.update", {}))
yield* commit(next)
}, semaphore.withPermit)
return {
@@ -55,9 +56,9 @@ export function create<State extends Objectish, Editor>(options: Options<State,
})
}),
update: Effect.fn("State.update")(function* (update, reason) {
const draft = createDraft(state)
yield* update(options.editor(draft))
yield* commit(draft, reason)
const api = options.editor(state as Draft<State>)
yield* update(api)
if (options.finalize) yield* options.finalize(api, reason)
}, semaphore.withPermit),
}
}