diff --git a/src/__tests__/taskRetryActions.test.ts b/src/__tests__/taskRetryActions.test.ts index ea172fb..5eee5fb 100644 --- a/src/__tests__/taskRetryActions.test.ts +++ b/src/__tests__/taskRetryActions.test.ts @@ -70,7 +70,11 @@ vi.mock('../features/interactive/index.js', () => ({ loadRunSessionContext: vi.fn(), getRunPaths: vi.fn(() => ({ logsDir: '/tmp/logs', reportsDir: '/tmp/reports' })), formatRunSessionForPrompt: vi.fn(() => ({ - runTask: '', runPiece: '', runStatus: '', runMovementLogs: '', runReports: '', + runTask: '', + runPiece: 'default', + runStatus: '', + runMovementLogs: '', + runReports: '', })), runRetryMode: (...args: unknown[]) => mockRunRetryMode(...args), findPreviousOrderContent: vi.fn(() => null), @@ -91,6 +95,17 @@ vi.mock('../features/tasks/execute/taskExecution.js', () => ({ executeAndCompleteTask: (...args: unknown[]) => mockExecuteAndCompleteTask(...args), })); +vi.mock('../shared/i18n/index.js', () => ({ + getLabel: vi.fn((key: string) => { + const labels: Record = { + 'retry.workflowPrompt': 'Select workflow:', + 'retry.usePreviousWorkflow': 'Use previous', + 'retry.changeWorkflow': 'Change workflow', + }; + return labels[key] ?? key; + }), +})); + import { retryFailedTask } from '../features/tasks/list/taskRetryActions.js'; import type { TaskListItem } from '../infra/task/types.js'; import type { PieceConfig } from '../core/models/index.js'; @@ -262,4 +277,54 @@ describe('retryFailedTask', () => { expect(mockRequeueTask).toHaveBeenCalledWith('my-task', ['failed'], undefined, '既存ノート\n\n追加指示A'); }); + + describe('when previous workflow exists', () => { + beforeEach(() => { + mockFindRunForTask.mockReturnValue('run-123'); + }); + + it('should show workflow selection prompt when runInfo.piece exists', async () => { + const task = makeFailedTask(); + + await retryFailedTask(task, '/project'); + + expect(mockSelectOptionWithDefault).toHaveBeenCalledWith( + 'Select workflow:', + expect.arrayContaining([ + expect.objectContaining({ value: 'use_previous' }), + expect.objectContaining({ value: 'change' }), + ]), + 'use_previous', + ); + }); + + it('should use previous workflow when use_previous is selected', async () => { + const task = makeFailedTask(); + mockSelectOptionWithDefault.mockResolvedValue('use_previous'); + + await retryFailedTask(task, '/project'); + + expect(mockSelectPiece).not.toHaveBeenCalled(); + expect(mockLoadPieceByIdentifier).toHaveBeenCalledWith('default', '/project'); + }); + + it('should call selectPiece when change is selected', async () => { + const task = makeFailedTask(); + mockSelectOptionWithDefault.mockResolvedValue('change'); + + await retryFailedTask(task, '/project'); + + expect(mockSelectPiece).toHaveBeenCalledWith('/project'); + }); + + it('should return false when workflow selection is cancelled', async () => { + const task = makeFailedTask(); + mockSelectOptionWithDefault.mockResolvedValue(null); + + const result = await retryFailedTask(task, '/project'); + + expect(result).toBe(false); + expect(mockLoadPieceByIdentifier).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/features/tasks/list/taskRetryActions.ts b/src/features/tasks/list/taskRetryActions.ts index 88aa354..565faf6 100644 --- a/src/features/tasks/list/taskRetryActions.ts +++ b/src/features/tasks/list/taskRetryActions.ts @@ -11,6 +11,7 @@ import { TaskRunner } from '../../../infra/task/index.js'; import { loadPieceByIdentifier, resolvePieceConfigValue, getPieceDescription } from '../../../infra/config/index.js'; import { selectPiece } from '../../pieceSelection/index.js'; import { selectOptionWithDefault } from '../../../shared/prompt/index.js'; +import { getLabel } from '../../../shared/i18n/index.js'; import { info, header, blankLine, status } from '../../../shared/ui/index.js'; import { createLogger } from '../../../shared/utils/index.js'; import type { PieceConfig } from '../../../core/models/index.js'; @@ -128,10 +129,44 @@ export async function retryFailedTask( displayFailureInfo(task); - const selectedPiece = await selectPiece(projectDir); - if (!selectedPiece) { - info('Cancelled'); - return false; + const matchedSlug = findRunForTask(worktreePath, task.content); + const runInfo = matchedSlug ? buildRetryRunInfo(worktreePath, matchedSlug) : null; + + let selectedPiece: string; + if (runInfo?.piece) { + const usePreviousLabel = getLabel('retry.usePreviousWorkflow'); + const changeWorkflowLabel = getLabel('retry.changeWorkflow'); + const choice = await selectOptionWithDefault( + getLabel('retry.workflowPrompt'), + [ + { label: `${runInfo.piece} - ${usePreviousLabel}`, value: 'use_previous' }, + { label: changeWorkflowLabel, value: 'change' }, + ], + 'use_previous', + ); + + if (choice === null) { + info('Cancelled'); + return false; + } + + if (choice === 'use_previous') { + selectedPiece = runInfo.piece; + } else { + const selected = await selectPiece(projectDir); + if (!selected) { + info('Cancelled'); + return false; + } + selectedPiece = selected; + } + } else { + const selected = await selectPiece(projectDir); + if (!selected) { + info('Cancelled'); + return false; + } + selectedPiece = selected; } const previewCount = resolvePieceConfigValue(projectDir, 'interactivePreviewMovements'); @@ -155,8 +190,6 @@ export async function retryFailedTask( }; // Runs data lives in the worktree (written during previous execution) - const matchedSlug = findRunForTask(worktreePath, task.content); - const runInfo = matchedSlug ? buildRetryRunInfo(worktreePath, matchedSlug) : null; const previousOrderContent = findPreviousOrderContent(worktreePath, matchedSlug); blankLine(); diff --git a/src/shared/i18n/labels_en.yaml b/src/shared/i18n/labels_en.yaml index 74bb946..07662c6 100644 --- a/src/shared/i18n/labels_en.yaml +++ b/src/shared/i18n/labels_en.yaml @@ -96,6 +96,9 @@ instruct: retry: ui: intro: "Retry mode - describe additional instructions. Commands: /go (create instruction & run), /retry (rerun previous order), /cancel (exit)" + workflowPrompt: "Select workflow:" + usePreviousWorkflow: "Use previous" + changeWorkflow: "Change workflow" run: notifyComplete: "Run complete ({total} tasks)" diff --git a/src/shared/i18n/labels_ja.yaml b/src/shared/i18n/labels_ja.yaml index 6b27047..51e8c3c 100644 --- a/src/shared/i18n/labels_ja.yaml +++ b/src/shared/i18n/labels_ja.yaml @@ -96,6 +96,9 @@ instruct: retry: ui: intro: "リトライモード - 追加指示を入力してください。コマンド: /go(指示書作成・実行), /retry(前回の指示書で再実行), /cancel(終了)" + workflowPrompt: "ワークフローを選択:" + usePreviousWorkflow: "前回のまま使用" + changeWorkflow: "ワークフローを変更" run: notifyComplete: "run完了 ({total} tasks)"