diff --git a/src/core/webview/__tests__/webviewMessageHandler.rollback.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.rollback.spec.ts index f75e3c4822..238e1daad5 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.rollback.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.rollback.spec.ts @@ -26,6 +26,20 @@ vi.mock("vscode", () => ({ }, })) +// The no-task failure posts localized copy; the extension i18n loader only +// populates resources outside tests, so the spec pins the English values the +// handler asks for (path is relative to this file: ../../../i18n = src/i18n). +vi.mock("../../../i18n", () => ({ + changeLanguage: vi.fn(), + t: (key: string) => { + const values: Record = { + "common:errors.message.no_active_task_to_roll_back": "No active task to roll back from", + "common:errors.message.no_active_task_to_restore": "No active task to restore from", + } + return values[key] ?? key + }, +})) + // Structural mock: the handler only needs the task identity for these cases. const mockTask = {} as Task const postMessageToWebview = vi.fn(async (_message: ExtensionMessage) => undefined) @@ -103,7 +117,28 @@ describe("webviewMessageHandler - change card rollback", () => { cardTs: 1000, filePath: "src/a.ts", success: false, - error: "No active task to roll back from.", + error: "No active task to roll back from", + }, + }) + }) + + it("posts a correlated failure when the rollback itself throws", async () => { + vi.mocked(rollbackFile).mockRejectedValueOnce(new Error("git restore failed")) + + await webviewMessageHandler(provider, { + type: "checkpointRollbackFile", + payload: { cardTs: 1000, checkpointId: "abc123", filePath: "src/a.ts" }, + }) + + // The card must not stay pending: the handler turns the throw into a + // correlated failure result instead of dropping the message. + expect(postMessageToWebview).toHaveBeenCalledWith({ + type: "checkpointRollbackResult", + checkpointRollbackResult: { + cardTs: 1000, + filePath: "src/a.ts", + success: false, + error: "Rollback failed: git restore failed", }, }) }) @@ -182,7 +217,25 @@ describe("webviewMessageHandler - change card rollback", () => { checkpointRollbackResult: { cardTs: 1000, success: false, - error: "No active task to roll back from.", + error: "No active task to roll back from", + }, + }) + }) + + it("posts a correlated failure when the step rollback itself throws", async () => { + vi.mocked(rollbackStep).mockRejectedValueOnce(new Error("journal unreadable")) + + await webviewMessageHandler(provider, { + type: "checkpointRollbackStep", + payload: { cardTs: 1000, filePaths: ["src/a.ts"] }, + }) + + expect(postMessageToWebview).toHaveBeenCalledWith({ + type: "checkpointRollbackResult", + checkpointRollbackResult: { + cardTs: 1000, + success: false, + error: "Rollback failed: journal unreadable", }, }) }) @@ -293,7 +346,27 @@ describe("webviewMessageHandler - change card rollback", () => { kind: "restore-latest", filePath: "src/a.ts", success: false, - error: "No active task to restore from.", + error: "No active task to restore from", + }, + }) + }) + + it("posts a correlated failure when the restore itself throws", async () => { + vi.mocked(restoreLatestFile).mockRejectedValueOnce(new Error("git checkout failed")) + + await webviewMessageHandler(provider, { + type: "checkpointRestoreLatestFile", + payload: { cardTs: 1000, filePath: "src/a.ts" }, + }) + + expect(postMessageToWebview).toHaveBeenCalledWith({ + type: "checkpointRollbackResult", + checkpointRollbackResult: { + cardTs: 1000, + kind: "restore-latest", + filePath: "src/a.ts", + success: false, + error: "Restore failed: git checkout failed", }, }) }) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 5ae28bc44a..2ba76f70c7 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1614,17 +1614,32 @@ export const webviewMessageHandler = async ( // editor integrations (DiffViewProvider) into the import graph. Loading it // only when a rollback is requested keeps specs that mock `vscode` minimally // from executing editor module-scope code at import time. - const { rollbackFile } = await import("../checkpoints/rollback") - const outcome = await rollbackFile(task, result.data.checkpointId, result.data.filePath) - await provider.postMessageToWebview({ - type: "checkpointRollbackResult", - checkpointRollbackResult: { - cardTs: result.data.cardTs, - filePath: outcome.filePath, - success: outcome.success, - ...(outcome.error ? { error: outcome.error } : {}), - }, - }) + try { + const { rollbackFile } = await import("../checkpoints/rollback") + const outcome = await rollbackFile(task, result.data.checkpointId, result.data.filePath) + await provider.postMessageToWebview({ + type: "checkpointRollbackResult", + checkpointRollbackResult: { + cardTs: result.data.cardTs, + filePath: outcome.filePath, + success: outcome.success, + ...(outcome.error ? { error: outcome.error } : {}), + }, + }) + } catch (error) { + // Correlated failure: a throw between the request and the result post + // (import, journal read, git restore) would otherwise leave the + // requesting card pending forever. + await provider.postMessageToWebview({ + type: "checkpointRollbackResult", + checkpointRollbackResult: { + cardTs: result.data.cardTs, + filePath: result.data.filePath, + success: false, + error: `Rollback failed: ${error instanceof Error ? error.message : String(error)}`, + }, + }) + } } else { // No active task: the rollback cannot run. Post the correlated // failure so the requesting card can clear its pending state @@ -1635,7 +1650,7 @@ export const webviewMessageHandler = async ( cardTs: result.data.cardTs, filePath: result.data.filePath, success: false, - error: "No active task to roll back from.", + error: t("common:errors.message.no_active_task_to_roll_back"), }, }) } @@ -1652,18 +1667,30 @@ export const webviewMessageHandler = async ( if (task) { // Lazy import (see the checkpointRollbackFile case above). - const { rollbackStep } = await import("../checkpoints/rollback") - const outcome = await rollbackStep(task, result.data.filePaths, result.data.checkpointId) - const firstFailure = outcome.files.find((file) => !file.success) - await provider.postMessageToWebview({ - type: "checkpointRollbackResult", - checkpointRollbackResult: { - cardTs: result.data.cardTs, - success: outcome.files.every((file) => file.success), - ...(firstFailure ? { error: firstFailure.error } : {}), - files: outcome.files, - }, - }) + try { + const { rollbackStep } = await import("../checkpoints/rollback") + const outcome = await rollbackStep(task, result.data.filePaths, result.data.checkpointId) + const firstFailure = outcome.files.find((file) => !file.success) + await provider.postMessageToWebview({ + type: "checkpointRollbackResult", + checkpointRollbackResult: { + cardTs: result.data.cardTs, + success: outcome.files.every((file) => file.success), + ...(firstFailure ? { error: firstFailure.error } : {}), + files: outcome.files, + }, + }) + } catch (error) { + // Correlated failure (see the checkpointRollbackFile case). + await provider.postMessageToWebview({ + type: "checkpointRollbackResult", + checkpointRollbackResult: { + cardTs: result.data.cardTs, + success: false, + error: `Rollback failed: ${error instanceof Error ? error.message : String(error)}`, + }, + }) + } } else { // No active task: post the correlated failure so the requesting // card can clear its pending state. @@ -1672,7 +1699,7 @@ export const webviewMessageHandler = async ( checkpointRollbackResult: { cardTs: result.data.cardTs, success: false, - error: "No active task to roll back from.", + error: t("common:errors.message.no_active_task_to_roll_back"), }, }) } @@ -1692,19 +1719,33 @@ export const webviewMessageHandler = async ( if (task) { // Lazy import (see the checkpointRollbackFile case above). - const { restoreLatestFile } = await import("../checkpoints/rollback") - const outcome = await restoreLatestFile(task, result.data.filePath) - await provider.postMessageToWebview({ - type: "checkpointRollbackResult", - checkpointRollbackResult: { - cardTs: result.data.cardTs, - kind: "restore-latest", - filePath: outcome.filePath, - success: outcome.success, - ...(outcome.noOp ? { noOp: true } : {}), - ...(outcome.error ? { error: outcome.error } : {}), - }, - }) + try { + const { restoreLatestFile } = await import("../checkpoints/rollback") + const outcome = await restoreLatestFile(task, result.data.filePath) + await provider.postMessageToWebview({ + type: "checkpointRollbackResult", + checkpointRollbackResult: { + cardTs: result.data.cardTs, + kind: "restore-latest", + filePath: outcome.filePath, + success: outcome.success, + ...(outcome.noOp ? { noOp: true } : {}), + ...(outcome.error ? { error: outcome.error } : {}), + }, + }) + } catch (error) { + // Correlated failure (see the checkpointRollbackFile case). + await provider.postMessageToWebview({ + type: "checkpointRollbackResult", + checkpointRollbackResult: { + cardTs: result.data.cardTs, + kind: "restore-latest", + filePath: result.data.filePath, + success: false, + error: `Restore failed: ${error instanceof Error ? error.message : String(error)}`, + }, + }) + } } else { // No active task: post the correlated failure so the requesting // card can clear its pending state. @@ -1715,7 +1756,7 @@ export const webviewMessageHandler = async ( kind: "restore-latest", filePath: result.data.filePath, success: false, - error: "No active task to restore from.", + error: t("common:errors.message.no_active_task_to_restore"), }, }) } diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 24ae3f310c..9500c6e35b 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -102,6 +102,8 @@ }, "message": { "no_active_task_to_delete": "No hi ha cap tasca activa de la qual eliminar missatges", + "no_active_task_to_roll_back": "No hi ha cap tasca activa per desfer", + "no_active_task_to_restore": "No hi ha cap tasca activa per restaurar", "invalid_timestamp_for_deletion": "Marca de temps del missatge no vàlida per a l'eliminació", "cannot_delete_missing_timestamp": "No es pot eliminar el missatge: falta la marca de temps", "cannot_delete_invalid_timestamp": "No es pot eliminar el missatge: marca de temps no vàlida", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 54fa0b3c22..b1affc0257 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -99,6 +99,8 @@ }, "message": { "no_active_task_to_delete": "Keine aktive Aufgabe, aus der Nachrichten gelöscht werden können", + "no_active_task_to_roll_back": "Keine aktive Aufgabe, von der aus zurückgerollt werden kann", + "no_active_task_to_restore": "Keine aktive Aufgabe, von der aus wiederhergestellt werden kann", "invalid_timestamp_for_deletion": "Ungültiger Nachrichten-Zeitstempel zum Löschen", "cannot_delete_missing_timestamp": "Nachricht kann nicht gelöscht werden: fehlender Zeitstempel", "cannot_delete_invalid_timestamp": "Nachricht kann nicht gelöscht werden: ungültiger Zeitstempel", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 516a3d4f88..8fd0c2049e 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -100,6 +100,8 @@ }, "message": { "no_active_task_to_delete": "No active task to delete messages from", + "no_active_task_to_roll_back": "No active task to roll back from", + "no_active_task_to_restore": "No active task to restore from", "invalid_timestamp_for_deletion": "Invalid message timestamp for deletion", "cannot_delete_missing_timestamp": "Cannot delete message: missing timestamp", "cannot_delete_invalid_timestamp": "Cannot delete message: invalid timestamp", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 71dc994516..9dcf4cd27b 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -99,6 +99,8 @@ }, "message": { "no_active_task_to_delete": "No hay tarea activa de la cual eliminar mensajes", + "no_active_task_to_roll_back": "No hay ninguna tarea activa desde la que deshacer los cambios", + "no_active_task_to_restore": "No hay ninguna tarea activa desde la que restaurar los cambios", "invalid_timestamp_for_deletion": "Marca de tiempo del mensaje no válida para eliminación", "cannot_delete_missing_timestamp": "No se puede eliminar el mensaje: falta marca de tiempo", "cannot_delete_invalid_timestamp": "No se puede eliminar el mensaje: marca de tiempo no válida", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 87009ee988..e6cc7e3cb7 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -99,6 +99,8 @@ }, "message": { "no_active_task_to_delete": "Aucune tâche active pour supprimer des messages", + "no_active_task_to_roll_back": "Aucune tâche active pour annuler les modifications", + "no_active_task_to_restore": "Aucune tâche active pour restaurer les modifications", "invalid_timestamp_for_deletion": "Horodatage du message invalide pour la suppression", "cannot_delete_missing_timestamp": "Impossible de supprimer le message : horodatage manquant", "cannot_delete_invalid_timestamp": "Impossible de supprimer le message : horodatage invalide", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index f4bd1c3055..4e4933134f 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -99,6 +99,8 @@ }, "message": { "no_active_task_to_delete": "संदेशों को हटाने के लिए कोई सक्रिय कार्य नहीं", + "no_active_task_to_roll_back": "वापस करने के लिए कोई सक्रिय कार्य नहीं है", + "no_active_task_to_restore": "पुनर्स्थापित करने के लिए कोई सक्रिय कार्य नहीं है", "invalid_timestamp_for_deletion": "हटाने के लिए अमान्य संदेश टाइमस्टैम्प", "cannot_delete_missing_timestamp": "संदेश हटाया नहीं जा सकता: टाइमस्टैम्प गुम है", "cannot_delete_invalid_timestamp": "संदेश हटाया नहीं जा सकता: अमान्य टाइमस्टैम्प", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index bcee321af5..47e37f4323 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -99,6 +99,8 @@ }, "message": { "no_active_task_to_delete": "Tidak ada tugas aktif untuk menghapus pesan", + "no_active_task_to_roll_back": "Tidak ada tugas aktif untuk di-rollback", + "no_active_task_to_restore": "Tidak ada tugas aktif untuk dipulihkan", "invalid_timestamp_for_deletion": "Timestamp pesan tidak valid untuk penghapusan", "cannot_delete_missing_timestamp": "Tidak dapat menghapus pesan: timestamp tidak ada", "cannot_delete_invalid_timestamp": "Tidak dapat menghapus pesan: timestamp tidak valid", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 395be16b84..5c3920390b 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -99,6 +99,8 @@ }, "message": { "no_active_task_to_delete": "Nessuna attività attiva da cui eliminare messaggi", + "no_active_task_to_roll_back": "Nessun task attivo da cui annullare le modifiche", + "no_active_task_to_restore": "Nessun task attivo da cui ripristinare le modifiche", "invalid_timestamp_for_deletion": "Timestamp del messaggio non valido per l'eliminazione", "cannot_delete_missing_timestamp": "Impossibile eliminare il messaggio: timestamp mancante", "cannot_delete_invalid_timestamp": "Impossibile eliminare il messaggio: timestamp non valido", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 7dccfcd837..2fb3351331 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -99,6 +99,8 @@ }, "message": { "no_active_task_to_delete": "メッセージを削除するアクティブなタスクがありません", + "no_active_task_to_roll_back": "ロールバックできるアクティブなタスクがありません", + "no_active_task_to_restore": "復元できるアクティブなタスクがありません", "invalid_timestamp_for_deletion": "削除用のメッセージタイムスタンプが無効です", "cannot_delete_missing_timestamp": "メッセージを削除できません:タイムスタンプがありません", "cannot_delete_invalid_timestamp": "メッセージを削除できません:タイムスタンプが無効です", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 0ca65be687..23eb829e21 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -99,6 +99,8 @@ }, "message": { "no_active_task_to_delete": "메시지를 삭제할 활성 작업이 없습니다", + "no_active_task_to_roll_back": "되돌릴 활성 작업이 없습니다", + "no_active_task_to_restore": "복원할 활성 작업이 없습니다", "invalid_timestamp_for_deletion": "삭제를 위한 메시지 타임스탬프가 유효하지 않습니다", "cannot_delete_missing_timestamp": "메시지를 삭제할 수 없습니다: 타임스탬프가 없습니다", "cannot_delete_invalid_timestamp": "메시지를 삭제할 수 없습니다: 타임스탬프가 유효하지 않습니다", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index a38415edfd..21cafafad4 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -99,6 +99,8 @@ }, "message": { "no_active_task_to_delete": "Geen actieve taak om berichten uit te verwijderen", + "no_active_task_to_roll_back": "Geen actieve taak om terug te draaien", + "no_active_task_to_restore": "Geen actieve taak om te herstellen", "invalid_timestamp_for_deletion": "Ongeldig bericht tijdstempel voor verwijdering", "cannot_delete_missing_timestamp": "Kan bericht niet verwijderen: tijdstempel ontbreekt", "cannot_delete_invalid_timestamp": "Kan bericht niet verwijderen: ongeldig tijdstempel", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index ff898e8987..41ef2d7a70 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -99,6 +99,8 @@ }, "message": { "no_active_task_to_delete": "Brak aktywnego zadania do usunięcia wiadomości", + "no_active_task_to_roll_back": "Brak aktywnego zadania do cofnięcia", + "no_active_task_to_restore": "Brak aktywnego zadania do przywrócenia", "invalid_timestamp_for_deletion": "Nieprawidłowy znacznik czasu wiadomości do usunięcia", "cannot_delete_missing_timestamp": "Nie można usunąć wiadomości: brak znacznika czasu", "cannot_delete_invalid_timestamp": "Nie można usunąć wiadomości: nieprawidłowy znacznik czasu", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index d3c31ed2dd..d76488a7e5 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -103,6 +103,8 @@ }, "message": { "no_active_task_to_delete": "Nenhuma tarefa ativa para excluir mensagens", + "no_active_task_to_roll_back": "Nenhuma tarefa ativa para desfazer", + "no_active_task_to_restore": "Nenhuma tarefa ativa para restaurar", "invalid_timestamp_for_deletion": "Timestamp da mensagem inválido para exclusão", "cannot_delete_missing_timestamp": "Não é possível excluir mensagem: timestamp ausente", "cannot_delete_invalid_timestamp": "Não é possível excluir mensagem: timestamp inválido", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 08d2e2aa2c..57b06a5325 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -99,6 +99,8 @@ }, "message": { "no_active_task_to_delete": "Нет активной задачи для удаления сообщений", + "no_active_task_to_roll_back": "Нет активного задания для отката", + "no_active_task_to_restore": "Нет активного задания для восстановления", "invalid_timestamp_for_deletion": "Недействительная временная метка сообщения для удаления", "cannot_delete_missing_timestamp": "Невозможно удалить сообщение: отсутствует временная метка", "cannot_delete_invalid_timestamp": "Невозможно удалить сообщение: недействительная временная метка", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 716ccbc6de..3b49e740fd 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -99,6 +99,8 @@ }, "message": { "no_active_task_to_delete": "Mesaj silinecek aktif görev yok", + "no_active_task_to_roll_back": "Geri alınacak etkin görev yok", + "no_active_task_to_restore": "Geri yüklenecek etkin görev yok", "invalid_timestamp_for_deletion": "Silme için geçersiz mesaj zaman damgası", "cannot_delete_missing_timestamp": "Mesaj silinemiyor: zaman damgası eksik", "cannot_delete_invalid_timestamp": "Mesaj silinemiyor: geçersiz zaman damgası", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 69c6343c31..3f5a5abb58 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -99,6 +99,8 @@ }, "message": { "no_active_task_to_delete": "Không có nhiệm vụ hoạt động để xóa tin nhắn", + "no_active_task_to_roll_back": "Không có tác vụ hoạt động để hoàn tác", + "no_active_task_to_restore": "Không có tác vụ hoạt động để khôi phục", "invalid_timestamp_for_deletion": "Dấu thời gian tin nhắn không hợp lệ để xóa", "cannot_delete_missing_timestamp": "Không thể xóa tin nhắn: thiếu dấu thời gian", "cannot_delete_invalid_timestamp": "Không thể xóa tin nhắn: dấu thời gian không hợp lệ", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 3600f0aa7c..0cac675500 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -104,6 +104,8 @@ }, "message": { "no_active_task_to_delete": "没有可删除消息的活跃任务", + "no_active_task_to_roll_back": "没有可回滚的活动任务", + "no_active_task_to_restore": "没有可恢复的活动任务", "invalid_timestamp_for_deletion": "删除操作的消息时间戳无效", "cannot_delete_missing_timestamp": "无法删除消息:缺少时间戳", "cannot_delete_invalid_timestamp": "无法删除消息:时间戳无效", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index c635769891..a11e2ac30f 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -98,6 +98,8 @@ }, "message": { "no_active_task_to_delete": "沒有可刪除訊息的活躍工作", + "no_active_task_to_roll_back": "沒有可還原的活動任務", + "no_active_task_to_restore": "沒有可復原的活動任務", "invalid_timestamp_for_deletion": "刪除操作的訊息時間戳無效", "cannot_delete_missing_timestamp": "無法刪除訊息:缺少時間戳", "cannot_delete_invalid_timestamp": "無法刪除訊息:時間戳無效", diff --git a/webview-ui/src/components/chat/ChangeCard.tsx b/webview-ui/src/components/chat/ChangeCard.tsx index 86393f7c9c..50d115d2ed 100644 --- a/webview-ui/src/components/chat/ChangeCard.tsx +++ b/webview-ui/src/components/chat/ChangeCard.tsx @@ -223,7 +223,12 @@ export const ChangeCard = ({ message }: { message: ClineMessage }) => { case "error": return ( + {/* Focusable status so the error detail is reachable by keyboard and + screen-reader users, not only via the hover tooltip. */} @@ -294,7 +299,11 @@ export const ChangeCard = ({ message }: { message: ClineMessage }) => { case "error": return ( + {/* Focusable status (see the file-rollback error span above). */} @@ -363,7 +372,11 @@ export const ChangeCard = ({ message }: { message: ClineMessage }) => { case "error": return ( + {/* Focusable status (see the file-rollback error span above). */} @@ -421,8 +434,8 @@ export const ChangeCard = ({ message }: { message: ClineMessage }) => {