Skip to content

feat(workflow)!: implement inline mode with visual indicators and interrupt recovery - #232

Merged
lavaman131 merged 14 commits into
mainfrom
lavaman131/hotfix/ralph
Feb 21, 2026
Merged

feat(workflow)!: implement inline mode with visual indicators and interrupt recovery#232
lavaman131 merged 14 commits into
mainfrom
lavaman131/hotfix/ralph

Conversation

@lavaman131

@lavaman131 lavaman131 commented Feb 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR implements a comprehensive workflow inline mode experience that provides clear visual feedback, improved interrupt handling, and context preservation throughout workflow execution. Users can now interrupt and redirect workflows mid-execution without losing orchestration state.

Key Changes

🎨 Visual Indicators

  • Teal border on chat input when workflow is active (uses themeColors.accent)
  • Workflow mode label displaying workflow type (e.g., "plan") with keyboard hints
  • Clear visual distinction between normal mode and workflow mode

⌨️ Improved Interrupt Handling

  • Single Ctrl+C: Interrupts current stream while keeping workflow active
  • Double Ctrl+C: Cancels workflow entirely
  • User input recovery: After stream interruption, workflow waits for user input and resumes with full context
  • ESC key: Only interrupts stream, doesn't cancel workflow

💾 Context Preservation

  • Removed clearContext() calls from Ralph workflow (src/ui/commands/workflow-commands.ts:684,734)
  • Conversation history persists throughout entire workflow execution
  • Task list panel stays visible during workflow, auto-hides only on natural completion

🐛 Bug Fixes

  • Normalize markdown newlines for correct terminal rendering
  • Clean sub-agent output and hide intermediate stream content
  • Scope Ralph session state reset to Copilot agent type
  • Add error handling to reset workflowActive on workflow failures

Breaking Changes

⚠️ Ctrl+C behavior during workflows has changed:

  • Single Ctrl+C press no longer cancels the workflow
  • Users must press Ctrl+C twice to exit an active workflow
  • First press interrupts the stream and allows user input within workflow context

Testing

  • Unit tests: Border color logic, Ctrl+C handler priority chain, state transitions
  • Integration tests: Workflow inline mode, interrupt recovery, user input passthrough
  • E2E tests: Visual verification, stream interruption, task list lifecycle

Related Documentation

  • Spec: specs/workflow-sdk-inline-mode.md
  • Research: research/docs/2026-02-21-workflow-sdk-inline-mode-research.md

lavaman131 and others added 14 commits February 21, 2026 01:47
… content

Refactor spawnSubagent to produce clean output for ralph review-fix
loops by instructing the sub-agent to return only its raw output without
additional commentary. Hide stream content during sub-agent execution to
prevent intermediate steps from polluting the chat UI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reset ralph session state (session dir, session id, task ids, todo
items) on /clear and non-ralph slash commands for Copilot agent only.
Guard existing ralph panel dismissal on regular messages with agentType
check to prevent unintended resets for other agent types.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Updated borderColor to conditionally use accent color (teal) when workflowActive is true
- Falls back to inputFocus color when workflow is inactive
- Single-line change at line 5688 in src/ui/chat.tsx
Tasks #6 and #7:
- Remove clearContext() call before review iteration (line 684)
- Remove clearContext() call before fix-spec decomposition (line 734)
- Update test to remove assertion on clearContext() being called
- Maintains context continuity throughout Ralph workflow
- All workflow-commands tests passing (14 tests, 32 assertions)
- Add test for workflow completion returning stateUpdate with workflowActive: false
- Add test for waitForUserInput presence in CommandContext interface
- Add test for mock waitForUserInput resolving with a string
- Add test verifying clearContext is not called during workflow execution
- Add test for interrupted step1 returning stateUpdate to deactivate workflow

All tests pass and typecheck succeeds.
- Test #16: Ralph end-to-end without clearContext calls
  - Verifies clearContext is never called during full workflow
  - Tests complete workflow with review and fix cycles
  - Confirms stateUpdate.workflowActive is false on completion

