feat: add error feedback and unstick functionality for stuck tasks#1859
feat: add error feedback and unstick functionality for stuck tasks#1859dertuerke wants to merge 8 commits into
Conversation
When task recovery fails (e.g., stuck subtasks with wrong file paths), the UI now provides clear feedback and an option to unstick subtasks. Changes: - Add error toast with stuck subtask count when recovery fails - Add "Unstick Subtasks" button in TaskWarnings component - Display stuck subtask reasons (file validation failures, etc.) - Add CLI --unstick command for manual recovery - Add get_stuck_subtasks() method to RecoveryManager Backend: - apps/backend/cli/main.py: Add --unstick flag - apps/backend/services/recovery.py: Add get_stuck_subtasks() Frontend: - Add IPC handlers TASK_GET_STUCK_INFO and TASK_UNSTICK_SUBTASKS - Add preload APIs getStuckInfo and unstickSubtasks - Update TaskWarnings to show stuck subtasks with unstick button - Update TaskCard to show error toast on recovery failure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary of ChangesHello @dertuerke, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly improves the handling of 'stuck' tasks by providing users with better visibility into why tasks are stalled and offering direct mechanisms to resolve these issues. It integrates both frontend and backend changes to deliver a more robust recovery experience, ensuring tasks can resume smoothly even after encountering unexpected states. Highlights
Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds stuck-subtask discovery and manual "unstick" flows across backend (service + CLI) and frontend (IPC, preload API, renderer UI, types, mocks, and i18n). CLI gains --unstick to list/clear stuck subtasks and the normal build flow is bypassed when used. Changes
Sequence DiagramsequenceDiagram
actor User
participant UI as Frontend UI
participant Preload as Preload/API
participant Main as Main (IPC)
participant Recovery as Recovery Service
participant FS as File System
User->>UI: Open warnings / recovery fails
UI->>Preload: getStuckInfo(projectId, specId)
Preload->>Main: TASK_GET_STUCK_INFO (projectId, specId)
Main->>Recovery: get_stuck_subtasks(spec_dir, project_dir)
Recovery->>FS: read attempt_history.json
FS-->>Recovery: attempt history (stuck_subtasks)
Recovery-->>Main: { stuckSubtasks }
Main-->>Preload: { stuckSubtasks }
Preload-->>UI: { stuckSubtasks }
UI->>User: Render stuck list
User->>UI: Click "Unstick"
UI->>Preload: unstickSubtasks(projectId, specId)
Preload->>Main: TASK_UNSTICK_SUBTASKS (projectId, specId)
Main->>Recovery: clear_stuck_subtasks(spec_dir, project_dir)
Recovery->>FS: write attempt_history.json (clear stuck), update subtask statuses
FS-->>Recovery: persisted
Recovery-->>Main: { cleared: N }
Main-->>Preload: { cleared: N }
Preload-->>UI: { cleared: N }
UI->>User: Show toast and refresh UI
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly Related PRs
Suggested Labels
Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Code Review
This pull request introduces a helpful feature for handling stuck tasks by providing error feedback in the UI and a mechanism to "unstick" them. The changes span both the backend and frontend, adding a new CLI flag, IPC handlers, and UI components.
My review focuses on two main points:
- A suggestion to improve code clarity in the new CLI command handling in
apps/backend/cli/main.py. - A more significant concern about logic duplication in
apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts, where logic for handling stuck tasks is reimplemented in TypeScript instead of leveraging the Python backend. Centralizing this logic would improve maintainability.
Overall, the feature is a valuable addition for robustness. The frontend changes, in particular, are well-implemented to provide better user feedback.
| ipcMain.handle( | ||
| IPC_CHANNELS.TASK_GET_STUCK_INFO, | ||
| async (_, projectId: string, specId: string): Promise<IPCResult<{ stuckSubtasks: Array<{ subtask_id: string; reason: string; escalated_at: string; attempt_count: number }> }>> => { | ||
| try { | ||
| const project = projectStore.getProject(projectId); | ||
| if (!project) { | ||
| return { success: false, error: 'Project not found' }; | ||
| } | ||
|
|
||
| const specsBaseDir = getSpecsDir(project.autoBuildPath); | ||
| const specDir = path.join(project.path, specsBaseDir, specId); | ||
| const attemptHistoryPath = path.join(specDir, 'memory', 'attempt_history.json'); | ||
|
|
||
| if (!existsSync(attemptHistoryPath)) { | ||
| return { success: true, data: { stuckSubtasks: [] } }; | ||
| } | ||
|
|
||
| const historyContent = safeReadFileSync(attemptHistoryPath); | ||
| if (!historyContent) { | ||
| return { success: true, data: { stuckSubtasks: [] } }; | ||
| } | ||
|
|
||
| const history = JSON.parse(historyContent); | ||
| return { success: true, data: { stuckSubtasks: history.stuck_subtasks || [] } }; | ||
| } catch (error) { | ||
| console.error('Failed to get stuck info:', error); | ||
| return { success: false, error: error instanceof Error ? error.message : 'Failed to get stuck info' }; | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| /** | ||
| * Clear stuck subtasks for a spec | ||
| */ | ||
| ipcMain.handle( | ||
| IPC_CHANNELS.TASK_UNSTICK_SUBTASKS, | ||
| async (_, projectId: string, specId: string): Promise<IPCResult<{ cleared: number }>> => { | ||
| try { | ||
| const project = projectStore.getProject(projectId); | ||
| if (!project) { | ||
| return { success: false, error: 'Project not found' }; | ||
| } | ||
|
|
||
| const specsBaseDir = getSpecsDir(project.autoBuildPath); | ||
| const specDir = path.join(project.path, specsBaseDir, specId); | ||
| const attemptHistoryPath = path.join(specDir, 'memory', 'attempt_history.json'); | ||
|
|
||
| if (!existsSync(attemptHistoryPath)) { | ||
| return { success: true, data: { cleared: 0 } }; | ||
| } | ||
|
|
||
| const historyContent = safeReadFileSync(attemptHistoryPath); | ||
| if (!historyContent) { | ||
| return { success: true, data: { cleared: 0 } }; | ||
| } | ||
|
|
||
| const history = JSON.parse(historyContent); | ||
| const count = (history.stuck_subtasks || []).length; | ||
|
|
||
| // Clear stuck subtasks list | ||
| history.stuck_subtasks = []; | ||
|
|
||
| // Reset any subtasks marked as 'stuck' to 'pending' | ||
| if (history.subtasks) { | ||
| for (const subtaskId in history.subtasks) { | ||
| if (history.subtasks[subtaskId].status === 'stuck') { | ||
| history.subtasks[subtaskId].status = 'pending'; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Save updated history | ||
| writeFileAtomicSync(attemptHistoryPath, JSON.stringify(history, null, 2)); | ||
|
|
||
| // Also update worktree copy if it exists | ||
| const worktreePath = findTaskWorktree(project.path, specId); | ||
| if (worktreePath) { | ||
| const worktreeSpecDir = path.join(worktreePath, specsBaseDir, specId); | ||
| const worktreeHistoryPath = path.join(worktreeSpecDir, 'memory', 'attempt_history.json'); | ||
| if (existsSync(worktreeHistoryPath)) { | ||
| writeFileAtomicSync(worktreeHistoryPath, JSON.stringify(history, null, 2)); | ||
| } | ||
| } | ||
|
|
||
| console.log(`[Unstick] Cleared ${count} stuck subtasks for ${specId}`); | ||
| return { success: true, data: { cleared: count } }; | ||
| } catch (error) { | ||
| console.error('Failed to unstick subtasks:', error); | ||
| return { success: false, error: error instanceof Error ? error.message : 'Failed to unstick subtasks' }; | ||
| } | ||
| } | ||
| ); |
There was a problem hiding this comment.
These new IPC handlers (TASK_GET_STUCK_INFO and TASK_UNSTICK_SUBTASKS) duplicate logic that exists or should exist in the Python backend. For example, they directly read and manipulate attempt_history.json, which is also handled by services/recovery.py.
Duplicating this logic across the TypeScript main process and the Python backend can lead to inconsistencies and maintenance issues. For instance, the logic in TASK_UNSTICK_SUBTASKS to reset subtask statuses from 'stuck' to 'pending' is more complete than the current Python clear_stuck_subtasks function, which only clears the stuck_subtasks array.
To improve maintainability and ensure a single source of truth, this logic should be centralized in the Python backend. The services/recovery.py module should be updated to fully handle unsticking tasks (including resetting statuses), and these IPC handlers should then execute the backend logic, for example, by shelling out to a CLI command. The --unstick flag has already been added, and a similar one could be added for getting stuck info.
| if args.unstick: | ||
| from services.recovery import get_stuck_subtasks as get_stuck | ||
| from services.recovery import clear_stuck_subtasks as clear_stuck | ||
|
|
||
| stuck = get_stuck(spec_dir, project_dir) | ||
| if stuck: | ||
| clear_stuck(spec_dir, project_dir) | ||
| print(f"Cleared {len(stuck)} stuck subtasks for {args.spec}") | ||
| for s in stuck: | ||
| print(f" - {s.get('subtask_id', 'unknown')}: {s.get('reason', 'no reason')[:80]}") | ||
| else: | ||
| print(f"No stuck subtasks found for {args.spec}") | ||
| return |
There was a problem hiding this comment.
The aliases get_stuck and clear_stuck can be confusing, especially since clear_stuck is used as a function name but it's an alias for clear_stuck_subtasks. For better readability and maintainability, it's recommended to use the full function names directly by changing the import statement and the subsequent calls.
| if args.unstick: | |
| from services.recovery import get_stuck_subtasks as get_stuck | |
| from services.recovery import clear_stuck_subtasks as clear_stuck | |
| stuck = get_stuck(spec_dir, project_dir) | |
| if stuck: | |
| clear_stuck(spec_dir, project_dir) | |
| print(f"Cleared {len(stuck)} stuck subtasks for {args.spec}") | |
| for s in stuck: | |
| print(f" - {s.get('subtask_id', 'unknown')}: {s.get('reason', 'no reason')[:80]}") | |
| else: | |
| print(f"No stuck subtasks found for {args.spec}") | |
| return | |
| if args.unstick: | |
| from services.recovery import get_stuck_subtasks, clear_stuck_subtasks | |
| stuck = get_stuck_subtasks(spec_dir, project_dir) | |
| if stuck: | |
| clear_stuck_subtasks(spec_dir, project_dir) | |
| print(f"Cleared {len(stuck)} stuck subtasks for {args.spec}") | |
| for s in stuck: | |
| print(f" - {s.get('subtask_id', 'unknown')}: {s.get('reason', 'no reason')[:80]}") | |
| else: | |
| print(f"No stuck subtasks found for {args.spec}") | |
| return |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/backend/cli/main.py`:
- Around line 476-477: The two separate imports from the same module should be
combined into a single import to satisfy the linter: replace the two lines
importing get_stuck_subtasks and clear_stuck_subtasks from services.recovery
with one statement that imports both symbols (get_stuck_subtasks,
clear_stuck_subtasks) in a single from ... import ... line; update any local
aliasing if needed (e.g., if you previously aliased one as get_stuck or
clear_stuck, preserve those aliases in the combined import).
In `@apps/backend/services/recovery.py`:
- Around line 580-588: The file defines duplicate methods named
get_stuck_subtasks on class RecoveryManager; remove the earlier/first definition
so only the later implementation (with the more detailed docstring and the call
to self._load_attempt_history) remains. Locate the first get_stuck_subtasks
definition in class RecoveryManager and delete that entire method block,
ensuring there is only one get_stuck_subtasks method (the version returning
history.get("stuck_subtasks", [])) to resolve the Ruff F811 duplicate-definition
error.
In `@apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts`:
- Around line 1228-1257: Remove the redundant existsSync check and rely on
safeReadFileSync's ENOENT handling: in the IPC handler registered under
IPC_CHANNELS.TASK_GET_STUCK_INFO, delete the existsSync(attemptHistoryPath)
branch and simply call safeReadFileSync(attemptHistoryPath), return an empty
stuckSubtasks array when safeReadFileSync returns null/empty, then parse JSON
when non-null; apply the same simplification to the TASK_UNSTICK_SUBTASKS
handler (remove existsSync usage and handle null from safeReadFileSync before
parsing) so there is no TOCTOU gap and behavior remains the same.
- Around line 1284-1297: The loop that iterates subtasks using "for (const
subtaskId in history.subtasks)" can pick up prototype-inherited keys; change it
to iterate only own keys (e.g., Object.keys(history.subtasks).forEach(...) or
use Object.hasOwn(history.subtasks, subtaskId) inside the loop) and preserve the
existing behavior of checking status === 'stuck' and setting it to
'pending'—apply this change where history is parsed and the subtasks loop is
implemented to defensively guard against inherited properties.
In `@apps/frontend/src/renderer/components/task-detail/TaskWarnings.tsx`:
- Around line 43-56: The effect may apply stale getStuckInfo results when
projectId/specId change quickly; fix useEffect by introducing a local
canceled/ignore flag (e.g., let cancelled = false) inside the useEffect, pass
projectId/specId to getStuckInfo as before, and before calling
setStuckSubtasks(...) or setIsLoadingStuck(false) ensure !cancelled; in the
cleanup return () => { cancelled = true } so resolved promises from prior calls
don't overwrite newer state (also guard the else branch behavior accordingly).
- Around line 5-6: Replace the deep relative imports in TaskWarnings.tsx with
the project's tsconfig path aliases: change imports of getStuckInfo and
unstickSubtasks from '../../stores/task-store' to '@/stores/task-store' (maps to
src/renderer/stores) and change useToast from '../../hooks/use-toast' to
'@hooks/use-toast' (maps to src/renderer/shared/hooks); update the import
statements where getStuckInfo, unstickSubtasks, and useToast are referenced so
module resolution uses the configured aliases.
In `@apps/frontend/src/renderer/stores/task-store.ts`:
- Around line 988-999: The function unstickSubtasks currently returns the raw
IPCResult from window.electronAPI.unstickSubtasks, which nests the payload under
result.data so callers never see cleared; update unstickSubtasks to inspect the
returned result, extract cleared from result.data (safely handling when data is
undefined), and return a flattened object like { success: result.success,
cleared: result.data?.cleared, error: result.error } while preserving the
existing try/catch error handling; reference the unstickSubtasks function and
the call to window.electronAPI.unstickSubtasks when making the change (see
getStuckInfo for the correct extraction pattern).
In `@apps/frontend/src/shared/types/ipc.ts`:
- Around line 208-209: The duplicated inline stuck-subtask shape should be
extracted to a single shared interface (e.g., export interface StuckSubtaskInfo
{ subtask_id: string; reason: string; escalated_at: string; attempt_count:
number }) placed in the shared types module (suggested:
apps/frontend/src/shared/types/task.ts), then replace the inline type
occurrences in IPC signatures getStuckInfo and unstickSubtasks (in ipc.ts) and
the corresponding types/usages in task-api.ts, task-store.ts, and
TaskWarnings.tsx to reference StuckSubtaskInfo (and update any IPCResult
generics to use Array<StuckSubtaskInfo>) so all four locations share the same
definition.
| getStuckInfo: (projectId: string, specId: string) => Promise<IPCResult<{ stuckSubtasks: Array<{ subtask_id: string; reason: string; escalated_at: string; attempt_count: number }> }>>; | ||
| unstickSubtasks: (projectId: string, specId: string) => Promise<IPCResult<{ cleared: number }>>; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Extract the duplicated stuck-subtask shape into a shared type.
The inline type { subtask_id: string; reason: string; escalated_at: string; attempt_count: number } is repeated verbatim in at least four places: ipc.ts, task-api.ts, task-store.ts, and TaskWarnings.tsx. A single shared interface would reduce duplication and make future changes (e.g., adding fields) safer.
♻️ Suggested approach
Define a shared type in apps/frontend/src/shared/types/task.ts (or wherever TaskRecoveryResult lives):
export interface StuckSubtaskInfo {
subtask_id: string;
reason: string;
escalated_at: string;
attempt_count: number;
}Then reference it in all four locations:
- getStuckInfo: (projectId: string, specId: string) => Promise<IPCResult<{ stuckSubtasks: Array<{ subtask_id: string; reason: string; escalated_at: string; attempt_count: number }> }>>;
+ getStuckInfo: (projectId: string, specId: string) => Promise<IPCResult<{ stuckSubtasks: StuckSubtaskInfo[] }>>;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/frontend/src/shared/types/ipc.ts` around lines 208 - 209, The duplicated
inline stuck-subtask shape should be extracted to a single shared interface
(e.g., export interface StuckSubtaskInfo { subtask_id: string; reason: string;
escalated_at: string; attempt_count: number }) placed in the shared types module
(suggested: apps/frontend/src/shared/types/task.ts), then replace the inline
type occurrences in IPC signatures getStuckInfo and unstickSubtasks (in ipc.ts)
and the corresponding types/usages in task-api.ts, task-store.ts, and
TaskWarnings.tsx to reference StuckSubtaskInfo (and update any IPCResult
generics to use Array<StuckSubtaskInfo>) so all four locations share the same
definition.
- Fix Ruff I001: combine duplicate imports in cli/main.py - Fix Ruff F811: remove duplicate get_stuck_subtasks method in recovery.py - Fix bug where unstickSubtasks toast never fired (return raw IPCResult without flattening) - Add race condition fix in TaskWarnings useEffect with ignore flag - Remove redundant existsSync checks (TOCTOU fix) - Use Object.keys() instead of for...in for prototype safety - Use path aliases instead of deep relative imports - Extract StuckSubtaskInfo into shared type Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/backend/cli/main.py`:
- Around line 474-486: The long f-strings in the --unstick block (when
args.unstick) are tripping Ruff; modify the print statements around
get_stuck_subtasks and clear_stuck_subtasks to break long f-strings into shorter
expressions or concatenated parts (e.g., assign the message to a local variable
like msg = f"...{args.spec}" or use two prints), and truncate the reason using
slicing or textwrap.shorten into a separate variable before printing; update the
calls that print each stuck subtask (the f"{s.get('subtask_id', 'unknown')}:
{s.get('reason', 'no reason')[:80]}") to build the subtask_id and reason into
small variables and then print them to satisfy Ruff line-length/formatting
checks.
In `@apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts`:
- Around line 1233-1251: Replace hardcoded English error strings in this IPC
handler with i18n keys and optional params: instead of returning { success:
false, error: 'Project not found' } return something like { success: false,
errorKey: 'error.projectNotFound' }, and in the catch block return { success:
false, errorKey: 'error.failedToGetStuckInfo', errorParams: { message: error
instanceof Error ? error.message : undefined } }; update any other similar
returns in this function (references: project, specsBaseDir, specDir,
attemptHistoryPath, history) and ensure corresponding keys are added to both
en/*.json and fr/*.json; keep translation responsibility in the renderer (use
react-i18next to translate errorKey + errorParams when showing toasts).
In `@apps/frontend/src/renderer/components/task-detail/TaskWarnings.tsx`:
- Around line 96-99: The JSX currently hardcodes the truncation suffix "..." for
stuck.reason in TaskWarnings.tsx; replace the hardcoded string by using the
react-i18next translator (e.g., call t('taskWarnings.truncationSuffix') or
similar) where the suffix is appended, and add the corresponding key/value to
both the en and fr JSON resource files (e.g., "taskWarnings": {
"truncationSuffix": "..." } and the French equivalent); alternatively, remove
the manual suffix and rely on CSS truncation (keep text in <span
className="truncate">) if preferred, but ensure all user-facing text uses
react-i18next and the new key is added to both en and fr locales.
In `@apps/frontend/src/renderer/stores/task-store.ts`:
- Line 3: Replace the relative import of shared types with the frontend path
alias: change the import that currently pulls "Task, TaskStatus, SubtaskStatus,
ImplementationPlan, Subtask, TaskMetadata, ExecutionProgress, ExecutionPhase,
ReviewReason, TaskDraft, ImageAttachment, TaskOrderState, StuckSubtaskInfo" from
'../../shared/types' to use the '@shared' alias (e.g., '@shared/types') so the
module resolution follows the project's tsconfig alias conventions; update the
import statement in task-store.ts where those symbols are referenced.
| if (!project) { | ||
| return { success: false, error: 'Project not found' }; | ||
| } | ||
|
|
||
| const specsBaseDir = getSpecsDir(project.autoBuildPath); | ||
| const specDir = path.join(project.path, specsBaseDir, specId); | ||
| const attemptHistoryPath = path.join(specDir, 'memory', 'attempt_history.json'); | ||
|
|
||
| const historyContent = safeReadFileSync(attemptHistoryPath); | ||
| if (!historyContent) { | ||
| return { success: true, data: { stuckSubtasks: [] } }; | ||
| } | ||
|
|
||
| const history = JSON.parse(historyContent); | ||
| return { success: true, data: { stuckSubtasks: history.stuck_subtasks || [] } }; | ||
| } catch (error) { | ||
| console.error('Failed to get stuck info:', error); | ||
| return { success: false, error: error instanceof Error ? error.message : 'Failed to get stuck info' }; | ||
| } |
There was a problem hiding this comment.
Return i18n keys (not hardcoded strings) in IPC error payloads.
These error messages are surfaced to the renderer (e.g., toast descriptions), so they should be localized. Prefer returning an error key/params and translating in the UI layer rather than emitting raw English strings here.
As per coding guidelines: **/*.{tsx,ts}: All frontend user-facing text MUST use react-i18next translation keys. Never hardcode strings in JSX/TSX. Add keys to both en/*.json and fr/*.json.
Also applies to: 1263-1309
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts` around lines
1233 - 1251, Replace hardcoded English error strings in this IPC handler with
i18n keys and optional params: instead of returning { success: false, error:
'Project not found' } return something like { success: false, errorKey:
'error.projectNotFound' }, and in the catch block return { success: false,
errorKey: 'error.failedToGetStuckInfo', errorParams: { message: error instanceof
Error ? error.message : undefined } }; update any other similar returns in this
function (references: project, specsBaseDir, specDir, attemptHistoryPath,
history) and ensure corresponding keys are added to both en/*.json and
fr/*.json; keep translation responsibility in the renderer (use react-i18next to
translate errorKey + errorParams when showing toasts).
| <span className="font-mono">{stuck.subtask_id}</span>:{' '} | ||
| <span className="truncate" title={stuck.reason}> | ||
| {stuck.reason.length > 80 ? `${stuck.reason.slice(0, 80)}...` : stuck.reason} | ||
| </span> |
There was a problem hiding this comment.
Localize the truncation suffix instead of hardcoding "...".
Use an i18n key for the truncated form (or rely on CSS truncation) and add the key to both en and fr.
🌍 Possible i18n-safe approach
- {stuck.reason.length > 80 ? `${stuck.reason.slice(0, 80)}...` : stuck.reason}
+ {stuck.reason.length > 80
+ ? t('labels.truncatedReason', { reason: stuck.reason.slice(0, 80) })
+ : stuck.reason}As per coding guidelines: **/*.{tsx,ts}: All frontend user-facing text MUST use react-i18next translation keys. Never hardcode strings in JSX/TSX. Add keys to both en/*.json and fr/*.json.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/frontend/src/renderer/components/task-detail/TaskWarnings.tsx` around
lines 96 - 99, The JSX currently hardcodes the truncation suffix "..." for
stuck.reason in TaskWarnings.tsx; replace the hardcoded string by using the
react-i18next translator (e.g., call t('taskWarnings.truncationSuffix') or
similar) where the suffix is appended, and add the corresponding key/value to
both the en and fr JSON resource files (e.g., "taskWarnings": {
"truncationSuffix": "..." } and the French equivalent); alternatively, remove
the manual suffix and rely on CSS truncation (keep text in <span
className="truncate">) if preferred, but ensure all user-facing text uses
react-i18next and the new key is added to both en and fr locales.
| import { create } from 'zustand'; | ||
| import { arrayMove } from '@dnd-kit/sortable'; | ||
| import type { Task, TaskStatus, SubtaskStatus, ImplementationPlan, Subtask, TaskMetadata, ExecutionProgress, ExecutionPhase, ReviewReason, TaskDraft, ImageAttachment, TaskOrderState } from '../../shared/types'; | ||
| import type { Task, TaskStatus, SubtaskStatus, ImplementationPlan, Subtask, TaskMetadata, ExecutionProgress, ExecutionPhase, ReviewReason, TaskDraft, ImageAttachment, TaskOrderState, StuckSubtaskInfo } from '../../shared/types'; |
There was a problem hiding this comment.
Use the @shared path alias for shared types.
This import should follow the frontend alias conventions.
♻️ Suggested change
-import type { Task, TaskStatus, SubtaskStatus, ImplementationPlan, Subtask, TaskMetadata, ExecutionProgress, ExecutionPhase, ReviewReason, TaskDraft, ImageAttachment, TaskOrderState, StuckSubtaskInfo } from '../../shared/types';
+import type { Task, TaskStatus, SubtaskStatus, ImplementationPlan, Subtask, TaskMetadata, ExecutionProgress, ExecutionPhase, ReviewReason, TaskDraft, ImageAttachment, TaskOrderState, StuckSubtaskInfo } from '@shared/types';As per coding guidelines: apps/frontend/**/*.{ts,tsx}: Use path aliases defined in tsconfig.json: @/* → src/renderer/*, @shared/* → src/shared/*, @preload/* → src/preload/*, @features/* → src/renderer/features/*, @components/* → src/renderer/shared/components/*, @hooks/* → src/renderer/shared/hooks/*, @lib/* → src/renderer/shared/lib/*.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import type { Task, TaskStatus, SubtaskStatus, ImplementationPlan, Subtask, TaskMetadata, ExecutionProgress, ExecutionPhase, ReviewReason, TaskDraft, ImageAttachment, TaskOrderState, StuckSubtaskInfo } from '../../shared/types'; | |
| import type { Task, TaskStatus, SubtaskStatus, ImplementationPlan, Subtask, TaskMetadata, ExecutionProgress, ExecutionPhase, ReviewReason, TaskDraft, ImageAttachment, TaskOrderState, StuckSubtaskInfo } from '@shared/types'; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/frontend/src/renderer/stores/task-store.ts` at line 3, Replace the
relative import of shared types with the frontend path alias: change the import
that currently pulls "Task, TaskStatus, SubtaskStatus, ImplementationPlan,
Subtask, TaskMetadata, ExecutionProgress, ExecutionPhase, ReviewReason,
TaskDraft, ImageAttachment, TaskOrderState, StuckSubtaskInfo" from
'../../shared/types' to use the '@shared' alias (e.g., '@shared/types') so the
module resolution follows the project's tsconfig alias conventions; update the
import statement in task-store.ts where those symbols are referenced.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/backend/cli/main.py`:
- Around line 474-488: The --unstick flag is currently handled outside the
mutually-exclusive build_group so args.unstick can be ignored when combined with
other actions; add the --unstick option to the same argparse mutually-exclusive
group (build_group) where --merge/--review/--discard etc. are defined and remove
the standalone handling block that checks args.unstick in main.py, ensuring
unstick is rejected when combined with other action flags and handled uniformly
with the other build_group actions.
Ensures --unstick is rejected when combined with other action flags like --merge, --review, --discard, or --create-pr. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/backend/cli/main.py`:
- Around line 472-486: Wrap the call to clear_stuck_subtasks in a targeted
try/except inside main() around the existing --unstick handling: call
get_stuck_subtasks first as is, and when stuck is truthy, call
clear_stuck_subtasks within a try block and on exception print a clear CLI
message like "Failed to clear stuck subtasks: <error>" (including the exception
text) and return a non-zero exit or propagate appropriately; keep the existing
success prints when no exception occurs. Ensure you reference
clear_stuck_subtasks and get_stuck_subtasks and do not change the overall
control flow for the --unstick branch.
Wraps the clear_stuck_subtasks call in try/except and exits with non-zero status on failure, providing clear error message to user. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/backend/cli/main.py`:
- Around line 472-490: The current unstick flow calls get_stuck_subtasks outside
the try/except, so exceptions from get_stuck_subtasks bubble up; move the call
to get_stuck_subtasks(spec_dir, project_dir) inside the same try block that
calls clear_stuck_subtasks, and catch exceptions there (same except Exception as
e) to print a contextual "Failed to clear stuck subtasks: {e}" message and
sys.exit(1); keep the existing logic that prints "No stuck subtasks found for
{args.spec}" when get_stuck_subtasks returns empty.
| # Handle --unstick command | ||
| if args.unstick: | ||
| from services.recovery import clear_stuck_subtasks, get_stuck_subtasks | ||
|
|
||
| stuck = get_stuck_subtasks(spec_dir, project_dir) | ||
| if stuck: | ||
| try: | ||
| clear_stuck_subtasks(spec_dir, project_dir) | ||
| print(f"Cleared {len(stuck)} stuck subtasks for {args.spec}") | ||
| for s in stuck: | ||
| print( | ||
| f" - {s.get('subtask_id', 'unknown')}: {s.get('reason', 'no reason')[:80]}" | ||
| ) | ||
| except Exception as e: | ||
| print(f"Failed to clear stuck subtasks: {e}") | ||
| sys.exit(1) | ||
| else: | ||
| print(f"No stuck subtasks found for {args.spec}") | ||
| return |
There was a problem hiding this comment.
Wrap get_stuck_subtasks in the same error-handling block.
If get_stuck_subtasks on line 476 raises (e.g., corrupted/missing history file), the exception bubbles to the outer main() handler and the user sees a generic "Unexpected error: ..." instead of a contextual CLI message. Consider moving it inside the try block for a consistent experience.
Proposed fix
# Handle --unstick command
if args.unstick:
from services.recovery import clear_stuck_subtasks, get_stuck_subtasks
- stuck = get_stuck_subtasks(spec_dir, project_dir)
- if stuck:
- try:
+ try:
+ stuck = get_stuck_subtasks(spec_dir, project_dir)
+ if stuck:
clear_stuck_subtasks(spec_dir, project_dir)
print(f"Cleared {len(stuck)} stuck subtasks for {args.spec}")
for s in stuck:
print(
f" - {s.get('subtask_id', 'unknown')}: {s.get('reason', 'no reason')[:80]}"
)
- except Exception as e:
- print(f"Failed to clear stuck subtasks: {e}")
- sys.exit(1)
- else:
- print(f"No stuck subtasks found for {args.spec}")
+ else:
+ print(f"No stuck subtasks found for {args.spec}")
+ except Exception as e:
+ print(f"Failed to clear stuck subtasks: {e}")
+ sys.exit(1)
return📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Handle --unstick command | |
| if args.unstick: | |
| from services.recovery import clear_stuck_subtasks, get_stuck_subtasks | |
| stuck = get_stuck_subtasks(spec_dir, project_dir) | |
| if stuck: | |
| try: | |
| clear_stuck_subtasks(spec_dir, project_dir) | |
| print(f"Cleared {len(stuck)} stuck subtasks for {args.spec}") | |
| for s in stuck: | |
| print( | |
| f" - {s.get('subtask_id', 'unknown')}: {s.get('reason', 'no reason')[:80]}" | |
| ) | |
| except Exception as e: | |
| print(f"Failed to clear stuck subtasks: {e}") | |
| sys.exit(1) | |
| else: | |
| print(f"No stuck subtasks found for {args.spec}") | |
| return | |
| # Handle --unstick command | |
| if args.unstick: | |
| from services.recovery import clear_stuck_subtasks, get_stuck_subtasks | |
| try: | |
| stuck = get_stuck_subtasks(spec_dir, project_dir) | |
| if stuck: | |
| clear_stuck_subtasks(spec_dir, project_dir) | |
| print(f"Cleared {len(stuck)} stuck subtasks for {args.spec}") | |
| for s in stuck: | |
| print( | |
| f" - {s.get('subtask_id', 'unknown')}: {s.get('reason', 'no reason')[:80]}" | |
| ) | |
| else: | |
| print(f"No stuck subtasks found for {args.spec}") | |
| except Exception as e: | |
| print(f"Failed to clear stuck subtasks: {e}") | |
| sys.exit(1) | |
| return |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/backend/cli/main.py` around lines 472 - 490, The current unstick flow
calls get_stuck_subtasks outside the try/except, so exceptions from
get_stuck_subtasks bubble up; move the call to get_stuck_subtasks(spec_dir,
project_dir) inside the same try block that calls clear_stuck_subtasks, and
catch exceptions there (same except Exception as e) to print a contextual
"Failed to clear stuck subtasks: {e}" message and sys.exit(1); keep the existing
logic that prints "No stuck subtasks found for {args.spec}" when
get_stuck_subtasks returns empty.
Both get_stuck_subtasks and clear_stuck_subtasks are now protected by the same exception handler. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
|
@AndyMik90 i have sign the CLA but they say no? |
|
👋 Thanks so much for your interest in contributing to Aperant! Pull requests are currently paused. We're rebuilding the entire app from the ground up for Aperant 3.0 (including new cloud features). Most of that work is happening in a separate development repo right now and will be merged here once it's ready — so although this repo looks quiet, the project is very much alive. 🚀 Because the current codebase is being fully replaced, we're closing existing PRs so contributors aren't left waiting on changes that unfortunately can't be merged. This is not a reflection on your work, and we're sorry for any wasted effort. 🙏 We'd genuinely love your help shaping 3.0 instead:
Contributions will reopen once the 3.0 foundation lands. Thank you for understanding! |
Summary
When task recovery fails (e.g., stuck subtasks with wrong file paths), the UI now provides clear feedback and an option to unstick subtasks.
--unstickflag for manual recovery from command lineChanges
Backend
cli/main.py: Add--unstickCLI flag to clear stuck subtasksservices/recovery.py: Addget_stuck_subtasks()methodFrontend Main Process
execution-handlers.ts: AddTASK_GET_STUCK_INFOandTASK_UNSTICK_SUBTASKSIPC handlersipc.ts: Add new IPC channel constantstask-api.ts: ExposegetStuckInfoandunstickSubtaskspreload APIsFrontend Renderer
TaskWarnings.tsx: Show stuck subtasks with reasons and unstick buttonTaskCard.tsx: Show error toast on recovery failuretask-store.ts: AddgetStuckInfoandunstickSubtasksstore actionsi18n
Test Plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Localization
Types & Mocks