mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-17 06:16:20 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c8301e155 |
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@opencode/ai",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -113,7 +113,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"bin": {
|
||||
"opencode2": "./bin/opencode2.cjs",
|
||||
},
|
||||
@@ -177,7 +177,7 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@opencode/protocol": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -203,7 +203,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -217,7 +217,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -253,7 +253,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode/console-core",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -280,7 +280,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -297,7 +297,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode/console-mail",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -321,7 +321,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode/console-support",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -341,7 +341,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode/core",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
@@ -409,7 +409,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode/desktop",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"electron-context-menu": "4.1.2",
|
||||
@@ -461,7 +461,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/sdk": "1.18.21",
|
||||
@@ -498,7 +498,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode/function",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -514,7 +514,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode/http-recorder",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-rc.112",
|
||||
},
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"packages/httpapi-codegen": {
|
||||
"name": "@opencode/httpapi-codegen",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"prettier": "3.6.2",
|
||||
@@ -546,7 +546,7 @@
|
||||
},
|
||||
"packages/latex": {
|
||||
"name": "@opencode/latex",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -560,7 +560,7 @@
|
||||
},
|
||||
"packages/merman": {
|
||||
"name": "@opencode/merman",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -575,7 +575,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode/plugin",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode/ai": "workspace:*",
|
||||
@@ -614,7 +614,7 @@
|
||||
},
|
||||
"packages/plugin-browser": {
|
||||
"name": "@opencode/plugin-browser",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -644,7 +644,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@opencode/protocol",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@opencode/schema": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -659,7 +659,7 @@
|
||||
},
|
||||
"packages/schema": {
|
||||
"name": "@opencode/schema",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@opencode/sdk",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -704,7 +704,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode/server",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/platform-node-shared": "catalog:",
|
||||
@@ -726,7 +726,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode/session-ui",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode/client": "workspace:*",
|
||||
@@ -761,7 +761,7 @@
|
||||
},
|
||||
"packages/simulation": {
|
||||
"name": "@opencode/simulation",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@opencode/ai": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -781,7 +781,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode/stats-app",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -815,7 +815,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode/stats-core",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -834,7 +834,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode/stats-server",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -880,7 +880,7 @@
|
||||
},
|
||||
"packages/theme": {
|
||||
"name": "@opencode/theme",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -894,7 +894,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode/tui",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -929,7 +929,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode/ui",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -964,7 +964,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode/util",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -997,7 +997,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode/web",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1038,7 +1038,7 @@
|
||||
},
|
||||
"services/update": {
|
||||
"name": "@opencode/update",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"jose": "6.0.11",
|
||||
"semver": "catalog:",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "opencode",
|
||||
"description": "AI-powered development tool",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.4.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"name": "@opencode/ai",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -336,18 +336,12 @@ export type OutputItem = StreamItem & { readonly id: string }
|
||||
// Responses-compatible providers put streaming error details at the top level or
|
||||
// under `error`, and response failures under `response.error`. Accept all three shapes.
|
||||
// https://www.openresponses.org/specification
|
||||
const OpenResponsesErrorObject = Schema.Struct({
|
||||
const OpenResponsesErrorPayload = Schema.Struct({
|
||||
type: optionalNull(Schema.String),
|
||||
code: optionalNull(Schema.String),
|
||||
message: optionalNull(Schema.String),
|
||||
param: optionalNull(Schema.String),
|
||||
})
|
||||
const OpenResponsesErrorPayload = Schema.Union([Schema.String, OpenResponsesErrorObject]).pipe(
|
||||
Schema.decodeTo(OpenResponsesErrorObject, {
|
||||
decode: SchemaGetter.transform((error) => (typeof error === "string" ? { message: error } : error)),
|
||||
encode: SchemaGetter.passthrough(),
|
||||
}),
|
||||
)
|
||||
type OpenResponsesErrorPayload = Schema.Schema.Type<typeof OpenResponsesErrorPayload>
|
||||
|
||||
const WebSocketErrorHeader = Schema.Union([Schema.String, Schema.Number, Schema.Boolean])
|
||||
@@ -440,9 +434,7 @@ export const decodeChannelEvent = (frame: string) =>
|
||||
decodeFrame(frame).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeEventValue(
|
||||
ProviderShared.isRecord(value) &&
|
||||
value.type === undefined &&
|
||||
(typeof value.error === "string" || ProviderShared.isRecord(value.error))
|
||||
ProviderShared.isRecord(value) && value.type === undefined && ProviderShared.isRecord(value.error)
|
||||
? { ...value, type: "error" }
|
||||
: value,
|
||||
),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { LLM, LLMClient } from "../../src/index.js"
|
||||
import { OpenResponses } from "../../src/protocols/open-responses.js"
|
||||
import { Azure, Meta, OpenAI, XAI } from "../../src/providers/index.js"
|
||||
import { WebSocketTransport } from "../../src/route.js"
|
||||
import { Meta } from "../../src/providers/index.js"
|
||||
import { configure } from "../../src/providers/openai-compatible-responses.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
import { fixedResponse } from "../lib/http.js"
|
||||
@@ -45,59 +44,11 @@ it.effect("normalizes flat errors in shared SSE and WebSocket decoding", () =>
|
||||
|
||||
it.effect("continues to normalize untyped xAI WebSocket errors", () =>
|
||||
Effect.gen(function* () {
|
||||
const message = "gRPC error: Response with id=resp_missing not found"
|
||||
for (const error of [{ type: "api_error", message }, message]) {
|
||||
expect(yield* OpenResponses.decodeChannelEvent(JSON.stringify({ error }))).toEqual({
|
||||
type: "error",
|
||||
error: typeof error === "string" ? { message } : error,
|
||||
})
|
||||
}
|
||||
const frame = { error: { type: "api_error", message: "gRPC error: Response with id=resp_missing not found" } }
|
||||
expect(yield* OpenResponses.decodeChannelEvent(JSON.stringify(frame))).toEqual({ ...frame, type: "error" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("normalizes string errors in shared SSE and WebSocket decoding", () =>
|
||||
Effect.gen(function* () {
|
||||
for (const decode of [decodeEvent, OpenResponses.decodeChannelEvent]) {
|
||||
expect(yield* decode(JSON.stringify({ type: "error", error: "Gateway failed" }))).toEqual({
|
||||
type: "error",
|
||||
error: { message: "Gateway failed" },
|
||||
})
|
||||
expect(
|
||||
yield* decode(
|
||||
JSON.stringify({ type: "response.failed", response: { id: "resp_failed", error: "Gateway failed" } }),
|
||||
),
|
||||
).toEqual({
|
||||
type: "response.failed",
|
||||
response: { id: "resp_failed", error: { message: "Gateway failed" } },
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
for (const model of [
|
||||
OpenAI.configure({ apiKey: "fixture" }).responses("gpt-5.6-sol"),
|
||||
XAI.configure({ apiKey: "fixture" }).responses("grok-4.6"),
|
||||
Azure.configure({ apiKey: "fixture", resourceName: "fixture" }).responses("deployment"),
|
||||
]) {
|
||||
it.effect(`preserves string error messages and raw bodies through ${model.provider} Responses`, () =>
|
||||
Effect.gen(function* () {
|
||||
const raw = '{ "type": "error", "error": "Gateway rejected the request", "trace": "original" }'
|
||||
const webSocket = WebSocketTransport.makeDirect({
|
||||
open: () => Effect.succeed({ sendText: () => Effect.void, messages: Stream.make(raw), close: Effect.void }),
|
||||
})
|
||||
for (const options of [{ webSocket }, {}]) {
|
||||
const error = yield* LLMClient.generate(LLM.request({ model, prompt: "Hello" }), options).pipe(
|
||||
Effect.provide(fixedResponse(sseEvents(raw))),
|
||||
Effect.flip,
|
||||
)
|
||||
expect(error.reason._tag).toBe("UnknownProvider")
|
||||
expect(error.message).toBe("Gateway rejected the request")
|
||||
expect(error.reason.body).toBe(raw)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
it.effect("retains classification and original error bodies through Meta and generic Responses routes", () =>
|
||||
Effect.gen(function* () {
|
||||
const raw = `{
|
||||
|
||||
@@ -122,6 +122,35 @@ story("does not mask or pad controls when they fit", async ({ mount }) => {
|
||||
await expect(controls).toHaveCSS("padding-inline-end", "0px")
|
||||
})
|
||||
|
||||
story("grows suggestions while preserving visible timeline context", async ({ mount }) => {
|
||||
const component = await mount("opencode-composer-flow--constrained-command-suggestions")
|
||||
const boundary = component.locator('[data-slot="composer-suggestion-boundary-story"]')
|
||||
const suggestions = component.locator('[data-component="composer-suggestions"]')
|
||||
|
||||
await expect(suggestions).toHaveCSS("max-height", "166px")
|
||||
await expect(suggestions).toHaveCSS("scroll-padding-bottom", "18px")
|
||||
await expect.poll(() => suggestions.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true)
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const menu = await suggestions.boundingBox()
|
||||
const items = await suggestions.locator("[data-suggestion-id]").evaluateAll((elements) =>
|
||||
elements.map((element) => {
|
||||
const rect = element.getBoundingClientRect()
|
||||
return { top: rect.top, bottom: rect.bottom }
|
||||
}),
|
||||
)
|
||||
if (!menu) return false
|
||||
const bottom = menu.y + menu.height
|
||||
return items.some((item) => item.top < bottom && item.bottom > bottom)
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
await boundary.evaluate((element) => {
|
||||
element.style.height = "400px"
|
||||
})
|
||||
await expect(suggestions).toHaveCSS("max-height", "306px")
|
||||
})
|
||||
|
||||
// ThemeProvider writes resolved token values into a <style> block, so toggling data-color-scheme by hand
|
||||
// leaves every --v2-* variable at its previous value. Switch themes through the Storybook global instead.
|
||||
for (const [theme, background] of [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -57,6 +57,8 @@ function ComposerStory(props: {
|
||||
continueOnStop?: boolean
|
||||
longLabels?: boolean
|
||||
alternate?: "queue" | "steer"
|
||||
manySuggestions?: boolean
|
||||
suggestionBoundary?: () => HTMLElement | undefined
|
||||
}) {
|
||||
const [draft, setDraft] = createStore<ComposerPersistedState>({
|
||||
prompt: props.prompt ?? [{ type: "text", content: "", start: 0, end: 0 }],
|
||||
@@ -93,6 +95,15 @@ function ComposerStory(props: {
|
||||
const commands: ComposerSuggestion[] = [
|
||||
{ id: "command.test", kind: "command", label: "/test", trigger: "test", title: "Run tests" },
|
||||
{ id: "command.review", kind: "command", label: "/review", trigger: "review", title: "Review changes" },
|
||||
...(props.manySuggestions
|
||||
? Array.from({ length: 12 }, (_, index) => ({
|
||||
id: `command.example-${index}`,
|
||||
kind: "command" as const,
|
||||
label: `/example-${index}`,
|
||||
trigger: `example-${index}`,
|
||||
title: `Run example ${index}`,
|
||||
}))
|
||||
: []),
|
||||
]
|
||||
const context: ComposerSuggestion[] = [
|
||||
{
|
||||
@@ -206,7 +217,7 @@ function ComposerStory(props: {
|
||||
<output class="text-12-regular text-text-weak" aria-live="polite">
|
||||
{story.activity}
|
||||
</output>
|
||||
<Composer model={model} borderUnderlay />
|
||||
<Composer model={model} borderUnderlay suggestionBoundary={props.suggestionBoundary} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -273,6 +284,18 @@ export const SlashSuggestions = { render: () => <ComposerStory suggestions="comm
|
||||
|
||||
export const ContextSuggestions = { render: () => <ComposerStory suggestions="context" /> }
|
||||
|
||||
function ConstrainedCommandSuggestionsStory() {
|
||||
let boundary: HTMLDivElement | undefined
|
||||
return (
|
||||
<div class="mx-auto w-full max-w-200">
|
||||
<div ref={boundary} data-slot="composer-suggestion-boundary-story" class="h-60" />
|
||||
<ComposerStory suggestions="command" manySuggestions suggestionBoundary={() => boundary} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ConstrainedCommandSuggestions = { render: () => <ConstrainedCommandSuggestionsStory /> }
|
||||
|
||||
export const RunningAndStopping = { render: () => <ComposerStory working stopping label="Session is running" /> }
|
||||
|
||||
export const SteeringFollowUp = {
|
||||
|
||||
@@ -12,7 +12,12 @@ import { formatKeybind, useCommand } from "@/shell/commands/command"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import type { ComposerModel } from "./model"
|
||||
|
||||
export function Composer(props: { class?: string; model: ComposerModel; borderUnderlay?: boolean }) {
|
||||
export function Composer(props: {
|
||||
class?: string
|
||||
model: ComposerModel
|
||||
borderUnderlay?: boolean
|
||||
suggestionBoundary?: () => HTMLElement | undefined
|
||||
}) {
|
||||
const dialog = useDialog()
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
@@ -28,6 +33,7 @@ export function Composer(props: { class?: string; model: ComposerModel; borderUn
|
||||
attachShortcut={command.keybind("file.attach")}
|
||||
alternateKeybind={[formatKeybind("mod", language.t), "↵"]}
|
||||
exitShellKeybind={[formatKeybind("esc", language.t)]}
|
||||
suggestionBoundary={props.suggestionBoundary}
|
||||
modelControl={
|
||||
<ComposerModelControl
|
||||
loading={props.model.model.loading}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show, type JSX } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { FileIcon } from "@opencode/ui/file-icon"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { IconButton } from "@opencode/ui/icon-button"
|
||||
@@ -36,6 +37,12 @@ export type {
|
||||
} from "../types"
|
||||
|
||||
export type ComposerMode = "normal" | "shell"
|
||||
const COMPOSER_SUGGESTION_MAX_HEIGHT = 320
|
||||
const COMPOSER_SUGGESTION_ROW_HEIGHT = 28
|
||||
const COMPOSER_SUGGESTION_ROW_PEEK = 18
|
||||
const COMPOSER_SUGGESTION_TOP_PADDING = 8
|
||||
const COMPOSER_SUGGESTION_SEARCH_HEIGHT = 28
|
||||
const COMPOSER_SUGGESTION_CONTEXT_RESERVE = 80
|
||||
|
||||
export type ComposerEditorProps = {
|
||||
controller: ComposerEditorModel
|
||||
@@ -49,6 +56,7 @@ export type ComposerEditorProps = {
|
||||
attachShortcut?: string
|
||||
alternateKeybind?: string[]
|
||||
exitShellKeybind?: string[]
|
||||
suggestionBoundary?: () => HTMLElement | undefined
|
||||
}
|
||||
|
||||
export function ComposerEditor(props: ComposerEditorProps) {
|
||||
@@ -114,6 +122,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
|
||||
<ComposerEditorPopover
|
||||
emptyLabel={i18n.t("ui.promptInput.noMatchingItems")}
|
||||
items={props.controller.suggestions()}
|
||||
boundary={props.suggestionBoundary}
|
||||
activeID={state.popover.type === "closed" ? undefined : state.popover.activeID}
|
||||
search={
|
||||
state.popover.type === "command-menu"
|
||||
@@ -717,18 +726,29 @@ export function ComposerEditorPopover(props: {
|
||||
onValueChange: (value: string) => void
|
||||
onKeyDown: (event: KeyboardEvent) => void
|
||||
}
|
||||
boundary?: () => HTMLElement | undefined
|
||||
onActiveChange: (item: ComposerSuggestion) => void
|
||||
onSelect: (item: ComposerSuggestion) => void
|
||||
}) {
|
||||
const [store, setStore] = createStore({ maxHeight: COMPOSER_SUGGESTION_MAX_HEIGHT })
|
||||
const resize = (height: number) =>
|
||||
setStore("maxHeight", composerSuggestionMaxHeight(height, props.search !== undefined))
|
||||
createEffect(() => resize(props.boundary?.()?.clientHeight ?? COMPOSER_SUGGESTION_MAX_HEIGHT * 2))
|
||||
createResizeObserver(
|
||||
props.boundary ?? (() => undefined),
|
||||
(rect) => resize(rect.height),
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="composer-suggestions"
|
||||
class="absolute inset-x-0 -top-2 z-40 flex max-h-80 -translate-y-full flex-col overflow-auto rounded-xl bg-v2-background-bg-base p-2 shadow-[var(--v2-elevation-raised)] no-scrollbar"
|
||||
class="absolute inset-x-0 -top-2 z-40 flex -translate-y-full scroll-pb-[18px] flex-col overflow-auto rounded-xl bg-v2-background-bg-base p-2 shadow-[var(--v2-elevation-raised)] no-scrollbar"
|
||||
style={{ "max-height": `${store.maxHeight}px` }}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Show when={props.search}>
|
||||
{(search) => (
|
||||
<div class="px-2 py-1">
|
||||
<div class="shrink-0 px-2 py-1">
|
||||
<input
|
||||
ref={(element) => requestAnimationFrame(() => element.focus())}
|
||||
value={search().value}
|
||||
@@ -752,7 +772,7 @@ export function ComposerEditorPopover(props: {
|
||||
type="button"
|
||||
data-suggestion-id={item.id}
|
||||
data-active={props.activeID === item.id ? "" : undefined}
|
||||
class="flex w-full items-center gap-2 rounded-md px-2 py-1 text-start hover:bg-v2-overlay-simple-overlay-hover"
|
||||
class="flex h-7 w-full shrink-0 items-center gap-2 rounded-md px-2 py-1 text-start hover:bg-v2-overlay-simple-overlay-hover"
|
||||
classList={{ "bg-v2-overlay-simple-overlay-hover": props.activeID === item.id }}
|
||||
onPointerMove={() => props.onActiveChange(item)}
|
||||
onClick={() => props.onSelect(item)}
|
||||
@@ -777,6 +797,19 @@ export function ComposerEditorPopover(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function composerSuggestionMaxHeight(boundaryHeight: number, search: boolean) {
|
||||
const reserve = Math.min(COMPOSER_SUGGESTION_CONTEXT_RESERVE, boundaryHeight / 4)
|
||||
const limit = Math.min(COMPOSER_SUGGESTION_MAX_HEIGHT, boundaryHeight - reserve)
|
||||
const chrome = COMPOSER_SUGGESTION_TOP_PADDING + (search ? COMPOSER_SUGGESTION_SEARCH_HEIGHT : 0)
|
||||
if (limit < chrome + COMPOSER_SUGGESTION_ROW_HEIGHT + COMPOSER_SUGGESTION_ROW_PEEK) return limit
|
||||
return (
|
||||
chrome +
|
||||
Math.floor((limit - chrome - COMPOSER_SUGGESTION_ROW_PEEK) / COMPOSER_SUGGESTION_ROW_HEIGHT) *
|
||||
COMPOSER_SUGGESTION_ROW_HEIGHT +
|
||||
COMPOSER_SUGGESTION_ROW_PEEK
|
||||
)
|
||||
}
|
||||
|
||||
// "Steer ⌘⏎" / "Queue ⌘⏎" hint next to the submit button: submits with the
|
||||
// delivery opposite to what plain Enter does. Visible only while the queue
|
||||
// exposes an alternate (turn running and composer holding a value), so it
|
||||
|
||||
@@ -213,7 +213,10 @@ export function createActiveSessionRegion(input: {
|
||||
|
||||
export type ActiveSessionRegionModel = ReturnType<typeof createActiveSessionRegion>
|
||||
|
||||
export function ActiveSessionComposerRegion(props: { model: SessionComposerController }) {
|
||||
export function ActiveSessionComposerRegion(props: {
|
||||
model: SessionComposerController
|
||||
suggestionBoundary: () => HTMLElement | undefined
|
||||
}) {
|
||||
return (
|
||||
<SessionComposerRegion
|
||||
controller={props.model.region}
|
||||
@@ -221,7 +224,7 @@ export function ActiveSessionComposerRegion(props: { model: SessionComposerContr
|
||||
<div class="relative">
|
||||
<SessionQueuePanel queue={props.model.queue} />
|
||||
<div class="relative z-10">
|
||||
<Composer model={props.model.composer} borderUnderlay />
|
||||
<Composer model={props.model.composer} borderUnderlay suggestionBoundary={props.suggestionBoundary} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ export function SessionScreen(props: { session: SessionModel }) {
|
||||
</div>
|
||||
|
||||
<Show when={composer.active()} keyed>
|
||||
{(model) => <ActiveSessionComposerRegion model={model} />}
|
||||
{(model) => <ActiveSessionComposerRegion model={model} suggestionBoundary={timeline.scroller} />}
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -13,7 +13,6 @@ import { HttpServer } from "effect/unstable/http"
|
||||
import { Env } from "./env"
|
||||
import { ServiceConfig } from "./services/service-config"
|
||||
import { ServiceRegistration } from "./services/service-registration"
|
||||
import { ShellEnvironment } from "./shell-environment"
|
||||
import { Updater } from "./services/updater"
|
||||
import { WebUi } from "./services/web-ui"
|
||||
import { databasePath } from "./database-path"
|
||||
@@ -29,9 +28,6 @@ export type Options = {
|
||||
|
||||
// The process effect lives until server shutdown; tracing it would parent every request to one process-lifetime trace.
|
||||
export const run = Effect.fnUntraced(function* (options: Options) {
|
||||
// A managed service may have been started by a GUI client with launchd's environment. Adopt the
|
||||
// login shell's before anything reads process.env, including the OPENCODE_* settings below.
|
||||
if (options.mode === "service") yield* ShellEnvironment.adopt()
|
||||
return yield* processEffect(options).pipe(
|
||||
Effect.provide(
|
||||
LayerNode.compile(LayerNode.group([Global.node, AppProcess.node]), {
|
||||
@@ -42,8 +38,9 @@ export const run = Effect.fnUntraced(function* (options: Options) {
|
||||
],
|
||||
}),
|
||||
),
|
||||
Effect.provide(NodeServices.layer),
|
||||
)
|
||||
}, Effect.provide(NodeServices.layer))
|
||||
})
|
||||
|
||||
const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
const inherited = process.env.OPENCODE_PTY_HANDOFF
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
export * as ShellEnvironment from "./shell-environment"
|
||||
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { Effect } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
||||
|
||||
// Set on the probe shell so an rc file that itself starts opencode cannot recurse into another probe.
|
||||
export const RESOLVING = "OPENCODE_RESOLVING_SHELL_ENVIRONMENT"
|
||||
|
||||
// Shell bookkeeping that describes the probe process, not the user's environment.
|
||||
const TRANSIENT = new Set([RESOLVING, "SHLVL", "PWD", "OLDPWD", "_"])
|
||||
|
||||
export type Variables = Readonly<Record<string, string | undefined>>
|
||||
|
||||
/**
|
||||
* Resolve the environment of the user's interactive login shell when the current environment did
|
||||
* not come from one.
|
||||
*
|
||||
* A background service elected by a GUI client (the desktop app, an editor, a login item) inherits
|
||||
* launchd's environment: PATH is `/usr/bin:/bin:/usr/sbin:/sbin` and nothing exported from
|
||||
* `.zshrc` or `.bashrc` exists. Every process the server spawns extends `process.env`, so stdio MCP
|
||||
* servers using `#!/usr/bin/env node`, `npx`, `uvx`, formatters, and hooks fail with no useful error
|
||||
* while the bash tool, which prefers the client's terminal environment, keeps working. Editors such
|
||||
* as VS Code and Zed resolve the login shell at startup for the same reason.
|
||||
*
|
||||
* Returns `undefined` when there is nothing to adopt: Windows, an environment that already passed
|
||||
* through a shell (`SHLVL`), a probe already in progress, or a shell that fails to report.
|
||||
*/
|
||||
export const resolve = Effect.fnUntraced(function* (env: Variables = process.env) {
|
||||
if (process.platform === "win32") return undefined
|
||||
if (env[RESOLVING] !== undefined || env.SHLVL !== undefined) return undefined
|
||||
const spawner = yield* ChildProcessSpawner
|
||||
const marker = randomUUID()
|
||||
// Interactive and login: most PATH edits live in `.zshrc`/`.bashrc`, which only interactive shells read.
|
||||
// `env -0` keeps values containing newlines intact.
|
||||
const output = yield* spawner
|
||||
.string(
|
||||
ChildProcess.make(env.SHELL || "/bin/sh", ["-ilc", `printf '%s' '${marker}'; env -0; printf '%s' '${marker}'`], {
|
||||
env: { ...env, [RESOLVING]: "1" },
|
||||
stdin: "ignore",
|
||||
// Interactive shells without a terminal warn on stderr; nothing reads it, so never let it fill.
|
||||
stderr: "ignore",
|
||||
}),
|
||||
)
|
||||
.pipe(
|
||||
Effect.timeout("10 seconds"),
|
||||
Effect.tapError((error) => Effect.logWarning("shell environment unavailable", { error })),
|
||||
Effect.orElseSucceed(() => undefined),
|
||||
)
|
||||
if (output === undefined) return undefined
|
||||
const start = output.indexOf(marker)
|
||||
const end = output.lastIndexOf(marker)
|
||||
if (start === -1 || end === start) {
|
||||
yield* Effect.logWarning("shell environment unavailable", { shell: env.SHELL, reason: "missing markers" })
|
||||
return undefined
|
||||
}
|
||||
return Object.fromEntries(
|
||||
output
|
||||
.slice(start + marker.length, end)
|
||||
.split("\0")
|
||||
.flatMap((entry) => {
|
||||
const separator = entry.indexOf("=")
|
||||
if (separator <= 0) return []
|
||||
const key = entry.slice(0, separator)
|
||||
return TRANSIENT.has(key) ? [] : [[key, entry.slice(separator + 1)] as const]
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
/** Apply the resolved login-shell environment to this process before anything reads `process.env`. */
|
||||
export const adopt = Effect.fnUntraced(function* () {
|
||||
const variables = yield* resolve()
|
||||
if (variables === undefined) return
|
||||
Object.assign(process.env, variables)
|
||||
yield* Effect.logInfo("shell environment adopted", {
|
||||
shell: process.env.SHELL,
|
||||
variables: Object.keys(variables).length,
|
||||
})
|
||||
})
|
||||
@@ -1,68 +0,0 @@
|
||||
import { NodeServices } from "@effect/platform-node"
|
||||
import { afterAll, beforeAll, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { ShellEnvironment } from "../src/shell-environment"
|
||||
|
||||
// A stand-in login shell: it records every invocation, applies "rc file" exports, then runs the
|
||||
// probe script exactly as `$SHELL -ilc <script>` would.
|
||||
let root: string
|
||||
let shell: string
|
||||
let invocations: string
|
||||
|
||||
beforeAll(async () => {
|
||||
root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-shell-env-"))
|
||||
shell = path.join(root, "login-shell")
|
||||
invocations = path.join(root, "invocations")
|
||||
await fs.writeFile(
|
||||
shell,
|
||||
[
|
||||
"#!/bin/sh",
|
||||
`printf '%s\\n' "$1" >> '${invocations}'`,
|
||||
`[ "$1" = -ilc ] || exit 64`,
|
||||
'export PATH="/rc/bin:$PATH"',
|
||||
"export FROM_RC=yes",
|
||||
"export MULTILINE='first",
|
||||
"second'",
|
||||
'export SAW_GUARD="${OPENCODE_RESOLVING_SHELL_ENVIRONMENT:-unset}"',
|
||||
"export SHLVL=1 PWD=/rc OLDPWD=/rc",
|
||||
'eval "$2"',
|
||||
].join("\n"),
|
||||
{ mode: 0o755 },
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => fs.rm(root, { recursive: true, force: true }))
|
||||
|
||||
const skip = process.platform === "win32"
|
||||
const launchd = { SHELL: "", HOME: "", PATH: "/usr/bin:/bin:/usr/sbin:/sbin" }
|
||||
const resolve = (env: Record<string, string | undefined>) =>
|
||||
Effect.runPromise(
|
||||
ShellEnvironment.resolve({ ...launchd, ...env, SHELL: env.SHELL ?? shell, HOME: root }).pipe(
|
||||
Effect.provide(NodeServices.layer),
|
||||
),
|
||||
)
|
||||
|
||||
test.skipIf(skip)("adopts the login shell's exports over a launchd environment", async () => {
|
||||
const variables = await resolve({})
|
||||
expect(variables?.PATH).toBe("/rc/bin:/usr/bin:/bin:/usr/sbin:/sbin")
|
||||
expect(variables?.FROM_RC).toBe("yes")
|
||||
expect(variables?.MULTILINE).toBe("first\nsecond")
|
||||
expect(variables?.SAW_GUARD).toBe("1")
|
||||
for (const key of [ShellEnvironment.RESOLVING, "SHLVL", "PWD", "OLDPWD", "_"])
|
||||
expect(variables).not.toHaveProperty(key)
|
||||
})
|
||||
|
||||
test.skipIf(skip)("does not probe when the environment already came from a shell or a probe", async () => {
|
||||
const before = await fs.readFile(invocations, "utf8").catch(() => "")
|
||||
expect(await resolve({ SHLVL: "1" })).toBeUndefined()
|
||||
expect(await resolve({ [ShellEnvironment.RESOLVING]: "1" })).toBeUndefined()
|
||||
expect(await fs.readFile(invocations, "utf8").catch(() => "")).toBe(before)
|
||||
})
|
||||
|
||||
test.skipIf(skip)("falls back to the inherited environment when the shell cannot report", async () => {
|
||||
expect(await resolve({ SHELL: "/bin/false" })).toBeUndefined()
|
||||
expect(await resolve({ SHELL: path.join(root, "missing-shell") })).toBeUndefined()
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/console-core",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-mail",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-support",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"name": "@opencode/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -167,8 +167,7 @@ export const AzurePlugin = define({
|
||||
draft.settings.baseURL,
|
||||
resolveResourceName(draft.settings, resourceName) ?? resourceName,
|
||||
)
|
||||
if (responsesWebSocketCapable(item.provider, draft))
|
||||
draft.transport = item.provider.transport ?? "websocket"
|
||||
if (responsesWebSocketCapable(item.provider, draft)) draft.transport = "websocket"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ export const OpenAIPlugin = define({
|
||||
// ChatGPT-plan tokens only authorize codex-eligible models, and the
|
||||
// subscription covers usage, so hide the rest and zero the cost.
|
||||
models.update(model.providerID, model.id, (draft) => {
|
||||
draft.transport = models.provider.get(model.providerID)?.provider.transport ?? "websocket"
|
||||
draft.transport = "websocket"
|
||||
if (!chatgpt) return
|
||||
if (Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(draft.body?.reasoning)) {
|
||||
draft.enabled = false
|
||||
|
||||
@@ -98,7 +98,7 @@ export const XAIPlugin = define({
|
||||
yield* ctx.model.transform((models) => {
|
||||
for (const model of models.list(providerID)) {
|
||||
models.update(providerID, model.id, (draft) => {
|
||||
draft.transport = models.provider.get(providerID)?.provider.transport ?? "websocket"
|
||||
draft.transport = "websocket"
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -100,6 +100,7 @@ export const layer = (options?: Options) =>
|
||||
const recoverShell = Effect.fnUntraced(function* (
|
||||
background: Job.Background,
|
||||
recovery: Extract<Job.Recovery, { kind: "shell" }>,
|
||||
suspended: ReadonlySet<SessionSchema.ID>,
|
||||
) {
|
||||
const state = background.status === "running" ? "cancelled" : background.status
|
||||
const text =
|
||||
@@ -123,9 +124,7 @@ export const layer = (options?: Options) =>
|
||||
state,
|
||||
text,
|
||||
}),
|
||||
// Restart notices must not revive idle owners of long-lived shells.
|
||||
// Interrupted executions resume separately after their notices are admitted.
|
||||
resume: false,
|
||||
...(suspended.has(recovery.sessionID) ? { resume: false } : {}),
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchTag("Session.NotFoundError", () => Effect.void),
|
||||
@@ -209,7 +208,7 @@ export const layer = (options?: Options) =>
|
||||
if ((yield* jobs.get(background.id))?.status === "running") return
|
||||
const recovery = background.recovery
|
||||
yield* recovery.kind === "shell"
|
||||
? recoverShell(background, recovery)
|
||||
? recoverShell(background, recovery, suspended)
|
||||
: recoverSubagent(background, recovery, suspended)
|
||||
}),
|
||||
{ discard: true },
|
||||
|
||||
@@ -10,9 +10,6 @@ import { Model } from "@opencode/core/model"
|
||||
import { ModelResolver } from "@opencode/core/model-resolver"
|
||||
import { Plugin } from "@opencode/core/plugin"
|
||||
import { PluginHost } from "@opencode/core/plugin/host"
|
||||
import { AzurePlugin } from "@opencode/core/plugin/provider/azure"
|
||||
import { OpenAIPlugin } from "@opencode/core/plugin/provider/openai"
|
||||
import { XAIPlugin } from "@opencode/core/plugin/provider/xai"
|
||||
import { Provider } from "@opencode/core/provider"
|
||||
import { withEnv } from "../fixture/env"
|
||||
import { testEffect } from "../lib/effect"
|
||||
@@ -109,50 +106,6 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
for (const builtin of [
|
||||
{ id: "openai", model: "gpt-5.6-sol", package: "@opencode/ai/providers/openai/responses", plugin: OpenAIPlugin },
|
||||
{ id: "xai", model: "grok-4.6", package: "@opencode/ai/providers/xai", plugin: XAIPlugin },
|
||||
{ id: "azure", model: "gpt-5.6-sol", package: "@opencode/ai/providers/azure/responses", plugin: AzurePlugin },
|
||||
{ id: "custom-azure", model: "deployment", package: "@opencode/ai/providers/azure/responses", plugin: AzurePlugin },
|
||||
]) {
|
||||
it.live(`provider transport overrides ${builtin.id} defaults while model overrides still win`, () =>
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.Service
|
||||
const models = yield* Model.Service
|
||||
const plugin = yield* Plugin.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
const providerID = Provider.ID.make(builtin.id)
|
||||
const modelID = Model.ID.make(builtin.model)
|
||||
yield* providers.transform((editor) => {
|
||||
editor.update(providerID, (provider) => {
|
||||
provider.activation = "enabled"
|
||||
provider.package = builtin.package
|
||||
})
|
||||
editor.models.update(providerID, modelID, () => {})
|
||||
})
|
||||
yield* builtin.plugin.effect(host)
|
||||
expect((yield* models.get(providerID, modelID))?.transport).toBe("websocket")
|
||||
|
||||
yield* addPlugin([
|
||||
new Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
[builtin.id]: {
|
||||
transport: "http",
|
||||
models: { override: { modelID: builtin.model, transport: "websocket" } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
])
|
||||
|
||||
expect((yield* models.get(providerID, modelID))?.transport).toBe("http")
|
||||
expect((yield* models.get(providerID, Model.ID.make("override")))?.transport).toBe("websocket")
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
it.effect("adds key auth for custom providers without env credentials", () =>
|
||||
Effect.gen(function* () {
|
||||
const integrations = yield* Integration.Service
|
||||
|
||||
@@ -379,7 +379,7 @@ describe("SessionExecution lifecycle", () => {
|
||||
})
|
||||
|
||||
describe("SessionRestart background recovery", () => {
|
||||
it.effect("keeps shell owners idle until a user prompt delivers recovered notices exactly once", () =>
|
||||
it.effect("wakes idle shell owners and delivers recovered notices exactly once", () =>
|
||||
Effect.gen(function* () {
|
||||
const database = yield* Database.Service
|
||||
const store = yield* SessionStore.Service
|
||||
@@ -416,20 +416,6 @@ describe("SessionRestart background recovery", () => {
|
||||
yield* restart.resumeSuspendedSessions
|
||||
yield* Effect.forEach([parent, child], execution.awaitIdle, { discard: true })
|
||||
|
||||
expect(drained).toEqual([])
|
||||
expect(yield* SessionInbox.list(database.db, parent)).toHaveLength(1)
|
||||
expect(yield* SessionInbox.list(database.db, child)).toHaveLength(1)
|
||||
expect(yield* restarted.pendingBackground).toEqual([])
|
||||
yield* restart.resumeSuspendedSessions
|
||||
expect(drained).toEqual([])
|
||||
expect(yield* SessionInbox.list(database.db, parent)).toHaveLength(1)
|
||||
expect(yield* SessionInbox.list(database.db, child)).toHaveLength(1)
|
||||
|
||||
yield* seedInbox(database, parent, ["steer"])
|
||||
yield* seedInbox(database, child, ["steer"])
|
||||
yield* execution.wake(parent)
|
||||
yield* execution.wake(child)
|
||||
yield* Effect.forEach([parent, child], execution.awaitIdle, { discard: true })
|
||||
expect(drained.toSorted()).toEqual([parent, child].toSorted())
|
||||
expect((yield* store.context(parent)).filter((message) => message.type === "synthetic")).toMatchObject([
|
||||
{
|
||||
@@ -523,7 +509,7 @@ describe("SessionRestart background recovery", () => {
|
||||
yield* Context.get(context, SessionRestart.Service).resumeSuspendedSessions
|
||||
yield* Context.get(context, SessionExecution.Service).awaitIdle(sessionID)
|
||||
|
||||
expect(drained).toEqual([])
|
||||
expect(drained).toEqual([sessionID])
|
||||
const inbox = yield* SessionInbox.list(database.db, sessionID)
|
||||
expect(inbox).toMatchObject([
|
||||
{
|
||||
@@ -575,6 +561,7 @@ describe("SessionRestart background recovery", () => {
|
||||
expect(yield* restarted.pendingBackground).toEqual([])
|
||||
expect(yield* SessionInbox.list(database.db, sessionID)).toHaveLength(delivered ? 0 : 1)
|
||||
yield* SessionInbox.promote(database.db, bus, sessionID, "steer")
|
||||
// Recovery ends a busy period, so an idle marker follows the notification.
|
||||
const messages = (yield* sessions.messages({ sessionID })).filter((message) => message.type !== "idle")
|
||||
expect(messages).toMatchObject([
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode/desktop",
|
||||
"private": true,
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/function",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"name": "@opencode/http-recorder",
|
||||
"description": "Record and replay Effect HTTP and WebSocket traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/httpapi-codegen",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/latex",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/merman",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/plugin-browser",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"description": "OpenCode's desktop browser plugin",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/plugin",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/protocol",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/schema",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"name": "@opencode/sdk",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/server",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/session-ui",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/simulation",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/stats-app",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/stats-core",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { buildStatsQuery, toGeoAggregate, toModelAggregate, toProviderAggregate } from "./inference"
|
||||
import { toGeoAggregate, toModelAggregate, toProviderAggregate } from "./inference"
|
||||
import { modelAuthor, normalizeInferenceModel, statModel, statProvider } from "./model-normalization"
|
||||
|
||||
describe("inference stat normalization", () => {
|
||||
@@ -38,22 +38,6 @@ describe("inference stat normalization", () => {
|
||||
expect(statProvider("unknown", "", "custom-provider")).toBe("custom-provider")
|
||||
})
|
||||
|
||||
test("merges union alpha routes without exposing the provider", () => {
|
||||
;["opencode-go/union-alpha", "opencode/union-alpha"].forEach((model) => {
|
||||
expect(statModel(model, "")).toBe("union-alpha")
|
||||
expect(statProvider(model, "gpt-test-model", "test-provider")).toBe("unknown")
|
||||
expect(
|
||||
toModelAggregate({ ...aggregate(model, "test-provider"), provider_model: "gpt-test-model" }),
|
||||
).toMatchObject([{ model: "union-alpha", provider: "unknown", requests: 1 }])
|
||||
expect(toProviderAggregate(aggregate(model, "test-provider"))).toMatchObject([
|
||||
{ provider: "unknown", requests: 1 },
|
||||
])
|
||||
expect(toGeoAggregate({ ...aggregate(model, "test-provider"), country: "US" })).toMatchObject([
|
||||
{ model: "union-alpha", provider: "unknown", country: "US", requests: 1 },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
test("merges renamed models under their current name", () => {
|
||||
expect(statModel("deepseek-flash", "")).toBe("deepseek-v4.1-flash")
|
||||
expect(statModel("x-preview-f", "")).toBe("ox-alpha")
|
||||
@@ -110,20 +94,6 @@ describe("inference stat normalization", () => {
|
||||
}),
|
||||
).toMatchObject([{ period_key: "2026-W20" }])
|
||||
})
|
||||
|
||||
test("normalizes union alpha routes in stat queries", () => {
|
||||
process.env.SST_RESOURCE_InferenceEvent = JSON.stringify({
|
||||
catalog: "inference",
|
||||
database: "events",
|
||||
table: "generation",
|
||||
})
|
||||
process.env.SST_RESOURCE_StatsSyncConfig = JSON.stringify({ dataset: "zen" })
|
||||
const query = buildStatsQuery(new Date("2026-05-20T00:00:00.000Z"), new Date("2026-05-21T00:00:00.000Z"))
|
||||
|
||||
expect(query).toContain("WHEN lower(model) = 'opencode-go/union-alpha' THEN 'union-alpha'")
|
||||
expect(query).toContain("WHEN lower(model) = 'opencode/union-alpha' THEN 'union-alpha'")
|
||||
expect(query).toContain("WHEN lower(model) IN ('omen-alpha', 'union-alpha') THEN 'unknown'")
|
||||
})
|
||||
})
|
||||
|
||||
function aggregate(model: string, provider: string) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
MODEL_AUTHOR_RULES,
|
||||
MODEL_NAME_ALIASES,
|
||||
RETIRED_STAT_PROVIDERS,
|
||||
STEALTH_MODELS,
|
||||
statModel,
|
||||
statProvider,
|
||||
} from "./model-normalization"
|
||||
@@ -264,7 +263,6 @@ ${Object.entries(MODEL_NAME_ALIASES)
|
||||
|
||||
function statProviderSql(model: string, providerModel: string, provider: string) {
|
||||
return `CASE
|
||||
WHEN lower(${model}) IN (${[...STEALTH_MODELS].map(sqlString).join(", ")}) THEN 'unknown'
|
||||
${MODEL_AUTHOR_RULES.map((item) => ` WHEN strpos(lower(${providerModel}), ${sqlString(item.match)}) > 0 THEN ${sqlString(item.author)}`).join("\n")}
|
||||
${MODEL_AUTHOR_RULES.map((item) => ` WHEN strpos(lower(${model}), ${sqlString(item.match)}) > 0 THEN ${sqlString(item.author)}`).join("\n")}
|
||||
WHEN ${provider} <> '' AND lower(${provider}) NOT IN (${RETIRED_STAT_PROVIDERS.map(sqlString).join(", ")}) THEN ${provider}
|
||||
|
||||
@@ -13,11 +13,8 @@ export const MODEL_AUTHOR_RULES = [
|
||||
{ match: "qwen", author: "qwen" },
|
||||
] as const
|
||||
export const EXCLUDED_MODELS = new Set(["alpha-gpt-next"])
|
||||
export const STEALTH_MODELS = new Set(["omen-alpha", "union-alpha"])
|
||||
export const MODEL_NAME_ALIASES: Record<string, string> = {
|
||||
"deepseek-flash": "deepseek-v4.1-flash",
|
||||
"opencode-go/union-alpha": "union-alpha",
|
||||
"opencode/union-alpha": "union-alpha",
|
||||
"x-preview-f": "ox-alpha",
|
||||
"xiaomi/mimo-v2.5": "mimo-v2.5",
|
||||
}
|
||||
@@ -48,10 +45,7 @@ export function statProvider(
|
||||
providerModel: string | undefined,
|
||||
provider: string | undefined,
|
||||
) {
|
||||
const normalized = statModel(model, providerModel)
|
||||
if (STEALTH_MODELS.has(normalized.toLowerCase())) return "unknown"
|
||||
|
||||
const modelAuthorValue = modelAuthor(normalized)
|
||||
const modelAuthorValue = modelAuthor(statModel(model, providerModel))
|
||||
if (!modelAuthorValue) return undefined
|
||||
|
||||
const providerModelAuthor = modelAuthor(providerModel)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/stats-server",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/theme",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/tui",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -128,7 +128,7 @@ export function createTuiAttention(input: {
|
||||
return {
|
||||
async notify(request) {
|
||||
try {
|
||||
if (!input.config.attention.notifications && !input.config.attention.sound) return skipped("attention_disabled")
|
||||
if (!input.config.attention.enabled) return skipped("attention_disabled")
|
||||
if (disposed || input.renderer.isDestroyed) return skipped("renderer_destroyed")
|
||||
|
||||
const message = normalizeText(request.message, "", MESSAGE_LIMIT)
|
||||
|
||||
@@ -253,11 +253,20 @@ export const settings: Setting[] = [
|
||||
format: (value) => `${value} ms`,
|
||||
keywords: ["leader key", "shortcut timeout"],
|
||||
},
|
||||
{
|
||||
title: "Attention",
|
||||
category: "Alerts",
|
||||
path: ["attention", "enabled"],
|
||||
default: false,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
keywords: ["alerts"],
|
||||
},
|
||||
{
|
||||
title: "Notifications",
|
||||
category: "Alerts",
|
||||
path: ["attention", "notifications"],
|
||||
default: false,
|
||||
default: true,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
keywords: ["system notifications", "desktop notifications", "alerts"],
|
||||
@@ -266,10 +275,10 @@ export const settings: Setting[] = [
|
||||
title: "Sounds",
|
||||
category: "Alerts",
|
||||
path: ["attention", "sound"],
|
||||
default: false,
|
||||
default: true,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
keywords: ["audio", "sound effects", "alerts"],
|
||||
keywords: ["audio", "sound effects"],
|
||||
},
|
||||
{
|
||||
title: "Volume",
|
||||
|
||||
@@ -95,6 +95,7 @@ export const Info = Schema.Struct({
|
||||
).annotate({ description: "Scrolling behavior" }),
|
||||
attention: Schema.optional(
|
||||
Schema.Struct({
|
||||
enabled: Schema.optional(Schema.Boolean).annotate({ description: "Enable attention alerts" }),
|
||||
notifications: Schema.optional(Schema.Boolean).annotate({ description: "Show system notifications" }),
|
||||
sound: Schema.optional(Schema.Boolean).annotate({ description: "Play attention sounds" }),
|
||||
volume: Schema.optional(
|
||||
@@ -241,6 +242,7 @@ export type Info = Schema.Schema.Type<typeof Info>
|
||||
|
||||
export type Resolved = Omit<Info, "attention" | "cursor" | "keybinds" | "leader" | "mouse" | "session" | "tabs"> & {
|
||||
attention: {
|
||||
enabled: boolean
|
||||
notifications: boolean
|
||||
sound: boolean
|
||||
volume: number
|
||||
@@ -283,8 +285,9 @@ export function resolve(input: Info, options: { terminalSuspend: boolean }): Res
|
||||
return {
|
||||
...input,
|
||||
attention: {
|
||||
notifications: input.attention?.notifications ?? false,
|
||||
sound: input.attention?.sound ?? false,
|
||||
enabled: input.attention?.enabled ?? false,
|
||||
notifications: input.attention?.notifications ?? true,
|
||||
sound: input.attention?.sound ?? true,
|
||||
volume: input.attention?.volume ?? 0.4,
|
||||
sound_pack: input.attention?.sound_pack ?? "opencode.default",
|
||||
sounds: input.attention?.sounds ?? {},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/ui",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/util",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/update",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -108,11 +108,12 @@ Set scroll speed and acceleration:
|
||||
|
||||
## Attention
|
||||
|
||||
Configure notifications and sounds. Both are off by default and can be enabled independently:
|
||||
Configure notifications and sounds:
|
||||
|
||||
```json title="cli.json"
|
||||
{
|
||||
"attention": {
|
||||
"enabled": true,
|
||||
"notifications": true,
|
||||
"sound": true,
|
||||
"volume": 0.4,
|
||||
@@ -126,7 +127,8 @@ Configure notifications and sounds. Both are off by default and can be enabled i
|
||||
|
||||
| Field | Values | Description |
|
||||
| --------------- | ---------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `notifications` | boolean | Shows system notifications when the terminal is not focused. |
|
||||
| `enabled` | boolean | Enables attention alerts. |
|
||||
| `notifications` | boolean | Shows system notifications. |
|
||||
| `sound` | boolean | Plays attention sounds. |
|
||||
| `volume` | number from `0` to `1` | Sets sound volume. |
|
||||
| `sound_pack` | string | Selects the active sound pack. |
|
||||
|
||||
Reference in New Issue
Block a user