mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 19:49:48 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bc33c255c | |||
| 11c24c324f |
@@ -450,7 +450,7 @@
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@valibot/to-json-schema": "1.6.0",
|
||||
"electron": "42.8.1",
|
||||
"electron": "42.3.3",
|
||||
"electron-builder": "26.15.2",
|
||||
"electron-vite": "^5",
|
||||
"solid-js": "catalog:",
|
||||
@@ -1599,8 +1599,6 @@
|
||||
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.101", "", { "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-s6AC7LXCEjCN+nKegKFY4MOi6bmT1+SLR9YHEYwhY3P5qyQQB4R5yYLgt+3J4EPp5fa3t4FIDdPEUwz9LdKm6g=="],
|
||||
|
||||
"@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.5", "", {}, "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA=="],
|
||||
|
||||
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||
|
||||
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
|
||||
@@ -3225,6 +3223,8 @@
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
||||
|
||||
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251207.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251207.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-4QcRnzB0pi9rS0AOvg8kWbmuwHv5X7B2EXHbgcms9+56hsZ8SZrZjNgBJb2rUIodJ4kU5mrkj/xlTTT4r9VcpQ=="],
|
||||
|
||||
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251207.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-waWJnuuvkXh4WdpbTjYf7pyahJzx0ycesV2BylyHrE9OxU9FSKcD/cRLQYvbq3YcBSdF7sZwRLDBer7qTeLsYA=="],
|
||||
@@ -3915,7 +3915,7 @@
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
"electron": ["electron@42.8.1", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-zQnW4hKUyxzdsw3gvtBszEfZ0d5SGqkAsfdaVFVdHDmzRsgw3rR5so/gB7LS9TLlewoIC0wg0sTIjtn3ilICuA=="],
|
||||
"electron": ["electron@42.3.3", "", { "dependencies": { "@electron/get": "^5.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-0MwYp9wTb7TrtTalOYqeW+suqd9T/Znstr/nDLKqFGIjHdBZX339guo3mQqTPURRZ/UQmYM4uMpzKpI5wLptfQ=="],
|
||||
|
||||
"electron-builder": ["electron-builder@26.15.2", "", { "dependencies": { "app-builder-lib": "26.15.2", "builder-util": "26.15.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.15.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-veKM9+dCljaC5A74Pwc0ZWQ9arOHREXWh9hUIf8NGg49ch7x+IB4QhbMzIrV5ONZIXM2OEkaxW11cAPjPtoi4A=="],
|
||||
|
||||
@@ -4063,6 +4063,8 @@
|
||||
|
||||
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
|
||||
|
||||
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
|
||||
|
||||
"fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
@@ -4087,6 +4089,8 @@
|
||||
|
||||
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
||||
|
||||
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
@@ -4177,7 +4181,7 @@
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
|
||||
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
|
||||
|
||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||
|
||||
@@ -5067,6 +5071,8 @@
|
||||
|
||||
"peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="],
|
||||
|
||||
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
|
||||
|
||||
"piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
@@ -5949,6 +5955,8 @@
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
|
||||
|
||||
"yocto-spinner": ["yocto-spinner@0.2.3", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ=="],
|
||||
@@ -6629,8 +6637,6 @@
|
||||
|
||||
"builder-util/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
|
||||
|
||||
"cacheable-request/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
|
||||
|
||||
"cbor-extract/node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.1.1", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-test": "build-test.js", "node-gyp-build-optional-packages-optional": "optional.js" } }, "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw=="],
|
||||
|
||||
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
@@ -6713,6 +6719,8 @@
|
||||
|
||||
"estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
|
||||
|
||||
"execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
|
||||
|
||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
@@ -6943,6 +6951,8 @@
|
||||
|
||||
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
||||
|
||||
"zod-to-ts/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@actions/github/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
@@ -185,15 +185,15 @@ const secretValues = (request: HttpClientRequest.HttpClientRequest) => {
|
||||
// Two passes: structural (redact `"name": "value"` and `name=value` patterns
|
||||
// for any field name that looks sensitive) plus literal (replace any actual
|
||||
// secret values we sent in the request, in case the response echoes one back).
|
||||
const redactBody = (body: string, secrets: ReadonlySet<string>) =>
|
||||
Array.from(secrets).reduce(
|
||||
const redactBody = (body: string, request: HttpClientRequest.HttpClientRequest) =>
|
||||
Array.from(secretValues(request)).reduce(
|
||||
(text, secret) => text.split(secret).join(REDACTED),
|
||||
body.replace(REDACT_JSON_FIELD, `$1"${REDACTED}"`).replace(REDACT_QUERY_FIELD, `$1${REDACTED}`),
|
||||
)
|
||||
|
||||
const responseBody = (body: string | void, secrets: ReadonlySet<string>) => {
|
||||
const responseBody = (body: string | void, request: HttpClientRequest.HttpClientRequest) => {
|
||||
if (body === undefined) return {}
|
||||
const redacted = redactBody(body, secrets)
|
||||
const redacted = redactBody(body, request)
|
||||
if (redacted.length <= BODY_LIMIT) return { body: redacted }
|
||||
return { body: redacted.slice(0, BODY_LIMIT), bodyTruncated: true }
|
||||
}
|
||||
@@ -240,7 +240,7 @@ const statusError =
|
||||
const headers = normalizedHeaders(response.headers)
|
||||
const retryAfter = retryAfterMs(headers)
|
||||
const rateLimit = rateLimitDetails(headers, retryAfter)
|
||||
const details = responseBody(body, secretValues(request))
|
||||
const details = responseBody(body, request)
|
||||
return yield* new AIError({
|
||||
module: "RequestExecutor",
|
||||
method: "execute",
|
||||
@@ -261,42 +261,6 @@ const statusError =
|
||||
})
|
||||
})
|
||||
|
||||
// Classifies an HTTP failure captured outside the executor (for example by the
|
||||
// AI SDK's own fetch) onto the same reason types and redacted HttpContext that
|
||||
// executor-driven requests produce. The originating request is not available on
|
||||
// that path, so the method is assumed (language model calls are always POST),
|
||||
// request headers are empty, and only structural body redaction applies.
|
||||
export const classifyHttpFailure = (input: {
|
||||
readonly message: string
|
||||
readonly url: string
|
||||
readonly status?: number | undefined
|
||||
readonly code?: string | undefined
|
||||
readonly responseHeaders?: Record<string, string> | undefined
|
||||
readonly responseBody?: string | undefined
|
||||
}) => {
|
||||
const headers = normalizedHeaders(Headers.fromInput(input.responseHeaders))
|
||||
const retryAfter = retryAfterMs(headers)
|
||||
const rateLimit = rateLimitDetails(headers, retryAfter)
|
||||
const details = responseBody(input.responseBody ?? undefined, new Set<string>())
|
||||
return classifyProviderFailure({
|
||||
message: input.message,
|
||||
status: input.status,
|
||||
code: input.code,
|
||||
retryAfterMs: retryAfter,
|
||||
rateLimit,
|
||||
http: new HttpContext({
|
||||
request: new HttpRequestDetails({ method: "POST", url: redactUrl(input.url), headers: {} }),
|
||||
response:
|
||||
input.status === undefined
|
||||
? undefined
|
||||
: new HttpResponseDetails({ status: input.status, headers: redactHeaders(Headers.fromInput(headers), []) }),
|
||||
...details,
|
||||
requestId: requestId(headers),
|
||||
rateLimit,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const toHttpError = (redactedNames: ReadonlyArray<string | RegExp>) => (error: unknown) => {
|
||||
const transportError = (input: {
|
||||
readonly message: string
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export * as AISDK from "./aisdk"
|
||||
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { APICallError } from "@ai-sdk/provider"
|
||||
import type {
|
||||
JSONSchema7,
|
||||
JSONValue,
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
LanguageModel,
|
||||
ProviderID,
|
||||
ProviderMetadata,
|
||||
TransportReason,
|
||||
ToolResultValue,
|
||||
UnknownProviderReason,
|
||||
type ContentPart,
|
||||
@@ -31,7 +29,7 @@ import {
|
||||
type ToolDefinition,
|
||||
type UsageInput,
|
||||
} from "@opencode-ai/ai"
|
||||
import { Auth, Endpoint, RequestExecutor, type AnyRoute } from "@opencode-ai/ai/route"
|
||||
import { Auth, Endpoint, type AnyRoute } from "@opencode-ai/ai/route"
|
||||
import { ProviderShared } from "@opencode-ai/ai/protocols/shared"
|
||||
import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect"
|
||||
import type { ID, Info } from "./model"
|
||||
@@ -725,9 +723,7 @@ function llmError(method: string, error: unknown) {
|
||||
const reason =
|
||||
error instanceof AIError
|
||||
? new InvalidProviderOutputReason({ message: error.message })
|
||||
: APICallError.isInstance(error)
|
||||
? apiCallErrorReason(error)
|
||||
: new UnknownProviderReason({ message: unknownErrorMessage(error) })
|
||||
: new UnknownProviderReason({ message: error instanceof Error ? error.message : String(error) })
|
||||
return new AIError({
|
||||
module: "AISDK",
|
||||
method,
|
||||
@@ -735,57 +731,4 @@ function llmError(method: string, error: unknown) {
|
||||
})
|
||||
}
|
||||
|
||||
function apiCallErrorReason(error: APICallError) {
|
||||
const details = providerErrorDetails(error)
|
||||
const reason = RequestExecutor.classifyHttpFailure({
|
||||
message: details.message,
|
||||
url: error.url,
|
||||
status: error.statusCode,
|
||||
code: details.code,
|
||||
responseHeaders: error.responseHeaders,
|
||||
responseBody: error.responseBody,
|
||||
})
|
||||
if (error.statusCode !== undefined || !error.isRetryable) return reason
|
||||
return new TransportReason({
|
||||
message: reason.message,
|
||||
kind: error.name,
|
||||
url: error.url,
|
||||
http: "http" in reason ? reason.http : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const ProviderErrorCode = Schema.Union([Schema.String, Schema.Finite])
|
||||
const ProviderErrorDetail = Schema.Struct({
|
||||
message: Schema.optionalKey(Schema.String),
|
||||
code: Schema.optionalKey(ProviderErrorCode),
|
||||
})
|
||||
const ProviderErrorBody = Schema.Struct({
|
||||
...ProviderErrorDetail.fields,
|
||||
error: Schema.optionalKey(ProviderErrorDetail),
|
||||
})
|
||||
const decodeProviderError = Schema.decodeUnknownOption(
|
||||
Schema.Union([ProviderErrorBody, Schema.fromJsonString(ProviderErrorBody)]),
|
||||
)
|
||||
|
||||
function unknownErrorMessage(error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return message.trim() === "" ? "Provider request failed" : message
|
||||
}
|
||||
|
||||
function providerErrorDetails(error: APICallError) {
|
||||
const data = Option.getOrUndefined(decodeProviderError(error.data))
|
||||
const body = Option.getOrUndefined(decodeProviderError(error.responseBody))
|
||||
const details = [data?.error, data, body?.error, body]
|
||||
const message = details.map((detail) => detail?.message).find((value) => value?.trim())
|
||||
const value = details.map((detail) => detail?.code).find((value) => value !== undefined)
|
||||
const code = value === undefined ? undefined : String(value)
|
||||
const prefix =
|
||||
error.statusCode === undefined ? "Provider request failed" : `Provider request failed with HTTP ${error.statusCode}`
|
||||
return {
|
||||
code,
|
||||
message:
|
||||
error.message.trim() !== "" ? error.message : (message ?? (code === undefined ? prefix : `${prefix}: ${code}`)),
|
||||
}
|
||||
}
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [] })
|
||||
|
||||
@@ -151,15 +151,7 @@ export const layer = (options?: Options) =>
|
||||
)
|
||||
if (!credential || credential.value.type !== "key") return []
|
||||
const variables = { [auth.env]: credential.value.key }
|
||||
const configs = yield* wellknown
|
||||
.resolve(entry, variables)
|
||||
.pipe(
|
||||
Effect.catch(() =>
|
||||
Effect.logWarning("failed to load wellknown config", { source: entry.origin }).pipe(
|
||||
Effect.as([] as const),
|
||||
),
|
||||
),
|
||||
)
|
||||
const configs = yield* wellknown.resolve(entry, variables).pipe(Effect.orDie)
|
||||
return yield* Effect.forEach(configs, (config) =>
|
||||
ConfigVariable.substitute({
|
||||
type: "virtual",
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { APICallError } from "@ai-sdk/provider"
|
||||
import type { LanguageModelV3, LanguageModelV3StreamPart } from "@ai-sdk/provider"
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { SessionRunnerRetry } from "@opencode-ai/core/session/runner/retry"
|
||||
import { toSessionError } from "@opencode-ai/core/session/to-session-error"
|
||||
import { Model } from "@opencode-ai/core/model"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
import { LLM, AIError, LLMEvent, Message, isContextOverflowFailure } from "@opencode-ai/ai"
|
||||
import { LLM, AIError, LLMEvent, Message } from "@opencode-ai/ai"
|
||||
import { LLMClient, RequestExecutor } from "@opencode-ai/ai/route"
|
||||
import { compileRequest } from "@opencode-ai/ai/route/client"
|
||||
import { expect } from "bun:test"
|
||||
@@ -340,170 +337,3 @@ it.effect("keeps malformed provider-executed AI SDK input terminal", () =>
|
||||
expect(error.message).toContain("Invalid JSON input for aisdk tool call web_search")
|
||||
}),
|
||||
)
|
||||
|
||||
const failingModel = (failure: unknown): LanguageModelV3 => ({
|
||||
specificationVersion: "v3",
|
||||
provider: "test",
|
||||
modelId: "test",
|
||||
supportedUrls: {},
|
||||
doGenerate: () => Promise.reject(new Error("Unexpected non-streaming request")),
|
||||
doStream: () => Promise.reject(failure),
|
||||
})
|
||||
|
||||
const streamFailure = (failure: unknown) =>
|
||||
Effect.gen(function* () {
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* aisdk.hook.sdk((event) => {
|
||||
event.sdk = { languageModel: () => failingModel(failure) }
|
||||
})
|
||||
const resolved = yield* aisdk.model(model("test-ai-sdk"))
|
||||
return yield* LLMClient.generate(LLM.request({ model: resolved, prompt: "Hello" })).pipe(
|
||||
Effect.provide(client),
|
||||
Effect.flip,
|
||||
)
|
||||
})
|
||||
|
||||
it.effect("preserves non-empty AI SDK error messages", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* streamFailure(new Error("Bad Request"))
|
||||
expect(error).toBeInstanceOf(AIError)
|
||||
expect(error.reason).toMatchObject({ _tag: "UnknownProvider", message: "Bad Request" })
|
||||
}),
|
||||
)
|
||||
|
||||
const apiCallError = (input: Partial<ConstructorParameters<typeof APICallError>[0]>) =>
|
||||
new APICallError({
|
||||
message: "",
|
||||
url: "https://api.example.com/chat",
|
||||
requestBodyValues: { messages: [{ role: "user", content: "private prompt" }] },
|
||||
responseHeaders: { authorization: "Bearer secret-token" },
|
||||
...input,
|
||||
})
|
||||
|
||||
it.effect("derives status and code when the AI SDK error message is empty", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* streamFailure(
|
||||
apiCallError({
|
||||
statusCode: 404,
|
||||
responseBody: '{"error":{"message":"","code":"not_found"}}',
|
||||
data: { error: { message: "", code: "not_found" } },
|
||||
}),
|
||||
)
|
||||
expect(error.reason.message).toBe("Provider request failed with HTTP 404: not_found")
|
||||
expect(error.reason.message).not.toContain("secret-token")
|
||||
expect(error.reason.message).not.toContain("private prompt")
|
||||
const projected = toSessionError(error)
|
||||
expect(projected.type).toBe("provider.invalid-request")
|
||||
expect(projected.status).toBe(404)
|
||||
expect(projected.message).not.toBe("")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves redacted HTTP context on AI SDK call errors", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* streamFailure(
|
||||
apiCallError({
|
||||
statusCode: 404,
|
||||
responseBody: '{"error":{"message":"","code":"not_found"}}',
|
||||
}),
|
||||
)
|
||||
expect(error.reason).toMatchObject({ _tag: "InvalidRequest" })
|
||||
const http = "http" in error.reason ? error.reason.http : undefined
|
||||
expect(http?.request.url).toBe("https://api.example.com/chat")
|
||||
expect(http?.response?.status).toBe(404)
|
||||
expect(http?.response?.headers["authorization"]).toBe("<redacted>")
|
||||
expect(http?.body).toBe('{"error":{"message":"","code":"not_found"}}')
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("classifies retryable AI SDK failures with retry-after details", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* streamFailure(
|
||||
apiCallError({
|
||||
statusCode: 429,
|
||||
responseHeaders: { "retry-after": "7" },
|
||||
}),
|
||||
)
|
||||
expect(error.reason).toMatchObject({ _tag: "RateLimit", retryAfterMs: 7000 })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("classifies data-only AI SDK provider codes", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* streamFailure(
|
||||
apiCallError({
|
||||
statusCode: 400,
|
||||
data: { error: { code: "api_error" } },
|
||||
}),
|
||||
)
|
||||
expect(error.reason).toMatchObject({ _tag: "ProviderInternal", status: 400 })
|
||||
expect(SessionRunnerRetry.isRetryable(error)).toBeTrue()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("classifies data-only AI SDK authentication errors", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* streamFailure(
|
||||
apiCallError({
|
||||
statusCode: 400,
|
||||
data: { error: { code: "authentication_error" } },
|
||||
}),
|
||||
)
|
||||
expect(error.reason).toMatchObject({ _tag: "Authentication", kind: "invalid" })
|
||||
expect(SessionRunnerRetry.isRetryable(error)).toBeFalse()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("detects context overflow from data-only AI SDK errors", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* streamFailure(
|
||||
apiCallError({
|
||||
statusCode: 400,
|
||||
data: { error: { code: "context_length_exceeded" } },
|
||||
}),
|
||||
)
|
||||
expect(error.reason).toMatchObject({ _tag: "InvalidRequest", classification: "context-overflow" })
|
||||
expect(isContextOverflowFailure(error)).toBeTrue()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("retries status-less AI SDK transport failures", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* streamFailure(
|
||||
apiCallError({
|
||||
message: "Cannot connect to API: connection refused",
|
||||
isRetryable: true,
|
||||
}),
|
||||
)
|
||||
expect(error.reason).toMatchObject({ _tag: "Transport", kind: "AI_APICallError" })
|
||||
expect(SessionRunnerRetry.isRetryable(error)).toBeTrue()
|
||||
expect("http" in error.reason ? error.reason.http?.request.url : undefined).toBe("https://api.example.com/chat")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("prefers a structured provider message over the code fallback", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* streamFailure(
|
||||
apiCallError({
|
||||
statusCode: 404,
|
||||
data: { error: { code: "not_found" } },
|
||||
responseBody: '{"message":"The requested model does not exist"}',
|
||||
}),
|
||||
)
|
||||
expect(error.reason.message).toBe("The requested model does not exist")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("falls back to the status alone for malformed response bodies", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* streamFailure(
|
||||
apiCallError({
|
||||
statusCode: 502,
|
||||
isRetryable: false,
|
||||
responseBody: "<html>Bad Gateway</html>",
|
||||
}),
|
||||
)
|
||||
expect(error.reason).toMatchObject({ _tag: "ProviderInternal", status: 502 })
|
||||
expect(error.reason.message).toBe("Provider request failed with HTTP 502")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -307,7 +307,7 @@ describe("Config", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("tolerates unavailable authenticated wellknown config and reloads it later", () =>
|
||||
it.live("loads authenticated wellknown config before user configuration", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) =>
|
||||
@@ -322,7 +322,6 @@ describe("Config", () => {
|
||||
})
|
||||
|
||||
const integrationID = Integration.ID.make("https://example.com")
|
||||
let available = false
|
||||
let key = "secret"
|
||||
const credentialNode = makeGlobalNode({
|
||||
service: Credential.Service,
|
||||
@@ -362,10 +361,7 @@ describe("Config", () => {
|
||||
refresh: () => Effect.succeed(false),
|
||||
add: () => Effect.die("unused Wellknown.add"),
|
||||
remove: () => Effect.die("unused Wellknown.remove"),
|
||||
resolve: (_entry, variables) =>
|
||||
available
|
||||
? Effect.succeed([{ shell: variables.TOKEN }])
|
||||
: Effect.fail(new Error("expired credential")),
|
||||
resolve: (_entry, variables) => Effect.succeed([{ shell: variables.TOKEN }]),
|
||||
}),
|
||||
),
|
||||
deps: [],
|
||||
@@ -378,12 +374,11 @@ describe("Config", () => {
|
||||
expect(Config.latest(initial, "shell")).toBe("project")
|
||||
expect(
|
||||
initial.flatMap((entry) => (entry.type === "document" && entry.info.shell ? [entry.info.shell] : [])),
|
||||
).toEqual(["global", "project"])
|
||||
).toEqual(["secret", "global", "project"])
|
||||
const updated = yield* bus
|
||||
.subscribe(Event.Updated)
|
||||
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
||||
yield* Effect.yieldNow
|
||||
available = true
|
||||
key = "next"
|
||||
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID })
|
||||
expect(yield* Fiber.join(updated)).toHaveLength(1)
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@valibot/to-json-schema": "1.6.0",
|
||||
"electron": "42.8.1",
|
||||
"electron": "42.3.3",
|
||||
"electron-builder": "26.15.2",
|
||||
"electron-vite": "^5",
|
||||
"solid-js": "catalog:",
|
||||
|
||||
@@ -12,7 +12,7 @@ export function generateSyntax(theme: ResolvedThemeTokens, mode: Mode) {
|
||||
rule(["prompt"], theme.hue.accent[step]),
|
||||
rule(["extmark.file"], feedback.warning.default, { bold: true }),
|
||||
rule(["extmark.agent"], theme.categorical[0][step], { bold: true }),
|
||||
rule(["extmark.skill"], (theme.categorical[1] ?? theme.categorical[0])[step], { bold: true }),
|
||||
rule(["extmark.skill"], theme.categorical[1][step], { bold: true }),
|
||||
// V1 migration preserves its selected/inverse foreground in this action state.
|
||||
rule(["extmark.paste"], theme.text.action.primary.focused, {
|
||||
background: feedback.warning.default,
|
||||
|
||||
@@ -70,7 +70,7 @@ import { DialogAgent } from "./component/dialog-agent"
|
||||
import { DialogSessionList } from "./component/dialog-session-list"
|
||||
import { DialogOpen } from "./component/dialog-open"
|
||||
import { SessionTabs } from "./component/session-tabs"
|
||||
import { sessionTabsFitVertically } from "./ui/layout"
|
||||
import { effectiveSessionTabPosition } from "./ui/layout"
|
||||
import { ThemeErrorToast } from "./component/theme-error-toast"
|
||||
import { ThemeProvider, useTheme, useThemes } from "./context/theme"
|
||||
import { Home } from "./routes/home"
|
||||
@@ -513,7 +513,7 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
const terminalTitleEnabled = () => config.data.terminal?.title ?? true
|
||||
const copyOnSelectEnabled = () => config.data.terminal?.copy_on_select ?? process.platform !== "win32"
|
||||
const pasteSummaryEnabled = () => config.data.prompt?.paste !== "full"
|
||||
const tabsVertical = () => config.data.tabs.layout === "vertical" && sessionTabsFitVertically(dimensions().width)
|
||||
const tabPosition = () => effectiveSessionTabPosition(config.data.tabs.position, dimensions().width)
|
||||
const tabsVisible = () =>
|
||||
sessionTabs.enabled() && (sessionTabs.tabs().length > 0 || sessionTabs.newTab()) && route.data.type !== "plugin"
|
||||
|
||||
@@ -1198,13 +1198,13 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
onMouseUp={copyOnSelectEnabled() ? () => Selection.copy(renderer, toast, clipboard) : undefined}
|
||||
>
|
||||
<box flexGrow={1} minHeight={0} flexDirection="row">
|
||||
<Show when={tabsVisible() && tabsVertical()}>
|
||||
<Show when={tabsVisible() && tabPosition() === "left"}>
|
||||
<SessionTabs orientation="vertical" />
|
||||
</Show>
|
||||
<box flexGrow={1} minWidth={0} flexDirection="column">
|
||||
<Show when={plugins.ready()}>
|
||||
<box flexGrow={1} minHeight={0} flexDirection="column">
|
||||
<Show when={tabsVisible() && !tabsVertical()}>
|
||||
<Show when={tabsVisible() && tabPosition() === "top"}>
|
||||
<SessionTabs />
|
||||
</Show>
|
||||
<Switch>
|
||||
@@ -1224,10 +1224,16 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={tabsVisible() && tabPosition() === "bottom"}>
|
||||
<SessionTabs />
|
||||
</Show>
|
||||
</box>
|
||||
<PluginSlot name="app" input={{}} mode="all" />
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={tabsVisible() && tabPosition() === "right"}>
|
||||
<SessionTabs orientation="vertical" />
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={devtools()}>
|
||||
<DevToolsBar />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createMemo, createSignal } from "solid-js"
|
||||
import { useConfig } from "../config"
|
||||
import { TabPosition, useConfig } from "../config"
|
||||
import { useThemes } from "../context/theme"
|
||||
import { DialogSelect } from "../ui/dialog-select"
|
||||
import { useToast } from "../ui/toast"
|
||||
@@ -101,12 +101,12 @@ export const settings: Setting[] = [
|
||||
labels: ["current directory", "global"],
|
||||
},
|
||||
{
|
||||
title: "Layout",
|
||||
title: "Position",
|
||||
category: "Tabs",
|
||||
path: ["tabs", "layout"],
|
||||
default: "horizontal",
|
||||
values: ["horizontal", "vertical"],
|
||||
keywords: ["sidebar", "orientation", "left"],
|
||||
path: ["tabs", "position"],
|
||||
default: "top",
|
||||
values: TabPosition.literals,
|
||||
keywords: ["sidebar", "orientation", "layout"],
|
||||
},
|
||||
{
|
||||
title: "Layout",
|
||||
|
||||
@@ -44,6 +44,9 @@ export const Cursor = Schema.Struct({
|
||||
}),
|
||||
}).annotate({ description: "Terminal cursor settings" })
|
||||
|
||||
export const TabPosition = Schema.Literals(["top", "bottom", "left", "right"])
|
||||
export type TabPosition = Schema.Schema.Type<typeof TabPosition>
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
theme: Schema.optional(
|
||||
Schema.Struct({
|
||||
@@ -141,8 +144,8 @@ export const Info = Schema.Struct({
|
||||
scope: Schema.optional(Schema.Literals(["global", "cwd"])).annotate({
|
||||
description: "Share tabs globally or keep a separate set for each working directory",
|
||||
}),
|
||||
layout: Schema.optional(Schema.Literals(["horizontal", "vertical"])).annotate({
|
||||
description: "Show tabs in a horizontal strip or vertical sidebar",
|
||||
position: Schema.optional(TabPosition).annotate({
|
||||
description: "Show tabs along the top, bottom, left, or right edge",
|
||||
}),
|
||||
}),
|
||||
).annotate({ description: "Tab strip settings" }),
|
||||
@@ -208,7 +211,7 @@ export type Resolved = Omit<Info, "attention" | "cursor" | "keybinds" | "leader"
|
||||
tabs: {
|
||||
enabled: boolean
|
||||
scope: "global" | "cwd"
|
||||
layout: "horizontal" | "vertical"
|
||||
position: TabPosition
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +253,7 @@ export function resolve(input: Info, options: { terminalSuspend: boolean }): Res
|
||||
...input.tabs,
|
||||
enabled: input.tabs?.enabled ?? true,
|
||||
scope: input.tabs?.scope ?? "cwd",
|
||||
layout: input.tabs?.layout ?? "horizontal",
|
||||
position: input.tabs?.position ?? "top",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ import { errorMessage } from "../../util/error"
|
||||
import { useToast } from "../../ui/toast"
|
||||
import stripAnsi from "strip-ansi"
|
||||
import { usePromptRef } from "../../context/prompt"
|
||||
import { sessionTabsFitVertically, SESSION_SIDEBAR_WIDTH } from "../../ui/layout"
|
||||
import { sessionTabSidebarWidth } from "../../ui/layout"
|
||||
import { projectedPromptInput } from "../../prompt/codec"
|
||||
import { useEpilogue } from "../../context/epilogue"
|
||||
import { normalizePath } from "../../util/path"
|
||||
@@ -225,9 +225,7 @@ export function Session() {
|
||||
const availableWidth = createMemo(
|
||||
() =>
|
||||
dimensions().width -
|
||||
(config.tabs?.enabled && config.tabs.layout === "vertical" && sessionTabsFitVertically(dimensions().width)
|
||||
? SESSION_SIDEBAR_WIDTH
|
||||
: 0),
|
||||
(config.tabs?.enabled ? sessionTabSidebarWidth(config.tabs.position, dimensions().width) : 0),
|
||||
)
|
||||
const wide = createMemo(() => availableWidth() > 120)
|
||||
const sidebarVisible = createMemo(() => {
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import type { TabPosition } from "../config"
|
||||
|
||||
export const SESSION_SIDEBAR_WIDTH = 42
|
||||
const SESSION_CONTENT_MIN_WIDTH = 44
|
||||
|
||||
export function sessionTabsFitVertically(total: number) {
|
||||
return total >= SESSION_SIDEBAR_WIDTH + SESSION_CONTENT_MIN_WIDTH
|
||||
}
|
||||
|
||||
export function effectiveSessionTabPosition(position: TabPosition, total: number): TabPosition {
|
||||
if ((position === "left" || position === "right") && !sessionTabsFitVertically(total)) return "top"
|
||||
return position
|
||||
}
|
||||
|
||||
export function sessionTabSidebarWidth(position: TabPosition, total: number) {
|
||||
const effective = effectiveSessionTabPosition(position, total)
|
||||
if (effective === "left" || effective === "right") return SESSION_SIDEBAR_WIDTH
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { resolve, ConfigProvider, Info, useConfig, type Interface } from "../src/config"
|
||||
import { settings } from "../src/component/dialog-config"
|
||||
import { resolve, ConfigProvider, Info, TabPosition, useConfig, type Interface } from "../src/config"
|
||||
import { settingID, settings } from "../src/component/dialog-config"
|
||||
|
||||
test("validates mini replay settings", () => {
|
||||
const decode = Schema.decodeUnknownSync(Info)
|
||||
@@ -18,10 +18,10 @@ test("validates mini replay settings", () => {
|
||||
test("validates the session tabs setting", () => {
|
||||
const decode = Schema.decodeUnknownSync(Info)
|
||||
|
||||
expect(decode({ tabs: { enabled: true, layout: "vertical" } })).toEqual({
|
||||
tabs: { enabled: true, layout: "vertical" },
|
||||
expect(decode({ tabs: { enabled: true, position: "right" } })).toEqual({
|
||||
tabs: { enabled: true, position: "right" },
|
||||
})
|
||||
expect(() => decode({ tabs: { layout: true } })).toThrow()
|
||||
expect(() => decode({ tabs: { position: "vertical" } })).toThrow()
|
||||
expect(() => decode({ tabs: { enabled: "on" } })).toThrow()
|
||||
})
|
||||
|
||||
@@ -42,13 +42,15 @@ test("resolves nested config and keybind defaults", () => {
|
||||
expect(config.scroll).toEqual({ speed: 2, acceleration: true })
|
||||
expect(config.diffs).toEqual({ view: "split" })
|
||||
expect(config.debug).toEqual({ devtools: true })
|
||||
expect(config.tabs).toEqual({ enabled: true, scope: "cwd", layout: "horizontal" })
|
||||
expect(config.tabs).toEqual({ enabled: true, scope: "cwd", position: "top" })
|
||||
})
|
||||
|
||||
test("shows resolved tab defaults in settings", () => {
|
||||
expect(settings.find((setting) => setting.path.join(".") === "tabs.enabled")?.default).toBe(true)
|
||||
expect(settings.find((setting) => setting.path.join(".") === "tabs.scope")?.default).toBe("cwd")
|
||||
expect(settings.find((setting) => setting.path.join(".") === "tabs.layout")?.default).toBe("horizontal")
|
||||
expect(settings.find((setting) => settingID(setting) === "tabs.enabled")?.default).toBe(true)
|
||||
expect(settings.find((setting) => settingID(setting) === "tabs.scope")?.default).toBe("cwd")
|
||||
const position = settings.find((setting) => settingID(setting) === "tabs.position")
|
||||
expect(position?.default).toBe("top")
|
||||
expect(position?.values).toBe(TabPosition.literals)
|
||||
})
|
||||
|
||||
test("provides config and its host interface", async () => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { expect, test } from "bun:test"
|
||||
import { RGBA } from "@opentui/core"
|
||||
import {
|
||||
DEFAULT_THEME,
|
||||
generateSyntax,
|
||||
resolveTheme,
|
||||
resolveThemeDocument,
|
||||
selectTheme,
|
||||
@@ -43,14 +42,6 @@ test("validates and resolves categorical hues in configured order", () => {
|
||||
expect(() => resolveSource({ version: 2, light: { categorical: ["magenta"] } }, "light")).toThrow("Invalid theme")
|
||||
})
|
||||
|
||||
test("generates syntax with one categorical hue", () => {
|
||||
const theme = resolveSource({ version: 2, light: { categorical: ["red"] } }, "light")
|
||||
const syntax = generateSyntax(theme, "light")
|
||||
|
||||
expect(syntax.getStyleId("extmark.skill")).not.toBeNull()
|
||||
syntax.destroy()
|
||||
})
|
||||
|
||||
test("uses the default categorical order for direct definitions", () => {
|
||||
const theme = resolveTheme({ ...light, categorical: undefined })
|
||||
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { sessionTabsFitVertically, SESSION_SIDEBAR_WIDTH } from "../../src/ui/layout"
|
||||
import { TabPosition } from "../../src/config"
|
||||
import {
|
||||
effectiveSessionTabPosition,
|
||||
SESSION_SIDEBAR_WIDTH,
|
||||
sessionTabSidebarWidth,
|
||||
sessionTabsFitVertically,
|
||||
} from "../../src/ui/layout"
|
||||
|
||||
test("vertical tabs match the session sidebar and preserve compact content width", () => {
|
||||
expect(SESSION_SIDEBAR_WIDTH).toBe(42)
|
||||
expect(sessionTabsFitVertically(86)).toBe(true)
|
||||
expect(sessionTabsFitVertically(85)).toBe(false)
|
||||
})
|
||||
|
||||
test("preserves all tab positions when they fit", () => {
|
||||
expect(TabPosition.literals.map((position) => effectiveSessionTabPosition(position, 120))).toEqual([
|
||||
...TabPosition.literals,
|
||||
])
|
||||
})
|
||||
|
||||
test("falls side tabs back to the top strip when narrow", () => {
|
||||
expect(effectiveSessionTabPosition("left", 85)).toBe("top")
|
||||
expect(effectiveSessionTabPosition("right", 85)).toBe("top")
|
||||
expect(sessionTabSidebarWidth("left", 85)).toBe(0)
|
||||
expect(sessionTabSidebarWidth("right", 86)).toBe(SESSION_SIDEBAR_WIDTH)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user