How Auto-Scroll and Conversation Title Generation Work in Claudian
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. The scroll listener monitors every scroll event on the messages container:
- If the user scrolls away from the bottom, it immediately sets
state.autoScrollEnabledtofalse - 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.
StreamController scrollToBottom Implementation
When a new assistant message finishes streaming, StreamController.ts invokes the scrollToBottom helper. This method performs a strict double-check before scrolling:
- Verify
plugin.settings.enableAutoScrollis true (global setting) - Verify
state.autoScrollEnabledis 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.
InputController Synchronization
For non-streaming updates, 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.
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 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.
AI-Powered Title Generation Service
The TitleGenerationService.ts orchestrates the LLM-based generation:
- Model selection: Uses the
titleGenerationModelsetting, falling back to an environment variable, then to a default - Prompt preparation: Crafts a prompt requesting a concise title
- Streaming response: Launches Claude via the SDK and streams the response
- Sanitization: Strips quotes, trailing punctuation, and enforces the 50-character maximum
- Callback delivery: Returns either
{success: true, title}or an error object
The generation logic appears in lines 66–88, with sanitization in lines 188–202.
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)
- ConversationController: Handles manual regeneration when the user clicks "Regenerate title" (lines 30–78)
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
// 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 always sets state.autoScrollEnabled = false, so new messages appear without the view jumping to the bottom.
Triggering Title Generation Programmatically
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
enableAutoScrollsetting and the per-tabstate.autoScrollEnabledflag 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.tsfor streamed responses andInputController.tsfor 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.tswith model selection, streaming, and 50-character sanitization - User edits are protected via the
userManuallyRenamedflag, which prevents AI-generated titles from overwriting manual changes - Cancellation is handled per-conversation using
AbortControllerto 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. 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 and 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 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. 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) controls the global behavior. When disabled, the scroll listener in Tab.ts effectively prevents state.autoScrollEnabled from remaining true during new message renders, and both StreamController.ts and InputController.ts respect this global flag in their scroll checks.
How does Claudian handle concurrent title generation requests?
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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →