mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Merge pull request #714 from RooVetGit/opened_tabs_and_selection_mentions
Mention shortcuts to open tabs
This commit is contained in:
5
.changeset/blue-masks-camp.md
Normal file
5
.changeset/blue-masks-camp.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"roo-cline": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add shortcuts to the currently open tabs in the "Add File" section of @-mentions (thanks @olup!)
|
||||||
@@ -5,9 +5,25 @@ const vscode = {
|
|||||||
createTextEditorDecorationType: jest.fn().mockReturnValue({
|
createTextEditorDecorationType: jest.fn().mockReturnValue({
|
||||||
dispose: jest.fn(),
|
dispose: jest.fn(),
|
||||||
}),
|
}),
|
||||||
|
tabGroups: {
|
||||||
|
onDidChangeTabs: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
dispose: jest.fn(),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
all: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
onDidSaveTextDocument: jest.fn(),
|
onDidSaveTextDocument: jest.fn(),
|
||||||
|
createFileSystemWatcher: jest.fn().mockReturnValue({
|
||||||
|
onDidCreate: jest.fn().mockReturnValue({ dispose: jest.fn() }),
|
||||||
|
onDidDelete: jest.fn().mockReturnValue({ dispose: jest.fn() }),
|
||||||
|
dispose: jest.fn(),
|
||||||
|
}),
|
||||||
|
fs: {
|
||||||
|
stat: jest.fn(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Disposable: class {
|
Disposable: class {
|
||||||
dispose() {}
|
dispose() {}
|
||||||
@@ -57,6 +73,17 @@ const vscode = {
|
|||||||
Development: 2,
|
Development: 2,
|
||||||
Test: 3,
|
Test: 3,
|
||||||
},
|
},
|
||||||
|
FileType: {
|
||||||
|
Unknown: 0,
|
||||||
|
File: 1,
|
||||||
|
Directory: 2,
|
||||||
|
SymbolicLink: 64,
|
||||||
|
},
|
||||||
|
TabInputText: class {
|
||||||
|
constructor(uri) {
|
||||||
|
this.uri = uri
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = vscode
|
module.exports = vscode
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ jest.mock("vscode", () => {
|
|||||||
visibleTextEditors: [mockTextEditor],
|
visibleTextEditors: [mockTextEditor],
|
||||||
tabGroups: {
|
tabGroups: {
|
||||||
all: [mockTabGroup],
|
all: [mockTabGroup],
|
||||||
|
onDidChangeTabs: jest.fn(() => ({ dispose: jest.fn() })),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as vscode from "vscode"
|
|||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { listFiles } from "../../services/glob/list-files"
|
import { listFiles } from "../../services/glob/list-files"
|
||||||
import { ClineProvider } from "../../core/webview/ClineProvider"
|
import { ClineProvider } from "../../core/webview/ClineProvider"
|
||||||
|
import { toRelativePath } from "../../utils/path"
|
||||||
|
|
||||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||||
const MAX_INITIAL_FILES = 1_000
|
const MAX_INITIAL_FILES = 1_000
|
||||||
@@ -48,6 +49,23 @@ class WorkspaceTracker {
|
|||||||
)
|
)
|
||||||
|
|
||||||
this.disposables.push(watcher)
|
this.disposables.push(watcher)
|
||||||
|
|
||||||
|
this.disposables.push(vscode.window.tabGroups.onDidChangeTabs(() => this.workspaceDidUpdate()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOpenedTabsInfo() {
|
||||||
|
return vscode.window.tabGroups.all.flatMap((group) =>
|
||||||
|
group.tabs
|
||||||
|
.filter((tab) => tab.input instanceof vscode.TabInputText)
|
||||||
|
.map((tab) => {
|
||||||
|
const path = (tab.input as vscode.TabInputText).uri.fsPath
|
||||||
|
return {
|
||||||
|
label: tab.label,
|
||||||
|
isActive: tab.isActive,
|
||||||
|
path: toRelativePath(path, cwd || ""),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private workspaceDidUpdate() {
|
private workspaceDidUpdate() {
|
||||||
@@ -59,12 +77,12 @@ class WorkspaceTracker {
|
|||||||
if (!cwd) {
|
if (!cwd) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const relativeFilePaths = Array.from(this.filePaths).map((file) => toRelativePath(file, cwd))
|
||||||
this.providerRef.deref()?.postMessageToWebview({
|
this.providerRef.deref()?.postMessageToWebview({
|
||||||
type: "workspaceUpdated",
|
type: "workspaceUpdated",
|
||||||
filePaths: Array.from(this.filePaths).map((file) => {
|
filePaths: relativeFilePaths,
|
||||||
const relativePath = path.relative(cwd, file).toPosix()
|
openedTabs: this.getOpenedTabsInfo(),
|
||||||
return file.endsWith("/") ? relativePath + "/" : relativePath
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
this.updateTimer = null
|
this.updateTimer = null
|
||||||
}, 300) // Debounce for 300ms
|
}, 300) // Debounce for 300ms
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ const mockWatcher = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jest.mock("vscode", () => ({
|
jest.mock("vscode", () => ({
|
||||||
|
window: {
|
||||||
|
tabGroups: {
|
||||||
|
onDidChangeTabs: jest.fn(() => ({ dispose: jest.fn() })),
|
||||||
|
all: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
workspaceFolders: [
|
workspaceFolders: [
|
||||||
{
|
{
|
||||||
@@ -61,6 +67,7 @@ describe("WorkspaceTracker", () => {
|
|||||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||||
type: "workspaceUpdated",
|
type: "workspaceUpdated",
|
||||||
filePaths: expect.arrayContaining(["file1.ts", "file2.ts"]),
|
filePaths: expect.arrayContaining(["file1.ts", "file2.ts"]),
|
||||||
|
openedTabs: [],
|
||||||
})
|
})
|
||||||
expect((mockProvider.postMessageToWebview as jest.Mock).mock.calls[0][0].filePaths).toHaveLength(2)
|
expect((mockProvider.postMessageToWebview as jest.Mock).mock.calls[0][0].filePaths).toHaveLength(2)
|
||||||
})
|
})
|
||||||
@@ -74,6 +81,7 @@ describe("WorkspaceTracker", () => {
|
|||||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||||
type: "workspaceUpdated",
|
type: "workspaceUpdated",
|
||||||
filePaths: ["newfile.ts"],
|
filePaths: ["newfile.ts"],
|
||||||
|
openedTabs: [],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -92,6 +100,7 @@ describe("WorkspaceTracker", () => {
|
|||||||
expect(mockProvider.postMessageToWebview).toHaveBeenLastCalledWith({
|
expect(mockProvider.postMessageToWebview).toHaveBeenLastCalledWith({
|
||||||
type: "workspaceUpdated",
|
type: "workspaceUpdated",
|
||||||
filePaths: [],
|
filePaths: [],
|
||||||
|
openedTabs: [],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -106,6 +115,7 @@ describe("WorkspaceTracker", () => {
|
|||||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||||
type: "workspaceUpdated",
|
type: "workspaceUpdated",
|
||||||
filePaths: expect.arrayContaining(["newdir"]),
|
filePaths: expect.arrayContaining(["newdir"]),
|
||||||
|
openedTabs: [],
|
||||||
})
|
})
|
||||||
const lastCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
|
const lastCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
|
||||||
expect(lastCall[0].filePaths).toHaveLength(1)
|
expect(lastCall[0].filePaths).toHaveLength(1)
|
||||||
@@ -126,6 +136,7 @@ describe("WorkspaceTracker", () => {
|
|||||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||||
type: "workspaceUpdated",
|
type: "workspaceUpdated",
|
||||||
filePaths: expect.arrayContaining(expectedFiles),
|
filePaths: expect.arrayContaining(expectedFiles),
|
||||||
|
openedTabs: [],
|
||||||
})
|
})
|
||||||
expect(calls[0][0].filePaths).toHaveLength(1000)
|
expect(calls[0][0].filePaths).toHaveLength(1000)
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ export interface ExtensionMessage {
|
|||||||
lmStudioModels?: string[]
|
lmStudioModels?: string[]
|
||||||
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
|
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
|
||||||
filePaths?: string[]
|
filePaths?: string[]
|
||||||
|
openedTabs?: Array<{
|
||||||
|
label: string
|
||||||
|
isActive: boolean
|
||||||
|
path?: string
|
||||||
|
}>
|
||||||
partialMessage?: ClineMessage
|
partialMessage?: ClineMessage
|
||||||
glamaModels?: Record<string, ModelInfo>
|
glamaModels?: Record<string, ModelInfo>
|
||||||
openRouterModels?: Record<string, ModelInfo>
|
openRouterModels?: Record<string, ModelInfo>
|
||||||
|
|||||||
@@ -99,3 +99,8 @@ export function getReadablePath(cwd: string, relPath?: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const toRelativePath = (filePath: string, cwd: string) => {
|
||||||
|
const relativePath = path.relative(cwd, filePath).toPosix()
|
||||||
|
return filePath.endsWith("/") ? relativePath + "/" : relativePath
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const { filePaths, currentApiConfigName, listApiConfigMeta, customModes } = useExtensionState()
|
const { filePaths, openedTabs, currentApiConfigName, listApiConfigMeta, customModes } = useExtensionState()
|
||||||
const [gitCommits, setGitCommits] = useState<any[]>([])
|
const [gitCommits, setGitCommits] = useState<any[]>([])
|
||||||
const [showDropdown, setShowDropdown] = useState(false)
|
const [showDropdown, setShowDropdown] = useState(false)
|
||||||
|
|
||||||
@@ -138,14 +138,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
return [
|
return [
|
||||||
{ type: ContextMenuOptionType.Problems, value: "problems" },
|
{ type: ContextMenuOptionType.Problems, value: "problems" },
|
||||||
...gitCommits,
|
...gitCommits,
|
||||||
|
...openedTabs
|
||||||
|
.filter((tab) => tab.path)
|
||||||
|
.map((tab) => ({
|
||||||
|
type: ContextMenuOptionType.OpenedFile,
|
||||||
|
value: "/" + tab.path,
|
||||||
|
})),
|
||||||
...filePaths
|
...filePaths
|
||||||
.map((file) => "/" + file)
|
.map((file) => "/" + file)
|
||||||
|
.filter((path) => !openedTabs.some((tab) => tab.path && "/" + tab.path === path)) // Filter out paths that are already in openedTabs
|
||||||
.map((path) => ({
|
.map((path) => ({
|
||||||
type: path.endsWith("/") ? ContextMenuOptionType.Folder : ContextMenuOptionType.File,
|
type: path.endsWith("/") ? ContextMenuOptionType.Folder : ContextMenuOptionType.File,
|
||||||
value: path,
|
value: path,
|
||||||
})),
|
})),
|
||||||
]
|
]
|
||||||
}, [filePaths, gitCommits])
|
}, [filePaths, gitCommits, openedTabs])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||||||
return <span>Git Commits</span>
|
return <span>Git Commits</span>
|
||||||
}
|
}
|
||||||
case ContextMenuOptionType.File:
|
case ContextMenuOptionType.File:
|
||||||
|
case ContextMenuOptionType.OpenedFile:
|
||||||
case ContextMenuOptionType.Folder:
|
case ContextMenuOptionType.Folder:
|
||||||
if (option.value) {
|
if (option.value) {
|
||||||
return (
|
return (
|
||||||
@@ -100,6 +101,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||||||
|
|
||||||
const getIconForOption = (option: ContextMenuQueryItem): string => {
|
const getIconForOption = (option: ContextMenuQueryItem): string => {
|
||||||
switch (option.type) {
|
switch (option.type) {
|
||||||
|
case ContextMenuOptionType.OpenedFile:
|
||||||
|
return "window"
|
||||||
case ContextMenuOptionType.File:
|
case ContextMenuOptionType.File:
|
||||||
return "file"
|
return "file"
|
||||||
case ContextMenuOptionType.Folder:
|
case ContextMenuOptionType.Folder:
|
||||||
@@ -194,6 +197,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||||||
{(option.type === ContextMenuOptionType.Problems ||
|
{(option.type === ContextMenuOptionType.Problems ||
|
||||||
((option.type === ContextMenuOptionType.File ||
|
((option.type === ContextMenuOptionType.File ||
|
||||||
option.type === ContextMenuOptionType.Folder ||
|
option.type === ContextMenuOptionType.Folder ||
|
||||||
|
option.type === ContextMenuOptionType.OpenedFile ||
|
||||||
option.type === ContextMenuOptionType.Git) &&
|
option.type === ContextMenuOptionType.Git) &&
|
||||||
option.value)) && (
|
option.value)) && (
|
||||||
<i
|
<i
|
||||||
|
|||||||
@@ -1,212 +0,0 @@
|
|||||||
import { render, fireEvent, screen } from "@testing-library/react"
|
|
||||||
import { useExtensionState } from "../../../context/ExtensionStateContext"
|
|
||||||
import AutoApproveMenu from "../AutoApproveMenu"
|
|
||||||
import { defaultModeSlug, defaultPrompts } from "../../../../../src/shared/modes"
|
|
||||||
import { experimentDefault } from "../../../../../src/shared/experiments"
|
|
||||||
|
|
||||||
// Mock the ExtensionStateContext hook
|
|
||||||
jest.mock("../../../context/ExtensionStateContext")
|
|
||||||
|
|
||||||
const mockUseExtensionState = useExtensionState as jest.MockedFunction<typeof useExtensionState>
|
|
||||||
|
|
||||||
describe("AutoApproveMenu", () => {
|
|
||||||
const defaultMockState = {
|
|
||||||
// Required state properties
|
|
||||||
version: "1.0.0",
|
|
||||||
clineMessages: [],
|
|
||||||
taskHistory: [],
|
|
||||||
shouldShowAnnouncement: false,
|
|
||||||
allowedCommands: [],
|
|
||||||
soundEnabled: false,
|
|
||||||
soundVolume: 0.5,
|
|
||||||
diffEnabled: false,
|
|
||||||
fuzzyMatchThreshold: 1.0,
|
|
||||||
preferredLanguage: "English",
|
|
||||||
writeDelayMs: 1000,
|
|
||||||
browserViewportSize: "900x600",
|
|
||||||
screenshotQuality: 75,
|
|
||||||
terminalOutputLineLimit: 500,
|
|
||||||
mcpEnabled: true,
|
|
||||||
requestDelaySeconds: 5,
|
|
||||||
rateLimitSeconds: 0,
|
|
||||||
currentApiConfigName: "default",
|
|
||||||
listApiConfigMeta: [],
|
|
||||||
mode: defaultModeSlug,
|
|
||||||
customModePrompts: defaultPrompts,
|
|
||||||
customSupportPrompts: {},
|
|
||||||
enhancementApiConfigId: "",
|
|
||||||
didHydrateState: true,
|
|
||||||
showWelcome: false,
|
|
||||||
theme: {},
|
|
||||||
glamaModels: {},
|
|
||||||
openRouterModels: {},
|
|
||||||
openAiModels: [],
|
|
||||||
mcpServers: [],
|
|
||||||
filePaths: [],
|
|
||||||
experiments: experimentDefault,
|
|
||||||
customModes: [],
|
|
||||||
enableMcpServerCreation: false,
|
|
||||||
|
|
||||||
// Auto-approve specific properties
|
|
||||||
alwaysAllowReadOnly: false,
|
|
||||||
alwaysAllowWrite: false,
|
|
||||||
alwaysAllowExecute: false,
|
|
||||||
alwaysAllowBrowser: false,
|
|
||||||
alwaysAllowMcp: false,
|
|
||||||
alwaysApproveResubmit: false,
|
|
||||||
alwaysAllowModeSwitch: false,
|
|
||||||
autoApprovalEnabled: false,
|
|
||||||
|
|
||||||
// Required setter functions
|
|
||||||
setApiConfiguration: jest.fn(),
|
|
||||||
setCustomInstructions: jest.fn(),
|
|
||||||
setAlwaysAllowReadOnly: jest.fn(),
|
|
||||||
setAlwaysAllowWrite: jest.fn(),
|
|
||||||
setAlwaysAllowExecute: jest.fn(),
|
|
||||||
setAlwaysAllowBrowser: jest.fn(),
|
|
||||||
setAlwaysAllowMcp: jest.fn(),
|
|
||||||
setAlwaysAllowModeSwitch: jest.fn(),
|
|
||||||
setShowAnnouncement: jest.fn(),
|
|
||||||
setAllowedCommands: jest.fn(),
|
|
||||||
setSoundEnabled: jest.fn(),
|
|
||||||
setSoundVolume: jest.fn(),
|
|
||||||
setDiffEnabled: jest.fn(),
|
|
||||||
setBrowserViewportSize: jest.fn(),
|
|
||||||
setFuzzyMatchThreshold: jest.fn(),
|
|
||||||
setPreferredLanguage: jest.fn(),
|
|
||||||
setWriteDelayMs: jest.fn(),
|
|
||||||
setScreenshotQuality: jest.fn(),
|
|
||||||
setTerminalOutputLineLimit: jest.fn(),
|
|
||||||
setMcpEnabled: jest.fn(),
|
|
||||||
setAlwaysApproveResubmit: jest.fn(),
|
|
||||||
setRequestDelaySeconds: jest.fn(),
|
|
||||||
setRateLimitSeconds: jest.fn(),
|
|
||||||
setCurrentApiConfigName: jest.fn(),
|
|
||||||
setListApiConfigMeta: jest.fn(),
|
|
||||||
onUpdateApiConfig: jest.fn(),
|
|
||||||
setMode: jest.fn(),
|
|
||||||
setCustomModePrompts: jest.fn(),
|
|
||||||
setCustomSupportPrompts: jest.fn(),
|
|
||||||
setEnhancementApiConfigId: jest.fn(),
|
|
||||||
setAutoApprovalEnabled: jest.fn(),
|
|
||||||
setExperimentEnabled: jest.fn(),
|
|
||||||
handleInputChange: jest.fn(),
|
|
||||||
setCustomModes: jest.fn(),
|
|
||||||
setEnableMcpServerCreation: jest.fn(),
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockUseExtensionState.mockReturnValue(defaultMockState)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("renders with initial collapsed state", () => {
|
|
||||||
render(<AutoApproveMenu />)
|
|
||||||
|
|
||||||
// Check for main checkbox and label
|
|
||||||
expect(screen.getByText("Auto-approve:")).toBeInTheDocument()
|
|
||||||
expect(screen.getByText("None")).toBeInTheDocument()
|
|
||||||
|
|
||||||
// Verify the menu is collapsed (actions not visible)
|
|
||||||
expect(screen.queryByText("Read files and directories")).not.toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("expands menu when clicked", () => {
|
|
||||||
render(<AutoApproveMenu />)
|
|
||||||
|
|
||||||
// Click to expand
|
|
||||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
|
||||||
|
|
||||||
// Verify menu items are visible
|
|
||||||
expect(screen.getByText("Read files and directories")).toBeInTheDocument()
|
|
||||||
expect(screen.getByText("Edit files")).toBeInTheDocument()
|
|
||||||
expect(screen.getByText("Execute approved commands")).toBeInTheDocument()
|
|
||||||
expect(screen.getByText("Use the browser")).toBeInTheDocument()
|
|
||||||
expect(screen.getByText("Use MCP servers")).toBeInTheDocument()
|
|
||||||
expect(screen.getByText("Retry failed requests")).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("toggles main auto-approval checkbox", () => {
|
|
||||||
render(<AutoApproveMenu />)
|
|
||||||
|
|
||||||
const mainCheckbox = screen.getByRole("checkbox")
|
|
||||||
fireEvent.click(mainCheckbox)
|
|
||||||
|
|
||||||
expect(defaultMockState.setAutoApprovalEnabled).toHaveBeenCalledWith(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("toggles individual permissions", () => {
|
|
||||||
render(<AutoApproveMenu />)
|
|
||||||
|
|
||||||
// Expand menu
|
|
||||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
|
||||||
|
|
||||||
// Click read files checkbox
|
|
||||||
fireEvent.click(screen.getByText("Read files and directories"))
|
|
||||||
expect(defaultMockState.setAlwaysAllowReadOnly).toHaveBeenCalledWith(true)
|
|
||||||
|
|
||||||
// Click edit files checkbox
|
|
||||||
fireEvent.click(screen.getByText("Edit files"))
|
|
||||||
expect(defaultMockState.setAlwaysAllowWrite).toHaveBeenCalledWith(true)
|
|
||||||
|
|
||||||
// Click execute commands checkbox
|
|
||||||
fireEvent.click(screen.getByText("Execute approved commands"))
|
|
||||||
expect(defaultMockState.setAlwaysAllowExecute).toHaveBeenCalledWith(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("displays enabled actions in summary", () => {
|
|
||||||
mockUseExtensionState.mockReturnValue({
|
|
||||||
...defaultMockState,
|
|
||||||
alwaysAllowReadOnly: true,
|
|
||||||
alwaysAllowWrite: true,
|
|
||||||
autoApprovalEnabled: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
render(<AutoApproveMenu />)
|
|
||||||
|
|
||||||
// Check that enabled actions are shown in summary
|
|
||||||
expect(screen.getByText("Read, Edit")).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("preserves checkbox states", () => {
|
|
||||||
// Mock state with some permissions enabled
|
|
||||||
const mockState = {
|
|
||||||
...defaultMockState,
|
|
||||||
alwaysAllowReadOnly: true,
|
|
||||||
alwaysAllowWrite: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update mock to return our state
|
|
||||||
mockUseExtensionState.mockReturnValue(mockState)
|
|
||||||
|
|
||||||
render(<AutoApproveMenu />)
|
|
||||||
|
|
||||||
// Expand menu
|
|
||||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
|
||||||
|
|
||||||
// Verify read and edit checkboxes are checked
|
|
||||||
expect(screen.getByLabelText("Read files and directories")).toBeInTheDocument()
|
|
||||||
expect(screen.getByLabelText("Edit files")).toBeInTheDocument()
|
|
||||||
|
|
||||||
// Verify the setters haven't been called yet
|
|
||||||
expect(mockState.setAlwaysAllowReadOnly).not.toHaveBeenCalled()
|
|
||||||
expect(mockState.setAlwaysAllowWrite).not.toHaveBeenCalled()
|
|
||||||
|
|
||||||
// Collapse menu
|
|
||||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
|
||||||
|
|
||||||
// Expand again
|
|
||||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
|
||||||
|
|
||||||
// Verify checkboxes are still present
|
|
||||||
expect(screen.getByLabelText("Read files and directories")).toBeInTheDocument()
|
|
||||||
expect(screen.getByLabelText("Edit files")).toBeInTheDocument()
|
|
||||||
|
|
||||||
// Verify the setters still haven't been called
|
|
||||||
expect(mockState.setAlwaysAllowReadOnly).not.toHaveBeenCalled()
|
|
||||||
expect(mockState.setAlwaysAllowWrite).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -41,6 +41,7 @@ describe("ChatTextArea", () => {
|
|||||||
// Default mock implementation for useExtensionState
|
// Default mock implementation for useExtensionState
|
||||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||||
filePaths: [],
|
filePaths: [],
|
||||||
|
openedTabs: [],
|
||||||
apiConfiguration: {
|
apiConfiguration: {
|
||||||
apiProvider: "anthropic",
|
apiProvider: "anthropic",
|
||||||
},
|
},
|
||||||
@@ -51,6 +52,7 @@ describe("ChatTextArea", () => {
|
|||||||
it("should be disabled when textAreaDisabled is true", () => {
|
it("should be disabled when textAreaDisabled is true", () => {
|
||||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||||
filePaths: [],
|
filePaths: [],
|
||||||
|
openedTabs: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<ChatTextArea {...defaultProps} textAreaDisabled={true} />)
|
render(<ChatTextArea {...defaultProps} textAreaDisabled={true} />)
|
||||||
@@ -68,6 +70,7 @@ describe("ChatTextArea", () => {
|
|||||||
|
|
||||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||||
filePaths: [],
|
filePaths: [],
|
||||||
|
openedTabs: [],
|
||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -85,6 +88,7 @@ describe("ChatTextArea", () => {
|
|||||||
it("should not send message when input is empty", () => {
|
it("should not send message when input is empty", () => {
|
||||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||||
filePaths: [],
|
filePaths: [],
|
||||||
|
openedTabs: [],
|
||||||
apiConfiguration: {
|
apiConfiguration: {
|
||||||
apiProvider: "openrouter",
|
apiProvider: "openrouter",
|
||||||
},
|
},
|
||||||
@@ -101,6 +105,7 @@ describe("ChatTextArea", () => {
|
|||||||
it("should show loading state while enhancing", () => {
|
it("should show loading state while enhancing", () => {
|
||||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||||
filePaths: [],
|
filePaths: [],
|
||||||
|
openedTabs: [],
|
||||||
apiConfiguration: {
|
apiConfiguration: {
|
||||||
apiProvider: "openrouter",
|
apiProvider: "openrouter",
|
||||||
},
|
},
|
||||||
@@ -123,6 +128,7 @@ describe("ChatTextArea", () => {
|
|||||||
// Update apiConfiguration
|
// Update apiConfiguration
|
||||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||||
filePaths: [],
|
filePaths: [],
|
||||||
|
openedTabs: [],
|
||||||
apiConfiguration: {
|
apiConfiguration: {
|
||||||
apiProvider: "openrouter",
|
apiProvider: "openrouter",
|
||||||
newSetting: "test",
|
newSetting: "test",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||||||
openAiModels: string[]
|
openAiModels: string[]
|
||||||
mcpServers: McpServer[]
|
mcpServers: McpServer[]
|
||||||
filePaths: string[]
|
filePaths: string[]
|
||||||
|
openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
|
||||||
setApiConfiguration: (config: ApiConfiguration) => void
|
setApiConfiguration: (config: ApiConfiguration) => void
|
||||||
setCustomInstructions: (value?: string) => void
|
setCustomInstructions: (value?: string) => void
|
||||||
setAlwaysAllowReadOnly: (value: boolean) => void
|
setAlwaysAllowReadOnly: (value: boolean) => void
|
||||||
@@ -116,6 +117,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
const [glamaModels, setGlamaModels] = useState<Record<string, ModelInfo>>({
|
const [glamaModels, setGlamaModels] = useState<Record<string, ModelInfo>>({
|
||||||
[glamaDefaultModelId]: glamaDefaultModelInfo,
|
[glamaDefaultModelId]: glamaDefaultModelInfo,
|
||||||
})
|
})
|
||||||
|
const [openedTabs, setOpenedTabs] = useState<Array<{ label: string; isActive: boolean; path?: string }>>([])
|
||||||
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
|
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
|
||||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
|
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
|
||||||
})
|
})
|
||||||
@@ -176,7 +178,11 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "workspaceUpdated": {
|
case "workspaceUpdated": {
|
||||||
setFilePaths(message.filePaths ?? [])
|
const paths = message.filePaths ?? []
|
||||||
|
const tabs = message.openedTabs ?? []
|
||||||
|
|
||||||
|
setFilePaths(paths)
|
||||||
|
setOpenedTabs(tabs)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "partialMessage": {
|
case "partialMessage": {
|
||||||
@@ -243,6 +249,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
openAiModels,
|
openAiModels,
|
||||||
mcpServers,
|
mcpServers,
|
||||||
filePaths,
|
filePaths,
|
||||||
|
openedTabs,
|
||||||
soundVolume: state.soundVolume,
|
soundVolume: state.soundVolume,
|
||||||
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
|
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
|
||||||
writeDelayMs: state.writeDelayMs,
|
writeDelayMs: state.writeDelayMs,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export function removeMention(text: string, position: number): { newText: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum ContextMenuOptionType {
|
export enum ContextMenuOptionType {
|
||||||
|
OpenedFile = "openedFile",
|
||||||
File = "file",
|
File = "file",
|
||||||
Folder = "folder",
|
Folder = "folder",
|
||||||
Problems = "problems",
|
Problems = "problems",
|
||||||
@@ -80,8 +81,14 @@ export function getContextMenuOptions(
|
|||||||
if (query === "") {
|
if (query === "") {
|
||||||
if (selectedType === ContextMenuOptionType.File) {
|
if (selectedType === ContextMenuOptionType.File) {
|
||||||
const files = queryItems
|
const files = queryItems
|
||||||
.filter((item) => item.type === ContextMenuOptionType.File)
|
.filter(
|
||||||
.map((item) => ({ type: ContextMenuOptionType.File, value: item.value }))
|
(item) =>
|
||||||
|
item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.OpenedFile,
|
||||||
|
)
|
||||||
|
.map((item) => ({
|
||||||
|
type: item.type,
|
||||||
|
value: item.value,
|
||||||
|
}))
|
||||||
return files.length > 0 ? files : [{ type: ContextMenuOptionType.NoResults }]
|
return files.length > 0 ? files : [{ type: ContextMenuOptionType.NoResults }]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,12 +169,16 @@ export function getContextMenuOptions(
|
|||||||
|
|
||||||
// Separate matches by type
|
// Separate matches by type
|
||||||
const fileMatches = matchingItems.filter(
|
const fileMatches = matchingItems.filter(
|
||||||
(item) => item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.Folder,
|
(item) =>
|
||||||
|
item.type === ContextMenuOptionType.File ||
|
||||||
|
item.type === ContextMenuOptionType.OpenedFile ||
|
||||||
|
item.type === ContextMenuOptionType.Folder,
|
||||||
)
|
)
|
||||||
const gitMatches = matchingItems.filter((item) => item.type === ContextMenuOptionType.Git)
|
const gitMatches = matchingItems.filter((item) => item.type === ContextMenuOptionType.Git)
|
||||||
const otherMatches = matchingItems.filter(
|
const otherMatches = matchingItems.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.type !== ContextMenuOptionType.File &&
|
item.type !== ContextMenuOptionType.File &&
|
||||||
|
item.type !== ContextMenuOptionType.OpenedFile &&
|
||||||
item.type !== ContextMenuOptionType.Folder &&
|
item.type !== ContextMenuOptionType.Folder &&
|
||||||
item.type !== ContextMenuOptionType.Git,
|
item.type !== ContextMenuOptionType.Git,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user