mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-20 21:54:48 +00:00
Compare commits
4 Commits
dev
...
kit/effect
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f53b3a56ba | ||
|
|
cb39863e95 | ||
|
|
a7d55d642b | ||
|
|
cf7762957f |
@@ -89,6 +89,8 @@
|
||||
"@ai-sdk/xai": "2.0.51",
|
||||
"@aws-sdk/credential-providers": "3.993.0",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"gitlab-ai-provider": "5.2.2",
|
||||
"opencode-gitlab-auth": "2.0.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
@@ -121,7 +123,6 @@
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gitlab-ai-provider": "5.2.2",
|
||||
"glob": "13.0.5",
|
||||
"google-auth-library": "10.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
@@ -132,7 +133,6 @@
|
||||
"mime-types": "3.0.2",
|
||||
"minimatch": "10.0.3",
|
||||
"open": "10.1.2",
|
||||
"opencode-gitlab-auth": "2.0.0",
|
||||
"opentui-spinner": "0.0.6",
|
||||
"partial-json": "0.1.7",
|
||||
"remeda": "catalog:",
|
||||
|
||||
@@ -199,19 +199,6 @@ for (const item of targets) {
|
||||
},
|
||||
})
|
||||
|
||||
// Smoke test: only run if binary is for current platform
|
||||
if (item.os === process.platform && item.arch === process.arch && !item.abi) {
|
||||
const binaryPath = `dist/${name}/bin/opencode`
|
||||
console.log(`Running smoke test: ${binaryPath} --version`)
|
||||
try {
|
||||
const versionOutput = await $`${binaryPath} --version`.text()
|
||||
console.log(`Smoke test passed: ${versionOutput.trim()}`)
|
||||
} catch (e) {
|
||||
console.error(`Smoke test failed for ${name}:`, e)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
await $`rm -rf ./dist/${name}/bin/tui`
|
||||
await Bun.file(`dist/${name}/package.json`).write(
|
||||
JSON.stringify(
|
||||
|
||||
@@ -140,5 +140,5 @@ Still open and likely worth migrating:
|
||||
- [ ] `SessionCompaction`
|
||||
- [ ] `Provider`
|
||||
- [ ] `Project`
|
||||
- [ ] `LSP`
|
||||
- [x] `LSP`
|
||||
- [ ] `MCP`
|
||||
|
||||
@@ -136,8 +136,6 @@ export class AccountRepo extends ServiceMap.Service<AccountRepo, AccountRepo.Ser
|
||||
.onConflictDoUpdate({
|
||||
target: AccountTable.id,
|
||||
set: {
|
||||
email: input.email,
|
||||
url: input.url,
|
||||
access_token: input.accessToken,
|
||||
refresh_token: input.refreshToken,
|
||||
token_expiry: input.expiry,
|
||||
|
||||
@@ -14,7 +14,7 @@ import PROMPT_COMPACTION from "./prompt/compaction.txt"
|
||||
import PROMPT_EXPLORE from "./prompt/explore.txt"
|
||||
import PROMPT_SUMMARY from "./prompt/summary.txt"
|
||||
import PROMPT_TITLE from "./prompt/title.txt"
|
||||
import { Permission as PermissionNext } from "@/permission/service"
|
||||
import { PermissionNext } from "@/permission"
|
||||
import { mergeDeep, pipe, sortBy, values } from "remeda"
|
||||
import { Global } from "@/global"
|
||||
import path from "path"
|
||||
|
||||
@@ -22,7 +22,7 @@ import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import type { Snapshot } from "@/snapshot/service"
|
||||
import type { Snapshot } from "@/snapshot"
|
||||
import { useExit } from "./exit"
|
||||
import { useArgs } from "./args"
|
||||
import { batch, onMount } from "solid-js"
|
||||
|
||||
@@ -1667,7 +1667,6 @@ function InlineTool(props: {
|
||||
|
||||
const denied = createMemo(
|
||||
() =>
|
||||
error()?.includes("QuestionRejectedError") ||
|
||||
error()?.includes("rejected permission") ||
|
||||
error()?.includes("specified a rule") ||
|
||||
error()?.includes("user dismissed"),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { File } from "@/file/service"
|
||||
import { FileTime } from "@/file/time-service"
|
||||
import { FileWatcher } from "@/file/watcher"
|
||||
import { Format } from "@/format/service"
|
||||
import { LSP } from "@/lsp"
|
||||
import { Permission } from "@/permission/service"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Vcs } from "@/project/vcs"
|
||||
@@ -26,6 +27,7 @@ export type InstanceServices =
|
||||
| File.Service
|
||||
| Skill.Service
|
||||
| Snapshot.Service
|
||||
| LSP.Service
|
||||
|
||||
// NOTE: LayerMap only passes the key (directory string) to lookup, but we need
|
||||
// the full instance context (directory, worktree, project). We read from the
|
||||
@@ -46,6 +48,7 @@ function lookup(_key: string) {
|
||||
File.layer,
|
||||
Skill.defaultLayer,
|
||||
Snapshot.defaultLayer,
|
||||
LSP.layer,
|
||||
).pipe(Layer.provide(ctx))
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ export function runPromiseInstance<A, E>(effect: Effect.Effect<A, E, InstanceSer
|
||||
return runtime.runPromise(effect.pipe(Effect.provide(Instances.get(Instance.directory))))
|
||||
}
|
||||
|
||||
export function runSyncInstance<A, E>(effect: Effect.Effect<A, E, InstanceServices>) {
|
||||
return runtime.runSync(effect.pipe(Effect.provide(Instances.get(Instance.directory))))
|
||||
}
|
||||
|
||||
export function disposeRuntime() {
|
||||
return runtime.dispose()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import { InstanceContext } from "@/effect/instance-context"
|
||||
import { runPromiseInstance } from "@/effect/runtime"
|
||||
import { Log } from "../util/log"
|
||||
import { LSPClient } from "./client"
|
||||
import path from "path"
|
||||
@@ -7,10 +9,10 @@ import { pathToFileURL, fileURLToPath } from "url"
|
||||
import { LSPServer } from "./server"
|
||||
import z from "zod"
|
||||
import { Config } from "../config/config"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Process } from "../util/process"
|
||||
import { spawn as lspspawn } from "./launch"
|
||||
import { Effect, Fiber, Layer, ServiceMap } from "effect"
|
||||
|
||||
export namespace LSP {
|
||||
const log = Log.create({ service: "lsp" })
|
||||
@@ -77,77 +79,6 @@ export namespace LSP {
|
||||
}
|
||||
}
|
||||
|
||||
const state = Instance.state(
|
||||
async () => {
|
||||
const clients: LSPClient.Info[] = []
|
||||
const servers: Record<string, LSPServer.Info> = {}
|
||||
const cfg = await Config.get()
|
||||
|
||||
if (cfg.lsp === false) {
|
||||
log.info("all LSPs are disabled")
|
||||
return {
|
||||
broken: new Set<string>(),
|
||||
servers,
|
||||
clients,
|
||||
spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
|
||||
}
|
||||
}
|
||||
|
||||
for (const server of Object.values(LSPServer)) {
|
||||
servers[server.id] = server
|
||||
}
|
||||
|
||||
filterExperimentalServers(servers)
|
||||
|
||||
for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
|
||||
const existing = servers[name]
|
||||
if (item.disabled) {
|
||||
log.info(`LSP server ${name} is disabled`)
|
||||
delete servers[name]
|
||||
continue
|
||||
}
|
||||
servers[name] = {
|
||||
...existing,
|
||||
id: name,
|
||||
root: existing?.root ?? (async () => Instance.directory),
|
||||
extensions: item.extensions ?? existing?.extensions ?? [],
|
||||
spawn: async (root) => {
|
||||
return {
|
||||
process: lspspawn(item.command[0], item.command.slice(1), {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
...item.env,
|
||||
},
|
||||
}),
|
||||
initialization: item.initialization,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
log.info("enabled LSP servers", {
|
||||
serverIds: Object.values(servers)
|
||||
.map((server) => server.id)
|
||||
.join(", "),
|
||||
})
|
||||
|
||||
return {
|
||||
broken: new Set<string>(),
|
||||
servers,
|
||||
clients,
|
||||
spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
|
||||
}
|
||||
},
|
||||
async (state) => {
|
||||
await Promise.all(state.clients.map((client) => client.shutdown()))
|
||||
},
|
||||
)
|
||||
|
||||
export async function init() {
|
||||
return state()
|
||||
}
|
||||
|
||||
export const Status = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
@@ -160,162 +91,6 @@ export namespace LSP {
|
||||
})
|
||||
export type Status = z.infer<typeof Status>
|
||||
|
||||
export async function status() {
|
||||
return state().then((x) => {
|
||||
const result: Status[] = []
|
||||
for (const client of x.clients) {
|
||||
result.push({
|
||||
id: client.serverID,
|
||||
name: x.servers[client.serverID].id,
|
||||
root: path.relative(Instance.directory, client.root),
|
||||
status: "connected",
|
||||
})
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async function getClients(file: string) {
|
||||
const s = await state()
|
||||
const extension = path.parse(file).ext || file
|
||||
const result: LSPClient.Info[] = []
|
||||
|
||||
async function schedule(server: LSPServer.Info, root: string, key: string) {
|
||||
const handle = await server
|
||||
.spawn(root)
|
||||
.then((value) => {
|
||||
if (!value) s.broken.add(key)
|
||||
return value
|
||||
})
|
||||
.catch((err) => {
|
||||
s.broken.add(key)
|
||||
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!handle) return undefined
|
||||
log.info("spawned lsp server", { serverID: server.id })
|
||||
|
||||
const client = await LSPClient.create({
|
||||
serverID: server.id,
|
||||
server: handle,
|
||||
root,
|
||||
}).catch(async (err) => {
|
||||
s.broken.add(key)
|
||||
await Process.stop(handle.process)
|
||||
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!client) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const existing = s.clients.find((x) => x.root === root && x.serverID === server.id)
|
||||
if (existing) {
|
||||
await Process.stop(handle.process)
|
||||
return existing
|
||||
}
|
||||
|
||||
s.clients.push(client)
|
||||
return client
|
||||
}
|
||||
|
||||
for (const server of Object.values(s.servers)) {
|
||||
if (server.extensions.length && !server.extensions.includes(extension)) continue
|
||||
|
||||
const root = await server.root(file)
|
||||
if (!root) continue
|
||||
if (s.broken.has(root + server.id)) continue
|
||||
|
||||
const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
|
||||
if (match) {
|
||||
result.push(match)
|
||||
continue
|
||||
}
|
||||
|
||||
const inflight = s.spawning.get(root + server.id)
|
||||
if (inflight) {
|
||||
const client = await inflight
|
||||
if (!client) continue
|
||||
result.push(client)
|
||||
continue
|
||||
}
|
||||
|
||||
const task = schedule(server, root, root + server.id)
|
||||
s.spawning.set(root + server.id, task)
|
||||
|
||||
task.finally(() => {
|
||||
if (s.spawning.get(root + server.id) === task) {
|
||||
s.spawning.delete(root + server.id)
|
||||
}
|
||||
})
|
||||
|
||||
const client = await task
|
||||
if (!client) continue
|
||||
|
||||
result.push(client)
|
||||
Bus.publish(Event.Updated, {})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export async function hasClients(file: string) {
|
||||
const s = await state()
|
||||
const extension = path.parse(file).ext || file
|
||||
for (const server of Object.values(s.servers)) {
|
||||
if (server.extensions.length && !server.extensions.includes(extension)) continue
|
||||
const root = await server.root(file)
|
||||
if (!root) continue
|
||||
if (s.broken.has(root + server.id)) continue
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
|
||||
log.info("touching file", { file: input })
|
||||
const clients = await getClients(input)
|
||||
await Promise.all(
|
||||
clients.map(async (client) => {
|
||||
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
|
||||
await client.notify.open({ path: input })
|
||||
return wait
|
||||
}),
|
||||
).catch((err) => {
|
||||
log.error("failed to touch file", { err, file: input })
|
||||
})
|
||||
}
|
||||
|
||||
export async function diagnostics() {
|
||||
const results: Record<string, LSPClient.Diagnostic[]> = {}
|
||||
for (const result of await runAll(async (client) => client.diagnostics)) {
|
||||
for (const [path, diagnostics] of result.entries()) {
|
||||
const arr = results[path] || []
|
||||
arr.push(...diagnostics)
|
||||
results[path] = arr
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
export async function hover(input: { file: string; line: number; character: number }) {
|
||||
return run(input.file, (client) => {
|
||||
return client.connection
|
||||
.sendRequest("textDocument/hover", {
|
||||
textDocument: {
|
||||
uri: pathToFileURL(input.file).href,
|
||||
},
|
||||
position: {
|
||||
line: input.line,
|
||||
character: input.character,
|
||||
},
|
||||
})
|
||||
.catch(() => null)
|
||||
})
|
||||
}
|
||||
|
||||
enum SymbolKind {
|
||||
File = 1,
|
||||
Module = 2,
|
||||
@@ -356,114 +131,504 @@ export namespace LSP {
|
||||
SymbolKind.Enum,
|
||||
]
|
||||
|
||||
export async function workspaceSymbol(query: string) {
|
||||
return runAll((client) =>
|
||||
client.connection
|
||||
.sendRequest("workspace/symbol", {
|
||||
query,
|
||||
export interface Interface {
|
||||
readonly init: () => Effect.Effect<void>
|
||||
readonly status: () => Effect.Effect<Status[]>
|
||||
readonly hasClients: (file: string) => Effect.Effect<boolean>
|
||||
readonly touchFile: (input: string, waitForDiagnostics?: boolean) => Effect.Effect<void>
|
||||
readonly diagnostics: () => Effect.Effect<Record<string, LSPClient.Diagnostic[]>>
|
||||
readonly hover: (input: { file: string; line: number; character: number }) => Effect.Effect<any>
|
||||
readonly workspaceSymbol: (query: string) => Effect.Effect<LSP.Symbol[]>
|
||||
readonly documentSymbol: (uri: string) => Effect.Effect<(LSP.DocumentSymbol | LSP.Symbol)[]>
|
||||
readonly definition: (input: { file: string; line: number; character: number }) => Effect.Effect<any[]>
|
||||
readonly references: (input: { file: string; line: number; character: number }) => Effect.Effect<any[]>
|
||||
readonly implementation: (input: { file: string; line: number; character: number }) => Effect.Effect<any[]>
|
||||
readonly prepareCallHierarchy: (input: { file: string; line: number; character: number }) => Effect.Effect<any[]>
|
||||
readonly incomingCalls: (input: { file: string; line: number; character: number }) => Effect.Effect<any[]>
|
||||
readonly outgoingCalls: (input: { file: string; line: number; character: number }) => Effect.Effect<any[]>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/LSP") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const instance = yield* InstanceContext
|
||||
const directory = instance.directory
|
||||
|
||||
const clients: LSPClient.Info[] = []
|
||||
const servers: Record<string, LSPServer.Info> = {}
|
||||
const broken = new Set<string>()
|
||||
const spawning = new Map<string, Promise<LSPClient.Info | undefined>>()
|
||||
|
||||
// Load server configs lazily — forkScoped so it doesn't block layer construction
|
||||
const load = Effect.fn("LSP.load")(function* () {
|
||||
yield* Effect.promise(async () => {
|
||||
const cfg = await Config.get()
|
||||
|
||||
if (cfg.lsp === false) {
|
||||
log.info("all LSPs are disabled")
|
||||
return
|
||||
}
|
||||
|
||||
for (const server of Object.values(LSPServer)) {
|
||||
servers[server.id] = server
|
||||
}
|
||||
|
||||
filterExperimentalServers(servers)
|
||||
|
||||
for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
|
||||
const existing = servers[name]
|
||||
if (item.disabled) {
|
||||
log.info(`LSP server ${name} is disabled`)
|
||||
delete servers[name]
|
||||
continue
|
||||
}
|
||||
servers[name] = {
|
||||
...existing,
|
||||
id: name,
|
||||
root: existing?.root ?? (async () => directory),
|
||||
extensions: item.extensions ?? existing?.extensions ?? [],
|
||||
spawn: async (root) => {
|
||||
return {
|
||||
process: lspspawn(item.command[0], item.command.slice(1), {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
...item.env,
|
||||
},
|
||||
}),
|
||||
initialization: item.initialization,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
log.info("enabled LSP servers", {
|
||||
serverIds: Object.values(servers)
|
||||
.map((server) => server.id)
|
||||
.join(", "),
|
||||
})
|
||||
})
|
||||
.then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind)))
|
||||
.then((result: any) => result.slice(0, 10))
|
||||
.catch(() => []),
|
||||
).then((result) => result.flat() as LSP.Symbol[])
|
||||
})
|
||||
|
||||
const loadFiber = yield* load().pipe(
|
||||
Effect.catchCause((cause) => Effect.sync(() => log.error("init failed", { cause }))),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
// Cleanup: shut down all LSP clients when the scope is closed
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(async () => {
|
||||
await Promise.all(clients.map((client) => client.shutdown()))
|
||||
}),
|
||||
)
|
||||
|
||||
async function getClientsForFile(file: string) {
|
||||
const extension = path.parse(file).ext || file
|
||||
const result: LSPClient.Info[] = []
|
||||
|
||||
async function schedule(server: LSPServer.Info, root: string, key: string) {
|
||||
const handle = await server
|
||||
.spawn(root)
|
||||
.then((value) => {
|
||||
if (!value) broken.add(key)
|
||||
return value
|
||||
})
|
||||
.catch((err) => {
|
||||
broken.add(key)
|
||||
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!handle) return undefined
|
||||
log.info("spawned lsp server", { serverID: server.id })
|
||||
|
||||
const client = await LSPClient.create({
|
||||
serverID: server.id,
|
||||
server: handle,
|
||||
root,
|
||||
}).catch(async (err) => {
|
||||
broken.add(key)
|
||||
await Process.stop(handle.process)
|
||||
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!client) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const existing = clients.find((x) => x.root === root && x.serverID === server.id)
|
||||
if (existing) {
|
||||
await Process.stop(handle.process)
|
||||
return existing
|
||||
}
|
||||
|
||||
clients.push(client)
|
||||
return client
|
||||
}
|
||||
|
||||
for (const server of Object.values(servers)) {
|
||||
if (server.extensions.length && !server.extensions.includes(extension)) continue
|
||||
|
||||
const root = await server.root(file)
|
||||
if (!root) continue
|
||||
if (broken.has(root + server.id)) continue
|
||||
|
||||
const match = clients.find((x) => x.root === root && x.serverID === server.id)
|
||||
if (match) {
|
||||
result.push(match)
|
||||
continue
|
||||
}
|
||||
|
||||
const inflight = spawning.get(root + server.id)
|
||||
if (inflight) {
|
||||
const client = await inflight
|
||||
if (!client) continue
|
||||
result.push(client)
|
||||
continue
|
||||
}
|
||||
|
||||
const task = schedule(server, root, root + server.id)
|
||||
spawning.set(root + server.id, task)
|
||||
|
||||
task.finally(() => {
|
||||
if (spawning.get(root + server.id) === task) {
|
||||
spawning.delete(root + server.id)
|
||||
}
|
||||
})
|
||||
|
||||
const client = await task
|
||||
if (!client) continue
|
||||
|
||||
result.push(client)
|
||||
Bus.publish(Event.Updated, {})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function runAllClients<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
|
||||
const tasks = clients.map((x) => input(x))
|
||||
return Promise.all(tasks)
|
||||
}
|
||||
|
||||
async function runForFile<T>(file: string, input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
|
||||
const matched = await getClientsForFile(file)
|
||||
const tasks = matched.map((x) => input(x))
|
||||
return Promise.all(tasks)
|
||||
}
|
||||
|
||||
const init = Effect.fn("LSP.init")(function* () {
|
||||
yield* Fiber.join(loadFiber)
|
||||
})
|
||||
|
||||
const status = Effect.fn("LSP.status")(function* () {
|
||||
yield* Fiber.join(loadFiber)
|
||||
const result: Status[] = []
|
||||
for (const client of clients) {
|
||||
result.push({
|
||||
id: client.serverID,
|
||||
name: servers[client.serverID].id,
|
||||
root: path.relative(directory, client.root),
|
||||
status: "connected",
|
||||
})
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const hasClients = Effect.fn("LSP.hasClients")(function* (file: string) {
|
||||
yield* Fiber.join(loadFiber)
|
||||
return yield* Effect.promise(async () => {
|
||||
const extension = path.parse(file).ext || file
|
||||
for (const server of Object.values(servers)) {
|
||||
if (server.extensions.length && !server.extensions.includes(extension)) continue
|
||||
const root = await server.root(file)
|
||||
if (!root) continue
|
||||
if (broken.has(root + server.id)) continue
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
})
|
||||
|
||||
const touchFile = Effect.fn("LSP.touchFile")(function* (input: string, waitForDiagnostics?: boolean) {
|
||||
yield* Fiber.join(loadFiber)
|
||||
yield* Effect.promise(async () => {
|
||||
log.info("touching file", { file: input })
|
||||
const matched = await getClientsForFile(input)
|
||||
await Promise.all(
|
||||
matched.map(async (client) => {
|
||||
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
|
||||
await client.notify.open({ path: input })
|
||||
return wait
|
||||
}),
|
||||
).catch((err) => {
|
||||
log.error("failed to touch file", { err, file: input })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const diagnostics = Effect.fn("LSP.diagnostics")(function* () {
|
||||
yield* Fiber.join(loadFiber)
|
||||
return yield* Effect.promise(async () => {
|
||||
const results: Record<string, LSPClient.Diagnostic[]> = {}
|
||||
for (const result of await runAllClients(async (client) => client.diagnostics)) {
|
||||
for (const [path, diagnostics] of result.entries()) {
|
||||
const arr = results[path] || []
|
||||
arr.push(...diagnostics)
|
||||
results[path] = arr
|
||||
}
|
||||
}
|
||||
return results
|
||||
})
|
||||
})
|
||||
|
||||
const hover = Effect.fn("LSP.hover")(function* (input: { file: string; line: number; character: number }) {
|
||||
yield* Fiber.join(loadFiber)
|
||||
return yield* Effect.promise(async () => {
|
||||
return runForFile(input.file, (client) => {
|
||||
return client.connection
|
||||
.sendRequest("textDocument/hover", {
|
||||
textDocument: {
|
||||
uri: pathToFileURL(input.file).href,
|
||||
},
|
||||
position: {
|
||||
line: input.line,
|
||||
character: input.character,
|
||||
},
|
||||
})
|
||||
.catch(() => null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const workspaceSymbol = Effect.fn("LSP.workspaceSymbol")(function* (query: string) {
|
||||
yield* Fiber.join(loadFiber)
|
||||
return yield* Effect.promise(async () => {
|
||||
return runAllClients((client) =>
|
||||
client.connection
|
||||
.sendRequest("workspace/symbol", {
|
||||
query,
|
||||
})
|
||||
.then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind)))
|
||||
.then((result: any) => result.slice(0, 10))
|
||||
.catch(() => []),
|
||||
).then((result) => result.flat() as LSP.Symbol[])
|
||||
})
|
||||
})
|
||||
|
||||
const documentSymbol = Effect.fn("LSP.documentSymbol")(function* (uri: string) {
|
||||
yield* Fiber.join(loadFiber)
|
||||
return yield* Effect.promise(async () => {
|
||||
const file = fileURLToPath(uri)
|
||||
return runForFile(file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/documentSymbol", {
|
||||
textDocument: {
|
||||
uri,
|
||||
},
|
||||
})
|
||||
.catch(() => []),
|
||||
)
|
||||
.then((result) => result.flat() as (LSP.DocumentSymbol | LSP.Symbol)[])
|
||||
.then((result) => result.filter(Boolean))
|
||||
})
|
||||
})
|
||||
|
||||
const definition = Effect.fn("LSP.definition")(function* (input: {
|
||||
file: string
|
||||
line: number
|
||||
character: number
|
||||
}) {
|
||||
yield* Fiber.join(loadFiber)
|
||||
return yield* Effect.promise(async () => {
|
||||
return runForFile(input.file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/definition", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => null),
|
||||
).then((result) => result.flat().filter(Boolean))
|
||||
})
|
||||
})
|
||||
|
||||
const references = Effect.fn("LSP.references")(function* (input: {
|
||||
file: string
|
||||
line: number
|
||||
character: number
|
||||
}) {
|
||||
yield* Fiber.join(loadFiber)
|
||||
return yield* Effect.promise(async () => {
|
||||
return runForFile(input.file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/references", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
context: { includeDeclaration: true },
|
||||
})
|
||||
.catch(() => []),
|
||||
).then((result) => result.flat().filter(Boolean))
|
||||
})
|
||||
})
|
||||
|
||||
const implementation = Effect.fn("LSP.implementation")(function* (input: {
|
||||
file: string
|
||||
line: number
|
||||
character: number
|
||||
}) {
|
||||
yield* Fiber.join(loadFiber)
|
||||
return yield* Effect.promise(async () => {
|
||||
return runForFile(input.file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/implementation", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => null),
|
||||
).then((result) => result.flat().filter(Boolean))
|
||||
})
|
||||
})
|
||||
|
||||
const prepareCallHierarchy = Effect.fn("LSP.prepareCallHierarchy")(function* (input: {
|
||||
file: string
|
||||
line: number
|
||||
character: number
|
||||
}) {
|
||||
yield* Fiber.join(loadFiber)
|
||||
return yield* Effect.promise(async () => {
|
||||
return runForFile(input.file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/prepareCallHierarchy", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => []),
|
||||
).then((result) => result.flat().filter(Boolean))
|
||||
})
|
||||
})
|
||||
|
||||
const incomingCalls = Effect.fn("LSP.incomingCalls")(function* (input: {
|
||||
file: string
|
||||
line: number
|
||||
character: number
|
||||
}) {
|
||||
yield* Fiber.join(loadFiber)
|
||||
return yield* Effect.promise(async () => {
|
||||
return runForFile(input.file, async (client) => {
|
||||
const items = (await client.connection
|
||||
.sendRequest("textDocument/prepareCallHierarchy", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => [])) as any[]
|
||||
if (!items?.length) return []
|
||||
return client.connection
|
||||
.sendRequest("callHierarchy/incomingCalls", { item: items[0] })
|
||||
.catch(() => [])
|
||||
}).then((result) => result.flat().filter(Boolean))
|
||||
})
|
||||
})
|
||||
|
||||
const outgoingCalls = Effect.fn("LSP.outgoingCalls")(function* (input: {
|
||||
file: string
|
||||
line: number
|
||||
character: number
|
||||
}) {
|
||||
yield* Fiber.join(loadFiber)
|
||||
return yield* Effect.promise(async () => {
|
||||
return runForFile(input.file, async (client) => {
|
||||
const items = (await client.connection
|
||||
.sendRequest("textDocument/prepareCallHierarchy", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => [])) as any[]
|
||||
if (!items?.length) return []
|
||||
return client.connection
|
||||
.sendRequest("callHierarchy/outgoingCalls", { item: items[0] })
|
||||
.catch(() => [])
|
||||
}).then((result) => result.flat().filter(Boolean))
|
||||
})
|
||||
})
|
||||
|
||||
log.info("init")
|
||||
return Service.of({
|
||||
init,
|
||||
status,
|
||||
hasClients,
|
||||
touchFile,
|
||||
diagnostics,
|
||||
hover,
|
||||
workspaceSymbol,
|
||||
documentSymbol,
|
||||
definition,
|
||||
references,
|
||||
implementation,
|
||||
prepareCallHierarchy,
|
||||
incomingCalls,
|
||||
outgoingCalls,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
// Async facades
|
||||
export async function init() {
|
||||
return runPromiseInstance(Service.use((svc) => svc.init()))
|
||||
}
|
||||
|
||||
export async function status() {
|
||||
return runPromiseInstance(Service.use((svc) => svc.status()))
|
||||
}
|
||||
|
||||
export async function hasClients(file: string) {
|
||||
return runPromiseInstance(Service.use((svc) => svc.hasClients(file)))
|
||||
}
|
||||
|
||||
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
|
||||
return runPromiseInstance(Service.use((svc) => svc.touchFile(input, waitForDiagnostics)))
|
||||
}
|
||||
|
||||
export async function diagnostics() {
|
||||
return runPromiseInstance(Service.use((svc) => svc.diagnostics()))
|
||||
}
|
||||
|
||||
export async function hover(input: { file: string; line: number; character: number }) {
|
||||
return runPromiseInstance(Service.use((svc) => svc.hover(input)))
|
||||
}
|
||||
|
||||
export async function workspaceSymbol(query: string) {
|
||||
return runPromiseInstance(Service.use((svc) => svc.workspaceSymbol(query)))
|
||||
}
|
||||
|
||||
export async function documentSymbol(uri: string) {
|
||||
const file = fileURLToPath(uri)
|
||||
return run(file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/documentSymbol", {
|
||||
textDocument: {
|
||||
uri,
|
||||
},
|
||||
})
|
||||
.catch(() => []),
|
||||
)
|
||||
.then((result) => result.flat() as (LSP.DocumentSymbol | LSP.Symbol)[])
|
||||
.then((result) => result.filter(Boolean))
|
||||
return runPromiseInstance(Service.use((svc) => svc.documentSymbol(uri)))
|
||||
}
|
||||
|
||||
export async function definition(input: { file: string; line: number; character: number }) {
|
||||
return run(input.file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/definition", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => null),
|
||||
).then((result) => result.flat().filter(Boolean))
|
||||
return runPromiseInstance(Service.use((svc) => svc.definition(input)))
|
||||
}
|
||||
|
||||
export async function references(input: { file: string; line: number; character: number }) {
|
||||
return run(input.file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/references", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
context: { includeDeclaration: true },
|
||||
})
|
||||
.catch(() => []),
|
||||
).then((result) => result.flat().filter(Boolean))
|
||||
return runPromiseInstance(Service.use((svc) => svc.references(input)))
|
||||
}
|
||||
|
||||
export async function implementation(input: { file: string; line: number; character: number }) {
|
||||
return run(input.file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/implementation", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => null),
|
||||
).then((result) => result.flat().filter(Boolean))
|
||||
return runPromiseInstance(Service.use((svc) => svc.implementation(input)))
|
||||
}
|
||||
|
||||
export async function prepareCallHierarchy(input: { file: string; line: number; character: number }) {
|
||||
return run(input.file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/prepareCallHierarchy", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => []),
|
||||
).then((result) => result.flat().filter(Boolean))
|
||||
return runPromiseInstance(Service.use((svc) => svc.prepareCallHierarchy(input)))
|
||||
}
|
||||
|
||||
export async function incomingCalls(input: { file: string; line: number; character: number }) {
|
||||
return run(input.file, async (client) => {
|
||||
const items = (await client.connection
|
||||
.sendRequest("textDocument/prepareCallHierarchy", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => [])) as any[]
|
||||
if (!items?.length) return []
|
||||
return client.connection.sendRequest("callHierarchy/incomingCalls", { item: items[0] }).catch(() => [])
|
||||
}).then((result) => result.flat().filter(Boolean))
|
||||
return runPromiseInstance(Service.use((svc) => svc.incomingCalls(input)))
|
||||
}
|
||||
|
||||
export async function outgoingCalls(input: { file: string; line: number; character: number }) {
|
||||
return run(input.file, async (client) => {
|
||||
const items = (await client.connection
|
||||
.sendRequest("textDocument/prepareCallHierarchy", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => [])) as any[]
|
||||
if (!items?.length) return []
|
||||
return client.connection.sendRequest("callHierarchy/outgoingCalls", { item: items[0] }).catch(() => [])
|
||||
}).then((result) => result.flat().filter(Boolean))
|
||||
}
|
||||
|
||||
async function runAll<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
|
||||
const clients = await state().then((x) => x.clients)
|
||||
const tasks = clients.map((x) => input(x))
|
||||
return Promise.all(tasks)
|
||||
}
|
||||
|
||||
async function run<T>(file: string, input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
|
||||
const clients = await getClients(file)
|
||||
const tasks = clients.map((x) => input(x))
|
||||
return Promise.all(tasks)
|
||||
return runPromiseInstance(Service.use((svc) => svc.outgoingCalls(input)))
|
||||
}
|
||||
|
||||
export namespace Diagnostic {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { SessionStatus } from "@/session/status"
|
||||
import { SessionSummary } from "@/session/summary"
|
||||
import { Todo } from "../../session/todo"
|
||||
import { Agent } from "../../agent/agent"
|
||||
import { Snapshot } from "@/snapshot/service"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { Log } from "../../util/log"
|
||||
import { PermissionNext } from "@/permission"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Instance } from "../project/instance"
|
||||
import { SessionPrompt } from "./prompt"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Command } from "../command"
|
||||
import { Snapshot } from "@/snapshot/service"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { WorkspaceContext } from "../control-plane/workspace-context"
|
||||
import { ProjectID } from "../project/schema"
|
||||
import { WorkspaceID } from "../control-plane/schema"
|
||||
@@ -28,7 +28,7 @@ import { SessionID, MessageID, PartID } from "./schema"
|
||||
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Permission as PermissionNext } from "@/permission/service"
|
||||
import { PermissionNext } from "@/permission"
|
||||
import { Global } from "@/global"
|
||||
import type { LanguageModelV2Usage } from "@ai-sdk/provider"
|
||||
import { iife } from "@/util/iife"
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { MessageV2 } from "./message-v2"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { SystemPrompt } from "./system"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Permission as PermissionNext } from "@/permission/service"
|
||||
import { PermissionNext } from "@/permission"
|
||||
import { Auth } from "@/auth"
|
||||
|
||||
export namespace LLM {
|
||||
|
||||
@@ -4,7 +4,7 @@ import z from "zod"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
|
||||
import { LSP } from "../lsp"
|
||||
import { Snapshot } from "@/snapshot/service"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Database, NotFoundError, and, desc, eq, inArray, lt, or } from "@/storage/db"
|
||||
import { MessageTable, PartTable, SessionTable } from "./session.sql"
|
||||
|
||||
@@ -13,7 +13,7 @@ import { LLM } from "./llm"
|
||||
import { Config } from "@/config/config"
|
||||
import { SessionCompaction } from "./compaction"
|
||||
import { PermissionNext } from "@/permission"
|
||||
import { Question } from "@/question/service"
|
||||
import { Question } from "@/question"
|
||||
import { PartID } from "./schema"
|
||||
import type { SessionID, MessageID } from "./schema"
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
import type { MessageV2 } from "./message-v2"
|
||||
import type { Snapshot } from "../snapshot/service"
|
||||
import type { Permission as PermissionNext } from "../permission/service"
|
||||
import type { Snapshot } from "../snapshot"
|
||||
import type { PermissionNext } from "../permission"
|
||||
import type { ProjectID } from "../project/schema"
|
||||
import type { SessionID, MessageID, PartID } from "./schema"
|
||||
import type { WorkspaceID } from "../control-plane/schema"
|
||||
|
||||
@@ -11,7 +11,7 @@ import PROMPT_CODEX from "./prompt/codex.txt"
|
||||
import PROMPT_TRINITY from "./prompt/trinity.txt"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import type { Agent } from "@/agent/agent"
|
||||
import { Permission as PermissionNext } from "@/permission/service"
|
||||
import { PermissionNext } from "@/permission"
|
||||
import { Skill } from "@/skill"
|
||||
|
||||
export namespace SystemPrompt {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { trimDiff } from "./edit"
|
||||
import { LSP } from "../lsp"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import DESCRIPTION from "./apply_patch.txt"
|
||||
import { File } from "../file/service"
|
||||
import { File } from "../file"
|
||||
|
||||
const PatchParams = z.object({
|
||||
patchText: z.string().describe("The full patch text that describes all changes to be made"),
|
||||
|
||||
@@ -9,13 +9,13 @@ import { Tool } from "./tool"
|
||||
import { LSP } from "../lsp"
|
||||
import { createTwoFilesPatch, diffLines } from "diff"
|
||||
import DESCRIPTION from "./edit.txt"
|
||||
import { File } from "../file/service"
|
||||
import { File } from "../file"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
import { Bus } from "../bus"
|
||||
import { FileTime } from "../file/time"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Snapshot } from "@/snapshot/service"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { assertExternalDirectory } from "./external-directory"
|
||||
|
||||
const MAX_DIAGNOSTICS_PER_FILE = 20
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import z from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import { Question } from "../question/service"
|
||||
import { Question as QuestionApi } from "../question"
|
||||
import { Question } from "../question"
|
||||
import DESCRIPTION from "./question.txt"
|
||||
|
||||
export const QuestionTool = Tool.define("question", {
|
||||
@@ -10,7 +9,7 @@ export const QuestionTool = Tool.define("question", {
|
||||
questions: z.array(Question.Info.omit({ custom: true })).describe("Questions to ask"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const answers = await QuestionApi.ask({
|
||||
const answers = await Question.ask({
|
||||
sessionID: ctx.sessionID,
|
||||
questions: params.questions,
|
||||
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { SessionPrompt } from "../session/prompt"
|
||||
import { iife } from "@/util/iife"
|
||||
import { defer } from "@/util/defer"
|
||||
import { Config } from "../config/config"
|
||||
import { Permission as PermissionNext } from "@/permission/service"
|
||||
import { PermissionNext } from "@/permission"
|
||||
|
||||
const parameters = z.object({
|
||||
description: z.string().describe("A short (3-5 words) description of the task"),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import z from "zod"
|
||||
import type { MessageV2 } from "../session/message-v2"
|
||||
import type { Agent } from "../agent/agent"
|
||||
import type { Permission as PermissionNext } from "../permission/service"
|
||||
import type { PermissionNext } from "../permission"
|
||||
import type { SessionID, MessageID } from "../session/schema"
|
||||
import { Truncate } from "./truncate"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { LSP } from "../lsp"
|
||||
import { createTwoFilesPatch } from "diff"
|
||||
import DESCRIPTION from "./write.txt"
|
||||
import { Bus } from "../bus"
|
||||
import { File } from "../file/service"
|
||||
import { File } from "../file"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
import { FileTime } from "../file/time"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
Reference in New Issue
Block a user