mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Message edit/delete overhaul (#5538)
* improved chat row first pass * big UI improvements * working functionality * tests working * ok finally tests working for real! * translations * add back hidden flag * remove option to skip notif * fixed image issue * ui fix * put back edit flag * oops test fix * reduce margins * code review
This commit is contained in:
parent
6cf376f832
commit
fb374b3e94
68 changed files with 1461 additions and 711 deletions
|
|
@ -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 格式無效",
|
||||
|
|
|
|||
|
|
@ -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 }))
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
@ -722,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
|
||||
|
|
|
|||
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" />
|
||||
)
|
||||
|
|
@ -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,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")
|
||||
})
|
||||
})
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Desar",
|
||||
"tooltip": "Desa els canvis del fitxer"
|
||||
"tooltip": "Desa els canvis del missatge"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Rebutjar",
|
||||
|
|
@ -322,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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Speichern",
|
||||
"tooltip": "Dateiänderungen speichern"
|
||||
"tooltip": "Nachrichtenänderungen speichern"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Ablehnen",
|
||||
|
|
@ -322,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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -322,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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Enregistrer",
|
||||
"tooltip": "Sauvegarder les modifications du fichier"
|
||||
"tooltip": "Enregistrer les modifications du message"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Rejeter",
|
||||
|
|
@ -322,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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "सहेजें",
|
||||
"tooltip": "फ़ाइल परिवर्तन सहेजें"
|
||||
"tooltip": "संदेश के बदलाव सहेजें"
|
||||
},
|
||||
"reject": {
|
||||
"title": "अस्वीकार करें",
|
||||
|
|
@ -322,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "संस्करण {{version}} - रिलीज़ नोट्स देखने के लिए क्लिक करें"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "अपना संदेश संपादित करें..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "इमेज डेटा URI क्लिपबोर्ड में कॉपी हो गया"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "संदेश हटाएं",
|
||||
"deleteWarning": "इस संदेश को हटाने से बातचीत के सभी बाद के संदेश हट जाएंगे। क्या आप जारी रखना चाहते हैं?",
|
||||
"editMessage": "संदेश संपादित करें",
|
||||
"editWarning": "इस संदेश को संपादित करने से बातचीत के सभी बाद के संदेश हट जाएंगे। क्या आप जारी रखना चाहते हैं?",
|
||||
"proceed": "जारी रखें"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Simpan",
|
||||
"tooltip": "Simpan perubahan file"
|
||||
"tooltip": "Simpan perubahan pesan"
|
||||
},
|
||||
"tokenProgress": {
|
||||
"availableSpace": "Ruang tersedia: {{amount}} token",
|
||||
|
|
@ -328,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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Salva",
|
||||
"tooltip": "Salva le modifiche al file"
|
||||
"tooltip": "Salva le modifiche del messaggio"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Rifiuta",
|
||||
|
|
@ -322,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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "保存",
|
||||
"tooltip": "ファイル変更を保存"
|
||||
"tooltip": "メッセージの変更を保存"
|
||||
},
|
||||
"reject": {
|
||||
"title": "拒否",
|
||||
|
|
@ -322,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "バージョン {{version}} - クリックしてリリースノートを表示"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "メッセージを編集..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "画像データURIをクリップボードにコピーしました"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "メッセージを削除",
|
||||
"deleteWarning": "このメッセージを削除すると、会話内の後続のメッセージもすべて削除されます。続行しますか?",
|
||||
"editMessage": "メッセージを編集",
|
||||
"editWarning": "このメッセージを編集すると、会話内の後続のメッセージもすべて削除されます。続行しますか?",
|
||||
"proceed": "続行"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "저장",
|
||||
"tooltip": "파일 변경사항 저장"
|
||||
"tooltip": "메시지 변경사항 저장"
|
||||
},
|
||||
"reject": {
|
||||
"title": "거부",
|
||||
|
|
@ -322,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "버전 {{version}} - 릴리스 노트를 보려면 클릭하세요"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "메시지 편집..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "이미지 데이터 URI가 클립보드에 복사됨"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "메시지 삭제",
|
||||
"deleteWarning": "이 메시지를 삭제하면 대화의 모든 후속 메시지가 삭제됩니다. 계속하시겠습니까?",
|
||||
"editMessage": "메시지 편집",
|
||||
"editWarning": "이 메시지를 편집하면 대화의 모든 후속 메시지가 삭제됩니다. 계속하시겠습니까?",
|
||||
"proceed": "계속"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Opslaan",
|
||||
"tooltip": "Bestandswijzigingen opslaan"
|
||||
"tooltip": "Berichtwijzigingen opslaan"
|
||||
},
|
||||
"tokenProgress": {
|
||||
"availableSpace": "Beschikbare ruimte: {{amount}} tokens",
|
||||
|
|
@ -322,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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Zapisz",
|
||||
"tooltip": "Zapisz zmiany w pliku"
|
||||
"tooltip": "Zapisz zmiany wiadomości"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Odrzuć",
|
||||
|
|
@ -322,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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Salvar",
|
||||
"tooltip": "Salvar as alterações do arquivo"
|
||||
"tooltip": "Salvar as alterações da mensagem"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Rejeitar",
|
||||
|
|
@ -322,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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Сохранить",
|
||||
"tooltip": "Сохранить изменения в файле"
|
||||
"tooltip": "Сохранить изменения сообщения"
|
||||
},
|
||||
"tokenProgress": {
|
||||
"availableSpace": "Доступно места: {{amount}} токенов",
|
||||
|
|
@ -322,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "Версия {{version}} - Нажмите, чтобы просмотреть примечания к выпуску"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "Редактировать сообщение..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "URI данных изображения скопирован в буфер обмена"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "Удалить Сообщение",
|
||||
"deleteWarning": "Удаление этого сообщения приведет к удалению всех последующих сообщений в разговоре. Хотите продолжить?",
|
||||
"editMessage": "Редактировать Сообщение",
|
||||
"editWarning": "Редактирование этого сообщения приведет к удалению всех последующих сообщений в разговоре. Хотите продолжить?",
|
||||
"proceed": "Продолжить"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "Kaydet",
|
||||
"tooltip": "Dosya değişikliklerini kaydet"
|
||||
"tooltip": "Mesaj değişikliklerini kaydet"
|
||||
},
|
||||
"reject": {
|
||||
"title": "Reddet",
|
||||
|
|
@ -322,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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -322,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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "保存",
|
||||
"tooltip": "保存文件更改"
|
||||
"tooltip": "保存消息更改"
|
||||
},
|
||||
"reject": {
|
||||
"title": "拒绝",
|
||||
|
|
@ -322,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "版本 {{version}} - 点击查看发布说明"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "编辑消息..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "图片数据 URI 已复制到剪贴板"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "删除消息",
|
||||
"deleteWarning": "删除此消息将删除对话中的所有后续消息。是否继续?",
|
||||
"editMessage": "编辑消息",
|
||||
"editWarning": "编辑此消息将删除对话中的所有后续消息。是否继续?",
|
||||
"proceed": "继续"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"save": {
|
||||
"title": "儲存",
|
||||
"tooltip": "儲存檔案變更"
|
||||
"tooltip": "儲存訊息變更"
|
||||
},
|
||||
"reject": {
|
||||
"title": "拒絕",
|
||||
|
|
@ -322,5 +322,8 @@
|
|||
},
|
||||
"versionIndicator": {
|
||||
"ariaLabel": "版本 {{version}} - 點擊查看發布說明"
|
||||
},
|
||||
"editMessage": {
|
||||
"placeholder": "編輯訊息..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "圖片資料 URI 已複製到剪貼簿"
|
||||
}
|
||||
},
|
||||
"confirmation": {
|
||||
"deleteMessage": "刪除訊息",
|
||||
"deleteWarning": "刪除此訊息將刪除對話中的所有後續訊息。是否繼續?",
|
||||
"editMessage": "編輯訊息",
|
||||
"editWarning": "編輯此訊息將刪除對話中的所有後續訊息。是否繼續?",
|
||||
"proceed": "繼續"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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