Skip to content

feat: add auto-scroll toggle for AI output#390

Merged
pedramamini merged 10 commits intoRunMaestro:mainfrom
openasocket:feat/auto-scroll-toggle
Feb 17, 2026
Merged

feat: add auto-scroll toggle for AI output#390
pedramamini merged 10 commits intoRunMaestro:mainfrom
openasocket:feat/auto-scroll-toggle

Conversation

@openasocket
Copy link
Contributor

@openasocket openasocket commented Feb 16, 2026

Auto-Scroll Toggle for AI Output

Feature Overview

Adds configurable auto-scroll behavior for AI mode output. When enabled, the terminal output automatically scrolls to the bottom as new AI content arrives — matching terminal mode behavior. The feature includes smart pause/resume: scrolling up pauses auto-scroll so you can read content, and scrolling back to bottom resumes it.

Controls

Method Action
Settings Modal General tab → "Auto-scroll AI Output" checkbox
Inline Button Floating toggle button (bottom-left of AI output)
Keyboard Shortcut Alt+Cmd+S (works even with modals open)

Behavior

  • Auto-scroll ON → New AI output scrolls viewport to bottom instantly
  • Scroll up → Auto-scroll pauses, button icon dims to indicate paused state, new message indicator appears
  • Scroll back to bottom → Auto-scroll resumes automatically, button returns to active state
  • Click button while paused → Resumes auto-scroll without toggling the setting off
  • Click button while active → Disables auto-scroll entirely
  • Default: OFF → Preserves existing behavior where users manually scroll

Visual Feedback

The inline toggle button reflects three states:

  • Active (accent color, ↓ icon): Auto-scroll is on and scrolling
  • Paused (dim, scroll icon): Auto-scroll is on but user scrolled up — tooltip says "paused (click to resume)"
  • Off (dim, scroll icon): Auto-scroll is disabled — tooltip says "click to enable"

Technical Details

  • Setting persisted via electron-store as autoScrollAiMode (boolean)
  • Uses behavior: auto (instant) instead of smooth to prevent jitter during rapid streaming
  • Disables browser native overflow-anchor CSS on the scroll container when auto-scroll is off/paused, preventing the browser from pinning the viewport to bottom
  • autoScrollPaused is React state (not a ref) so the toggle button re-renders to reflect pause state
  • Settings Modal receives autoScrollAiMode/setAutoScrollAiMode as props from App (not from its own useSettings() instance) to ensure changes take immediate effect
  • Keyboard shortcut registered as system utility shortcut so it works even when modals are open

Files Changed

File Change
useSettings.ts autoScrollAiMode state, persistence, loading
App.tsx Extract setting, pass to keyboard context, MainPanel props, and SettingsModal
useMainPanelProps.ts Thread setting + setter through props builder
MainPanel.tsx Accept and forward props to TerminalOutput
TerminalOutput.tsx Auto-scroll effect, pause/resume logic, overflow-anchor fix, inline toggle button
SettingsModal.tsx Props-based checkbox for immediate effect
shortcuts.ts toggleAutoScroll shortcut definition (Alt+Meta+S)
useMainKeyboardHandler.ts Shortcut handler + system utility allowlist

Test plan

  • Lint passes (all 3 TS configs)
  • All 19,412 tests pass (456 test files)
  • Toggle via Settings → General → "Auto-scroll AI Output" — verify immediate effect
  • Toggle via inline button (bottom-left of AI output)
  • Toggle via Alt+Cmd+S keyboard shortcut
  • Scroll up while auto-scroll is on — verify button dims and auto-scroll pauses
  • Scroll back to bottom — verify auto-scroll resumes and button re-activates
  • Click button while paused — verify it resumes (does not toggle off)
  • With auto-scroll off, verify new content does NOT scroll viewport
  • Verify setting persists across app restarts

🤖 Generated with Claude Code

openasocket and others added 2 commits February 16, 2026 00:04
…sume

Add configurable auto-scroll behavior for AI mode output that automatically
scrolls to bottom when new content arrives, with smart pause when user scrolls
up to read content and automatic resume when they scroll back to bottom.