- Test #17: User prompt passthrough after Ctrl+C in workflow
  - Simulates Ctrl+C interruption during implementation
  - Verifies waitForUserInput is called to get user's follow-up prompt
  - Confirms user's prompt is passed to the next streamAndWait call

- Test #18: Task list persists after Ctrl+C, hides on completion
  - Verifies setRalphSessionDir is called with non-null path at start
  - Confirms session dir is NOT cleared (null) during workflow
  - Validates stateUpdate.workflowActive is false to signal UI to hide task list
Add comprehensive E2E tests validating the complete lifecycle of the /ralph
workflow in inline mode:

- Test #19: Teal border lifecycle during /ralph workflow
  - Verifies workflowActive state drives teal border
  - Tracks updateWorkflowState calls throughout lifecycle
  - Validates border returns to normal after completion

- Test #20: Ctrl+C + user prompt + workflow continuation E2E
  - Full lifecycle: decomposition → Ctrl+C → user input → continuation
  - Verifies waitForUserInput() mechanism
  - Validates workflow continues with user's prompt
  - Confirms clean completion after interruption

- Test #21: Task list persistence and tasks.json maintenance
  - Verifies session dir creation and persistence
  - Validates tasks.json is written and updated correctly
  - Confirms task tracking through interruption
  - Ensures final state reflects all completed tasks

All tests follow the existing E2E test pattern from background-agent-e2e.test.ts
and use the same createMockContext pattern from workflow-commands.test.ts.

Tests validate multiple concerns across the workflow lifecycle:
- State management (workflowActive, workflowType)
- User intervention handling (Ctrl+C, waitForUserInput)
- Task persistence (tasks.json, session directory)
- Review integration (clean review with no findings)
- Cleanup behavior (stateUpdate signals UI reset)

All 1426 tests pass including 3 new E2E tests.
No type errors.
Fixes stale closure issue in useEffect hook that auto-hides task list panel
when workflow ends. The effect references ralphSessionDir in its body but was
missing it from the dependency array, causing React to use stale values.

Changed line 2685 to include ralphSessionDir in dependencies:
[workflowState.workflowActive, ralphSessionDir]

Testing:
- TypeScript compilation: ✅ Passed
- All tests: ✅ Passed (1426 tests, 9410 assertions)
- Display workflow type (e.g., 'plan') instead of generic 'workflow'
- Add 'shift+tab switch mode' hint for user guidance
- Style label in teal accent color to match workflow theme
- Show label when workflow is active and not streaming
Remove separator and 'shift+tab switch mode' hint from the workflow
mode label, keeping only the workflow type indicator.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assistant-model: GitHub Copilot
Collapse single newlines to spaces (standard markdown soft-break
behavior) while preserving code fences and paragraph breaks. OpenTUI
renders literal \n as hard line breaks unlike HTML, so this
normalization is needed for correct paragraph rendering.

Apply normalizeMarkdownNewlines to both text and reasoning part
displays.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rework Ctrl+C behavior during workflows: single Ctrl+C interrupts the
current stream while keeping the workflow alive (waits for user input),
double Ctrl+C cancels the workflow entirely.

- Change waitForUserInputResolverRef to support reject for cancellation
- Add wasCancelled flag to StreamResult interface
- ESC during workflow only interrupts the stream, no longer cancels
- Show streaming hints (esc/ctrl+q) in workflow mode bar when idle
- Handle "Workflow cancelled" error silently in workflow catch block

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Handle interruptions consistently across planning, execution, and review loops by waiting for user input and resuming the stream. Update workflow status hints and tests to reflect cancel-versus-interrupt behavior.

Assistant-model: openai/gpt-5.3-codex
@lavaman131
lavaman131 merged commit 36a9624 into main Feb 21, 2026
3 checks passed
@claude claude Bot changed the title Lavaman131/hotfix/ralph feat(workflow)!: implement inline mode with visual indicators and interrupt recovery Feb 21, 2026
@claude

claude Bot commented Feb 21, 2026

