Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit b3bfbb3

Browse files
committed
Revert "refactor: remove approval flow from skill tool"
This reverts commit 40dd577.
1 parent 6280775 commit b3bfbb3

4 files changed

Lines changed: 180 additions & 7 deletions

File tree

src/core/auto-approval/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ export async function checkAutoApproval({
151151
return { decision: "approve" }
152152
}
153153

154+
if (tool.tool === "skill") {
155+
return { decision: "approve" }
156+
}
157+
154158
if (tool?.tool === "switchMode") {
155159
return state.alwaysAllowModeSwitch === true ? { decision: "approve" } : { decision: "ask" }
156160
}

src/core/tools/SkillTool.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Task } from "../task/Task"
22
import { formatResponse } from "../prompts/responses"
33
import { BaseTool, ToolCallbacks } from "./BaseTool"
4+
import type { ToolUse } from "../../shared/tools"
45

56
interface SkillParams {
67
skill: string
@@ -12,7 +13,7 @@ export class SkillTool extends BaseTool<"skill"> {
1213

1314
async execute(params: SkillParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
1415
const { skill: skillName, args } = params
15-
const { handleError, pushToolResult } = callbacks
16+
const { askApproval, handleError, pushToolResult } = callbacks
1617

1718
try {
1819
// Validate skill name parameter
@@ -59,7 +60,22 @@ export class SkillTool extends BaseTool<"skill"> {
5960
return
6061
}
6162

62-
// Build the result message - no approval needed, skills just execute
63+
// Build approval message
64+
const toolMessage = JSON.stringify({
65+
tool: "skill",
66+
skill: skillName,
67+
args: args,
68+
source: skillContent.source,
69+
description: skillContent.description,
70+
})
71+
72+
const didApprove = await askApproval("tool", toolMessage)
73+
74+
if (!didApprove) {
75+
return
76+
}
77+
78+
// Build the result message
6379
let result = `Skill: ${skillName}`
6480

6581
if (skillContent.description) {
@@ -79,7 +95,18 @@ export class SkillTool extends BaseTool<"skill"> {
7995
}
8096
}
8197

82-
// No handlePartial - skills execute silently without streaming UI
98+
override async handlePartial(task: Task, block: ToolUse<"skill">): Promise<void> {
99+
const skillName: string | undefined = block.params.skill
100+
const args: string | undefined = block.params.args
101+
102+
const partialMessage = JSON.stringify({
103+
tool: "skill",
104+
skill: skillName,
105+
args: args,
106+
})
107+
108+
await task.ask("tool", partialMessage, block.partial).catch(() => {})
109+
}
83110
}
84111

85112
export const skillTool = new SkillTool()

src/core/tools/__tests__/skillTool.spec.ts

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe("skillTool", () => {
2222
recordToolError: vi.fn(),
2323
didToolFailInCurrentTurn: false,
2424
sayAndCreateMissingParamError: vi.fn().mockResolvedValue("Missing parameter error"),
25+
ask: vi.fn().mockResolvedValue({}),
2526
providerRef: {
2627
deref: vi.fn().mockReturnValue({
2728
getState: vi.fn().mockResolvedValue({ mode: "code" }),
@@ -31,6 +32,7 @@ describe("skillTool", () => {
3132
}
3233

3334
mockCallbacks = {
35+
askApproval: vi.fn().mockResolvedValue(true),
3436
handleError: vi.fn(),
3537
pushToolResult: vi.fn(),
3638
}
@@ -97,7 +99,7 @@ describe("skillTool", () => {
9799
)
98100
})
99101

100-
it("should successfully load built-in skill without approval", async () => {
102+
it("should successfully load built-in skill", async () => {
101103
const block: ToolUse<"skill"> = {
102104
type: "tool_use" as const,
103105
name: "skill" as const,
@@ -119,7 +121,17 @@ describe("skillTool", () => {
119121

120122
await skillTool.handle(mockTask as Task, block, mockCallbacks)
121123

122-
// Skills execute directly without approval
124+
expect(mockCallbacks.askApproval).toHaveBeenCalledWith(
125+
"tool",
126+
JSON.stringify({
127+
tool: "skill",
128+
skill: "create-mcp-server",
129+
args: undefined,
130+
source: "built-in",
131+
description: "Instructions for creating MCP servers",
132+
}),
133+
)
134+
123135
expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith(
124136
`Skill: create-mcp-server
125137
Description: Instructions for creating MCP servers
@@ -166,6 +178,57 @@ Step 1: Create the server...`,
166178
)
167179
})
168180

181+
it("should handle user rejection", async () => {
182+
const block: ToolUse<"skill"> = {
183+
type: "tool_use" as const,
184+
name: "skill" as const,
185+
params: {},
186+
partial: false,
187+
nativeArgs: {
188+
skill: "create-mcp-server",
189+
},
190+
}
191+
192+
mockSkillsManager.getSkillContent.mockResolvedValue({
193+
name: "create-mcp-server",
194+
description: "Test",
195+
source: "built-in",
196+
instructions: "Test instructions",
197+
})
198+
199+
mockCallbacks.askApproval.mockResolvedValue(false)
200+
201+
await skillTool.handle(mockTask as Task, block, mockCallbacks)
202+
203+
expect(mockCallbacks.pushToolResult).not.toHaveBeenCalled()
204+
})
205+
206+
it("should handle partial block", async () => {
207+
const block: ToolUse<"skill"> = {
208+
type: "tool_use" as const,
209+
name: "skill" as const,
210+
params: {
211+
skill: "create-mcp-server",
212+
args: "",
213+
},
214+
partial: true,
215+
}
216+
217+
await skillTool.handle(mockTask as Task, block, mockCallbacks)
218+
219+
expect(mockTask.ask).toHaveBeenCalledWith(
220+
"tool",
221+
JSON.stringify({
222+
tool: "skill",
223+
skill: "create-mcp-server",
224+
args: "",
225+
}),
226+
true,
227+
)
228+
229+
expect(mockCallbacks.pushToolResult).not.toHaveBeenCalled()
230+
})
231+
169232
it("should handle errors during execution", async () => {
170233
const block: ToolUse<"skill"> = {
171234
type: "tool_use" as const,
@@ -236,7 +299,7 @@ Step 1: Create the server...`,
236299
)
237300
})
238301

239-
it("should load project skill without approval", async () => {
302+
it("should load project skill", async () => {
240303
const block: ToolUse<"skill"> = {
241304
type: "tool_use" as const,
242305
name: "skill" as const,
@@ -258,7 +321,17 @@ Step 1: Create the server...`,
258321

259322
await skillTool.handle(mockTask as Task, block, mockCallbacks)
260323

261-
// Skills execute directly without approval
324+
expect(mockCallbacks.askApproval).toHaveBeenCalledWith(
325+
"tool",
326+
JSON.stringify({
327+
tool: "skill",
328+
skill: "my-project-skill",
329+
args: undefined,
330+
source: "project",
331+
description: "A custom project skill",
332+
}),
333+
)
334+
262335
expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith(
263336
`Skill: my-project-skill
264337
Description: A custom project skill

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,75 @@ export const ChatRowContent = ({
666666
</div>
667667
</>
668668
)
669+
case "skill": {
670+
const skillInfo = tool
671+
return (
672+
<>
673+
<div style={headerStyle}>
674+
{toolIcon("book")}
675+
<span style={{ fontWeight: "bold" }}>
676+
{message.type === "ask" ? t("chat:skill.wantsToLoad") : t("chat:skill.didLoad")}
677+
</span>
678+
</div>
679+
<div
680+
style={{
681+
marginTop: "4px",
682+
backgroundColor: "var(--vscode-editor-background)",
683+
border: "1px solid var(--vscode-editorGroup-border)",
684+
borderRadius: "4px",
685+
overflow: "hidden",
686+
cursor: "pointer",
687+
}}
688+
onClick={handleToggleExpand}>
689+
<ToolUseBlockHeader
690+
className="group"
691+
style={{
692+
display: "flex",
693+
alignItems: "center",
694+
justifyContent: "space-between",
695+
padding: "10px 12px",
696+
}}>
697+
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
698+
<span style={{ fontWeight: "500", fontSize: "var(--vscode-font-size)" }}>
699+
{skillInfo.skill}
700+
</span>
701+
{skillInfo.source && (
702+
<VSCodeBadge style={{ fontSize: "calc(var(--vscode-font-size) - 2px)" }}>
703+
{skillInfo.source}
704+
</VSCodeBadge>
705+
)}
706+
</div>
707+
<span
708+
className={`codicon codicon-chevron-${isExpanded ? "up" : "down"} opacity-0 group-hover:opacity-100 transition-opacity duration-200`}></span>
709+
</ToolUseBlockHeader>
710+
{isExpanded && (skillInfo.args || skillInfo.description) && (
711+
<div
712+
style={{
713+
padding: "12px 16px",
714+
borderTop: "1px solid var(--vscode-editorGroup-border)",
715+
display: "flex",
716+
flexDirection: "column",
717+
gap: "8px",
718+
}}>
719+
{skillInfo.description && (
720+
<div style={{ color: "var(--vscode-descriptionForeground)" }}>
721+
{skillInfo.description}
722+
</div>
723+
)}
724+
{skillInfo.args && (
725+
<div>
726+
<span style={{ fontWeight: "500" }}>Arguments: </span>
727+
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
728+
{skillInfo.args}
729+
</span>
730+
</div>
731+
)}
732+
</div>
733+
)}
734+
</div>
735+
</>
736+
)
737+
}
669738
case "listFilesTopLevel":
670739
return (
671740
<>

0 commit comments

Comments
 (0)