mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-11 07:03:42 +00:00
Merge branch 'dev' into mcp-resource-content
This commit is contained in:
commit
4d6add0985
19 changed files with 267 additions and 45 deletions
|
|
@ -10,7 +10,11 @@ export async function statsProxy(evt: APIEvent) {
|
|||
targetUrl.hostname = Resource.App.stage === "production" ? "stats.opencode.ai" : "stats.dev.opencode.ai"
|
||||
targetUrl.port = ""
|
||||
|
||||
if (targetUrl.pathname.startsWith(`${dataPath}/_build/`) || targetUrl.pathname === `${dataPath}/banner.jpg`) {
|
||||
if (
|
||||
targetUrl.pathname.startsWith(`${dataPath}/_build/`) ||
|
||||
targetUrl.pathname === `${dataPath}/banner.jpg` ||
|
||||
targetUrl.pathname === `${dataPath}/banner.png`
|
||||
) {
|
||||
targetUrl.pathname = targetUrl.pathname.slice(dataPath.length)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -272,7 +272,10 @@ export namespace Billing {
|
|||
},
|
||||
payment_method_options: {
|
||||
card: {
|
||||
setup_future_usage: "on_session",
|
||||
setup_future_usage: "off_session",
|
||||
},
|
||||
link: {
|
||||
setup_future_usage: "off_session",
|
||||
},
|
||||
},
|
||||
//payment_method_data: {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import { PositiveInt } from "../schema"
|
|||
export class Local extends Schema.Class<Local>("ConfigV2.MCP.Local")({
|
||||
type: Schema.Literal("local"),
|
||||
command: Schema.String.pipe(Schema.Array),
|
||||
cwd: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Working directory for the MCP server process. Relative paths resolve from the workspace directory.",
|
||||
}),
|
||||
environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||
timeout: PositiveInt.pipe(Schema.optional),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ export const Local = Schema.Struct({
|
|||
command: Schema.mutable(Schema.Array(Schema.String)).annotate({
|
||||
description: "Command and arguments to run the MCP server",
|
||||
}),
|
||||
cwd: Schema.optional(Schema.String).annotate({
|
||||
description: "Working directory for the MCP server process. Relative paths resolve from the workspace directory.",
|
||||
}),
|
||||
environment: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({
|
||||
description: "Environment variables to set when running the MCP server",
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -139,7 +139,14 @@ function mcp(info: typeof ConfigV1.Info.Type) {
|
|||
function migrateMcp(info: ConfigMCPV1.Info) {
|
||||
const disabled = info.enabled === undefined ? undefined : !info.enabled
|
||||
if (info.type === "local")
|
||||
return { type: info.type, command: info.command, environment: info.environment, disabled, timeout: info.timeout }
|
||||
return {
|
||||
type: info.type,
|
||||
command: info.command,
|
||||
cwd: info.cwd,
|
||||
environment: info.environment,
|
||||
disabled,
|
||||
timeout: info.timeout,
|
||||
}
|
||||
return {
|
||||
type: info.type,
|
||||
url: info.url,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import path from "node:path"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { type Tool } from "ai"
|
||||
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
|
||||
|
|
@ -304,7 +305,8 @@ export const layer = Layer.effect(
|
|||
mcp: ConfigMCPV1.Info & { type: "local" },
|
||||
) {
|
||||
const [cmd, ...args] = mcp.command
|
||||
const cwd = yield* InstanceState.directory
|
||||
const baseDir = yield* InstanceState.directory
|
||||
const cwd = mcp.cwd ? path.resolve(baseDir, mcp.cwd) : baseDir
|
||||
const transport = new StdioClientTransport({
|
||||
stderr: "pipe",
|
||||
command: cmd,
|
||||
|
|
|
|||
|
|
@ -1375,6 +1375,24 @@ export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7
|
|||
}
|
||||
}
|
||||
|
||||
// Gemini requires a single `type`, not a JSON Schema type array such as
|
||||
// `["number","string"]` (emitted by some MCP servers). Plain `@ai-sdk/google`
|
||||
// rewrites these into an `anyOf` of single-type schemas, but OpenAI-compatible
|
||||
// transports (e.g. GitHub Copilot proxying to Gemini) forward them verbatim
|
||||
// and the backend rejects the array form. Mirror the SDK: split non-null
|
||||
// types into `anyOf`, and lift `null` into `nullable`.
|
||||
if (Array.isArray(result.type)) {
|
||||
const hasNull = result.type.includes("null")
|
||||
const nonNull = result.type.filter((entry: unknown) => entry !== "null")
|
||||
if (nonNull.length === 0) {
|
||||
result.type = "null"
|
||||
} else {
|
||||
delete result.type
|
||||
result.anyOf = nonNull.map((entry: unknown) => ({ type: entry }))
|
||||
if (hasNull) result.nullable = true
|
||||
}
|
||||
}
|
||||
|
||||
// Filter required array to only include fields that exist in properties
|
||||
if (result.type === "object" && result.properties && Array.isArray(result.required)) {
|
||||
result.required = result.required.filter((field: any) => field in result.properties)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import path from "node:path"
|
||||
import { expect, mock, beforeEach } from "bun:test"
|
||||
import { ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js"
|
||||
import { Cause, Effect, Exit } from "effect"
|
||||
import type { MCP as MCPNS } from "../../src/mcp/index"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { TestInstance } from "../fixture/fixture"
|
||||
|
||||
// --- Mock infrastructure ---
|
||||
|
||||
|
|
@ -48,6 +50,8 @@ let connectError = "Mock transport cannot connect"
|
|||
let clientCreateCount = 0
|
||||
// Tracks how many times transport.close() is called across all mock transports
|
||||
let transportCloseCount = 0
|
||||
// Captures the opts passed to each MockStdioTransport, keyed by lastCreatedClientName
|
||||
const stdioOptsByName = new Map<string, any>()
|
||||
|
||||
function getOrCreateClientState(name?: string): MockClientState {
|
||||
const key = name ?? "default"
|
||||
|
|
@ -82,8 +86,9 @@ function getOrCreateClientState(name?: string): MockClientState {
|
|||
class MockStdioTransport {
|
||||
stderr: null = null
|
||||
pid = 12345
|
||||
// oxlint-disable-next-line no-useless-constructor
|
||||
constructor(_opts: any) {}
|
||||
constructor(opts: any) {
|
||||
if (lastCreatedClientName) stdioOptsByName.set(lastCreatedClientName, opts)
|
||||
}
|
||||
async start() {
|
||||
if (connectShouldHang) return new Promise<void>(() => {}) // never resolves
|
||||
if (connectShouldFail) throw new Error(connectError)
|
||||
|
|
@ -246,6 +251,20 @@ function statusName(status: Record<string, MCPNS.Status> | MCPNS.Status, server:
|
|||
return status[server]?.status
|
||||
}
|
||||
|
||||
it.instance(
|
||||
"local mcp cwd resolves relative paths against instance directory",
|
||||
() =>
|
||||
MCP.Service.use((mcp: MCPNS.Interface) =>
|
||||
Effect.gen(function* () {
|
||||
const { directory } = yield* TestInstance
|
||||
lastCreatedClientName = "rel-cwd"
|
||||
yield* mcp.add("rel-cwd", { type: "local", command: ["echo", "test"], cwd: "plugins/sub" })
|
||||
expect(stdioOptsByName.get("rel-cwd")?.cwd).toBe(path.resolve(directory, "plugins/sub"))
|
||||
}),
|
||||
),
|
||||
{ config: { mcp: {} } },
|
||||
)
|
||||
|
||||
// ========================================================================
|
||||
// Test: tools() are cached after connect
|
||||
// ========================================================================
|
||||
|
|
|
|||
|
|
@ -857,6 +857,93 @@ describe("ProviderTransform.schema - gemini nested array items", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("ProviderTransform.schema - gemini type arrays", () => {
|
||||
// Mirrors @ai-sdk/google's convertJSONSchemaToOpenAPISchema: JSON Schema type
|
||||
// arrays (e.g. `["number","string"]`, common in MCP tool schemas) become an
|
||||
// `anyOf` of single-type schemas, with `null` lifted into `nullable`. Plain
|
||||
// @ai-sdk/google rewrites these, but OpenAI-compatible transports such as
|
||||
// GitHub Copilot (proxying to Gemini) forward them verbatim and the backend
|
||||
// rejects the array form.
|
||||
const geminiModel = {
|
||||
providerID: "google",
|
||||
api: {
|
||||
id: "gemini-3-pro",
|
||||
},
|
||||
} as any
|
||||
|
||||
test("splits a multi-type array into anyOf and drops the type array", () => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
status: { type: ["number", "string"], description: "status filter" },
|
||||
},
|
||||
} as any
|
||||
|
||||
const result = ProviderTransform.schema(geminiModel, schema) as any
|
||||
|
||||
expect(result.properties.status.type).toBeUndefined()
|
||||
expect(result.properties.status.anyOf).toEqual([{ type: "number" }, { type: "string" }])
|
||||
expect(result.properties.status.nullable).toBeUndefined()
|
||||
// Sibling keywords stay alongside the generated anyOf.
|
||||
expect(result.properties.status.description).toBe("status filter")
|
||||
})
|
||||
|
||||
test("lifts null into nullable for a nullable type array", () => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
maybe: { type: ["string", "null"], description: "nullable string" },
|
||||
},
|
||||
} as any
|
||||
|
||||
const result = ProviderTransform.schema(geminiModel, schema) as any
|
||||
|
||||
expect(result.properties.maybe.type).toBeUndefined()
|
||||
expect(result.properties.maybe.anyOf).toEqual([{ type: "string" }])
|
||||
expect(result.properties.maybe.nullable).toBe(true)
|
||||
})
|
||||
|
||||
test("collapses an all-null type array to type null", () => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
nothing: { type: ["null"] },
|
||||
},
|
||||
} as any
|
||||
|
||||
const result = ProviderTransform.schema(geminiModel, schema) as any
|
||||
|
||||
expect(result.properties.nothing.type).toBe("null")
|
||||
expect(result.properties.nothing.anyOf).toBeUndefined()
|
||||
})
|
||||
|
||||
test("rewrites type arrays for gemini served through github-copilot", () => {
|
||||
const copilotGeminiModel = {
|
||||
providerID: "github-copilot",
|
||||
api: {
|
||||
id: "gemini-3.5-flash",
|
||||
npm: "@ai-sdk/github-copilot",
|
||||
},
|
||||
} as any
|
||||
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
hook_id: { type: "number", description: "ID of the webhook" },
|
||||
status: { type: ["number", "string"], description: "Filter by response status code" },
|
||||
},
|
||||
required: ["hook_id"],
|
||||
additionalProperties: false,
|
||||
} as any
|
||||
|
||||
const result = ProviderTransform.schema(copilotGeminiModel, schema) as any
|
||||
|
||||
expect(result.properties.status.anyOf).toEqual([{ type: "number" }, { type: "string" }])
|
||||
expect(result.properties.status.type).toBeUndefined()
|
||||
expect(result.properties.hook_id.type).toBe("number")
|
||||
})
|
||||
})
|
||||
|
||||
describe("ProviderTransform.schema - gemini combiner nodes", () => {
|
||||
const geminiModel = {
|
||||
providerID: "google",
|
||||
|
|
|
|||
|
|
@ -1875,6 +1875,7 @@ export type McpLocalConfig = {
|
|||
* Command and arguments to run the MCP server
|
||||
*/
|
||||
command: Array<string>
|
||||
cwd?: string
|
||||
environment?: {
|
||||
[key: string]: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19296,6 +19296,9 @@
|
|||
},
|
||||
"description": "Command and arguments to run the MCP server"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
|
|
|
|||
BIN
packages/stats/app/public/banner.png
Normal file
BIN
packages/stats/app/public/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
|
|
@ -32,7 +32,10 @@ import {
|
|||
type ThemePreference,
|
||||
} from "../stats-shell"
|
||||
|
||||
const statsModelFallbackUrl = "https://stats.opencode.ai"
|
||||
const statsCanonicalBaseUrl = "https://opencode.ai/data/"
|
||||
const statsUnfurlPath = "banner.png"
|
||||
const statsUnfurlAlt = "OpenCode Data wordmark on a dark patterned background"
|
||||
const statsUnfurlUrl = new URL(statsUnfurlPath, statsCanonicalBaseUrl).toString()
|
||||
const modelHeaderLinks: readonly HeaderLink[] = [
|
||||
{ href: "#overview", label: "Overview" },
|
||||
{ href: "#usage", label: "Usage" },
|
||||
|
|
@ -118,8 +121,8 @@ export default function StatsModel() {
|
|||
)
|
||||
const modelUrl = createMemo(() =>
|
||||
new URL(
|
||||
`${import.meta.env.BASE_URL}${catalogEntry()?.id ?? `${labParam()}/${stats()?.slug ?? modelParam()}`}`,
|
||||
event?.request.url ?? (typeof window === "undefined" ? statsModelFallbackUrl : window.location.href),
|
||||
catalogEntry()?.id ?? [labParam(), stats()?.slug ?? modelParam()].filter((part) => part.length > 0).join("/"),
|
||||
statsCanonicalBaseUrl,
|
||||
).toString(),
|
||||
)
|
||||
const updateThemePreference = (preference: ThemePreference) => {
|
||||
|
|
@ -147,9 +150,16 @@ export default function StatsModel() {
|
|||
<Meta property="og:title" content={modelTitle()} />
|
||||
<Meta property="og:description" content={modelDescription()} />
|
||||
<Meta property="og:url" content={modelUrl()} />
|
||||
<Meta name="twitter:card" content="summary" />
|
||||
<Meta property="og:image" content={statsUnfurlUrl} />
|
||||
<Meta property="og:image:type" content="image/png" />
|
||||
<Meta property="og:image:width" content="1200" />
|
||||
<Meta property="og:image:height" content="630" />
|
||||
<Meta property="og:image:alt" content={statsUnfurlAlt} />
|
||||
<Meta name="twitter:card" content="summary_large_image" />
|
||||
<Meta name="twitter:title" content={modelTitle()} />
|
||||
<Meta name="twitter:description" content={modelDescription()} />
|
||||
<Meta name="twitter:image" content={statsUnfurlUrl} />
|
||||
<Meta name="twitter:image:alt" content={statsUnfurlAlt} />
|
||||
<Header githubStars={githubStars() ?? "150K"} links={modelHeaderLinks} brandHref={import.meta.env.BASE_URL} />
|
||||
<div data-component="container">
|
||||
<div data-component="content">
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@ import {
|
|||
type ThemePreference,
|
||||
} from "../stats-shell"
|
||||
|
||||
const statsLabFallbackUrl = "https://stats.opencode.ai"
|
||||
const statsCanonicalBaseUrl = "https://opencode.ai/data/"
|
||||
const statsUnfurlPath = "banner.png"
|
||||
const statsUnfurlAlt = "OpenCode Data wordmark on a dark patterned background"
|
||||
const statsUnfurlUrl = new URL(statsUnfurlPath, statsCanonicalBaseUrl).toString()
|
||||
const labHeaderLinks: readonly HeaderLink[] = [
|
||||
{ href: "#overview", label: "Overview" },
|
||||
{ href: "#usage", label: "Usage" },
|
||||
|
|
@ -71,12 +74,7 @@ export default function StatsLab() {
|
|||
() =>
|
||||
`Explore ${labName()} models used in OpenCode, with recent token usage, context windows, release dates, and model-specific data.`,
|
||||
)
|
||||
const labUrl = createMemo(() =>
|
||||
new URL(
|
||||
`${import.meta.env.BASE_URL}${lab()?.id ?? labParam()}`,
|
||||
event?.request.url ?? (typeof window === "undefined" ? statsLabFallbackUrl : window.location.href),
|
||||
).toString(),
|
||||
)
|
||||
const labUrl = createMemo(() => new URL(lab()?.id ?? labParam(), statsCanonicalBaseUrl).toString())
|
||||
const updateThemePreference = (preference: ThemePreference) => {
|
||||
applyThemePreference(preference)
|
||||
setThemePreference(preference)
|
||||
|
|
@ -102,9 +100,16 @@ export default function StatsLab() {
|
|||
<Meta property="og:title" content={labTitle()} />
|
||||
<Meta property="og:description" content={labDescription()} />
|
||||
<Meta property="og:url" content={labUrl()} />
|
||||
<Meta name="twitter:card" content="summary" />
|
||||
<Meta property="og:image" content={statsUnfurlUrl} />
|
||||
<Meta property="og:image:type" content="image/png" />
|
||||
<Meta property="og:image:width" content="1200" />
|
||||
<Meta property="og:image:height" content="630" />
|
||||
<Meta property="og:image:alt" content={statsUnfurlAlt} />
|
||||
<Meta name="twitter:card" content="summary_large_image" />
|
||||
<Meta name="twitter:title" content={labTitle()} />
|
||||
<Meta name="twitter:description" content={labDescription()} />
|
||||
<Meta name="twitter:image" content={statsUnfurlUrl} />
|
||||
<Meta name="twitter:image:alt" content={statsUnfurlAlt} />
|
||||
<Header githubStars={githubStars() ?? "150K"} links={labHeaderLinks} brandHref={import.meta.env.BASE_URL} />
|
||||
<div data-component="container">
|
||||
<div data-component="content">
|
||||
|
|
|
|||
|
|
@ -540,7 +540,7 @@ export function Prompt(props: PromptProps) {
|
|||
},
|
||||
{
|
||||
title: "Move session",
|
||||
desc: "Move the session to another project directory",
|
||||
desc: "Move to another project dir",
|
||||
name: "session.move",
|
||||
category: "Session",
|
||||
slashName: "move",
|
||||
|
|
@ -1086,22 +1086,31 @@ export function Prompt(props: PromptProps) {
|
|||
} else {
|
||||
move.startSubmit()
|
||||
sdk.client.session
|
||||
.prompt({
|
||||
sessionID,
|
||||
...selectedModel,
|
||||
agent: agent.name,
|
||||
model: selectedModel,
|
||||
variant,
|
||||
parts: [
|
||||
...editorParts,
|
||||
{
|
||||
type: "text",
|
||||
text: inputText,
|
||||
},
|
||||
...nonTextParts,
|
||||
],
|
||||
.prompt(
|
||||
{
|
||||
sessionID,
|
||||
...selectedModel,
|
||||
agent: agent.name,
|
||||
model: selectedModel,
|
||||
variant,
|
||||
parts: [
|
||||
...editorParts,
|
||||
{
|
||||
type: "text",
|
||||
text: inputText,
|
||||
},
|
||||
...nonTextParts,
|
||||
],
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
.catch((error) => {
|
||||
toast.show({
|
||||
title: "Failed to send prompt",
|
||||
message: errorMessage(error),
|
||||
variant: "error",
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
if (editorParts.length > 0) editor.markSelectionSent()
|
||||
}
|
||||
history.append({
|
||||
|
|
|
|||
|
|
@ -1831,6 +1831,7 @@ function InlineTool(props: {
|
|||
color?: RGBA
|
||||
complete: unknown
|
||||
pending: string
|
||||
failure?: string
|
||||
spinner?: boolean
|
||||
subagent?: boolean
|
||||
children: JSX.Element
|
||||
|
|
@ -1884,6 +1885,7 @@ function InlineTool(props: {
|
|||
errorExpanded={errorExpanded()}
|
||||
complete={props.complete}
|
||||
pending={props.pending}
|
||||
failure={props.failure}
|
||||
spinner={props.spinner}
|
||||
subagent={props.subagent}
|
||||
separateAfter={(id) => id !== undefined && ctx.userMessageIDs().has(id)}
|
||||
|
|
@ -1915,6 +1917,7 @@ export function InlineToolRow(props: {
|
|||
errorExpanded?: boolean
|
||||
complete: unknown
|
||||
pending: string
|
||||
failure?: string
|
||||
spinner?: boolean
|
||||
subagent?: boolean
|
||||
children: JSX.Element
|
||||
|
|
@ -1958,7 +1961,7 @@ export function InlineToolRow(props: {
|
|||
~ {props.pending}
|
||||
</text>
|
||||
}
|
||||
when={props.complete}
|
||||
when={props.complete || props.failed}
|
||||
>
|
||||
<box flexDirection="row">
|
||||
<text
|
||||
|
|
@ -1973,7 +1976,7 @@ export function InlineToolRow(props: {
|
|||
fg={props.failed ? props.errorColor : props.color}
|
||||
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
|
||||
>
|
||||
{props.children}
|
||||
{props.failed && !props.complete ? (props.failure ?? props.children) : props.children}
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
|
|
@ -2445,7 +2448,7 @@ function ApplyPatch(props: ToolProps) {
|
|||
</For>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<InlineTool icon="%" pending="Preparing patch..." complete={false} part={props.part}>
|
||||
<InlineTool icon="%" pending="Preparing patch..." failure="Patch failed" complete={false} part={props.part}>
|
||||
Patch
|
||||
</InlineTool>
|
||||
</Match>
|
||||
|
|
@ -2465,7 +2468,13 @@ function TodoWrite(props: ToolProps) {
|
|||
</BlockTool>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<InlineTool icon="⚙" pending="Updating todos..." complete={false} part={props.part}>
|
||||
<InlineTool
|
||||
icon="⚙"
|
||||
pending="Updating todos..."
|
||||
failure="Todo update failed"
|
||||
complete={false}
|
||||
part={props.part}
|
||||
>
|
||||
Updating todos...
|
||||
</InlineTool>
|
||||
</Match>
|
||||
|
|
|
|||
10
packages/tui/sst-env.d.ts
vendored
Normal file
10
packages/tui/sst-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
|
|
@ -155,6 +155,22 @@ function StickyScrollFixture(props: { separated: boolean; scroll: (scroll: Scrol
|
|||
)
|
||||
}
|
||||
|
||||
function FailedPendingToolFixture() {
|
||||
return (
|
||||
<InlineToolRow icon="%" complete={false} pending="Preparing patch..." failed={true} failure="Patch failed">
|
||||
Patch
|
||||
</InlineToolRow>
|
||||
)
|
||||
}
|
||||
|
||||
function FailedCompleteToolFixture() {
|
||||
return (
|
||||
<InlineToolRow icon="→" complete={true} pending="Reading file..." failed={true} failure="Read failed">
|
||||
Read src/index.ts
|
||||
</InlineToolRow>
|
||||
)
|
||||
}
|
||||
|
||||
async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) {
|
||||
testSetup = await testRender(component, options)
|
||||
await testSetup.renderOnce()
|
||||
|
|
@ -173,6 +189,18 @@ describe("TUI inline tool wrapping", () => {
|
|||
expect(toolDisplay("plugin_tool")).toBe("generic")
|
||||
})
|
||||
|
||||
test("replaces pending copy when a tool fails before completion", async () => {
|
||||
const frame = await renderFrame(() => <FailedPendingToolFixture />, { width: 72, height: 3 })
|
||||
expect(frame).toContain("Patch failed")
|
||||
expect(frame).not.toContain("Preparing patch")
|
||||
})
|
||||
|
||||
test("preserves useful completed copy when a tool fails", async () => {
|
||||
const frame = await renderFrame(() => <FailedCompleteToolFixture />, { width: 72, height: 3 })
|
||||
expect(frame).toContain("Read src/index.ts")
|
||||
expect(frame).not.toContain("Read failed")
|
||||
})
|
||||
|
||||
test("filters malformed nested tool wire data", () => {
|
||||
expect(
|
||||
parseApplyPatchFiles([
|
||||
|
|
|
|||
|
|
@ -116,13 +116,14 @@ use the mcp_everything tool to add the number 3 and 4
|
|||
|
||||
Here are all the options for configuring a local MCP server.
|
||||
|
||||
| Option | Type | Required | Description |
|
||||
| ------------- | ------- | -------- | ----------------------------------------------------------------------------------- |
|
||||
| `type` | String | Y | Type of MCP server connection, must be `"local"`. |
|
||||
| `command` | Array | Y | Command and arguments to run the MCP server. |
|
||||
| `environment` | Object | | Environment variables to set when running the server. |
|
||||
| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
|
||||
| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
|
||||
| Option | Type | Required | Description |
|
||||
| ------------- | ------- | -------- | ---------------------------------------------------------------------------------------- |
|
||||
| `type` | String | Y | Type of MCP server connection, must be `"local"`. |
|
||||
| `command` | Array | Y | Command and arguments to run the MCP server. |
|
||||
| `cwd` | String | | Working directory for the MCP server process. Relative paths resolve from the workspace. |
|
||||
| `environment` | Object | | Environment variables to set when running the server. |
|
||||
| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
|
||||
| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue