mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
Merge branch 'RooCodeInc:main' into main
This commit is contained in:
commit
faf2ee591e
100 changed files with 2973 additions and 841 deletions
|
|
@ -401,6 +401,14 @@ export class CustomModesManager {
|
|||
|
||||
public async updateCustomMode(slug: string, config: ModeConfig): Promise<void> {
|
||||
try {
|
||||
// Validate the mode configuration before saving
|
||||
const validationResult = modeConfigSchema.safeParse(config)
|
||||
if (!validationResult.success) {
|
||||
const errors = validationResult.error.errors.map((e) => e.message).join(", ")
|
||||
logger.error(`Invalid mode configuration for ${slug}`, { errors: validationResult.error.errors })
|
||||
throw new Error(`Invalid mode configuration: ${errors}`)
|
||||
}
|
||||
|
||||
const isProjectMode = config.source === "project"
|
||||
let targetPath: string
|
||||
|
||||
|
|
|
|||
|
|
@ -179,22 +179,12 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
|
|||
// Add current time information with timezone.
|
||||
const now = new Date()
|
||||
|
||||
const formatter = new Intl.DateTimeFormat(undefined, {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: true,
|
||||
})
|
||||
|
||||
const timeZone = formatter.resolvedOptions().timeZone
|
||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
const timeZoneOffset = -now.getTimezoneOffset() / 60 // Convert to hours and invert sign to match conventional notation
|
||||
const timeZoneOffsetHours = Math.floor(Math.abs(timeZoneOffset))
|
||||
const timeZoneOffsetMinutes = Math.abs(Math.round((Math.abs(timeZoneOffset) - timeZoneOffsetHours) * 60))
|
||||
const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : "-"}${timeZoneOffsetHours}:${timeZoneOffsetMinutes.toString().padStart(2, "0")}`
|
||||
details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})`
|
||||
details += `\n\n# Current Time\nCurrent time in ISO 8601 UTC format: ${now.toISOString()}\nUser time zone: ${timeZone}, UTC${timeZoneOffsetStr}`
|
||||
|
||||
// Add context tokens information.
|
||||
const { contextTokens, totalCost } = getApiMetrics(cline.clineMessages)
|
||||
|
|
|
|||
|
|
@ -1163,15 +1163,10 @@ describe("ClineProvider", () => {
|
|||
|
||||
describe("deleteMessage", () => {
|
||||
beforeEach(async () => {
|
||||
// Mock window.showInformationMessage
|
||||
;(vscode.window.showInformationMessage as any) = vi.fn()
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
})
|
||||
|
||||
test('handles "Just this message" deletion correctly', async () => {
|
||||
// Mock user selecting "Just this message"
|
||||
;(vscode.window.showInformationMessage as any).mockResolvedValue("confirmation.delete_just_this_message")
|
||||
|
||||
test("handles deletion with confirmation dialog", async () => {
|
||||
// Setup mock messages
|
||||
const mockMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback" }, // User message 1
|
||||
|
|
@ -1202,103 +1197,58 @@ describe("ClineProvider", () => {
|
|||
historyItem: { id: "test-task-id" },
|
||||
})
|
||||
|
||||
// Mock initClineWithHistoryItem
|
||||
;(provider as any).initClineWithHistoryItem = vi.fn()
|
||||
|
||||
// Trigger message deletion
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
|
||||
await messageHandler({ type: "deleteMessage", value: 4000 })
|
||||
|
||||
// Verify correct messages were kept
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
|
||||
mockMessages[0],
|
||||
mockMessages[1],
|
||||
mockMessages[4],
|
||||
mockMessages[5],
|
||||
])
|
||||
// Verify that the dialog message was sent to webview
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showDeleteMessageDialog",
|
||||
messageTs: 4000,
|
||||
})
|
||||
|
||||
// Verify correct API messages were kept
|
||||
// Simulate user confirming deletion through the dialog
|
||||
await messageHandler({ type: "deleteMessageConfirm", messageTs: 4000 })
|
||||
|
||||
// Verify only messages before the deleted message were kept
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
|
||||
|
||||
// Verify only API messages before the deleted message were kept
|
||||
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
|
||||
mockApiHistory[0],
|
||||
mockApiHistory[1],
|
||||
mockApiHistory[4],
|
||||
mockApiHistory[5],
|
||||
])
|
||||
|
||||
// Verify initClineWithHistoryItem was called
|
||||
expect((provider as any).initClineWithHistoryItem).toHaveBeenCalledWith({ id: "test-task-id" })
|
||||
})
|
||||
|
||||
test('handles "This and all subsequent messages" deletion correctly', async () => {
|
||||
// Mock user selecting "This and all subsequent messages"
|
||||
;(vscode.window.showInformationMessage as any).mockResolvedValue("confirmation.delete_this_and_subsequent")
|
||||
|
||||
// Setup mock messages
|
||||
const mockMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback" },
|
||||
{ ts: 2000, type: "say", say: "text", value: 3000 }, // Message to delete
|
||||
{ ts: 3000, type: "say", say: "user_feedback" },
|
||||
{ ts: 4000, type: "say", say: "user_feedback" },
|
||||
] as ClineMessage[]
|
||||
|
||||
const mockApiHistory = [
|
||||
{ ts: 1000 },
|
||||
{ ts: 2000 },
|
||||
{ ts: 3000 },
|
||||
{ ts: 4000 },
|
||||
] as (Anthropic.MessageParam & {
|
||||
ts?: number
|
||||
})[]
|
||||
|
||||
// Setup Cline instance with auto-mock from the top of the file
|
||||
const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
|
||||
mockCline.clineMessages = mockMessages
|
||||
mockCline.apiConversationHistory = mockApiHistory
|
||||
await provider.addClineToStack(mockCline)
|
||||
|
||||
// Mock getTaskWithId
|
||||
;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
|
||||
historyItem: { id: "test-task-id" },
|
||||
})
|
||||
|
||||
// Trigger message deletion
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
|
||||
await messageHandler({ type: "deleteMessage", value: 3000 })
|
||||
|
||||
// Verify only messages before the deleted message were kept
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
|
||||
|
||||
// Verify only API messages before the deleted message were kept
|
||||
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([mockApiHistory[0]])
|
||||
})
|
||||
|
||||
test("handles Cancel correctly", async () => {
|
||||
// Mock user selecting "Cancel"
|
||||
;(vscode.window.showInformationMessage as any).mockResolvedValue("Cancel")
|
||||
|
||||
// Setup Cline instance with auto-mock from the top of the file
|
||||
const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
|
||||
mockCline.clineMessages = [{ ts: 1000 }, { ts: 2000 }] as ClineMessage[]
|
||||
mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as (Anthropic.MessageParam & {
|
||||
ts?: number
|
||||
})[]
|
||||
await provider.addClineToStack(mockCline)
|
||||
test("handles case when no current task exists", async () => {
|
||||
// Clear the cline stack
|
||||
;(provider as any).clineStack = []
|
||||
|
||||
// Trigger message deletion
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
|
||||
await messageHandler({ type: "deleteMessage", value: 2000 })
|
||||
|
||||
// Verify no messages were deleted
|
||||
expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
|
||||
expect(mockCline.overwriteApiConversationHistory).not.toHaveBeenCalled()
|
||||
// Verify no dialog was shown since there's no current cline
|
||||
expect(mockPostMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "showDeleteMessageDialog",
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("editMessage", () => {
|
||||
beforeEach(async () => {
|
||||
// Mock window.showWarningMessage
|
||||
;(vscode.window.showWarningMessage as any) = vi.fn()
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
})
|
||||
|
||||
test('handles "Proceed" edit correctly', async () => {
|
||||
// Mock user selecting "Proceed" - need to use the localized string key
|
||||
;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
|
||||
|
||||
test("handles edit with confirmation dialog", async () => {
|
||||
// Setup mock messages
|
||||
const mockMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback" }, // User message 1
|
||||
|
|
@ -1346,6 +1296,20 @@ describe("ClineProvider", () => {
|
|||
editedMessageContent: "Edited message content",
|
||||
})
|
||||
|
||||
// Verify that the dialog message was sent to webview
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showEditMessageDialog",
|
||||
messageTs: 4000,
|
||||
text: "Edited message content",
|
||||
})
|
||||
|
||||
// Simulate user confirming edit through the dialog
|
||||
await messageHandler({
|
||||
type: "editMessageConfirm",
|
||||
messageTs: 4000,
|
||||
text: "Edited message content",
|
||||
})
|
||||
|
||||
// Verify correct messages were kept (only messages before the edited one)
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
|
||||
|
||||
|
|
@ -1355,12 +1319,9 @@ describe("ClineProvider", () => {
|
|||
mockApiHistory[1],
|
||||
])
|
||||
|
||||
// Verify handleWebviewAskResponse was called with the edited content
|
||||
expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith(
|
||||
"messageResponse",
|
||||
"Edited message content",
|
||||
undefined,
|
||||
)
|
||||
// The new flow calls webviewMessageHandler recursively with askResponse
|
||||
// We need to verify the recursive call happened by checking if the handler was called again
|
||||
expect((mockWebviewView.webview.onDidReceiveMessage as any).mock.calls.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -2705,13 +2666,10 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
|
||||
describe("Edit Messages with Images and Attachments", () => {
|
||||
beforeEach(async () => {
|
||||
;(vscode.window.showInformationMessage as any) = vi.fn()
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
})
|
||||
|
||||
test("handles editing messages containing images", async () => {
|
||||
;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
|
||||
|
||||
const mockMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback", text: "Original message" },
|
||||
{
|
||||
|
|
@ -2746,17 +2704,26 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
editedMessageContent: "Edited message with preserved images",
|
||||
})
|
||||
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
|
||||
expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith(
|
||||
"messageResponse",
|
||||
"Edited message with preserved images",
|
||||
undefined,
|
||||
)
|
||||
// Verify dialog was shown
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showEditMessageDialog",
|
||||
messageTs: 3000,
|
||||
text: "Edited message with preserved images",
|
||||
})
|
||||
|
||||
// Simulate confirmation
|
||||
await messageHandler({
|
||||
type: "editMessageConfirm",
|
||||
messageTs: 3000,
|
||||
text: "Edited message with preserved images",
|
||||
})
|
||||
|
||||
// Verify messages were edited correctly - only the first message should remain
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
|
||||
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }])
|
||||
})
|
||||
|
||||
test("handles editing messages with file attachments", async () => {
|
||||
;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
|
||||
|
||||
const mockMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback", text: "Original message" },
|
||||
{
|
||||
|
|
@ -2789,6 +2756,20 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
editedMessageContent: "Edited message with file attachment",
|
||||
})
|
||||
|
||||
// Verify dialog was shown
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showEditMessageDialog",
|
||||
messageTs: 3000,
|
||||
text: "Edited message with file attachment",
|
||||
})
|
||||
|
||||
// Simulate user confirming the edit
|
||||
await messageHandler({
|
||||
type: "editMessageConfirm",
|
||||
messageTs: 3000,
|
||||
text: "Edited message with file attachment",
|
||||
})
|
||||
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
|
||||
expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith(
|
||||
"messageResponse",
|
||||
|
|
@ -2805,8 +2786,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
})
|
||||
|
||||
test("handles network timeout during edit submission", async () => {
|
||||
;(vscode.window.showInformationMessage as any).mockResolvedValue("confirmation.proceed")
|
||||
|
||||
const mockCline = new Task(defaultTaskOptions)
|
||||
mockCline.clineMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
|
||||
|
|
@ -2833,12 +2812,20 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
}),
|
||||
).resolves.toBeUndefined()
|
||||
|
||||
// Verify dialog was shown
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showEditMessageDialog",
|
||||
messageTs: 2000,
|
||||
text: "Edited message",
|
||||
})
|
||||
|
||||
// Simulate user confirming the edit
|
||||
await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" })
|
||||
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("handles connection drops during edit operation", async () => {
|
||||
;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
|
||||
|
||||
const mockCline = new Task(defaultTaskOptions)
|
||||
mockCline.clineMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
|
||||
|
|
@ -2865,6 +2852,17 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
}),
|
||||
).resolves.toBeUndefined()
|
||||
|
||||
// Verify dialog was shown
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showEditMessageDialog",
|
||||
messageTs: 2000,
|
||||
text: "Edited message",
|
||||
})
|
||||
|
||||
// Simulate user confirming the edit
|
||||
await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" })
|
||||
|
||||
// The error should be caught and shown
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Connection lost")
|
||||
})
|
||||
})
|
||||
|
|
@ -2876,8 +2874,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
})
|
||||
|
||||
test("handles race conditions with simultaneous edits", async () => {
|
||||
;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
|
||||
|
||||
const mockCline = new Task(defaultTaskOptions)
|
||||
mockCline.clineMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback", text: "Message 1", value: 2000 },
|
||||
|
|
@ -2912,6 +2908,22 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
|
||||
await Promise.all([edit1Promise, edit2Promise])
|
||||
|
||||
// Verify dialogs were shown for both edits
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showEditMessageDialog",
|
||||
messageTs: 2000,
|
||||
text: "Edited message 1",
|
||||
})
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showEditMessageDialog",
|
||||
messageTs: 4000,
|
||||
text: "Edited message 2",
|
||||
})
|
||||
|
||||
// Simulate user confirming both edits
|
||||
await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message 1" })
|
||||
await messageHandler({ type: "editMessageConfirm", messageTs: 4000, text: "Edited message 2" })
|
||||
|
||||
// Both operations should complete without throwing
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
|
||||
})
|
||||
|
|
@ -2940,8 +2952,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
})
|
||||
|
||||
test("handles authorization failures during edit", async () => {
|
||||
;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
|
||||
|
||||
const mockCline = new Task(defaultTaskOptions)
|
||||
mockCline.clineMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
|
||||
|
|
@ -2965,6 +2975,13 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
editedMessageContent: "Edited message",
|
||||
})
|
||||
|
||||
// Simulate confirmation
|
||||
await messageHandler({
|
||||
type: "editMessageConfirm",
|
||||
messageTs: 2000,
|
||||
text: "Edited message",
|
||||
})
|
||||
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Unauthorized")
|
||||
})
|
||||
|
||||
|
|
@ -3058,8 +3075,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
})
|
||||
|
||||
test("handles edit operations on deleted messages", async () => {
|
||||
;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
|
||||
|
||||
const mockCline = new Task(defaultTaskOptions)
|
||||
mockCline.clineMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback", text: "Existing message" },
|
||||
|
|
@ -3083,17 +3098,26 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
editedMessageContent: "Edited non-existent message",
|
||||
})
|
||||
|
||||
// Should show confirmation dialog but not perform any operations
|
||||
expect(vscode.window.showWarningMessage).toHaveBeenCalled()
|
||||
// Should show edit dialog
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showEditMessageDialog",
|
||||
messageTs: 5000,
|
||||
text: "Edited non-existent message",
|
||||
})
|
||||
|
||||
// Simulate user confirming the edit
|
||||
await messageHandler({
|
||||
type: "editMessageConfirm",
|
||||
messageTs: 5000,
|
||||
text: "Edited non-existent message",
|
||||
})
|
||||
|
||||
// Should not perform any operations since message doesn't exist
|
||||
expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
|
||||
expect(mockCline.handleWebviewAskResponse).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("handles delete operations on non-existent messages", async () => {
|
||||
;(vscode.window.showInformationMessage as any).mockResolvedValue(
|
||||
"confirmation.delete_just_this_message",
|
||||
)
|
||||
|
||||
const mockCline = new Task(defaultTaskOptions)
|
||||
mockCline.clineMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback", text: "Existing message" },
|
||||
|
|
@ -3115,8 +3139,16 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
value: 5000,
|
||||
})
|
||||
|
||||
// Should show confirmation dialog but not perform any operations
|
||||
expect(vscode.window.showInformationMessage).toHaveBeenCalled()
|
||||
// Should show delete dialog
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showDeleteMessageDialog",
|
||||
messageTs: 5000,
|
||||
})
|
||||
|
||||
// Simulate user confirming the delete
|
||||
await messageHandler({ type: "deleteMessageConfirm", messageTs: 5000 })
|
||||
|
||||
// Should not perform any operations since message doesn't exist
|
||||
expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
@ -3128,8 +3160,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
})
|
||||
|
||||
test("validates proper cleanup during failed edit operations", async () => {
|
||||
;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
|
||||
|
||||
const mockCline = new Task(defaultTaskOptions)
|
||||
mockCline.clineMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
|
||||
|
|
@ -3159,16 +3189,22 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
editedMessageContent: "Edited message",
|
||||
})
|
||||
|
||||
// Should show edit dialog
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showEditMessageDialog",
|
||||
messageTs: 2000,
|
||||
text: "Edited message",
|
||||
})
|
||||
|
||||
// Simulate user confirming the edit
|
||||
await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" })
|
||||
|
||||
// Verify cleanup was attempted before failure
|
||||
expect(cleanupSpy).toHaveBeenCalled()
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Operation failed")
|
||||
})
|
||||
|
||||
test("validates proper cleanup during failed delete operations", async () => {
|
||||
;(vscode.window.showInformationMessage as any).mockResolvedValue(
|
||||
"confirmation.delete_just_this_message",
|
||||
)
|
||||
|
||||
const mockCline = new Task(defaultTaskOptions)
|
||||
mockCline.clineMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback", text: "Message to delete" },
|
||||
|
|
@ -3193,6 +3229,15 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
|
||||
await messageHandler({ type: "deleteMessage", value: 2000 })
|
||||
|
||||
// Should show delete dialog
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showDeleteMessageDialog",
|
||||
messageTs: 2000,
|
||||
})
|
||||
|
||||
// Simulate user confirming the delete
|
||||
await messageHandler({ type: "deleteMessageConfirm", messageTs: 2000 })
|
||||
|
||||
// Verify cleanup was attempted before failure
|
||||
expect(cleanupSpy).toHaveBeenCalled()
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
|
||||
|
|
@ -3208,8 +3253,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
})
|
||||
|
||||
test("handles editing messages with large text content", async () => {
|
||||
;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
|
||||
|
||||
// Create a large message (10KB of text)
|
||||
const largeText = "A".repeat(10000)
|
||||
const mockMessages = [
|
||||
|
|
@ -3238,6 +3281,16 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
editedMessageContent: largeEditedContent,
|
||||
})
|
||||
|
||||
// Should show edit dialog
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showEditMessageDialog",
|
||||
messageTs: 2000,
|
||||
text: largeEditedContent,
|
||||
})
|
||||
|
||||
// Simulate user confirming the edit
|
||||
await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: largeEditedContent })
|
||||
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
|
||||
expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith(
|
||||
"messageResponse",
|
||||
|
|
@ -3247,10 +3300,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
})
|
||||
|
||||
test("handles deleting messages with large payloads", async () => {
|
||||
;(vscode.window.showInformationMessage as any).mockResolvedValue(
|
||||
"confirmation.delete_this_and_subsequent",
|
||||
)
|
||||
|
||||
// Create messages with large payloads
|
||||
const largeText = "X".repeat(50000)
|
||||
const mockMessages = [
|
||||
|
|
@ -3275,6 +3324,15 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
|
||||
await messageHandler({ type: "deleteMessage", value: 3000 })
|
||||
|
||||
// Should show delete dialog
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showDeleteMessageDialog",
|
||||
messageTs: 3000,
|
||||
})
|
||||
|
||||
// Simulate user confirming the delete
|
||||
await messageHandler({ type: "deleteMessageConfirm", messageTs: 3000 })
|
||||
|
||||
// Should handle large payloads without issues
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
|
||||
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }])
|
||||
|
|
@ -3285,10 +3343,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
// Note: Error messaging test removed as the implementation may not have proper error handling in place
|
||||
|
||||
test("provides user feedback for successful operations", async () => {
|
||||
;(vscode.window.showInformationMessage as any).mockResolvedValue(
|
||||
"confirmation.delete_just_this_message",
|
||||
)
|
||||
|
||||
const mockCline = new Task(defaultTaskOptions)
|
||||
mockCline.clineMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback", text: "Message to delete" },
|
||||
|
|
@ -3308,6 +3362,15 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
|
||||
await messageHandler({ type: "deleteMessage", value: 2000 })
|
||||
|
||||
// Should show delete dialog
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showDeleteMessageDialog",
|
||||
messageTs: 2000,
|
||||
})
|
||||
|
||||
// Simulate user confirming the delete
|
||||
await messageHandler({ type: "deleteMessageConfirm", messageTs: 2000 })
|
||||
|
||||
// Verify successful operation completed
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
|
||||
expect(provider.initClineWithHistoryItem).toHaveBeenCalled()
|
||||
|
|
@ -3315,8 +3378,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
})
|
||||
|
||||
test("handles user cancellation gracefully", async () => {
|
||||
// Mock user canceling the operation
|
||||
;(vscode.window.showWarningMessage as any).mockResolvedValue(undefined)
|
||||
// Test cancellation by not sending confirmation
|
||||
|
||||
const mockCline = new Task(defaultTaskOptions)
|
||||
mockCline.clineMessages = [
|
||||
|
|
@ -3353,10 +3415,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
})
|
||||
|
||||
test("handles messages with identical timestamps", async () => {
|
||||
;(vscode.window.showInformationMessage as any).mockResolvedValue(
|
||||
"confirmation.delete_just_this_message",
|
||||
)
|
||||
|
||||
const mockCline = new Task(defaultTaskOptions)
|
||||
mockCline.clineMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback", text: "Message 1" },
|
||||
|
|
@ -3377,13 +3435,20 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
|
||||
await messageHandler({ type: "deleteMessage", value: 1000 })
|
||||
|
||||
// Should show delete dialog
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showDeleteMessageDialog",
|
||||
messageTs: 1000,
|
||||
})
|
||||
|
||||
// Simulate user confirming the delete
|
||||
await messageHandler({ type: "deleteMessageConfirm", messageTs: 1000 })
|
||||
|
||||
// Should handle identical timestamps gracefully
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("handles messages with future timestamps", async () => {
|
||||
;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
|
||||
|
||||
const futureTimestamp = Date.now() + 100000 // Future timestamp
|
||||
const mockCline = new Task(defaultTaskOptions)
|
||||
mockCline.clineMessages = [
|
||||
|
|
@ -3419,6 +3484,20 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
|
|||
editedMessageContent: "Edited future message",
|
||||
})
|
||||
|
||||
// Should show edit dialog
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "showEditMessageDialog",
|
||||
messageTs: futureTimestamp + 1000,
|
||||
text: "Edited future message",
|
||||
})
|
||||
|
||||
// Simulate user confirming the edit
|
||||
await messageHandler({
|
||||
type: "editMessageConfirm",
|
||||
messageTs: futureTimestamp + 1000,
|
||||
text: "Edited future message",
|
||||
})
|
||||
|
||||
// Should handle future timestamps correctly
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
|
||||
expect(mockCline.handleWebviewAskResponse).toHaveBeenCalled()
|
||||
|
|
|
|||
|
|
@ -28,9 +28,13 @@ const mockClineProvider = {
|
|||
globalStorageUri: { fsPath: "/mock/global/storage" },
|
||||
},
|
||||
setValue: vi.fn(),
|
||||
getValue: vi.fn(),
|
||||
},
|
||||
log: vi.fn(),
|
||||
postStateToWebview: vi.fn(),
|
||||
getCurrentCline: vi.fn(),
|
||||
getTaskWithId: vi.fn(),
|
||||
initClineWithHistoryItem: vi.fn(),
|
||||
} as unknown as ClineProvider
|
||||
|
||||
import { t } from "../../../i18n"
|
||||
|
|
@ -482,3 +486,51 @@ describe("webviewMessageHandler - deleteCustomMode", () => {
|
|||
expect(mockClineProvider.postMessageToWebview).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("webviewMessageHandler - message dialog preferences", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Mock a current Cline instance
|
||||
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({
|
||||
taskId: "test-task-id",
|
||||
apiConversationHistory: [],
|
||||
clineMessages: [],
|
||||
} as any)
|
||||
// Reset getValue mock
|
||||
vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(false)
|
||||
})
|
||||
|
||||
describe("deleteMessage", () => {
|
||||
it("should always show dialog for delete confirmation", async () => {
|
||||
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists
|
||||
|
||||
await webviewMessageHandler(mockClineProvider, {
|
||||
type: "deleteMessage",
|
||||
value: 123456789, // Changed from messageTs to value
|
||||
})
|
||||
|
||||
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "showDeleteMessageDialog",
|
||||
messageTs: 123456789,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("submitEditedMessage", () => {
|
||||
it("should always show dialog for edit confirmation", async () => {
|
||||
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists
|
||||
|
||||
await webviewMessageHandler(mockClineProvider, {
|
||||
type: "submitEditedMessage",
|
||||
value: 123456789, // messageTs as number
|
||||
editedMessageContent: "edited content", // text content in editedMessageContent field
|
||||
})
|
||||
|
||||
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "showEditMessageDialog",
|
||||
messageTs: 123456789,
|
||||
text: "edited content",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -77,55 +77,6 @@ export const webviewMessageHandler = async (
|
|||
return { messageIndex, apiConversationHistoryIndex }
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes just the target message, preserving messages after the next user message
|
||||
*/
|
||||
const removeMessagesJustThis = async (
|
||||
currentCline: any,
|
||||
messageIndex: number,
|
||||
apiConversationHistoryIndex: number,
|
||||
) => {
|
||||
// Find the next user message first
|
||||
const nextUserMessage = currentCline.clineMessages
|
||||
.slice(messageIndex + 1)
|
||||
.find((msg: ClineMessage) => msg.type === "say" && msg.say === "user_feedback")
|
||||
|
||||
// Handle UI messages
|
||||
if (nextUserMessage) {
|
||||
// Find absolute index of next user message
|
||||
const nextUserMessageIndex = currentCline.clineMessages.findIndex(
|
||||
(msg: ClineMessage) => msg === nextUserMessage,
|
||||
)
|
||||
|
||||
// Keep messages before current message and after next user message
|
||||
await currentCline.overwriteClineMessages([
|
||||
...currentCline.clineMessages.slice(0, messageIndex),
|
||||
...currentCline.clineMessages.slice(nextUserMessageIndex),
|
||||
])
|
||||
} else {
|
||||
// If no next user message, keep only messages before current message
|
||||
await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex))
|
||||
}
|
||||
|
||||
// Handle API messages
|
||||
if (apiConversationHistoryIndex !== -1) {
|
||||
if (nextUserMessage && nextUserMessage.ts) {
|
||||
// Keep messages before current API message and after next user message
|
||||
await currentCline.overwriteApiConversationHistory([
|
||||
...currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
|
||||
...currentCline.apiConversationHistory.filter(
|
||||
(msg: ApiMessage) => msg.ts && msg.ts >= nextUserMessage.ts,
|
||||
),
|
||||
])
|
||||
} else {
|
||||
// If no next user message, keep only messages before current API message
|
||||
await currentCline.overwriteApiConversationHistory(
|
||||
currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the target message and all subsequent messages
|
||||
*/
|
||||
|
|
@ -148,19 +99,19 @@ export const webviewMessageHandler = async (
|
|||
* Handles message deletion operations with user confirmation
|
||||
*/
|
||||
const handleDeleteOperation = async (messageTs: number): Promise<void> => {
|
||||
const options = [
|
||||
t("common:confirmation.delete_just_this_message"),
|
||||
t("common:confirmation.delete_this_and_subsequent"),
|
||||
]
|
||||
// Send message to webview to show delete confirmation dialog
|
||||
await provider.postMessageToWebview({
|
||||
type: "showDeleteMessageDialog",
|
||||
messageTs,
|
||||
})
|
||||
}
|
||||
|
||||
const answer = await vscode.window.showInformationMessage(
|
||||
t("common:confirmation.delete_message"),
|
||||
{ modal: true },
|
||||
...options,
|
||||
)
|
||||
|
||||
// Only proceed if user selected one of the options and we have a current cline
|
||||
if (answer && options.includes(answer) && provider.getCurrentCline()) {
|
||||
/**
|
||||
* Handles confirmed message deletion from webview dialog
|
||||
*/
|
||||
const handleDeleteMessageConfirm = async (messageTs: number): Promise<void> => {
|
||||
// Only proceed if we have a current cline
|
||||
if (provider.getCurrentCline()) {
|
||||
const currentCline = provider.getCurrentCline()!
|
||||
const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
|
||||
|
||||
|
|
@ -168,14 +119,8 @@ export const webviewMessageHandler = async (
|
|||
try {
|
||||
const { historyItem } = await provider.getTaskWithId(currentCline.taskId)
|
||||
|
||||
// Check which option the user selected
|
||||
if (answer === options[0]) {
|
||||
// Delete just this message
|
||||
await removeMessagesJustThis(currentCline, messageIndex, apiConversationHistoryIndex)
|
||||
} else if (answer === options[1]) {
|
||||
// Delete this message and all subsequent
|
||||
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
|
||||
}
|
||||
// Delete this message and all subsequent messages
|
||||
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
|
||||
|
||||
// Initialize with history item after deletion
|
||||
await provider.initClineWithHistoryItem(historyItem)
|
||||
|
|
@ -192,15 +137,26 @@ export const webviewMessageHandler = async (
|
|||
/**
|
||||
* Handles message editing operations with user confirmation
|
||||
*/
|
||||
const handleEditOperation = async (messageTs: number, editedContent: string): Promise<void> => {
|
||||
const answer = await vscode.window.showWarningMessage(
|
||||
t("common:confirmation.edit_warning"),
|
||||
{ modal: true },
|
||||
t("common:confirmation.proceed"),
|
||||
)
|
||||
const handleEditOperation = async (messageTs: number, editedContent: string, images?: string[]): Promise<void> => {
|
||||
// Send message to webview to show edit confirmation dialog
|
||||
await provider.postMessageToWebview({
|
||||
type: "showEditMessageDialog",
|
||||
messageTs,
|
||||
text: editedContent,
|
||||
images,
|
||||
})
|
||||
}
|
||||
|
||||
// Only proceed if user selected "Proceed" and we have a current cline
|
||||
if (answer === t("common:confirmation.proceed") && provider.getCurrentCline()) {
|
||||
/**
|
||||
* Handles confirmed message editing from webview dialog
|
||||
*/
|
||||
const handleEditMessageConfirm = async (
|
||||
messageTs: number,
|
||||
editedContent: string,
|
||||
images?: string[],
|
||||
): Promise<void> => {
|
||||
// Only proceed if we have a current cline
|
||||
if (provider.getCurrentCline()) {
|
||||
const currentCline = provider.getCurrentCline()!
|
||||
|
||||
// Use findMessageIndices to find messages based on timestamp
|
||||
|
|
@ -217,6 +173,7 @@ export const webviewMessageHandler = async (
|
|||
type: "askResponse",
|
||||
askResponse: "messageResponse",
|
||||
text: editedContent,
|
||||
images,
|
||||
})
|
||||
|
||||
// Don't initialize with history item for edit operations
|
||||
|
|
@ -242,11 +199,12 @@ export const webviewMessageHandler = async (
|
|||
messageTs: number,
|
||||
operation: "delete" | "edit",
|
||||
editedContent?: string,
|
||||
images?: string[],
|
||||
): Promise<void> => {
|
||||
if (operation === "delete") {
|
||||
await handleDeleteOperation(messageTs)
|
||||
} else if (operation === "edit" && editedContent) {
|
||||
await handleEditOperation(messageTs, editedContent)
|
||||
await handleEditOperation(messageTs, editedContent, images)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -416,7 +374,12 @@ export const webviewMessageHandler = async (
|
|||
break
|
||||
case "selectImages":
|
||||
const images = await selectImages()
|
||||
await provider.postMessageToWebview({ type: "selectedImages", images })
|
||||
await provider.postMessageToWebview({
|
||||
type: "selectedImages",
|
||||
images,
|
||||
context: message.context,
|
||||
messageTs: message.messageTs,
|
||||
})
|
||||
break
|
||||
case "exportCurrentTask":
|
||||
const currentTaskId = provider.getCurrentCline()?.taskId
|
||||
|
|
@ -1209,7 +1172,12 @@ export const webviewMessageHandler = async (
|
|||
message.value &&
|
||||
message.editedMessageContent
|
||||
) {
|
||||
await handleMessageModificationsOperation(message.value, "edit", message.editedMessageContent)
|
||||
await handleMessageModificationsOperation(
|
||||
message.value,
|
||||
"edit",
|
||||
message.editedMessageContent,
|
||||
message.images,
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
@ -1542,6 +1510,16 @@ export const webviewMessageHandler = async (
|
|||
}
|
||||
}
|
||||
break
|
||||
case "deleteMessageConfirm":
|
||||
if (message.messageTs) {
|
||||
await handleDeleteMessageConfirm(message.messageTs)
|
||||
}
|
||||
break
|
||||
case "editMessageConfirm":
|
||||
if (message.messageTs && message.text) {
|
||||
await handleEditMessageConfirm(message.messageTs, message.text, message.images)
|
||||
}
|
||||
break
|
||||
case "getListApiConfiguration":
|
||||
try {
|
||||
const listApiConfig = await provider.providerSettingsManager.listConfig()
|
||||
|
|
|
|||
|
|
@ -21,12 +21,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "Estàs segur que vols restablir tots els estats i emmagatzematge secret a l'extensió? Això no es pot desfer.",
|
||||
"delete_config_profile": "Estàs segur que vols eliminar aquest perfil de configuració?",
|
||||
"delete_custom_mode_with_rules": "Esteu segur que voleu suprimir aquest mode {scope}?\n\nAixò també suprimirà la carpeta de regles associada a:\n{rulesFolderPath}",
|
||||
"delete_message": "Què vols eliminar?",
|
||||
"edit_warning": "Editar aquest missatge eliminarà tots els missatges posteriors de la conversa. Vols continuar?",
|
||||
"delete_just_this_message": "Només aquest missatge",
|
||||
"delete_this_and_subsequent": "Aquest i tots els missatges posteriors",
|
||||
"proceed": "Continuar"
|
||||
"delete_custom_mode_with_rules": "Esteu segur que voleu suprimir aquest mode {scope}?\n\nAixò també suprimirà la carpeta de regles associada a:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "Format d'URI de dades no vàlid",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "Möchtest du wirklich alle Zustände und geheimen Speicher in der Erweiterung zurücksetzen? Dies kann nicht rückgängig gemacht werden.",
|
||||
"delete_config_profile": "Möchtest du dieses Konfigurationsprofil wirklich löschen?",
|
||||
"delete_custom_mode_with_rules": "Bist du sicher, dass du diesen {scope}-Modus löschen möchtest?\n\nDadurch wird auch der zugehörige Regelordner unter folgender Adresse gelöscht:\n{rulesFolderPath}",
|
||||
"delete_message": "Was möchtest du löschen?",
|
||||
"edit_warning": "Das Bearbeiten dieser Nachricht wird alle nachfolgenden Nachrichten in der Unterhaltung löschen. Möchtest du fortfahren?",
|
||||
"delete_just_this_message": "Nur diese Nachricht",
|
||||
"delete_this_and_subsequent": "Diese und alle nachfolgenden Nachrichten",
|
||||
"proceed": "Fortfahren"
|
||||
"delete_custom_mode_with_rules": "Bist du sicher, dass du diesen {scope}-Modus löschen möchtest?\n\nDadurch wird auch der zugehörige Regelordner unter folgender Adresse gelöscht:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "Ungültiges Daten-URI-Format",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "Are you sure you want to reset all state and secret storage in the extension? This cannot be undone.",
|
||||
"delete_config_profile": "Are you sure you want to delete this configuration profile?",
|
||||
"delete_custom_mode_with_rules": "Are you sure you want to delete this {scope} mode?\n\nThis will also delete the associated rules folder at:\n{rulesFolderPath}",
|
||||
"delete_message": "What would you like to delete?",
|
||||
"edit_warning": "Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?",
|
||||
"delete_just_this_message": "Just this message",
|
||||
"delete_this_and_subsequent": "This and all subsequent messages",
|
||||
"proceed": "Proceed"
|
||||
"delete_custom_mode_with_rules": "Are you sure you want to delete this {scope} mode?\n\nThis will also delete the associated rules folder at:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "Invalid data URI format",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "¿Estás seguro de que deseas restablecer todo el estado y el almacenamiento secreto en la extensión? Esta acción no se puede deshacer.",
|
||||
"delete_config_profile": "¿Estás seguro de que deseas eliminar este perfil de configuración?",
|
||||
"delete_custom_mode_with_rules": "¿Estás seguro de que quieres eliminar este modo {scope}?\n\nEsto también eliminará la carpeta de reglas asociada en:\n{rulesFolderPath}",
|
||||
"delete_message": "¿Qué deseas eliminar?",
|
||||
"edit_warning": "Editar este mensaje eliminará todos los mensajes posteriores en la conversación. ¿Deseas continuar?",
|
||||
"delete_just_this_message": "Solo este mensaje",
|
||||
"delete_this_and_subsequent": "Este y todos los mensajes posteriores",
|
||||
"proceed": "Continuar"
|
||||
"delete_custom_mode_with_rules": "¿Estás seguro de que quieres eliminar este modo {scope}?\n\nEsto también eliminará la carpeta de reglas asociada en:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "Formato de URI de datos no válido",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "Êtes-vous sûr de vouloir réinitialiser le global state et le stockage de secrets de l'extension ? Cette action est irréversible.",
|
||||
"delete_config_profile": "Êtes-vous sûr de vouloir supprimer ce profil de configuration ?",
|
||||
"delete_custom_mode_with_rules": "Êtes-vous sûr de vouloir supprimer ce mode {scope} ?\n\nCela supprimera également le dossier de règles associé à l'adresse :\n{rulesFolderPath}",
|
||||
"delete_message": "Que souhaitez-vous supprimer ?",
|
||||
"edit_warning": "Modifier ce message supprimera tous les messages suivants dans la conversation. Voulez-vous continuer ?",
|
||||
"delete_just_this_message": "Uniquement ce message",
|
||||
"delete_this_and_subsequent": "Ce message et tous les messages suivants",
|
||||
"proceed": "Continuer"
|
||||
"delete_custom_mode_with_rules": "Êtes-vous sûr de vouloir supprimer ce mode {scope} ?\n\nCela supprimera également le dossier de règles associé à l'adresse :\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "Format d'URI de données invalide",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "क्या आप वाकई एक्सटेंशन में सभी स्टेट और गुप्त स्टोरेज रीसेट करना चाहते हैं? इसे पूर्ववत नहीं किया जा सकता है।",
|
||||
"delete_config_profile": "क्या आप वाकई इस कॉन्फ़िगरेशन प्रोफ़ाइल को हटाना चाहते हैं?",
|
||||
"delete_custom_mode_with_rules": "क्या आप वाकई इस {scope} मोड को हटाना चाहते हैं?\n\nयह संबंधित नियम फ़ोल्डर को भी यहाँ हटा देगा:\n{rulesFolderPath}",
|
||||
"delete_message": "आप क्या हटाना चाहते हैं?",
|
||||
"edit_warning": "इस संदेश को संपादित करने से बातचीत के सभी बाद के संदेश हट जाएंगे। क्या आप जारी रखना चाहते हैं?",
|
||||
"delete_just_this_message": "सिर्फ यह संदेश",
|
||||
"delete_this_and_subsequent": "यह और सभी बाद के संदेश",
|
||||
"proceed": "जारी रखें"
|
||||
"delete_custom_mode_with_rules": "क्या आप वाकई इस {scope} मोड को हटाना चाहते हैं?\n\nयह संबंधित नियम फ़ोल्डर को भी यहाँ हटा देगा:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "अमान्य डेटा URI फॉर्मेट",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "Apakah kamu yakin ingin mereset semua state dan secret storage di ekstensi? Ini tidak dapat dibatalkan.",
|
||||
"delete_config_profile": "Apakah kamu yakin ingin menghapus profil konfigurasi ini?",
|
||||
"delete_custom_mode_with_rules": "Anda yakin ingin menghapus mode {scope} ini?\n\nIni juga akan menghapus folder aturan terkait di:\n{rulesFolderPath}",
|
||||
"delete_message": "Apa yang ingin kamu hapus?",
|
||||
"edit_warning": "Mengedit pesan ini akan menghapus semua pesan selanjutnya dalam percakapan. Apakah kamu ingin melanjutkan?",
|
||||
"delete_just_this_message": "Hanya pesan ini",
|
||||
"delete_this_and_subsequent": "Ini dan semua pesan selanjutnya",
|
||||
"proceed": "Lanjutkan"
|
||||
"delete_custom_mode_with_rules": "Anda yakin ingin menghapus mode {scope} ini?\n\nIni juga akan menghapus folder aturan terkait di:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "Format data URI tidak valid",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "Sei sicuro di voler reimpostare tutti gli stati e l'archiviazione segreta nell'estensione? Questa azione non può essere annullata.",
|
||||
"delete_config_profile": "Sei sicuro di voler eliminare questo profilo di configurazione?",
|
||||
"delete_custom_mode_with_rules": "Sei sicuro di voler eliminare questa modalità {scope}?\n\nQuesto eliminerà anche la cartella delle regole associata in:\n{rulesFolderPath}",
|
||||
"delete_message": "Cosa desideri eliminare?",
|
||||
"edit_warning": "Modificare questo messaggio eliminerà tutti i messaggi successivi nella conversazione. Vuoi continuare?",
|
||||
"delete_just_this_message": "Solo questo messaggio",
|
||||
"delete_this_and_subsequent": "Questo e tutti i messaggi successivi",
|
||||
"proceed": "Continua"
|
||||
"delete_custom_mode_with_rules": "Sei sicuro di voler eliminare questa modalità {scope}?\n\nQuesto eliminerà anche la cartella delle regole associata in:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "Formato URI dati non valido",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "拡張機能のすべての状態とシークレットストレージをリセットしてもよろしいですか?この操作は元に戻せません。",
|
||||
"delete_config_profile": "この設定プロファイルを削除してもよろしいですか?",
|
||||
"delete_custom_mode_with_rules": "この{scope}モードを削除してもよろしいですか?\n\nこれにより、関連するルールフォルダも次の場所で削除されます:\n{rulesFolderPath}",
|
||||
"delete_message": "何を削除しますか?",
|
||||
"edit_warning": "このメッセージを編集すると、会話内のすべての後続メッセージが削除されます。続行しますか?",
|
||||
"delete_just_this_message": "このメッセージのみ",
|
||||
"delete_this_and_subsequent": "これ以降のすべてのメッセージ",
|
||||
"proceed": "続行"
|
||||
"delete_custom_mode_with_rules": "この{scope}モードを削除してもよろしいですか?\n\nこれにより、関連するルールフォルダも次の場所で削除されます:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "データURIフォーマットが無効です",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "확장 프로그램의 모든 상태와 보안 저장소를 재설정하시겠습니까? 이 작업은 취소할 수 없습니다.",
|
||||
"delete_config_profile": "이 구성 프로필을 삭제하시겠습니까?",
|
||||
"delete_custom_mode_with_rules": "이 {scope} 모드를 삭제하시겠습니까?\n\n이렇게 하면 연결된 규칙 폴더도 다음 위치에서 삭제됩니다:\n{rulesFolderPath}",
|
||||
"delete_message": "무엇을 삭제하시겠습니까?",
|
||||
"edit_warning": "이 메시지를 편집하면 대화의 모든 후속 메시지가 삭제됩니다. 계속하시겠습니까?",
|
||||
"delete_just_this_message": "이 메시지만",
|
||||
"delete_this_and_subsequent": "이 메시지와 모든 후속 메시지",
|
||||
"proceed": "계속"
|
||||
"delete_custom_mode_with_rules": "이 {scope} 모드를 삭제하시겠습니까?\n\n이렇게 하면 연결된 규칙 폴더도 다음 위치에서 삭제됩니다:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "잘못된 데이터 URI 형식",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "Weet je zeker dat je alle status en geheime opslag in de extensie wilt resetten? Dit kan niet ongedaan worden gemaakt.",
|
||||
"delete_config_profile": "Weet je zeker dat je dit configuratieprofiel wilt verwijderen?",
|
||||
"delete_custom_mode_with_rules": "Weet je zeker dat je deze {scope}-modus wilt verwijderen?\n\nDit verwijdert ook de bijbehorende regelsmap op:\n{rulesFolderPath}",
|
||||
"delete_message": "Wat wil je verwijderen?",
|
||||
"delete_just_this_message": "Alleen dit bericht",
|
||||
"delete_this_and_subsequent": "Dit en alle volgende berichten",
|
||||
"edit_warning": "Het bewerken van dit bericht zal alle volgende berichten in het gesprek verwijderen. Wil je doorgaan?",
|
||||
"proceed": "Doorgaan"
|
||||
"delete_custom_mode_with_rules": "Weet je zeker dat je deze {scope}-modus wilt verwijderen?\n\nDit verwijdert ook de bijbehorende regelsmap op:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "Ongeldig data-URI-formaat",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "Czy na pewno chcesz zresetować wszystkie stany i tajne magazyny w rozszerzeniu? Tej operacji nie można cofnąć.",
|
||||
"delete_config_profile": "Czy na pewno chcesz usunąć ten profil konfiguracyjny?",
|
||||
"delete_custom_mode_with_rules": "Czy na pewno chcesz usunąć ten tryb {scope}?\n\nSpowoduje to również usunięcie powiązanego folderu reguł pod adresem:\n{rulesFolderPath}",
|
||||
"delete_message": "Co chcesz usunąć?",
|
||||
"delete_just_this_message": "Tylko tę wiadomość",
|
||||
"delete_this_and_subsequent": "Tę i wszystkie kolejne wiadomości",
|
||||
"edit_warning": "Edytowanie tej wiadomości usunie wszystkie kolejne wiadomości w rozmowie. Czy chcesz kontynuować?",
|
||||
"proceed": "Kontynuuj"
|
||||
"delete_custom_mode_with_rules": "Czy na pewno chcesz usunąć ten tryb {scope}?\n\nSpowoduje to również usunięcie powiązanego folderu reguł pod adresem:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "Nieprawidłowy format URI danych",
|
||||
|
|
|
|||
|
|
@ -21,12 +21,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "Tem certeza de que deseja redefinir todo o estado e armazenamento secreto na extensão? Isso não pode ser desfeito.",
|
||||
"delete_config_profile": "Tem certeza de que deseja excluir este perfil de configuração?",
|
||||
"delete_custom_mode_with_rules": "Tem certeza de que deseja excluir este modo {scope}?\n\nIsso também excluirá a pasta de regras associada em:\n{rulesFolderPath}",
|
||||
"delete_message": "O que você gostaria de excluir?",
|
||||
"delete_just_this_message": "Apenas esta mensagem",
|
||||
"delete_this_and_subsequent": "Esta e todas as mensagens subsequentes",
|
||||
"edit_warning": "Editar esta mensagem excluirá todas as mensagens subsequentes na conversa. Deseja continuar?",
|
||||
"proceed": "Continuar"
|
||||
"delete_custom_mode_with_rules": "Tem certeza de que deseja excluir este modo {scope}?\n\nIsso também excluirá a pasta de regras associada em:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "Formato de URI de dados inválido",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "Вы уверены, что хотите сбросить все состояние и секретное хранилище в расширении? Это действие нельзя отменить.",
|
||||
"delete_config_profile": "Вы уверены, что хотите удалить этот профиль конфигурации?",
|
||||
"delete_custom_mode_with_rules": "Вы уверены, что хотите удалить этот режим {scope}?\n\nЭто также приведет к удалению соответствующей папки правил по адресу:\n{rulesFolderPath}",
|
||||
"delete_message": "Что вы хотите удалить?",
|
||||
"delete_just_this_message": "Только это сообщение",
|
||||
"delete_this_and_subsequent": "Это и все последующие сообщения",
|
||||
"edit_warning": "Редактирование этого сообщения удалит все последующие сообщения в разговоре. Хотите продолжить?",
|
||||
"proceed": "Продолжить"
|
||||
"delete_custom_mode_with_rules": "Вы уверены, что хотите удалить этот режим {scope}?\n\nЭто также приведет к удалению соответствующей папки правил по адресу:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "Неверный формат URI данных",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "Uzantıdaki tüm durumları ve gizli depolamayı sıfırlamak istediğinizden emin misiniz? Bu işlem geri alınamaz.",
|
||||
"delete_config_profile": "Bu yapılandırma profilini silmek istediğinizden emin misiniz?",
|
||||
"delete_custom_mode_with_rules": "Bu {scope} modunu silmek istediğinizden emin misiniz?\n\nBu işlem, ilişkili kurallar klasörünü de şu konumdan silecektir:\n{rulesFolderPath}",
|
||||
"delete_message": "Neyi silmek istersiniz?",
|
||||
"delete_just_this_message": "Sadece bu mesajı",
|
||||
"delete_this_and_subsequent": "Bu ve sonraki tüm mesajları",
|
||||
"edit_warning": "Bu mesajı düzenlemek konuşmadaki tüm sonraki mesajları silecektir. Devam etmek istiyor musunuz?",
|
||||
"proceed": "Devam et"
|
||||
"delete_custom_mode_with_rules": "Bu {scope} modunu silmek istediğinizden emin misiniz?\n\nBu işlem, ilişkili kurallar klasörünü de şu konumdan silecektir:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "Geçersiz veri URI formatı",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "Bạn có chắc chắn muốn đặt lại tất cả trạng thái và lưu trữ bí mật trong tiện ích mở rộng không? Hành động này không thể hoàn tác.",
|
||||
"delete_config_profile": "Bạn có chắc chắn muốn xóa hồ sơ cấu hình này không?",
|
||||
"delete_custom_mode_with_rules": "Bạn có chắc chắn muốn xóa chế độ {scope} này không?\n\nThao tác này cũng sẽ xóa thư mục quy tắc liên quan tại:\n{rulesFolderPath}",
|
||||
"delete_message": "Bạn muốn xóa gì?",
|
||||
"delete_just_this_message": "Chỉ tin nhắn này",
|
||||
"delete_this_and_subsequent": "Tin nhắn này và tất cả tin nhắn tiếp theo",
|
||||
"edit_warning": "Chỉnh sửa tin nhắn này sẽ xóa tất cả tin nhắn tiếp theo trong cuộc trò chuyện. Bạn có muốn tiếp tục không?",
|
||||
"proceed": "Tiếp tục"
|
||||
"delete_custom_mode_with_rules": "Bạn có chắc chắn muốn xóa chế độ {scope} này không?\n\nThao tác này cũng sẽ xóa thư mục quy tắc liên quan tại:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "Định dạng URI dữ liệu không hợp lệ",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "您确定要重置扩展中的所有状态和密钥存储吗?此操作无法撤消。",
|
||||
"delete_config_profile": "您确定要删除此配置文件吗?",
|
||||
"delete_custom_mode_with_rules": "您确定要删除此 {scope} 模式吗?\n\n这也将删除位于以下位置的关联规则文件夹:\n{rulesFolderPath}",
|
||||
"delete_message": "您想删除什么?",
|
||||
"edit_warning": "编辑此消息将删除对话中的所有后续消息。您要继续吗?",
|
||||
"delete_just_this_message": "仅此消息",
|
||||
"delete_this_and_subsequent": "此消息及所有后续消息",
|
||||
"proceed": "继续"
|
||||
"delete_custom_mode_with_rules": "您确定要删除此 {scope} 模式吗?\n\n这也将删除位于以下位置的关联规则文件夹:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_mcp_config": "项目MCP配置格式无效",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@
|
|||
"confirmation": {
|
||||
"reset_state": "您確定要重設擴充套件中的所有狀態和金鑰儲存嗎?此操作無法復原。",
|
||||
"delete_config_profile": "您確定要刪除此設定檔案嗎?",
|
||||
"delete_custom_mode_with_rules": "您確定要刪除此 {scope} 模式嗎?\n\n這也將刪除位於以下位置的關聯規則資料夾:\n{rulesFolderPath}",
|
||||
"delete_message": "您想刪除哪些內容?",
|
||||
"edit_warning": "編輯此訊息將刪除對話中的所有後續訊息。您要繼續嗎?",
|
||||
"delete_just_this_message": "僅這則訊息",
|
||||
"delete_this_and_subsequent": "這則訊息及所有後續訊息",
|
||||
"proceed": "繼續"
|
||||
"delete_custom_mode_with_rules": "您確定要刪除此 {scope} 模式嗎?\n\n這也將刪除位於以下位置的關聯規則資料夾:\n{rulesFolderPath}"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_data_uri": "資料 URI 格式無效",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const QDRANT_CODE_BLOCK_NAMESPACE = "f47ac10b-58cc-4372-a567-0e02b2c3d479
|
|||
export const MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024 // 1MB
|
||||
|
||||
/**Directory Scanner */
|
||||
export const MAX_LIST_FILES_LIMIT = 3_000
|
||||
export const MAX_LIST_FILES_LIMIT_CODE_INDEX = 50_000
|
||||
export const BATCH_SEGMENT_THRESHOLD = 60 // Number of code segments to batch for embeddings/upserts
|
||||
export const MAX_BATCH_RETRIES = 3
|
||||
export const INITIAL_RETRY_DELAY_MS = 500
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ export interface IDirectoryScanner {
|
|||
onBlocksIndexed?: (indexedCount: number) => void,
|
||||
onFileParsed?: (fileBlockCount: number) => void,
|
||||
): Promise<{
|
||||
codeBlocks: CodeBlock[]
|
||||
stats: {
|
||||
processed: number
|
||||
skipped: number
|
||||
|
|
|
|||
|
|
@ -168,7 +168,16 @@ describe("DirectoryScanner", () => {
|
|||
expect(mockCodeParser.parseFile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should parse changed files and return code blocks", async () => {
|
||||
it("should parse changed files and return empty codeBlocks array", async () => {
|
||||
// Create scanner without embedder to test the non-embedding path
|
||||
const scannerNoEmbeddings = new DirectoryScanner(
|
||||
null as any, // No embedder
|
||||
null as any, // No vector store
|
||||
mockCodeParser,
|
||||
mockCacheManager,
|
||||
mockIgnoreInstance,
|
||||
)
|
||||
|
||||
const { listFiles } = await import("../../../glob/list-files")
|
||||
vi.mocked(listFiles).mockResolvedValue([["test/file1.js"], false])
|
||||
const mockBlocks: any[] = [
|
||||
|
|
@ -185,8 +194,7 @@ describe("DirectoryScanner", () => {
|
|||
]
|
||||
;(mockCodeParser.parseFile as any).mockResolvedValue(mockBlocks)
|
||||
|
||||
const result = await scanner.scanDirectory("/test")
|
||||
expect(result.codeBlocks).toEqual(mockBlocks)
|
||||
const result = await scannerNoEmbeddings.scanDirectory("/test")
|
||||
expect(result.stats.processed).toBe(1)
|
||||
})
|
||||
|
||||
|
|
@ -252,6 +260,15 @@ describe("DirectoryScanner", () => {
|
|||
})
|
||||
|
||||
it("should process markdown files alongside code files", async () => {
|
||||
// Create scanner without embedder to test the non-embedding path
|
||||
const scannerNoEmbeddings = new DirectoryScanner(
|
||||
null as any, // No embedder
|
||||
null as any, // No vector store
|
||||
mockCodeParser,
|
||||
mockCacheManager,
|
||||
mockIgnoreInstance,
|
||||
)
|
||||
|
||||
const { listFiles } = await import("../../../glob/list-files")
|
||||
vi.mocked(listFiles).mockResolvedValue([["test/README.md", "test/app.js", "docs/guide.markdown"], false])
|
||||
|
||||
|
|
@ -306,7 +323,7 @@ describe("DirectoryScanner", () => {
|
|||
return []
|
||||
})
|
||||
|
||||
const result = await scanner.scanDirectory("/test")
|
||||
const result = await scannerNoEmbeddings.scanDirectory("/test")
|
||||
|
||||
// Verify all files were processed
|
||||
expect(mockCodeParser.parseFile).toHaveBeenCalledTimes(3)
|
||||
|
|
@ -314,16 +331,7 @@ describe("DirectoryScanner", () => {
|
|||
expect(mockCodeParser.parseFile).toHaveBeenCalledWith("test/app.js", expect.any(Object))
|
||||
expect(mockCodeParser.parseFile).toHaveBeenCalledWith("docs/guide.markdown", expect.any(Object))
|
||||
|
||||
// Verify code blocks include both markdown and code content
|
||||
expect(result.codeBlocks).toHaveLength(3)
|
||||
expect(result.codeBlocks).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ type: "markdown_header_h1" }),
|
||||
expect.objectContaining({ type: "function" }),
|
||||
expect.objectContaining({ type: "markdown_header_h2" }),
|
||||
]),
|
||||
)
|
||||
|
||||
// Verify processing still works without codeBlocks accumulation
|
||||
expect(result.stats.processed).toBe(3)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { t } from "../../../i18n"
|
|||
import {
|
||||
QDRANT_CODE_BLOCK_NAMESPACE,
|
||||
MAX_FILE_SIZE_BYTES,
|
||||
MAX_LIST_FILES_LIMIT,
|
||||
MAX_LIST_FILES_LIMIT_CODE_INDEX,
|
||||
BATCH_SEGMENT_THRESHOLD,
|
||||
MAX_BATCH_RETRIES,
|
||||
INITIAL_RETRY_DELAY_MS,
|
||||
|
|
@ -51,13 +51,13 @@ export class DirectoryScanner implements IDirectoryScanner {
|
|||
onError?: (error: Error) => void,
|
||||
onBlocksIndexed?: (indexedCount: number) => void,
|
||||
onFileParsed?: (fileBlockCount: number) => void,
|
||||
): Promise<{ codeBlocks: CodeBlock[]; stats: { processed: number; skipped: number }; totalBlockCount: number }> {
|
||||
): Promise<{ stats: { processed: number; skipped: number }; totalBlockCount: number }> {
|
||||
const directoryPath = directory
|
||||
// Capture workspace context at scan start
|
||||
const scanWorkspace = getWorkspacePathForContext(directoryPath)
|
||||
|
||||
// Get all files recursively (handles .gitignore automatically)
|
||||
const [allPaths, _] = await listFiles(directoryPath, true, MAX_LIST_FILES_LIMIT)
|
||||
const [allPaths, _] = await listFiles(directoryPath, true, MAX_LIST_FILES_LIMIT_CODE_INDEX)
|
||||
|
||||
// Filter out directories (marked with trailing '/')
|
||||
const filePaths = allPaths.filter((p) => !p.endsWith("/"))
|
||||
|
|
@ -85,7 +85,6 @@ export class DirectoryScanner implements IDirectoryScanner {
|
|||
|
||||
// Initialize tracking variables
|
||||
const processedFiles = new Set<string>()
|
||||
const codeBlocks: CodeBlock[] = []
|
||||
let processedCount = 0
|
||||
let skippedCount = 0
|
||||
|
||||
|
|
@ -98,7 +97,7 @@ export class DirectoryScanner implements IDirectoryScanner {
|
|||
let currentBatchBlocks: CodeBlock[] = []
|
||||
let currentBatchTexts: string[] = []
|
||||
let currentBatchFileInfos: { filePath: string; fileHash: string; isNew: boolean }[] = []
|
||||
const activeBatchPromises: Promise<void>[] = []
|
||||
const activeBatchPromises = new Set<Promise<void>>()
|
||||
|
||||
// Initialize block counter
|
||||
let totalBlockCount = 0
|
||||
|
|
@ -125,6 +124,7 @@ export class DirectoryScanner implements IDirectoryScanner {
|
|||
|
||||
// Check against cache
|
||||
const cachedFileHash = this.cacheManager.getHash(filePath)
|
||||
const isNewFile = !cachedFileHash
|
||||
if (cachedFileHash === currentFileHash) {
|
||||
// File is unchanged
|
||||
skippedCount++
|
||||
|
|
@ -135,7 +135,6 @@ export class DirectoryScanner implements IDirectoryScanner {
|
|||
const blocks = await this.codeParser.parseFile(filePath, { content, fileHash: currentFileHash })
|
||||
const fileBlockCount = blocks.length
|
||||
onFileParsed?.(fileBlockCount)
|
||||
codeBlocks.push(...blocks)
|
||||
processedCount++
|
||||
|
||||
// Process embeddings if configured
|
||||
|
|
@ -146,20 +145,11 @@ export class DirectoryScanner implements IDirectoryScanner {
|
|||
const trimmedContent = block.content.trim()
|
||||
if (trimmedContent) {
|
||||
const release = await mutex.acquire()
|
||||
totalBlockCount += fileBlockCount
|
||||
try {
|
||||
currentBatchBlocks.push(block)
|
||||
currentBatchTexts.push(trimmedContent)
|
||||
addedBlocksFromFile = true
|
||||
|
||||
if (addedBlocksFromFile) {
|
||||
currentBatchFileInfos.push({
|
||||
filePath,
|
||||
fileHash: currentFileHash,
|
||||
isNew: !this.cacheManager.getHash(filePath),
|
||||
})
|
||||
}
|
||||
|
||||
// Check if batch threshold is met
|
||||
if (currentBatchBlocks.length >= BATCH_SEGMENT_THRESHOLD) {
|
||||
// Copy current batch data and clear accumulators
|
||||
|
|
@ -181,13 +171,33 @@ export class DirectoryScanner implements IDirectoryScanner {
|
|||
onBlocksIndexed,
|
||||
),
|
||||
)
|
||||
activeBatchPromises.push(batchPromise)
|
||||
activeBatchPromises.add(batchPromise)
|
||||
|
||||
// Clean up completed promises to prevent memory accumulation
|
||||
batchPromise.finally(() => {
|
||||
activeBatchPromises.delete(batchPromise)
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add file info once per file (outside the block loop)
|
||||
if (addedBlocksFromFile) {
|
||||
const release = await mutex.acquire()
|
||||
try {
|
||||
totalBlockCount += fileBlockCount
|
||||
currentBatchFileInfos.push({
|
||||
filePath,
|
||||
fileHash: currentFileHash,
|
||||
isNew: isNewFile,
|
||||
})
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Only update hash if not being processed in a batch
|
||||
await this.cacheManager.updateHash(filePath, currentFileHash)
|
||||
|
|
@ -232,7 +242,12 @@ export class DirectoryScanner implements IDirectoryScanner {
|
|||
const batchPromise = batchLimiter(() =>
|
||||
this.processBatch(batchBlocks, batchTexts, batchFileInfos, scanWorkspace, onError, onBlocksIndexed),
|
||||
)
|
||||
activeBatchPromises.push(batchPromise)
|
||||
activeBatchPromises.add(batchPromise)
|
||||
|
||||
// Clean up completed promises to prevent memory accumulation
|
||||
batchPromise.finally(() => {
|
||||
activeBatchPromises.delete(batchPromise)
|
||||
})
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
|
|
@ -280,7 +295,6 @@ export class DirectoryScanner implements IDirectoryScanner {
|
|||
}
|
||||
|
||||
return {
|
||||
codeBlocks,
|
||||
stats: {
|
||||
processed: processedCount,
|
||||
skipped: skippedCount,
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ export interface ExtensionMessage {
|
|||
| "shareTaskSuccess"
|
||||
| "codeIndexSettingsSaved"
|
||||
| "codeIndexSecretStatus"
|
||||
| "showDeleteMessageDialog"
|
||||
| "showEditMessageDialog"
|
||||
text?: string
|
||||
payload?: any // Add a generic payload for now, can refine later
|
||||
action?:
|
||||
|
|
@ -157,6 +159,8 @@ export interface ExtensionMessage {
|
|||
visibility?: ShareVisibility
|
||||
rulesFolderPath?: string
|
||||
settings?: any
|
||||
messageTs?: number
|
||||
context?: string
|
||||
}
|
||||
|
||||
export type ExtensionState = Pick<
|
||||
|
|
|
|||
|
|
@ -111,7 +111,9 @@ export interface WebviewMessage {
|
|||
| "enhancedPrompt"
|
||||
| "draggedImages"
|
||||
| "deleteMessage"
|
||||
| "deleteMessageConfirm"
|
||||
| "submitEditedMessage"
|
||||
| "editMessageConfirm"
|
||||
| "terminalOutputLineLimit"
|
||||
| "terminalShellIntegrationTimeout"
|
||||
| "terminalShellIntegrationDisabled"
|
||||
|
|
@ -198,6 +200,7 @@ export interface WebviewMessage {
|
|||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
|
||||
disabled?: boolean
|
||||
context?: string
|
||||
dataUri?: string
|
||||
askResponse?: ClineAskResponse
|
||||
apiConfiguration?: ProviderSettings
|
||||
|
|
@ -226,6 +229,7 @@ export interface WebviewMessage {
|
|||
ids?: string[]
|
||||
hasSystemPromptOverride?: boolean
|
||||
terminalOperation?: "continue" | "abort"
|
||||
messageTs?: number
|
||||
historyPreviewCollapsed?: boolean
|
||||
filters?: { type?: string; search?: string; tags?: string[] }
|
||||
url?: string // For openExternal
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useRef, useState, useMemo } from "react"
|
||||
import React, { useCallback, useEffect, useRef, useState, useMemo } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ import McpView from "./components/mcp/McpView"
|
|||
import { MarketplaceView } from "./components/marketplace/MarketplaceView"
|
||||
import ModesView from "./components/modes/ModesView"
|
||||
import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
|
||||
import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog"
|
||||
import { AccountView } from "./components/account/AccountView"
|
||||
import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick"
|
||||
import { TooltipProvider } from "./components/ui/tooltip"
|
||||
|
|
@ -25,6 +26,29 @@ import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip"
|
|||
|
||||
type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
|
||||
|
||||
interface HumanRelayDialogState {
|
||||
isOpen: boolean
|
||||
requestId: string
|
||||
promptText: string
|
||||
}
|
||||
|
||||
interface DeleteMessageDialogState {
|
||||
isOpen: boolean
|
||||
messageTs: number
|
||||
}
|
||||
|
||||
interface EditMessageDialogState {
|
||||
isOpen: boolean
|
||||
messageTs: number
|
||||
text: string
|
||||
images?: string[]
|
||||
}
|
||||
|
||||
// Memoize dialog components to prevent unnecessary re-renders
|
||||
const MemoizedDeleteMessageDialog = React.memo(DeleteMessageDialog)
|
||||
const MemoizedEditMessageDialog = React.memo(EditMessageDialog)
|
||||
const MemoizedHumanRelayDialog = React.memo(HumanRelayDialog)
|
||||
|
||||
const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]>, Tab>> = {
|
||||
chatButtonClicked: "chat",
|
||||
settingsButtonClicked: "settings",
|
||||
|
|
@ -56,16 +80,24 @@ const App = () => {
|
|||
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
||||
const [tab, setTab] = useState<Tab>("chat")
|
||||
|
||||
const [humanRelayDialogState, setHumanRelayDialogState] = useState<{
|
||||
isOpen: boolean
|
||||
requestId: string
|
||||
promptText: string
|
||||
}>({
|
||||
const [humanRelayDialogState, setHumanRelayDialogState] = useState<HumanRelayDialogState>({
|
||||
isOpen: false,
|
||||
requestId: "",
|
||||
promptText: "",
|
||||
})
|
||||
|
||||
const [deleteMessageDialogState, setDeleteMessageDialogState] = useState<DeleteMessageDialogState>({
|
||||
isOpen: false,
|
||||
messageTs: 0,
|
||||
})
|
||||
|
||||
const [editMessageDialogState, setEditMessageDialogState] = useState<EditMessageDialogState>({
|
||||
isOpen: false,
|
||||
messageTs: 0,
|
||||
text: "",
|
||||
images: [],
|
||||
})
|
||||
|
||||
const settingsRef = useRef<SettingsViewRef>(null)
|
||||
const chatViewRef = useRef<ChatViewRef>(null)
|
||||
|
||||
|
|
@ -121,6 +153,19 @@ const App = () => {
|
|||
setHumanRelayDialogState({ isOpen: true, requestId, promptText })
|
||||
}
|
||||
|
||||
if (message.type === "showDeleteMessageDialog" && message.messageTs) {
|
||||
setDeleteMessageDialogState({ isOpen: true, messageTs: message.messageTs })
|
||||
}
|
||||
|
||||
if (message.type === "showEditMessageDialog" && message.messageTs && message.text) {
|
||||
setEditMessageDialogState({
|
||||
isOpen: true,
|
||||
messageTs: message.messageTs,
|
||||
text: message.text,
|
||||
images: message.images || [],
|
||||
})
|
||||
}
|
||||
|
||||
if (message.type === "acceptInput") {
|
||||
chatViewRef.current?.acceptInput()
|
||||
}
|
||||
|
|
@ -199,7 +244,7 @@ const App = () => {
|
|||
showAnnouncement={showAnnouncement}
|
||||
hideAnnouncement={() => setShowAnnouncement(false)}
|
||||
/>
|
||||
<HumanRelayDialog
|
||||
<MemoizedHumanRelayDialog
|
||||
isOpen={humanRelayDialogState.isOpen}
|
||||
requestId={humanRelayDialogState.requestId}
|
||||
promptText={humanRelayDialogState.promptText}
|
||||
|
|
@ -207,6 +252,30 @@ const App = () => {
|
|||
onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
|
||||
onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
|
||||
/>
|
||||
<MemoizedDeleteMessageDialog
|
||||
open={deleteMessageDialogState.isOpen}
|
||||
onOpenChange={(open) => setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
|
||||
onConfirm={() => {
|
||||
vscode.postMessage({
|
||||
type: "deleteMessageConfirm",
|
||||
messageTs: deleteMessageDialogState.messageTs,
|
||||
})
|
||||
setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false }))
|
||||
}}
|
||||
/>
|
||||
<MemoizedEditMessageDialog
|
||||
open={editMessageDialogState.isOpen}
|
||||
onOpenChange={(open) => setEditMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
|
||||
onConfirm={() => {
|
||||
vscode.postMessage({
|
||||
type: "editMessageConfirm",
|
||||
messageTs: editMessageDialogState.messageTs,
|
||||
text: editMessageDialogState.text,
|
||||
images: editMessageDialogState.images,
|
||||
})
|
||||
setEditMessageDialogState((prev) => ({ ...prev, isOpen: false }))
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import { vscode } from "@src/utils/vscode"
|
|||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { AutoApproveToggle, AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle"
|
||||
import { StandardTooltip } from "@src/components/ui"
|
||||
import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
|
||||
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
|
||||
|
||||
interface AutoApproveMenuProps {
|
||||
style?: React.CSSProperties
|
||||
|
|
@ -17,16 +20,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||
const {
|
||||
autoApprovalEnabled,
|
||||
setAutoApprovalEnabled,
|
||||
alwaysAllowReadOnly,
|
||||
alwaysAllowWrite,
|
||||
alwaysAllowExecute,
|
||||
alwaysAllowBrowser,
|
||||
alwaysAllowMcp,
|
||||
alwaysAllowModeSwitch,
|
||||
alwaysAllowSubtasks,
|
||||
alwaysApproveResubmit,
|
||||
alwaysAllowFollowupQuestions,
|
||||
alwaysAllowUpdateTodoList,
|
||||
allowedMaxRequests,
|
||||
setAlwaysAllowReadOnly,
|
||||
setAlwaysAllowWrite,
|
||||
|
|
@ -43,10 +37,24 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const baseToggles = useAutoApprovalToggles()
|
||||
|
||||
// AutoApproveMenu needs alwaysApproveResubmit in addition to the base toggles
|
||||
const toggles = useMemo(
|
||||
() => ({
|
||||
...baseToggles,
|
||||
alwaysApproveResubmit: alwaysApproveResubmit,
|
||||
}),
|
||||
[baseToggles, alwaysApproveResubmit],
|
||||
)
|
||||
|
||||
const { hasEnabledOptions, effectiveAutoApprovalEnabled } = useAutoApprovalState(toggles, autoApprovalEnabled)
|
||||
|
||||
const onAutoApproveToggle = useCallback(
|
||||
(key: AutoApproveSetting, value: boolean) => {
|
||||
vscode.postMessage({ type: key, bool: value })
|
||||
|
||||
// Update the specific toggle state
|
||||
switch (key) {
|
||||
case "alwaysAllowReadOnly":
|
||||
setAlwaysAllowReadOnly(value)
|
||||
|
|
@ -79,8 +87,30 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||
setAlwaysAllowUpdateTodoList(value)
|
||||
break
|
||||
}
|
||||
|
||||
// Check if we need to update the master auto-approval state
|
||||
// Create a new toggles state with the updated value
|
||||
const updatedToggles = {
|
||||
...toggles,
|
||||
[key]: value,
|
||||
}
|
||||
|
||||
const willHaveEnabledOptions = Object.values(updatedToggles).some((v) => !!v)
|
||||
|
||||
// If enabling the first option, enable master auto-approval
|
||||
if (value && !hasEnabledOptions && willHaveEnabledOptions) {
|
||||
setAutoApprovalEnabled(true)
|
||||
vscode.postMessage({ type: "autoApprovalEnabled", bool: true })
|
||||
}
|
||||
// If disabling the last option, disable master auto-approval
|
||||
else if (!value && hasEnabledOptions && !willHaveEnabledOptions) {
|
||||
setAutoApprovalEnabled(false)
|
||||
vscode.postMessage({ type: "autoApprovalEnabled", bool: false })
|
||||
}
|
||||
},
|
||||
[
|
||||
toggles,
|
||||
hasEnabledOptions,
|
||||
setAlwaysAllowReadOnly,
|
||||
setAlwaysAllowWrite,
|
||||
setAlwaysAllowExecute,
|
||||
|
|
@ -91,43 +121,32 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||
setAlwaysApproveResubmit,
|
||||
setAlwaysAllowFollowupQuestions,
|
||||
setAlwaysAllowUpdateTodoList,
|
||||
setAutoApprovalEnabled,
|
||||
],
|
||||
)
|
||||
|
||||
const toggleExpanded = useCallback(() => setIsExpanded((prev) => !prev), [])
|
||||
const toggleExpanded = useCallback(() => {
|
||||
setIsExpanded((prev) => !prev)
|
||||
}, [])
|
||||
|
||||
const toggles = useMemo(
|
||||
() => ({
|
||||
alwaysAllowReadOnly: alwaysAllowReadOnly,
|
||||
alwaysAllowWrite: alwaysAllowWrite,
|
||||
alwaysAllowExecute: alwaysAllowExecute,
|
||||
alwaysAllowBrowser: alwaysAllowBrowser,
|
||||
alwaysAllowMcp: alwaysAllowMcp,
|
||||
alwaysAllowModeSwitch: alwaysAllowModeSwitch,
|
||||
alwaysAllowSubtasks: alwaysAllowSubtasks,
|
||||
alwaysApproveResubmit: alwaysApproveResubmit,
|
||||
alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions,
|
||||
alwaysAllowUpdateTodoList: alwaysAllowUpdateTodoList,
|
||||
}),
|
||||
[
|
||||
alwaysAllowReadOnly,
|
||||
alwaysAllowWrite,
|
||||
alwaysAllowExecute,
|
||||
alwaysAllowBrowser,
|
||||
alwaysAllowMcp,
|
||||
alwaysAllowModeSwitch,
|
||||
alwaysAllowSubtasks,
|
||||
alwaysApproveResubmit,
|
||||
alwaysAllowFollowupQuestions,
|
||||
alwaysAllowUpdateTodoList,
|
||||
],
|
||||
)
|
||||
// Disable main checkbox while menu is open or no options selected
|
||||
const isCheckboxDisabled = useMemo(() => {
|
||||
return !hasEnabledOptions || isExpanded
|
||||
}, [hasEnabledOptions, isExpanded])
|
||||
|
||||
const enabledActionsList = Object.entries(toggles)
|
||||
.filter(([_key, value]) => !!value)
|
||||
.map(([key]) => t(autoApproveSettingsConfig[key as AutoApproveSetting].labelKey))
|
||||
.join(", ")
|
||||
|
||||
// Update displayed text logic
|
||||
const displayText = useMemo(() => {
|
||||
if (!effectiveAutoApprovalEnabled || !hasEnabledOptions) {
|
||||
return t("chat:autoApprove.none")
|
||||
}
|
||||
return enabledActionsList || t("chat:autoApprove.none")
|
||||
}, [effectiveAutoApprovalEnabled, hasEnabledOptions, enabledActionsList, t])
|
||||
|
||||
const handleOpenSettings = useCallback(
|
||||
() =>
|
||||
window.postMessage({ type: "action", action: "settingsButtonClicked", values: { section: "autoApprove" } }),
|
||||
|
|
@ -155,14 +174,26 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||
}}
|
||||
onClick={toggleExpanded}>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<VSCodeCheckbox
|
||||
checked={autoApprovalEnabled ?? false}
|
||||
onChange={() => {
|
||||
const newValue = !(autoApprovalEnabled ?? false)
|
||||
setAutoApprovalEnabled(newValue)
|
||||
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
|
||||
}}
|
||||
/>
|
||||
<StandardTooltip
|
||||
content={!hasEnabledOptions ? t("chat:autoApprove.selectOptionsFirst") : undefined}>
|
||||
<VSCodeCheckbox
|
||||
checked={effectiveAutoApprovalEnabled}
|
||||
disabled={isCheckboxDisabled}
|
||||
aria-label={
|
||||
hasEnabledOptions
|
||||
? t("chat:autoApprove.toggleAriaLabel")
|
||||
: t("chat:autoApprove.disabledAriaLabel")
|
||||
}
|
||||
onChange={() => {
|
||||
if (hasEnabledOptions) {
|
||||
const newValue = !(autoApprovalEnabled ?? false)
|
||||
setAutoApprovalEnabled(newValue)
|
||||
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
|
||||
}
|
||||
// If no options enabled, do nothing
|
||||
}}
|
||||
/>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -188,7 +219,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||
flex: 1,
|
||||
minWidth: 0,
|
||||
}}>
|
||||
{enabledActionsList || t("chat:autoApprove.none")}
|
||||
{displayText}
|
||||
</span>
|
||||
<span
|
||||
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { appendImages } from "@src/utils/imageUtils"
|
||||
import { McpExecution } from "./McpExecution"
|
||||
import { useSize } from "react-use"
|
||||
import { useTranslation, Trans } from "react-i18next"
|
||||
|
|
@ -6,6 +7,7 @@ import deepEqual from "fast-deep-equal"
|
|||
import { VSCodeBadge, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import type { ClineMessage } from "@roo-code/types"
|
||||
import { Mode } from "@roo/modes"
|
||||
|
||||
import { ClineApiReqInfo, ClineAskUseMcpServer, ClineSayTool } from "@roo/ExtensionMessage"
|
||||
import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences"
|
||||
|
|
@ -20,6 +22,9 @@ import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanu
|
|||
import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
|
||||
import { Button } from "@src/components/ui"
|
||||
|
||||
import ChatTextArea from "./ChatTextArea"
|
||||
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
|
||||
|
||||
import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
|
||||
import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock"
|
||||
import CodeAccordian from "../common/CodeAccordian"
|
||||
|
|
@ -109,14 +114,29 @@ export const ChatRowContent = ({
|
|||
editable,
|
||||
}: ChatRowContentProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState()
|
||||
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode } = useExtensionState()
|
||||
const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
|
||||
const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
|
||||
const [showCopySuccess, setShowCopySuccess] = useState(false)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editedContent, setEditedContent] = useState("")
|
||||
const [editMode, setEditMode] = useState<Mode>(mode || "code")
|
||||
const [editImages, setEditImages] = useState<string[]>([])
|
||||
const { copyWithFeedback } = useCopyToClipboard()
|
||||
|
||||
// Handle message events for image selection during edit mode
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const msg = event.data
|
||||
if (msg.type === "selectedImages" && msg.context === "edit" && msg.messageTs === message.ts && isEditing) {
|
||||
setEditImages((prevImages) => appendImages(prevImages, msg.images, MAX_IMAGES_PER_MESSAGE))
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage)
|
||||
return () => window.removeEventListener("message", handleMessage)
|
||||
}, [isEditing, message.ts])
|
||||
|
||||
// Memoized callback to prevent re-renders caused by inline arrow functions
|
||||
const handleToggleExpand = useCallback(() => {
|
||||
onToggleExpand(message.ts)
|
||||
|
|
@ -126,15 +146,19 @@ export const ChatRowContent = ({
|
|||
const handleEditClick = useCallback(() => {
|
||||
setIsEditing(true)
|
||||
setEditedContent(message.text || "")
|
||||
setEditImages(message.images || [])
|
||||
setEditMode(mode || "code")
|
||||
// Edit mode is now handled entirely in the frontend
|
||||
// No need to notify the backend
|
||||
}, [message.text])
|
||||
}, [message.text, message.images, mode])
|
||||
|
||||
// Handle cancel edit
|
||||
const handleCancelEdit = useCallback(() => {
|
||||
setIsEditing(false)
|
||||
setEditedContent(message.text || "")
|
||||
}, [message.text])
|
||||
setEditImages(message.images || [])
|
||||
setEditMode(mode || "code")
|
||||
}, [message.text, message.images, mode])
|
||||
|
||||
// Handle save edit
|
||||
const handleSaveEdit = useCallback(() => {
|
||||
|
|
@ -144,8 +168,14 @@ export const ChatRowContent = ({
|
|||
type: "submitEditedMessage",
|
||||
value: message.ts,
|
||||
editedMessageContent: editedContent,
|
||||
images: editImages,
|
||||
})
|
||||
}, [message.ts, editedContent])
|
||||
}, [message.ts, editedContent, editImages])
|
||||
|
||||
// Handle image selection for editing
|
||||
const handleSelectImages = useCallback(() => {
|
||||
vscode.postMessage({ type: "selectImages", context: "edit", messageTs: message.ts })
|
||||
}, [message.ts])
|
||||
|
||||
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
|
||||
if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
|
||||
|
|
@ -1032,21 +1062,23 @@ export const ChatRowContent = ({
|
|||
<div className="bg-vscode-editor-background border rounded-xs p-1 overflow-hidden whitespace-pre-wrap">
|
||||
{isEditing ? (
|
||||
<div className="flex flex-col gap-2 p-2">
|
||||
<textarea
|
||||
className="w-full p-2 bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded-xs"
|
||||
value={editedContent}
|
||||
onChange={(e) => setEditedContent(e.target.value)}
|
||||
rows={5}
|
||||
autoFocus
|
||||
<ChatTextArea
|
||||
inputValue={editedContent}
|
||||
setInputValue={setEditedContent}
|
||||
sendingDisabled={false}
|
||||
selectApiConfigDisabled={true}
|
||||
placeholderText={t("chat:editMessage.placeholder")}
|
||||
selectedImages={editImages}
|
||||
setSelectedImages={setEditImages}
|
||||
onSend={handleSaveEdit}
|
||||
onSelectImages={handleSelectImages}
|
||||
shouldDisableImages={false}
|
||||
mode={editMode}
|
||||
setMode={setEditMode}
|
||||
modeShortcutText=""
|
||||
isEditMode={true}
|
||||
onCancel={handleCancelEdit}
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="secondary" size="sm" onClick={handleCancelEdit}>
|
||||
{t("chat:cancel.title")}
|
||||
</Button>
|
||||
<Button variant="default" size="sm" onClick={handleSaveEdit}>
|
||||
{t("chat:save.title")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-between">
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { VolumeX, Pin, Check, Image, WandSparkles, SendHorizontal } from "lucide
|
|||
import { IndexingStatusBadge } from "./IndexingStatusBadge"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { usePromptHistory } from "./hooks/usePromptHistory"
|
||||
import { EditModeControls } from "./EditModeControls"
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
inputValue: string
|
||||
|
|
@ -45,6 +46,9 @@ interface ChatTextAreaProps {
|
|||
mode: Mode
|
||||
setMode: (value: Mode) => void
|
||||
modeShortcutText: string
|
||||
// Edit mode props
|
||||
isEditMode?: boolean
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
|
|
@ -64,6 +68,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
mode,
|
||||
setMode,
|
||||
modeShortcutText,
|
||||
isEditMode = false,
|
||||
onCancel,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
|
|
@ -796,6 +802,378 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
|
||||
const placeholderBottomText = `\n(${t("chat:addContext")}${shouldDisableImages ? `, ${t("chat:dragFiles")}` : `, ${t("chat:dragFilesImages")}`})`
|
||||
|
||||
// Common mode selector handler
|
||||
const handleModeChange = useCallback(
|
||||
(value: Mode) => {
|
||||
setMode(value)
|
||||
vscode.postMessage({ type: "mode", text: value })
|
||||
},
|
||||
[setMode],
|
||||
)
|
||||
|
||||
// Helper function to render mode selector
|
||||
const renderModeSelector = () => (
|
||||
<ModeSelector
|
||||
value={mode}
|
||||
title={t("chat:selectMode")}
|
||||
onChange={handleModeChange}
|
||||
triggerClassName="w-full"
|
||||
modeShortcutText={modeShortcutText}
|
||||
customModes={customModes}
|
||||
customModePrompts={customModePrompts}
|
||||
/>
|
||||
)
|
||||
|
||||
// Helper function to get API config dropdown options
|
||||
const getApiConfigOptions = useMemo(() => {
|
||||
const pinnedConfigs = (listApiConfigMeta || [])
|
||||
.filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id])
|
||||
.map((config) => ({
|
||||
value: config.id,
|
||||
label: config.name,
|
||||
name: config.name,
|
||||
type: DropdownOptionType.ITEM,
|
||||
pinned: true,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
|
||||
const unpinnedConfigs = (listApiConfigMeta || [])
|
||||
.filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id])
|
||||
.map((config) => ({
|
||||
value: config.id,
|
||||
label: config.name,
|
||||
name: config.name,
|
||||
type: DropdownOptionType.ITEM,
|
||||
pinned: false,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
|
||||
const hasPinnedAndUnpinned = pinnedConfigs.length > 0 && unpinnedConfigs.length > 0
|
||||
|
||||
return [
|
||||
...pinnedConfigs,
|
||||
...(hasPinnedAndUnpinned
|
||||
? [
|
||||
{
|
||||
value: "sep-pinned",
|
||||
label: t("chat:separator"),
|
||||
type: DropdownOptionType.SEPARATOR,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...unpinnedConfigs,
|
||||
{
|
||||
value: "sep-2",
|
||||
label: t("chat:separator"),
|
||||
type: DropdownOptionType.SEPARATOR,
|
||||
},
|
||||
{
|
||||
value: "settingsButtonClicked",
|
||||
label: t("chat:edit"),
|
||||
type: DropdownOptionType.ACTION,
|
||||
},
|
||||
]
|
||||
}, [listApiConfigMeta, pinnedApiConfigs, t])
|
||||
|
||||
// Helper function to handle API config change
|
||||
const handleApiConfigChange = useCallback((value: string) => {
|
||||
if (value === "settingsButtonClicked") {
|
||||
vscode.postMessage({
|
||||
type: "loadApiConfiguration",
|
||||
text: value,
|
||||
values: { section: "providers" },
|
||||
})
|
||||
} else {
|
||||
vscode.postMessage({ type: "loadApiConfigurationById", text: value })
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Helper function to render API config item
|
||||
const renderApiConfigItem = useCallback(
|
||||
({ type, value, label, pinned }: any) => {
|
||||
if (type !== DropdownOptionType.ITEM) {
|
||||
return label
|
||||
}
|
||||
|
||||
const config = listApiConfigMeta?.find((c) => c.id === value)
|
||||
const isCurrentConfig = config?.name === currentApiConfigName
|
||||
|
||||
return (
|
||||
<div className="flex justify-between gap-2 w-full h-5">
|
||||
<div
|
||||
className={cn("truncate min-w-0 overflow-hidden", {
|
||||
"font-medium": isCurrentConfig,
|
||||
})}>
|
||||
{label}
|
||||
</div>
|
||||
<div className="flex justify-end w-10 flex-shrink-0">
|
||||
<div
|
||||
className={cn("size-5 p-1", {
|
||||
"block group-hover:hidden": !pinned,
|
||||
hidden: !isCurrentConfig,
|
||||
})}>
|
||||
<Check className="size-3" />
|
||||
</div>
|
||||
<StandardTooltip content={pinned ? t("chat:unpin") : t("chat:pin")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
togglePinnedApiConfig(value)
|
||||
vscode.postMessage({
|
||||
type: "toggleApiConfigPin",
|
||||
text: value,
|
||||
})
|
||||
}}
|
||||
className={cn("size-5", {
|
||||
"hidden group-hover:flex": !pinned,
|
||||
"bg-accent": pinned,
|
||||
})}>
|
||||
<Pin className="size-3 p-0.5 opacity-50" />
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
[listApiConfigMeta, currentApiConfigName, t, togglePinnedApiConfig],
|
||||
)
|
||||
|
||||
// Helper function to render non-edit mode controls
|
||||
const renderNonEditModeControls = () => (
|
||||
<div className={cn("flex", "justify-between", "items-center", "mt-auto")}>
|
||||
<div className={cn("flex", "items-center", "gap-1", "min-w-0")}>
|
||||
<div className="shrink-0">{renderModeSelector()}</div>
|
||||
|
||||
<div className={cn("flex-1", "min-w-0", "overflow-hidden")}>
|
||||
<SelectDropdown
|
||||
value={currentConfigId}
|
||||
disabled={selectApiConfigDisabled}
|
||||
title={t("chat:selectApiConfig")}
|
||||
disableSearch={false}
|
||||
placeholder={displayName}
|
||||
options={getApiConfigOptions}
|
||||
onChange={handleApiConfigChange}
|
||||
triggerClassName="w-full text-ellipsis overflow-hidden"
|
||||
itemClassName="group"
|
||||
renderItem={renderApiConfigItem}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn("flex", "items-center", "gap-0.5", "shrink-0")}>
|
||||
{isTtsPlaying && (
|
||||
<StandardTooltip content={t("chat:stopTts")}>
|
||||
<button
|
||||
aria-label={t("chat:stopTts")}
|
||||
onClick={() => vscode.postMessage({ type: "stopTts" })}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"text-vscode-foreground opacity-85",
|
||||
"transition-all duration-150",
|
||||
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
"cursor-pointer",
|
||||
)}>
|
||||
<VolumeX className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
<IndexingStatusBadge />
|
||||
<StandardTooltip content={t("chat:addImages")}>
|
||||
<button
|
||||
aria-label={t("chat:addImages")}
|
||||
disabled={shouldDisableImages}
|
||||
onClick={!shouldDisableImages ? onSelectImages : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"text-vscode-foreground opacity-85",
|
||||
"transition-all duration-150",
|
||||
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
!shouldDisableImages && "cursor-pointer",
|
||||
shouldDisableImages &&
|
||||
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
|
||||
"mr-1",
|
||||
)}>
|
||||
<Image className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Helper function to render the text area section
|
||||
const renderTextAreaSection = () => (
|
||||
<div
|
||||
className={cn(
|
||||
"relative",
|
||||
"flex-1",
|
||||
"flex",
|
||||
"flex-col-reverse",
|
||||
"min-h-0",
|
||||
"overflow-hidden",
|
||||
"rounded",
|
||||
)}>
|
||||
<div
|
||||
ref={highlightLayerRef}
|
||||
className={cn(
|
||||
"absolute",
|
||||
"inset-0",
|
||||
"pointer-events-none",
|
||||
"whitespace-pre-wrap",
|
||||
"break-words",
|
||||
"text-transparent",
|
||||
"overflow-hidden",
|
||||
"font-vscode-font-family",
|
||||
"text-vscode-editor-font-size",
|
||||
"leading-vscode-editor-line-height",
|
||||
"py-2",
|
||||
"px-[9px]",
|
||||
"z-10",
|
||||
"forced-color-adjust-none",
|
||||
)}
|
||||
style={{
|
||||
color: "transparent",
|
||||
}}
|
||||
/>
|
||||
<DynamicTextArea
|
||||
ref={(el) => {
|
||||
if (typeof ref === "function") {
|
||||
ref(el)
|
||||
} else if (ref) {
|
||||
ref.current = el
|
||||
}
|
||||
textAreaRef.current = el
|
||||
}}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
handleInputChange(e)
|
||||
updateHighlights()
|
||||
}}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onBlur={handleBlur}
|
||||
onPaste={handlePaste}
|
||||
onSelect={updateCursorPosition}
|
||||
onMouseUp={updateCursorPosition}
|
||||
onHeightChange={(height) => {
|
||||
if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
|
||||
setTextAreaBaseHeight(height)
|
||||
}
|
||||
|
||||
onHeightChange?.(height)
|
||||
}}
|
||||
placeholder={placeholderText}
|
||||
minRows={3}
|
||||
maxRows={15}
|
||||
autoFocus={true}
|
||||
className={cn(
|
||||
"w-full",
|
||||
"text-vscode-input-foreground",
|
||||
"font-vscode-font-family",
|
||||
"text-vscode-editor-font-size",
|
||||
"leading-vscode-editor-line-height",
|
||||
"cursor-text",
|
||||
isEditMode ? "pt-1.5 pb-10 px-2" : "py-1.5 px-2",
|
||||
isFocused
|
||||
? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
|
||||
: isDraggingOver
|
||||
? "border-2 border-dashed border-vscode-focusBorder"
|
||||
: "border border-transparent",
|
||||
isDraggingOver
|
||||
? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]"
|
||||
: "bg-vscode-input-background",
|
||||
"transition-background-color duration-150 ease-in-out",
|
||||
"will-change-background-color",
|
||||
"min-h-[90px]",
|
||||
"box-border",
|
||||
"rounded",
|
||||
"resize-none",
|
||||
"overflow-x-hidden",
|
||||
"overflow-y-auto",
|
||||
"pr-9",
|
||||
"flex-none flex-grow",
|
||||
"z-[2]",
|
||||
"scrollbar-none",
|
||||
"scrollbar-hide",
|
||||
)}
|
||||
onScroll={() => updateHighlights()}
|
||||
/>
|
||||
|
||||
<div className="absolute top-1 right-1 z-30">
|
||||
<StandardTooltip content={t("chat:enhancePrompt")}>
|
||||
<button
|
||||
aria-label={t("chat:enhancePrompt")}
|
||||
disabled={sendingDisabled}
|
||||
onClick={!sendingDisabled ? handleEnhancePrompt : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
|
||||
"transition-all duration-150",
|
||||
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
!sendingDisabled && "cursor-pointer",
|
||||
sendingDisabled &&
|
||||
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
|
||||
)}>
|
||||
<WandSparkles className={cn("w-4 h-4", isEnhancingPrompt && "animate-spin")} />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
|
||||
{!isEditMode && (
|
||||
<div className="absolute bottom-1 right-1 z-30">
|
||||
<StandardTooltip content={t("chat:sendMessage")}>
|
||||
<button
|
||||
aria-label={t("chat:sendMessage")}
|
||||
disabled={sendingDisabled}
|
||||
onClick={!sendingDisabled ? onSend : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
|
||||
"transition-all duration-150",
|
||||
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
!sendingDisabled && "cursor-pointer",
|
||||
sendingDisabled &&
|
||||
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
|
||||
)}>
|
||||
<SendHorizontal className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!inputValue && !isEditMode && (
|
||||
<div
|
||||
className="absolute left-2 z-30 pr-9 flex items-center h-8"
|
||||
style={{
|
||||
bottom: "0.25rem",
|
||||
color: "var(--vscode-tab-inactiveForeground)",
|
||||
userSelect: "none",
|
||||
pointerEvents: "none",
|
||||
}}>
|
||||
{placeholderBottomText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -804,12 +1182,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
"flex-col",
|
||||
"gap-1",
|
||||
"bg-editor-background",
|
||||
"px-1.5",
|
||||
isEditMode ? "px-0" : "px-1.5",
|
||||
"pb-1",
|
||||
"outline-none",
|
||||
"border",
|
||||
"border-none",
|
||||
"w-[calc(100%-16px)]",
|
||||
isEditMode ? "w-full" : "w-[calc(100%-16px)]",
|
||||
"ml-auto",
|
||||
"mr-auto",
|
||||
"box-border",
|
||||
|
|
@ -870,165 +1248,24 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"relative",
|
||||
"flex-1",
|
||||
"flex",
|
||||
"flex-col-reverse",
|
||||
"min-h-0",
|
||||
"overflow-hidden",
|
||||
"rounded",
|
||||
)}>
|
||||
<div
|
||||
ref={highlightLayerRef}
|
||||
className={cn(
|
||||
"absolute",
|
||||
"inset-0",
|
||||
"pointer-events-none",
|
||||
"whitespace-pre-wrap",
|
||||
"break-words",
|
||||
"text-transparent",
|
||||
"overflow-hidden",
|
||||
"font-vscode-font-family",
|
||||
"text-vscode-editor-font-size",
|
||||
"leading-vscode-editor-line-height",
|
||||
"py-2",
|
||||
"px-[9px]",
|
||||
"z-10",
|
||||
"forced-color-adjust-none",
|
||||
)}
|
||||
style={{
|
||||
color: "transparent",
|
||||
}}
|
||||
/>
|
||||
<DynamicTextArea
|
||||
ref={(el) => {
|
||||
if (typeof ref === "function") {
|
||||
ref(el)
|
||||
} else if (ref) {
|
||||
ref.current = el
|
||||
}
|
||||
textAreaRef.current = el
|
||||
}}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
handleInputChange(e)
|
||||
updateHighlights()
|
||||
}}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onBlur={handleBlur}
|
||||
onPaste={handlePaste}
|
||||
onSelect={updateCursorPosition}
|
||||
onMouseUp={updateCursorPosition}
|
||||
onHeightChange={(height) => {
|
||||
if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
|
||||
setTextAreaBaseHeight(height)
|
||||
}
|
||||
|
||||
onHeightChange?.(height)
|
||||
}}
|
||||
placeholder={placeholderText}
|
||||
minRows={3}
|
||||
maxRows={15}
|
||||
autoFocus={true}
|
||||
className={cn(
|
||||
"w-full",
|
||||
"text-vscode-input-foreground",
|
||||
"font-vscode-font-family",
|
||||
"text-vscode-editor-font-size",
|
||||
"leading-vscode-editor-line-height",
|
||||
"cursor-text",
|
||||
"py-1.5 px-2",
|
||||
isFocused
|
||||
? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
|
||||
: isDraggingOver
|
||||
? "border-2 border-dashed border-vscode-focusBorder"
|
||||
: "border border-transparent",
|
||||
isDraggingOver
|
||||
? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]"
|
||||
: "bg-vscode-input-background",
|
||||
"transition-background-color duration-150 ease-in-out",
|
||||
"will-change-background-color",
|
||||
"min-h-[90px]",
|
||||
"box-border",
|
||||
"rounded",
|
||||
"resize-none",
|
||||
"overflow-x-hidden",
|
||||
"overflow-y-auto",
|
||||
"pr-9",
|
||||
"flex-none flex-grow",
|
||||
"z-[2]",
|
||||
"scrollbar-none",
|
||||
"scrollbar-hide",
|
||||
)}
|
||||
onScroll={() => updateHighlights()}
|
||||
/>
|
||||
|
||||
<div className="absolute top-1 right-1 z-30">
|
||||
<StandardTooltip content={t("chat:enhancePrompt")}>
|
||||
<button
|
||||
aria-label={t("chat:enhancePrompt")}
|
||||
disabled={sendingDisabled}
|
||||
onClick={!sendingDisabled ? handleEnhancePrompt : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
|
||||
"transition-all duration-150",
|
||||
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
!sendingDisabled && "cursor-pointer",
|
||||
sendingDisabled &&
|
||||
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
|
||||
)}>
|
||||
<WandSparkles className={cn("w-4 h-4", isEnhancingPrompt && "animate-spin")} />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-1 right-1 z-30">
|
||||
<StandardTooltip content={t("chat:sendMessage")}>
|
||||
<button
|
||||
aria-label={t("chat:sendMessage")}
|
||||
disabled={sendingDisabled}
|
||||
onClick={!sendingDisabled ? onSend : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
|
||||
"transition-all duration-150",
|
||||
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
!sendingDisabled && "cursor-pointer",
|
||||
sendingDisabled &&
|
||||
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
|
||||
)}>
|
||||
<SendHorizontal className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
|
||||
{!inputValue && (
|
||||
<div
|
||||
className="absolute left-2 z-30 pr-9 flex items-center h-8"
|
||||
style={{
|
||||
bottom: "0.25rem",
|
||||
color: "var(--vscode-tab-inactiveForeground)",
|
||||
userSelect: "none",
|
||||
pointerEvents: "none",
|
||||
}}>
|
||||
{placeholderBottomText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{renderTextAreaSection()}
|
||||
</div>
|
||||
|
||||
{isEditMode && (
|
||||
<EditModeControls
|
||||
mode={mode}
|
||||
onModeChange={handleModeChange}
|
||||
modeShortcutText={modeShortcutText}
|
||||
customModes={customModes}
|
||||
customModePrompts={customModePrompts}
|
||||
onCancel={onCancel}
|
||||
onSend={onSend}
|
||||
onSelectImages={onSelectImages}
|
||||
sendingDisabled={sendingDisabled}
|
||||
shouldDisableImages={shouldDisableImages}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedImages.length > 0 && (
|
||||
|
|
@ -1043,186 +1280,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
/>
|
||||
)}
|
||||
|
||||
<div className={cn("flex", "justify-between", "items-center", "mt-auto")}>
|
||||
<div className={cn("flex", "items-center", "gap-1", "min-w-0")}>
|
||||
<div className="shrink-0">
|
||||
<ModeSelector
|
||||
value={mode}
|
||||
title={t("chat:selectMode")}
|
||||
onChange={(value) => {
|
||||
setMode(value)
|
||||
vscode.postMessage({ type: "mode", text: value })
|
||||
}}
|
||||
triggerClassName="w-full"
|
||||
modeShortcutText={modeShortcutText}
|
||||
customModes={customModes}
|
||||
customModePrompts={customModePrompts}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cn("flex-1", "min-w-0", "overflow-hidden")}>
|
||||
<SelectDropdown
|
||||
value={currentConfigId}
|
||||
disabled={selectApiConfigDisabled}
|
||||
title={t("chat:selectApiConfig")}
|
||||
disableSearch={false}
|
||||
placeholder={displayName}
|
||||
options={[
|
||||
// Pinned items first.
|
||||
...(listApiConfigMeta || [])
|
||||
.filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id])
|
||||
.map((config) => ({
|
||||
value: config.id,
|
||||
label: config.name,
|
||||
name: config.name, // Keep name for comparison with currentApiConfigName.
|
||||
type: DropdownOptionType.ITEM,
|
||||
pinned: true,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label)),
|
||||
// If we have pinned items and unpinned items, add a separator.
|
||||
...(pinnedApiConfigs &&
|
||||
Object.keys(pinnedApiConfigs).length > 0 &&
|
||||
(listApiConfigMeta || []).some((config) => !pinnedApiConfigs[config.id])
|
||||
? [
|
||||
{
|
||||
value: "sep-pinned",
|
||||
label: t("chat:separator"),
|
||||
type: DropdownOptionType.SEPARATOR,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Unpinned items sorted alphabetically.
|
||||
...(listApiConfigMeta || [])
|
||||
.filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id])
|
||||
.map((config) => ({
|
||||
value: config.id,
|
||||
label: config.name,
|
||||
name: config.name, // Keep name for comparison with currentApiConfigName.
|
||||
type: DropdownOptionType.ITEM,
|
||||
pinned: false,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label)),
|
||||
{
|
||||
value: "sep-2",
|
||||
label: t("chat:separator"),
|
||||
type: DropdownOptionType.SEPARATOR,
|
||||
},
|
||||
{
|
||||
value: "settingsButtonClicked",
|
||||
label: t("chat:edit"),
|
||||
type: DropdownOptionType.ACTION,
|
||||
},
|
||||
]}
|
||||
onChange={(value) => {
|
||||
if (value === "settingsButtonClicked") {
|
||||
vscode.postMessage({
|
||||
type: "loadApiConfiguration",
|
||||
text: value,
|
||||
values: { section: "providers" },
|
||||
})
|
||||
} else {
|
||||
vscode.postMessage({ type: "loadApiConfigurationById", text: value })
|
||||
}
|
||||
}}
|
||||
triggerClassName="w-full text-ellipsis overflow-hidden"
|
||||
itemClassName="group"
|
||||
renderItem={({ type, value, label, pinned }) => {
|
||||
if (type !== DropdownOptionType.ITEM) {
|
||||
return label
|
||||
}
|
||||
|
||||
const config = listApiConfigMeta?.find((c) => c.id === value)
|
||||
const isCurrentConfig = config?.name === currentApiConfigName
|
||||
|
||||
return (
|
||||
<div className="flex justify-between gap-2 w-full h-5">
|
||||
<div
|
||||
className={cn("truncate min-w-0 overflow-hidden", {
|
||||
"font-medium": isCurrentConfig,
|
||||
})}>
|
||||
{label}
|
||||
</div>
|
||||
<div className="flex justify-end w-10 flex-shrink-0">
|
||||
<div
|
||||
className={cn("size-5 p-1", {
|
||||
"block group-hover:hidden": !pinned,
|
||||
hidden: !isCurrentConfig,
|
||||
})}>
|
||||
<Check className="size-3" />
|
||||
</div>
|
||||
<StandardTooltip content={pinned ? t("chat:unpin") : t("chat:pin")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
togglePinnedApiConfig(value)
|
||||
vscode.postMessage({
|
||||
type: "toggleApiConfigPin",
|
||||
text: value,
|
||||
})
|
||||
}}
|
||||
className={cn("size-5", {
|
||||
"hidden group-hover:flex": !pinned,
|
||||
"bg-accent": pinned,
|
||||
})}>
|
||||
<Pin className="size-3 p-0.5 opacity-50" />
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn("flex", "items-center", "gap-0.5", "shrink-0")}>
|
||||
{isTtsPlaying && (
|
||||
<StandardTooltip content={t("chat:stopTts")}>
|
||||
<button
|
||||
aria-label={t("chat:stopTts")}
|
||||
onClick={() => vscode.postMessage({ type: "stopTts" })}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"text-vscode-foreground opacity-85",
|
||||
"transition-all duration-150",
|
||||
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
"cursor-pointer",
|
||||
)}>
|
||||
<VolumeX className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
<IndexingStatusBadge />
|
||||
<StandardTooltip content={t("chat:addImages")}>
|
||||
<button
|
||||
aria-label={t("chat:addImages")}
|
||||
disabled={shouldDisableImages}
|
||||
onClick={!shouldDisableImages ? onSelectImages : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"text-vscode-foreground opacity-85",
|
||||
"transition-all duration-150",
|
||||
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
!shouldDisableImages && "cursor-pointer",
|
||||
shouldDisableImages &&
|
||||
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
|
||||
"mr-1",
|
||||
)}>
|
||||
<Image className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
</div>
|
||||
{!isEditMode && renderNonEditModeControls()}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import useSound from "use-sound"
|
|||
import { LRUCache } from "lru-cache"
|
||||
|
||||
import { useDebounceEffect } from "@src/utils/useDebounceEffect"
|
||||
import { appendImages } from "@src/utils/imageUtils"
|
||||
|
||||
import type { ClineAsk, ClineMessage } from "@roo-code/types"
|
||||
|
||||
|
|
@ -38,6 +39,8 @@ import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
|
|||
import RooHero from "@src/components/welcome/RooHero"
|
||||
import RooTips from "@src/components/welcome/RooTips"
|
||||
import { StandardTooltip } from "@src/components/ui"
|
||||
import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
|
||||
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
|
||||
|
||||
import TelemetryBanner from "../common/TelemetryBanner"
|
||||
import VersionIndicator from "../common/VersionIndicator"
|
||||
|
|
@ -720,10 +723,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
}
|
||||
break
|
||||
case "selectedImages":
|
||||
const newImages = message.images ?? []
|
||||
if (newImages.length > 0) {
|
||||
// Only handle selectedImages if it's not for editing context
|
||||
// When context is "edit", ChatRow will handle the images
|
||||
if (message.context !== "edit") {
|
||||
setSelectedImages((prevImages) =>
|
||||
[...prevImages, ...newImages].slice(0, MAX_IMAGES_PER_MESSAGE),
|
||||
appendImages(prevImages, message.images, MAX_IMAGES_PER_MESSAGE),
|
||||
)
|
||||
}
|
||||
break
|
||||
|
|
@ -959,12 +963,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
[deniedCommands],
|
||||
)
|
||||
|
||||
// Create toggles object for useAutoApprovalState hook
|
||||
const autoApprovalToggles = useAutoApprovalToggles()
|
||||
|
||||
const { hasEnabledOptions } = useAutoApprovalState(autoApprovalToggles, autoApprovalEnabled)
|
||||
|
||||
const isAutoApproved = useCallback(
|
||||
(message: ClineMessage | undefined) => {
|
||||
// First check if auto-approval is enabled AND we have at least one permission
|
||||
if (!autoApprovalEnabled || !message || message.type !== "ask") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Use the hook's result instead of duplicating the logic
|
||||
if (!hasEnabledOptions) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (message.ask === "followup") {
|
||||
return alwaysAllowFollowupQuestions
|
||||
}
|
||||
|
|
@ -1038,6 +1053,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
},
|
||||
[
|
||||
autoApprovalEnabled,
|
||||
hasEnabledOptions,
|
||||
alwaysAllowBrowser,
|
||||
alwaysAllowReadOnly,
|
||||
alwaysAllowReadOnlyOutsideWorkspace,
|
||||
|
|
|
|||
115
webview-ui/src/components/chat/EditModeControls.tsx
Normal file
115
webview-ui/src/components/chat/EditModeControls.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import React from "react"
|
||||
import { Mode } from "@roo/modes"
|
||||
import { Button, StandardTooltip } from "@/components/ui"
|
||||
import { Image, SendHorizontal } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import ModeSelector from "./ModeSelector"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
|
||||
interface EditModeControlsProps {
|
||||
mode: Mode
|
||||
onModeChange: (value: Mode) => void
|
||||
modeShortcutText: string
|
||||
customModes: any
|
||||
customModePrompts: any
|
||||
onCancel?: () => void
|
||||
onSend: () => void
|
||||
onSelectImages: () => void
|
||||
sendingDisabled: boolean
|
||||
shouldDisableImages: boolean
|
||||
}
|
||||
|
||||
export const EditModeControls: React.FC<EditModeControlsProps> = ({
|
||||
mode,
|
||||
onModeChange,
|
||||
modeShortcutText,
|
||||
customModes,
|
||||
customModePrompts,
|
||||
onCancel,
|
||||
onSend,
|
||||
onSelectImages,
|
||||
sendingDisabled,
|
||||
shouldDisableImages,
|
||||
}) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"items-center",
|
||||
"justify-between",
|
||||
"absolute",
|
||||
"bottom-2",
|
||||
"left-2",
|
||||
"right-2",
|
||||
"z-30",
|
||||
)}>
|
||||
<div className={cn("flex", "items-center", "gap-1", "flex-1", "min-w-0")}>
|
||||
<div className="shrink-0">
|
||||
<ModeSelector
|
||||
value={mode}
|
||||
title={t("chat:selectMode")}
|
||||
onChange={onModeChange}
|
||||
triggerClassName="w-full"
|
||||
modeShortcutText={modeShortcutText}
|
||||
customModes={customModes}
|
||||
customModePrompts={customModePrompts}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn("flex", "items-center", "gap-0.5", "shrink-0", "ml-2")}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={onCancel}
|
||||
disabled={sendingDisabled}
|
||||
className="text-xs bg-vscode-toolbar-hoverBackground hover:bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground">
|
||||
Cancel
|
||||
</Button>
|
||||
<StandardTooltip content={t("chat:addImages")}>
|
||||
<button
|
||||
aria-label={t("chat:addImages")}
|
||||
disabled={shouldDisableImages}
|
||||
onClick={!shouldDisableImages ? onSelectImages : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
|
||||
"transition-all duration-150",
|
||||
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
!shouldDisableImages && "cursor-pointer",
|
||||
shouldDisableImages &&
|
||||
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
|
||||
)}>
|
||||
<Image className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
<StandardTooltip content={t("chat:save.tooltip")}>
|
||||
<button
|
||||
aria-label={t("chat:save.tooltip")}
|
||||
disabled={sendingDisabled}
|
||||
onClick={!sendingDisabled ? onSend : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
|
||||
"transition-all duration-150",
|
||||
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
!sendingDisabled && "cursor-pointer",
|
||||
sendingDisabled &&
|
||||
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
|
||||
)}>
|
||||
<SendHorizontal className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import React from "react"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@src/components/ui"
|
||||
|
||||
interface MessageModificationConfirmationDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onConfirm: () => void
|
||||
type: "edit" | "delete"
|
||||
}
|
||||
|
||||
export const MessageModificationConfirmationDialog: React.FC<MessageModificationConfirmationDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
type,
|
||||
}) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const isEdit = type === "edit"
|
||||
const title = isEdit ? t("common:confirmation.editMessage") : t("common:confirmation.deleteMessage")
|
||||
const description = isEdit ? t("common:confirmation.editWarning") : t("common:confirmation.deleteWarning")
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-lg">{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-base">{description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="flex-col gap-2">
|
||||
<AlertDialogCancel className="bg-vscode-button-secondaryBackground hover:bg-vscode-button-secondaryHoverBackground text-vscode-button-secondaryForeground border-vscode-button-border">
|
||||
{t("common:answers.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onConfirm}
|
||||
className="bg-vscode-button-background hover:bg-vscode-button-hoverBackground text-vscode-button-foreground border-vscode-button-border">
|
||||
{t("common:confirmation.proceed")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
// Export convenience components for backward compatibility
|
||||
export const EditMessageDialog: React.FC<Omit<MessageModificationConfirmationDialogProps, "type">> = (props) => (
|
||||
<MessageModificationConfirmationDialog {...props} type="edit" />
|
||||
)
|
||||
|
||||
export const DeleteMessageDialog: React.FC<Omit<MessageModificationConfirmationDialogProps, "type">> = (props) => (
|
||||
<MessageModificationConfirmationDialog {...props} type="delete" />
|
||||
)
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
import { render, fireEvent, screen, waitFor } from "@/utils/test-utils"
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import AutoApproveMenu from "../AutoApproveMenu"
|
||||
|
||||
// Mock vscode API
|
||||
vi.mock("@src/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock ExtensionStateContext
|
||||
vi.mock("@src/context/ExtensionStateContext")
|
||||
|
||||
// Mock translation hook
|
||||
vi.mock("@src/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"chat:autoApprove.title": "Auto-approve",
|
||||
"chat:autoApprove.none": "None selected",
|
||||
"chat:autoApprove.selectOptionsFirst": "Select at least one option below to enable auto-approval",
|
||||
"chat:autoApprove.description": "Configure auto-approval settings",
|
||||
"settings:autoApprove.readOnly.label": "Read-only operations",
|
||||
"settings:autoApprove.write.label": "Write operations",
|
||||
"settings:autoApprove.execute.label": "Execute operations",
|
||||
"settings:autoApprove.browser.label": "Browser operations",
|
||||
"settings:autoApprove.modeSwitch.label": "Mode switches",
|
||||
"settings:autoApprove.mcp.label": "MCP operations",
|
||||
"settings:autoApprove.subtasks.label": "Subtasks",
|
||||
"settings:autoApprove.resubmit.label": "Resubmit",
|
||||
"settings:autoApprove.followupQuestions.label": "Follow-up questions",
|
||||
"settings:autoApprove.updateTodoList.label": "Update todo list",
|
||||
"settings:autoApprove.apiRequestLimit.title": "API request limit",
|
||||
"settings:autoApprove.apiRequestLimit.unlimited": "Unlimited",
|
||||
"settings:autoApprove.apiRequestLimit.description": "Limit the number of API requests",
|
||||
"settings:autoApprove.readOnly.outsideWorkspace": "Also allow outside workspace",
|
||||
"settings:autoApprove.write.outsideWorkspace": "Also allow outside workspace",
|
||||
"settings:autoApprove.write.delay": "Delay",
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Get the mocked postMessage function
|
||||
const mockPostMessage = vscode.postMessage as ReturnType<typeof vi.fn>
|
||||
|
||||
describe("AutoApproveMenu", () => {
|
||||
const defaultExtensionState = {
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: false,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: false,
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowWriteOutsideWorkspace: false,
|
||||
alwaysAllowExecute: false,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowMcp: false,
|
||||
alwaysAllowModeSwitch: false,
|
||||
alwaysAllowSubtasks: false,
|
||||
alwaysApproveResubmit: false,
|
||||
alwaysAllowFollowupQuestions: false,
|
||||
alwaysAllowUpdateTodoList: false,
|
||||
writeDelayMs: 3000,
|
||||
allowedMaxRequests: undefined,
|
||||
setAutoApprovalEnabled: vi.fn(),
|
||||
setAlwaysAllowReadOnly: vi.fn(),
|
||||
setAlwaysAllowWrite: vi.fn(),
|
||||
setAlwaysAllowExecute: vi.fn(),
|
||||
setAlwaysAllowBrowser: vi.fn(),
|
||||
setAlwaysAllowMcp: vi.fn(),
|
||||
setAlwaysAllowModeSwitch: vi.fn(),
|
||||
setAlwaysAllowSubtasks: vi.fn(),
|
||||
setAlwaysApproveResubmit: vi.fn(),
|
||||
setAlwaysAllowFollowupQuestions: vi.fn(),
|
||||
setAlwaysAllowUpdateTodoList: vi.fn(),
|
||||
setAllowedMaxRequests: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue(defaultExtensionState)
|
||||
})
|
||||
|
||||
describe("Master checkbox behavior", () => {
|
||||
it("should show 'None selected' when no sub-options are selected", () => {
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
...defaultExtensionState,
|
||||
autoApprovalEnabled: false,
|
||||
alwaysAllowReadOnly: false,
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowExecute: false,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowModeSwitch: false,
|
||||
})
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Check that the text shows "None selected"
|
||||
expect(screen.getByText("None selected")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should show enabled options when sub-options are selected", () => {
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
...defaultExtensionState,
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: false,
|
||||
})
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Check that the text shows the enabled option
|
||||
expect(screen.getByText("Read-only operations")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should not allow toggling master checkbox when no options are selected", () => {
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
...defaultExtensionState,
|
||||
autoApprovalEnabled: false,
|
||||
alwaysAllowReadOnly: false,
|
||||
})
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Click on the master checkbox
|
||||
const masterCheckbox = screen.getByRole("checkbox")
|
||||
fireEvent.click(masterCheckbox)
|
||||
|
||||
// Should not send any message since no options are selected
|
||||
expect(mockPostMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should toggle master checkbox when options are selected", () => {
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
...defaultExtensionState,
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
})
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Click on the master checkbox
|
||||
const masterCheckbox = screen.getByRole("checkbox")
|
||||
fireEvent.click(masterCheckbox)
|
||||
|
||||
// Should toggle the master checkbox
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "autoApprovalEnabled",
|
||||
bool: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Sub-option toggles", () => {
|
||||
it("should toggle read-only operations", async () => {
|
||||
const mockSetAlwaysAllowReadOnly = vi.fn()
|
||||
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
...defaultExtensionState,
|
||||
setAlwaysAllowReadOnly: mockSetAlwaysAllowReadOnly,
|
||||
})
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand the menu
|
||||
const menuContainer = screen.getByText("Auto-approve").parentElement
|
||||
fireEvent.click(menuContainer!)
|
||||
|
||||
// Wait for the menu to expand and find the read-only button
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("always-allow-readonly-toggle")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const readOnlyButton = screen.getByTestId("always-allow-readonly-toggle")
|
||||
fireEvent.click(readOnlyButton)
|
||||
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "alwaysAllowReadOnly",
|
||||
bool: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should toggle write operations", async () => {
|
||||
const mockSetAlwaysAllowWrite = vi.fn()
|
||||
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
...defaultExtensionState,
|
||||
setAlwaysAllowWrite: mockSetAlwaysAllowWrite,
|
||||
})
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand the menu
|
||||
const menuContainer = screen.getByText("Auto-approve").parentElement
|
||||
fireEvent.click(menuContainer!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("always-allow-write-toggle")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const writeButton = screen.getByTestId("always-allow-write-toggle")
|
||||
fireEvent.click(writeButton)
|
||||
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "alwaysAllowWrite",
|
||||
bool: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Complex scenarios", () => {
|
||||
it("should display multiple enabled options in summary text", () => {
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
...defaultExtensionState,
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true,
|
||||
})
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Should show all enabled options in the summary
|
||||
expect(screen.getByText("Read-only operations, Write operations, Execute operations")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should handle enabling first option when none selected", async () => {
|
||||
const mockSetAutoApprovalEnabled = vi.fn()
|
||||
const mockSetAlwaysAllowReadOnly = vi.fn()
|
||||
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
...defaultExtensionState,
|
||||
autoApprovalEnabled: false,
|
||||
alwaysAllowReadOnly: false,
|
||||
setAutoApprovalEnabled: mockSetAutoApprovalEnabled,
|
||||
setAlwaysAllowReadOnly: mockSetAlwaysAllowReadOnly,
|
||||
})
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand the menu
|
||||
const menuContainer = screen.getByText("Auto-approve").parentElement
|
||||
fireEvent.click(menuContainer!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("always-allow-readonly-toggle")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Enable read-only
|
||||
const readOnlyButton = screen.getByTestId("always-allow-readonly-toggle")
|
||||
fireEvent.click(readOnlyButton)
|
||||
|
||||
// Should enable the sub-option
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "alwaysAllowReadOnly",
|
||||
bool: true,
|
||||
})
|
||||
|
||||
// Should also enable master auto-approval
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "autoApprovalEnabled",
|
||||
bool: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle disabling last option", async () => {
|
||||
const mockSetAutoApprovalEnabled = vi.fn()
|
||||
const mockSetAlwaysAllowReadOnly = vi.fn()
|
||||
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
...defaultExtensionState,
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
setAutoApprovalEnabled: mockSetAutoApprovalEnabled,
|
||||
setAlwaysAllowReadOnly: mockSetAlwaysAllowReadOnly,
|
||||
})
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand the menu
|
||||
const menuContainer = screen.getByText("Auto-approve").parentElement
|
||||
fireEvent.click(menuContainer!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("always-allow-readonly-toggle")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Disable read-only (the last enabled option)
|
||||
const readOnlyButton = screen.getByTestId("always-allow-readonly-toggle")
|
||||
fireEvent.click(readOnlyButton)
|
||||
|
||||
// Should disable the sub-option
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "alwaysAllowReadOnly",
|
||||
bool: false,
|
||||
})
|
||||
|
||||
// Should also disable master auto-approval
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "autoApprovalEnabled",
|
||||
bool: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -920,4 +920,54 @@ describe("ChatTextArea", () => {
|
|||
expect(apiConfigDropdown).toHaveAttribute("disabled")
|
||||
})
|
||||
})
|
||||
describe("edit mode integration", () => {
|
||||
it("should render edit mode UI when isEditMode is true", () => {
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
filePaths: [],
|
||||
openedTabs: [],
|
||||
taskHistory: [],
|
||||
cwd: "/test/workspace",
|
||||
customModes: [],
|
||||
customModePrompts: {},
|
||||
})
|
||||
|
||||
render(<ChatTextArea {...defaultProps} isEditMode={true} />)
|
||||
|
||||
// The edit mode UI should be rendered
|
||||
// We can verify this by checking for the presence of elements that are unique to edit mode
|
||||
const cancelButton = screen.getByRole("button", { name: /cancel/i })
|
||||
expect(cancelButton).toBeInTheDocument()
|
||||
|
||||
// Should show save button instead of send button
|
||||
const saveButton = screen.getByRole("button", { name: /save/i })
|
||||
expect(saveButton).toBeInTheDocument()
|
||||
|
||||
// Should not show send button in edit mode
|
||||
const sendButton = screen.queryByRole("button", { name: /send.*message/i })
|
||||
expect(sendButton).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should not render edit mode UI when isEditMode is false", () => {
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
filePaths: [],
|
||||
openedTabs: [],
|
||||
taskHistory: [],
|
||||
cwd: "/test/workspace",
|
||||
})
|
||||
|
||||
render(<ChatTextArea {...defaultProps} isEditMode={false} />)
|
||||
|
||||
// The edit mode UI should not be rendered
|
||||
const cancelButton = screen.queryByRole("button", { name: /cancel/i })
|
||||
expect(cancelButton).not.toBeInTheDocument()
|
||||
|
||||
// Should show send button when not in edit mode
|
||||
const sendButton = screen.getByRole("button", { name: /send.*message/i })
|
||||
expect(sendButton).toBeInTheDocument()
|
||||
|
||||
// Should not show save button when not in edit mode
|
||||
const saveButton = screen.queryByRole("button", { name: /save/i })
|
||||
expect(saveButton).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,480 @@
|
|||
// npx vitest run src/components/chat/__tests__/ChatView.auto-approve-new.spec.tsx
|
||||
|
||||
import { render, waitFor } from "@/utils/test-utils"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
|
||||
import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
|
||||
import ChatView, { ChatViewProps } from "../ChatView"
|
||||
|
||||
// Mock vscode API
|
||||
vi.mock("@src/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock all problematic dependencies
|
||||
vi.mock("rehype-highlight", () => ({
|
||||
default: () => () => {},
|
||||
}))
|
||||
|
||||
vi.mock("hast-util-to-text", () => ({
|
||||
default: () => "",
|
||||
}))
|
||||
|
||||
// Mock components that use ESM dependencies
|
||||
vi.mock("../BrowserSessionRow", () => ({
|
||||
default: function MockBrowserSessionRow({ messages }: { messages: any[] }) {
|
||||
return <div data-testid="browser-session">{JSON.stringify(messages)}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("../ChatRow", () => ({
|
||||
default: function MockChatRow({ message }: { message: any }) {
|
||||
return <div data-testid="chat-row">{JSON.stringify(message)}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("../TaskHeader", () => ({
|
||||
default: function MockTaskHeader({ task }: { task: any }) {
|
||||
return <div data-testid="task-header">{JSON.stringify(task)}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("../AutoApproveMenu", () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
vi.mock("@src/components/common/CodeBlock", () => ({
|
||||
default: () => null,
|
||||
CODE_BLOCK_BG_COLOR: "rgb(30, 30, 30)",
|
||||
}))
|
||||
|
||||
vi.mock("@src/components/common/CodeAccordion", () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
vi.mock("@src/components/chat/ContextMenu", () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
// Mock window.postMessage to trigger state hydration
|
||||
const mockPostMessage = (state: any) => {
|
||||
window.postMessage(
|
||||
{
|
||||
type: "state",
|
||||
state: {
|
||||
version: "1.0.0",
|
||||
clineMessages: [],
|
||||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
allowedCommands: [],
|
||||
alwaysAllowExecute: false,
|
||||
autoApprovalEnabled: true,
|
||||
...state,
|
||||
},
|
||||
},
|
||||
"*",
|
||||
)
|
||||
}
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const defaultProps: ChatViewProps = {
|
||||
isHidden: false,
|
||||
showAnnouncement: false,
|
||||
hideAnnouncement: () => {},
|
||||
}
|
||||
|
||||
const renderChatView = (props: Partial<ChatViewProps> = {}) => {
|
||||
return render(
|
||||
<ExtensionStateContextProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChatView {...defaultProps} {...props} />
|
||||
</QueryClientProvider>
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe("ChatView - New Auto Approval Logic Tests", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("Master auto-approval with no sub-options enabled", () => {
|
||||
it("should NOT auto-approve when autoApprovalEnabled is true but no sub-options are enabled", async () => {
|
||||
renderChatView()
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true, // Master is enabled
|
||||
alwaysAllowReadOnly: false, // But no sub-options are enabled
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowExecute: false,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowModeSwitch: false,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Then send a read tool ask message
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: false,
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowExecute: false,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowModeSwitch: false,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: "readFile", path: "test.txt" }),
|
||||
partial: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Wait and verify no auto-approval message was sent
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
})
|
||||
|
||||
it("should NOT auto-approve write operations when only master is enabled", async () => {
|
||||
renderChatView()
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true, // Master is enabled
|
||||
alwaysAllowReadOnly: false,
|
||||
alwaysAllowWrite: false, // Write is not enabled
|
||||
writeDelayMs: 0,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Then send a write tool ask message
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: false,
|
||||
alwaysAllowWrite: false,
|
||||
writeDelayMs: 0,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: "editedExistingFile", path: "test.txt" }),
|
||||
partial: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Wait and verify no auto-approval message was sent
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
})
|
||||
|
||||
it("should NOT auto-approve browser actions when only master is enabled", async () => {
|
||||
renderChatView()
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true, // Master is enabled
|
||||
alwaysAllowBrowser: false, // Browser is not enabled
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Then send a browser action ask message
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowBrowser: false,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "browser_action_launch",
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ action: "launch", url: "http://example.com" }),
|
||||
partial: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Wait and verify no auto-approval message was sent
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Correct auto-approval with sub-options enabled", () => {
|
||||
it("should auto-approve when master and at least one sub-option are enabled", async () => {
|
||||
renderChatView()
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: true, // At least one sub-option is enabled
|
||||
alwaysAllowWrite: false,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Then send a read tool ask message
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: false,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: "readFile", path: "test.txt" }),
|
||||
partial: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should auto-approve when multiple sub-options are enabled", async () => {
|
||||
renderChatView()
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: true, // Multiple sub-options enabled
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true,
|
||||
writeDelayMs: 0,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Then send a write tool ask message
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true,
|
||||
writeDelayMs: 0,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: "editedExistingFile", path: "test.txt" }),
|
||||
partial: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge cases", () => {
|
||||
it("should handle state transitions correctly", async () => {
|
||||
renderChatView()
|
||||
|
||||
// Start with auto-approval properly configured
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Then transition to a state where no sub-options are enabled
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true, // Master still true
|
||||
alwaysAllowReadOnly: false, // All sub-options now false
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowExecute: false,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowModeSwitch: false,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: "readFile", path: "test.txt" }),
|
||||
partial: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Wait and verify no auto-approval message was sent
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
})
|
||||
|
||||
it("should respect the hasEnabledOptions check in isAutoApproved", async () => {
|
||||
renderChatView()
|
||||
|
||||
// Configure state where master is true but effective approval should be false
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: false,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: false,
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowWriteOutsideWorkspace: false,
|
||||
alwaysAllowExecute: false,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowModeSwitch: false,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Try various tool types - none should auto-approve
|
||||
const toolRequests = [
|
||||
{ tool: "readFile", path: "test.txt" },
|
||||
{ tool: "editedExistingFile", path: "test.txt" },
|
||||
{ tool: "executeCommand", command: "ls" },
|
||||
{ tool: "switchMode", mode: "architect" },
|
||||
]
|
||||
|
||||
for (const toolRequest of toolRequests) {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: false,
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowExecute: false,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowModeSwitch: false,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify(toolRequest),
|
||||
partial: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Wait and verify no auto-approval for any tool type
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import React from "react"
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { EditModeControls } from "../EditModeControls"
|
||||
import { Mode } from "@roo/modes"
|
||||
|
||||
// Mock the translation hook
|
||||
vi.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock the UI components
|
||||
vi.mock("@/components/ui", () => ({
|
||||
Button: ({ children, onClick, disabled, ...props }: any) => (
|
||||
<button onClick={onClick} disabled={disabled} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
StandardTooltip: ({ children, content }: any) => <div title={content}>{children}</div>,
|
||||
}))
|
||||
|
||||
// Mock ModeSelector
|
||||
vi.mock("../ModeSelector", () => ({
|
||||
default: ({ value, onChange, title }: any) => (
|
||||
<select value={value} onChange={(e) => onChange(e.target.value)} title={title}>
|
||||
<option value="code">Code</option>
|
||||
<option value="architect">Architect</option>
|
||||
</select>
|
||||
),
|
||||
}))
|
||||
|
||||
describe("EditModeControls", () => {
|
||||
const defaultProps = {
|
||||
mode: "code" as Mode,
|
||||
onModeChange: vi.fn(),
|
||||
modeShortcutText: "Ctrl+M",
|
||||
customModes: [],
|
||||
customModePrompts: {},
|
||||
onCancel: vi.fn(),
|
||||
onSend: vi.fn(),
|
||||
onSelectImages: vi.fn(),
|
||||
sendingDisabled: false,
|
||||
shouldDisableImages: false,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders all controls correctly", () => {
|
||||
render(<EditModeControls {...defaultProps} />)
|
||||
|
||||
// Check for mode selector
|
||||
expect(screen.getByTitle("chat:selectMode")).toBeInTheDocument()
|
||||
|
||||
// Check for Cancel button
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument()
|
||||
|
||||
// Check for image button
|
||||
expect(screen.getByTitle("chat:addImages")).toBeInTheDocument()
|
||||
|
||||
// Check for send button
|
||||
expect(screen.getByTitle("chat:save.tooltip")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("calls onCancel when Cancel button is clicked", () => {
|
||||
render(<EditModeControls {...defaultProps} />)
|
||||
|
||||
const cancelButton = screen.getByText("Cancel")
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("calls onSend when send button is clicked", () => {
|
||||
render(<EditModeControls {...defaultProps} />)
|
||||
|
||||
const sendButton = screen.getByLabelText("chat:save.tooltip")
|
||||
fireEvent.click(sendButton)
|
||||
|
||||
expect(defaultProps.onSend).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("calls onSelectImages when image button is clicked", () => {
|
||||
render(<EditModeControls {...defaultProps} />)
|
||||
|
||||
const imageButton = screen.getByLabelText("chat:addImages")
|
||||
fireEvent.click(imageButton)
|
||||
|
||||
expect(defaultProps.onSelectImages).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("disables buttons when sendingDisabled is true", () => {
|
||||
render(<EditModeControls {...defaultProps} sendingDisabled={true} />)
|
||||
|
||||
const cancelButton = screen.getByText("Cancel")
|
||||
const sendButton = screen.getByLabelText("chat:save.tooltip")
|
||||
|
||||
expect(cancelButton).toBeDisabled()
|
||||
expect(sendButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it("disables image button when shouldDisableImages is true", () => {
|
||||
render(<EditModeControls {...defaultProps} shouldDisableImages={true} />)
|
||||
|
||||
const imageButton = screen.getByLabelText("chat:addImages")
|
||||
expect(imageButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it("does not call onSelectImages when image button is disabled", () => {
|
||||
render(<EditModeControls {...defaultProps} shouldDisableImages={true} />)
|
||||
|
||||
const imageButton = screen.getByLabelText("chat:addImages")
|
||||
fireEvent.click(imageButton)
|
||||
|
||||
expect(defaultProps.onSelectImages).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("does not call onSend when send button is disabled", () => {
|
||||
render(<EditModeControls {...defaultProps} sendingDisabled={true} />)
|
||||
|
||||
const sendButton = screen.getByLabelText("chat:save.tooltip")
|
||||
fireEvent.click(sendButton)
|
||||
|
||||
expect(defaultProps.onSend).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("calls onModeChange when mode is changed", () => {
|
||||
render(<EditModeControls {...defaultProps} />)
|
||||
|
||||
const modeSelector = screen.getByTitle("chat:selectMode")
|
||||
fireEvent.change(modeSelector, { target: { value: "architect" } })
|
||||
|
||||
expect(defaultProps.onModeChange).toHaveBeenCalledWith("architect")
|
||||
})
|
||||
})
|
||||
|
|
@ -110,6 +110,10 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
const [searchValue, setSearchValue] = useState("")
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Local state for mode name input to allow visual emptying
|
||||
const [localModeName, setLocalModeName] = useState<string>("")
|
||||
const [currentEditingModeSlug, setCurrentEditingModeSlug] = useState<string | null>(null)
|
||||
|
||||
// Direct update functions
|
||||
const updateAgentPrompt = useCallback(
|
||||
(mode: Mode, promptData: PromptComponent) => {
|
||||
|
|
@ -218,6 +222,14 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
}
|
||||
}, [getCurrentMode, checkRulesDirectory, hasRulesToExport])
|
||||
|
||||
// Reset local name state when mode changes
|
||||
useEffect(() => {
|
||||
if (currentEditingModeSlug && currentEditingModeSlug !== visualMode) {
|
||||
setCurrentEditingModeSlug(null)
|
||||
setLocalModeName("")
|
||||
}
|
||||
}, [visualMode, currentEditingModeSlug])
|
||||
|
||||
// Helper function to safely access mode properties
|
||||
const getModeProperty = <T extends keyof ModeConfig>(
|
||||
mode: ModeConfig | undefined,
|
||||
|
|
@ -725,16 +737,34 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={getModeProperty(findModeBySlug(visualMode, customModes), "name") ?? ""}
|
||||
onChange={(e) => {
|
||||
value={
|
||||
currentEditingModeSlug === visualMode
|
||||
? localModeName
|
||||
: (getModeProperty(findModeBySlug(visualMode, customModes), "name") ??
|
||||
"")
|
||||
}
|
||||
onFocus={() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
if (customMode) {
|
||||
setCurrentEditingModeSlug(visualMode)
|
||||
setLocalModeName(customMode.name)
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setLocalModeName(e.target.value)
|
||||
}}
|
||||
onBlur={() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
if (customMode && localModeName.trim()) {
|
||||
// Only update if the name is not empty
|
||||
updateCustomMode(visualMode, {
|
||||
...customMode,
|
||||
name: e.target.value,
|
||||
name: localModeName,
|
||||
source: customMode.source || "global",
|
||||
})
|
||||
}
|
||||
// Clear the editing state
|
||||
setCurrentEditingModeSlug(null)
|
||||
}}
|
||||
className="w-full"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@ import { X } from "lucide-react"
|
|||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { Button, Input, Slider } from "@/components/ui"
|
||||
import { Button, Input, Slider, StandardTooltip } from "@/components/ui"
|
||||
|
||||
import { SetCachedStateField } from "./types"
|
||||
import { SectionHeader } from "./SectionHeader"
|
||||
import { Section } from "./Section"
|
||||
import { AutoApproveToggle } from "./AutoApproveToggle"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { useAutoApprovalState } from "@/hooks/useAutoApprovalState"
|
||||
import { useAutoApprovalToggles } from "@/hooks/useAutoApprovalToggles"
|
||||
|
||||
type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
||||
alwaysAllowReadOnly?: boolean
|
||||
|
|
@ -77,6 +80,11 @@ export const AutoApproveSettings = ({
|
|||
const { t } = useAppTranslation()
|
||||
const [commandInput, setCommandInput] = useState("")
|
||||
const [deniedCommandInput, setDeniedCommandInput] = useState("")
|
||||
const { autoApprovalEnabled, setAutoApprovalEnabled } = useExtensionState()
|
||||
|
||||
const toggles = useAutoApprovalToggles()
|
||||
|
||||
const { hasEnabledOptions, effectiveAutoApprovalEnabled } = useAutoApprovalState(toggles, autoApprovalEnabled)
|
||||
|
||||
const handleAddCommand = () => {
|
||||
const currentCommands = allowedCommands ?? []
|
||||
|
|
@ -104,6 +112,30 @@ export const AutoApproveSettings = ({
|
|||
<div {...props}>
|
||||
<SectionHeader description={t("settings:autoApprove.description")}>
|
||||
<div className="flex items-center gap-2">
|
||||
{!hasEnabledOptions ? (
|
||||
<StandardTooltip content={t("settings:autoApprove.selectOptionsFirst")}>
|
||||
<VSCodeCheckbox
|
||||
checked={effectiveAutoApprovalEnabled}
|
||||
disabled={!hasEnabledOptions}
|
||||
aria-label={t("settings:autoApprove.disabledAriaLabel")}
|
||||
onChange={() => {
|
||||
// Do nothing when no options are enabled
|
||||
return
|
||||
}}
|
||||
/>
|
||||
</StandardTooltip>
|
||||
) : (
|
||||
<VSCodeCheckbox
|
||||
checked={effectiveAutoApprovalEnabled}
|
||||
disabled={!hasEnabledOptions}
|
||||
aria-label={t("settings:autoApprove.toggleAriaLabel")}
|
||||
onChange={() => {
|
||||
const newValue = !(autoApprovalEnabled ?? false)
|
||||
setAutoApprovalEnabled(newValue)
|
||||
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="codicon codicon-check w-4" />
|
||||
<div>{t("settings:sections.autoApprove")}</div>
|
||||
</div>
|
||||
|
|
|
|||
282
webview-ui/src/hooks/__tests__/useAutoApprovalState.spec.ts
Normal file
282
webview-ui/src/hooks/__tests__/useAutoApprovalState.spec.ts
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
import { renderHook } from "@testing-library/react"
|
||||
import { useAutoApprovalState } from "../useAutoApprovalState"
|
||||
|
||||
describe("useAutoApprovalState", () => {
|
||||
describe("hasEnabledOptions", () => {
|
||||
it("should return false when all toggles are false", () => {
|
||||
const toggles = {
|
||||
alwaysAllowReadOnly: false,
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowExecute: false,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowMcp: false,
|
||||
alwaysAllowModeSwitch: false,
|
||||
alwaysAllowSubtasks: false,
|
||||
alwaysApproveResubmit: false,
|
||||
alwaysAllowFollowupQuestions: false,
|
||||
alwaysAllowUpdateTodoList: false,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() => useAutoApprovalState(toggles, true))
|
||||
|
||||
expect(result.current.hasEnabledOptions).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false when all toggles are undefined", () => {
|
||||
const toggles = {
|
||||
alwaysAllowReadOnly: undefined,
|
||||
alwaysAllowWrite: undefined,
|
||||
alwaysAllowExecute: undefined,
|
||||
alwaysAllowBrowser: undefined,
|
||||
alwaysAllowMcp: undefined,
|
||||
alwaysAllowModeSwitch: undefined,
|
||||
alwaysAllowSubtasks: undefined,
|
||||
alwaysApproveResubmit: undefined,
|
||||
alwaysAllowFollowupQuestions: undefined,
|
||||
alwaysAllowUpdateTodoList: undefined,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() => useAutoApprovalState(toggles, true))
|
||||
|
||||
expect(result.current.hasEnabledOptions).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true when at least one toggle is true", () => {
|
||||
const toggles = {
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowExecute: false,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowMcp: false,
|
||||
alwaysAllowModeSwitch: false,
|
||||
alwaysAllowSubtasks: false,
|
||||
alwaysApproveResubmit: false,
|
||||
alwaysAllowFollowupQuestions: false,
|
||||
alwaysAllowUpdateTodoList: false,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() => useAutoApprovalState(toggles, true))
|
||||
|
||||
expect(result.current.hasEnabledOptions).toBe(true)
|
||||
})
|
||||
|
||||
it("should return true when multiple toggles are true", () => {
|
||||
const toggles = {
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowMcp: false,
|
||||
alwaysAllowModeSwitch: false,
|
||||
alwaysAllowSubtasks: false,
|
||||
alwaysApproveResubmit: false,
|
||||
alwaysAllowFollowupQuestions: false,
|
||||
alwaysAllowUpdateTodoList: false,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() => useAutoApprovalState(toggles, true))
|
||||
|
||||
expect(result.current.hasEnabledOptions).toBe(true)
|
||||
})
|
||||
|
||||
it("should return true when all toggles are true", () => {
|
||||
const toggles = {
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true,
|
||||
alwaysAllowBrowser: true,
|
||||
alwaysAllowMcp: true,
|
||||
alwaysAllowModeSwitch: true,
|
||||
alwaysAllowSubtasks: true,
|
||||
alwaysApproveResubmit: true,
|
||||
alwaysAllowFollowupQuestions: true,
|
||||
alwaysAllowUpdateTodoList: true,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() => useAutoApprovalState(toggles, true))
|
||||
|
||||
expect(result.current.hasEnabledOptions).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("effectiveAutoApprovalEnabled", () => {
|
||||
it("should return false when autoApprovalEnabled is false regardless of toggles", () => {
|
||||
const toggles = {
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() => useAutoApprovalState(toggles, false))
|
||||
|
||||
expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false when autoApprovalEnabled is undefined regardless of toggles", () => {
|
||||
const toggles = {
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() => useAutoApprovalState(toggles, undefined))
|
||||
|
||||
expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false when autoApprovalEnabled is true but no toggles are enabled", () => {
|
||||
const toggles = {
|
||||
alwaysAllowReadOnly: false,
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowExecute: false,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowMcp: false,
|
||||
alwaysAllowModeSwitch: false,
|
||||
alwaysAllowSubtasks: false,
|
||||
alwaysApproveResubmit: false,
|
||||
alwaysAllowFollowupQuestions: false,
|
||||
alwaysAllowUpdateTodoList: false,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() => useAutoApprovalState(toggles, true))
|
||||
|
||||
expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true when autoApprovalEnabled is true and at least one toggle is enabled", () => {
|
||||
const toggles = {
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowExecute: false,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() => useAutoApprovalState(toggles, true))
|
||||
|
||||
expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("memoization", () => {
|
||||
it("should not recompute hasEnabledOptions when toggles object reference changes but values are the same", () => {
|
||||
const initialToggles = {
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: false,
|
||||
}
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ toggles, autoApprovalEnabled }) => useAutoApprovalState(toggles, autoApprovalEnabled),
|
||||
{
|
||||
initialProps: {
|
||||
toggles: initialToggles,
|
||||
autoApprovalEnabled: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const firstHasEnabledOptions = result.current.hasEnabledOptions
|
||||
const firstEffectiveAutoApprovalEnabled = result.current.effectiveAutoApprovalEnabled
|
||||
|
||||
// Create new object with same values
|
||||
const newToggles = {
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: false,
|
||||
}
|
||||
|
||||
rerender({ toggles: newToggles, autoApprovalEnabled: true })
|
||||
|
||||
// The computed values should be the same due to memoization
|
||||
expect(result.current.hasEnabledOptions).toBe(firstHasEnabledOptions)
|
||||
expect(result.current.effectiveAutoApprovalEnabled).toBe(firstEffectiveAutoApprovalEnabled)
|
||||
})
|
||||
|
||||
it("should recompute when toggle values change", () => {
|
||||
const initialToggles = {
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: false,
|
||||
}
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ toggles, autoApprovalEnabled }) => useAutoApprovalState(toggles, autoApprovalEnabled),
|
||||
{
|
||||
initialProps: {
|
||||
toggles: initialToggles,
|
||||
autoApprovalEnabled: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(result.current.hasEnabledOptions).toBe(true)
|
||||
expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
|
||||
|
||||
// Change toggle values
|
||||
const newToggles = {
|
||||
alwaysAllowReadOnly: false,
|
||||
alwaysAllowWrite: false,
|
||||
}
|
||||
|
||||
rerender({ toggles: newToggles, autoApprovalEnabled: true })
|
||||
|
||||
expect(result.current.hasEnabledOptions).toBe(false)
|
||||
expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
|
||||
})
|
||||
|
||||
it("should recompute effectiveAutoApprovalEnabled when autoApprovalEnabled changes", () => {
|
||||
const toggles = {
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: false,
|
||||
}
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ toggles, autoApprovalEnabled }) => useAutoApprovalState(toggles, autoApprovalEnabled),
|
||||
{
|
||||
initialProps: {
|
||||
toggles,
|
||||
autoApprovalEnabled: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
|
||||
|
||||
rerender({ toggles, autoApprovalEnabled: false })
|
||||
|
||||
expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle partial toggle objects", () => {
|
||||
const toggles = {
|
||||
alwaysAllowReadOnly: true,
|
||||
// Other properties are optional
|
||||
}
|
||||
|
||||
const { result } = renderHook(() => useAutoApprovalState(toggles, true))
|
||||
|
||||
expect(result.current.hasEnabledOptions).toBe(true)
|
||||
expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle empty toggle object", () => {
|
||||
const toggles = {}
|
||||
|
||||
const { result } = renderHook(() => useAutoApprovalState(toggles, true))
|
||||
|
||||
expect(result.current.hasEnabledOptions).toBe(false)
|
||||
expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle mixed truthy/falsy values correctly", () => {
|
||||
const toggles = {
|
||||
alwaysAllowReadOnly: 1 as any, // truthy non-boolean
|
||||
alwaysAllowWrite: "" as any, // falsy non-boolean
|
||||
alwaysAllowExecute: null as any, // falsy non-boolean
|
||||
alwaysAllowBrowser: "yes" as any, // truthy non-boolean
|
||||
}
|
||||
|
||||
const { result } = renderHook(() => useAutoApprovalState(toggles, true))
|
||||
|
||||
expect(result.current.hasEnabledOptions).toBe(true) // Because some values are truthy
|
||||
})
|
||||
})
|
||||
})
|
||||
29
webview-ui/src/hooks/useAutoApprovalState.ts
Normal file
29
webview-ui/src/hooks/useAutoApprovalState.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { useMemo } from "react"
|
||||
|
||||
interface AutoApprovalToggles {
|
||||
alwaysAllowReadOnly?: boolean
|
||||
alwaysAllowWrite?: boolean
|
||||
alwaysAllowExecute?: boolean
|
||||
alwaysAllowBrowser?: boolean
|
||||
alwaysAllowMcp?: boolean
|
||||
alwaysAllowModeSwitch?: boolean
|
||||
alwaysAllowSubtasks?: boolean
|
||||
alwaysApproveResubmit?: boolean
|
||||
alwaysAllowFollowupQuestions?: boolean
|
||||
alwaysAllowUpdateTodoList?: boolean
|
||||
}
|
||||
|
||||
export function useAutoApprovalState(toggles: AutoApprovalToggles, autoApprovalEnabled?: boolean) {
|
||||
const hasEnabledOptions = useMemo(() => {
|
||||
return Object.values(toggles).some((value) => !!value)
|
||||
}, [toggles])
|
||||
|
||||
const effectiveAutoApprovalEnabled = useMemo(() => {
|
||||
return hasEnabledOptions && (autoApprovalEnabled ?? false)
|
||||
}, [hasEnabledOptions, autoApprovalEnabled])
|
||||
|
||||
return {
|
||||
hasEnabledOptions,
|
||||
effectiveAutoApprovalEnabled,
|
||||
}
|
||||
}
|
||||
50
webview-ui/src/hooks/useAutoApprovalToggles.ts
Normal file
50
webview-ui/src/hooks/useAutoApprovalToggles.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { useMemo } from "react"
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
|
||||
/**
|
||||
* Custom hook that creates and returns the auto-approval toggles object
|
||||
* This encapsulates the logic for creating the toggles object from extension state
|
||||
*/
|
||||
export function useAutoApprovalToggles() {
|
||||
const {
|
||||
alwaysAllowReadOnly,
|
||||
alwaysAllowWrite,
|
||||
alwaysAllowExecute,
|
||||
alwaysAllowBrowser,
|
||||
alwaysAllowMcp,
|
||||
alwaysAllowModeSwitch,
|
||||
alwaysAllowSubtasks,
|
||||
alwaysApproveResubmit,
|
||||
alwaysAllowFollowupQuestions,
|
||||
alwaysAllowUpdateTodoList,
|
||||
} = useExtensionState()
|
||||
|
||||
const toggles = useMemo(
|
||||
() => ({
|
||||
alwaysAllowReadOnly,
|
||||
alwaysAllowWrite,
|
||||
alwaysAllowExecute,
|
||||
alwaysAllowBrowser,
|
||||
alwaysAllowMcp,
|
||||
alwaysAllowModeSwitch,
|
||||
alwaysAllowSubtasks,
|
||||
alwaysApproveResubmit,
|
||||
alwaysAllowFollowupQuestions,
|
||||
alwaysAllowUpdateTodoList,
|
||||
}),
|
||||
[
|
||||
alwaysAllowReadOnly,
|
||||
alwaysAllowWrite,
|
||||
alwaysAllowExecute,
|
||||
alwaysAllowBrowser,
|
||||
alwaysAllowMcp,
|
||||
alwaysAllowModeSwitch,
|
||||
alwaysAllowSubtasks,
|
||||
alwaysApproveResubmit,
|
||||
alwaysAllowFollowupQuestions,
|
||||
alwaysAllowUpdateTodoList,
|
||||
],
|
||||
)
|
||||
|
||||
return toggles
|
||||
}
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Desar",
|
||||
"tooltip": "Desa els canvis del fitxer"
|
||||
"tooltip": "Desa els canvis del missatge"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Rebutjar",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "Aprovació automàtica:",
|
||||
"none": "Cap",
|
||||
"description": "L'aprovació automàtica permet a Roo Code realitzar accions sense demanar permís. Activa-la només per a accions en les que confies plenament. Configuració més detallada disponible a la <settingsLink>Configuració</settingsLink>."
|
||||
"description": "L'aprovació automàtica permet a Roo Code realitzar accions sense demanar permís. Activa-la només per a accions en les que confies plenament. Configuració més detallada disponible a la <settingsLink>Configuració</settingsLink>.",
|
||||
"selectOptionsFirst": "Selecciona almenys una opció a continuació per activar l'aprovació automàtica",
|
||||
"toggleAriaLabel": "Commuta l'aprovació automàtica",
|
||||
"disabledAriaLabel": "Aprovació automàtica desactivada: seleccioneu primer les opcions"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "Pensant",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "Versió {{version}} - Feu clic per veure les notes de llançament"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Edita el teu missatge..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "URI de dades de la imatge copiada al porta-retalls"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Eliminar missatge",
|
||||
"deleteWarning": "Eliminar aquest missatge eliminarà tots els missatges posteriors de la conversa. Vols continuar?",
|
||||
"editMessage": "Editar missatge",
|
||||
"editWarning": "Editar aquest missatge eliminarà tots els missatges posteriors de la conversa. Vols continuar?",
|
||||
"proceed": "Continuar"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Permet que Roo realitzi operacions automàticament sense requerir aprovació. Activeu aquesta configuració només si confieu plenament en la IA i enteneu els riscos de seguretat associats.",
|
||||
"toggleAriaLabel": "Commuta l'aprovació automàtica",
|
||||
"disabledAriaLabel": "Aprovació automàtica desactivada: seleccioneu primer les opcions",
|
||||
"readOnly": {
|
||||
"label": "Llegir",
|
||||
"description": "Quan està activat, Roo veurà automàticament el contingut del directori i llegirà fitxers sense que calgui fer clic al botó Aprovar.",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "Màximes Sol·licituds",
|
||||
"description": "Fes aquesta quantitat de sol·licituds API automàticament abans de demanar aprovació per continuar amb la tasca.",
|
||||
"unlimited": "Il·limitat"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "Seleccioneu almenys una opció a continuació per activar l'aprovació automàtica"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "Documentació de {{provider}}",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Speichern",
|
||||
"tooltip": "Dateiänderungen speichern"
|
||||
"tooltip": "Nachrichtenänderungen speichern"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Ablehnen",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "Automatische Genehmigung:",
|
||||
"none": "Keine",
|
||||
"description": "Automatische Genehmigung erlaubt Roo Code, Aktionen ohne Nachfrage auszuführen. Aktiviere dies nur für Aktionen, denen du vollständig vertraust. Detailliertere Konfiguration verfügbar in den <settingsLink>Einstellungen</settingsLink>."
|
||||
"description": "Automatische Genehmigung erlaubt Roo Code, Aktionen ohne Nachfrage auszuführen. Aktiviere dies nur für Aktionen, denen du vollständig vertraust. Detailliertere Konfiguration verfügbar in den <settingsLink>Einstellungen</settingsLink>.",
|
||||
"selectOptionsFirst": "Wähle mindestens eine der folgenden Optionen aus, um die automatische Genehmigung zu aktivieren",
|
||||
"toggleAriaLabel": "Automatische Genehmigung umschalten",
|
||||
"disabledAriaLabel": "Automatische Genehmigung deaktiviert - zuerst Optionen auswählen"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "Denke nach",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "Version {{version}} - Klicken Sie, um die Versionshinweise anzuzeigen"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Bearbeite deine Nachricht..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "Bild-Daten-URI in die Zwischenablage kopiert"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Nachricht löschen",
|
||||
"deleteWarning": "Das Löschen dieser Nachricht wird alle nachfolgenden Nachrichten in der Unterhaltung löschen. Möchtest du fortfahren?",
|
||||
"editMessage": "Nachricht bearbeiten",
|
||||
"editWarning": "Das Bearbeiten dieser Nachricht wird alle nachfolgenden Nachrichten in der Unterhaltung löschen. Möchtest du fortfahren?",
|
||||
"proceed": "Fortfahren"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Erlaubt Roo, Operationen automatisch ohne Genehmigung durchzuführen. Aktiviere diese Einstellungen nur, wenn du der KI vollständig vertraust und die damit verbundenen Sicherheitsrisiken verstehst.",
|
||||
"toggleAriaLabel": "Automatische Genehmigung umschalten",
|
||||
"disabledAriaLabel": "Automatische Genehmigung deaktiviert - zuerst Optionen auswählen",
|
||||
"readOnly": {
|
||||
"label": "Lesen",
|
||||
"description": "Wenn aktiviert, wird Roo automatisch Verzeichnisinhalte anzeigen und Dateien lesen, ohne dass du auf die Genehmigen-Schaltfläche klicken musst.",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "Maximale Anfragen",
|
||||
"description": "Automatisch so viele API-Anfragen stellen, bevor du um die Erlaubnis gebeten wirst, mit der Aufgabe fortzufahren.",
|
||||
"unlimited": "Unbegrenzt"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "Wähle mindestens eine Option unten aus, um die automatische Genehmigung zu aktivieren"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "{{provider}}-Dokumentation",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Save",
|
||||
"tooltip": "Save the file changes"
|
||||
"tooltip": "Save the message changes"
|
||||
},
|
||||
"tokenProgress": {
|
||||
"availableSpace": "Available space: {{amount}} tokens",
|
||||
|
|
@ -87,6 +87,9 @@
|
|||
"title": "Cancel",
|
||||
"tooltip": "Cancel the current operation"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Edit your message..."
|
||||
},
|
||||
"scrollToBottom": "Scroll to bottom of chat",
|
||||
"about": "Generate, refactor, and debug code with AI assistance. Check out our <DocsLink>documentation</DocsLink> to learn more.",
|
||||
"onboarding": "Your task list in this workspace is empty.",
|
||||
|
|
@ -244,7 +247,10 @@
|
|||
"autoApprove": {
|
||||
"title": "Auto-approve:",
|
||||
"none": "None",
|
||||
"description": "Auto-approve allows Roo Code to perform actions without asking for permission. Only enable for actions you fully trust. More detailed configuration available in <settingsLink>Settings</settingsLink>."
|
||||
"description": "Auto-approve allows Roo Code to perform actions without asking for permission. Only enable for actions you fully trust. More detailed configuration available in <settingsLink>Settings</settingsLink>.",
|
||||
"selectOptionsFirst": "Select at least one option below to enable auto-approval",
|
||||
"toggleAriaLabel": "Toggle auto-approval",
|
||||
"disabledAriaLabel": "Auto-approval disabled - select options first"
|
||||
},
|
||||
"announcement": {
|
||||
"title": "🎉 Roo Code {{version}} Released",
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "Image data URI copied to clipboard"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Delete Message",
|
||||
"deleteWarning": "Deleting this message will delete all subsequent messages in the conversation. Do you want to proceed?",
|
||||
"editMessage": "Edit Message",
|
||||
"editWarning": "Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?",
|
||||
"proceed": "Proceed"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,7 +190,10 @@
|
|||
"title": "Max Requests",
|
||||
"description": "Automatically make this many API requests before asking for approval to continue with the task.",
|
||||
"unlimited": "Unlimited"
|
||||
}
|
||||
},
|
||||
"toggleAriaLabel": "Toggle auto-approval",
|
||||
"disabledAriaLabel": "Auto-approval disabled - select options first",
|
||||
"selectOptionsFirst": "Select at least one option below to enable auto-approval"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "{{provider}} documentation",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Guardar",
|
||||
"tooltip": "Guardar los cambios del archivo"
|
||||
"tooltip": "Guardar los cambios del mensaje"
|
||||
},
|
||||
"tokenProgress": {
|
||||
"availableSpace": "Espacio disponible: {{amount}} tokens",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "Auto-aprobar:",
|
||||
"none": "Ninguno",
|
||||
"description": "Auto-aprobar permite a Roo Code realizar acciones sin pedir permiso. Habilita solo para acciones en las que confíes plenamente. Configuración más detallada disponible en <settingsLink>Configuración</settingsLink>."
|
||||
"description": "Auto-aprobar permite a Roo Code realizar acciones sin pedir permiso. Habilita solo para acciones en las que confíes plenamente. Configuración más detallada disponible en <settingsLink>Configuración</settingsLink>.",
|
||||
"selectOptionsFirst": "Selecciona al menos una opción a continuación para habilitar la aprobación automática",
|
||||
"toggleAriaLabel": "Alternar aprobación automática",
|
||||
"disabledAriaLabel": "Aprobación automática desactivada: seleccione primero las opciones"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "Pensando",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "Versión {{version}} - Haz clic para ver las notas de la versión"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Edita tu mensaje..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "URI de datos de imagen copiada al portapapeles"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Eliminar mensaje",
|
||||
"deleteWarning": "Eliminar este mensaje eliminará todos los mensajes posteriores en la conversación. ¿Deseas continuar?",
|
||||
"editMessage": "Editar mensaje",
|
||||
"editWarning": "Editar este mensaje eliminará todos los mensajes posteriores en la conversación. ¿Deseas continuar?",
|
||||
"proceed": "Continuar"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Permitir que Roo realice operaciones automáticamente sin requerir aprobación. Habilite esta configuración solo si confía plenamente en la IA y comprende los riesgos de seguridad asociados.",
|
||||
"toggleAriaLabel": "Alternar aprobación automática",
|
||||
"disabledAriaLabel": "Aprobación automática desactivada: seleccione primero las opciones",
|
||||
"readOnly": {
|
||||
"label": "Lectura",
|
||||
"description": "Cuando está habilitado, Roo verá automáticamente el contenido del directorio y leerá archivos sin que necesite hacer clic en el botón Aprobar.",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "Solicitudes máximas",
|
||||
"description": "Realizar automáticamente esta cantidad de solicitudes a la API antes de pedir aprobación para continuar con la tarea.",
|
||||
"unlimited": "Ilimitado"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "Selecciona al menos una opción a continuación para habilitar la aprobación automática"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "Documentación de {{provider}}",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Enregistrer",
|
||||
"tooltip": "Sauvegarder les modifications du fichier"
|
||||
"tooltip": "Enregistrer les modifications du message"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Rejeter",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "Auto-approbation :",
|
||||
"none": "Aucune",
|
||||
"description": "L'auto-approbation permet à Roo Code d'effectuer des actions sans demander d'autorisation. Activez-la uniquement pour les actions auxquelles vous faites entièrement confiance. Configuration plus détaillée disponible dans les <settingsLink>Paramètres</settingsLink>."
|
||||
"description": "L'auto-approbation permet à Roo Code d'effectuer des actions sans demander d'autorisation. Activez-la uniquement pour les actions auxquelles vous faites entièrement confiance. Configuration plus détaillée disponible dans les <settingsLink>Paramètres</settingsLink>.",
|
||||
"selectOptionsFirst": "Sélectionnez au moins une option ci-dessous pour activer l'auto-approbation",
|
||||
"toggleAriaLabel": "Activer/désactiver l'approbation automatique",
|
||||
"disabledAriaLabel": "Approbation automatique désactivée - sélectionnez d'abord les options"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "Réflexion",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "Version {{version}} - Cliquez pour voir les notes de version"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Modifiez votre message..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "URI de données d'image copiée dans le presse-papiers"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Supprimer le message",
|
||||
"deleteWarning": "Supprimer ce message supprimera tous les messages suivants dans la conversation. Voulez-vous continuer ?",
|
||||
"editMessage": "Modifier le message",
|
||||
"editWarning": "Modifier ce message supprimera tous les messages suivants dans la conversation. Voulez-vous continuer ?",
|
||||
"proceed": "Continuer"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,9 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Permettre à Roo d'effectuer automatiquement des opérations sans requérir d'approbation. Activez ces paramètres uniquement si vous faites entièrement confiance à l'IA et que vous comprenez les risques de sécurité associés.",
|
||||
"toggleAriaLabel": "Activer/désactiver l'approbation automatique",
|
||||
"disabledAriaLabel": "Approbation automatique désactivée - sélectionnez d'abord les options",
|
||||
"selectOptionsFirst": "Sélectionnez au moins une option ci-dessous pour activer l'approbation automatique",
|
||||
"readOnly": {
|
||||
"label": "Lecture",
|
||||
"description": "Lorsque cette option est activée, Roo affichera automatiquement le contenu des répertoires et lira les fichiers sans que vous ayez à cliquer sur le bouton Approuver.",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "सहेजें",
|
||||
"tooltip": "फ़ाइल परिवर्तन सहेजें"
|
||||
"tooltip": "संदेश के बदलाव सहेजें"
|
||||
},
|
||||
"reject": {
|
||||
"title": "अस्वीकार करें",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "स्वत:-स्वीकृति:",
|
||||
"none": "कोई नहीं",
|
||||
"description": "स्वत:-स्वीकृति Roo Code को अनुमति मांगे बिना क्रियाएँ करने की अनुमति देती है। केवल उन क्रियाओं के लिए सक्षम करें जिन पर आप पूरी तरह से विश्वास करते हैं। अधिक विस्तृत कॉन्फ़िगरेशन <settingsLink>सेटिंग्स</settingsLink> में उपलब्ध है।"
|
||||
"description": "स्वत:-स्वीकृति Roo Code को अनुमति मांगे बिना क्रियाएँ करने की अनुमति देती है। केवल उन क्रियाओं के लिए सक्षम करें जिन पर आप पूरी तरह से विश्वास करते हैं। अधिक विस्तृत कॉन्फ़िगरेशन <settingsLink>सेटिंग्स</settingsLink> में उपलब्ध है।",
|
||||
"selectOptionsFirst": "स्वतः-अनुमोदन सक्षम करने के लिए नीचे दिए گئے विकल्पों में से कम से कम एक का चयन करें",
|
||||
"toggleAriaLabel": "स्वतः-अनुमोदन टॉगल करें",
|
||||
"disabledAriaLabel": "स्वतः-अनुमोदन अक्षम - पहले विकल्प चुनें"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "विचार कर रहा है",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "संस्करण {{version}} - रिलीज़ नोट्स देखने के लिए क्लिक करें"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "अपना संदेश संपादित करें..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "इमेज डेटा URI क्लिपबोर्ड में कॉपी हो गया"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "संदेश हटाएं",
|
||||
"deleteWarning": "इस संदेश को हटाने से बातचीत के सभी बाद के संदेश हट जाएंगे। क्या आप जारी रखना चाहते हैं?",
|
||||
"editMessage": "संदेश संपादित करें",
|
||||
"editWarning": "इस संदेश को संपादित करने से बातचीत के सभी बाद के संदेश हट जाएंगे। क्या आप जारी रखना चाहते हैं?",
|
||||
"proceed": "जारी रखें"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Roo को अनुमोदन की आवश्यकता के बिना स्वचालित रूप से ऑपरेशन करने की अनुमति दें। इन सेटिंग्स को केवल तभी सक्षम करें जब आप AI पर पूरी तरह से भरोसा करते हों और संबंधित सुरक्षा जोखिमों को समझते हों।",
|
||||
"toggleAriaLabel": "स्वतः-अनुमोदन टॉगल करें",
|
||||
"disabledAriaLabel": "स्वतः-अनुमोदन अक्षम - पहले विकल्प चुनें",
|
||||
"readOnly": {
|
||||
"label": "पढ़ें",
|
||||
"description": "जब सक्षम होता है, तो Roo आपके अनुमोदित बटन पर क्लिक किए बिना स्वचालित रूप से निर्देशिका सामग्री देखेगा और फाइलें पढ़ेगा।",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "अधिकतम अनुरोध",
|
||||
"description": "कार्य जारी रखने के लिए अनुमति मांगने से पहले स्वचालित रूप से इतने API अनुरोध करें।",
|
||||
"unlimited": "असीमित"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "स्वतः-अनुमोदन सक्षम करने के लिए नीचे से कम से कम एक विकल्प चुनें"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "{{provider}} दस्तावेज़ीकरण",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Simpan",
|
||||
"tooltip": "Simpan perubahan file"
|
||||
"tooltip": "Simpan perubahan pesan"
|
||||
},
|
||||
"tokenProgress": {
|
||||
"availableSpace": "Ruang tersedia: {{amount}} token",
|
||||
|
|
@ -250,7 +250,10 @@
|
|||
"autoApprove": {
|
||||
"title": "Auto-approve:",
|
||||
"none": "Tidak Ada",
|
||||
"description": "Auto-approve memungkinkan Roo Code melakukan aksi tanpa meminta izin. Hanya aktifkan untuk aksi yang benar-benar kamu percayai. Konfigurasi lebih detail tersedia di <settingsLink>Pengaturan</settingsLink>."
|
||||
"description": "Auto-approve memungkinkan Roo Code melakukan aksi tanpa meminta izin. Hanya aktifkan untuk aksi yang benar-benar kamu percayai. Konfigurasi lebih detail tersedia di <settingsLink>Pengaturan</settingsLink>.",
|
||||
"selectOptionsFirst": "Pilih setidaknya satu opsi di bawah untuk mengaktifkan persetujuan otomatis",
|
||||
"toggleAriaLabel": "Beralih persetujuan otomatis",
|
||||
"disabledAriaLabel": "Persetujuan otomatis dinonaktifkan - pilih opsi terlebih dahulu"
|
||||
},
|
||||
"announcement": {
|
||||
"title": "🎉 Roo Code {{version}} Dirilis",
|
||||
|
|
@ -325,5 +328,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "Versi {{version}} - Klik untuk melihat catatan rilis"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Edit pesan Anda..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "Data URI gambar disalin ke clipboard"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Hapus Pesan",
|
||||
"deleteWarning": "Menghapus pesan ini akan menghapus semua pesan selanjutnya dalam percakapan. Apakah kamu ingin melanjutkan?",
|
||||
"editMessage": "Edit Pesan",
|
||||
"editWarning": "Mengedit pesan ini akan menghapus semua pesan selanjutnya dalam percakapan. Apakah kamu ingin melanjutkan?",
|
||||
"proceed": "Lanjutkan"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Izinkan Roo untuk secara otomatis melakukan operasi tanpa memerlukan persetujuan. Aktifkan pengaturan ini hanya jika kamu sepenuhnya mempercayai AI dan memahami risiko keamanan yang terkait.",
|
||||
"toggleAriaLabel": "Beralih persetujuan otomatis",
|
||||
"disabledAriaLabel": "Persetujuan otomatis dinonaktifkan - pilih opsi terlebih dahulu",
|
||||
"readOnly": {
|
||||
"label": "Baca",
|
||||
"description": "Ketika diaktifkan, Roo akan secara otomatis melihat konten direktori dan membaca file tanpa memerlukan kamu mengklik tombol Setujui.",
|
||||
|
|
@ -194,7 +196,8 @@
|
|||
"title": "Permintaan Maks",
|
||||
"description": "Secara otomatis membuat sejumlah permintaan API ini sebelum meminta persetujuan untuk melanjutkan tugas.",
|
||||
"unlimited": "Tidak terbatas"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "Pilih setidaknya satu opsi di bawah ini untuk mengaktifkan persetujuan otomatis"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "Dokumentasi {{provider}}",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Salva",
|
||||
"tooltip": "Salva le modifiche al file"
|
||||
"tooltip": "Salva le modifiche del messaggio"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Rifiuta",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "Auto-approvazione:",
|
||||
"none": "Nessuna",
|
||||
"description": "L'auto-approvazione permette a Roo Code di eseguire azioni senza chiedere permesso. Abilita solo per azioni di cui ti fidi completamente. Configurazione più dettagliata disponibile nelle <settingsLink>Impostazioni</settingsLink>."
|
||||
"description": "L'auto-approvazione permette a Roo Code di eseguire azioni senza chiedere permesso. Abilita solo per azioni di cui ti fidi completamente. Configurazione più dettagliata disponibile nelle <settingsLink>Impostazioni</settingsLink>.",
|
||||
"selectOptionsFirst": "Seleziona almeno un'opzione qui sotto per abilitare l'auto-approvazione",
|
||||
"toggleAriaLabel": "Attiva/disattiva approvazione automatica",
|
||||
"disabledAriaLabel": "Approvazione automatica disabilitata - seleziona prima le opzioni"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "Sto pensando",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "Versione {{version}} - Clicca per visualizzare le note di rilascio"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Modifica il tuo messaggio..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "URI dati immagine copiato negli appunti"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Elimina Messaggio",
|
||||
"deleteWarning": "Eliminando questo messaggio verranno eliminati tutti i messaggi successivi nella conversazione. Vuoi procedere?",
|
||||
"editMessage": "Modifica Messaggio",
|
||||
"editWarning": "Modificando questo messaggio verranno eliminati tutti i messaggi successivi nella conversazione. Vuoi procedere?",
|
||||
"proceed": "Procedi"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Permetti a Roo di eseguire automaticamente operazioni senza richiedere approvazione. Abilita queste impostazioni solo se ti fidi completamente dell'IA e comprendi i rischi di sicurezza associati.",
|
||||
"toggleAriaLabel": "Attiva/disattiva approvazione automatica",
|
||||
"disabledAriaLabel": "Approvazione automatica disabilitata - seleziona prima le opzioni",
|
||||
"readOnly": {
|
||||
"label": "Leggi",
|
||||
"description": "Quando abilitato, Roo visualizzerà automaticamente i contenuti della directory e leggerà i file senza richiedere di cliccare sul pulsante Approva.",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "Richieste massime",
|
||||
"description": "Esegui automaticamente questo numero di richieste API prima di chiedere l'approvazione per continuare con l'attività.",
|
||||
"unlimited": "Illimitato"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "Seleziona almeno un'opzione qui sotto per abilitare l'approvazione automatica"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "Documentazione {{provider}}",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "保存",
|
||||
"tooltip": "ファイル変更を保存"
|
||||
"tooltip": "メッセージの変更を保存"
|
||||
},
|
||||
"reject": {
|
||||
"title": "拒否",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "自動承認:",
|
||||
"none": "なし",
|
||||
"description": "自動承認はRoo Codeに許可を求めずに操作を実行する権限を与えます。完全に信頼できる操作のみ有効にしてください。より詳細な設定は<settingsLink>設定</settingsLink>で利用できます。"
|
||||
"description": "自動承認はRoo Codeに許可を求めずに操作を実行する権限を与えます。完全に信頼できる操作のみ有効にしてください。より詳細な設定は<settingsLink>設定</settingsLink>で利用できます。",
|
||||
"selectOptionsFirst": "自動承認を有効にするには、以下のオプションを少なくとも1つ選択してください",
|
||||
"toggleAriaLabel": "自動承認の切り替え",
|
||||
"disabledAriaLabel": "自動承認が無効です - 最初にオプションを選択してください"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "考え中",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "バージョン {{version}} - クリックしてリリースノートを表示"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "メッセージを編集..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "画像データURIをクリップボードにコピーしました"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "メッセージを削除",
|
||||
"deleteWarning": "このメッセージを削除すると、会話内の後続のメッセージもすべて削除されます。続行しますか?",
|
||||
"editMessage": "メッセージを編集",
|
||||
"editWarning": "このメッセージを編集すると、会話内の後続のメッセージもすべて削除されます。続行しますか?",
|
||||
"proceed": "続行"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Rooが承認なしで自動的に操作を実行できるようにします。AIを完全に信頼し、関連するセキュリティリスクを理解している場合にのみ、これらの設定を有効にしてください。",
|
||||
"toggleAriaLabel": "自動承認の切り替え",
|
||||
"disabledAriaLabel": "自動承認が無効です - 最初にオプションを選択してください",
|
||||
"readOnly": {
|
||||
"label": "読み取り",
|
||||
"description": "有効にすると、Rooは承認ボタンをクリックすることなく、自動的にディレクトリの内容を表示してファイルを読み取ります。",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "最大リクエスト数",
|
||||
"description": "タスクを続行するための承認を求める前に、自動的にこの数のAPIリクエストを行います。",
|
||||
"unlimited": "無制限"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "自動承認を有効にするには、以下のオプションを少なくとも1つ選択してください"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "{{provider}}のドキュメント",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "저장",
|
||||
"tooltip": "파일 변경사항 저장"
|
||||
"tooltip": "메시지 변경사항 저장"
|
||||
},
|
||||
"reject": {
|
||||
"title": "거부",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "자동 승인:",
|
||||
"none": "없음",
|
||||
"description": "자동 승인을 사용하면 Roo Code가 권한을 요청하지 않고 작업을 수행할 수 있습니다. 완전히 신뢰할 수 있는 작업에만 활성화하세요. 더 자세한 구성은 <settingsLink>설정</settingsLink>에서 사용할 수 있습니다."
|
||||
"description": "자동 승인을 사용하면 Roo Code가 권한을 요청하지 않고 작업을 수행할 수 있습니다. 완전히 신뢰할 수 있는 작업에만 활성화하세요. 더 자세한 구성은 <settingsLink>설정</settingsLink>에서 사용할 수 있습니다.",
|
||||
"selectOptionsFirst": "자동 승인을 활성화하려면 아래 옵션 중 하나 이상을 선택하세요",
|
||||
"toggleAriaLabel": "자동 승인 전환",
|
||||
"disabledAriaLabel": "자동 승인 비활성화됨 - 먼저 옵션을 선택하세요"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "생각 중",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "버전 {{version}} - 릴리스 노트를 보려면 클릭하세요"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "메시지 편집..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "이미지 데이터 URI가 클립보드에 복사됨"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "메시지 삭제",
|
||||
"deleteWarning": "이 메시지를 삭제하면 대화의 모든 후속 메시지가 삭제됩니다. 계속하시겠습니까?",
|
||||
"editMessage": "메시지 편집",
|
||||
"editWarning": "이 메시지를 편집하면 대화의 모든 후속 메시지가 삭제됩니다. 계속하시겠습니까?",
|
||||
"proceed": "계속"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Roo가 승인 없이 자동으로 작업을 수행할 수 있도록 허용합니다. AI를 완전히 신뢰하고 관련 보안 위험을 이해하는 경우에만 이러한 설정을 활성화하세요.",
|
||||
"toggleAriaLabel": "자동 승인 전환",
|
||||
"disabledAriaLabel": "자동 승인 비활성화됨 - 먼저 옵션을 선택하세요",
|
||||
"readOnly": {
|
||||
"label": "읽기",
|
||||
"description": "활성화되면 Roo는 승인 버튼을 클릭하지 않고도 자동으로 디렉토리 내용을 보고 파일을 읽습니다.",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "최대 요청 수",
|
||||
"description": "작업을 계속하기 위한 승인을 요청하기 전에 자동으로 이 수의 API 요청을 수행합니다.",
|
||||
"unlimited": "무제한"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "자동 승인을 활성화하려면 아래에서 하나 이상의 옵션을 선택하세요"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "{{provider}} 문서",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Opslaan",
|
||||
"tooltip": "Bestandswijzigingen opslaan"
|
||||
"tooltip": "Berichtwijzigingen opslaan"
|
||||
},
|
||||
"tokenProgress": {
|
||||
"availableSpace": "Beschikbare ruimte: {{amount}} tokens",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "Automatisch goedkeuren:",
|
||||
"none": "Geen",
|
||||
"description": "Met automatisch goedkeuren kan Roo Code acties uitvoeren zonder om toestemming te vragen. Schakel dit alleen in voor acties die je volledig vertrouwt. Meer gedetailleerde configuratie beschikbaar in de <settingsLink>Instellingen</settingsLink>."
|
||||
"description": "Met automatisch goedkeuren kan Roo Code acties uitvoeren zonder om toestemming te vragen. Schakel dit alleen in voor acties die je volledig vertrouwt. Meer gedetailleerde configuratie beschikbaar in de <settingsLink>Instellingen</settingsLink>.",
|
||||
"selectOptionsFirst": "Selecteer hieronder minstens één optie om automatische goedkeuring in te schakelen",
|
||||
"toggleAriaLabel": "Automatisch goedkeuren in-/uitschakelen",
|
||||
"disabledAriaLabel": "Automatisch goedkeuren uitgeschakeld - selecteer eerst opties"
|
||||
},
|
||||
"announcement": {
|
||||
"title": "🎉 Roo Code {{version}} uitgebracht",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "Versie {{version}} - Klik om release notes te bekijken"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Bewerk je bericht..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "Afbeelding data-URI gekopieerd naar klembord"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Bericht Verwijderen",
|
||||
"deleteWarning": "Het verwijderen van dit bericht zal alle volgende berichten in het gesprek verwijderen. Wil je doorgaan?",
|
||||
"editMessage": "Bericht Bewerken",
|
||||
"editWarning": "Het bewerken van dit bericht zal alle volgende berichten in het gesprek verwijderen. Wil je doorgaan?",
|
||||
"proceed": "Doorgaan"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Sta Roo toe om automatisch handelingen uit te voeren zonder goedkeuring. Schakel deze instellingen alleen in als je de AI volledig vertrouwt en de bijbehorende beveiligingsrisico's begrijpt.",
|
||||
"toggleAriaLabel": "Automatisch goedkeuren in-/uitschakelen",
|
||||
"disabledAriaLabel": "Automatisch goedkeuren uitgeschakeld - selecteer eerst opties",
|
||||
"readOnly": {
|
||||
"label": "Lezen",
|
||||
"description": "Indien ingeschakeld, bekijkt Roo automatisch de inhoud van mappen en leest bestanden zonder dat je op de Goedkeuren-knop hoeft te klikken.",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "Maximale verzoeken",
|
||||
"description": "Voer automatisch dit aantal API-verzoeken uit voordat om goedkeuring wordt gevraagd om door te gaan met de taak.",
|
||||
"unlimited": "Onbeperkt"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "Selecteer ten minste één optie hieronder om automatische goedkeuring in te schakelen"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "{{provider}} documentatie",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Zapisz",
|
||||
"tooltip": "Zapisz zmiany w pliku"
|
||||
"tooltip": "Zapisz zmiany wiadomości"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Odrzuć",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "Automatyczne zatwierdzanie:",
|
||||
"none": "Brak",
|
||||
"description": "Automatyczne zatwierdzanie pozwala Roo Code wykonywać działania bez pytania o pozwolenie. Włącz tylko dla działań, którym w pełni ufasz. Bardziej szczegółowa konfiguracja dostępna w <settingsLink>Ustawieniach</settingsLink>."
|
||||
"description": "Automatyczne zatwierdzanie pozwala Roo Code wykonywać działania bez pytania o pozwolenie. Włącz tylko dla działań, którym w pełni ufasz. Bardziej szczegółowa konfiguracja dostępna w <settingsLink>Ustawieniach</settingsLink>.",
|
||||
"selectOptionsFirst": "Wybierz co najmniej jedną opcję poniżej, aby włączyć automatyczne zatwierdzanie",
|
||||
"toggleAriaLabel": "Przełącz automatyczne zatwierdzanie",
|
||||
"disabledAriaLabel": "Automatyczne zatwierdzanie wyłączone - najpierw wybierz opcje"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "Myślenie",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "Wersja {{version}} - Kliknij, aby wyświetlić informacje o wydaniu"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Edytuj swoją wiadomość..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "URI danych obrazu skopiowane do schowka"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Usuń Wiadomość",
|
||||
"deleteWarning": "Usunięcie tej wiadomości spowoduje usunięcie wszystkich kolejnych wiadomości w rozmowie. Czy chcesz kontynuować?",
|
||||
"editMessage": "Edytuj Wiadomość",
|
||||
"editWarning": "Edycja tej wiadomości spowoduje usunięcie wszystkich kolejnych wiadomości w rozmowie. Czy chcesz kontynuować?",
|
||||
"proceed": "Kontynuuj"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Pozwól Roo na automatyczne wykonywanie operacji bez wymagania zatwierdzenia. Włącz te ustawienia tylko jeśli w pełni ufasz AI i rozumiesz związane z tym zagrożenia bezpieczeństwa.",
|
||||
"toggleAriaLabel": "Przełącz automatyczne zatwierdzanie",
|
||||
"disabledAriaLabel": "Automatyczne zatwierdzanie wyłączone - najpierw wybierz opcje",
|
||||
"readOnly": {
|
||||
"label": "Odczyt",
|
||||
"description": "Gdy włączone, Roo automatycznie będzie wyświetlać zawartość katalogów i czytać pliki bez konieczności klikania przycisku Zatwierdź.",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "Maksymalna liczba żądań",
|
||||
"description": "Automatycznie wykonaj tyle żądań API przed poproszeniem o zgodę na kontynuowanie zadania.",
|
||||
"unlimited": "Bez limitu"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "Wybierz co najmniej jedną opcję poniżej, aby włączyć automatyczne zatwierdzanie"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "Dokumentacja {{provider}}",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Salvar",
|
||||
"tooltip": "Salvar as alterações do arquivo"
|
||||
"tooltip": "Salvar as alterações da mensagem"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Rejeitar",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "Aprovação automática:",
|
||||
"none": "Nenhuma",
|
||||
"description": "A aprovação automática permite que o Roo Code execute ações sem pedir permissão. Ative apenas para ações nas quais você confia totalmente. Configuração mais detalhada disponível nas <settingsLink>Configurações</settingsLink>."
|
||||
"description": "A aprovação automática permite que o Roo Code execute ações sem pedir permissão. Ative apenas para ações nas quais você confia totalmente. Configuração mais detalhada disponível nas <settingsLink>Configurações</settingsLink>.",
|
||||
"selectOptionsFirst": "Selecione pelo menos uma opção abaixo para ativar a aprovação automática",
|
||||
"toggleAriaLabel": "Alternar aprovação automática",
|
||||
"disabledAriaLabel": "Aprovação automática desativada - selecione as opções primeiro"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "Pensando",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "Versão {{version}} - Clique para ver as notas de lançamento"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Edite sua mensagem..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "URI de dados da imagem copiada para a área de transferência"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Excluir Mensagem",
|
||||
"deleteWarning": "Excluir esta mensagem irá excluir todas as mensagens subsequentes na conversa. Deseja prosseguir?",
|
||||
"editMessage": "Editar Mensagem",
|
||||
"editWarning": "Editar esta mensagem irá excluir todas as mensagens subsequentes na conversa. Deseja prosseguir?",
|
||||
"proceed": "Prosseguir"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Permitir que o Roo realize operações automaticamente sem exigir aprovação. Ative essas configurações apenas se confiar totalmente na IA e compreender os riscos de segurança associados.",
|
||||
"toggleAriaLabel": "Alternar aprovação automática",
|
||||
"disabledAriaLabel": "Aprovação automática desativada - selecione as opções primeiro",
|
||||
"readOnly": {
|
||||
"label": "Leitura",
|
||||
"description": "Quando ativado, o Roo visualizará automaticamente o conteúdo do diretório e lerá arquivos sem que você precise clicar no botão Aprovar.",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "Máximo de Solicitações",
|
||||
"description": "Fazer automaticamente este número de requisições à API antes de pedir aprovação para continuar com a tarefa.",
|
||||
"unlimited": "Ilimitado"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "Selecione pelo menos uma opção abaixo para habilitar a aprovação automática"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "Documentação do {{provider}}",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Сохранить",
|
||||
"tooltip": "Сохранить изменения в файле"
|
||||
"tooltip": "Сохранить изменения сообщения"
|
||||
},
|
||||
"tokenProgress": {
|
||||
"availableSpace": "Доступно места: {{amount}} токенов",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "Автоодобрение:",
|
||||
"none": "Нет",
|
||||
"description": "Автоодобрение позволяет Roo Code выполнять действия без запроса разрешения. Включайте только для полностью доверенных действий. Более подробная настройка доступна в <settingsLink>Настройках</settingsLink>."
|
||||
"description": "Автоодобрение позволяет Roo Code выполнять действия без запроса разрешения. Включайте только для полностью доверенных действий. Более подробная настройка доступна в <settingsLink>Настройках</settingsLink>.",
|
||||
"selectOptionsFirst": "Выберите хотя бы один параметр ниже, чтобы включить автоодобрение",
|
||||
"toggleAriaLabel": "Переключить автоодобрение",
|
||||
"disabledAriaLabel": "Автоодобрение отключено - сначала выберите опции"
|
||||
},
|
||||
"announcement": {
|
||||
"title": "🎉 Выпущен Roo Code {{version}}",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "Версия {{version}} - Нажмите, чтобы просмотреть примечания к выпуску"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Редактировать сообщение..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "URI данных изображения скопирован в буфер обмена"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Удалить Сообщение",
|
||||
"deleteWarning": "Удаление этого сообщения приведет к удалению всех последующих сообщений в разговоре. Хотите продолжить?",
|
||||
"editMessage": "Редактировать Сообщение",
|
||||
"editWarning": "Редактирование этого сообщения приведет к удалению всех последующих сообщений в разговоре. Хотите продолжить?",
|
||||
"proceed": "Продолжить"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Разрешить Roo автоматически выполнять операции без необходимости одобрения. Включайте эти параметры только если полностью доверяете ИИ и понимаете связанные с этим риски безопасности.",
|
||||
"toggleAriaLabel": "Переключить автоодобрение",
|
||||
"disabledAriaLabel": "Автоодобрение отключено - сначала выберите опции",
|
||||
"readOnly": {
|
||||
"label": "Чтение",
|
||||
"description": "Если включено, Roo будет автоматически просматривать содержимое каталогов и читать файлы без необходимости нажимать кнопку \"Одобрить\".",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "Максимум запросов",
|
||||
"description": "Автоматически выполнять это количество API-запросов перед запросом разрешения на продолжение задачи.",
|
||||
"unlimited": "Без ограничений"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "Выберите хотя бы один вариант ниже, чтобы включить автоодобрение"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "Документация {{provider}}",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Kaydet",
|
||||
"tooltip": "Dosya değişikliklerini kaydet"
|
||||
"tooltip": "Mesaj değişikliklerini kaydet"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Reddet",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "Otomatik-onay:",
|
||||
"none": "Hiçbiri",
|
||||
"description": "Otomatik onay, Roo Code'un izin istemeden işlemler gerçekleştirmesine olanak tanır. Yalnızca tamamen güvendiğiniz eylemler için etkinleştirin. Daha detaylı yapılandırma <settingsLink>Ayarlar</settingsLink>'da mevcuttur."
|
||||
"description": "Otomatik onay, Roo Code'un izin istemeden işlemler gerçekleştirmesine olanak tanır. Yalnızca tamamen güvendiğiniz eylemler için etkinleştirin. Daha detaylı yapılandırma <settingsLink>Ayarlar</settingsLink>'da mevcuttur.",
|
||||
"selectOptionsFirst": "Otomatik onayı etkinleştirmek için aşağıdan en az bir seçenek belirleyin",
|
||||
"toggleAriaLabel": "Otomatik onayı değiştir",
|
||||
"disabledAriaLabel": "Otomatik onay devre dışı - önce seçenekleri belirleyin"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "Düşünüyor",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "Sürüm {{version}} - Sürüm notlarını görüntülemek için tıklayın"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Mesajını düzenle..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "Görsel veri URI'si panoya kopyalandı"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Mesajı Sil",
|
||||
"deleteWarning": "Bu mesajı silmek, konuşmadaki sonraki tüm mesajları da silecektir. Devam etmek istiyor musun?",
|
||||
"editMessage": "Mesajı Düzenle",
|
||||
"editWarning": "Bu mesajı düzenlemek, konuşmadaki sonraki tüm mesajları da silecektir. Devam etmek istiyor musun?",
|
||||
"proceed": "Devam Et"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Roo'nun onay gerektirmeden otomatik olarak işlemler gerçekleştirmesine izin verin. Bu ayarları yalnızca yapay zekaya tamamen güveniyorsanız ve ilgili güvenlik risklerini anlıyorsanız etkinleştirin.",
|
||||
"toggleAriaLabel": "Otomatik onayı değiştir",
|
||||
"disabledAriaLabel": "Otomatik onay devre dışı - önce seçenekleri belirleyin",
|
||||
"readOnly": {
|
||||
"label": "Okuma",
|
||||
"description": "Etkinleştirildiğinde, Roo otomatik olarak dizin içeriğini görüntüleyecek ve Onayla düğmesine tıklamanıza gerek kalmadan dosyaları okuyacaktır.",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "Maksimum İstek",
|
||||
"description": "Göreve devam etmek için onay istemeden önce bu sayıda API isteği otomatik olarak yap.",
|
||||
"unlimited": "Sınırsız"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "Otomatik onayı etkinleştirmek için aşağıdan en az bir seçenek seçin"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "{{provider}} Dokümantasyonu",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Lưu",
|
||||
"tooltip": "Lưu các thay đổi tệp"
|
||||
"tooltip": "Lưu các thay đổi tin nhắn"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Từ chối",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "Tự động phê duyệt:",
|
||||
"none": "Không",
|
||||
"description": "Tự động phê duyệt cho phép Roo Code thực hiện hành động mà không cần xin phép. Chỉ bật cho các hành động bạn hoàn toàn tin tưởng. Cấu hình chi tiết hơn có sẵn trong <settingsLink>Cài đặt</settingsLink>."
|
||||
"description": "Tự động phê duyệt cho phép Roo Code thực hiện hành động mà không cần xin phép. Chỉ bật cho các hành động bạn hoàn toàn tin tưởng. Cấu hình chi tiết hơn có sẵn trong <settingsLink>Cài đặt</settingsLink>.",
|
||||
"selectOptionsFirst": "Chọn ít nhất một tùy chọn bên dưới để bật tự động phê duyệt",
|
||||
"toggleAriaLabel": "Chuyển đổi tự động phê duyệt",
|
||||
"disabledAriaLabel": "Tự động phê duyệt bị vô hiệu hóa - hãy chọn các tùy chọn trước"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "Đang suy nghĩ",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "Phiên bản {{version}} - Nhấp để xem ghi chú phát hành"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Chỉnh sửa tin nhắn của bạn..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "URI dữ liệu hình ảnh đã được sao chép vào clipboard"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Xóa Tin Nhắn",
|
||||
"deleteWarning": "Xóa tin nhắn này sẽ xóa tất cả các tin nhắn tiếp theo trong cuộc trò chuyện. Bạn có muốn tiếp tục không?",
|
||||
"editMessage": "Chỉnh Sửa Tin Nhắn",
|
||||
"editWarning": "Chỉnh sửa tin nhắn này sẽ xóa tất cả các tin nhắn tiếp theo trong cuộc trò chuyện. Bạn có muốn tiếp tục không?",
|
||||
"proceed": "Tiếp Tục"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "Cho phép Roo tự động thực hiện các hoạt động mà không cần phê duyệt. Chỉ bật những cài đặt này nếu bạn hoàn toàn tin tưởng AI và hiểu rõ các rủi ro bảo mật liên quan.",
|
||||
"toggleAriaLabel": "Chuyển đổi tự động phê duyệt",
|
||||
"disabledAriaLabel": "Tự động phê duyệt bị vô hiệu hóa - hãy chọn các tùy chọn trước",
|
||||
"readOnly": {
|
||||
"label": "Đọc",
|
||||
"description": "Khi được bật, Roo sẽ tự động xem nội dung thư mục và đọc tệp mà không yêu cầu bạn nhấp vào nút Phê duyệt.",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "Số lượng yêu cầu tối đa",
|
||||
"description": "Tự động thực hiện số lượng API request này trước khi yêu cầu phê duyệt để tiếp tục với nhiệm vụ.",
|
||||
"unlimited": "Không giới hạn"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "Chọn ít nhất một tùy chọn bên dưới để bật tự động phê duyệt"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "Tài liệu {{provider}}",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "保存",
|
||||
"tooltip": "保存文件更改"
|
||||
"tooltip": "保存消息更改"
|
||||
},
|
||||
"reject": {
|
||||
"title": "拒绝",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "自动批准:",
|
||||
"none": "无",
|
||||
"description": "允许直接执行操作无需确认,请谨慎启用。前往<settingsLink>设置</settingsLink>调整"
|
||||
"description": "允许直接执行操作无需确认,请谨慎启用。前往<settingsLink>设置</settingsLink>调整",
|
||||
"selectOptionsFirst": "选择至少一个下面的选项以启用自动批准",
|
||||
"toggleAriaLabel": "切换自动批准",
|
||||
"disabledAriaLabel": "自动批准已禁用 - 请先选择选项"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "思考中",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "版本 {{version}} - 点击查看发布说明"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "编辑消息..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "图片数据 URI 已复制到剪贴板"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "删除消息",
|
||||
"deleteWarning": "删除此消息将删除对话中的所有后续消息。是否继续?",
|
||||
"editMessage": "编辑消息",
|
||||
"editWarning": "编辑此消息将删除对话中的所有后续消息。是否继续?",
|
||||
"proceed": "继续"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "允许 Roo 自动执行操作而无需批准。只有在您完全信任 AI 并了解相关安全风险的情况下才启用这些设置。",
|
||||
"toggleAriaLabel": "切换自动批准",
|
||||
"disabledAriaLabel": "自动批准已禁用 - 请先选择选项",
|
||||
"readOnly": {
|
||||
"label": "读取",
|
||||
"description": "启用后,Roo 将自动浏览目录和读取文件内容,无需人工确认。",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "最大请求数",
|
||||
"description": "在请求批准以继续执行任务之前,自动发出此数量的 API 请求。",
|
||||
"unlimited": "无限制"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "请至少选择以下一个选项以启用自动批准"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "{{provider}} 文档",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "儲存",
|
||||
"tooltip": "儲存檔案變更"
|
||||
"tooltip": "儲存訊息變更"
|
||||
},
|
||||
"reject": {
|
||||
"title": "拒絕",
|
||||
|
|
@ -223,7 +223,10 @@
|
|||
"autoApprove": {
|
||||
"title": "自動核准:",
|
||||
"none": "無",
|
||||
"description": "自動核准讓 Roo Code 可以在無需徵求您同意的情況下執行動作。請僅對您完全信任的動作啟用此功能。您可以在<settingsLink>設定</settingsLink>中進行更詳細的調整。"
|
||||
"description": "自動核准讓 Roo Code 可以在無需徵求您同意的情況下執行動作。請僅對您完全信任的動作啟用此功能。您可以在<settingsLink>設定</settingsLink>中進行更詳細的調整。",
|
||||
"selectOptionsFirst": "請至少選擇以下一個選項以啟用自動核准",
|
||||
"toggleAriaLabel": "切換自動核准",
|
||||
"disabledAriaLabel": "自動核准已停用 - 請先選取選項"
|
||||
},
|
||||
"reasoning": {
|
||||
"thinking": "思考中",
|
||||
|
|
@ -319,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "版本 {{version}} - 點擊查看發布說明"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "編輯訊息..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "圖片資料 URI 已複製到剪貼簿"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "刪除訊息",
|
||||
"deleteWarning": "刪除此訊息將刪除對話中的所有後續訊息。是否繼續?",
|
||||
"editMessage": "編輯訊息",
|
||||
"editWarning": "編輯此訊息將刪除對話中的所有後續訊息。是否繼續?",
|
||||
"proceed": "繼續"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
},
|
||||
"autoApprove": {
|
||||
"description": "允許 Roo 無需核准即執行操作。僅在您完全信任 AI 並了解相關安全風險時啟用這些設定。",
|
||||
"toggleAriaLabel": "切換自動核准",
|
||||
"disabledAriaLabel": "自動核准已停用 - 請先選取選項",
|
||||
"readOnly": {
|
||||
"label": "讀取",
|
||||
"description": "啟用後,Roo 將自動檢視目錄內容並讀取檔案,無需點選核准按鈕。",
|
||||
|
|
@ -190,7 +192,8 @@
|
|||
"title": "最大請求數",
|
||||
"description": "在請求批准以繼續執行工作之前,自動發出此數量的 API 請求。",
|
||||
"unlimited": "無限制"
|
||||
}
|
||||
},
|
||||
"selectOptionsFirst": "請至少選擇以下一個選項以啟用自動核准"
|
||||
},
|
||||
"providers": {
|
||||
"providerDocumentation": "{{provider}} 文件",
|
||||
|
|
|
|||
17
webview-ui/src/utils/imageUtils.ts
Normal file
17
webview-ui/src/utils/imageUtils.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Utility function to append new images to existing images array
|
||||
* while respecting the maximum image limit
|
||||
*
|
||||
* @param currentImages - The current array of images
|
||||
* @param newImages - The new images to append
|
||||
* @param maxImages - The maximum number of images allowed
|
||||
* @returns The updated images array
|
||||
*/
|
||||
export function appendImages(currentImages: string[], newImages: string[] | undefined, maxImages: number): string[] {
|
||||
const imagesToAdd = newImages ?? []
|
||||
if (imagesToAdd.length === 0) {
|
||||
return currentImages
|
||||
}
|
||||
|
||||
return [...currentImages, ...imagesToAdd].slice(0, maxImages)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue