mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-18 21:53:12 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fdd4fddb6 |
@@ -534,11 +534,14 @@ describe("prompt submit worktree selection", () => {
|
|||||||
id: expect.stringMatching(/^msg_/),
|
id: expect.stringMatching(/^msg_/),
|
||||||
command: "review",
|
command: "review",
|
||||||
arguments: "staged changes",
|
arguments: "staged changes",
|
||||||
agent: "agent",
|
|
||||||
model: { id: "model", providerID: "provider", variant: "high" },
|
|
||||||
files: [],
|
files: [],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
expect(switchedAgents).toEqual([{ sessionID: "session-1", agent: "agent" }])
|
||||||
|
expect(switchedModels).toEqual([
|
||||||
|
{ sessionID: "session-1", model: { id: "model", providerID: "provider", variant: "high" } },
|
||||||
|
])
|
||||||
|
expect(sessionRequestOrder).toEqual(["agent", "model"])
|
||||||
expect(serverSessionSyncs).toBe(0)
|
expect(serverSessionSyncs).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -61,23 +61,39 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
|||||||
input.serverSync.session.set("session_status", input.draft.sessionID, { type: "idle" })
|
input.serverSync.session.set("session_status", input.draft.sessionID, { type: "idle" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const select = async () => {
|
||||||
|
const session = input.session()
|
||||||
|
if (session?.agent !== input.draft.agent) {
|
||||||
|
await input.api.switchAgent({ sessionID: input.draft.sessionID, agent: input.draft.agent })
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
session?.model?.providerID === input.draft.model.providerID &&
|
||||||
|
session.model.id === input.draft.model.modelID &&
|
||||||
|
(session.model.variant ?? "default") === (input.draft.variant ?? "default")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await input.api.switchModel({
|
||||||
|
sessionID: input.draft.sessionID,
|
||||||
|
model: {
|
||||||
|
id: input.draft.model.modelID,
|
||||||
|
providerID: input.draft.model.providerID,
|
||||||
|
variant: input.draft.variant,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const [head, ...tail] = text.split(" ")
|
const [head, ...tail] = text.split(" ")
|
||||||
const cmd = head?.startsWith("/") ? head.slice(1) : undefined
|
const cmd = head?.startsWith("/") ? head.slice(1) : undefined
|
||||||
if (cmd && input.sync.data.command.find((item) => item.name === cmd)) {
|
if (cmd && input.sync.data.command.find((item) => item.name === cmd)) {
|
||||||
setBusy()
|
setBusy()
|
||||||
try {
|
try {
|
||||||
|
await select()
|
||||||
const messageID = Identifier.ascending("message")
|
const messageID = Identifier.ascending("message")
|
||||||
await input.api.command({
|
await input.api.command({
|
||||||
sessionID: input.draft.sessionID,
|
sessionID: input.draft.sessionID,
|
||||||
id: messageID,
|
id: messageID,
|
||||||
command: cmd,
|
command: cmd,
|
||||||
arguments: tail.join(" "),
|
arguments: tail.join(" "),
|
||||||
agent: input.draft.agent,
|
|
||||||
model: {
|
|
||||||
id: input.draft.model.modelID,
|
|
||||||
providerID: input.draft.model.providerID,
|
|
||||||
variant: input.draft.variant,
|
|
||||||
},
|
|
||||||
files: await Promise.all(
|
files: await Promise.all(
|
||||||
images.map(async (attachment) => ({
|
images.map(async (attachment) => ({
|
||||||
uri: await blobDataUrl(attachment.blob, attachment.mime),
|
uri: await blobDataUrl(attachment.blob, attachment.mime),
|
||||||
@@ -118,24 +134,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = input.session()
|
await select()
|
||||||
if (session?.agent !== input.draft.agent) {
|
|
||||||
await input.api.switchAgent({ sessionID: input.draft.sessionID, agent: input.draft.agent })
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
session?.model?.providerID !== input.draft.model.providerID ||
|
|
||||||
session.model.id !== input.draft.model.modelID ||
|
|
||||||
(session.model.variant ?? "default") !== (input.draft.variant ?? "default")
|
|
||||||
) {
|
|
||||||
await input.api.switchModel({
|
|
||||||
sessionID: input.draft.sessionID,
|
|
||||||
model: {
|
|
||||||
id: input.draft.model.modelID,
|
|
||||||
providerID: input.draft.model.providerID,
|
|
||||||
variant: input.draft.variant,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const admitted = await input.api.prompt({
|
const admitted = await input.api.prompt({
|
||||||
sessionID: input.draft.sessionID,
|
sessionID: input.draft.sessionID,
|
||||||
@@ -469,14 +468,23 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
clearInput()
|
clearInput()
|
||||||
const messageID = Identifier.ascending("message")
|
const messageID = Identifier.ascending("message")
|
||||||
submissionServerSync.session.set("session_status", session.id, { type: "busy" })
|
submissionServerSync.session.set("session_status", session.id, { type: "busy" })
|
||||||
void submissionSDK.api.session
|
void (async () => {
|
||||||
.command({
|
if (session.agent !== agent)
|
||||||
|
await submissionSDK.api.session.switchAgent({ sessionID: session.id, agent })
|
||||||
|
if (
|
||||||
|
session.model?.providerID !== model.providerID ||
|
||||||
|
session.model.id !== model.modelID ||
|
||||||
|
(session.model.variant ?? "default") !== (variant ?? "default")
|
||||||
|
)
|
||||||
|
await submissionSDK.api.session.switchModel({
|
||||||
|
sessionID: session.id,
|
||||||
|
model: { id: model.modelID, providerID: model.providerID, variant },
|
||||||
|
})
|
||||||
|
await submissionSDK.api.session.command({
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
id: messageID,
|
id: messageID,
|
||||||
command: commandName,
|
command: commandName,
|
||||||
arguments: args.join(" "),
|
arguments: args.join(" "),
|
||||||
agent,
|
|
||||||
model: { id: model.modelID, providerID: model.providerID, variant },
|
|
||||||
files: await Promise.all(
|
files: await Promise.all(
|
||||||
images.map(async (attachment) => ({
|
images.map(async (attachment) => ({
|
||||||
uri: await blobDataUrl(attachment.blob, attachment.mime),
|
uri: await blobDataUrl(attachment.blob, attachment.mime),
|
||||||
@@ -484,14 +492,14 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
})().catch((err) => {
|
||||||
submissionServerSync.session.set("session_status", session.id, { type: "idle" })
|
submissionServerSync.session.set("session_status", session.id, { type: "idle" })
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("prompt.toast.commandSendFailed.title"),
|
title: language.t("prompt.toast.commandSendFailed.title"),
|
||||||
description: formatServerError(err, language.t, language.t("common.requestFailed")),
|
description: formatServerError(err, language.t, language.t("common.requestFailed")),
|
||||||
})
|
|
||||||
restoreInput()
|
|
||||||
})
|
})
|
||||||
|
restoreInput()
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,8 +192,6 @@ export type Endpoint5_13Input = {
|
|||||||
readonly id?: SessionMessage.ID | undefined
|
readonly id?: SessionMessage.ID | undefined
|
||||||
readonly command: string
|
readonly command: string
|
||||||
readonly arguments?: string | undefined
|
readonly arguments?: string | undefined
|
||||||
readonly agent?: Agent.ID | undefined
|
|
||||||
readonly model?: Model.Ref | undefined
|
|
||||||
readonly files?: ReadonlyArray<PromptInput.FileAttachment> | undefined
|
readonly files?: ReadonlyArray<PromptInput.FileAttachment> | undefined
|
||||||
readonly agents?: ReadonlyArray<AgentAttachment> | undefined
|
readonly agents?: ReadonlyArray<AgentAttachment> | undefined
|
||||||
readonly skills?: ReadonlyArray<PromptInput.SkillAttachment> | undefined
|
readonly skills?: ReadonlyArray<PromptInput.SkillAttachment> | undefined
|
||||||
|
|||||||
@@ -423,8 +423,6 @@ const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13I
|
|||||||
id: input["id"],
|
id: input["id"],
|
||||||
command: input["command"],
|
command: input["command"],
|
||||||
arguments: input["arguments"],
|
arguments: input["arguments"],
|
||||||
agent: input["agent"],
|
|
||||||
model: input["model"],
|
|
||||||
files: input["files"],
|
files: input["files"],
|
||||||
agents: input["agents"],
|
agents: input["agents"],
|
||||||
skills: input["skills"],
|
skills: input["skills"],
|
||||||
|
|||||||
@@ -627,8 +627,6 @@ export function make(options: ClientOptions) {
|
|||||||
id: input["id"],
|
id: input["id"],
|
||||||
command: input["command"],
|
command: input["command"],
|
||||||
arguments: input["arguments"],
|
arguments: input["arguments"],
|
||||||
agent: input["agent"],
|
|
||||||
model: input["model"],
|
|
||||||
files: input["files"],
|
files: input["files"],
|
||||||
agents: input["agents"],
|
agents: input["agents"],
|
||||||
skills: input["skills"],
|
skills: input["skills"],
|
||||||
|
|||||||
@@ -3511,8 +3511,6 @@ export type SessionCommandInput = {
|
|||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly command: string
|
readonly command: string
|
||||||
readonly arguments?: string | null
|
readonly arguments?: string | null
|
||||||
readonly agent?: string | null
|
|
||||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly uri: string
|
readonly uri: string
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
@@ -3534,8 +3532,6 @@ export type SessionCommandInput = {
|
|||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly command: string
|
readonly command: string
|
||||||
readonly arguments?: string | null
|
readonly arguments?: string | null
|
||||||
readonly agent?: string | null
|
|
||||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly uri: string
|
readonly uri: string
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
@@ -3557,8 +3553,6 @@ export type SessionCommandInput = {
|
|||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly command: string
|
readonly command: string
|
||||||
readonly arguments?: string | null
|
readonly arguments?: string | null
|
||||||
readonly agent?: string | null
|
|
||||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly uri: string
|
readonly uri: string
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
@@ -3576,58 +3570,10 @@ export type SessionCommandInput = {
|
|||||||
readonly delivery?: ("steer" | "queue") | null
|
readonly delivery?: ("steer" | "queue") | null
|
||||||
readonly resume?: boolean | null
|
readonly resume?: boolean | null
|
||||||
}["arguments"]
|
}["arguments"]
|
||||||
readonly agent?: {
|
|
||||||
readonly id?: string | null
|
|
||||||
readonly command: string
|
|
||||||
readonly arguments?: string | null
|
|
||||||
readonly agent?: string | null
|
|
||||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
|
||||||
readonly files?: ReadonlyArray<{
|
|
||||||
readonly uri: string
|
|
||||||
readonly name?: string
|
|
||||||
readonly description?: string
|
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
|
||||||
}>
|
|
||||||
readonly agents?: ReadonlyArray<{
|
|
||||||
readonly name: string
|
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
|
||||||
}>
|
|
||||||
readonly skills?: ReadonlyArray<{
|
|
||||||
readonly id: string
|
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
|
||||||
}>
|
|
||||||
readonly delivery?: ("steer" | "queue") | null
|
|
||||||
readonly resume?: boolean | null
|
|
||||||
}["agent"]
|
|
||||||
readonly model?: {
|
|
||||||
readonly id?: string | null
|
|
||||||
readonly command: string
|
|
||||||
readonly arguments?: string | null
|
|
||||||
readonly agent?: string | null
|
|
||||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
|
||||||
readonly files?: ReadonlyArray<{
|
|
||||||
readonly uri: string
|
|
||||||
readonly name?: string
|
|
||||||
readonly description?: string
|
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
|
||||||
}>
|
|
||||||
readonly agents?: ReadonlyArray<{
|
|
||||||
readonly name: string
|
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
|
||||||
}>
|
|
||||||
readonly skills?: ReadonlyArray<{
|
|
||||||
readonly id: string
|
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
|
||||||
}>
|
|
||||||
readonly delivery?: ("steer" | "queue") | null
|
|
||||||
readonly resume?: boolean | null
|
|
||||||
}["model"]
|
|
||||||
readonly files?: {
|
readonly files?: {
|
||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly command: string
|
readonly command: string
|
||||||
readonly arguments?: string | null
|
readonly arguments?: string | null
|
||||||
readonly agent?: string | null
|
|
||||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly uri: string
|
readonly uri: string
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
@@ -3649,8 +3595,6 @@ export type SessionCommandInput = {
|
|||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly command: string
|
readonly command: string
|
||||||
readonly arguments?: string | null
|
readonly arguments?: string | null
|
||||||
readonly agent?: string | null
|
|
||||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly uri: string
|
readonly uri: string
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
@@ -3672,8 +3616,6 @@ export type SessionCommandInput = {
|
|||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly command: string
|
readonly command: string
|
||||||
readonly arguments?: string | null
|
readonly arguments?: string | null
|
||||||
readonly agent?: string | null
|
|
||||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly uri: string
|
readonly uri: string
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
@@ -3695,8 +3637,6 @@ export type SessionCommandInput = {
|
|||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly command: string
|
readonly command: string
|
||||||
readonly arguments?: string | null
|
readonly arguments?: string | null
|
||||||
readonly agent?: string | null
|
|
||||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly uri: string
|
readonly uri: string
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
@@ -3718,8 +3658,6 @@ export type SessionCommandInput = {
|
|||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly command: string
|
readonly command: string
|
||||||
readonly arguments?: string | null
|
readonly arguments?: string | null
|
||||||
readonly agent?: string | null
|
|
||||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly uri: string
|
readonly uri: string
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ import { AgentPlugin } from "./agent.js"
|
|||||||
import { CommandPlugin } from "./command.js"
|
import { CommandPlugin } from "./command.js"
|
||||||
import { PlanPlugin } from "./plan.js"
|
import { PlanPlugin } from "./plan.js"
|
||||||
import { ModelsDevPlugin } from "./models-dev.js"
|
import { ModelsDevPlugin } from "./models-dev.js"
|
||||||
import { McpCodeModePlugin } from "./mcp-codemode.js"
|
|
||||||
import { ProviderPlugins } from "./provider.js"
|
import { ProviderPlugins } from "./provider.js"
|
||||||
import { WebSearchPlugins } from "./websearch/index.js"
|
import { WebSearchPlugins } from "./websearch/index.js"
|
||||||
import { PluginRuntime } from "./runtime.js"
|
import { PluginRuntime } from "./runtime.js"
|
||||||
@@ -95,7 +94,6 @@ const services = Effect.fn("PluginInternal.services")(function* () {
|
|||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const locationMutation = yield* LocationMutation.Service
|
const locationMutation = yield* LocationMutation.Service
|
||||||
const models = yield* ModelsDev.Service
|
const models = yield* ModelsDev.Service
|
||||||
const mcpCodeMode = yield* McpCodeModePlugin.Service
|
|
||||||
const npm = yield* Npm.Service
|
const npm = yield* Npm.Service
|
||||||
const permission = yield* Permission.Service
|
const permission = yield* Permission.Service
|
||||||
const runtime = yield* PluginRuntime.Service
|
const runtime = yield* PluginRuntime.Service
|
||||||
@@ -133,7 +131,6 @@ const services = Effect.fn("PluginInternal.services")(function* () {
|
|||||||
Context.make(Location.Service, location),
|
Context.make(Location.Service, location),
|
||||||
Context.make(LocationMutation.Service, locationMutation),
|
Context.make(LocationMutation.Service, locationMutation),
|
||||||
Context.make(ModelsDev.Service, models),
|
Context.make(ModelsDev.Service, models),
|
||||||
Context.make(McpCodeModePlugin.Service, mcpCodeMode),
|
|
||||||
Context.make(Npm.Service, npm),
|
Context.make(Npm.Service, npm),
|
||||||
Context.make(Permission.Service, permission),
|
Context.make(Permission.Service, permission),
|
||||||
Context.make(PluginRuntime.Service, runtime),
|
Context.make(PluginRuntime.Service, runtime),
|
||||||
@@ -178,7 +175,6 @@ export const requirements = LayerNode.group([
|
|||||||
Location.node,
|
Location.node,
|
||||||
LocationMutation.node,
|
LocationMutation.node,
|
||||||
ModelsDev.node,
|
ModelsDev.node,
|
||||||
McpCodeModePlugin.node,
|
|
||||||
Npm.node,
|
Npm.node,
|
||||||
Permission.node,
|
Permission.node,
|
||||||
PluginRuntime.node,
|
PluginRuntime.node,
|
||||||
@@ -206,7 +202,6 @@ const pre = [
|
|||||||
SkillPlugin.Plugin,
|
SkillPlugin.Plugin,
|
||||||
...SystemPromptPlugin.Plugins,
|
...SystemPromptPlugin.Plugins,
|
||||||
ModelsDevPlugin,
|
ModelsDevPlugin,
|
||||||
McpCodeModePlugin.Plugin,
|
|
||||||
...ProviderPlugins,
|
...ProviderPlugins,
|
||||||
...WebSearchPlugins,
|
...WebSearchPlugins,
|
||||||
PatchTool.Plugin,
|
PatchTool.Plugin,
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
export * as McpCodeModePlugin from "./mcp-codemode.js"
|
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
|
||||||
import { ConfigMCP } from "@opencode-ai/schema/config/mcp"
|
|
||||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
|
||||||
import { Context, Effect, Layer, Scope } from "effect"
|
|
||||||
|
|
||||||
const directToolHosts = new Set(["mcp.cloudflare.com"])
|
|
||||||
type Resolver = (config: typeof ConfigMCP.Server.Type) => boolean | undefined
|
|
||||||
|
|
||||||
export interface Interface {
|
|
||||||
register: (resolver: Resolver) => Effect.Effect<void, never, Scope.Scope>
|
|
||||||
resolve: (config: typeof ConfigMCP.Server.Type) => boolean | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/McpCodeModePlugin") {}
|
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
|
||||||
Service,
|
|
||||||
Effect.sync(() => {
|
|
||||||
let resolvers: Resolver[] = []
|
|
||||||
return Service.of({
|
|
||||||
register: Effect.fn("McpCodeModePlugin.register")(function* (resolver) {
|
|
||||||
resolvers = [...resolvers, resolver]
|
|
||||||
yield* Effect.addFinalizer(() => Effect.sync(() => (resolvers = resolvers.filter((item) => item !== resolver))))
|
|
||||||
}),
|
|
||||||
resolve: (config) =>
|
|
||||||
resolvers
|
|
||||||
.toReversed()
|
|
||||||
.map((resolver) => resolver(config))
|
|
||||||
.find((value) => value !== undefined),
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const node = makeLocationNode({ service: Service, layer, deps: [] })
|
|
||||||
|
|
||||||
export const Plugin = define({
|
|
||||||
id: "opencode.mcp.codemode-compatibility",
|
|
||||||
effect: Effect.fn(function* () {
|
|
||||||
const defaults = yield* Service
|
|
||||||
yield* defaults.register(codeModeCompatibilityDefault)
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
export function codeModeCompatibilityDefault(config: typeof ConfigMCP.Server.Type) {
|
|
||||||
if (config.type !== "remote") return
|
|
||||||
const url = URL.parse(config.url)
|
|
||||||
if (!url || !directToolHosts.has(url.hostname.toLowerCase())) return
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -233,8 +233,6 @@ export interface Interface {
|
|||||||
sessionID: SessionSchema.ID
|
sessionID: SessionSchema.ID
|
||||||
command: string
|
command: string
|
||||||
arguments?: string
|
arguments?: string
|
||||||
agent?: Agent.ID
|
|
||||||
model?: Model.Ref
|
|
||||||
files?: PromptInput.Prompt["files"]
|
files?: PromptInput.Prompt["files"]
|
||||||
agents?: PromptInput.Prompt["agents"]
|
agents?: PromptInput.Prompt["agents"]
|
||||||
skills?: PromptInput.Prompt["skills"]
|
skills?: PromptInput.Prompt["skills"]
|
||||||
@@ -621,13 +619,13 @@ const layer = Layer.effect(
|
|||||||
const evaluated = yield* commands.evaluate({ name: input.command, arguments: input.arguments })
|
const evaluated = yield* commands.evaluate({ name: input.command, arguments: input.arguments })
|
||||||
|
|
||||||
// TODO(v2 commands): decide whether command-level subtask/background execution belongs in v2 commands.
|
// TODO(v2 commands): decide whether command-level subtask/background execution belongs in v2 commands.
|
||||||
const agent = command.agent ?? input.agent
|
const agent = command.agent
|
||||||
const commandAgent = yield* Effect.gen(function* () {
|
const commandAgent = yield* Effect.gen(function* () {
|
||||||
if (!command.agent) return undefined
|
if (!command.agent) return undefined
|
||||||
const agents = yield* Agent.Service.pipe(Effect.provide(locations.get(session.location)))
|
const agents = yield* Agent.Service.pipe(Effect.provide(locations.get(session.location)))
|
||||||
return yield* agents.get(Agent.ID.make(command.agent))
|
return yield* agents.get(Agent.ID.make(command.agent))
|
||||||
})
|
})
|
||||||
const model = command.model ?? commandAgent?.model ?? input.model
|
const model = command.model ?? commandAgent?.model
|
||||||
if (agent !== undefined && session.agent !== Agent.ID.make(agent))
|
if (agent !== undefined && session.agent !== Agent.ID.make(agent))
|
||||||
yield* result.switchAgent({ sessionID: input.sessionID, agent: Agent.ID.make(agent) })
|
yield* result.switchAgent({ sessionID: input.sessionID, agent: Agent.ID.make(agent) })
|
||||||
if (model !== undefined) yield* result.switchModel({ sessionID: input.sessionID, model })
|
if (model !== undefined) yield* result.switchModel({ sessionID: input.sessionID, model })
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { SessionContext } from "./context.js"
|
|||||||
import { SessionGenerate } from "./generate.js"
|
import { SessionGenerate } from "./generate.js"
|
||||||
import { SessionHistory } from "./history.js"
|
import { SessionHistory } from "./history.js"
|
||||||
import { SessionModelHeaders } from "./model-headers.js"
|
import { SessionModelHeaders } from "./model-headers.js"
|
||||||
import { SessionModelHttp } from "./model-http.js"
|
|
||||||
import { SessionPromptCacheKey } from "./prompt-cache-key.js"
|
import { SessionPromptCacheKey } from "./prompt-cache-key.js"
|
||||||
import { SessionRunnerModel } from "./runner/model.js"
|
import { SessionRunnerModel } from "./runner/model.js"
|
||||||
import { SessionSystemPrompt } from "./system-prompt.js"
|
import { SessionSystemPrompt } from "./system-prompt.js"
|
||||||
@@ -80,13 +79,6 @@ export const layer = Layer.effect(
|
|||||||
messages: contextEvent.messages,
|
messages: contextEvent.messages,
|
||||||
tools: hookedTools,
|
tools: hookedTools,
|
||||||
}),
|
}),
|
||||||
{
|
|
||||||
http: SessionModelHttp.middleware(hooks, {
|
|
||||||
sessionID: selection.session.id,
|
|
||||||
agent: selection.agent.id,
|
|
||||||
model: model.ref,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
yield* Effect.logInfo("session generation usage diagnostic", { usage: response.usage })
|
yield* Effect.logInfo("session generation usage diagnostic", { usage: response.usage })
|
||||||
return response.text
|
return response.text
|
||||||
|
|||||||
@@ -2,17 +2,12 @@ export * as McpTool from "./mcp.js"
|
|||||||
|
|
||||||
import { ToolFailure } from "@opencode-ai/ai"
|
import { ToolFailure } from "@opencode-ai/ai"
|
||||||
import { McpEvent } from "@opencode-ai/schema/mcp-event"
|
import { McpEvent } from "@opencode-ai/schema/mcp-event"
|
||||||
import { Document } from "@opencode-ai/schema/config"
|
|
||||||
import { Context, Effect, Exit, Fiber, type JsonSchema, Layer, Scope, Semaphore, Stream } from "effect"
|
import { Context, Effect, Exit, Fiber, type JsonSchema, Layer, Scope, Semaphore, Stream } from "effect"
|
||||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||||
import { Bus } from "../bus.js"
|
import { Bus } from "../bus.js"
|
||||||
|
|
||||||
import { MCP } from "../mcp/index.js"
|
import { MCP } from "../mcp/index.js"
|
||||||
import { Config } from "../config.js"
|
|
||||||
import { Permission } from "../permission.js"
|
import { Permission } from "../permission.js"
|
||||||
import { Plugin } from "../plugin.js"
|
|
||||||
import { McpCodeModePlugin } from "../plugin/mcp-codemode.js"
|
|
||||||
import { PluginSupervisor } from "../plugin/supervisor.js"
|
|
||||||
import { Tool } from "../tool.js"
|
import { Tool } from "../tool.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,8 +27,6 @@ export const layer = Layer.effect(
|
|||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const mcp = yield* MCP.Service
|
const mcp = yield* MCP.Service
|
||||||
const config = yield* Config.Service
|
|
||||||
const codemode = yield* McpCodeModePlugin.Service
|
|
||||||
const tools = yield* Tool.Service
|
const tools = yield* Tool.Service
|
||||||
const bus = yield* Bus.Service
|
const bus = yield* Bus.Service
|
||||||
const permission = yield* Permission.Service
|
const permission = yield* Permission.Service
|
||||||
@@ -46,23 +39,14 @@ export const layer = Layer.effect(
|
|||||||
const reconcile = lock.withPermit(
|
const reconcile = lock.withPermit(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const discovered = yield* mcp.tools()
|
const discovered = yield* mcp.tools()
|
||||||
const servers = new Map(
|
|
||||||
(yield* config.entries())
|
|
||||||
.filter((entry): entry is Document => entry.type === "document")
|
|
||||||
.flatMap((entry) => Object.entries(entry.info.mcp?.servers ?? {})),
|
|
||||||
)
|
|
||||||
const next = yield* Scope.fork(scope)
|
const next = yield* Scope.fork(scope)
|
||||||
yield* tools
|
yield* tools
|
||||||
.transform((draft) => {
|
.transform((draft) => {
|
||||||
for (const tool of discovered) {
|
for (const tool of discovered) {
|
||||||
const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema
|
const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema
|
||||||
const server = servers.get(tool.server)
|
|
||||||
draft.add({
|
draft.add({
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
options: {
|
options: { namespace: namespace(tool.server), codemode: tool.codemode !== false },
|
||||||
namespace: namespace(tool.server),
|
|
||||||
codemode: (tool.codemode ?? (server ? codemode.resolve(server) : undefined)) !== false,
|
|
||||||
},
|
|
||||||
description: tool.description ?? "",
|
description: tool.description ?? "",
|
||||||
input: {
|
input: {
|
||||||
...schema,
|
...schema,
|
||||||
@@ -138,7 +122,7 @@ export const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const initial = yield* reconcile.pipe(Effect.forkScoped)
|
const initial = yield* reconcile.pipe(Effect.forkScoped)
|
||||||
yield* bus.subscribe([McpEvent.ToolsChanged, Plugin.Event.Updated]).pipe(
|
yield* bus.subscribe(McpEvent.ToolsChanged).pipe(
|
||||||
Stream.runForEach(() => reconcile),
|
Stream.runForEach(() => reconcile),
|
||||||
Effect.forkScoped({ startImmediately: true }),
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
)
|
)
|
||||||
@@ -149,5 +133,5 @@ export const layer = Layer.effect(
|
|||||||
export const node = makeLocationNode({
|
export const node = makeLocationNode({
|
||||||
service: Service,
|
service: Service,
|
||||||
layer,
|
layer,
|
||||||
deps: [Tool.node, MCP.node, McpCodeModePlugin.node, Config.node, Bus.node, Permission.node, PluginSupervisor.node],
|
deps: [Tool.node, MCP.node, Bus.node, Permission.node],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ import { MCP } from "@opencode-ai/core/mcp/index"
|
|||||||
import { MCPClient } from "@opencode-ai/core/mcp/client"
|
import { MCPClient } from "@opencode-ai/core/mcp/client"
|
||||||
import { MCPStdio } from "@opencode-ai/core/mcp/stdio"
|
import { MCPStdio } from "@opencode-ai/core/mcp/stdio"
|
||||||
import { Permission } from "@opencode-ai/core/permission"
|
import { Permission } from "@opencode-ai/core/permission"
|
||||||
import { McpCodeModePlugin } from "@opencode-ai/core/plugin/mcp-codemode"
|
|
||||||
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { Session } from "@opencode-ai/core/session"
|
import { Session } from "@opencode-ai/core/session"
|
||||||
import { McpTool } from "@opencode-ai/core/tool/mcp"
|
import { McpTool } from "@opencode-ai/core/tool/mcp"
|
||||||
@@ -338,16 +336,12 @@ const permissions = Layer.mock(Permission.Service, {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const events = Layer.mock(Bus.Service, { subscribe: () => Stream.never })
|
const events = Layer.mock(Bus.Service, { subscribe: () => Stream.never })
|
||||||
const plugins = Layer.succeed(PluginSupervisor.Service, PluginSupervisor.Service.of({ flush: Effect.void }))
|
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
AppNodeBuilder.build(LayerNode.group([Tool.node, McpTool.node]), [
|
AppNodeBuilder.build(LayerNode.group([Tool.node, McpTool.node]), [
|
||||||
[MCP.node, mcp],
|
[MCP.node, mcp],
|
||||||
[McpCodeModePlugin.node, McpCodeModePlugin.layer],
|
|
||||||
[Config.node, Config.testLayer()],
|
|
||||||
[Permission.node, permissions],
|
[Permission.node, permissions],
|
||||||
[Bus.node, events],
|
[Bus.node, events],
|
||||||
[Image.node, imagePassthrough],
|
[Image.node, imagePassthrough],
|
||||||
[PluginSupervisor.node, plugins],
|
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { ConfigMCP } from "@opencode-ai/schema/config/mcp"
|
|
||||||
import { codeModeCompatibilityDefault, McpCodeModePlugin } from "@opencode-ai/core/plugin/mcp-codemode"
|
|
||||||
import { describe, expect, it } from "bun:test"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
describe("MCP Code Mode compatibility defaults", () => {
|
|
||||||
it("keeps Cloudflare's Code Mode MCP server direct by default", () => {
|
|
||||||
const config = new ConfigMCP.Remote({ type: "remote", url: "https://mcp.cloudflare.com/mcp" })
|
|
||||||
expect(codeModeCompatibilityDefault(config)).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("does not change Cloudflare's product-specific MCP servers", () => {
|
|
||||||
const config = new ConfigMCP.Remote({ type: "remote", url: "https://docs.mcp.cloudflare.com/mcp" })
|
|
||||||
expect(codeModeCompatibilityDefault(config)).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("does not change local or unrelated remote servers", () => {
|
|
||||||
const local = new ConfigMCP.Local({ type: "local", command: ["server"] })
|
|
||||||
const remote = new ConfigMCP.Remote({ type: "remote", url: "https://example.com/mcp" })
|
|
||||||
expect(codeModeCompatibilityDefault(local)).toBeUndefined()
|
|
||||||
expect(codeModeCompatibilityDefault(remote)).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("retains a registered direct-tool default", async () => {
|
|
||||||
await Effect.runPromise(
|
|
||||||
Effect.scoped(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const defaults = yield* McpCodeModePlugin.Service
|
|
||||||
yield* defaults.register(codeModeCompatibilityDefault)
|
|
||||||
const config = new ConfigMCP.Remote({ type: "remote", url: "https://mcp.cloudflare.com/mcp" })
|
|
||||||
expect(defaults.resolve(config)).toBe(false)
|
|
||||||
}).pipe(Effect.provide(McpCodeModePlugin.layer)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -51,17 +51,15 @@ import { Effect, Layer, Schema, Stream } from "effect"
|
|||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
const requests: LLMRequest[] = []
|
const requests: LLMRequest[] = []
|
||||||
let hasHttpMiddleware = false
|
|
||||||
let instruction: string | Instructions.Unavailable = "Initial context"
|
let instruction: string | Instructions.Unavailable = "Initial context"
|
||||||
const sessionID = SessionSchema.ID.make("ses_generate_test")
|
const sessionID = SessionSchema.ID.make("ses_generate_test")
|
||||||
|
|
||||||
const model = LanguageModel.make({ id: "generate-model", provider: "test", route: OpenAIChat.route })
|
const model = LanguageModel.make({ id: "generate-model", provider: "test", route: OpenAIChat.route })
|
||||||
const client = Layer.mock(LLMClient.Service)({
|
const client = Layer.mock(LLMClient.Service)({
|
||||||
stream: () => Stream.die(new Error("unused")),
|
stream: () => Stream.die(new Error("unused")),
|
||||||
generate: (request, options) =>
|
generate: (request) =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
requests.push(request)
|
requests.push(request)
|
||||||
hasHttpMiddleware = typeof options?.http === "function"
|
|
||||||
const response = LLMResponse.fromEvents([
|
const response = LLMResponse.fromEvents([
|
||||||
LLMEvent.stepStart({ index: 0 }),
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
LLMEvent.textStart({ id: "generate" }),
|
LLMEvent.textStart({ id: "generate" }),
|
||||||
@@ -223,7 +221,6 @@ const setup = Effect.gen(function* () {
|
|||||||
it.effect("generates from fresh settled Session context without durable mutation", () =>
|
it.effect("generates from fresh settled Session context without durable mutation", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
requests.length = 0
|
requests.length = 0
|
||||||
hasHttpMiddleware = false
|
|
||||||
instruction = "Initial context"
|
instruction = "Initial context"
|
||||||
const { db, bus, instructions } = yield* setup
|
const { db, bus, instructions } = yield* setup
|
||||||
yield* InstructionState.prepare(db, bus, instructions, sessionID)
|
yield* InstructionState.prepare(db, bus, instructions, sessionID)
|
||||||
@@ -301,7 +298,6 @@ it.effect("generates from fresh settled Session context without durable mutation
|
|||||||
|
|
||||||
expect(result).toBe("Transient answer")
|
expect(result).toBe("Transient answer")
|
||||||
expect(requests).toHaveLength(1)
|
expect(requests).toHaveLength(1)
|
||||||
expect(hasHttpMiddleware).toBe(true)
|
|
||||||
expect(requests[0]?.model).toBe(model)
|
expect(requests[0]?.model).toBe(model)
|
||||||
expect(requests[0]?.system[0]?.text).toBe("Hooked system")
|
expect(requests[0]?.system[0]?.text).toBe("Hooked system")
|
||||||
expect(requests[0]?.system.map((part) => part.text)).toContain("Initial context")
|
expect(requests[0]?.system.map((part) => part.text)).toContain("Initial context")
|
||||||
|
|||||||
@@ -341,8 +341,6 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
|||||||
id: SessionMessage.ID.pipe(Schema.optional),
|
id: SessionMessage.ID.pipe(Schema.optional),
|
||||||
command: Schema.String,
|
command: Schema.String,
|
||||||
arguments: Schema.String.pipe(Schema.optional),
|
arguments: Schema.String.pipe(Schema.optional),
|
||||||
agent: Agent.ID.pipe(Schema.optional),
|
|
||||||
model: Model.Ref.pipe(Schema.optional),
|
|
||||||
files: PromptInput.Prompt.fields.files,
|
files: PromptInput.Prompt.fields.files,
|
||||||
agents: PromptInput.Prompt.fields.agents,
|
agents: PromptInput.Prompt.fields.agents,
|
||||||
skills: PromptInput.Prompt.fields.skills,
|
skills: PromptInput.Prompt.fields.skills,
|
||||||
|
|||||||
@@ -356,8 +356,6 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||||||
id: ctx.payload.id,
|
id: ctx.payload.id,
|
||||||
command: ctx.payload.command,
|
command: ctx.payload.command,
|
||||||
arguments: ctx.payload.arguments,
|
arguments: ctx.payload.arguments,
|
||||||
agent: ctx.payload.agent,
|
|
||||||
model: ctx.payload.model,
|
|
||||||
files: ctx.payload.files,
|
files: ctx.payload.files,
|
||||||
agents: ctx.payload.agents,
|
agents: ctx.payload.agents,
|
||||||
skills: ctx.payload.skills,
|
skills: ctx.payload.skills,
|
||||||
|
|||||||
@@ -547,12 +547,6 @@ export function getToolInfo(
|
|||||||
title: i18n.t("ui.tool.shell"),
|
title: i18n.t("ui.tool.shell"),
|
||||||
subtitle: input.command,
|
subtitle: input.command,
|
||||||
}
|
}
|
||||||
case "execute":
|
|
||||||
return {
|
|
||||||
icon: "console",
|
|
||||||
title: i18n.t("ui.tool.execute"),
|
|
||||||
subtitle: input.code,
|
|
||||||
}
|
|
||||||
case "edit":
|
case "edit":
|
||||||
return {
|
return {
|
||||||
icon: "code-lines",
|
icon: "code-lines",
|
||||||
@@ -1580,7 +1574,6 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|||||||
if (typeof value === "string" && value) return value
|
if (typeof value === "string" && value) return value
|
||||||
return taskId()
|
return taskId()
|
||||||
})
|
})
|
||||||
const toolError = createMemo(() => partError(part(), i18n.t("ui.toolErrorCard.failed")))
|
|
||||||
|
|
||||||
const render = createMemo(() => ToolRegistry.render(part().tool) ?? GenericTool)
|
const render = createMemo(() => ToolRegistry.render(part().tool) ?? GenericTool)
|
||||||
const controlledOpen = () => (props.onToolOpenChange ? (props.toolOpen ?? props.defaultOpen) : undefined)
|
const controlledOpen = () => (props.onToolOpenChange ? (props.toolOpen ?? props.defaultOpen) : undefined)
|
||||||
@@ -1590,7 +1583,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|||||||
<Show when={!hideQuestion()}>
|
<Show when={!hideQuestion()}>
|
||||||
<div data-component="tool-part-wrapper" data-timeline-part-id={part().id}>
|
<div data-component="tool-part-wrapper" data-timeline-part-id={part().id}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={toolError()}>
|
<Match when={part().state.status === "error" && (part().state as any).error}>
|
||||||
{(error) => {
|
{(error) => {
|
||||||
const cleaned = error().replace("Error: ", "")
|
const cleaned = error().replace("Error: ", "")
|
||||||
if (part().tool === "question" && cleaned.includes("dismissed this question")) {
|
if (part().tool === "question" && cleaned.includes("dismissed this question")) {
|
||||||
@@ -1651,26 +1644,6 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function partError(part: ToolPart, fallback: string) {
|
|
||||||
if (part.state.status === "error") return part.state.error
|
|
||||||
if (part.tool !== "execute" || !("metadata" in part.state)) return undefined
|
|
||||||
const calls = part.state.metadata?.toolCalls
|
|
||||||
const failed =
|
|
||||||
part.state.metadata?.error === true ||
|
|
||||||
(Array.isArray(calls) &&
|
|
||||||
calls.some(
|
|
||||||
(call) =>
|
|
||||||
call !== null &&
|
|
||||||
typeof call === "object" &&
|
|
||||||
!Array.isArray(call) &&
|
|
||||||
"status" in call &&
|
|
||||||
call.status === "error",
|
|
||||||
))
|
|
||||||
if (!failed) return undefined
|
|
||||||
if ("output" in part.state && typeof part.state.output === "string" && part.state.output) return part.state.output
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MessageDivider(props: { label: string }) {
|
export function MessageDivider(props: { label: string }) {
|
||||||
return (
|
return (
|
||||||
<div data-component="compaction-part">
|
<div data-component="compaction-part">
|
||||||
@@ -2131,84 +2104,6 @@ ToolRegistry.register({
|
|||||||
|
|
||||||
ToolRegistry.register({ name: "subagent", render: ToolRegistry.render("task") })
|
ToolRegistry.register({ name: "subagent", render: ToolRegistry.render("task") })
|
||||||
|
|
||||||
function ConsoleOutput(props: { copy: string; children: JSX.Element }) {
|
|
||||||
const i18n = useI18n()
|
|
||||||
const [copied, setCopied] = createSignal(false)
|
|
||||||
|
|
||||||
const copy = async () => {
|
|
||||||
if (!props.copy) return
|
|
||||||
if (!(await writeClipboard(props.copy))) return
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-component="bash-output" dir="ltr">
|
|
||||||
<div data-slot="bash-copy">
|
|
||||||
<TooltipV2 value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top">
|
|
||||||
<IconButtonV2
|
|
||||||
icon={<IconV2 name={copied() ? "check" : "outline-copy"} size="small" />}
|
|
||||||
size="normal"
|
|
||||||
variant="ghost-muted"
|
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
|
||||||
onClick={copy}
|
|
||||||
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
|
||||||
/>
|
|
||||||
</TooltipV2>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
data-slot="bash-scroll"
|
|
||||||
data-scrollable
|
|
||||||
tabIndex={0}
|
|
||||||
role="region"
|
|
||||||
aria-label={i18n.t("ui.scrollView.ariaLabel")}
|
|
||||||
>
|
|
||||||
<pre data-slot="bash-pre">
|
|
||||||
<code>{props.children}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolRegistry.register({
|
|
||||||
name: "execute",
|
|
||||||
render(props) {
|
|
||||||
const i18n = useI18n()
|
|
||||||
const pending = () => props.status === "pending" || props.status === "streaming" || props.status === "running"
|
|
||||||
const code = createMemo(() => (typeof props.input.code === "string" ? props.input.code : ""))
|
|
||||||
const text = createMemo(() => {
|
|
||||||
const output = stripAnsi(props.output ?? "").replace(/\r\n?/g, "\n")
|
|
||||||
return `${code()}${output ? "\n\n" + output : ""}`
|
|
||||||
})
|
|
||||||
const sawPending = pending()
|
|
||||||
return (
|
|
||||||
<BasicTool
|
|
||||||
{...props}
|
|
||||||
icon="console"
|
|
||||||
allowOpenWhilePending
|
|
||||||
trigger={(open) => (
|
|
||||||
<div data-slot="basic-tool-tool-info-structured">
|
|
||||||
<span data-slot="basic-tool-tool-indicator">
|
|
||||||
<Icon name="console" size="small" />
|
|
||||||
</span>
|
|
||||||
<div data-slot="basic-tool-tool-info-main">
|
|
||||||
<span data-slot="basic-tool-tool-title">
|
|
||||||
<TextShimmer text={i18n.t("ui.tool.execute")} active={pending()} />
|
|
||||||
</span>
|
|
||||||
<Show when={!open() && code()}>
|
|
||||||
<ShellSubmessage text={code()} animate={sawPending} />
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ConsoleOutput copy={text()}>{text()}</ConsoleOutput>
|
|
||||||
</BasicTool>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
ToolRegistry.register({
|
ToolRegistry.register({
|
||||||
name: "shell",
|
name: "shell",
|
||||||
render(props) {
|
render(props) {
|
||||||
@@ -2221,6 +2116,17 @@ ToolRegistry.register({
|
|||||||
const out = stripAnsi(props.output || props.metadata.output || "").replace(/\r\n?/g, "\n")
|
const out = stripAnsi(props.output || props.metadata.output || "").replace(/\r\n?/g, "\n")
|
||||||
return `${command()}${out ? "\n\n" + out : ""}`
|
return `${command()}${out ? "\n\n" + out : ""}`
|
||||||
})
|
})
|
||||||
|
const [copied, setCopied] = createSignal(false)
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
const content = command()
|
||||||
|
if (!content) return
|
||||||
|
if (await writeClipboard(content)) {
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
@@ -2239,12 +2145,36 @@ ToolRegistry.register({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ConsoleOutput copy={command()}>
|
<div data-component="bash-output" dir="ltr">
|
||||||
<span data-slot="bash-prompt" aria-hidden="true">
|
<div data-slot="bash-copy">
|
||||||
{"$ "}
|
<TooltipV2 value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top">
|
||||||
</span>
|
<IconButtonV2
|
||||||
{text()}
|
icon={<IconV2 name={copied() ? "check" : "outline-copy"} size="small" />}
|
||||||
</ConsoleOutput>
|
size="normal"
|
||||||
|
variant="ghost-muted"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onClick={handleCopy}
|
||||||
|
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
||||||
|
/>
|
||||||
|
</TooltipV2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-slot="bash-scroll"
|
||||||
|
data-scrollable
|
||||||
|
tabIndex={0}
|
||||||
|
role="region"
|
||||||
|
aria-label={i18n.t("ui.scrollView.ariaLabel")}
|
||||||
|
>
|
||||||
|
<pre data-slot="bash-pre">
|
||||||
|
<code>
|
||||||
|
<span data-slot="bash-prompt" aria-hidden="true">
|
||||||
|
{"$ "}
|
||||||
|
</span>
|
||||||
|
{text()}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</BasicTool>
|
</BasicTool>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -71,9 +71,8 @@ describe("partDefaultOpen", () => {
|
|||||||
).toBe(true)
|
).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("applies shell defaults to console tools", () => {
|
test("preserves shell defaults", () => {
|
||||||
expect(partDefaultOpen(tool("shell", {}), true, false)).toBe(true)
|
expect(partDefaultOpen(tool("shell", {}), true, false)).toBe(true)
|
||||||
expect(partDefaultOpen(tool("execute", {}), true, false)).toBe(true)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function deletionOnly(part: ToolPart) {
|
|||||||
|
|
||||||
export function partDefaultOpen(part: PartType, shell = false, edit = false): boolean | undefined {
|
export function partDefaultOpen(part: PartType, shell = false, edit = false): boolean | undefined {
|
||||||
if (part.type !== "tool") return undefined
|
if (part.type !== "tool") return undefined
|
||||||
if (part.tool === "bash" || part.tool === "shell" || part.tool === "execute") return shell
|
if (part.tool === "bash" || part.tool === "shell") return shell
|
||||||
if (part.tool === "edit" || part.tool === "write" || part.tool === "patch" || part.tool === "apply_patch") {
|
if (part.tool === "edit" || part.tool === "write" || part.tool === "patch" || part.tool === "apply_patch") {
|
||||||
if (!edit) return false
|
if (!edit) return false
|
||||||
return !deletionOnly(part)
|
return !deletionOnly(part)
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
|
|||||||
websearch: "ui.tool.websearch",
|
websearch: "ui.tool.websearch",
|
||||||
bash: "ui.tool.shell",
|
bash: "ui.tool.shell",
|
||||||
shell: "ui.tool.shell",
|
shell: "ui.tool.shell",
|
||||||
execute: "ui.tool.execute",
|
|
||||||
patch: "ui.tool.patch",
|
patch: "ui.tool.patch",
|
||||||
apply_patch: "ui.tool.patch",
|
apply_patch: "ui.tool.patch",
|
||||||
question: "ui.tool.questions",
|
question: "ui.tool.questions",
|
||||||
|
|||||||
@@ -1220,15 +1220,23 @@ export function Prompt(props: PromptProps) {
|
|||||||
} else if (slashHead && isCommand) {
|
} else if (slashHead && isCommand) {
|
||||||
move.startSubmit()
|
move.startSubmit()
|
||||||
const model = { providerID: selection.providerID, id: selection.modelID, variant }
|
const model = { providerID: selection.providerID, id: selection.modelID, variant }
|
||||||
|
if (session?.agent !== agent.id) await client.api.session.switchAgent({ sessionID, agent: agent.id })
|
||||||
const cancelCommit = local.model.trackSessionCommit(sessionID, model)
|
const cancelCommit = local.model.trackSessionCommit(sessionID, model)
|
||||||
|
if (
|
||||||
|
session?.model?.providerID !== model.providerID ||
|
||||||
|
session.model.id !== model.id ||
|
||||||
|
(session.model.variant ?? "default") !== (model.variant ?? "default")
|
||||||
|
)
|
||||||
|
await client.api.session.switchModel({ sessionID, model }).catch((error) => {
|
||||||
|
cancelCommit()
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
|
||||||
void client.api.session
|
void client.api.session
|
||||||
.command({
|
.command({
|
||||||
sessionID,
|
sessionID,
|
||||||
command: slashHead.name,
|
command: slashHead.name,
|
||||||
arguments: slashHead.arguments,
|
arguments: slashHead.arguments,
|
||||||
agent: agent.id,
|
|
||||||
model,
|
|
||||||
files: store.prompt.files,
|
files: store.prompt.files,
|
||||||
agents: store.prompt.agents,
|
agents: store.prompt.agents,
|
||||||
skills: store.prompt.skills?.length ? store.prompt.skills : undefined,
|
skills: store.prompt.skills?.length ? store.prompt.skills : undefined,
|
||||||
|
|||||||
@@ -1647,8 +1647,6 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = await resolveSelectedModel(input, client, next)
|
|
||||||
if (next.variant && !selected) throw new Error("Cannot select a variant before selecting a model")
|
|
||||||
input.trace?.write("send.command", { sessionID: input.sessionID, messageID, command: command.name, delivery })
|
input.trace?.write("send.command", { sessionID: input.sessionID, messageID, command: command.name, delivery })
|
||||||
return client.session.command(
|
return client.session.command(
|
||||||
{
|
{
|
||||||
@@ -1656,8 +1654,6 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||||||
id: messageID,
|
id: messageID,
|
||||||
command: command.name,
|
command: command.name,
|
||||||
arguments: command.arguments,
|
arguments: command.arguments,
|
||||||
agent: next.agent,
|
|
||||||
model: selected,
|
|
||||||
files: attachments.files.length ? attachments.files : undefined,
|
files: attachments.files.length ? attachments.files : undefined,
|
||||||
agents: agents.length ? agents : undefined,
|
agents: agents.length ? agents : undefined,
|
||||||
skills: skills.length ? skills : undefined,
|
skills: skills.length ? skills : undefined,
|
||||||
@@ -1700,12 +1696,10 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||||||
const client = sdk
|
const client = sdk
|
||||||
if (next.agent)
|
if (next.agent)
|
||||||
await client.session.switchAgent({ sessionID: input.sessionID, agent: next.agent }, { signal: next.signal })
|
await client.session.switchAgent({ sessionID: input.sessionID, agent: next.agent }, { signal: next.signal })
|
||||||
if (!next.prompt.command) {
|
const selected = await resolveSelectedModel(input, client, next)
|
||||||
const selected = await resolveSelectedModel(input, client, next)
|
if (next.variant && !selected) throw new Error("Cannot select a variant before selecting a model")
|
||||||
if (next.variant && !selected) throw new Error("Cannot select a variant before selecting a model")
|
if (selected)
|
||||||
if (selected)
|
await client.session.switchModel({ sessionID: input.sessionID, model: selected }, { signal: next.signal })
|
||||||
await client.session.switchModel({ sessionID: input.sessionID, model: selected }, { signal: next.signal })
|
|
||||||
}
|
|
||||||
mergePending(await admitPrompt(next, client, delivery))
|
mergePending(await admitPrompt(next, client, delivery))
|
||||||
settlementClient = client
|
settlementClient = client
|
||||||
},
|
},
|
||||||
@@ -1744,6 +1738,12 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (command) {
|
if (command) {
|
||||||
|
if (next.agent)
|
||||||
|
await client.session.switchAgent({ sessionID: input.sessionID, agent: next.agent }, { signal: next.signal })
|
||||||
|
const selected = await resolveSelectedModel(input, client, next)
|
||||||
|
if (next.variant && !selected) throw new Error("Cannot select a variant before selecting a model")
|
||||||
|
if (selected)
|
||||||
|
await client.session.switchModel({ sessionID: input.sessionID, model: selected }, { signal: next.signal })
|
||||||
await runTurnWait(
|
await runTurnWait(
|
||||||
next,
|
next,
|
||||||
messageID,
|
messageID,
|
||||||
|
|||||||
@@ -2855,8 +2855,6 @@ describe("V2 mini transport", () => {
|
|||||||
id: "msg_cmd",
|
id: "msg_cmd",
|
||||||
command: "deploy",
|
command: "deploy",
|
||||||
arguments: "prod",
|
arguments: "prod",
|
||||||
agent: "build",
|
|
||||||
model: { providerID: "test", id: "model" },
|
|
||||||
files: [
|
files: [
|
||||||
{ uri: "file:///tmp/context.txt", name: "context.txt" },
|
{ uri: "file:///tmp/context.txt", name: "context.txt" },
|
||||||
{
|
{
|
||||||
@@ -2868,9 +2866,11 @@ describe("V2 mini transport", () => {
|
|||||||
skills: [{ id: "api-design", mention: { start: 13, end: 24, text: "/api-design" } }],
|
skills: [{ id: "api-design", mention: { start: 13, end: 24, text: "/api-design" } }],
|
||||||
delivery: "steer",
|
delivery: "steer",
|
||||||
})
|
})
|
||||||
// Selection rides the command payload; no separate client-side switch.
|
expect(client.session.switchAgent).toHaveBeenCalledWith({ sessionID: "ses_1", agent: "build" }, expect.anything())
|
||||||
expect(client.session.switchAgent).not.toHaveBeenCalled()
|
expect(client.session.switchModel).toHaveBeenCalledWith(
|
||||||
expect(client.session.switchModel).not.toHaveBeenCalled()
|
{ sessionID: "ses_1", model: { providerID: "test", id: "model" } },
|
||||||
|
expect.anything(),
|
||||||
|
)
|
||||||
await transport.close()
|
await transport.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ const source = {
|
|||||||
"ui.tool.websearch": "Web Search",
|
"ui.tool.websearch": "Web Search",
|
||||||
"ui.tool.websearch.provider": "{{provider}} Web Search",
|
"ui.tool.websearch.provider": "{{provider}} Web Search",
|
||||||
"ui.tool.shell": "Shell",
|
"ui.tool.shell": "Shell",
|
||||||
"ui.tool.execute": "Execute",
|
|
||||||
"ui.tool.patch": "Patch",
|
"ui.tool.patch": "Patch",
|
||||||
"ui.tool.questions": "Questions",
|
"ui.tool.questions": "Questions",
|
||||||
"ui.tool.questions.numbered": "Questions {{number}}",
|
"ui.tool.questions.numbered": "Questions {{number}}",
|
||||||
|
|||||||
Reference in New Issue
Block a user