Skip to content

fix(lifecycle): surface errors from lifecycle actions and auto-run setup#1330

Merged
arnestrickmann merged 3 commits intogeneralaction:mainfrom
HajekTim:fix/lifecycle-error-handling
Mar 6, 2026
Merged

fix(lifecycle): surface errors from lifecycle actions and auto-run setup#1330
arnestrickmann merged 3 commits intogeneralaction:mainfrom
HajekTim:fix/lifecycle-error-handling

Conversation

@HajekTim
Copy link
Contributor

@HajekTim HajekTim commented Mar 6, 2026

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:

  • Silent failures: errors from lifecycle actions were never shown to the user
  • Greyed-out button after failure: the Run button was permanently disabled after setup failed, with no way to retry except restarting the app
  • No debug info: error messages like "Exited with code 1" gave no actionable information

Changes

File What
src/renderer/components/TaskTerminalPanel.tsx Show toast with "View logs" action on lifecycle failures; show retry icon (RotateCw) when setup failed; keep Run button enabled after failure; surface IPC exceptions
src/main/services/TaskLifecycleService.ts Auto-run setup when status is idle or failed (retry); include last 5 lines of script output in error results for debugging

Detailed changes

Error surfacing (TaskTerminalPanel.tsx):

  • Check return value of all lifecycle IPC calls (lifecycleSetup, lifecycleRunStart, lifecycleTeardown)
  • Show destructive toast on failure with a "View logs" action button that navigates to the relevant lifecycle tab (e.g. Setup) where full script output is visible
  • When Run fails due to setup, the toast navigates to the Setup tab specifically
  • Surface IPC-level exceptions (main process crash, serialization errors) as toasts, not just console.error
  • Show toast on any !result.success, with fallback text when error string is empty

Retry support (both files):

  • Remove setupStatus !== 'failed' from canStartRun guard so the Run button stays enabled after setup failure
  • Show RotateCw icon instead of Play when setup has failed, making it clear clicking will retry
  • Tooltip reads "Retry setup and start run script" in this state
  • startRunInternal auto-runs setup when status is idle (after restart) or failed (retry)

Actionable error messages (TaskLifecycleService.ts):

  • buildErrorDetail() appends the last 5 lines of script output to error messages
  • Toast description shows the first line; clicking "View logs" shows the full output in the lifecycle tab

Root cause

Lifecycle state (setup.status, run.status) is stored in-memory only (private states = new Map<>()). After an app restart, all states reset to idle. If a project has a setup script configured in .emdash.json, clicking "Run" would hit the guard at startRunInternal which checks setupStatus !== 'succeeded' and silently returned { ok: false, error: 'Setup has not completed yet' }. The renderer never checked this return value.

Test plan

  • Configure .emdash.json with a setup script: "setup": "echo 'setting up' && sleep 1"
  • Start a task, click the Run play button — verify setup auto-runs before run starts
  • Restart emdash, open the same task, click Run — verify it auto-runs setup instead of silently failing
  • Set a failing setup: "setup": "echo 'bad config' && exit 1" — click Run:
    • Verify destructive toast appears with "Setup failed" title
    • Verify toast description includes script output (e.g. "bad config")
    • Verify "View logs" button navigates to the Setup lifecycle tab
    • Verify the play button shows a retry icon (↻) instead of ▶
    • Verify the button is not greyed out — clicking retries setup
    • Verify tooltip reads "Retry setup and start run script"
  • Click Setup directly with a failing script — verify error toast with "View logs"
  • Click Teardown with "teardown": "exit 1" — verify error toast with "View logs"
  • Verify successful lifecycle actions show no toast (no regression)

🤖 Generated with Claude Code

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>
@vercel
Copy link

vercel bot commented Mar 6, 2026

@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-apps
Copy link
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR fixes two related lifecycle bugs: silently swallowed errors in the renderer and a broken post-restart "Run" flow. It adds useToast to TaskTerminalPanel so that { success: false } results from any lifecycle IPC call now surface as a destructive toast, and it changes startRunInternal to auto-trigger setup when the setup status is idle (the state after an app restart, since lifecycle state is in-memory only).

Key points:

  • TaskLifecycleService.ts: The idle guard replaces the old catch-all setupStatus !== 'succeeded' check. All four setup states (idle, running, failed, succeeded) are now correctly handled. Inflight deduplication via setupInflight prevents double-execution if runSetup is called concurrently.
  • TaskTerminalPanel.tsx: Two issues remain with the error-surfacing approach: (1) the toast condition result.error is falsy when the error field is empty or absent, silently dropping the toast even when success: false; (2) the catch block that handles IPC-level exceptions still only logs to the console and shows no toast to the user, partially defeating the goal of this PR.

Confidence Score: 3/5

  • Safe to merge with minor fixes — the core logic is sound but error-surfacing in the renderer is incomplete in two edge cases.
  • The main-process change in TaskLifecycleService.ts is clean and well-reasoned. The renderer change improves error visibility but has two gaps: a falsy-error guard that can suppress toasts on { success: false } results with no error message, and a catch block that still silently swallows IPC exceptions without notifying the user. Neither gap causes data loss, but they leave the stated goal partially unmet.
  • src/renderer/components/TaskTerminalPanel.tsx — the toast condition and catch block need attention

Important Files Changed

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()
Loading

Last reviewed commit: 4b78f52

Comment on lines +288 to +294
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',
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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',
});
}

Comment on lines 295 to 297
} catch (error) {
console.error('Failed lifecycle play action:', error);
} finally {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
} 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',
});

HajekTim and others added 2 commits March 6, 2026 15:32
- 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>
@arnestrickmann
Copy link
Contributor

Good catch and thanks for the PR!

Looks good to me, merging.

@arnestrickmann arnestrickmann merged commit 3951d3a into generalaction:main Mar 6, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants