fix(lifecycle): surface errors from lifecycle actions and auto-run setup#1330
Conversation
The lifecycle play button (setup/run/teardown) silently swallowed errors returned from the main process. Most notably, clicking "Run" when a setup script is configured but hasn't been run (e.g. after app restart, since lifecycle state is in-memory only) would silently fail with "Setup has not completed yet". Two fixes: 1. TaskTerminalPanel: check the result of lifecycle API calls and show a toast notification when they fail, so users see what went wrong. 2. TaskLifecycleService: when startRunInternal finds setup status is 'idle' (lost after restart), auto-run the setup phase before proceeding instead of returning an error. This matches user expectations since the worktree is already set up on disk. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@HajekTim is attempting to deploy a commit to the General Action Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR fixes two related lifecycle bugs: silently swallowed errors in the renderer and a broken post-restart "Run" flow. It adds Key points:
Confidence Score: 3/5
|
| Filename | Overview |
|---|---|
| src/renderer/components/TaskTerminalPanel.tsx | Adds useToast and checks the IPC result to show a destructive toast on failure. Two issues: (1) the result.error guard can silently drop toasts when error is empty/undefined, and (2) the catch block still swallows IPC-level exceptions without user feedback. |
| src/main/services/TaskLifecycleService.ts | Correctly handles the post-restart idle setup state by auto-running setup before run. All four setup statuses (idle, running, failed, succeeded) are now explicitly or implicitly handled, and inflight deduplication via setupInflight prevents double-execution. |
Sequence Diagram
sequenceDiagram
participant UI as TaskTerminalPanel (renderer)
participant IPC as lifecycleIpc (main)
participant SVC as TaskLifecycleService (main)
UI->>UI: handlePlay() — setRunActionBusy(true)
UI->>IPC: lifecycleRunStart({ taskId, taskPath, ... })
IPC->>SVC: startRun(...)
SVC->>SVC: startRunInternal(...)
SVC->>SVC: getScript(projectPath, 'setup') → found
alt setupStatus === 'idle' (e.g. after restart)
SVC->>SVC: log "Auto-running setup before run"
SVC->>SVC: await runSetup(...)
Note over SVC: runFinite sets status='running', spawns process
SVC-->>SVC: setupResult = { ok: true } or { ok: false, error }
alt setupResult.ok === false
SVC-->>IPC: { ok: false, error: "Auto-setup failed: ..." }
IPC-->>UI: { success: false, error: "Auto-setup failed: ..." }
UI->>UI: toast(destructive, error message)
else setupResult.ok === true
SVC->>SVC: spawn run process
SVC-->>IPC: { ok: true }
IPC-->>UI: { success: true }
end
else setupStatus === 'running'
SVC-->>IPC: { ok: false, error: 'Setup is still running' }
IPC-->>UI: { success: false, error: ... }
UI->>UI: toast(destructive, error message)
else setupStatus === 'failed'
SVC-->>IPC: { ok: false, error: 'Setup failed. Fix setup before starting run' }
IPC-->>UI: { success: false, error: ... }
UI->>UI: toast(destructive, error message)
else setupStatus === 'succeeded'
SVC->>SVC: spawn run process
SVC-->>IPC: { ok: true }
IPC-->>UI: { success: true }
end
UI->>UI: setRunActionBusy(false)
UI->>UI: refreshLifecycleState()
Last reviewed commit: 4b78f52
| if (result && !result.success && result.error) { | ||
| toast({ | ||
| title: `${(selection.selectedLifecycle || 'run').charAt(0).toUpperCase()}${(selection.selectedLifecycle || 'run').slice(1)} failed`, | ||
| description: result.error, | ||
| variant: 'destructive', | ||
| }); | ||
| } |
There was a problem hiding this comment.
Silent failure when error is falsy on a failed result
The condition result.error is falsy when error is an empty string or undefined. In that case, the toast is suppressed even though result.success === false, leaving the user with no feedback.
While the current service always populates error when ok: false, this is an implicit contract and can break silently. A defensive fallback keeps the toast guarantee intact:
| if (result && !result.success && result.error) { | |
| toast({ | |
| title: `${(selection.selectedLifecycle || 'run').charAt(0).toUpperCase()}${(selection.selectedLifecycle || 'run').slice(1)} failed`, | |
| description: result.error, | |
| variant: 'destructive', | |
| }); | |
| } | |
| if (result && !result.success) { | |
| toast({ | |
| title: `${(selection.selectedLifecycle || 'run').charAt(0).toUpperCase()}${(selection.selectedLifecycle || 'run').slice(1)} failed`, | |
| description: result.error || 'An unknown error occurred.', | |
| variant: 'destructive', | |
| }); | |
| } |
| } catch (error) { | ||
| console.error('Failed lifecycle play action:', error); | ||
| } finally { |
There was a problem hiding this comment.
IPC-level exceptions are silently swallowed
The catch block only logs to the console. If the Electron IPC call itself throws (e.g., the main process crashes, serialisation error, or channel is unavailable), the user receives zero feedback — exactly the silent-failure pattern this PR is trying to fix.
A toast in the catch handler would complete the error-surfacing story:
| } catch (error) { | |
| console.error('Failed lifecycle play action:', error); | |
| } finally { | |
| } catch (error) { | |
| console.error('Failed lifecycle play action:', error); | |
| toast({ | |
| title: `${(selection.selectedLifecycle || 'run').charAt(0).toUpperCase()}${(selection.selectedLifecycle || 'run').slice(1)} failed`, | |
| description: error instanceof Error ? error.message : String(error), | |
| variant: 'destructive', | |
| }); |
- Show toast even when error string is empty/undefined on a failed result - Surface IPC-level exceptions (main process crash, serialization error) as toasts instead of only logging to console - Improve tooltip to guide users: "Setup failed — select Setup to retry" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rrors - Show RotateCw icon instead of Play when setup has failed - Include last 5 lines of script output in error details for debugging - Add "View logs" action button to error toasts that navigates to the failing phase's tab (e.g. Setup) where full output is visible - Keep Run button enabled after setup failure so users can retry directly (auto-runs setup before run) - Surface IPC-level exceptions as toasts, not just console.error - Show toast on any !result.success, with fallback text when error string is empty Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Good catch and thanks for the PR! Looks good to me, merging. |
Summary
Lifecycle play button actions (setup/run/teardown) silently swallowed errors, leaving users with no feedback when something failed. Most notably, clicking "Run" after an app restart with a setup script configured would silently fail because in-memory lifecycle state resets to
idle.This PR fixes three problems:
Changes
src/renderer/components/TaskTerminalPanel.tsxsrc/main/services/TaskLifecycleService.tsidleorfailed(retry); include last 5 lines of script output in error results for debuggingDetailed changes
Error surfacing (TaskTerminalPanel.tsx):
lifecycleSetup,lifecycleRunStart,lifecycleTeardown)console.error!result.success, with fallback text when error string is emptyRetry support (both files):
setupStatus !== 'failed'fromcanStartRunguard so the Run button stays enabled after setup failureRotateCwicon instead ofPlaywhen setup has failed, making it clear clicking will retrystartRunInternalauto-runs setup when status isidle(after restart) orfailed(retry)Actionable error messages (TaskLifecycleService.ts):
buildErrorDetail()appends the last 5 lines of script output to error messagesRoot cause
Lifecycle state (
setup.status,run.status) is stored in-memory only (private states = new Map<>()). After an app restart, all states reset toidle. If a project has a setup script configured in.emdash.json, clicking "Run" would hit the guard atstartRunInternalwhich checkssetupStatus !== 'succeeded'and silently returned{ ok: false, error: 'Setup has not completed yet' }. The renderer never checked this return value.Test plan
.emdash.jsonwith a setup script:"setup": "echo 'setting up' && sleep 1""setup": "echo 'bad config' && exit 1"— click Run:"teardown": "exit 1"— verify error toast with "View logs"🤖 Generated with Claude Code