Copy link
Copy Markdown

PR Review: Workflow SDK Inline Mode Implementation

This PR implements a significant enhancement to the workflow system, adding visual mode indicators (teal border), double Ctrl+C workflow exit, user prompt passthrough after interruption, and removal of clearContext() calls. Here's my detailed review:


✅ Code Quality & Best Practices

Strengths:

  • Well-documented with comprehensive research doc and spec (research/docs/2026-02-21-workflow-sdk-inline-mode-research.md and specs/workflow-sdk-inline-mode.md)
  • Good separation of concerns with the streamWithInterruptRecovery helper function in workflow-commands.ts
  • Proper TypeScript typing throughout (e.g., StreamAndWaitResult, wasCancelled property)
  • Comprehensive test coverage with unit tests, integration tests, and E2E tests

Suggestions:

  1. Unused parameter prefix: In workflow-commands.ts:576, the onInterrupted callback receives userPrompt but some call sites don't use the returned options:

    const result = await streamWithInterruptRecovery(
        context,
        prompt,
    );

    This is fine, but consider documenting that onInterrupted defaults to passing through the user prompt directly.

  2. Magic string: The error message "Workflow cancelled" is used as a control flow signal in chat.tsx:2063 and caught in workflow-commands.ts:1998. Consider extracting this to a constant:

    export const WORKFLOW_CANCELLED_ERROR = "Workflow cancelled";

