mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 02:49:57 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84a1b0280e |
@@ -9,7 +9,6 @@ import { Context, Effect, Layer } from "effect"
|
|||||||
import { stringifyKeyStroke } from "@opentui/keymap"
|
import { stringifyKeyStroke } from "@opentui/keymap"
|
||||||
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
||||||
import { TuiKeybind } from "@/cli/cmd/tui/config/keybind"
|
import { TuiKeybind } from "@/cli/cmd/tui/config/keybind"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
|
||||||
import { reusePendingTask } from "./runtime.shared"
|
import { reusePendingTask } from "./runtime.shared"
|
||||||
import { resolveSession, sessionHistory } from "./session.shared"
|
import { resolveSession, sessionHistory } from "./session.shared"
|
||||||
import type { FooterKeybinds, RunDiffStyle, RunInput, RunPrompt, RunProvider } from "./types"
|
import type { FooterKeybinds, RunDiffStyle, RunInput, RunPrompt, RunProvider } from "./types"
|
||||||
@@ -28,6 +27,8 @@ const DEFAULT_KEYBINDS: FooterKeybinds = {
|
|||||||
inputNewline: [{ key: "shift+return,ctrl+return,alt+return,ctrl+j" }],
|
inputNewline: [{ key: "shift+return,ctrl+return,alt+return,ctrl+j" }],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defaultKeybinds: FooterKeybinds = DEFAULT_KEYBINDS
|
||||||
|
|
||||||
export type ModelInfo = {
|
export type ModelInfo = {
|
||||||
providers: RunProvider[]
|
providers: RunProvider[]
|
||||||
variants: string[]
|
variants: string[]
|
||||||
@@ -41,7 +42,8 @@ export type SessionInfo = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Config = Awaited<ReturnType<typeof TuiConfig.get>>
|
type Config = Awaited<ReturnType<typeof TuiConfig.get>>
|
||||||
type BootService = {
|
|
||||||
|
export interface Interface {
|
||||||
readonly resolveModelInfo: (
|
readonly resolveModelInfo: (
|
||||||
sdk: RunInput["sdk"],
|
sdk: RunInput["sdk"],
|
||||||
directory: string,
|
directory: string,
|
||||||
@@ -58,13 +60,13 @@ type BootService = {
|
|||||||
|
|
||||||
const configTask: { current?: Promise<Config> } = {}
|
const configTask: { current?: Promise<Config> } = {}
|
||||||
|
|
||||||
class Service extends Context.Service<Service, BootService>()("@opencode/RunBoot") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/RunBoot") {}
|
||||||
|
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
return reusePendingTask(configTask, () => TuiConfig.get())
|
return reusePendingTask(configTask, () => TuiConfig.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyModelInfo(): ModelInfo {
|
export function emptyModelInfo(): ModelInfo {
|
||||||
return {
|
return {
|
||||||
providers: [],
|
providers: [],
|
||||||
variants: [],
|
variants: [],
|
||||||
@@ -72,7 +74,7 @@ function emptyModelInfo(): ModelInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptySessionInfo(): SessionInfo {
|
export function emptySessionInfo(): SessionInfo {
|
||||||
return {
|
return {
|
||||||
first: true,
|
first: true,
|
||||||
history: [],
|
history: [],
|
||||||
@@ -105,7 +107,7 @@ function footerKeybinds(config: Config | undefined): FooterKeybinds {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const config = Effect.fn("RunBoot.config")(() => Effect.promise(() => loadConfig().catch(() => undefined)))
|
const config = Effect.fn("RunBoot.config")(() => Effect.promise(() => loadConfig().catch(() => undefined)))
|
||||||
@@ -192,31 +194,6 @@ const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const runtime = makeRuntime(Service, layer)
|
export const defaultLayer = layer
|
||||||
|
|
||||||
// Fetches available variants and context limits for every provider/model pair.
|
export * as RunBoot from "./runtime.boot"
|
||||||
export async function resolveModelInfo(
|
|
||||||
sdk: RunInput["sdk"],
|
|
||||||
directory: string,
|
|
||||||
model: RunInput["model"],
|
|
||||||
): Promise<ModelInfo> {
|
|
||||||
return runtime.runPromise((svc) => svc.resolveModelInfo(sdk, directory, model)).catch(() => emptyModelInfo())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetches session messages to determine if this is the first turn and build prompt history.
|
|
||||||
export async function resolveSessionInfo(
|
|
||||||
sdk: RunInput["sdk"],
|
|
||||||
sessionID: string,
|
|
||||||
model: RunInput["model"],
|
|
||||||
): Promise<SessionInfo> {
|
|
||||||
return runtime.runPromise((svc) => svc.resolveSessionInfo(sdk, sessionID, model)).catch(() => emptySessionInfo())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reads keybind overrides from TUI config and merges them with defaults.
|
|
||||||
export async function resolveFooterKeybinds(): Promise<FooterKeybinds> {
|
|
||||||
return runtime.runPromise((svc) => svc.resolveFooterKeybinds()).catch(() => DEFAULT_KEYBINDS)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resolveDiffStyle(): Promise<RunDiffStyle> {
|
|
||||||
return runtime.runPromise((svc) => svc.resolveDiffStyle()).catch(() => "auto")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,13 +14,33 @@
|
|||||||
// 4. runs the prompt queue until the footer closes.
|
// 4. runs the prompt queue until the footer closes.
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
|
import { Effect, Layer } from "effect"
|
||||||
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { createRunDemo } from "./demo"
|
import { createRunDemo } from "./demo"
|
||||||
import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo, resolveSessionInfo } from "./runtime.boot"
|
import {
|
||||||
|
RunBoot,
|
||||||
|
defaultKeybinds,
|
||||||
|
emptyModelInfo,
|
||||||
|
emptySessionInfo,
|
||||||
|
type ModelInfo,
|
||||||
|
type SessionInfo,
|
||||||
|
} from "./runtime.boot"
|
||||||
import { createRuntimeLifecycle } from "./runtime.lifecycle"
|
import { createRuntimeLifecycle } from "./runtime.lifecycle"
|
||||||
import { recordRunSpanError, setRunSpanAttributes, withRunSpan } from "./otel"
|
import { recordRunSpanError, setRunSpanAttributes, withRunSpan } from "./otel"
|
||||||
import { trace } from "./trace"
|
import { trace } from "./trace"
|
||||||
import { cycleVariant, formatModelLabel, resolveSavedVariant, resolveVariant, saveVariant } from "./variant.shared"
|
import { Variant, cycleVariant, formatModelLabel, resolveVariant } from "./variant.shared"
|
||||||
import type { RunInput, RunPrompt, RunProvider } from "./types"
|
import type { RunDiffStyle, RunInput, RunPrompt, RunProvider } from "./types"
|
||||||
|
|
||||||
|
const BootLayer = Layer.mergeAll(RunBoot.defaultLayer, Variant.defaultLayer)
|
||||||
|
|
||||||
|
function persistVariant(model: RunInput["model"], variant: string | undefined) {
|
||||||
|
AppRuntime.runFork(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const variantSvc = yield* Variant.Service
|
||||||
|
yield* variantSvc.saveVariant(model, variant).pipe(Effect.orElseSucceed(() => undefined))
|
||||||
|
}).pipe(Effect.provide(BootLayer)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** @internal Exported for testing */
|
/** @internal Exported for testing */
|
||||||
export { pickVariant, resolveVariant } from "./variant.shared"
|
export { pickVariant, resolveVariant } from "./variant.shared"
|
||||||
@@ -169,25 +189,44 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
|||||||
async (span) => {
|
async (span) => {
|
||||||
const start = performance.now()
|
const start = performance.now()
|
||||||
const log = trace()
|
const log = trace()
|
||||||
const keybindTask = resolveFooterKeybinds()
|
const earlyTask = AppRuntime.runPromise(
|
||||||
const diffTask = resolveDiffStyle()
|
Effect.gen(function* () {
|
||||||
|
const boot = yield* RunBoot.Service
|
||||||
|
return yield* Effect.all(
|
||||||
|
{
|
||||||
|
keybinds: boot.resolveFooterKeybinds().pipe(Effect.orElseSucceed(() => defaultKeybinds)),
|
||||||
|
diffStyle: boot.resolveDiffStyle().pipe(Effect.orElseSucceed((): RunDiffStyle => "auto")),
|
||||||
|
},
|
||||||
|
{ concurrency: "unbounded" },
|
||||||
|
)
|
||||||
|
}).pipe(Effect.provide(BootLayer)),
|
||||||
|
)
|
||||||
const ctx = await input.boot()
|
const ctx = await input.boot()
|
||||||
const modelTask = resolveModelInfo(ctx.sdk, ctx.directory, ctx.model)
|
const modelTask = AppRuntime.runPromise(
|
||||||
const sessionTask =
|
Effect.gen(function* () {
|
||||||
ctx.resume === true
|
const boot = yield* RunBoot.Service
|
||||||
? resolveSessionInfo(ctx.sdk, ctx.sessionID, ctx.model)
|
return yield* boot.resolveModelInfo(ctx.sdk, ctx.directory, ctx.model)
|
||||||
: Promise.resolve({
|
}).pipe(Effect.provide(BootLayer)),
|
||||||
first: true,
|
).catch((): ModelInfo => emptyModelInfo())
|
||||||
history: [],
|
const sessionAndSavedTask = AppRuntime.runPromise(
|
||||||
variant: undefined,
|
Effect.gen(function* () {
|
||||||
})
|
const boot = yield* RunBoot.Service
|
||||||
const savedTask = resolveSavedVariant(ctx.model)
|
const variantSvc = yield* Variant.Service
|
||||||
const [keybinds, diffStyle, session, savedVariant] = await Promise.all([
|
return yield* Effect.all(
|
||||||
keybindTask,
|
{
|
||||||
diffTask,
|
session:
|
||||||
sessionTask,
|
ctx.resume === true
|
||||||
savedTask,
|
? boot
|
||||||
])
|
.resolveSessionInfo(ctx.sdk, ctx.sessionID, ctx.model)
|
||||||
|
.pipe(Effect.orElseSucceed((): SessionInfo => emptySessionInfo()))
|
||||||
|
: Effect.succeed<SessionInfo>({ first: true, history: [], variant: undefined }),
|
||||||
|
savedVariant: variantSvc.resolveSavedVariant(ctx.model).pipe(Effect.orElseSucceed(() => undefined)),
|
||||||
|
},
|
||||||
|
{ concurrency: "unbounded" },
|
||||||
|
)
|
||||||
|
}).pipe(Effect.provide(BootLayer)),
|
||||||
|
)
|
||||||
|
const [{ keybinds, diffStyle }, { session, savedVariant }] = await Promise.all([earlyTask, sessionAndSavedTask])
|
||||||
const state: RuntimeState = {
|
const state: RuntimeState = {
|
||||||
shown: !session.first,
|
shown: !session.first,
|
||||||
aborting: false,
|
aborting: false,
|
||||||
@@ -280,7 +319,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.activeVariant = cycleVariant(state.activeVariant, state.variants)
|
state.activeVariant = cycleVariant(state.activeVariant, state.variants)
|
||||||
saveVariant(state.model, state.activeVariant)
|
persistVariant(state.model, state.activeVariant)
|
||||||
setRunSpanAttributes(span, {
|
setRunSpanAttributes(span, {
|
||||||
"opencode.model.variant": state.activeVariant,
|
"opencode.model.variant": state.activeVariant,
|
||||||
})
|
})
|
||||||
@@ -298,7 +337,12 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
|||||||
state.model = model
|
state.model = model
|
||||||
state.activeVariant = undefined
|
state.activeVariant = undefined
|
||||||
state.variants = variantsFor(state.providers, model)
|
state.variants = variantsFor(state.providers, model)
|
||||||
const switching = resolveSavedVariant(model).then((saved) => {
|
const switching = AppRuntime.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const variantSvc = yield* Variant.Service
|
||||||
|
return yield* variantSvc.resolveSavedVariant(model).pipe(Effect.orElseSucceed(() => undefined))
|
||||||
|
}).pipe(Effect.provide(BootLayer)),
|
||||||
|
).then((saved) => {
|
||||||
const current = state.model
|
const current = state.model
|
||||||
if (!current || current.providerID !== model.providerID || current.modelID !== model.modelID) {
|
if (!current || current.providerID !== model.providerID || current.modelID !== model.modelID) {
|
||||||
return
|
return
|
||||||
@@ -343,7 +387,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.activeVariant = variant
|
state.activeVariant = variant
|
||||||
saveVariant(state.model, state.activeVariant)
|
persistVariant(state.model, state.activeVariant)
|
||||||
setRunSpanAttributes(span, {
|
setRunSpanAttributes(span, {
|
||||||
"opencode.model.variant": state.activeVariant,
|
"opencode.model.variant": state.activeVariant,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { isRecord } from "@/util/record"
|
import { isRecord } from "@/util/record"
|
||||||
import { createSession, sessionVariant, type RunSession, type SessionMessages } from "./session.shared"
|
import { createSession, sessionVariant, type RunSession, type SessionMessages } from "./session.shared"
|
||||||
@@ -20,16 +19,13 @@ const MODEL_FILE = path.join(Global.Path.state, "model.json")
|
|||||||
type ModelState = Record<string, unknown> & {
|
type ModelState = Record<string, unknown> & {
|
||||||
variant?: Record<string, string | undefined>
|
variant?: Record<string, string | undefined>
|
||||||
}
|
}
|
||||||
type VariantService = {
|
|
||||||
|
export interface Interface {
|
||||||
readonly resolveSavedVariant: (model: RunInput["model"]) => Effect.Effect<string | undefined>
|
readonly resolveSavedVariant: (model: RunInput["model"]) => Effect.Effect<string | undefined>
|
||||||
readonly saveVariant: (model: RunInput["model"], variant: string | undefined) => Effect.Effect<void>
|
readonly saveVariant: (model: RunInput["model"], variant: string | undefined) => Effect.Effect<void>
|
||||||
}
|
}
|
||||||
type VariantRuntime = {
|
|
||||||
resolveSavedVariant(model: RunInput["model"]): Promise<string | undefined>
|
|
||||||
saveVariant(model: RunInput["model"], variant: string | undefined): Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
class Service extends Context.Service<Service, VariantService>()("@opencode/RunVariant") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/RunVariant") {}
|
||||||
|
|
||||||
function modelKey(provider: string, model: string): string {
|
function modelKey(provider: string, model: string): string {
|
||||||
return `${provider}/${model}`
|
return `${provider}/${model}`
|
||||||
@@ -135,81 +131,62 @@ function state(value: unknown): ModelState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLayer(fs = AppFileSystem.defaultLayer) {
|
export const layer = Layer.effect(
|
||||||
return Layer.fresh(
|
Service,
|
||||||
Layer.effect(
|
Effect.gen(function* () {
|
||||||
Service,
|
const file = yield* AppFileSystem.Service
|
||||||
Effect.gen(function* () {
|
|
||||||
const file = yield* AppFileSystem.Service
|
|
||||||
|
|
||||||
const read = Effect.fn("RunVariant.read")(function* () {
|
const read = Effect.fn("RunVariant.read")(function* () {
|
||||||
return yield* file.readJson(MODEL_FILE).pipe(
|
return yield* file.readJson(MODEL_FILE).pipe(
|
||||||
Effect.map(state),
|
Effect.map(state),
|
||||||
Effect.catchCause(() => Effect.succeed(state(undefined))),
|
Effect.catchCause(() => Effect.succeed(state(undefined))),
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolveSavedVariant = Effect.fn("RunVariant.resolveSavedVariant")(function* (model: RunInput["model"]) {
|
||||||
|
if (!model) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return (yield* read()).variant?.[variantKey(model)]
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveVariant = Effect.fn("RunVariant.saveVariant")(function* (
|
||||||
|
model: RunInput["model"],
|
||||||
|
variant: string | undefined,
|
||||||
|
) {
|
||||||
|
if (!model) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = yield* read()
|
||||||
|
const next = {
|
||||||
|
...current.variant,
|
||||||
|
}
|
||||||
|
const key = variantKey(model)
|
||||||
|
if (variant) {
|
||||||
|
next[key] = variant
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!variant) {
|
||||||
|
delete next[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* file
|
||||||
|
.writeJson(MODEL_FILE, {
|
||||||
|
...current,
|
||||||
|
variant: next,
|
||||||
})
|
})
|
||||||
|
.pipe(Effect.orElseSucceed(() => undefined))
|
||||||
|
})
|
||||||
|
|
||||||
const resolveSavedVariant = Effect.fn("RunVariant.resolveSavedVariant")(function* (model: RunInput["model"]) {
|
return Service.of({
|
||||||
if (!model) {
|
resolveSavedVariant,
|
||||||
return undefined
|
saveVariant,
|
||||||
}
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
return (yield* read()).variant?.[variantKey(model)]
|
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
||||||
})
|
|
||||||
|
|
||||||
const saveVariant = Effect.fn("RunVariant.saveVariant")(function* (
|
export * as Variant from "./variant.shared"
|
||||||
model: RunInput["model"],
|
|
||||||
variant: string | undefined,
|
|
||||||
) {
|
|
||||||
if (!model) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = yield* read()
|
|
||||||
const next = {
|
|
||||||
...current.variant,
|
|
||||||
}
|
|
||||||
const key = variantKey(model)
|
|
||||||
if (variant) {
|
|
||||||
next[key] = variant
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!variant) {
|
|
||||||
delete next[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
yield* file
|
|
||||||
.writeJson(MODEL_FILE, {
|
|
||||||
...current,
|
|
||||||
variant: next,
|
|
||||||
})
|
|
||||||
.pipe(Effect.orElseSucceed(() => undefined))
|
|
||||||
})
|
|
||||||
|
|
||||||
return Service.of({
|
|
||||||
resolveSavedVariant,
|
|
||||||
saveVariant,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
).pipe(Layer.provide(fs)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @internal Exported for testing. */
|
|
||||||
export function createVariantRuntime(fs = AppFileSystem.defaultLayer): VariantRuntime {
|
|
||||||
const runtime = makeRuntime(Service, createLayer(fs))
|
|
||||||
return {
|
|
||||||
resolveSavedVariant: (model) => runtime.runPromise((svc) => svc.resolveSavedVariant(model)).catch(() => undefined),
|
|
||||||
saveVariant: (model, variant) => runtime.runPromise((svc) => svc.saveVariant(model, variant)).catch(() => {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runtime = createVariantRuntime()
|
|
||||||
|
|
||||||
export async function resolveSavedVariant(model: RunInput["model"]): Promise<string | undefined> {
|
|
||||||
return runtime.resolveSavedVariant(model)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveVariant(model: RunInput["model"], variant: string | undefined): void {
|
|
||||||
void runtime.saveVariant(model, variant)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
|
import { afterEach, describe, expect, mock, spyOn } from "bun:test"
|
||||||
import type { KeyEvent, Renderable } from "@opentui/core"
|
import type { KeyEvent, Renderable } from "@opentui/core"
|
||||||
import type { Binding } from "@opentui/keymap"
|
import type { Binding } from "@opentui/keymap"
|
||||||
import { createBindingLookup } from "@opentui/keymap/extras"
|
import { createBindingLookup } from "@opentui/keymap/extras"
|
||||||
import { OpencodeClient, type Provider } from "@opencode-ai/sdk/v2"
|
import { OpencodeClient, type Provider } from "@opencode-ai/sdk/v2"
|
||||||
|
import { Effect } from "effect"
|
||||||
import { TuiConfig, type Resolved } from "@/cli/cmd/tui/config/tui"
|
import { TuiConfig, type Resolved } from "@/cli/cmd/tui/config/tui"
|
||||||
import { formatBindings } from "@/cli/cmd/run/keymap.shared"
|
import { formatBindings } from "@/cli/cmd/run/keymap.shared"
|
||||||
import { TuiKeybind } from "@/cli/cmd/tui/config/keybind"
|
import { TuiKeybind } from "@/cli/cmd/tui/config/keybind"
|
||||||
import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo } from "@/cli/cmd/run/runtime.boot"
|
import { RunBoot } from "@/cli/cmd/run/runtime.boot"
|
||||||
|
import { testEffect } from "../../lib/effect"
|
||||||
|
|
||||||
type RunBinding = Binding<Renderable, KeyEvent>
|
type RunBinding = Binding<Renderable, KeyEvent>
|
||||||
|
|
||||||
@@ -102,186 +104,206 @@ function config(input?: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const it = testEffect(RunBoot.defaultLayer)
|
||||||
|
|
||||||
describe("run runtime boot", () => {
|
describe("run runtime boot", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.restore()
|
mock.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("reads footer keybinds from resolved keybind config", async () => {
|
it.live("reads footer keybinds from resolved keybind config", () =>
|
||||||
spyOn(TuiConfig, "get").mockResolvedValue(
|
Effect.gen(function* () {
|
||||||
config({
|
spyOn(TuiConfig, "get").mockResolvedValue(
|
||||||
leader: "ctrl+g",
|
config({
|
||||||
bindings: {
|
leader: "ctrl+g",
|
||||||
commandList: bindings("ctrl+p"),
|
bindings: {
|
||||||
variantCycle: bindings("ctrl+t", "alt+t"),
|
commandList: bindings("ctrl+p"),
|
||||||
interrupt: bindings("ctrl+c"),
|
variantCycle: bindings("ctrl+t", "alt+t"),
|
||||||
historyPrevious: bindings("k"),
|
interrupt: bindings("ctrl+c"),
|
||||||
historyNext: bindings("j"),
|
historyPrevious: bindings("k"),
|
||||||
inputClear: bindings("ctrl+l"),
|
historyNext: bindings("j"),
|
||||||
inputSubmit: bindings("ctrl+s"),
|
inputClear: bindings("ctrl+l"),
|
||||||
inputNewline: bindings("alt+return"),
|
inputSubmit: bindings("ctrl+s"),
|
||||||
},
|
inputNewline: bindings("alt+return"),
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = await resolveFooterKeybinds()
|
|
||||||
|
|
||||||
expect(result.leader).toBe("ctrl+g")
|
|
||||||
expect(result.leaderTimeout).toBe(2000)
|
|
||||||
expect(formatBindings(result.commandList, result.leader)).toBe("ctrl+p")
|
|
||||||
expect(formatBindings(result.variantCycle, result.leader)).toBe("ctrl+t, alt+t")
|
|
||||||
expect(formatBindings(result.interrupt, result.leader)).toBe("ctrl+c")
|
|
||||||
expect(formatBindings(result.historyPrevious, result.leader)).toBe("k")
|
|
||||||
expect(formatBindings(result.historyNext, result.leader)).toBe("j")
|
|
||||||
expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+l")
|
|
||||||
expect(formatBindings(result.inputSubmit, result.leader)).toBe("ctrl+s")
|
|
||||||
expect(formatBindings(result.inputNewline, result.leader)).toBe("alt+return")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("falls back to default keybinds when config load fails", async () => {
|
|
||||||
spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom"))
|
|
||||||
|
|
||||||
const result = await resolveFooterKeybinds()
|
|
||||||
|
|
||||||
expect(result.leader).toBe("ctrl+x")
|
|
||||||
expect(result.leaderTimeout).toBe(2000)
|
|
||||||
expect(formatBindings(result.commandList, result.leader)).toBe("ctrl+p")
|
|
||||||
expect(formatBindings(result.variantCycle, result.leader)).toBe("ctrl+t")
|
|
||||||
expect(formatBindings(result.interrupt, result.leader)).toBe("esc")
|
|
||||||
expect(formatBindings(result.historyPrevious, result.leader)).toBe("up")
|
|
||||||
expect(formatBindings(result.historyNext, result.leader)).toBe("down")
|
|
||||||
expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+c")
|
|
||||||
expect(formatBindings(result.inputSubmit, result.leader)).toBe("return")
|
|
||||||
expect(formatBindings(result.inputNewline, result.leader)).toBe("shift+return, ctrl+return, alt+return, ctrl+j")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("reads diff style and falls back to auto", async () => {
|
|
||||||
spyOn(TuiConfig, "get").mockResolvedValue(config({ diff_style: "stacked" }))
|
|
||||||
await expect(resolveDiffStyle()).resolves.toBe("stacked")
|
|
||||||
|
|
||||||
mock.restore()
|
|
||||||
spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom"))
|
|
||||||
await expect(resolveDiffStyle()).resolves.toBe("auto")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("prefers configured providers for model selector data", async () => {
|
|
||||||
const sdk = new OpencodeClient()
|
|
||||||
const data: {
|
|
||||||
all: Provider[]
|
|
||||||
default: Record<string, string>
|
|
||||||
connected: string[]
|
|
||||||
} = {
|
|
||||||
all: [
|
|
||||||
{
|
|
||||||
id: "openai",
|
|
||||||
name: "OpenAI",
|
|
||||||
source: "api",
|
|
||||||
env: [],
|
|
||||||
options: {},
|
|
||||||
models: {
|
|
||||||
"gpt-5": model("gpt-5", "openai", 128000, {
|
|
||||||
high: {},
|
|
||||||
minimal: {},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
{
|
)
|
||||||
id: "anthropic",
|
|
||||||
name: "Anthropic",
|
|
||||||
source: "api",
|
|
||||||
env: [],
|
|
||||||
options: {},
|
|
||||||
models: {
|
|
||||||
sonnet: model("sonnet", "anthropic", 200000),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: {},
|
|
||||||
connected: [],
|
|
||||||
}
|
|
||||||
const configured = {
|
|
||||||
providers: [data.all[0]!],
|
|
||||||
default: {},
|
|
||||||
}
|
|
||||||
const list = spyOn(sdk.provider, "list").mockImplementation(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
data,
|
|
||||||
error: undefined,
|
|
||||||
request: new Request("https://opencode.test"),
|
|
||||||
response: new Response(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
spyOn(sdk.config, "providers").mockImplementation(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
data: configured,
|
|
||||||
error: undefined,
|
|
||||||
request: new Request("https://opencode.test"),
|
|
||||||
response: new Response(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
await expect(resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })).resolves.toEqual({
|
const boot = yield* RunBoot.Service
|
||||||
providers: configured.providers,
|
const result = yield* boot.resolveFooterKeybinds()
|
||||||
variants: ["high", "minimal"],
|
|
||||||
limits: {
|
|
||||||
"openai/gpt-5": 128000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(list).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("falls back to provider list when configured providers are unavailable", async () => {
|
expect(result.leader).toBe("ctrl+g")
|
||||||
const sdk = new OpencodeClient()
|
expect(result.leaderTimeout).toBe(2000)
|
||||||
const data: {
|
expect(formatBindings(result.commandList, result.leader)).toBe("ctrl+p")
|
||||||
all: Provider[]
|
expect(formatBindings(result.variantCycle, result.leader)).toBe("ctrl+t, alt+t")
|
||||||
default: Record<string, string>
|
expect(formatBindings(result.interrupt, result.leader)).toBe("ctrl+c")
|
||||||
connected: string[]
|
expect(formatBindings(result.historyPrevious, result.leader)).toBe("k")
|
||||||
} = {
|
expect(formatBindings(result.historyNext, result.leader)).toBe("j")
|
||||||
all: [
|
expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+l")
|
||||||
{
|
expect(formatBindings(result.inputSubmit, result.leader)).toBe("ctrl+s")
|
||||||
id: "openai",
|
expect(formatBindings(result.inputNewline, result.leader)).toBe("alt+return")
|
||||||
name: "OpenAI",
|
}),
|
||||||
source: "api",
|
)
|
||||||
env: [],
|
|
||||||
options: {},
|
|
||||||
models: {
|
|
||||||
"gpt-5": model("gpt-5", "openai", 128000, {
|
|
||||||
high: {},
|
|
||||||
minimal: {},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "anthropic",
|
|
||||||
name: "Anthropic",
|
|
||||||
source: "api",
|
|
||||||
env: [],
|
|
||||||
options: {},
|
|
||||||
models: {
|
|
||||||
sonnet: model("sonnet", "anthropic", 200000),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: {},
|
|
||||||
connected: [],
|
|
||||||
}
|
|
||||||
spyOn(sdk.config, "providers").mockRejectedValue(new Error("boom"))
|
|
||||||
spyOn(sdk.provider, "list").mockImplementation(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
data,
|
|
||||||
error: undefined,
|
|
||||||
request: new Request("https://opencode.test"),
|
|
||||||
response: new Response(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
await expect(resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })).resolves.toEqual({
|
it.live("falls back to default keybinds when config load fails", () =>
|
||||||
providers: data.all,
|
Effect.gen(function* () {
|
||||||
variants: ["high", "minimal"],
|
spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom"))
|
||||||
limits: {
|
|
||||||
"openai/gpt-5": 128000,
|
const boot = yield* RunBoot.Service
|
||||||
"anthropic/sonnet": 200000,
|
const result = yield* boot.resolveFooterKeybinds()
|
||||||
},
|
|
||||||
})
|
expect(result.leader).toBe("ctrl+x")
|
||||||
})
|
expect(result.leaderTimeout).toBe(2000)
|
||||||
|
expect(formatBindings(result.commandList, result.leader)).toBe("ctrl+p")
|
||||||
|
expect(formatBindings(result.variantCycle, result.leader)).toBe("ctrl+t")
|
||||||
|
expect(formatBindings(result.interrupt, result.leader)).toBe("esc")
|
||||||
|
expect(formatBindings(result.historyPrevious, result.leader)).toBe("up")
|
||||||
|
expect(formatBindings(result.historyNext, result.leader)).toBe("down")
|
||||||
|
expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+c")
|
||||||
|
expect(formatBindings(result.inputSubmit, result.leader)).toBe("return")
|
||||||
|
expect(formatBindings(result.inputNewline, result.leader)).toBe("shift+return, ctrl+return, alt+return, ctrl+j")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("reads diff style and falls back to auto", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
spyOn(TuiConfig, "get").mockResolvedValue(config({ diff_style: "stacked" }))
|
||||||
|
const boot = yield* RunBoot.Service
|
||||||
|
expect(yield* boot.resolveDiffStyle()).toBe("stacked")
|
||||||
|
|
||||||
|
mock.restore()
|
||||||
|
spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom"))
|
||||||
|
expect(yield* boot.resolveDiffStyle()).toBe("auto")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("prefers configured providers for model selector data", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const sdk = new OpencodeClient()
|
||||||
|
const data: {
|
||||||
|
all: Provider[]
|
||||||
|
default: Record<string, string>
|
||||||
|
connected: string[]
|
||||||
|
} = {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
id: "openai",
|
||||||
|
name: "OpenAI",
|
||||||
|
source: "api",
|
||||||
|
env: [],
|
||||||
|
options: {},
|
||||||
|
models: {
|
||||||
|
"gpt-5": model("gpt-5", "openai", 128000, {
|
||||||
|
high: {},
|
||||||
|
minimal: {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "anthropic",
|
||||||
|
name: "Anthropic",
|
||||||
|
source: "api",
|
||||||
|
env: [],
|
||||||
|
options: {},
|
||||||
|
models: {
|
||||||
|
sonnet: model("sonnet", "anthropic", 200000),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: {},
|
||||||
|
connected: [],
|
||||||
|
}
|
||||||
|
const configured = {
|
||||||
|
providers: [data.all[0]!],
|
||||||
|
default: {},
|
||||||
|
}
|
||||||
|
const list = spyOn(sdk.provider, "list").mockImplementation(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data,
|
||||||
|
error: undefined,
|
||||||
|
request: new Request("https://opencode.test"),
|
||||||
|
response: new Response(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
spyOn(sdk.config, "providers").mockImplementation(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: configured,
|
||||||
|
error: undefined,
|
||||||
|
request: new Request("https://opencode.test"),
|
||||||
|
response: new Response(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const boot = yield* RunBoot.Service
|
||||||
|
const result = yield* boot.resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })
|
||||||
|
expect(result).toEqual({
|
||||||
|
providers: configured.providers,
|
||||||
|
variants: ["high", "minimal"],
|
||||||
|
limits: {
|
||||||
|
"openai/gpt-5": 128000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(list).not.toHaveBeenCalled()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("falls back to provider list when configured providers are unavailable", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const sdk = new OpencodeClient()
|
||||||
|
const data: {
|
||||||
|
all: Provider[]
|
||||||
|
default: Record<string, string>
|
||||||
|
connected: string[]
|
||||||
|
} = {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
id: "openai",
|
||||||
|
name: "OpenAI",
|
||||||
|
source: "api",
|
||||||
|
env: [],
|
||||||
|
options: {},
|
||||||
|
models: {
|
||||||
|
"gpt-5": model("gpt-5", "openai", 128000, {
|
||||||
|
high: {},
|
||||||
|
minimal: {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "anthropic",
|
||||||
|
name: "Anthropic",
|
||||||
|
source: "api",
|
||||||
|
env: [],
|
||||||
|
options: {},
|
||||||
|
models: {
|
||||||
|
sonnet: model("sonnet", "anthropic", 200000),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: {},
|
||||||
|
connected: [],
|
||||||
|
}
|
||||||
|
spyOn(sdk.config, "providers").mockRejectedValue(new Error("boom"))
|
||||||
|
spyOn(sdk.provider, "list").mockImplementation(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data,
|
||||||
|
error: undefined,
|
||||||
|
request: new Request("https://opencode.test"),
|
||||||
|
response: new Response(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const boot = yield* RunBoot.Service
|
||||||
|
const result = yield* boot.resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })
|
||||||
|
expect(result).toEqual({
|
||||||
|
providers: data.all,
|
||||||
|
variants: ["high", "minimal"],
|
||||||
|
limits: {
|
||||||
|
"openai/gpt-5": 128000,
|
||||||
|
"anthropic/sonnet": 200000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { Effect, FileSystem, Layer } from "effect"
|
import { Effect, FileSystem, Layer } from "effect"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import {
|
import { Variant, cycleVariant, formatModelLabel, pickVariant, resolveVariant } from "@/cli/cmd/run/variant.shared"
|
||||||
createVariantRuntime,
|
|
||||||
cycleVariant,
|
|
||||||
formatModelLabel,
|
|
||||||
pickVariant,
|
|
||||||
resolveVariant,
|
|
||||||
} from "@/cli/cmd/run/variant.shared"
|
|
||||||
import type { SessionMessages } from "@/cli/cmd/run/session.shared"
|
import type { SessionMessages } from "@/cli/cmd/run/session.shared"
|
||||||
import type { RunProvider } from "@/cli/cmd/run/types"
|
import type { RunProvider } from "@/cli/cmd/run/types"
|
||||||
import { testEffect } from "../../lib/effect"
|
import { testEffect } from "../../lib/effect"
|
||||||
@@ -171,26 +165,27 @@ describe("run variant shared", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const svc = createVariantRuntime(remappedFs(root))
|
yield* Effect.gen(function* () {
|
||||||
|
const svc = yield* Variant.Service
|
||||||
|
yield* svc.saveVariant(model, "high")
|
||||||
|
expect(yield* svc.resolveSavedVariant(model)).toBe("high")
|
||||||
|
expect(yield* fs.readJson(file)).toEqual({
|
||||||
|
recent: [{ providerID: "anthropic", modelID: "sonnet" }],
|
||||||
|
variant: {
|
||||||
|
"openai/gpt-4.1": "low",
|
||||||
|
"openai/gpt-5": "high",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
yield* Effect.promise(() => svc.saveVariant(model, "high"))
|
yield* svc.saveVariant(model, undefined)
|
||||||
expect(yield* Effect.promise(() => svc.resolveSavedVariant(model))).toBe("high")
|
expect(yield* svc.resolveSavedVariant(model)).toBeUndefined()
|
||||||
expect(yield* fs.readJson(file)).toEqual({
|
expect(yield* fs.readJson(file)).toEqual({
|
||||||
recent: [{ providerID: "anthropic", modelID: "sonnet" }],
|
recent: [{ providerID: "anthropic", modelID: "sonnet" }],
|
||||||
variant: {
|
variant: {
|
||||||
"openai/gpt-4.1": "low",
|
"openai/gpt-4.1": "low",
|
||||||
"openai/gpt-5": "high",
|
},
|
||||||
},
|
})
|
||||||
})
|
}).pipe(Effect.provide(Variant.layer.pipe(Layer.provide(remappedFs(root)))))
|
||||||
|
|
||||||
yield* Effect.promise(() => svc.saveVariant(model, undefined))
|
|
||||||
expect(yield* Effect.promise(() => svc.resolveSavedVariant(model))).toBeUndefined()
|
|
||||||
expect(yield* fs.readJson(file)).toEqual({
|
|
||||||
recent: [{ providerID: "anthropic", modelID: "sonnet" }],
|
|
||||||
variant: {
|
|
||||||
"openai/gpt-4.1": "low",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -203,15 +198,16 @@ describe("run variant shared", () => {
|
|||||||
|
|
||||||
yield* filesys.writeFileString(file, "{")
|
yield* filesys.writeFileString(file, "{")
|
||||||
|
|
||||||
const svc = createVariantRuntime(remappedFs(root))
|
yield* Effect.gen(function* () {
|
||||||
|
const svc = yield* Variant.Service
|
||||||
yield* Effect.promise(() => svc.saveVariant(model, "high"))
|
yield* svc.saveVariant(model, "high")
|
||||||
expect(yield* Effect.promise(() => svc.resolveSavedVariant(model))).toBe("high")
|
expect(yield* svc.resolveSavedVariant(model)).toBe("high")
|
||||||
expect(yield* fs.readJson(file)).toEqual({
|
expect(yield* fs.readJson(file)).toEqual({
|
||||||
variant: {
|
variant: {
|
||||||
"openai/gpt-5": "high",
|
"openai/gpt-5": "high",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
}).pipe(Effect.provide(Variant.layer.pipe(Layer.provide(remappedFs(root)))))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user