Resolve merge conflicts - keep local plugin versions and settings
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vendored
+2
-3
@@ -14,12 +14,11 @@
|
||||
"highlightr-plugin",
|
||||
"obsidian-excalidraw-plugin",
|
||||
"code-styler",
|
||||
"note-buddy",
|
||||
"claudian",
|
||||
"calendar",
|
||||
"better-word-count",
|
||||
"table-editor-obsidian",
|
||||
"obsidian-advanced-uri",
|
||||
"advanced-canvas",
|
||||
"obsidian-git"
|
||||
"obsidian-git",
|
||||
"claudian"
|
||||
]
|
||||
Vendored
+497
-98
@@ -239,6 +239,16 @@ var OpenCodeClient = class _OpenCodeClient {
|
||||
this.providersCacheTime = 0;
|
||||
this.cachedDefaults = null;
|
||||
}
|
||||
/**
|
||||
* Fetches available AI providers and models from the OpenCode server.
|
||||
*
|
||||
* Results are cached for 5 minutes to reduce API calls. The cache can be bypassed
|
||||
* by setting forceRefresh to true or by calling clearModelsCache() first.
|
||||
*
|
||||
* @param forceRefresh - If true, bypasses cache and fetches fresh data from server
|
||||
* @returns Array of providers with their available models
|
||||
* @throws Error if the server is unreachable, returns invalid JSON, or responds with non-200 status
|
||||
*/
|
||||
async getCapabilities(forceRefresh = false) {
|
||||
const now = Date.now();
|
||||
if (!forceRefresh && this.cachedProviders && now - this.providersCacheTime < MODELS_CACHE_TTL) {
|
||||
@@ -294,12 +304,360 @@ var ChatView = class extends import_obsidian2.ItemView {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
this.messages = [];
|
||||
this.isUserNearBottom = true;
|
||||
this.hasInitialScroll = false;
|
||||
this.previousScrollTop = 0;
|
||||
this.previousWasNearBottom = true;
|
||||
this.isInitialized = false;
|
||||
this.handleMessagesScroll = () => {
|
||||
const threshold = 32;
|
||||
const distanceToBottom = this.messagesContainer.scrollHeight - this.messagesContainer.scrollTop - this.messagesContainer.clientHeight;
|
||||
this.isUserNearBottom = distanceToBottom <= threshold;
|
||||
this.previousScrollTop = this.messagesContainer.scrollTop;
|
||||
this.previousWasNearBottom = this.isUserNearBottom;
|
||||
if (this.isUserNearBottom) {
|
||||
this.hideNewMessageIndicator();
|
||||
}
|
||||
};
|
||||
this.client = new OpenCodeClient(
|
||||
plugin.settings.serviceUrl,
|
||||
plugin.settings.defaultModelId,
|
||||
plugin.onModelUnavailable?.bind(plugin)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Show loading indicator
|
||||
*/
|
||||
showLoading() {
|
||||
if (this.loadingIndicator) return;
|
||||
this.loadingIndicator = this.messagesContainer.createDiv({ cls: "nb-loading-indicator" });
|
||||
const dotsContainer = this.loadingIndicator.createDiv({ cls: "nb-loading-dots" });
|
||||
dotsContainer.createDiv({ cls: "nb-loading-dot" });
|
||||
dotsContainer.createDiv({ cls: "nb-loading-dot" });
|
||||
dotsContainer.createDiv({ cls: "nb-loading-dot" });
|
||||
this.maybeAutoScroll();
|
||||
}
|
||||
/**
|
||||
* Hide loading indicator
|
||||
*/
|
||||
hideLoading() {
|
||||
if (this.loadingIndicator) {
|
||||
this.loadingIndicator.remove();
|
||||
this.loadingIndicator = void 0;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Scroll to bottom of messages
|
||||
*/
|
||||
scrollToBottom() {
|
||||
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
|
||||
this.hideNewMessageIndicator();
|
||||
}
|
||||
showNewMessageIndicator() {
|
||||
if (!this.newMessageIndicator) return;
|
||||
this.newMessageIndicator.style.display = "flex";
|
||||
}
|
||||
hideNewMessageIndicator() {
|
||||
if (!this.newMessageIndicator) return;
|
||||
this.newMessageIndicator.style.display = "none";
|
||||
}
|
||||
maybeAutoScroll() {
|
||||
if (!this.messagesContainer) return;
|
||||
if (!this.hasInitialScroll) {
|
||||
this.scrollToBottom();
|
||||
this.hasInitialScroll = true;
|
||||
return;
|
||||
}
|
||||
if (this.isUserNearBottom) {
|
||||
this.scrollToBottom();
|
||||
} else {
|
||||
this.showNewMessageIndicator();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Render error message with retry button
|
||||
*/
|
||||
renderErrorMessage(errorText, retryMessage) {
|
||||
const errorContainer = this.messagesContainer.createDiv({ cls: "nb-error-container" });
|
||||
errorContainer.createDiv({ cls: "nb-error-message", text: errorText });
|
||||
if (retryMessage) {
|
||||
const retryButton = errorContainer.createEl("button", {
|
||||
cls: "nb-retry-button",
|
||||
text: "Retry"
|
||||
});
|
||||
retryButton.onclick = () => {
|
||||
const errorIndex = this.messages.findIndex((m) => m.kind === "error" && m.text === errorText);
|
||||
if (errorIndex !== -1) {
|
||||
this.messages.splice(errorIndex, 1);
|
||||
}
|
||||
this.retrySendMessage(retryMessage);
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Send message with timeout
|
||||
*/
|
||||
async sendWithTimeout(promise, timeoutMs) {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise(
|
||||
(_, reject) => setTimeout(() => reject(new Error("Request timed out after 30 seconds")), timeoutMs)
|
||||
)
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Retry sending a message
|
||||
*/
|
||||
async retrySendMessage(message) {
|
||||
this.renderMessages();
|
||||
this.showLoading();
|
||||
try {
|
||||
if (!this.plugin.sessionState) {
|
||||
this.plugin.sessionState = await this.sendWithTimeout(
|
||||
this.client.createSession(),
|
||||
3e4
|
||||
);
|
||||
}
|
||||
const sendData = {
|
||||
parts: [{ type: "text", text: message }]
|
||||
};
|
||||
if (this.plugin.settings.defaultModelId) {
|
||||
const parts = this.plugin.settings.defaultModelId.split("/");
|
||||
if (parts.length === 2) {
|
||||
sendData.model = {
|
||||
providerID: parts[0],
|
||||
modelID: parts[1]
|
||||
};
|
||||
}
|
||||
}
|
||||
let response;
|
||||
try {
|
||||
response = await this.sendWithTimeout(
|
||||
this.client.sendMessageToSession(this.plugin.sessionState.sessionID, sendData),
|
||||
3e4
|
||||
);
|
||||
} catch (sendError) {
|
||||
const err = sendError;
|
||||
if (err.message.includes("Session not found")) {
|
||||
this.plugin.sessionState = await this.sendWithTimeout(
|
||||
this.client.createSession(),
|
||||
3e4
|
||||
);
|
||||
response = await this.sendWithTimeout(
|
||||
this.client.sendMessageToSession(this.plugin.sessionState.sessionID, sendData),
|
||||
3e4
|
||||
);
|
||||
} else {
|
||||
throw sendError;
|
||||
}
|
||||
}
|
||||
this.hideLoading();
|
||||
for (const part of response.parts) {
|
||||
this.messages.push({ kind: "assistantPart", part });
|
||||
}
|
||||
this.renderMessages();
|
||||
await this.saveChatState();
|
||||
} catch (error) {
|
||||
this.hideLoading();
|
||||
this.messages.push({
|
||||
kind: "error",
|
||||
text: `Failed to send message: ${error.message}`,
|
||||
retryMessage: message
|
||||
});
|
||||
this.renderMessages();
|
||||
await this.saveChatState();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clear chat history with confirmation
|
||||
*/
|
||||
async clearChat() {
|
||||
const confirmed = confirm("Are you sure you want to clear the chat history? This cannot be undone.");
|
||||
if (!confirmed) return;
|
||||
this.messages = [];
|
||||
this.plugin.sessionState = void 0;
|
||||
await this.saveChatState();
|
||||
this.renderMessages();
|
||||
try {
|
||||
this.plugin.sessionState = await this.sendWithTimeout(
|
||||
this.client.createSession(),
|
||||
3e4
|
||||
);
|
||||
await this.saveChatState();
|
||||
} catch (error) {
|
||||
console.error("[NoteBuddy] Failed to create new session after clearing chat:", error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Render markdown text to HTML elements
|
||||
* Supports: code blocks, inline code, bold, italic, lists
|
||||
*/
|
||||
renderMarkdown(text, container) {
|
||||
const lines = text.split("\n");
|
||||
let inCodeBlock = false;
|
||||
let codeBlockContent = [];
|
||||
let listItems = [];
|
||||
let inList = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.trim().startsWith("```")) {
|
||||
if (inCodeBlock) {
|
||||
const pre = container.createEl("pre");
|
||||
const code = pre.createEl("code");
|
||||
code.textContent = codeBlockContent.join("\n");
|
||||
codeBlockContent = [];
|
||||
inCodeBlock = false;
|
||||
} else {
|
||||
inCodeBlock = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inCodeBlock) {
|
||||
codeBlockContent.push(line);
|
||||
continue;
|
||||
}
|
||||
const listMatch = line.match(/^(\s*)([-*]|\d+\.)\s+(.+)$/);
|
||||
if (listMatch) {
|
||||
if (!inList) {
|
||||
inList = true;
|
||||
listItems = [];
|
||||
}
|
||||
listItems.push(listMatch[3]);
|
||||
continue;
|
||||
} else if (inList) {
|
||||
const ul = container.createEl("ul");
|
||||
listItems.forEach((item) => {
|
||||
const li = ul.createEl("li");
|
||||
this.renderInlineMarkdown(item, li);
|
||||
});
|
||||
listItems = [];
|
||||
inList = false;
|
||||
}
|
||||
if (line.trim()) {
|
||||
const p = container.createEl("span");
|
||||
this.renderInlineMarkdown(line, p);
|
||||
if (i < lines.length - 1) {
|
||||
container.createEl("br");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inList) {
|
||||
const ul = container.createEl("ul");
|
||||
listItems.forEach((item) => {
|
||||
const li = ul.createEl("li");
|
||||
this.renderInlineMarkdown(item, li);
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Render inline markdown (bold, italic, inline code)
|
||||
*/
|
||||
renderInlineMarkdown(text, container) {
|
||||
const codeRegex = /`([^`]+)`/g;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
const parts = [];
|
||||
while ((match = codeRegex.exec(text)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
parts.push({ type: "text", content: text.substring(lastIndex, match.index) });
|
||||
}
|
||||
parts.push({ type: "code", content: match[1] });
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
if (lastIndex < text.length) {
|
||||
parts.push({ type: "text", content: text.substring(lastIndex) });
|
||||
}
|
||||
parts.forEach((part) => {
|
||||
if (part.type === "code") {
|
||||
container.createEl("code", { text: part.content });
|
||||
} else {
|
||||
this.renderTextWithFormatting(part.content, container);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Render a message bubble with appropriate styling
|
||||
*/
|
||||
renderMessageBubble(kind, content, container) {
|
||||
const messageEl = container.createDiv({
|
||||
cls: `nb-message nb-message-${kind}`
|
||||
});
|
||||
this.renderMarkdown(content, messageEl);
|
||||
return messageEl;
|
||||
}
|
||||
/**
|
||||
* Render text with bold and italic formatting
|
||||
*/
|
||||
renderTextWithFormatting(text, container) {
|
||||
const boldRegex = /\*\*([^*]+)\*\*/g;
|
||||
const italicRegex = /\*([^*]+)\*/g;
|
||||
let result = text;
|
||||
const elements = [];
|
||||
let match;
|
||||
while ((match = boldRegex.exec(text)) !== null) {
|
||||
elements.push({ start: match.index, end: match.index + match[0].length, type: "bold" });
|
||||
}
|
||||
while ((match = italicRegex.exec(text)) !== null) {
|
||||
const currentMatch = match;
|
||||
const isBold = elements.some((e) => e.start <= currentMatch.index && currentMatch.index < e.end);
|
||||
if (!isBold) {
|
||||
elements.push({
|
||||
start: currentMatch.index,
|
||||
end: currentMatch.index + currentMatch[0].length,
|
||||
type: "italic"
|
||||
});
|
||||
}
|
||||
}
|
||||
elements.sort((a, b) => a.start - b.start);
|
||||
let lastIndex = 0;
|
||||
elements.forEach((el) => {
|
||||
if (el.start > lastIndex) {
|
||||
container.appendText(text.substring(lastIndex, el.start));
|
||||
}
|
||||
const content = text.substring(el.start, el.end);
|
||||
if (el.type === "bold") {
|
||||
const cleaned = content.replace(/\*\*/g, "");
|
||||
container.createEl("strong", { text: cleaned });
|
||||
} else {
|
||||
const cleaned = content.replace(/\*/g, "");
|
||||
container.createEl("em", { text: cleaned });
|
||||
}
|
||||
lastIndex = el.end;
|
||||
});
|
||||
if (lastIndex < text.length) {
|
||||
container.appendText(text.substring(lastIndex));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Save chat state to plugin data
|
||||
*/
|
||||
async saveChatState() {
|
||||
try {
|
||||
const chatState = {
|
||||
messages: this.messages,
|
||||
sessionState: this.plugin.sessionState
|
||||
};
|
||||
await this.plugin.saveData({ ...this.plugin.settings, chatState });
|
||||
} catch (error) {
|
||||
console.error("[NoteBuddy] Failed to save chat state:", error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Load chat state from plugin data
|
||||
*/
|
||||
async loadChatState() {
|
||||
try {
|
||||
const data = await this.plugin.loadData();
|
||||
if (data?.chatState) {
|
||||
this.messages = data.chatState.messages || [];
|
||||
if (data.chatState.sessionState) {
|
||||
this.plugin.sessionState = data.chatState.sessionState;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[NoteBuddy] Failed to load chat state:", error);
|
||||
}
|
||||
}
|
||||
getViewType() {
|
||||
return VIEW_TYPE_CHAT;
|
||||
}
|
||||
@@ -307,80 +665,63 @@ var ChatView = class extends import_obsidian2.ItemView {
|
||||
return "NoteBuddy Chat";
|
||||
}
|
||||
async onOpen() {
|
||||
if (!this.isInitialized) {
|
||||
await this.loadChatState();
|
||||
this.isInitialized = true;
|
||||
}
|
||||
const container = this.containerEl.children[1];
|
||||
container.empty();
|
||||
container.addClass("note-buddy-chat-view");
|
||||
const chatContainer = container.createDiv({ cls: "nb-chat-container" });
|
||||
chatContainer.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 1rem;
|
||||
`;
|
||||
const toolbar = chatContainer.createDiv({ cls: "nb-toolbar" });
|
||||
toolbar.createDiv({ cls: "nb-toolbar-title", text: "NoteBuddy Chat" });
|
||||
const toolbarActions = toolbar.createDiv({ cls: "nb-toolbar-actions" });
|
||||
const clearButton = toolbarActions.createEl("button", {
|
||||
cls: "nb-toolbar-button",
|
||||
attr: { "aria-label": "Clear chat", title: "Clear chat" }
|
||||
});
|
||||
(0, import_obsidian2.setIcon)(clearButton, "trash-2");
|
||||
clearButton.onclick = () => this.clearChat();
|
||||
const messagesContainer = chatContainer.createDiv({ cls: "nb-messages-container" });
|
||||
this.messagesContainer = messagesContainer;
|
||||
messagesContainer.style.cssText = `
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
`;
|
||||
this.messagesContainer.addEventListener("scroll", this.handleMessagesScroll);
|
||||
this.previousScrollTop = 0;
|
||||
this.previousWasNearBottom = true;
|
||||
const newMessageIndicator = chatContainer.createDiv({ cls: "nb-new-message-indicator" });
|
||||
const indicatorButton = newMessageIndicator.createEl("button", {
|
||||
cls: "nb-new-message-button",
|
||||
text: "\u65B0\u6D88\u606F",
|
||||
attr: { "aria-label": "New messages" }
|
||||
});
|
||||
indicatorButton.onclick = () => {
|
||||
this.scrollToBottom();
|
||||
};
|
||||
this.newMessageIndicator = newMessageIndicator;
|
||||
this.hideNewMessageIndicator();
|
||||
this.renderMessages();
|
||||
const welcomeMessage = messagesContainer.createDiv({ cls: "nb-message nb-message-system" });
|
||||
welcomeMessage.createSpan({ text: "Welcome to NoteBuddy! Chat with your notes here." });
|
||||
welcomeMessage.style.cssText = `
|
||||
padding: 0.75rem;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
`;
|
||||
const inputContainer = chatContainer.createDiv({ cls: "nb-input-container" });
|
||||
inputContainer.style.cssText = `
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
`;
|
||||
const textarea = inputContainer.createEl("textarea", {
|
||||
cls: "nb-chat-input",
|
||||
attr: { placeholder: "Type your message..." }
|
||||
});
|
||||
textarea.style.cssText = `
|
||||
flex: 1;
|
||||
min-height: 60px;
|
||||
max-height: 150px;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
`;
|
||||
const sendButton = inputContainer.createEl("button", {
|
||||
cls: "nb-send-button",
|
||||
text: "Send"
|
||||
attr: { "aria-label": "Send message", title: "Send message" }
|
||||
});
|
||||
sendButton.style.cssText = `
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
`;
|
||||
(0, import_obsidian2.setIcon)(sendButton, "send");
|
||||
const sendMessage = async () => {
|
||||
const message = textarea.value.trim();
|
||||
if (!message) return;
|
||||
this.messages.push({ kind: "userText", text: message });
|
||||
this.renderMessages();
|
||||
textarea.value = "";
|
||||
this.showLoading();
|
||||
try {
|
||||
if (!this.plugin.sessionState) {
|
||||
this.plugin.sessionState = await this.client.createSession();
|
||||
this.plugin.sessionState = await this.sendWithTimeout(
|
||||
this.client.createSession(),
|
||||
3e4
|
||||
);
|
||||
}
|
||||
const sendData = {
|
||||
parts: [{ type: "text", text: message }]
|
||||
@@ -396,24 +737,41 @@ var ChatView = class extends import_obsidian2.ItemView {
|
||||
}
|
||||
let response;
|
||||
try {
|
||||
response = await this.client.sendMessageToSession(this.plugin.sessionState.sessionID, sendData);
|
||||
response = await this.sendWithTimeout(
|
||||
this.client.sendMessageToSession(this.plugin.sessionState.sessionID, sendData),
|
||||
3e4
|
||||
);
|
||||
} catch (sendError) {
|
||||
const err = sendError;
|
||||
if (err.message.includes("Session not found")) {
|
||||
this.plugin.sessionState = await this.client.createSession();
|
||||
response = await this.client.sendMessageToSession(this.plugin.sessionState.sessionID, sendData);
|
||||
this.plugin.sessionState = await this.sendWithTimeout(
|
||||
this.client.createSession(),
|
||||
3e4
|
||||
);
|
||||
response = await this.sendWithTimeout(
|
||||
this.client.sendMessageToSession(this.plugin.sessionState.sessionID, sendData),
|
||||
3e4
|
||||
);
|
||||
} else {
|
||||
throw sendError;
|
||||
}
|
||||
}
|
||||
this.hideLoading();
|
||||
for (const part of response.parts) {
|
||||
this.messages.push({ kind: "assistantPart", part });
|
||||
}
|
||||
this.renderMessages();
|
||||
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
|
||||
await this.saveChatState();
|
||||
} catch (error) {
|
||||
this.hideLoading();
|
||||
console.error("[NoteBuddy] Send failed:", error);
|
||||
new import_obsidian2.Notice(`Failed to send message: ${error.message}`);
|
||||
this.messages.push({
|
||||
kind: "error",
|
||||
text: `Failed to send message: ${error.message}`,
|
||||
retryMessage: message
|
||||
});
|
||||
this.renderMessages();
|
||||
await this.saveChatState();
|
||||
}
|
||||
};
|
||||
sendButton.onclick = sendMessage;
|
||||
@@ -428,49 +786,38 @@ var ChatView = class extends import_obsidian2.ItemView {
|
||||
console.log("[NoteBuddy] Chat view closed");
|
||||
}
|
||||
renderMessages() {
|
||||
const shouldRestoreScroll = !this.isUserNearBottom;
|
||||
const previousScrollTop = this.previousScrollTop;
|
||||
this.messagesContainer.empty();
|
||||
if (this.messages.length === 0) {
|
||||
const welcomeMessage = this.messagesContainer.createDiv({ cls: "nb-message nb-message-system" });
|
||||
welcomeMessage.createSpan({ text: "Welcome to NoteBuddy! Chat with your notes here." });
|
||||
welcomeMessage.style.cssText = `
|
||||
padding: 0.75rem;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
`;
|
||||
const emptyState = this.messagesContainer.createDiv({ cls: "nb-empty-state" });
|
||||
emptyState.createDiv({ cls: "nb-empty-title", text: "Welcome to NoteBuddy" });
|
||||
emptyState.createDiv({
|
||||
cls: "nb-empty-subtitle",
|
||||
text: "Ask your notes a question or start a new conversation."
|
||||
});
|
||||
this.hideNewMessageIndicator();
|
||||
this.hasInitialScroll = true;
|
||||
return;
|
||||
}
|
||||
for (const item of this.messages) {
|
||||
if (item.kind === "userText") {
|
||||
const messageEl2 = this.messagesContainer.createDiv({ cls: "nb-message nb-message-user" });
|
||||
messageEl2.createSpan({ text: item.text });
|
||||
messageEl2.style.cssText = `
|
||||
padding: 0.75rem;
|
||||
background-color: var(--background-modifier-accent);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
align-self: flex-end;
|
||||
max-width: 70%;
|
||||
`;
|
||||
this.renderMessageBubble("user", item.text, this.messagesContainer);
|
||||
continue;
|
||||
}
|
||||
if (item.kind === "error") {
|
||||
this.renderErrorMessage(item.text, item.retryMessage);
|
||||
continue;
|
||||
}
|
||||
const part = item.part;
|
||||
const messageEl = this.messagesContainer.createDiv({ cls: "nb-message nb-message-assistant" });
|
||||
messageEl.style.cssText = `
|
||||
padding: 0.75rem;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
align-self: flex-start;
|
||||
max-width: 70%;
|
||||
`;
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
messageEl.createSpan({ text: part.text });
|
||||
this.renderMarkdown(part.text, messageEl);
|
||||
break;
|
||||
case "reasoning":
|
||||
messageEl.createSpan({ text: part.text });
|
||||
messageEl.createEl("div", { text: "Reasoning:", cls: "nb-part-label" });
|
||||
this.renderMarkdown(part.text, messageEl);
|
||||
break;
|
||||
case "tool":
|
||||
messageEl.createEl("strong", { text: `Tool: ${part.tool}` });
|
||||
@@ -514,6 +861,12 @@ var ChatView = class extends import_obsidian2.ItemView {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldRestoreScroll) {
|
||||
this.messagesContainer.scrollTop = previousScrollTop;
|
||||
this.showNewMessageIndicator();
|
||||
} else {
|
||||
this.maybeAutoScroll();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -536,44 +889,81 @@ var NoteBuddySettingTab = class extends import_obsidian3.PluginSettingTab {
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
new import_obsidian3.Setting(containerEl).setName("Service URL").setDesc("The URL of the OpenCode service (must include protocol, hostname, and port)").addText(
|
||||
const connectionGroup = containerEl.createDiv({ cls: "nb-settings-group" });
|
||||
connectionGroup.createEl("h3", { text: "Connection Settings", cls: "nb-settings-group-title" });
|
||||
new import_obsidian3.Setting(connectionGroup).setName("Service URL").setDesc("The URL of the OpenCode service (must include protocol, hostname, and port)").addText(
|
||||
(text) => text.setPlaceholder("http://127.0.0.1:4096").setValue(this.plugin.settings.serviceUrl).onChange(async (value) => {
|
||||
const previousServiceUrl = this.plugin.settings.serviceUrl;
|
||||
const validationEl = text.inputEl.parentElement?.querySelector(".nb-settings-validation-error, .nb-settings-validation-success");
|
||||
if (validationEl) {
|
||||
validationEl.remove();
|
||||
}
|
||||
if (!value.startsWith("http://") && !value.startsWith("https://")) {
|
||||
new import_obsidian3.Notice("Service URL must start with http:// or https://");
|
||||
const errorEl = text.inputEl.parentElement?.createDiv({ cls: "nb-settings-validation-error" });
|
||||
if (errorEl) errorEl.textContent = "Service URL must start with http:// or https://";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const url = new URL(value);
|
||||
if (!url.hostname || url.hostname === "") {
|
||||
new import_obsidian3.Notice("Service URL must include a valid hostname");
|
||||
const errorEl = text.inputEl.parentElement?.createDiv({ cls: "nb-settings-validation-error" });
|
||||
if (errorEl) errorEl.textContent = "Service URL must include a valid hostname";
|
||||
return;
|
||||
}
|
||||
if (!url.port || url.port === "") {
|
||||
new import_obsidian3.Notice("Service URL must include a port number");
|
||||
const errorEl = text.inputEl.parentElement?.createDiv({ cls: "nb-settings-validation-error" });
|
||||
if (errorEl) errorEl.textContent = "Service URL must include a port number";
|
||||
return;
|
||||
}
|
||||
const successEl = text.inputEl.parentElement?.createDiv({ cls: "nb-settings-validation-success" });
|
||||
if (successEl) successEl.textContent = "\u2713 Valid URL format";
|
||||
} catch (e) {
|
||||
new import_obsidian3.Notice("Service URL must be a valid URL format");
|
||||
const errorEl = text.inputEl.parentElement?.createDiv({ cls: "nb-settings-validation-error" });
|
||||
if (errorEl) errorEl.textContent = "Service URL must be a valid URL format";
|
||||
return;
|
||||
}
|
||||
if (this.plugin.sessionState && previousServiceUrl !== value) {
|
||||
if (this.lastServiceUrlWarning !== value) {
|
||||
new import_obsidian3.Notice("Active chat detected. Clear the current conversation before switching the Service URL.");
|
||||
this.lastServiceUrlWarning = value;
|
||||
}
|
||||
text.setValue(previousServiceUrl);
|
||||
return;
|
||||
}
|
||||
this.plugin.settings.serviceUrl = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new import_obsidian3.Setting(containerEl).setName("Test Connection").setDesc("Test the connection to the service").addButton(
|
||||
(button) => button.setButtonText("Test").setCta().onClick(async () => {
|
||||
new import_obsidian3.Setting(connectionGroup).setName("Test Connection").setDesc("Test the connection to the service").addButton(
|
||||
(button) => button.setIcon("plug").setButtonText("Test").setCta().onClick(async () => {
|
||||
button.setButtonText("Testing...");
|
||||
button.setDisabled(true);
|
||||
const client = new OpenCodeClient(this.plugin.settings.serviceUrl, this.plugin.settings.defaultModelId);
|
||||
const result = await client.healthCheck();
|
||||
button.setButtonText("Test");
|
||||
button.setDisabled(false);
|
||||
if (result.status === "connected") {
|
||||
new import_obsidian3.Notice("Connection successful!");
|
||||
new import_obsidian3.Notice("\u2713 Connection successful!");
|
||||
} else {
|
||||
new import_obsidian3.Notice(`Connection failed: ${result.lastError}`);
|
||||
new import_obsidian3.Notice(`\u2717 Connection failed: ${result.lastError}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
this.displayModelSelection(containerEl);
|
||||
this.displayRefreshButton(containerEl);
|
||||
const modelGroup = containerEl.createDiv({ cls: "nb-settings-group" });
|
||||
modelGroup.createEl("h3", { text: "Model Selection", cls: "nb-settings-group-title" });
|
||||
this.displayModelSelection(modelGroup);
|
||||
this.displayRefreshButton(modelGroup);
|
||||
}
|
||||
/**
|
||||
* Loads available AI providers and models from the OpenCode server.
|
||||
*
|
||||
* Sets the loading state and updates the providers list. On error, sets providers
|
||||
* to an empty array and logs the error. The loading state is always cleared in finally.
|
||||
*
|
||||
* @param client - Optional OpenCodeClient instance to use (creates new one if not provided)
|
||||
* @param forceRefresh - If true, bypasses cache and fetches fresh data from server
|
||||
* @throws Error if the server request fails (error is logged and re-thrown)
|
||||
*/
|
||||
async loadProviders(client, forceRefresh) {
|
||||
if (this.isLoadingModels) {
|
||||
return;
|
||||
@@ -590,6 +980,15 @@ var NoteBuddySettingTab = class extends import_obsidian3.PluginSettingTab {
|
||||
this.isLoadingModels = false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Displays the model selection dropdown in the settings UI.
|
||||
*
|
||||
* Builds a dropdown with options in "Provider Name - Model Name" format, including
|
||||
* a "Use server default" option. The current selection is restored from settings,
|
||||
* and changes are immediately persisted via plugin.saveSettings().
|
||||
*
|
||||
* @param containerEl - The HTML element to add the dropdown setting to
|
||||
*/
|
||||
displayModelSelection(containerEl) {
|
||||
const modelOptions = {};
|
||||
modelOptions[""] = "Use server default";
|
||||
@@ -616,7 +1015,7 @@ var NoteBuddySettingTab = class extends import_obsidian3.PluginSettingTab {
|
||||
}
|
||||
displayRefreshButton(container) {
|
||||
new import_obsidian3.Setting(container).setName("Refresh Models").setDesc("Reload the list of available models from the service").addButton(
|
||||
(button) => button.setButtonText("Refresh").onClick(async () => {
|
||||
(button) => button.setIcon("refresh-cw").setButtonText("Refresh").onClick(async () => {
|
||||
const client = new OpenCodeClient(this.plugin.settings.serviceUrl, this.plugin.settings.defaultModelId);
|
||||
client.clearModelsCache();
|
||||
try {
|
||||
|
||||
Vendored
+56
-1
@@ -1,5 +1,60 @@
|
||||
{
|
||||
"choices": [],
|
||||
"choices": [
|
||||
{
|
||||
"id": "daily-note-choice",
|
||||
"name": "📅 Daily Note",
|
||||
"type": "Template",
|
||||
"command": true,
|
||||
"templatePath": "06_Metadata/Templates/Daily Note Template.md",
|
||||
"fileNameFormat": {
|
||||
"enabled": true,
|
||||
"format": "{{DATE:YYYY-MM-DD}}"
|
||||
},
|
||||
"folder": {
|
||||
"enabled": true,
|
||||
"folders": ["00_Inbox"],
|
||||
"chooseWhenCreatingNote": false,
|
||||
"createFolder": false,
|
||||
"chooseFromSubfolders": false
|
||||
},
|
||||
"appendLink": false,
|
||||
"incrementFileName": false,
|
||||
"openFileInNewTab": {
|
||||
"enabled": true,
|
||||
"direction": "vertical",
|
||||
"focus": true
|
||||
},
|
||||
"openFile": true,
|
||||
"openFileInMode": "default"
|
||||
},
|
||||
{
|
||||
"id": "quick-capture-choice",
|
||||
"name": "💡 Quick Capture",
|
||||
"type": "Template",
|
||||
"command": true,
|
||||
"templatePath": "06_Metadata/Templates/quick-note.md",
|
||||
"fileNameFormat": {
|
||||
"enabled": true,
|
||||
"format": "{{DATE:YYYY-MM-DD-HHmm}} - {{VALUE}}"
|
||||
},
|
||||
"folder": {
|
||||
"enabled": true,
|
||||
"folders": ["00_Inbox"],
|
||||
"chooseWhenCreatingNote": false,
|
||||
"createFolder": false,
|
||||
"chooseFromSubfolders": false
|
||||
},
|
||||
"appendLink": false,
|
||||
"incrementFileName": false,
|
||||
"openFileInNewTab": {
|
||||
"enabled": true,
|
||||
"direction": "vertical",
|
||||
"focus": true
|
||||
},
|
||||
"openFile": true,
|
||||
"openFileInMode": "default"
|
||||
}
|
||||
],
|
||||
"inputPrompt": "single-line",
|
||||
"persistInputPromptDrafts": true,
|
||||
"useSelectionAsCaptureValue": true,
|
||||
|
||||
Reference in New Issue
Block a user