Expose API methods to create and open chats from other plugins#382
Expose API methods to create and open chats from other plugins#382taydr wants to merge 3 commits intoglowingjade:mainfrom
Conversation
- Create ChatService class as central API for managing conversations - Add methods for programmatic chat creation and management - Add waitUntilFinished to track assistant response completion - Add createChatAndOpen convenience method for flow control - Make document attachments optional in chat creation - Improve chat title generation using initial message content - Handle file system race conditions in chat persistence - Add command to open conversations by ID - Add command to list all available conversation IDs
Lint & formatting issues resolved: • Fixed unused evt param in src/settings/CreateChatModal.ts • Replaced any and refined error typing in src/core/chat/ChatService.ts • Ran Prettier on src/main.ts npm run lint:check now passes (warnings only about React version / TS version, no errors)
WalkthroughThis set of changes introduces a headless chat service abstraction and enhances the chat UI and plugin interfaces to support programmatic chat creation, conversation loading, and block-based chat submission. New modal dialogs are added for user prompts when opening or creating chats. The Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Plugin
participant ChatView
participant ChatComponent
participant ChatService
User->>Plugin: Initiates chat creation (UI or API)
Plugin->>CreateChatModal: (Optional) Prompt for message and file
CreateChatModal-->>Plugin: Return message and file
Plugin->>ChatService: createChat(initialText, opts)
ChatService->>Plugin: Return conversationId
Plugin->>ChatView: Open and load conversation (if UI)
ChatView->>ChatComponent: loadConversation(conversationId)
ChatComponent-->>ChatView: Display conversation
sequenceDiagram
participant Plugin
participant ChatView
participant ChatComponent
Plugin->>ChatView: createAndSubmitChatFromBlock(blockData)
ChatView->>ChatComponent: createAndSubmitChat(blockData)
ChatComponent->>Plugin: Return conversationId
Poem
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (10)
src/utils/chat/plain-text-to-editor-state.ts (1)
1-31: Well-documented utility with clear purpose.The function provides a clean conversion from plain text to the required editor state format, which is essential for the programmatic chat creation capabilities being added.
However, the type assertion at line 30 (
as unknown as SerializedEditorState) might be masking potential issues with the structure being created. Consider:- } as unknown as SerializedEditorState + } as SerializedEditorStateIf the structure is incomplete, it would be better to properly match the full type or create a more specific intermediate type.
src/settings/ConversationPromptModal.ts (1)
1-61: Well-implemented modal for conversation selection.The implementation follows Obsidian's modal patterns and includes good UX features like auto-focus, keyboard handling, and clear UI elements.
One minor improvement could be to add form validation to prevent submission of empty conversation IDs:
- .onClick(() => { - this.close() - this.onSubmit(this.conversationId) + .onClick(() => { + if (this.conversationId.trim()) { + this.close() + this.onSubmit(this.conversationId) + }The same change should be applied to the Enter key handler at line 27.
src/ChatView.tsx (1)
142-161: Comprehensive implementation for creating chats from blocks.The method cleanly converts block data to the required format and delegates to the Chat component.
Consider adding validation for the input parameters to ensure that required fields are present and valid:
async createAndSubmitChatFromBlock(blockData: { file: TFile text: string startLine: number endLine: number }): Promise<string | undefined> { + // Validate input + if (!blockData.file || !blockData.text) { + console.error("Invalid block data: file and text are required"); + return undefined; + } // Convert to MentionableBlockData formatsrc/components/chat-view/Chat.tsx (2)
514-529: Duplicate block mentionable is added twice
handleNewChat(selectedBlock)has already inserted the selected block into the input message.
createAndSubmitChatbuilds anothernewMessagethat includes the same block again, which means the block will appear twice in the first conversation context.If you keep the explicit construction below, remove the block insertion in
handleNewChat(or vice-versa).
Leaving it unchanged is harmless but adds noise to the prompt and affects token-count based costs.
532-549: Minor readability nit – shadowed variable name
const compiledMessage = await promptGenerator.compileUserMessagePrompt(...)A variable called message usually represents a
ChatMessage, but here it is a prompt compilation result.
Renaming to something likecompiledorcompileResultreduces mental friction.src/core/chat/ChatService.ts (2)
78-110: Type safety & property completeness in fallback objectIn the RAG-fallback branch you cast a literal to
CompilePromptResultwhile omitting
similaritySearchResults, which that type likely marks as optional, and adding
shouldUseRAG, which the type probably doesn’t contain.This works only because of the explicit cast.
Prefer constructing an object that really satisfies the type to avoid silent drifts when the
type definition evolves.- compiled = { - promptContent: [...], - shouldUseRAG: false, - } as CompilePromptResult + compiled = { + promptContent: [...], + similaritySearchResults: [], + } satisfies CompilePromptResult
178-193: High-frequency persistence may cause excessive disk writes
responseGenerator.subscribewrites every token/partial chunk directly to disk via
chatManager.updateChat. For large responses this results in dozens or hundreds of
file writes per reply, which can be slow on some file systems and increases wear on
mobile devices.Consider debouncing/throttling the
updateChatcalls (e.g. every 250 ms) or only
persisting on newline / paragraph boundaries.src/settings/CreateChatModal.ts (1)
108-125: Trim and validate the initial message before submissionIf the user types only whitespace the current
!this.messageguard evaluates to
false, leading to an empty chat being created.- if (!this.message) { + if (!this.message.trim()) { new Notice('Please enter a message') return }src/main.ts (2)
162-167: Leak-safe unload: dispose the ChatService as well
onunload()nulls thechatService, but any internal listeners, timers, or FS watchers held byChatManager(instantiated insideChatService) will still be alive.
Consider adding an explicitdispose()/cleanup()method onChatService(and chaining toChatManager) and invoking it here before nulling the reference.- // ChatService cleanup - this.chatService = null + // ChatService cleanup + await this.chatService?.dispose?.(); // no-op if method not defined + this.chatService = nullThis prevents hidden leaks when the plugin is disabled/reloaded.
442-459: Minor: duplicateChatManagerinstances & bundler warnings
getChatService()dynamically importsChatManagerand instantiates a new one every time the plugin loads, independent of theDatabaseManagerhanded out elsewhere. IfChatManagersets up file-system watchers, multiple plugin reloads could layer duplicate watchers.Two low-impact tweaks:
- Hoist the
importto the top of the file so bundlers (esbuild / rollup) can tree-shake and avoid extra chunks.- Cache the
ChatManageralongsidechatService, or better, inject the existingDatabaseManagerto share resources.- const chatManagerModule = await import('./database/json/chat/ChatManager') - const chatManager = new chatManagerModule.ChatManager(this.app) + if (!this._chatManager) { + const { ChatManager } = await import('./database/json/chat/ChatManager') + this._chatManager = new ChatManager(this.app) + } + const chatManager = this._chatManager(Not critical for correctness, but improves resource usage and bundling.)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/ChatView.tsx(4 hunks)src/components/chat-view/Chat.tsx(2 hunks)src/core/chat/ChatService.ts(1 hunks)src/hooks/useChatHistory.ts(1 hunks)src/main.ts(4 hunks)src/settings/ConversationPromptModal.ts(1 hunks)src/settings/CreateChatModal.ts(1 hunks)src/utils/chat/plain-text-to-editor-state.ts(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/hooks/useChatHistory.ts (1)
src/types/chat.ts (2)
ChatMessage(42-45)SerializedChatMessage(82-85)
src/components/chat-view/Chat.tsx (2)
src/types/mentionable.ts (1)
MentionableBlockData(18-23)src/types/chat.ts (1)
ChatUserMessage(11-20)
src/ChatView.tsx (2)
__mocks__/obsidian.ts (1)
TFile(4-4)src/types/mentionable.ts (1)
MentionableBlockData(18-23)
src/main.ts (6)
src/ChatView.tsx (1)
ChatView(20-162)src/core/chat/ChatService.ts (2)
ChatService(21-204)CreateChatOptions(16-19)src/constants.ts (1)
CHAT_VIEW_TYPE(5-5)src/settings/ConversationPromptModal.ts (1)
ConversationPromptModal(3-61)__mocks__/obsidian.ts (1)
TFile(4-4)src/settings/CreateChatModal.ts (1)
CreateChatModal(45-137)
🔇 Additional comments (5)
src/hooks/useChatHistory.ts (1)
130-132: Appropriate export of utility function.Making
serializeChatMessageexportable enables reuse across components, which supports the new chat service functionality and programmatic chat creation flows introduced in this PR.src/ChatView.tsx (3)
31-32: Store view reference for external access.Storing a reference to the ChatView instance in the plugin enables other plugins to interact with it, which is essential for the new API capabilities.
55-57: Proper cleanup of view reference.Good practice to clear the reference when the view is closed to prevent memory leaks and stale references.
135-140: New method for loading conversations by ID.Well-documented method that delegates to the Chat component, enabling external plugins to open specific conversations.
src/main.ts (1)
35-38: Public-facing references are fine, just keep encapsulation in mindExposing
chatViewand cachingchatServiceare practical for cross-component coordination and lazy init; no immediate issues spotted.
Just be mindful that other modules could start relying on these props directly – if you ever change the lifecycle you may need to turn them into proper getters or an evented API.
| selectedBlock: MentionableBlockData, | ||
| ): Promise<string> => { | ||
| // Create a new chat | ||
| handleNewChat(selectedBlock) | ||
|
|
||
| // Get the new conversation ID | ||
| const newConversationId = currentConversationId | ||
|
|
||
| // Create a new message with the selected block | ||
| const newMessage: ChatUserMessage = { | ||
| role: 'user', | ||
| content: null, | ||
| promptContent: null, | ||
| id: uuidv4(), | ||
| mentionables: [ | ||
| { | ||
| type: 'current-file', | ||
| file: app.workspace.getActiveFile(), | ||
| }, | ||
| { | ||
| type: 'block', | ||
| ...selectedBlock, | ||
| }, | ||
| ], | ||
| } | ||
|
|
||
| // Compile and submit the message | ||
| const compiledMessage = await promptGenerator.compileUserMessagePrompt({ | ||
| message: newMessage, | ||
| useVaultSearch: true, | ||
| onQueryProgressChange: setQueryProgress, | ||
| }) | ||
|
|
||
| const compiledMessages = [ | ||
| { | ||
| ...newMessage, | ||
| promptContent: compiledMessage.promptContent, | ||
| similaritySearchResults: compiledMessage.similaritySearchResults, | ||
| }, | ||
| ] | ||
|
|
||
| // Update the chat messages | ||
| setChatMessages(compiledMessages) | ||
|
|
||
| // Submit the message | ||
| submitChatMutation.mutate({ | ||
| chatMessages: compiledMessages, | ||
| conversationId: newConversationId, | ||
| }) | ||
|
|
||
| return newConversationId | ||
| }, |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Conversation ID captured before state update – returned ID may be stale
handleNewChat(selectedBlock) triggers setCurrentConversationId(uuidv4()), but setState is asynchronous.
Immediately after the call you read currentConversationId into newConversationId, which still contains the old conversation id, so the method is very likely to return the wrong id and to submit the first message to the previous conversation.
- // Create a new chat
- handleNewChat(selectedBlock)
-
- // Get the new conversation ID
- const newConversationId = currentConversationId
+ // Create a new chat **and capture the newly-generated id**.
+ const newConversationId = uuidv4()
+ setCurrentConversationId(newConversationId)
+
+ // Reset chat state
+ handleNewChat(selectedBlock) // adapt handleNewChat to accept an id or remove its own uuid generationConsider returning the id from handleNewChat, or accepting the generated id as an argument, so that the value used everywhere is guaranteed to be the same.
Committable suggestion skipped: line range outside the PR's diff.
| const donePromise = this.startStreamingResponse({ | ||
| conversationId: chat.id, | ||
| initialMessages: compiledMessages, | ||
| promptGenerator, | ||
| }) | ||
|
|
||
| this.streamDoneMap.set(chat.id, donePromise) | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
streamDoneMap is never cleaned – potential memory leak
You store every donePromise in streamDoneMap but never delete it once the
stream finishes. In a long-running session the map could grow indefinitely.
this.streamDoneMap.set(chat.id, donePromise)
donePromise.finally(() => {
this.streamDoneMap.delete(chat.id)
})| async openConversation(conversationId: string): Promise<void> { | ||
| // Make sure chat view is open | ||
| await this.activateChatView() | ||
|
|
||
| // Get the chat view | ||
| const leaf = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0] | ||
| if (!leaf || !(leaf.view instanceof ChatView)) { | ||
| throw new Error('Failed to open chat view') | ||
| } | ||
|
|
||
| // Open the conversation | ||
| await leaf.view.loadConversation(conversationId) | ||
|
|
||
| // Reveal the leaf | ||
| this.app.workspace.revealLeaf(leaf) | ||
| } |
There was a problem hiding this comment.
Race condition: loadConversation may execute before the view is ready
activateChatView() resolves once the workspace leaf’s viewState is set, not when ChatView.onOpen → render() finishes.
If ChatView.render() hasn’t mounted yet, chatRef.current is still null, so loadConversation() silently does nothing and the user sees an empty chat.
A safe pattern is to expose a ready() promise from ChatView that resolves after the first render, then await it here:
- // Open the conversation
- await leaf.view.loadConversation(conversationId)
+ // Wait until React tree is mounted before loading conversation
+ // (ChatView.ready() should resolve after the first render)
+ if ('ready' in leaf.view && typeof (leaf.view as any).ready === 'function') {
+ await (leaf.view as any).ready()
+ }
+ await (leaf.view as ChatView).loadConversation(conversationId)You’ll need to add in ChatView:
private readyResolver!: () => void
public readonly ready = new Promise<void>((res) => { this.readyResolver = res })
async onOpen() {
await this.render()
this.readyResolver?.() // signal ready
this.initialChatProps = undefined
}Failing to guard against this race makes the new API unreliable for external plugins.
Description
Hello! On several occasions I have wanted to integrate other plugins with Smart Composer, but lacked the tools.
I've added methods that let me create (and start) a new conversation (returning the ID), and open a specific conversation by ID. This means I can create Smart Composer conversations from objects in my other plugins, and associate them so I can open them.
I'm submitting a PR in case you find it useful to include these changes, but for my purposes, I'm happy using a fork, so no pressure.
Thanks again for making Smart Composer! It's great.
Taylor
Checklist before requesting a review
npm run lint:checkandnpm run type:check)npm run test)Summary by CodeRabbit
New Features
Improvements
Bug Fixes