# How Auto-Scroll and Conversation Title Generation Work in Claudian

> Discover how Claudian enables auto-scroll by tracking tab state and generates conversation titles with a specialized LLM service. Optimize your coding workflow today.

- Repository: [YishenTu/claudian](https://github.com/YishenTu/claudian)
- Tags: internals
- Published: 2026-03-17

---

**Claudian uses a per-tab scroll state flag coupled with a global setting to conditionally auto-scroll only when the user is already at the bottom, while it generates conversation titles via a dedicated LLM service that sanitizes AI output and respects manual user edits.**

The Obsidian plugin Claudian implements sophisticated UX patterns to keep chat interfaces responsive and organized. This article examines the exact implementation of auto-scroll and conversation title generation based on the source code in `YishenTu/claudian`.

## Auto-Scroll Mechanics in Claudian

Claudian pins the chat view to the newest messages only when the user has not manually scrolled up. The system relies on a dual-layer check: a **global setting** (`plugin.settings.enableAutoScroll`) and a **per-tab state flag** (`state.autoScrollEnabled`).

### Scroll Listener and State Management

The core logic resides in [`src/features/chat/tabs/Tab.ts`](https://github.com/YishenTu/claudian/blob/main/src/features/chat/tabs/Tab.ts). The scroll listener monitors every scroll event on the messages container:

- If the user scrolls away from the bottom, it immediately sets `state.autoScrollEnabled` to `false`
- When the user scrolls back within a **20 px threshold** of the bottom, it debounces re-enabling auto-scroll for **150 ms** to prevent flicker

This implementation appears in [lines 1006–1041 of Tab.ts](https://github.com/YishenTu/claudian/blob/main/src/features/chat/tabs/Tab.ts#L1006-L1041).

### StreamController scrollToBottom Implementation

When a new assistant message finishes streaming, [`StreamController.ts`](https://github.com/YishenTu/claudian/blob/main/StreamController.ts) invokes the `scrollToBottom` helper. This method performs a strict double-check before scrolling:

1. Verify `plugin.settings.enableAutoScroll` is true (global setting)
2. Verify `state.autoScrollEnabled` is true (per-tab flag)

Only if both conditions pass does the view scroll to the bottom. This prevents jarring jumps when the user is reading history while a stream completes. See [lines 982–990 of StreamController.ts](https://github.com/YishenTu/claudian/blob/main/src/features/chat/controllers/StreamController.ts#L982-L990).

### InputController Synchronization

For non-streaming updates, [`InputController.ts`](https://github.com/YishenTu/claudian/blob/main/InputController.ts) handles scroll synchronization via `syncScrollToBottomAfterRenderUpdates`. This method uses `requestAnimationFrame` to ensure the DOM has settled before checking the same dual flags and forcing the scroll. It guarantees the view stays glued to the bottom during normal message renders without interfering with user navigation. The implementation is in [lines 56–68 of InputController.ts](https://github.com/YishenTu/claudian/blob/main/src/features/chat/controllers/InputController.ts#L56-L68).

## Conversation Title Generation Architecture

Claudian creates human-readable titles automatically using Claude’s LLM, with a deterministic fallback mechanism and protection against overwriting manual edits.

### Fallback Title Generation

If AI title generation fails or is disabled, [`ConversationController.ts`](https://github.com/YishenTu/claudian/blob/main/ConversationController.ts) builds a deterministic title from the first user message. It extracts the first sentence and trims it to **50 characters**. This ensures every conversation has a meaningful label even when the LLM service is unavailable. The logic is in [lines 22–28 of ConversationController.ts](https://github.com/YishenTu/claudian/blob/main/src/features/chat/controllers/ConversationController.ts#L22-L28).

### AI-Powered Title Generation Service

The [`TitleGenerationService.ts`](https://github.com/YishenTu/claudian/blob/main/TitleGenerationService.ts) orchestrates the LLM-based generation:

1. **Model selection**: Uses the `titleGenerationModel` setting, falling back to an environment variable, then to a default
2. **Prompt preparation**: Crafts a prompt requesting a concise title
3. **Streaming response**: Launches Claude via the SDK and streams the response
4. **Sanitization**: Strips quotes, trailing punctuation, and enforces the **50-character maximum**
5. **Callback delivery**: Returns either `{success: true, title}` or an error object

The generation logic appears in [lines 66–88](https://github.com/YishenTu/claudian/blob/main/src/features/chat/services/TitleGenerationService.ts#L66-L88), with sanitization in [lines 188–202](https://github.com/YishenTu/claudian/blob/main/src/features/chat/services/TitleGenerationService.ts#L188-L202).

### Invocation Flow and User Edit Protection

Two controllers invoke the title service:

- **InputController**: Triggers generation after the first user message is sent ([lines 601–640](https://github.com/YishenTu/claudian/blob/main/src/features/chat/controllers/InputController.ts#L601-L640))
- **ConversationController**: Handles manual regeneration when the user clicks "Regenerate title" ([lines 30–78](https://github.com/YishenTu/claudian/blob/main/src/features/chat/controllers/ConversationController.ts#L30-L78))

Both implementations check the `userManuallyRenamed` flag before applying the AI-generated title. If the user has manually edited the title during generation, the callback discards the AI result and sets the status to `failed` or clears it. The `titleGenerationStatus` field (`pending`, `success`, `failed`) drives UI spinners and error badges in the conversation list.

**Cancellation handling**: The service maintains a per-conversation `AbortController` in an `activeGenerations` map. Starting a new generation aborts any in-flight request, ensuring only the latest generation completes.

## Practical Code Examples

### Manually Toggling Auto-Scroll

```typescript
// Assuming `plugin` is your ClaudianPlugin instance
function toggleAutoScroll(enable: boolean) {
  plugin.settings.enableAutoScroll = enable;
  plugin.saveSettings();          // persist the change
  // Force a UI refresh – the next message render will respect the flag
}

```

When `enable` is `false`, the scroll listener in [`Tab.ts`](https://github.com/YishenTu/claudian/blob/main/Tab.ts) always sets `state.autoScrollEnabled = false`, so new messages appear without the view jumping to the bottom.

### Triggering Title Generation Programmatically

```typescript
import type ClaudianPlugin from '@/main';
import type { TitleGenerationService } from '@/features/chat/services/TitleGenerationService';

async function forceTitleRefresh(plugin: ClaudianPlugin, convId: string) {
  const titleService = plugin.getTitleGenerationService();
  if (!titleService) return;

  const conv = await plugin.getConversationById(convId);
  if (!conv) return;

  // Use the first user message as the source text
  const firstUserMsg = conv.messages.find(m => m.role === 'user');
  if (!firstUserMsg) return;

  const content = firstUserMsg.displayContent ?? firstUserMsg.content;

  await titleService.generateTitle(convId, content, async (id, result) => {
    if (result.success) {
      await plugin.renameConversation(id, result.title);
      await plugin.updateConversation(id, { titleGenerationStatus: 'success' });
    } else {
      await plugin.updateConversation(id, { titleGenerationStatus: 'failed' });
    }
  });
}

```

This mirrors `ConversationController.regenerateTitle` and can be invoked from custom UI commands.

## Summary

- **Auto-scroll** requires both the global `enableAutoScroll` setting and the per-tab `state.autoScrollEnabled` flag to be true; scrolling up automatically disables the per-tab flag until the user returns to within 20 px of the bottom
- **Scroll synchronization** happens in [`StreamController.ts`](https://github.com/YishenTu/claudian/blob/main/StreamController.ts) for streamed responses and [`InputController.ts`](https://github.com/YishenTu/claudian/blob/main/InputController.ts) for regular message renders, both using the same guard-checked routine
- **Title generation** uses a deterministic fallback (first 50 chars of the first message) when AI generation fails
- **AI titles** are generated by [`TitleGenerationService.ts`](https://github.com/YishenTu/claudian/blob/main/TitleGenerationService.ts) with model selection, streaming, and 50-character sanitization
- **User edits are protected** via the `userManuallyRenamed` flag, which prevents AI-generated titles from overwriting manual changes
- **Cancellation** is handled per-conversation using `AbortController` to prevent race conditions

## Frequently Asked Questions

### How does Claudian prevent auto-scroll when reading old messages?

Claudian tracks a per-tab boolean `state.autoScrollEnabled` in [`Tab.ts`](https://github.com/YishenTu/claudian/blob/main/Tab.ts). When the user scrolls up past a 20-pixel threshold from the bottom, the scroll listener immediately sets this flag to `false`. Both [`StreamController.ts`](https://github.com/YishenTu/claudian/blob/main/StreamController.ts) and [`InputController.ts`](https://github.com/YishenTu/claudian/blob/main/InputController.ts) check this flag before calling `scrollToBottom`, effectively pausing auto-scroll until the user manually returns to the bottom of the conversation.

### What happens if the AI fails to generate a conversation title?

If the AI service returns an error or the streaming fails, the [`TitleGenerationService.ts`](https://github.com/YishenTu/claudian/blob/main/TitleGenerationService.ts) callback returns a failure status. The controller then falls back to a deterministic title generated from the first user message (first sentence, truncated to 50 characters) as implemented in [`ConversationController.ts`](https://github.com/YishenTu/claudian/blob/main/ConversationController.ts). The `titleGenerationStatus` field is set to `failed` to indicate the issue in the UI.

### Can I disable auto-scroll globally in Claudian?

Yes. The setting `plugin.settings.enableAutoScroll` (defined in [`src/core/types/settings.ts`](https://github.com/YishenTu/claudian/blob/main/src/core/types/settings.ts)) controls the global behavior. When disabled, the scroll listener in [`Tab.ts`](https://github.com/YishenTu/claudian/blob/main/Tab.ts) effectively prevents `state.autoScrollEnabled` from remaining `true` during new message renders, and both [`StreamController.ts`](https://github.com/YishenTu/claudian/blob/main/StreamController.ts) and [`InputController.ts`](https://github.com/YishenTu/claudian/blob/main/InputController.ts) respect this global flag in their scroll checks.

### How does Claudian handle concurrent title generation requests?

[`TitleGenerationService.ts`](https://github.com/YishenTu/claudian/blob/main/TitleGenerationService.ts) maintains an `activeGenerations` map that stores an `AbortController` for each conversation ID. When a new generation starts, any existing controller for that conversation is aborted. This ensures that rapid successive triggers (such as multiple "Regenerate title" clicks) only process the latest request, preventing race conditions and UI flicker.