mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-04 06:26:19 +00:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de305669c2 | ||
|
|
1bbe738c73 | ||
|
|
d1f8f5b35e | ||
|
|
334f7a3510 | ||
|
|
f02e184ba4 | ||
|
|
5ad0786536 | ||
|
|
d8a3962923 | ||
|
|
40679546d4 | ||
|
|
12810e84d3 | ||
|
|
3e1aa1e58c | ||
|
|
7ced9326e6 | ||
|
|
d1ff6f90cf | ||
|
|
d9d094a543 | ||
|
|
d113a40f55 | ||
|
|
54956aac22 | ||
|
|
c1536734ce | ||
|
|
cf7fcfad0a | ||
|
|
17955a1100 | ||
|
|
c0c9c69a27 | ||
|
|
eb1a9ea4d9 | ||
|
|
680bca3183 | ||
|
|
22f81eed0b | ||
|
|
421a04f5fd |
No files matched your search
@@ -122,7 +122,7 @@
|
||||
"@clack/core": "1.0.0-alpha.1",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/pty": "0.1.13",
|
||||
"@opencode-ai/pty": "0.2.0",
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -355,7 +355,7 @@
|
||||
"@lydell/node-pty": "catalog:",
|
||||
"@modelcontextprotocol/client": "2.0.0",
|
||||
"@modelcontextprotocol/core": "2.0.0",
|
||||
"@opencode-ai/pty": "0.1.13",
|
||||
"@opencode-ai/pty": "0.2.0",
|
||||
"@opencode/ai": "workspace:*",
|
||||
"@opencode/codemode": "workspace:*",
|
||||
"@opencode/plugin": "workspace:*",
|
||||
@@ -365,7 +365,7 @@
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"bun-pty": "0.4.8",
|
||||
"bun-pty": "0.4.9",
|
||||
"diff": "catalog:",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -2167,19 +2167,17 @@
|
||||
|
||||
"@opencode-ai/protocol": ["@opencode-ai/protocol@0.0.0-beta-18050", "", { "dependencies": { "@opencode-ai/schema": "0.0.0-beta-18050", "effect": "4.0.0-rc.111" } }, "sha512-HDQMnvGp8IU0MdBRbEuydX1WQm09BZ4HJm9iSMQwzweJuQ2HNscgzHJPIH6P02BsbbtfJ8J7sZGPItrz1tWSgw=="],
|
||||
|
||||
"@opencode-ai/pty": ["@opencode-ai/pty@0.1.13", "", { "optionalDependencies": { "@opencode-ai/pty-darwin-arm64": "0.1.13", "@opencode-ai/pty-darwin-x64": "0.1.13", "@opencode-ai/pty-linux-arm64-gnu": "0.1.13", "@opencode-ai/pty-linux-arm64-musl": "0.1.13", "@opencode-ai/pty-linux-x64-gnu": "0.1.13", "@opencode-ai/pty-linux-x64-musl": "0.1.13" }, "bin": { "opencode-pty": "bin/opencode-pty.js" } }, "sha512-WPCN8h8HaZhhUcrMG0zu+4D9vco0EZiEg/gCF1K3JPRN6UsHMiXq1HVIy5IlyfcoyjfViRmQmXYE4AuU3laBjA=="],
|
||||
"@opencode-ai/pty": ["@opencode-ai/pty@0.2.0", "", { "optionalDependencies": { "@opencode-ai/pty-darwin-arm64": "0.2.0", "@opencode-ai/pty-darwin-x64": "0.2.0", "@opencode-ai/pty-linux-arm64-gnu": "0.2.0", "@opencode-ai/pty-linux-arm64-musl": "0.2.0", "@opencode-ai/pty-linux-x64-gnu": "0.2.0", "@opencode-ai/pty-linux-x64-musl": "0.2.0" }, "bin": { "opencode-pty": "bin/opencode-pty.js" } }, "sha512-pV86urAwinpwFXX8AJlOf8S9CVJhGsV+11I/J6TcxMW0c7u6LgeODzdj/BVBC6jUhsG2aKDxg8rACMcDCy3HiA=="],
|
||||
|
||||
"@opencode-ai/pty-darwin-arm64": ["@opencode-ai/pty-darwin-arm64@0.1.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fVtQZqVLBuJx/aB+5ojfmQifS1KMc9gxlxpFQ6bxEFU8tn8xHQTiFPaNroZgOtaw7I4ceGyx/eXieK1wp68yAA=="],
|
||||
"@opencode-ai/pty-darwin-arm64": ["@opencode-ai/pty-darwin-arm64@0.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2y6xrktb2J7rRk+mzAJHA8cCbWhC4Lo2zJ66t9ad59qFhL5nzAPfpEOwnyvvFqhxebHNvL08Mp64M9IHnM0aiA=="],
|
||||
|
||||
"@opencode-ai/pty-darwin-x64": ["@opencode-ai/pty-darwin-x64@0.1.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-b/tAEm0hCMXraPM9cxR8Rg7X1UBZInRTaxWAS4Ht9eH1nWj1rANOLvHWiWX/vVh5TB0Ubg8bWPu4B0nZkEHROQ=="],
|
||||
"@opencode-ai/pty-darwin-x64": ["@opencode-ai/pty-darwin-x64@0.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-XVQwK9+KgVGYunCYPCCBf1Or0z6zSkzjgfdJd7dEe/LOFg5vmMkOfSB9dCXnoRCWGviWYk7xd07iFIFOgyj5Ig=="],
|
||||
|
||||
"@opencode-ai/pty-linux-arm64-gnu": ["@opencode-ai/pty-linux-arm64-gnu@0.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-I124aSYBBjpGZnYExHfIajkvVK1FiK+//OJBGdqqFp5pas2Oruq4O8tv+pMoxomZIYh2ce/QhOOYLHRwXsthTg=="],
|
||||
"@opencode-ai/pty-linux-arm64-gnu": ["@opencode-ai/pty-linux-arm64-gnu@0.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TPVGCQk5E65IY3ipcpd17rwKehdvXcXEYRhhGzok/6Dske69ei2S+ERz7NXZ8cKFAvbiiT771HHM+XyVYAl+7A=="],
|
||||
|
||||
"@opencode-ai/pty-linux-arm64-musl": ["@opencode-ai/pty-linux-arm64-musl@0.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-feWsfKpaDytGJzutoK43GqQwVghG2vHZt6BE/ydPZNuqIrySQ/6JfliUAMwn5BWs/Ky7ouSwKHCyAVeukusSvg=="],
|
||||
"@opencode-ai/pty-linux-arm64-musl": ["@opencode-ai/pty-linux-arm64-musl@0.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-iH6/liY7xN1OXVD9eGzdH11BVGvnpPsH5Z1Unz0Pn9EzkPFf28oDNKNyeXV0xIAP53oiYp8gpRq2PADNwbVsTg=="],
|
||||
|
||||
"@opencode-ai/pty-linux-x64-gnu": ["@opencode-ai/pty-linux-x64-gnu@0.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-jliNgsevGuxfIeX7eyzjHhrJkF8uEUPnDLbF2v16uv69FhEHrraf7jyWkxazMP6rNvn2CGtwMMc4BXPS5pzjhg=="],
|
||||
|
||||
"@opencode-ai/pty-linux-x64-musl": ["@opencode-ai/pty-linux-x64-musl@0.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-rXDpidW66gz2b2M/NbUN8ZKmAxaJcASnuHATeXevlrFdiPUv8uJwvkRd6Pla1fp01Q65MkBmgRa7Q9c+H1PlzA=="],
|
||||
"@opencode-ai/pty-linux-x64-gnu": ["@opencode-ai/pty-linux-x64-gnu@0.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-GhdrmbUzxGHRfWvt1qutgVD4HLb7aSBsgWGjFsc6k8HjO5ZbdaC3ScRyd512YAAG5KAROLKZ6+0SEBsm5xFYlA=="],
|
||||
|
||||
"@opencode-ai/schema": ["@opencode-ai/schema@0.0.0-beta-18050", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.111" } }, "sha512-/D6VXaWlytTXR3IOiMLIKuPcfp7FQNUzRPm9z3K7UBFd1Bw4q/WZksaf5RVcBGz+0YRxYMc1V4D7MFlceSgtyg=="],
|
||||
|
||||
@@ -3549,7 +3547,7 @@
|
||||
|
||||
"bun-ffi-structs": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="],
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||
"bun-pty": ["bun-pty@0.4.9", "", {}, "sha512-IUF/B3FANo8vIQ775Zt7Er7lphMpYMhLkes45am2WE8FaVI7KRYtj1rQwliZ18b6GpNzBNWcl7sz9QT5wDFBeQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="],
|
||||
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-EEBz2IQ14YPShAzY13zpZ7btq/EuSw86pZ6nrizc9BI=",
|
||||
"aarch64-linux": "sha256-X7om4U4OlKL4OddEMcyKL+1DCQXc9/f1xzzopE16Zmk=",
|
||||
"aarch64-darwin": "sha256-3bejEuX3AGv4SR/16O8KjNSO2t9WDaWCHRbBXsJ6z8A="
|
||||
"x86_64-linux": "sha256-onbxt1PCyU4Nh14S5gIeLH20OtbQ7v2ipRq1zqZkLWs=",
|
||||
"aarch64-linux": "sha256-P7DAE018lTJNGmttg87U9oaTAuw/wHlnMStdnXYKCr0=",
|
||||
"aarch64-darwin": "sha256-N+NfV1ObOTnW+ez7As+CS+6ci/iRGCQ6cslvyQFCp6E="
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,15 @@ const fromRequest = Effect.fn("CohereChat.fromRequest")(function* (request: LLMR
|
||||
const options = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(request.providerOptions ?? {})
|
||||
const flattened = ProviderShared.flattenToolRequest(request)
|
||||
const messages: (typeof Message.Type)[] = request.system.length
|
||||
? [{ role: "system", content: ProviderShared.joinText(request.system) }]
|
||||
? [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
request.system.length === 1
|
||||
? request.system[0].text
|
||||
: request.system.map((part) => ({ type: "text", text: part.text })),
|
||||
},
|
||||
]
|
||||
: []
|
||||
for (const message of flattened.request.messages) {
|
||||
if (message.role === "system") {
|
||||
|
||||
@@ -475,7 +475,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque
|
||||
safetySettings: options.safetySettings,
|
||||
serviceTier: options.serviceTier,
|
||||
systemInstruction:
|
||||
request.system.length === 0 ? undefined : { parts: [{ text: ProviderShared.joinText(request.system) }] },
|
||||
request.system.length === 0 ? undefined : { parts: request.system.map((part) => ({ text: part.text })) },
|
||||
tools: hasTools
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -68,7 +68,10 @@ const MistralAssistantToolCall = Schema.Struct({
|
||||
type MistralAssistantToolCall = Schema.Schema.Type<typeof MistralAssistantToolCall>
|
||||
|
||||
const MistralMessage = Schema.Union([
|
||||
Schema.Struct({ role: Schema.Literal("system"), content: Schema.String }),
|
||||
Schema.Struct({
|
||||
role: Schema.Literal("system"),
|
||||
content: Schema.Union([Schema.String, Schema.Array(MistralTextContent)]),
|
||||
}),
|
||||
Schema.Struct({
|
||||
role: Schema.Literal("user"),
|
||||
content: Schema.Union([Schema.String, Schema.Array(MistralUserContent)]),
|
||||
@@ -335,7 +338,17 @@ const lowerToolResults = Effect.fn("MistralChat.lowerToolResults")(function* (
|
||||
const lowerMessages = Effect.fn("MistralChat.lowerMessages")(function* (request: LLMRequest) {
|
||||
const normalizeID = MistralToolID.normalizer(request)
|
||||
const messages: MistralMessage[] =
|
||||
request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }]
|
||||
request.system.length === 0
|
||||
? []
|
||||
: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
request.system.length === 1
|
||||
? request.system[0].text
|
||||
: request.system.map((part) => ({ type: "text", text: part.text })),
|
||||
},
|
||||
]
|
||||
for (const message of request.messages) {
|
||||
if (message.role === "system") {
|
||||
const update = yield* ProviderShared.wrappedSystemUpdate("Mistral Chat", message)
|
||||
|
||||
@@ -14,7 +14,6 @@ import * as GoogleVertexChat from "../src/providers/google-vertex-chat.js"
|
||||
import * as GoogleVertexMessages from "../src/providers/google-vertex-messages.js"
|
||||
import * as GoogleVertexResponses from "../src/providers/google-vertex-responses.js"
|
||||
import * as OpenAI from "../src/providers/openai.js"
|
||||
import * as OpenAICompatible from "../src/providers/openai-compatible.js"
|
||||
import * as OpenRouter from "../src/providers/openrouter.js"
|
||||
import * as XAI from "../src/providers/xai.js"
|
||||
|
||||
|
||||
+5
-5
@@ -6,7 +6,7 @@
|
||||
"provider:cohere"
|
||||
],
|
||||
"name": "cohere/continues-a-native-tool-call",
|
||||
"recordedAt": "2026-10-02T02:36:16.143Z"
|
||||
"recordedAt": "2026-10-03T04:09:51.600Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
@@ -17,14 +17,14 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"command-a-plus-05-2026\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]}],\"stream\":true,\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"max_tokens\":2048}"
|
||||
"body": "{\"model\":\"command-a-plus-05-2026\",\"messages\":[{\"role\":\"system\",\"content\":[{\"type\":\"text\",\"text\":\"Use the get_weather tool exactly once.\"},{\"type\":\"text\",\"text\":\"After the tool result, reply exactly: Paris is sunny.\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]}],\"stream\":true,\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"max_tokens\":2048}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "event: message-start\ndata: {\"id\":\"a57af21c-0496-4123-8e83-bcc6abb9bcae\",\"type\":\"message-start\",\"delta\":{\"message\":{\"role\":\"assistant\",\"content\":[],\"tool_plan\":\"\",\"tool_calls\":[],\"citations\":[]}}}\n\nevent: content-start\ndata: {\"type\":\"content-start\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"type\":\"thinking\",\"thinking\":\"\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\"The\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" user\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" is\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" asking\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" about\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" the\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" weather\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" in\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" Paris\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\".\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" I\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" have\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" access\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" to\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" a\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" get\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\"_weather\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" function\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" that\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" takes\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" a\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" city\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" parameter\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\".\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" The\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" instructions\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" say\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" to\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" use\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" the\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" get\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\"_weather\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" tool\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" exactly\"}}}}\n\nevent: conteLine truncated
|
||||
"body": "event: message-start\ndata: {\"id\":\"d6c0d412-926c-42ed-b152-043404f8c33c\",\"type\":\"message-start\",\"delta\":{\"message\":{\"role\":\"assistant\",\"content\":[],\"tool_plan\":\"\",\"tool_calls\":[],\"citations\":[]}}}\n\nevent: content-start\ndata: {\"type\":\"content-start\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"type\":\"thinking\",\"thinking\":\"\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\"The\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" user\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" is\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" asking\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" for\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" the\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" weather\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" in\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" Paris\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\".\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" I\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" have\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" a\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" get\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\"_weather\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" function\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" available\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" that\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" takes\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" a\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" city\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" parameter\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\".\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" The\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" user\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" specifically\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" mentioned\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" \\\"\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\"Paris\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\"\\\"\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" as\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" the\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" city\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\".\"}}}}\n\nevent: conteLine truncated
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -35,14 +35,14 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"command-a-plus-05-2026\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"thinking\",\"thinking\":\"The user is asking about the weather in Paris. I have access to a get_weather function that takes a city parameter. The instructions say to use the get_weather tool exactly once, and then reply exactly \\\"Paris is sunny\\\" after getting the tool result.\\n\\nLet me call the get_weather function with \\\"Paris\\\" as the city parameter.\"}],\"tool_calls\":[{\"id\":\"get_weather_mdhvw7kbf1e3\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},{\"role\":\"tool\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\",\"tool_call_id\":\"get_weather_mdhvw7kbf1e3\"}],\"stream\":true,\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"max_tokens\":2048}"
|
||||
"body": "{\"model\":\"command-a-plus-05-2026\",\"messages\":[{\"role\":\"system\",\"content\":[{\"type\":\"text\",\"text\":\"Use the get_weather tool exactly once.\"},{\"type\":\"text\",\"text\":\"After the tool result, reply exactly: Paris is sunny.\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"thinking\",\"thinking\":\"The user is asking for the weather in Paris. I have a get_weather function available that takes a city parameter. The user specifically mentioned \\\"Paris\\\" as the city. I need to use the get_weather tool exactly once as per the policies, and I have all the required parameters (city = \\\"Paris\\\").\\n\\nLet me call the function with the city parameter set to \\\"Paris\\\".\"}],\"tool_calls\":[{\"id\":\"get_weather_vfp5ryredwrg\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},{\"role\":\"tool\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\",\"tool_call_id\":\"get_weather_vfp5ryredwrg\"}],\"stream\":true,\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"max_tokens\":2048}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "event: message-start\ndata: {\"id\":\"fd1ac100-b2c5-42f4-ae1c-6de65aa5ef6a\",\"type\":\"message-start\",\"delta\":{\"message\":{\"role\":\"assistant\",\"content\":[],\"tool_plan\":\"\",\"tool_calls\":[],\"citations\":[]}}}\n\nevent: content-start\ndata: {\"type\":\"content-start\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"type\":\"thinking\",\"thinking\":\"\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\"The\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" tool\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" result\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" shows\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" that\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" Paris\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" has\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" sunny\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" weather\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" with\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" a\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" temperature\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" of\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" 22\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" degrees\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\".\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" According\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" to\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" the\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" instructions\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\",\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" after\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" getting\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" the\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" tool\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" result\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\",\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" I\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" need\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" to\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" reply\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" exactly\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\":\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" \\\"\"}}}}\n\nevent: cLine truncated
|
||||
"body": "event: message-start\ndata: {\"id\":\"0c6cba0e-442d-4e28-96b2-c3cd6ff3b22d\",\"type\":\"message-start\",\"delta\":{\"message\":{\"role\":\"assistant\",\"content\":[],\"tool_plan\":\"\",\"tool_calls\":[],\"citations\":[]}}}\n\nevent: content-start\ndata: {\"type\":\"content-start\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"type\":\"thinking\",\"thinking\":\"\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\"The\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" function\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" returned\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" weather\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" data\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" for\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" Paris\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\".\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" The\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" result\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" shows\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\":\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\"\\n-\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" Temperature\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\":\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" 22\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" (\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\"presumably\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" degrees\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" Celsius\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\")\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\"\\n-\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" Condition\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\":\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" sunny\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\"\\n\\nI\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" should\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" present\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" this\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" information\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" to\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" the\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" user\"}}}}\n\nevent: content-delta\ndata: {\"type\":\"content-delta\",\"index\":0,\"delta\":{\"message\":{\"content\":{\"thinking\":\" in\"}}}}\Line truncated
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "mistral-small-latest",
|
||||
"tags": ["prefix:mistral-chat", "provider:mistral", "protocol:mistral-chat", "tool", "tool-loop", "usage"],
|
||||
"tags": [
|
||||
"prefix:mistral-chat",
|
||||
"provider:mistral",
|
||||
"protocol:mistral-chat",
|
||||
"tool",
|
||||
"tool-loop",
|
||||
"usage"
|
||||
],
|
||||
"name": "mistral-chat/drives-a-tool-loop",
|
||||
"recordedAt": "2026-08-30T17:18:49.552Z"
|
||||
"recordedAt": "2026-10-03T04:09:49.878Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
@@ -15,14 +22,14 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"mistral-small-latest\",\"messages\":[{\"role\":\"system\",\"content\":\"Call lookup_weather exactly once with Paris.\"},{\"role\":\"user\",\"content\":\"What is the weather?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":{\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\"}},\"stream\":true,\"max_tokens\":160,\"temperature\":0,\"reasoning_effort\":\"none\"}"
|
||||
"body": "{\"model\":\"mistral-small-latest\",\"messages\":[{\"role\":\"system\",\"content\":[{\"type\":\"text\",\"text\":\"Call lookup_weather exactly once with Paris.\"},{\"type\":\"text\",\"text\":\"After the tool result, describe the weather briefly.\"}]},{\"role\":\"user\",\"content\":\"What is the weather?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":{\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\"}},\"stream\":true,\"max_tokens\":160,\"temperature\":0,\"reasoning_effort\":\"none\"}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"id\":\"07491e37a5ed48f9987f1583753a466b\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"07491e37a5ed48f9987f1583753a466b\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"id\":\"ffJovBNqY\",\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\": \\\"Paris\\\"}\"},\"index\":0}]},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":110,\"total_tokens\":122,\"completion_tokens\":12,\"prompt_tokens_details\":{\"cached_tokens\":0},\"service_tier\":\"standard\"},\"p\":\"abcdefghijklm\"}\n\ndata: [DONE]\n\n"
|
||||
"body": "data: {\"id\":\"81417fdbfbeb4714ae737aab701cbee4\",\"object\":\"chat.completion.chunk\",\"created\":1791000589,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"81417fdbfbeb4714ae737aab701cbee4\",\"object\":\"chat.completion.chunk\",\"created\":1791000589,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"id\":\"EwgHkPRLW\",\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\": \\\"Paris\\\"}\"},\"index\":0}]},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":120,\"total_tokens\":132,\"completion_tokens\":12,\"prompt_tokens_details\":{\"cached_tokens\":0},\"service_tier\":\"standard\"},\"p\":\"abcdefghij\"}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -33,14 +40,14 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"mistral-small-latest\",\"messages\":[{\"role\":\"system\",\"content\":\"Call lookup_weather exactly once with Paris.\"},{\"role\":\"user\",\"content\":\"What is the weather?\"},{\"role\":\"assistant\",\"content\":\"\",\"tool_calls\":[{\"id\":\"ffJovBNqY\",\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"ffJovBNqY\",\"name\":\"lookup_weather\",\"content\":\"{\\\"condition\\\":\\\"sunny\\\",\\\"temperature\\\":\\\"18C\\\"}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":\"none\",\"stream\":true,\"max_tokens\":160,\"temperature\":0,\"reasoning_effort\":\"none\"}"
|
||||
"body": "{\"model\":\"mistral-small-latest\",\"messages\":[{\"role\":\"system\",\"content\":[{\"type\":\"text\",\"text\":\"Call lookup_weather exactly once with Paris.\"},{\"type\":\"text\",\"text\":\"After the tool result, describe the weather briefly.\"}]},{\"role\":\"user\",\"content\":\"What is the weather?\"},{\"role\":\"assistant\",\"content\":\"\",\"tool_calls\":[{\"id\":\"EwgHkPRLW\",\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"EwgHkPRLW\",\"name\":\"lookup_weather\",\"content\":\"{\\\"condition\\\":\\\"sunny\\\",\\\"temperature\\\":\\\"18C\\\"}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":\"none\",\"stream\":true,\"max_tokens\":160,\"temperature\":0,\"reasoning_effort\":\"none\"}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"The\"},\"finish_reason\":null}],\"p\":\"abcdefghijklmn\"}\n\ndata: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" weather in Paris is\"},\"finish_reason\":null}],\"p\":\"abcdefghijklmn\"}\n\ndata: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" currently sunny with\"},\"finish_reason\":null}],\"p\":\"abcdefghijklmnopqrstu\"}\n\ndata: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" a temperature of \"},\"finish_reason\":null}],\"p\":\"abcdef\"}\n\ndata: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"18°C\"},\"finish_reason\":null}],\"p\":\"abcdefghijklmnopqr\"}\n\ndata: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":57,\"total_tokens\":74,\"completion_tokens\":17,\"prompt_tokens_details\":{\"cached_tokens\":0},\"service_tier\":\"standard\"},\"p\":\"abcdefghijklmnopqrstuvwxyz\"}\n\ndata: [DONE]\n\n"
|
||||
"body": "data: {\"id\":\"a46eb8ccd1b947ca9d360c3446b295a5\",\"object\":\"chat.completion.chunk\",\"created\":1791000589,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"a46eb8ccd1b947ca9d360c3446b295a5\",\"object\":\"chat.completion.chunk\",\"created\":1791000589,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"The\"},\"finish_reason\":null}],\"p\":\"abcdefghijklmnopqrstuvwxyz01234\"}\n\ndata: {\"id\":\"a46eb8ccd1b947ca9d360c3446b295a5\",\"object\":\"chat.completion.chunk\",\"created\":1791000589,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" weather in Paris is\"},\"finish_reason\":null}],\"p\":\"abcdefghijkl\"}\n\ndata: {\"id\":\"a46eb8ccd1b947ca9d360c3446b295a5\",\"object\":\"chat.completion.chunk\",\"created\":1791000589,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" currently sunny with a\"},\"finish_reason\":null}],\"p\":\"abcdefghijklmn\"}\n\ndata: {\"id\":\"a46eb8ccd1b947ca9d360c3446b295a5\",\"object\":\"chat.completion.chunk\",\"created\":1791000589,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" temperature of 1\"},\"finish_reason\":null}],\"p\":\"abcdefghi\"}\n\ndata: {\"id\":\"a46eb8ccd1b947ca9d360c3446b295a5\",\"object\":\"chat.completion.chunk\",\"created\":1791000589,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"8°C\"},\"finish_reason\":null}],\"p\":\"abcdefghijklmnopqrstuvwx\"}\n\ndata: {\"id\":\"a46eb8ccd1b947ca9d360c3446b295a5\",\"object\":\"chat.completion.chunk\",\"created\":1791000589,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":67,\"total_tokens\":84,\"completion_tokens\":17,\"prompt_tokens_details\":{\"cached_tokens\":0},\"service_tier\":\"standard\"},\"p\":\"abcdefghijklmnopqrs\"}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMEvent } from "../../src/index.js"
|
||||
import { LLM, LLMEvent, LLMRequest, SystemPart } from "../../src/index.js"
|
||||
import { Cohere } from "../../src/providers/cohere.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
import { expectWeatherToolLoop, goldenWeatherToolLoopRequest, runWeatherToolLoop } from "../recorded-scenarios.js"
|
||||
@@ -55,12 +55,20 @@ recorded.effect(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* runWeatherToolLoop(
|
||||
goldenWeatherToolLoopRequest({
|
||||
id: "cohere-tool-loop",
|
||||
model: cohere.model("command-a-plus-05-2026"),
|
||||
maxTokens: 2048,
|
||||
temperature: false,
|
||||
}),
|
||||
LLMRequest.update(
|
||||
goldenWeatherToolLoopRequest({
|
||||
id: "cohere-tool-loop",
|
||||
model: cohere.model("command-a-plus-05-2026"),
|
||||
maxTokens: 2048,
|
||||
temperature: false,
|
||||
}),
|
||||
{
|
||||
system: [
|
||||
SystemPart.make("Use the get_weather tool exactly once."),
|
||||
SystemPart.make("After the tool result, reply exactly: Paris is sunny."),
|
||||
],
|
||||
},
|
||||
),
|
||||
)
|
||||
expectWeatherToolLoop(events)
|
||||
expect(events.some(LLMEvent.is.toolInputDelta)).toBe(true)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMClient, LLMEvent, Media, Message, isRetryable } from "../../src/index.js"
|
||||
import { LLM, LLMClient, LLMEvent, Media, Message, SystemPart, isRetryable } from "../../src/index.js"
|
||||
import { Cohere } from "../../src/providers/cohere.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
@@ -23,7 +23,7 @@ it.effect("Cohere lowers native history, thinking, tools, and sampling", () =>
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: cohere.model("command-a-reasoning-08-2025"),
|
||||
system: "Be concise.",
|
||||
system: [SystemPart.make("Be concise.\nKeep this newline."), SystemPart.make("Second instructions.")],
|
||||
messages: [
|
||||
Message.user("Lookup Paris"),
|
||||
Message.assistant([{ type: "tool-call", id: "lookup-1", name: "lookup", input: { city: "Paris" } }]),
|
||||
@@ -44,7 +44,13 @@ it.effect("Cohere lowers native history, thinking, tools, and sampling", () =>
|
||||
thinking: { type: "enabled", token_budget: 128 },
|
||||
tool_choice: "REQUIRED",
|
||||
messages: [
|
||||
{ role: "system", content: "Be concise." },
|
||||
{
|
||||
role: "system",
|
||||
content: [
|
||||
{ type: "text", text: "Be concise.\nKeep this newline." },
|
||||
{ type: "text", text: "Second instructions." },
|
||||
],
|
||||
},
|
||||
{ role: "user", content: [{ type: "text", text: "Lookup Paris" }] },
|
||||
{
|
||||
role: "assistant",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { LLM, LLMEvent, Message, ToolDefinition, Media } from "../../src/index.js"
|
||||
import { LLM, LLMEvent, Message, SystemPart, ToolDefinition, Media } from "../../src/index.js"
|
||||
import { Mistral } from "../../src/providers/index.js"
|
||||
import { MistralChat } from "../../src/protocols/index.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
@@ -41,7 +41,7 @@ describe("Mistral Chat", () => {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model,
|
||||
system: "Initial",
|
||||
system: [SystemPart.make("Initial\nKeep this newline."), SystemPart.make("Second instructions.")],
|
||||
messages: [
|
||||
Message.system("Updated"),
|
||||
Message.user([
|
||||
@@ -105,7 +105,13 @@ describe("Mistral Chat", () => {
|
||||
reasoning_effort: "high",
|
||||
})
|
||||
expect(prepared.body.messages.slice(0, 4)).toMatchObject([
|
||||
{ role: "system", content: "Initial" },
|
||||
{
|
||||
role: "system",
|
||||
content: [
|
||||
{ type: "text", text: "Initial\nKeep this newline." },
|
||||
{ type: "text", text: "Second instructions." },
|
||||
],
|
||||
},
|
||||
{ role: "user", content: "<system-update>\nUpdated\n</system-update>" },
|
||||
{
|
||||
role: "user",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { configure } from "@opencode/ai/providers/mistral"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMEvent, LLMRequest, Message, ToolChoice, ToolDefinition } from "../../src/index.js"
|
||||
import { LLM, LLMEvent, LLMRequest, Message, SystemPart, ToolChoice, ToolDefinition } from "../../src/index.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
import { recordedTests } from "../recorded-test.js"
|
||||
@@ -97,7 +97,10 @@ describe("Mistral recorded", () => {
|
||||
const model = configure({ apiKey, providerOptions: { reasoningEffort: "none" } }).model("mistral-small-latest")
|
||||
const firstRequest = LLM.request({
|
||||
model,
|
||||
system: "Call lookup_weather exactly once with Paris.",
|
||||
system: [
|
||||
SystemPart.make("Call lookup_weather exactly once with Paris."),
|
||||
SystemPart.make("After the tool result, describe the weather briefly."),
|
||||
],
|
||||
prompt: "What is the weather?",
|
||||
tools: [weather],
|
||||
toolChoice: weather,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig.json",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"allowImportingTsExtensions": false,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
mapVisualRegions,
|
||||
reportVisualStability,
|
||||
startVisualProbe,
|
||||
stopVisualProbe,
|
||||
|
||||
@@ -242,7 +242,7 @@ test("keeps an older answer selected while scrolling within the interaction buff
|
||||
|
||||
test("tracks keyboard scrolling from a focused timeline descendant", async ({ page }, testInfo) => {
|
||||
const shellID = "prt_descendant_keyboard_01_shell"
|
||||
const timeline = await setupTimeline(page, {
|
||||
await setupTimeline(page, {
|
||||
messages: [...history(12), userMessage(), assistantMessage([shell(shellID, "completed", lines(5))])],
|
||||
settings: { shellToolPartsExpanded: false },
|
||||
cpuRate: 4,
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
partUpdated,
|
||||
renderedPartID,
|
||||
setupTimeline,
|
||||
shell,
|
||||
status,
|
||||
textPart,
|
||||
toolPart,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CDPSession, Page } from "@playwright/test"
|
||||
import type { Page } from "@playwright/test"
|
||||
import path from "node:path"
|
||||
import { mkdir, writeFile } from "node:fs/promises"
|
||||
|
||||
|
||||
@@ -30,14 +30,7 @@ import type { Upload } from "../attachments/uploads"
|
||||
import { CommentCard } from "@opencode/session-ui/comment-card"
|
||||
import { typeLabel } from "@opencode/session-ui/message-file"
|
||||
import { Skill } from "@opencode/schema/skill"
|
||||
import type {
|
||||
ComposerAttachment,
|
||||
ComposerComment,
|
||||
ComposerOption,
|
||||
ComposerPersistedState,
|
||||
ComposerPrompt,
|
||||
ComposerSuggestion,
|
||||
} from "../types"
|
||||
import type { ComposerAttachment, ComposerComment, ComposerOption, ComposerPrompt, ComposerSuggestion } from "../types"
|
||||
import type { ComposerEditorModel, ComposerSelectControl } from "./interaction"
|
||||
import { isAttachment } from "../prompt-parts"
|
||||
import "../attachments/attachments.css"
|
||||
|
||||
@@ -35,7 +35,6 @@ import { useIntegrations } from "@/providers/catalog/integrations"
|
||||
import { CustomProviderForm } from "@/providers/credentials/dialog"
|
||||
import { ProviderModelGroup, ProviderModelIcon } from "@/providers/models/provider-group"
|
||||
import type { ModelSelection } from "@/providers/models/selection"
|
||||
import { OpenCodeLogo } from "@/providers/opencode-logo"
|
||||
import { decode64 } from "@/runtime/persistence/base64"
|
||||
import { SettingsList } from "@/settings/list"
|
||||
import { useTabs } from "@/shell/tabs/tabs"
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Component, Show, createMemo, createResource } from "solid-js"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { Select } from "@opencode/ui/select"
|
||||
import { Switch } from "@opencode/ui/switch"
|
||||
import { TextInput } from "@opencode/ui/text-input"
|
||||
import { TimelineDetailControl } from "@/settings/timeline-detail"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
@@ -14,42 +13,12 @@ import {
|
||||
useSettings,
|
||||
} from "@/settings/model"
|
||||
import { formatKeybind } from "@/shell/commands/command"
|
||||
import { ExternalLink } from "@/runtime/platform/external-link"
|
||||
import { SettingsList } from "@/settings/list"
|
||||
import { SettingsRow } from "@/settings/row"
|
||||
import {
|
||||
createAppearanceSettingsController,
|
||||
createShellOptions,
|
||||
type AppearanceSettingsController,
|
||||
type ShellSettingsController,
|
||||
} from "./controllers"
|
||||
import { createShellOptions, type ShellSettingsController } from "./controllers"
|
||||
import "@/settings/settings.css"
|
||||
|
||||
const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"]
|
||||
const tabLayoutOptions: ("horizontal" | "vertical")[] = ["horizontal", "vertical"]
|
||||
const fontSettings = {
|
||||
ui: {
|
||||
action: "settings-ui-font",
|
||||
title: "settings.general.row.uiFont.title",
|
||||
description: "settings.general.row.uiFont.description",
|
||||
font: "ui",
|
||||
input: "setUI",
|
||||
},
|
||||
code: {
|
||||
action: "settings-code-font",
|
||||
title: "settings.general.row.font.title",
|
||||
description: "settings.general.row.font.description",
|
||||
font: "code",
|
||||
input: "setCode",
|
||||
},
|
||||
terminal: {
|
||||
action: "settings-terminal-font",
|
||||
title: "settings.general.row.terminalFont.title",
|
||||
description: "settings.general.row.terminalFont.description",
|
||||
font: "terminal",
|
||||
input: "setTerminal",
|
||||
},
|
||||
} as const
|
||||
const AutoApprovePermissionsSetting: Component = () => {
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
@@ -183,90 +152,6 @@ const FollowUpBehaviorSetting: Component = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const AppearanceSection: Component<{ controller: AppearanceSettingsController }> = (props) => {
|
||||
const language = useLanguage()
|
||||
return (
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">{language.t("settings.general.section.appearance")}</h3>
|
||||
<SettingsList>
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.colorScheme.title")}
|
||||
description={language.t("settings.general.row.colorScheme.description")}
|
||||
>
|
||||
<Select
|
||||
data-action="settings-color-scheme"
|
||||
options={schemeOptions}
|
||||
current={schemeOptions.find((option) => option === props.controller.scheme.current())}
|
||||
placement="bottom-end"
|
||||
gutter={6}
|
||||
label={(option) => {
|
||||
if (option === "system") return language.t("theme.scheme.system")
|
||||
if (option === "light") return language.t("theme.scheme.light")
|
||||
return language.t("theme.scheme.dark")
|
||||
}}
|
||||
onSelect={(option) => option && props.controller.scheme.select(option)}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.theme.title")}
|
||||
description={
|
||||
<>
|
||||
{language.t("settings.general.row.theme.description")}{" "}
|
||||
<ExternalLink class="settings-link" href="https://opencode.ai/docs/themes/">
|
||||
{language.t("common.learnMore")}
|
||||
</ExternalLink>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
data-action="settings-theme"
|
||||
options={props.controller.theme.options()}
|
||||
current={props.controller.theme.current()}
|
||||
placement="bottom-end"
|
||||
gutter={6}
|
||||
value={(option) => option.id}
|
||||
label={(option) => option.name}
|
||||
onSelect={props.controller.theme.select}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<FontSetting kind="ui" fonts={props.controller.fonts} />
|
||||
<FontSetting kind="code" fonts={props.controller.fonts} />
|
||||
<FontSetting kind="terminal" fonts={props.controller.fonts} />
|
||||
</SettingsList>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const FontSetting: Component<{
|
||||
kind: "ui" | "code" | "terminal"
|
||||
fonts: AppearanceSettingsController["fonts"]
|
||||
}> = (props) => {
|
||||
const language = useLanguage()
|
||||
const config = () => fontSettings[props.kind]
|
||||
return (
|
||||
<SettingsRow title={language.t(config().title)} description={language.t(config().description)}>
|
||||
<div class="w-full sm:w-[220px]">
|
||||
<TextInput
|
||||
data-action={config().action}
|
||||
type="text"
|
||||
appearance="base"
|
||||
value={props.fonts[config().font]().value}
|
||||
onInput={(event) => props.fonts[config().input](event.currentTarget.value)}
|
||||
placeholder={props.fonts[config().font]().placeholder}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
aria-label={language.t(config().title)}
|
||||
style={{ "font-family": props.fonts[config().font]().family }}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
)
|
||||
}
|
||||
|
||||
const LanguageSetting = () => {
|
||||
const language = useLanguage()
|
||||
const options = createMemo(() =>
|
||||
@@ -409,50 +294,6 @@ export const SettingsGeneral: Component = () => {
|
||||
</div>
|
||||
)
|
||||
|
||||
const NotificationsSection = () => (
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">{language.t("settings.general.section.notifications")}</h3>
|
||||
|
||||
<SettingsList>
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.notifications.agent.title")}
|
||||
description={language.t("settings.general.notifications.agent.description")}
|
||||
>
|
||||
<div data-action="settings-notifications-agent">
|
||||
<Switch
|
||||
checked={settings.notifications.agent()}
|
||||
onChange={(checked) => settings.notifications.setAgent(checked)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.notifications.permissions.title")}
|
||||
description={language.t("settings.general.notifications.permissions.description")}
|
||||
>
|
||||
<div data-action="settings-notifications-permissions">
|
||||
<Switch
|
||||
checked={settings.notifications.permissions()}
|
||||
onChange={(checked) => settings.notifications.setPermissions(checked)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.notifications.errors.title")}
|
||||
description={language.t("settings.general.notifications.errors.description")}
|
||||
>
|
||||
<div data-action="settings-notifications-errors">
|
||||
<Switch
|
||||
checked={settings.notifications.errors()}
|
||||
onChange={(checked) => settings.notifications.setErrors(checked)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</SettingsList>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="settings-tab-header">
|
||||
|
||||
@@ -15,7 +15,6 @@ import { displayName } from "@opencode/ui/project-avatar"
|
||||
import { resolveProjectForSession } from "@/shell/layout/helpers"
|
||||
import { useExtensionHost } from "@/runtime/extension/host"
|
||||
import { useExtensionAttachment } from "@/runtime/extension/services"
|
||||
import { useServer } from "@/runtime/server/current"
|
||||
import { looksLikeSessionID } from "@/session/search"
|
||||
|
||||
export type CommandPaletteEntry = {
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { Schema } from "effect"
|
||||
import { SessionError } from "@opencode/schema/session-error"
|
||||
import { type Accessor, batch, createEffect, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode/ui/context"
|
||||
import { batch, createEffect, onCleanup } from "solid-js"
|
||||
import type { ServerSDK } from "@/runtime/server/client"
|
||||
import type { Data } from "@opencode/client/solid"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { decode64 } from "@/runtime/persistence/base64"
|
||||
import { Persist, persisted } from "@/runtime/persistence/storage"
|
||||
import { Persistence } from "@/runtime/persistence/schema"
|
||||
import { playSoundById } from "@/shell/notifications/sound"
|
||||
import type { createNotificationCoordinator } from "@/shell/notifications/coordinator"
|
||||
import { useGlobal } from "@/runtime/server/runtime"
|
||||
import { ServerConnection, useServers } from "@/runtime/server/registry"
|
||||
import { ServerConnection } from "@/runtime/server/registry"
|
||||
import { sessionIDHasOpenTab, useTabs } from "@/shell/tabs/tabs"
|
||||
import { requireServerKey, sessionHref } from "@/shell/routes/session"
|
||||
import type { ServerScope } from "@/runtime/server/scope"
|
||||
import { sessionHref } from "@/shell/routes/session"
|
||||
import { useServer } from "@/runtime/server/current"
|
||||
|
||||
const NotificationBase = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Persist, persisted, removePersisted, draftPersistedKeys } from "@/runti
|
||||
import { ServerConnection, useServers } from "@/runtime/server/registry"
|
||||
import { useExtensionServers } from "@/runtime/extension/servers"
|
||||
import { createEffect, getOwner, onCleanup, startTransition } from "solid-js"
|
||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||
import { useLocation, useNavigate } from "@solidjs/router"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { uuid } from "@/runtime/persistence/uuid"
|
||||
import { SessionTabsRemovedDetail } from "@/shell/titlebar/session-events"
|
||||
@@ -80,7 +80,6 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||
const [closed, setClosed, , closedReady] = persisted(Persist.window("tabs.closed"), TabStorage.Closed, [])
|
||||
const [pending, setPending] = createStore<Record<string, PendingSession | undefined>>({})
|
||||
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const memory = createTabMemory(getOwner())
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createMutation } from "@tanstack/solid-query"
|
||||
import { IconButton } from "@opencode/ui/icon-button"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { Menu } from "@opencode/ui/menu"
|
||||
import { useGlobal, useServerCtx } from "@/runtime/server/runtime"
|
||||
import { useServerCtx } from "@/runtime/server/runtime"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { ServerConnection, serverName, useServers } from "@/runtime/server/registry"
|
||||
import { displayName } from "@opencode/ui/project-avatar"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noEmit": false,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "node_modules/.ts-dist",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opencode-ai/pty": "0.1.13",
|
||||
"@opencode-ai/pty": "0.2.0",
|
||||
"@opencode/schema": "workspace:*",
|
||||
"@opencode/server": "workspace:*",
|
||||
"@opencode/tui": "workspace:*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EOL } from "node:os"
|
||||
import { Cause, Effect, Exit } from "effect"
|
||||
import { OpenCode, type PluginInfo } from "@opencode/client"
|
||||
import { OpenCode } from "@opencode/client"
|
||||
import { Service } from "@opencode/client/effect/service"
|
||||
import { Npm } from "@opencode/util/npm"
|
||||
import { Config } from "../../../config"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { EOL } from "node:os"
|
||||
import path from "node:path"
|
||||
import { readFile, rename, writeFile } from "node:fs/promises"
|
||||
import { Effect } from "effect"
|
||||
import { applyEdits, modify, parse, type ParseError } from "jsonc-parser"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Service, type Endpoint } from "@opencode/client/effect/service"
|
||||
import { OpenCode, type OpenCodeClient, type SessionMessageAssistantTool } from "@opencode/client/promise"
|
||||
import { OpenCode, type SessionMessageAssistantTool } from "@opencode/client/promise"
|
||||
import { FSUtil } from "@opencode/util/fs-util"
|
||||
import { open } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
|
||||
@@ -49,7 +49,11 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
inherited === undefined
|
||||
? undefined
|
||||
: yield* Schema.decodeUnknownEffect(Schema.fromJsonString(PersistentPty.Handoff))(inherited).pipe(
|
||||
Effect.mapError(() => new Error("Invalid PTY restart handoff")),
|
||||
Effect.catch(() =>
|
||||
Effect.logWarning("Ignoring invalid PTY restart handoff; persistent terminals will start fresh").pipe(
|
||||
Effect.as(undefined),
|
||||
),
|
||||
),
|
||||
)
|
||||
const global = yield* Global.Service
|
||||
if (options.mode === "service") yield* Effect.sync(() => process.chdir(global.home))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Service } from "@opencode/client/effect/service"
|
||||
import { CrossSpawnSpawner } from "@opencode/util/cross-spawn-spawner"
|
||||
import { LayerNode } from "@opencode/util/effect/layer-node"
|
||||
import { Deferred, Effect, Schema, Stream } from "effect"
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "@opentui/solid",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
|
||||
"noUncheckedIndexedAccess": false
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"noUnusedLocals": true
|
||||
},
|
||||
"exclude": ["dist", "dist-node"],
|
||||
"references": [{ "path": "../core" }]
|
||||
|
||||
@@ -145,11 +145,14 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
|
||||
/** Stop the registered local service. */
|
||||
export const stop = Effect.fn("service.stop")(function* (options: StopOptions = {}) {
|
||||
const info = yield* read(options.file)
|
||||
if (options.pty === "handoff" && info !== undefined)
|
||||
yield* Effect.tryPromise(() =>
|
||||
PtyHandoff.prepare(options.file ?? fallback(), info, defaultEnsureTiming.requestTimeout),
|
||||
)
|
||||
else yield* Effect.tryPromise(() => PtyHandoff.clear(options.file ?? fallback()))
|
||||
// Terminal handoff is best-effort; it must never keep the old service running.
|
||||
yield* Effect.tryPromise(() =>
|
||||
options.pty === "handoff" && info !== undefined
|
||||
? PtyHandoff.prepare(options.file ?? fallback(), info, defaultEnsureTiming.requestTimeout)
|
||||
: PtyHandoff.clear(options.file ?? fallback()),
|
||||
).pipe(
|
||||
Effect.catch((cause) => Effect.logWarning("Failed to prepare persistent terminals for replacement", cause)),
|
||||
)
|
||||
if (info !== undefined) yield* terminate(info, options, defaultEnsureTiming)
|
||||
})
|
||||
|
||||
|
||||
@@ -449,7 +449,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/location`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -474,7 +474,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/agent`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -500,7 +500,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/plugin`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -513,7 +513,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input?.["location"] },
|
||||
body: { target: input?.["target"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -526,7 +526,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { targets: input["targets"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 404, 503],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1116,7 +1116,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/model`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1128,7 +1128,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/model/default`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1142,7 +1142,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/experimental/generate`,
|
||||
body: { prompt: input["prompt"], model: input["model"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1156,7 +1156,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/provider`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1182,7 +1182,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/integration`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1208,7 +1208,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { url: input["url"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1238,7 +1238,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { methodID: input["methodID"], answer: input["answer"], label: input["label"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1275,7 +1275,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1314,7 +1314,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1329,7 +1329,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/mcp`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1342,7 +1342,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { config: input["config"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1391,7 +1391,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/mcp/resource`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1460,7 +1460,7 @@ export function make(options: ClientOptions) {
|
||||
project: {
|
||||
list: (requestOptions?: RequestOptions) =>
|
||||
request<ProjectListOutput>(
|
||||
{ method: "GET", path: `/api/project`, successStatus: 200, declaredStatuses: [400, 401], empty: false },
|
||||
{ method: "GET", path: `/api/project`, successStatus: 200, declaredStatuses: [400, 401, 404], empty: false },
|
||||
requestOptions,
|
||||
),
|
||||
update: (input: ProjectUpdateInput, requestOptions?: RequestOptions) =>
|
||||
@@ -1489,7 +1489,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/form`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1504,7 +1504,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/permission/request`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1518,7 +1518,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/permission/saved`,
|
||||
query: { projectID: input?.["projectID"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1529,7 +1529,7 @@ export function make(options: ClientOptions) {
|
||||
method: "DELETE",
|
||||
path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1611,7 +1611,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/fs/list`,
|
||||
query: { location: input?.["location"], path: input?.["path"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1623,7 +1623,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/fs/find`,
|
||||
query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1636,7 +1636,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"], path: input["path"] },
|
||||
body: input["payload"],
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
binaryBody: true,
|
||||
},
|
||||
@@ -1651,7 +1651,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/command`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1665,7 +1665,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/skill`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1680,7 +1680,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { input: input["input"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 500],
|
||||
declaredStatuses: [400, 401, 404, 500],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1701,7 +1701,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/pty`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1720,7 +1720,7 @@ export function make(options: ClientOptions) {
|
||||
env: input?.["env"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1911,7 +1911,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/shell`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1929,7 +1929,7 @@ export function make(options: ClientOptions) {
|
||||
metadata: input["metadata"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1965,7 +1965,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/shell/${encodeURIComponent(input.id)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1979,7 +1979,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/reference`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2049,7 +2049,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/vcs/init`,
|
||||
query: { location: input?.["location"], provider: input?.["provider"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 409, 501, 503],
|
||||
declaredStatuses: [400, 401, 404, 409, 501, 503],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2061,7 +2061,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/vcs`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2073,7 +2073,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/vcs/base`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2085,7 +2085,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/vcs/status`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2098,7 +2098,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/vcs/branch`,
|
||||
query: { location: input?.["location"], search: input?.["search"], limit: input?.["limit"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2111,7 +2111,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/vcs/diff`,
|
||||
query: { location: input["location"], mode: input["mode"], base: input["base"], context: input["context"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2167,7 +2167,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/websearch/provider`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2180,7 +2180,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { query: input["query"], providerID: input["providerID"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 503],
|
||||
declaredStatuses: [400, 401, 404, 503],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -2194,14 +2194,20 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/config`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
shells: (requestOptions?: RequestOptions) =>
|
||||
request<ConfigShellsOutput>(
|
||||
{ method: "GET", path: `/api/config/shell`, successStatus: 200, declaredStatuses: [400, 401], empty: false },
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/config/shell`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
update: (input: ConfigUpdateInput, requestOptions?: RequestOptions) =>
|
||||
@@ -2211,7 +2217,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/experimental/config`,
|
||||
body: { shell: input["shell"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
|
||||
@@ -2531,6 +2531,14 @@ export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly m
|
||||
export const isUnauthorizedError = (value: unknown): value is UnauthorizedError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError"
|
||||
|
||||
export type LocationNotFoundError = {
|
||||
readonly _tag: "LocationNotFoundError"
|
||||
readonly location: { readonly directory: string }
|
||||
readonly message: string
|
||||
}
|
||||
export const isLocationNotFoundError = (value: unknown): value is LocationNotFoundError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "LocationNotFoundError"
|
||||
|
||||
export type ServiceUnavailableError = {
|
||||
readonly _tag: "ServiceUnavailableError"
|
||||
readonly message: string
|
||||
|
||||
@@ -118,9 +118,12 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
||||
/** Stop the registered local service. */
|
||||
export async function stop(options: StopOptions = {}) {
|
||||
const info = await read(options.file)
|
||||
if (options.pty === "handoff" && info !== undefined)
|
||||
await PtyHandoff.prepare(options.file ?? fallback(), info, defaultEnsureTiming.requestTimeout)
|
||||
else await PtyHandoff.clear(options.file ?? fallback())
|
||||
// Terminal handoff is best-effort; it must never keep the old service running.
|
||||
await (
|
||||
options.pty === "handoff" && info !== undefined
|
||||
? PtyHandoff.prepare(options.file ?? fallback(), info, defaultEnsureTiming.requestTimeout)
|
||||
: PtyHandoff.clear(options.file ?? fallback())
|
||||
).catch((cause: unknown) => console.warn("Failed to prepare persistent terminals for replacement", cause))
|
||||
if (info !== undefined) await terminate(info, options, defaultEnsureTiming)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,14 @@ type Sidecar = {
|
||||
readonly expiresAt: number
|
||||
}
|
||||
|
||||
/** Publish the ticket before stopping its owner so every replacement contender can adopt it. */
|
||||
/**
|
||||
* Publish the ticket before stopping its owner so every replacement contender can adopt it.
|
||||
* Never throws: terminals are best-effort, so any failure replaces the service without a handoff.
|
||||
*/
|
||||
export async function prepare(file: string, info: Info, timeout: number) {
|
||||
const existing = await read(file)
|
||||
if (existing !== undefined && existing.expiresAt > Date.now() && same(existing.source, info)) return
|
||||
const { ClientError, OpenCode } = await import("./promise/index.js")
|
||||
const { OpenCode } = await import("./promise/index.js")
|
||||
const client = OpenCode.make({
|
||||
baseUrl: info.url,
|
||||
headers:
|
||||
@@ -22,43 +25,30 @@ export async function prepare(file: string, info: Info, timeout: number) {
|
||||
? undefined
|
||||
: { authorization: "Basic " + Buffer.from(`opencode:${info.password}`).toString("base64") },
|
||||
})
|
||||
const missing = (error: unknown) =>
|
||||
error instanceof ClientError &&
|
||||
error.reason === "UnexpectedStatus" &&
|
||||
typeof error.cause === "object" &&
|
||||
error.cause !== null &&
|
||||
"status" in error.cause &&
|
||||
error.cause.status === 404
|
||||
const result = await client.experimental.persistentPty.handoff({ signal: AbortSignal.timeout(timeout) }).then(
|
||||
(value) => ({ value }),
|
||||
(cause: unknown) => ({ cause }),
|
||||
const handoff = await client.experimental.persistentPty
|
||||
.handoff({ signal: AbortSignal.timeout(timeout) })
|
||||
.then(parse)
|
||||
.catch(async (cause: unknown) => {
|
||||
// Another caller may already have prepared and stopped this server.
|
||||
const concurrent = await read(file)
|
||||
if (concurrent !== undefined && concurrent.expiresAt > Date.now() && same(concurrent.source, info)) return
|
||||
console.warn("Background service cannot hand off persistent terminals; shutting them down before replacement", cause)
|
||||
await client.experimental.persistentPty.shutdown({ signal: AbortSignal.timeout(timeout) }).catch(() => {})
|
||||
return null
|
||||
})
|
||||
if (handoff === undefined) return
|
||||
await publish(file, info, handoff).catch((cause: unknown) =>
|
||||
console.warn("Failed to publish persistent terminal handoff", cause),
|
||||
)
|
||||
if ("cause" in result) {
|
||||
// Another caller may already have prepared and stopped this server.
|
||||
const concurrent = await read(file)
|
||||
if (concurrent !== undefined && concurrent.expiresAt > Date.now() && same(concurrent.source, info)) return
|
||||
if (!missing(result.cause))
|
||||
throw new Error("Failed to prepare persistent terminals for service replacement", { cause: result.cause })
|
||||
console.warn("Background service cannot hand off persistent terminals; shutting them down before replacement")
|
||||
await client.experimental.persistentPty
|
||||
.shutdown({ signal: AbortSignal.timeout(timeout) })
|
||||
.catch((cause: unknown) => {
|
||||
if (missing(cause)) return
|
||||
throw new Error("Failed to shut down persistent terminals before service replacement", { cause })
|
||||
})
|
||||
await publish(file, info, null)
|
||||
return
|
||||
}
|
||||
const body: unknown = result.value
|
||||
}
|
||||
|
||||
function parse(body: unknown) {
|
||||
if (typeof body !== "object" || body === null || !("handoff" in body))
|
||||
throw new Error("Invalid persistent terminal handoff response")
|
||||
if (body.handoff === null) {
|
||||
await publish(file, info, null)
|
||||
return
|
||||
}
|
||||
if (body.handoff === null) return null
|
||||
if (!isHandoff(body.handoff) || body.handoff.expiresAt <= Date.now())
|
||||
throw new Error("Invalid or expired persistent terminal handoff")
|
||||
await publish(file, info, body.handoff)
|
||||
return body.handoff
|
||||
}
|
||||
|
||||
async function publish(file: string, info: Info, handoff: PersistentPty.Handoff | null) {
|
||||
|
||||
@@ -52,6 +52,8 @@ const server = Bun.serve({
|
||||
await writeFile(registration + ".prepared", JSON.stringify(handoff))
|
||||
return Response.json({ handoff })
|
||||
}
|
||||
if (pathname === "/api/experimental/persistent-pty/handoff" && mode === "handoff-broken")
|
||||
return new Response(null, { status: 500 })
|
||||
if (pathname !== "/api/info") return new Response(null, { status: 404 })
|
||||
requests += 1
|
||||
if (mode === "starting") await writeFile(registration + ".status-request", "")
|
||||
|
||||
@@ -147,6 +147,19 @@ test("evicts an unresponsive registered service before starting its replacement"
|
||||
expect(endpoint.url).toBe(replacement.url)
|
||||
})
|
||||
|
||||
test("stops the registered service even when terminal handoff fails", async () => {
|
||||
await using fixture = await serviceFixture()
|
||||
const registration = fixture.registration
|
||||
fixture.spawn("handoff-broken")
|
||||
await fixture.waitForFile()
|
||||
|
||||
await Service.stop({ file: registration, pty: "handoff" })
|
||||
|
||||
expect(await Bun.file(registration + ".signal").text()).toBe("SIGTERM")
|
||||
expect(await Bun.file(registration).exists()).toBe(false)
|
||||
expect((await Bun.file(registration + ".pty-handoff").json()).handoff).toBeNull()
|
||||
})
|
||||
|
||||
test("signals the registered service process", async () => {
|
||||
await using fixture = await serviceFixture()
|
||||
const registration = fixture.registration
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"allowImportingTsExtensions": false,
|
||||
"allowJs": false,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["@cloudflare/workers-types", "node"]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"noUncheckedIndexedAccess": false
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"@lydell/node-pty": "catalog:",
|
||||
"@modelcontextprotocol/client": "2.0.0",
|
||||
"@modelcontextprotocol/core": "2.0.0",
|
||||
"@opencode-ai/pty": "0.1.13",
|
||||
"@opencode-ai/pty": "0.2.0",
|
||||
"@opencode/ai": "workspace:*",
|
||||
"@opencode/codemode": "workspace:*",
|
||||
"@opencode/plugin": "workspace:*",
|
||||
@@ -116,7 +116,7 @@
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"bun-pty": "0.4.8",
|
||||
"bun-pty": "0.4.9",
|
||||
"diff": "catalog:",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ConfigCompaction } from "@opencode/schema/config/compaction"
|
||||
import { ConfigFormatter } from "@opencode/schema/config/formatter"
|
||||
import { ConfigLSP } from "@opencode/schema/config/lsp"
|
||||
import { ConfigMedia } from "@opencode/schema/config/media"
|
||||
import { ConfigModel } from "@opencode/schema/config/model"
|
||||
import { ConfigMCP } from "@opencode/schema/config/mcp"
|
||||
import { ConfigPlugin } from "@opencode/schema/config/plugin"
|
||||
import { ConfigPolicy } from "@opencode/schema/config/policy"
|
||||
@@ -18,13 +19,11 @@ import { ConfigReference } from "@opencode/schema/config/reference"
|
||||
import { ConfigExperimental } from "@opencode/schema/config/experimental"
|
||||
import { Permission } from "@opencode/schema/permission"
|
||||
import { ConfigAgentV1 } from "../v1/config/agent.js"
|
||||
import { ConfigAttachmentV1 } from "../v1/config/attachment.js"
|
||||
import { ConfigCommandV1 } from "../v1/config/command.js"
|
||||
import { ConfigMCPV1 } from "../v1/config/mcp.js"
|
||||
import { ConfigPermissionV1 } from "../v1/config/permission.js"
|
||||
import { ConfigPluginV1 } from "../v1/config/plugin.js"
|
||||
import { ConfigProviderV1 } from "../v1/config/provider.js"
|
||||
import { ConfigV1 } from "../v1/config/config.js"
|
||||
import { ConfigMigrateV1 } from "../v1/config/migrate.js"
|
||||
import { PositiveInt } from "../schema.js"
|
||||
|
||||
@@ -53,6 +52,7 @@ const unsupportedExperimental = [
|
||||
] as const
|
||||
const unsupportedProvider = ["id", "whitelist", "blacklist"] as const
|
||||
const unsupportedModel = ["release_date", "attachment", "reasoning", "temperature", "experimental"] as const
|
||||
const LegacyAutoupdate = Schema.Literals([true, false, "notify"])
|
||||
|
||||
export function normalize(input: unknown): Result {
|
||||
if (!isRecord(input))
|
||||
@@ -71,7 +71,7 @@ export function normalize(input: unknown): Result {
|
||||
? decodeEncoded(Schema.Boolean, input.snapshot, ["snapshot"], diagnostics)
|
||||
: undefined
|
||||
const legacyUpdate = own(input, "autoupdate")
|
||||
? decodeValue(ConfigV1.Info.fields.autoupdate, input.autoupdate, ["autoupdate"], diagnostics)
|
||||
? decodeValue(LegacyAutoupdate, input.autoupdate, ["autoupdate"], diagnostics)
|
||||
: undefined
|
||||
const nativeUpdate = own(input, "update")
|
||||
? decodeEncoded(Info.fields.update, input.update, ["update"], diagnostics)
|
||||
@@ -82,15 +82,18 @@ export function normalize(input: unknown): Result {
|
||||
: undefined
|
||||
: undefined
|
||||
const legacyMedia = own(input, "attachment")
|
||||
? decodeValue(ConfigAttachmentV1.Info, input.attachment, ["attachment"], diagnostics)
|
||||
? decodeEncoded(ConfigMedia.Info, input.attachment, ["attachment"], diagnostics)
|
||||
: undefined
|
||||
if (legacyMedia !== undefined) {
|
||||
const migrated = ConfigMigrateV1.migrate({ attachment: legacyMedia }).media
|
||||
if (migrated !== undefined) encoded.media = canonical(ConfigMedia.Info, migrated)
|
||||
}
|
||||
if (legacyMedia !== undefined) encoded.media = legacyMedia
|
||||
if (legacySnapshots !== undefined) encoded.snapshots = legacySnapshots
|
||||
const migratedUpdate =
|
||||
legacyUpdate === undefined ? undefined : ConfigMigrateV1.migrate({ autoupdate: legacyUpdate }).update
|
||||
legacyUpdate === false
|
||||
? "disable"
|
||||
: legacyUpdate === "notify"
|
||||
? "notify"
|
||||
: legacyUpdate === true
|
||||
? "auto"
|
||||
: undefined
|
||||
const update = prefer(migratedUpdate, nativeUpdate, ["update"], diagnostics)
|
||||
if (update !== undefined) encoded.update = update
|
||||
if (legacyShare !== undefined) encoded.share = legacyShare
|
||||
@@ -114,10 +117,9 @@ export function normalize(input: unknown): Result {
|
||||
|
||||
const legacyCommands = decodeMap(input.command, ConfigCommandV1.Info, ["command"], diagnostics, decodeValue)
|
||||
diagnoseSelectionMap(input.command, ["command"], diagnostics)
|
||||
const migratedCommands = mapValues(legacyCommands, (value) => {
|
||||
const migrated = ConfigMigrateV1.commands({ value })?.value
|
||||
return migrated === undefined ? undefined : canonical(ConfigCommand.Info, migrated)
|
||||
})
|
||||
const migratedCommands = mapValues(legacyCommands, (value) =>
|
||||
canonical(ConfigCommand.Info, ConfigMigrateV1.migrateCommand(value)),
|
||||
)
|
||||
const nativeCommands = decodeMap(input.commands, ConfigCommand.Info, ["commands"], diagnostics, decodeEncoded)
|
||||
mergeMap(
|
||||
encoded,
|
||||
@@ -135,9 +137,7 @@ export function normalize(input: unknown): Result {
|
||||
const legacySmallModel = own(input, "small_model")
|
||||
? decodeValue(Schema.String, input.small_model, ["small_model"], diagnostics)
|
||||
: undefined
|
||||
const migratedSmallModel = legacySmallModel
|
||||
? ConfigMigrateV1.migrate({ small_model: legacySmallModel }).agents?.title?.model
|
||||
: undefined
|
||||
const migratedSmallModel = ConfigMigrateV1.modelSelection(legacySmallModel)
|
||||
if (legacySmallModel && !migratedSmallModel)
|
||||
diagnostics.push({
|
||||
kind: "unsupported",
|
||||
@@ -197,10 +197,14 @@ export function normalize(input: unknown): Result {
|
||||
normalizeFormatter(input, encoded, diagnostics)
|
||||
normalizeLsp(input, encoded, diagnostics)
|
||||
|
||||
const model = own(input, "model")
|
||||
? decodeValue(ConfigModel.Selection, input.model, ["model"], diagnostics)
|
||||
: undefined
|
||||
if (model) encoded.model = { ...model, providerID: ConfigMigrateV1.providerID(model.providerID) }
|
||||
|
||||
const nativeAtomic = {
|
||||
$schema: Info.fields.$schema,
|
||||
shell: Info.fields.shell,
|
||||
model: Info.fields.model,
|
||||
default_agent: Info.fields.default_agent,
|
||||
share: Info.fields.share,
|
||||
enterprise: Info.fields.enterprise,
|
||||
|
||||
@@ -114,7 +114,7 @@ const nodes = [
|
||||
export const graph = LayerNode.group(nodes)
|
||||
|
||||
export type Services = LayerNode.Output<typeof graph>
|
||||
export type Error = Layer.Error<ReturnType<typeof layer>>
|
||||
export type Error = FileSystem.DirectoryNotFoundError
|
||||
|
||||
export interface Options {
|
||||
// Plugins this instance is born with; empty and absent are equivalent.
|
||||
@@ -145,7 +145,7 @@ const vanillaReplacements: LayerNode.Replacements = [
|
||||
]
|
||||
|
||||
// One instance is one compiled, fresh copy of the graph standing on a directory.
|
||||
export function layer(ref: Location.Ref, options: Options = {}): Layer.Layer<Services> {
|
||||
export function layer(ref: Location.Ref, options: Options = {}): Layer.Layer<Services, Error> {
|
||||
const startedAt = performance.now()
|
||||
// Ordered: vanilla defaults, then caller replacements (which win over the
|
||||
// defaults), then instance bindings (which win over everything).
|
||||
@@ -157,8 +157,18 @@ export function layer(ref: Location.Ref, options: Options = {}): Layer.Layer<Ser
|
||||
]
|
||||
|
||||
return LayerNode.compile(graph, { replacements, shared: Node.tags.values.global }).pipe(
|
||||
// Instance boot failures are defects; provided operations retain their typed errors.
|
||||
Layer.orDie,
|
||||
// A missing directory is expected; other instance boot failures remain defects.
|
||||
Layer.catchCause(
|
||||
(cause): Layer.Layer<Services, Error> =>
|
||||
Layer.unwrap(
|
||||
Effect.failCause(cause).pipe(
|
||||
Effect.catch(
|
||||
(error): Effect.Effect<never, Error> =>
|
||||
error instanceof FileSystem.DirectoryNotFoundError ? Effect.fail(error) : Effect.die(error),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Layer.tap((context) => Effect.addFinalizer(() => Context.get(context, LocationLifecycle.Service).shutdown)),
|
||||
Layer.tap(() =>
|
||||
Effect.logInfo("location services booted", {
|
||||
|
||||
@@ -5,13 +5,13 @@ import { Context, type Effect } from "effect"
|
||||
import type { Session } from "@opencode/schema/session"
|
||||
import { Node } from "@opencode/util/effect/app-node"
|
||||
import { LayerNode } from "@opencode/util/effect/layer-node"
|
||||
import type { Services } from "../instance.js"
|
||||
import type { Services, Error } from "../instance.js"
|
||||
|
||||
/** Selects Session capabilities; implementations own caching and lifetime. */
|
||||
export interface Interface {
|
||||
readonly provide: (
|
||||
session: Session.Info,
|
||||
) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Services>>
|
||||
) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | Error, Exclude<R, Services>>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Instance") {}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function buildLocationServiceMap(
|
||||
Effect.gen(function* () {
|
||||
const owner = yield* Effect.scope
|
||||
const builds = MutableHashMap.empty<Location.Ref, { close?: Effect.Effect<void> }>()
|
||||
const inner: LayerMap.LayerMap<Location.Ref, LocationServices> = yield* LayerMap.make(
|
||||
const inner: LayerMap.LayerMap<Location.Ref, LocationServices, Instance.Error> = yield* LayerMap.make(
|
||||
(ref: Location.Ref) => {
|
||||
const build: { close?: Effect.Effect<void> } = {}
|
||||
MutableHashMap.set(builds, ref, build)
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
type Transport,
|
||||
type VersionNegotiationOptions,
|
||||
} from "@modelcontextprotocol/client"
|
||||
import { Cause, Effect, Exit, Schema } from "effect"
|
||||
import { Cause, Clock, Duration, Effect, Exit, Schedule, Schema } from "effect"
|
||||
import { ConfigMCP } from "@opencode/schema/config/mcp"
|
||||
import type { Session } from "@opencode/schema/session"
|
||||
import { McpStdio } from "./stdio.js"
|
||||
@@ -49,6 +49,14 @@ const describe = (error: unknown) => {
|
||||
return detail && !error.message.includes(detail) ? `${error.message} (${detail})` : error.message
|
||||
}
|
||||
|
||||
// Overloaded or restarting servers and dropped connections usually recover within a second.
|
||||
// Fetch reports network failures as a TypeError carrying a system error code.
|
||||
// 501 means the server will never support the request.
|
||||
const isTransient = (error: unknown) =>
|
||||
error instanceof SdkHttpError
|
||||
? error.status === 408 || error.status === 429 || (error.status >= 500 && error.status !== 501)
|
||||
: error instanceof TypeError && "code" in error
|
||||
|
||||
export type { GetPromptResult, Prompt, ReadResourceResult, Resource, Tool }
|
||||
export type ResourceTemplate = ResourceTemplateType
|
||||
|
||||
@@ -159,6 +167,20 @@ export const connect = Effect.fnUntraced(function* (
|
||||
? `${describe(error)}: ${lastError}`
|
||||
: describe(error)
|
||||
|
||||
// Two more attempts, 250 ms and 1 s later, while the failure is transient and the deadline has not passed.
|
||||
const retrying = <A, E>(what: string, effect: Effect.Effect<A, E>, deadline = Infinity) =>
|
||||
Effect.retry(
|
||||
effect,
|
||||
Schedule.max([Schedule.exponential("250 millis", 4), Schedule.recurs(2)]).pipe(
|
||||
Schedule.setInputType<E>(),
|
||||
Schedule.while(({ input, now, duration }) =>
|
||||
!isTransient(input) || now + Duration.toMillis(duration) >= deadline
|
||||
? Effect.succeed(false)
|
||||
: Effect.logWarning(`retrying ${what}`, { server, error: describe(input) }).pipe(Effect.as(true)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const initialize = Effect.fnUntraced(function* (transport: Transport) {
|
||||
const runFork = Effect.runForkWith(yield* Effect.context())
|
||||
const client = new Client(clientInfo, {
|
||||
@@ -245,22 +267,28 @@ export const connect = Effect.fnUntraced(function* (
|
||||
const url = new URL(config.url)
|
||||
const addedCodemode = config.codemode !== false && !url.searchParams.has("codemode")
|
||||
if (addedCodemode) url.searchParams.set("codemode", "false")
|
||||
const open = (url: URL) => {
|
||||
const open = Effect.fnUntraced(function* (url: URL) {
|
||||
session.transport = new StreamableHTTPClientTransport(url, {
|
||||
requestInit: config.headers ? { headers: config.headers } : undefined,
|
||||
authProvider,
|
||||
fetch,
|
||||
})
|
||||
return initialize(session.transport)
|
||||
}
|
||||
return yield* initialize(session.transport)
|
||||
})
|
||||
|
||||
return yield* open(url).pipe(
|
||||
Effect.catch((error) => {
|
||||
if (!addedCodemode || !(error instanceof SdkHttpError) || (error.status !== 400 && error.status !== 404))
|
||||
return Effect.fail(error)
|
||||
// Servers that reject unknown query params get one retry at the configured URL.
|
||||
return open(new URL(config.url))
|
||||
}),
|
||||
// Every attempt opens a fresh transport; no retry starts once the startup timeout has passed.
|
||||
const deadline = (yield* Clock.currentTimeMillis) + (config.timeout?.startup ?? DEFAULT_STARTUP_TIMEOUT)
|
||||
return yield* retrying(
|
||||
"MCP connect",
|
||||
open(url).pipe(
|
||||
Effect.catch((error) => {
|
||||
if (!addedCodemode || !(error instanceof SdkHttpError) || (error.status !== 400 && error.status !== 404))
|
||||
return Effect.fail(error)
|
||||
// Servers that reject unknown query params get one retry at the configured URL.
|
||||
return open(new URL(config.url))
|
||||
}),
|
||||
),
|
||||
deadline,
|
||||
)
|
||||
}).pipe(Effect.exit)
|
||||
if (Exit.isSuccess(exit)) {
|
||||
@@ -292,20 +320,23 @@ export const connect = Effect.fnUntraced(function* (
|
||||
Effect.tryPromise({ try: run, catch: failure }).pipe(
|
||||
Effect.tapError((error) => Effect.logWarning(`failed to ${what}`, { server, error: error.message })),
|
||||
)
|
||||
// Listing is read-only, so a transient failure is safe to retry.
|
||||
const list = <A>(what: string, run: () => Promise<A>) =>
|
||||
retrying(what, Effect.tryPromise({ try: run, catch: (error) => error })).pipe(
|
||||
Effect.mapError(failure),
|
||||
Effect.tapError((error) => Effect.logWarning(`failed to ${what}`, { server, error: error.message })),
|
||||
)
|
||||
|
||||
return {
|
||||
modern: client.getProtocolEra() === "modern",
|
||||
instructions: client.getInstructions()?.trim() || undefined,
|
||||
tools: () =>
|
||||
request("list MCP tools", () => client.listTools(undefined, catalog)).pipe(Effect.map((r) => r.tools)),
|
||||
tools: () => list("list MCP tools", () => client.listTools(undefined, catalog)).pipe(Effect.map((r) => r.tools)),
|
||||
prompts: () =>
|
||||
request("list MCP prompts", () => client.listPrompts(undefined, catalog)).pipe(Effect.map((r) => r.prompts)),
|
||||
list("list MCP prompts", () => client.listPrompts(undefined, catalog)).pipe(Effect.map((r) => r.prompts)),
|
||||
resources: () =>
|
||||
request("list MCP resources", () => client.listResources(undefined, catalog)).pipe(
|
||||
Effect.map((r) => r.resources),
|
||||
),
|
||||
list("list MCP resources", () => client.listResources(undefined, catalog)).pipe(Effect.map((r) => r.resources)),
|
||||
resourceTemplates: () =>
|
||||
request("list MCP resource templates", () => client.listResourceTemplates(undefined, catalog)).pipe(
|
||||
list("list MCP resource templates", () => client.listResourceTemplates(undefined, catalog)).pipe(
|
||||
Effect.map((r) => r.resourceTemplates),
|
||||
),
|
||||
readResource: (input) => {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"deepinfra":{"id":"deepinfra","env":["DEEPINFRA_API_KEY"],"npm":"@ai-sdk/deepinfra","name":"Deep Infra","doc":"https://deepinfra.com/models","models":{"tencent/Hy3":{"id":"tencent/Hy3","name":"Hy3","description":"Tencent Hy reasoning model for coding, instruction following, and agent tasks","family":"Hy","attachment":false,"reasoning":true,"reasoning_options":[],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-07-06","last_updated":"2026-07-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":262144,"input":192000,"output":128000},"cost":{"input":0.13,"output":0.53,"cache_read":0.033},"canonical_model_id":"tencent/hy3"},"tencent/Hy4-preview":{"id":"tencent/Hy4-preview","name":"Hy4 preview","description":"A next-generation productivity model with significantly enhanced Agent and complex task execution capabilities.","family":"Hy","attachment":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","high"]}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-08-28","last_updated":"2026-08-28","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":64000},"cost":{"input":0.834,"output":2.501,"cache_read":0.042},"canonical_model_id":"tencent/hy4-preview"},"meta-llama/Llama-3.3-70B-Instruct-Turbo":{"id":"meta-llama/Llama-3.3-70B-Instruct-Turbo","name":"Llama 3.3 70B Turbo","description":"Compact Llama instruction model for fast chat and local deployment","family":"llama","attachment":false,"reasoning":false,"tool_call":true,"structured_output":true,"release_date":"2024-12-06","last_updated":"2024-12-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":131072,"output":16384},"cost":{"input":0.1,"output":0.32}},"meta-llama/Llama-4-Scout-17B-16E-Instruct":{"id":"meta-llama/Llama-4-Scout-17B-16E-Instruct","name":"Llama 4 Scout 17B","description":"Open multimodal Llama model for long-context analysis and efficient agents","family":"llama","attachment":true,"reasoning":false,"tool_call":true,"structured_output":true,"release_date":"2025-04-05","last_updated":"2025-04-05","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"limit":{"context":327680,"output":16384},"cost":{"input":0.1,"output":0.3}},"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":{"id":"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8","name":"Llama 4 Maverick 17B FP8","description":"Open multimodal Llama model for strong reasoning and fast responses","family":"llama","attachment":true,"reasoning":false,"tool_call":false,"structured_output":true,"release_date":"2025-04-05","last_updated":"2025-04-05","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":16384},"status":"deprecated","cost":{"input":0.2,"output":0.8}},"XiaomiMiMo/MiMo-V2.6-Pro":{"id":"XiaomiMiMo/MiMo-V2.6-Pro","name":"MiMo-V2.6-Pro","description":"Stronger MiMo Pro tier for multimodal reasoning and coding-agent execution","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-09-22","last_updated":"2026-09-22","modalities":{"input":["text","image","audio","video"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":131072},"cost":{"input":0.43,"output":0.87,"cache_read":0.0036},"canonical_model_id":"xiaomi/mimo-v2.6-pro"},"XiaomiMiMo/MiMo-V2.5-Pro":{"id":"XiaomiMiMo/MiMo-V2.5-Pro","name":"MiMo-V2.5-Pro","description":"Stronger MiMo Pro tier for multimodal reasoning and coding-agent execution","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"interleaved":{"field":"reasoning_content"},"structured_output":true,"temperature":true,"knowledge":"2024-12","release_date":"2026-04-22","last_updated":"2026-04-22","modalities":{"input":["text","audio"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":16384},"status":"deprecated","cost":{"input":1,"output":3,"cache_read":0.2},"canonical_model_id":"xiaomi/mimo-v2.5-pro"},"XiaomiMiMo/MiMo-V2.6-Flash":{"id":"XiaomiMiMo/MiMo-V2.6-Flash","name":"MiMo-V2.6-Flash","description":"MiMo Flash model for multimodal coding agents and long-context automation","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-09-22","last_updated":"2026-09-22","modalities":{"input":["text","image","audio","video"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":131072},"cost":{"input":0.14,"output":0.28,"cache_read":0.0028},"canonical_model_id":"xiaomi/mimo-v2.6-flash"},"XiaomiMiMo/MiMo-V2.5":{"id":"XiaomiMiMo/MiMo-V2.5","name":"MiMo-V2.5","description":"Open MiMo model for multimodal coding agents and long-coLine truncated
|
||||
{"deepinfra":{"id":"deepinfra","env":["DEEPINFRA_API_KEY"],"npm":"@ai-sdk/deepinfra","name":"Deep Infra","doc":"https://deepinfra.com/models","models":{"tencent/Hy3":{"id":"tencent/Hy3","name":"Hy3","description":"Tencent Hy reasoning model for coding, instruction following, and agent tasks","family":"Hy","attachment":false,"reasoning":true,"reasoning_options":[],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-07-06","last_updated":"2026-07-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":262144,"input":192000,"output":128000},"cost":{"input":0.13,"output":0.53,"cache_read":0.033},"canonical_model_id":"tencent/hy3"},"tencent/Hy4-preview":{"id":"tencent/Hy4-preview","name":"Hy4 preview","description":"A next-generation productivity model with significantly enhanced Agent and complex task execution capabilities.","family":"Hy","attachment":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","high"]}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-08-28","last_updated":"2026-08-28","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":64000},"cost":{"input":0.834,"output":2.501,"cache_read":0.042},"canonical_model_id":"tencent/hy4-preview"},"meta-llama/Llama-3.3-70B-Instruct-Turbo":{"id":"meta-llama/Llama-3.3-70B-Instruct-Turbo","name":"Llama 3.3 70B Turbo","description":"Compact Llama instruction model for fast chat and local deployment","family":"llama","attachment":false,"reasoning":false,"tool_call":true,"structured_output":true,"release_date":"2024-12-06","last_updated":"2024-12-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":131072,"output":16384},"cost":{"input":0.1,"output":0.32}},"meta-llama/Llama-4-Scout-17B-16E-Instruct":{"id":"meta-llama/Llama-4-Scout-17B-16E-Instruct","name":"Llama 4 Scout 17B","description":"Open multimodal Llama model for long-context analysis and efficient agents","family":"llama","attachment":true,"reasoning":false,"tool_call":true,"structured_output":true,"release_date":"2025-04-05","last_updated":"2025-04-05","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"limit":{"context":327680,"output":16384},"cost":{"input":0.1,"output":0.3}},"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":{"id":"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8","name":"Llama 4 Maverick 17B FP8","description":"Open multimodal Llama model for strong reasoning and fast responses","family":"llama","attachment":true,"reasoning":false,"tool_call":false,"structured_output":true,"release_date":"2025-04-05","last_updated":"2025-04-05","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":16384},"status":"deprecated","cost":{"input":0.2,"output":0.8}},"XiaomiMiMo/MiMo-V2.6-Pro":{"id":"XiaomiMiMo/MiMo-V2.6-Pro","name":"MiMo-V2.6-Pro","description":"Stronger MiMo Pro tier for multimodal reasoning and coding-agent execution","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-09-22","last_updated":"2026-09-22","modalities":{"input":["text","image","audio","video"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":131072},"cost":{"input":0.43,"output":0.87,"cache_read":0.0036},"canonical_model_id":"xiaomi/mimo-v2.6-pro"},"XiaomiMiMo/MiMo-V2.5-Pro":{"id":"XiaomiMiMo/MiMo-V2.5-Pro","name":"MiMo-V2.5-Pro","description":"Stronger MiMo Pro tier for multimodal reasoning and coding-agent execution","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"interleaved":{"field":"reasoning_content"},"structured_output":true,"temperature":true,"knowledge":"2024-12","release_date":"2026-04-22","last_updated":"2026-04-22","modalities":{"input":["text","audio"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":16384},"status":"deprecated","cost":{"input":1,"output":3,"cache_read":0.2},"canonical_model_id":"xiaomi/mimo-v2.5-pro"},"XiaomiMiMo/MiMo-V2.6-Flash":{"id":"XiaomiMiMo/MiMo-V2.6-Flash","name":"MiMo-V2.6-Flash","description":"MiMo Flash model for multimodal coding agents and long-context automation","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-09-22","last_updated":"2026-09-22","modalities":{"input":["text","image","audio","video"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":131072},"cost":{"input":0.14,"output":0.28,"cache_read":0.0028},"canonical_model_id":"xiaomi/mimo-v2.6-flash"},"XiaomiMiMo/MiMo-V2.5":{"id":"XiaomiMiMo/MiMo-V2.5","name":"MiMo-V2.5","description":"Open MiMo model for multimodal coding agents and long-coLine truncated
|
||||
@@ -1,8 +1,8 @@
|
||||
import { spawn } from "node:child_process"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import { mkdir, readFile, rename } from "node:fs/promises"
|
||||
import net from "node:net"
|
||||
import path from "node:path"
|
||||
import { Data, Duration, Effect, Schema, Semaphore } from "effect"
|
||||
import { Cause, Data, Duration, Effect, Schema, Semaphore } from "effect"
|
||||
import type { Handoff } from "@opencode/schema/persistent-pty"
|
||||
|
||||
const ProtocolVersion = 7
|
||||
@@ -156,11 +156,14 @@ export interface DaemonTransport {
|
||||
}
|
||||
|
||||
export const makeDaemonTransport = Effect.fn("PersistentPty.makeDaemonTransport")(function* (
|
||||
directory: string,
|
||||
root: string,
|
||||
initialID: string,
|
||||
binary: () => Promise<string> = () => Promise.resolve(process.env.OPENCODE_PTY_BIN || "opencode-pty"),
|
||||
inherited?: Handoff,
|
||||
) {
|
||||
const startup = Semaphore.makeUnsafe(1)
|
||||
// Each daemon owns the runtime directory `<root>/<id>`.
|
||||
let id = initialID
|
||||
let registration: Registration | undefined
|
||||
let owner: Awaited<ReturnType<typeof openOwner>> | undefined
|
||||
let closed = false
|
||||
@@ -195,7 +198,7 @@ export const makeDaemonTransport = Effect.fn("PersistentPty.makeDaemonTransport"
|
||||
}),
|
||||
)
|
||||
|
||||
const discover = Effect.fn("PersistentPty.daemon.discover")(function* () {
|
||||
const discover = Effect.fn("PersistentPty.daemon.discover")(function* (directory = path.join(root, id)) {
|
||||
const value = yield* Effect.tryPromise({
|
||||
try: () => readFile(path.join(directory, "service.json"), "utf8"),
|
||||
catch: (cause) => failure("connect", cause),
|
||||
@@ -222,16 +225,16 @@ export const makeDaemonTransport = Effect.fn("PersistentPty.makeDaemonTransport"
|
||||
return decoded
|
||||
})
|
||||
|
||||
const start = Effect.fn("PersistentPty.daemon.start")(function* () {
|
||||
const spawnDaemon = Effect.fn("PersistentPty.daemon.spawn")(function* () {
|
||||
const executable = yield* Effect.tryPromise({ try: binary, catch: (cause) => failure("spawn", cause) })
|
||||
const child = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
new Promise<ReturnType<typeof spawn>>((resolve, reject) => {
|
||||
const child = spawn(executable, ["daemon"], {
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
env: { ...process.env, OPENCODE_PTY_RUNTIME_DIR: directory },
|
||||
})
|
||||
const child = spawn(
|
||||
executable,
|
||||
["daemon", "--runtime-dir", root, "--name", id],
|
||||
{ detached: true, stdio: "ignore" },
|
||||
)
|
||||
child.once("spawn", () => {
|
||||
child.unref()
|
||||
resolve(child)
|
||||
@@ -243,7 +246,7 @@ export const makeDaemonTransport = Effect.fn("PersistentPty.makeDaemonTransport"
|
||||
return yield* Effect.gen(function* () {
|
||||
const deadline = Date.now() + 5_000
|
||||
let last: DaemonError | undefined
|
||||
while (Date.now() < deadline) {
|
||||
while (Date.now() < deadline && child.exitCode === null && child.signalCode === null) {
|
||||
const found = yield* discover().pipe(
|
||||
Effect.map((value) => ({ value })),
|
||||
Effect.catch((error) => {
|
||||
@@ -269,6 +272,25 @@ export const makeDaemonTransport = Effect.fn("PersistentPty.makeDaemonTransport"
|
||||
)
|
||||
})
|
||||
|
||||
// An unreachable daemon can still hold this directory's lock, so retry once in a fresh directory.
|
||||
const start = () =>
|
||||
spawnDaemon().pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.logWarning("Starting opencode-pty in a fresh runtime directory", { cause: error.message })
|
||||
abandon()
|
||||
return yield* spawnDaemon()
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const abandon = () => {
|
||||
owner?.socket.destroy()
|
||||
owner = undefined
|
||||
registration = undefined
|
||||
id = crypto.randomUUID()
|
||||
}
|
||||
|
||||
const connect = Effect.fn("PersistentPty.daemon.connect")(function* (shouldStart: boolean) {
|
||||
if (closed) return yield* Effect.fail(new DaemonError({ kind: "connect", message: "PTY owner scope is closed" }))
|
||||
if (registration) return registration
|
||||
@@ -348,18 +370,23 @@ export const makeDaemonTransport = Effect.fn("PersistentPty.makeDaemonTransport"
|
||||
)
|
||||
if (response.type !== "handoff")
|
||||
return yield* Effect.fail(new DaemonError({ kind: "protocol", message: "Expected PTY handoff ticket" }))
|
||||
return { directory, instanceID: registered.instance_id, ticket: response.ticket, expiresAt: response.expires_at }
|
||||
return {
|
||||
directory: path.join(root, id),
|
||||
instanceID: registered.instance_id,
|
||||
ticket: response.ticket,
|
||||
expiresAt: response.expires_at,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const subscribe = Effect.fn("PersistentPty.daemon.subscribe")(function* (
|
||||
id: number,
|
||||
terminalID: number,
|
||||
input: Parameters<DaemonTransport["subscribe"]>[1],
|
||||
) {
|
||||
const attempt = Effect.gen(function* () {
|
||||
const current = yield* connect(false)
|
||||
return yield* Effect.tryPromise({
|
||||
try: () => subscribePromise(current, id, input),
|
||||
try: () => subscribePromise(current, terminalID, input),
|
||||
catch: (cause) => (cause instanceof DaemonError ? cause : failure("connect", cause)),
|
||||
}).pipe(
|
||||
Effect.catch((error) => {
|
||||
@@ -371,19 +398,51 @@ export const makeDaemonTransport = Effect.fn("PersistentPty.makeDaemonTransport"
|
||||
return yield* attempt.pipe(Effect.catch((error) => (error.kind === "registration" ? attempt : Effect.fail(error))))
|
||||
})
|
||||
|
||||
// A replacement must own its inherited daemon before the server becomes ready.
|
||||
// A replacement must own its inherited daemon before the server becomes ready. Terminals are
|
||||
// best-effort: any failure abandons the old daemon so the restart itself always succeeds.
|
||||
if (inherited) {
|
||||
if (inherited.expiresAt <= Date.now())
|
||||
return yield* Effect.fail(new DaemonError({ kind: "registration", message: "PTY restart handoff expired" }))
|
||||
const current = yield* discover()
|
||||
if (current.instance_id !== inherited.instanceID)
|
||||
return yield* Effect.fail(new DaemonError({ kind: "registration", message: "PTY restart daemon changed" }))
|
||||
yield* claim(current, inherited.ticket)
|
||||
let found: Registration | undefined
|
||||
yield* Effect.gen(function* () {
|
||||
if (inherited.expiresAt <= Date.now())
|
||||
return yield* Effect.fail(new DaemonError({ kind: "registration", message: "PTY restart handoff expired" }))
|
||||
// Older releases put the daemon directory outside the root; its directory name is still the ID.
|
||||
id = path.basename(inherited.directory)
|
||||
found = yield* discover(inherited.directory)
|
||||
if (found.instance_id !== inherited.instanceID)
|
||||
return yield* Effect.fail(new DaemonError({ kind: "registration", message: "PTY restart daemon changed" }))
|
||||
if (path.resolve(inherited.directory) !== path.resolve(root, id))
|
||||
yield* migrate(inherited.directory, path.join(root, id))
|
||||
yield* claim(found, inherited.ticket)
|
||||
}).pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.gen(function* () {
|
||||
if (Cause.hasInterruptsOnly(cause)) return yield* Effect.failCause(cause)
|
||||
yield* Effect.logWarning("Discarding PTY restart handoff; persistent terminals will start fresh", {
|
||||
cause: Cause.pretty(cause),
|
||||
})
|
||||
if (found) yield* oneShot(found, { op: "shutdown" }).pipe(Effect.ignore)
|
||||
abandon()
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return { request, requestIfRunning, shutdown, handoff, subscribe } satisfies DaemonTransport
|
||||
})
|
||||
|
||||
// Older releases kept registrations in purgeable temp directories. Renaming keeps the lock file's
|
||||
// inode, so the still-running daemon keeps holding its lock in the new location.
|
||||
const migrate = Effect.fn("PersistentPty.daemon.migrate")(function* (directory: string, target: string) {
|
||||
yield* Effect.tryPromise({
|
||||
try: async () => {
|
||||
await mkdir(target, { recursive: true, mode: 0o700 })
|
||||
await rename(path.join(directory, "service.lock"), path.join(target, "service.lock"))
|
||||
await rename(path.join(directory, "service.json"), path.join(target, "service.json"))
|
||||
},
|
||||
catch: (cause) => failure("registration", cause),
|
||||
})
|
||||
})
|
||||
|
||||
async function openOwner(registration: Registration, ticket: string | undefined, signal: AbortSignal) {
|
||||
const socket = net.createConnection({ path: registration.socket })
|
||||
const frames = decoder(socket)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export * as PersistentPty from "./index.js"
|
||||
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { makeGlobalNode } from "@opencode/util/effect/app-node"
|
||||
@@ -24,7 +23,11 @@ export type { Role, StreamEvent } from "./daemon.js"
|
||||
export { Handoff } from "@opencode/schema/persistent-pty"
|
||||
export { available } from "#persistent-pty-binary"
|
||||
|
||||
export const Options = Schema.Struct({ handoff: Schema.optional(Handoff) })
|
||||
export const Options = Schema.Struct({
|
||||
handoff: Schema.optional(Handoff),
|
||||
/** Parent of per-server daemon runtime directories; defaults to `<state>/pty`. */
|
||||
root: Schema.optional(Schema.String),
|
||||
})
|
||||
export type Options = typeof Options.Type
|
||||
|
||||
export type Info = PersistentPty.Info
|
||||
@@ -123,7 +126,8 @@ const makeLayer = (options: Options = {}) =>
|
||||
const runFork = Effect.runForkWith(context)
|
||||
let binary: Promise<string> | undefined
|
||||
const daemon = yield* makeDaemonTransport(
|
||||
options.handoff?.directory ?? runtimeDirectory(),
|
||||
options.root ?? path.join(global.state, "pty"),
|
||||
crypto.randomUUID(),
|
||||
() =>
|
||||
(binary ??= resolveBinary(global.bin).catch((error) => {
|
||||
binary = undefined
|
||||
@@ -383,18 +387,6 @@ const unexpected = (response: WireResponse) =>
|
||||
const unavailable = (error: unknown) =>
|
||||
new UnavailableError({ message: error instanceof Error ? error.message : String(error) })
|
||||
|
||||
const runtimeDirectory = () => {
|
||||
const root =
|
||||
process.env.OPENCODE_PTY_RUNTIME_DIR ??
|
||||
(process.env.XDG_RUNTIME_DIR
|
||||
? path.join(process.env.XDG_RUNTIME_DIR, "opencode-pty")
|
||||
: path.join(
|
||||
os.tmpdir(),
|
||||
`opencode-pty-${typeof process.getuid === "function" ? process.getuid() : process.env.USER || "unknown"}`,
|
||||
))
|
||||
return path.join(root, crypto.randomUUID())
|
||||
}
|
||||
|
||||
function toInfo(value: WireTerminal): Info {
|
||||
const status = value.lifecycle.status
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { and, desc, eq } from "drizzle-orm"
|
||||
import { Project } from "./project.js"
|
||||
import { Model } from "@opencode/schema/model"
|
||||
import { Location } from "./location.js"
|
||||
import type { FileSystem } from "./filesystem.js"
|
||||
import { SessionMessage } from "./session/message.js"
|
||||
import { PromptInput } from "@opencode/schema/prompt-input"
|
||||
import { Bus } from "./bus.js"
|
||||
@@ -145,7 +146,10 @@ export interface Interface {
|
||||
readonly from?: SessionMessage.ID
|
||||
readonly to?: SessionMessage.ID
|
||||
readonly context?: number
|
||||
}) => Effect.Effect<readonly FileDiff.Info[], NotFoundError | MessageNotFoundError | TurnRangeError | Snapshot.Error>
|
||||
}) => Effect.Effect<
|
||||
readonly FileDiff.Info[],
|
||||
NotFoundError | MessageNotFoundError | TurnRangeError | Snapshot.Error | FileSystem.DirectoryNotFoundError
|
||||
>
|
||||
/**
|
||||
* Durable admitted session work not yet visible in projected history,
|
||||
* ordered by admission. Includes unpromoted user and synthetic inputs and
|
||||
@@ -195,7 +199,10 @@ export interface Interface {
|
||||
agents?: PromptInput.Prompt["agents"]
|
||||
skills?: PromptInput.Prompt["skills"]
|
||||
delivery?: SessionInbox.Delivery
|
||||
}) => Effect.Effect<void, NotFoundError | Command.NotFoundError | Command.ExecutionError>
|
||||
}) => Effect.Effect<
|
||||
void,
|
||||
NotFoundError | Command.NotFoundError | Command.ExecutionError | FileSystem.DirectoryNotFoundError
|
||||
>
|
||||
readonly shell: (
|
||||
input: Parameters<Session.Handle["shell"]>[0] & { sessionID: SessionSchema.ID },
|
||||
) => ReturnType<Session.Handle["shell"]>
|
||||
@@ -218,8 +225,13 @@ export interface Interface {
|
||||
sessionID: SessionSchema.ID
|
||||
messageID: SessionMessage.ID
|
||||
files?: boolean
|
||||
}) => Effect.Effect<SessionSchema.Revert, NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error>
|
||||
readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError | Snapshot.Error>
|
||||
}) => Effect.Effect<
|
||||
SessionSchema.Revert,
|
||||
NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error | FileSystem.DirectoryNotFoundError
|
||||
>
|
||||
readonly clear: (
|
||||
sessionID: SessionSchema.ID,
|
||||
) => Effect.Effect<void, NotFoundError | BusyError | Snapshot.Error | FileSystem.DirectoryNotFoundError>
|
||||
readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * as SessionGenerate from "./generate.js"
|
||||
|
||||
import type { FileSystem } from "../filesystem.js"
|
||||
import { LLMClient, Message, type AIError } from "@opencode/ai"
|
||||
import { Effect } from "effect"
|
||||
import { Database } from "../database/database.js"
|
||||
@@ -14,7 +15,12 @@ import { SessionModelRequest } from "./model-request.js"
|
||||
import type { SessionRunnerModel } from "./runner/model.js"
|
||||
import type { SessionSchema } from "./schema.js"
|
||||
|
||||
export type Error = AgentNotFoundError | Instructions.InitializationBlocked | SessionRunnerModel.Error | AIError
|
||||
export type Error =
|
||||
| AgentNotFoundError
|
||||
| Instructions.InitializationBlocked
|
||||
| SessionRunnerModel.Error
|
||||
| AIError
|
||||
| FileSystem.DirectoryNotFoundError
|
||||
|
||||
/** Generates text from current Session context without mutating the Session. */
|
||||
export const generate = Effect.fn("SessionGenerate.generate")(function* (input: {
|
||||
|
||||
@@ -91,7 +91,8 @@ const layer = Layer.effect(
|
||||
yield* locations.contextEffect(destination.location).pipe(
|
||||
Effect.scoped,
|
||||
Effect.catchCause((cause) => {
|
||||
if (Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause)
|
||||
if (Cause.hasInterruptsOnly(cause))
|
||||
return Effect.failCause(Cause.fromReasons<never>(cause.reasons.filter(Cause.isInterruptReason)))
|
||||
return Effect.logWarning("session move destination unavailable", { directory, cause }).pipe(
|
||||
Effect.andThen(Effect.fail(new DestinationUnavailableError({ directory }))),
|
||||
)
|
||||
@@ -106,7 +107,11 @@ const layer = Layer.effect(
|
||||
return yield* SessionRunner.Service.pipe(
|
||||
instances.provide(session),
|
||||
Effect.as(false),
|
||||
Effect.catchCause((cause) => (Cause.hasInterrupts(cause) ? Effect.failCause(cause) : Effect.succeed(true))),
|
||||
Effect.catchCause((cause) =>
|
||||
Cause.hasInterrupts(cause)
|
||||
? Effect.failCause(Cause.fromReasons<never>(cause.reasons.filter(Cause.isInterruptReason)))
|
||||
: Effect.succeed(true),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * as SessionRunner from "./index.js"
|
||||
|
||||
import type { FileSystem } from "../../filesystem.js"
|
||||
import type { AIError } from "@opencode/ai"
|
||||
import { Context, Data, Effect } from "effect"
|
||||
import { SessionSchema } from "../schema.js"
|
||||
@@ -15,6 +16,7 @@ export type RunError =
|
||||
| AgentNotFoundError
|
||||
| StepFailedError
|
||||
| Instructions.InitializationBlocked
|
||||
| FileSystem.DirectoryNotFoundError
|
||||
|
||||
export type Continuation = { readonly step: number }
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
export * as ConfigMigrateV1 from "./migrate.js"
|
||||
|
||||
import { Info } from "@opencode/schema/config"
|
||||
import { ConfigAgent } from "@opencode/schema/config/agent"
|
||||
import { Schema } from "effect"
|
||||
import { ConfigV1 } from "./config.js"
|
||||
import { ConfigAgentV1 } from "./agent.js"
|
||||
import { ConfigCommandV1 } from "./command.js"
|
||||
import { ConfigMCPV1 } from "./mcp.js"
|
||||
@@ -14,102 +12,16 @@ import { Provider } from "../../provider.js"
|
||||
import { Model } from "../../model.js"
|
||||
|
||||
const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
|
||||
const decodeInfo = Schema.decodeUnknownSync(Schema.fromJsonString(Info), decodeOptions)
|
||||
const encodeInfo = Schema.encodeSync(Info)
|
||||
const decodeAgent = Schema.decodeUnknownSync(Schema.fromJsonString(ConfigAgent.Info), decodeOptions)
|
||||
const encodeAgent = Schema.encodeSync(ConfigAgent.Info)
|
||||
export function migrate(info: typeof ConfigV1.Info.Type) {
|
||||
return encodeInfo(
|
||||
decodeInfo(
|
||||
JSON.stringify({
|
||||
$schema: info.$schema,
|
||||
shell: info.shell,
|
||||
model: modelSelection(info.model),
|
||||
default_agent: info.default_agent,
|
||||
update:
|
||||
info.autoupdate === false
|
||||
? "disable"
|
||||
: info.autoupdate === "notify"
|
||||
? "notify"
|
||||
: info.autoupdate === true
|
||||
? "auto"
|
||||
: undefined,
|
||||
share: info.share ?? (info.autoshare ? "auto" : undefined),
|
||||
enterprise: info.enterprise,
|
||||
username: info.username,
|
||||
permissions: permissions(info.permission, info.tools),
|
||||
agents: agents(info),
|
||||
snapshots: info.snapshot,
|
||||
watcher: info.watcher,
|
||||
formatter: info.formatter,
|
||||
lsp: info.lsp,
|
||||
media: info.attachment,
|
||||
tool_output: info.tool_output,
|
||||
mcp: mcp(info),
|
||||
compaction: info.compaction && {
|
||||
auto: info.compaction.auto,
|
||||
prune: info.compaction.prune,
|
||||
keep: {
|
||||
tokens: info.compaction.preserve_recent_tokens,
|
||||
},
|
||||
buffer: info.compaction.reserved,
|
||||
},
|
||||
skills: info.skills && [...(info.skills.paths ?? []), ...(info.skills.urls ?? [])],
|
||||
commands: commands(info.command),
|
||||
instructions: info.instructions,
|
||||
references: info.references ?? info.reference,
|
||||
experimental: experimental(info),
|
||||
plugins: info.plugin?.map((plugin) =>
|
||||
typeof plugin === "string" ? plugin : { package: plugin[0], options: plugin[1] },
|
||||
),
|
||||
providers: providers(info.provider),
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function experimental(info: typeof ConfigV1.Info.Type) {
|
||||
const policies = [
|
||||
...(info.enabled_providers === undefined
|
||||
? []
|
||||
: [
|
||||
{ action: "provider.use" as const, resource: "*", effect: "deny" as const },
|
||||
...info.enabled_providers.map((resource) => ({
|
||||
action: "provider.use" as const,
|
||||
resource: providerID(resource),
|
||||
effect: "allow" as const,
|
||||
})),
|
||||
]),
|
||||
...(info.disabled_providers ?? []).map((resource) => ({
|
||||
action: "provider.use" as const,
|
||||
resource: providerID(resource),
|
||||
effect: "deny" as const,
|
||||
})),
|
||||
]
|
||||
if (info.experimental?.subagent_depth === undefined && !policies.length) return
|
||||
return {
|
||||
subagent_depth: info.experimental?.subagent_depth,
|
||||
policies: policies.length ? policies : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function permissions(info?: ConfigPermissionV1.Info, tools?: Readonly<Record<string, boolean>>) {
|
||||
const rules: Array<{ action: string; resource: string; effect: ConfigPermissionV1.Action }> = Object.entries(
|
||||
tools ?? {},
|
||||
).map(([action, enabled]) => ({
|
||||
action: normalizeAction(action),
|
||||
resource: "*",
|
||||
effect: enabled ? ("allow" as const) : ("deny" as const),
|
||||
}))
|
||||
for (const [key, rule] of Object.entries(info ?? {})) {
|
||||
if (!rule) continue
|
||||
function permissions(info?: ConfigPermissionV1.Info) {
|
||||
const rules = Object.entries(info ?? {}).flatMap(([key, rule]) => {
|
||||
if (!rule) return []
|
||||
const action = normalizeAction(key)
|
||||
if (typeof rule === "string") {
|
||||
rules.push({ action, resource: "*", effect: rule })
|
||||
continue
|
||||
}
|
||||
rules.push(...Object.entries(rule).map(([resource, effect]) => ({ action, resource, effect })))
|
||||
}
|
||||
if (typeof rule === "string") return [{ action, resource: "*", effect: rule }]
|
||||
return Object.entries(rule).map(([resource, effect]) => ({ action, resource, effect }))
|
||||
})
|
||||
return rules.length ? rules : undefined
|
||||
}
|
||||
|
||||
@@ -121,23 +33,6 @@ export function normalizeAction(action: string) {
|
||||
return action
|
||||
}
|
||||
|
||||
function agents(info: typeof ConfigV1.Info.Type) {
|
||||
const entries = [
|
||||
...Object.entries(info.agent ?? {}),
|
||||
...Object.entries(info.mode ?? {}).map(([name, agent]) => [name, { ...agent, mode: "primary" as const }] as const),
|
||||
]
|
||||
const result = Object.fromEntries(entries.flatMap(([name, agent]) => (agent ? [[name, migrateAgent(agent)]] : [])))
|
||||
const small = modelSelection(info.small_model)
|
||||
if (!small) return entries.length ? result : undefined
|
||||
return {
|
||||
...result,
|
||||
title: {
|
||||
model: small,
|
||||
...result.title,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function migrateAgent(info: ConfigAgentV1.Info) {
|
||||
const body = {
|
||||
...info.options,
|
||||
@@ -162,23 +57,17 @@ export function migrateAgent(info: ConfigAgentV1.Info) {
|
||||
)
|
||||
}
|
||||
|
||||
export function commands(info?: Readonly<Record<string, ConfigCommandV1.Info>>) {
|
||||
if (!info) return undefined
|
||||
return Object.fromEntries(
|
||||
Object.entries(info).map(([id, command]) => [
|
||||
id,
|
||||
{
|
||||
template: command.template,
|
||||
description: command.description,
|
||||
agent: command.agent,
|
||||
model: modelSelection(command.model, command.variant),
|
||||
subagent: command.subtask,
|
||||
},
|
||||
]),
|
||||
)
|
||||
export function migrateCommand(command: ConfigCommandV1.Info) {
|
||||
return {
|
||||
template: command.template,
|
||||
description: command.description,
|
||||
agent: command.agent,
|
||||
model: modelSelection(command.model, command.variant),
|
||||
subagent: command.subtask,
|
||||
}
|
||||
}
|
||||
|
||||
function modelSelection(input?: string, variant?: string) {
|
||||
export function modelSelection(input?: string, variant?: string) {
|
||||
if (input === undefined || !/^[^/#]+\/[^#]+$/.test(input)) return undefined
|
||||
const separator = input.indexOf("/")
|
||||
return {
|
||||
@@ -188,17 +77,6 @@ function modelSelection(input?: string, variant?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function mcp(info: typeof ConfigV1.Info.Type) {
|
||||
const servers = Object.fromEntries(
|
||||
Object.entries(info.mcp ?? {}).flatMap(([name, server]) =>
|
||||
"type" in server ? [[name, migrateMcp(server)] as const] : [],
|
||||
),
|
||||
)
|
||||
const timeout = info.experimental?.mcp_timeout
|
||||
if (!timeout && !Object.keys(servers).length) return undefined
|
||||
return { timeout: timeout === undefined ? undefined : { catalog: timeout, execution: timeout }, servers }
|
||||
}
|
||||
|
||||
export function migrateMcp(info: ConfigMCPV1.Info) {
|
||||
const disabled = info.enabled === undefined ? undefined : !info.enabled
|
||||
if (info.type === "local")
|
||||
@@ -226,18 +104,6 @@ export function migrateMcp(info: ConfigMCPV1.Info) {
|
||||
}
|
||||
}
|
||||
|
||||
function providers(info?: Readonly<Record<string, ConfigProviderV1.Info>>) {
|
||||
if (!info) return undefined
|
||||
return Object.fromEntries(
|
||||
Object.entries(info).flatMap(([name, provider]) => {
|
||||
const id = providerID(name)
|
||||
// If both names are present, keep the settings under the current name and ignore the old one.
|
||||
if (id !== name && info[id]) return []
|
||||
return [[id, migrateProvider(name, provider)]]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function migrateProvider(sourceID: string, info: ConfigProviderV1.Info) {
|
||||
if (sourceID === "azure-cognitive-services") return migrateAzureCognitiveServicesProvider(info)
|
||||
if (sourceID === "google-vertex-anthropic") return migrateGoogleVertexAnthropicProvider(info)
|
||||
@@ -294,7 +160,7 @@ function migrateGoogleVertexAnthropicProvider(info: ConfigProviderV1.Info) {
|
||||
}
|
||||
}
|
||||
|
||||
// Rename these only while migrating unambiguous V1 fields.
|
||||
// Renames retired provider IDs in V1 fields and the shared top-level model.
|
||||
export function providerID(input: string) {
|
||||
if (input === "azure-cognitive-services") return "azure"
|
||||
if (input === "google-vertex-anthropic") return "google-vertex"
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Slug } from "./util/slug.js"
|
||||
import { Bus } from "./bus.js"
|
||||
import { Database } from "./database/database.js"
|
||||
import { Location } from "./location.js"
|
||||
import type { FileSystem } from "./filesystem.js"
|
||||
import { LocationServiceMap } from "./location-service-map.js"
|
||||
import { Project } from "./project.js"
|
||||
import { Worktree } from "@opencode/schema/worktree"
|
||||
@@ -67,6 +68,7 @@ export class StrategyUnavailableError extends Schema.TaggedError<StrategyUnavail
|
||||
) {}
|
||||
|
||||
export type Error =
|
||||
| FileSystem.DirectoryNotFoundError
|
||||
| Project.NotFoundError
|
||||
| SourceDirectoryNotFoundError
|
||||
| DestinationExistsError
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Global } from "@opencode/util/global"
|
||||
import { Permission } from "@opencode/core/permission"
|
||||
import { AgentPlugin } from "@opencode/core/plugin/agent"
|
||||
import { AbsolutePath } from "@opencode/core/schema"
|
||||
import { ConfigMigrateV1 } from "@opencode/core/v1/config/migrate"
|
||||
import { ConfigNormalize } from "@opencode/core/config/normalize"
|
||||
import { ConfigAgentV1 } from "@opencode/core/v1/config/agent"
|
||||
import { advance, drain } from "../lib/clock"
|
||||
import { tmpdir, tmpdirScoped } from "../fixture/tmpdir"
|
||||
@@ -24,6 +24,12 @@ import { agentHost, host } from "../plugin/host"
|
||||
|
||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Agent.node, Bus.node, FSUtil.node, Global.node])))
|
||||
const decode = Schema.decodeUnknownSync(Info)
|
||||
|
||||
function migrateV1(input: unknown) {
|
||||
const result = ConfigNormalize.normalize(input)
|
||||
if (result.type !== "normalized") throw new Error("expected normalized config")
|
||||
return result.encoded
|
||||
}
|
||||
const defaultPermissions = (global: Global.Interface): Permission.Ruleset => [
|
||||
...Agent.Info.default(Agent.ID.make("test")).permissions,
|
||||
{ action: "external_directory", resource: path.join(global.data, "shell", "*", "*"), effect: "allow" },
|
||||
@@ -171,7 +177,7 @@ permissions:
|
||||
new Document({
|
||||
type: "document",
|
||||
info: decode(
|
||||
ConfigMigrateV1.migrate({
|
||||
migrateV1({
|
||||
permission: {
|
||||
bash: "ask",
|
||||
edit: "ask",
|
||||
@@ -752,7 +758,7 @@ function loadHomePermissions(home: string) {
|
||||
new Document({
|
||||
type: "document",
|
||||
info: decode(
|
||||
ConfigMigrateV1.migrate({
|
||||
migrateV1({
|
||||
permission: {
|
||||
external_directory: {
|
||||
"~/p/**": "allow",
|
||||
|
||||
@@ -11,8 +11,7 @@ import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
|
||||
import { makeGlobalNode } from "@opencode/util/effect/app-node"
|
||||
import { LayerNode } from "@opencode/util/effect/layer-node"
|
||||
import { Credential } from "@opencode/core/credential"
|
||||
import { ConfigMigrateV1 } from "@opencode/core/v1/config/migrate"
|
||||
import { ConfigV1 } from "@opencode/core/v1/config/config"
|
||||
import { ConfigV1 } from "../fixture/v1-config/config"
|
||||
import { ConfigNormalize } from "@opencode/core/config/normalize"
|
||||
import { Watcher } from "@opencode/core/filesystem/watcher"
|
||||
import { Bus } from "@opencode/core/bus"
|
||||
@@ -30,6 +29,18 @@ import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(Layer.empty)
|
||||
const selection = Schema.decodeUnknownSync(ConfigModel.Selection)
|
||||
const decodeInfo = Schema.decodeUnknownSync(Info, {
|
||||
errors: "all",
|
||||
onExcessProperty: "ignore",
|
||||
propertyOrder: "original",
|
||||
})
|
||||
const encodeInfo = Schema.encodeSync(Info)
|
||||
|
||||
function migrateV1(input: unknown) {
|
||||
const result = ConfigNormalize.normalize(input)
|
||||
if (result.type !== "normalized") throw new Error("expected normalized config")
|
||||
return encodeInfo(decodeInfo(result.encoded))
|
||||
}
|
||||
|
||||
function inFixture(root: string, target: string) {
|
||||
const relative = path.relative(root, target)
|
||||
@@ -634,38 +645,21 @@ describe("Config", () => {
|
||||
test("migrates arbitrary v1 configuration into valid v2 configuration", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(Schema.toArbitrary(ConfigV1.Info)(FastCheck), (info) => {
|
||||
const parsed = Schema.decodeUnknownSync(ConfigV1.Info)(
|
||||
Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown))(
|
||||
Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown))(info),
|
||||
),
|
||||
)
|
||||
Schema.decodeUnknownSync(Info)(ConfigMigrateV1.migrate(parsed), { errors: "all" })
|
||||
migrateV1(JSON.parse(JSON.stringify(info)))
|
||||
}),
|
||||
{ numRuns: 100 },
|
||||
)
|
||||
}, 30_000)
|
||||
|
||||
test("migrates the v1 experimental subagent depth", () => {
|
||||
expect(ConfigMigrateV1.migrate({ experimental: { subagent_depth: 2 } }).experimental?.subagent_depth).toBe(2)
|
||||
})
|
||||
|
||||
test("migrates the v1 small model to the title agent", () => {
|
||||
expect(
|
||||
ConfigMigrateV1.migrate({
|
||||
small_model: "anthropic/claude-haiku-4-5",
|
||||
agent: { title: { prompt: "Custom title prompt" } },
|
||||
}).agents?.title,
|
||||
).toEqual({
|
||||
model: { providerID: "anthropic", model: "claude-haiku-4-5" },
|
||||
system: "Custom title prompt",
|
||||
})
|
||||
expect(migrateV1({ experimental: { subagent_depth: 2 } }).experimental?.subagent_depth).toBe(2)
|
||||
})
|
||||
|
||||
test("migrates the v1 update policy", () => {
|
||||
expect(ConfigMigrateV1.migrate({ autoupdate: false }).update).toBe("disable")
|
||||
expect(ConfigMigrateV1.migrate({ autoupdate: "notify" }).update).toBe("notify")
|
||||
expect(ConfigMigrateV1.migrate({ autoupdate: true }).update).toBe("auto")
|
||||
expect(ConfigMigrateV1.migrate({}).update).toBeUndefined()
|
||||
expect(migrateV1({ autoupdate: false }).update).toBe("disable")
|
||||
expect(migrateV1({ autoupdate: "notify" }).update).toBe("notify")
|
||||
expect(migrateV1({ autoupdate: true }).update).toBe("auto")
|
||||
expect(migrateV1({}).update).toBeUndefined()
|
||||
})
|
||||
|
||||
test("normalizes the native auto update policy", () => {
|
||||
@@ -678,7 +672,7 @@ describe("Config", () => {
|
||||
|
||||
test("migrates v1 provider lists to policies", () => {
|
||||
expect(
|
||||
ConfigMigrateV1.migrate({
|
||||
migrateV1({
|
||||
enabled_providers: ["anthropic", "openai"],
|
||||
disabled_providers: ["openai"],
|
||||
}).experimental?.policies,
|
||||
@@ -688,13 +682,13 @@ describe("Config", () => {
|
||||
{ action: "provider.use", resource: "openai", effect: "allow" },
|
||||
{ action: "provider.use", resource: "openai", effect: "deny" },
|
||||
])
|
||||
expect(ConfigMigrateV1.migrate({ enabled_providers: [] }).experimental?.policies).toEqual([
|
||||
expect(migrateV1({ enabled_providers: [] }).experimental?.policies).toEqual([
|
||||
{ action: "provider.use", resource: "*", effect: "deny" },
|
||||
])
|
||||
})
|
||||
|
||||
test("migrates v1 provider setup options into AISDK settings", () => {
|
||||
const migrated = ConfigMigrateV1.migrate({
|
||||
const migrated = migrateV1({
|
||||
provider: {
|
||||
bedrock: {
|
||||
npm: "@ai-sdk/amazon-bedrock",
|
||||
@@ -719,7 +713,7 @@ describe("Config", () => {
|
||||
})
|
||||
|
||||
test("renames old provider IDs while migrating v1 configuration", () => {
|
||||
const migrated = ConfigMigrateV1.migrate({
|
||||
const migrated = migrateV1({
|
||||
model: "azure-cognitive-services/deployment",
|
||||
enabled_providers: ["google-vertex-anthropic"],
|
||||
disabled_providers: ["azure-cognitive-services"],
|
||||
@@ -768,7 +762,7 @@ describe("Config", () => {
|
||||
})
|
||||
|
||||
test("preserves the generated base URL for v1 Azure OpenAI-compatible providers", () => {
|
||||
const migrated = ConfigMigrateV1.migrate({
|
||||
const migrated = migrateV1({
|
||||
provider: {
|
||||
"azure-cognitive-services": {
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
@@ -787,7 +781,7 @@ describe("Config", () => {
|
||||
})
|
||||
|
||||
test("ignores old provider IDs when the current provider ID is configured", () => {
|
||||
const migrated = ConfigMigrateV1.migrate({
|
||||
const migrated = migrateV1({
|
||||
provider: {
|
||||
azure: { models: { current: {} } },
|
||||
"azure-cognitive-services": { models: { legacy: {} } },
|
||||
@@ -801,7 +795,7 @@ describe("Config", () => {
|
||||
})
|
||||
|
||||
test("preserves the built-in package for v1 Vertex Anthropic custom models", () => {
|
||||
const migrated = ConfigMigrateV1.migrate({
|
||||
const migrated = migrateV1({
|
||||
provider: {
|
||||
"google-vertex-anthropic": {
|
||||
models: { claude: {} },
|
||||
@@ -816,7 +810,7 @@ describe("Config", () => {
|
||||
})
|
||||
|
||||
test("migrates v1 interleaved fields to compatibility", () => {
|
||||
const migrated = ConfigMigrateV1.migrate({
|
||||
const migrated = migrateV1({
|
||||
provider: {
|
||||
custom: {
|
||||
models: {
|
||||
@@ -838,7 +832,7 @@ describe("Config", () => {
|
||||
for (const subtask of [true, false]) {
|
||||
test(`migrates v1 command configuration with subtask: ${subtask}`, () => {
|
||||
expect(
|
||||
ConfigMigrateV1.migrate({
|
||||
migrateV1({
|
||||
command: {
|
||||
review: {
|
||||
template: "Review changes",
|
||||
@@ -864,7 +858,7 @@ describe("Config", () => {
|
||||
|
||||
test("normalizes renamed permission actions when migrating v1 permissions", () => {
|
||||
expect(
|
||||
ConfigMigrateV1.migrate({
|
||||
migrateV1({
|
||||
permission: {
|
||||
task: "ask",
|
||||
bash: { "git status": "allow", "*": "deny" },
|
||||
|
||||
@@ -149,6 +149,21 @@ describe("ConfigNormalize", () => {
|
||||
expect(() => Schema.decodeUnknownSync(Info)(result.encoded)).not.toThrow()
|
||||
})
|
||||
|
||||
test("renames legacy provider IDs in the top-level model", () => {
|
||||
expect(normalized({ model: "google-vertex-anthropic/claude#high" }).encoded.model).toEqual({
|
||||
providerID: "google-vertex",
|
||||
model: "claude",
|
||||
variant: "high",
|
||||
})
|
||||
expect(
|
||||
normalized({ model: { providerID: "azure-cognitive-services", model: "deployment" } }).encoded.model,
|
||||
).toEqual({ providerID: "azure", model: "deployment" })
|
||||
expect(normalized({ model: "anthropic/claude" }).encoded.model).toEqual({
|
||||
providerID: "anthropic",
|
||||
model: "claude",
|
||||
})
|
||||
})
|
||||
|
||||
test("migrates the legacy small model to the title agent", () => {
|
||||
const result = normalized({ small_model: "anthropic/claude-haiku-4-5" })
|
||||
expect(result.encoded.agents).toEqual({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FileSystem } from "@opencode/core/filesystem"
|
||||
import { Bus } from "@opencode/core/bus"
|
||||
import { Image } from "@opencode/core/image"
|
||||
import { LocationServiceMap } from "@opencode/core/location-service-map"
|
||||
@@ -24,7 +25,7 @@ export const promptLocationNode = makeGlobalNode({
|
||||
Bus.node.replace(Layer.succeed(Bus.Service, bus)),
|
||||
Plugin.node.replace(Layer.mock(Plugin.Service, { awaitActivation: Effect.void })),
|
||||
],
|
||||
}) as Layer.Layer<LocationServices>,
|
||||
}) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
|
||||
)
|
||||
}),
|
||||
),
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
export * as ConfigAttachmentV1 from "./attachment.js"
|
||||
export * as ConfigAttachmentV1 from "./attachment"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { PositiveInt } from "../../schema.js"
|
||||
import { PositiveInt } from "@opencode/core/schema"
|
||||
|
||||
export const Image = Schema.Struct({
|
||||
auto_resize: Schema.optional(Schema.Boolean).annotate({
|
||||
+14
-23
@@ -1,27 +1,20 @@
|
||||
export * as ConfigV1 from "./config.js"
|
||||
export * as ConfigV1 from "./config"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { ConfigReference } from "@opencode/schema/config/reference"
|
||||
import { NonNegativeInt, PositiveInt, type DeepMutable } from "../../schema.js"
|
||||
import { ConfigAgentV1 } from "./agent.js"
|
||||
import { ConfigAttachmentV1 } from "./attachment.js"
|
||||
import { ConfigCommandV1 } from "./command.js"
|
||||
import { ConfigFormatterV1 } from "./formatter.js"
|
||||
import { ConfigLayoutV1 } from "./layout.js"
|
||||
import { ConfigLSPV1 } from "./lsp.js"
|
||||
import { ConfigMCPV1 } from "./mcp.js"
|
||||
import { ConfigPermissionV1 } from "./permission.js"
|
||||
import { ConfigPluginV1 } from "./plugin.js"
|
||||
import { ConfigProviderV1 } from "./provider.js"
|
||||
import { ConfigServerV1 } from "./server.js"
|
||||
import { ConfigSkillsV1 } from "./skills.js"
|
||||
|
||||
export type Layout = ConfigLayoutV1.Layout
|
||||
|
||||
export const WellKnown = Schema.Struct({
|
||||
config: Schema.optional(Schema.Json),
|
||||
remote_config: Schema.optional(Schema.Json),
|
||||
})
|
||||
import { NonNegativeInt, PositiveInt } from "@opencode/core/schema"
|
||||
import { ConfigAgentV1 } from "@opencode/core/v1/config/agent"
|
||||
import { ConfigAttachmentV1 } from "./attachment"
|
||||
import { ConfigCommandV1 } from "@opencode/core/v1/config/command"
|
||||
import { ConfigFormatterV1 } from "./formatter"
|
||||
import { ConfigLayoutV1 } from "./layout"
|
||||
import { ConfigLSPV1 } from "./lsp"
|
||||
import { ConfigMCPV1 } from "@opencode/core/v1/config/mcp"
|
||||
import { ConfigPermissionV1 } from "@opencode/core/v1/config/permission"
|
||||
import { ConfigPluginV1 } from "@opencode/core/v1/config/plugin"
|
||||
import { ConfigProviderV1 } from "@opencode/core/v1/config/provider"
|
||||
import { ConfigServerV1 } from "./server"
|
||||
import { ConfigSkillsV1 } from "./skills"
|
||||
|
||||
const LogLevelRef = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({
|
||||
identifier: "LogLevel",
|
||||
@@ -187,5 +180,3 @@ export const Info = Schema.Struct({
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "Config" })
|
||||
|
||||
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export * as ConfigFormatterV1 from "./formatter.js"
|
||||
export * as ConfigFormatterV1 from "./formatter"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export * as ConfigLayoutV1 from "./layout.js"
|
||||
export * as ConfigLayoutV1 from "./layout"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as ConfigLSPV1 from "./lsp.js"
|
||||
export * as ConfigLSPV1 from "./lsp"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
export * as ConfigServerV1 from "./server.js"
|
||||
export * as ConfigServerV1 from "./server"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { PositiveInt } from "../../schema.js"
|
||||
import { PositiveInt } from "@opencode/core/schema"
|
||||
|
||||
export const Server = Schema.Struct({
|
||||
port: Schema.optional(PositiveInt).annotate({
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export * as ConfigSkillsV1 from "./skills.js"
|
||||
export * as ConfigSkillsV1 from "./skills"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FileSystem } from "@opencode/core/filesystem"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Context, Deferred, Duration, Effect, Fiber, Layer, LayerMap, RcMap, Schema } from "effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
@@ -65,7 +66,7 @@ const locations = Layer.effect(
|
||||
Layer.provideMerge(Form.layer),
|
||||
Layer.provide(Layer.succeed(Bus.Service, bus)),
|
||||
Layer.fresh,
|
||||
) as unknown as Layer.Layer<LocationServices>,
|
||||
) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
|
||||
{ idleTimeToLive: Duration.infinity },
|
||||
)
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FileSystem } from "@opencode/core/filesystem"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { describe, expect } from "bun:test"
|
||||
@@ -67,7 +68,7 @@ const activityLocations = Layer.effect(
|
||||
workspaceID: ref.workspaceID,
|
||||
project: { id: Project.ID.global, directory: ref.directory, canonical: ref.directory },
|
||||
}),
|
||||
) as unknown as Layer.Layer<LocationServices>,
|
||||
) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
|
||||
{ idleTimeToLive: Duration.infinity },
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1048,7 +1048,7 @@ for (const entry of [
|
||||
{ name: "second 400", status: 400, query: "", codemode: undefined, attempts: 2 },
|
||||
{ name: "401", status: 401, query: "", codemode: undefined, attempts: 1 },
|
||||
{ name: "403", status: 403, query: "", codemode: undefined, attempts: 1 },
|
||||
{ name: "500", status: 500, query: "", codemode: undefined, attempts: 1 },
|
||||
{ name: "501", status: 501, query: "", codemode: undefined, attempts: 1 },
|
||||
{ name: "user codemode=true", status: 404, query: "?codemode=true", codemode: undefined, attempts: 1 },
|
||||
{ name: "user codemode=false", status: 404, query: "?codemode=false", codemode: undefined, attempts: 1 },
|
||||
{ name: "empty user codemode", status: 404, query: "?codemode=", codemode: undefined, attempts: 1 },
|
||||
|
||||
@@ -279,7 +279,7 @@ describe("Npm.add", () => {
|
||||
const result = await Effect.gen(function* () {
|
||||
const npm = yield* Npm.Service
|
||||
const mutableEntry = yield* npm.add(mutable)
|
||||
const pinnedEntry = yield* npm.add(pinned)
|
||||
yield* npm.add(pinned)
|
||||
yield* Effect.promise(async () => {
|
||||
await Bun.write(path.join(fixture.repository, "index.js"), 'export default { root: "second" }\n')
|
||||
await Bun.$`git -C ${fixture.repository} add .`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect } from "bun:test"
|
||||
import { spawn } from "node:child_process"
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises"
|
||||
import { existsSync } from "node:fs"
|
||||
import { mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises"
|
||||
import net from "node:net"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
@@ -12,7 +13,7 @@ const pong = { type: "pong", instance_id: "test", pid: process.pid, protocol: 7
|
||||
|
||||
it.live("rediscovers a same-protocol daemon after its registration rotates", () =>
|
||||
Effect.gen(function* () {
|
||||
const directory = yield* temporaryDirectory()
|
||||
const { root, directory } = yield* temporaryRuntime()
|
||||
const socketPath = path.join(directory, "daemon.sock")
|
||||
let token = "old-token"
|
||||
let instance = "old-instance"
|
||||
@@ -24,7 +25,7 @@ it.live("rediscovers a same-protocol daemon after its registration rotates", ()
|
||||
return request.op === "list" ? { type: "terminals", terminals: [] } : { type: "ok" }
|
||||
})
|
||||
yield* Effect.promise(() => writeRegistration(directory, socketPath, instance, token))
|
||||
const daemon = yield* makeDaemonTransport(directory)
|
||||
const daemon = yield* makeDaemonTransport(root, "daemon")
|
||||
yield* daemon.request({ op: "list" })
|
||||
|
||||
token = "new-token"
|
||||
@@ -44,7 +45,7 @@ it.live("rediscovers a same-protocol daemon after its registration rotates", ()
|
||||
|
||||
it.live("rediscovers a rotated registration when acquiring a subscription", () =>
|
||||
Effect.gen(function* () {
|
||||
const directory = yield* temporaryDirectory()
|
||||
const { root, directory } = yield* temporaryRuntime()
|
||||
const socketPath = path.join(directory, "daemon.sock")
|
||||
let token = "old-token"
|
||||
let instance = "old-instance"
|
||||
@@ -67,7 +68,7 @@ it.live("rediscovers a rotated registration when acquiring a subscription", () =
|
||||
}
|
||||
})
|
||||
yield* Effect.promise(() => writeRegistration(directory, socketPath, instance, token))
|
||||
const daemon = yield* makeDaemonTransport(directory)
|
||||
const daemon = yield* makeDaemonTransport(root, "daemon")
|
||||
yield* daemon.request({ op: "list" })
|
||||
|
||||
token = "new-token"
|
||||
@@ -89,7 +90,7 @@ it.live("rediscovers a rotated registration when acquiring a subscription", () =
|
||||
|
||||
it.live("retries a start-required request when connection fails before dispatch", () =>
|
||||
Effect.gen(function* () {
|
||||
const directory = yield* temporaryDirectory()
|
||||
const { root, directory } = yield* temporaryRuntime()
|
||||
const firstSocket = path.join(directory, "first.sock")
|
||||
const secondSocket = path.join(directory, "second.sock")
|
||||
const first = yield* listen(firstSocket, (_socket, request) => {
|
||||
@@ -97,7 +98,7 @@ it.live("retries a start-required request when connection fails before dispatch"
|
||||
return { type: "terminals", terminals: [] }
|
||||
})
|
||||
yield* Effect.promise(() => writeRegistration(directory, firstSocket))
|
||||
const daemon = yield* makeDaemonTransport(directory)
|
||||
const daemon = yield* makeDaemonTransport(root, "daemon")
|
||||
yield* daemon.request({ op: "list" })
|
||||
yield* Effect.promise(first.close)
|
||||
|
||||
@@ -115,7 +116,7 @@ it.live("retries a start-required request when connection fails before dispatch"
|
||||
|
||||
it.live("does not replay a dispatched mutating request when its response is lost", () =>
|
||||
Effect.gen(function* () {
|
||||
const directory = yield* temporaryDirectory()
|
||||
const { root, directory } = yield* temporaryRuntime()
|
||||
const socketPath = path.join(directory, "daemon.sock")
|
||||
let creates = 0
|
||||
yield* listen(socketPath, (socket, request) => {
|
||||
@@ -125,7 +126,7 @@ it.live("does not replay a dispatched mutating request when its response is lost
|
||||
return undefined
|
||||
})
|
||||
yield* Effect.promise(() => writeRegistration(directory, socketPath))
|
||||
const daemon = yield* makeDaemonTransport(directory)
|
||||
const daemon = yield* makeDaemonTransport(root, "daemon")
|
||||
const error = yield* Effect.flip(daemon.request({ op: "create" }, true))
|
||||
|
||||
expect(error.kind).toBe("response")
|
||||
@@ -135,7 +136,7 @@ it.live("does not replay a dispatched mutating request when its response is lost
|
||||
|
||||
it.live("rejects incompatible daemons without replacing or killing them", () =>
|
||||
Effect.gen(function* () {
|
||||
const directory = yield* temporaryDirectory()
|
||||
const { root, directory } = yield* temporaryRuntime()
|
||||
const existing = yield* Effect.acquireRelease(
|
||||
Effect.sync(() => spawn("sleep", ["30"])),
|
||||
(child) =>
|
||||
@@ -153,7 +154,7 @@ it.live("rejects incompatible daemons without replacing or killing them", () =>
|
||||
JSON.stringify({ instance_id: "old", pid: existing.pid, protocol: 6, socket: "/unused", token: "old" }),
|
||||
),
|
||||
)
|
||||
const daemon = yield* makeDaemonTransport(directory, () => Promise.resolve("/missing/opencode-pty"))
|
||||
const daemon = yield* makeDaemonTransport(root, "daemon", () => Promise.resolve("/missing/opencode-pty"))
|
||||
|
||||
const optional = yield* Effect.flip(daemon.requestIfRunning({ op: "list" }))
|
||||
expect(optional).toMatchObject({
|
||||
@@ -170,6 +171,84 @@ it.live("rejects incompatible daemons without replacing or killing them", () =>
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("moves an inherited legacy registration into the runtime root and keeps its lock file", () =>
|
||||
Effect.gen(function* () {
|
||||
const base = yield* temporaryDirectory()
|
||||
const legacy = path.join(base, "legacy", "inherited")
|
||||
const socketPath = path.join(base, "daemon.sock")
|
||||
yield* listen(socketPath, (_socket, request) => {
|
||||
if (request.op === "ping") return pong
|
||||
return { type: "terminals", terminals: [] }
|
||||
})
|
||||
yield* Effect.promise(async () => {
|
||||
await mkdir(legacy, { recursive: true })
|
||||
await writeFile(path.join(legacy, "service.lock"), "")
|
||||
await writeRegistration(legacy, socketPath)
|
||||
})
|
||||
const lock = yield* Effect.promise(() => stat(path.join(legacy, "service.lock")))
|
||||
const daemon = yield* makeDaemonTransport(path.join(base, "root"), "fresh", undefined, {
|
||||
directory: legacy,
|
||||
instanceID: "test",
|
||||
ticket: "ticket",
|
||||
expiresAt: Date.now() + 60_000,
|
||||
})
|
||||
|
||||
const migrated = path.join(base, "root", "inherited")
|
||||
expect(existsSync(path.join(legacy, "service.json"))).toBeFalse()
|
||||
expect((yield* Effect.promise(() => stat(path.join(migrated, "service.lock")))).ino).toBe(lock.ino)
|
||||
expect(existsSync(path.join(migrated, "service.json"))).toBeTrue()
|
||||
expect(yield* daemon.request({ op: "list" })).toEqual({ type: "terminals", terminals: [] })
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("starts fresh instead of failing when an inherited registration is gone or expired", () =>
|
||||
Effect.gen(function* () {
|
||||
const base = yield* temporaryDirectory()
|
||||
const legacy = path.join(base, "legacy")
|
||||
yield* Effect.promise(() => mkdir(legacy))
|
||||
for (const expiresAt of [Date.now() + 60_000, Date.now() - 1]) {
|
||||
const daemon = yield* makeDaemonTransport(path.join(base, "root"), "fresh", undefined, {
|
||||
directory: legacy,
|
||||
instanceID: "test",
|
||||
ticket: "ticket",
|
||||
expiresAt,
|
||||
})
|
||||
expect(yield* daemon.handoff).toBeNull()
|
||||
expect(yield* daemon.requestIfRunning({ op: "list" })).toBeUndefined()
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("stops a discovered inherited daemon when adoption fails", () =>
|
||||
Effect.gen(function* () {
|
||||
const { root, directory } = yield* temporaryRuntime()
|
||||
const socketPath = path.join(directory, "daemon.sock")
|
||||
let shutdowns = 0
|
||||
yield* listen(socketPath, (_socket, request) => {
|
||||
if (request.op === "ping") return pong
|
||||
if (request.op === "shutdown") shutdowns++
|
||||
return { type: "ok" }
|
||||
})
|
||||
yield* Effect.promise(() => writeRegistration(directory, socketPath))
|
||||
yield* makeDaemonTransport(root, "fresh", undefined, {
|
||||
directory,
|
||||
instanceID: "replaced",
|
||||
ticket: "ticket",
|
||||
expiresAt: Date.now() + 60_000,
|
||||
})
|
||||
expect(shutdowns).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
function temporaryRuntime() {
|
||||
return Effect.gen(function* () {
|
||||
const root = yield* temporaryDirectory()
|
||||
const directory = path.join(root, "daemon")
|
||||
yield* Effect.promise(() => mkdir(directory))
|
||||
return { root, directory }
|
||||
})
|
||||
}
|
||||
|
||||
function temporaryDirectory() {
|
||||
return Effect.acquireRelease(
|
||||
Effect.promise(() => mkdtemp(path.join(os.tmpdir(), "opencode-pty-test-"))),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { OpenAIResponses } from "@opencode/ai/protocols/openai-responses"
|
||||
import { AIError, HttpContext, RateLimitError } from "@opencode/ai"
|
||||
import { classifyProviderFailure } from "@opencode/ai/provider-error"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { ConfigProvider, DateTime, Deferred, Effect, Schedule } from "effect"
|
||||
import { DateTime, Deferred, Effect, Schedule } from "effect"
|
||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
import { exportJWK, generateKeyPair, SignJWT } from "jose"
|
||||
import { App } from "@opencode/core/app"
|
||||
|
||||
@@ -14,7 +14,6 @@ const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* GoogleVertexPlugin.effect(host)
|
||||
})
|
||||
@@ -307,7 +306,6 @@ describe("GoogleVertexPlugin", () => {
|
||||
|
||||
it.effect("trims model IDs before selecting language models", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Agent } from "@opencode/schema/agent"
|
||||
import { Session } from "@opencode/core/session"
|
||||
import { OpenAIResponses } from "@opencode/ai/protocols/openai-responses"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { ConfigProvider, DateTime, Effect } from "effect"
|
||||
import { DateTime, Effect } from "effect"
|
||||
import { Credential } from "@opencode/core/credential"
|
||||
import { Integration } from "@opencode/core/integration"
|
||||
import { Location } from "@opencode/core/location"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FileSystem } from "@opencode/core/filesystem"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { LLMClient, LLMEvent, LanguageModel, type LLMRequest } from "@opencode/ai"
|
||||
import { OpenAIChat } from "@opencode/ai/protocols"
|
||||
@@ -58,7 +59,7 @@ const locations = Layer.effect(
|
||||
Layer.provide(client),
|
||||
Layer.provide(config),
|
||||
Layer.provide(models),
|
||||
) as unknown as Layer.Layer<LocationServices>,
|
||||
) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
|
||||
),
|
||||
)
|
||||
const it = testEffect(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FileSystem } from "@opencode/core/filesystem"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { AIError, TransportError } from "@opencode/ai"
|
||||
import { Database } from "@opencode/core/database/database"
|
||||
@@ -1371,7 +1372,7 @@ function buildExecution(
|
||||
() =>
|
||||
// The local execution test only needs the Session runner from the Location graph.
|
||||
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion
|
||||
runner as unknown as Layer.Layer<LocationServices>,
|
||||
runner as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
|
||||
),
|
||||
)
|
||||
return yield* Layer.buildWithScope(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FileSystem } from "@opencode/core/filesystem"
|
||||
import { describe, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import { chmod, mkdir, readdir, rm } from "fs/promises"
|
||||
@@ -58,7 +59,7 @@ const itWithActiveExecution = testEffect(
|
||||
replacements: [Project.node.replace(globalProjectNode), offlineModels],
|
||||
}),
|
||||
Layer.succeed(SessionRunner.Service, { drain: () => Effect.never }),
|
||||
) as unknown as Layer.Layer<LocationServices>,
|
||||
) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -68,7 +69,11 @@ const itWithActiveExecution = testEffect(
|
||||
const unavailableLocations = Layer.effect(
|
||||
LocationServiceMap.Service,
|
||||
LayerMap.make(
|
||||
() => Layer.effectDiscard(Effect.fail(new Error("broken location"))) as unknown as Layer.Layer<LocationServices>,
|
||||
() =>
|
||||
Layer.effectDiscard(Effect.die(new Error("broken location"))) as unknown as Layer.Layer<
|
||||
LocationServices,
|
||||
FileSystem.DirectoryNotFoundError
|
||||
>,
|
||||
),
|
||||
)
|
||||
const itWithUnavailableDestination = testEffect(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import type { FileSystem } from "@opencode/core/filesystem"
|
||||
import { DateTime, Effect, Fiber, Layer, LayerMap, Schema, Stream } from "effect"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
@@ -83,7 +84,7 @@ const locations = makeGlobalNode({
|
||||
restore: () => Effect.void,
|
||||
}),
|
||||
Layer.mock(Plugin.Service, { awaitActivation: Effect.void }),
|
||||
).pipe(Layer.fresh) as unknown as Layer.Layer<LocationServices>,
|
||||
).pipe(Layer.fresh) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
|
||||
)
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FileSystem } from "@opencode/core/filesystem"
|
||||
import path from "path"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer, LayerMap } from "effect"
|
||||
@@ -49,7 +50,7 @@ const locations = makeGlobalNode({
|
||||
list: () => Effect.succeed([info]),
|
||||
}),
|
||||
Layer.mock(Plugin.Service, { awaitActivation: Effect.void }),
|
||||
) as unknown as Layer.Layer<LocationServices>,
|
||||
) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
|
||||
),
|
||||
),
|
||||
deps: [],
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"incremental": true,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"tsBuildInfoFile": "node_modules/.ts-dist/tests.tsbuildinfo"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["@cloudflare/workers-types", "node"]
|
||||
|
||||
@@ -251,10 +251,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
private _nullCellCount: number = 0
|
||||
private _cursorStyle: IBufferCell
|
||||
private _firstRow: number = 0
|
||||
private _lastCursorRow: number = 0
|
||||
private _lastCursorCol: number = 0
|
||||
private _lastContentCursorRow: number = 0
|
||||
private _lastContentCursorCol: number = 0
|
||||
|
||||
constructor(
|
||||
buffer: IBuffer,
|
||||
@@ -274,7 +271,6 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
this._cursorStyle = this._buffer.getNullCell()
|
||||
|
||||
this._lastContentCursorRow = start
|
||||
this._lastCursorRow = start
|
||||
this._firstRow = start
|
||||
}
|
||||
|
||||
@@ -292,8 +288,6 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
|
||||
if (!isLastRow && !wrapped) {
|
||||
rowSeparator = "\r\n"
|
||||
this._lastCursorRow = row + 1
|
||||
this._lastCursorCol = 0
|
||||
}
|
||||
|
||||
this._allRows[this._rowIndex] = this._currentRow
|
||||
@@ -437,8 +431,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
this._nullCellCount = 0
|
||||
}
|
||||
|
||||
this._lastContentCursorRow = this._lastCursorRow = row
|
||||
this._lastContentCursorCol = this._lastCursorCol = col
|
||||
this._lastContentCursorRow = row
|
||||
|
||||
this._currentRow += `\u001b[${sgrSeq.join(";")}m`
|
||||
|
||||
@@ -456,8 +449,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
|
||||
this._currentRow += cell.getChars()
|
||||
|
||||
this._lastContentCursorRow = this._lastCursorRow = row
|
||||
this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth()
|
||||
this._lastContentCursorRow = row
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,8 +458,6 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
|
||||
if (this._buffer.length - this._firstRow <= this._terminal.rows) {
|
||||
rowEnd = this._lastContentCursorRow + 1 - this._firstRow
|
||||
this._lastCursorCol = this._lastContentCursorCol
|
||||
this._lastCursorRow = this._lastContentCursorRow
|
||||
}
|
||||
|
||||
let content = ""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"target": "ESNext",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import type { CassetteMetadata, JsonValue } from "../api.js"
|
||||
import type { JsonValue } from "../api.js"
|
||||
import { HttpInteractionSchema } from "../http/model.js"
|
||||
import { WebSocketInteractionSchema } from "../websocket/model.js"
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Schema } from "effect"
|
||||
import type { RequestSnapshot } from "../api.js"
|
||||
|
||||
export const RequestSnapshotSchema = Schema.Struct({
|
||||
method: Schema.String,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"module": "nodenext",
|
||||
|
||||
@@ -1919,7 +1919,6 @@ function renderSchemas(slots: ReadonlyArray<Slot>) {
|
||||
tagged.fields.map(([name, schema]) => ({ name: `Class${classIndex}${name}`, schema })),
|
||||
),
|
||||
]
|
||||
const [first, ...rest] = expanded
|
||||
const document = SchemaRepresentation.toCodeDocument(
|
||||
SchemaRepresentation.toRepresentations(
|
||||
codegenAsts(expanded.map((slot) => slot.schema.ast)) as [SchemaAST.AST, ...Array<SchemaAST.AST>],
|
||||
|
||||
@@ -32,4 +32,4 @@ export const program = OpenCode.make().pipe(
|
||||
}),
|
||||
)
|
||||
|
||||
const _requiresHttpClient: Effect.Effect<unknown, never, HttpClient.HttpClient> = program
|
||||
export const requiresHttpClient: Effect.Effect<unknown, never, HttpClient.HttpClient> = program
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"noUncheckedIndexedAccess": false
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUnusedLocals": true,
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"noUncheckedIndexedAccess": false
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
} from "./types.js"
|
||||
import { MermaidSyntaxError } from "../diagnostics.js"
|
||||
import {
|
||||
decodeMermaidText,
|
||||
firstMeaningfulMermaidLine,
|
||||
meaningfulNumberedMermaidLines,
|
||||
stripMermaidQuotes as stripQuotes,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { DiagramCanvas } from "../core/canvas.js"
|
||||
import { diagramTextWidth } from "../core/text.js"
|
||||
import type { GanttGrid } from "./render-grid.js"
|
||||
import type {
|
||||
GanttCellStyle,
|
||||
GanttDiagram,
|
||||
GanttDiagramRenderOptions,
|
||||
GanttLabelLayout,
|
||||
|
||||
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user