refactor(core): remove shell description input (#32823)
This commit is contained in:
parent
fbf889db83
commit
d29f5eba92
@ -139,7 +139,7 @@ function toolPart(
|
||||
status: "completed",
|
||||
input,
|
||||
output: lorem(index * 23 + partIndex, outputLength),
|
||||
title: tool === "bash" ? "Verify generated output" : input.filePath || input.path || input.pattern || "completed",
|
||||
title: tool === "bash" ? input.command : input.filePath || input.path || input.pattern || "completed",
|
||||
metadata,
|
||||
time: { start: 1700000000000 + index * 10_000, end: 1700000000000 + index * 10_000 + 400 },
|
||||
},
|
||||
@ -201,7 +201,7 @@ function turn(index: number): Message[] {
|
||||
? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
|
||||
: []),
|
||||
...(index % 7 === 0
|
||||
? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)]
|
||||
? [toolPart(index, 4, "bash", { command: "bun typecheck" }, 620)]
|
||||
: []),
|
||||
...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
|
||||
...(index % 11 === 0 ? [toolPart(index, 10, "websearch", { query: "sample movement notes" }, 240)] : []),
|
||||
@ -295,6 +295,7 @@ export const fixture = {
|
||||
.filter(renderable)
|
||||
.map((part) => part.id),
|
||||
),
|
||||
expandedShellPartID: targetMessages.flatMap((message) => message.parts).find((part) => part.tool === "bash")!.id,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -327,6 +327,18 @@ test.describe("smoke: session timeline", () => {
|
||||
const expectedMessageIDs = fixture.expected.targetMessageIDs
|
||||
await expectSessionTimelineReady(page, expectedPartIDs, expectedMessageIDs, errors)
|
||||
await expectCanScrollToStart(page, expectedPartIDs, expectedMessageIDs, errors)
|
||||
|
||||
const shell = page.locator(`[data-timeline-part-id="${fixture.expected.expandedShellPartID}"]`)
|
||||
const shellTrigger = shell.locator('[data-slot="collapsible-trigger"]')
|
||||
const shellSubtitle = shell.locator('[data-slot="basic-tool-tool-subtitle"]')
|
||||
await expect(shellSubtitle).toHaveCount(0)
|
||||
await expect(shell.locator('[data-slot="bash-pre"]')).toContainText("$ bun typecheck")
|
||||
await shellTrigger.click()
|
||||
await expect(shellTrigger).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(shellSubtitle).toHaveText("bun typecheck")
|
||||
await shellTrigger.click()
|
||||
await expect(shellTrigger).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(shellSubtitle).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -28,9 +28,6 @@ export const Input = Schema.Struct({
|
||||
.annotate({
|
||||
description: `Timeout in milliseconds. Defaults to ${DEFAULT_TIMEOUT_MS} and may not exceed ${MAX_TIMEOUT_MS}.`,
|
||||
}),
|
||||
description: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Concise description of the command's purpose",
|
||||
}),
|
||||
})
|
||||
|
||||
const Output = Schema.Struct({
|
||||
|
||||
@ -134,10 +134,9 @@ describe("BashTool", () => {
|
||||
const definitions = yield* toolDefinitions(registry)
|
||||
expect(definitions.map((tool) => tool.name)).toEqual(["bash"])
|
||||
expect(definitions[0]?.inputSchema).not.toHaveProperty("properties.background")
|
||||
expect(definitions[0]?.inputSchema).not.toHaveProperty("properties.description")
|
||||
expect(yield* toolDefinitions(registry, [{ action: "bash", resource: "*", effect: "deny" }])).toEqual([])
|
||||
expect(
|
||||
yield* settleTool(registry, call({ command: "pwd", description: "Print working directory" })),
|
||||
).toEqual({
|
||||
expect(yield* settleTool(registry, call({ command: "pwd" }))).toEqual({
|
||||
result: { type: "text", value: "hello\n\n\nCommand exited with code 0." },
|
||||
output: {
|
||||
structured: {
|
||||
|
||||
@ -266,7 +266,7 @@ export function shellOutputSnapshot(state: { readonly metadata?: unknown }) {
|
||||
// For shell tools, surface the actual command as the title so it stays visible
|
||||
// before output lands; non-shell tools keep their model-provided title.
|
||||
function toolTitle(toolName: string, input: ToolInput, fallback: string | undefined) {
|
||||
if (isShell(toolName)) return shellCommand(input) ?? stringValue(input.description) ?? fallback ?? toolName
|
||||
if (isShell(toolName)) return shellCommand(input) ?? fallback ?? toolName
|
||||
return fallback || toolName
|
||||
}
|
||||
|
||||
|
||||
@ -623,20 +623,18 @@ function snapQuestion(p: ToolProps<typeof QuestionTool>): ToolSnapshot {
|
||||
|
||||
function scrollBashStart(p: ToolProps<typeof BashTool>): string {
|
||||
const cmd = p.input.command ?? ""
|
||||
const desc = p.input.description || "Shell"
|
||||
const wd = p.input.workdir ?? ""
|
||||
const dir = wd && wd !== "." ? toolPath(wd) : ""
|
||||
if (cmd && desc === "Shell" && !dir) {
|
||||
const formatted = wd && wd !== "." ? toolPath(wd) : ""
|
||||
const dir = formatted === "." ? "" : formatted
|
||||
if (cmd && !dir) {
|
||||
return `$ ${cmd}`
|
||||
}
|
||||
|
||||
const title = dir && !desc.includes(dir) ? `${desc} in ${dir}` : desc
|
||||
|
||||
if (!cmd) {
|
||||
return `# ${title}`
|
||||
return dir ? `# Running in ${dir}` : ""
|
||||
}
|
||||
|
||||
return `# ${title}\n$ ${cmd}`
|
||||
return `# Running in ${dir}\n$ ${cmd}`
|
||||
}
|
||||
|
||||
function scrollBashProgress(p: ToolProps<typeof BashTool>): string {
|
||||
@ -968,11 +966,10 @@ function permList(p: ToolPermissionProps): ToolPermissionInfo {
|
||||
}
|
||||
|
||||
function permBash(p: ToolPermissionProps<typeof BashTool>): ToolPermissionInfo {
|
||||
const title = p.input.description || "Shell command"
|
||||
const cmd = p.input.command || ""
|
||||
return {
|
||||
icon: "#",
|
||||
title,
|
||||
title: "Shell command",
|
||||
lines: cmd ? [`$ ${cmd}`] : p.patterns.map((item) => `- ${item}`),
|
||||
}
|
||||
}
|
||||
|
||||
@ -542,7 +542,7 @@ export const layer = Layer.effect(
|
||||
time: { ...part.state.time, end: completed },
|
||||
input: part.state.input,
|
||||
title: "",
|
||||
metadata: { output, description: "" },
|
||||
metadata: { output },
|
||||
output,
|
||||
}
|
||||
yield* sessions.updatePart(part)
|
||||
@ -569,7 +569,7 @@ export const layer = Layer.effect(
|
||||
Effect.gen(function* () {
|
||||
output += chunk
|
||||
if (part.state.status === "running") {
|
||||
part.state.metadata = { output, description: "" }
|
||||
part.state.metadata = { output }
|
||||
yield* sessions.updatePart(part)
|
||||
}
|
||||
}),
|
||||
|
||||
@ -263,7 +263,7 @@ const parse = Effect.fn("ShellTool.parse")(function* (command: string, ps: boole
|
||||
const ask = Effect.fn("ShellTool.ask")(function* (
|
||||
ctx: Tool.Context,
|
||||
scan: Scan,
|
||||
input: { command: string; description: string },
|
||||
input: { command: string },
|
||||
) {
|
||||
if (scan.dirs.size > 0) {
|
||||
const directories = Array.from(scan.dirs)
|
||||
@ -277,7 +277,6 @@ const ask = Effect.fn("ShellTool.ask")(function* (
|
||||
always: globs,
|
||||
metadata: {
|
||||
command: input.command,
|
||||
description: input.description,
|
||||
directories,
|
||||
patterns: globs,
|
||||
},
|
||||
@ -291,7 +290,6 @@ const ask = Effect.fn("ShellTool.ask")(function* (
|
||||
always: Array.from(scan.always),
|
||||
metadata: {
|
||||
command: input.command,
|
||||
description: input.description,
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -438,7 +436,6 @@ export const ShellTool = Tool.define(
|
||||
cwd: string
|
||||
env: NodeJS.ProcessEnv
|
||||
timeout: number
|
||||
description: string
|
||||
},
|
||||
ctx: Tool.Context,
|
||||
) {
|
||||
@ -482,7 +479,6 @@ export const ShellTool = Tool.define(
|
||||
yield* ctx.metadata({
|
||||
metadata: {
|
||||
output: "",
|
||||
description: input.description,
|
||||
},
|
||||
})
|
||||
|
||||
@ -523,7 +519,6 @@ export const ShellTool = Tool.define(
|
||||
ctx.metadata({
|
||||
metadata: {
|
||||
output: last,
|
||||
description: input.description,
|
||||
},
|
||||
}),
|
||||
),
|
||||
@ -534,7 +529,6 @@ export const ShellTool = Tool.define(
|
||||
return ctx.metadata({
|
||||
metadata: {
|
||||
output: last,
|
||||
description: input.description,
|
||||
},
|
||||
})
|
||||
}),
|
||||
@ -593,11 +587,10 @@ export const ShellTool = Tool.define(
|
||||
output += "\n\n<shell_metadata>\n" + meta.join("\n") + "\n</shell_metadata>"
|
||||
}
|
||||
return {
|
||||
title: input.description,
|
||||
title: input.command,
|
||||
metadata: {
|
||||
output: last || preview(output),
|
||||
exit: code,
|
||||
description: input.description,
|
||||
truncated: cut,
|
||||
...(cut && file ? { outputPath: file } : {}),
|
||||
},
|
||||
@ -646,7 +639,6 @@ export const ShellTool = Tool.define(
|
||||
cwd,
|
||||
env: yield* shellEnv(ctx, cwd),
|
||||
timeout,
|
||||
description: params.description,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
|
||||
@ -7,30 +7,22 @@ import { ShellID } from "./id"
|
||||
const PS = new Set(["powershell", "pwsh"])
|
||||
const CMD = new Set(["cmd"])
|
||||
|
||||
const descriptions = {
|
||||
bash: "Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'",
|
||||
powershell:
|
||||
'Clear, concise description of what this command does in 5-10 words. Examples:\nInput: Get-ChildItem -LiteralPath "."\nOutput: Lists current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: New-Item -ItemType Directory -Path "tmp"\nOutput: Creates directory tmp',
|
||||
cmd: 'Clear, concise description of what this command does in 5-10 words. Examples:\nInput: dir\nOutput: Lists current directory\n\nInput: if exist "package.json" type "package.json"\nOutput: Prints package.json when it exists\n\nInput: mkdir tmp\nOutput: Creates directory tmp',
|
||||
}
|
||||
|
||||
export type Limits = {
|
||||
maxLines: number
|
||||
maxBytes: number
|
||||
}
|
||||
|
||||
export function parameterSchema(description: string) {
|
||||
export function parameterSchema() {
|
||||
return Schema.Struct({
|
||||
command: Schema.String.annotate({ description: "The command to execute" }),
|
||||
timeout: Schema.optional(PositiveInt).annotate({ description: "Optional timeout in milliseconds" }),
|
||||
workdir: Schema.optional(Schema.String).annotate({
|
||||
description: `The working directory to run the command in. Defaults to the current directory. Use this instead of 'cd' commands.`,
|
||||
}),
|
||||
description: Schema.String.annotate({ description }),
|
||||
})
|
||||
}
|
||||
|
||||
export const Parameters = parameterSchema(descriptions.bash)
|
||||
export const Parameters = parameterSchema()
|
||||
export type Parameters = Schema.Schema.Type<typeof Parameters>
|
||||
|
||||
function renderPrompt(template: string, values: Record<string, string>) {
|
||||
@ -103,7 +95,6 @@ function bashCommandSection(chain: string, limits: Limits, defaultTimeoutMs: num
|
||||
Usage notes:
|
||||
- The command argument is required.
|
||||
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms.
|
||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
||||
- If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use \`head\`, \`tail\`, or other truncation commands to limit output; the full output will already be captured to a file for more precise searching.
|
||||
|
||||
- Avoid using Bash with the \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
|
||||
@ -155,7 +146,6 @@ Before executing the command, please follow these steps:
|
||||
Usage notes:
|
||||
- The command argument is required.
|
||||
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms.
|
||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
||||
- If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use \`Select-Object -First\`, \`Select-Object -Last\`, or other truncation commands to limit output; the full output will already be captured to a file for more precise searching.
|
||||
|
||||
- Avoid using Shell with PowerShell file/content cmdlets unless explicitly instructed or when these cmdlets are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
|
||||
@ -205,7 +195,6 @@ Before executing the command, please follow these steps:
|
||||
Usage notes:
|
||||
- The command argument is required.
|
||||
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms.
|
||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
||||
- If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use \`more\` or other pagination commands to limit output; the full output will already be captured to a file for more precise searching.
|
||||
|
||||
- Avoid using Shell with cmd.exe file/content commands unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
|
||||
@ -242,7 +231,6 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits, defaul
|
||||
gitCommandRestriction: "git commands",
|
||||
createPrInstruction: "Create PR using a temporary body file so cmd.exe quoting stays simple.",
|
||||
createPrExample: `(\n echo ## Summary\n echo - ^<1-3 bullet points^>\n) > pr-body.txt\ngh pr create --title "the pr title" --body-file pr-body.txt`,
|
||||
parameterDescription: descriptions.cmd,
|
||||
}
|
||||
}
|
||||
if (isPowerShell) {
|
||||
@ -264,7 +252,6 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits, defaul
|
||||
## Summary
|
||||
- <1-3 bullet points>
|
||||
'@`,
|
||||
parameterDescription: descriptions.powershell,
|
||||
}
|
||||
}
|
||||
return {
|
||||
@ -280,7 +267,6 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits, defaul
|
||||
createPrExample: `gh pr create --title "the pr title" --body "$(cat <<'EOF'
|
||||
## Summary
|
||||
<1-3 bullet points>`,
|
||||
parameterDescription: descriptions.bash,
|
||||
}
|
||||
}
|
||||
|
||||
@ -300,7 +286,7 @@ export function render(name: string, platform: NodeJS.Platform, limits: Limits,
|
||||
createPrInstruction: selected.createPrInstruction,
|
||||
createPrExample: selected.createPrExample,
|
||||
}),
|
||||
parameters: parameterSchema(selected.parameterDescription),
|
||||
parameters: parameterSchema(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -589,6 +589,38 @@ test("coalesces same-line tool progress into one snapshot", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("omits the current directory from bash titles", async () => {
|
||||
const out = await setup()
|
||||
|
||||
try {
|
||||
await out.scrollback.append(
|
||||
toolCommit({
|
||||
tool: "bash",
|
||||
phase: "start",
|
||||
toolState: "running",
|
||||
state: {
|
||||
status: "running",
|
||||
input: {
|
||||
command: "pwd",
|
||||
workdir: process.cwd(),
|
||||
},
|
||||
time: { start: 1 },
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const commits = claim(out.renderer)
|
||||
try {
|
||||
expect(render(commits)).toContain("$ pwd")
|
||||
expect(render(commits)).not.toContain("Running in .")
|
||||
} finally {
|
||||
destroy(commits)
|
||||
}
|
||||
} finally {
|
||||
out.scrollback.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("renders completed bash output with one blank line after the command and before the next group", async () => {
|
||||
const out = await setup()
|
||||
|
||||
@ -615,7 +647,6 @@ test("renders completed bash output with one blank line after the command and be
|
||||
input: {
|
||||
command: "git status",
|
||||
workdir: "/tmp/demo",
|
||||
description: "Show git status",
|
||||
},
|
||||
time: { start: 1 },
|
||||
},
|
||||
@ -633,7 +664,6 @@ test("renders completed bash output with one blank line after the command and be
|
||||
input: {
|
||||
command: "git status",
|
||||
workdir: "/tmp/demo",
|
||||
description: "Show git status",
|
||||
},
|
||||
time: { start: 1, end: 2 },
|
||||
},
|
||||
@ -645,6 +675,7 @@ test("renders completed bash output with one blank line after the command and be
|
||||
take()
|
||||
|
||||
const output = lines.join("\n")
|
||||
expect(output).toContain("# Running in /tmp/demo\n$ git status")
|
||||
expect(output).toContain("$ git status\n\nOn branch demo")
|
||||
expect(output).toContain("nothing to commit, working tree clean\n\noc-run-dev ahead 1")
|
||||
expect(output).not.toContain("nothing to commit, working tree clean\n\n\noc-run-dev ahead 1")
|
||||
@ -677,7 +708,6 @@ test("inserts a spacer before the next tool after completed multiline bash outpu
|
||||
input: {
|
||||
command: "pwd; ls -la",
|
||||
workdir: "/tmp/demo",
|
||||
description: "Lists current directory files",
|
||||
},
|
||||
time: { start: 1 },
|
||||
},
|
||||
@ -695,7 +725,6 @@ test("inserts a spacer before the next tool after completed multiline bash outpu
|
||||
input: {
|
||||
command: "pwd; ls -la",
|
||||
workdir: "/tmp/demo",
|
||||
description: "Lists current directory files",
|
||||
},
|
||||
output: ["/tmp/demo", "pwd; ls -la", "/tmp/demo", "total 4", "", ""].join("\n"),
|
||||
title: "pwd; ls -la",
|
||||
@ -755,7 +784,6 @@ test("does not double-space before completed bash output when inline tool header
|
||||
input: {
|
||||
command: "ls",
|
||||
workdir: "src/cli/cmd/run",
|
||||
description: "Lists files in run directory",
|
||||
},
|
||||
time: { start: 1 },
|
||||
},
|
||||
@ -805,7 +833,6 @@ test("does not double-space before completed bash output when inline tool header
|
||||
input: {
|
||||
command: "ls",
|
||||
workdir: "src/cli/cmd/run",
|
||||
description: "Lists files in run directory",
|
||||
},
|
||||
output: ["src/cli/cmd/run", "ls", "demo.ts", "entry.body.ts", "", ""].join("\n"),
|
||||
title: "ls",
|
||||
|
||||
@ -435,7 +435,6 @@ describe("run session data", () => {
|
||||
title: "",
|
||||
metadata: {
|
||||
output: "/tmp/demo\n",
|
||||
description: "",
|
||||
},
|
||||
time: { start: 1, end: 2 },
|
||||
},
|
||||
@ -490,7 +489,6 @@ describe("run session data", () => {
|
||||
title: "",
|
||||
metadata: {
|
||||
output: "/tmp/demo\n",
|
||||
description: "",
|
||||
},
|
||||
time: { start: 1, end: 2 },
|
||||
},
|
||||
|
||||
@ -238,7 +238,6 @@ function shellAssistantMessage(id: string, parentID: string): SessionMessages[nu
|
||||
title: "",
|
||||
metadata: {
|
||||
output: "account.ts\n",
|
||||
description: "",
|
||||
},
|
||||
time: {
|
||||
start: 200,
|
||||
|
||||
@ -1811,7 +1811,6 @@ unix(
|
||||
yield* llm.tool("bash", {
|
||||
command:
|
||||
'i=0; while [ "$i" -lt 4000 ]; do printf "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx %05d\\n" "$i"; i=$((i + 1)); done; printf truncation-ready; sleep 30',
|
||||
description: "Print many lines",
|
||||
timeout: 30_000,
|
||||
workdir: path.resolve(dir),
|
||||
})
|
||||
|
||||
@ -139,7 +139,6 @@ it.live("tool execution produces non-empty session diff (snapshot race)", () =>
|
||||
const command = `echo 'snapshot race test content' > ${path.join(dir, "race-test.txt")}`
|
||||
yield* llm.toolMatch((hit) => JSON.stringify(hit.body).includes("create the file"), "bash", {
|
||||
command,
|
||||
description: "create test file",
|
||||
})
|
||||
yield* llm.textMatch((hit) => JSON.stringify(hit.body).includes("bash"), "done")
|
||||
|
||||
|
||||
@ -24,23 +24,6 @@ exports[`tool parameters JSON Schema (wire shape) bash 1`] = `
|
||||
"description": "The command to execute",
|
||||
"type": "string",
|
||||
},
|
||||
"description": {
|
||||
"description":
|
||||
"Clear, concise description of what this command does in 5-10 words. Examples:
|
||||
Input: ls
|
||||
Output: Lists files in current directory
|
||||
|
||||
Input: git status
|
||||
Output: Shows working tree status
|
||||
|
||||
Input: npm install
|
||||
Output: Installs package dependencies
|
||||
|
||||
Input: mkdir foo
|
||||
Output: Creates directory 'foo'"
|
||||
,
|
||||
"type": "string",
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Optional timeout in milliseconds",
|
||||
"exclusiveMinimum": 0,
|
||||
@ -55,7 +38,6 @@ Output: Creates directory 'foo'"
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"description",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
@ -106,19 +106,16 @@ describe("tool parameters", () => {
|
||||
})
|
||||
|
||||
describe("shell", () => {
|
||||
test("accepts minimum: command + description", () => {
|
||||
expect(parse(Shell, { command: "ls", description: "list" })).toEqual({ command: "ls", description: "list" })
|
||||
test("accepts command", () => {
|
||||
expect(parse(Shell, { command: "ls" })).toEqual({ command: "ls" })
|
||||
})
|
||||
test("accepts optional timeout + workdir", () => {
|
||||
const parsed = parse(Shell, { command: "ls", description: "list", timeout: 5000, workdir: "/tmp" })
|
||||
const parsed = parse(Shell, { command: "ls", timeout: 5000, workdir: "/tmp" })
|
||||
expect(parsed.timeout).toBe(5000)
|
||||
expect(parsed.workdir).toBe("/tmp")
|
||||
})
|
||||
test("rejects missing description", () => {
|
||||
expect(accepts(Shell, { command: "ls" })).toBe(false)
|
||||
})
|
||||
test("rejects missing command", () => {
|
||||
expect(accepts(Shell, { description: "list" })).toBe(false)
|
||||
expect(accepts(Shell, {})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -182,7 +182,6 @@ describe("tool.shell", () => {
|
||||
Effect.gen(function* () {
|
||||
const result = yield* run({
|
||||
command: "echo test",
|
||||
description: "Echo test message",
|
||||
})
|
||||
expect(result.metadata.exit).toBe(0)
|
||||
expect(result.metadata.output).toContain("test")
|
||||
@ -204,7 +203,6 @@ describe("tool.shell", () => {
|
||||
const result = yield* bash.execute(
|
||||
{
|
||||
command: "echo fallback",
|
||||
description: "Echo fallback text",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
@ -227,7 +225,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* run(
|
||||
{
|
||||
command: "echo hello",
|
||||
description: "Echo hello",
|
||||
},
|
||||
capture(requests),
|
||||
)
|
||||
@ -249,7 +246,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* run(
|
||||
{
|
||||
command: "echo foo && echo bar",
|
||||
description: "Echo twice",
|
||||
},
|
||||
capture(requests),
|
||||
)
|
||||
@ -273,7 +269,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* run(
|
||||
{
|
||||
command: "Write-Host foo; if ($?) { Write-Host bar }",
|
||||
description: "Check PowerShell conditional",
|
||||
},
|
||||
capture(requests),
|
||||
)
|
||||
@ -303,7 +298,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* fail(
|
||||
{
|
||||
command: "Remove-Item -Recurse tmp",
|
||||
description: "Remove a temp directory",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -331,7 +325,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* fail(
|
||||
{
|
||||
command: `cat ${file}`,
|
||||
description: "Read wildcard path",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -359,7 +352,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* run(
|
||||
{
|
||||
command: `echo $(cat "${file}")`,
|
||||
description: "Read nested bash file",
|
||||
},
|
||||
capture(requests),
|
||||
)
|
||||
@ -389,7 +381,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* fail(
|
||||
{
|
||||
command: `Copy-Item -PassThru "${process.env.WINDIR!.replaceAll("\\", "/")}/win.ini" ./out`,
|
||||
description: "Copy Windows ini",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -415,7 +406,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* run(
|
||||
{
|
||||
command: `Write-Output $(Get-Content ${file})`,
|
||||
description: "Read nested PowerShell file",
|
||||
},
|
||||
capture(requests),
|
||||
)
|
||||
@ -446,7 +436,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* fail(
|
||||
{
|
||||
command: 'Get-Content "C:../outside.txt"',
|
||||
description: "Read drive-relative file",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -474,7 +463,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* fail(
|
||||
{
|
||||
command: 'Get-Content "$HOME/.ssh/config"',
|
||||
description: "Read home config",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -503,7 +491,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* fail(
|
||||
{
|
||||
command: 'Get-Content "$PWD/../outside.txt"',
|
||||
description: "Read pwd-relative file",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -531,7 +518,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* fail(
|
||||
{
|
||||
command: 'Get-Content "$PSHOME/outside.txt"',
|
||||
description: "Read pshome file",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -567,7 +553,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* fail(
|
||||
{
|
||||
command: `Get-Content -Path "${root}$env:${key}\\Windows\\win.ini"`,
|
||||
description: "Read Windows ini with missing env",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -598,7 +583,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* run(
|
||||
{
|
||||
command: "Get-Content $env:WINDIR/win.ini",
|
||||
description: "Read Windows ini from env",
|
||||
},
|
||||
capture(requests),
|
||||
)
|
||||
@ -626,7 +610,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* fail(
|
||||
{
|
||||
command: `Get-Content -Path FileSystem::${process.env.WINDIR!.replaceAll("\\", "/")}/win.ini`,
|
||||
description: "Read Windows ini from FileSystem provider",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -655,7 +638,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* fail(
|
||||
{
|
||||
command: "Get-Content ${env:WINDIR}/win.ini",
|
||||
description: "Read Windows ini from braced env",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -682,7 +664,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* run(
|
||||
{
|
||||
command: "Set-Location C:/Windows",
|
||||
description: "Change location",
|
||||
},
|
||||
capture(requests),
|
||||
)
|
||||
@ -710,7 +691,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* run(
|
||||
{
|
||||
command: "Write-Output ('a' * 3)",
|
||||
description: "Write repeated text",
|
||||
},
|
||||
capture(requests),
|
||||
)
|
||||
@ -736,7 +716,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* run(
|
||||
{
|
||||
command: `TYPE "${path.join(process.env.WINDIR!, "win.ini")}"`,
|
||||
description: "Read Windows ini with cmd",
|
||||
},
|
||||
capture(requests),
|
||||
)
|
||||
@ -761,7 +740,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* fail(
|
||||
{
|
||||
command: "cd ../",
|
||||
description: "Change to parent directory",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -786,7 +764,6 @@ describe("tool.shell permissions", () => {
|
||||
{
|
||||
command: "echo ok",
|
||||
workdir: os.tmpdir(),
|
||||
description: "Echo from temp dir",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -817,7 +794,6 @@ describe("tool.shell permissions", () => {
|
||||
{
|
||||
command: "echo ok",
|
||||
workdir: dir,
|
||||
description: "Echo from external dir",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -850,7 +826,6 @@ describe("tool.shell permissions", () => {
|
||||
{
|
||||
command: "echo ok",
|
||||
workdir: "/tmp",
|
||||
description: "Echo from Git Bash tmp",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -878,7 +853,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* fail(
|
||||
{
|
||||
command: "cat /tmp/opencode-does-not-exist",
|
||||
description: "Read Git Bash tmp file",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -910,7 +884,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* fail(
|
||||
{
|
||||
command: `cat ${filepath}`,
|
||||
description: "Read external file",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
@ -922,7 +895,6 @@ describe("tool.shell permissions", () => {
|
||||
expect(extDirReq!.always).toContain(expected)
|
||||
expect(extDirReq!.metadata).toMatchObject({
|
||||
command: `cat ${filepath}`,
|
||||
description: "Read external file",
|
||||
directories: [outerTmp],
|
||||
patterns: [expected],
|
||||
})
|
||||
@ -942,7 +914,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* run(
|
||||
{
|
||||
command: `rm -rf ${path.join(tmp, "nested")}`,
|
||||
description: "Remove nested dir",
|
||||
},
|
||||
capture(requests),
|
||||
)
|
||||
@ -963,7 +934,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* run(
|
||||
{
|
||||
command: "git log --oneline -5",
|
||||
description: "Git log",
|
||||
},
|
||||
capture(requests),
|
||||
)
|
||||
@ -985,7 +955,6 @@ describe("tool.shell permissions", () => {
|
||||
yield* run(
|
||||
{
|
||||
command: "cd .",
|
||||
description: "Stay in current directory",
|
||||
},
|
||||
capture(requests),
|
||||
)
|
||||
@ -1006,7 +975,7 @@ describe("tool.shell permissions", () => {
|
||||
const requests: Array<Omit<PermissionV1.Request, "id" | "sessionID" | "tool">> = []
|
||||
expect(
|
||||
yield* fail(
|
||||
{ command: "echo test > output.txt", description: "Redirect test output" },
|
||||
{ command: "echo test > output.txt" },
|
||||
capture(requests, err),
|
||||
),
|
||||
).toMatchObject({ message: err.message })
|
||||
@ -1025,7 +994,7 @@ describe("tool.shell permissions", () => {
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const requests: Array<Omit<PermissionV1.Request, "id" | "sessionID" | "tool">> = []
|
||||
yield* run({ command: "ls -la", description: "List" }, capture(requests))
|
||||
yield* run({ command: "ls -la" }, capture(requests))
|
||||
const bashReq = requests.find((r) => r.permission === "bash")
|
||||
expect(bashReq).toBeDefined()
|
||||
expect(bashReq!.always[0]).toBe("ls *")
|
||||
@ -1047,7 +1016,6 @@ describe("tool.shell abort", () => {
|
||||
const res = yield* run(
|
||||
{
|
||||
command: `echo before && sleep 30`,
|
||||
description: "Long running command",
|
||||
},
|
||||
{
|
||||
...ctx,
|
||||
@ -1078,7 +1046,6 @@ describe("tool.shell abort", () => {
|
||||
Effect.gen(function* () {
|
||||
const result = yield* run({
|
||||
command: `sleep 60`,
|
||||
description: "Timeout test",
|
||||
timeout: 500,
|
||||
})
|
||||
expect(result.output).toContain("shell tool terminated command after exceeding timeout")
|
||||
@ -1099,7 +1066,6 @@ describe("tool.shell abort", () => {
|
||||
const result = yield* tool.execute(
|
||||
{
|
||||
command: `sleep 60`,
|
||||
description: "Default timeout test",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
@ -1116,7 +1082,6 @@ describe("tool.shell abort", () => {
|
||||
Effect.gen(function* () {
|
||||
const result = yield* run({
|
||||
command: `echo stdout_msg && echo stderr_msg >&2`,
|
||||
description: "Stderr test",
|
||||
})
|
||||
expect(result.output).toContain("stdout_msg")
|
||||
expect(result.output).toContain("stderr_msg")
|
||||
@ -1132,7 +1097,6 @@ describe("tool.shell abort", () => {
|
||||
Effect.gen(function* () {
|
||||
const result = yield* run({
|
||||
command: `exit 42`,
|
||||
description: "Non-zero exit",
|
||||
})
|
||||
expect(result.metadata.exit).toBe(42)
|
||||
}),
|
||||
@ -1147,7 +1111,6 @@ describe("tool.shell abort", () => {
|
||||
const result = yield* run(
|
||||
{
|
||||
command: `echo first && sleep 0.1 && echo second`,
|
||||
description: "Streaming test",
|
||||
},
|
||||
{
|
||||
...ctx,
|
||||
@ -1174,7 +1137,6 @@ describe("tool.shell truncation", () => {
|
||||
const lineCount = Truncate.MAX_LINES + 500
|
||||
const result = yield* run({
|
||||
command: fill("lines", lineCount),
|
||||
description: "Generate lines exceeding limit",
|
||||
})
|
||||
mustTruncate(result)
|
||||
expect(result.output).toMatch(/\.\.\.output truncated\.\.\./)
|
||||
@ -1190,7 +1152,6 @@ describe("tool.shell truncation", () => {
|
||||
const byteCount = Truncate.MAX_BYTES + 10000
|
||||
const result = yield* run({
|
||||
command: fill("bytes", byteCount),
|
||||
description: "Generate bytes exceeding limit",
|
||||
})
|
||||
mustTruncate(result)
|
||||
expect(result.output).toMatch(/\.\.\.output truncated\.\.\./)
|
||||
@ -1205,7 +1166,6 @@ describe("tool.shell truncation", () => {
|
||||
Effect.gen(function* () {
|
||||
const result = yield* run({
|
||||
command: fill("lines", 1),
|
||||
description: "Generate one line",
|
||||
})
|
||||
expect((result.metadata as { truncated?: boolean }).truncated).toBe(false)
|
||||
expect(result.output).toContain("1")
|
||||
@ -1220,7 +1180,6 @@ describe("tool.shell truncation", () => {
|
||||
const lineCount = Truncate.MAX_LINES + 100
|
||||
const result = yield* run({
|
||||
command: fill("lines", lineCount),
|
||||
description: "Generate lines for file check",
|
||||
})
|
||||
mustTruncate(result)
|
||||
|
||||
|
||||
@ -1994,7 +1994,7 @@ export function InlineToolRow(props: {
|
||||
}
|
||||
|
||||
function BlockTool(props: {
|
||||
title: string
|
||||
title?: string
|
||||
children: JSX.Element
|
||||
onClick?: () => void
|
||||
part?: ToolPart
|
||||
@ -2023,15 +2023,19 @@ function BlockTool(props: {
|
||||
props.onClick?.()
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={props.spinner}
|
||||
fallback={
|
||||
<text paddingLeft={3} fg={theme.textMuted}>
|
||||
{props.title}
|
||||
</text>
|
||||
}
|
||||
>
|
||||
<Spinner color={theme.textMuted}>{props.title.replace(/^# /, "")}</Spinner>
|
||||
<Show when={props.title}>
|
||||
{(title) => (
|
||||
<Show
|
||||
when={props.spinner}
|
||||
fallback={
|
||||
<text paddingLeft={3} fg={theme.textMuted}>
|
||||
{title()}
|
||||
</text>
|
||||
}
|
||||
>
|
||||
<Spinner color={theme.textMuted}>{title().replace(/^# /, "")}</Spinner>
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
{props.children}
|
||||
<Show when={error()}>
|
||||
@ -2059,15 +2063,15 @@ function Shell(props: ToolProps) {
|
||||
const workdirDisplay = createMemo(() => {
|
||||
const workdir = stringValue(props.input.workdir)
|
||||
if (!workdir || workdir === ".") return undefined
|
||||
return pathFormatter.format(workdir)
|
||||
const formatted = pathFormatter.format(workdir)
|
||||
if (formatted === ".") return undefined
|
||||
return formatted
|
||||
})
|
||||
|
||||
const title = createMemo(() => {
|
||||
const desc = stringValue(props.input.description) ?? "Shell"
|
||||
const wd = workdirDisplay()
|
||||
if (!wd) return `# ${desc}`
|
||||
if (desc.includes(wd)) return `# ${desc}`
|
||||
return `# ${desc} in ${wd}`
|
||||
if (!wd) return
|
||||
return `# Running in ${wd}`
|
||||
})
|
||||
|
||||
return (
|
||||
@ -2076,11 +2080,15 @@ function Shell(props: ToolProps) {
|
||||
<BlockTool
|
||||
title={title()}
|
||||
part={props.part}
|
||||
spinner={isRunning()}
|
||||
onClick={collapsed().overflow ? () => setExpanded((prev) => !prev) : undefined}
|
||||
>
|
||||
<box gap={1}>
|
||||
<text fg={theme.text}>$ {stringValue(props.input.command)}</text>
|
||||
<Show
|
||||
when={isRunning()}
|
||||
fallback={<text fg={theme.text}>$ {stringValue(props.input.command)}</text>}
|
||||
>
|
||||
<Spinner color={theme.text}>{stringValue(props.input.command)}</Spinner>
|
||||
</Show>
|
||||
<Show when={output()}>
|
||||
<text fg={theme.text}>{limited()}</text>
|
||||
</Show>
|
||||
|
||||
@ -269,12 +269,10 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
|
||||
}
|
||||
|
||||
if (permission === "bash") {
|
||||
const title =
|
||||
typeof data.description === "string" && data.description ? data.description : "Shell command"
|
||||
const command = typeof data.command === "string" ? data.command : ""
|
||||
return {
|
||||
icon: "#",
|
||||
title,
|
||||
title: "Shell command",
|
||||
body: (
|
||||
<Show when={command}>
|
||||
<box paddingLeft={1}>
|
||||
|
||||
@ -27,8 +27,6 @@ exports[`TUI inline tool wrapping snapshots expanded tool errors under the tool
|
||||
exports[`TUI inline tool wrapping keeps separation after a shell output block 1`] = `
|
||||
"
|
||||
|
||||
# List files
|
||||
|
||||
$ ls
|
||||
|
||||
file.ts
|
||||
|
||||
@ -62,7 +62,6 @@ function ShellOutput() {
|
||||
paddingLeft={2}
|
||||
gap={1}
|
||||
>
|
||||
<text paddingLeft={3}># List files</text>
|
||||
<box gap={1}>
|
||||
<text>$ ls</text>
|
||||
<text>file.ts</text>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createEffect, For, Match, on, onCleanup, onMount, Show, Switch, type JSX } from "solid-js"
|
||||
import { createEffect, For, Match, on, onCleanup, onMount, Show, Switch, type Accessor, type JSX } from "solid-js"
|
||||
import { animate, type AnimationPlaybackControls } from "motion"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { createStore } from "solid-js/store"
|
||||
@ -24,7 +24,7 @@ const isTriggerTitle = (val: any): val is TriggerTitle => {
|
||||
|
||||
export interface BasicToolProps {
|
||||
icon: IconProps["name"]
|
||||
trigger: TriggerTitle | JSX.Element
|
||||
trigger: TriggerTitle | JSX.Element | ((open: Accessor<boolean>) => JSX.Element)
|
||||
children?: JSX.Element
|
||||
status?: string
|
||||
hideDetails?: boolean
|
||||
@ -89,6 +89,7 @@ export function BasicTool(props: BasicToolProps) {
|
||||
const ready = () => state.ready
|
||||
const pending = () => props.status === "pending" || props.status === "running"
|
||||
const hasChildren = () => (props.defer ? "children" in props : props.children)
|
||||
const dynamicTrigger = typeof props.trigger === "function" ? props.trigger(open) : undefined
|
||||
|
||||
let cancelReady: (() => void) | undefined
|
||||
|
||||
@ -187,6 +188,7 @@ export function BasicTool(props: BasicToolProps) {
|
||||
<div data-slot="basic-tool-tool-trigger-content">
|
||||
<div data-slot="basic-tool-tool-info">
|
||||
<Switch>
|
||||
<Match when={dynamicTrigger !== undefined}>{dynamicTrigger}</Match>
|
||||
<Match when={isTriggerTitle(props.trigger) && props.trigger}>
|
||||
{(title) => (
|
||||
<div data-slot="basic-tool-tool-info-structured">
|
||||
|
||||
@ -422,7 +422,7 @@ export function getToolInfo(
|
||||
return {
|
||||
icon: "console",
|
||||
title: i18n.t("ui.tool.shell"),
|
||||
subtitle: input.description,
|
||||
subtitle: input.command,
|
||||
}
|
||||
case "edit":
|
||||
return {
|
||||
@ -1905,18 +1905,18 @@ ToolRegistry.register({
|
||||
<BasicTool
|
||||
{...props}
|
||||
icon="console"
|
||||
trigger={
|
||||
trigger={(open) => (
|
||||
<div data-slot="basic-tool-tool-info-structured">
|
||||
<div data-slot="basic-tool-tool-info-main">
|
||||
<span data-slot="basic-tool-tool-title">
|
||||
<TextShimmer text={i18n.t("ui.tool.shell")} active={pending()} />
|
||||
</span>
|
||||
<Show when={!pending() && props.input.description}>
|
||||
<ShellSubmessage text={props.input.description} animate={sawPending} />
|
||||
<Show when={!pending() && !open() && props.input.command}>
|
||||
<ShellSubmessage text={props.input.command} animate={sawPending} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div data-component="bash-output">
|
||||
<div data-slot="bash-copy">
|
||||
|
||||
@ -316,10 +316,10 @@ const TOOL_SAMPLES = {
|
||||
},
|
||||
bash: {
|
||||
tool: "bash",
|
||||
input: { command: "bun test --filter session", description: "Run session tests" },
|
||||
input: { command: "bun test --filter session" },
|
||||
output:
|
||||
"bun test v1.3.14\n\n✓ session-turn.test.tsx (3 tests) 45ms\n✓ message-part.test.tsx (7 tests) 120ms\n\nTest Suites: 2 passed, 2 total\nTests: 10 passed, 10 total\nTime: 0.89s",
|
||||
title: "Run session tests",
|
||||
title: "bun test --filter session",
|
||||
metadata: { command: "bun test --filter session" },
|
||||
},
|
||||
edit: {
|
||||
|
||||
@ -6,7 +6,6 @@ import { codeToHtml } from "shiki"
|
||||
interface Props {
|
||||
command: string
|
||||
output: string
|
||||
description?: string
|
||||
expand?: boolean
|
||||
}
|
||||
|
||||
@ -45,7 +44,7 @@ export function ContentBash(props: Props) {
|
||||
<div class={style.root} data-expanded={expanded() || props.expand === true ? true : undefined}>
|
||||
<div data-slot="body">
|
||||
<div data-slot="header">
|
||||
<span>{props.description}</span>
|
||||
<span>Shell</span>
|
||||
</div>
|
||||
<div data-slot="content">
|
||||
<div innerHTML={commandHtml()} />
|
||||
|
||||
@ -616,7 +616,6 @@ export function BashTool(props: ToolProps) {
|
||||
<ContentBash
|
||||
command={props.state.input.command}
|
||||
output={props.state.metadata.output ?? props.state.metadata?.stdout}
|
||||
description={props.state.metadata.description}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -714,6 +714,22 @@ Compatibility:
|
||||
- Foreground V2 bash execution is unchanged.
|
||||
- Reintroduce background bash only with durable status observation, completion delivery, and explicit cancellation semantics.
|
||||
|
||||
## 2026-06-18: Remove Bash Description Input
|
||||
|
||||
Affected schema:
|
||||
|
||||
- V1 and Core V2 model-facing `bash` tool parameters.
|
||||
|
||||
Change:
|
||||
|
||||
- Remove the V1 required and V2 optional `description` parameter.
|
||||
- Derive shell presentation from the command or a generic shell label instead of model-authored description metadata.
|
||||
|
||||
Compatibility:
|
||||
|
||||
- Existing persisted tool calls may still contain `description`, but new tool definitions no longer expose or require it.
|
||||
- Shell command execution behavior is unchanged.
|
||||
|
||||
## 2026-06-04: Add Durable Session Context Snapshots
|
||||
|
||||
Affected schema:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user