- Add autoScrollAiMode setting persisted via electron-store (default: off)
- Add inline floating toggle button (bottom-left) with visual state feedback
- Add keyboard shortcut (Alt+Cmd+S) registered as system utility shortcut
- Add Settings Modal checkbox in General tab
- Thread setting through App → MainPanel → TerminalOutput via props
- Use instant scroll (behavior: 'auto') to prevent jitter during streaming
- Pause auto-scroll when user scrolls up, resume on scroll-to-bottom
- Toggle button reflects paused state (dim icon when paused)
- New message indicator shown when paused (user scrolled away from bottom)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pass autoScrollAiMode/setAutoScrollAiMode as props from App to
  SettingsModal so toggling takes immediate effect (was using independent
  useSettings() instance that didn't propagate to App's render tree)
- Disable browser native scroll anchoring (overflow-anchor: none) on the
  AI output scroll container when auto-scroll is off or paused, preventing
  the browser from pinning viewport to bottom on new content
- Add programmaticScrollRef guard and autoScrollPaused state to properly
  distinguish user scrolls from effect-driven scrolls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Collaborator

@pedramamini pedramamini left a comment

Choose a reason for hiding this comment

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

Clean feature, well-scoped, follows existing patterns. A few issues worth fixing before merge.

Summary:

  • Dead ref (programmaticScrollRef) — remove
  • IIFE in JSX allocates every render — extract to a variable
  • Resume-from-pause doesn't scroll to bottom — users will be stuck at old position until new content arrives
  • Double dep trigger on auto-scroll effect — filteredLogs.length and shellLogs.length both change per message
  • No test updates despite 8 files changed with new state/props/shortcut/UI

Overall approach is solid — the tri-control (setting + shortcut + inline button), overflow-anchor: none, behavior: 'auto' for streaming, and default-OFF are all good calls.


{/* Inline auto-scroll toggle - floating button (AI mode only) */}
{/* Shows active state only when auto-scroll is on AND not paused by user scrolling up */}
{session.inputMode === 'ai' && setAutoScrollAiMode && (() => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This IIFE creates a new function every render just to hoist isActive into scope. Extract to a variable instead:

const isAutoScrollActive = autoScrollAiMode && !autoScrollPaused;
// ... in JSX:
{session.inputMode === 'ai' && setAutoScrollAiMode && (
    <button
        style={{ backgroundColor: isAutoScrollActive ? ... }}
        ...
    />
)}

Simpler, no allocation, easier to read.

return (
<button
onClick={() => {
if (autoScrollPaused) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

When paused, clicking resume clears the pause flag but doesn't scroll to bottom. The user stays at their current scroll position until new content arrives and triggers the auto-scroll effect.

Should call scrollToBottom() here too:

if (autoScrollPaused) {
    setAutoScrollPaused(false);
    scrollToBottom(); // <-- jump to bottom immediately
} else {

});
}
}, [session.inputMode, session.shellLogs.length]);
}, [session.inputMode, session.shellLogs.length, filteredLogs.length, autoScrollAiMode, autoScrollPaused]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Both session.shellLogs.length and filteredLogs.length are in the dep array, but filteredLogs is derived from shellLogs. When a new log arrives, both change, potentially firing this effect twice per message.

Consider using only filteredLogs.length since it's the more correct dependency (accounts for search filtering). Or if you need the raw length for terminal mode, keep both but be aware of the double-fire.

openasocket and others added 6 commits February 17, 2026 00:10
Remove unused programmaticScrollRef declaration that was left over from
an earlier iteration. It was declared but never read or written.

Addresses PR RunMaestro#390 review feedback (Task 1).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace inline IIFE that computed isActive within JSX with a
pre-computed isAutoScrollActive variable before the return statement.
Eliminates unnecessary function creation per render and improves readability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…new content

When auto-scroll is paused and the user clicks resume, immediately scroll
to the bottom using behavior: 'auto' (instant snap) rather than leaving
the viewport at the old scroll position until new content arrives.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ffect deps

filteredLogs is derived from shellLogs via activeLogs → collapsedLogs →
filteredLogs, so having both in the dependency array caused the effect to
double-fire on every new log message. Keep only filteredLogs.length which
correctly covers both AI and terminal modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…button rendering, and props threading

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…and palette

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@openasocket
Copy link
Contributor Author

