feat: add Ollama icon and modern chat UI styling

- src/chat-view.ts: Add getIcon() returning 'bot' for the view tab icon.
  Improve render() with role-specific CSS classes (user vs assistant) and
  message header structure for better styling hooks.

- src/main.ts: Add ribbon icon ('bot') in the left sidebar that opens the
  chat view with a single click.

- styles.css (new): Modern chat UI with message bubbles, distinct user and
  assistant themes using Obsidian CSS variables, sticky input bar, styled
  send button with accent color, and emoji role indicators.

- install.sh: Copy styles.css into the plugin directory and verify its
  presence during installation.

- README.md: Include styles.css in manual install instructions.

- __mocks__/obsidian.ts: Add addRibbonIcon() mock for test compatibility.

- tests/chat-view.test.ts: Add getIcon() assertion.
This commit is contained in:
2026-05-19 21:18:50 +02:00
parent 138890b9d2
commit 810676ff21
8 changed files with 212 additions and 18 deletions
+26 -13
View File
@@ -76,6 +76,10 @@ export class ChatView extends ItemView {
return 'Ollama Chat';
}
getIcon(): string {
return 'bot';
}
async onOpen(): Promise<void> {
try {
await this.ollamaClient.initializeCache();
@@ -145,9 +149,14 @@ export class ChatView extends ItemView {
contentEl.textContent = msg.content;
}
} else {
const messageEl = container.createEl('div', { cls: 'ollama-message' });
const messageEl = container.createEl('div', {
cls: `ollama-message ollama-message-${msg.role}`,
});
messageEl.setAttribute('data-msg-id', msg.id);
messageEl.createEl('div', { cls: 'ollama-message-role', text: msg.role });
const headerEl = messageEl.createEl('div', { cls: 'ollama-message-header' });
headerEl.createEl('span', { cls: 'ollama-message-role', text: msg.role });
const contentEl = messageEl.createEl('div', { cls: 'ollama-message-content' });
contentEl.textContent = msg.content;
}
@@ -366,15 +375,15 @@ export class ChatView extends ItemView {
): Promise<void> {
const toolResults = (
await Promise.all(
toolCalls.slice(0, MAX_TOOL_CALLS).map(async (toolCall) => {
try {
const toolResult = await this.toolExecutor.handleToolCall(toolCall);
return { ...toolResult, id: toolCall.id };
} catch (error) {
ErrorHandler.handleError(error, 'ChatView.handleUserInput');
return null;
}
})
toolCalls.slice(0, MAX_TOOL_CALLS).map(async (toolCall) => {
try {
const toolResult = await this.toolExecutor.handleToolCall(toolCall);
return { ...toolResult, id: toolCall.id };
} catch (error) {
ErrorHandler.handleError(error, 'ChatView.handleUserInput');
return null;
}
})
)
).filter((result): result is NonNullable<typeof result> => result !== null);
@@ -449,7 +458,10 @@ export class ChatView extends ItemView {
}
try {
const entries = await this.vaultIndexer.searchVault(userMessage, this.settings.vaultSearchLimit);
const entries = await this.vaultIndexer.searchVault(
userMessage,
this.settings.vaultSearchLimit
);
const context = entries
.map((entry) => `${entry.title}\n${entry.content}`)
.join('\n\n')
@@ -459,7 +471,8 @@ export class ChatView extends ItemView {
: userMessage;
// Get the complete messages array for the LLM with all context layers
const completeMessages = this.conversationStateManager.getCompleteMessages(userMessageWithContext);
const completeMessages =
this.conversationStateManager.getCompleteMessages(userMessageWithContext);
const stream = this.ollamaClient.streamChat(completeMessages, tools);