⚠️ Potential Bugs or Issues

  1. Race condition in waitForUserInput (chat.tsx:3754-3758):
    The waitForUserInputResolverRef is set but there's no timeout or cleanup if the user never submits. If the workflow is cancelled while waiting, the promise might stay pending. While the catch block handles "Workflow cancelled" errors, consider adding a cleanup in the workflow cancellation path.

  2. Missing null check (chat.tsx:4345):

    if (hideStreamContentRef.current && interruptedId) {

    The interruptedId might be undefined if isStreamingRef.current was false when Ctrl+C was pressed. This is guarded by the outer streaming check, but adding defensive coding would be safer.

  3. Duplicate logic (chat.tsx:4440-4449 and chat.tsx:4329-4341):
    The workflow termination logic on double Ctrl+C is duplicated in two places (streaming and non-streaming paths). Consider extracting to a helper function:

    function terminateWorkflow() {
      updateWorkflowState({ workflowActive: false, workflowType: null, initialPrompt: null });
      if (waitForUserInputResolverRef.current) {
        waitForUserInputResolverRef.current.reject(new Error("Workflow cancelled"));
        waitForUserInputResolverRef.current = null;
      }
    }
  4. State cleanup on ESC (chat.tsx:4600-4618):
    The ESC handler also resolves streamCompletionResolverRef but does NOT call waitForUserInputResolverRef.reject(). If a user presses ESC during waitForUserInput, the workflow might hang. Consider aligning ESC behavior with Ctrl+C for consistency.


🚀 Performance Considerations

  1. useEffect dependency (chat.tsx:2697-2703):

    useEffect(() => {
      if (!workflowState.workflowActive && ralphSessionDir) {
        setRalphSessionDir(null);
        setRalphSessionId(null);
      }
    }, [workflowState.workflowActive, ralphSessionDir]);

    This effect runs whenever ralphSessionDir changes. If workflowActive is false and ralphSessionDir is set, it will immediately clear it. This is intentional but could cause unnecessary re-renders. Consider using a ref to track the previous workflowActive state to only trigger on transitions.

  2. Markdown normalization (format.ts:2542-2556):
    The normalizeMarkdownNewlines function uses regex replacement which is called on every render of text/reasoning parts. For large content, this could be expensive. Consider memoizing the normalized content at the component level.


🔒 Security Concerns

No significant security concerns identified. The changes are primarily UI/UX focused and don't introduce new attack surfaces.


🧪 Test Coverage

Excellent coverage with:

  • Unit tests for waitForUserInput context method
  • Integration tests for the full workflow lifecycle (workflow-commands.test.ts)
  • E2E tests for teal border state, Ctrl+C interruption, and task list persistence (workflow-inline-mode-e2e.test.ts)

One test removal noted (workflow-commands.test.ts:283-285):

-    // Verify context was cleared before review
-    expect(clearCallCount).toBeGreaterThanOrEqual(1);

This is intentional as clearContext() calls were removed, but ensure this behavioral change is documented in the PR description.


📝 Minor Style/Consistency Issues

  1. Inconsistent agent type check (chat.tsx:3807-3815 and chat.tsx:5405-5413):
    The Ralph state reset on /clear only applies to agentType === "copilot". This is mentioned in the spec but consider adding a comment explaining why this is Copilot-only.

  2. Try-catch nesting (workflow-commands.ts:851):
    The closing brace for the try block is not aligned with its opening, making the code harder to read. The catch at line 1997 seems to be catching errors from a large try block starting around line 623. Consider refactoring for clarity.


📋 Summary

Aspect Rating
Code Quality ⭐⭐⭐⭐
Test Coverage ⭐⭐⭐⭐⭐
Documentation ⭐⭐⭐⭐⭐
Performance ⭐⭐⭐
Security ⭐⭐⭐⭐⭐

Overall: This is a well-structured PR with good documentation and test coverage. The main concerns are around potential edge cases with the waitForUserInput promise lifecycle and some duplicated code that could be refactored. The removal of clearContext() calls is a significant behavioral change that should be validated in manual testing to ensure the reviewer sub-agent still functions correctly with the full conversation context.

Recommendation: Approve with minor suggestions. Consider addressing the waitForUserInput cleanup and duplicated workflow termination logic before merging.

@claude claude Bot mentioned this pull request Feb 22, 2026
@lavaman131
lavaman131 deleted the lavaman131/hotfix/ralph branch February 22, 2026 00:53
lavaman131 added a commit that referenced this pull request Mar 26, 2026
* fix(chat): ensure clean sub-agent output and hide intermediate stream content

Refactor spawnSubagent to produce clean output for ralph review-fix
loops by instructing the sub-agent to return only its raw output without
additional commentary. Hide stream content during sub-agent execution to
prevent intermediate steps from polluting the chat UI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(chat): scope ralph state reset to copilot agent type

Reset ralph session state (session dir, session id, task ids, todo
items) on /clear and non-ralph slash commands for Copilot agent only.
Guard existing ralph panel dismissal on regular messages with agentType
check to prevent unintended resets for other agent types.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(ui): change chat input border to teal when workflow is active

- Updated borderColor to conditionally use accent color (teal) when workflowActive is true
- Falls back to inputFocus color when workflow is inactive
- Single-line change at line 5688 in src/ui/chat.tsx

* refactor(ralph): remove clearContext() calls from workflow

Tasks #6 and #7:
- Remove clearContext() call before review iteration (line 684)
- Remove clearContext() call before fix-spec decomposition (line 734)
- Update test to remove assertion on clearContext() being called
- Maintains context continuity throughout Ralph workflow
- All workflow-commands tests passing (14 tests, 32 assertions)

* test(workflow): add unit tests for workflow inline mode changes

- Add test for workflow completion returning stateUpdate with workflowActive: false
- Add test for waitForUserInput presence in CommandContext interface
- Add test for mock waitForUserInput resolving with a string
- Add test verifying clearContext is not called during workflow execution
- Add test for interrupted step1 returning stateUpdate to deactivate workflow

All tests pass and typecheck succeeds.

* test(workflow): add 3 integration tests for workflow inline mode

- Test #16: Ralph end-to-end without clearContext calls
  - Verifies clearContext is never called during full workflow
  - Tests complete workflow with review and fix cycles
  - Confirms stateUpdate.workflowActive is false on completion

- Test #17: User prompt passthrough after Ctrl+C in workflow
  - Simulates Ctrl+C interruption during implementation
  - Verifies waitForUserInput is called to get user's follow-up prompt
  - Confirms user's prompt is passed to the next streamAndWait call

- Test #18: Task list persists after Ctrl+C, hides on completion
  - Verifies setRalphSessionDir is called with non-null path at start
  - Confirms session dir is NOT cleared (null) during workflow
  - Validates stateUpdate.workflowActive is false to signal UI to hide task list

* test(workflow): add 3 E2E tests for workflow inline mode

Add comprehensive E2E tests validating the complete lifecycle of the /ralph
workflow in inline mode:

- Test #19: Teal border lifecycle during /ralph workflow
  - Verifies workflowActive state drives teal border
  - Tracks updateWorkflowState calls throughout lifecycle
  - Validates border returns to normal after completion

- Test #20: Ctrl+C + user prompt + workflow continuation E2E
  - Full lifecycle: decomposition → Ctrl+C → user input → continuation
  - Verifies waitForUserInput() mechanism
  - Validates workflow continues with user's prompt
  - Confirms clean completion after interruption

- Test #21: Task list persistence and tasks.json maintenance
  - Verifies session dir creation and persistence
  - Validates tasks.json is written and updated correctly
  - Confirms task tracking through interruption
  - Ensures final state reflects all completed tasks

All tests follow the existing E2E test pattern from background-agent-e2e.test.ts
and use the same createMockContext pattern from workflow-commands.test.ts.

Tests validate multiple concerns across the workflow lifecycle:
- State management (workflowActive, workflowType)
- User intervention handling (Ctrl+C, waitForUserInput)
- Task persistence (tasks.json, session directory)
- Review integration (clean review with no findings)
- Cleanup behavior (stateUpdate signals UI reset)

All 1426 tests pass including 3 new E2E tests.
No type errors.

* fix(workflow): wrap execute body in try-catch to reset workflowActive on error

* fix(chat): add ralphSessionDir to useEffect dependency array

Fixes stale closure issue in useEffect hook that auto-hides task list panel
when workflow ends. The effect references ralphSessionDir in its body but was
missing it from the dependency array, causing React to use stale values.

Changed line 2685 to include ralphSessionDir in dependencies:
[workflowState.workflowActive, ralphSessionDir]

Testing:
- TypeScript compilation: ✅ Passed
- All tests: ✅ Passed (1426 tests, 9410 assertions)

* feat(ui): add workflow mode label with type and keyboard hint

- Display workflow type (e.g., 'plan') instead of generic 'workflow'
- Add 'shift+tab switch mode' hint for user guidance
- Style label in teal accent color to match workflow theme
- Show label when workflow is active and not streaming

* fix(ui): simplify workflow mode label

Remove separator and 'shift+tab switch mode' hint from the workflow
mode label, keeping only the workflow type indicator.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assistant-model: GitHub Copilot

* fix(ui): normalize markdown newlines for terminal rendering

Collapse single newlines to spaces (standard markdown soft-break
behavior) while preserving code fences and paragraph breaks. OpenTUI
renders literal \n as hard line breaks unlike HTML, so this
normalization is needed for correct paragraph rendering.

Apply normalizeMarkdownNewlines to both text and reasoning part
displays.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor(workflow): improve interrupt and cancellation handling

Rework Ctrl+C behavior during workflows: single Ctrl+C interrupts the
current stream while keeping the workflow alive (waits for user input),
double Ctrl+C cancels the workflow entirely.

- Change waitForUserInputResolverRef to support reject for cancellation
- Add wasCancelled flag to StreamResult interface
- ESC during workflow only interrupts the stream, no longer cancels
- Show streaming hints (esc/ctrl+q) in workflow mode bar when idle
- Handle "Workflow cancelled" error silently in workflow catch block

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(workflow): recover /ralph flow after interrupted streams

Handle interruptions consistently across planning, execution, and review loops by waiting for user input and resuming the stream. Update workflow status hints and tests to reflect cancel-versus-interrupt behavior.

Assistant-model: openai/gpt-5.3-codex

---------

Co-authored-by: lavaman131 <dev@example.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

1 participant