All review items addressed + additional fix

Every item from the review has been resolved. Here's the full breakdown:


Review Item #1 — Dead programmaticScrollRef

Commit: 719b6600

Removed the unused programmaticScrollRef from TerminalOutput.tsx.

Review Item #2 — IIFE in JSX allocates every render ✅

Commit: 3dbedf4c

Extracted the inline IIFE to a const isAutoScrollActive = autoScrollAiMode && !autoScrollPaused variable above the return statement. Used in button styling, title, and icon selection.

Review Item #3 — Resume-from-pause doesn't scroll to bottom ✅

Commit: a3551878

Clicking the auto-scroll button while paused now immediately snaps to bottom via scrollTo({ top: scrollHeight, behavior: 'auto' }) instead of waiting for new content to arrive.

Review Item #4 — Double dep trigger on auto-scroll effect ✅

Commit: f2efaf08

Removed session.shellLogs.length from the effect dependency array. Only filteredLogs.length remains — both change per message, so having both was redundant and caused double-firing.

Review Item #5 — No tests despite 8 files changed ✅

Commit: 4406da63

Added src/__tests__/renderer/components/auto-scroll.test.tsx (412 lines) covering:

  • Settings integration (default value, persistence via button click)
  • Keyboard shortcut registration (Alt+Meta+S in DEFAULT_SHORTCUTS)
  • Button rendering (AI mode only, not in terminal mode, backward compatible without props)
  • Button state (active vs inactive styling, click toggles setAutoScrollAiMode)
  • Pause/resume behavior (scroll away pauses, click resume snaps to bottom)
  • Props threading (accepts and uses autoScrollAiMode/setAutoScrollAiMode)

Additional Fix: Missing QuickActionsModal entry

Commit: 31f2d522

Found that toggleAutoScroll was the only shortcut from this PR without a Cmd+K command palette entry — unlike comparable shortcuts (toggleBookmark, usageDashboard, openSymphony, directorNotes) which all have entries.

Files changed:

  • QuickActionsModal.tsx — Added action entry with dynamic label ("Enable/Disable Auto-Scroll AI Output") + shortcut display
  • AppModals.tsx — Threaded autoScrollAiMode/setAutoScrollAiMode through both AppUtilityModalsProps and AppModalsProps interfaces
  • App.tsx — Passed the two props to AppModals

All 19,426 tests pass, lint and build clean.

…ggle

# Conflicts:
#	src/renderer/components/TerminalOutput.tsx
#	src/renderer/hooks/settings/useSettings.ts
@openasocket
Copy link
Contributor Author

Branch synced with latest main (e373e63)

Merged upstream/main into feat/auto-scroll-toggle to resolve the two merge conflicts:

  1. TerminalOutput.tsxonOpenInTab prop signature was reformatted to multi-line on main; resolved by keeping the auto-scroll props (autoScrollAiMode, setAutoScrollAiMode) alongside the updated onOpenInTab format.

  2. useSettings.ts — Settings hook was refactored on main to use a Zustand store (settingsStore.ts); resolved by integrating the autoScrollAiMode setting into the new store pattern (state field, setter action, and loadAllSettings loader).

Lint and all 19,697 tests pass.

Copy link
Collaborator

@pedramamini pedramamini left a comment

Choose a reason for hiding this comment

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

All four review items addressed cleanly in dedicated commits. Good additions beyond the review:

  • QuickActionsModal integration — auto-scroll toggle is now discoverable via Cmd+K command palette, consistent with other toggles
  • settingsStore.ts migration — implemented in the Zustand store pattern rather than only in useSettings hook, following the codebase's migration direction
  • 14 tests covering defaults, toggle persistence, shortcut registration, button rendering (AI vs terminal), click behavior, styling states, pause-on-scroll, resume-with-snap, props threading, and backward compatibility

The inline scrollTo on resume using behavior: 'auto' instead of reusing scrollToBottom() (which uses 'smooth') is the right call — snap > animate for resume.

LGTM.

@pedramamini pedramamini merged commit f3d7f55 into RunMaestro:main Feb 17, 2026
1 check failed
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

Comments