Skip to content

Expose API methods to create and open chats from other plugins#382

Open
taydr wants to merge 3 commits intoglowingjade:mainfrom
taydr:feature/obsidian-chat-api
Open

Expose API methods to create and open chats from other plugins#382
taydr wants to merge 3 commits intoglowingjade:mainfrom
taydr:feature/obsidian-chat-api

Conversation

@taydr
Copy link

@taydr taydr commented May 2, 2025

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

  • I have reviewed the guidelines for contributing to this repository.
  • I have performed a self-review of my code
  • I have performed a code linting check and type check (by running npm run lint:check and npm run type:check)
  • I have run the test suite (by running npm run test)
  • I have tested the functionality manually

Summary by CodeRabbit

  • New Features

    • Added the ability to create new chat conversations from selected text blocks or with an optional file attachment.
    • Introduced modals for entering a conversation ID or creating a new chat, improving chat management and accessibility.
    • Enabled programmatic loading and submission of chat conversations, allowing for more flexible chat workflows.
  • Improvements

    • Enhanced chat conversation management, including opening specific conversations by ID and awaiting assistant responses.
    • Streamlined chat creation with a new service for handling conversation lifecycle and real-time updates.
  • Bug Fixes

    • Improved handling of plugin unload to ensure chat-related references are properly cleared.

taydr added 3 commits May 2, 2025 13:34
- 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)
@coderabbitai
Copy link

coderabbitai bot commented May 2, 2025

Walkthrough

This 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 ChatService class is introduced to manage chat lifecycle, including prompt compilation, message persistence, and assistant response streaming, with robust error handling. The chat view and component APIs are extended to allow external control. Utility functions and modal UI components are added to support these workflows, and public APIs are exposed for plugin and external integration.

Changes

File(s) Change Summary
src/ChatView.tsx ChatView now stores a reference in the plugin, clears it on close, and exposes public methods for loading conversations and creating/submitting chats from blocks.
src/components/chat-view/Chat.tsx Chat component's imperative handle extended with loadConversation and createAndSubmitChat methods for programmatic control.
src/core/chat/ChatService.ts New ChatService class introduced to manage chat creation, prompt compilation, message persistence, and assistant streaming, with error handling and progress tracking.
src/hooks/useChatHistory.ts serializeChatMessage function is now exported for use in other modules.
src/main.ts Plugin class extended with chat service and chat view references, new public and private methods for chat management, UI modals integration, and lifecycle cleanup.
src/settings/ConversationPromptModal.ts New modal class for prompting user to enter a conversation ID, with UI and callback handling.
src/settings/CreateChatModal.ts New modal classes for creating a chat with an optional file, including file suggestion and selection UI.
src/utils/chat/plain-text-to-editor-state.ts New utility function for converting plain text to a minimal serialized editor state format.

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
Loading
sequenceDiagram
    participant Plugin
    participant ChatView
    participant ChatComponent

    Plugin->>ChatView: createAndSubmitChatFromBlock(blockData)
    ChatView->>ChatComponent: createAndSubmitChat(blockData)
    ChatComponent->>Plugin: Return conversationId
Loading

Poem

In the warren of code, new tunnels appear,
Chats now spring from blocks, or files held dear.
Modals pop up with a gentle request,
"What conversation? Which file suits you best?"
A service hops in, with streaming replies—
The rabbit approves, with bright, eager eyes!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 SerializedEditorState

If 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 format
src/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.
createAndSubmitChat builds another newMessage that 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 like compiled or compileResult reduces mental friction.

src/core/chat/ChatService.ts (2)

78-110: Type safety & property completeness in fallback object

In the RAG-fallback branch you cast a literal to CompilePromptResult while 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.subscribe writes 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 updateChat calls (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 submission

If the user types only whitespace the current !this.message guard 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 the chatService, but any internal listeners, timers, or FS watchers held by ChatManager (instantiated inside ChatService) will still be alive.
Consider adding an explicit dispose()/cleanup() method on ChatService (and chaining to ChatManager) 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 = null

This prevents hidden leaks when the plugin is disabled/reloaded.


442-459: Minor: duplicate ChatManager instances & bundler warnings

getChatService() dynamically imports ChatManager and instantiates a new one every time the plugin loads, independent of the DatabaseManager handed out elsewhere. If ChatManager sets up file-system watchers, multiple plugin reloads could layer duplicate watchers.

Two low-impact tweaks:

  1. Hoist the import to the top of the file so bundlers (esbuild / rollup) can tree-shake and avoid extra chunks.
  2. Cache the ChatManager alongside chatService, or better, inject the existing DatabaseManager to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 50708ad and b5ea582.

📒 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 serializeChatMessage exportable 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 mind

Exposing chatView and caching chatService are 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.

Comment on lines +505 to +556
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
},
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

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 generation

Consider 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.

Comment on lines +128 to +135
const donePromise = this.startStreamingResponse({
conversationId: chat.id,
initialMessages: compiledMessages,
promptGenerator,
})

this.streamDoneMap.set(chat.id, donePromise)

Copy link

Choose a reason for hiding this comment

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

🛠️ 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)
})

Comment on lines +360 to +375
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)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

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