mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Add options to always approve write and execute operations
This commit is contained in:
21
jest.config.js
Normal file
21
jest.config.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.tsx?$': ['ts-jest', {
|
||||||
|
tsconfig: 'tsconfig.json'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^vscode$': '<rootDir>/node_modules/@types/vscode/index.d.ts'
|
||||||
|
},
|
||||||
|
setupFiles: [],
|
||||||
|
globals: {
|
||||||
|
'ts-jest': {
|
||||||
|
diagnostics: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
3244
package-lock.json
generated
3244
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -136,16 +136,17 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/diff": "^5.2.1",
|
"@types/diff": "^5.2.1",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
"@types/mocha": "^10.0.7",
|
"@types/mocha": "^10.0.7",
|
||||||
"@types/node": "20.x",
|
"@types/node": "20.x",
|
||||||
"@types/vscode": "^1.84.0",
|
"@types/vscode": "^1.95.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||||
"@typescript-eslint/parser": "^7.11.0",
|
"@typescript-eslint/parser": "^7.11.0",
|
||||||
"@vscode/test-cli": "^0.0.9",
|
"@vscode/test-cli": "^0.0.9",
|
||||||
"@vscode/test-electron": "^2.4.0",
|
"@vscode/test-electron": "^2.4.0",
|
||||||
"esbuild": "^0.21.5",
|
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.4.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -163,9 +164,11 @@
|
|||||||
"default-shell": "^2.2.0",
|
"default-shell": "^2.2.0",
|
||||||
"delay": "^6.0.0",
|
"delay": "^6.0.0",
|
||||||
"diff": "^5.2.0",
|
"diff": "^5.2.0",
|
||||||
|
"esbuild": "^0.24.0",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"globby": "^14.0.2",
|
"globby": "^14.0.2",
|
||||||
"isbinaryfile": "^5.0.2",
|
"isbinaryfile": "^5.0.2",
|
||||||
|
"jest": "^29.7.0",
|
||||||
"mammoth": "^1.8.0",
|
"mammoth": "^1.8.0",
|
||||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||||
"openai": "^4.61.0",
|
"openai": "^4.61.0",
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ export class Cline {
|
|||||||
private didEditFile: boolean = false
|
private didEditFile: boolean = false
|
||||||
customInstructions?: string
|
customInstructions?: string
|
||||||
alwaysAllowReadOnly: boolean
|
alwaysAllowReadOnly: boolean
|
||||||
|
alwaysAllowWrite: boolean
|
||||||
|
alwaysAllowExecute: boolean
|
||||||
|
|
||||||
apiConversationHistory: Anthropic.MessageParam[] = []
|
apiConversationHistory: Anthropic.MessageParam[] = []
|
||||||
clineMessages: ClineMessage[] = []
|
clineMessages: ClineMessage[] = []
|
||||||
private askResponse?: ClineAskResponse
|
private askResponse?: ClineAskResponse
|
||||||
@@ -93,6 +96,8 @@ export class Cline {
|
|||||||
apiConfiguration: ApiConfiguration,
|
apiConfiguration: ApiConfiguration,
|
||||||
customInstructions?: string,
|
customInstructions?: string,
|
||||||
alwaysAllowReadOnly?: boolean,
|
alwaysAllowReadOnly?: boolean,
|
||||||
|
alwaysAllowWrite?: boolean,
|
||||||
|
alwaysAllowExecute?: boolean,
|
||||||
task?: string,
|
task?: string,
|
||||||
images?: string[],
|
images?: string[],
|
||||||
historyItem?: HistoryItem
|
historyItem?: HistoryItem
|
||||||
@@ -105,6 +110,8 @@ export class Cline {
|
|||||||
this.diffViewProvider = new DiffViewProvider(cwd)
|
this.diffViewProvider = new DiffViewProvider(cwd)
|
||||||
this.customInstructions = customInstructions
|
this.customInstructions = customInstructions
|
||||||
this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false
|
this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false
|
||||||
|
this.alwaysAllowWrite = alwaysAllowWrite ?? false
|
||||||
|
this.alwaysAllowExecute = alwaysAllowExecute ?? false
|
||||||
|
|
||||||
if (historyItem) {
|
if (historyItem) {
|
||||||
this.taskId = historyItem.id
|
this.taskId = historyItem.id
|
||||||
@@ -1052,7 +1059,11 @@ export class Cline {
|
|||||||
if (block.partial) {
|
if (block.partial) {
|
||||||
// update gui message
|
// update gui message
|
||||||
const partialMessage = JSON.stringify(sharedMessageProps)
|
const partialMessage = JSON.stringify(sharedMessageProps)
|
||||||
|
if (this.alwaysAllowWrite) {
|
||||||
|
await this.say("tool", partialMessage, undefined, block.partial)
|
||||||
|
} else {
|
||||||
await this.ask("tool", partialMessage, block.partial).catch(() => {})
|
await this.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||||
|
}
|
||||||
// update editor
|
// update editor
|
||||||
if (!this.diffViewProvider.isEditing) {
|
if (!this.diffViewProvider.isEditing) {
|
||||||
// open the editor and prepare to stream content in
|
// open the editor and prepare to stream content in
|
||||||
@@ -1082,7 +1093,11 @@ export class Cline {
|
|||||||
if (!this.diffViewProvider.isEditing) {
|
if (!this.diffViewProvider.isEditing) {
|
||||||
// show gui message before showing edit animation
|
// show gui message before showing edit animation
|
||||||
const partialMessage = JSON.stringify(sharedMessageProps)
|
const partialMessage = JSON.stringify(sharedMessageProps)
|
||||||
|
if (this.alwaysAllowWrite) {
|
||||||
|
await this.say("tool", partialMessage, undefined, true)
|
||||||
|
} else {
|
||||||
await this.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor
|
await this.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor
|
||||||
|
}
|
||||||
await this.diffViewProvider.open(relPath)
|
await this.diffViewProvider.open(relPath)
|
||||||
}
|
}
|
||||||
await this.diffViewProvider.update(newContent, true)
|
await this.diffViewProvider.update(newContent, true)
|
||||||
@@ -1101,7 +1116,7 @@ export class Cline {
|
|||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
} satisfies ClineSayTool)
|
} satisfies ClineSayTool)
|
||||||
const didApprove = await askApproval("tool", completeMessage)
|
const didApprove = this.alwaysAllowWrite || (await askApproval("tool", completeMessage))
|
||||||
if (!didApprove) {
|
if (!didApprove) {
|
||||||
await this.diffViewProvider.revertChanges()
|
await this.diffViewProvider.revertChanges()
|
||||||
break
|
break
|
||||||
@@ -1492,9 +1507,13 @@ export class Cline {
|
|||||||
const command: string | undefined = block.params.command
|
const command: string | undefined = block.params.command
|
||||||
try {
|
try {
|
||||||
if (block.partial) {
|
if (block.partial) {
|
||||||
|
if (this.alwaysAllowExecute) {
|
||||||
|
await this.say("command", command, undefined, block.partial)
|
||||||
|
} else {
|
||||||
await this.ask("command", removeClosingTag("command", command), block.partial).catch(
|
await this.ask("command", removeClosingTag("command", command), block.partial).catch(
|
||||||
() => {}
|
() => {}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
if (!command) {
|
if (!command) {
|
||||||
@@ -1505,7 +1524,7 @@ export class Cline {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
this.consecutiveMistakeCount = 0
|
this.consecutiveMistakeCount = 0
|
||||||
const didApprove = await askApproval("command", command)
|
const didApprove = this.alwaysAllowExecute || (await askApproval("command", command))
|
||||||
if (!didApprove) {
|
if (!didApprove) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
322
src/core/__tests__/Cline.test.ts
Normal file
322
src/core/__tests__/Cline.test.ts
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
import { Cline } from '../Cline';
|
||||||
|
import { ClineProvider } from '../webview/ClineProvider';
|
||||||
|
import { ApiConfiguration } from '../../shared/api';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
// Mock fileExistsAtPath
|
||||||
|
jest.mock('../../utils/fs', () => ({
|
||||||
|
fileExistsAtPath: jest.fn().mockImplementation((filePath) => {
|
||||||
|
return filePath.includes('ui_messages.json') ||
|
||||||
|
filePath.includes('api_conversation_history.json');
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock fs/promises
|
||||||
|
const mockMessages = [{
|
||||||
|
ts: Date.now(),
|
||||||
|
type: 'say',
|
||||||
|
say: 'text',
|
||||||
|
text: 'historical task'
|
||||||
|
}];
|
||||||
|
|
||||||
|
jest.mock('fs/promises', () => ({
|
||||||
|
mkdir: jest.fn().mockResolvedValue(undefined),
|
||||||
|
writeFile: jest.fn().mockResolvedValue(undefined),
|
||||||
|
readFile: jest.fn().mockImplementation((filePath) => {
|
||||||
|
if (filePath.includes('ui_messages.json')) {
|
||||||
|
return Promise.resolve(JSON.stringify(mockMessages));
|
||||||
|
}
|
||||||
|
if (filePath.includes('api_conversation_history.json')) {
|
||||||
|
return Promise.resolve('[]');
|
||||||
|
}
|
||||||
|
return Promise.resolve('[]');
|
||||||
|
}),
|
||||||
|
unlink: jest.fn().mockResolvedValue(undefined),
|
||||||
|
rmdir: jest.fn().mockResolvedValue(undefined)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('vscode', () => {
|
||||||
|
const mockDisposable = { dispose: jest.fn() };
|
||||||
|
const mockEventEmitter = {
|
||||||
|
event: jest.fn(),
|
||||||
|
fire: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTextDocument = {
|
||||||
|
uri: {
|
||||||
|
fsPath: '/mock/workspace/path/file.ts'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTextEditor = {
|
||||||
|
document: mockTextDocument
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTab = {
|
||||||
|
input: {
|
||||||
|
uri: {
|
||||||
|
fsPath: '/mock/workspace/path/file.ts'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTabGroup = {
|
||||||
|
tabs: [mockTab]
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
window: {
|
||||||
|
createTextEditorDecorationType: jest.fn().mockReturnValue({
|
||||||
|
dispose: jest.fn()
|
||||||
|
}),
|
||||||
|
visibleTextEditors: [mockTextEditor],
|
||||||
|
tabGroups: {
|
||||||
|
all: [mockTabGroup]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
workspaceFolders: [{
|
||||||
|
uri: {
|
||||||
|
fsPath: '/mock/workspace/path'
|
||||||
|
},
|
||||||
|
name: 'mock-workspace',
|
||||||
|
index: 0
|
||||||
|
}],
|
||||||
|
onDidCreateFiles: jest.fn(() => mockDisposable),
|
||||||
|
onDidDeleteFiles: jest.fn(() => mockDisposable),
|
||||||
|
onDidRenameFiles: jest.fn(() => mockDisposable)
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
uriScheme: 'vscode',
|
||||||
|
language: 'en'
|
||||||
|
},
|
||||||
|
EventEmitter: jest.fn().mockImplementation(() => mockEventEmitter),
|
||||||
|
Disposable: {
|
||||||
|
from: jest.fn()
|
||||||
|
},
|
||||||
|
TabInputText: jest.fn()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock p-wait-for to resolve immediately
|
||||||
|
jest.mock('p-wait-for', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn().mockImplementation(async () => Promise.resolve())
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('delay', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn().mockImplementation(async () => Promise.resolve())
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('serialize-error', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
serializeError: jest.fn().mockImplementation((error) => ({
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('strip-ansi', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn().mockImplementation((str) => str.replace(/\u001B\[\d+m/g, ''))
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('globby', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
globby: jest.fn().mockImplementation(async () => [])
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('os-name', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn().mockReturnValue('Mock OS Name')
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('default-shell', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: '/bin/bash' // Mock default shell path
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Cline', () => {
|
||||||
|
let mockProvider: jest.Mocked<ClineProvider>;
|
||||||
|
let mockApiConfig: ApiConfiguration;
|
||||||
|
let mockOutputChannel: any;
|
||||||
|
let mockExtensionContext: vscode.ExtensionContext;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Setup mock extension context
|
||||||
|
mockExtensionContext = {
|
||||||
|
globalState: {
|
||||||
|
get: jest.fn().mockImplementation((key) => {
|
||||||
|
if (key === 'taskHistory') {
|
||||||
|
return [{
|
||||||
|
id: '123',
|
||||||
|
ts: Date.now(),
|
||||||
|
task: 'historical task',
|
||||||
|
tokensIn: 100,
|
||||||
|
tokensOut: 200,
|
||||||
|
cacheWrites: 0,
|
||||||
|
cacheReads: 0,
|
||||||
|
totalCost: 0.001
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}),
|
||||||
|
update: jest.fn().mockImplementation((key, value) => Promise.resolve()),
|
||||||
|
keys: jest.fn().mockReturnValue([])
|
||||||
|
},
|
||||||
|
workspaceState: {
|
||||||
|
get: jest.fn().mockImplementation((key) => undefined),
|
||||||
|
update: jest.fn().mockImplementation((key, value) => Promise.resolve()),
|
||||||
|
keys: jest.fn().mockReturnValue([])
|
||||||
|
},
|
||||||
|
secrets: {
|
||||||
|
get: jest.fn().mockImplementation((key) => Promise.resolve(undefined)),
|
||||||
|
store: jest.fn().mockImplementation((key, value) => Promise.resolve()),
|
||||||
|
delete: jest.fn().mockImplementation((key) => Promise.resolve())
|
||||||
|
},
|
||||||
|
extensionUri: {
|
||||||
|
fsPath: '/mock/extension/path'
|
||||||
|
},
|
||||||
|
globalStorageUri: {
|
||||||
|
fsPath: '/mock/storage/path'
|
||||||
|
},
|
||||||
|
extension: {
|
||||||
|
packageJSON: {
|
||||||
|
version: '1.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as unknown as vscode.ExtensionContext;
|
||||||
|
|
||||||
|
// Setup mock output channel
|
||||||
|
mockOutputChannel = {
|
||||||
|
appendLine: jest.fn(),
|
||||||
|
append: jest.fn(),
|
||||||
|
clear: jest.fn(),
|
||||||
|
show: jest.fn(),
|
||||||
|
hide: jest.fn(),
|
||||||
|
dispose: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup mock provider with output channel
|
||||||
|
mockProvider = new ClineProvider(mockExtensionContext, mockOutputChannel) as jest.Mocked<ClineProvider>;
|
||||||
|
|
||||||
|
// Setup mock API configuration
|
||||||
|
mockApiConfig = {
|
||||||
|
apiProvider: 'anthropic',
|
||||||
|
apiModelId: 'claude-3-sonnet'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock provider methods
|
||||||
|
mockProvider.postMessageToWebview = jest.fn().mockResolvedValue(undefined);
|
||||||
|
mockProvider.postStateToWebview = jest.fn().mockResolvedValue(undefined);
|
||||||
|
mockProvider.getTaskWithId = jest.fn().mockImplementation(async (id) => ({
|
||||||
|
historyItem: {
|
||||||
|
id,
|
||||||
|
ts: Date.now(),
|
||||||
|
task: 'historical task',
|
||||||
|
tokensIn: 100,
|
||||||
|
tokensOut: 200,
|
||||||
|
cacheWrites: 0,
|
||||||
|
cacheReads: 0,
|
||||||
|
totalCost: 0.001
|
||||||
|
},
|
||||||
|
taskDirPath: '/mock/storage/path/tasks/123',
|
||||||
|
apiConversationHistoryFilePath: '/mock/storage/path/tasks/123/api_conversation_history.json',
|
||||||
|
uiMessagesFilePath: '/mock/storage/path/tasks/123/ui_messages.json',
|
||||||
|
apiConversationHistory: []
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should initialize with default settings', () => {
|
||||||
|
const cline = new Cline(
|
||||||
|
mockProvider,
|
||||||
|
mockApiConfig,
|
||||||
|
undefined, // customInstructions
|
||||||
|
undefined, // alwaysAllowReadOnly
|
||||||
|
undefined, // alwaysAllowWrite
|
||||||
|
undefined, // alwaysAllowExecute
|
||||||
|
'test task'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(cline.alwaysAllowReadOnly).toBe(false);
|
||||||
|
expect(cline.alwaysAllowWrite).toBe(false);
|
||||||
|
expect(cline.alwaysAllowExecute).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect provided settings', () => {
|
||||||
|
const cline = new Cline(
|
||||||
|
mockProvider,
|
||||||
|
mockApiConfig,
|
||||||
|
'custom instructions',
|
||||||
|
true, // alwaysAllowReadOnly
|
||||||
|
true, // alwaysAllowWrite
|
||||||
|
true, // alwaysAllowExecute
|
||||||
|
'test task'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(cline.alwaysAllowReadOnly).toBe(true);
|
||||||
|
expect(cline.alwaysAllowWrite).toBe(true);
|
||||||
|
expect(cline.alwaysAllowExecute).toBe(true);
|
||||||
|
expect(cline.customInstructions).toBe('custom instructions');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require either task or historyItem', () => {
|
||||||
|
expect(() => {
|
||||||
|
new Cline(
|
||||||
|
mockProvider,
|
||||||
|
mockApiConfig
|
||||||
|
);
|
||||||
|
}).toThrow('Either historyItem or task/images must be provided');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('file operations', () => {
|
||||||
|
let cline: Cline;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cline = new Cline(
|
||||||
|
mockProvider,
|
||||||
|
mockApiConfig,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
'test task'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bypass approval when alwaysAllowWrite is true', async () => {
|
||||||
|
const writeEnabledCline = new Cline(
|
||||||
|
mockProvider,
|
||||||
|
mockApiConfig,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
true, // alwaysAllowWrite
|
||||||
|
false,
|
||||||
|
'test task'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(writeEnabledCline.alwaysAllowWrite).toBe(true);
|
||||||
|
// The write operation would bypass approval in actual implementation
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require approval when alwaysAllowWrite is false', async () => {
|
||||||
|
const writeDisabledCline = new Cline(
|
||||||
|
mockProvider,
|
||||||
|
mockApiConfig,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
false, // alwaysAllowWrite
|
||||||
|
false,
|
||||||
|
'test task'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(writeDisabledCline.alwaysAllowWrite).toBe(false);
|
||||||
|
// The write operation would require approval in actual implementation
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -45,6 +45,8 @@ type GlobalStateKey =
|
|||||||
| "lastShownAnnouncementId"
|
| "lastShownAnnouncementId"
|
||||||
| "customInstructions"
|
| "customInstructions"
|
||||||
| "alwaysAllowReadOnly"
|
| "alwaysAllowReadOnly"
|
||||||
|
| "alwaysAllowWrite"
|
||||||
|
| "alwaysAllowExecute"
|
||||||
| "taskHistory"
|
| "taskHistory"
|
||||||
| "openAiBaseUrl"
|
| "openAiBaseUrl"
|
||||||
| "openAiModelId"
|
| "openAiModelId"
|
||||||
@@ -185,18 +187,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
|
|
||||||
async initClineWithTask(task?: string, images?: string[]) {
|
async initClineWithTask(task?: string, images?: string[]) {
|
||||||
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||||
const { apiConfiguration, customInstructions, alwaysAllowReadOnly } = await this.getState()
|
const { apiConfiguration, customInstructions, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute } = await this.getState()
|
||||||
this.cline = new Cline(this, apiConfiguration, customInstructions, alwaysAllowReadOnly, task, images)
|
this.cline = new Cline(this, apiConfiguration, customInstructions, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, task, images)
|
||||||
}
|
}
|
||||||
|
|
||||||
async initClineWithHistoryItem(historyItem: HistoryItem) {
|
async initClineWithHistoryItem(historyItem: HistoryItem) {
|
||||||
await this.clearTask()
|
await this.clearTask()
|
||||||
const { apiConfiguration, customInstructions, alwaysAllowReadOnly } = await this.getState()
|
const { apiConfiguration, customInstructions, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute } = await this.getState()
|
||||||
this.cline = new Cline(
|
this.cline = new Cline(
|
||||||
this,
|
this,
|
||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
customInstructions,
|
customInstructions,
|
||||||
alwaysAllowReadOnly,
|
alwaysAllowReadOnly,
|
||||||
|
alwaysAllowWrite,
|
||||||
|
alwaysAllowExecute,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
historyItem
|
historyItem
|
||||||
@@ -401,6 +405,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
break
|
break
|
||||||
|
case "alwaysAllowWrite":
|
||||||
|
await this.updateGlobalState("alwaysAllowWrite", message.bool ?? undefined)
|
||||||
|
if (this.cline) {
|
||||||
|
this.cline.alwaysAllowWrite = message.bool ?? false
|
||||||
|
}
|
||||||
|
await this.postStateToWebview()
|
||||||
|
break
|
||||||
|
case "alwaysAllowExecute":
|
||||||
|
await this.updateGlobalState("alwaysAllowExecute", message.bool ?? undefined)
|
||||||
|
if (this.cline) {
|
||||||
|
this.cline.alwaysAllowExecute = message.bool ?? false
|
||||||
|
}
|
||||||
|
await this.postStateToWebview()
|
||||||
|
break
|
||||||
case "askResponse":
|
case "askResponse":
|
||||||
this.cline?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
|
this.cline?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
|
||||||
break
|
break
|
||||||
@@ -737,13 +755,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getStateToPostToWebview() {
|
async getStateToPostToWebview() {
|
||||||
const { apiConfiguration, lastShownAnnouncementId, customInstructions, alwaysAllowReadOnly, taskHistory } =
|
const { apiConfiguration, lastShownAnnouncementId, customInstructions, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, taskHistory } =
|
||||||
await this.getState()
|
await this.getState()
|
||||||
return {
|
return {
|
||||||
version: this.context.extension?.packageJSON?.version ?? "",
|
version: this.context.extension?.packageJSON?.version ?? "",
|
||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
customInstructions,
|
customInstructions,
|
||||||
alwaysAllowReadOnly,
|
alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
|
||||||
|
alwaysAllowWrite: alwaysAllowWrite ?? false,
|
||||||
|
alwaysAllowExecute: alwaysAllowExecute ?? false,
|
||||||
uriScheme: vscode.env.uriScheme,
|
uriScheme: vscode.env.uriScheme,
|
||||||
clineMessages: this.cline?.clineMessages || [],
|
clineMessages: this.cline?.clineMessages || [],
|
||||||
taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
|
taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
|
||||||
@@ -828,6 +848,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
lastShownAnnouncementId,
|
lastShownAnnouncementId,
|
||||||
customInstructions,
|
customInstructions,
|
||||||
alwaysAllowReadOnly,
|
alwaysAllowReadOnly,
|
||||||
|
alwaysAllowWrite,
|
||||||
|
alwaysAllowExecute,
|
||||||
taskHistory,
|
taskHistory,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||||
@@ -854,6 +876,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
|
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
|
||||||
this.getGlobalState("customInstructions") as Promise<string | undefined>,
|
this.getGlobalState("customInstructions") as Promise<string | undefined>,
|
||||||
this.getGlobalState("alwaysAllowReadOnly") as Promise<boolean | undefined>,
|
this.getGlobalState("alwaysAllowReadOnly") as Promise<boolean | undefined>,
|
||||||
|
this.getGlobalState("alwaysAllowWrite") as Promise<boolean | undefined>,
|
||||||
|
this.getGlobalState("alwaysAllowExecute") as Promise<boolean | undefined>,
|
||||||
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
|
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -898,6 +922,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
lastShownAnnouncementId,
|
lastShownAnnouncementId,
|
||||||
customInstructions,
|
customInstructions,
|
||||||
alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
|
alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
|
||||||
|
alwaysAllowWrite: alwaysAllowWrite ?? false,
|
||||||
|
alwaysAllowExecute: alwaysAllowExecute ?? false,
|
||||||
taskHistory,
|
taskHistory,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export interface ExtensionState {
|
|||||||
apiConfiguration?: ApiConfiguration
|
apiConfiguration?: ApiConfiguration
|
||||||
customInstructions?: string
|
customInstructions?: string
|
||||||
alwaysAllowReadOnly?: boolean
|
alwaysAllowReadOnly?: boolean
|
||||||
|
alwaysAllowWrite?: boolean
|
||||||
|
alwaysAllowExecute?: boolean
|
||||||
uriScheme?: string
|
uriScheme?: string
|
||||||
clineMessages: ClineMessage[]
|
clineMessages: ClineMessage[]
|
||||||
taskHistory: HistoryItem[]
|
taskHistory: HistoryItem[]
|
||||||
@@ -74,6 +76,7 @@ export type ClineSay =
|
|||||||
| "shell_integration_warning"
|
| "shell_integration_warning"
|
||||||
| "browser_action"
|
| "browser_action"
|
||||||
| "browser_action_result"
|
| "browser_action_result"
|
||||||
|
| "command"
|
||||||
|
|
||||||
export interface ClineSayTool {
|
export interface ClineSayTool {
|
||||||
tool:
|
tool:
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export interface WebviewMessage {
|
|||||||
| "apiConfiguration"
|
| "apiConfiguration"
|
||||||
| "customInstructions"
|
| "customInstructions"
|
||||||
| "alwaysAllowReadOnly"
|
| "alwaysAllowReadOnly"
|
||||||
|
| "alwaysAllowWrite"
|
||||||
|
| "alwaysAllowExecute"
|
||||||
| "webviewDidLaunch"
|
| "webviewDidLaunch"
|
||||||
| "newTask"
|
| "newTask"
|
||||||
| "askResponse"
|
| "askResponse"
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
setCustomInstructions,
|
setCustomInstructions,
|
||||||
alwaysAllowReadOnly,
|
alwaysAllowReadOnly,
|
||||||
setAlwaysAllowReadOnly,
|
setAlwaysAllowReadOnly,
|
||||||
|
alwaysAllowWrite,
|
||||||
|
setAlwaysAllowWrite,
|
||||||
|
alwaysAllowExecute,
|
||||||
|
setAlwaysAllowExecute,
|
||||||
openRouterModels,
|
openRouterModels,
|
||||||
} = useExtensionState()
|
} = useExtensionState()
|
||||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||||
@@ -33,6 +37,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
|
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
|
||||||
vscode.postMessage({ type: "customInstructions", text: customInstructions })
|
vscode.postMessage({ type: "customInstructions", text: customInstructions })
|
||||||
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
|
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
|
||||||
|
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
|
||||||
|
vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
|
||||||
onDone()
|
onDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,6 +136,41 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 5 }}>
|
||||||
|
<VSCodeCheckbox
|
||||||
|
checked={alwaysAllowWrite}
|
||||||
|
onChange={(e: any) => setAlwaysAllowWrite(e.target.checked)}>
|
||||||
|
<span style={{ fontWeight: "500" }}>Always approve write operations</span>
|
||||||
|
</VSCodeCheckbox>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
marginTop: "5px",
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
}}>
|
||||||
|
When enabled, Cline will automatically write to files and create directories
|
||||||
|
without requiring you to click the Approve button.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 5 }}>
|
||||||
|
<VSCodeCheckbox
|
||||||
|
checked={alwaysAllowExecute}
|
||||||
|
onChange={(e: any) => setAlwaysAllowExecute(e.target.checked)}>
|
||||||
|
<span style={{ fontWeight: "500" }}>Always approve execute operations</span>
|
||||||
|
</VSCodeCheckbox>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
marginTop: "5px",
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
}}>
|
||||||
|
When enabled, Cline will automatically CLI commands without requiring
|
||||||
|
you to click the Approve button.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{IS_DEV && (
|
{IS_DEV && (
|
||||||
<>
|
<>
|
||||||
<div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>
|
<div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import { render, screen, act } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { ExtensionStateContextType } from '../../../context/ExtensionStateContext'
|
||||||
|
import SettingsView from '../SettingsView'
|
||||||
|
import { vscode } from '../../../utils/vscode'
|
||||||
|
import * as ExtensionStateContext from '../../../context/ExtensionStateContext'
|
||||||
|
import { ModelInfo } from '../../../../../src/shared/api'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('../../../utils/vscode', () => ({
|
||||||
|
vscode: {
|
||||||
|
postMessage: jest.fn()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock validation functions
|
||||||
|
jest.mock('../../../utils/validate', () => ({
|
||||||
|
validateApiConfiguration: jest.fn(() => undefined),
|
||||||
|
validateModelId: jest.fn(() => undefined)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock ApiOptions component
|
||||||
|
jest.mock('../ApiOptions', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div data-testid="mock-api-options" />
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock VS Code components
|
||||||
|
jest.mock('@vscode/webview-ui-toolkit/react', () => ({
|
||||||
|
VSCodeButton: ({ children, onClick }: any) => (
|
||||||
|
<button onClick={onClick}>{children}</button>
|
||||||
|
),
|
||||||
|
VSCodeCheckbox: ({ children, checked, onChange }: any) => (
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={e => onChange(e)}
|
||||||
|
aria-checked={checked}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
),
|
||||||
|
VSCodeTextArea: ({ children, value, onInput }: any) => (
|
||||||
|
<textarea
|
||||||
|
data-testid="custom-instructions"
|
||||||
|
value={value}
|
||||||
|
readOnly
|
||||||
|
aria-label="Custom Instructions"
|
||||||
|
>{children}</textarea>
|
||||||
|
),
|
||||||
|
VSCodeLink: ({ children, href }: any) => (
|
||||||
|
<a href={href}>{children}</a>
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('SettingsView', () => {
|
||||||
|
const mockOnDone = jest.fn()
|
||||||
|
const mockSetAlwaysAllowWrite = jest.fn()
|
||||||
|
const mockSetAlwaysAllowReadOnly = jest.fn()
|
||||||
|
const mockSetCustomInstructions = jest.fn()
|
||||||
|
const mockSetAlwaysAllowExecute = jest.fn()
|
||||||
|
|
||||||
|
let mockState: ExtensionStateContextType
|
||||||
|
|
||||||
|
const mockOpenRouterModels: Record<string, ModelInfo> = {
|
||||||
|
'claude-3-sonnet': {
|
||||||
|
maxTokens: 200000,
|
||||||
|
contextWindow: 200000,
|
||||||
|
supportsImages: true,
|
||||||
|
supportsComputerUse: true,
|
||||||
|
supportsPromptCache: true,
|
||||||
|
inputPrice: 0.000008,
|
||||||
|
outputPrice: 0.000024,
|
||||||
|
description: "Anthropic's Claude 3 Sonnet model"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
mockState = {
|
||||||
|
apiConfiguration: {
|
||||||
|
apiProvider: 'anthropic',
|
||||||
|
apiModelId: 'claude-3-sonnet'
|
||||||
|
},
|
||||||
|
version: '1.0.0',
|
||||||
|
customInstructions: 'Test instructions',
|
||||||
|
alwaysAllowReadOnly: true,
|
||||||
|
alwaysAllowWrite: true,
|
||||||
|
alwaysAllowExecute: true,
|
||||||
|
openRouterModels: mockOpenRouterModels,
|
||||||
|
didHydrateState: true,
|
||||||
|
showWelcome: false,
|
||||||
|
theme: 'dark',
|
||||||
|
filePaths: [],
|
||||||
|
taskHistory: [],
|
||||||
|
shouldShowAnnouncement: false,
|
||||||
|
clineMessages: [],
|
||||||
|
uriScheme: 'vscode',
|
||||||
|
|
||||||
|
setAlwaysAllowReadOnly: mockSetAlwaysAllowReadOnly,
|
||||||
|
setAlwaysAllowWrite: mockSetAlwaysAllowWrite,
|
||||||
|
setCustomInstructions: mockSetCustomInstructions,
|
||||||
|
setAlwaysAllowExecute: mockSetAlwaysAllowExecute,
|
||||||
|
setApiConfiguration: jest.fn(),
|
||||||
|
setShowAnnouncement: jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock the useExtensionState hook
|
||||||
|
jest.spyOn(ExtensionStateContext, 'useExtensionState').mockReturnValue(mockState)
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderSettingsView = () => {
|
||||||
|
return render(
|
||||||
|
<SettingsView onDone={mockOnDone} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Checkboxes', () => {
|
||||||
|
it('should toggle alwaysAllowWrite checkbox', async () => {
|
||||||
|
mockState.alwaysAllowWrite = false
|
||||||
|
renderSettingsView()
|
||||||
|
|
||||||
|
const writeCheckbox = screen.getByRole('checkbox', {
|
||||||
|
name: /Always approve write operations/i
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(writeCheckbox).not.toBeChecked()
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(writeCheckbox)
|
||||||
|
})
|
||||||
|
expect(mockSetAlwaysAllowWrite).toHaveBeenCalledWith(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should toggle alwaysAllowExecute checkbox', async () => {
|
||||||
|
mockState.alwaysAllowExecute = false
|
||||||
|
renderSettingsView()
|
||||||
|
|
||||||
|
const executeCheckbox = screen.getByRole('checkbox', {
|
||||||
|
name: /Always approve execute operations/i
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(executeCheckbox).not.toBeChecked()
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(executeCheckbox)
|
||||||
|
})
|
||||||
|
expect(mockSetAlwaysAllowExecute).toHaveBeenCalledWith(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should toggle alwaysAllowReadOnly checkbox', async () => {
|
||||||
|
mockState.alwaysAllowReadOnly = false
|
||||||
|
renderSettingsView()
|
||||||
|
|
||||||
|
const readOnlyCheckbox = screen.getByRole('checkbox', {
|
||||||
|
name: /Always approve read-only operations/i
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(readOnlyCheckbox).not.toBeChecked()
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(readOnlyCheckbox)
|
||||||
|
})
|
||||||
|
expect(mockSetAlwaysAllowReadOnly).toHaveBeenCalledWith(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Form Submission', () => {
|
||||||
|
it('should send correct messages when form is submitted', async () => {
|
||||||
|
renderSettingsView()
|
||||||
|
|
||||||
|
// Submit form
|
||||||
|
const doneButton = screen.getByRole('button', { name: /Done/i })
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(doneButton)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify messages were sent in the correct order
|
||||||
|
const calls = (vscode.postMessage as jest.Mock).mock.calls
|
||||||
|
expect(calls).toHaveLength(5)
|
||||||
|
|
||||||
|
expect(calls[0][0]).toEqual({
|
||||||
|
type: 'apiConfiguration',
|
||||||
|
apiConfiguration: {
|
||||||
|
apiProvider: 'anthropic',
|
||||||
|
apiModelId: 'claude-3-sonnet'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(calls[1][0]).toEqual({
|
||||||
|
type: 'customInstructions',
|
||||||
|
text: 'Test instructions'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(calls[2][0]).toEqual({
|
||||||
|
type: 'alwaysAllowReadOnly',
|
||||||
|
bool: true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(calls[3][0]).toEqual({
|
||||||
|
type: 'alwaysAllowWrite',
|
||||||
|
bool: true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(calls[4][0]).toEqual({
|
||||||
|
type: 'alwaysAllowExecute',
|
||||||
|
bool: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify onDone was called
|
||||||
|
expect(mockOnDone).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('should have accessible form controls', () => {
|
||||||
|
renderSettingsView()
|
||||||
|
|
||||||
|
// Check for proper labels and ARIA attributes
|
||||||
|
const writeCheckbox = screen.getByRole('checkbox', {
|
||||||
|
name: /Always approve write operations/i
|
||||||
|
})
|
||||||
|
expect(writeCheckbox).toHaveAttribute('aria-checked')
|
||||||
|
|
||||||
|
const textarea = screen.getByRole('textbox', {
|
||||||
|
name: /Custom Instructions/i
|
||||||
|
})
|
||||||
|
expect(textarea).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -11,7 +11,7 @@ import { vscode } from "../utils/vscode"
|
|||||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||||
import { findLastIndex } from "../../../src/shared/array"
|
import { findLastIndex } from "../../../src/shared/array"
|
||||||
|
|
||||||
interface ExtensionStateContextType extends ExtensionState {
|
export interface ExtensionStateContextType extends ExtensionState {
|
||||||
didHydrateState: boolean
|
didHydrateState: boolean
|
||||||
showWelcome: boolean
|
showWelcome: boolean
|
||||||
theme: any
|
theme: any
|
||||||
@@ -20,6 +20,8 @@ interface ExtensionStateContextType extends ExtensionState {
|
|||||||
setApiConfiguration: (config: ApiConfiguration) => void
|
setApiConfiguration: (config: ApiConfiguration) => void
|
||||||
setCustomInstructions: (value?: string) => void
|
setCustomInstructions: (value?: string) => void
|
||||||
setAlwaysAllowReadOnly: (value: boolean) => void
|
setAlwaysAllowReadOnly: (value: boolean) => void
|
||||||
|
setAlwaysAllowWrite: (value: boolean) => void
|
||||||
|
setAlwaysAllowExecute: (value: boolean) => void
|
||||||
setShowAnnouncement: (value: boolean) => void
|
setShowAnnouncement: (value: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +115,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
|
setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
|
||||||
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
|
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
|
||||||
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
|
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
|
||||||
|
setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })),
|
||||||
|
setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })),
|
||||||
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
|
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user