Skip to content

Commit 82a007a

Browse files
authored
Fix the UI for approving chained commands (#6623)
1 parent 3f966df commit 82a007a

File tree

4 files changed

+53
-52
lines changed

4 files changed

+53
-52
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,6 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
192192
</div>
193193
{command && command.trim() && (
194194
<CommandPatternSelector
195-
command={command}
196195
patterns={commandPatterns}
197196
allowedCommands={allowedCommands}
198197
deniedCommands={deniedCommands}

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ interface CommandPattern {
1111
}
1212

1313
interface CommandPatternSelectorProps {
14-
command: string
1514
patterns: CommandPattern[]
1615
allowedCommands: string[]
1716
deniedCommands: string[]
@@ -20,7 +19,6 @@ interface CommandPatternSelectorProps {
2019
}
2120

2221
export const CommandPatternSelector: React.FC<CommandPatternSelectorProps> = ({
23-
command,
2422
patterns,
2523
allowedCommands,
2624
deniedCommands,
@@ -37,13 +35,8 @@ export const CommandPatternSelector: React.FC<CommandPatternSelectorProps> = ({
3735

3836
// Create a combined list with full command first, then patterns
3937
const allPatterns = useMemo(() => {
40-
// Trim the command to ensure consistency with extracted patterns
41-
const trimmedCommand = command.trim()
42-
const fullCommandPattern: CommandPattern = { pattern: trimmedCommand }
43-
4438
// Create a set to track unique patterns we've already seen
4539
const seenPatterns = new Set<string>()
46-
seenPatterns.add(trimmedCommand) // Add the trimmed full command first
4740

4841
// Filter out any patterns that are duplicates or are the same as the full command
4942
const uniquePatterns = patterns.filter((p) => {
@@ -54,8 +47,8 @@ export const CommandPatternSelector: React.FC<CommandPatternSelectorProps> = ({
5447
return true
5548
})
5649

57-
return [fullCommandPattern, ...uniquePatterns]
58-
}, [command, patterns])
50+
return uniquePatterns
51+
}, [patterns])
5952

6053
const getPatternStatus = (pattern: string): "allowed" | "denied" | "none" => {
6154
if (allowedCommands.includes(pattern)) return "allowed"

webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ vi.mock("../../common/CodeBlock", () => ({
2222
}))
2323

2424
vi.mock("../CommandPatternSelector", () => ({
25-
CommandPatternSelector: ({ command, onAllowPatternChange, onDenyPatternChange }: any) => (
25+
CommandPatternSelector: ({ patterns, onAllowPatternChange, onDenyPatternChange }: any) => (
2626
<div data-testid="command-pattern-selector">
27-
<span>{command}</span>
28-
<button onClick={() => onAllowPatternChange(command)}>Allow {command}</button>
29-
<button onClick={() => onDenyPatternChange(command)}>Deny {command}</button>
27+
{patterns.map((pattern: any, index: number) => (
28+
<span key={index}>{pattern.pattern}</span>
29+
))}
30+
<button onClick={() => onAllowPatternChange(patterns[0]?.pattern)}>Allow</button>
31+
<button onClick={() => onDenyPatternChange(patterns[0]?.pattern)}>Deny</button>
3032
</div>
3133
),
3234
}))
@@ -104,7 +106,7 @@ describe("CommandExecution", () => {
104106
</ExtensionStateWrapper>,
105107
)
106108

107-
const allowButton = screen.getByText("Allow git push")
109+
const allowButton = screen.getByText("Allow")
108110
fireEvent.click(allowButton)
109111

110112
expect(mockExtensionState.setAllowedCommands).toHaveBeenCalledWith(["npm", "git push"])
@@ -120,7 +122,7 @@ describe("CommandExecution", () => {
120122
</ExtensionStateWrapper>,
121123
)
122124

123-
const denyButton = screen.getByText("Deny docker run")
125+
const denyButton = screen.getByText("Deny")
124126
fireEvent.click(denyButton)
125127

126128
expect(mockExtensionState.setAllowedCommands).toHaveBeenCalledWith(["npm"])
@@ -143,7 +145,7 @@ describe("CommandExecution", () => {
143145
</ExtensionStateContext.Provider>,
144146
)
145147

146-
const allowButton = screen.getByText("Allow npm test")
148+
const allowButton = screen.getByText("Allow")
147149
fireEvent.click(allowButton)
148150

149151
// "npm test" is already in allowedCommands, so it should be removed
@@ -167,7 +169,7 @@ describe("CommandExecution", () => {
167169
</ExtensionStateContext.Provider>,
168170
)
169171

170-
const denyButton = screen.getByText("Deny rm -rf")
172+
const denyButton = screen.getByText("Deny")
171173
fireEvent.click(denyButton)
172174

173175
// "rm -rf" is already in deniedCommands, so it should be removed
@@ -223,7 +225,8 @@ Suggested patterns: npm, npm install, npm run`
223225

224226
const selector = screen.getByTestId("command-pattern-selector")
225227
expect(selector).toBeInTheDocument()
226-
expect(selector).toHaveTextContent("ls -la | grep test")
228+
// Should show one of the individual commands from the pipe
229+
expect(selector.textContent).toMatch(/ls -la|grep test/)
227230
})
228231

229232
it("should handle commands with && operator", () => {
@@ -235,7 +238,8 @@ Suggested patterns: npm, npm install, npm run`
235238

236239
const selector = screen.getByTestId("command-pattern-selector")
237240
expect(selector).toBeInTheDocument()
238-
expect(selector).toHaveTextContent("npm install && npm test")
241+
// Should show one of the individual commands from the && chain
242+
expect(selector.textContent).toMatch(/npm install|npm test|npm/)
239243
})
240244

241245
it("should not show pattern selector for empty commands", () => {
@@ -301,7 +305,7 @@ Output here`
301305
</ExtensionStateContext.Provider>,
302306
)
303307

304-
const allowButton = screen.getByText("Allow rm file.txt")
308+
const allowButton = screen.getByText("Allow")
305309
fireEvent.click(allowButton)
306310

307311
// "rm file.txt" should be removed from denied and added to allowed
@@ -321,7 +325,8 @@ Output here`
321325

322326
const selector = screen.getByTestId("command-pattern-selector")
323327
expect(selector).toBeInTheDocument()
324-
expect(selector).toHaveTextContent("npm install && npm test || echo 'failed'")
328+
// Should show one of the individual commands from the complex chain
329+
expect(selector.textContent).toMatch(/npm install|npm test|echo|npm/)
325330
})
326331

327332
it("should handle commands with output", () => {
@@ -356,7 +361,8 @@ Other output here`
356361

357362
const selector = screen.getByTestId("command-pattern-selector")
358363
expect(selector).toBeInTheDocument()
359-
expect(selector).toHaveTextContent("echo $(whoami) && git status")
364+
// Should show one of the individual commands
365+
expect(selector.textContent).toMatch(/echo|whoami|git status|git/)
360366
})
361367

362368
it("should handle commands with backtick subshells", () => {
@@ -368,7 +374,8 @@ Other output here`
368374

369375
const selector = screen.getByTestId("command-pattern-selector")
370376
expect(selector).toBeInTheDocument()
371-
expect(selector).toHaveTextContent("git commit -m `date`")
377+
// Should show one of the individual commands
378+
expect(selector.textContent).toMatch(/git commit|date|git/)
372379
})
373380

374381
it("should handle commands with special characters", () => {
@@ -380,7 +387,8 @@ Other output here`
380387

381388
const selector = screen.getByTestId("command-pattern-selector")
382389
expect(selector).toBeInTheDocument()
383-
expect(selector).toHaveTextContent("cd ~/projects && npm start")
390+
// Should show one of the individual commands
391+
expect(selector.textContent).toMatch(/cd ~\/projects|npm start|cd|npm/)
384392
})
385393

386394
it("should handle commands with mixed content including output", () => {
@@ -421,7 +429,7 @@ Running tests...
421429
)
422430

423431
// Click to allow "git push origin main"
424-
const allowButton = screen.getByText("Allow git push origin main")
432+
const allowButton = screen.getByText("Allow")
425433
fireEvent.click(allowButton)
426434

427435
// Should add to allowed and remove from denied
@@ -442,10 +450,10 @@ Running tests...
442450
// Should still render the command
443451
expect(screen.getByTestId("code-block")).toHaveTextContent("echo 'test with unclosed quote")
444452

445-
// Should show pattern selector with the full command
453+
// Should show pattern selector with a command pattern
446454
const selector = screen.getByTestId("command-pattern-selector")
447455
expect(selector).toBeInTheDocument()
448-
expect(selector).toHaveTextContent("echo 'test with unclosed quote")
456+
expect(selector.textContent).toMatch(/echo/)
449457
})
450458

451459
it("should handle empty or whitespace-only commands", () => {
@@ -525,8 +533,8 @@ Output:
525533
const selector = screen.getByTestId("command-pattern-selector")
526534
expect(selector).toBeInTheDocument()
527535

528-
// Should show the full command in the selector
529-
expect(selector).toHaveTextContent("wc -l *.go *.java")
536+
// Should show a command pattern
537+
expect(selector.textContent).toMatch(/wc/)
530538

531539
// The output should still be displayed in the code block
532540
expect(codeBlocks.length).toBeGreaterThan(1)
@@ -548,8 +556,8 @@ Output:
548556
const selector = screen.getByTestId("command-pattern-selector")
549557
expect(selector).toBeInTheDocument()
550558

551-
// Should show the full command in the selector
552-
expect(selector).toHaveTextContent("wc -l *.go *.java")
559+
// Should show a command pattern
560+
expect(selector.textContent).toMatch(/wc/)
553561

554562
// The output should still be displayed in the code block
555563
const codeBlocks = screen.getAllByTestId("code-block")

webview-ui/src/components/chat/__tests__/CommandPatternSelector.spec.tsx

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ const TestWrapper = ({ children }: { children: React.ReactNode }) => <TooltipPro
2626

2727
describe("CommandPatternSelector", () => {
2828
const defaultProps = {
29-
command: "npm install express",
3029
patterns: [
30+
{ pattern: "npm install express", description: "Full command" },
3131
{ pattern: "npm install", description: "Install npm packages" },
3232
{ pattern: "npm *", description: "Any npm command" },
3333
],
@@ -51,7 +51,7 @@ describe("CommandPatternSelector", () => {
5151
expect(screen.getByText("chat:commandExecution.manageCommands")).toBeInTheDocument()
5252
})
5353

54-
it("should show full command as first pattern when expanded", () => {
54+
it("should show patterns when expanded", () => {
5555
render(
5656
<TestWrapper>
5757
<CommandPatternSelector {...defaultProps} />
@@ -62,8 +62,9 @@ describe("CommandPatternSelector", () => {
6262
const expandButton = screen.getByRole("button")
6363
fireEvent.click(expandButton)
6464

65-
// Check that the full command is shown
65+
// Check that the patterns are shown
6666
expect(screen.getByText("npm install express")).toBeInTheDocument()
67+
expect(screen.getByText("- Full command")).toBeInTheDocument()
6768
})
6869

6970
it("should show extracted patterns when expanded", () => {
@@ -95,9 +96,9 @@ describe("CommandPatternSelector", () => {
9596
const expandButton = screen.getByRole("button")
9697
fireEvent.click(expandButton)
9798

98-
// Click on the full command pattern
99-
const fullCommandDiv = screen.getByText("npm install express").closest("div")
100-
fireEvent.click(fullCommandDiv!)
99+
// Click on a pattern
100+
const patternDiv = screen.getByText("npm install express").closest("div")
101+
fireEvent.click(patternDiv!)
101102

102103
// An input should appear
103104
const input = screen.getByDisplayValue("npm install express") as HTMLInputElement
@@ -168,9 +169,9 @@ describe("CommandPatternSelector", () => {
168169
const expandButton = screen.getByRole("button")
169170
fireEvent.click(expandButton)
170171

171-
// Find the full command pattern row and click allow
172-
const fullCommandPattern = screen.getByText("npm install express").closest(".ml-5")
173-
const allowButton = fullCommandPattern?.querySelector('button[aria-label*="addToAllowed"]')
172+
// Find a pattern row and click allow
173+
const patternRow = screen.getByText("npm install express").closest(".ml-5")
174+
const allowButton = patternRow?.querySelector('button[aria-label*="addToAllowed"]')
174175
fireEvent.click(allowButton!)
175176

176177
// Check that the callback was called with the pattern
@@ -194,9 +195,9 @@ describe("CommandPatternSelector", () => {
194195
const expandButton = screen.getByRole("button")
195196
fireEvent.click(expandButton)
196197

197-
// Find the full command pattern row and click deny
198-
const fullCommandPattern = screen.getByText("npm install express").closest(".ml-5")
199-
const denyButton = fullCommandPattern?.querySelector('button[aria-label*="addToDenied"]')
198+
// Find a pattern row and click deny
199+
const patternRow = screen.getByText("npm install express").closest(".ml-5")
200+
const denyButton = patternRow?.querySelector('button[aria-label*="addToDenied"]')
200201
fireEvent.click(denyButton!)
201202

202203
// Check that the callback was called with the pattern
@@ -220,11 +221,11 @@ describe("CommandPatternSelector", () => {
220221
const expandButton = screen.getByRole("button")
221222
fireEvent.click(expandButton)
222223

223-
// Click on the full command pattern to edit
224-
const fullCommandDiv = screen.getByText("npm install express").closest("div")
225-
fireEvent.click(fullCommandDiv!)
224+
// Click on a pattern to edit
225+
const patternDiv = screen.getByText("npm install express").closest("div")
226+
fireEvent.click(patternDiv!)
226227

227-
// Edit the command
228+
// Edit the pattern
228229
const input = screen.getByDisplayValue("npm install express") as HTMLInputElement
229230
fireEvent.change(input, { target: { value: "npm install react" } })
230231

@@ -254,11 +255,11 @@ describe("CommandPatternSelector", () => {
254255
const expandButton = screen.getByRole("button")
255256
fireEvent.click(expandButton)
256257

257-
// Click on the full command pattern to edit
258-
const fullCommandDiv = screen.getByText("npm install express").closest("div")
259-
fireEvent.click(fullCommandDiv!)
258+
// Click on a pattern to edit
259+
const patternDiv = screen.getByText("npm install express").closest("div")
260+
fireEvent.click(patternDiv!)
260261

261-
// Edit the command
262+
// Edit the pattern
262263
const input = screen.getByDisplayValue("npm install express") as HTMLInputElement
263264
fireEvent.change(input, { target: { value: "npm install react" } })
264265

0 commit comments

Comments
 (0)