Compare commits

...

38 Commits

Author SHA1 Message Date
Kit Langton f840b3e3a2 fix: complete legacy workspace removal 2026-08-05 18:52:44 -04:00
Kit Langton 05c3866788 fix(cli): remove legacy workspace request fields 2026-08-05 18:33:39 -04:00
Kit Langton 3eca11a7da chore(client): regenerate workspace-free API 2026-08-05 18:32:05 -04:00
Kit Langton 794283057a refactor: remove legacy workspace control plane 2026-08-05 18:29:26 -04:00
Kit Langton 56c33e84a3 fix(tui): keep model search order stable (#40753) 2026-08-05 18:19:26 -04:00
Kit Langton 73581b3c3b refactor(web): remove unused icons (#40744) 2026-08-05 21:51:54 +00:00
Kit Langton 6e4d01f846 refactor(session-ui): remove unused panel title (#40741) 2026-08-05 21:50:46 +00:00
Kit Langton 8646587c95 refactor(web): remove unused share anchor (#40742) 2026-08-05 17:46:40 -04:00
Kit Langton fb47f06228 refactor(desktop): remove superseded local sidecar (#40743) 2026-08-05 17:46:19 -04:00
Kit Langton 3acaa5a359 refactor(core): remove unused shell posix metadata (#40734) 2026-08-05 21:31:35 +00:00
Kit Langton 0726a25142 refactor(console): remove abandoned desktop promo (#40737) 2026-08-05 21:30:53 +00:00
Kit Langton 0df96ddeb0 refactor(codemode): remove unused spread helper (#40729) 2026-08-05 21:26:33 +00:00
Kit Langton 887673310b refactor(core): remove unused session exports (#40730) 2026-08-05 21:26:16 +00:00
Kit Langton 693a1bff81 refactor(core): remove unused copy strategy registry (#40735) 2026-08-05 21:25:18 +00:00
Kit Langton b40ed3aa85 refactor(console): remove unused mail helpers (#40740) 2026-08-05 21:24:21 +00:00
Kit Langton 732bb9c3cb refactor(core): remove discarded MCP connection details (#40726) 2026-08-05 17:20:13 -04:00
Kit Langton 2fd1660b4d refactor(core): remove test-only ignore helper (#40733) 2026-08-05 21:15:22 +00:00
Kit Langton 681526d348 refactor(core): remove duplicate directory query (#40731) 2026-08-05 21:10:01 +00:00
Kit Langton e78869c849 refactor(core): remove unused agent default method (#40725) 2026-08-05 20:46:36 +00:00
Kit Langton 934935963d refactor(core): remove unused filesystem search methods (#40703) 2026-08-05 19:42:30 +00:00
Kit Langton f3912a2a8a refactor(app): remove unused UI helpers (#40686) 2026-08-05 15:15:15 -04:00
Kit Langton 45d58717a4 fix(core): bound protected home searches (#40518) 2026-08-05 15:15:02 -04:00
Kit Langton 74e3155ef0 refactor(session-ui): remove unused exported components (#40685) 2026-08-05 15:13:39 -04:00
Kit Langton 0af6c82563 refactor(core): remove orphaned message projector paths (#40705) 2026-08-05 15:13:15 -04:00
Kit Langton 686127f809 refactor(core): remove unreachable repository validation (#40700) 2026-08-05 14:30:29 -04:00
Kit Langton 5256655c4d refactor(core): remove obsolete project commit bridge (#40701) 2026-08-05 18:16:55 +00:00
Kit Langton d10b652637 refactor(core): remove unused formatter methods (#40684) 2026-08-05 13:45:23 -04:00
Kit Langton b03ca0d4e2 refactor(core): remove unused bus sequences (#40681) 2026-08-05 13:41:38 -04:00
Kit Langton 25aaea3d31 refactor(core): remove unused shell tree killer (#40680) 2026-08-05 13:38:59 -04:00
Kit Langton cae7a139bc refactor(server): remove obsolete auth header helpers (#40682) 2026-08-05 13:37:29 -04:00
Kit Langton 5ea62ab05f refactor(core): remove orphaned git change operations (#40683) 2026-08-05 13:32:03 -04:00
Kit Langton faadc05c88 refactor(core): remove unused snapshot operations (#40687) 2026-08-05 13:01:48 -04:00
Kit Langton 3e253c589e refactor(core): persist instruction updates as messages (#40679) 2026-08-05 12:55:29 -04:00
Kit Langton 0a0fc09533 refactor(core): remove unused file mutation methods (#40667) 2026-08-05 12:33:20 -04:00
Kit Langton 5aa0413fea refactor(core): remove dead fork pending copy (#40675) 2026-08-05 12:30:42 -04:00
Kit Langton 6f4c199629 fix(tui): wait for diff request in tests (#40670) 2026-08-05 16:02:22 +00:00
Kit Langton ed8e1f4654 fix(core): reconcile promoted prompt retries from messages (#40664) 2026-08-05 11:43:03 -04:00
Kit Langton 3b0195e045 fix(core): avoid eager directory snapshots (#40552) 2026-08-05 14:52:07 +00:00
119 changed files with 658 additions and 7229 deletions
-5
View File
@@ -248,11 +248,6 @@ export function formatKeybind(config: string, t?: (key: KeyLabel) => string): st
return IS_MAC ? parts.join("") : parts.join("+") return IS_MAC ? parts.join("") : parts.join("+")
} }
// KeybindV2 takes an array instead of a string
export function formatKeybindKeys(config: string, t?: (key: KeyLabel) => string): string[] {
return formatKeybindParts(config, t)
}
function isEditableTarget(target: EventTarget | null) { function isEditableTarget(target: EventTarget | null) {
if (!(target instanceof HTMLElement)) return false if (!(target instanceof HTMLElement)) return false
if (target.isContentEditable) return true if (target.isContentEditable) return true
-7
View File
@@ -286,13 +286,6 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
children: tree.children, children: tree.children,
expand: tree.expandDir, expand: tree.expandDir,
collapse: tree.collapseDir, collapse: tree.collapseDir,
toggle(input: string) {
if (tree.dirState(input)?.expanded) {
tree.collapseDir(input)
return
}
tree.expandDir(input)
},
}, },
get, get,
load, load,
@@ -153,18 +153,6 @@ export function normalizeProviderList(
} }
} }
export function sanitizeProject(project: Project) {
if (!project.icon?.url && !project.icon?.override) return project
return {
...project,
icon: {
...project.icon,
url: undefined,
override: undefined,
},
}
}
export function normalizeProjectInfo(project: Project | CurrentProject): Project { export function normalizeProjectInfo(project: Project | CurrentProject): Project {
return { return {
...project, ...project,
-30
View File
@@ -753,9 +753,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
mobileSidebar: { mobileSidebar: {
opened: createMemo(() => store.mobileSidebar?.opened ?? false), opened: createMemo(() => store.mobileSidebar?.opened ?? false),
show() {
setStore("mobileSidebar", "opened", true)
},
hide() { hide() {
setStore("mobileSidebar", "opened", false) setStore("mobileSidebar", "opened", false)
}, },
@@ -961,33 +958,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
if (current.reviewOpen.includes(path)) return if (current.reviewOpen.includes(path)) return
setStore("sessionView", session, "reviewOpen", current.reviewOpen.length, path) setStore("sessionView", session, "reviewOpen", current.reviewOpen.length, path)
}, },
closePath(path: string) {
const session = key()
const current = store.sessionView[session]?.reviewOpen
if (!current) return
const index = current.indexOf(path)
if (index === -1) return
setStore(
"sessionView",
session,
"reviewOpen",
produce((draft) => {
if (!draft) return
draft.splice(index, 1)
}),
)
},
togglePath(path: string) {
const session = key()
const current = store.sessionView[session]?.reviewOpen
if (!current || !current.includes(path)) {
this.openPath(path)
return
}
this.closePath(path)
},
}, },
} }
}, },
@@ -22,8 +22,6 @@ type TabsInput = {
fileBrowser?: Accessor<boolean> fileBrowser?: Accessor<boolean>
} }
export const getSessionKey = (dir: string | undefined, id: string | undefined) => `${dir ?? ""}${id ? `/${id}` : ""}`
export function shouldShowFileTree(input: { visible: boolean; opened: boolean }) { export function shouldShowFileTree(input: { visible: boolean; opened: boolean }) {
return input.opened && input.visible return input.opened && input.visible
} }
+1 -1
View File
@@ -86,7 +86,7 @@ export async function runMini(input: MiniCommandInput) {
) => ) =>
resolveSessionTarget({ resolveSessionTarget({
client, client,
location: { directory: next.location.directory, workspace: next.location.workspaceID }, location: { directory: next.location.directory },
agent: next.agent, agent: next.agent,
model: next.model model: next.model
? { providerID: next.model.providerID, id: next.model.modelID, variant: next.variant } ? { providerID: next.model.providerID, id: next.model.modelID, variant: next.variant }
+1 -2
View File
@@ -703,7 +703,7 @@ export async function runNonInteractivePrompt(input: Input) {
? Promise.resolve(undefined) ? Promise.resolve(undefined)
: input.client.form.request : input.client.form.request
.list({ .list({
location: { directory: input.location.directory, workspace: input.location.workspaceID }, location: { directory: input.location.directory },
}) })
.catch(() => undefined), .catch(() => undefined),
]) ])
@@ -757,7 +757,6 @@ function formRequestOptions(location: LocationRef | undefined): [] | [{ headers:
{ {
headers: { headers: {
"x-opencode-directory": encodeURIComponent(location.directory), "x-opencode-directory": encodeURIComponent(location.directory),
...(location.workspaceID ? { "x-opencode-workspace": location.workspaceID } : {}),
}, },
}, },
] ]
+1 -1
View File
@@ -96,7 +96,7 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: End
next.model ?? next.model ??
(options.variant (options.variant
? await client.model ? await client.model
.default({ location: { directory: next.location.directory, workspace: next.location.workspaceID } }) .default({ location: { directory: next.location.directory } })
.then((result) => result.data) .then((result) => result.data)
: undefined) : undefined)
const model = selected const model = selected
-1
View File
@@ -136,7 +136,6 @@ async function latestSession(
const page = await client.session.list( const page = await client.session.list(
{ {
directory: location.directory, directory: location.directory,
workspace: location.workspaceID,
parentID: null, parentID: null,
limit: SESSION_PAGE_LIMIT, limit: SESSION_PAGE_LIMIT,
order: "desc", order: "desc",
+1 -2
View File
@@ -426,14 +426,13 @@ describe("runNonInteractivePrompt", () => {
const globalOptions = { const globalOptions = {
headers: { headers: {
"x-opencode-directory": "%2Fwork%20tree", "x-opencode-directory": "%2Fwork%20tree",
"x-opencode-workspace": "wrk_1",
}, },
} }
expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_live" }, globalOptions) expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_live" }, globalOptions)
expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "ses_1", formID: "frm_pending" }) expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "ses_1", formID: "frm_pending" })
expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_pending_global" }, globalOptions) expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_pending_global" }, globalOptions)
expect(sdk.form.request.list).toHaveBeenCalledWith({ expect(sdk.form.request.list).toHaveBeenCalledWith({
location: { directory: "/work tree", workspace: "wrk_1" }, location: { directory: "/work tree" },
}) })
expect(sdk.question.list).not.toHaveBeenCalled() expect(sdk.question.list).not.toHaveBeenCalled()
expect(sdk.question.reject).not.toHaveBeenCalled() expect(sdk.question.reject).not.toHaveBeenCalled()
+60 -104
View File
@@ -3,13 +3,13 @@ import type { Effect, Stream } from "effect"
import type { Location } from "@opencode-ai/schema/location" import type { Location } from "@opencode-ai/schema/location"
import type { Agent } from "@opencode-ai/schema/agent" import type { Agent } from "@opencode-ai/schema/agent"
import type { Plugin } from "@opencode-ai/schema/plugin" import type { Plugin } from "@opencode-ai/schema/plugin"
import type { Workspace } from "@opencode-ai/schema/workspace"
import type { Session } from "@opencode-ai/schema/session" import type { Session } from "@opencode-ai/schema/session"
import type { AbsolutePath } from "@opencode-ai/schema/schema" import type { AbsolutePath } from "@opencode-ai/schema/schema"
import type { Project } from "@opencode-ai/schema/project" import type { Project } from "@opencode-ai/schema/project"
import type { RelativePath } from "@opencode-ai/schema/schema" import type { RelativePath } from "@opencode-ai/schema/schema"
import type { Brand } from "effect" import type { Brand } from "effect"
import type { Model } from "@opencode-ai/schema/model" import type { Model } from "@opencode-ai/schema/model"
import type { Workspace } from "@opencode-ai/schema/workspace"
import type { SessionMessage } from "@opencode-ai/schema/session-message" import type { SessionMessage } from "@opencode-ai/schema/session-message"
import type { PromptInput } from "@opencode-ai/schema/prompt-input" import type { PromptInput } from "@opencode-ai/schema/prompt-input"
import type { AgentAttachment } from "@opencode-ai/schema/prompt" import type { AgentAttachment } from "@opencode-ai/schema/prompt"
@@ -58,9 +58,7 @@ export interface ServerApi<E = never> {
readonly get: ServerGetOperation<E> readonly get: ServerGetOperation<E>
} }
export type Endpoint2_0Input = { export type Endpoint2_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint2_0Output = Location.Info export type Endpoint2_0Output = Location.Info
export type LocationGetOperation<E = never> = (input?: Endpoint2_0Input) => Effect.Effect<Endpoint2_0Output, E> export type LocationGetOperation<E = never> = (input?: Endpoint2_0Input) => Effect.Effect<Endpoint2_0Output, E>
@@ -68,15 +66,13 @@ export interface LocationApi<E = never> {
readonly get: LocationGetOperation<E> readonly get: LocationGetOperation<E>
} }
export type Endpoint3_0Input = { export type Endpoint3_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint3_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Agent.Info> } export type Endpoint3_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Agent.Info> }
export type AgentListOperation<E = never> = (input?: Endpoint3_0Input) => Effect.Effect<Endpoint3_0Output, E> export type AgentListOperation<E = never> = (input?: Endpoint3_0Input) => Effect.Effect<Endpoint3_0Output, E>
export type Endpoint3_1Input = { export type Endpoint3_1Input = {
readonly agentID: Agent.ID readonly agentID: Agent.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint3_1Output = { readonly location: Location.Info; readonly data: Agent.Info } export type Endpoint3_1Output = { readonly location: Location.Info; readonly data: Agent.Info }
export type AgentGetOperation<E = never> = (input: Endpoint3_1Input) => Effect.Effect<Endpoint3_1Output, E> export type AgentGetOperation<E = never> = (input: Endpoint3_1Input) => Effect.Effect<Endpoint3_1Output, E>
@@ -86,9 +82,7 @@ export interface AgentApi<E = never> {
readonly get: AgentGetOperation<E> readonly get: AgentGetOperation<E>
} }
export type Endpoint4_0Input = { export type Endpoint4_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint4_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Plugin.Info> } export type Endpoint4_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Plugin.Info> }
export type PluginListOperation<E = never> = (input?: Endpoint4_0Input) => Effect.Effect<Endpoint4_0Output, E> export type PluginListOperation<E = never> = (input?: Endpoint4_0Input) => Effect.Effect<Endpoint4_0Output, E>
@@ -97,7 +91,6 @@ export interface PluginApi<E = never> {
} }
export type Endpoint5_0Input = { export type Endpoint5_0Input = {
readonly workspace?: Workspace.ID | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -420,6 +413,7 @@ export type Endpoint5_26Output =
readonly data: { readonly data: {
readonly sessionID: Session.ID readonly sessionID: Session.ID
readonly delta: { readonly [x: string]: (string & Brand.Brand<"Instruction.Hash">) | "removed" } readonly delta: { readonly [x: string]: (string & Brand.Brand<"Instruction.Hash">) | "removed" }
readonly text?: string | undefined
} }
} }
| { | {
@@ -897,15 +891,11 @@ export interface MessageApi<E = never> {
readonly list: MessageListOperation<E> readonly list: MessageListOperation<E>
} }
export type Endpoint7_0Input = { export type Endpoint7_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint7_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Model.Info> } export type Endpoint7_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Model.Info> }
export type ModelListOperation<E = never> = (input?: Endpoint7_0Input) => Effect.Effect<Endpoint7_0Output, E> export type ModelListOperation<E = never> = (input?: Endpoint7_0Input) => Effect.Effect<Endpoint7_0Output, E>
export type Endpoint7_1Input = { export type Endpoint7_1Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint7_1Output = { readonly location: Location.Info; readonly data: Model.Info | undefined } export type Endpoint7_1Output = { readonly location: Location.Info; readonly data: Model.Info | undefined }
export type ModelDefaultOperation<E = never> = (input?: Endpoint7_1Input) => Effect.Effect<Endpoint7_1Output, E> export type ModelDefaultOperation<E = never> = (input?: Endpoint7_1Input) => Effect.Effect<Endpoint7_1Output, E>
@@ -915,7 +905,7 @@ export interface ModelApi<E = never> {
} }
export type Endpoint8_0Input = { export type Endpoint8_0Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly prompt: string readonly prompt: string
readonly model?: Model.Ref | undefined readonly model?: Model.Ref | undefined
} }
@@ -926,15 +916,13 @@ export interface GenerateApi<E = never> {
readonly text: GenerateTextOperation<E> readonly text: GenerateTextOperation<E>
} }
export type Endpoint9_0Input = { export type Endpoint9_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint9_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Provider.Info> } export type Endpoint9_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Provider.Info> }
export type ProviderListOperation<E = never> = (input?: Endpoint9_0Input) => Effect.Effect<Endpoint9_0Output, E> export type ProviderListOperation<E = never> = (input?: Endpoint9_0Input) => Effect.Effect<Endpoint9_0Output, E>
export type Endpoint9_1Input = { export type Endpoint9_1Input = {
readonly providerID: Provider.ID readonly providerID: Provider.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint9_1Output = { readonly location: Location.Info; readonly data: Provider.Info } export type Endpoint9_1Output = { readonly location: Location.Info; readonly data: Provider.Info }
export type ProviderGetOperation<E = never> = (input: Endpoint9_1Input) => Effect.Effect<Endpoint9_1Output, E> export type ProviderGetOperation<E = never> = (input: Endpoint9_1Input) => Effect.Effect<Endpoint9_1Output, E>
@@ -944,21 +932,19 @@ export interface ProviderApi<E = never> {
readonly get: ProviderGetOperation<E> readonly get: ProviderGetOperation<E>
} }
export type Endpoint10_0Input = { export type Endpoint10_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint10_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Integration.Info> } export type Endpoint10_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Integration.Info> }
export type IntegrationListOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E> export type IntegrationListOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E>
export type Endpoint10_1Input = { export type Endpoint10_1Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint10_1Output = { readonly location: Location.Info; readonly data: Integration.Info | undefined } export type Endpoint10_1Output = { readonly location: Location.Info; readonly data: Integration.Info | undefined }
export type IntegrationGetOperation<E = never> = (input: Endpoint10_1Input) => Effect.Effect<Endpoint10_1Output, E> export type IntegrationGetOperation<E = never> = (input: Endpoint10_1Input) => Effect.Effect<Endpoint10_1Output, E>
export type Endpoint10_2Input = { export type Endpoint10_2Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly url: string readonly url: string
} }
export type Endpoint10_2Output = void export type Endpoint10_2Output = void
@@ -968,7 +954,7 @@ export type IntegrationWellknownAddOperation<E = never> = (
export type Endpoint10_3Input = { export type Endpoint10_3Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly key: string readonly key: string
readonly label?: string | undefined readonly label?: string | undefined
} }
@@ -979,7 +965,7 @@ export type IntegrationConnectKeyOperation<E = never> = (
export type Endpoint10_4Input = { export type Endpoint10_4Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly methodID: Integration.MethodID readonly methodID: Integration.MethodID
readonly inputs: { readonly [x: string]: string } readonly inputs: { readonly [x: string]: string }
readonly label?: string | undefined readonly label?: string | undefined
@@ -992,7 +978,7 @@ export type IntegrationOauthConnectOperation<E = never> = (
export type Endpoint10_5Input = { export type Endpoint10_5Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly attemptID: Integration.AttemptID readonly attemptID: Integration.AttemptID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint10_5Output = { readonly location: Location.Info; readonly data: Integration.AttemptStatus } export type Endpoint10_5Output = { readonly location: Location.Info; readonly data: Integration.AttemptStatus }
export type IntegrationOauthStatusOperation<E = never> = ( export type IntegrationOauthStatusOperation<E = never> = (
@@ -1002,7 +988,7 @@ export type IntegrationOauthStatusOperation<E = never> = (
export type Endpoint10_6Input = { export type Endpoint10_6Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly attemptID: Integration.AttemptID readonly attemptID: Integration.AttemptID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly code?: string | undefined readonly code?: string | undefined
} }
export type Endpoint10_6Output = void export type Endpoint10_6Output = void
@@ -1013,7 +999,7 @@ export type IntegrationOauthCompleteOperation<E = never> = (
export type Endpoint10_7Input = { export type Endpoint10_7Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly attemptID: Integration.AttemptID readonly attemptID: Integration.AttemptID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint10_7Output = void export type Endpoint10_7Output = void
export type IntegrationOauthCancelOperation<E = never> = ( export type IntegrationOauthCancelOperation<E = never> = (
@@ -1022,7 +1008,7 @@ export type IntegrationOauthCancelOperation<E = never> = (
export type Endpoint10_8Input = { export type Endpoint10_8Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly methodID: Integration.MethodID readonly methodID: Integration.MethodID
readonly label?: string | undefined readonly label?: string | undefined
} }
@@ -1034,7 +1020,7 @@ export type IntegrationCommandConnectOperation<E = never> = (
export type Endpoint10_9Input = { export type Endpoint10_9Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly attemptID: Integration.AttemptID readonly attemptID: Integration.AttemptID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint10_9Output = { readonly location: Location.Info; readonly data: Integration.CommandAttemptStatus } export type Endpoint10_9Output = { readonly location: Location.Info; readonly data: Integration.CommandAttemptStatus }
export type IntegrationCommandStatusOperation<E = never> = ( export type IntegrationCommandStatusOperation<E = never> = (
@@ -1044,7 +1030,7 @@ export type IntegrationCommandStatusOperation<E = never> = (
export type Endpoint10_10Input = { export type Endpoint10_10Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly attemptID: Integration.AttemptID readonly attemptID: Integration.AttemptID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint10_10Output = void export type Endpoint10_10Output = void
export type IntegrationCommandCancelOperation<E = never> = ( export type IntegrationCommandCancelOperation<E = never> = (
@@ -1069,15 +1055,13 @@ export interface IntegrationApi<E = never> {
} }
} }
export type Endpoint11_0Input = { export type Endpoint11_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint11_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Mcp.Server> } export type Endpoint11_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Mcp.Server> }
export type McpListOperation<E = never> = (input?: Endpoint11_0Input) => Effect.Effect<Endpoint11_0Output, E> export type McpListOperation<E = never> = (input?: Endpoint11_0Input) => Effect.Effect<Endpoint11_0Output, E>
export type Endpoint11_1Input = { export type Endpoint11_1Input = {
readonly server: string readonly server: string
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly config: Mcp.LocalConfig | Mcp.RemoteConfig readonly config: Mcp.LocalConfig | Mcp.RemoteConfig
} }
export type Endpoint11_1Output = void export type Endpoint11_1Output = void
@@ -1085,28 +1069,26 @@ export type McpAddOperation<E = never> = (input: Endpoint11_1Input) => Effect.Ef
export type Endpoint11_2Input = { export type Endpoint11_2Input = {
readonly server: string readonly server: string
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint11_2Output = void export type Endpoint11_2Output = void
export type McpRemoveOperation<E = never> = (input: Endpoint11_2Input) => Effect.Effect<Endpoint11_2Output, E> export type McpRemoveOperation<E = never> = (input: Endpoint11_2Input) => Effect.Effect<Endpoint11_2Output, E>
export type Endpoint11_3Input = { export type Endpoint11_3Input = {
readonly server: string readonly server: string
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint11_3Output = void export type Endpoint11_3Output = void
export type McpConnectOperation<E = never> = (input: Endpoint11_3Input) => Effect.Effect<Endpoint11_3Output, E> export type McpConnectOperation<E = never> = (input: Endpoint11_3Input) => Effect.Effect<Endpoint11_3Output, E>
export type Endpoint11_4Input = { export type Endpoint11_4Input = {
readonly server: string readonly server: string
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint11_4Output = void export type Endpoint11_4Output = void
export type McpDisconnectOperation<E = never> = (input: Endpoint11_4Input) => Effect.Effect<Endpoint11_4Output, E> export type McpDisconnectOperation<E = never> = (input: Endpoint11_4Input) => Effect.Effect<Endpoint11_4Output, E>
export type Endpoint11_5Input = { export type Endpoint11_5Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint11_5Output = { readonly location: Location.Info; readonly data: Mcp.ResourceCatalog } export type Endpoint11_5Output = { readonly location: Location.Info; readonly data: Mcp.ResourceCatalog }
export type McpResourceCatalogOperation<E = never> = (input?: Endpoint11_5Input) => Effect.Effect<Endpoint11_5Output, E> export type McpResourceCatalogOperation<E = never> = (input?: Endpoint11_5Input) => Effect.Effect<Endpoint11_5Output, E>
@@ -1121,7 +1103,7 @@ export interface McpApi<E = never> {
export type Endpoint12_0Input = { export type Endpoint12_0Input = {
readonly credentialID: Credential.ID readonly credentialID: Credential.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly label: string readonly label: string
} }
export type Endpoint12_0Output = void export type Endpoint12_0Output = void
@@ -1129,7 +1111,7 @@ export type CredentialUpdateOperation<E = never> = (input: Endpoint12_0Input) =>
export type Endpoint12_1Input = { export type Endpoint12_1Input = {
readonly credentialID: Credential.ID readonly credentialID: Credential.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint12_1Output = void export type Endpoint12_1Output = void
export type CredentialRemoveOperation<E = never> = (input: Endpoint12_1Input) => Effect.Effect<Endpoint12_1Output, E> export type CredentialRemoveOperation<E = never> = (input: Endpoint12_1Input) => Effect.Effect<Endpoint12_1Output, E>
@@ -1142,15 +1124,13 @@ export interface CredentialApi<E = never> {
export type Endpoint13_0Output = ReadonlyArray<Project.Info> export type Endpoint13_0Output = ReadonlyArray<Project.Info>
export type ProjectListOperation<E = never> = () => Effect.Effect<Endpoint13_0Output, E> export type ProjectListOperation<E = never> = () => Effect.Effect<Endpoint13_0Output, E>
export type Endpoint13_1Input = { export type Endpoint13_1Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint13_1Output = Project.Current export type Endpoint13_1Output = Project.Current
export type ProjectCurrentOperation<E = never> = (input?: Endpoint13_1Input) => Effect.Effect<Endpoint13_1Output, E> export type ProjectCurrentOperation<E = never> = (input?: Endpoint13_1Input) => Effect.Effect<Endpoint13_1Output, E>
export type Endpoint13_2Input = { export type Endpoint13_2Input = {
readonly projectID: Project.ID readonly projectID: Project.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint13_2Output = Project.Directories export type Endpoint13_2Output = Project.Directories
export type ProjectDirectoriesOperation<E = never> = (input: Endpoint13_2Input) => Effect.Effect<Endpoint13_2Output, E> export type ProjectDirectoriesOperation<E = never> = (input: Endpoint13_2Input) => Effect.Effect<Endpoint13_2Output, E>
@@ -1161,9 +1141,7 @@ export interface ProjectApi<E = never> {
readonly directories: ProjectDirectoriesOperation<E> readonly directories: ProjectDirectoriesOperation<E>
} }
export type Endpoint14_0Input = { export type Endpoint14_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint14_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Form.Info> } export type Endpoint14_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Form.Info> }
export type FormRequestListOperation<E = never> = (input?: Endpoint14_0Input) => Effect.Effect<Endpoint14_0Output, E> export type FormRequestListOperation<E = never> = (input?: Endpoint14_0Input) => Effect.Effect<Endpoint14_0Output, E>
@@ -1207,9 +1185,7 @@ export interface FormApi<E = never> {
readonly cancel: FormCancelOperation<E> readonly cancel: FormCancelOperation<E>
} }
export type Endpoint15_0Input = { export type Endpoint15_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint15_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Permission.Request> } export type Endpoint15_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Permission.Request> }
export type PermissionRequestListOperation<E = never> = ( export type PermissionRequestListOperation<E = never> = (
input?: Endpoint15_0Input, input?: Endpoint15_0Input,
@@ -1267,14 +1243,14 @@ export interface PermissionApi<E = never> {
} }
export type Endpoint16_0Input = { export type Endpoint16_0Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly path?: RelativePath | undefined readonly path?: RelativePath | undefined
} }
export type Endpoint16_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<FileSystem.Entry> } export type Endpoint16_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<FileSystem.Entry> }
export type FileListOperation<E = never> = (input?: Endpoint16_0Input) => Effect.Effect<Endpoint16_0Output, E> export type FileListOperation<E = never> = (input?: Endpoint16_0Input) => Effect.Effect<Endpoint16_0Output, E>
export type Endpoint16_1Input = { export type Endpoint16_1Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly query: string readonly query: string
readonly type?: "file" | "directory" | undefined readonly type?: "file" | "directory" | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
@@ -1287,9 +1263,7 @@ export interface FileApi<E = never> {
readonly find: FileFindOperation<E> readonly find: FileFindOperation<E>
} }
export type Endpoint17_0Input = { export type Endpoint17_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint17_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Command.Info> } export type Endpoint17_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Command.Info> }
export type CommandListOperation<E = never> = (input?: Endpoint17_0Input) => Effect.Effect<Endpoint17_0Output, E> export type CommandListOperation<E = never> = (input?: Endpoint17_0Input) => Effect.Effect<Endpoint17_0Output, E>
@@ -1297,9 +1271,7 @@ export interface CommandApi<E = never> {
readonly list: CommandListOperation<E> readonly list: CommandListOperation<E>
} }
export type Endpoint18_0Input = { export type Endpoint18_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint18_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Skill.Info> } export type Endpoint18_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Skill.Info> }
export type SkillListOperation<E = never> = (input?: Endpoint18_0Input) => Effect.Effect<Endpoint18_0Output, E> export type SkillListOperation<E = never> = (input?: Endpoint18_0Input) => Effect.Effect<Endpoint18_0Output, E>
@@ -1314,14 +1286,12 @@ export interface EventApi<E = never> {
readonly subscribe: EventSubscribeOperation<E> readonly subscribe: EventSubscribeOperation<E>
} }
export type Endpoint20_0Input = { export type Endpoint20_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint20_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Pty.Info> } export type Endpoint20_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Pty.Info> }
export type PtyListOperation<E = never> = (input?: Endpoint20_0Input) => Effect.Effect<Endpoint20_0Output, E> export type PtyListOperation<E = never> = (input?: Endpoint20_0Input) => Effect.Effect<Endpoint20_0Output, E>
export type Endpoint20_1Input = { export type Endpoint20_1Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly command?: string | undefined readonly command?: string | undefined
readonly args?: ReadonlyArray<string> | undefined readonly args?: ReadonlyArray<string> | undefined
readonly cwd?: string | undefined readonly cwd?: string | undefined
@@ -1333,14 +1303,14 @@ export type PtyCreateOperation<E = never> = (input?: Endpoint20_1Input) => Effec
export type Endpoint20_2Input = { export type Endpoint20_2Input = {
readonly ptyID: Pty.ID readonly ptyID: Pty.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint20_2Output = { readonly location: Location.Info; readonly data: Pty.Info } export type Endpoint20_2Output = { readonly location: Location.Info; readonly data: Pty.Info }
export type PtyGetOperation<E = never> = (input: Endpoint20_2Input) => Effect.Effect<Endpoint20_2Output, E> export type PtyGetOperation<E = never> = (input: Endpoint20_2Input) => Effect.Effect<Endpoint20_2Output, E>
export type Endpoint20_3Input = { export type Endpoint20_3Input = {
readonly ptyID: Pty.ID readonly ptyID: Pty.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly title?: string | undefined readonly title?: string | undefined
readonly size?: { readonly rows: number; readonly cols: number } | undefined readonly size?: { readonly rows: number; readonly cols: number } | undefined
} }
@@ -1349,7 +1319,7 @@ export type PtyUpdateOperation<E = never> = (input: Endpoint20_3Input) => Effect
export type Endpoint20_4Input = { export type Endpoint20_4Input = {
readonly ptyID: Pty.ID readonly ptyID: Pty.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint20_4Output = void export type Endpoint20_4Output = void
export type PtyRemoveOperation<E = never> = (input: Endpoint20_4Input) => Effect.Effect<Endpoint20_4Output, E> export type PtyRemoveOperation<E = never> = (input: Endpoint20_4Input) => Effect.Effect<Endpoint20_4Output, E>
@@ -1362,14 +1332,12 @@ export interface PtyApi<E = never> {
readonly remove: PtyRemoveOperation<E> readonly remove: PtyRemoveOperation<E>
} }
export type Endpoint21_0Input = { export type Endpoint21_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint21_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Shell.Info> } export type Endpoint21_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Shell.Info> }
export type ShellListOperation<E = never> = (input?: Endpoint21_0Input) => Effect.Effect<Endpoint21_0Output, E> export type ShellListOperation<E = never> = (input?: Endpoint21_0Input) => Effect.Effect<Endpoint21_0Output, E>
export type Endpoint21_1Input = { export type Endpoint21_1Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly command: string readonly command: string
readonly cwd?: string | undefined readonly cwd?: string | undefined
readonly timeout: number readonly timeout: number
@@ -1380,14 +1348,14 @@ export type ShellCreateOperation<E = never> = (input: Endpoint21_1Input) => Effe
export type Endpoint21_2Input = { export type Endpoint21_2Input = {
readonly id: Shell.ID readonly id: Shell.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint21_2Output = { readonly location: Location.Info; readonly data: Shell.Info } export type Endpoint21_2Output = { readonly location: Location.Info; readonly data: Shell.Info }
export type ShellGetOperation<E = never> = (input: Endpoint21_2Input) => Effect.Effect<Endpoint21_2Output, E> export type ShellGetOperation<E = never> = (input: Endpoint21_2Input) => Effect.Effect<Endpoint21_2Output, E>
export type Endpoint21_3Input = { export type Endpoint21_3Input = {
readonly id: Shell.ID readonly id: Shell.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly timeout: number readonly timeout: number
} }
export type Endpoint21_3Output = { readonly location: Location.Info; readonly data: Shell.Info } export type Endpoint21_3Output = { readonly location: Location.Info; readonly data: Shell.Info }
@@ -1395,7 +1363,7 @@ export type ShellTimeoutOperation<E = never> = (input: Endpoint21_3Input) => Eff
export type Endpoint21_4Input = { export type Endpoint21_4Input = {
readonly id: Shell.ID readonly id: Shell.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly cursor?: number | undefined readonly cursor?: number | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
} }
@@ -1412,7 +1380,7 @@ export type ShellOutputOperation<E = never> = (input: Endpoint21_4Input) => Effe
export type Endpoint21_5Input = { export type Endpoint21_5Input = {
readonly id: Shell.ID readonly id: Shell.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint21_5Output = void export type Endpoint21_5Output = void
export type ShellRemoveOperation<E = never> = (input: Endpoint21_5Input) => Effect.Effect<Endpoint21_5Output, E> export type ShellRemoveOperation<E = never> = (input: Endpoint21_5Input) => Effect.Effect<Endpoint21_5Output, E>
@@ -1426,9 +1394,7 @@ export interface ShellApi<E = never> {
readonly remove: ShellRemoveOperation<E> readonly remove: ShellRemoveOperation<E>
} }
export type Endpoint22_0Input = { export type Endpoint22_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint22_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Question.Request> } export type Endpoint22_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Question.Request> }
export type QuestionRequestListOperation<E = never> = ( export type QuestionRequestListOperation<E = never> = (
input?: Endpoint22_0Input, input?: Endpoint22_0Input,
@@ -1457,9 +1423,7 @@ export interface QuestionApi<E = never> {
readonly reject: QuestionRejectOperation<E> readonly reject: QuestionRejectOperation<E>
} }
export type Endpoint23_0Input = { export type Endpoint23_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint23_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Reference.Info> } export type Endpoint23_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Reference.Info> }
export type ReferenceListOperation<E = never> = (input?: Endpoint23_0Input) => Effect.Effect<Endpoint23_0Output, E> export type ReferenceListOperation<E = never> = (input?: Endpoint23_0Input) => Effect.Effect<Endpoint23_0Output, E>
@@ -1469,7 +1433,7 @@ export interface ReferenceApi<E = never> {
export type Endpoint24_0Input = { export type Endpoint24_0Input = {
readonly projectID: Project.ID readonly projectID: Project.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly strategy: ProjectCopy.StrategyID readonly strategy: ProjectCopy.StrategyID
readonly directory: AbsolutePath readonly directory: AbsolutePath
readonly name?: string | undefined readonly name?: string | undefined
@@ -1479,7 +1443,7 @@ export type ProjectCopyCreateOperation<E = never> = (input: Endpoint24_0Input) =
export type Endpoint24_1Input = { export type Endpoint24_1Input = {
readonly projectID: Project.ID readonly projectID: Project.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly directory: AbsolutePath readonly directory: AbsolutePath
readonly force: boolean readonly force: boolean
} }
@@ -1488,7 +1452,7 @@ export type ProjectCopyRemoveOperation<E = never> = (input: Endpoint24_1Input) =
export type Endpoint24_2Input = { export type Endpoint24_2Input = {
readonly projectID: Project.ID readonly projectID: Project.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint24_2Output = void export type Endpoint24_2Output = void
export type ProjectCopyRefreshOperation<E = never> = (input: Endpoint24_2Input) => Effect.Effect<Endpoint24_2Output, E> export type ProjectCopyRefreshOperation<E = never> = (input: Endpoint24_2Input) => Effect.Effect<Endpoint24_2Output, E>
@@ -1499,20 +1463,16 @@ export interface ProjectCopyApi<E = never> {
readonly refresh: ProjectCopyRefreshOperation<E> readonly refresh: ProjectCopyRefreshOperation<E>
} }
export type Endpoint25_0Input = { export type Endpoint25_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint25_0Output = { readonly location: Location.Info; readonly data: Vcs.Info } export type Endpoint25_0Output = { readonly location: Location.Info; readonly data: Vcs.Info }
export type VcsGetOperation<E = never> = (input?: Endpoint25_0Input) => Effect.Effect<Endpoint25_0Output, E> export type VcsGetOperation<E = never> = (input?: Endpoint25_0Input) => Effect.Effect<Endpoint25_0Output, E>
export type Endpoint25_1Input = { export type Endpoint25_1Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint25_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Vcs.FileStatus> } export type Endpoint25_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Vcs.FileStatus> }
export type VcsStatusOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E> export type VcsStatusOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
export type Endpoint25_2Input = { export type Endpoint25_2Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly mode: Vcs.Mode readonly mode: Vcs.Mode
readonly context?: number | undefined readonly context?: number | undefined
} }
@@ -1528,9 +1488,7 @@ export interface VcsApi<E = never> {
export type Endpoint26_0Output = ReadonlyArray<Location.Ref> export type Endpoint26_0Output = ReadonlyArray<Location.Ref>
export type DebugLocationListOperation<E = never> = () => Effect.Effect<Endpoint26_0Output, E> export type DebugLocationListOperation<E = never> = () => Effect.Effect<Endpoint26_0Output, E>
export type Endpoint26_1Input = { export type Endpoint26_1Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint26_1Output = void export type Endpoint26_1Output = void
export type DebugLocationEvictOperation<E = never> = (input?: Endpoint26_1Input) => Effect.Effect<Endpoint26_1Output, E> export type DebugLocationEvictOperation<E = never> = (input?: Endpoint26_1Input) => Effect.Effect<Endpoint26_1Output, E>
@@ -1538,14 +1496,12 @@ export interface DebugApi<E = never> {
readonly location: { readonly list: DebugLocationListOperation<E>; readonly evict: DebugLocationEvictOperation<E> } readonly location: { readonly list: DebugLocationListOperation<E>; readonly evict: DebugLocationEvictOperation<E> }
} }
export type Endpoint27_0Input = { export type Endpoint27_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint27_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<WebSearch.Provider> } export type Endpoint27_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<WebSearch.Provider> }
export type WebsearchProvidersOperation<E = never> = (input?: Endpoint27_0Input) => Effect.Effect<Endpoint27_0Output, E> export type WebsearchProvidersOperation<E = never> = (input?: Endpoint27_0Input) => Effect.Effect<Endpoint27_0Output, E>
export type Endpoint27_1Input = { export type Endpoint27_1Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly query: string readonly query: string
readonly providerID?: WebSearch.ID | undefined readonly providerID?: WebSearch.ID | undefined
} }
@@ -285,7 +285,6 @@ const Endpoint5_0 = (raw: RawClient["server.session"]) => (input?: Endpoint5_0In
preserveEffect<Endpoint5_0Output>()( preserveEffect<Endpoint5_0Output>()(
raw["session.list"]({ raw["session.list"]({
query: { query: {
workspace: input?.["workspace"],
limit: input?.["limit"], limit: input?.["limit"],
order: input?.["order"], order: input?.["order"],
search: input?.["search"], search: input?.["search"],
@@ -441,7 +441,6 @@ export function make(options: ClientOptions) {
method: "GET", method: "GET",
path: `/api/session`, path: `/api/session`,
query: { query: {
workspace: input?.["workspace"],
limit: input?.["limit"], limit: input?.["limit"],
order: input?.["order"], order: input?.["order"],
search: input?.["search"], search: input?.["search"],
+68 -197
View File
@@ -676,7 +676,7 @@ export type SessionInstructionsUpdated = {
type: "session.instructions.updated" type: "session.instructions.updated"
durable: { aggregateID: string; seq: number; version: 2 } durable: { aggregateID: string; seq: number; version: 2 }
location?: LocationRef location?: LocationRef
data: { sessionID: string; delta: { [x: string]: string | "removed" } } data: { sessionID: string; delta: { [x: string]: string | "removed" }; text?: string }
} }
export type SessionSynthetic = { export type SessionSynthetic = {
@@ -2514,9 +2514,7 @@ export type HealthStopOutput = ServiceStopResponse
export type ServerGetOutput = { urls: Array<string> } export type ServerGetOutput = { urls: Array<string> }
export type LocationGetInput = { export type LocationGetInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type LocationGetOutput = { export type LocationGetOutput = {
@@ -2526,9 +2524,7 @@ export type LocationGetOutput = {
} }
export type AgentListInput = { export type AgentListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type AgentListOutput = { export type AgentListOutput = {
@@ -2538,9 +2534,7 @@ export type AgentListOutput = {
export type AgentGetInput = { export type AgentGetInput = {
readonly agentID: { readonly agentID: string }["agentID"] readonly agentID: { readonly agentID: string }["agentID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type AgentGetOutput = { export type AgentGetOutput = {
@@ -2549,9 +2543,7 @@ export type AgentGetOutput = {
} }
export type PluginListInput = { export type PluginListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type PluginListOutput = { export type PluginListOutput = {
@@ -2560,19 +2552,7 @@ export type PluginListOutput = {
} }
export type SessionListInput = { export type SessionListInput = {
readonly workspace?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined
readonly parentID?: string | null | undefined
readonly directory?: string | undefined
readonly project?: string | undefined
readonly subpath?: string | undefined
readonly cursor?: string | undefined
}["workspace"]
readonly limit?: { readonly limit?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2583,7 +2563,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["limit"] }["limit"]
readonly order?: { readonly order?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2594,7 +2573,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["order"] }["order"]
readonly search?: { readonly search?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2605,7 +2583,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["search"] }["search"]
readonly parentID?: { readonly parentID?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2616,7 +2593,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["parentID"] }["parentID"]
readonly directory?: { readonly directory?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2627,7 +2603,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["directory"] }["directory"]
readonly project?: { readonly project?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2638,7 +2613,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["project"] }["project"]
readonly subpath?: { readonly subpath?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2649,7 +2623,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["subpath"] }["subpath"]
readonly cursor?: { readonly cursor?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -3244,9 +3217,7 @@ export type MessageListInput = {
export type MessageListOutput = SessionMessagesResponse export type MessageListOutput = SessionMessagesResponse
export type ModelListInput = { export type ModelListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ModelListOutput = { export type ModelListOutput = {
@@ -3255,9 +3226,7 @@ export type ModelListOutput = {
} }
export type ModelDefaultInput = { export type ModelDefaultInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ModelDefaultOutput = { export type ModelDefaultOutput = {
@@ -3266,9 +3235,7 @@ export type ModelDefaultOutput = {
} }
export type GenerateTextInput = { export type GenerateTextInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly prompt: { readonly prompt: {
readonly prompt: string readonly prompt: string
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
@@ -3282,9 +3249,7 @@ export type GenerateTextInput = {
export type GenerateTextOutput = GenerateTextResponse["data"] export type GenerateTextOutput = GenerateTextResponse["data"]
export type ProviderListInput = { export type ProviderListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ProviderListOutput = { export type ProviderListOutput = {
@@ -3294,9 +3259,7 @@ export type ProviderListOutput = {
export type ProviderGetInput = { export type ProviderGetInput = {
readonly providerID: { readonly providerID: string }["providerID"] readonly providerID: { readonly providerID: string }["providerID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ProviderGetOutput = { export type ProviderGetOutput = {
@@ -3305,9 +3268,7 @@ export type ProviderGetOutput = {
} }
export type IntegrationListInput = { export type IntegrationListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type IntegrationListOutput = { export type IntegrationListOutput = {
@@ -3317,9 +3278,7 @@ export type IntegrationListOutput = {
export type IntegrationGetInput = { export type IntegrationGetInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"] readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type IntegrationGetOutput = { export type IntegrationGetOutput = {
@@ -3328,9 +3287,7 @@ export type IntegrationGetOutput = {
} }
export type IntegrationWellknownAddInput = { export type IntegrationWellknownAddInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly url: { readonly url: string }["url"] readonly url: { readonly url: string }["url"]
} }
@@ -3338,9 +3295,7 @@ export type IntegrationWellknownAddOutput = void
export type IntegrationConnectKeyInput = { export type IntegrationConnectKeyInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"] readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly key: { readonly key: string; readonly label?: string | undefined }["key"] readonly key: { readonly key: string; readonly label?: string | undefined }["key"]
readonly label?: { readonly key: string; readonly label?: string | undefined }["label"] readonly label?: { readonly key: string; readonly label?: string | undefined }["label"]
} }
@@ -3349,9 +3304,7 @@ export type IntegrationConnectKeyOutput = void
export type IntegrationOauthConnectInput = { export type IntegrationOauthConnectInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"] readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly methodID: { readonly methodID: {
readonly methodID: string readonly methodID: string
readonly inputs: { readonly [x: string]: string } readonly inputs: { readonly [x: string]: string }
@@ -3383,9 +3336,7 @@ export type IntegrationOauthConnectOutput = {
export type IntegrationOauthStatusInput = { export type IntegrationOauthStatusInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type IntegrationOauthStatusOutput = { export type IntegrationOauthStatusOutput = {
@@ -3396,9 +3347,7 @@ export type IntegrationOauthStatusOutput = {
export type IntegrationOauthCompleteInput = { export type IntegrationOauthCompleteInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly code?: { readonly code?: string | undefined }["code"] readonly code?: { readonly code?: string | undefined }["code"]
} }
@@ -3407,18 +3356,14 @@ export type IntegrationOauthCompleteOutput = void
export type IntegrationOauthCancelInput = { export type IntegrationOauthCancelInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type IntegrationOauthCancelOutput = void export type IntegrationOauthCancelOutput = void
export type IntegrationCommandConnectInput = { export type IntegrationCommandConnectInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"] readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly methodID: { readonly methodID: string; readonly label?: string | undefined }["methodID"] readonly methodID: { readonly methodID: string; readonly label?: string | undefined }["methodID"]
readonly label?: { readonly methodID: string; readonly label?: string | undefined }["label"] readonly label?: { readonly methodID: string; readonly label?: string | undefined }["label"]
} }
@@ -3431,9 +3376,7 @@ export type IntegrationCommandConnectOutput = {
export type IntegrationCommandStatusInput = { export type IntegrationCommandStatusInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type IntegrationCommandStatusOutput = { export type IntegrationCommandStatusOutput = {
@@ -3444,17 +3387,13 @@ export type IntegrationCommandStatusOutput = {
export type IntegrationCommandCancelInput = { export type IntegrationCommandCancelInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type IntegrationCommandCancelOutput = void export type IntegrationCommandCancelOutput = void
export type McpListInput = { export type McpListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type McpListOutput = { export type McpListOutput = {
@@ -3464,9 +3403,7 @@ export type McpListOutput = {
export type McpAddInput = { export type McpAddInput = {
readonly server: { readonly server: string }["server"] readonly server: { readonly server: string }["server"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly config: { readonly config: {
readonly config: readonly config:
| { | {
@@ -3515,35 +3452,27 @@ export type McpAddOutput = void
export type McpRemoveInput = { export type McpRemoveInput = {
readonly server: { readonly server: string }["server"] readonly server: { readonly server: string }["server"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type McpRemoveOutput = void export type McpRemoveOutput = void
export type McpConnectInput = { export type McpConnectInput = {
readonly server: { readonly server: string }["server"] readonly server: { readonly server: string }["server"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type McpConnectOutput = void export type McpConnectOutput = void
export type McpDisconnectInput = { export type McpDisconnectInput = {
readonly server: { readonly server: string }["server"] readonly server: { readonly server: string }["server"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type McpDisconnectOutput = void export type McpDisconnectOutput = void
export type McpResourceCatalogInput = { export type McpResourceCatalogInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type McpResourceCatalogOutput = { export type McpResourceCatalogOutput = {
@@ -3553,9 +3482,7 @@ export type McpResourceCatalogOutput = {
export type CredentialUpdateInput = { export type CredentialUpdateInput = {
readonly credentialID: { readonly credentialID: string }["credentialID"] readonly credentialID: { readonly credentialID: string }["credentialID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly label: { readonly label: string }["label"] readonly label: { readonly label: string }["label"]
} }
@@ -3563,9 +3490,7 @@ export type CredentialUpdateOutput = void
export type CredentialRemoveInput = { export type CredentialRemoveInput = {
readonly credentialID: { readonly credentialID: string }["credentialID"] readonly credentialID: { readonly credentialID: string }["credentialID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type CredentialRemoveOutput = void export type CredentialRemoveOutput = void
@@ -3573,26 +3498,20 @@ export type CredentialRemoveOutput = void
export type ProjectListOutput = Array<Project> export type ProjectListOutput = Array<Project>
export type ProjectCurrentInput = { export type ProjectCurrentInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ProjectCurrentOutput = ProjectCurrent export type ProjectCurrentOutput = ProjectCurrent
export type ProjectDirectoriesInput = { export type ProjectDirectoriesInput = {
readonly projectID: { readonly projectID: string }["projectID"] readonly projectID: { readonly projectID: string }["projectID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ProjectDirectoriesOutput = ProjectDirectories export type ProjectDirectoriesOutput = ProjectDirectories
export type FormRequestListInput = { export type FormRequestListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type FormRequestListOutput = { export type FormRequestListOutput = {
@@ -4446,9 +4365,7 @@ export type FormCancelInput = {
export type FormCancelOutput = void export type FormCancelOutput = void
export type PermissionRequestListInput = { export type PermissionRequestListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type PermissionRequestListOutput = { export type PermissionRequestListOutput = {
@@ -4554,9 +4471,7 @@ export type PermissionReplyInput = {
export type PermissionReplyOutput = void export type PermissionReplyOutput = void
export type FileReadInput = { export type FileReadInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly path: string readonly path: string
} }
@@ -4564,11 +4479,11 @@ export type FileReadOutput = globalThis.Uint8Array
export type FileListInput = { export type FileListInput = {
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly path?: string | undefined readonly path?: string | undefined
}["location"] }["location"]
readonly path?: { readonly path?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly path?: string | undefined readonly path?: string | undefined
}["path"] }["path"]
} }
@@ -4580,25 +4495,25 @@ export type FileListOutput = {
export type FileFindInput = { export type FileFindInput = {
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly query: string readonly query: string
readonly type?: "file" | "directory" | undefined readonly type?: "file" | "directory" | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
}["location"] }["location"]
readonly query: { readonly query: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly query: string readonly query: string
readonly type?: "file" | "directory" | undefined readonly type?: "file" | "directory" | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
}["query"] }["query"]
readonly type?: { readonly type?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly query: string readonly query: string
readonly type?: "file" | "directory" | undefined readonly type?: "file" | "directory" | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
}["type"] }["type"]
readonly limit?: { readonly limit?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly query: string readonly query: string
readonly type?: "file" | "directory" | undefined readonly type?: "file" | "directory" | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
@@ -4611,9 +4526,7 @@ export type FileFindOutput = {
} }
export type CommandListInput = { export type CommandListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type CommandListOutput = { export type CommandListOutput = {
@@ -4622,9 +4535,7 @@ export type CommandListOutput = {
} }
export type SkillListInput = { export type SkillListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type SkillListOutput = { export type SkillListOutput = {
@@ -4635,9 +4546,7 @@ export type SkillListOutput = {
export type EventSubscribeOutput = V2Event export type EventSubscribeOutput = V2Event
export type PtyListInput = { export type PtyListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type PtyListOutput = { export type PtyListOutput = {
@@ -4646,9 +4555,7 @@ export type PtyListOutput = {
} }
export type PtyCreateInput = { export type PtyCreateInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly command?: { readonly command?: {
readonly command?: string readonly command?: string
readonly args?: ReadonlyArray<string> readonly args?: ReadonlyArray<string>
@@ -4693,9 +4600,7 @@ export type PtyCreateOutput = {
export type PtyGetInput = { export type PtyGetInput = {
readonly ptyID: { readonly ptyID: string }["ptyID"] readonly ptyID: { readonly ptyID: string }["ptyID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type PtyGetOutput = { export type PtyGetOutput = {
@@ -4705,9 +4610,7 @@ export type PtyGetOutput = {
export type PtyUpdateInput = { export type PtyUpdateInput = {
readonly ptyID: { readonly ptyID: string }["ptyID"] readonly ptyID: { readonly ptyID: string }["ptyID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly title?: { readonly title?: {
readonly title?: string readonly title?: string
readonly size?: { readonly rows: number; readonly cols: number } readonly size?: { readonly rows: number; readonly cols: number }
@@ -4722,17 +4625,13 @@ export type PtyUpdateOutput = {
export type PtyRemoveInput = { export type PtyRemoveInput = {
readonly ptyID: { readonly ptyID: string }["ptyID"] readonly ptyID: { readonly ptyID: string }["ptyID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type PtyRemoveOutput = void export type PtyRemoveOutput = void
export type ShellListInput = { export type ShellListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ShellListOutput = { export type ShellListOutput = {
@@ -4741,9 +4640,7 @@ export type ShellListOutput = {
} }
export type ShellCreateInput = { export type ShellCreateInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly command: { readonly command: {
readonly command: string readonly command: string
readonly cwd?: string readonly cwd?: string
@@ -4777,9 +4674,7 @@ export type ShellCreateOutput = {
export type ShellGetInput = { export type ShellGetInput = {
readonly id: { readonly id: string }["id"] readonly id: { readonly id: string }["id"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ShellGetOutput = { export type ShellGetOutput = {
@@ -4789,9 +4684,7 @@ export type ShellGetOutput = {
export type ShellTimeoutInput = { export type ShellTimeoutInput = {
readonly id: { readonly id: string }["id"] readonly id: { readonly id: string }["id"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly timeout: { readonly timeout: number }["timeout"] readonly timeout: { readonly timeout: number }["timeout"]
} }
@@ -4803,17 +4696,17 @@ export type ShellTimeoutOutput = {
export type ShellOutputInput = { export type ShellOutputInput = {
readonly id: { readonly id: string }["id"] readonly id: { readonly id: string }["id"]
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly cursor?: number | undefined readonly cursor?: number | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
}["location"] }["location"]
readonly cursor?: { readonly cursor?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly cursor?: number | undefined readonly cursor?: number | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
}["cursor"] }["cursor"]
readonly limit?: { readonly limit?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly cursor?: number | undefined readonly cursor?: number | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
}["limit"] }["limit"]
@@ -4826,17 +4719,13 @@ export type ShellOutputOutput = {
export type ShellRemoveInput = { export type ShellRemoveInput = {
readonly id: { readonly id: string }["id"] readonly id: { readonly id: string }["id"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ShellRemoveOutput = void export type ShellRemoveOutput = void
export type QuestionRequestListInput = { export type QuestionRequestListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type QuestionRequestListOutput = { export type QuestionRequestListOutput = {
@@ -4864,9 +4753,7 @@ export type QuestionRejectInput = {
export type QuestionRejectOutput = void export type QuestionRejectOutput = void
export type ReferenceListInput = { export type ReferenceListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ReferenceListOutput = { export type ReferenceListOutput = {
@@ -4876,9 +4763,7 @@ export type ReferenceListOutput = {
export type ProjectCopyCreateInput = { export type ProjectCopyCreateInput = {
readonly projectID: { readonly projectID: string }["projectID"] readonly projectID: { readonly projectID: string }["projectID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly strategy: { readonly strategy: string; readonly directory: string; readonly name?: string }["strategy"] readonly strategy: { readonly strategy: string; readonly directory: string; readonly name?: string }["strategy"]
readonly directory: { readonly strategy: string; readonly directory: string; readonly name?: string }["directory"] readonly directory: { readonly strategy: string; readonly directory: string; readonly name?: string }["directory"]
readonly name?: { readonly strategy: string; readonly directory: string; readonly name?: string }["name"] readonly name?: { readonly strategy: string; readonly directory: string; readonly name?: string }["name"]
@@ -4888,9 +4773,7 @@ export type ProjectCopyCreateOutput = ProjectCopyCopy
export type ProjectCopyRemoveInput = { export type ProjectCopyRemoveInput = {
readonly projectID: { readonly projectID: string }["projectID"] readonly projectID: { readonly projectID: string }["projectID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly directory: { readonly directory: string; readonly force: boolean }["directory"] readonly directory: { readonly directory: string; readonly force: boolean }["directory"]
readonly force: { readonly directory: string; readonly force: boolean }["force"] readonly force: { readonly directory: string; readonly force: boolean }["force"]
} }
@@ -4899,17 +4782,13 @@ export type ProjectCopyRemoveOutput = void
export type ProjectCopyRefreshInput = { export type ProjectCopyRefreshInput = {
readonly projectID: { readonly projectID: string }["projectID"] readonly projectID: { readonly projectID: string }["projectID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ProjectCopyRefreshOutput = void export type ProjectCopyRefreshOutput = void
export type VcsGetInput = { export type VcsGetInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type VcsGetOutput = { export type VcsGetOutput = {
@@ -4918,9 +4797,7 @@ export type VcsGetOutput = {
} }
export type VcsStatusInput = { export type VcsStatusInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type VcsStatusOutput = { export type VcsStatusOutput = {
@@ -4930,17 +4807,17 @@ export type VcsStatusOutput = {
export type VcsDiffInput = { export type VcsDiffInput = {
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly mode: "working" | "branch" readonly mode: "working" | "branch"
readonly context?: number | undefined readonly context?: number | undefined
}["location"] }["location"]
readonly mode: { readonly mode: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly mode: "working" | "branch" readonly mode: "working" | "branch"
readonly context?: number | undefined readonly context?: number | undefined
}["mode"] }["mode"]
readonly context?: { readonly context?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly mode: "working" | "branch" readonly mode: "working" | "branch"
readonly context?: number | undefined readonly context?: number | undefined
}["context"] }["context"]
@@ -4954,17 +4831,13 @@ export type VcsDiffOutput = {
export type DebugLocationListOutput = Array<LocationRef> export type DebugLocationListOutput = Array<LocationRef>
export type DebugLocationEvictInput = { export type DebugLocationEvictInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type DebugLocationEvictOutput = void export type DebugLocationEvictOutput = void
export type WebsearchProvidersInput = { export type WebsearchProvidersInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type WebsearchProvidersOutput = { export type WebsearchProvidersOutput = {
@@ -4973,9 +4846,7 @@ export type WebsearchProvidersOutput = {
} }
export type WebsearchQueryInput = { export type WebsearchQueryInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly query: { readonly query: string; readonly providerID?: string }["query"] readonly query: { readonly query: string; readonly providerID?: string }["query"]
readonly providerID?: { readonly query: string; readonly providerID?: string }["providerID"] readonly providerID?: { readonly query: string; readonly providerID?: string }["providerID"]
} }
@@ -58,13 +58,3 @@ export const setMethods = new Set([
"isSupersetOf", "isSupersetOf",
"isDisjointFrom", "isDisjointFrom",
]) ])
export const spreadItems = (value: unknown): Array<unknown> | undefined => {
if (Array.isArray(value)) return value
if (typeof value === "string") return Array.from(value)
if (value instanceof CodeModeMap) return Array.from(value.map.entries(), ([key, item]) => [key, item])
if (value instanceof CodeModeSet) return Array.from(value.set.values())
if (value instanceof CodeModeURLSearchParams) return Array.from(value.params.entries(), ([key, item]) => [key, item])
return undefined
}
import { CodeModeMap, CodeModeSet, CodeModeURLSearchParams } from "../values.js"
+1 -2
View File
@@ -1021,13 +1021,12 @@ describe("OpenAPI.fromSpec", () => {
await Effect.runPromise( await Effect.runPromise(
location location
.execute({ location: { directory: "/tmp", workspace: "workspace-1" } }) .execute({ location: { directory: "/tmp" } })
.pipe(Effect.provide(client.layer)), .pipe(Effect.provide(client.layer)),
) )
const url = new URL(client.requests[0]!.url) const url = new URL(client.requests[0]!.url)
expect(url.searchParams.get("location[directory]")).toBe("/tmp") expect(url.searchParams.get("location[directory]")).toBe("/tmp")
expect(url.searchParams.get("location[workspace]")).toBe("workspace-1")
}) })
test("serializes supported simple and form parameter shapes", async () => { test("serializes supported simple and form parameter shapes", async () => {
@@ -1,103 +0,0 @@
[data-component="desktop-promo"] {
--promo-background: hsl(0, 20%, 99%);
--promo-background-weak: hsl(0, 8%, 97%);
--promo-text: hsl(0, 1%, 39%);
--promo-text-strong: hsl(0, 5%, 12%);
--promo-border: hsla(0, 100%, 3%, 0.12);
position: fixed;
z-index: 20;
right: 1.5rem;
bottom: 1.5rem;
width: min(28rem, calc(100vw - 2rem));
padding: 4px;
overflow: hidden;
color: var(--promo-text);
border: 1px solid var(--promo-border);
border-radius: 8px;
background: var(--promo-background);
box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 15%);
font-family: var(--font-mono);
@media (prefers-color-scheme: dark) {
--promo-background: hsl(0, 9%, 7%);
--promo-background-weak: hsl(0, 6%, 10%);
--promo-text: hsl(0, 4%, 71%);
--promo-text-strong: hsl(0, 15%, 94%);
--promo-border: hsl(0, 4%, 23%);
}
@media (max-width: 40rem) {
right: 1rem;
bottom: 1rem;
}
[data-slot="desktop-promo-link"] {
display: block;
color: var(--promo-text);
text-decoration: none;
}
[data-slot="desktop-promo-link"]:focus-visible {
outline: 2px solid var(--promo-text-strong);
outline-offset: -3px;
}
video {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 4px;
background: var(--promo-background-weak);
}
[data-slot="desktop-promo-copy"] {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding: 1rem;
font-size: 0.875rem;
line-height: 1.4;
}
[data-slot="desktop-promo-copy"] strong {
color: var(--promo-text-strong);
font-weight: 500;
}
[data-slot="desktop-promo-close"] {
position: absolute;
top: 0.5rem;
right: 0.5rem;
display: grid;
width: 2rem;
height: 2rem;
padding: 0;
place-items: center;
cursor: pointer;
color: white;
border: none;
border-radius: 0.25rem;
background: rgb(0 0 0 / 70%);
opacity: 0;
transition:
opacity 150ms ease,
background 150ms ease;
}
&:hover [data-slot="desktop-promo-close"],
[data-slot="desktop-promo-close"]:focus-visible {
opacity: 1;
}
[data-slot="desktop-promo-close"]:hover {
background: rgb(0 0 0 / 90%);
}
@media (hover: none) {
[data-slot="desktop-promo-close"] {
opacity: 1;
}
}
}
@@ -1,60 +0,0 @@
import "./desktop-promo.css"
import { A, useLocation } from "@solidjs/router"
import { createSignal, Show } from "solid-js"
import { getRequestEvent } from "solid-js/web"
import desktopPromoVideo from "~/asset/lander/desktop-tabs-landscape.mp4"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
import { strip } from "~/lib/language"
const DISMISSED_COOKIE = "desktop_promo_dismissed"
export function DesktopPromo() {
const i18n = useI18n()
const language = useLanguage()
const location = useLocation()
const request = getRequestEvent()?.request
const cookie = request?.headers.get("cookie") ?? (typeof document === "object" ? document.cookie : "")
const [visible, setVisible] = createSignal(
!cookie.split(";").some((value) => value.trim() === `${DISMISSED_COOKIE}=1`),
)
const hostname = request ? new URL(request.url).hostname : typeof window === "object" ? window.location.hostname : ""
const primaryHost =
hostname === "opencode.ai" || hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"
return (
<Show
when={
visible() &&
primaryHost &&
strip(location.pathname) !== "/download" &&
!strip(location.pathname).startsWith("/download/")
}
>
<aside data-component="desktop-promo">
<A href={language.route("/download")} data-slot="desktop-promo-link">
<video src={desktopPromoVideo} autoplay playsinline loop muted preload="metadata" aria-hidden="true" />
<span data-slot="desktop-promo-copy">
<strong>{i18n.t("home.promo.title")}</strong>
<span>
{i18n.t("home.promo.body")} {i18n.t("home.promo.cta")}
</span>
</span>
</A>
<button
type="button"
data-slot="desktop-promo-close"
onClick={() => {
document.cookie = `${DISMISSED_COOKIE}=1; Path=/; Max-Age=31536000; SameSite=Lax`
setVisible(false)
}}
>
<span class="sr-only">{i18n.t("home.promo.close")}</span>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path d="M5 5L15 15M15 5L5 15" stroke="currentColor" stroke-width="1.5" />
</svg>
</button>
</aside>
</Show>
)
}
@@ -19,10 +19,6 @@ export function Span({ children, ...props }: SpanProps) {
return React.createElement("span", props, children) return React.createElement("span", props, children)
} }
export function Wbr({ children, ...props }: WbrProps) {
return React.createElement("wbr", props, children)
}
export function Fonts({ assetsUrl }: { assetsUrl: string }) { export function Fonts({ assetsUrl }: { assetsUrl: string }) {
return ( return (
<> <>
@@ -59,14 +55,3 @@ export function Fonts({ assetsUrl }: { assetsUrl: string }) {
</> </>
) )
} }
export function SplitString({ text, split }: { text: string; split: number }) {
const segments: JSX.Element[] = []
for (let i = 0; i < text.length; i += split) {
segments.push(<>{text.slice(i, i + split)}</>)
if (i + split < text.length) {
segments.push(<Wbr key={`${i}wbr`} />)
}
}
return <>{segments}</>
}
+2 -110
View File
@@ -1,15 +1,11 @@
{ {
"version": "7", "version": "7",
"dialect": "sqlite", "dialect": "sqlite",
"id": "e43ed7e2-b9fc-4178-beae-3646e4a976e1", "id": "6307bc18-2612-47e2-acfc-2a6a68ff28c5",
"prevIds": [ "prevIds": [
"db37a97f-9b5e-4c87-be8b-4feace35136c" "e43ed7e2-b9fc-4178-beae-3646e4a976e1"
], ],
"ddl": [ "ddl": [
{
"name": "workspace",
"entityType": "tables"
},
{ {
"name": "data_migration", "name": "data_migration",
"entityType": "tables" "entityType": "tables"
@@ -90,86 +86,6 @@
"name": "session_share", "name": "session_share",
"entityType": "tables" "entityType": "tables"
}, },
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "type",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": "''",
"generated": null,
"name": "name",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "branch",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "directory",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "extra",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "workspace"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_used",
"entityType": "columns",
"table": "workspace"
},
{ {
"type": "text", "type": "text",
"notNull": false, "notNull": false,
@@ -1590,21 +1506,6 @@
"entityType": "columns", "entityType": "columns",
"table": "session_share" "table": "session_share"
}, },
{
"columns": [
"project_id"
],
"tableTo": "project",
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_workspace_project_id_project_id_fk",
"entityType": "fks",
"table": "workspace"
},
{ {
"columns": [ "columns": [
"active_account_id" "active_account_id"
@@ -1815,15 +1716,6 @@
"entityType": "pks", "entityType": "pks",
"table": "instruction_entry" "table": "instruction_entry"
}, },
{
"columns": [
"id"
],
"nameExplicit": false,
"name": "workspace_pk",
"table": "workspace",
"entityType": "pks"
},
{ {
"columns": [ "columns": [
"name" "name"
-4
View File
@@ -44,7 +44,6 @@ export type Draft = {
export interface Interface extends State.Transformable<Draft> { export interface Interface extends State.Transformable<Draft> {
readonly get: (id: ID) => Effect.Effect<Info | undefined> readonly get: (id: ID) => Effect.Effect<Info | undefined>
readonly default: () => Effect.Effect<Info | undefined>
readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined> readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined>
readonly select: (id?: ID | string) => Effect.Effect<Selection> readonly select: (id?: ID | string) => Effect.Effect<Selection>
readonly list: () => Effect.Effect<Info[]> readonly list: () => Effect.Effect<Info[]>
@@ -110,9 +109,6 @@ const layer = Layer.effect(
get: Effect.fn("Agent.get")(function* (id) { get: Effect.fn("Agent.get")(function* (id) {
return state.get().agents.get(id) return state.get().agents.get(id)
}), }),
default: Effect.fn("Agent.default")(function* () {
return selectedDefault()
}),
resolve: Effect.fn("Agent.resolve")(function* (id) { resolve: Effect.fn("Agent.resolve")(function* (id) {
if (id !== undefined) return state.get().agents.get(ID.make(id)) if (id !== undefined) return state.get().agents.get(ID.make(id))
return selectedDefault() return selectedDefault()
+1 -17
View File
@@ -3,7 +3,7 @@ export * as Bus from "./bus"
import { Cause, Context, DateTime, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" import { Cause, Context, DateTime, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
import { Event } from "@opencode-ai/schema/event" import { Event } from "@opencode-ai/schema/event"
import type { EventLog } from "@opencode-ai/schema/event-log" import type { EventLog } from "@opencode-ai/schema/event-log"
import { and, asc, eq, gt, inArray, lte, sql } from "drizzle-orm" import { and, asc, eq, gt, lte, sql } from "drizzle-orm"
import { Database } from "./database/database" import { Database } from "./database/database"
import { EventSequenceTable, EventTable } from "./event/sql" import { EventSequenceTable, EventTable } from "./event/sql"
import { Location } from "./location" import { Location } from "./location"
@@ -134,8 +134,6 @@ export interface Interface {
readonly after?: number readonly after?: number
readonly follow?: boolean readonly follow?: boolean
}) => Stream.Stream<LogItem> }) => Stream.Stream<LogItem>
/** Latest committed seq per aggregate. Aggregates without events are absent. */
readonly sequences: (aggregateIDs: ReadonlyArray<string>) => Effect.Effect<ReadonlyMap<string, Event.Seq>>
/** @deprecated Use `subscribe()` and consume the returned stream. */ /** @deprecated Use `subscribe()` and consume the returned stream. */
readonly listen: (listener: Subscriber) => Effect.Effect<Unsubscribe> readonly listen: (listener: Subscriber) => Effect.Effect<Unsubscribe>
readonly project: <D extends Event.Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void> readonly project: <D extends Event.Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void>
@@ -657,19 +655,6 @@ export const layerWith = (options?: LayerOptions) =>
}), }),
) )
const sequences = (aggregateIDs: ReadonlyArray<string>): Effect.Effect<ReadonlyMap<string, Event.Seq>> => {
if (aggregateIDs.length === 0) return Effect.succeed(new Map())
return db
.select({ aggregateID: EventSequenceTable.aggregate_id, seq: EventSequenceTable.seq })
.from(EventSequenceTable)
.where(inArray(EventSequenceTable.aggregate_id, Array.from(aggregateIDs)))
.all()
.pipe(
Effect.orDie,
Effect.map((rows) => new Map(rows.map((row) => [row.aggregateID, Event.Seq.make(row.seq)]))),
)
}
const listen = (listener: Subscriber): Effect.Effect<Unsubscribe> => const listen = (listener: Subscriber): Effect.Effect<Unsubscribe> =>
Effect.sync(() => { Effect.sync(() => {
listeners.push(listener) listeners.push(listener)
@@ -691,7 +676,6 @@ export const layerWith = (options?: LayerOptions) =>
publish, publish,
subscribe, subscribe,
log, log,
sequences,
listen, listen,
project, project,
replay, replay,
@@ -1,20 +0,0 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
import { ProjectTable } from "../project/sql"
import { Project } from "../project"
import { Workspace } from "../workspace"
export const WorkspaceTable = sqliteTable("workspace", {
id: text().$type<Workspace.ID>().primaryKey(),
type: text().notNull(),
name: text().notNull().default(""),
branch: text(),
directory: text(),
extra: text({ mode: "json" }),
project_id: text()
.$type<Project.ID>()
.notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }),
time_used: integer()
.notNull()
.$default(() => Date.now()),
})
+1
View File
@@ -59,5 +59,6 @@ export const migrations = (
import("./migration/20260722170000_canonical_tool_results"), import("./migration/20260722170000_canonical_tool_results"),
import("./migration/20260729022634_session_fork_boundary"), import("./migration/20260729022634_session_fork_boundary"),
import("./migration/20260730195856_optional_session_title"), import("./migration/20260730195856_optional_session_title"),
import("./migration/20260805225117_remove_workspace"),
]) ])
).map((module) => module.default) satisfies DatabaseMigration.Migration[] ).map((module) => module.default) satisfies DatabaseMigration.Migration[]
@@ -0,0 +1,11 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration"
export default {
id: "20260805225117_remove_workspace",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`DROP TABLE \`workspace\`;`)
})
},
} satisfies DatabaseMigration.Migration
-13
View File
@@ -4,19 +4,6 @@ import type { DatabaseMigration } from "./migration"
export default { export default {
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* tx.run(`
CREATE TABLE \`workspace\` (
\`id\` text PRIMARY KEY,
\`type\` text NOT NULL,
\`name\` text DEFAULT '' NOT NULL,
\`branch\` text,
\`directory\` text,
\`extra\` text,
\`project_id\` text NOT NULL,
\`time_used\` integer NOT NULL,
CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(` yield* tx.run(`
CREATE TABLE \`data_migration\` ( CREATE TABLE \`data_migration\` (
\`name\` text PRIMARY KEY, \`name\` text PRIMARY KEY,
+2 -92
View File
@@ -1,8 +1,7 @@
export * as FileMutation from "./file-mutation" export * as FileMutation from "./file-mutation"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer } from "effect"
import { dirname } from "path"
import { KeyedMutex } from "./effect/keyed-mutex" import { KeyedMutex } from "./effect/keyed-mutex"
import { FSUtil } from "@opencode-ai/util/fs-util" import { FSUtil } from "@opencode-ai/util/fs-util"
import { Bom } from "@opencode-ai/util/bom" import { Bom } from "@opencode-ai/util/bom"
@@ -22,22 +21,6 @@ export interface TextWriteInput {
readonly content: string readonly content: string
} }
export interface ConditionalWriteInput extends WriteInput {
readonly expected: Uint8Array
}
export interface RemoveInput {
readonly target: Target
}
export class StaleContentError extends Schema.TaggedErrorClass<StaleContentError>()("FileMutation.StaleContentError", {
path: Schema.String,
}) {}
export class TargetExistsError extends Schema.TaggedErrorClass<TargetExistsError>()("FileMutation.TargetExistsError", {
path: Schema.String,
}) {}
export interface WriteResult { export interface WriteResult {
readonly operation: "write" readonly operation: "write"
readonly target: string readonly target: string
@@ -45,24 +28,10 @@ export interface WriteResult {
readonly existed: boolean readonly existed: boolean
} }
export interface RemoveResult {
readonly operation: "remove"
readonly target: string
readonly resource: string
readonly existed: boolean
}
export interface Interface { export interface Interface {
/** Create without replacing an existing target. */
readonly create: (input: WriteInput) => Effect.Effect<WriteResult, TargetExistsError | FSUtil.Error>
readonly write: (input: WriteInput) => Effect.Effect<WriteResult, FSUtil.Error> readonly write: (input: WriteInput) => Effect.Effect<WriteResult, FSUtil.Error>
/** Write text while retaining an existing UTF-8 BOM and emitting at most one BOM. */ /** Write text while retaining an existing UTF-8 BOM and emitting at most one BOM. */
readonly writeTextPreservingBom: (input: TextWriteInput) => Effect.Effect<WriteResult, FSUtil.Error> readonly writeTextPreservingBom: (input: TextWriteInput) => Effect.Effect<WriteResult, FSUtil.Error>
/** Commit only if an existing target still has the expected bytes. */
readonly writeIfUnchanged: (
input: ConditionalWriteInput,
) => Effect.Effect<WriteResult, StaleContentError | FSUtil.Error>
readonly remove: (input: RemoveInput) => Effect.Effect<RemoveResult, FSUtil.Error>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/FileMutation") {} export class Service extends Context.Service<Service, Interface>()("@opencode/FileMutation") {}
@@ -89,13 +58,6 @@ const layer = Layer.effect(
existed, existed,
}) })
const removeResult = (target: Target, existed: boolean): RemoveResult => ({
operation: "remove",
target: target.canonical,
resource: target.resource,
existed,
})
const write = Effect.fn("FileMutation.write")((input: WriteInput) => const write = Effect.fn("FileMutation.write")((input: WriteInput) =>
withTargetLock(input.target)( withTargetLock(input.target)(
Effect.gen(function* () { Effect.gen(function* () {
@@ -122,62 +84,10 @@ const layer = Layer.effect(
), ),
) )
const create = Effect.fn("FileMutation.create")((input: WriteInput) => return Service.of({ write, writeTextPreservingBom })
withTargetLock(input.target)(
Effect.gen(function* () {
const write =
typeof input.content === "string"
? fs.writeFileString(input.target.canonical, input.content, { flag: "wx" })
: fs.writeFile(input.target.canonical, input.content, { flag: "wx" })
yield* write.pipe(
Effect.catchReason("PlatformError", "NotFound", () =>
fs.ensureDir(dirname(input.target.canonical)).pipe(Effect.andThen(write)),
),
Effect.catchReason("PlatformError", "AlreadyExists", () =>
Effect.fail(new TargetExistsError({ path: input.target.canonical })),
),
)
return writeResult(input.target, false)
}),
),
)
const writeIfUnchanged = Effect.fn("FileMutation.writeIfUnchanged")((input: ConditionalWriteInput) =>
withTargetLock(input.target)(
Effect.gen(function* () {
const current = yield* fs.readFile(input.target.canonical)
if (!sameBytes(current, input.expected)) {
return yield* new StaleContentError({ path: input.target.canonical })
}
yield* typeof input.content === "string"
? fs.writeFileString(input.target.canonical, input.content)
: fs.writeFile(input.target.canonical, input.content)
return writeResult(input.target, true)
}),
),
)
const remove = Effect.fn("FileMutation.remove")((input: RemoveInput) =>
withTargetLock(input.target)(
Effect.gen(function* () {
const existed = yield* fs.remove(input.target.canonical).pipe(
Effect.as(true),
Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(false)),
)
return removeResult(input.target, existed)
}),
),
)
return Service.of({ create, write, writeTextPreservingBom, writeIfUnchanged, remove })
}), }),
) )
function sameBytes(left: Uint8Array, right: Uint8Array) {
if (left.length !== right.length) return false
return left.every((byte, index) => byte === right[index])
}
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] }) export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] })
/** /**
+1 -5
View File
@@ -7,7 +7,7 @@ import { FSUtil } from "@opencode-ai/util/fs-util"
import { Location } from "./location" import { Location } from "./location"
import { PositiveInt, RelativePath } from "./schema" import { PositiveInt, RelativePath } from "./schema"
import { FileSystemSearch } from "./filesystem/search" import { FileSystemSearch } from "./filesystem/search"
import { Entry, FileSystem, FindInput, Match } from "@opencode-ai/schema/filesystem" import { Entry, FileSystem, FindInput } from "@opencode-ai/schema/filesystem"
export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem" export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem"
export const ReadInput = Schema.Struct({ export const ReadInput = Schema.Struct({
@@ -53,8 +53,6 @@ export interface Interface {
readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }> readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }>
readonly list: (input?: ListInput) => Effect.Effect<Entry[]> readonly list: (input?: ListInput) => Effect.Effect<Entry[]>
readonly find: (input: FindInput) => Effect.Effect<Entry[]> readonly find: (input: FindInput) => Effect.Effect<Entry[]>
readonly glob: (input: GlobInput) => Effect.Effect<readonly Entry[]>
readonly grep: (input: GrepInput) => Effect.Effect<readonly Match[]>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {} export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
@@ -76,8 +74,6 @@ const baseLayer = Layer.effect(
}) })
return Service.of({ return Service.of({
find: search.find, find: search.find,
glob: search.glob,
grep: search.grep,
read: Effect.fn("FileSystem.read")(function* (input) { read: Effect.fn("FileSystem.read")(function* (input) {
const target = yield* resolve(input.path) const target = yield* resolve(input.path)
const info = yield* fs.stat(target.real).pipe(Effect.orDie) const info = yield* fs.stat(target.real).pipe(Effect.orDie)
-38
View File
@@ -3,9 +3,6 @@ import {
type DirItem, type DirItem,
type DirSearchResult, type DirSearchResult,
type FileItem, type FileItem,
type GrepCursor,
type GrepMatch,
type GrepResult,
type InitOptions, type InitOptions,
type MixedItem, type MixedItem,
type MixedSearchResult, type MixedSearchResult,
@@ -45,19 +42,6 @@ export interface MixedSearch {
export type File = FileItem export type File = FileItem
export type Directory = DirItem export type Directory = DirItem
export type Mixed = MixedItem export type Mixed = MixedItem
export type Cursor = GrepCursor | null
export type Hit = GrepMatch
export interface Grep {
items: GrepResult["items"]
totalMatched: number
totalFilesSearched: number
totalFiles: number
filteredFileCount: number
nextCursor: Cursor
regexFallbackError?: string
}
export interface Picker { export interface Picker {
destroy(): void destroy(): void
isScanning(): boolean isScanning(): boolean
@@ -71,14 +55,6 @@ export interface Picker {
pageSize?: number pageSize?: number
}, },
): Result<Search> ): Result<Search>
glob(
pattern: string,
opts?: {
currentFile?: string
pageIndex?: number
pageSize?: number
},
): Result<Search>
directorySearch( directorySearch(
query: string, query: string,
opts?: { opts?: {
@@ -95,18 +71,6 @@ export interface Picker {
pageSize?: number pageSize?: number
}, },
): Result<MixedSearch> ): Result<MixedSearch>
grep(
query: string,
opts?: {
mode?: "plain" | "regex" | "fuzzy"
maxMatchesPerFile?: number
timeBudgetMs?: number
beforeContext?: number
afterContext?: number
cursor?: Cursor
pageSize?: number
},
): Result<Grep>
trackQuery(query: string, file: string): Result<boolean> trackQuery(query: string, file: string): Result<boolean>
getHistoricalQuery(offset: number): Result<string | null> getHistoricalQuery(offset: number): Result<string | null>
} }
@@ -127,10 +91,8 @@ export function create(opts: Init): Result<Picker> {
waitForScan: (timeoutMs) => pick.waitForScan(timeoutMs), waitForScan: (timeoutMs) => pick.waitForScan(timeoutMs),
refreshGitStatus: () => pick.refreshGitStatus(), refreshGitStatus: () => pick.refreshGitStatus(),
fileSearch: (query, next) => pick.fileSearch(query, next), fileSearch: (query, next) => pick.fileSearch(query, next),
glob: (pattern, next) => pick.glob(pattern, next),
directorySearch: (query, next) => pick.directorySearch(query, next), directorySearch: (query, next) => pick.directorySearch(query, next),
mixedSearch: (query, next) => pick.mixedSearch(query, next), mixedSearch: (query, next) => pick.mixedSearch(query, next),
grep: (query, next) => pick.grep(query, next),
trackQuery: (query, file) => pick.trackQuery(query, file), trackQuery: (query, file) => pick.trackQuery(query, file),
getHistoricalQuery: (offset) => pick.getHistoricalQuery(offset), getHistoricalQuery: (offset) => pick.getHistoricalQuery(offset),
}, },
-38
View File
@@ -2,9 +2,6 @@ import type {
DirItem, DirItem,
DirSearchResult, DirSearchResult,
FileItem, FileItem,
GrepCursor,
GrepMatch,
GrepResult,
InitOptions, InitOptions,
MixedItem, MixedItem,
MixedSearchResult, MixedSearchResult,
@@ -42,19 +39,6 @@ export interface MixedSearch {
export type File = FileItem export type File = FileItem
export type Directory = DirItem export type Directory = DirItem
export type Mixed = MixedItem export type Mixed = MixedItem
export type Cursor = GrepCursor | null
export type Hit = GrepMatch
export interface Grep {
items: GrepResult["items"]
totalMatched: number
totalFilesSearched: number
totalFiles: number
filteredFileCount: number
nextCursor: Cursor
regexFallbackError?: string
}
export interface Picker { export interface Picker {
destroy(): void destroy(): void
isScanning(): boolean isScanning(): boolean
@@ -68,14 +52,6 @@ export interface Picker {
pageSize?: number pageSize?: number
}, },
): Result<Search> ): Result<Search>
glob(
pattern: string,
opts?: {
currentFile?: string
pageIndex?: number
pageSize?: number
},
): Result<Search>
directorySearch( directorySearch(
query: string, query: string,
opts?: { opts?: {
@@ -92,18 +68,6 @@ export interface Picker {
pageSize?: number pageSize?: number
}, },
): Result<MixedSearch> ): Result<MixedSearch>
grep(
query: string,
opts?: {
mode?: "plain" | "regex" | "fuzzy"
maxMatchesPerFile?: number
timeBudgetMs?: number
beforeContext?: number
afterContext?: number
cursor?: Cursor
pageSize?: number
},
): Result<Grep>
trackQuery(query: string, file: string): Result<boolean> trackQuery(query: string, file: string): Result<boolean>
getHistoricalQuery(offset: number): Result<string | null> getHistoricalQuery(offset: number): Result<string | null>
} }
@@ -125,10 +89,8 @@ export function create(opts: Init): Result<Picker> {
waitForScan: (timeoutMs) => pick.waitForScan(timeoutMs), waitForScan: (timeoutMs) => pick.waitForScan(timeoutMs),
refreshGitStatus: () => pick.refreshGitStatus(), refreshGitStatus: () => pick.refreshGitStatus(),
fileSearch: (query, next) => pick.fileSearch(query, next), fileSearch: (query, next) => pick.fileSearch(query, next),
glob: (pattern, next) => pick.glob(pattern, next),
directorySearch: (query, next) => pick.directorySearch(query, next), directorySearch: (query, next) => pick.directorySearch(query, next),
mixedSearch: (query, next) => pick.mixedSearch(query, next), mixedSearch: (query, next) => pick.mixedSearch(query, next),
grep: (query, next) => pick.grep(query, next),
trackQuery: (query, file) => pick.trackQuery(query, file), trackQuery: (query, file) => pick.trackQuery(query, file),
getHistoricalQuery: (offset) => pick.getHistoricalQuery(offset), getHistoricalQuery: (offset) => pick.getHistoricalQuery(offset),
}, },
-19
View File
@@ -1,5 +1,3 @@
import { Glob } from "@opencode-ai/util/glob"
const FOLDERS = new Set([ const FOLDERS = new Set([
"node_modules", "node_modules",
"bower_components", "bower_components",
@@ -47,21 +45,4 @@ const FILES = [
export const PATTERNS = [...FILES, ...FOLDERS, `**/{${Array.from(FOLDERS).join(",")}}/**`] export const PATTERNS = [...FILES, ...FOLDERS, `**/{${Array.from(FOLDERS).join(",")}}/**`]
export function match(filepath: string, opts?: { extra?: string[]; whitelist?: string[] }) {
for (const pattern of opts?.whitelist || []) {
if (Glob.match(pattern, filepath)) return false
}
const parts = filepath.split(/[/\\]/)
for (const part of parts) {
if (FOLDERS.has(part)) return true
}
for (const pattern of [...FILES, ...(opts?.extra || [])]) {
if (Glob.match(pattern, filepath)) return true
}
return false
}
export * as Ignore from "./ignore" export * as Ignore from "./ignore"
@@ -3,7 +3,6 @@ export * as LocationWatcher from "./location-watcher"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Effect, Layer, Stream } from "effect" import { Context, Effect, Layer, Stream } from "effect"
import { FileSystem } from "@opencode-ai/schema/filesystem" import { FileSystem } from "@opencode-ai/schema/filesystem"
import os from "os"
import path from "path" import path from "path"
import { Config } from "../config" import { Config } from "../config"
import { Bus } from "../bus" import { Bus } from "../bus"
@@ -44,7 +43,7 @@ const layer = Layer.effect(
const config = (yield* configService.entries()) const config = (yield* configService.entries())
.filter((entry): entry is Config.Document => entry.type === "document") .filter((entry): entry is Config.Document => entry.type === "document")
.flatMap((item) => item.info.watcher?.ignore ?? []) .flatMap((item) => item.info.watcher?.ignore ?? [])
const home = path.resolve(location.directory) === path.resolve(os.homedir()) const home = Protected.isHome(location.directory)
if (!home && location.vcs) { if (!home && location.vcs) {
const updates = yield* watcher.subscribe({ const updates = yield* watcher.subscribe({
@@ -3,6 +3,10 @@ import path from "path"
const home = os.homedir() const home = os.homedir()
export function isHome(directory: string) {
return path.resolve(directory) === path.resolve(home)
}
const DARWIN_HOME = [ const DARWIN_HOME = [
"Music", "Music",
"Pictures", "Pictures",
+20 -120
View File
@@ -6,15 +6,13 @@ import { Context, Effect, Layer, Schema, Scope } from "effect"
import { Fff } from "#fff" import { Fff } from "#fff"
import fuzzysort from "fuzzysort" import fuzzysort from "fuzzysort"
import { FileSystem } from "../filesystem" import { FileSystem } from "../filesystem"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Location } from "../location" import { Location } from "../location"
import { Ripgrep } from "../ripgrep" import { Ripgrep } from "../ripgrep"
import { RelativePath } from "../schema" import { RelativePath } from "../schema"
import { Protected } from "./protected"
export interface Interface { export interface Interface {
readonly find: (input: FileSystem.FindInput) => Effect.Effect<FileSystem.Entry[]> readonly find: (input: FileSystem.FindInput) => Effect.Effect<FileSystem.Entry[]>
readonly glob: (input: FileSystem.GlobInput) => Effect.Effect<readonly FileSystem.Entry[]>
readonly grep: (input: FileSystem.GrepInput) => Effect.Effect<readonly FileSystem.Match[]>
} }
export const Options = Schema.Struct({ export const Options = Schema.Struct({
@@ -27,89 +25,35 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Fi
export const ripgrepLayer = Layer.effect( export const ripgrepLayer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const fs = yield* FSUtil.Service
const location = yield* Location.Service const location = yield* Location.Service
const ripgrep = yield* Ripgrep.Service const ripgrep = yield* Ripgrep.Service
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
const state = { const files: string[] = []
files: [] as string[],
directories: [] as string[],
}
const directories = new Set<string>() const directories = new Set<string>()
const home = Protected.isHome(location.directory)
yield* ripgrep yield* ripgrep
.find({ .find({
cwd: location.directory, cwd: location.directory,
pattern: "*", pattern: "*",
limit: location.vcs ? Number.MAX_SAFE_INTEGER : 100_000, limit: location.vcs && !home ? Number.MAX_SAFE_INTEGER : 100_000,
exclude: home ? [...Protected.names()].map((name) => `${name}/**`) : undefined,
onEntry: (entry) => onEntry: (entry) =>
Effect.sync(() => { Effect.sync(() => {
state.files.push(entry.path) files.push(entry.path)
const parts = entry.path.split("/") const parts = entry.path.split("/")
parts.slice(0, -1).forEach((_, index) => directories.add(parts.slice(0, index + 1).join("/") + path.sep)) parts.slice(0, -1).forEach((_, index) => directories.add(parts.slice(0, index + 1).join("/") + path.sep))
state.directories = Array.from(directories)
}), }),
}) })
.pipe(Effect.orDie, Effect.asVoid, Effect.forkIn(scope)) .pipe(Effect.orDie, Effect.asVoid, Effect.forkIn(scope))
return Service.of({ return Service.of({
glob: (input) =>
Effect.gen(function* () {
const target = path.resolve(location.directory, input.path ?? ".")
const info = yield* fs.stat(target).pipe(Effect.orDie)
const cwd = info.type === "File" ? path.dirname(target) : target
return yield* ripgrep
.glob({
cwd,
pattern: input.pattern,
limit: input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT,
})
.pipe(
Effect.map((result) =>
result.map((entry) =>
FileSystem.Entry.make({
...entry,
path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, entry.path))),
}),
),
),
Effect.orDie,
)
}),
grep: (input) =>
Effect.gen(function* () {
const target = path.resolve(location.directory, input.path ?? ".")
const info = yield* fs.stat(target).pipe(Effect.orDie)
const cwd = info.type === "File" ? path.dirname(target) : target
return yield* ripgrep
.grep({
cwd,
pattern: input.pattern,
file: info.type === "File" ? path.basename(target) : undefined,
include: input.include,
limit: input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT,
})
.pipe(
Effect.map((result) =>
result.map((match) =>
FileSystem.Match.make({
...match,
entry: FileSystem.Entry.make({
...match.entry,
path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, match.entry.path))),
}),
}),
),
),
Effect.orDie,
)
}),
find: (input) => find: (input) =>
Effect.gen(function* () { Effect.gen(function* () {
const items = const items =
input.type === "file" input.type === "file"
? state.files ? files
: input.type === "directory" : input.type === "directory"
? state.directories ? Array.from(directories)
: [...state.files, ...state.directories] : [...files, ...directories]
return fuzzysort.go(input.query, items, { limit: input.limit ?? 50 }).map((item) => { return fuzzysort.go(input.query, items, { limit: input.limit ?? 50 }).map((item) => {
const relative = item.target const relative = item.target
const type = relative.endsWith(path.sep) ? ("directory" as const) : ("file" as const) const type = relative.endsWith(path.sep) ? ("directory" as const) : ("file" as const)
@@ -143,55 +87,10 @@ export const fffLayer = Layer.effect(
if (result) yield* Effect.logWarning("failed to initialize fff", { error: result.error }) if (result) yield* Effect.logWarning("failed to initialize fff", { error: result.error })
return Service.of({ return Service.of({
find: () => Effect.succeed([]), find: () => Effect.succeed([]),
glob: () => Effect.succeed([]),
grep: () => Effect.succeed([]),
}) })
} }
yield* Effect.addFinalizer(() => Effect.sync(() => result.value.destroy()).pipe(Effect.ignore)) yield* Effect.addFinalizer(() => Effect.sync(() => result.value.destroy()).pipe(Effect.ignore))
return Service.of({ return Service.of({
glob: (input) =>
Effect.sync(() => {
const prefix = input.path?.replaceAll("\\", "/").replace(/\/$/, "")
const found = result.value.glob(prefix ? `${prefix}/${input.pattern}` : input.pattern, {
pageIndex: 0,
pageSize: input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT,
})
if (!found.ok) throw found.error
return found.value.items.map((item) =>
FileSystem.Entry.make({
path: RelativePath.make(item.relativePath.replaceAll("\\", "/")),
type: "file",
}),
)
}),
grep: (input) =>
Effect.sync(() => {
const prefix = input.path?.replaceAll("\\", "/").replace(/\/$/, "")
const found = result.value.grep(
[prefix ? `${prefix}/**` : undefined, input.include, input.pattern]
.filter((value) => value !== undefined)
.join(" "),
{ mode: "regex", pageSize: input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT, timeBudgetMs: 1_500 },
)
if (!found.ok) throw found.error
return found.value.items.map((match) => {
const bytes = Buffer.from(match.lineContent)
return FileSystem.Match.make({
entry: FileSystem.Entry.make({
path: RelativePath.make(match.relativePath.replaceAll("\\", "/")),
type: "file",
}),
line: match.lineNumber,
offset: match.byteOffset,
text: match.lineContent.length > 2_000 ? match.lineContent.slice(0, 2_000) + "..." : match.lineContent,
submatches: match.matchRanges.map(([start, end]) => ({
text: bytes.subarray(start, end).toString("utf8"),
start,
end,
})),
})
})
}),
find: (input) => find: (input) =>
Effect.sync(() => { Effect.sync(() => {
const options = { pageIndex: 0, pageSize: input.limit ?? 50 } const options = { pageIndex: 0, pageSize: input.limit ?? 50 }
@@ -236,18 +135,19 @@ export const fffLayer = Layer.effect(
}), }),
) )
export const layer = (options?: Options) => Layer.unwrap( export const layer = (options?: Options) =>
Effect.gen(function* () { Layer.unwrap(
if (options?.fff === false || (options?.fff === undefined && process.platform === "win32") || !Fff.available()) Effect.gen(function* () {
return ripgrepLayer if (options?.fff === false || (options?.fff === undefined && process.platform === "win32") || !Fff.available())
const location = yield* Location.Service return ripgrepLayer
// Non-VCS locations can contain many repositories, so avoid eagerly content-indexing the entire aggregate tree. const location = yield* Location.Service
return location.vcs ? fffLayer : ripgrepLayer // Non-VCS locations can contain many repositories, so avoid eagerly content-indexing the entire aggregate tree.
}), return location.vcs && !Protected.isHome(location.directory) ? fffLayer : ripgrepLayer
) }),
)
export function configured(options?: Options) { export function configured(options?: Options) {
return makeLocationNode({ service: Service, layer: layer(options), deps: [FSUtil.node, Location.node, Ripgrep.node] }) return makeLocationNode({ service: Service, layer: layer(options), deps: [Location.node, Ripgrep.node] })
} }
export const node = configured() export const node = configured()
+2 -28
View File
@@ -1,6 +1,6 @@
export * as Formatter from "./formatter" export * as Formatter from "./formatter"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer } from "effect"
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
import path from "path" import path from "path"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
@@ -11,16 +11,7 @@ import { Config } from "./config"
import { Location } from "./location" import { Location } from "./location"
import { make, type Info } from "./formatter/builtins" import { make, type Info } from "./formatter/builtins"
export const Status = Schema.Struct({
name: Schema.String,
extensions: Schema.Array(Schema.String),
enabled: Schema.Boolean,
}).annotate({ identifier: "FormatterStatus" })
export type Status = typeof Status.Type
export interface Interface { export interface Interface {
readonly init: () => Effect.Effect<void>
readonly status: () => Effect.Effect<Status[]>
readonly file: (filepath: string) => Effect.Effect<boolean> readonly file: (filepath: string) => Effect.Effect<boolean>
} }
@@ -84,23 +75,6 @@ const layer = Layer.effect(
return result return result
}) })
const init = Effect.fn("Formatter.init")(function* () {
yield* load
})
const status = Effect.fn("Formatter.status")(function* () {
yield* load
return yield* Effect.forEach(formatters, (formatter) =>
command(formatter).pipe(
Effect.map((enabled) => ({
name: formatter.name,
extensions: [...formatter.extensions],
enabled: enabled !== false,
})),
),
)
})
const file = Effect.fn("Formatter.file")(function* (filepath: string) { const file = Effect.fn("Formatter.file")(function* (filepath: string) {
yield* load yield* load
const matching = formatters.filter((formatter) => const matching = formatters.filter((formatter) =>
@@ -143,7 +117,7 @@ const layer = Layer.effect(
return false return false
}) })
return Service.of({ init, status, file }) return Service.of({ file })
}), }),
) )
+1 -224
View File
@@ -1,8 +1,7 @@
export * as Git from "./git" export * as Git from "./git"
import path from "path" import path from "path"
import { randomUUID } from "crypto" import { Context, Effect, Layer, Schema } from "effect"
import { Context, Effect, Layer, Schema, Stream } from "effect"
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
import { AbsolutePath, RelativePath } from "./schema" import { AbsolutePath, RelativePath } from "./schema"
import { FSUtil } from "@opencode-ai/util/fs-util" import { FSUtil } from "@opencode-ai/util/fs-util"
@@ -36,9 +35,6 @@ const snapshotConfig = `[core]
threads = true threads = true
` `
export const ChangeSet = Schema.String.pipe(Schema.brand("Git.ChangeSet"))
export type ChangeSet = typeof ChangeSet.Type
export const TreeID = Schema.String.pipe(Schema.brand("Git.TreeID")) export const TreeID = Schema.String.pipe(Schema.brand("Git.TreeID"))
export type TreeID = typeof TreeID.Type export type TreeID = typeof TreeID.Type
@@ -73,13 +69,6 @@ export class WorktreeError extends Schema.TaggedErrorClass<WorktreeError>()("Git
cause: Schema.optional(Schema.Defect()), cause: Schema.optional(Schema.Defect()),
}) {} }) {}
export class PatchError extends Schema.TaggedErrorClass<PatchError>()("Git.PatchError", {
operation: Schema.Literals(["capture", "apply", "reset"]),
directory: AbsolutePath,
message: Schema.String,
cause: Schema.optional(Schema.Defect()),
}) {}
export interface Interface { export interface Interface {
readonly repo: { readonly repo: {
readonly discover: (input: AbsolutePath) => Effect.Effect<Repository | undefined> readonly discover: (input: AbsolutePath) => Effect.Effect<Repository | undefined>
@@ -116,20 +105,6 @@ export interface Interface {
) => Effect.Effect<void, OperationError> ) => Effect.Effect<void, OperationError>
readonly resetHard: (repository: Repository, revision: string) => Effect.Effect<void, OperationError> readonly resetHard: (repository: Repository, revision: string) => Effect.Effect<void, OperationError>
} }
readonly change: {
readonly capture: (input: { repository: Repository; path: AbsolutePath }) => Effect.Effect<ChangeSet, PatchError>
readonly apply: (input: {
repository: Repository
path: AbsolutePath
changes: ChangeSet
}) => Effect.Effect<void, PatchError>
readonly discard: (input: {
repository: Repository
path: AbsolutePath
index: "preserve" | "reset"
untracked: "preserve" | "remove"
}) => Effect.Effect<void, PatchError>
}
readonly worktree: { readonly worktree: {
readonly create: (input: { readonly create: (input: {
repository: Repository repository: Repository
@@ -175,17 +150,10 @@ export interface Interface {
context?: number context?: number
paths?: readonly RelativePath[] paths?: readonly RelativePath[]
}) => Effect.Effect<readonly File.Diff[], OperationError> }) => Effect.Effect<readonly File.Diff[], OperationError>
readonly preview: (input: {
repository: Repository
current: TreeID
files: ReadonlyMap<RelativePath, TreeID>
context?: number
}) => Effect.Effect<readonly File.Diff[], OperationError>
readonly restore: (input: { readonly restore: (input: {
repository: Repository repository: Repository
files: ReadonlyMap<RelativePath, TreeID> files: ReadonlyMap<RelativePath, TreeID>
}) => Effect.Effect<void, OperationError> }) => Effect.Effect<void, OperationError>
readonly checkout: (input: { repository: Repository; tree: TreeID }) => Effect.Effect<void, OperationError>
} }
} }
@@ -657,58 +625,6 @@ const layer = Layer.effect(
return { mode: match[1], object: match[2] } return { mode: match[1], object: match[2] }
}) })
const preview = Effect.fn("Git.tree.preview")(
(input: {
repository: Repository
current: TreeID
files: ReadonlyMap<RelativePath, TreeID>
context?: number
}) =>
locked(
input.repository,
Effect.gen(function* () {
const index = path.join(input.repository.gitDirectory, `preview-${randomUUID()}.index`)
const env = { GIT_INDEX_FILE: index }
return yield* Effect.gen(function* () {
yield* repositoryOperation("diff", input.repository, ["read-tree", input.current], { env })
yield* Effect.forEach(
input.files,
([file, tree]) =>
Effect.gen(function* () {
const source = yield* entry(input.repository, tree, file)
if (!source) {
yield* repositoryOperation(
"diff",
input.repository,
["update-index", "--force-remove", "--", file],
{ env },
)
return
}
yield* repositoryOperation(
"diff",
input.repository,
["update-index", "--add", "--cacheinfo", source.mode, source.object, file],
{ env },
)
}),
{ discard: true },
)
const target = TreeID.make(
(yield* repositoryOperation("diff", input.repository, ["write-tree"], { env })).text.trim(),
)
return yield* treeDiff({
repository: input.repository,
from: input.current,
to: target,
context: input.context,
paths: Array.from(input.files.keys()),
})
}).pipe(Effect.ensuring(fs.remove(index).pipe(Effect.catch(() => Effect.void))))
}),
),
)
const restore = Effect.fn("Git.tree.restore")( const restore = Effect.fn("Git.tree.restore")(
(input: { repository: Repository; files: ReadonlyMap<RelativePath, TreeID> }) => (input: { repository: Repository; files: ReadonlyMap<RelativePath, TreeID> }) =>
locked( locked(
@@ -738,142 +654,6 @@ const layer = Layer.effect(
), ),
) )
const checkoutTree = Effect.fn("Git.tree.checkout")((input: { repository: Repository; tree: TreeID }) =>
locked(
input.repository,
Effect.gen(function* () {
yield* repositoryOperation("restore", input.repository, ["read-tree", input.tree])
yield* repositoryOperation("restore", input.repository, ["checkout-index", "--all", "--force"])
}),
),
)
const capture = Effect.fn("Git.change.capture")(function* (input: { repository: Repository; path: AbsolutePath }) {
const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "."
const tracked = yield* execute(
input.repository.worktree,
proc,
)(["diff", "--binary", "HEAD", "--", scope]).pipe(
Effect.mapError(
(cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }),
),
)
if (tracked.exitCode !== 0) {
return yield* new PatchError({
operation: "capture",
directory: input.path,
message: tracked.stderr.trim() || tracked.text.trim() || "Failed to capture tracked changes",
})
}
const untracked = yield* execute(
input.repository.worktree,
proc,
)(["ls-files", "--others", "--exclude-standard", "-z", "--", scope]).pipe(
Effect.mapError(
(cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }),
),
)
if (untracked.exitCode !== 0) {
return yield* new PatchError({
operation: "capture",
directory: input.path,
message: untracked.stderr.trim() || untracked.text.trim() || "Failed to list untracked changes",
})
}
const created = yield* Effect.forEach(untracked.text.split("\0").filter(Boolean), (file) =>
execute(
input.repository.worktree,
proc,
)(["diff", "--binary", "--no-index", "--", "/dev/null", file]).pipe(
Effect.mapError(
(cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }),
),
Effect.flatMap((result) =>
// git diff --no-index returns 1 when differences were found.
result.exitCode === 0 || result.exitCode === 1
? Effect.succeed(result.text)
: Effect.fail(
new PatchError({
operation: "capture",
directory: input.path,
message:
result.stderr.trim() || result.text.trim() || `Failed to capture untracked change: ${file}`,
}),
),
),
),
)
return ChangeSet.make([tracked.text, ...created].filter(Boolean).join("\n"))
})
const apply = Effect.fn("Git.change.apply")(function* (input: {
repository: Repository
path: AbsolutePath
changes: ChangeSet
}) {
const result = yield* proc
.run(
ChildProcess.make("git", ["apply", "-"], {
cwd: input.path,
extendEnv: true,
stdin: Stream.make(new TextEncoder().encode(input.changes)),
}),
)
.pipe(
Effect.mapError(
(cause) => new PatchError({ operation: "apply", directory: input.path, message: cause.message, cause }),
),
)
if (result.exitCode === 0) return
return yield* new PatchError({
operation: "apply",
directory: input.path,
message:
result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim() || "Failed to apply changes",
})
})
const discard = Effect.fn("Git.change.discard")(function* (input: {
repository: Repository
path: AbsolutePath
index: "preserve" | "reset"
untracked: "preserve" | "remove"
}) {
const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "."
const restore = yield* execute(
input.repository.worktree,
proc,
)(input.index === "reset" ? ["checkout", "HEAD", "--", scope] : ["checkout", "--", scope]).pipe(
Effect.mapError(
(cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }),
),
)
if (restore.exitCode !== 0) {
return yield* new PatchError({
operation: "reset",
directory: input.path,
message: restore.stderr.trim() || restore.text.trim() || "Failed to restore tracked changes",
})
}
if (input.untracked === "preserve") return
const clean = yield* execute(
input.repository.worktree,
proc,
)(["clean", "-fd", "--", scope]).pipe(
Effect.mapError(
(cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }),
),
)
if (clean.exitCode === 0) return
return yield* new PatchError({
operation: "reset",
directory: input.path,
message: clean.stderr.trim() || clean.text.trim() || "Failed to clean untracked changes",
})
})
const worktreeRun = Effect.fnUntraced(function* ( const worktreeRun = Effect.fnUntraced(function* (
operation: "create" | "remove" | "list", operation: "create" | "remove" | "list",
repository: Repository, repository: Repository,
@@ -949,7 +729,6 @@ const layer = Layer.effect(
remote: { get: remote }, remote: { get: remote },
history: { head, branch, defaultRemoteBranch: remoteHead, rootCommits: roots }, history: { head, branch, defaultRemoteBranch: remoteHead, rootCommits: roots },
sync: { fetchRemotes: fetch, fetchBranch, checkoutRemoteBranch: checkout, resetHard: reset }, sync: { fetchRemotes: fetch, fetchBranch, checkoutRemoteBranch: checkout, resetHard: reset },
change: { capture, apply, discard },
worktree: { create: worktreeCreate, remove: worktreeRemove, list: worktreeList }, worktree: { create: worktreeCreate, remove: worktreeRemove, list: worktreeList },
index: { refresh, ignored }, index: { refresh, ignored },
tree: { tree: {
@@ -957,9 +736,7 @@ const layer = Layer.effect(
write: writeTree, write: writeTree,
files: treeFiles, files: treeFiles,
diff: treeDiff, diff: treeDiff,
preview,
restore, restore,
checkout: checkoutTree,
}, },
}) })
}), }),
-1
View File
@@ -10,7 +10,6 @@ const prefixes = {
part: "prt", part: "prt",
pty: "pty", pty: "pty",
tool: "tool", tool: "tool",
workspace: "wrk",
} as const } as const
export function ascending(prefix: keyof typeof prefixes, given?: string) { export function ascending(prefix: keyof typeof prefixes, given?: string) {
+6 -19
View File
@@ -12,7 +12,6 @@ import { Credential } from "../credential"
import { Bus } from "../bus" import { Bus } from "../bus"
import { Form } from "../form" import { Form } from "../form"
import { Integration } from "../integration" import { Integration } from "../integration"
import { IntegrationConnection } from "../integration/connection"
import { KeyedMutex } from "../effect/keyed-mutex" import { KeyedMutex } from "../effect/keyed-mutex"
import { Location } from "../location" import { Location } from "../location"
import { waitForAbort } from "@opencode-ai/util/process" import { waitForAbort } from "@opencode-ai/util/process"
@@ -31,7 +30,6 @@ export class ServerInfo extends Schema.Class<ServerInfo>("MCP.ServerInfo")({
name: ServerName, name: ServerName,
status: Status, status: Status,
integrationID: Integration.ID.pipe(Schema.optional), integrationID: Integration.ID.pipe(Schema.optional),
connection: IntegrationConnection.Info.pipe(Schema.optional),
}) {} }) {}
export class ServerInstructions extends Schema.Class<ServerInstructions>("MCP.ServerInstructions")({ export class ServerInstructions extends Schema.Class<ServerInstructions>("MCP.ServerInstructions")({
@@ -247,14 +245,6 @@ export const layer = (options?: Options) => Layer.effect(
return { name, entry } return { name, entry }
}) })
const info = (name: ServerName, entry: ServerEntry, connection: IntegrationConnection.Info | undefined) =>
new ServerInfo({
name,
status: entry.status,
integrationID: entry.integrationID,
connection,
})
// Builds the connect-time auth provider for a remote OAuth-integration server. The SDK presents and // Builds the connect-time auth provider for a remote OAuth-integration server. The SDK presents and
// refreshes stored tokens, persisting refreshes back to the same credential row. The provider never // refreshes stored tokens, persisting refreshes back to the same credential row. The provider never
// opens a browser, so an auth-gated connect ends in UnauthorizedError -> needs_auth rather than a redirect. // opens a browser, so an auth-gated connect ends in UnauthorizedError -> needs_auth rather than a redirect.
@@ -603,15 +593,12 @@ export const layer = (options?: Options) => Layer.effect(
) )
return Service.of({ return Service.of({
servers: Effect.fn("MCP.servers")(function* () { servers: Effect.fn("MCP.servers")(function* () {
const entries = Array.from(runtime).toSorted(([a], [b]) => a.localeCompare(b)) return Array.from(runtime)
return yield* Effect.forEach(entries, ([name, entry]) => .toSorted(([a], [b]) => a.localeCompare(b))
Effect.gen(function* () { .map(
const connection = entry.integrationID ([name, entry]) =>
? yield* integration.connection.active(entry.integrationID) new ServerInfo({ name, status: entry.status, integrationID: entry.integrationID }),
: undefined )
return info(name, entry, connection)
}),
)
}), }),
add: Effect.fn("MCP.add")(function* (server, config) { add: Effect.fn("MCP.add")(function* (server, config) {
const name = ServerName.make(server) const name = ServerName.make(server)
+1 -15
View File
@@ -59,16 +59,6 @@ export interface Interface {
readonly list: () => Effect.Effect<ReadonlyArray<Info>> readonly list: () => Effect.Effect<ReadonlyArray<Info>>
readonly directories: (input: DirectoriesInput) => Effect.Effect<Directories> readonly directories: (input: DirectoriesInput) => Effect.Effect<Directories>
readonly resolve: (input: AbsolutePath) => Effect.Effect<Resolved> readonly resolve: (input: AbsolutePath) => Effect.Effect<Resolved>
/**
* Temporary bridge method for writing the resolved project ID to the repo-local cache.
*
* This exists while the old opencode project service and this core project
* service work together: core resolves the ID, while the old service still owns
* database migration and persistence. The old service should call this after it
* finishes migrating from `resolve().previous` to `resolve().id`; once project
* persistence moves into core, this separate bridge method can go away.
*/
readonly commit: (input: { store: AbsolutePath; id: ID }) => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Project") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Project") {}
@@ -268,11 +258,7 @@ const layer = Layer.effect(
return yield* persist({ id: ID.global, directory, canonical: directory, vcs: undefined }) return yield* persist({ id: ID.global, directory, canonical: directory, vcs: undefined })
}) })
const commit = Effect.fn("Project.commit")(function* (input: { store: AbsolutePath; id: ID }) { return Service.of({ list, directories, resolve })
yield* fs.writeFileString(path.join(input.store, "opencode"), input.id).pipe(Effect.ignore)
})
return Service.of({ list, directories, resolve, commit })
}), }),
) )
+12 -32
View File
@@ -70,11 +70,6 @@ export class StrategyUnavailableError extends Schema.TaggedErrorClass<StrategyUn
{ strategy: StrategyID }, { strategy: StrategyID },
) {} ) {}
export class DuplicateStrategyError extends Schema.TaggedErrorClass<DuplicateStrategyError>()(
"ProjectCopy.DuplicateStrategyError",
{ strategy: StrategyID },
) {}
export type Error = export type Error =
| SourceDirectoryNotFoundError | SourceDirectoryNotFoundError
| DestinationExistsError | DestinationExistsError
@@ -99,7 +94,6 @@ export interface Strategy {
export { Event } export { Event }
export interface Interface { export interface Interface {
readonly register: (strategy: Strategy) => Effect.Effect<void, DuplicateStrategyError>
readonly create: (input: CreateInput) => Effect.Effect<Copy, Error> readonly create: (input: CreateInput) => Effect.Effect<Copy, Error>
readonly remove: (input: RemoveInput) => Effect.Effect<void, Error> readonly remove: (input: RemoveInput) => Effect.Effect<void, Error>
readonly refresh: (input: RefreshInput) => Effect.Effect<RefreshResult, Error> readonly refresh: (input: RefreshInput) => Effect.Effect<RefreshResult, Error>
@@ -144,29 +138,18 @@ const layer = Layer.effect(
return resolved return resolved
}) })
const registry = new Map<StrategyID, Strategy>() const strategy = makeGitWorktreeStrategy({ git, canonical })
const register = Effect.fn("ProjectCopy.register")(function* (strategy: Strategy) {
if (registry.has(strategy.id)) return yield* new DuplicateStrategyError({ strategy: strategy.id })
registry.set(strategy.id, strategy)
})
// Register default strategies
yield* register(makeGitWorktreeStrategy({ git, canonical })).pipe(Effect.orDie)
const strategies = () => Array.from(registry.values())
const source = Effect.fnUntraced(function* (input: AbsolutePath, projectID: Project.ID) { const source = Effect.fnUntraced(function* (input: AbsolutePath, projectID: Project.ID) {
const sourceDirectory = yield* canonical(input) const sourceDirectory = yield* canonical(input)
if (!(yield* directories.contains({ projectID, directory: sourceDirectory }))) if ((yield* directories.get({ projectID, directory: sourceDirectory })) === undefined)
return yield* new SourceDirectoryNotFoundError({ directory: sourceDirectory }) return yield* new SourceDirectoryNotFoundError({ directory: sourceDirectory })
return sourceDirectory return sourceDirectory
}) })
const getStrategy = Effect.fnUntraced(function* (id: StrategyID) { const getStrategy = Effect.fnUntraced(function* (id: StrategyID) {
const found = registry.get(id) if (id !== strategy.id) return yield* new StrategyUnavailableError({ strategy: id })
if (!found) return yield* new StrategyUnavailableError({ strategy: id }) return strategy
return found
}) })
const create = Effect.fn("ProjectCopy.create")(function* (input: CreateInput) { const create = Effect.fn("ProjectCopy.create")(function* (input: CreateInput) {
@@ -226,20 +209,18 @@ const layer = Layer.effect(
const discovered = yield* Effect.forEach( const discovered = yield* Effect.forEach(
sourceDirectories, sourceDirectories,
(sourceDirectory) => (sourceDirectory) =>
Effect.forEach(strategies(), (strategy) => strategy.list(sourceDirectory).pipe(
strategy.list(sourceDirectory).pipe( Effect.catchTag("ProjectCopy.DirectoryUnavailableError", () => Effect.succeed([])),
Effect.catchTag("ProjectCopy.DirectoryUnavailableError", () => Effect.succeed([])), Effect.map((items) =>
Effect.map((items) => items.map((item) => ({
items.map((item) => ({ directory: item.directory,
directory: item.directory, strategy: item.type === "copy" ? strategy.id : undefined,
strategy: item.type === "copy" ? strategy.id : undefined, })),
})),
),
), ),
), ),
{ concurrency: "unbounded" }, { concurrency: "unbounded" },
).pipe( ).pipe(
Effect.map((sets) => new Map(sets.flat(2).map((item) => [item.directory, item] as const)).values().toArray()), Effect.map((sets) => new Map(sets.flat().map((item) => [item.directory, item] as const)).values().toArray()),
) )
const removed = checked.filter((item) => !item.exists).map((item) => item.directory) const removed = checked.filter((item) => !item.exists).map((item) => item.directory)
const result = yield* db const result = yield* db
@@ -271,7 +252,6 @@ const layer = Layer.effect(
}) })
return Service.of({ return Service.of({
register,
create, create,
remove, remove,
refresh, refresh,
-21
View File
@@ -41,7 +41,6 @@ export interface Interface {
projectID: ProjectSchema.ID projectID: ProjectSchema.ID
directory: AbsolutePath directory: AbsolutePath
}) => Effect.Effect<Directory | undefined> }) => Effect.Effect<Directory | undefined>
readonly contains: (input: { projectID: ProjectSchema.ID; directory: AbsolutePath }) => Effect.Effect<boolean>
readonly create: (input: CreateInput, tx?: Transaction) => Effect.Effect<boolean> readonly create: (input: CreateInput, tx?: Transaction) => Effect.Effect<boolean>
readonly remove: (input: RemoveInput, tx?: Transaction) => Effect.Effect<boolean> readonly remove: (input: RemoveInput, tx?: Transaction) => Effect.Effect<boolean>
} }
@@ -99,25 +98,6 @@ const layer = Layer.effect(
return rows.map((row) => ({ directory: row.directory, strategy: row.strategy ?? undefined })) return rows.map((row) => ({ directory: row.directory, strategy: row.strategy ?? undefined }))
}) })
const contains = Effect.fn("ProjectDirectories.contains")(function* (input: {
projectID: ProjectSchema.ID
directory: AbsolutePath
}) {
return (
(yield* db
.select({ directory: ProjectDirectoryTable.directory })
.from(ProjectDirectoryTable)
.where(
and(
eq(ProjectDirectoryTable.project_id, input.projectID),
eq(ProjectDirectoryTable.directory, input.directory),
),
)
.get()
.pipe(Effect.orDie)) !== undefined
)
})
const get = Effect.fn("ProjectDirectories.get")(function* (input: { const get = Effect.fn("ProjectDirectories.get")(function* (input: {
projectID: ProjectSchema.ID projectID: ProjectSchema.ID
directory: AbsolutePath directory: AbsolutePath
@@ -139,7 +119,6 @@ const layer = Layer.effect(
return Service.of({ return Service.of({
list, list,
get, get,
contains,
create, create,
remove, remove,
}) })
+1 -5
View File
@@ -1,6 +1,5 @@
export * as PtyTicket from "./ticket" export * as PtyTicket from "./ticket"
import { Workspace } from "../workspace"
import { PtyTicket } from "@opencode-ai/schema/pty-ticket" import { PtyTicket } from "@opencode-ai/schema/pty-ticket"
import { PtyID } from "./schema" import { PtyID } from "./schema"
import { Cache, Context, Duration, Effect, Layer } from "effect" import { Cache, Context, Duration, Effect, Layer } from "effect"
@@ -14,7 +13,6 @@ export const ConnectToken = PtyTicket.ConnectToken
export type Scope = { export type Scope = {
readonly ptyID: PtyID readonly ptyID: PtyID
readonly directory?: string readonly directory?: string
readonly workspaceID?: Workspace.ID
} }
export interface Interface { export interface Interface {
@@ -25,9 +23,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/PtyTicket") {} export class Service extends Context.Service<Service, Interface>()("@opencode/PtyTicket") {}
function matches(record: Scope, input: Scope) { function matches(record: Scope, input: Scope) {
return ( return record.ptyID === input.ptyID && record.directory === input.directory
record.ptyID === input.ptyID && record.directory === input.directory && record.workspaceID === input.workspaceID
)
} }
// Tickets are inserted via Cache.set and removed atomically via invalidateWhen. The lookup is // Tickets are inserted via Cache.set and removed atomically via invalidateWhen. The lookup is
-17
View File
@@ -31,14 +31,6 @@ export type EnsureInput = {
readonly branch?: string readonly branch?: string
} }
export class InvalidRepositoryError extends Schema.TaggedErrorClass<InvalidRepositoryError>()(
"RepositoryCacheInvalidRepositoryError",
{
repository: Schema.String,
message: Schema.String,
},
) {}
export class InvalidBranchError extends Schema.TaggedErrorClass<InvalidBranchError>()( export class InvalidBranchError extends Schema.TaggedErrorClass<InvalidBranchError>()(
"RepositoryCacheInvalidBranchError", "RepositoryCacheInvalidBranchError",
{ {
@@ -86,7 +78,6 @@ export class CacheOperationError extends Schema.TaggedErrorClass<CacheOperationE
) {} ) {}
export type Error = export type Error =
| InvalidRepositoryError
| InvalidBranchError | InvalidBranchError
| CloneFailedError | CloneFailedError
| FetchFailedError | FetchFailedError
@@ -103,7 +94,6 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Re
export function isError(error: unknown): error is Error { export function isError(error: unknown): error is Error {
return ( return (
error instanceof InvalidRepositoryError ||
error instanceof InvalidBranchError || error instanceof InvalidBranchError ||
error instanceof CloneFailedError || error instanceof CloneFailedError ||
error instanceof FetchFailedError || error instanceof FetchFailedError ||
@@ -114,13 +104,6 @@ export function isError(error: unknown): error is Error {
) )
} }
export const parseRemote = Effect.fn("RepositoryCache.parseRemote")(function* (repository: string) {
return yield* Effect.try({
try: () => Repository.parseRemote(repository),
catch: (error) => new InvalidRepositoryError({ repository, message: errorMessage(error) }),
})
})
export const validateBranch = Effect.fn("RepositoryCache.validateBranch")(function* (branch: string) { export const validateBranch = Effect.fn("RepositoryCache.validateBranch")(function* (branch: string) {
return yield* Effect.try({ return yield* Effect.try({
try: () => Repository.validateBranch(branch), try: () => Repository.validateBranch(branch),
-10
View File
@@ -44,16 +44,6 @@ export class InvalidBranchError extends Schema.TaggedErrorClass<InvalidBranchErr
message: Schema.String, message: Schema.String,
}) {} }) {}
export type Error = InvalidReferenceError | UnsupportedLocalRepositoryError | InvalidBranchError
export function isError(error: unknown): error is Error {
return (
error instanceof InvalidReferenceError ||
error instanceof UnsupportedLocalRepositoryError ||
error instanceof InvalidBranchError
)
}
export function parse(input: string): Reference | undefined { export function parse(input: string): Reference | undefined {
const cleaned = normalizeInput(input) const cleaned = normalizeInput(input)
if (!cleaned) return if (!cleaned) return
+2
View File
@@ -52,6 +52,7 @@ export interface FindInput {
readonly cwd: string readonly cwd: string
readonly pattern: string readonly pattern: string
readonly limit: number readonly limit: number
readonly exclude?: readonly string[]
readonly hidden?: boolean readonly hidden?: boolean
readonly follow?: boolean readonly follow?: boolean
readonly signal?: AbortSignal readonly signal?: AbortSignal
@@ -195,6 +196,7 @@ const layer = Layer.effect(
...(input.hidden ? ["--hidden"] : []), ...(input.hidden ? ["--hidden"] : []),
...(input.follow ? ["--follow"] : []), ...(input.follow ? ["--follow"] : []),
...(input.pattern === "*" ? [] : [`--glob=${input.pattern}`]), ...(input.pattern === "*" ? [] : [`--glob=${input.pattern}`]),
...(input.exclude ?? []).map((pattern) => `--glob=!${pattern}`),
"--glob=!**/.git/**", "--glob=!**/.git/**",
".", ".",
], ],
+7 -44
View File
@@ -5,7 +5,6 @@ import { Effect, Layer, Schema, Context, Stream, Scope } from "effect"
import { ListAnchor } from "@opencode-ai/schema/session" import { ListAnchor } from "@opencode-ai/schema/session"
import { and, asc, desc, eq, gt, isNotNull, isNull, like, lt, ne, or, type SQL } from "drizzle-orm" import { and, asc, desc, eq, gt, isNotNull, isNull, like, lt, ne, or, type SQL } from "drizzle-orm"
import { Project } from "./project" import { Project } from "./project"
import { Workspace } from "./workspace"
import { Model } from "./model" import { Model } from "./model"
import { Location } from "./location" import { Location } from "./location"
import { SessionMessage } from "./session/message" import { SessionMessage } from "./session/message"
@@ -52,9 +51,6 @@ import { Shell as ShellSchema } from "@opencode-ai/schema/shell"
import { KeyedMutex } from "./effect/keyed-mutex" import { KeyedMutex } from "./effect/keyed-mutex"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
export const RevertState = Session.Revert
export type RevertState = Session.Revert
// get project -> project.locations // get project -> project.locations
// //
// get all sessions // get all sessions
@@ -62,12 +58,9 @@ export type RevertState = Session.Revert
// - by project // - by project
// - by subpath // - by subpath
// - by workspace (home is special)
export { ListAnchor } export { ListAnchor }
const ListInputBase = { const ListInputBase = {
workspaceID: Workspace.ID.pipe(Schema.optional),
search: Schema.String.pipe(Schema.optional), search: Schema.String.pipe(Schema.optional),
limit: PositiveInt.pipe(Schema.optional), limit: PositiveInt.pipe(Schema.optional),
order: Schema.Literals(["asc", "desc"]).pipe(Schema.optional), order: Schema.Literals(["asc", "desc"]).pipe(Schema.optional),
@@ -110,13 +103,6 @@ type ForkInput = {
boundary: Session.ForkRequestBoundary boundary: Session.ForkRequestBoundary
} }
export class OperationUnavailableError extends Schema.TaggedErrorClass<OperationUnavailableError>()(
"Session.OperationUnavailableError",
{
operation: Schema.Literals(["move", "skill", "switchAgent", "compact"]),
},
) {}
export { MessageDecodeError, NotFoundError } export { MessageDecodeError, NotFoundError }
export class PromptConflictError extends Schema.TaggedErrorClass<PromptConflictError>()("Session.PromptConflictError", { export class PromptConflictError extends Schema.TaggedErrorClass<PromptConflictError>()("Session.PromptConflictError", {
@@ -160,23 +146,6 @@ export class DestinationNotDirectoryError extends Schema.TaggedErrorClass<Destin
export const MessageNotFoundError = SessionRevert.MessageNotFoundError export const MessageNotFoundError = SessionRevert.MessageNotFoundError
export type MessageNotFoundError = SessionRevert.MessageNotFoundError export type MessageNotFoundError = SessionRevert.MessageNotFoundError
export type Error =
| NotFoundError
| MessageDecodeError
| OperationUnavailableError
| PromptConflictError
| SyntheticConflictError
| AttachmentError
| CompactionConflictError
| BusyError
| SkillNotFoundError
| DestinationNotFoundError
| DestinationNotDirectoryError
| Command.NotFoundError
| Command.EvaluationError
| MessageNotFoundError
| SessionGenerate.Error
export interface Interface { export interface Interface {
readonly list: (input?: ListInput) => Effect.Effect<{ readonly list: (input?: ListInput) => Effect.Effect<{
readonly data: SessionSchema.Info[] readonly data: SessionSchema.Info[]
@@ -233,7 +202,6 @@ export interface Interface {
readonly move: (input: { readonly move: (input: {
sessionID: SessionSchema.ID sessionID: SessionSchema.ID
directory: AbsolutePath directory: AbsolutePath
workspaceID?: Location.Ref["workspaceID"]
}) => Effect.Effect<void, NotFoundError | DestinationNotFoundError | DestinationNotDirectoryError> }) => Effect.Effect<void, NotFoundError | DestinationNotFoundError | DestinationNotDirectoryError>
readonly prompt: (input: { readonly prompt: (input: {
id?: SessionMessage.ID id?: SessionMessage.ID
@@ -373,7 +341,6 @@ const layer = Layer.effect(
parentID: input.parentID, parentID: input.parentID,
directory: location.directory, directory: location.directory,
path: path.relative(project.directory, location.directory).replaceAll("\\", "/"), path: path.relative(project.directory, location.directory).replaceAll("\\", "/"),
workspaceID: location.workspaceID ? Workspace.ID.make(location.workspaceID) : undefined,
title: input.title, title: input.title,
agent: input.agent, agent: input.agent,
model: input.model model: input.model
@@ -410,7 +377,7 @@ const layer = Layer.effect(
fork: Effect.fn("Session.fork")(function* (input) { fork: Effect.fn("Session.fork")(function* (input) {
const parent = yield* result.get(input.sessionID) const parent = yield* result.get(input.sessionID)
const boundary = yield* db const boundary = yield* db
.select({ id: SessionMessageTable.id, seq: SessionMessageTable.seq }) .select({ id: SessionMessageTable.id })
.from(SessionMessageTable) .from(SessionMessageTable)
.where( .where(
and( and(
@@ -429,13 +396,14 @@ const layer = Layer.effect(
}) })
if (!boundary) return yield* new ForkEmptyError({ sessionID: input.sessionID }) if (!boundary) return yield* new ForkEmptyError({ sessionID: input.sessionID })
const sessionID = SessionSchema.ID.create() const sessionID = SessionSchema.ID.create()
const instructionThrough = // The fork adopts the parent's newest instruction values rather than the
input.boundary.type === "before" ? boundary.seq - 1 : yield* Bus.latestSequence(db, parent.id) // values in effect at the boundary; copied history may contain frozen
// instruction-update text the initial baseline already reflects.
yield* bus.publish(SessionEvent.Forked, { yield* bus.publish(SessionEvent.Forked, {
sessionID, sessionID,
parentID: parent.id, parentID: parent.id,
boundary: { ...input.boundary, messageID: boundary.id }, boundary: { ...input.boundary, messageID: boundary.id },
instructions: yield* InstructionState.valuesAt(db, parent.id, instructionThrough), instructions: yield* InstructionState.current(db, parent.id),
}) })
return yield* result.get(sessionID).pipe(Effect.orDie) return yield* result.get(sessionID).pipe(Effect.orDie)
}), }),
@@ -460,7 +428,6 @@ const layer = Layer.effect(
const sortColumn = SessionTable.time_updated const sortColumn = SessionTable.time_updated
const conditions: SQL[] = [] const conditions: SQL[] = []
if ("directory" in input) conditions.push(eq(SessionTable.directory, input.directory)) if ("directory" in input) conditions.push(eq(SessionTable.directory, input.directory))
if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
if ("project" in input) conditions.push(eq(SessionTable.project_id, input.project)) if ("project" in input) conditions.push(eq(SessionTable.project_id, input.project))
if ("project" in input && input.subpath !== undefined) conditions.push(eq(SessionTable.path, input.subpath)) if ("project" in input && input.subpath !== undefined) conditions.push(eq(SessionTable.path, input.subpath))
if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`)) if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`))
@@ -738,11 +705,7 @@ const layer = Layer.effect(
const info = yield* fs.stat(directory).pipe(Effect.catch(() => Effect.succeed(undefined))) const info = yield* fs.stat(directory).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!info) return yield* new DestinationNotFoundError({ directory }) if (!info) return yield* new DestinationNotFoundError({ directory })
if (info.type !== "Directory") return yield* new DestinationNotDirectoryError({ directory }) if (info.type !== "Directory") return yield* new DestinationNotDirectoryError({ directory })
if ( if (current.location.directory === directory) return
current.location.directory === directory &&
current.location.workspaceID === input.workspaceID
)
return
const project = yield* projects.resolve(directory) const project = yield* projects.resolve(directory)
yield* persistProject(project) yield* persistProject(project)
if ((yield* execution.active).has(input.sessionID)) { if ((yield* execution.active).has(input.sessionID)) {
@@ -751,7 +714,7 @@ const layer = Layer.effect(
} }
yield* bus.publish(SessionEvent.Moved, { yield* bus.publish(SessionEvent.Moved, {
sessionID: input.sessionID, sessionID: input.sessionID,
location: Location.Ref.make({ directory, workspaceID: input.workspaceID }), location: Location.Ref.make({ directory }),
projectID: project.id, projectID: project.id,
subpath: RelativePath.make(path.relative(project.directory, directory).replaceAll("\\", "/")), subpath: RelativePath.make(path.relative(project.directory, directory).replaceAll("\\", "/")),
}) })
+3 -5
View File
@@ -80,10 +80,9 @@ export const entriesForRunner = Effect.fn("SessionHistory.entriesForRunner")(fun
.transaction(() => .transaction(() =>
Effect.gen(function* () { Effect.gen(function* () {
const messages = yield* messageEntries(db, sessionID) const messages = yield* messageEntries(db, sessionID)
const assembled = yield* InstructionState.assemble(db, sessionID, instructions)
return { return {
initial: assembled.initial, initial: yield* InstructionState.initial(db, sessionID, instructions),
entries: [...messages, ...assembled.updates].toSorted((a, b) => a.seq - b.seq), entries: messages,
} }
}), }),
) )
@@ -106,10 +105,9 @@ export const preview = Effect.fn("SessionHistory.preview")(function* (
) )
const settled = unsettled === -1 ? messages : messages.slice(0, unsettled) const settled = unsettled === -1 ? messages : messages.slice(0, unsettled)
const assembled = yield* InstructionState.preview(db, sessionID, instructions, observed) const assembled = yield* InstructionState.preview(db, sessionID, instructions, observed)
const entries = [...settled, ...assembled.updates].toSorted((a, b) => a.seq - b.seq)
return { return {
initial: assembled.initial, initial: assembled.initial,
messages: entries.map((entry) => entry.message), messages: settled.map((entry) => entry.message),
instructionUpdate: assembled.update, instructionUpdate: assembled.update,
} }
}), }),
+56 -230
View File
@@ -1,25 +1,20 @@
export * as InstructionState from "./instruction-state" export * as InstructionState from "./instruction-state"
import { and, asc, desc, eq, gt, inArray, lte, sql } from "drizzle-orm" import { eq, inArray, sql } from "drizzle-orm"
import { DateTime, Effect, Option, Schema } from "effect" import { Effect, Option, Schema } from "effect"
import type { Database } from "../database/database" import type { Database } from "../database/database"
import { Bus } from "../bus" import type { Bus } from "../bus"
import { EventTable } from "../event/sql"
import { Instructions } from "../instructions/index" import { Instructions } from "../instructions/index"
import { SessionEvent } from "./event" import { SessionEvent } from "./event"
import { SessionMessage } from "./message"
import { Event } from "@opencode-ai/schema/event"
import { SessionSchema } from "./schema" import { SessionSchema } from "./schema"
import { InstructionBlobTable, InstructionStateTable } from "./sql" import { InstructionBlobTable, InstructionStateTable } from "./sql"
type DatabaseService = Database.Interface["db"] type DatabaseService = Database.Interface["db"]
const decodeInstructionsUpdated = Schema.decodeUnknownSync(SessionEvent.InstructionsUpdated.data)
const decodeForked = Schema.decodeUnknownSync(SessionEvent.Forked.data)
export interface Observation extends Instructions.Admission { export interface Observation extends Instructions.Admission {
readonly sessionID: SessionSchema.ID readonly sessionID: SessionSchema.ID
readonly initial: boolean readonly initial: boolean
readonly previous: Instructions.Values
readonly current: Instructions.Values readonly current: Instructions.Values
} }
@@ -28,13 +23,14 @@ export const observe = Effect.fn("InstructionState.observe")(function* (
instructions: Instructions.Instructions, instructions: Instructions.Instructions,
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
): Effect.fn.Return<Observation, Instructions.InitializationBlocked> { ): Effect.fn.Return<Observation, Instructions.InitializationBlocked> {
const [observed, stored] = yield* Effect.all([Instructions.read(instructions), ensure(db, sessionID)], { const [observed, stored] = yield* Effect.all([Instructions.read(instructions), find(db, sessionID)], {
concurrency: "unbounded", concurrency: "unbounded",
}) })
const result = yield* observeAgainst(observed, stored?.current_values) const result = yield* observeAgainst(observed, stored?.current_values)
return { return {
sessionID, sessionID,
initial: !stored, initial: !stored,
previous: stored?.current_values ?? {},
...result, ...result,
} }
}) })
@@ -42,12 +38,20 @@ export const observe = Effect.fn("InstructionState.observe")(function* (
export const commit = Effect.fn("InstructionState.commit")(function* ( export const commit = Effect.fn("InstructionState.commit")(function* (
db: DatabaseService, db: DatabaseService,
bus: Bus.Interface, bus: Bus.Interface,
instructions: Instructions.Instructions,
observation: Observation, observation: Observation,
) { ) {
if (!observation.initial && Object.keys(observation.delta).length === 0) return if (!observation.initial && Object.keys(observation.delta).length === 0) return
// The rendered text is frozen into the durable event: replaying it later would
// require the Location-scoped registry that produced it.
const text = observation.initial ? "" : yield* renderUpdateText(db, instructions, observation)
yield* bus.publish( yield* bus.publish(
SessionEvent.InstructionsUpdated, SessionEvent.InstructionsUpdated,
{ sessionID: observation.sessionID, delta: observation.delta }, {
sessionID: observation.sessionID,
delta: observation.delta,
...(text.length > 0 ? { text } : {}),
},
{ {
// Initial sync establishes the baseline; unlike later deltas it is not chronological history. // Initial sync establishes the baseline; unlike later deltas it is not chronological history.
...(observation.initial ? { metadata: { instructions: { initial: true } } } : {}), ...(observation.initial ? { metadata: { instructions: { initial: true } } } : {}),
@@ -56,13 +60,27 @@ export const commit = Effect.fn("InstructionState.commit")(function* (
) )
}) })
const renderUpdateText = Effect.fnUntraced(function* (
db: DatabaseService,
instructions: Instructions.Instructions,
observation: Observation,
) {
const replaced = Object.entries(observation.previous).filter(([key]) => Object.hasOwn(observation.delta, key))
const blobs = yield* loadBlobs(db, replaced.map(([, hash]) => hash))
const previous = Object.fromEntries(replaced.map(([key, hash]) => [key, requireBlob(blobs, hash)]))
const admitted = new Map(
Object.entries(observation.blobs).map(([hash, value]) => [Instructions.Hash.make(hash), value]),
)
return Instructions.renderUpdate(instructions, previous, dereferenceDelta(observation.delta, admitted))
})
export const prepare = Effect.fn("InstructionState.prepare")(function* ( export const prepare = Effect.fn("InstructionState.prepare")(function* (
db: DatabaseService, db: DatabaseService,
bus: Bus.Interface, bus: Bus.Interface,
instructions: Instructions.Instructions, instructions: Instructions.Instructions,
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
) { ) {
yield* commit(db, bus, yield* observe(db, instructions, sessionID)) yield* commit(db, bus, instructions, yield* observe(db, instructions, sessionID))
}) })
export const apply = Effect.fn("InstructionState.apply")(function* ( export const apply = Effect.fn("InstructionState.apply")(function* (
@@ -140,79 +158,24 @@ export const reset = Effect.fn("InstructionState.reset")(function* (db: Database
.pipe(Effect.orDie) .pipe(Effect.orDie)
}) })
export const rebuild = Effect.fn("InstructionState.rebuild")(function* ( /** Renders the epoch baseline shown at the start of every model request. */
db: DatabaseService, export const initial = Effect.fn("InstructionState.initial")(function* (
sessionID: SessionSchema.ID,
) {
const state = yield* stateFromEvents(db, sessionID)
if (!state) {
yield* reset(db, sessionID)
return undefined
}
yield* db
.insert(InstructionStateTable)
.values(state)
.onConflictDoUpdate({
target: InstructionStateTable.session_id,
set: {
epoch_start: state.epoch_start,
through_seq: state.through_seq,
initial_values: state.initial_values,
current_values: state.current_values,
},
})
.run()
.pipe(Effect.orDie)
return state
})
const assembleState = Effect.fnUntraced(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
instructions: Instructions.Instructions,
state: typeof InstructionStateTable.$inferSelect,
) {
const rows = yield* instructionUpdatesAfter(db, sessionID, state.epoch_start)
const updates = rows.map((row) => ({
row,
delta: decodeInstructionsUpdated(row.data).delta,
}))
const blobs = yield* loadBlobs(db, [
...Object.values(state.initial_values),
...updates.flatMap((update) =>
Object.values(update.delta).filter((hash): hash is Instructions.Hash => hash !== "removed"),
),
])
const valuesAtStart = dereference(state.initial_values, blobs)
let values = valuesAtStart
const result: Array<{ readonly seq: number; readonly message: SessionMessage.System }> = []
for (const update of updates) {
const delta = dereferenceDelta(update.delta, blobs)
const text = Instructions.renderUpdate(instructions, values, delta)
if (text.length > 0)
result.push({
seq: update.row.seq,
message: SessionMessage.System.make({
id: SessionMessage.ID.fromEvent(Event.ID.make(update.row.id)),
type: "system",
text,
time: { created: DateTime.makeUnsafe(update.row.created) },
}),
})
values = Instructions.applyDelta(values, delta)
}
return { initial: Instructions.renderInitial(instructions, valuesAtStart), updates: result, current: values }
})
export const assemble = Effect.fn("InstructionState.assemble")(function* (
db: DatabaseService, db: DatabaseService,
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
instructions: Instructions.Instructions, instructions: Instructions.Instructions,
) { ) {
const state = yield* find(db, sessionID) const state = yield* find(db, sessionID)
if (!state) return yield* Effect.die(new Error(`Instruction state not found during assembly: ${sessionID}`)) if (!state) return yield* Effect.die(new Error(`Instruction state not found during assembly: ${sessionID}`))
const assembled = yield* assembleState(db, sessionID, instructions, state) const blobs = yield* loadBlobs(db, Object.values(state.initial_values))
return { initial: assembled.initial, updates: assembled.updates } return Instructions.renderInitial(instructions, dereference(state.initial_values, blobs))
})
/** The current instruction values, used to seed a fork's baseline. */
export const current = Effect.fn("InstructionState.current")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
) {
return (yield* find(db, sessionID))?.current_values
}) })
export const preview = Effect.fn("InstructionState.preview")(function* ( export const preview = Effect.fn("InstructionState.preview")(function* (
@@ -221,20 +184,26 @@ export const preview = Effect.fn("InstructionState.preview")(function* (
instructions: Instructions.Instructions, instructions: Instructions.Instructions,
observed: Instructions.ReadResult, observed: Instructions.ReadResult,
) { ) {
const state = yield* readState(db, sessionID) const state = yield* find(db, sessionID)
const result = yield* observeAgainst(observed, state?.current_values) const result = yield* observeAgainst(observed, state?.current_values)
const blobs = new Map<Instructions.Hash, Schema.Json>( const observedBlobs = new Map<Instructions.Hash, Schema.Json>(
Object.entries(result.blobs).map(([hash, value]) => [Instructions.Hash.make(hash), value]), Object.entries(result.blobs).map(([hash, value]) => [Instructions.Hash.make(hash), value]),
) )
if (!state) { if (!state) {
const values = dereference(result.current, blobs) const values = dereference(result.current, observedBlobs)
return { initial: Instructions.renderInitial(instructions, values), updates: [], update: "" } return { initial: Instructions.renderInitial(instructions, values), update: "" }
} }
const assembled = yield* assembleState(db, sessionID, instructions, state) const stored = yield* loadBlobs(db, [
...Object.values(state.initial_values),
...Object.values(state.current_values),
])
return { return {
initial: assembled.initial, initial: Instructions.renderInitial(instructions, dereference(state.initial_values, stored)),
updates: assembled.updates, update: Instructions.renderUpdate(
update: Instructions.renderUpdate(instructions, assembled.current, dereferenceDelta(result.delta, blobs)), instructions,
dereference(state.current_values, stored),
dereferenceDelta(result.delta, new Map([...stored, ...observedBlobs])),
),
} }
}) })
@@ -255,46 +224,6 @@ const find = Effect.fnUntraced(function* (db: DatabaseService, sessionID: Sessio
.pipe(Effect.orDie) .pipe(Effect.orDie)
}) })
const ensure = Effect.fnUntraced(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
const stored = yield* find(db, sessionID)
if (!stored) return yield* rebuild(db, sessionID)
const latest = yield* latestRelevantSequence(db, sessionID)
if (!latest || latest.seq <= stored.through_seq) return stored
return yield* rebuild(db, sessionID)
})
const readState = Effect.fnUntraced(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
const stored = yield* find(db, sessionID)
if (!stored) return yield* stateFromEvents(db, sessionID)
const latest = yield* latestRelevantSequence(db, sessionID)
if (!latest || latest.seq <= stored.through_seq) return stored
return yield* stateFromEvents(db, sessionID)
})
const stateFromEvents = Effect.fnUntraced(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
const folded = fold(yield* instructionEvents(db, sessionID))
return folded ? foldedState(sessionID, folded) : undefined
})
export const valuesAt = Effect.fn("InstructionState.valuesAt")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
through: number,
) {
return fold(yield* instructionEvents(db, sessionID, through))?.current
})
const latestRelevantSequence = Effect.fnUntraced(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
return yield* db
.select({ seq: EventTable.seq })
.from(EventTable)
.where(and(eq(EventTable.aggregate_id, sessionID), inArray(EventTable.type, relevantEventTypes)))
.orderBy(desc(EventTable.seq))
.limit(1)
.get()
.pipe(Effect.orDie)
})
const insertBlobs = Effect.fnUntraced(function* (db: DatabaseService, blobs: Readonly<Record<string, Schema.Json>>) { const insertBlobs = Effect.fnUntraced(function* (db: DatabaseService, blobs: Readonly<Record<string, Schema.Json>>) {
const rows = Object.entries(blobs).map(([hash, value]) => ({ hash: Instructions.Hash.make(hash), value })) const rows = Object.entries(blobs).map(([hash, value]) => ({ hash: Instructions.Hash.make(hash), value }))
if (rows.length === 0) return if (rows.length === 0) return
@@ -339,106 +268,3 @@ function requireBlob(blobs: ReadonlyMap<Instructions.Hash, Schema.Json>, hash: I
if (value === undefined) throw new Error(`Instruction blob not found: ${hash}`) if (value === undefined) throw new Error(`Instruction blob not found: ${hash}`)
return value return value
} }
const instructionEventType = Bus.versionedType(
SessionEvent.InstructionsUpdated.type,
SessionEvent.InstructionsUpdated.durable.version,
)
const compactionEventType = Bus.versionedType(
SessionEvent.Compaction.Ended.type,
SessionEvent.Compaction.Ended.durable.version,
)
const movedEventType = Bus.versionedType(SessionEvent.Moved.type, SessionEvent.Moved.durable.version)
const revertedEventType = Bus.versionedType(
SessionEvent.RevertEvent.Committed.type,
SessionEvent.RevertEvent.Committed.durable.version,
)
const forkedEventType = Bus.versionedType(SessionEvent.Forked.type, SessionEvent.Forked.durable.version)
const relevantEventTypes = [
forkedEventType,
instructionEventType,
compactionEventType,
movedEventType,
revertedEventType,
]
type InstructionEventRow = typeof EventTable.$inferSelect
const instructionEvents = Effect.fnUntraced(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
through?: number,
): Effect.fn.Return<ReadonlyArray<InstructionEventRow>> {
return yield* eventRows(db, sessionID, relevantEventTypes, undefined, through)
})
const instructionUpdatesAfter = Effect.fnUntraced(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
after: number,
) {
return yield* eventRows(db, sessionID, [instructionEventType], after)
})
const eventRows = Effect.fnUntraced(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
types: ReadonlyArray<string>,
after?: number,
through?: number,
): Effect.fn.Return<ReadonlyArray<InstructionEventRow>> {
return yield* db
.select()
.from(EventTable)
.where(
and(
eq(EventTable.aggregate_id, sessionID),
inArray(EventTable.type, types),
after === undefined ? undefined : gt(EventTable.seq, after),
through === undefined ? undefined : lte(EventTable.seq, through),
),
)
.orderBy(asc(EventTable.seq))
.all()
.pipe(Effect.orDie)
})
function fold(rows: ReadonlyArray<InstructionEventRow>) {
return rows.reduce<
| {
readonly epochStart: number
readonly throughSeq: number
readonly initial: Instructions.Values
readonly current: Instructions.Values
}
| undefined
>((state, row) => {
if (row.type === forkedEventType) {
const instructions = decodeForked(row.data).instructions
return instructions
? { epochStart: row.seq, throughSeq: row.seq, initial: instructions, current: instructions }
: undefined
}
if (row.type === movedEventType || row.type === revertedEventType) return undefined
if (row.type === compactionEventType)
return state
? { epochStart: row.seq, throughSeq: row.seq, initial: state.current, current: state.current }
: undefined
if (row.type !== instructionEventType) return state
const delta = decodeInstructionsUpdated(row.data).delta
const current = Instructions.applyHashDelta(state?.current ?? {}, delta)
return state
? { ...state, throughSeq: row.seq, current }
: { epochStart: row.seq, throughSeq: row.seq, initial: current, current }
}, undefined)
}
function foldedState(sessionID: SessionSchema.ID, folded: NonNullable<ReturnType<typeof fold>>) {
return {
session_id: sessionID,
epoch_start: folded.epochStart,
through_seq: folded.throughSeq,
initial_values: folded.initial,
current_values: folded.current,
}
}
+20 -121
View File
@@ -1,12 +1,8 @@
import { castDraft, produce, type WritableDraft } from "immer" import { castDraft, produce, type WritableDraft } from "immer"
import { DateTime, Effect } from "effect" import { DateTime, Effect, Match, pipe } from "effect"
import { SessionEvent } from "./event" import { SessionEvent } from "./event"
import { SessionMessage } from "./message" import { SessionMessage } from "./message"
export type MemoryState = {
messages: SessionMessage.Info[]
}
export interface Adapter { export interface Adapter {
readonly getModel: () => Effect.Effect<SessionMessage.ModelSelected["model"] | undefined, never, never> readonly getModel: () => Effect.Effect<SessionMessage.ModelSelected["model"] | undefined, never, never>
readonly getCurrentAssistant: () => Effect.Effect<SessionMessage.Assistant | undefined, never, never> readonly getCurrentAssistant: () => Effect.Effect<SessionMessage.Assistant | undefined, never, never>
@@ -23,89 +19,7 @@ export interface Adapter {
readonly appendMessage: (message: SessionMessage.Info) => Effect.Effect<void, never, never> readonly appendMessage: (message: SessionMessage.Info) => Effect.Effect<void, never, never>
} }
export function memory(state: MemoryState): Adapter { export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
const assistantIndex = (messageID: SessionMessage.ID) =>
state.messages.findLastIndex((message) => message.id === messageID)
const shellIndex = (messageID: SessionMessage.ID) =>
state.messages.findLastIndex((message) => message.id === messageID)
const compactionIndex = () =>
state.messages.findLastIndex((message) => message.type === "compaction" && message.status === "running")
// A newer step supersedes stale incomplete rows; never resume an older assistant projection.
const latestAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant")
return {
getModel() {
return Effect.sync(
() =>
state.messages.findLast(
(message): message is SessionMessage.ModelSelected | SessionMessage.Assistant =>
message.type === "model-switched" || message.type === "assistant",
)?.model,
)
},
getCurrentAssistant() {
return Effect.sync(() => {
const index = latestAssistantIndex()
if (index < 0) return
const assistant = state.messages[index]
return assistant?.type === "assistant" && !assistant.time.completed ? assistant : undefined
})
},
getAssistant(messageID) {
return Effect.sync(() => {
const index = assistantIndex(messageID)
if (index < 0) return
const assistant = state.messages[index]
return assistant?.type === "assistant" ? assistant : undefined
})
},
getShell(shellID) {
return Effect.sync(() => {
return state.messages.find((message): message is SessionMessage.Shell => {
return message.type === "shell" && message.shellID === shellID
})
})
},
getCompaction() {
return Effect.sync(() => {
const index = compactionIndex()
const message = state.messages[index]
return message?.type === "compaction" ? message : undefined
})
},
updateAssistant(assistant) {
return Effect.sync(() => {
const index = assistantIndex(assistant.id)
if (index < 0) return
const current = state.messages[index]
if (current?.type !== "assistant") return
state.messages[index] = assistant
})
},
updateShell(shell) {
return Effect.sync(() => {
const index = shellIndex(shell.id)
if (index < 0) return
const current = state.messages[index]
if (current?.type !== "shell") return
state.messages[index] = shell
})
},
updateCompaction(compaction) {
return Effect.sync(() => {
const index = state.messages.findLastIndex((message) => message.id === compaction.id)
if (index >= 0) state.messages[index] = compaction
})
},
appendMessage(message) {
return Effect.sync(() => {
state.messages.push(message)
})
},
}
}
export function update(adapter: Adapter, event: SessionEvent.Event) {
type DraftAssistant = WritableDraft<SessionMessage.Assistant> type DraftAssistant = WritableDraft<SessionMessage.Assistant>
type DraftTool = WritableDraft<SessionMessage.AssistantTool> type DraftTool = WritableDraft<SessionMessage.AssistantTool>
type DraftText = WritableDraft<SessionMessage.AssistantText> type DraftText = WritableDraft<SessionMessage.AssistantText>
@@ -139,9 +53,9 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
} }
}) })
return Effect.gen(function* () { const project = pipe(
yield* SessionEvent.All.match(event, { Match.type<SessionEvent.DurableEvent>(),
"session.usage.updated": () => Effect.void, Match.discriminatorsExhaustive("type")({
"session.usage.recorded": () => Effect.void, "session.usage.recorded": () => Effect.void,
"session.agent.selected": (event) => { "session.agent.selected": (event) => {
return adapter.appendMessage( return adapter.appendMessage(
@@ -179,7 +93,18 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
"session.execution.succeeded": () => clearCurrentRetry, "session.execution.succeeded": () => clearCurrentRetry,
"session.execution.failed": () => clearCurrentRetry, "session.execution.failed": () => clearCurrentRetry,
"session.execution.interrupted": () => clearCurrentRetry, "session.execution.interrupted": () => clearCurrentRetry,
"session.instructions.updated": () => Effect.void, "session.instructions.updated": (event) => {
if (event.data.text === undefined) return Effect.void
return adapter.appendMessage(
SessionMessage.System.make({
id: SessionMessage.ID.fromEvent(event.id),
type: "system",
text: event.data.text,
metadata: event.metadata,
time: { created: event.created },
}),
)
},
"session.synthetic": (event) => { "session.synthetic": (event) => {
return adapter.appendMessage( return adapter.appendMessage(
SessionMessage.Synthetic.make({ SessionMessage.Synthetic.make({
@@ -310,12 +235,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
draft.content.push(castDraft(SessionMessage.AssistantText.make({ type: "text", text: "" }))) draft.content.push(castDraft(SessionMessage.AssistantText.make({ type: "text", text: "" })))
}) })
}, },
"session.text.delta": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestText(draft)
if (match) match.text += event.data.delta
})
},
"session.text.ended": (event) => { "session.text.ended": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestText(draft) const match = latestText(draft)
@@ -340,7 +259,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
) )
}) })
}, },
"session.tool.input.delta": () => Effect.void,
"session.tool.input.ended": (event) => { "session.tool.input.ended": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestTool(draft, event.data.id) const match = latestTool(draft, event.data.id)
@@ -364,14 +282,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
} }
}) })
}, },
"session.tool.progress": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestTool(draft, event.data.id)
if (match && match.state.status === "running") {
match.state.metadata = event.data.metadata
}
})
},
// Terminal tool events are self-contained; projection is a direct copy and // Terminal tool events are self-contained; projection is a direct copy and
// never reaches into ephemeral progress history. // never reaches into ephemeral progress history.
"session.tool.success": (event) => { "session.tool.success": (event) => {
@@ -425,12 +335,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
) )
}) })
}, },
"session.reasoning.delta": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestReasoning(draft)
if (match) match.text += event.data.delta
})
},
"session.reasoning.ended": (event) => { "session.reasoning.ended": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestReasoning(draft) const match = latestReasoning(draft)
@@ -464,12 +368,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
time: { created: event.created }, time: { created: event.created },
}), }),
), ),
"session.compaction.delta": (event) =>
Effect.gen(function* () {
const current = yield* adapter.getCompaction()
if (current?.status !== "running") return
yield* adapter.updateCompaction({ ...current, summary: current.summary + event.data.text })
}),
"session.compaction.ended": (event) => { "session.compaction.ended": (event) => {
return Effect.gen(function* () { return Effect.gen(function* () {
const current = yield* adapter.getCompaction() const current = yield* adapter.getCompaction()
@@ -515,8 +413,9 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
"session.revert.staged": () => Effect.void, "session.revert.staged": () => Effect.void,
"session.revert.cleared": () => Effect.void, "session.revert.cleared": () => Effect.void,
"session.revert.committed": () => Effect.void, "session.revert.committed": () => Effect.void,
}) }),
}) )
return project(event)
} }
export * as SessionMessageUpdater from "./message-updater" export * as SessionMessageUpdater from "./message-updater"
+22 -39
View File
@@ -12,10 +12,8 @@ import {
User, User,
UserData, UserData,
} from "@opencode-ai/schema/session-pending" } from "@opencode-ai/schema/session-pending"
import { Event } from "@opencode-ai/schema/event"
import type { Database } from "../database/database" import type { Database } from "../database/database"
import { Bus } from "../bus" import { Bus } from "../bus"
import { EventTable } from "../event/sql"
import { KeyedMutex } from "../effect/keyed-mutex" import { KeyedMutex } from "../effect/keyed-mutex"
import { SessionEvent } from "./event" import { SessionEvent } from "./event"
import { SessionMessage } from "./message" import { SessionMessage } from "./message"
@@ -37,11 +35,7 @@ const decodeUser = Schema.decodeUnknownSync(UserData)
const encodeUser = Schema.encodeSync(UserData) const encodeUser = Schema.encodeSync(UserData)
const decodeSynthetic = Schema.decodeUnknownSync(SyntheticData) const decodeSynthetic = Schema.decodeUnknownSync(SyntheticData)
const encodeSynthetic = Schema.encodeSync(SyntheticData) const encodeSynthetic = Schema.encodeSync(SyntheticData)
const decodeAdmittedEvent = Schema.decodeUnknownOption(SessionEvent.InputAdmitted.data) const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Info)
const admittedEventType = Bus.versionedType(
SessionEvent.InputAdmitted.type,
SessionEvent.InputAdmitted.durable.version,
)
const inboxLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>() const inboxLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>()
export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict>()( export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict>()(
@@ -103,46 +97,35 @@ export const compaction = Effect.fn("SessionPending.compaction")(function* (
return entry.type === "compaction" ? entry : undefined return entry.type === "compaction" ? entry : undefined
}) })
/** const promotedFromMessage = Effect.fn("SessionPending.promotedFromMessage")(function* (
* Reconstruct the admitted record for a pending row that was already consumed
* by promotion. The projected `session_message` row proves promotion happened;
* the durable `session.input.admitted` event retains the exact admitted
* message, including delivery.
*/
const promotedFromHistory = Effect.fn("SessionPending.promotedFromHistory")(function* (
db: DatabaseService, db: DatabaseService,
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
id: SessionMessage.ID, id: SessionMessage.ID,
delivery: Delivery,
) { ) {
const message = yield* db const row = yield* db
.select() .select()
.from(SessionMessageTable) .from(SessionMessageTable)
.where(eq(SessionMessageTable.id, id)) .where(eq(SessionMessageTable.id, id))
.get() .get()
.pipe(Effect.orDie) .pipe(Effect.orDie)
if (message === undefined) return undefined if (row === undefined) return undefined
if (message.session_id !== sessionID || (message.type !== "user" && message.type !== "synthetic")) if (row.session_id !== sessionID || (row.type !== "user" && row.type !== "synthetic"))
return yield* Effect.die(new LifecycleConflict({ id })) return yield* Effect.die(new LifecycleConflict({ id }))
const rows = yield* db const message = decodeMessage({ ...row.data, id: row.id, type: row.type })
.select() const base = { id, sessionID, timeCreated: message.time.created, delivery }
.from(EventTable) if (message.type === "user")
.where(and(eq(EventTable.aggregate_id, sessionID), eq(EventTable.type, admittedEventType))) return User.make({
.all() ...base,
.pipe(Effect.orDie) type: "user",
for (const row of rows) { data: decodeUser(message),
const decoded = decodeAdmittedEvent(row.data) })
if (decoded._tag !== "Some" || decoded.value.inputID !== id) continue if (message.type === "synthetic")
const base = { return Synthetic.make({
id, ...base,
sessionID, type: "synthetic",
timeCreated: DateTime.makeUnsafe(row.created), data: decodeSynthetic(message),
} })
return decoded.value.input.type === "user"
? User.make({ ...base, ...decoded.value.input })
: Synthetic.make({ ...base, ...decoded.value.input })
}
// A projected message without an admitted event in this aggregate (for
// example fork-copied history) is not a retryable admission.
return yield* Effect.die(new LifecycleConflict({ id })) return yield* Effect.die(new LifecycleConflict({ id }))
}) })
@@ -160,7 +143,7 @@ export const admit = Effect.fn("SessionPending.admit")(function* (
if (existing.type === "compaction") return yield* Effect.die(new LifecycleConflict({ id: request.id })) if (existing.type === "compaction") return yield* Effect.die(new LifecycleConflict({ id: request.id }))
return existing return existing
} }
const promoted = yield* promotedFromHistory(db, request.sessionID, request.id) const promoted = yield* promotedFromMessage(db, request.sessionID, request.id, request.input.delivery)
if (promoted !== undefined) return promoted if (promoted !== undefined) return promoted
return yield* bus return yield* bus
.publish(SessionEvent.InputAdmitted, { .publish(SessionEvent.InputAdmitted, {
@@ -426,7 +409,7 @@ const publish = Effect.fn("SessionPending.publish")(function* (
.pipe( .pipe(
Effect.catchDefect((defect) => Effect.catchDefect((defect) =>
defect instanceof LifecycleConflict defect instanceof LifecycleConflict
? promotedFromHistory(db, sessionID, entry.id).pipe( ? promotedFromMessage(db, sessionID, entry.id, entry.delivery).pipe(
Effect.flatMap((stored) => (stored !== undefined ? Effect.void : Effect.die(defect))), Effect.flatMap((stored) => (stored !== undefined ? Effect.void : Effect.die(defect))),
) )
: Effect.die(defect), : Effect.die(defect),
+17 -71
View File
@@ -1,6 +1,6 @@
export * as SessionProjector from "./projector" export * as SessionProjector from "./projector"
import { and, asc, desc, eq, gt, gte, inArray, lt, lte, sql } from "drizzle-orm" import { and, asc, desc, eq, gt, gte, lt, lte, sql } from "drizzle-orm"
import { DateTime, Effect, Layer, Schema, Stream } from "effect" import { DateTime, Effect, Layer, Schema, Stream } from "effect"
import { Database } from "../database/database" import { Database } from "../database/database"
import { Bus } from "../bus" import { Bus } from "../bus"
@@ -8,11 +8,9 @@ import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { Model } from "../model" import { Model } from "../model"
import { SessionEvent } from "./event" import { SessionEvent } from "./event"
import { SessionV1 } from "../v1/session" import { SessionV1 } from "../v1/session"
import { WorkspaceTable } from "../control-plane/workspace.sql"
import { SessionMessage } from "./message" import { SessionMessage } from "./message"
import { SessionMessageUpdater } from "./message-updater" import { SessionMessageUpdater } from "./message-updater"
import { SessionPending } from "./pending" import { SessionPending } from "./pending"
import { Workspace } from "../workspace"
import { InstructionState } from "./instruction-state" import { InstructionState } from "./instruction-state"
import { MessageTable, PartTable, SessionPendingTable, SessionMessageTable, SessionTable } from "./sql" import { MessageTable, PartTable, SessionPendingTable, SessionMessageTable, SessionTable } from "./sql"
import type { DeepMutable } from "../schema" import type { DeepMutable } from "../schema"
@@ -21,10 +19,7 @@ import { Money } from "@opencode-ai/schema/money"
type DatabaseService = Database.Interface["db"] type DatabaseService = Database.Interface["db"]
type CurrentDurableEvent = Extract<SessionEvent.Event, { readonly durable: object }> type CurrentDurableEvent = Extract<SessionEvent.Event, { readonly durable: object }>
type MessageEvent = Exclude< type MessageEvent = Exclude<CurrentDurableEvent, typeof SessionEvent.Forked.Type | typeof SessionEvent.Deleted.Type>
CurrentDurableEvent,
typeof SessionEvent.Forked.Type | typeof SessionEvent.Deleted.Type | typeof SessionEvent.InstructionsUpdated.Type
>
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Info) const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Info)
const encodeMessage = Schema.encodeSync(SessionMessage.Info) const encodeMessage = Schema.encodeSync(SessionMessage.Info)
@@ -62,7 +57,7 @@ function sessionRow(info: SessionV1.SessionInfo): typeof SessionTable.$inferInse
return { return {
id: info.id, id: info.id,
project_id: info.projectID, project_id: info.projectID,
workspace_id: info.workspaceID ?? null, workspace_id: null,
parent_id: info.parentID, parent_id: info.parentID,
slug: info.slug, slug: info.slug,
directory: info.directory, directory: info.directory,
@@ -255,66 +250,22 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
.pipe(Effect.orDie) .pipe(Effect.orDie)
if (rows.length === 0) break if (rows.length === 0) break
const idMap = new Map(rows.map((row) => [row.id, SessionMessage.ID.create()]))
yield* db yield* db
.insert(SessionMessageTable) .insert(SessionMessageTable)
.values( .values(
rows.map((row) => { rows.map((row) => ({
const id = idMap.get(row.id) id: SessionMessage.ID.create(),
if (!id) throw new Error(`Fork message ID mapping missing: ${row.id}`) session_id: event.data.sessionID,
return { type: row.type,
id, seq: row.seq,
session_id: event.data.sessionID, time_created: row.time_created,
type: row.type, time_updated: row.time_updated,
seq: row.seq, data: row.data,
time_created: row.time_created, })),
time_updated: row.time_updated,
data: row.data,
}
}),
) )
.run() .run()
.pipe(Effect.orDie) .pipe(Effect.orDie)
const pendingRows = yield* db
.select()
.from(SessionPendingTable)
.where(
and(
eq(SessionPendingTable.session_id, event.data.parentID),
inArray(
SessionPendingTable.id,
rows.map((row) => row.id),
),
),
)
.all()
.pipe(Effect.orDie)
if (pendingRows.length > 0) {
yield* db
.insert(SessionPendingTable)
.values(
pendingRows.flatMap((row) => {
const id = idMap.get(row.id)
return id && row.type !== "compaction"
? [
{
id,
session_id: event.data.sessionID,
type: row.type,
data: row.data,
delivery: row.delivery,
admitted_seq: row.admitted_seq,
time_created: row.time_created,
},
]
: []
}),
)
.run()
.pipe(Effect.orDie)
}
cursor = rows.at(-1)!.seq cursor = rows.at(-1)!.seq
} }
if (copiedSeq !== undefined) yield* Bus.reserveSequence(db, event.data.sessionID, copiedSeq) if (copiedSeq !== undefined) yield* Bus.reserveSequence(db, event.data.sessionID, copiedSeq)
@@ -476,14 +427,6 @@ const layer = Layer.effectDiscard(
.get() .get()
.pipe(Effect.orDie) .pipe(Effect.orDie)
if (!stored) return yield* Effect.die(new SessionAlreadyProjected()) if (!stored) return yield* Effect.die(new SessionAlreadyProjected())
if (event.data.info.workspaceID) {
yield* db
.update(WorkspaceTable)
.set({ time_used: Date.now() })
.where(eq(WorkspaceTable.id, event.data.info.workspaceID))
.run()
.pipe(Effect.orDie)
}
}), }),
) )
yield* bus.project(SessionV1.Event.Updated, (event) => yield* bus.project(SessionV1.Event.Updated, (event) =>
@@ -502,7 +445,7 @@ const layer = Layer.effectDiscard(
directory: event.data.location.directory, directory: event.data.location.directory,
path: event.data.subpath, path: event.data.subpath,
...(event.data.projectID ? { project_id: event.data.projectID } : {}), ...(event.data.projectID ? { project_id: event.data.projectID } : {}),
workspace_id: event.data.location.workspaceID ? Workspace.ID.make(event.data.location.workspaceID) : null, workspace_id: null,
time_updated: DateTime.toEpochMillis(event.created), time_updated: DateTime.toEpochMillis(event.created),
}) })
.where(eq(SessionTable.id, event.data.sessionID)) .where(eq(SessionTable.id, event.data.sessionID))
@@ -682,7 +625,10 @@ const layer = Layer.effectDiscard(
yield* bus.project(SessionEvent.Execution.Failed, (event) => run(db, event)) yield* bus.project(SessionEvent.Execution.Failed, (event) => run(db, event))
yield* bus.project(SessionEvent.Execution.Interrupted, (event) => run(db, event)) yield* bus.project(SessionEvent.Execution.Interrupted, (event) => run(db, event))
yield* bus.project(SessionEvent.InstructionsUpdated, (event) => yield* bus.project(SessionEvent.InstructionsUpdated, (event) =>
InstructionState.apply(db, event.data.sessionID, event.durable.seq, event.data.delta), Effect.gen(function* () {
yield* run(db, event)
yield* InstructionState.apply(db, event.data.sessionID, event.durable.seq, event.data.delta)
}),
) )
yield* bus.project(SessionEvent.Synthetic, (event) => run(db, event)) yield* bus.project(SessionEvent.Synthetic, (event) => run(db, event))
yield* bus.project(SessionEvent.Skill.Activated, (event) => run(db, event)) yield* bus.project(SessionEvent.Skill.Activated, (event) => run(db, event))
+6 -44
View File
@@ -1,25 +1,22 @@
export * as ShellSelect from "./select" export * as ShellSelect from "./select"
import path from "path" import path from "path"
import { spawn, type ChildProcess } from "child_process"
import { readFile } from "fs/promises" import { readFile } from "fs/promises"
import { statSync } from "fs" import { statSync } from "fs"
import { setTimeout } from "node:timers/promises"
import { Schema } from "effect" import { Schema } from "effect"
import { FSUtil } from "@opencode-ai/util/fs-util" import { FSUtil } from "@opencode-ai/util/fs-util"
import { which } from "../util/which" import { which } from "../util/which"
const SIGKILL_TIMEOUT_MS = 200 const META: Record<string, { deny?: boolean; login?: boolean; ps?: boolean }> = {
const META: Record<string, { deny?: boolean; login?: boolean; posix?: boolean; ps?: boolean }> = { bash: { login: true },
bash: { login: true, posix: true }, dash: { login: true },
dash: { login: true, posix: true },
fish: { deny: true, login: true }, fish: { deny: true, login: true },
ksh: { login: true, posix: true }, ksh: { login: true },
nu: { deny: true }, nu: { deny: true },
powershell: { ps: true }, powershell: { ps: true },
pwsh: { ps: true }, pwsh: { ps: true },
sh: { login: true, posix: true }, sh: { login: true },
zsh: { login: true, posix: true }, zsh: { login: true },
} }
export type Item = { export type Item = {
@@ -33,37 +30,6 @@ export const Options = Schema.Struct({
}) })
export type Options = typeof Options.Type export type Options = typeof Options.Type
export async function killTree(proc: ChildProcess, opts?: { exited?: () => boolean }): Promise<void> {
const pid = proc.pid
if (!pid || opts?.exited?.()) return
if (process.platform === "win32") {
await new Promise<void>((resolve) => {
const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], {
stdio: "ignore",
windowsHide: true,
})
killer.once("exit", () => resolve())
killer.once("error", () => resolve())
})
return
}
try {
process.kill(-pid, "SIGTERM")
await setTimeout(SIGKILL_TIMEOUT_MS)
if (!opts?.exited?.()) {
process.kill(-pid, "SIGKILL")
}
} catch {
proc.kill("SIGTERM")
await setTimeout(SIGKILL_TIMEOUT_MS)
if (!opts?.exited?.()) {
proc.kill("SIGKILL")
}
}
}
function stat(file: string) { function stat(file: string) {
return statSync(file, { throwIfNoEntry: false }) ?? undefined return statSync(file, { throwIfNoEntry: false }) ?? undefined
} }
@@ -150,10 +116,6 @@ export function login(file: string) {
return meta(file)?.login === true return meta(file)?.login === true
} }
export function posix(file: string) {
return meta(file)?.posix === true
}
export function ps(file: string) { export function ps(file: string) {
return meta(file)?.ps === true return meta(file)?.ps === true
} }
+6 -59
View File
@@ -16,7 +16,7 @@ import { Hash } from "@opencode-ai/util/hash"
export { ID } export { ID }
export class Error extends Schema.TaggedErrorClass<Error>()("Snapshot.Error", { export class Error extends Schema.TaggedErrorClass<Error>()("Snapshot.Error", {
operation: Schema.Literals(["capture", "files", "diff", "preview", "restore"]), operation: Schema.Literals(["capture", "files", "diff", "restore"]),
message: Schema.String, message: Schema.String,
cause: Schema.optional(Schema.Defect()), cause: Schema.optional(Schema.Defect()),
}) {} }) {}
@@ -36,10 +36,6 @@ export interface RestoreInput {
readonly files: ReadonlyMap<RelativePath, ID> readonly files: ReadonlyMap<RelativePath, ID>
} }
export interface PreviewInput extends RestoreInput {
readonly context?: number
}
export interface Interface { export interface Interface {
/** /**
* Capture the current Location-scoped filesystem state as a content-addressed * Capture the current Location-scoped filesystem state as a content-addressed
@@ -60,25 +56,11 @@ export interface Interface {
*/ */
readonly diff: (input: DiffInput) => Effect.Effect<readonly File.Diff[], Error> readonly diff: (input: DiffInput) => Effect.Effect<readonly File.Diff[], Error>
/**
* Preview the filesystem result of a selective restore without modifying the
* worktree. Each project-relative path maps to the tree it would be restored
* from.
*/
readonly preview: (input: PreviewInput) => Effect.Effect<readonly File.Diff[], Error>
/** /**
* Restore selected project-relative paths from their associated trees. A path * Restore selected project-relative paths from their associated trees. A path
* absent from its selected tree is removed; paths outside the map are untouched. * absent from its selected tree is removed; paths outside the map are untouched.
*/ */
readonly restore: (input: RestoreInput) => Effect.Effect<void, Error> readonly restore: (input: RestoreInput) => Effect.Effect<void, Error>
/**
* Replace the snapshot index with a captured tree and check out all its entries.
* Files absent from the tree remain untouched. Prefer selective `restore` when
* only known paths should change.
*/
readonly checkout: (snapshot: ID) => Effect.Effect<void, Error>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Snapshot") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Snapshot") {}
@@ -176,59 +158,26 @@ const layer = Layer.effect(
.pipe(Effect.mapError((cause) => failure("diff", cause))) .pipe(Effect.mapError((cause) => failure("diff", cause)))
}) })
const plan = Effect.fnUntraced(function* ( const plan = Effect.fnUntraced(function* (worktree: AbsolutePath, input: RestoreInput) {
operation: "preview" | "restore",
worktree: AbsolutePath,
input: RestoreInput,
) {
const files = new Map<RelativePath, Git.TreeID>() const files = new Map<RelativePath, Git.TreeID>()
for (const [file, snapshot] of input.files) { for (const [file, snapshot] of input.files) {
const absolute = path.resolve(worktree, file) const absolute = path.resolve(worktree, file)
if (!FSUtil.contains(worktree, absolute)) if (!FSUtil.contains(worktree, absolute))
return yield* new Error({ operation, message: `Path escapes the project: ${file}` }) return yield* new Error({ operation: "restore", message: `Path escapes the project: ${file}` })
files.set(file, Git.TreeID.make(snapshot)) files.set(file, Git.TreeID.make(snapshot))
} }
return files return files
}) })
const preview = Effect.fn("Snapshot.preview")(function* (input: PreviewInput) {
if (!(yield* enabled())) return yield* new Error({ operation: "preview", message: "Snapshots are disabled" })
const repo = yield* repository.pipe(Effect.mapError((cause) => failure("preview", cause)))
const files = yield* plan("preview", repo.worktree, input)
const current = yield* git.tree
.capture({
repository: repo.snapshotRepository,
scopes: Array.from(files.keys()),
ignores: repo.source,
maximumUntrackedFileBytes: 2 * 1024 * 1024,
})
.pipe(Effect.mapError((cause) => failure("preview", cause)))
return yield* git.tree
.preview({
repository: repo.snapshotRepository,
current,
files,
context: input.context,
})
.pipe(Effect.mapError((cause) => failure("preview", cause)))
})
const restore = Effect.fn("Snapshot.restore")(function* (input: RestoreInput) { const restore = Effect.fn("Snapshot.restore")(function* (input: RestoreInput) {
if (!(yield* enabled())) return yield* new Error({ operation: "restore", message: "Snapshots are disabled" }) if (!(yield* enabled())) return yield* new Error({ operation: "restore", message: "Snapshots are disabled" })
const repo = yield* repository.pipe(Effect.mapError((cause) => failure("restore", cause))) const repo = yield* repository.pipe(Effect.mapError((cause) => failure("restore", cause)))
yield* git.tree yield* git.tree
.restore({ repository: repo.snapshotRepository, files: yield* plan("restore", repo.worktree, input) }) .restore({ repository: repo.snapshotRepository, files: yield* plan(repo.worktree, input) })
.pipe(Effect.mapError((cause) => failure("restore", cause))) .pipe(Effect.mapError((cause) => failure("restore", cause)))
}) })
const checkout = Effect.fn("Snapshot.checkout")(function* (snapshot: ID) { return Service.of({ capture, files, diff, restore })
const repo = yield* repository.pipe(Effect.mapError((cause) => failure("restore", cause)))
yield* git.tree
.checkout({ repository: repo.snapshotRepository, tree: Git.TreeID.make(snapshot) })
.pipe(Effect.mapError((cause) => failure("restore", cause)))
})
return Service.of({ capture, files, diff, preview, restore, checkout })
}).pipe(Effect.withSpan("Snapshot.boot")), }).pipe(Effect.withSpan("Snapshot.boot")),
) )
@@ -244,9 +193,7 @@ export const noopLayer = Layer.succeed(
capture: () => Effect.succeed(undefined), capture: () => Effect.succeed(undefined),
files: () => Effect.succeed([]), files: () => Effect.succeed([]),
diff: () => Effect.succeed([]), diff: () => Effect.succeed([]),
preview: () => Effect.succeed([]),
restore: () => Effect.void, restore: () => Effect.void,
checkout: () => Effect.void,
}), }),
) )
-20
View File
@@ -1298,24 +1298,4 @@ describe("Bus", () => {
}), }),
) )
it.effect("sequences returns the latest committed seq per aggregate and omits unknown aggregates", () =>
Effect.gen(function* () {
const bus = yield* Bus.Service
const first = Session.ID.create()
const second = Session.ID.create()
yield* bus.publish(DurableMessage, durableData(first, "zero"))
yield* bus.publish(DurableMessage, durableData(first, "one"))
yield* bus.publish(DurableMessage, durableData(second, "zero"))
const sequences = yield* bus.sequences([first, second, Session.ID.create()])
expect(sequences).toEqual(
new Map([
[first, Event.Seq.make(1)],
[second, Event.Seq.make(0)],
]),
)
expect(yield* bus.sequences([])).toEqual(new Map())
}),
)
}) })
@@ -967,9 +967,6 @@ describe("DatabaseMigration", () => {
yield* db.run( yield* db.run(
sql`INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) VALUES ('global', '/project', 1, 1, '[]')`, sql`INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) VALUES ('global', '/project', 1, 1, '[]')`,
) )
yield* db.run(
sql`INSERT INTO workspace (id, type, project_id, time_used) VALUES ('workspace', 'local', 'global', 1)`,
)
yield* db.run( yield* db.run(
sql`INSERT INTO session (id, project_id, workspace_id, slug, directory, title, version, time_created, time_updated) VALUES ('session', 'global', 'workspace', 'session', '/project', 'Before', 'test', 1, 1)`, sql`INSERT INTO session (id, project_id, workspace_id, slug, directory, title, version, time_created, time_updated) VALUES ('session', 'global', 'workspace', 'session', '/project', 'Before', 'test', 1, 1)`,
) )
@@ -994,6 +991,7 @@ describe("DatabaseMigration", () => {
// must drop before the historical rename dance and recreate after. // must drop before the historical rename dance and recreate after.
yield* db.run(sql`DROP INDEX session_pending_session_compaction_idx`) yield* db.run(sql`DROP INDEX session_pending_session_compaction_idx`)
yield* db.run(sql`ALTER TABLE session_pending RENAME TO session_input`) yield* db.run(sql`ALTER TABLE session_pending RENAME TO session_input`)
yield* db.run(sql`CREATE TABLE workspace (id text PRIMARY KEY)`)
yield* db.run(sql`DELETE FROM migration WHERE id = ${simplifySessionPendingMigration.id}`) yield* db.run(sql`DELETE FROM migration WHERE id = ${simplifySessionPendingMigration.id}`)
yield* DatabaseMigration.applyOnly(db, [simplifySessionPendingMigration]) yield* DatabaseMigration.applyOnly(db, [simplifySessionPendingMigration])
yield* db.run(sql`DROP TABLE session_context_epoch`) yield* db.run(sql`DROP TABLE session_context_epoch`)
@@ -1029,7 +1027,6 @@ describe("DatabaseMigration", () => {
(SELECT workspace_id FROM session WHERE id = 'session') AS workspaceID, (SELECT workspace_id FROM session WHERE id = 'session') AS workspaceID,
(SELECT COUNT(*) FROM message WHERE id = 'message') AS messages, (SELECT COUNT(*) FROM message WHERE id = 'message') AS messages,
(SELECT COUNT(*) FROM part WHERE id = 'part') AS parts, (SELECT COUNT(*) FROM part WHERE id = 'part') AS parts,
(SELECT COUNT(*) FROM workspace) AS workspaces,
(SELECT COUNT(*) FROM session_pending) AS sessionInputs, (SELECT COUNT(*) FROM session_pending) AS sessionInputs,
(SELECT COUNT(*) FROM session_message) AS sessionMessages, (SELECT COUNT(*) FROM session_message) AS sessionMessages,
(SELECT COUNT(*) FROM instruction_state) AS instructionStates, (SELECT COUNT(*) FROM instruction_state) AS instructionStates,
@@ -1041,7 +1038,6 @@ describe("DatabaseMigration", () => {
workspaceID: null, workspaceID: null,
messages: 1, messages: 1,
parts: 1, parts: 1,
workspaces: 0,
sessionInputs: 0, sessionInputs: 0,
sessionMessages: 0, sessionMessages: 0,
instructionStates: 0, instructionStates: 0,
@@ -80,7 +80,6 @@ describe("node build", () => {
list: () => Effect.succeed([]), list: () => Effect.succeed([]),
directories: () => Effect.succeed([]), directories: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }), resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }),
commit: () => Effect.void,
}) })
}), }),
) )
-162
View File
@@ -89,68 +89,6 @@ describe("FileMutation", () => {
), ),
) )
it.live("rejects create when a prospective target appears after resolution", () =>
withTmp((directory) =>
Effect.gen(function* () {
const targetPath = path.join(directory, "appeared.txt")
const target = yield* (yield* LocationMutation.Service).resolve({ path: "appeared.txt" })
yield* Effect.promise(() => fs.writeFile(targetPath, "winner"))
expect(
yield* (yield* FileMutation.Service).create({ target, content: "replacement" }).pipe(Effect.flip),
).toMatchObject({
_tag: "FileMutation.TargetExistsError",
})
expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("winner")
}).pipe(provide(directory)),
),
)
it.live("creates when an existing target disappears after resolution", () =>
withTmp((directory) =>
Effect.gen(function* () {
const targetPath = path.join(directory, "removed.txt")
yield* Effect.promise(() => fs.writeFile(targetPath, "before"))
const target = yield* (yield* LocationMutation.Service).resolve({ path: "removed.txt" })
yield* Effect.promise(() => fs.rm(targetPath))
expect(yield* (yield* FileMutation.Service).create({ target, content: "after" })).toEqual({
operation: "write",
target: target.canonical,
resource: "removed.txt",
existed: false,
})
expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("after")
}).pipe(provide(directory)),
),
)
it.live("removes an existing internal file", () =>
withTmp((directory) =>
Effect.gen(function* () {
const targetPath = path.join(directory, "remove.txt")
yield* Effect.promise(() => fs.writeFile(targetPath, "remove"))
const target = yield* (yield* LocationMutation.Service).resolve({ path: "remove.txt" })
const result = yield* (yield* FileMutation.Service).remove({ target })
expect(result).toEqual({
operation: "remove",
target: target.canonical,
resource: "remove.txt",
existed: true,
})
expect(
yield* Effect.promise(() =>
fs.stat(targetPath).then(
() => true,
() => false,
),
),
).toBe(false)
}).pipe(provide(directory)),
),
)
it.live("writes an explicitly resolved external target", () => it.live("writes an explicitly resolved external target", () =>
withTmp((directory) => withTmp((directory) =>
withTmp((outside) => withTmp((outside) =>
@@ -171,49 +109,6 @@ describe("FileMutation", () => {
), ),
) )
it.live("removes an explicitly resolved external target", () =>
withTmp((directory) =>
withTmp((outside) =>
Effect.gen(function* () {
const targetPath = path.join(outside, "external.txt")
yield* Effect.promise(() => fs.writeFile(targetPath, "external"))
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
const result = yield* (yield* FileMutation.Service).remove({ target })
expect(result).toEqual({
operation: "remove",
target: target.canonical,
resource: target.resource,
existed: true,
})
expect(
yield* Effect.promise(() =>
fs.stat(targetPath).then(
() => true,
() => false,
),
),
).toBe(false)
}).pipe(provide(directory)),
),
),
)
it.live("reports a missing target as not removed without checking existence first", () =>
withTmp((directory) =>
Effect.gen(function* () {
const target = yield* (yield* LocationMutation.Service).resolve({ path: "missing.txt" })
expect(yield* (yield* FileMutation.Service).remove({ target })).toEqual({
operation: "remove",
target: target.canonical,
resource: "missing.txt",
existed: false,
})
}).pipe(provide(directory)),
),
)
it.live("serializes concurrent writes to the same canonical target", () => it.live("serializes concurrent writes to the same canonical target", () =>
withTmp((directory) => withTmp((directory) =>
Effect.gen(function* () { Effect.gen(function* () {
@@ -257,63 +152,6 @@ describe("FileMutation", () => {
), ),
) )
it.live("allows only one concurrent conditional write based on the same bytes", () =>
withTmp((directory) =>
Effect.gen(function* () {
const targetPath = path.join(directory, "shared.txt")
yield* Effect.promise(() => fs.writeFile(targetPath, "initial"))
const firstStarted = yield* Deferred.make<void>()
const releaseFirst = yield* Deferred.make<void>()
let writes = 0
const filesystem = instrumentWrites((write) =>
Effect.gen(function* () {
writes++
if (writes === 1) {
yield* Deferred.succeed(firstStarted, undefined)
yield* Deferred.await(releaseFirst)
}
yield* write
}),
)
yield* Effect.gen(function* () {
const mutation = yield* LocationMutation.Service
const files = yield* FileMutation.Service
const target = yield* mutation.resolve({ path: "shared.txt" })
const expected = new TextEncoder().encode("initial")
const first = yield* files.writeIfUnchanged({ target, expected, content: "first" }).pipe(Effect.forkChild)
yield* Deferred.await(firstStarted)
const second = yield* files
.writeIfUnchanged({ target, expected, content: "second" })
.pipe(Effect.flip, Effect.forkChild)
yield* Deferred.succeed(releaseFirst, undefined)
yield* Fiber.join(first)
expect(yield* Fiber.join(second)).toMatchObject({ _tag: "FileMutation.StaleContentError" })
expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("first")
expect(writes).toBe(1)
}).pipe(provide(directory, filesystem))
}),
),
)
it.live("rejects a conditional write when target content is already stale", () =>
withTmp((directory) =>
Effect.gen(function* () {
const targetPath = path.join(directory, "stale.txt")
yield* Effect.promise(() => fs.writeFile(targetPath, "current"))
const target = yield* (yield* LocationMutation.Service).resolve({ path: "stale.txt" })
expect(
yield* (yield* FileMutation.Service)
.writeIfUnchanged({ target, expected: new TextEncoder().encode("older"), content: "replacement" })
.pipe(Effect.flip),
).toMatchObject({ _tag: "FileMutation.StaleContentError", path: target.canonical })
expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("current")
}).pipe(provide(directory)),
),
)
it.live("allows distinct canonical targets to proceed independently", () => it.live("allows distinct canonical targets to proceed independently", () =>
withTmp((directory) => withTmp((directory) =>
Effect.gen(function* () { Effect.gen(function* () {
@@ -3,14 +3,6 @@ import { Ignore } from "@opencode-ai/core/filesystem/ignore"
// @ts-ignore // @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper" import { createWrapper } from "@parcel/watcher/wrapper"
test("match nested and non-nested", () => {
expect(Ignore.match("node_modules/index.js")).toBe(true)
expect(Ignore.match("node_modules")).toBe(true)
expect(Ignore.match("node_modules/")).toBe(true)
expect(Ignore.match("node_modules/bar")).toBe(true)
expect(Ignore.match("node_modules/bar/")).toBe(true)
})
test("parcel patterns ignore built-in folders at any depth", async () => { test("parcel patterns ignore built-in folders at any depth", async () => {
let ignoreGlobs: string[] = [] let ignoreGlobs: string[] = []
const watcher = createWrapper({ const watcher = createWrapper({
+52 -37
View File
@@ -1,44 +1,59 @@
import { describe, expect } from "bun:test" import { describe, expect, test } from "bun:test"
import fs from "fs/promises" import os from "os"
import path from "path" import path from "path"
import { Effect } from "effect" import { Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { Protected } from "@opencode-ai/core/filesystem/protected"
import { FileSystemSearch } from "@opencode-ai/core/filesystem/search"
import { Location } from "@opencode-ai/core/location"
import { Ripgrep } from "@opencode-ai/core/ripgrep" import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema" import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { tmpdir } from "../fixture/tmpdir" import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
const it = testEffect(LayerNode.compile(Ripgrep.node)) describe("FileSystemSearch", () => {
test("bounds a home scan even when home is detected as a repository", async () => {
let observed: Ripgrep.FindInput | undefined
const home = AbsolutePath.make(os.homedir())
const layer = AppNodeBuilder.build(FileSystemSearch.node, [
[
Location.node,
Layer.succeed(
Location.Service,
Location.Service.of(
location({ directory: home }, { vcs: { type: "git", store: AbsolutePath.make(path.join(home, ".git")) } }),
),
),
],
[
Ripgrep.node,
Layer.succeed(
Ripgrep.Service,
Ripgrep.Service.of({
find: (input) =>
Effect.gen(function* () {
observed = input
if (input.onEntry)
yield* input.onEntry(FileSystem.Entry.make({ path: RelativePath.make("src/index.ts"), type: "file" }))
return []
}),
glob: () => Effect.succeed([]),
grep: () => Effect.succeed([]),
}),
),
],
])
const withTmp = <A, E, R>(f: (directory: AbsolutePath) => Effect.Effect<A, E, R>) => await Effect.runPromise(
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
).pipe(Effect.flatMap((tmp) => f(AbsolutePath.make(tmp.path))))
describe("Ripgrep", () => {
it.live("globs files as an array", () =>
withTmp((cwd) =>
Effect.gen(function* () { Effect.gen(function* () {
yield* Effect.promise(() => fs.mkdir(path.join(cwd, "src"))) const search = yield* FileSystemSearch.Service
yield* Effect.promise(() => fs.writeFile(path.join(cwd, "src", "match.ts"), "needle\n")) yield* Effect.sleep("10 millis")
const result = yield* (yield* Ripgrep.Service).glob({ cwd, pattern: "**/*.ts", limit: 10 }) expect(observed?.limit).toBe(100_000)
expect(result.map((item) => item.path)).toEqual([RelativePath.make("src/match.ts")]) expect(observed?.exclude).toEqual([...Protected.names()].map((name) => `${name}/**`))
}), expect((yield* search.find({ query: "src", type: "directory" }))[0]?.path).toBe(
), RelativePath.make(`src${path.sep}`),
) )
}).pipe(Effect.provide(layer), Effect.scoped),
it.live("greps files with include filtering", () => )
withTmp((cwd) => })
Effect.gen(function* () {
yield* Effect.promise(() => fs.mkdir(path.join(cwd, "src")))
yield* Effect.promise(() => fs.writeFile(path.join(cwd, "src", "match.ts"), "needle\n"))
yield* Effect.promise(() => fs.writeFile(path.join(cwd, "src", "skip.txt"), "needle\n"))
const result = yield* (yield* Ripgrep.Service).grep({ cwd, pattern: "needle", include: "*.ts", limit: 10 })
expect(result).toHaveLength(1)
expect(result[0]?.entry.path).toBe(RelativePath.make("src/match.ts"))
expect(result[0]?.submatches[0]?.text).toBe("needle")
}),
),
)
}) })
+34 -57
View File
@@ -56,52 +56,22 @@ function withTemp<A, E, R>(body: (directory: string) => Effect.Effect<A, E, R>)
} }
describe("Formatter", () => { describe("Formatter", () => {
it.live("status() returns empty list when no formatters are configured", () => it.live("does not run formatters marked as disabled in config", () =>
withTemp((directory) => withTemp((directory) =>
Formatter.Service.use((formatter) => formatter.status()).pipe(Effect.provide(formatterLayer(directory))), Effect.gen(function* () {
), const file = path.join(directory, "test.disabled")
) expect(yield* Formatter.Service.use((formatter) => formatter.file(file))).toBe(false)
}).pipe(
it.live("status() returns built-in formatters when formatter is true", () => Effect.provide(
withTemp((directory) => formatterLayer(directory, {
Formatter.Service.use((formatter) => disabled: {
Effect.gen(function* () { disabled: true,
const statuses = yield* formatter.status() command: [process.execPath, "-e", "process.exit(0)", "$FILE"],
const gofmt = statuses.find((item) => item.name === "gofmt") extensions: [".disabled"],
expect(gofmt).toBeDefined() },
expect(gofmt?.extensions).toContain(".go") }),
}), ),
).pipe(Effect.provide(formatterLayer(directory, true))), ),
),
)
it.live("status() keeps built-in formatters when config object is provided", () =>
withTemp((directory) =>
Formatter.Service.use((formatter) =>
Effect.gen(function* () {
const statuses = yield* formatter.status()
expect(statuses.find((item) => item.name === "gofmt")?.extensions).toContain(".go")
expect(statuses.find((item) => item.name === "mix")).toBeDefined()
}),
).pipe(Effect.provide(formatterLayer(directory, { gofmt: {} }))),
),
)
it.live("status() excludes formatters marked as disabled in config", () =>
withTemp((directory) =>
Formatter.Service.use((formatter) =>
Effect.gen(function* () {
const statuses = yield* formatter.status()
expect(statuses.find((item) => item.name === "gofmt")).toBeUndefined()
expect(statuses.find((item) => item.name === "mix")).toBeDefined()
}),
).pipe(Effect.provide(formatterLayer(directory, { gofmt: { disabled: true } }))),
),
)
it.live("service initializes without error", () =>
withTemp((directory) =>
Formatter.Service.use((formatter) => formatter.init()).pipe(Effect.provide(formatterLayer(directory))),
), ),
) )
@@ -115,22 +85,29 @@ describe("Formatter", () => {
), ),
) )
it.live("status() initializes formatter state per directory", () => it.live("loads formatter state per directory", () =>
Effect.acquireUseRelease( withTemp((off) =>
Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), withTemp((on) =>
([off, on]) =>
Effect.gen(function* () { Effect.gen(function* () {
const disabled = yield* Formatter.Service.use((formatter) => formatter.status()).pipe( const offFile = path.join(off, "test.isolated")
Effect.provide(formatterLayer(off.path, false)), const onFile = path.join(on, "test.isolated")
const disabled = yield* Formatter.Service.use((formatter) => formatter.file(offFile)).pipe(
Effect.provide(formatterLayer(off, false)),
) )
const enabled = yield* Formatter.Service.use((formatter) => formatter.status()).pipe( const enabled = yield* Formatter.Service.use((formatter) => formatter.file(onFile)).pipe(
Effect.provide(formatterLayer(on.path, true)), Effect.provide(
formatterLayer(on, {
isolated: {
command: [process.execPath, "-e", "process.exit(0)", "$FILE"],
extensions: [".isolated"],
},
}),
),
) )
expect(disabled).toEqual([]) expect(disabled).toBe(false)
expect(enabled.find((item) => item.name === "gofmt")).toBeDefined() expect(enabled).toBe(true)
}), }),
(directories) => ),
Effect.promise(() => Promise.all(directories.map((tmp) => tmp[Symbol.asyncDispose]())).then(() => undefined)),
), ),
) )
-3
View File
@@ -185,9 +185,6 @@ describe("Git trees", () => {
]) ])
const files = new Map([[RelativePath.make("scope/tracked.txt"), before]]) const files = new Map([[RelativePath.make("scope/tracked.txt"), before]])
const preview = yield* git.tree.preview({ repository, current: after, files, context: 1 })
expect(preview).toHaveLength(1)
expect(preview[0]?.file).toBe(RelativePath.make("scope/tracked.txt"))
yield* git.tree.restore({ repository, files }) yield* git.tree.restore({ repository, files })
expect(yield* read(path.join(root.path, "scope", "tracked.txt"))).toBe("one\n") expect(yield* read(path.join(root.path, "scope", "tracked.txt"))).toBe("one\n")
expect(yield* read(path.join(root.path, "scope", "added.txt"))).toBe("added\n") expect(yield* read(path.join(root.path, "scope", "added.txt"))).toBe("added\n")
+55 -11
View File
@@ -14,7 +14,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
import { InstructionState } from "@opencode-ai/core/session/instruction-state" import { InstructionState } from "@opencode-ai/core/session/instruction-state"
import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionSchema } from "@opencode-ai/core/session/schema" import { SessionSchema } from "@opencode-ai/core/session/schema"
import { InstructionBlobTable, InstructionStateTable, SessionTable } from "@opencode-ai/core/session/sql" import { InstructionBlobTable, InstructionStateTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql"
import { testEffect } from "./lib/effect" import { testEffect } from "./lib/effect"
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SessionProjector.node]))) const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SessionProjector.node])))
@@ -105,6 +105,7 @@ describe("InstructionState", () => {
expect(observation).toEqual({ expect(observation).toEqual({
sessionID, sessionID,
initial: true, initial: true,
previous: {},
current: { current: {
"test/first": Instructions.hash("first"), "test/first": Instructions.hash("first"),
"test/second": Instructions.hash("second"), "test/second": Instructions.hash("second"),
@@ -156,7 +157,7 @@ describe("InstructionState", () => {
const initial = yield* InstructionState.observe(db, instructions, sessionID) const initial = yield* InstructionState.observe(db, instructions, sessionID)
expect(reads).toBe(2) expect(reads).toBe(2)
yield* InstructionState.commit(db, events, initial) yield* InstructionState.commit(db, events, instructions, initial)
expect(reads).toBe(2) expect(reads).toBe(2)
current = "changed" current = "changed"
@@ -166,6 +167,10 @@ describe("InstructionState", () => {
expect(changed).toMatchObject({ expect(changed).toMatchObject({
sessionID, sessionID,
initial: false, initial: false,
previous: {
"test/current": Instructions.hash("initial"),
"test/retired": Instructions.hash("retired"),
},
current: { "test/current": Instructions.hash("changed") }, current: { "test/current": Instructions.hash("changed") },
delta: { delta: {
"test/current": Instructions.hash("changed"), "test/current": Instructions.hash("changed"),
@@ -173,7 +178,7 @@ describe("InstructionState", () => {
}, },
blobs: { [Instructions.hash("changed")]: "changed" }, blobs: { [Instructions.hash("changed")]: "changed" },
}) })
yield* InstructionState.commit(db, events, changed) yield* InstructionState.commit(db, events, instructions, changed)
expect(reads).toBe(4) expect(reads).toBe(4)
yield* unsubscribe yield* unsubscribe
@@ -190,6 +195,11 @@ describe("InstructionState", () => {
"test/retired": "removed", "test/retired": "removed",
}, },
]) ])
// The chronological update text is frozen into the event; the baseline has none.
expect((yield* instructionEvents(db, sessionID)).map((event) => event.data.text)).toEqual([
undefined,
"changed\n\nRemoved retired",
])
expect(yield* db.select().from(InstructionStateTable).get().pipe(Effect.orDie)).toMatchObject({ expect(yield* db.select().from(InstructionStateTable).get().pipe(Effect.orDie)).toMatchObject({
initial_values: { initial_values: {
"test/current": Instructions.hash("initial"), "test/current": Instructions.hash("initial"),
@@ -222,18 +232,19 @@ describe("InstructionState", () => {
expect(observation).toEqual({ expect(observation).toEqual({
sessionID, sessionID,
initial: false, initial: false,
previous: { "test/context": Instructions.hash("unchanged") },
current: { "test/context": Instructions.hash("unchanged") }, current: { "test/context": Instructions.hash("unchanged") },
delta: {}, delta: {},
blobs: {}, blobs: {},
}) })
yield* InstructionState.commit(db, events, observation) yield* InstructionState.commit(db, events, instructions, observation)
expect(yield* instructionEvents(db, sessionID)).toEqual(beforeEvents) expect(yield* instructionEvents(db, sessionID)).toEqual(beforeEvents)
expect(yield* db.select().from(InstructionBlobTable).all().pipe(Effect.orDie)).toEqual(beforeBlobs) expect(yield* db.select().from(InstructionBlobTable).all().pipe(Effect.orDie)).toEqual(beforeBlobs)
}), }),
) )
it.effect("assembles a fresh private update without repairing a missing cache", () => it.effect("treats a missing state row as a fresh baseline without repairing it", () =>
Effect.gen(function* () { Effect.gen(function* () {
const sessionID = SessionSchema.ID.make("ses_instruction_generate") const sessionID = SessionSchema.ID.make("ses_instruction_generate")
const { db, events } = yield* setup(sessionID) const { db, events } = yield* setup(sessionID)
@@ -254,7 +265,7 @@ describe("InstructionState", () => {
const assembled = yield* preview(db, sessionID, instructions) const assembled = yield* preview(db, sessionID, instructions)
expect(assembled).toEqual({ initial: "Initial context", updates: [], update: "Changed context" }) expect(assembled).toEqual({ initial: "Changed context", update: "" })
expect(yield* instructionEvents(db, sessionID)).toEqual(beforeEvents) expect(yield* instructionEvents(db, sessionID)).toEqual(beforeEvents)
expect(yield* db.select().from(InstructionBlobTable).all().pipe(Effect.orDie)).toEqual(beforeBlobs) expect(yield* db.select().from(InstructionBlobTable).all().pipe(Effect.orDie)).toEqual(beforeBlobs)
expect( expect(
@@ -268,7 +279,7 @@ describe("InstructionState", () => {
}), }),
) )
it.effect("reads through a stale cache without repairing it", () => it.effect("trusts the projected state without consulting durable events", () =>
Effect.gen(function* () { Effect.gen(function* () {
const sessionID = SessionSchema.ID.make("ses_instruction_generate_stale") const sessionID = SessionSchema.ID.make("ses_instruction_generate_stale")
const { db, events } = yield* setup(sessionID) const { db, events } = yield* setup(sessionID)
@@ -280,6 +291,7 @@ describe("InstructionState", () => {
yield* InstructionState.prepare(db, events, instructions, sessionID) yield* InstructionState.prepare(db, events, instructions, sessionID)
value = "Committed update" value = "Committed update"
yield* InstructionState.prepare(db, events, instructions, sessionID) yield* InstructionState.prepare(db, events, instructions, sessionID)
// Tamper with the projected state; the authoritative row wins over event history.
yield* db yield* db
.update(InstructionStateTable) .update(InstructionStateTable)
.set({ through_seq: 0, current_values: { "test/context": Instructions.hash("Initial context") } }) .set({ through_seq: 0, current_values: { "test/context": Instructions.hash("Initial context") } })
@@ -294,7 +306,6 @@ describe("InstructionState", () => {
const assembled = yield* preview(db, sessionID, instructions) const assembled = yield* preview(db, sessionID, instructions)
expect(assembled.initial).toBe("Initial context") expect(assembled.initial).toBe("Initial context")
expect(assembled.updates.map((entry) => entry.message.text)).toEqual(["Committed update"])
expect(assembled.update).toBe("Private update") expect(assembled.update).toBe("Private update")
expect(yield* instructionEvents(db, sessionID)).toEqual(beforeEvents) expect(yield* instructionEvents(db, sessionID)).toEqual(beforeEvents)
expect(yield* db.select().from(InstructionBlobTable).all().pipe(Effect.orDie)).toEqual(beforeBlobs) expect(yield* db.select().from(InstructionBlobTable).all().pipe(Effect.orDie)).toEqual(beforeBlobs)
@@ -302,6 +313,41 @@ describe("InstructionState", () => {
}), }),
) )
it.effect("persists chronological updates as system messages", () =>
Effect.gen(function* () {
const sessionID = SessionSchema.ID.make("ses_instruction_messages")
const { db, events } = yield* setup(sessionID)
let value = "Initial context"
const instructions = source(
"test/context",
Effect.sync(() => value),
)
const messages = () =>
db
.select()
.from(SessionMessageTable)
.where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "system")))
.orderBy(asc(SessionMessageTable.seq))
.all()
.pipe(Effect.orDie)
// The initial baseline is not chronological history and produces no message.
yield* InstructionState.prepare(db, events, instructions, sessionID)
expect(yield* messages()).toEqual([])
value = "Changed context"
yield* InstructionState.prepare(db, events, instructions, sessionID)
const rows = yield* messages()
expect(rows).toHaveLength(1)
expect(rows[0]?.data).toMatchObject({ text: "Changed context" })
expect(rows.map((row) => row.seq)).toEqual([(yield* instructionEvents(db, sessionID)).at(-1)!.seq])
// A no-op observation adds nothing.
yield* InstructionState.prepare(db, events, instructions, sessionID)
expect(yield* messages()).toHaveLength(1)
}),
)
it.effect("assembles initial instructions without persisting a baseline", () => it.effect("assembles initial instructions without persisting a baseline", () =>
Effect.gen(function* () { Effect.gen(function* () {
const sessionID = SessionSchema.ID.make("ses_instruction_generate_initial") const sessionID = SessionSchema.ID.make("ses_instruction_generate_initial")
@@ -310,7 +356,6 @@ describe("InstructionState", () => {
expect(yield* preview(db, sessionID, instructions)).toEqual({ expect(yield* preview(db, sessionID, instructions)).toEqual({
initial: "Initial context", initial: "Initial context",
updates: [],
update: "", update: "",
}) })
expect(yield* instructionEvents(db, sessionID)).toEqual([]) expect(yield* instructionEvents(db, sessionID)).toEqual([])
@@ -336,7 +381,6 @@ describe("InstructionState", () => {
expect(yield* preview(db, sessionID, instructions)).toEqual({ expect(yield* preview(db, sessionID, instructions)).toEqual({
initial: "Committed context", initial: "Committed context",
updates: [],
update: "", update: "",
}) })
expect(yield* instructionEvents(db, sessionID)).toEqual(beforeEvents) expect(yield* instructionEvents(db, sessionID)).toEqual(beforeEvents)
@@ -388,7 +432,7 @@ describe("InstructionState", () => {
for (const next of ["initial", "changed", "changed", Instructions.removed] as const) { for (const next of ["initial", "changed", "changed", Instructions.removed] as const) {
value = next value = next
yield* InstructionState.observe(db, observedInstructions, observedSessionID).pipe( yield* InstructionState.observe(db, observedInstructions, observedSessionID).pipe(
Effect.flatMap((observation) => InstructionState.commit(db, events, observation)), Effect.flatMap((observation) => InstructionState.commit(db, events, observedInstructions, observation)),
) )
yield* InstructionState.prepare(db, events, preparedInstructions, preparedSessionID) yield* InstructionState.prepare(db, events, preparedInstructions, preparedSessionID)
} }
-1
View File
@@ -21,7 +21,6 @@ const projectLayer = Layer.succeed(
canonical: AbsolutePath.make("/main/repo"), canonical: AbsolutePath.make("/main/repo"),
vcs: { type: "git", store: AbsolutePath.make("/repo/.git") }, vcs: { type: "git", store: AbsolutePath.make("/repo/.git") },
}), }),
commit: () => Effect.void,
}), }),
) )
const it = testEffect(AppNodeBuilder.build(Location.boundNode(ref), [[Project.node, projectLayer]])) const it = testEffect(AppNodeBuilder.build(Location.boundNode(ref), [[Project.node, projectLayer]]))
+1 -11
View File
@@ -83,20 +83,10 @@ describe("ProjectCopy", () => {
}), }),
) )
it.effect("rejects duplicate strategies and reports unavailable ids", () => it.effect("reports unavailable strategy ids", () =>
Effect.gen(function* () { Effect.gen(function* () {
const input = yield* setup() const input = yield* setup()
const copy = yield* ProjectCopy.Service const copy = yield* ProjectCopy.Service
const strategy: ProjectCopy.Strategy = {
id: ProjectCopy.StrategyID.make("test/duplicate"),
create: () => Effect.die("unused"),
remove: () => Effect.die("unused"),
list: () => Effect.succeed([]),
}
yield* copy.register(strategy)
expect(yield* copy.register(strategy).pipe(Effect.flip)).toBeInstanceOf(ProjectCopy.DuplicateStrategyError)
const unavailable = ProjectCopy.StrategyID.make("acme/missing") const unavailable = ProjectCopy.StrategyID.make("acme/missing")
const error = yield* copy const error = yield* copy
.create({ .create({
+4 -8
View File
@@ -3,7 +3,6 @@ import { Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { PtyID } from "@opencode-ai/core/pty/schema" import { PtyID } from "@opencode-ai/core/pty/schema"
import { PtyTicket } from "@opencode-ai/core/pty/ticket" import { PtyTicket } from "@opencode-ai/core/pty/ticket"
import { Workspace } from "@opencode-ai/core/workspace"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
const it = testEffect(LayerNode.compile(PtyTicket.node)) const it = testEffect(LayerNode.compile(PtyTicket.node))
@@ -46,17 +45,14 @@ describe("PTY websocket tickets", () => {
}), }),
) )
it.live("rejects tickets scoped to a different workspace", () => it.live("rejects tickets scoped to a different pty", () =>
Effect.gen(function* () { Effect.gen(function* () {
const tickets = yield* PtyTicket.Service const tickets = yield* PtyTicket.Service
const ptyID = PtyID.ascending() const ptyID = PtyID.ascending()
const workspaceID = Workspace.ID.ascending() const issued = yield* tickets.issue({ ptyID })
const issued = yield* tickets.issue({ ptyID, workspaceID })
expect(yield* tickets.consume({ ptyID, workspaceID: Workspace.ID.ascending(), ticket: issued.ticket })).toBe( expect(yield* tickets.consume({ ptyID: PtyID.ascending(), ticket: issued.ticket })).toBe(false)
false, expect(yield* tickets.consume({ ptyID, ticket: issued.ticket })).toBe(true)
)
expect(yield* tickets.consume({ ptyID, workspaceID, ticket: issued.ticket })).toBe(true)
}), }),
) )
}) })
+1 -4
View File
@@ -101,13 +101,10 @@ describe("RepositoryCache", () => {
), ),
) )
it.live("returns typed validation and clone failures", () => it.live("returns typed branch validation and clone failures", () =>
withRemote((fixture) => withRemote((fixture) =>
Effect.gen(function* () { Effect.gen(function* () {
const cache = yield* RepositoryCache.Service const cache = yield* RepositoryCache.Service
const invalidRepository = yield* Effect.flip(RepositoryCache.parseRemote("not-a-repo"))
expect(invalidRepository).toBeInstanceOf(RepositoryCache.InvalidRepositoryError)
const invalidBranch = yield* Effect.flip(cache.ensure({ reference: fixture.reference, branch: "../unsafe" })) const invalidBranch = yield* Effect.flip(cache.ensure({ reference: fixture.reference, branch: "../unsafe" }))
expect(invalidBranch).toBeInstanceOf(RepositoryCache.InvalidBranchError) expect(invalidBranch).toBeInstanceOf(RepositoryCache.InvalidBranchError)
+61
View File
@@ -11,6 +11,44 @@ import { testEffect } from "./lib/effect"
const it = testEffect(LayerNode.compile(Ripgrep.node)) const it = testEffect(LayerNode.compile(Ripgrep.node))
describe("Ripgrep", () => { describe("Ripgrep", () => {
it.live("globs files as an array", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) =>
Effect.gen(function* () {
yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "src")))
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "match.ts"), "needle\n"))
const result = yield* (yield* Ripgrep.Service).glob({ cwd: tmp.path, pattern: "**/*.ts", limit: 10 })
expect(result.map((item) => item.path)).toEqual([RelativePath.make("src/match.ts")])
}),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
it.live("greps files with include filtering", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) =>
Effect.gen(function* () {
yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "src")))
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "match.ts"), "needle\n"))
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "skip.txt"), "needle\n"))
const result = yield* (yield* Ripgrep.Service).grep({
cwd: tmp.path,
pattern: "needle",
include: "*.ts",
limit: 10,
})
expect(result).toHaveLength(1)
expect(result[0]?.entry.path).toBe(RelativePath.make("src/match.ts"))
expect(result[0]?.submatches[0]?.text).toBe("needle")
}),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
it.live("keeps ignored files out of catch-all find results", () => it.live("keeps ignored files out of catch-all find results", () =>
Effect.acquireUseRelease( Effect.acquireUseRelease(
Effect.promise(() => tmpdir()), Effect.promise(() => tmpdir()),
@@ -63,6 +101,29 @@ describe("Ripgrep", () => {
), ),
) )
it.live("excludes protected directory trees from catch-all find results", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) =>
Effect.gen(function* () {
yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "Pictures")))
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "Pictures", "private.jpg"), "private\n"))
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "visible.txt"), "visible\n"))
const files = yield* (yield* Ripgrep.Service).find({
cwd: tmp.path,
pattern: "*",
limit: 10,
exclude: ["Pictures/**"],
})
expect(files.map((item) => item.path)).toContain(RelativePath.make("visible.txt"))
expect(files.map((item) => item.path)).not.toContain(RelativePath.make("Pictures/private.jpg"))
}),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
it.live("returns a bounded preview for matches on oversized lines", () => it.live("returns a bounded preview for matches on oversized lines", () =>
Effect.acquireUseRelease( Effect.acquireUseRelease(
Effect.promise(() => tmpdir()), Effect.promise(() => tmpdir()),
@@ -36,7 +36,6 @@ const projects = Layer.succeed(
list: () => Effect.succeed([]), list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }), resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }),
directories: () => Effect.succeed([]), directories: () => Effect.succeed([]),
commit: () => Effect.void,
}), }),
) )
let requests: LLMRequest[] = [] let requests: LLMRequest[] = []
+4 -11
View File
@@ -24,7 +24,6 @@ import { SessionPending } from "@opencode-ai/core/session/pending"
import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionTable } from "@opencode-ai/core/session/sql"
import { SessionStore } from "@opencode-ai/core/session/store" import { SessionStore } from "@opencode-ai/core/session/store"
import { Workspace } from "@opencode-ai/core/workspace"
import { testEffect } from "./lib/effect" import { testEffect } from "./lib/effect"
import { tmpdir } from "./fixture/tmpdir" import { tmpdir } from "./fixture/tmpdir"
@@ -34,7 +33,6 @@ const projects = Layer.succeed(
list: () => Effect.succeed([]), list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }), resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }),
directories: () => Effect.succeed([]), directories: () => Effect.succeed([]),
commit: () => Effect.void,
}), }),
) )
const it = testEffect( const it = testEffect(
@@ -120,7 +118,6 @@ describe("Session.create", () => {
it.effect("stores supplied immutable create attributes", () => it.effect("stores supplied immutable create attributes", () =>
Effect.gen(function* () { Effect.gen(function* () {
const session = yield* Session.Service const session = yield* Session.Service
const workspaceID = Workspace.ID.make("wrk_test")
const model = Model.Ref.make({ const model = Model.Ref.make({
id: Model.ID.make("sonnet"), id: Model.ID.make("sonnet"),
providerID: Provider.ID.anthropic, providerID: Provider.ID.anthropic,
@@ -129,11 +126,11 @@ describe("Session.create", () => {
expect( expect(
yield* session.create({ yield* session.create({
location: Location.Ref.make({ directory: location.directory, workspaceID }), location: Location.Ref.make({ directory: location.directory }),
agent: Agent.ID.make("build"), agent: Agent.ID.make("build"),
model, model,
}), }),
).toMatchObject({ location: { directory: location.directory, workspaceID }, agent: "build", model }) ).toMatchObject({ location: { directory: location.directory }, agent: "build", model })
}), }),
) )
@@ -284,13 +281,9 @@ describe("Session.create", () => {
}) })
expect(yield* SessionPending.find(db, forkContext[0].id)).toBeUndefined() expect(yield* SessionPending.find(db, forkContext[0].id)).toBeUndefined()
expect(yield* SessionPending.find(db, forkContext[1].id)).toBeUndefined() expect(yield* SessionPending.find(db, forkContext[1].id)).toBeUndefined()
// Fork-copied messages have no admitted event in the fork aggregate, so
// reusing their IDs as prompt IDs is conflicting reuse, not a retry.
expect( expect(
yield* session yield* session.prompt({ id: forkContext[0].id, sessionID: forked.id, text: "First", resume: false }),
.prompt({ id: forkContext[0].id, sessionID: forked.id, text: "First", resume: false }) ).toMatchObject({ id: forkContext[0].id, type: "user", data: { text: "First" } })
.pipe(Effect.flip),
).toMatchObject({ _tag: "Session.PromptConflictError", messageID: forkContext[0].id })
yield* session.prompt({ yield* session.prompt({
sessionID: parent.id, sessionID: parent.id,
@@ -56,7 +56,6 @@ const projects = Layer.succeed(
list: () => Effect.succeed([]), list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }), resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }),
directories: () => Effect.succeed([]), directories: () => Effect.succeed([]),
commit: () => Effect.void,
}), }),
) )
const permission = Layer.succeed( const permission = Layer.succeed(
+1 -4
View File
@@ -23,7 +23,6 @@ const projects = Layer.succeed(
list: () => Effect.succeed([]), list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }), resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }),
directories: () => Effect.succeed([]), directories: () => Effect.succeed([]),
commit: () => Effect.void,
}), }),
) )
const it = testEffect( const it = testEffect(
@@ -41,17 +40,15 @@ describe("Session.log", () => {
it.effect("replays public session events and marks synced at the aggregate watermark", () => it.effect("replays public session events and marks synced at the aggregate watermark", () =>
Effect.gen(function* () { Effect.gen(function* () {
const session = yield* Session.Service const session = yield* Session.Service
const bus = yield* Bus.Service
const created = yield* session.create({ location }) const created = yield* session.create({ location })
yield* session.rename({ sessionID: created.id, title: "session.renamed" }) yield* session.rename({ sessionID: created.id, title: "session.renamed" })
const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id }))) const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id })))
const watermark = (yield* bus.sequences([created.id])).get(created.id)
// Session creation commits a non-public durable event, so the marker's // Session creation commits a non-public durable event, so the marker's
// seq covers more of the aggregate than the public events emitted. // seq covers more of the aggregate than the public events emitted.
expect(items.map((item) => item.type)).toEqual(["session.renamed", "log.synced"]) expect(items.map((item) => item.type)).toEqual(["session.renamed", "log.synced"])
expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID: created.id, seq: watermark }) expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID: created.id, seq: Event.Seq.make(1) })
}), }),
) )
@@ -17,7 +17,6 @@ import { Session } from "@opencode-ai/core/session"
import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionMessage } from "@opencode-ai/core/session/message"
import { Money } from "@opencode-ai/schema/money" import { Money } from "@opencode-ai/schema/money"
import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater"
import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionExecution } from "@opencode-ai/core/session/execution"
import { fromRow } from "@opencode-ai/core/session/info" import { fromRow } from "@opencode-ai/core/session/info"
@@ -126,34 +125,6 @@ describe("SessionProjector", () => {
}), }),
) )
it.effect("folds live compaction deltas into running memory state", () =>
Effect.gen(function* () {
const state = {
messages: [
SessionMessage.CompactionRunning.make({
id: SessionMessage.ID.make("msg_compaction"),
type: "compaction",
status: "running",
reason: "manual",
summary: "partial ",
recent: "recent",
time: { created },
}),
],
}
yield* SessionMessageUpdater.update(
SessionMessageUpdater.memory(state),
SessionEvent.Compaction.Delta.make({
id: Event.ID.make("evt_delta"),
type: "session.compaction.delta",
created,
data: { sessionID, text: "summary" },
}),
)
expect(state.messages[0]).toMatchObject({ status: "running", summary: "partial summary", recent: "recent" })
}),
)
it.effect("projects staged, cleared, and committed reverts", () => it.effect("projects staged, cleared, and committed reverts", () =>
Effect.gen(function* () { Effect.gen(function* () {
const db = (yield* Database.Service).db const db = (yield* Database.Service).db
@@ -550,31 +521,6 @@ describe("SessionProjector", () => {
}), }),
) )
it.effect("does not revive a stale incomplete in-memory assistant projection", () =>
Effect.gen(function* () {
const stale = SessionMessage.Assistant.make({
id: SessionMessage.ID.make("msg_assistant_stale"),
type: "assistant",
agent: build,
model,
content: [],
time: { created },
})
const completed = SessionMessage.Assistant.make({
id: SessionMessage.ID.make("msg_assistant_completed"),
type: "assistant",
agent: build,
model,
content: [],
time: { created: DateTime.makeUnsafe(1), completed: DateTime.makeUnsafe(2) },
})
expect(
yield* SessionMessageUpdater.memory({ messages: [stale, completed] }).getCurrentAssistant(),
).toBeUndefined()
}),
)
it.effect("projects retry state and clears it at the next step or execution terminal", () => it.effect("projects retry state and clears it at the next step or execution terminal", () =>
Effect.gen(function* () { Effect.gen(function* () {
const { db } = yield* Database.Service const { db } = yield* Database.Service
+41
View File
@@ -553,6 +553,47 @@ describe("Session.prompt", () => {
}), }),
) )
it.effect("reconciles an exact retry from the promoted message without admission history", () =>
Effect.gen(function* () {
yield* setup
const session = yield* Session.Service
const bus = yield* Bus.Service
const { db } = yield* Database.Service
const input = { sessionID, id: messageID, text: "Fix the failing tests", resume: false }
const first = yield* session.prompt(input)
yield* SessionPending.promote(db, bus, sessionID, "steer")
yield* db
.delete(EventTable)
.where(eq(EventTable.aggregate_id, sessionID))
.run()
.pipe(Effect.orDie)
const retried = yield* session.prompt(input)
expect(retried).toMatchObject({ id: first.id, type: "user", data: { text: first.data.text } })
expect(yield* session.messages({ sessionID })).toMatchObject([
{ id: messageID, type: "user", text: "Fix the failing tests" },
])
}),
)
it.effect("ignores delivery when retrying a promoted message", () =>
Effect.gen(function* () {
yield* setup
const session = yield* Session.Service
const bus = yield* Bus.Service
const { db } = yield* Database.Service
const input = { sessionID, id: messageID, text: "Fix the failing tests", resume: false }
yield* session.prompt(input)
yield* SessionPending.promote(db, bus, sessionID, "steer")
const retried = yield* session.prompt({ ...input, delivery: "queue" })
expect(retried).toMatchObject({ id: messageID, type: "user", data: { text: input.text } })
expect(yield* admitted(messageID)).toBeUndefined()
}),
)
it.effect("wakes execution when an exact prompt retry recovers a committed message", () => it.effect("wakes execution when an exact prompt retry recovers a committed message", () =>
Effect.gen(function* () { Effect.gen(function* () {
yield* setup yield* setup
@@ -19,7 +19,6 @@ const projects = Layer.succeed(
list: () => Effect.succeed([]), list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }), resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }),
directories: () => Effect.succeed([]), directories: () => Effect.succeed([]),
commit: () => Effect.void,
}), }),
) )
const it = testEffect( const it = testEffect(
+22 -12
View File
@@ -1180,7 +1180,7 @@ describe("SessionRunnerLLM", () => {
}), }),
) )
it.effect("forks instruction values at the selected message instead of the parent's latest state", () => it.effect("seeds a fork with the parent's newest instruction values", () =>
Effect.gen(function* () { Effect.gen(function* () {
const session = yield* setup const session = yield* setup
yield* runPrompt(session, "First") yield* runPrompt(session, "First")
@@ -1197,14 +1197,16 @@ describe("SessionRunnerLLM", () => {
.where(eq(InstructionStateTable.session_id, forked.id)) .where(eq(InstructionStateTable.session_id, forked.id))
.get(), .get(),
).toMatchObject({ ).toMatchObject({
initial_values: { "test/context": Instructions.hash("Changed context") }, initial_values: { "test/context": Instructions.hash("Latest context") },
current_values: { "test/context": Instructions.hash("Changed context") }, current_values: { "test/context": Instructions.hash("Latest context") },
}) })
yield* session.prompt({ sessionID: forked.id, text: "Forked", resume: false }) yield* session.prompt({ sessionID: forked.id, text: "Forked", resume: false })
yield* session.resume(forked.id) yield* session.resume(forked.id)
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([defaultSystem, "Changed context"]) expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([defaultSystem, "Latest context"])
expect(systemTexts(requests.at(-1)!)).toContain("Latest context") // Copied history keeps the frozen chronological update; no new update is emitted.
expect(systemTexts(requests.at(-1)!)).toContain("Changed context")
expect(systemTexts(requests.at(-1)!)).not.toContain("Latest context")
const { db } = yield* Database.Service const { db } = yield* Database.Service
const bus = yield* Bus.Service const bus = yield* Bus.Service
@@ -1263,7 +1265,7 @@ describe("SessionRunnerLLM", () => {
}), }),
) )
it.effect("rebuilds a missing instruction cache without admitting another delta", () => it.effect("re-establishes a fresh baseline when instruction state is missing", () =>
Effect.gen(function* () { Effect.gen(function* () {
const session = yield* setup const session = yield* setup
const { db } = yield* Database.Service const { db } = yield* Database.Service
@@ -1277,13 +1279,15 @@ describe("SessionRunnerLLM", () => {
expect(requests).toHaveLength(1) expect(requests).toHaveLength(1)
expect(requests[0]?.system.map((part) => part.text)).toEqual([defaultSystem, "Initial context"]) expect(requests[0]?.system.map((part) => part.text)).toEqual([defaultSystem, "Initial context"])
expect(messageRoles(requests[0])).toEqual(["user", "user"]) expect(messageRoles(requests[0])).toEqual(["user", "user"])
// The projected row is authoritative: a missing row admits a fresh baseline
// instead of rebuilding from durable events.
expect( expect(
yield* db yield* db
.select({ id: EventTable.id }) .select({ data: EventTable.data })
.from(EventTable) .from(EventTable)
.where(eq(EventTable.type, "session.instructions.updated.2")) .where(eq(EventTable.type, "session.instructions.updated.2"))
.all(), .all(),
).toHaveLength(1) ).toHaveLength(2)
expect(yield* db.select().from(InstructionStateTable).get()).toMatchObject({ expect(yield* db.select().from(InstructionStateTable).get()).toMatchObject({
initial_values: { "test/context": Instructions.hash("Initial context") }, initial_values: { "test/context": Instructions.hash("Initial context") },
current_values: { "test/context": Instructions.hash("Initial context") }, current_values: { "test/context": Instructions.hash("Initial context") },
@@ -1310,7 +1314,10 @@ describe("SessionRunnerLLM", () => {
]) ])
expect(messageRoles(requests[1])).toEqual(["user", "system", "user"]) expect(messageRoles(requests[1])).toEqual(["user", "system", "user"])
expect(requests[1]?.messages.at(1)?.content).toEqual([{ type: "text", text: "Changed context" }]) expect(requests[1]?.messages.at(1)?.content).toEqual([{ type: "text", text: "Changed context" }])
expect(yield* session.messages({ sessionID })).toHaveLength(2) // The chronological update is a durable client-visible system message.
const messages = yield* session.messages({ sessionID })
expect(messages).toHaveLength(3)
expect(messages[1]).toMatchObject({ type: "system", text: "Changed context" })
const { db } = yield* Database.Service const { db } = yield* Database.Service
const updates = yield* db const updates = yield* db
.select({ data: EventTable.data }) .select({ data: EventTable.data })
@@ -1327,9 +1334,10 @@ describe("SessionRunnerLLM", () => {
expect(updates[1]?.data).toEqual({ expect(updates[1]?.data).toEqual({
sessionID, sessionID,
delta: { "test/context": Instructions.hash("Changed context") }, delta: { "test/context": Instructions.hash("Changed context") },
text: "Changed context",
}) })
yield* replaySessionProjection(sessionID) yield* replaySessionProjection(sessionID)
expect(yield* session.messages({ sessionID })).toHaveLength(2) expect(yield* session.messages({ sessionID })).toHaveLength(3)
}), }),
) )
@@ -1596,7 +1604,7 @@ describe("SessionRunnerLLM", () => {
expect(requests[1]?.messages.at(1)?.content).toEqual([ expect(requests[1]?.messages.at(1)?.content).toEqual([
{ type: "text", text: "System context source removed: test/context" }, { type: "text", text: "System context source removed: test/context" },
]) ])
expect(yield* session.messages({ sessionID })).toHaveLength(2) expect(yield* session.messages({ sessionID })).toHaveLength(3)
}), }),
) )
@@ -1708,12 +1716,14 @@ describe("SessionRunnerLLM", () => {
expect(requests[2]?.messages.filter((message) => message.role === "system")).toHaveLength(2) expect(requests[2]?.messages.filter((message) => message.role === "system")).toHaveLength(2)
expect((yield* session.context(sessionID)).map((message) => message.type)).toEqual([ expect((yield* session.context(sessionID)).map((message) => message.type)).toEqual([
"user", "user",
"system",
"user", "user",
"model-switched", "model-switched",
"system",
"user", "user",
]) ])
yield* replaySessionProjection(sessionID) yield* replaySessionProjection(sessionID)
expect(yield* session.messages({ sessionID })).toHaveLength(4) expect(yield* session.messages({ sessionID })).toHaveLength(6)
yield* runPrompt(session, "Fourth") yield* runPrompt(session, "Fourth")
}), }),
) )
-6
View File
@@ -34,12 +34,6 @@ describe("shell", () => {
expect(ShellSelect.login("C:/tools/pwsh.exe")).toBe(false) expect(ShellSelect.login("C:/tools/pwsh.exe")).toBe(false)
}) })
test("detects posix shells", () => {
expect(ShellSelect.posix("/bin/bash")).toBe(true)
expect(ShellSelect.posix("/bin/fish")).toBe(false)
expect(ShellSelect.posix("C:/tools/pwsh.exe")).toBe(false)
})
test("falls back when configured shell cannot be resolved", async () => { test("falls back when configured shell cannot be resolved", async () => {
await withShell(undefined, async () => { await withShell(undefined, async () => {
const preferred = ShellSelect.preferred() const preferred = ShellSelect.preferred()
-33
View File
@@ -117,9 +117,6 @@ describe("Snapshot", () => {
RelativePath.make("scope/tracked.txt"), RelativePath.make("scope/tracked.txt"),
]) ])
const plan = new Map([[RelativePath.make("scope/tracked.txt"), before]]) const plan = new Map([[RelativePath.make("scope/tracked.txt"), before]])
const preview = yield* snapshot.preview({ files: plan, context: 1 })
expect(preview).toHaveLength(1)
expect(preview[0]?.file).toBe(RelativePath.make("scope/tracked.txt"))
yield* snapshot.restore({ files: plan }) yield* snapshot.restore({ files: plan })
expect(yield* read(path.join(location, "tracked.txt"))).toBe("one\n") expect(yield* read(path.join(location, "tracked.txt"))).toBe("one\n")
expect(yield* read(path.join(location, "added.txt"))).toBe("added\n") expect(yield* read(path.join(location, "added.txt"))).toBe("added\n")
@@ -185,36 +182,6 @@ describe("Snapshot", () => {
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
), ),
) )
testEffect(Layer.empty).live("checks out a legacy revert snapshot without removing unrelated files", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) =>
Effect.gen(function* () {
const project = path.join(tmp.path, "project")
yield* Effect.promise(async () => {
await fs.mkdir(project)
await fs.writeFile(path.join(project, "tracked.txt"), "one\n")
await initGit(project)
})
yield* Effect.gen(function* () {
const snapshot = yield* Snapshot.Service
const before = yield* snapshot.capture()
expect(before).toBeDefined()
if (!before) return
yield* Effect.promise(async () => {
await fs.writeFile(path.join(project, "tracked.txt"), "two\n")
await fs.writeFile(path.join(project, "unrelated.txt"), "keep\n")
})
yield* snapshot.checkout(before)
expect(yield* read(path.join(project, "tracked.txt"))).toBe("one\n")
expect(yield* read(path.join(project, "unrelated.txt"))).toBe("keep\n")
}).pipe(Effect.provide(snapshotLayer(tmp.path, project)))
}),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
}) })
function snapshotLayer(data: string, directory: string) { function snapshotLayer(data: string, directory: string) {
-12
View File
@@ -5,15 +5,3 @@ interface ImportMetaEnv {
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv readonly env: ImportMetaEnv
} }
declare module "virtual:opencode-server" {
export namespace Server {
export const listen: typeof import("../../../opencode/dist/types/src/node").Server.listen
export type Listener = import("../../../opencode/dist/types/src/node").Server.Listener
}
export namespace Config {
export const get: typeof import("../../../opencode/dist/types/src/node").Config.get
export type Info = import("../../../opencode/dist/types/src/node").Config.Info
}
export const bootstrap: typeof import("../../../opencode/dist/types/src/node").bootstrap
}
-181
View File
@@ -1,32 +1,8 @@
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import { app, utilityProcess } from "electron"
import type { Details } from "electron"
import { getLogger } from "./logging" import { getLogger } from "./logging"
import { getUserShell, loadShellEnv } from "./shell-env" import { getUserShell, loadShellEnv } from "./shell-env"
import { getStore } from "./store" import { getStore } from "./store"
import { DEFAULT_SERVER_URL_KEY } from "./store-keys" import { DEFAULT_SERVER_URL_KEY } from "./store-keys"
export type HealthCheck = { wait: Promise<void> }
type SidecarMessage =
| { type: "ready" }
| { type: "stopped" }
| { type: "error"; error: { message: string; stack?: string } }
export type SidecarListener = { stop: () => Promise<void> }
const SIDECAR_SERVICE_NAME = "opencode server"
const SIDECAR_START_STALL_TIMEOUT = 60_000
const SIDECAR_STOP_TIMEOUT = 6_000
type SpawnLocalServerOptions = {
userDataPath: string
onStdout?: (message: string) => void
onStderr?: (message: string) => void
onExit?: (code: number) => void
}
export function getDefaultServerUrl(): string | null { export function getDefaultServerUrl(): string | null {
const value = getStore().get(DEFAULT_SERVER_URL_KEY) const value = getStore().get(DEFAULT_SERVER_URL_KEY)
return typeof value === "string" ? value : null return typeof value === "string" ? value : null
@@ -54,135 +30,6 @@ export function preferAppEnv(userDataPath: string) {
return shellEnv return shellEnv
} }
export async function spawnLocalServer(
hostname: string,
port: number,
password: string,
options: SpawnLocalServerOptions,
) {
const sidecar = join(dirname(fileURLToPath(import.meta.url)), "sidecar.js")
const child = utilityProcess.fork(sidecar, [], {
cwd: process.cwd(),
env: createSidecarEnv(),
serviceName: SIDECAR_SERVICE_NAME,
stdio: "pipe",
})
let exited = false
const exit = defer<number>()
const onProcessGone = (_event: unknown, details: Details) => {
if (details.type !== "Utility" || details.name !== SIDECAR_SERVICE_NAME) return
options.onStderr?.(`utility process gone reason=${details.reason} exitCode=${details.exitCode}`)
}
app.on("child-process-gone", onProcessGone)
child.once("exit", (code) => {
exited = true
app.off("child-process-gone", onProcessGone)
options.onExit?.(code)
exit.resolve(code)
})
child.on("error", (error) => options.onStderr?.(`utility process error: ${serializeError(error).message}`))
child.stdout?.on("data", (chunk: Buffer) => options.onStdout?.(chunk.toString("utf8").trimEnd()))
child.stderr?.on("data", (chunk: Buffer) => options.onStderr?.(chunk.toString("utf8").trimEnd()))
await new Promise<void>((resolve, reject) => {
let done = false
let timeout: NodeJS.Timeout
const fail = (error: Error) => {
if (done) return
done = true
cleanup()
reject(error)
}
const refreshTimeout = () => {
clearTimeout(timeout)
timeout = setTimeout(() => {
fail(new Error(`Sidecar did not become ready within ${SIDECAR_START_STALL_TIMEOUT}ms: ${sidecar}`))
}, SIDECAR_START_STALL_TIMEOUT)
}
const onMessage = (message: SidecarMessage) => {
if (message.type === "ready") {
if (done) return
done = true
cleanup()
resolve()
return
}
if (message.type === "error") {
fail(Object.assign(new Error(message.error.message), { stack: message.error.stack }))
}
}
const onExit = (code: number) => {
fail(new Error(`Sidecar exited before ready with code ${code}`))
}
const cleanup = () => {
clearTimeout(timeout)
child.off("message", onMessage)
child.off("exit", onExit)
}
child.on("message", onMessage)
child.on("exit", onExit)
refreshTimeout()
child.postMessage({
type: "start",
hostname,
port,
password,
userDataPath: options.userDataPath,
})
}).catch((error) => {
if (!exited) child.kill()
throw error
})
const wait = (async () => {
const url = `http://${hostname}:${port}`
let healthy = false
const gone = exit.promise.then((code) => {
if (healthy) return
throw new Error(`Sidecar exited before health check passed with code ${code}`)
})
const ready = async () => {
while (true) {
await new Promise((resolve) => setTimeout(resolve, 100))
if (await checkHealth(url, password)) {
healthy = true
return
}
}
}
await Promise.race([ready(), gone])
})()
let stopping: Promise<void> | undefined
return {
listener: {
stop: () => {
if (stopping) return stopping
if (exited) return Promise.resolve()
child.postMessage({ type: "stop" })
stopping = Promise.race([
exit.promise.then(() => undefined),
delay(SIDECAR_STOP_TIMEOUT).then(() => {
if (!exited) child.kill()
}),
])
return stopping
},
},
health: { wait },
}
}
export async function checkHealth(url: string, password?: string | null): Promise<boolean> { export async function checkHealth(url: string, password?: string | null): Promise<boolean> {
let healthUrls: URL[] let healthUrls: URL[]
try { try {
@@ -209,31 +56,3 @@ export async function checkHealth(url: string, password?: string | null): Promis
} }
return false return false
} }
function createSidecarEnv(): Record<string, string> {
const env = Object.fromEntries(
Object.entries(process.env).flatMap(([key, value]) => (value === undefined ? [] : [[key, String(value)]])),
)
delete env.DEBUG
if (process.platform === "linux") delete env.LD_PRELOAD
return env
}
function delay(ms: number) {
return new Promise<void>((resolve) => setTimeout(resolve, ms))
}
function serializeError(error: unknown) {
if (error instanceof Error) return { message: error.message, stack: error.stack }
return { message: String(error) }
}
function defer<T>() {
let resolve!: (value: T) => void
let reject!: (error: Error) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
-157
View File
@@ -1,157 +0,0 @@
import * as http from "node:http"
import * as tls from "node:tls"
type NodeHttpWithEnvProxy = typeof http & {
setGlobalProxyFromEnv: () => void
}
type NodeTlsWithSystemCertificates = typeof tls & {
getCACertificates: (type: "default" | "system") => string[]
setDefaultCACertificates: (certificates: string[]) => void
}
type StartCommand = {
type: "start"
hostname: string
port: number
password: string
userDataPath: string
}
type StopCommand = { type: "stop" }
type SidecarCommand = StartCommand | StopCommand
type SidecarMessage =
| { type: "ready" }
| { type: "stopped" }
| { type: "error"; error: { message: string; stack?: string } }
type ParentPort = {
postMessage(message: SidecarMessage): void
on(event: "message", listener: (event: { data: unknown }) => void): void
}
type Listener = {
stop(close?: boolean): void | Promise<void>
}
const parentPort = getParentPort()
let listener: Listener | undefined
parentPort.on("message", (event) => {
const command = parseCommand(event.data)
if (!command) return
if (command.type === "stop") {
void stop()
return
}
void start(command)
})
async function start(command: StartCommand) {
try {
prepareSidecarEnv(command.password, command.userDataPath)
ensureLoopbackNoProxy()
useSystemCertificates()
useEnvProxy()
const { Server } = await import("virtual:opencode-server")
listener = await Server.listen({
port: command.port,
hostname: command.hostname,
username: "opencode",
password: command.password,
cors: ["oc://renderer"],
})
parentPort.postMessage({ type: "ready" })
} catch (error) {
parentPort.postMessage({ type: "error", error: serializeError(error) })
setImmediate(() => process.exit(1))
}
}
async function stop() {
try {
await listener?.stop()
} finally {
listener = undefined
parentPort.postMessage({ type: "stopped" })
setImmediate(() => process.exit(0))
}
}
function prepareSidecarEnv(password: string, userDataPath: string) {
Object.assign(process.env, {
OPENCODE_SERVER_USERNAME: "opencode",
OPENCODE_SERVER_PASSWORD: password,
XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? userDataPath,
})
}
function ensureLoopbackNoProxy() {
const loopback = ["127.0.0.1", "localhost", "::1"]
const upsert = (key: string) => {
const items = (process.env[key] ?? "")
.split(",")
.map((value: string) => value.trim())
.filter((value: string) => Boolean(value))
for (const host of loopback) {
if (items.some((value: string) => value.toLowerCase() === host)) continue
items.push(host)
}
process.env[key] = items.join(",")
}
upsert("NO_PROXY")
upsert("no_proxy")
}
function useSystemCertificates() {
try {
const nodeTls = tls as NodeTlsWithSystemCertificates
nodeTls.setDefaultCACertificates([
...new Set([...nodeTls.getCACertificates("default"), ...nodeTls.getCACertificates("system")]),
])
} catch (error) {
console.warn("failed to load system certificates", error)
}
}
function useEnvProxy() {
try {
;(http as NodeHttpWithEnvProxy).setGlobalProxyFromEnv()
} catch (error) {
console.warn("failed to load proxy environment", error)
}
}
function parseCommand(value: unknown): SidecarCommand | undefined {
if (!value || typeof value !== "object") return
const command = value as Partial<StartCommand | StopCommand>
if (command.type === "stop") return { type: "stop" }
if (command.type !== "start") return
if (typeof command.hostname !== "string") return
if (typeof command.port !== "number") return
if (typeof command.password !== "string") return
if (typeof command.userDataPath !== "string") return
return {
type: "start",
hostname: command.hostname,
port: command.port,
password: command.password,
userDataPath: command.userDataPath,
}
}
function serializeError(error: unknown) {
if (error instanceof Error) return { message: error.message, stack: error.stack }
return { message: String(error) }
}
function getParentPort() {
const port = process.parentPort as ParentPort | undefined
if (!port) throw new Error("Sidecar parent port unavailable")
return port
}
-33
View File
@@ -23,44 +23,11 @@ export type ProviderContext = {
options: Record<string, any> options: Record<string, any>
} }
export type WorkspaceInfo = {
id: string
type: string
name: string
branch: string | null
directory: string | null
extra: unknown | null
projectID: string
}
export type WorkspaceTarget =
| {
type: "local"
directory: string
}
| {
type: "remote"
url: string | URL
headers?: HeadersInit
}
export type WorkspaceAdapter = {
name: string
description: string
configure(config: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
create(config: WorkspaceInfo, env: Record<string, string | undefined>, from?: WorkspaceInfo): Promise<void>
remove(config: WorkspaceInfo): Promise<void>
target(config: WorkspaceInfo): WorkspaceTarget | Promise<WorkspaceTarget>
}
export type PluginInput = { export type PluginInput = {
client: ReturnType<typeof createOpencodeClient> client: ReturnType<typeof createOpencodeClient>
project: Project project: Project
directory: string directory: string
worktree: string worktree: string
experimental_workspace: {
register(type: string, adapter: WorkspaceAdapter): void
}
serverUrl: URL serverUrl: URL
$: BunShell $: BunShell
} }
-1
View File
@@ -6,7 +6,6 @@ export const LocationQuery = Schema.Struct({
location: Schema.optional( location: Schema.optional(
Schema.Struct({ Schema.Struct({
directory: Schema.optional(Schema.String), directory: Schema.optional(Schema.String),
workspace: Schema.optional(Schema.String),
}), }),
), ),
}).annotate({ identifier: "LocationQuery" }) }).annotate({ identifier: "LocationQuery" })
+1 -1
View File
@@ -130,7 +130,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
"x-websocket": true, "x-websocket": true,
parameters: [ parameters: [
...(operation.parameters ?? []), ...(operation.parameters ?? []),
...["location[directory]", "location[workspace]", "cursor", PTY_CONNECT_TICKET_QUERY].map((name) => ({ ...["location[directory]", "cursor", PTY_CONNECT_TICKET_QUERY].map((name) => ({
in: "query", in: "query",
name, name,
schema: { type: "string" }, schema: { type: "string" },
-2
View File
@@ -6,7 +6,6 @@ import { InstructionEntry } from "@opencode-ai/schema/instruction-entry"
import { Project } from "@opencode-ai/schema/project" import { Project } from "@opencode-ai/schema/project"
import { AbsolutePath, PositiveInt, RelativePath, statics } from "@opencode-ai/schema/schema" import { AbsolutePath, PositiveInt, RelativePath, statics } from "@opencode-ai/schema/schema"
import { Event } from "@opencode-ai/schema/event" import { Event } from "@opencode-ai/schema/event"
import { Workspace } from "@opencode-ai/schema/workspace"
import { Context, Effect, Encoding, Result, Schema, SchemaGetter, Struct } from "effect" import { Context, Effect, Encoding, Result, Schema, SchemaGetter, Struct } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { import {
@@ -42,7 +41,6 @@ const ParentIDFilter = Schema.Union([
}) })
const SessionsQueryFields = { const SessionsQueryFields = {
workspace: Workspace.ID.pipe(Schema.optional),
limit: Schema.NumberFromString.pipe(Schema.decodeTo(PositiveInt), Schema.optional).annotate({ limit: Schema.NumberFromString.pipe(Schema.decodeTo(PositiveInt), Schema.optional).annotate({
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.", description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
}), }),
@@ -6,7 +6,6 @@ import { Session } from "@opencode-ai/schema/session"
describe("SessionsCursor", () => { describe("SessionsCursor", () => {
test("round trips without Node globals", async () => { test("round trips without Node globals", async () => {
const input = { const input = {
workspace: undefined,
search: "protocol", search: "protocol",
order: "desc" as const, order: "desc" as const,
anchor: { id: Session.ID.make("ses_test"), time: 1, direction: "next" as const }, anchor: { id: Session.ID.make("ses_test"), time: 1, direction: "next" as const },
-2
View File
@@ -32,7 +32,6 @@ import { SessionStatusEvent } from "./session-status-event.js"
import { SessionV1 } from "./session-v1.js" import { SessionV1 } from "./session-v1.js"
import { TuiEvent } from "./tui-event.js" import { TuiEvent } from "./tui-event.js"
import { VcsEvent } from "./vcs-event.js" import { VcsEvent } from "./vcs-event.js"
import { WorkspaceEvent } from "./workspace-event.js"
import { WorktreeEvent } from "./worktree-event.js" import { WorktreeEvent } from "./worktree-event.js"
import { WebSearch } from "./websearch.js" import { WebSearch } from "./websearch.js"
@@ -100,7 +99,6 @@ export const Definitions = Event.inventory(
...SessionStatusEvent.Definitions, ...SessionStatusEvent.Definitions,
...SessionCompactionEvent.Definitions, ...SessionCompactionEvent.Definitions,
...VcsEvent.Definitions, ...VcsEvent.Definitions,
...WorkspaceEvent.Definitions,
...WorktreeEvent.Definitions, ...WorktreeEvent.Definitions,
...ServerEvent.Definitions, ...ServerEvent.Definitions,
) )
+5
View File
@@ -186,6 +186,11 @@ export const InstructionsUpdated = Event.durable({
schema: { schema: {
...Base, ...Base,
delta: Instruction.Delta, delta: Instruction.Delta,
/**
* The rendered chronological update shown to the model, frozen at emit time.
* Absent for the initial baseline observation and for deltas that render empty.
*/
text: Schema.String.pipe(optional),
}, },
}) })
export type InstructionsUpdated = typeof InstructionsUpdated.Type export type InstructionsUpdated = typeof InstructionsUpdated.Type
-32
View File
@@ -1,32 +0,0 @@
export * as WorkspaceEvent from "./workspace-event.js"
import { Schema } from "effect"
import { Event } from "./event.js"
import { WorkspaceID } from "./workspace-id.js"
export const ConnectionStatus = Schema.Struct({
workspaceID: WorkspaceID,
status: Schema.Literals(["connected", "connecting", "disconnected", "error"]),
}).annotate({ identifier: "WorkspaceEvent.ConnectionStatus" })
export interface ConnectionStatus extends Schema.Schema.Type<typeof ConnectionStatus> {}
export const Ready = Event.ephemeral({
type: "workspace.ready",
schema: {
name: Schema.String,
},
})
export const Failed = Event.ephemeral({
type: "workspace.failed",
schema: {
message: Schema.String,
},
})
export const Status = Event.ephemeral({
type: "workspace.status",
schema: ConnectionStatus.fields,
})
export const Definitions = Event.inventory(Ready, Failed, Status)
-3
View File
@@ -1,9 +1,6 @@
export * as Workspace from "./workspace.js" export * as Workspace from "./workspace.js"
import { WorkspaceEvent } from "./workspace-event.js"
import { WorkspaceID } from "./workspace-id.js" import { WorkspaceID } from "./workspace-id.js"
export const ID = WorkspaceID export const ID = WorkspaceID
export type ID = WorkspaceID export type ID = WorkspaceID
export const Event = WorkspaceEvent
@@ -9,7 +9,6 @@ import {
Project, Project,
Reference, Reference,
Session, Session,
Workspace,
} from "../src/index.js" } from "../src/index.js"
import { EventManifest } from "../src/event-manifest.js" import { EventManifest } from "../src/event-manifest.js"
import { FileSystemV1 } from "../src/filesystem-v1.js" import { FileSystemV1 } from "../src/filesystem-v1.js"
@@ -20,7 +19,6 @@ import { SessionEvent } from "../src/session-event.js"
import { SessionID } from "../src/session-id.js" import { SessionID } from "../src/session-id.js"
import { SessionMessage } from "../src/session-message.js" import { SessionMessage } from "../src/session-message.js"
import { SessionV1 } from "../src/session-v1.js" import { SessionV1 } from "../src/session-v1.js"
import { WorkspaceEvent } from "../src/workspace-event.js"
describe("public event manifest", () => { describe("public event manifest", () => {
test("owns the complete public event surface", () => { test("owns the complete public event surface", () => {
@@ -60,8 +58,6 @@ describe("public event manifest", () => {
test("uses canonical definitions for current public events", () => { test("uses canonical definitions for current public events", () => {
expect(Session.Event).toBe(SessionEvent) expect(Session.Event).toBe(SessionEvent)
expect(Session.Event.Definitions).toBe(SessionEvent.Definitions) expect(Session.Event.Definitions).toBe(SessionEvent.Definitions)
expect(Workspace.Event).toBe(WorkspaceEvent)
expect(Workspace.Event.Definitions).toBe(WorkspaceEvent.Definitions)
expect(EventManifest.Latest.get("session.step.ended")).toBe(SessionEvent.Step.Ended) expect(EventManifest.Latest.get("session.step.ended")).toBe(SessionEvent.Step.Ended)
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated) expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
expect(EventManifest.Latest.get("project.updated")).toBe(Project.Event.Updated) expect(EventManifest.Latest.get("project.updated")).toBe(Project.Event.Updated)
-17
View File
@@ -2,10 +2,6 @@ export * as ServerAuth from "./auth"
import { Context, Layer, Option, Redacted } from "effect" import { Context, Layer, Option, Redacted } from "effect"
export type Credentials = {
password?: string
}
export type DecodedCredentials = { export type DecodedCredentials = {
readonly username: string readonly username: string
readonly password: Redacted.Redacted readonly password: Redacted.Redacted
@@ -37,16 +33,3 @@ export function authorized(credentials: DecodedCredentials, config: Info) {
Redacted.value(credentials.password) === config.password.value Redacted.value(credentials.password) === config.password.value
) )
} }
export function header(credentials?: Credentials) {
const password = credentials?.password
if (!password) return undefined
return `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
}
export function headers(credentials?: Credentials) {
const authorization = header(credentials)
if (!authorization) return undefined
return { Authorization: authorization }
}
-1
View File
@@ -10,7 +10,6 @@ export const LocationHandler = HttpApiBuilder.group(Api, "server.location", (han
const location = yield* Location.Service const location = yield* Location.Service
return new Location.Info({ return new Location.Info({
directory: location.directory, directory: location.directory,
workspaceID: location.workspaceID,
project: location.project, project: location.project,
}) })
}), }),
+1 -1
View File
@@ -19,7 +19,7 @@ import { PtyEnvironment } from "../pty-environment"
const ticketScope = Effect.gen(function* () { const ticketScope = Effect.gen(function* () {
const location = yield* Location.Service const location = yield* Location.Service
return { directory: location.directory as string, workspaceID: location.workspaceID } return { directory: location.directory as string }
}) })
export const PtyHandler = HttpApiBuilder.group(Api, "server.pty", (handlers) => export const PtyHandler = HttpApiBuilder.group(Api, "server.pty", (handlers) =>
-2
View File
@@ -37,7 +37,6 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
: ctx.query : ctx.query
const page = yield* session.list({ const page = yield* session.list({
...query, ...query,
workspaceID: query.workspace,
limit: ctx.query.limit ?? DefaultSessionsLimit, limit: ctx.query.limit ?? DefaultSessionsLimit,
}) })
const sessions = page.data const sessions = page.data
@@ -213,7 +212,6 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
.move({ .move({
sessionID: ctx.params.sessionID, sessionID: ctx.params.sessionID,
directory: ctx.payload.directory, directory: ctx.payload.directory,
workspaceID: ctx.payload.workspaceID,
}) })
.pipe( .pipe(
Effect.catchTag("Session.NotFoundError", (error) => Effect.catchTag("Session.NotFoundError", (error) =>
-4
View File
@@ -1,7 +1,6 @@
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap } from "@opencode-ai/core/location-services" import { LocationServiceMap } from "@opencode-ai/core/location-services"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { Workspace } from "@opencode-ai/core/workspace"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { HttpServerRequest } from "effect/unstable/http" import { HttpServerRequest } from "effect/unstable/http"
import { HttpApiMiddleware } from "effect/unstable/httpapi" import { HttpApiMiddleware } from "effect/unstable/httpapi"
@@ -18,7 +17,6 @@ export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
return { return {
location: new Location.Info({ location: new Location.Info({
directory: location.directory, directory: location.directory,
workspaceID: location.workspaceID,
project: location.project, project: location.project,
}), }),
data: yield* data, data: yield* data,
@@ -28,13 +26,11 @@ export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
export function requestRef(request: HttpServerRequest.HttpServerRequest): Location.Ref { export function requestRef(request: HttpServerRequest.HttpServerRequest): Location.Ref {
const query = new URL(request.url, "http://localhost").searchParams const query = new URL(request.url, "http://localhost").searchParams
const workspaceID = query.get("location[workspace]") || request.headers["x-opencode-workspace"]
const directory = const directory =
query.get("location[directory]") || query.get("location[directory]") ||
(request.headers["x-opencode-directory"] ? decode(request.headers["x-opencode-directory"]) : process.cwd()) (request.headers["x-opencode-directory"] ? decode(request.headers["x-opencode-directory"]) : process.cwd())
return Location.Ref.make({ return Location.Ref.make({
directory: AbsolutePath.make(directory), directory: AbsolutePath.make(directory),
workspaceID: workspaceID ? Workspace.ID.make(workspaceID) : undefined,
}) })
} }
@@ -4,7 +4,6 @@ import { Location } from "@opencode-ai/core/location"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { Session } from "@opencode-ai/core/session" import { Session } from "@opencode-ai/core/session"
import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionTable } from "@opencode-ai/core/session/sql"
import { Workspace } from "@opencode-ai/core/workspace"
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { Effect, Layer, Schema } from "effect" import { Effect, Layer, Schema } from "effect"
import { HttpRouter } from "effect/unstable/http" import { HttpRouter } from "effect/unstable/http"
@@ -40,7 +39,7 @@ export const sessionLocationLayer = Layer.effect(
), ),
) )
const row = yield* db const row = yield* db
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id }) .select({ directory: SessionTable.directory })
.from(SessionTable) .from(SessionTable)
.where(eq(SessionTable.id, sessionID)) .where(eq(SessionTable.id, sessionID))
.get() .get()
@@ -56,7 +55,6 @@ export const sessionLocationLayer = Layer.effect(
locations.get( locations.get(
Location.Ref.make({ Location.Ref.make({
directory: AbsolutePath.make(row.directory), directory: AbsolutePath.make(row.directory),
workspaceID: row.workspaceID ? Workspace.ID.make(row.workspaceID) : undefined,
}), }),
), ),
), ),
-4
View File
@@ -7,7 +7,3 @@ test("accepts only the fixed opencode username", () => {
expect(ServerAuth.authorized({ username: "opencode", password: Redacted.make("secret") }, config)).toBe(true) expect(ServerAuth.authorized({ username: "opencode", password: Redacted.make("secret") }, config)).toBe(true)
expect(ServerAuth.authorized({ username: "custom", password: Redacted.make("secret") }, config)).toBe(false) expect(ServerAuth.authorized({ username: "custom", password: Redacted.make("secret") }, config)).toBe(false)
}) })
test("encodes the fixed opencode username", () => {
expect(ServerAuth.header({ password: "secret" })).toBe(`Basic ${Buffer.from("opencode:secret").toString("base64")}`)
})
@@ -36,10 +36,6 @@ export const lineCommentStyles = `
border: none; border: none;
} }
[data-component="line-comment"][data-variant="add"] [data-slot="line-comment-button"] {
background: var(--syntax-diff-add);
}
[data-component="line-comment"] [data-component="icon"] { [data-component="line-comment"] [data-component="icon"] {
color: var(--white); color: var(--white);
} }
@@ -9,7 +9,7 @@ import { useI18n } from "@opencode-ai/ui/context/i18n"
installLineCommentStyles() installLineCommentStyles()
export type LineCommentVariant = "default" | "editor" | "add" export type LineCommentVariant = "default" | "editor"
function InlineGlyph(props: { icon: "comment" | "plus" }) { function InlineGlyph(props: { icon: "comment" | "plus" }) {
return ( return (
@@ -156,25 +156,6 @@ export const LineComment = (props: LineCommentProps) => {
) )
} }
export type LineCommentAddProps = Omit<LineCommentAnchorProps, "children" | "variant" | "open" | "icon"> & {
label?: string
}
export const LineCommentAdd = (props: LineCommentAddProps) => {
const [split, rest] = splitProps(props, ["label"])
const i18n = useI18n()
return (
<LineCommentAnchor
{...rest}
open={false}
variant="add"
icon="plus"
buttonLabel={split.label ?? i18n.t("ui.lineComment.submit")}
/>
)
}
export type LineCommentEditorProps = Omit<LineCommentAnchorProps, "children" | "open" | "variant" | "onClick"> & { export type LineCommentEditorProps = Omit<LineCommentAnchorProps, "children" | "open" | "variant" | "onClick"> & {
value: string value: string
selection: JSX.Element selection: JSX.Element
@@ -948,10 +948,6 @@ function ExaOutput(props: { output?: string }) {
) )
} }
export function registerPartComponent(type: string, component: PartComponent) {
PART_MAPPING[type] = component
}
export function Message(props: MessageProps) { export function Message(props: MessageProps) {
return ( return (
<Switch> <Switch>

Some files were not shown because too many files have changed in this diff Show More