mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-04 01:06:16 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e73ec6d2d7 |
@@ -41,13 +41,5 @@ runs:
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
# Workaround for patched peer variants
|
||||
# e.g. ./patches/ for standard-openapi
|
||||
# https://github.com/oven-sh/bun/issues/28147
|
||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||
bun install --linker hoisted
|
||||
else
|
||||
bun install
|
||||
fi
|
||||
run: bun install
|
||||
shell: bash
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -77,7 +77,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -111,7 +111,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -138,7 +138,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -162,7 +162,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -186,7 +186,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -219,7 +219,7 @@
|
||||
},
|
||||
"packages/desktop-electron": {
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -250,7 +250,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -279,7 +279,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -295,7 +295,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -416,7 +416,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -440,7 +440,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.90.10",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -451,7 +451,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -486,7 +486,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -532,7 +532,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -543,7 +543,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type ComposerProbeState,
|
||||
type ComposerWindow,
|
||||
} from "../../src/testing/session-composer"
|
||||
import { cleanupSession } from "../actions"
|
||||
import { cleanupSession, clearSessionDockSeed, seedSessionQuestion } from "../actions"
|
||||
import {
|
||||
permissionDockSelector,
|
||||
promptSelector,
|
||||
@@ -13,11 +13,9 @@ import {
|
||||
sessionComposerDockSelector,
|
||||
sessionTodoToggleButtonSelector,
|
||||
} from "../selectors"
|
||||
import { createSdk } from "../utils"
|
||||
|
||||
type Sdk = ReturnType<typeof createSdk>
|
||||
type Sdk = Parameters<typeof clearSessionDockSeed>[0]
|
||||
type PermissionRule = { permission: string; pattern: string; action: "allow" | "deny" | "ask" }
|
||||
type Child = { id: string }
|
||||
|
||||
async function withDockSession<T>(
|
||||
sdk: Sdk,
|
||||
@@ -38,6 +36,14 @@ async function withDockSession<T>(
|
||||
|
||||
test.setTimeout(120_000)
|
||||
|
||||
async function withDockSeed<T>(sdk: Sdk, sessionID: string, fn: () => Promise<T>) {
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
await clearSessionDockSeed(sdk, sessionID).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
async function clearPermissionDock(page: any, label: RegExp) {
|
||||
const dock = page.locator(permissionDockSelector)
|
||||
await expect(dock).toBeVisible()
|
||||
@@ -73,91 +79,6 @@ async function expectPermissionOpen(page: any) {
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
}
|
||||
|
||||
async function withMockSession<T>(page: any, child: Child | undefined, fn: () => Promise<T>) {
|
||||
if (!child) return await fn()
|
||||
|
||||
const list = async (route: any) => {
|
||||
const res = await route.fetch()
|
||||
const json = await res.json()
|
||||
const list = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : undefined
|
||||
if (Array.isArray(list) && !list.some((item) => item?.id === child.id)) list.push(child)
|
||||
await route.fulfill({
|
||||
status: res.status(),
|
||||
headers: res.headers(),
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(json),
|
||||
})
|
||||
}
|
||||
|
||||
await page.route("**/session?*", list)
|
||||
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
await page.unroute("**/session?*", list)
|
||||
}
|
||||
}
|
||||
|
||||
async function withMockQuestion<T>(
|
||||
page: any,
|
||||
request: {
|
||||
id: string
|
||||
sessionID: string
|
||||
questions: Array<{
|
||||
header: string
|
||||
question: string
|
||||
options: Array<{ label: string; description: string }>
|
||||
multiple?: boolean
|
||||
custom?: boolean
|
||||
}>
|
||||
},
|
||||
child: Child | undefined,
|
||||
fn: (state: { resolved: () => Promise<void> }) => Promise<T>,
|
||||
) {
|
||||
let pending = [
|
||||
{
|
||||
id: request.id,
|
||||
sessionID: request.sessionID,
|
||||
questions: request.questions,
|
||||
},
|
||||
]
|
||||
|
||||
const list = async (route: any) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(pending),
|
||||
})
|
||||
}
|
||||
|
||||
const reply = async (route: any) => {
|
||||
const url = new URL(route.request().url())
|
||||
const id = url.pathname.split("/").at(-2)
|
||||
pending = pending.filter((item) => item.id !== id)
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(true),
|
||||
})
|
||||
}
|
||||
|
||||
await page.route("**/question", list)
|
||||
await page.route("**/question/*/reply", reply)
|
||||
|
||||
const state = {
|
||||
async resolved() {
|
||||
await expect.poll(() => pending.length, { timeout: 10_000 }).toBe(0)
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
return await withMockSession(page, child, () => fn(state))
|
||||
} finally {
|
||||
await page.unroute("**/question", list)
|
||||
await page.unroute("**/question/*/reply", reply)
|
||||
}
|
||||
}
|
||||
|
||||
async function todoDock(page: any, sessionID: string) {
|
||||
await page.addInitScript(() => {
|
||||
const win = window as ComposerWindow
|
||||
@@ -262,7 +183,7 @@ async function withMockPermission<T>(
|
||||
metadata?: Record<string, unknown>
|
||||
always?: string[]
|
||||
},
|
||||
child: Child | undefined,
|
||||
opts: { child?: any } | undefined,
|
||||
fn: (state: { resolved: () => Promise<void> }) => Promise<T>,
|
||||
) {
|
||||
let pending = [
|
||||
@@ -295,6 +216,23 @@ async function withMockPermission<T>(
|
||||
await page.route("**/permission", list)
|
||||
await page.route("**/session/*/permissions/*", reply)
|
||||
|
||||
const sessionList = opts?.child
|
||||
? async (route: any) => {
|
||||
const res = await route.fetch()
|
||||
const json = await res.json()
|
||||
const list = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : undefined
|
||||
if (Array.isArray(list) && !list.some((item) => item?.id === opts.child?.id)) list.push(opts.child)
|
||||
await route.fulfill({
|
||||
status: res.status(),
|
||||
headers: res.headers(),
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(json),
|
||||
})
|
||||
}
|
||||
: undefined
|
||||
|
||||
if (sessionList) await page.route("**/session?*", sessionList)
|
||||
|
||||
const state = {
|
||||
async resolved() {
|
||||
await expect.poll(() => pending.length, { timeout: 10_000 }).toBe(0)
|
||||
@@ -302,10 +240,11 @@ async function withMockPermission<T>(
|
||||
}
|
||||
|
||||
try {
|
||||
return await withMockSession(page, child, () => fn(state))
|
||||
return await fn(state)
|
||||
} finally {
|
||||
await page.unroute("**/permission", list)
|
||||
await page.unroute("**/session/*/permissions/*", reply)
|
||||
if (sessionList) await page.unroute("**/session?*", sessionList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,12 +275,10 @@ test("auto-accept toggle works before first submit", async ({ page, gotoSession
|
||||
|
||||
test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSession }) => {
|
||||
await withDockSession(sdk, "e2e composer dock question", async (session) => {
|
||||
await gotoSession(session.id)
|
||||
await withDockSeed(sdk, session.id, async () => {
|
||||
await gotoSession(session.id)
|
||||
|
||||
await withMockQuestion(
|
||||
page,
|
||||
{
|
||||
id: "que_e2e_question",
|
||||
await seedSessionQuestion(sdk, {
|
||||
sessionID: session.id,
|
||||
questions: [
|
||||
{
|
||||
@@ -353,20 +290,16 @@ test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSess
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
undefined,
|
||||
async (state) => {
|
||||
await page.goto(page.url())
|
||||
await expectQuestionBlocked(page)
|
||||
})
|
||||
|
||||
const dock = page.locator(questionDockSelector)
|
||||
await dock.locator('[data-slot="question-option"]').first().click()
|
||||
await dock.getByRole("button", { name: /submit/i }).click()
|
||||
await state.resolved()
|
||||
await page.goto(page.url())
|
||||
await expectQuestionOpen(page)
|
||||
},
|
||||
)
|
||||
const dock = page.locator(questionDockSelector)
|
||||
await expectQuestionBlocked(page)
|
||||
|
||||
await dock.locator('[data-slot="question-option"]').first().click()
|
||||
await dock.getByRole("button", { name: /submit/i }).click()
|
||||
|
||||
await expectQuestionOpen(page)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -467,10 +400,8 @@ test("child session question request blocks parent dock and unblocks after submi
|
||||
if (!child?.id) throw new Error("Child session create did not return an id")
|
||||
|
||||
try {
|
||||
await withMockQuestion(
|
||||
page,
|
||||
{
|
||||
id: "que_e2e_child_question",
|
||||
await withDockSeed(sdk, child.id, async () => {
|
||||
await seedSessionQuestion(sdk, {
|
||||
sessionID: child.id,
|
||||
questions: [
|
||||
{
|
||||
@@ -482,20 +413,16 @@ test("child session question request blocks parent dock and unblocks after submi
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
child,
|
||||
async (state) => {
|
||||
await page.goto(page.url())
|
||||
await expectQuestionBlocked(page)
|
||||
})
|
||||
|
||||
const dock = page.locator(questionDockSelector)
|
||||
await dock.locator('[data-slot="question-option"]').first().click()
|
||||
await dock.getByRole("button", { name: /submit/i }).click()
|
||||
await state.resolved()
|
||||
await page.goto(page.url())
|
||||
await expectQuestionOpen(page)
|
||||
},
|
||||
)
|
||||
const dock = page.locator(questionDockSelector)
|
||||
await expectQuestionBlocked(page)
|
||||
|
||||
await dock.locator('[data-slot="question-option"]').first().click()
|
||||
await dock.getByRole("button", { name: /submit/i }).click()
|
||||
|
||||
await expectQuestionOpen(page)
|
||||
})
|
||||
} finally {
|
||||
await cleanupSession({ sdk, sessionID: child.id })
|
||||
}
|
||||
@@ -529,7 +456,7 @@ test("child session permission request blocks parent dock and supports allow onc
|
||||
patterns: ["/tmp/opencode-e2e-perm-child"],
|
||||
metadata: { description: "Need child permission" },
|
||||
},
|
||||
child,
|
||||
{ child },
|
||||
async (state) => {
|
||||
await page.goto(page.url())
|
||||
await expectPermissionBlocked(page)
|
||||
@@ -579,12 +506,10 @@ test("todo dock transitions and collapse behavior", async ({ page, sdk, gotoSess
|
||||
|
||||
test("keyboard focus stays off prompt while blocked", async ({ page, sdk, gotoSession }) => {
|
||||
await withDockSession(sdk, "e2e composer dock keyboard", async (session) => {
|
||||
await gotoSession(session.id)
|
||||
await withDockSeed(sdk, session.id, async () => {
|
||||
await gotoSession(session.id)
|
||||
|
||||
await withMockQuestion(
|
||||
page,
|
||||
{
|
||||
id: "que_e2e_keyboard",
|
||||
await seedSessionQuestion(sdk, {
|
||||
sessionID: session.id,
|
||||
questions: [
|
||||
{
|
||||
@@ -593,16 +518,13 @@ test("keyboard focus stays off prompt while blocked", async ({ page, sdk, gotoSe
|
||||
options: [{ label: "Continue", description: "Continue now" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
undefined,
|
||||
async () => {
|
||||
await page.goto(page.url())
|
||||
await expectQuestionBlocked(page)
|
||||
})
|
||||
|
||||
await page.locator("main").click({ position: { x: 5, y: 5 } })
|
||||
await page.keyboard.type("abc")
|
||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
||||
},
|
||||
)
|
||||
await expectQuestionBlocked(page)
|
||||
|
||||
await page.locator("main").click({ position: { x: 5, y: 5 } })
|
||||
await page.keyboard.type("abc")
|
||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -349,25 +349,3 @@ test("session model restore across workspaces", async ({ page, withProject }) =>
|
||||
await waitFooter(page, firstState)
|
||||
})
|
||||
})
|
||||
|
||||
test("variant preserved when switching agent modes", async ({ page, withProject }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 })
|
||||
|
||||
await withProject(async ({ directory, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await ensureVariant(page, directory)
|
||||
const updated = await chooseDifferentVariant(page)
|
||||
|
||||
const available = await agents(page)
|
||||
const other = available.find((name) => name !== updated.agent)
|
||||
test.skip(!other, "only one agent available")
|
||||
if (!other) return
|
||||
|
||||
await choose(page, promptAgentSelector, other)
|
||||
await waitFooter(page, { agent: other, variant: updated.variant })
|
||||
|
||||
await choose(page, promptAgentSelector, updated.agent)
|
||||
await waitFooter(page, { agent: updated.agent, variant: updated.variant })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1023,6 +1023,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
})
|
||||
|
||||
const variants = createMemo(() => ["default", ...local.model.variant.list()])
|
||||
const fast = createMemo(() => local.model.fast.available())
|
||||
const fastLabel = createMemo(() =>
|
||||
language.t(local.model.fast.current() ? "command.model.fast.disable" : "command.model.fast.enable"),
|
||||
)
|
||||
const accepting = createMemo(() => {
|
||||
const id = params.id
|
||||
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
|
||||
@@ -1534,6 +1538,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<Show when={fast()}>
|
||||
<Tooltip placement="top" gutter={8} value={fastLabel()}>
|
||||
<Button
|
||||
data-action="prompt-fast"
|
||||
variant="ghost"
|
||||
onClick={() => local.model.fast.toggle()}
|
||||
class="h-7 px-2 shrink-0 text-13-medium"
|
||||
classList={{
|
||||
"text-text-base": !local.model.fast.current(),
|
||||
"text-icon-warning-base bg-surface-warning-base": local.model.fast.current(),
|
||||
}}
|
||||
style={control()}
|
||||
aria-label={fastLabel()}
|
||||
aria-pressed={local.model.fast.current()}
|
||||
>
|
||||
{language.t("command.model.fast.label")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={8}
|
||||
|
||||
@@ -34,6 +34,7 @@ export type FollowupDraft = {
|
||||
agent: string
|
||||
model: { providerID: string; modelID: string }
|
||||
variant?: string
|
||||
fast?: boolean
|
||||
}
|
||||
|
||||
type FollowupSendInput = {
|
||||
@@ -88,6 +89,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||
agent: input.draft.agent,
|
||||
model: `${input.draft.model.providerID}/${input.draft.model.modelID}`,
|
||||
variant: input.draft.variant,
|
||||
fast: input.draft.fast,
|
||||
parts: images.map((attachment) => ({
|
||||
id: Identifier.ascending("part"),
|
||||
type: "file" as const,
|
||||
@@ -122,6 +124,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||
agent: input.draft.agent,
|
||||
model: input.draft.model,
|
||||
variant: input.draft.variant,
|
||||
fast: input.draft.fast,
|
||||
}
|
||||
|
||||
const add = () =>
|
||||
@@ -156,6 +159,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||
messageID,
|
||||
parts: requestParts,
|
||||
variant: input.draft.variant,
|
||||
fast: input.draft.fast,
|
||||
})
|
||||
return true
|
||||
} catch (err) {
|
||||
@@ -297,6 +301,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
const currentModel = local.model.current()
|
||||
const currentAgent = local.agent.current()
|
||||
const variant = local.model.variant.current()
|
||||
const fast = local.model.fast.current()
|
||||
if (!currentModel || !currentAgent) {
|
||||
showToast({
|
||||
title: language.t("prompt.toast.modelAgentRequired.title"),
|
||||
@@ -398,6 +403,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
agent,
|
||||
model,
|
||||
variant,
|
||||
fast,
|
||||
}
|
||||
|
||||
const clearInput = () => {
|
||||
@@ -461,6 +467,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
agent,
|
||||
model: `${model.providerID}/${model.modelID}`,
|
||||
variant,
|
||||
fast,
|
||||
parts: images.map((attachment) => ({
|
||||
id: Identifier.ascending("part"),
|
||||
type: "file" as const,
|
||||
|
||||
@@ -326,7 +326,7 @@ export function SessionHeader() {
|
||||
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="rounded-none h-full px-0.5 border-none shadow-none disabled:!cursor-default"
|
||||
class="rounded-none h-full py-0 pr-1.5 pl-px gap-1.5 border-none shadow-none disabled:!cursor-default"
|
||||
classList={{
|
||||
"bg-surface-raised-base-active": opening(),
|
||||
}}
|
||||
@@ -339,6 +339,7 @@ export function SessionHeader() {
|
||||
<Spinner class="size-3.5" style={{ color: tint() ?? "var(--icon-base)" }} />
|
||||
</Show>
|
||||
</div>
|
||||
<span class="text-12-regular text-text-strong">{language.t("common.open")}</span>
|
||||
</Button>
|
||||
<DropdownMenu
|
||||
gutter={4}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useProviders } from "@/hooks/use-providers"
|
||||
import { modelEnabled, modelProbe } from "@/testing/model-selection"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||
import * as Fast from "./model-fast"
|
||||
import { useSDK } from "./sdk"
|
||||
import { useSync } from "./sync"
|
||||
|
||||
@@ -17,6 +18,7 @@ type State = {
|
||||
agent?: string
|
||||
model?: ModelKey
|
||||
variant?: string | null
|
||||
fast?: boolean
|
||||
}
|
||||
|
||||
type Saved = {
|
||||
@@ -79,10 +81,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
current?: string
|
||||
draft?: State
|
||||
last?: {
|
||||
type: "agent" | "model" | "variant"
|
||||
type: "agent" | "model" | "variant" | "fast"
|
||||
agent?: string
|
||||
model?: ModelKey | null
|
||||
variant?: string | null
|
||||
fast?: boolean
|
||||
}
|
||||
}>({
|
||||
current: list()[0]?.name,
|
||||
@@ -191,12 +194,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
agent: item.name,
|
||||
model: item.model,
|
||||
variant: item.variant ?? null,
|
||||
fast: scope()?.fast,
|
||||
})
|
||||
const prev = scope()
|
||||
const next = {
|
||||
agent: item.name,
|
||||
model: item.model ?? prev?.model,
|
||||
variant: item.variant ?? prev?.variant,
|
||||
model: item.model,
|
||||
variant: item.variant,
|
||||
fast: scope()?.fast,
|
||||
} satisfies State
|
||||
const session = id()
|
||||
if (session) {
|
||||
@@ -250,6 +254,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
agent: agent.current()?.name,
|
||||
model: model ? { providerID: model.provider.id, modelID: model.id } : undefined,
|
||||
variant: selected(),
|
||||
fast: !!scope()?.fast && Fast.enabled(model),
|
||||
} satisfies State
|
||||
}
|
||||
|
||||
@@ -297,6 +302,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
agent: agent.current()?.name,
|
||||
model: item ?? null,
|
||||
variant: selected(),
|
||||
fast: model.fast.current(),
|
||||
})
|
||||
write({ model: item })
|
||||
if (!item) return
|
||||
@@ -334,6 +340,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
agent: agent.current()?.name,
|
||||
model: model ? { providerID: model.provider.id, modelID: model.id } : null,
|
||||
variant: value ?? null,
|
||||
fast: !!scope()?.fast && Fast.enabled(model),
|
||||
})
|
||||
write({ variant: value ?? null })
|
||||
})
|
||||
@@ -350,6 +357,34 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
)
|
||||
},
|
||||
},
|
||||
fast: {
|
||||
selected() {
|
||||
return scope()?.fast === true
|
||||
},
|
||||
current() {
|
||||
return this.selected() && this.available()
|
||||
},
|
||||
available() {
|
||||
return Fast.enabled(current())
|
||||
},
|
||||
set(value: boolean) {
|
||||
if (value && !this.available()) return
|
||||
const model = current()
|
||||
batch(() => {
|
||||
setStore("last", {
|
||||
type: "fast",
|
||||
agent: agent.current()?.name,
|
||||
model: model ? { providerID: model.provider.id, modelID: model.id } : null,
|
||||
variant: selected(),
|
||||
fast: value,
|
||||
})
|
||||
write({ fast: value || undefined })
|
||||
})
|
||||
},
|
||||
toggle() {
|
||||
this.set(!this.current())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const result = {
|
||||
@@ -373,7 +408,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
handoff.set(handoffKey(dir, session), next)
|
||||
setStore("draft", undefined)
|
||||
},
|
||||
restore(msg: { sessionID: string; agent: string; model: ModelKey; variant?: string }) {
|
||||
restore(msg: { sessionID: string; agent: string; model: ModelKey; variant?: string; fast?: boolean }) {
|
||||
const session = id()
|
||||
if (!session) return
|
||||
if (msg.sessionID !== session) return
|
||||
@@ -384,6 +419,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
agent: msg.agent,
|
||||
model: msg.model,
|
||||
variant: msg.variant ?? null,
|
||||
fast: msg.fast === true,
|
||||
})
|
||||
},
|
||||
},
|
||||
@@ -406,6 +442,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
: undefined,
|
||||
variant: result.model.variant.current() ?? null,
|
||||
fast: result.model.fast.current(),
|
||||
selected: result.model.variant.selected(),
|
||||
configured: result.model.variant.configured(),
|
||||
pick: scope(),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
type Model = {
|
||||
id: string
|
||||
provider: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
function lower(model: Model) {
|
||||
return model.id.toLowerCase()
|
||||
}
|
||||
|
||||
export function kind(model: Model | undefined) {
|
||||
if (!model) return
|
||||
const id = lower(model)
|
||||
if (
|
||||
model.provider.id === "anthropic" &&
|
||||
(id.includes("claude-opus-4-6") || id.includes("claude-opus-4.6") || id.includes("opus-4-6"))
|
||||
) {
|
||||
return "claude"
|
||||
}
|
||||
if (model.provider.id === "openai" && id.includes("gpt-5.4")) {
|
||||
return "codex"
|
||||
}
|
||||
}
|
||||
|
||||
export function enabled(model: Model | undefined) {
|
||||
return !!kind(model)
|
||||
}
|
||||
@@ -67,6 +67,10 @@ export const dict = {
|
||||
"command.agent.cycle.description": "Switch to the next agent",
|
||||
"command.agent.cycle.reverse": "Cycle agent backwards",
|
||||
"command.agent.cycle.reverse.description": "Switch to the previous agent",
|
||||
"command.model.fast.label": "Fast",
|
||||
"command.model.fast.enable": "Enable fast mode",
|
||||
"command.model.fast.disable": "Disable fast mode",
|
||||
"command.model.fast.description": "Toggle provider fast mode for supported Claude and Codex models",
|
||||
"command.model.variant.cycle": "Cycle thinking effort",
|
||||
"command.model.variant.cycle.description": "Switch to the next effort level",
|
||||
"command.prompt.mode.shell": "Shell",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
|
||||
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { resetSessionModel, syncSessionModel } from "./session-model-helpers"
|
||||
|
||||
const message = (input?: Partial<Pick<UserMessage, "agent" | "model" | "variant">>) =>
|
||||
const message = (input?: Partial<Pick<UserMessage, "agent" | "model" | "variant" | "fast">>) =>
|
||||
({
|
||||
id: "msg",
|
||||
sessionID: "session",
|
||||
@@ -31,6 +31,24 @@ describe("syncSessionModel", () => {
|
||||
|
||||
expect(calls).toEqual([message({ variant: "high" })])
|
||||
})
|
||||
|
||||
test("restores fast mode from the last message", () => {
|
||||
const calls: unknown[] = []
|
||||
|
||||
syncSessionModel(
|
||||
{
|
||||
session: {
|
||||
restore(value) {
|
||||
calls.push(value)
|
||||
},
|
||||
reset() {},
|
||||
},
|
||||
},
|
||||
message({ fast: true }),
|
||||
)
|
||||
|
||||
expect(calls).toEqual([message({ fast: true })])
|
||||
})
|
||||
})
|
||||
|
||||
describe("resetSessionModel", () => {
|
||||
|
||||
@@ -353,6 +353,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
slash: "model",
|
||||
onSelect: () => dialog.show(() => <DialogSelectModel model={local.model} />),
|
||||
}),
|
||||
modelCommand({
|
||||
id: "model.fast.toggle",
|
||||
title: language.t(local.model.fast.current() ? "command.model.fast.disable" : "command.model.fast.enable"),
|
||||
description: language.t("command.model.fast.description"),
|
||||
slash: "fast",
|
||||
disabled: !local.model.fast.available(),
|
||||
onSelect: () => local.model.fast.toggle(),
|
||||
}),
|
||||
mcpCommand({
|
||||
id: "mcp.toggle",
|
||||
title: language.t("command.mcp.toggle"),
|
||||
|
||||
@@ -7,20 +7,23 @@ type State = {
|
||||
agent?: string
|
||||
model?: ModelKey | null
|
||||
variant?: string | null
|
||||
fast?: boolean
|
||||
}
|
||||
|
||||
export type ModelProbeState = {
|
||||
dir?: string
|
||||
sessionID?: string
|
||||
last?: {
|
||||
type: "agent" | "model" | "variant"
|
||||
type: "agent" | "model" | "variant" | "fast"
|
||||
agent?: string
|
||||
model?: ModelKey | null
|
||||
variant?: string | null
|
||||
fast?: boolean
|
||||
}
|
||||
agent?: string
|
||||
model?: (ModelKey & { name?: string }) | undefined
|
||||
variant?: string | null
|
||||
fast?: boolean
|
||||
selected?: string | null
|
||||
configured?: string
|
||||
pick?: State
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"private": true,
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.2.27"
|
||||
version = "1.2.26"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.26/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.26/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.26/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.26/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.26/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -11,11 +11,6 @@ const openBrowser = (url: string) => Effect.promise(() => open(url).catch(() =>
|
||||
|
||||
const println = (msg: string) => Effect.sync(() => UI.println(msg))
|
||||
|
||||
const isActiveOrgChoice = (
|
||||
active: Option.Option<{ id: AccountID; active_org_id: OrgID | null }>,
|
||||
choice: { accountID: AccountID; orgID: OrgID },
|
||||
) => Option.isSome(active) && active.value.id === choice.accountID && active.value.active_org_id === choice.orgID
|
||||
|
||||
const loginEffect = Effect.fn("login")(function* (url: string) {
|
||||
const service = yield* AccountService
|
||||
|
||||
@@ -104,10 +99,11 @@ const switchEffect = Effect.fn("switch")(function* () {
|
||||
if (groups.length === 0) return yield* println("Not logged in")
|
||||
|
||||
const active = yield* service.active()
|
||||
const activeOrgID = Option.flatMap(active, (a) => Option.fromNullishOr(a.active_org_id))
|
||||
|
||||
const opts = groups.flatMap((group) =>
|
||||
group.orgs.map((org) => {
|
||||
const isActive = isActiveOrgChoice(active, { accountID: group.account.id, orgID: org.id })
|
||||
const isActive = Option.isSome(activeOrgID) && activeOrgID.value === org.id
|
||||
return {
|
||||
value: { orgID: org.id, accountID: group.account.id, label: org.name },
|
||||
label: isActive
|
||||
@@ -136,10 +132,11 @@ const orgsEffect = Effect.fn("orgs")(function* () {
|
||||
if (!groups.some((group) => group.orgs.length > 0)) return yield* println("No orgs found")
|
||||
|
||||
const active = yield* service.active()
|
||||
const activeOrgID = Option.flatMap(active, (a) => Option.fromNullishOr(a.active_org_id))
|
||||
|
||||
for (const group of groups) {
|
||||
for (const org of group.orgs) {
|
||||
const isActive = isActiveOrgChoice(active, { accountID: group.account.id, orgID: org.id })
|
||||
const isActive = Option.isSome(activeOrgID) && activeOrgID.value === org.id
|
||||
const dot = isActive ? UI.Style.TEXT_SUCCESS + "●" + UI.Style.TEXT_NORMAL : " "
|
||||
const name = isActive ? UI.Style.TEXT_HIGHLIGHT_BOLD + org.name + UI.Style.TEXT_NORMAL : org.name
|
||||
const email = UI.Style.TEXT_DIM + group.account.email + UI.Style.TEXT_NORMAL
|
||||
|
||||
@@ -164,7 +164,8 @@ export function Prompt(props: PromptProps) {
|
||||
if (msg.agent && isPrimaryAgent) {
|
||||
local.agent.set(msg.agent)
|
||||
if (msg.model) local.model.set(msg.model)
|
||||
if (msg.variant) local.model.variant.set(msg.variant)
|
||||
local.model.variant.set(msg.variant)
|
||||
local.model.fast.set(msg.fast === true)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -330,6 +331,19 @@ export function Prompt(props: PromptProps) {
|
||||
input.cursorOffset = Bun.stringWidth(content)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: local.model.fast.current() ? "Disable fast mode" : "Enable fast mode",
|
||||
value: "model.fast",
|
||||
category: "Model",
|
||||
enabled: local.model.fast.available(),
|
||||
slash: {
|
||||
name: "fast",
|
||||
},
|
||||
onSelect: (dialog) => {
|
||||
local.model.fast.toggle()
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Skills",
|
||||
value: "prompt.skills",
|
||||
@@ -586,6 +600,7 @@ export function Prompt(props: PromptProps) {
|
||||
// Capture mode before it gets reset
|
||||
const currentMode = store.mode
|
||||
const variant = local.model.variant.current()
|
||||
const fast = local.model.fast.current()
|
||||
|
||||
if (store.mode === "shell") {
|
||||
sdk.client.session.shell({
|
||||
@@ -621,6 +636,7 @@ export function Prompt(props: PromptProps) {
|
||||
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
|
||||
messageID,
|
||||
variant,
|
||||
fast,
|
||||
parts: nonTextParts
|
||||
.filter((x) => x.type === "file")
|
||||
.map((x) => ({
|
||||
@@ -637,6 +653,7 @@ export function Prompt(props: PromptProps) {
|
||||
agent: local.agent.current().name,
|
||||
model: selectedModel,
|
||||
variant,
|
||||
fast,
|
||||
parts: [
|
||||
{
|
||||
id: PartID.ascending(),
|
||||
@@ -765,6 +782,8 @@ export function Prompt(props: PromptProps) {
|
||||
return !!current
|
||||
})
|
||||
|
||||
const showFast = createMemo(() => local.model.fast.current())
|
||||
|
||||
const placeholderText = createMemo(() => {
|
||||
if (props.sessionID) return undefined
|
||||
if (store.mode === "shell") {
|
||||
@@ -1028,6 +1047,12 @@ export function Prompt(props: PromptProps) {
|
||||
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
|
||||
</text>
|
||||
</Show>
|
||||
<Show when={showFast()}>
|
||||
<text fg={theme.textMuted}>·</text>
|
||||
<text>
|
||||
<span style={{ fg: theme.info, bold: true }}>fast</span>
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useArgs } from "./args"
|
||||
import { useSDK } from "./sdk"
|
||||
import { RGBA } from "@opentui/core"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import * as Fast from "@/provider/fast"
|
||||
|
||||
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
name: "Local",
|
||||
@@ -112,12 +113,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
modelID: string
|
||||
}[]
|
||||
variant: Record<string, string | undefined>
|
||||
fast: Record<string, boolean | undefined>
|
||||
}>({
|
||||
ready: false,
|
||||
model: {},
|
||||
recent: [],
|
||||
favorite: [],
|
||||
variant: {},
|
||||
fast: {},
|
||||
})
|
||||
|
||||
const filePath = path.join(Global.Path.state, "model.json")
|
||||
@@ -135,6 +138,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
recent: modelStore.recent,
|
||||
favorite: modelStore.favorite,
|
||||
variant: modelStore.variant,
|
||||
fast: modelStore.fast,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -143,6 +147,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
if (Array.isArray(x.recent)) setModelStore("recent", x.recent)
|
||||
if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
|
||||
if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant)
|
||||
if (typeof x.fast === "object" && x.fast !== null) setModelStore("fast", x.fast)
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
@@ -358,6 +363,36 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
this.set(variants[index + 1])
|
||||
},
|
||||
},
|
||||
fast: {
|
||||
selected() {
|
||||
const m = currentModel()
|
||||
if (!m) return false
|
||||
const key = `${m.providerID}/${m.modelID}`
|
||||
return modelStore.fast[key] === true
|
||||
},
|
||||
current() {
|
||||
return this.selected() && this.available()
|
||||
},
|
||||
available() {
|
||||
const m = currentModel()
|
||||
if (!m) return false
|
||||
const provider = sync.data.provider.find((x) => x.id === m.providerID)
|
||||
const info = provider?.models[m.modelID]
|
||||
if (!info) return false
|
||||
return Fast.enabled(info, { codex: info.providerID === "openai" })
|
||||
},
|
||||
set(value: boolean) {
|
||||
const m = currentModel()
|
||||
if (!m) return
|
||||
if (value && !this.available()) return
|
||||
const key = `${m.providerID}/${m.modelID}`
|
||||
setModelStore("fast", key, value || undefined)
|
||||
save()
|
||||
},
|
||||
toggle() {
|
||||
this.set(!this.current())
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -47,6 +47,11 @@ process.on("uncaughtException", (e) => {
|
||||
})
|
||||
})
|
||||
|
||||
// Ensure the process exits on terminal hangup (eg. closing the terminal tab).
|
||||
// Without this, long-running commands like `serve` block on a never-resolving
|
||||
// promise and survive as orphaned processes.
|
||||
process.on("SIGHUP", () => process.exit())
|
||||
|
||||
let cli = yargs(hideBin(process.argv))
|
||||
.parserConfiguration({ "populate--": true })
|
||||
.scriptName("opencode")
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import z from "zod"
|
||||
import { Log } from "../util/log"
|
||||
import { Plugin } from "../plugin"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Wildcard } from "../util/wildcard"
|
||||
import { PermissionID } from "./schema"
|
||||
|
||||
export namespace Permission {
|
||||
const log = Log.create({ service: "permission" })
|
||||
|
||||
function toKeys(pattern: Info["pattern"], type: string): string[] {
|
||||
return pattern === undefined ? [type] : Array.isArray(pattern) ? pattern : [pattern]
|
||||
}
|
||||
|
||||
function covered(keys: string[], approved: Map<string, boolean>): boolean {
|
||||
return keys.every((k) => {
|
||||
for (const p of approved.keys()) {
|
||||
if (Wildcard.match(k, p)) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: PermissionID.zod,
|
||||
type: z.string(),
|
||||
pattern: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
callID: z.string().optional(),
|
||||
message: z.string(),
|
||||
metadata: z.record(z.string(), z.any()),
|
||||
time: z.object({
|
||||
created: z.number(),
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
ref: "Permission",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
interface PendingEntry {
|
||||
info: Info
|
||||
resolve: () => void
|
||||
reject: (e: any) => void
|
||||
}
|
||||
|
||||
export const Event = {
|
||||
Updated: BusEvent.define("permission.updated", Info),
|
||||
Replied: BusEvent.define(
|
||||
"permission.replied",
|
||||
z.object({
|
||||
sessionID: SessionID.zod,
|
||||
permissionID: PermissionID.zod,
|
||||
response: z.string(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
const state = Instance.state(
|
||||
() => ({
|
||||
pending: new Map<SessionID, Map<PermissionID, PendingEntry>>(),
|
||||
approved: new Map<SessionID, Map<string, boolean>>(),
|
||||
}),
|
||||
async (state) => {
|
||||
for (const session of state.pending.values()) {
|
||||
for (const item of session.values()) {
|
||||
item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID, item.info.metadata))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export function pending() {
|
||||
return state().pending
|
||||
}
|
||||
|
||||
export function list() {
|
||||
const { pending } = state()
|
||||
const result: Info[] = []
|
||||
for (const session of pending.values()) {
|
||||
for (const item of session.values()) {
|
||||
result.push(item.info)
|
||||
}
|
||||
}
|
||||
return result.sort((a, b) => String(a.id).localeCompare(String(b.id)))
|
||||
}
|
||||
|
||||
export async function ask(input: {
|
||||
type: Info["type"]
|
||||
message: Info["message"]
|
||||
pattern?: Info["pattern"]
|
||||
callID?: Info["callID"]
|
||||
sessionID: Info["sessionID"]
|
||||
messageID: Info["messageID"]
|
||||
metadata: Info["metadata"]
|
||||
}) {
|
||||
const { pending, approved } = state()
|
||||
log.info("asking", {
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
toolCallID: input.callID,
|
||||
pattern: input.pattern,
|
||||
})
|
||||
const approvedForSession = approved.get(input.sessionID)
|
||||
const keys = toKeys(input.pattern, input.type)
|
||||
if (approvedForSession && covered(keys, approvedForSession)) return
|
||||
const info: Info = {
|
||||
id: PermissionID.ascending(),
|
||||
type: input.type,
|
||||
pattern: input.pattern,
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
callID: input.callID,
|
||||
message: input.message,
|
||||
metadata: input.metadata,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
}
|
||||
|
||||
switch (
|
||||
await Plugin.trigger("permission.ask", info, {
|
||||
status: "ask",
|
||||
}).then((x) => x.status)
|
||||
) {
|
||||
case "deny":
|
||||
throw new RejectedError(info.sessionID, info.id, info.callID, info.metadata)
|
||||
case "allow":
|
||||
return
|
||||
}
|
||||
|
||||
if (!pending.has(input.sessionID)) pending.set(input.sessionID, new Map())
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
pending.get(input.sessionID)!.set(info.id, {
|
||||
info,
|
||||
resolve,
|
||||
reject,
|
||||
})
|
||||
Bus.publish(Event.Updated, info)
|
||||
})
|
||||
}
|
||||
|
||||
export const Response = z.enum(["once", "always", "reject"])
|
||||
export type Response = z.infer<typeof Response>
|
||||
|
||||
export function respond(input: { sessionID: Info["sessionID"]; permissionID: Info["id"]; response: Response }) {
|
||||
log.info("response", input)
|
||||
const { pending, approved } = state()
|
||||
const session = pending.get(input.sessionID)
|
||||
const match = session?.get(input.permissionID)
|
||||
if (!session || !match) return
|
||||
session.delete(input.permissionID)
|
||||
if (session.size === 0) pending.delete(input.sessionID)
|
||||
Bus.publish(Event.Replied, {
|
||||
sessionID: input.sessionID,
|
||||
permissionID: input.permissionID,
|
||||
response: input.response,
|
||||
})
|
||||
if (input.response === "reject") {
|
||||
match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID, match.info.metadata))
|
||||
return
|
||||
}
|
||||
match.resolve()
|
||||
if (input.response === "always") {
|
||||
if (!approved.has(input.sessionID)) approved.set(input.sessionID, new Map())
|
||||
const approvedSession = approved.get(input.sessionID)!
|
||||
const approveKeys = toKeys(match.info.pattern, match.info.type)
|
||||
for (const k of approveKeys) {
|
||||
approvedSession.set(k, true)
|
||||
}
|
||||
const items = pending.get(input.sessionID)
|
||||
if (!items) return
|
||||
const toRespond: Info[] = []
|
||||
for (const item of items.values()) {
|
||||
const itemKeys = toKeys(item.info.pattern, item.info.type)
|
||||
if (covered(itemKeys, approvedSession)) {
|
||||
toRespond.push(item.info)
|
||||
}
|
||||
}
|
||||
for (const item of toRespond) {
|
||||
respond({
|
||||
sessionID: item.sessionID,
|
||||
permissionID: item.id,
|
||||
response: input.response,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RejectedError extends Error {
|
||||
constructor(
|
||||
public readonly sessionID: SessionID,
|
||||
public readonly permissionID: PermissionID,
|
||||
public readonly toolCallID?: string,
|
||||
public readonly metadata?: Record<string, any>,
|
||||
public readonly reason?: string,
|
||||
) {
|
||||
super(
|
||||
reason !== undefined
|
||||
? reason
|
||||
: `The user rejected permission to use this specific tool call. You may try again with different parameters.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export namespace Vcs {
|
||||
log.info("initialized", { branch: current })
|
||||
|
||||
const unsubscribe = Bus.subscribe(FileWatcher.Event.Updated, async (evt) => {
|
||||
if (!evt.properties.file.endsWith("HEAD")) return
|
||||
if (evt.properties.file.endsWith("HEAD")) return
|
||||
const next = await currentBranch()
|
||||
if (next !== current) {
|
||||
log.info("branch changed", { from: current, to: next })
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
type Model = {
|
||||
providerID: string
|
||||
api: {
|
||||
id: string
|
||||
npm: string
|
||||
}
|
||||
}
|
||||
|
||||
function lower(model: Pick<Model, "api">) {
|
||||
return model.api.id.toLowerCase()
|
||||
}
|
||||
|
||||
type Input = {
|
||||
codex?: boolean
|
||||
}
|
||||
|
||||
export function kind(model: Pick<Model, "providerID" | "api">, input?: Input) {
|
||||
const id = lower(model)
|
||||
if (
|
||||
model.providerID === "anthropic" &&
|
||||
model.api.npm === "@ai-sdk/anthropic" &&
|
||||
(id.includes("claude-opus-4-6") || id.includes("claude-opus-4.6") || id.includes("opus-4-6"))
|
||||
) {
|
||||
return "claude"
|
||||
}
|
||||
if (
|
||||
model.providerID === "openai" &&
|
||||
input?.codex === true &&
|
||||
model.api.npm === "@ai-sdk/openai" &&
|
||||
id.includes("gpt-5.4")
|
||||
) {
|
||||
return "codex"
|
||||
}
|
||||
}
|
||||
|
||||
export function enabled(model: Pick<Model, "providerID" | "api">, input?: Input) {
|
||||
return !!kind(model, input)
|
||||
}
|
||||
|
||||
export function options(model: Pick<Model, "providerID" | "api">, input?: Input) {
|
||||
const mode = kind(model, input)
|
||||
if (mode === "claude") return { speed: "fast" }
|
||||
if (mode === "codex") return { serviceTier: "priority" }
|
||||
return {}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type { Provider } from "./provider"
|
||||
import type { ModelsDev } from "./models"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import * as Fast from "./fast"
|
||||
|
||||
type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]
|
||||
|
||||
@@ -905,6 +906,10 @@ export namespace ProviderTransform {
|
||||
return { [key]: options }
|
||||
}
|
||||
|
||||
export function fast(model: Provider.Model, input?: { codex?: boolean }) {
|
||||
return Fast.options(model, input)
|
||||
}
|
||||
|
||||
export function maxOutputTokens(model: Provider.Model): number {
|
||||
return Math.min(model.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
|
||||
}
|
||||
|
||||
@@ -129,12 +129,7 @@ export class QuestionService extends ServiceMap.Service<QuestionService, Questio
|
||||
pending.set(id, { info, deferred })
|
||||
Bus.publish(Event.Asked, info)
|
||||
|
||||
return yield* Effect.ensuring(
|
||||
Deferred.await(deferred),
|
||||
Effect.sync(() => {
|
||||
pending.delete(id)
|
||||
}),
|
||||
)
|
||||
return yield* Deferred.await(deferred)
|
||||
})
|
||||
|
||||
const reply = Effect.fn("QuestionService.reply")(function* (input: { requestID: QuestionID; answers: Answer[] }) {
|
||||
|
||||
@@ -141,6 +141,7 @@ export namespace SessionCompaction {
|
||||
mode: "compaction",
|
||||
agent: "compaction",
|
||||
variant: userMessage.variant,
|
||||
fast: userMessage.fast,
|
||||
summary: true,
|
||||
path: {
|
||||
cwd: Instance.directory,
|
||||
|
||||
@@ -32,7 +32,6 @@ export namespace LLM {
|
||||
sessionID: string
|
||||
model: Provider.Model
|
||||
agent: Agent.Info
|
||||
permission?: PermissionNext.Ruleset
|
||||
system: string[]
|
||||
abort: AbortSignal
|
||||
messages: ModelMessage[]
|
||||
@@ -102,11 +101,15 @@ export namespace LLM {
|
||||
sessionID: input.sessionID,
|
||||
providerOptions: provider.options,
|
||||
})
|
||||
const fast = (
|
||||
input.small || !input.user.fast ? {} : ProviderTransform.fast(input.model, { codex: isCodex })
|
||||
) as Record<string, any>
|
||||
const options: Record<string, any> = pipe(
|
||||
base,
|
||||
mergeDeep(input.model.options),
|
||||
mergeDeep(input.agent.options),
|
||||
mergeDeep(variant),
|
||||
base as Record<string, any>,
|
||||
mergeDeep(input.model.options as Record<string, any>),
|
||||
mergeDeep(input.agent.options as Record<string, any>),
|
||||
mergeDeep(variant as Record<string, any>),
|
||||
mergeDeep(fast),
|
||||
)
|
||||
if (isCodex) {
|
||||
options.instructions = SystemPrompt.instructions()
|
||||
@@ -256,11 +259,8 @@ export namespace LLM {
|
||||
})
|
||||
}
|
||||
|
||||
async function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "permission" | "user">) {
|
||||
const disabled = PermissionNext.disabled(
|
||||
Object.keys(input.tools),
|
||||
PermissionNext.merge(input.agent.permission, input.permission ?? []),
|
||||
)
|
||||
async function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "user">) {
|
||||
const disabled = PermissionNext.disabled(Object.keys(input.tools), input.agent.permission)
|
||||
for (const tool of Object.keys(input.tools)) {
|
||||
if (input.user.tools?.[tool] === false || disabled.has(tool)) {
|
||||
delete input.tools[tool]
|
||||
|
||||
@@ -369,6 +369,7 @@ export namespace MessageV2 {
|
||||
system: z.string().optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
variant: z.string().optional(),
|
||||
fast: z.boolean().optional(),
|
||||
}).meta({
|
||||
ref: "UserMessage",
|
||||
})
|
||||
@@ -437,6 +438,7 @@ export namespace MessageV2 {
|
||||
}),
|
||||
structured: z.any().optional(),
|
||||
variant: z.string().optional(),
|
||||
fast: z.boolean().optional(),
|
||||
finish: z.string().optional(),
|
||||
}).meta({
|
||||
ref: "AssistantMessage",
|
||||
|
||||
@@ -111,6 +111,7 @@ export namespace SessionPrompt {
|
||||
format: MessageV2.Format.optional(),
|
||||
system: z.string().optional(),
|
||||
variant: z.string().optional(),
|
||||
fast: z.boolean().optional(),
|
||||
parts: z.array(
|
||||
z.discriminatedUnion("type", [
|
||||
MessageV2.TextPart.omit({
|
||||
@@ -363,6 +364,7 @@ export namespace SessionPrompt {
|
||||
mode: task.agent,
|
||||
agent: task.agent,
|
||||
variant: lastUser.variant,
|
||||
fast: lastUser.fast,
|
||||
path: {
|
||||
cwd: Instance.directory,
|
||||
root: Instance.worktree,
|
||||
@@ -575,6 +577,7 @@ export namespace SessionPrompt {
|
||||
mode: agent.name,
|
||||
agent: agent.name,
|
||||
variant: lastUser.variant,
|
||||
fast: lastUser.fast,
|
||||
path: {
|
||||
cwd: Instance.directory,
|
||||
root: Instance.worktree,
|
||||
@@ -666,7 +669,6 @@ export namespace SessionPrompt {
|
||||
const result = await processor.process({
|
||||
user: lastUser,
|
||||
agent,
|
||||
permission: session.permission,
|
||||
abort,
|
||||
sessionID,
|
||||
system,
|
||||
@@ -985,6 +987,7 @@ export namespace SessionPrompt {
|
||||
system: input.system,
|
||||
format: input.format,
|
||||
variant,
|
||||
fast: input.fast,
|
||||
}
|
||||
using _ = defer(() => InstructionPrompt.clear(info.id))
|
||||
|
||||
@@ -1311,6 +1314,7 @@ export namespace SessionPrompt {
|
||||
model: input.model,
|
||||
messageID: input.messageID,
|
||||
variant: input.variant,
|
||||
fast: input.fast,
|
||||
},
|
||||
{
|
||||
message: info,
|
||||
@@ -1728,6 +1732,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
arguments: z.string(),
|
||||
command: z.string(),
|
||||
variant: z.string().optional(),
|
||||
fast: z.boolean().optional(),
|
||||
parts: z
|
||||
.array(
|
||||
z.discriminatedUnion("type", [
|
||||
@@ -1885,6 +1890,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
agent: userAgent,
|
||||
parts,
|
||||
variant: input.variant,
|
||||
fast: input.fast,
|
||||
})) as MessageV2.WithParts
|
||||
|
||||
Bus.publish(Command.Event.Executed, {
|
||||
|
||||
@@ -176,6 +176,70 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("ProviderTransform.fast", () => {
|
||||
const createModel = (input: { providerID: string; modelID: string; npm: string }) =>
|
||||
({
|
||||
id: input.modelID,
|
||||
providerID: input.providerID,
|
||||
api: {
|
||||
id: input.modelID,
|
||||
url: "https://example.com",
|
||||
npm: input.npm,
|
||||
},
|
||||
name: input.modelID,
|
||||
capabilities: {
|
||||
temperature: true,
|
||||
reasoning: true,
|
||||
attachment: true,
|
||||
toolcall: true,
|
||||
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
||||
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||
interleaved: false,
|
||||
},
|
||||
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
||||
limit: { context: 200000, output: 8192 },
|
||||
status: "active",
|
||||
options: {},
|
||||
headers: {},
|
||||
}) as any
|
||||
|
||||
test("uses speed fast for anthropic opus 4.6", () => {
|
||||
const model = createModel({
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-opus-4-6",
|
||||
npm: "@ai-sdk/anthropic",
|
||||
})
|
||||
expect(ProviderTransform.fast(model)).toEqual({ speed: "fast" })
|
||||
})
|
||||
|
||||
test("uses priority service tier for openai gpt-5 codex models", () => {
|
||||
const model = createModel({
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5.4",
|
||||
npm: "@ai-sdk/openai",
|
||||
})
|
||||
expect(ProviderTransform.fast(model, { codex: true })).toEqual({ serviceTier: "priority" })
|
||||
})
|
||||
|
||||
test("returns empty options for unsupported models", () => {
|
||||
const model = createModel({
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-sonnet-4-6",
|
||||
npm: "@ai-sdk/anthropic",
|
||||
})
|
||||
expect(ProviderTransform.fast(model)).toEqual({})
|
||||
})
|
||||
|
||||
test("returns empty options for openai api mode", () => {
|
||||
const model = createModel({
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5.4",
|
||||
npm: "@ai-sdk/openai",
|
||||
})
|
||||
expect(ProviderTransform.fast(model)).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe("ProviderTransform.options - gateway", () => {
|
||||
const sessionID = "test-session-123"
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { tool, type ModelMessage } from "ai"
|
||||
import z from "zod"
|
||||
import type { ModelMessage } from "ai"
|
||||
import { LLM } from "../../src/session/llm"
|
||||
import { Global } from "../../src/global"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
@@ -326,95 +325,6 @@ describe("session.llm.stream", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("keeps tools enabled by prompt permissions", async () => {
|
||||
const server = state.server
|
||||
if (!server) {
|
||||
throw new Error("Server not initialized")
|
||||
}
|
||||
|
||||
const providerID = "alibaba"
|
||||
const modelID = "qwen-plus"
|
||||
const fixture = await loadFixture(providerID, modelID)
|
||||
const model = fixture.model
|
||||
|
||||
const request = waitRequest(
|
||||
"/chat/completions",
|
||||
new Response(createChatStream("Hello"), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
}),
|
||||
)
|
||||
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
enabled_providers: [providerID],
|
||||
provider: {
|
||||
[providerID]: {
|
||||
options: {
|
||||
apiKey: "test-key",
|
||||
baseURL: `${server.url.origin}/v1`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-tools")
|
||||
const agent = {
|
||||
name: "test",
|
||||
mode: "primary",
|
||||
options: {},
|
||||
permission: [{ permission: "question", pattern: "*", action: "deny" }],
|
||||
} satisfies Agent.Info
|
||||
|
||||
const user = {
|
||||
id: MessageID.make("user-tools"),
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make(providerID), modelID: resolved.id },
|
||||
tools: { question: true },
|
||||
} satisfies MessageV2.User
|
||||
|
||||
const stream = await LLM.stream({
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
permission: [{ permission: "question", pattern: "*", action: "allow" }],
|
||||
system: ["You are a helpful assistant."],
|
||||
abort: new AbortController().signal,
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {
|
||||
question: tool({
|
||||
description: "Ask a question",
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => ({ output: "" }),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
for await (const _ of stream.fullStream) {
|
||||
}
|
||||
|
||||
const capture = await request
|
||||
const tools = capture.body.tools as Array<{ function?: { name?: string } }> | undefined
|
||||
expect(tools?.some((item) => item.function?.name === "question")).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("sends responses API payload for OpenAI models", async () => {
|
||||
const server = state.server
|
||||
if (!server) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1841,6 +1841,7 @@ export class Session2 extends HeyApiClient {
|
||||
format?: OutputFormat
|
||||
system?: string
|
||||
variant?: string
|
||||
fast?: boolean
|
||||
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
@@ -1861,6 +1862,7 @@ export class Session2 extends HeyApiClient {
|
||||
{ in: "body", key: "format" },
|
||||
{ in: "body", key: "system" },
|
||||
{ in: "body", key: "variant" },
|
||||
{ in: "body", key: "fast" },
|
||||
{ in: "body", key: "parts" },
|
||||
],
|
||||
},
|
||||
@@ -1973,6 +1975,7 @@ export class Session2 extends HeyApiClient {
|
||||
format?: OutputFormat
|
||||
system?: string
|
||||
variant?: string
|
||||
fast?: boolean
|
||||
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
@@ -1993,6 +1996,7 @@ export class Session2 extends HeyApiClient {
|
||||
{ in: "body", key: "format" },
|
||||
{ in: "body", key: "system" },
|
||||
{ in: "body", key: "variant" },
|
||||
{ in: "body", key: "fast" },
|
||||
{ in: "body", key: "parts" },
|
||||
],
|
||||
},
|
||||
@@ -2026,6 +2030,7 @@ export class Session2 extends HeyApiClient {
|
||||
arguments?: string
|
||||
command?: string
|
||||
variant?: string
|
||||
fast?: boolean
|
||||
parts?: Array<{
|
||||
id?: string
|
||||
type: "file"
|
||||
@@ -2051,6 +2056,7 @@ export class Session2 extends HeyApiClient {
|
||||
{ in: "body", key: "arguments" },
|
||||
{ in: "body", key: "command" },
|
||||
{ in: "body", key: "variant" },
|
||||
{ in: "body", key: "fast" },
|
||||
{ in: "body", key: "parts" },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -238,6 +238,7 @@ export type UserMessage = {
|
||||
[key: string]: boolean
|
||||
}
|
||||
variant?: string
|
||||
fast?: boolean
|
||||
}
|
||||
|
||||
export type ProviderAuthError = {
|
||||
@@ -340,6 +341,7 @@ export type AssistantMessage = {
|
||||
}
|
||||
structured?: unknown
|
||||
variant?: string
|
||||
fast?: boolean
|
||||
finish?: string
|
||||
}
|
||||
|
||||
@@ -3284,6 +3286,7 @@ export type SessionPromptData = {
|
||||
format?: OutputFormat
|
||||
system?: string
|
||||
variant?: string
|
||||
fast?: boolean
|
||||
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||
}
|
||||
path: {
|
||||
@@ -3484,6 +3487,7 @@ export type SessionPromptAsyncData = {
|
||||
format?: OutputFormat
|
||||
system?: string
|
||||
variant?: string
|
||||
fast?: boolean
|
||||
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||
}
|
||||
path: {
|
||||
@@ -3526,6 +3530,7 @@ export type SessionCommandData = {
|
||||
arguments: string
|
||||
command: string
|
||||
variant?: string
|
||||
fast?: boolean
|
||||
parts?: Array<{
|
||||
id?: string
|
||||
type: "file"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -1050,8 +1050,18 @@
|
||||
line-height: var(--line-height-large);
|
||||
color: var(--text-base);
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-slot="question-option"][data-custom="true"] {
|
||||
[data-slot="option-description"] {
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="question-custom"] {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -49,7 +49,6 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
|
||||
| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Bundled multi-agent orchestration harness – 16 components, one install |
|
||||
| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Zero-friction git worktrees for OpenCode |
|
||||
| [opencode-sentry-monitor](https://github.com/stolinski/opencode-sentry-monitor) | Trace and debug your AI agents with Sentry AI Monitoring |
|
||||
| [opencode-firecrawl](https://github.com/firecrawl/opencode-firecrawl) | Web scraping, crawling, and search via the Firecrawl CLI |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
---
|
||||
title: Introducción
|
||||
description: Comience a usar OpenCode.
|
||||
description: Comience con OpenCode.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem } from "@astrojs/starlight/components"
|
||||
import config from "../../../../config.mjs"
|
||||
export const console = config.console
|
||||
|
||||
[**OpenCode**](/) es un agente de codigo de IA de código abierto. Está disponible como interfaz basada en terminal, aplicación de escritorio o extensión IDE.
|
||||
[**OpenCode**](/) es un agente de codificación de IA de código abierto. Está disponible como interfaz basada en terminal, aplicación de escritorio o extensión IDE.
|
||||
|
||||

|
||||
|
||||
Comencemos.
|
||||
Empecemos.
|
||||
|
||||
---
|
||||
|
||||
#### Requisitos previos
|
||||
|
||||
Para usar OpenCode en la terminal, necesitará:
|
||||
Para usar OpenCode en su terminal, necesitará:
|
||||
|
||||
1. Un emulador de terminal moderno como:
|
||||
- [WezTerm](https://wezterm.org), multiplataforma
|
||||
@@ -25,7 +25,7 @@ Para usar OpenCode en la terminal, necesitará:
|
||||
- [Ghostty](https://ghostty.org), Linux y macOS
|
||||
- [Kitty](https://sw.kovidgoyal.net/kitty/), Linux y macOS
|
||||
|
||||
2. Claves de API de los proveedores de LLM que quiera usar.
|
||||
2. Claves API para los LLM proveedores que desea utilizar.
|
||||
|
||||
---
|
||||
|
||||
@@ -37,7 +37,7 @@ La forma más sencilla de instalar OpenCode es mediante el script de instalació
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
|
||||
También puedes instalarlo con alguno de los siguientes métodos:
|
||||
También puedes instalarlo con los siguientes comandos:
|
||||
|
||||
- **Usando Node.js**
|
||||
|
||||
@@ -91,7 +91,7 @@ También puedes instalarlo con alguno de los siguientes métodos:
|
||||
#### Windows
|
||||
|
||||
:::tip[Recomendado: Usar WSL]
|
||||
Para obtener la mejor experiencia en Windows, recomendamos utilizar [Windows Subsystem for Linux (WSL)](/docs/windows-wsl). Ofrece mejor rendimiento y compatibilidad total con las funciones de OpenCode.
|
||||
Para obtener la mejor experiencia en Windows, recomendamos utilizar [Windows Subsystem for Linux (WSL)](/docs/windows-wsl). Proporciona un mejor rendimiento y compatibilidad total con las funciones de OpenCode.
|
||||
:::
|
||||
|
||||
- **Usando Chocolatey**
|
||||
@@ -124,28 +124,28 @@ Para obtener la mejor experiencia en Windows, recomendamos utilizar [Windows Sub
|
||||
docker run -it --rm ghcr.io/anomalyco/opencode
|
||||
```
|
||||
|
||||
El soporte para instalar OpenCode en Windows usando Bun todavía está en desarrollo.
|
||||
Actualmente se encuentra en progreso el soporte para instalar OpenCode en Windows usando Bun.
|
||||
|
||||
También puede obtener el binario desde [Versiones](https://github.com/anomalyco/opencode/releases).
|
||||
También puede obtener el binario de [Versiones](https://github.com/anomalyco/opencode/releases).
|
||||
|
||||
---
|
||||
|
||||
## Configuración
|
||||
|
||||
Con OpenCode, puede usar cualquier proveedor de LLM configurando sus claves de API.
|
||||
Con OpenCode puedes usar cualquier proveedor LLM configurando sus claves API.
|
||||
|
||||
Si es nuevo en el uso de proveedores de LLM, le recomendamos usar [OpenCode Zen](/docs/zen).
|
||||
Es una selección de modelos probados y verificados por el equipo de OpenCode.
|
||||
Si es nuevo en el uso de proveedores LLM, le recomendamos usar [OpenCode Zen](/docs/zen).
|
||||
Es una lista seleccionada de modelos que han sido probados y verificados por el equipo de OpenCode.
|
||||
|
||||
1. Ejecute el comando `/connect` en la TUI, seleccione opencode y diríjase a [opencode.ai/auth](https://opencode.ai/auth).
|
||||
1. Ejecute el comando `/connect` en TUI, seleccione opencode y diríjase a [opencode.ai/auth](https://opencode.ai/auth).
|
||||
|
||||
```txt
|
||||
/connect
|
||||
```
|
||||
|
||||
2. Inicie sesión, agregue sus datos de facturación y copie su clave de API.
|
||||
2. Inicie sesión, agregue sus datos de facturación y copie su clave API.
|
||||
|
||||
3. Pega tu clave de API.
|
||||
3. Pega tu clave API.
|
||||
|
||||
```txt
|
||||
┌ API key
|
||||
@@ -154,45 +154,50 @@ Es una selección de modelos probados y verificados por el equipo de OpenCode.
|
||||
└ enter
|
||||
```
|
||||
|
||||
También puede seleccionar otro proveedor. [Más información](/docs/providers#directory).
|
||||
Alternativamente, puede seleccionar uno de los otros proveedores. [Más información](/docs/providers#directory).
|
||||
|
||||
---
|
||||
|
||||
## Inicializar
|
||||
|
||||
Ahora que ya configuró un proveedor, vaya al proyecto en el que quiera trabajar.
|
||||
Ahora que ha configurado un proveedor, puede navegar a un proyecto que
|
||||
quieres trabajar.
|
||||
|
||||
```bash
|
||||
cd /path/to/project
|
||||
```
|
||||
|
||||
Luego, ejecute OpenCode.
|
||||
Y ejecute OpenCode.
|
||||
|
||||
```bash
|
||||
opencode
|
||||
```
|
||||
|
||||
A continuación, inicialice OpenCode para el proyecto con el siguiente comando:
|
||||
A continuación, inicialice OpenCode para el proyecto ejecutando el siguiente comando.
|
||||
|
||||
```bash frame="none"
|
||||
/init
|
||||
```
|
||||
|
||||
OpenCode analizará su proyecto y creará un archivo AGENTS.md en la raíz.
|
||||
Esto hará que OpenCode analice su proyecto y cree un archivo `AGENTS.md` en
|
||||
la raíz del proyecto.
|
||||
|
||||
:::tip
|
||||
Asegúrese de versionar en Git el archivo AGENTS.md de su proyecto.
|
||||
Debes enviar el archivo `AGENTS.md` de tu proyecto a Git.
|
||||
:::
|
||||
|
||||
Esto ayuda a OpenCode a comprender la estructura del proyecto y los patrones de código que se usan en él.
|
||||
Esto ayuda a OpenCode a comprender la estructura del proyecto y los patrones de codificación.
|
||||
usado.
|
||||
|
||||
---
|
||||
|
||||
## Usar
|
||||
|
||||
Ahora ya está listo para usar OpenCode en su proyecto. Puede pedirle desde explicaciones del código hasta cambios concretos.
|
||||
Ahora está listo para usar OpenCode para trabajar en su proyecto. No dudes en preguntarle
|
||||
¡cualquier cosa!
|
||||
|
||||
Si es la primera vez que usa un agente de codigo con IA, estos ejemplos pueden servirle como punto de partida.
|
||||
Si es nuevo en el uso de un agente de codificación de IA, aquí hay algunos ejemplos que podrían
|
||||
ayuda.
|
||||
|
||||
---
|
||||
|
||||
@@ -201,117 +206,126 @@ Si es la primera vez que usa un agente de codigo con IA, estos ejemplos pueden s
|
||||
Puede pedirle a OpenCode que le explique el código base.
|
||||
|
||||
:::tip
|
||||
Utilice la tecla `@` para realizar una búsqueda aproximada de archivos dentro del proyecto.
|
||||
Utilice la tecla `@` para realizar una búsqueda aproximada de archivos en el proyecto.
|
||||
:::
|
||||
|
||||
```txt frame="none" "@packages/functions/src/api/index.ts"
|
||||
¿Cómo se maneja la autenticación en @packages/functions/src/api/index.ts
|
||||
How is authentication handled in @packages/functions/src/api/index.ts
|
||||
```
|
||||
|
||||
Esto resulta útil cuando hay una parte del código base en la que usted no ha trabajado.
|
||||
Esto es útil si hay una parte del código base en la que no trabajaste.
|
||||
|
||||
---
|
||||
|
||||
### Agregar funcionalidades
|
||||
### Agregar funciones
|
||||
|
||||
Puede pedirle a OpenCode que agregue nuevas funcionalidades a su proyecto. Aun así, primero recomendamos pedirle que cree un plan.
|
||||
Puede pedirle a OpenCode que agregue nuevas funciones a su proyecto. Aunque primero recomendamos pedirle que cree un plan.
|
||||
|
||||
1. **Crear un plan**
|
||||
1. **Crea un plan**
|
||||
|
||||
OpenCode tiene un modo Plan que desactiva temporalmente su capacidad de hacer cambios y, en su lugar, propone _cómo_ implementará la funcionalidad.
|
||||
OpenCode tiene un _Modo Plan_ que desactiva su capacidad para realizar cambios y
|
||||
en su lugar, sugiera _cómo_ implementará la función.
|
||||
|
||||
Cambie a este modo con la tecla **Tab.** Verá un indicador en la esquina inferior derecha.
|
||||
Cambie a él usando la tecla **Tab**. Verás un indicador para esto en la esquina inferior derecha.
|
||||
|
||||
```bash frame="none" title="Switch to Plan mode"
|
||||
<TAB>
|
||||
```
|
||||
|
||||
Ahora describa lo que quiere que haga.
|
||||
Ahora describamos lo que queremos que haga.
|
||||
|
||||
```txt frame="none"
|
||||
Cuando un usuario elimine una nota, queremos marcarla como eliminada en la base de datos.
|
||||
Luego, cree una pantalla que muestre todas las notas eliminadas recientemente.
|
||||
Desde esa pantalla, el usuario podrá restaurar una nota o eliminarla de forma permanente.
|
||||
When a user deletes a note, we'd like to flag it as deleted in the database.
|
||||
Then create a screen that shows all the recently deleted notes.
|
||||
From this screen, the user can undelete a note or permanently delete it.
|
||||
```
|
||||
|
||||
Procure darle a OpenCode suficiente contexto para que entienda exactamente lo que necesita. Ayuda hablarle como si estuviera hablando con un desarrollador junior de su equipo.
|
||||
Quiere darle a OpenCode suficientes detalles para entender lo que quiere. ayuda
|
||||
hablar con él como si estuviera hablando con un desarrollador junior de su equipo.
|
||||
|
||||
:::tip
|
||||
Déle a OpenCode todo el contexto y los ejemplos que pueda para ayudarle a comprender lo que desea.
|
||||
Dale a OpenCode mucho contexto y ejemplos para ayudarlo a comprender lo que
|
||||
desear.
|
||||
:::
|
||||
|
||||
2. **Iterar sobre el plan**
|
||||
2. **Repetir el plan**
|
||||
|
||||
Una vez que OpenCode le proponga un plan, puede darle comentarios o agregar más detalles.
|
||||
Una vez que le proporcione un plan, puede enviarle comentarios o agregar más detalles.
|
||||
|
||||
```txt frame="none"
|
||||
Queremos diseñar esta nueva pantalla usando un diseño que ya hemos usado antes.
|
||||
[Imagen #1] Revise esta imagen y úsela como referencia.
|
||||
We'd like to design this new screen using a design I've used before.
|
||||
[Image #1] Take a look at this image and use it as a reference.
|
||||
```
|
||||
|
||||
:::tip
|
||||
Arrastre y suelte imágenes en la terminal para agregarlas al mensaje.
|
||||
:::
|
||||
|
||||
OpenCode puede analizar cualquier imagen que usted le proporcione y añadirla al contexto del mensaje. Puede hacerlo arrastrando y soltando una imagen en la terminal.
|
||||
OpenCode puede escanear cualquier imagen que le proporcione y agregarla al mensaje. Puede
|
||||
Haga esto arrastrando y soltando una imagen en la terminal.
|
||||
|
||||
3. **Implementar la funcionalidad**
|
||||
3. **Crea la función**
|
||||
|
||||
Cuando esté conforme con el plan, vuelva al modo _Build_ presionando de nuevo la tecla Tab.
|
||||
Una vez que se sienta cómodo con el plan, vuelva al _Modo Build_
|
||||
presionando la tecla **Tab** nuevamente.
|
||||
|
||||
```bash frame="none"
|
||||
<TAB>
|
||||
```
|
||||
|
||||
Luego, pídale que haga los cambios.
|
||||
Y pidiéndole que haga los cambios.
|
||||
|
||||
```bash frame="none"
|
||||
Perfecto. Continúe y realice los cambios.
|
||||
Sounds good! Go ahead and make the changes.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Realizar cambios
|
||||
|
||||
Para cambios más sencillos, puede pedirle a OpenCode que los implemente directamente, sin revisar antes un plan.
|
||||
Para cambios más sencillos, puede pedirle a OpenCode que lo construya directamente.
|
||||
sin tener que revisar el plan primero.
|
||||
|
||||
```txt frame="none" "@packages/functions/src/settings.ts" "@packages/functions/src/notes.ts"
|
||||
Necesitamos agregar autenticación a la ruta /settings. Revise cómo se maneja esto
|
||||
en la ruta /notes en @packages/functions/src/notes.ts e implemente
|
||||
la misma lógica en @packages/functions/src/settings.ts.
|
||||
We need to add authentication to the /settings route. Take a look at how this is
|
||||
handled in the /notes route in @packages/functions/src/notes.ts and implement
|
||||
the same logic in @packages/functions/src/settings.ts
|
||||
```
|
||||
|
||||
Procure dar suficientes detalles para que OpenCode pueda tomar las decisiones correctas al hacer los cambios
|
||||
Desea asegurarse de proporcionar una buena cantidad de detalles para que OpenCode tome la decisión correcta.
|
||||
cambios.
|
||||
|
||||
---
|
||||
|
||||
### Deshacer cambios
|
||||
|
||||
Supongamos que le pide a OpenCode que haga algunos cambios.
|
||||
Digamos que le pides a OpenCode que haga algunos cambios.
|
||||
|
||||
```txt frame="none" "@packages/functions/src/api/index.ts"
|
||||
¿Puede refactorizar la función en @packages/functions/src/api/index.ts?
|
||||
Can you refactor the function in @packages/functions/src/api/index.ts?
|
||||
```
|
||||
|
||||
Pero luego se da cuenta de que no era lo que quería. Puede **deshacer** los cambios usando el comando `/undo`.
|
||||
Pero te das cuenta de que no es lo que querías. Puedes **deshacer** los cambios
|
||||
usando el comando `/undo`.
|
||||
|
||||
```bash frame="none"
|
||||
/undo
|
||||
```
|
||||
|
||||
OpenCode revertirá los cambios que hizo y volverá a mostrar su mensaje original.
|
||||
OpenCode ahora revertirá los cambios que realizó y mostrará su mensaje original
|
||||
de nuevo.
|
||||
|
||||
```txt frame="none" "@packages/functions/src/api/index.ts"
|
||||
¿Puede refactorizar la función en @packages/functions/src/api/index.ts?
|
||||
Can you refactor the function in @packages/functions/src/api/index.ts?
|
||||
```
|
||||
|
||||
Desde ahí, puede modificar el mensaje y pedirle a OpenCode que lo intente de nuevo.
|
||||
Desde aquí puedes modificar el mensaje y pedirle a OpenCode que vuelva a intentarlo.
|
||||
|
||||
:::tip
|
||||
Puede ejecutar `/undo` varias veces para deshacer varios cambios.
|
||||
:::
|
||||
|
||||
También puede rehacer los cambios usando el comando `/redo.`
|
||||
O **puedes rehacer** los cambios usando el comando `/redo`.
|
||||
|
||||
```bash frame="none"
|
||||
/redo
|
||||
@@ -321,7 +335,7 @@ También puede rehacer los cambios usando el comando `/redo.`
|
||||
|
||||
## Compartir
|
||||
|
||||
Las conversaciones que tenga con OpenCode pueden [compartirse con su
|
||||
Las conversaciones que tengas con OpenCode pueden ser [compartidas con tu
|
||||
equipo](/docs/share).
|
||||
|
||||
```bash frame="none"
|
||||
@@ -334,12 +348,12 @@ Esto creará un enlace a la conversación actual y lo copiará en su portapapele
|
||||
Las conversaciones no se comparten de forma predeterminada.
|
||||
:::
|
||||
|
||||
Aquí tiene una [conversación de ejemplo](https://opencode.ai/s/4XP1fce5) con OpenCode.
|
||||
Aquí hay una [conversación de ejemplo](https://opencode.ai/s/4XP1fce5) con OpenCode.
|
||||
|
||||
---
|
||||
|
||||
## Personalizar
|
||||
|
||||
Y eso es todo. Ya conoce lo básico para empezar a usar OpenCode.
|
||||
¡Y eso es todo! Ahora eres un profesional en el uso de OpenCode.
|
||||
|
||||
Para personalizarlo, recomendamos [elegir un tema](/docs/themes), [personalizar las combinaciones de teclas](/docs/keybinds), [configurar formateadores de código](/docs/formatters), [crear comandos personalizados](/docs/commands) o explorar la [configuración OpenCode](/docs/config).
|
||||
Para personalizarlo, recomendamos [elegir un tema](/docs/themes), [personalizar las combinaciones de teclas](/docs/keybinds), [configurar formateadores de código](/docs/formatters), [crear comandos personalizados](/docs/commands) o jugar con la [configuración OpenCode](/docs/config).
|
||||
|
||||
@@ -137,10 +137,12 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
||||
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
|
||||
| Kimi K2 | $0.40 | $2.50 | - | - |
|
||||
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
|
||||
| Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 |
|
||||
| Claude Sonnet 4.6 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
|
||||
| Claude Sonnet 4.6 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
|
||||
| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
|
||||
| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
|
||||
| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.26",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user