Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
802534891c | ||
|
|
3431dfb8b8 | ||
|
|
fefe8b500a | ||
|
|
5b8573e804 | ||
|
|
40123cbe2d | ||
|
|
016c641860 | ||
|
|
b41b0e3d2d | ||
|
|
3d3e50ebf0 | ||
|
|
0abc0d541a | ||
|
|
3f3989e694 | ||
|
|
b4667d9b0d | ||
|
|
3c68d75776 | ||
|
|
23ed876835 |
@@ -213,7 +213,7 @@ for (const item of targets) {
|
||||
},
|
||||
files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},
|
||||
entrypoints: [
|
||||
"./src/index.ts",
|
||||
"./src/temporary.ts",
|
||||
parserWorker,
|
||||
workerPath,
|
||||
rgPath,
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import z from "zod"
|
||||
import type { ZodType } from "zod"
|
||||
|
||||
export namespace BusEvent {
|
||||
export type Definition = ReturnType<typeof define>
|
||||
export type Definition = ReturnType<typeof define>
|
||||
|
||||
const registry = new Map<string, Definition>()
|
||||
const registry = new Map<string, Definition>()
|
||||
|
||||
export function define<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
|
||||
export function define<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
|
||||
const result = {
|
||||
type,
|
||||
properties,
|
||||
}
|
||||
registry.set(type, result)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export function payloads() {
|
||||
export function payloads() {
|
||||
return registry
|
||||
.entries()
|
||||
.map(([type, def]) => {
|
||||
@@ -29,5 +28,6 @@ export namespace BusEvent {
|
||||
})
|
||||
})
|
||||
.toArray()
|
||||
}
|
||||
}
|
||||
|
||||
export * as BusEvent from "./bus-event"
|
||||
|
||||
@@ -28,7 +28,7 @@ import { useEvent } from "@tui/context/event"
|
||||
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
||||
import { StartupLoading } from "@tui/component/startup-loading"
|
||||
import { SyncProvider, useSync } from "@tui/context/sync"
|
||||
import { LocalProvider, useLocal } from "@tui/context/local"
|
||||
import { LocalProvider, parseModel, useLocal } from "@tui/context/local"
|
||||
import { DialogModel, useConnected } from "@tui/component/dialog-model"
|
||||
import { DialogMcp } from "@tui/component/dialog-mcp"
|
||||
import { DialogStatus } from "@tui/component/dialog-status"
|
||||
@@ -49,10 +49,8 @@ import { DialogAlert } from "./ui/dialog-alert"
|
||||
import { DialogConfirm } from "./ui/dialog-confirm"
|
||||
import { ToastProvider, useToast } from "./ui/toast"
|
||||
import { ExitProvider, useExit } from "./context/exit"
|
||||
import { Session as SessionApi } from "@/session"
|
||||
import { TuiEvent } from "./event"
|
||||
import { KVProvider, useKV } from "./context/kv"
|
||||
import { Provider } from "@/provider"
|
||||
import { ArgsProvider, useArgs, type Args } from "./context/args"
|
||||
import open from "open"
|
||||
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
||||
@@ -304,7 +302,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
|
||||
if (route.data.type === "session") {
|
||||
const session = sync.session.get(route.data.sessionID)
|
||||
if (!session || SessionApi.isDefaultTitle(session.title)) {
|
||||
if (!session) {
|
||||
renderer.setTerminalTitle("OpenCode")
|
||||
return
|
||||
}
|
||||
@@ -324,7 +322,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
batch(() => {
|
||||
if (args.agent) local.agent.set(args.agent)
|
||||
if (args.model) {
|
||||
const { providerID, modelID } = Provider.parseModel(args.model)
|
||||
const { providerID, modelID } = parseModel(args.model)
|
||||
if (!providerID || !modelID)
|
||||
return toast.show({
|
||||
variant: "warning",
|
||||
|
||||
@@ -12,7 +12,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
|
||||
const [store, setStore] = createStore<Record<string, any>>()
|
||||
const filePath = path.join(Global.Path.state, "kv.json")
|
||||
|
||||
Filesystem.readJson(filePath)
|
||||
Filesystem.readJson<Record<string, any>>(filePath)
|
||||
.then((x) => {
|
||||
setStore(x)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import z from "zod"
|
||||
|
||||
export const TuiEvent = {
|
||||
@@ -42,7 +41,7 @@ export const TuiEvent = {
|
||||
SessionSelect: BusEvent.define(
|
||||
"tui.session.select",
|
||||
z.object({
|
||||
sessionID: SessionID.zod.describe("Session ID to navigate to"),
|
||||
sessionID: z.string().describe("Session ID to navigate to"),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ function View(props: { api: TuiPluginApi }) {
|
||||
const [open, setOpen] = createSignal(true)
|
||||
const theme = () => props.api.theme.current
|
||||
const list = createMemo(() => props.api.state.lsp())
|
||||
const off = createMemo(() => props.api.state.config.lsp === false)
|
||||
const off = createMemo(() => props.api.state.config.lsp)
|
||||
|
||||
return (
|
||||
<box>
|
||||
|
||||
@@ -16,7 +16,6 @@ import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
||||
import { Log } from "@/util"
|
||||
import { errorData, errorMessage } from "@/util/error"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { Instance } from "@/project/instance"
|
||||
import {
|
||||
readPackageThemes,
|
||||
readPluginId,
|
||||
@@ -790,10 +789,7 @@ async function addPluginBySpec(state: RuntimeState | undefined, raw: string) {
|
||||
state.pending.delete(spec)
|
||||
return true
|
||||
}
|
||||
const ready = await Instance.provide({
|
||||
directory: state.directory,
|
||||
fn: () => resolveExternalPlugins([cfg], () => TuiConfig.waitForDependencies()),
|
||||
}).catch((error) => {
|
||||
const ready = await resolveExternalPlugins([cfg], () => TuiConfig.waitForDependencies()).catch((error) => {
|
||||
fail("failed to add tui plugin", { path: next, error })
|
||||
return [] as PluginLoad[]
|
||||
})
|
||||
@@ -987,9 +983,6 @@ export namespace TuiPluginRuntime {
|
||||
}
|
||||
runtime = next
|
||||
try {
|
||||
await Instance.provide({
|
||||
directory: cwd,
|
||||
fn: async () => {
|
||||
const records = Flag.OPENCODE_PURE ? [] : (config.plugin_origins ?? [])
|
||||
if (Flag.OPENCODE_PURE && config.plugin_origins?.length) {
|
||||
log.info("skipping external tui plugins in pure mode", { count: config.plugin_origins.length })
|
||||
@@ -1021,8 +1014,6 @@ export namespace TuiPluginRuntime {
|
||||
// and hook chains rely on stable plugin ordering.
|
||||
await activatePluginEntry(next, plugin, false)
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
fail("failed to load tui plugins", { directory: cwd, error })
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ export function FormatError(input: unknown) {
|
||||
// ProviderModelNotFoundError: { providerID: string, modelID: string, suggestions?: string[] }
|
||||
if (NamedError.hasName(input, "ProviderModelNotFoundError")) {
|
||||
const data = (input as ErrorLike).data
|
||||
const suggestions = data?.suggestions as string[] | undefined
|
||||
const suggestions: string[] = Array.isArray(data?.suggestions) ? data.suggestions : []
|
||||
return [
|
||||
`Model not found: ${data?.providerID}/${data?.modelID}`,
|
||||
...(Array.isArray(suggestions) && suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []),
|
||||
...(suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []),
|
||||
`Try: \`opencode models\` to list available models`,
|
||||
`Or check your config (opencode.json) provider/model names`,
|
||||
].join("\n")
|
||||
@@ -64,10 +64,10 @@ export function FormatError(input: unknown) {
|
||||
const data = (input as ErrorLike).data
|
||||
const path = data?.path
|
||||
const message = data?.message
|
||||
const issues = data?.issues as Array<{ message: string; path: string[] }> | undefined
|
||||
const issues: Array<{ message: string; path: string[] }> = Array.isArray(data?.issues) ? data.issues : []
|
||||
return [
|
||||
`Configuration is invalid${path && path !== "config" ? ` at ${path}` : ""}` + (message ? `: ${message}` : ""),
|
||||
...(issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []),
|
||||
...issues.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")),
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ export const Info = z
|
||||
.optional(),
|
||||
lsp: z
|
||||
.union([
|
||||
z.literal(false),
|
||||
z.literal(true),
|
||||
z.record(
|
||||
z.string(),
|
||||
z.union([
|
||||
|
||||
@@ -5,7 +5,7 @@ import os from "os"
|
||||
import { Filesystem } from "@/util"
|
||||
import { InvalidError } from "./error"
|
||||
|
||||
type ParseSource =
|
||||
export type ParseSource =
|
||||
| {
|
||||
type: "path"
|
||||
path: string
|
||||
|
||||
@@ -8,10 +8,9 @@ import { ripgrep } from "ripgrep"
|
||||
import { Filesystem } from "@/util"
|
||||
import { Log } from "@/util"
|
||||
|
||||
export namespace Ripgrep {
|
||||
const log = Log.create({ service: "ripgrep" })
|
||||
const log = Log.create({ service: "ripgrep" })
|
||||
|
||||
const Stats = z.object({
|
||||
const Stats = z.object({
|
||||
elapsed: z.object({
|
||||
secs: z.number(),
|
||||
nanos: z.number(),
|
||||
@@ -23,18 +22,18 @@ export namespace Ripgrep {
|
||||
bytes_printed: z.number(),
|
||||
matched_lines: z.number(),
|
||||
matches: z.number(),
|
||||
})
|
||||
})
|
||||
|
||||
const Begin = z.object({
|
||||
const Begin = z.object({
|
||||
type: z.literal("begin"),
|
||||
data: z.object({
|
||||
path: z.object({
|
||||
text: z.string(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
export const Match = z.object({
|
||||
export const Match = z.object({
|
||||
type: z.literal("match"),
|
||||
data: z.object({
|
||||
path: z.object({
|
||||
@@ -55,9 +54,9 @@ export namespace Ripgrep {
|
||||
}),
|
||||
),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
const End = z.object({
|
||||
const End = z.object({
|
||||
type: z.literal("end"),
|
||||
data: z.object({
|
||||
path: z.object({
|
||||
@@ -66,9 +65,9 @@ export namespace Ripgrep {
|
||||
binary_offset: z.number().nullable(),
|
||||
stats: Stats,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
const Summary = z.object({
|
||||
const Summary = z.object({
|
||||
type: z.literal("summary"),
|
||||
data: z.object({
|
||||
elapsed_total: z.object({
|
||||
@@ -78,33 +77,33 @@ export namespace Ripgrep {
|
||||
}),
|
||||
stats: Stats,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
const Result = z.union([Begin, Match, End, Summary])
|
||||
const Result = z.union([Begin, Match, End, Summary])
|
||||
|
||||
export type Result = z.infer<typeof Result>
|
||||
export type Match = z.infer<typeof Match>
|
||||
export type Item = Match["data"]
|
||||
export type Begin = z.infer<typeof Begin>
|
||||
export type End = z.infer<typeof End>
|
||||
export type Summary = z.infer<typeof Summary>
|
||||
export type Row = Match["data"]
|
||||
export type Result = z.infer<typeof Result>
|
||||
export type Match = z.infer<typeof Match>
|
||||
export type Item = Match["data"]
|
||||
export type Begin = z.infer<typeof Begin>
|
||||
export type End = z.infer<typeof End>
|
||||
export type Summary = z.infer<typeof Summary>
|
||||
export type Row = Match["data"]
|
||||
|
||||
export interface SearchResult {
|
||||
export interface SearchResult {
|
||||
items: Item[]
|
||||
partial: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface FilesInput {
|
||||
export interface FilesInput {
|
||||
cwd: string
|
||||
glob?: string[]
|
||||
hidden?: boolean
|
||||
follow?: boolean
|
||||
maxDepth?: number
|
||||
signal?: AbortSignal
|
||||
}
|
||||
}
|
||||
|
||||
export interface SearchInput {
|
||||
export interface SearchInput {
|
||||
cwd: string
|
||||
pattern: string
|
||||
glob?: string[]
|
||||
@@ -112,91 +111,91 @@ export namespace Ripgrep {
|
||||
follow?: boolean
|
||||
file?: string[]
|
||||
signal?: AbortSignal
|
||||
}
|
||||
}
|
||||
|
||||
export interface TreeInput {
|
||||
export interface TreeInput {
|
||||
cwd: string
|
||||
limit?: number
|
||||
signal?: AbortSignal
|
||||
}
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
export interface Interface {
|
||||
readonly files: (input: FilesInput) => Stream.Stream<string, Error>
|
||||
readonly tree: (input: TreeInput) => Effect.Effect<string, Error>
|
||||
readonly search: (input: SearchInput) => Effect.Effect<SearchResult, Error>
|
||||
}
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
|
||||
|
||||
type Run = { kind: "files" | "search"; cwd: string; args: string[] }
|
||||
type Run = { kind: "files" | "search"; cwd: string; args: string[] }
|
||||
|
||||
type WorkerResult = {
|
||||
type WorkerResult = {
|
||||
type: "result"
|
||||
code: number
|
||||
stdout: string
|
||||
stderr: string
|
||||
}
|
||||
}
|
||||
|
||||
type WorkerLine = {
|
||||
type WorkerLine = {
|
||||
type: "line"
|
||||
line: string
|
||||
}
|
||||
}
|
||||
|
||||
type WorkerDone = {
|
||||
type WorkerDone = {
|
||||
type: "done"
|
||||
code: number
|
||||
stderr: string
|
||||
}
|
||||
}
|
||||
|
||||
type WorkerError = {
|
||||
type WorkerError = {
|
||||
type: "error"
|
||||
error: {
|
||||
message: string
|
||||
name?: string
|
||||
stack?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function env() {
|
||||
function env() {
|
||||
const env = Object.fromEntries(
|
||||
Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined),
|
||||
)
|
||||
delete env.RIPGREP_CONFIG_PATH
|
||||
return env
|
||||
}
|
||||
}
|
||||
|
||||
function text(input: unknown) {
|
||||
function text(input: unknown) {
|
||||
if (typeof input === "string") return input
|
||||
if (input instanceof ArrayBuffer) return Buffer.from(input).toString()
|
||||
if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString()
|
||||
return String(input)
|
||||
}
|
||||
}
|
||||
|
||||
function toError(input: unknown) {
|
||||
function toError(input: unknown) {
|
||||
if (input instanceof Error) return input
|
||||
if (typeof input === "string") return new Error(input)
|
||||
return new Error(String(input))
|
||||
}
|
||||
}
|
||||
|
||||
function abort(signal?: AbortSignal) {
|
||||
function abort(signal?: AbortSignal) {
|
||||
const err = signal?.reason
|
||||
if (err instanceof Error) return err
|
||||
const out = new Error("Aborted")
|
||||
out.name = "AbortError"
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
function error(stderr: string, code: number) {
|
||||
function error(stderr: string, code: number) {
|
||||
const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`)
|
||||
err.name = "RipgrepError"
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
function clean(file: string) {
|
||||
function clean(file: string) {
|
||||
return path.normalize(file.replace(/^\.[\\/]/, ""))
|
||||
}
|
||||
}
|
||||
|
||||
function row(data: Row): Row {
|
||||
function row(data: Row): Row {
|
||||
return {
|
||||
...data,
|
||||
path: {
|
||||
@@ -204,16 +203,16 @@ export namespace Ripgrep {
|
||||
text: clean(data.path.text),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function opts(cwd: string) {
|
||||
function opts(cwd: string) {
|
||||
return {
|
||||
env: env(),
|
||||
preopens: { ".": cwd },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function check(cwd: string) {
|
||||
function check(cwd: string) {
|
||||
return Effect.tryPromise({
|
||||
try: () => fs.stat(cwd).catch(() => undefined),
|
||||
catch: toError,
|
||||
@@ -230,9 +229,9 @@ export namespace Ripgrep {
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function filesArgs(input: FilesInput) {
|
||||
function filesArgs(input: FilesInput) {
|
||||
const args = ["--files", "--glob=!.git/*"]
|
||||
if (input.follow) args.push("--follow")
|
||||
if (input.hidden !== false) args.push("--hidden")
|
||||
@@ -244,9 +243,9 @@ export namespace Ripgrep {
|
||||
}
|
||||
args.push(".")
|
||||
return args
|
||||
}
|
||||
}
|
||||
|
||||
function searchArgs(input: SearchInput) {
|
||||
function searchArgs(input: SearchInput) {
|
||||
const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"]
|
||||
if (input.follow) args.push("--follow")
|
||||
if (input.glob) {
|
||||
@@ -257,20 +256,20 @@ export namespace Ripgrep {
|
||||
if (input.limit) args.push(`--max-count=${input.limit}`)
|
||||
args.push("--", input.pattern, ...(input.file ?? ["."]))
|
||||
return args
|
||||
}
|
||||
}
|
||||
|
||||
function parse(stdout: string) {
|
||||
function parse(stdout: string) {
|
||||
return stdout
|
||||
.trim()
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => Result.parse(JSON.parse(line)))
|
||||
.flatMap((item) => (item.type === "match" ? [row(item.data)] : []))
|
||||
}
|
||||
}
|
||||
|
||||
declare const OPENCODE_RIPGREP_WORKER_PATH: string
|
||||
declare const OPENCODE_RIPGREP_WORKER_PATH: string
|
||||
|
||||
function target(): Effect.Effect<string | URL, Error> {
|
||||
function target(): Effect.Effect<string | URL, Error> {
|
||||
if (typeof OPENCODE_RIPGREP_WORKER_PATH !== "undefined") {
|
||||
return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH)
|
||||
}
|
||||
@@ -279,26 +278,26 @@ export namespace Ripgrep {
|
||||
try: () => Filesystem.exists(fileURLToPath(js)),
|
||||
catch: toError,
|
||||
}).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url))))
|
||||
}
|
||||
}
|
||||
|
||||
function worker() {
|
||||
function worker() {
|
||||
return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() }))))
|
||||
}
|
||||
}
|
||||
|
||||
function drain(buf: string, chunk: unknown, push: (line: string) => void) {
|
||||
function drain(buf: string, chunk: unknown, push: (line: string) => void) {
|
||||
const lines = (buf + text(chunk)).split(/\r?\n/)
|
||||
buf = lines.pop() || ""
|
||||
for (const line of lines) {
|
||||
if (line) push(line)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
}
|
||||
|
||||
function fail(queue: Queue.Queue<string, Error | Cause.Done>, err: Error) {
|
||||
function fail(queue: Queue.Queue<string, Error | Cause.Done>, err: Error) {
|
||||
Queue.failCauseUnsafe(queue, Cause.fail(err))
|
||||
}
|
||||
}
|
||||
|
||||
function searchDirect(input: SearchInput) {
|
||||
function searchDirect(input: SearchInput) {
|
||||
return Effect.tryPromise({
|
||||
try: () =>
|
||||
ripgrep(searchArgs(input), {
|
||||
@@ -318,9 +317,9 @@ export namespace Ripgrep {
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function searchWorker(input: SearchInput) {
|
||||
function searchWorker(input: SearchInput) {
|
||||
if (input.signal?.aborted) return Effect.fail(abort(input.signal))
|
||||
|
||||
return Effect.acquireUseRelease(
|
||||
@@ -377,9 +376,9 @@ export namespace Ripgrep {
|
||||
}),
|
||||
(w) => Effect.sync(() => w.terminate()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function filesDirect(input: FilesInput) {
|
||||
function filesDirect(input: FilesInput) {
|
||||
return Stream.callback<string, Error>(
|
||||
Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
|
||||
let buf = ""
|
||||
@@ -427,9 +426,9 @@ export namespace Ripgrep {
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function filesWorker(input: FilesInput) {
|
||||
function filesWorker(input: FilesInput) {
|
||||
return Stream.callback<string, Error>(
|
||||
Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
|
||||
if (input.signal?.aborted) {
|
||||
@@ -489,9 +488,9 @@ export namespace Ripgrep {
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const source = (input: FilesInput) => {
|
||||
@@ -569,7 +568,8 @@ export namespace Ripgrep {
|
||||
|
||||
return Service.of({ files, tree, search })
|
||||
}),
|
||||
)
|
||||
)
|
||||
|
||||
export const defaultLayer = layer
|
||||
}
|
||||
export const defaultLayer = layer
|
||||
|
||||
export * as Ripgrep from "./ripgrep"
|
||||
|
||||
@@ -5,39 +5,38 @@ import { Flag } from "@/flag/flag"
|
||||
import type { SessionID } from "@/session/schema"
|
||||
import { Log } from "../util"
|
||||
|
||||
export namespace FileTime {
|
||||
const log = Log.create({ service: "file.time" })
|
||||
const log = Log.create({ service: "file.time" })
|
||||
|
||||
export type Stamp = {
|
||||
export type Stamp = {
|
||||
readonly read: Date
|
||||
readonly mtime: number | undefined
|
||||
readonly size: number | undefined
|
||||
}
|
||||
}
|
||||
|
||||
const session = (reads: Map<SessionID, Map<string, Stamp>>, sessionID: SessionID) => {
|
||||
const session = (reads: Map<SessionID, Map<string, Stamp>>, sessionID: SessionID) => {
|
||||
const value = reads.get(sessionID)
|
||||
if (value) return value
|
||||
|
||||
const next = new Map<string, Stamp>()
|
||||
reads.set(sessionID, next)
|
||||
return next
|
||||
}
|
||||
}
|
||||
|
||||
interface State {
|
||||
interface State {
|
||||
reads: Map<SessionID, Map<string, Stamp>>
|
||||
locks: Map<string, Semaphore.Semaphore>
|
||||
}
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
export interface Interface {
|
||||
readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void>
|
||||
readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined>
|
||||
readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void>
|
||||
readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T>
|
||||
}
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
@@ -107,7 +106,8 @@ export namespace FileTime {
|
||||
|
||||
return Service.of({ read, get, assert, withLock })
|
||||
}),
|
||||
).pipe(Layer.orDie)
|
||||
).pipe(Layer.orDie)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
||||
}
|
||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
||||
|
||||
export * as FileTime from "./time"
|
||||
|
||||
@@ -19,11 +19,10 @@ import { Log } from "../util"
|
||||
|
||||
declare const OPENCODE_LIBC: string | undefined
|
||||
|
||||
export namespace FileWatcher {
|
||||
const log = Log.create({ service: "file.watcher" })
|
||||
const SUBSCRIBE_TIMEOUT_MS = 10_000
|
||||
const log = Log.create({ service: "file.watcher" })
|
||||
const SUBSCRIBE_TIMEOUT_MS = 10_000
|
||||
|
||||
export const Event = {
|
||||
export const Event = {
|
||||
Updated: BusEvent.define(
|
||||
"file.watcher.updated",
|
||||
z.object({
|
||||
@@ -31,9 +30,9 @@ export namespace FileWatcher {
|
||||
event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]),
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
|
||||
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
|
||||
try {
|
||||
const binding = require(
|
||||
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${OPENCODE_LIBC || "glibc"}` : ""}`,
|
||||
@@ -43,30 +42,30 @@ export namespace FileWatcher {
|
||||
log.error("failed to load watcher binding", { error })
|
||||
return
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function getBackend() {
|
||||
function getBackend() {
|
||||
if (process.platform === "win32") return "windows"
|
||||
if (process.platform === "darwin") return "fs-events"
|
||||
if (process.platform === "linux") return "inotify"
|
||||
}
|
||||
}
|
||||
|
||||
function protecteds(dir: string) {
|
||||
function protecteds(dir: string) {
|
||||
return Protected.paths().filter((item) => {
|
||||
const rel = path.relative(dir, item)
|
||||
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const hasNativeBinding = () => !!watcher()
|
||||
export const hasNativeBinding = () => !!watcher()
|
||||
|
||||
export interface Interface {
|
||||
export interface Interface {
|
||||
readonly init: () => Effect.Effect<void>
|
||||
}
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/FileWatcher") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/FileWatcher") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
@@ -157,7 +156,8 @@ export namespace FileWatcher {
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||
}
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||
|
||||
export * as FileWatcher from "./watcher"
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import z from "zod"
|
||||
import { randomBytes } from "crypto"
|
||||
|
||||
export namespace Identifier {
|
||||
const prefixes = {
|
||||
const prefixes = {
|
||||
event: "evt",
|
||||
session: "ses",
|
||||
message: "msg",
|
||||
@@ -14,27 +13,27 @@ export namespace Identifier {
|
||||
tool: "tool",
|
||||
workspace: "wrk",
|
||||
entry: "ent",
|
||||
} as const
|
||||
} as const
|
||||
|
||||
export function schema(prefix: keyof typeof prefixes) {
|
||||
export function schema(prefix: keyof typeof prefixes) {
|
||||
return z.string().startsWith(prefixes[prefix])
|
||||
}
|
||||
}
|
||||
|
||||
const LENGTH = 26
|
||||
const LENGTH = 26
|
||||
|
||||
// State for monotonic ID generation
|
||||
let lastTimestamp = 0
|
||||
let counter = 0
|
||||
// State for monotonic ID generation
|
||||
let lastTimestamp = 0
|
||||
let counter = 0
|
||||
|
||||
export function ascending(prefix: keyof typeof prefixes, given?: string) {
|
||||
export function ascending(prefix: keyof typeof prefixes, given?: string) {
|
||||
return generateID(prefix, "ascending", given)
|
||||
}
|
||||
}
|
||||
|
||||
export function descending(prefix: keyof typeof prefixes, given?: string) {
|
||||
export function descending(prefix: keyof typeof prefixes, given?: string) {
|
||||
return generateID(prefix, "descending", given)
|
||||
}
|
||||
}
|
||||
|
||||
function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string {
|
||||
function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string {
|
||||
if (!given) {
|
||||
return create(prefixes[prefix], direction)
|
||||
}
|
||||
@@ -43,9 +42,9 @@ export namespace Identifier {
|
||||
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
|
||||
}
|
||||
return given
|
||||
}
|
||||
}
|
||||
|
||||
function randomBase62(length: number): string {
|
||||
function randomBase62(length: number): string {
|
||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
let result = ""
|
||||
const bytes = randomBytes(length)
|
||||
@@ -53,9 +52,9 @@ export namespace Identifier {
|
||||
result += chars[bytes[i] % 62]
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string {
|
||||
export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string {
|
||||
const currentTimestamp = timestamp ?? Date.now()
|
||||
|
||||
if (currentTimestamp !== lastTimestamp) {
|
||||
@@ -74,13 +73,14 @@ export namespace Identifier {
|
||||
}
|
||||
|
||||
return prefix + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12)
|
||||
}
|
||||
}
|
||||
|
||||
/** Extract timestamp from an ascending ID. Does not work with descending IDs. */
|
||||
export function timestamp(id: string): number {
|
||||
/** Extract timestamp from an ascending ID. Does not work with descending IDs. */
|
||||
export function timestamp(id: string): number {
|
||||
const prefix = id.split("_")[0]
|
||||
const hex = id.slice(prefix.length + 1, prefix.length + 13)
|
||||
const encoded = BigInt("0x" + hex)
|
||||
return Number(encoded / BigInt(0x1000))
|
||||
}
|
||||
}
|
||||
|
||||
export * as Identifier from "./id"
|
||||
|
||||
@@ -167,7 +167,7 @@ export const layer = Layer.effect(
|
||||
|
||||
const servers: Record<string, LSPServer.Info> = {}
|
||||
|
||||
if (cfg.lsp === false) {
|
||||
if (!cfg.lsp) {
|
||||
log.info("all LSPs are disabled")
|
||||
} else {
|
||||
for (const server of Object.values(LSPServer)) {
|
||||
@@ -440,12 +440,11 @@ export const layer = Layer.effect(
|
||||
const workspaceSymbol = Effect.fn("LSP.workspaceSymbol")(function* (query: string) {
|
||||
const results = yield* runAll((client) =>
|
||||
client.connection
|
||||
.sendRequest("workspace/symbol", { query })
|
||||
.then((result: any) => result.filter((x: Symbol) => kinds.includes(x.kind)))
|
||||
.then((result: any) => result.slice(0, 10))
|
||||
.catch(() => []),
|
||||
.sendRequest<Symbol[]>("workspace/symbol", { query })
|
||||
.then((result) => result.filter((x) => kinds.includes(x.kind)).slice(0, 10))
|
||||
.catch(() => [] as Symbol[]),
|
||||
)
|
||||
return results.flat() as Symbol[]
|
||||
return results.flat()
|
||||
})
|
||||
|
||||
const prepareCallHierarchy = Effect.fn("LSP.prepareCallHierarchy")(function* (input: LocInput) {
|
||||
|
||||
@@ -124,8 +124,17 @@ export async function install(dir: string) {
|
||||
return
|
||||
}
|
||||
|
||||
const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({}))
|
||||
const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({}))
|
||||
type PackageDeps = Record<string, string>
|
||||
type PackageJson = {
|
||||
dependencies?: PackageDeps
|
||||
devDependencies?: PackageDeps
|
||||
peerDependencies?: PackageDeps
|
||||
optionalDependencies?: PackageDeps
|
||||
}
|
||||
const pkg: PackageJson = await Filesystem.readJson<PackageJson>(path.join(dir, "package.json")).catch(() => ({}))
|
||||
const lock: { packages?: Record<string, PackageJson> } = await Filesystem.readJson<{
|
||||
packages?: Record<string, PackageJson>
|
||||
}>(path.join(dir, "package-lock.json")).catch(() => ({}))
|
||||
|
||||
const declared = new Set([
|
||||
...Object.keys(pkg.dependencies || {}),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
|
||||
import type { Model } from "@opencode-ai/sdk/v2"
|
||||
import { Installation } from "@/installation"
|
||||
import { InstallationVersion } from "@/installation/version"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Log } from "../../util"
|
||||
|
||||
@@ -11,6 +11,11 @@ export namespace CopilotModels {
|
||||
// every version looks like: `{model.id}-YYYY-MM-DD`
|
||||
version: z.string(),
|
||||
supported_endpoints: z.array(z.string()).optional(),
|
||||
policy: z
|
||||
.object({
|
||||
state: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
capabilities: z.object({
|
||||
family: z.string(),
|
||||
limits: z.object({
|
||||
@@ -123,7 +128,9 @@ export namespace CopilotModels {
|
||||
})
|
||||
|
||||
const result = { ...existing }
|
||||
const remote = new Map(data.data.filter((m) => m.model_picker_enabled).map((m) => [m.id, m] as const))
|
||||
const remote = new Map(
|
||||
data.data.filter((m) => m.model_picker_enabled && m.policy?.state !== "disabled").map((m) => [m.id, m] as const),
|
||||
)
|
||||
|
||||
// prune existing models whose api.id isn't in the endpoint response
|
||||
for (const [key, model] of Object.entries(result)) {
|
||||
|
||||
@@ -547,12 +547,14 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
|
||||
},
|
||||
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
||||
if (modelID.startsWith("duo-workflow-")) {
|
||||
const workflowRef = options?.workflowRef as string | undefined
|
||||
const workflowRef = typeof options?.workflowRef === "string" ? options.workflowRef : undefined
|
||||
// Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef
|
||||
const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow"
|
||||
const workflowDefinition =
|
||||
typeof options?.workflowDefinition === "string" ? options.workflowDefinition : undefined
|
||||
const model = sdk.workflowChat(sdkModelID, {
|
||||
featureFlags,
|
||||
workflowDefinition: options?.workflowDefinition as string | undefined,
|
||||
workflowDefinition,
|
||||
})
|
||||
if (workflowRef) {
|
||||
model.selectedModelRef = workflowRef
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { AsyncQueue } from "../../util/queue"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
import { SessionID } from "@/session/schema"
|
||||
|
||||
const TuiRequest = z.object({
|
||||
path: z.string(),
|
||||
@@ -371,7 +372,7 @@ export const TuiRoutes = lazy(() =>
|
||||
validator("json", TuiEvent.SessionSelect.properties),
|
||||
async (c) => {
|
||||
const { sessionID } = c.req.valid("json")
|
||||
await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID)))
|
||||
await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(SessionID.make(sessionID))))
|
||||
await Bus.publish(TuiEvent.SessionSelect, { sessionID })
|
||||
return c.json(true)
|
||||
},
|
||||
|
||||
@@ -3,11 +3,10 @@ import { Bonjour } from "bonjour-service"
|
||||
|
||||
const log = Log.create({ service: "mdns" })
|
||||
|
||||
export namespace MDNS {
|
||||
let bonjour: Bonjour | undefined
|
||||
let currentPort: number | undefined
|
||||
let bonjour: Bonjour | undefined
|
||||
let currentPort: number | undefined
|
||||
|
||||
export function publish(port: number, domain?: string) {
|
||||
export function publish(port: number, domain?: string) {
|
||||
if (currentPort === port) return
|
||||
if (bonjour) unpublish()
|
||||
|
||||
@@ -42,9 +41,9 @@ export namespace MDNS {
|
||||
bonjour = undefined
|
||||
currentPort = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function unpublish() {
|
||||
export function unpublish() {
|
||||
if (bonjour) {
|
||||
try {
|
||||
bonjour.unpublishAll()
|
||||
@@ -56,5 +55,6 @@ export namespace MDNS {
|
||||
currentPort = undefined
|
||||
log.info("mDNS service unpublished")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export * as MDNS from "./mdns"
|
||||
|
||||
@@ -101,15 +101,14 @@ const app = (upgrade: UpgradeWebSocket) =>
|
||||
}),
|
||||
)
|
||||
|
||||
export namespace ServerProxy {
|
||||
const log = Log.Default.clone().tag("service", "server-proxy")
|
||||
const log = Log.Default.clone().tag("service", "server-proxy")
|
||||
|
||||
export async function http(
|
||||
export async function http(
|
||||
url: string | URL,
|
||||
extra: HeadersInit | undefined,
|
||||
req: Request,
|
||||
workspaceID: WorkspaceID,
|
||||
) {
|
||||
) {
|
||||
if (!Workspace.isSyncing(workspaceID)) {
|
||||
return new Response(`broken sync connection for workspace: ${workspaceID}`, {
|
||||
status: 503,
|
||||
@@ -150,15 +149,15 @@ export namespace ServerProxy {
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function websocket(
|
||||
export function websocket(
|
||||
upgrade: UpgradeWebSocket,
|
||||
target: string | URL,
|
||||
extra: HeadersInit | undefined,
|
||||
req: Request,
|
||||
env: unknown,
|
||||
) {
|
||||
) {
|
||||
const proxy = new URL(req.url)
|
||||
proxy.pathname = "/__workspace_ws"
|
||||
proxy.search = ""
|
||||
@@ -179,5 +178,6 @@ export namespace ServerProxy {
|
||||
}),
|
||||
env as never,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export * as ServerProxy from "./proxy"
|
||||
|
||||
@@ -17,19 +17,18 @@ globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
|
||||
initProjectors()
|
||||
|
||||
export namespace Server {
|
||||
const log = Log.create({ service: "server" })
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
export type Listener = {
|
||||
export type Listener = {
|
||||
hostname: string
|
||||
port: number
|
||||
url: URL
|
||||
stop: (close?: boolean) => Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
export const Default = lazy(() => create({}))
|
||||
export const Default = lazy(() => create({}))
|
||||
|
||||
function create(opts: { cors?: string[] }) {
|
||||
function create(opts: { cors?: string[] }) {
|
||||
const app = new Hono()
|
||||
const runtime = adapter.create(app)
|
||||
|
||||
@@ -60,9 +59,9 @@ export namespace Server {
|
||||
.route("/", UIRoutes()),
|
||||
runtime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function openapi() {
|
||||
export async function openapi() {
|
||||
// Build a fresh app with all routes registered directly so
|
||||
// hono-openapi can see describeRoute metadata (`.route()` wraps
|
||||
// handlers when the sub-app has a custom errorHandler, which
|
||||
@@ -79,17 +78,17 @@ export namespace Server {
|
||||
},
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export let url: URL
|
||||
export let url: URL
|
||||
|
||||
export async function listen(opts: {
|
||||
export async function listen(opts: {
|
||||
port: number
|
||||
hostname: string
|
||||
mdns?: boolean
|
||||
mdnsDomain?: string
|
||||
cors?: string[]
|
||||
}): Promise<Listener> {
|
||||
}): Promise<Listener> {
|
||||
const built = create(opts)
|
||||
const server = await built.runtime.listen(opts)
|
||||
|
||||
@@ -123,5 +122,6 @@ export namespace Server {
|
||||
return closing
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export * as Server from "./server"
|
||||
|
||||
@@ -272,7 +272,8 @@ export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsa
|
||||
input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0,
|
||||
)
|
||||
const cacheWriteInputTokens = safe(
|
||||
(input.usage.inputTokenDetails?.cacheWriteTokens ??
|
||||
Number(
|
||||
input.usage.inputTokenDetails?.cacheWriteTokens ??
|
||||
input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
||||
// google-vertex-anthropic returns metadata under "vertex" key
|
||||
// (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages')
|
||||
@@ -281,7 +282,8 @@ export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsa
|
||||
input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
|
||||
// @ts-expect-error
|
||||
input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ??
|
||||
0) as number,
|
||||
0,
|
||||
),
|
||||
)
|
||||
|
||||
// AI SDK v6 normalized inputTokens to include cached tokens across all providers
|
||||
|
||||
@@ -1,33 +1,10 @@
|
||||
import yargs from "yargs"
|
||||
import { TuiThreadCommand } from "./cli/cmd/tui/thread"
|
||||
import { InstallationVersion } from "./installation/version"
|
||||
import { hideBin } from "yargs/helpers"
|
||||
import { Log } from "./node"
|
||||
|
||||
Log.init({
|
||||
print: false,
|
||||
})
|
||||
|
||||
const cli = yargs(hideBin(process.argv))
|
||||
.parserConfiguration({ "populate--": true })
|
||||
.scriptName("opencode")
|
||||
.wrap(100)
|
||||
.help("help", "show help")
|
||||
.alias("help", "h")
|
||||
.version("version", "show version number", InstallationVersion)
|
||||
.alias("version", "v")
|
||||
.option("print-logs", {
|
||||
describe: "print logs to stderr",
|
||||
type: "boolean",
|
||||
})
|
||||
.option("log-level", {
|
||||
describe: "log level",
|
||||
type: "string",
|
||||
choices: ["DEBUG", "INFO", "WARN", "ERROR"],
|
||||
})
|
||||
.option("pure", {
|
||||
describe: "run without external plugins",
|
||||
type: "boolean",
|
||||
})
|
||||
.command(TuiThreadCommand)
|
||||
.parse()
|
||||
console.log(TuiThreadCommand)
|
||||
|
||||
console.log(performance.now())
|
||||
|
||||
@@ -19,7 +19,7 @@ export type Context<M extends Metadata = Metadata> = {
|
||||
agent: string
|
||||
abort: AbortSignal
|
||||
callID?: string
|
||||
extra?: { [key: string]: any }
|
||||
extra?: { [key: string]: unknown }
|
||||
messages: MessageV2.WithParts[]
|
||||
metadata(input: { title?: string; metadata?: M }): Effect.Effect<void>
|
||||
ask(input: Omit<Permission.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function readText(p: string): Promise<string> {
|
||||
return readFile(p, "utf-8")
|
||||
}
|
||||
|
||||
export async function readJson<T = any>(p: string): Promise<T> {
|
||||
export async function readJson<T = unknown>(p: string): Promise<T> {
|
||||
return JSON.parse(await readFile(p, "utf-8"))
|
||||
}
|
||||
|
||||
|
||||
@@ -757,7 +757,7 @@ test("updates config and writes to file", async () => {
|
||||
const newConfig = { model: "updated/model" }
|
||||
await save(newConfig as any)
|
||||
|
||||
const writtenConfig = await Filesystem.readJson(path.join(tmp.path, "config.json"))
|
||||
const writtenConfig = await Filesystem.readJson<{ model: string }>(path.join(tmp.path, "config.json"))
|
||||
expect(writtenConfig.model).toBe("updated/model")
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1601,7 +1601,7 @@ export type Config = {
|
||||
}
|
||||
}
|
||||
lsp?:
|
||||
| false
|
||||
| true
|
||||
| {
|
||||
[key: string]:
|
||||
| {
|
||||
|
||||
@@ -6938,8 +6938,7 @@
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"description": "Session ID to navigate to",
|
||||
"type": "string",
|
||||
"pattern": "^ses.*"
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID"]
|
||||
@@ -8511,8 +8510,7 @@
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"description": "Session ID to navigate to",
|
||||
"type": "string",
|
||||
"pattern": "^ses.*"
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID"]
|
||||
@@ -11761,7 +11759,7 @@
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"const": false
|
||||
"const": true
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
||||
Reference in New Issue
Block a user