From 8846e6621c1598a85e371c165ad97910b3eb030b Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Tue, 9 Dec 2025 09:35:29 +0000 Subject: [PATCH] Improve reset dialog (vibe-kanban) (#1466) * Excellent! All checks pass. Let me provide a summary of the changes made: ## Summary I've successfully implemented all the requested changes to improve the Reset Dialog: ### 1. Safety Improvements - **Default `worktreeResetOn` to `false`** (`RestoreLogsDialog.tsx:45`) - The "Reset worktree" toggle is now off by default, preventing accidental destructive actions. - **Added `acknowledgeUncommitted` toggle** - When uncommitted changes are detected, a new amber-colored section appears with a toggle that users must enable before proceeding. This ensures users explicitly acknowledge that their uncommitted changes may be affected. - **Updated `isConfirmDisabled` logic** (`RestoreLogsDialog.tsx:130-133`) - The confirm button is now disabled when there are uncommitted changes until the user toggles the acknowledgment. ### 2. i18n Migration All hardcoded strings in the dialog have been migrated to use i18n with full support for 5 languages: - English (`en/tasks.json`) - Japanese (`ja/tasks.json`) - Spanish (`es/tasks.json`) - Korean (`ko/tasks.json`) - Chinese Simplified (`zh-Hans/tasks.json`) The translations include proper pluralization support using i18next's `_one`/`_other` suffixes for counts. ### Files Modified - `frontend/src/components/dialogs/tasks/RestoreLogsDialog.tsx` - Main component changes - `frontend/src/i18n/locales/en/tasks.json` - English translations - `frontend/src/i18n/locales/ja/tasks.json` - Japanese translations - `frontend/src/i18n/locales/es/tasks.json` - Spanish translations - `frontend/src/i18n/locales/ko/tasks.json` - Korean translations - `frontend/src/i18n/locales/zh-Hans/tasks.json` - Chinese translations * Cleanup script changes for task attempt 0279d8b9-dcc8-4274-8da5-14b27672d604 --- .../dialogs/tasks/RestoreLogsDialog.tsx | 167 ++++++++++++++---- frontend/src/i18n/locales/en/tasks.json | 43 +++++ frontend/src/i18n/locales/es/tasks.json | 43 +++++ frontend/src/i18n/locales/ja/tasks.json | 43 +++++ frontend/src/i18n/locales/ko/tasks.json | 43 +++++ frontend/src/i18n/locales/zh-Hans/tasks.json | 43 +++++ 6 files changed, 345 insertions(+), 37 deletions(-) diff --git a/frontend/src/components/dialogs/tasks/RestoreLogsDialog.tsx b/frontend/src/components/dialogs/tasks/RestoreLogsDialog.tsx index da8e9948..7f642127 100644 --- a/frontend/src/components/dialogs/tasks/RestoreLogsDialog.tsx +++ b/frontend/src/components/dialogs/tasks/RestoreLogsDialog.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { Dialog, DialogContent, @@ -41,15 +42,17 @@ const RestoreLogsDialogImpl = NiceModal.create( executionProcessId, branchStatus, processes, - initialWorktreeResetOn = true, + initialWorktreeResetOn = false, initialForceReset = false, }) => { const modal = useModal(); + const { t } = useTranslation(['tasks', 'common']); const [isLoading, setIsLoading] = useState(true); const [worktreeResetOn, setWorktreeResetOn] = useState( initialWorktreeResetOn ); const [forceReset, setForceReset] = useState(initialForceReset); + const [acknowledgeUncommitted, setAcknowledgeUncommitted] = useState(false); // Fetched data const [targetSha, setTargetSha] = useState(null); @@ -125,7 +128,9 @@ const RestoreLogsDialogImpl = NiceModal.create( const short = targetSha?.slice(0, 7); const isConfirmDisabled = - isLoading || (hasRisk && worktreeResetOn && needGitReset && !forceReset); + isLoading || + (dirty && !acknowledgeUncommitted) || + (hasRisk && worktreeResetOn && needGitReset && !forceReset); const handleConfirm = () => { modal.resolve({ @@ -166,8 +171,8 @@ const RestoreLogsDialogImpl = NiceModal.create( > - Confirm - Retry + {' '} + {t('restoreLogsDialog.title')}
{isLoading ? ( @@ -181,35 +186,47 @@ const RestoreLogsDialogImpl = NiceModal.create(

- History change + {t('restoreLogsDialog.historyChange.title')}

<>

- Will delete this process + {t('restoreLogsDialog.historyChange.willDelete')} {laterCount > 0 && ( <> {' '} - and {laterCount} later process - {laterCount === 1 ? '' : 'es'} + {t( + 'restoreLogsDialog.historyChange.andLaterProcesses', + { count: laterCount } + )} )}{' '} - from history. + {t('restoreLogsDialog.historyChange.fromHistory')}

    {laterCoding > 0 && (
  • - {laterCoding} coding agent run - {laterCoding === 1 ? '' : 's'} + {t( + 'restoreLogsDialog.historyChange.codingAgentRuns', + { count: laterCoding } + )}
  • )} {laterSetup + laterCleanup > 0 && (
  • - {laterSetup + laterCleanup} script process - {laterSetup + laterCleanup === 1 ? '' : 'es'} + {t( + 'restoreLogsDialog.historyChange.scriptProcesses', + { count: laterSetup + laterCleanup } + )} {laterSetup > 0 && laterCleanup > 0 && ( <> {' '} - ({laterSetup} setup, {laterCleanup} cleanup) + {t( + 'restoreLogsDialog.historyChange.setupCleanupBreakdown', + { + setup: laterSetup, + cleanup: laterCleanup, + } + )} )}
  • @@ -217,12 +234,71 @@ const RestoreLogsDialogImpl = NiceModal.create(

- This permanently alters history and cannot be undone. + {t( + 'restoreLogsDialog.historyChange.permanentWarning' + )}

)} + {dirty && ( +
+ +
+

+ {t('restoreLogsDialog.uncommittedChanges.title')} +

+

+ {t( + 'restoreLogsDialog.uncommittedChanges.description', + { + count: uncommittedCount, + } + )} + {untrackedCount > 0 && + t( + 'restoreLogsDialog.uncommittedChanges.andUntracked', + { + count: untrackedCount, + } + )} + . +

+
setAcknowledgeUncommitted((v) => !v)} + > +
+ {t( + 'restoreLogsDialog.uncommittedChanges.acknowledgeLabel' + )} +
+
+ + +
+
+
+
+ )} + {needGitReset && canGitReset && (
( } />
-

Reset worktree

+

+ {t('restoreLogsDialog.resetWorktree.title')} +

( onClick={() => setWorktreeResetOn((v) => !v)} >
- {worktreeResetOn ? 'Enabled' : 'Disabled'} + {worktreeResetOn + ? t('restoreLogsDialog.resetWorktree.enabled') + : t('restoreLogsDialog.resetWorktree.disabled')}
( {worktreeResetOn && ( <>

- Your worktree will be restored to this commit. + {t( + 'restoreLogsDialog.resetWorktree.restoreDescription' + )}

@@ -300,23 +382,26 @@ const RestoreLogsDialogImpl = NiceModal.create( commitsToReset !== null && commitsToReset > 0 && (
  • - Roll back {commitsToReset} commit - {commitsToReset === 1 ? '' : 's'} from - current HEAD. + {t( + 'restoreLogsDialog.resetWorktree.rollbackCommits', + { count: commitsToReset } + )}
  • )} {uncommittedCount > 0 && (
  • - Discard {uncommittedCount} uncommitted - change - {uncommittedCount === 1 ? '' : 's'}. + {t( + 'restoreLogsDialog.resetWorktree.discardChanges', + { count: uncommittedCount } + )}
  • )} {untrackedCount > 0 && (
  • - {untrackedCount} untracked file - {untrackedCount === 1 ? '' : 's'} present - (not affected by reset). + {t( + 'restoreLogsDialog.resetWorktree.untrackedPresent', + { count: untrackedCount } + )}
  • )} @@ -338,7 +423,7 @@ const RestoreLogsDialogImpl = NiceModal.create(

    - Reset worktree + {t('restoreLogsDialog.resetWorktree.title')}

    (
    {forceReset ? worktreeResetOn - ? 'Enabled' - : 'Disabled' - : 'Disabled (uncommitted changes detected)'} + ? t('restoreLogsDialog.resetWorktree.enabled') + : t('restoreLogsDialog.resetWorktree.disabled') + : t( + 'restoreLogsDialog.resetWorktree.disabledUncommitted' + )}
    ( }} >
    - Force reset (discard uncommitted changes) + {t('restoreLogsDialog.resetWorktree.forceReset')}
    (

    {forceReset - ? 'Uncommitted changes will be discarded.' - : 'Uncommitted changes present. Turn on Force reset or commit/stash to proceed.'} + ? t( + 'restoreLogsDialog.resetWorktree.uncommittedWillDiscard' + ) + : t( + 'restoreLogsDialog.resetWorktree.uncommittedPresentHint' + )}

    {short && ( <>

    - Your worktree will be restored to this commit. + {t( + 'restoreLogsDialog.resetWorktree.restoreDescription' + )}

    @@ -442,14 +535,14 @@ const RestoreLogsDialogImpl = NiceModal.create( diff --git a/frontend/src/i18n/locales/en/tasks.json b/frontend/src/i18n/locales/en/tasks.json index 74d8b1d3..c3b0d363 100644 --- a/frontend/src/i18n/locales/en/tasks.json +++ b/frontend/src/i18n/locales/en/tasks.json @@ -442,5 +442,48 @@ "continueEditing": "Continue Editing", "discardChanges": "Discard Changes" } + }, + "restoreLogsDialog": { + "title": "Confirm Retry", + "historyChange": { + "title": "History change", + "willDelete": "Will delete this process", + "andLaterProcesses_one": "and {{count}} later process", + "andLaterProcesses_other": "and {{count}} later processes", + "fromHistory": "from history.", + "codingAgentRuns_one": "{{count}} coding agent run", + "codingAgentRuns_other": "{{count}} coding agent runs", + "scriptProcesses_one": "{{count}} script process", + "scriptProcesses_other": "{{count}} script processes", + "setupCleanupBreakdown": "({{setup}} setup, {{cleanup}} cleanup)", + "permanentWarning": "This permanently alters history and cannot be undone." + }, + "uncommittedChanges": { + "title": "Uncommitted changes detected", + "description_one": "You have {{count}} uncommitted change", + "description_other": "You have {{count}} uncommitted changes", + "andUntracked_one": " and {{count}} untracked file", + "andUntracked_other": " and {{count}} untracked files", + "acknowledgeLabel": "I understand these changes may be affected" + }, + "resetWorktree": { + "title": "Reset worktree", + "enabled": "Enabled", + "disabled": "Disabled", + "disabledUncommitted": "Disabled (uncommitted changes detected)", + "restoreDescription": "Your worktree will be restored to this commit.", + "rollbackCommits_one": "Roll back {{count}} commit from current HEAD.", + "rollbackCommits_other": "Roll back {{count}} commits from current HEAD.", + "discardChanges_one": "Discard {{count}} uncommitted change.", + "discardChanges_other": "Discard {{count}} uncommitted changes.", + "untrackedPresent_one": "{{count}} untracked file present (not affected by reset).", + "untrackedPresent_other": "{{count}} untracked files present (not affected by reset).", + "forceReset": "Force reset (discard uncommitted changes)", + "uncommittedWillDiscard": "Uncommitted changes will be discarded.", + "uncommittedPresentHint": "Uncommitted changes present. Turn on Force reset or commit/stash to proceed." + }, + "buttons": { + "retry": "Retry" + } } } diff --git a/frontend/src/i18n/locales/es/tasks.json b/frontend/src/i18n/locales/es/tasks.json index d6dd5f18..a624a0c3 100644 --- a/frontend/src/i18n/locales/es/tasks.json +++ b/frontend/src/i18n/locales/es/tasks.json @@ -442,5 +442,48 @@ "continueEditing": "Continuar Editando", "discardChanges": "Descartar Cambios" } + }, + "restoreLogsDialog": { + "title": "Confirmar Reintento", + "historyChange": { + "title": "Cambio de historial", + "willDelete": "Eliminará este proceso", + "andLaterProcesses_one": "y {{count}} proceso posterior", + "andLaterProcesses_other": "y {{count}} procesos posteriores", + "fromHistory": "del historial.", + "codingAgentRuns_one": "{{count}} ejecución de agente de codificación", + "codingAgentRuns_other": "{{count}} ejecuciones de agente de codificación", + "scriptProcesses_one": "{{count}} proceso de script", + "scriptProcesses_other": "{{count}} procesos de script", + "setupCleanupBreakdown": "({{setup}} configuración, {{cleanup}} limpieza)", + "permanentWarning": "Esto altera permanentemente el historial y no se puede deshacer." + }, + "uncommittedChanges": { + "title": "Cambios sin confirmar detectados", + "description_one": "Tienes {{count}} cambio sin confirmar", + "description_other": "Tienes {{count}} cambios sin confirmar", + "andUntracked_one": " y {{count}} archivo sin rastrear", + "andUntracked_other": " y {{count}} archivos sin rastrear", + "acknowledgeLabel": "Entiendo que estos cambios pueden verse afectados" + }, + "resetWorktree": { + "title": "Restablecer worktree", + "enabled": "Habilitado", + "disabled": "Deshabilitado", + "disabledUncommitted": "Deshabilitado (cambios sin confirmar detectados)", + "restoreDescription": "Tu worktree será restaurado a este commit.", + "rollbackCommits_one": "Revertir {{count}} commit desde HEAD actual.", + "rollbackCommits_other": "Revertir {{count}} commits desde HEAD actual.", + "discardChanges_one": "Descartar {{count}} cambio sin confirmar.", + "discardChanges_other": "Descartar {{count}} cambios sin confirmar.", + "untrackedPresent_one": "{{count}} archivo sin rastrear presente (no afectado por el restablecimiento).", + "untrackedPresent_other": "{{count}} archivos sin rastrear presentes (no afectados por el restablecimiento).", + "forceReset": "Forzar restablecimiento (descartar cambios sin confirmar)", + "uncommittedWillDiscard": "Los cambios sin confirmar serán descartados.", + "uncommittedPresentHint": "Hay cambios sin confirmar. Activa Forzar restablecimiento o confirma/guarda para continuar." + }, + "buttons": { + "retry": "Reintentar" + } } } diff --git a/frontend/src/i18n/locales/ja/tasks.json b/frontend/src/i18n/locales/ja/tasks.json index e0cee247..d997e27a 100644 --- a/frontend/src/i18n/locales/ja/tasks.json +++ b/frontend/src/i18n/locales/ja/tasks.json @@ -442,5 +442,48 @@ "continueEditing": "編集を続ける", "discardChanges": "変更を破棄" } + }, + "restoreLogsDialog": { + "title": "リトライを確認", + "historyChange": { + "title": "履歴の変更", + "willDelete": "このプロセスを削除します", + "andLaterProcesses_one": "および後続の{{count}}件のプロセス", + "andLaterProcesses_other": "および後続の{{count}}件のプロセス", + "fromHistory": "を履歴から削除します。", + "codingAgentRuns_one": "{{count}}件のコーディングエージェント実行", + "codingAgentRuns_other": "{{count}}件のコーディングエージェント実行", + "scriptProcesses_one": "{{count}}件のスクリプトプロセス", + "scriptProcesses_other": "{{count}}件のスクリプトプロセス", + "setupCleanupBreakdown": "(セットアップ{{setup}}件、クリーンアップ{{cleanup}}件)", + "permanentWarning": "この操作は履歴を永久に変更し、元に戻すことはできません。" + }, + "uncommittedChanges": { + "title": "未コミットの変更が検出されました", + "description_one": "{{count}}件の未コミットの変更があります", + "description_other": "{{count}}件の未コミットの変更があります", + "andUntracked_one": "および{{count}}件の未追跡ファイル", + "andUntracked_other": "および{{count}}件の未追跡ファイル", + "acknowledgeLabel": "これらの変更が影響を受ける可能性があることを理解しています" + }, + "resetWorktree": { + "title": "ワークツリーをリセット", + "enabled": "有効", + "disabled": "無効", + "disabledUncommitted": "無効(未コミットの変更が検出されました)", + "restoreDescription": "ワークツリーはこのコミットに復元されます。", + "rollbackCommits_one": "現在のHEADから{{count}}件のコミットをロールバックします。", + "rollbackCommits_other": "現在のHEADから{{count}}件のコミットをロールバックします。", + "discardChanges_one": "{{count}}件の未コミットの変更を破棄します。", + "discardChanges_other": "{{count}}件の未コミットの変更を破棄します。", + "untrackedPresent_one": "{{count}}件の未追跡ファイルがあります(リセットの影響を受けません)。", + "untrackedPresent_other": "{{count}}件の未追跡ファイルがあります(リセットの影響を受けません)。", + "forceReset": "強制リセット(未コミットの変更を破棄)", + "uncommittedWillDiscard": "未コミットの変更は破棄されます。", + "uncommittedPresentHint": "未コミットの変更があります。強制リセットをオンにするか、コミット/スタッシュしてから続行してください。" + }, + "buttons": { + "retry": "リトライ" + } } } diff --git a/frontend/src/i18n/locales/ko/tasks.json b/frontend/src/i18n/locales/ko/tasks.json index 6320d9be..b2887974 100644 --- a/frontend/src/i18n/locales/ko/tasks.json +++ b/frontend/src/i18n/locales/ko/tasks.json @@ -442,5 +442,48 @@ "continueEditing": "계속 수정", "discardChanges": "변경사항 버리기" } + }, + "restoreLogsDialog": { + "title": "재시도 확인", + "historyChange": { + "title": "히스토리 변경", + "willDelete": "이 프로세스를 삭제합니다", + "andLaterProcesses_one": "및 이후 {{count}}개 프로세스", + "andLaterProcesses_other": "및 이후 {{count}}개 프로세스", + "fromHistory": "를 히스토리에서 삭제합니다.", + "codingAgentRuns_one": "{{count}}개 코딩 에이전트 실행", + "codingAgentRuns_other": "{{count}}개 코딩 에이전트 실행", + "scriptProcesses_one": "{{count}}개 스크립트 프로세스", + "scriptProcesses_other": "{{count}}개 스크립트 프로세스", + "setupCleanupBreakdown": "(설정 {{setup}}개, 정리 {{cleanup}}개)", + "permanentWarning": "이 작업은 히스토리를 영구적으로 변경하며 되돌릴 수 없습니다." + }, + "uncommittedChanges": { + "title": "커밋되지 않은 변경사항 감지됨", + "description_one": "{{count}}개의 커밋되지 않은 변경사항이 있습니다", + "description_other": "{{count}}개의 커밋되지 않은 변경사항이 있습니다", + "andUntracked_one": " 및 {{count}}개의 추적되지 않은 파일", + "andUntracked_other": " 및 {{count}}개의 추적되지 않은 파일", + "acknowledgeLabel": "이 변경사항이 영향을 받을 수 있음을 이해합니다" + }, + "resetWorktree": { + "title": "워크트리 초기화", + "enabled": "활성화", + "disabled": "비활성화", + "disabledUncommitted": "비활성화 (커밋되지 않은 변경사항 감지됨)", + "restoreDescription": "워크트리가 이 커밋으로 복원됩니다.", + "rollbackCommits_one": "현재 HEAD에서 {{count}}개 커밋을 롤백합니다.", + "rollbackCommits_other": "현재 HEAD에서 {{count}}개 커밋을 롤백합니다.", + "discardChanges_one": "{{count}}개의 커밋되지 않은 변경사항을 버립니다.", + "discardChanges_other": "{{count}}개의 커밋되지 않은 변경사항을 버립니다.", + "untrackedPresent_one": "{{count}}개의 추적되지 않은 파일이 있습니다 (초기화의 영향을 받지 않음).", + "untrackedPresent_other": "{{count}}개의 추적되지 않은 파일이 있습니다 (초기화의 영향을 받지 않음).", + "forceReset": "강제 초기화 (커밋되지 않은 변경사항 버리기)", + "uncommittedWillDiscard": "커밋되지 않은 변경사항이 버려집니다.", + "uncommittedPresentHint": "커밋되지 않은 변경사항이 있습니다. 강제 초기화를 켜거나 커밋/스태시한 후 진행하세요." + }, + "buttons": { + "retry": "재시도" + } } } diff --git a/frontend/src/i18n/locales/zh-Hans/tasks.json b/frontend/src/i18n/locales/zh-Hans/tasks.json index f2eab558..561ab891 100644 --- a/frontend/src/i18n/locales/zh-Hans/tasks.json +++ b/frontend/src/i18n/locales/zh-Hans/tasks.json @@ -442,5 +442,48 @@ "continueEditing": "继续编辑", "discardChanges": "放弃更改" } + }, + "restoreLogsDialog": { + "title": "确认重试", + "historyChange": { + "title": "历史记录变更", + "willDelete": "将删除此进程", + "andLaterProcesses_one": "及后续 {{count}} 个进程", + "andLaterProcesses_other": "及后续 {{count}} 个进程", + "fromHistory": "从历史记录中删除。", + "codingAgentRuns_one": "{{count}} 个编程代理运行", + "codingAgentRuns_other": "{{count}} 个编程代理运行", + "scriptProcesses_one": "{{count}} 个脚本进程", + "scriptProcesses_other": "{{count}} 个脚本进程", + "setupCleanupBreakdown": "({{setup}} 个设置,{{cleanup}} 个清理)", + "permanentWarning": "此操作将永久更改历史记录,无法撤消。" + }, + "uncommittedChanges": { + "title": "检测到未提交的更改", + "description_one": "您有 {{count}} 个未提交的更改", + "description_other": "您有 {{count}} 个未提交的更改", + "andUntracked_one": "和 {{count}} 个未跟踪的文件", + "andUntracked_other": "和 {{count}} 个未跟踪的文件", + "acknowledgeLabel": "我了解这些更改可能会受到影响" + }, + "resetWorktree": { + "title": "重置工作树", + "enabled": "已启用", + "disabled": "已禁用", + "disabledUncommitted": "已禁用(检测到未提交的更改)", + "restoreDescription": "您的工作树将被恢复到此提交。", + "rollbackCommits_one": "从当前 HEAD 回滚 {{count}} 个提交。", + "rollbackCommits_other": "从当前 HEAD 回滚 {{count}} 个提交。", + "discardChanges_one": "丢弃 {{count}} 个未提交的更改。", + "discardChanges_other": "丢弃 {{count}} 个未提交的更改。", + "untrackedPresent_one": "存在 {{count}} 个未跟踪的文件(不受重置影响)。", + "untrackedPresent_other": "存在 {{count}} 个未跟踪的文件(不受重置影响)。", + "forceReset": "强制重置(丢弃未提交的更改)", + "uncommittedWillDiscard": "未提交的更改将被丢弃。", + "uncommittedPresentHint": "存在未提交的更改。请打开强制重置或提交/暂存后继续。" + }, + "buttons": { + "retry": "重试" + } } }