From 1ef19eb9987bc604b0f6f9dd3efa1e0dca93c144 Mon Sep 17 00:00:00 2001 From: acxtrilla Date: Wed, 29 Apr 2026 12:54:01 +0100 Subject: [PATCH] Add global skill discovery for AI sidebar --- main.js | 16 ++--- main.ts | 190 ++++++++++++++++++++++++++++++++++++++++++++++++----- styles.css | 73 ++++++++++++++++++++ 3 files changed, 255 insertions(+), 24 deletions(-) diff --git a/main.js b/main.js index ca2c236..d5250ab 100644 --- a/main.js +++ b/main.js @@ -1,11 +1,11 @@ /* Obsidian AI Sidebar */ -var A=Object.defineProperty;var K=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var H=Object.prototype.hasOwnProperty;var q=(s,t)=>{for(var e in t)A(s,e,{get:t[e],enumerable:!0})},G=(s,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of W(t))!H.call(s,i)&&i!==e&&A(s,i,{get:()=>t[i],enumerable:!(n=K(t,i))||n.enumerable});return s};var J=s=>G(A({},"__esModule",{value:!0}),s);var Pe={};q(Pe,{default:()=>w});module.exports=J(Pe);var c=require("obsidian"),E=require("child_process"),m="ai-sidebar-view",x=[{id:"codex",name:"Codex",description:"Sign in with Codex locally and use the Codex CLI agent.",provider:{id:"codex",name:"Codex",authType:"cli",command:"codex exec",signInCommand:"codex login",models:"gpt-5.4, gpt-5.4-mini, gpt-5.3-codex",reasoningEffort:"medium"}},{id:"claude",name:"Claude Code",description:"Sign in with your Claude subscription or Anthropic Console account.",provider:{id:"claude",name:"Claude Code",authType:"cli",command:"claude",signInCommand:"claude auth login",setupUrl:"https://code.claude.com/docs/en/cli-usage",models:"opus, sonnet, haiku"}},{id:"opencode",name:"opencode",description:"Connect opencode to any provider it supports, including subscription/API accounts.",provider:{id:"opencode",name:"opencode",authType:"cli",command:"opencode",signInCommand:"opencode auth login",setupUrl:"https://opencode.ai/docs/cli/",models:"anthropic/claude-sonnet-4-5, openai/gpt-5.4, google/gemini-2.5-pro"}},{id:"openai",name:"OpenAI API",description:"Use an OpenAI API key with the Responses API.",provider:{id:"openai",name:"OpenAI",authType:"api-key",command:"",setupUrl:"https://platform.openai.com/api-keys",apiBaseUrl:"https://api.openai.com/v1/responses",model:"gpt-4.1-mini",models:"gpt-4.1-mini, gpt-4.1, o4-mini",reasoningEffort:"medium"}},{id:"anthropic-api",name:"Anthropic API",description:"Use an Anthropic API key directly through an OpenAI-compatible proxy endpoint.",provider:{id:"anthropic-api",name:"Anthropic API",authType:"api-key",command:"",setupUrl:"https://console.anthropic.com/settings/keys",apiBaseUrl:"",model:"claude-sonnet-4-5",models:"claude-sonnet-4-5, claude-opus-4-1, claude-haiku-4-5"}},{id:"gemini-cli",name:"Gemini CLI",description:"Use Google Gemini CLI with its native sign-in and subscription/API setup.",provider:{id:"gemini-cli",name:"Gemini CLI",authType:"cli",command:"gemini",signInCommand:"gemini",setupUrl:"https://google-gemini.github.io/gemini-cli/docs/",models:"gemini-2.5-pro, gemini-2.5-flash"}},{id:"openrouter",name:"OpenRouter",description:"Use one API key for many model providers through an OpenAI-compatible endpoint.",provider:{id:"openrouter",name:"OpenRouter",authType:"api-key",command:"",setupUrl:"https://openrouter.ai/settings/keys",apiBaseUrl:"https://openrouter.ai/api/v1/chat/completions",model:"openai/gpt-4.1-mini",models:"openai/gpt-4.1-mini, anthropic/claude-sonnet-4.5, google/gemini-2.5-pro",reasoningEffort:"medium"}},{id:"deepseek",name:"DeepSeek",description:"Use DeepSeek with an API key and OpenAI-compatible endpoint.",provider:{id:"deepseek",name:"DeepSeek",authType:"api-key",command:"",setupUrl:"https://platform.deepseek.com/api_keys",apiBaseUrl:"https://api.deepseek.com/v1/chat/completions",model:"deepseek-chat",models:"deepseek-chat, deepseek-reasoner"}},{id:"mistral",name:"Mistral",description:"Use Mistral models with an API key and OpenAI-compatible endpoint.",provider:{id:"mistral",name:"Mistral",authType:"api-key",command:"",setupUrl:"https://console.mistral.ai/api-keys",apiBaseUrl:"https://api.mistral.ai/v1/chat/completions",model:"mistral-large-latest",models:"mistral-large-latest, codestral-latest, ministral-8b-latest"}},{id:"aider",name:"Aider",description:"Use Aider with keys from OpenAI, Anthropic, Gemini, OpenRouter, DeepSeek, and more.",provider:{id:"aider",name:"Aider",authType:"cli",command:"aider",signInCommand:"aider",setupUrl:"https://aider.chat/docs/config/api-keys.html",models:"sonnet, opus, gpt-4.1, o4-mini, gemini/gemini-2.5-pro"}}],Q={providers:[S(x[0].provider),S(x[3].provider)],defaultProviderId:"codex",defaultAccessMode:"confirm",includeFolders:"",excludeFolders:".obsidian, node_modules, .git",maxContextChars:45e3,enableConversationMemory:!0,maxMemoryMessages:30,rememberedMessages:[]},w=class extends c.Plugin{settings;async onload(){await this.loadSettings(),this.registerView(m,t=>new b(t,this)),this.addRibbonIcon("sparkles","Toggle AI Sidebar",()=>{this.toggleSidebar()}),this.addCommand({id:"toggle-ai-sidebar",name:"Toggle AI Sidebar",hotkeys:[{modifiers:["Mod","Shift"],key:"A"}],callback:()=>{this.toggleSidebar()}}),this.addSettingTab(new C(this.app,this))}onunload(){this.app.workspace.detachLeavesOfType(m)}async toggleSidebar(){if(this.app.workspace.getLeavesOfType(m).first()){this.app.workspace.detachLeavesOfType(m);return}let e=this.app.workspace.getRightLeaf(!1);if(!e){new c.Notice("Could not open the AI sidebar.");return}await e.setViewState({type:m,active:!0}),this.app.workspace.revealLeaf(e)}async loadSettings(){let t=await this.loadData();this.settings=Object.assign({},Q,t),this.settings.providers=ye(this.settings.providers),this.settings.rememberedMessages=Array.isArray(this.settings.rememberedMessages)?this.settings.rememberedMessages:[]}async saveSettings(){await this.saveData(this.settings)}getProvider(t){return this.settings.providers.find(e=>e.id===t)}async rememberMessages(t){this.settings.enableConversationMemory&&(this.settings.rememberedMessages=t.slice(-this.settings.maxMemoryMessages),await this.saveSettings())}async clearMemory(){this.settings.rememberedMessages=[],await this.saveSettings()}async upsertProviderFromPreset(t){let e=S(t.provider),n=this.settings.providers.findIndex(i=>i.id===e.id);n>=0?this.settings.providers[n]={...e,apiKey:this.settings.providers[n].apiKey,accessToken:this.settings.providers[n].accessToken,refreshToken:this.settings.providers[n].refreshToken}:this.settings.providers.push(e),this.settings.defaultProviderId=e.id,await this.saveSettings()}},b=class extends c.ItemView{plugin;messages=[];providerId;accessMode;threadEl;textareaEl;contextEl;slashEl;activeContextPath="";refreshTimer;sendButton;isRunning=!1;constructor(t,e){super(t),this.plugin=e,this.messages=e.settings.enableConversationMemory?[...e.settings.rememberedMessages]:[],this.providerId=e.settings.defaultProviderId,this.accessMode=e.settings.defaultAccessMode}getViewType(){return m}getDisplayText(){return"AI Sidebar"}getIcon(){return"sparkles"}async onOpen(){this.render(),this.registerEvent(this.app.workspace.on("active-leaf-change",()=>{this.scheduleContextRefresh()})),this.registerEvent(this.app.workspace.on("file-open",()=>{this.scheduleContextRefresh()})),this.registerEvent(this.app.vault.on("modify",t=>{t instanceof c.TFile&&t.path===this.activeContextPath&&this.scheduleContextRefresh()})),await this.refreshContextPreview()}async onClose(){this.contentEl.empty()}render(){this.contentEl.empty(),this.contentEl.addClass("ai-sidebar");let t=this.contentEl.createDiv("ai-sidebar__header");t.createDiv({cls:"ai-sidebar__title",text:"AI Sidebar"}),t.createDiv({cls:"ai-sidebar__subtitle",text:"Vault-aware agents"});let e=this.contentEl.createDiv("ai-sidebar__controls");new c.DropdownComponent(e).addOptions(this.providerOptions()).setValue(this.providerId).onChange(a=>{this.providerId=a}),new c.DropdownComponent(e).addOptions({"read-only":"Read only",confirm:"Confirm actions","full-access":"Full access"}).setValue(this.accessMode).onChange(a=>{this.accessMode=a,this.refreshContextPreview()}),this.contextEl=this.contentEl.createDiv("ai-sidebar__context"),this.threadEl=this.contentEl.createDiv("ai-sidebar__thread"),this.renderMessages();let n=this.contentEl.createDiv("ai-sidebar__composer");this.textareaEl=n.createEl("textarea",{cls:"ai-sidebar__input",attr:{placeholder:"Ask about your vault, draft a note, or request an edit...",rows:"4"}}),this.textareaEl.addEventListener("keydown",a=>{if(a.key==="Escape"&&!this.slashEl.hasClass("is-hidden")){a.preventDefault(),this.hideSlashCommands();return}a.key==="Enter"&&!a.shiftKey&&(a.preventDefault(),this.sendPrompt())}),this.textareaEl.addEventListener("input",()=>{this.updateSlashCommands()}),this.slashEl=n.createDiv("ai-sidebar__slash is-hidden");let i=n.createDiv("ai-sidebar__composer-actions");new c.ButtonComponent(i).setButtonText("Refresh context").onClick(()=>{this.refreshContextPreview()}),new c.ButtonComponent(i).setButtonText("Clear").onClick(()=>{this.clearConversation()}),this.sendButton=new c.ButtonComponent(i).setCta().setButtonText("Send").onClick(()=>{this.sendPrompt()})}providerOptions(){return this.plugin.settings.providers.reduce((t,e)=>(t[e.id]=e.name||e.id,t),{})}renderMessages(){if(this.threadEl.empty(),this.messages.length===0){let t=this.threadEl.createDiv("ai-sidebar__empty");t.createDiv({text:"Ask a local agent to work with the notes in this vault."}),t.createDiv({text:"Use confirm mode when you want to review file edits first."});return}for(let t of this.messages){let e=this.threadEl.createDiv(`ai-sidebar__message ai-sidebar__message--${t.role}`);e.createDiv({cls:"ai-sidebar__message-role",text:t.role}),e.createEl("pre",{text:t.content})}this.threadEl.scrollTo({top:this.threadEl.scrollHeight})}async refreshContextPreview(t=""){let e=await F(this.plugin.app,this.plugin.settings,t,this.extractSelectedSkillNames(t));this.activeContextPath=e.activeFile?.path??"",this.contextEl.empty(),this.contextEl.createDiv({cls:"ai-sidebar__context-title",text:`Context preview \xB7 ${this.accessModeLabel()}`});let n=[e.activeFile?`Active: ${e.activeFile.path}`:"No active note",e.selection?"Selection included":"No selection",`${e.linkedFiles.length} linked`,`${e.backlinkFiles.length} backlinks`,`${e.relevantFiles.length} relevant`,`${e.baseFiles.length} bases`,`${e.selectedSkills.length} skills`],i=this.contextEl.createEl("ul");for(let a of n)i.createEl("li",{text:a})}scheduleContextRefresh(){window.clearTimeout(this.refreshTimer),this.refreshTimer=window.setTimeout(()=>{this.refreshContextPreview(this.textareaEl?.value??"")},150)}accessModeLabel(){return this.accessMode==="read-only"?"read only":this.accessMode==="full-access"?"full access":"confirm actions"}async sendPrompt(){let t=this.textareaEl.value.trim();if(!t||this.isRunning)return;let e=this.plugin.getProvider(this.providerId);if(!e){new c.Notice("Choose a provider in AI Sidebar settings.");return}if(!fe(e)){new c.Notice("Connect or configure this provider in AI Sidebar settings first.");return}this.isRunning=!0,this.sendButton.setButtonText("Running..."),this.textareaEl.value="";let n=this.parsePromptControls(t,e),i=n.accessMode??this.accessMode;this.messages.push({role:"user",content:t}),this.renderMessages();try{let a=await F(this.plugin.app,this.plugin.settings,n.cleanPrompt,n.selectedSkillNames);await this.refreshContextPreview(t);let r={prompt:n.cleanPrompt,accessMode:i,context:a,instructions:ie(i),conversation:this.plugin.settings.enableConversationMemory?this.messages.slice(0,-1):[],options:n.options},l=await ae(e,r,this.app),o=await this.handleActions(l),d=o?`${l} +var te=Object.create;var w=Object.defineProperty;var se=Object.getOwnPropertyDescriptor;var ne=Object.getOwnPropertyNames;var ie=Object.getPrototypeOf,ae=Object.prototype.hasOwnProperty;var oe=(s,t)=>{for(var e in t)w(s,e,{get:t[e],enumerable:!0})},R=(s,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of ne(t))!ae.call(s,i)&&i!==e&&w(s,i,{get:()=>t[i],enumerable:!(n=se(t,i))||n.enumerable});return s};var re=(s,t,e)=>(e=s!=null?te(ie(s)):{},R(t||!s||!s.__esModule?w(e,"default",{value:s,enumerable:!0}):e,s)),le=s=>R(w({},"__esModule",{value:!0}),s);var Le={};oe(Le,{default:()=>b});module.exports=le(Le);var c=require("obsidian"),I=require("child_process"),_=require("fs"),C=require("os"),A=re(require("path")),m="ai-sidebar-view",k=[{id:"codex",name:"Codex",description:"Sign in with Codex locally and use the Codex CLI agent.",provider:{id:"codex",name:"Codex",authType:"cli",command:"codex exec",signInCommand:"codex login",models:"gpt-5.4, gpt-5.4-mini, gpt-5.3-codex",reasoningEffort:"medium"}},{id:"claude",name:"Claude Code",description:"Sign in with your Claude subscription or Anthropic Console account.",provider:{id:"claude",name:"Claude Code",authType:"cli",command:"claude",signInCommand:"claude auth login",setupUrl:"https://code.claude.com/docs/en/cli-usage",models:"opus, sonnet, haiku"}},{id:"opencode",name:"opencode",description:"Connect opencode to any provider it supports, including subscription/API accounts.",provider:{id:"opencode",name:"opencode",authType:"cli",command:"opencode",signInCommand:"opencode auth login",setupUrl:"https://opencode.ai/docs/cli/",models:"anthropic/claude-sonnet-4-5, openai/gpt-5.4, google/gemini-2.5-pro"}},{id:"openai",name:"OpenAI API",description:"Use an OpenAI API key with the Responses API.",provider:{id:"openai",name:"OpenAI",authType:"api-key",command:"",setupUrl:"https://platform.openai.com/api-keys",apiBaseUrl:"https://api.openai.com/v1/responses",model:"gpt-4.1-mini",models:"gpt-4.1-mini, gpt-4.1, o4-mini",reasoningEffort:"medium"}},{id:"anthropic-api",name:"Anthropic API",description:"Use an Anthropic API key directly through an OpenAI-compatible proxy endpoint.",provider:{id:"anthropic-api",name:"Anthropic API",authType:"api-key",command:"",setupUrl:"https://console.anthropic.com/settings/keys",apiBaseUrl:"",model:"claude-sonnet-4-5",models:"claude-sonnet-4-5, claude-opus-4-1, claude-haiku-4-5"}},{id:"gemini-cli",name:"Gemini CLI",description:"Use Google Gemini CLI with its native sign-in and subscription/API setup.",provider:{id:"gemini-cli",name:"Gemini CLI",authType:"cli",command:"gemini",signInCommand:"gemini",setupUrl:"https://google-gemini.github.io/gemini-cli/docs/",models:"gemini-2.5-pro, gemini-2.5-flash"}},{id:"openrouter",name:"OpenRouter",description:"Use one API key for many model providers through an OpenAI-compatible endpoint.",provider:{id:"openrouter",name:"OpenRouter",authType:"api-key",command:"",setupUrl:"https://openrouter.ai/settings/keys",apiBaseUrl:"https://openrouter.ai/api/v1/chat/completions",model:"openai/gpt-4.1-mini",models:"openai/gpt-4.1-mini, anthropic/claude-sonnet-4.5, google/gemini-2.5-pro",reasoningEffort:"medium"}},{id:"deepseek",name:"DeepSeek",description:"Use DeepSeek with an API key and OpenAI-compatible endpoint.",provider:{id:"deepseek",name:"DeepSeek",authType:"api-key",command:"",setupUrl:"https://platform.deepseek.com/api_keys",apiBaseUrl:"https://api.deepseek.com/v1/chat/completions",model:"deepseek-chat",models:"deepseek-chat, deepseek-reasoner"}},{id:"mistral",name:"Mistral",description:"Use Mistral models with an API key and OpenAI-compatible endpoint.",provider:{id:"mistral",name:"Mistral",authType:"api-key",command:"",setupUrl:"https://console.mistral.ai/api-keys",apiBaseUrl:"https://api.mistral.ai/v1/chat/completions",model:"mistral-large-latest",models:"mistral-large-latest, codestral-latest, ministral-8b-latest"}},{id:"aider",name:"Aider",description:"Use Aider with keys from OpenAI, Anthropic, Gemini, OpenRouter, DeepSeek, and more.",provider:{id:"aider",name:"Aider",authType:"cli",command:"aider",signInCommand:"aider",setupUrl:"https://aider.chat/docs/config/api-keys.html",models:"sonnet, opus, gpt-4.1, o4-mini, gemini/gemini-2.5-pro"}}],j={providers:[M(k[0].provider),M(k[3].provider)],defaultProviderId:"codex",defaultAccessMode:"confirm",includeFolders:"",excludeFolders:".obsidian, node_modules, .git",maxContextChars:45e3,enableConversationMemory:!0,maxMemoryMessages:30,rememberedMessages:[]},b=class extends c.Plugin{settings;async onload(){await this.loadSettings(),this.registerView(m,t=>new P(t,this)),this.addRibbonIcon("sparkles","Toggle AI Sidebar",()=>{this.toggleSidebar()}),this.addCommand({id:"toggle-ai-sidebar",name:"Toggle AI Sidebar",hotkeys:[{modifiers:["Mod","Shift"],key:"A"}],callback:()=>{this.toggleSidebar()}}),this.addSettingTab(new S(this.app,this))}onunload(){this.app.workspace.detachLeavesOfType(m)}async toggleSidebar(){if(this.app.workspace.getLeavesOfType(m).first()){this.app.workspace.detachLeavesOfType(m);return}let e=this.app.workspace.getRightLeaf(!1);if(!e){new c.Notice("Could not open the AI sidebar.");return}await e.setViewState({type:m,active:!0}),this.app.workspace.revealLeaf(e)}async loadSettings(){let t=await this.loadData();this.settings=Object.assign({},j,t),this.settings.providers=Me(this.settings.providers),this.settings.rememberedMessages=Array.isArray(this.settings.rememberedMessages)?this.settings.rememberedMessages:[]}async saveSettings(){await this.saveData(this.settings)}getProvider(t){return this.settings.providers.find(e=>e.id===t)}async rememberMessages(t){this.settings.enableConversationMemory&&(this.settings.rememberedMessages=t.slice(-this.settings.maxMemoryMessages),await this.saveSettings())}async clearMemory(){this.settings.rememberedMessages=[],await this.saveSettings()}async upsertProviderFromPreset(t){let e=M(t.provider),n=this.settings.providers.findIndex(i=>i.id===e.id);n>=0?this.settings.providers[n]={...e,apiKey:this.settings.providers[n].apiKey,accessToken:this.settings.providers[n].accessToken,refreshToken:this.settings.providers[n].refreshToken}:this.settings.providers.push(e),this.settings.defaultProviderId=e.id,await this.saveSettings()}},P=class extends c.ItemView{plugin;messages=[];providerId;accessMode;threadEl;textareaEl;contextEl;slashEl;modelBadgeEl;activeContextPath="";refreshTimer;sendButton;isRunning=!1;constructor(t,e){super(t),this.plugin=e,this.messages=e.settings.enableConversationMemory?[...e.settings.rememberedMessages]:[],this.providerId=e.settings.defaultProviderId,this.accessMode=e.settings.defaultAccessMode}getViewType(){return m}getDisplayText(){return"AI Sidebar"}getIcon(){return"sparkles"}async onOpen(){this.render(),this.registerEvent(this.app.workspace.on("active-leaf-change",()=>{this.scheduleContextRefresh()})),this.registerEvent(this.app.workspace.on("file-open",()=>{this.scheduleContextRefresh()})),this.registerEvent(this.app.vault.on("modify",t=>{t instanceof c.TFile&&t.path===this.activeContextPath&&this.scheduleContextRefresh()})),await this.refreshContextPreview()}async onClose(){this.contentEl.empty()}render(){this.contentEl.empty(),this.contentEl.addClass("ai-sidebar");let t=this.contentEl.createDiv("ai-sidebar__header");t.createDiv({cls:"ai-sidebar__title",text:"AI Sidebar"}),t.createDiv({cls:"ai-sidebar__subtitle",text:"Vault-aware agents"});let e=this.contentEl.createDiv("ai-sidebar__controls");new c.DropdownComponent(e).addOptions(this.providerOptions()).setValue(this.providerId).onChange(o=>{this.providerId=o,this.updateModelBadge()}),new c.DropdownComponent(e).addOptions({"read-only":"Read only",confirm:"Confirm actions","full-access":"Full access"}).setValue(this.accessMode).onChange(o=>{this.accessMode=o,this.refreshContextPreview()}),this.contextEl=this.contentEl.createDiv("ai-sidebar__context"),this.threadEl=this.contentEl.createDiv("ai-sidebar__thread"),this.renderMessages();let n=this.contentEl.createDiv("ai-sidebar__composer");this.textareaEl=n.createEl("textarea",{cls:"ai-sidebar__input",attr:{placeholder:"Ask about your vault, draft a note, or request an edit...",rows:"4"}}),this.textareaEl.addEventListener("keydown",o=>{if(o.key==="Escape"&&!this.slashEl.hasClass("is-hidden")){o.preventDefault(),this.hideSlashCommands();return}o.key==="Enter"&&!o.shiftKey&&(o.preventDefault(),this.sendPrompt())}),this.textareaEl.addEventListener("input",()=>{this.updateSlashCommands(),this.updateModelBadge()}),this.slashEl=n.createDiv("ai-sidebar__slash is-hidden");let i=n.createDiv("ai-sidebar__composer-actions");new c.ButtonComponent(i).setButtonText("Refresh context").onClick(()=>{this.refreshContextPreview()}),new c.ButtonComponent(i).setButtonText("Clear").onClick(()=>{this.clearConversation()}),this.modelBadgeEl=i.createDiv("ai-sidebar__model-badge"),this.updateModelBadge(),this.sendButton=new c.ButtonComponent(i).setCta().setButtonText("Send").onClick(()=>{this.sendPrompt()})}providerOptions(){return this.plugin.settings.providers.reduce((t,e)=>(t[e.id]=e.name||e.id,t),{})}renderMessages(){if(this.threadEl.empty(),this.messages.length===0){let t=this.threadEl.createDiv("ai-sidebar__empty");t.createDiv({text:"Ask a local agent to work with the notes in this vault."}),t.createDiv({text:"Use confirm mode when you want to review file edits first."});return}for(let t of this.messages){let e=this.threadEl.createDiv(`ai-sidebar__message ai-sidebar__message--${t.role}`);e.createDiv({cls:"ai-sidebar__message-role",text:t.role}),e.createEl("pre",{text:t.content})}if(this.isRunning){let t=this.threadEl.createDiv("ai-sidebar__message ai-sidebar__message--thinking");t.createDiv({cls:"ai-sidebar__message-role",text:"assistant"});let e=t.createDiv("ai-sidebar__typing");e.createSpan({text:"Thinking"});let n=e.createSpan("ai-sidebar__typing-dots");n.createSpan(),n.createSpan(),n.createSpan()}this.threadEl.scrollTo({top:this.threadEl.scrollHeight})}async refreshContextPreview(t=""){let e=await B(this.plugin.app,this.plugin.settings,t,this.extractSelectedSkillNames(t));this.activeContextPath=e.activeFile?.path??"",this.contextEl.empty(),this.contextEl.createDiv({cls:"ai-sidebar__context-title",text:`Context preview \xB7 ${this.accessModeLabel()}`});let n=[e.activeFile?`Active: ${e.activeFile.path}`:"No active note",e.selection?"Selection included":"No selection",`${e.linkedFiles.length} linked`,`${e.backlinkFiles.length} backlinks`,`${e.relevantFiles.length} relevant`,`${e.baseFiles.length} bases`,`${e.selectedSkills.length} skills`],i=this.contextEl.createEl("ul");for(let o of n)i.createEl("li",{text:o})}scheduleContextRefresh(){window.clearTimeout(this.refreshTimer),this.refreshTimer=window.setTimeout(()=>{this.refreshContextPreview(this.textareaEl?.value??"")},150)}accessModeLabel(){return this.accessMode==="read-only"?"read only":this.accessMode==="full-access"?"full access":"confirm actions"}async sendPrompt(){let t=this.textareaEl.value.trim();if(!t||this.isRunning)return;let e=this.plugin.getProvider(this.providerId);if(!e){new c.Notice("Choose a provider in AI Sidebar settings.");return}if(!Ee(e)){new c.Notice("Connect or configure this provider in AI Sidebar settings first.");return}let n=this.parsePromptControls(t,e);if(this.isSlashOnlyPrompt(t,n)){await this.applySlashControls(e,n),this.textareaEl.value="",this.updateModelBadge(),await this.refreshContextPreview();return}this.isRunning=!0,this.sendButton.setButtonText("Running..."),this.sendButton.setDisabled(!0),this.textareaEl.value="";let i=n.accessMode??this.accessMode;this.messages.push({role:"user",content:t}),this.renderMessages();try{let o=await B(this.plugin.app,this.plugin.settings,n.cleanPrompt,n.selectedSkillNames);await this.refreshContextPreview(t);let r={prompt:n.cleanPrompt,accessMode:i,context:o,instructions:fe(i),conversation:this.plugin.settings.enableConversationMemory?this.messages.slice(0,-1):[],options:n.options},l=await ye(e,r,this.app),a=await this.handleActions(l),d=a?`${l} -${o}`:l;this.messages.push({role:"assistant",content:d.trim()||"No response."}),await this.plugin.rememberMessages(this.messages)}catch(a){let r=a instanceof Error?a.message:String(a);this.messages.push({role:"system",content:`Provider failed: ${r}`}),await this.plugin.rememberMessages(this.messages),new c.Notice("AI provider failed. Check the sidebar for details.")}finally{this.isRunning=!1,this.sendButton.setButtonText("Send"),this.renderMessages()}}async clearConversation(){this.messages=[],await this.plugin.clearMemory(),this.renderMessages(),new c.Notice("AI Sidebar memory cleared.")}async handleActions(t){let e=ve(t);if(e.length===0)return"";if(this.accessMode==="read-only")return"Vault actions were proposed, but read-only mode blocked all writes.";if(this.accessMode==="confirm"&&!await Ae(this.app,e))return"Vault actions were not applied.";let n=[];for(let i of e)try{await xe(this.app,i),n.push(`Applied ${i.type}: ${i.path}`)}catch(a){let r=a instanceof Error?a.message:String(a);n.push(`Failed ${i.type}: ${i.path} \xB7 ${r}`)}return n.join(` -`)}async updateSlashCommands(){let t=this.currentSlashQuery();if(t===null){this.hideSlashCommands();return}let e=this.plugin.getProvider(this.providerId),n=e?ee(e,t,this.accessMode):[],i=await se(this.plugin.app,t);if(this.slashEl.empty(),n.length===0&&i.length===0){this.hideSlashCommands();return}this.slashEl.removeClass("is-hidden");for(let a of n.slice(0,10)){let r=this.slashEl.createDiv("ai-sidebar__slash-item");r.createDiv({cls:"ai-sidebar__slash-name",text:a.label}),r.createDiv({cls:"ai-sidebar__slash-path",text:a.description}),r.addEventListener("mousedown",l=>{l.preventDefault(),this.insertSlashToken(a.insert)})}for(let a of i.slice(0,8)){let r=this.slashEl.createDiv("ai-sidebar__slash-item");r.createDiv({cls:"ai-sidebar__slash-name",text:`/${a.name}`}),r.createDiv({cls:"ai-sidebar__slash-path",text:a.path}),r.addEventListener("mousedown",l=>{l.preventDefault(),this.insertSlashToken(`/${a.name}`)})}}hideSlashCommands(){this.slashEl.empty(),this.slashEl.addClass("is-hidden")}currentSlashQuery(){let t=this.textareaEl.selectionStart,n=this.textareaEl.value.slice(0,t).match(/(?:^|\s)\/([A-Za-z0-9_.-]*)$/);return n?n[1].toLowerCase():null}insertSlashToken(t){let e=this.textareaEl.selectionStart,n=this.textareaEl.value,i=n.slice(0,e),a=n.slice(e),r=i.replace(/(?:^|\s)\/([A-Za-z0-9_.-]*)$/,l=>`${l.startsWith(" ")?" ":""}${t} `);this.textareaEl.value=`${r}${a}`,this.textareaEl.focus(),this.textareaEl.selectionStart=r.length,this.textareaEl.selectionEnd=r.length,this.hideSlashCommands(),this.scheduleContextRefresh()}extractSelectedSkillNames(t){let e=new Set(["model","reasoning","access","memory"]);return Array.from(t.matchAll(/(?:^|\s)\/([A-Za-z0-9_.-]+)/g),n=>n[1]).filter(n=>!e.has(n.split(":")[0].toLowerCase()))}parsePromptControls(t,e){let n={model:e.model,reasoningEffort:e.reasoningEffort,memoryEnabled:this.plugin.settings.enableConversationMemory},i;return{cleanPrompt:t.replace(/(?:^|\s)\/(model|reasoning|access|memory):([A-Za-z0-9_.-]+)/gi,(r,l,o)=>{let d=String(l).toLowerCase(),h=String(o).toLowerCase();return d==="model"?n.model=o:d==="reasoning"&&be(h)?n.reasoningEffort=h:d==="access"&&Ce(h)?i=h:d==="memory"&&(n.memoryEnabled=h==="on"),r.startsWith(" ")?" ":""}).trim()||t,selectedSkillNames:this.extractSelectedSkillNames(t),options:n,accessMode:i}}},C=class extends c.PluginSettingTab{plugin;constructor(t,e){super(t,e),this.plugin=e}display(){let{containerEl:t}=this;t.empty(),t.createEl("h2",{text:"AI Sidebar"}),new c.Setting(t).setName("Default provider").setDesc("The AI provider used when the sidebar opens.").addDropdown(e=>{e.addOptions(this.providerOptions()),e.setValue(this.plugin.settings.defaultProviderId),e.onChange(async n=>{this.plugin.settings.defaultProviderId=n,await this.plugin.saveSettings()})}),new c.Setting(t).setName("Default vault access").setDesc("How much permission the agent has when it proposes changes.").addDropdown(e=>{e.addOption("read-only","Read only").addOption("confirm","Confirm actions").addOption("full-access","Full access").setValue(this.plugin.settings.defaultAccessMode).onChange(async n=>{this.plugin.settings.defaultAccessMode=n,await this.plugin.saveSettings()})}),new c.Setting(t).setName("Include folders").setDesc("Comma-separated folder prefixes to include. Leave blank for the whole vault.").addText(e=>{e.setPlaceholder("Projects, Daily Notes").setValue(this.plugin.settings.includeFolders).onChange(async n=>{this.plugin.settings.includeFolders=n,await this.plugin.saveSettings()})}),new c.Setting(t).setName("Exclude folders").setDesc("Comma-separated folder prefixes that should never be sent to an agent.").addText(e=>{e.setValue(this.plugin.settings.excludeFolders).onChange(async n=>{this.plugin.settings.excludeFolders=n,await this.plugin.saveSettings()})}),new c.Setting(t).setName("Max context characters").setDesc("Upper bound for note text sent to the agent.").addText(e=>{e.setValue(String(this.plugin.settings.maxContextChars)).onChange(async n=>{let i=Number(n);Number.isFinite(i)&&i>1e3&&(this.plugin.settings.maxContextChars=i,await this.plugin.saveSettings())})}),new c.Setting(t).setName("Conversation memory").setDesc("Remember previous sidebar messages after closing and reopening Obsidian.").addToggle(e=>{e.setValue(this.plugin.settings.enableConversationMemory).onChange(async n=>{this.plugin.settings.enableConversationMemory=n,await this.plugin.saveSettings()})}),new c.Setting(t).setName("Memory length").setDesc("Maximum number of recent sidebar messages to remember.").addText(e=>{e.setValue(String(this.plugin.settings.maxMemoryMessages)).onChange(async n=>{let i=Number(n);Number.isFinite(i)&&i>=2&&(this.plugin.settings.maxMemoryMessages=i,await this.plugin.saveSettings())})}).addButton(e=>{e.setButtonText("Clear memory").onClick(async()=>{await this.plugin.clearMemory(),new c.Notice("AI Sidebar memory cleared.")})}),t.createEl("h3",{text:"Connect a Provider"}),this.renderProviderPresets(t),t.createEl("h3",{text:"Connected Providers"});for(let e of this.plugin.settings.providers)this.renderProviderSetting(t,e);new c.Setting(t).setName("Advanced provider").setDesc("Create a custom API, OAuth, or local CLI provider.").addButton(e=>{e.setButtonText("Add custom").onClick(async()=>{let n=`provider-${Date.now()}`;this.plugin.settings.providers.push({id:n,name:"New provider",authType:"oauth",command:""}),await this.plugin.saveSettings(),this.display()})})}renderProviderPresets(t){let e=t.createDiv("ai-sidebar-provider-grid");for(let n of x){let i=this.plugin.getProvider(n.provider.id),a=e.createDiv("ai-sidebar-provider-card");a.createDiv({cls:"ai-sidebar-provider-card__name",text:n.name}),a.createDiv({cls:"ai-sidebar-provider-card__desc",text:n.description});let r=i?$(i):"Not added";a.createDiv({cls:"ai-sidebar-provider-card__status",text:r});let l=a.createDiv("ai-sidebar-provider-card__actions");new c.ButtonComponent(l).setButtonText(i?"Use":"Add").onClick(async()=>{await this.plugin.upsertProviderFromPreset(n),this.display()}),new c.ButtonComponent(l).setCta().setButtonText(R(i??n.provider)).onClick(async()=>{await this.plugin.upsertProviderFromPreset(n);let o=this.plugin.getProvider(n.provider.id)??n.provider;await _(o)&&(o.connectedAt=Date.now(),await this.plugin.saveSettings()),this.display()})}}providerOptions(){return this.plugin.settings.providers.reduce((t,e)=>(t[e.id]=e.name||e.id,t),{})}renderProviderSetting(t,e){let n=t.createDiv("ai-sidebar-settings-provider"),i=n.createDiv("ai-sidebar-settings-provider__header"),a=i.createDiv("ai-sidebar-settings-provider__title");a.createDiv({cls:"ai-sidebar-settings-provider__name",text:e.name||e.id}),a.createDiv({cls:"ai-sidebar-settings-provider__desc",text:this.providerDescription(e)}),i.createSpan({cls:`ai-sidebar-settings-provider__badge ${I(e)?"is-connected":""}`,text:$(e)});let r=n.createDiv("ai-sidebar-settings-provider__actions");new c.ButtonComponent(r).setButtonText(this.plugin.settings.defaultProviderId===e.id?"Default":"Use").setDisabled(this.plugin.settings.defaultProviderId===e.id).onClick(async()=>{this.plugin.settings.defaultProviderId=e.id,await this.plugin.saveSettings(),this.display()}),this.configureConnectButton(new c.ButtonComponent(r),e),new c.ButtonComponent(r).setIcon("trash").setTooltip("Remove provider").onClick(async()=>{this.plugin.settings.providers=this.plugin.settings.providers.filter(o=>o!==e),this.plugin.settings.defaultProviderId===e.id&&(this.plugin.settings.defaultProviderId=this.plugin.settings.providers.first()?.id??""),await this.plugin.saveSettings(),this.display()});let l=n.createDiv("ai-sidebar-settings-provider__fields");this.renderTextField(l,"Name",e.name,async o=>{e.name=o,await this.plugin.saveSettings(),this.display()}),e.authType==="cli"&&(this.renderTextField(l,"Command",e.command,async o=>{e.command=o,await this.plugin.saveSettings()}),e.signInCommand&&this.renderTextField(l,"Sign-in command",e.signInCommand,async o=>{e.signInCommand=o,e.connectedAt=void 0,await this.plugin.saveSettings()})),e.authType==="api-key"&&(this.renderTextField(l,"API key",e.apiKey??"",async o=>{e.apiKey=o,e.connectedAt=o?Date.now():void 0,await this.plugin.saveSettings(),this.display()},!0),this.renderTextField(l,"Endpoint",e.apiBaseUrl??"",async o=>{e.apiBaseUrl=o,await this.plugin.saveSettings()}),this.renderTextField(l,"Default model",e.model??"",async o=>{e.model=o,await this.plugin.saveSettings()}),this.renderTextField(l,"Slash models",e.models??"",async o=>{e.models=o,await this.plugin.saveSettings()})),e.authType==="oauth"&&(this.renderTextField(l,"Authorization URL",e.authorizationUrl??"",async o=>{e.authorizationUrl=o,await this.plugin.saveSettings()}),this.renderTextField(l,"Client ID",e.clientId??"",async o=>{e.clientId=o,await this.plugin.saveSettings()}),this.renderTextField(l,"Access token",e.accessToken??"",async o=>{e.accessToken=o,e.connectedAt=o?Date.now():void 0,await this.plugin.saveSettings(),this.display()},!0))}renderTextField(t,e,n,i,a=!1){let r=t.createDiv("ai-sidebar-settings-field");r.createDiv({cls:"ai-sidebar-settings-field__label",text:e});let l=r.createEl("input",{cls:"ai-sidebar-settings-field__input",attr:{type:a?"password":"text"}});l.value=n,l.addEventListener("change",()=>{i(l.value)})}providerDescription(t){return t.authType==="api-key"?t.apiKey?"Connected with API key.":"Add an API key to connect.":t.authType==="oauth"?t.accessToken?"Connected with OAuth.":"Connect with OAuth when the provider exposes a desktop OAuth app.":t.command?`Runs local command: ${t.command}`:"Add the local CLI command."}configureConnectButton(t,e){t.setButtonText(R(e)).onClick(async()=>{await _(e)&&(e.connectedAt=Date.now(),await this.plugin.saveSettings(),this.display())})}},k=class extends c.Modal{actions;resolve;constructor(t,e,n){super(t),this.actions=e,this.resolve=n}onOpen(){let{contentEl:t}=this;t.empty(),t.createEl("h2",{text:"Apply AI vault actions?"}),t.createEl("p",{text:"Review these proposed file changes before they are applied."});for(let e of this.actions){let n=t.createDiv("ai-sidebar-action-preview");n.createEl("strong",{text:`${e.type}: ${e.path}`}),e.type==="rename"&&n.createDiv({text:`New path: ${e.newPath}`}),"content"in e&&n.createEl("pre",{text:e.content.slice(0,1200)})}new c.Setting(t).addButton(e=>{e.setButtonText("Cancel").onClick(()=>{this.resolve(!1),this.close()})}).addButton(e=>{e.setCta().setButtonText("Apply").onClick(()=>{this.resolve(!0),this.close()})})}onClose(){this.contentEl.empty()}};async function F(s,t,e,n=[]){let i=s.workspace.getActiveViewOfType(c.MarkdownView),a=i?.file??s.workspace.getActiveFile(),r=i?.editor.getSelection()||void 0,l=new P(t.maxContextChars),o={selection:r,linkedFiles:[],backlinkFiles:[],relevantFiles:[],baseFiles:[],selectedSkills:[]},d=a?await f(s,a,"active note",l):void 0;d&&(o.activeFile=d);let h=a?Z(s,a):[];for(let g of h){let p=await f(s,g,"linked note",l);p&&o.linkedFiles.push(p)}let u=a?Y(s,a):[];for(let g of u){let p=await f(s,g,"backlink",l);p&&o.backlinkFiles.push(p)}let y=s.vault.getFiles().filter(g=>g.extension==="base"&&L(g,t)).slice(0,5);for(let g of y){let p=await f(s,g,"base file",l);p&&o.baseFiles.push(p)}let v=await X(s,t,e,a,l.remaining());for(let g of v){let p=await f(s,g,"relevant match",l);p&&o.relevantFiles.push(p)}return o.selectedSkills=await ne(s,n,l),o}async function f(s,t,e,n){if(!n.hasRoom())return;let i=await s.vault.cachedRead(t),a=n.take(i);return{path:t.path,content:a,reason:e}}function Z(s,t){let n=s.metadataCache.getFileCache(t)?.links??[],i=[];for(let a of n){let r=s.metadataCache.getFirstLinkpathDest(a.link,t.path);r&&i.push(r)}return j(i).slice(0,8)}function Y(s,t){let e=s.metadataCache.resolvedLinks,n=[];for(let[i,a]of Object.entries(e))if(a[t.path]){let r=s.vault.getAbstractFileByPath(i);r instanceof c.TFile&&n.push(r)}return j(n).slice(0,8)}async function X(s,t,e,n,i){if(!e.trim()||i<=0)return[];let a=e.toLowerCase().split(/[^a-z0-9/_-]+/i).filter(o=>o.length>2).slice(0,16);if(a.length===0)return[];let r=[],l=s.vault.getFiles().filter(o=>o!==n&&L(o,t)&&O(o));for(let o of l){let d=(await s.vault.cachedRead(o)).toLowerCase(),h=o.path.toLowerCase(),u=0;for(let y of a){h.includes(y)&&(u+=4);let v=d.match(new RegExp(ke(y),"g"));u+=v?Math.min(v.length,8):0}u>0&&r.push({file:o,score:u})}return r.sort((o,d)=>d.score-o.score).slice(0,10).map(o=>o.file)}function L(s,t){let e=M(t.includeFolders),n=M(t.excludeFolders);return e.length>0&&!e.some(i=>s.path.startsWith(i))||n.some(i=>s.path.startsWith(i))?!1:O(s)}function O(s){return["md","canvas","base","json","txt"].includes(s.extension)}function ee(s,t,e){let n=[],i=te(s);for(let r of i)n.push({label:`/model:${r}`,description:`Use ${r} for this request`,insert:`/model:${r}`});if(s.authType==="api-key"||s.id.includes("codex")||s.id.includes("openai"))for(let r of["low","medium","high"])n.push({label:`/reasoning:${r}`,description:`Set reasoning effort to ${r}`,insert:`/reasoning:${r}`});for(let r of["read-only","confirm","full-access"])n.push({label:`/access:${r}`,description:r===e?"Current vault access mode":`Use ${r} for this request`,insert:`/access:${r}`});n.push({label:"/memory:on",description:"Include remembered chat history for this request",insert:"/memory:on"},{label:"/memory:off",description:"Do not include remembered chat history for this request",insert:"/memory:off"});let a=t.toLowerCase();return n.filter(r=>r.label.slice(1).toLowerCase().includes(a))}function te(s){let t=s.models?.split(",").map(n=>n.trim()).filter(Boolean)??[],e=t.length>0?t:[s.model].filter(n=>!!n);return Array.from(new Set(e)).slice(0,12)}async function se(s,t=""){let e=t.toLowerCase(),n=s.vault.getFiles().filter(a=>U(a)).map(a=>({name:V(a.path),path:a.path})).filter(a=>!e||a.name.toLowerCase().includes(e)).sort((a,r)=>a.name.localeCompare(r.name)),i=new Set;return n.filter(a=>{let r=a.name.toLowerCase();return i.has(r)?!1:(i.add(r),!0)})}async function ne(s,t,e){let n=new Set(t.map(a=>a.toLowerCase()));if(n.size===0)return[];let i=[];for(let a of s.vault.getFiles().filter(r=>U(r))){let r=V(a.path);if(!n.has(r.toLowerCase())||!e.hasRoom())continue;let l=await s.vault.cachedRead(a);i.push({name:r,path:a.path,content:e.take(l)})}return i}function U(s){return["md","txt","json","yaml","yml"].includes(s.extension)?s.path.startsWith(".agent/skills/")||s.path.startsWith(".agents/skills/"):!1}function V(s){let t=s.split("/"),e=t.last()??s;return e.toLowerCase()==="skill.md"&&t.length>1?t[t.length-2]:e.replace(/\.[^.]+$/,"")}function M(s){return s.split(",").map(t=>t.trim().replace(/^\/+/,"")).filter(Boolean)}var P=class{remainingChars;constructor(t){this.remainingChars=t}hasRoom(){return this.remainingChars>0}remaining(){return this.remainingChars}take(t){let e=t.slice(0,this.remainingChars);return this.remainingChars-=e.length,e}};function ie(s){return["You are running inside an Obsidian AI Sidebar plugin.","Use the provided vault context to answer the user.","If the prompt contains slash commands such as /writer or /reviewer, apply the matching selectedSkills instructions from the request context.","The request may include conversation memory and per-request options such as model or reasoning effort.","When you need file changes, include a JSON code block with this shape:",'{"actions":[{"type":"edit","path":"Note.md","content":"new content"}]}',"Allowed action types: create, edit, append, delete, rename.",`Current access mode: ${s}. The plugin enforces this mode before applying actions.`].join(` -`)}async function ae(s,t,e){if(s.authType==="api-key")return re(s,t);if(s.authType==="oauth"){if(!s.accessToken)throw new Error("This OAuth provider is not connected yet.");return oe(s,t)}return ce(s,t,e)}async function re(s,t){if(!s.apiKey)throw new Error("Missing API key.");let e=s.apiBaseUrl||"https://api.openai.com/v1/responses",n=t.options.model||s.model||"gpt-4.1-mini",i=JSON.stringify({prompt:t.prompt,accessMode:t.accessMode,context:t.context,options:t.options}),a=[{role:"system",content:t.instructions},...t.conversation.map(d=>({role:d.role==="assistant"?"assistant":"user",content:d.content})),{role:"user",content:i}],l=e.includes("/chat/completions")?{model:n,messages:a}:{model:n,input:a};t.options.reasoningEffort&&n.startsWith("o")&&(l.reasoning={effort:t.options.reasoningEffort});let o=await(0,c.requestUrl)({url:e,method:"POST",headers:{Authorization:`Bearer ${s.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(l),throw:!1});if(o.status<200||o.status>=300)throw new Error(o.text||`HTTP ${o.status}`);return ge(o.json)}async function oe(s,t){if(!s.apiBaseUrl)throw new Error("OAuth provider needs an API endpoint after login.");let e=await(0,c.requestUrl)({url:s.apiBaseUrl,method:"POST",headers:{Authorization:`Bearer ${s.accessToken}`,"Content-Type":"application/json"},body:JSON.stringify(t),throw:!1});if(e.status<200||e.status>=300)throw new Error(e.text||`HTTP ${e.status}`);return typeof e.json?.text=="string"?e.json.text:typeof e.json?.output_text=="string"?e.json.output_text:e.text}function ce(s,t,e){return new Promise((n,i)=>{let a=he(t),r=le(s,t,e),l=`${r} ${z(a)}`,o=(0,E.spawn)("/bin/zsh",["-lc",l],{stdio:["ignore","pipe","pipe"]}),d="",h="";o.stdout.on("data",u=>{d+=u.toString()}),o.stderr.on("data",u=>{h+=u.toString()}),o.on("error",i),o.on("close",u=>{u===0?n(d.trim()):i(new Error(h.trim()||`Command exited with code ${u}. Check that ${r} is installed and available in your shell.`))})})}function le(s,t,e){let n=ue(e);if(s.id==="codex"||s.command.startsWith("codex exec")){let i=de(t.accessMode),a=n?` -C ${z(n)}`:"";return`${s.command}${a} --skip-git-repo-check -s ${i}`}return s.command}function de(s){return s==="full-access"?"workspace-write":"read-only"}function ue(s){let t=s.vault.adapter;if("basePath"in t&&typeof t.basePath=="string")return t.basePath}function he(s){return[s.instructions,"","User request:",s.prompt,"","Vault context and options:",JSON.stringify({accessMode:s.accessMode,options:s.options,conversation:s.options.memoryEnabled?s.conversation:[],context:s.context},null,2)].join(` -`)}function pe(s,t){let e=N(s),n=['tell application "Terminal"',"activate",`do script "printf '\\\\033]0;${D(t)}\\\\007'; clear; echo 'AI Sidebar sign-in'; echo ''; echo 'Running: ${D(s)}'; echo ''; /bin/zsh -lc '${e}; echo; echo Sign-in command finished. You can close this window when done.'"`,"end tell"].join(` -`);return new Promise((i,a)=>{let r=(0,E.spawn)("osascript",["-e",n],{stdio:["ignore","ignore","pipe"]}),l="";r.stderr.on("data",o=>{l+=o.toString()}),r.on("error",a),r.on("close",o=>{o===0?i():a(new Error(l.trim()||`Could not open Terminal. Exit code ${o}`))})})}function ge(s){if(!s||typeof s!="object")return"";let t=s;if(typeof t.output_text=="string")return t.output_text;if(typeof t.choices?.[0]?.message?.content=="string")return t.choices[0].message.content;let e=[];for(let n of t.output??[])for(let i of n.content??[])typeof i.text=="string"&&e.push(i.text);return e.join(` -`)}async function me(s){if(!s.authorizationUrl||!s.clientId)return new c.Notice("Add an authorization URL and client ID first."),!1;let t=new URL(s.authorizationUrl);return t.searchParams.set("response_type","token"),t.searchParams.set("client_id",s.clientId),t.searchParams.set("redirect_uri","urn:ietf:wg:oauth:2.0:oob"),s.scope&&t.searchParams.set("scope",s.scope),t.searchParams.set("state",crypto.randomUUID()),window.open(t.toString()),new c.Notice("OAuth sign-in opened in your browser. Paste the returned token into this provider when available."),!0}async function _(s){if(s.authType==="cli"){if(!s.signInCommand)return new c.Notice("This local provider does not have a sign-in command configured."),!1;try{return await pe(s.signInCommand,`${s.name} Sign In`),new c.Notice(`Opened Terminal for ${s.name} sign-in.`),!0}catch(t){let e=t instanceof Error?t.message:String(t);return new c.Notice(`Could not open sign-in: ${e}`),!1}}return s.authType==="api-key"?(s.setupUrl&&window.open(s.setupUrl),new c.Notice(s.apiKey?`${s.name} is connected.`:`Add your ${s.name} API key below to connect.`),!!s.apiKey):me(s)}function N(s){return s.replace(/'/g,"'\\''")}function z(s){return`'${N(s)}'`}function D(s){return s.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}function R(s){return s.authType==="cli"?I(s)?"Reconnect":"Sign in":s.authType==="api-key"?s.apiKey?"Connected":"Get key":s.accessToken?"Reconnect":"Sign in"}function $(s){return I(s)?"Connected":s.authType==="api-key"?"Needs API key":s.authType==="oauth"?"Needs sign-in":s.signInCommand?"Ready to sign in":"Needs command"}function I(s){return s.authType==="api-key"?!!s.apiKey:s.authType==="oauth"?!!s.accessToken:!!s.connectedAt}function fe(s){return s.authType==="api-key"?!!(s.apiKey&&s.apiBaseUrl):s.authType==="oauth"?!!(s.accessToken&&s.apiBaseUrl):!!s.command.trim()}function ye(s){return s.map(t=>{let e=t,n=e.id==="codex"&&e.command==="codex"?"codex exec":e.command??"";return{...e,authType:e.authType??(e.command?"cli":"api-key"),command:n,connectedAt:e.connectedAt}})}function S(s){return{...s}}function ve(s){let t=[],e=/```(?:json)?\s*([\s\S]*?)```/gi,n=[],i;for(;(i=e.exec(s))!==null;)n.push(i[1]);n.push(s);for(let a of n)try{let r=JSON.parse(a.trim());Array.isArray(r.actions)&&t.push(...r.actions.filter(we))}catch{continue}return t}function we(s){if(!s||typeof s!="object")return!1;let t=s;return typeof t.type!="string"||typeof t.path!="string"?!1:["create","edit","append"].includes(t.type)?typeof t.content=="string":t.type==="delete"?!0:t.type==="rename"?typeof t.newPath=="string":!1}function Ae(s,t){return new Promise(e=>{new k(s,t,e).open()})}async function xe(s,t){let e=T(t.path),n=s.vault.getAbstractFileByPath(e);if(t.type==="create"){if(n)throw new Error("File already exists.");await B(s,e),await s.vault.create(e,t.content);return}if(!(n instanceof c.TFile))throw new Error("File does not exist.");if(t.type==="edit")await s.vault.modify(n,t.content);else if(t.type==="append"){let i=await s.vault.read(n);await s.vault.modify(n,`${i}${i.endsWith(` +${a}`:l;this.messages.push({role:"assistant",content:d.trim()||"No response."}),await this.plugin.rememberMessages(this.messages)}catch(o){let r=o instanceof Error?o.message:String(o);this.messages.push({role:"system",content:`Provider failed: ${r}`}),await this.plugin.rememberMessages(this.messages),new c.Notice("AI provider failed. Check the sidebar for details.")}finally{this.isRunning=!1,this.sendButton.setButtonText("Send"),this.sendButton.setDisabled(!1),this.updateModelBadge(),this.renderMessages()}}async clearConversation(){this.messages=[],await this.plugin.clearMemory(),this.renderMessages(),new c.Notice("AI Sidebar memory cleared.")}async handleActions(t){let e=Fe(t);if(e.length===0)return"";if(this.accessMode==="read-only")return"Vault actions were proposed, but read-only mode blocked all writes.";if(this.accessMode==="confirm"&&!await _e(this.app,e))return"Vault actions were not applied.";let n=[];for(let i of e)try{await De(this.app,i),n.push(`Applied ${i.type}: ${i.path}`)}catch(o){let r=o instanceof Error?o.message:String(o);n.push(`Failed ${i.type}: ${i.path} \xB7 ${r}`)}return n.join(` +`)}async updateSlashCommands(){let t=this.currentSlashQuery();if(t===null){this.hideSlashCommands();return}let e=this.plugin.getProvider(this.providerId),n=e?he(e,t,this.accessMode):[],i=await pe(this.plugin.app,t);if(this.slashEl.empty(),n.length===0&&i.length===0){this.hideSlashCommands();return}this.slashEl.removeClass("is-hidden");for(let o of n.slice(0,6)){let r=this.slashEl.createDiv("ai-sidebar__slash-item");r.createDiv({cls:"ai-sidebar__slash-name",text:o.label}),r.createDiv({cls:"ai-sidebar__slash-path",text:o.description}),r.addEventListener("mousedown",l=>{l.preventDefault(),this.insertSlashToken(o.insert)})}for(let o of i.slice(0,10)){let r=this.slashEl.createDiv("ai-sidebar__slash-item");r.createDiv({cls:"ai-sidebar__slash-name",text:`/${o.name}`}),r.createDiv({cls:"ai-sidebar__slash-path",text:o.path}),r.addEventListener("mousedown",l=>{l.preventDefault(),this.insertSlashToken(`/${o.name}`)})}}hideSlashCommands(){this.slashEl.empty(),this.slashEl.addClass("is-hidden")}currentSlashQuery(){let t=this.textareaEl.selectionStart,n=this.textareaEl.value.slice(0,t).match(/(?:^|\s)\/([A-Za-z0-9_.:-]*)$/);return n?n[1].toLowerCase():null}insertSlashToken(t){let e=this.textareaEl.selectionStart,n=this.textareaEl.value,i=n.slice(0,e),o=n.slice(e),r=i.replace(/(?:^|\s)\/([A-Za-z0-9_.:-]*)$/,l=>`${l.startsWith(" ")?" ":""}${t} `);this.textareaEl.value=`${r}${o}`,this.textareaEl.focus(),this.textareaEl.selectionStart=r.length,this.textareaEl.selectionEnd=r.length,this.hideSlashCommands(),this.scheduleContextRefresh()}extractSelectedSkillNames(t){let e=new Set(["model","reasoning","access","memory"]);return Array.from(t.matchAll(/(?:^|\s)\/([A-Za-z0-9_.-]+)/g),n=>n[1]).filter(n=>!e.has(n.split(":")[0].toLowerCase()))}parsePromptControls(t,e){let n={model:e.model,reasoningEffort:e.reasoningEffort,memoryEnabled:this.plugin.settings.enableConversationMemory},i;return{cleanPrompt:t.replace(/(?:^|\s)\/(model|reasoning|access|memory):([A-Za-z0-9_.-]+)/gi,(r,l,a)=>{let d=String(l).toLowerCase(),h=String(a).toLowerCase();return d==="model"?n.model=a:d==="reasoning"&&$e(h)?n.reasoningEffort=h:d==="access"&&Re(h)?i=h:d==="memory"&&(n.memoryEnabled=h==="on"),r.startsWith(" ")?" ":""}).trim(),selectedSkillNames:this.extractSelectedSkillNames(t),options:n,accessMode:i}}isSlashOnlyPrompt(t,e){return e.cleanPrompt.length===0&&this.extractSelectedSkillNames(t).length===0}async applySlashControls(t,e){let n=[];e.options.model&&e.options.model!==t.model&&(t.model=e.options.model,n.push(`model ${e.options.model}`)),e.options.reasoningEffort&&e.options.reasoningEffort!==t.reasoningEffort&&(t.reasoningEffort=e.options.reasoningEffort,n.push(`reasoning ${e.options.reasoningEffort}`)),e.accessMode&&e.accessMode!==this.accessMode&&(this.accessMode=e.accessMode,this.plugin.settings.defaultAccessMode=e.accessMode,n.push(`access ${this.accessModeLabel()}`)),e.options.memoryEnabled!==this.plugin.settings.enableConversationMemory&&(this.plugin.settings.enableConversationMemory=e.options.memoryEnabled,n.push(`memory ${e.options.memoryEnabled?"on":"off"}`)),await this.plugin.saveSettings(),new c.Notice(n.length>0?`Updated ${n.join(", ")}.`:"Slash command applied.")}updateModelBadge(){if(!this.modelBadgeEl)return;let t=this.plugin.getProvider(this.providerId),e=this.textareaEl?.value.match(/(?:^|\s)\/model:([A-Za-z0-9_.:/-]+)/i)?.[1],n=e||t?.model||H(t??j.providers[0]).first()||"default";this.modelBadgeEl.setText(n),this.modelBadgeEl.toggleClass("is-override",!!e),this.modelBadgeEl.setAttr("aria-label",e?`Model override: ${n}`:`Model: ${n}`)}},S=class extends c.PluginSettingTab{plugin;constructor(t,e){super(t,e),this.plugin=e}display(){let{containerEl:t}=this;t.empty(),t.createEl("h2",{text:"AI Sidebar"}),new c.Setting(t).setName("Default provider").setDesc("The AI provider used when the sidebar opens.").addDropdown(e=>{e.addOptions(this.providerOptions()),e.setValue(this.plugin.settings.defaultProviderId),e.onChange(async n=>{this.plugin.settings.defaultProviderId=n,await this.plugin.saveSettings()})}),new c.Setting(t).setName("Default vault access").setDesc("How much permission the agent has when it proposes changes.").addDropdown(e=>{e.addOption("read-only","Read only").addOption("confirm","Confirm actions").addOption("full-access","Full access").setValue(this.plugin.settings.defaultAccessMode).onChange(async n=>{this.plugin.settings.defaultAccessMode=n,await this.plugin.saveSettings()})}),new c.Setting(t).setName("Include folders").setDesc("Comma-separated folder prefixes to include. Leave blank for the whole vault.").addText(e=>{e.setPlaceholder("Projects, Daily Notes").setValue(this.plugin.settings.includeFolders).onChange(async n=>{this.plugin.settings.includeFolders=n,await this.plugin.saveSettings()})}),new c.Setting(t).setName("Exclude folders").setDesc("Comma-separated folder prefixes that should never be sent to an agent.").addText(e=>{e.setValue(this.plugin.settings.excludeFolders).onChange(async n=>{this.plugin.settings.excludeFolders=n,await this.plugin.saveSettings()})}),new c.Setting(t).setName("Max context characters").setDesc("Upper bound for note text sent to the agent.").addText(e=>{e.setValue(String(this.plugin.settings.maxContextChars)).onChange(async n=>{let i=Number(n);Number.isFinite(i)&&i>1e3&&(this.plugin.settings.maxContextChars=i,await this.plugin.saveSettings())})}),new c.Setting(t).setName("Conversation memory").setDesc("Remember previous sidebar messages after closing and reopening Obsidian.").addToggle(e=>{e.setValue(this.plugin.settings.enableConversationMemory).onChange(async n=>{this.plugin.settings.enableConversationMemory=n,await this.plugin.saveSettings()})}),new c.Setting(t).setName("Memory length").setDesc("Maximum number of recent sidebar messages to remember.").addText(e=>{e.setValue(String(this.plugin.settings.maxMemoryMessages)).onChange(async n=>{let i=Number(n);Number.isFinite(i)&&i>=2&&(this.plugin.settings.maxMemoryMessages=i,await this.plugin.saveSettings())})}).addButton(e=>{e.setButtonText("Clear memory").onClick(async()=>{await this.plugin.clearMemory(),new c.Notice("AI Sidebar memory cleared.")})}),t.createEl("h3",{text:"Connect a Provider"}),this.renderProviderPresets(t),t.createEl("h3",{text:"Connected Providers"});for(let e of this.plugin.settings.providers)this.renderProviderSetting(t,e);new c.Setting(t).setName("Advanced provider").setDesc("Create a custom API, OAuth, or local CLI provider.").addButton(e=>{e.setButtonText("Add custom").onClick(async()=>{let n=`provider-${Date.now()}`;this.plugin.settings.providers.push({id:n,name:"New provider",authType:"oauth",command:""}),await this.plugin.saveSettings(),this.display()})})}renderProviderPresets(t){let e=t.createDiv("ai-sidebar-provider-grid");for(let n of k){let i=this.plugin.getProvider(n.provider.id),o=e.createDiv("ai-sidebar-provider-card");o.createDiv({cls:"ai-sidebar-provider-card__name",text:n.name}),o.createDiv({cls:"ai-sidebar-provider-card__desc",text:n.description});let r=i?N(i):"Not added";o.createDiv({cls:"ai-sidebar-provider-card__status",text:r});let l=o.createDiv("ai-sidebar-provider-card__actions");new c.ButtonComponent(l).setButtonText(i?"Use":"Add").onClick(async()=>{await this.plugin.upsertProviderFromPreset(n),this.display()}),new c.ButtonComponent(l).setCta().setButtonText(V(i??n.provider)).onClick(async()=>{await this.plugin.upsertProviderFromPreset(n);let a=this.plugin.getProvider(n.provider.id)??n.provider;await O(a)&&(a.connectedAt=Date.now(),await this.plugin.saveSettings()),this.display()})}}providerOptions(){return this.plugin.settings.providers.reduce((t,e)=>(t[e.id]=e.name||e.id,t),{})}renderProviderSetting(t,e){let n=t.createDiv("ai-sidebar-settings-provider"),i=n.createDiv("ai-sidebar-settings-provider__header"),o=i.createDiv("ai-sidebar-settings-provider__title");o.createDiv({cls:"ai-sidebar-settings-provider__name",text:e.name||e.id}),o.createDiv({cls:"ai-sidebar-settings-provider__desc",text:this.providerDescription(e)}),i.createSpan({cls:`ai-sidebar-settings-provider__badge ${$(e)?"is-connected":""}`,text:N(e)});let r=n.createDiv("ai-sidebar-settings-provider__actions");new c.ButtonComponent(r).setButtonText(this.plugin.settings.defaultProviderId===e.id?"Default":"Use").setDisabled(this.plugin.settings.defaultProviderId===e.id).onClick(async()=>{this.plugin.settings.defaultProviderId=e.id,await this.plugin.saveSettings(),this.display()}),this.configureConnectButton(new c.ButtonComponent(r),e),new c.ButtonComponent(r).setIcon("trash").setTooltip("Remove provider").onClick(async()=>{this.plugin.settings.providers=this.plugin.settings.providers.filter(a=>a!==e),this.plugin.settings.defaultProviderId===e.id&&(this.plugin.settings.defaultProviderId=this.plugin.settings.providers.first()?.id??""),await this.plugin.saveSettings(),this.display()});let l=n.createDiv("ai-sidebar-settings-provider__fields");this.renderTextField(l,"Name",e.name,async a=>{e.name=a,await this.plugin.saveSettings(),this.display()}),e.authType==="cli"&&(this.renderTextField(l,"Command",e.command,async a=>{e.command=a,await this.plugin.saveSettings()}),e.signInCommand&&this.renderTextField(l,"Sign-in command",e.signInCommand,async a=>{e.signInCommand=a,e.connectedAt=void 0,await this.plugin.saveSettings()})),e.authType==="api-key"&&(this.renderTextField(l,"API key",e.apiKey??"",async a=>{e.apiKey=a,e.connectedAt=a?Date.now():void 0,await this.plugin.saveSettings(),this.display()},!0),this.renderTextField(l,"Endpoint",e.apiBaseUrl??"",async a=>{e.apiBaseUrl=a,await this.plugin.saveSettings()}),this.renderTextField(l,"Default model",e.model??"",async a=>{e.model=a,await this.plugin.saveSettings()}),this.renderTextField(l,"Slash models",e.models??"",async a=>{e.models=a,await this.plugin.saveSettings()})),e.authType==="oauth"&&(this.renderTextField(l,"Authorization URL",e.authorizationUrl??"",async a=>{e.authorizationUrl=a,await this.plugin.saveSettings()}),this.renderTextField(l,"Client ID",e.clientId??"",async a=>{e.clientId=a,await this.plugin.saveSettings()}),this.renderTextField(l,"Access token",e.accessToken??"",async a=>{e.accessToken=a,e.connectedAt=a?Date.now():void 0,await this.plugin.saveSettings(),this.display()},!0))}renderTextField(t,e,n,i,o=!1){let r=t.createDiv("ai-sidebar-settings-field");r.createDiv({cls:"ai-sidebar-settings-field__label",text:e});let l=r.createEl("input",{cls:"ai-sidebar-settings-field__input",attr:{type:o?"password":"text"}});l.value=n,l.addEventListener("change",()=>{i(l.value)})}providerDescription(t){return t.authType==="api-key"?t.apiKey?"Connected with API key.":"Add an API key to connect.":t.authType==="oauth"?t.accessToken?"Connected with OAuth.":"Connect with OAuth when the provider exposes a desktop OAuth app.":t.command?`Runs local command: ${t.command}`:"Add the local CLI command."}configureConnectButton(t,e){t.setButtonText(V(e)).onClick(async()=>{await O(e)&&(e.connectedAt=Date.now(),await this.plugin.saveSettings(),this.display())})}},T=class extends c.Modal{actions;resolve;constructor(t,e,n){super(t),this.actions=e,this.resolve=n}onOpen(){let{contentEl:t}=this;t.empty(),t.createEl("h2",{text:"Apply AI vault actions?"}),t.createEl("p",{text:"Review these proposed file changes before they are applied."});for(let e of this.actions){let n=t.createDiv("ai-sidebar-action-preview");n.createEl("strong",{text:`${e.type}: ${e.path}`}),e.type==="rename"&&n.createDiv({text:`New path: ${e.newPath}`}),"content"in e&&n.createEl("pre",{text:e.content.slice(0,1200)})}new c.Setting(t).addButton(e=>{e.setButtonText("Cancel").onClick(()=>{this.resolve(!1),this.close()})}).addButton(e=>{e.setCta().setButtonText("Apply").onClick(()=>{this.resolve(!0),this.close()})})}onClose(){this.contentEl.empty()}};async function B(s,t,e,n=[]){let i=s.workspace.getActiveViewOfType(c.MarkdownView),o=i?.file??s.workspace.getActiveFile(),r=i?.editor.getSelection()||void 0,l=new E(t.maxContextChars),a={selection:r,linkedFiles:[],backlinkFiles:[],relevantFiles:[],baseFiles:[],selectedSkills:[]},d=o?await f(s,o,"active note",l):void 0;d&&(a.activeFile=d);let h=o?ce(s,o):[];for(let g of h){let p=await f(s,g,"linked note",l);p&&a.linkedFiles.push(p)}let u=o?de(s,o):[];for(let g of u){let p=await f(s,g,"backlink",l);p&&a.backlinkFiles.push(p)}let y=s.vault.getFiles().filter(g=>g.extension==="base"&&K(g,t)).slice(0,5);for(let g of y){let p=await f(s,g,"base file",l);p&&a.baseFiles.push(p)}let v=await ue(s,t,e,o,l.remaining());for(let g of v){let p=await f(s,g,"relevant match",l);p&&a.relevantFiles.push(p)}return a.selectedSkills=await ge(s,n,l),a}async function f(s,t,e,n){if(!n.hasRoom())return;let i=await s.vault.cachedRead(t),o=n.take(i);return{path:t.path,content:o,reason:e}}function ce(s,t){let n=s.metadataCache.getFileCache(t)?.links??[],i=[];for(let o of n){let r=s.metadataCache.getFirstLinkpathDest(o.link,t.path);r&&i.push(r)}return ee(i).slice(0,8)}function de(s,t){let e=s.metadataCache.resolvedLinks,n=[];for(let[i,o]of Object.entries(e))if(o[t.path]){let r=s.vault.getAbstractFileByPath(i);r instanceof c.TFile&&n.push(r)}return ee(n).slice(0,8)}async function ue(s,t,e,n,i){if(!e.trim()||i<=0)return[];let o=e.toLowerCase().split(/[^a-z0-9/_-]+/i).filter(a=>a.length>2).slice(0,16);if(o.length===0)return[];let r=[],l=s.vault.getFiles().filter(a=>a!==n&&K(a,t)&&W(a));for(let a of l){let d=(await s.vault.cachedRead(a)).toLowerCase(),h=a.path.toLowerCase(),u=0;for(let y of o){h.includes(y)&&(u+=4);let v=d.match(new RegExp(Be(y),"g"));u+=v?Math.min(v.length,8):0}u>0&&r.push({file:a,score:u})}return r.sort((a,d)=>d.score-a.score).slice(0,10).map(a=>a.file)}function K(s,t){let e=L(t.includeFolders),n=L(t.excludeFolders);return e.length>0&&!e.some(i=>s.path.startsWith(i))||n.some(i=>s.path.startsWith(i))?!1:W(s)}function W(s){return["md","canvas","base","json","txt"].includes(s.extension)}function he(s,t,e){let n=[],i=H(s);for(let r of i)n.push({label:`/model:${r}`,description:`Use ${r} for this request`,insert:`/model:${r}`});if(s.authType==="api-key"||s.id.includes("codex")||s.id.includes("openai"))for(let r of["low","medium","high"])n.push({label:`/reasoning:${r}`,description:`Set reasoning effort to ${r}`,insert:`/reasoning:${r}`});for(let r of["read-only","confirm","full-access"])n.push({label:`/access:${r}`,description:r===e?"Current vault access mode":`Use ${r} for this request`,insert:`/access:${r}`});n.push({label:"/memory:on",description:"Include remembered chat history for this request",insert:"/memory:on"},{label:"/memory:off",description:"Do not include remembered chat history for this request",insert:"/memory:off"});let o=t.toLowerCase();return n.filter(r=>r.label.slice(1).toLowerCase().includes(o))}function H(s){let t=s.models?.split(",").map(n=>n.trim()).filter(Boolean)??[],e=t.length>0?t:[s.model].filter(n=>!!n);return Array.from(new Set(e)).slice(0,12)}async function pe(s,t=""){let e=t.toLowerCase(),n=s.vault.getFiles().filter(a=>Z(a)).map(a=>({name:x(a.path),path:a.path,source:"vault"})),i=await q(s),o=await G(),r=[...n,...i,...o].filter(a=>!e||a.name.toLowerCase().includes(e)).sort((a,d)=>a.name.localeCompare(d.name)),l=new Set;return r.filter(a=>{let d=`${a.source}:${a.name.toLowerCase()}`;return l.has(d)?!1:(l.add(d),!0)})}async function q(s){let t=[".agent/skills",".agents/skills","agent/skills","agents/skills"],e=[];for(let n of t)e.push(...await Q(s,n));return e.filter(n=>D(n)).map(n=>({name:x(n),path:n,source:"vault"}))}async function G(){let s=[A.join((0,C.homedir)(),".agent","skills"),A.join((0,C.homedir)(),".agents","skills")],t=[];for(let e of s)t.push(...await J(e));return t.filter(e=>D(e)).map(e=>({name:x(e),path:e,source:"global"}))}async function J(s){try{let t=await _.promises.readdir(s,{withFileTypes:!0});return(await Promise.all(t.map(async n=>{let i=A.join(s,n.name);return n.isDirectory()?J(i):n.isFile()?[i]:[]}))).flat()}catch{return[]}}async function Q(s,t){try{let e=await s.vault.adapter.list(t),n=e.files,i=await Promise.all(e.folders.map(o=>Q(s,o)));return[...n,...i.flat()]}catch{return[]}}async function ge(s,t,e){let n=new Set(t.map(l=>l.toLowerCase()));if(n.size===0)return[];let i=[],o=[...await q(s),...await G()],r=s.vault.getFiles().filter(l=>Z(l)).map(l=>({name:x(l.path),path:l.path,source:"vault"}));for(let l of[...r,...o]){let a=l.name;if(!n.has(a.toLowerCase())||!e.hasRoom())continue;let d=await me(s,l);i.push({name:a,path:l.path,content:e.take(d)})}return i}async function me(s,t){if(t.source==="global")return _.promises.readFile(t.path,"utf8");let e=s.vault.getAbstractFileByPath(t.path);return e instanceof c.TFile?s.vault.cachedRead(e):s.vault.adapter.read(t.path)}function Z(s){return D(s.path)}function D(s){let t=s.split(".").last()?.toLowerCase()??"";if(!["md","txt","json","yaml","yml","prompt"].includes(t))return!1;let e=s.replace(/^\/+/,"");return e.startsWith(".agent/skills/")||e.startsWith(".agents/skills/")||e.startsWith("agent/skills/")||e.startsWith("agents/skills/")}function x(s){let t=s.split("/"),e=t.last()??s;return e.toLowerCase()==="skill.md"&&t.length>1?t[t.length-2]:e.replace(/\.[^.]+$/,"")}function L(s){return s.split(",").map(t=>t.trim().replace(/^\/+/,"")).filter(Boolean)}var E=class{remainingChars;constructor(t){this.remainingChars=t}hasRoom(){return this.remainingChars>0}remaining(){return this.remainingChars}take(t){let e=t.slice(0,this.remainingChars);return this.remainingChars-=e.length,e}};function fe(s){return["You are running inside an Obsidian AI Sidebar plugin.","Use the provided vault context to answer the user.","If the prompt contains slash commands such as /writer or /reviewer, apply the matching selectedSkills instructions from the request context.","The request may include conversation memory and per-request options such as model or reasoning effort.","When you need file changes, include a JSON code block with this shape:",'{"actions":[{"type":"edit","path":"Note.md","content":"new content"}]}',"Allowed action types: create, edit, append, delete, rename.",`Current access mode: ${s}. The plugin enforces this mode before applying actions.`].join(` +`)}async function ye(s,t,e){if(s.authType==="api-key")return ve(s,t);if(s.authType==="oauth"){if(!s.accessToken)throw new Error("This OAuth provider is not connected yet.");return we(s,t)}return Ae(s,t,e)}async function ve(s,t){if(!s.apiKey)throw new Error("Missing API key.");let e=s.apiBaseUrl||"https://api.openai.com/v1/responses",n=t.options.model||s.model||"gpt-4.1-mini",i=JSON.stringify({prompt:t.prompt,accessMode:t.accessMode,context:t.context,options:t.options}),o=[{role:"system",content:t.instructions},...t.conversation.map(d=>({role:d.role==="assistant"?"assistant":"user",content:d.content})),{role:"user",content:i}],l=e.includes("/chat/completions")?{model:n,messages:o}:{model:n,input:o};t.options.reasoningEffort&&n.startsWith("o")&&(l.reasoning={effort:t.options.reasoningEffort});let a=await(0,c.requestUrl)({url:e,method:"POST",headers:{Authorization:`Bearer ${s.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(l),throw:!1});if(a.status<200||a.status>=300)throw new Error(a.text||`HTTP ${a.status}`);return Se(a.json)}async function we(s,t){if(!s.apiBaseUrl)throw new Error("OAuth provider needs an API endpoint after login.");let e=await(0,c.requestUrl)({url:s.apiBaseUrl,method:"POST",headers:{Authorization:`Bearer ${s.accessToken}`,"Content-Type":"application/json"},body:JSON.stringify(t),throw:!1});if(e.status<200||e.status>=300)throw new Error(e.text||`HTTP ${e.status}`);return typeof e.json?.text=="string"?e.json.text:typeof e.json?.output_text=="string"?e.json.output_text:e.text}function Ae(s,t,e){return new Promise((n,i)=>{let o=ke(t),r=be(s,t,e),l=`${r} ${X(o)}`,a=(0,I.spawn)("/bin/zsh",["-lc",l],{stdio:["ignore","pipe","pipe"]}),d="",h="";a.stdout.on("data",u=>{d+=u.toString()}),a.stderr.on("data",u=>{h+=u.toString()}),a.on("error",i),a.on("close",u=>{u===0?n(d.trim()):i(new Error(h.trim()||`Command exited with code ${u}. Check that ${r} is installed and available in your shell.`))})})}function be(s,t,e){let n=Ce(e);if(s.id==="codex"||s.command.startsWith("codex exec")){let i=xe(t.accessMode),o=n?` -C ${X(n)}`:"";return`${s.command}${o} --skip-git-repo-check -s ${i}`}return s.command}function xe(s){return s==="full-access"?"workspace-write":"read-only"}function Ce(s){let t=s.vault.adapter;if("basePath"in t&&typeof t.basePath=="string")return t.basePath}function ke(s){return[s.instructions,"","User request:",s.prompt,"","Vault context and options:",JSON.stringify({accessMode:s.accessMode,options:s.options,conversation:s.options.memoryEnabled?s.conversation:[],context:s.context},null,2)].join(` +`)}function Pe(s,t){let e=Y(s),n=['tell application "Terminal"',"activate",`do script "printf '\\\\033]0;${U(t)}\\\\007'; clear; echo 'AI Sidebar sign-in'; echo ''; echo 'Running: ${U(s)}'; echo ''; /bin/zsh -lc '${e}; echo; echo Sign-in command finished. You can close this window when done.'"`,"end tell"].join(` +`);return new Promise((i,o)=>{let r=(0,I.spawn)("osascript",["-e",n],{stdio:["ignore","ignore","pipe"]}),l="";r.stderr.on("data",a=>{l+=a.toString()}),r.on("error",o),r.on("close",a=>{a===0?i():o(new Error(l.trim()||`Could not open Terminal. Exit code ${a}`))})})}function Se(s){if(!s||typeof s!="object")return"";let t=s;if(typeof t.output_text=="string")return t.output_text;if(typeof t.choices?.[0]?.message?.content=="string")return t.choices[0].message.content;let e=[];for(let n of t.output??[])for(let i of n.content??[])typeof i.text=="string"&&e.push(i.text);return e.join(` +`)}async function Te(s){if(!s.authorizationUrl||!s.clientId)return new c.Notice("Add an authorization URL and client ID first."),!1;let t=new URL(s.authorizationUrl);return t.searchParams.set("response_type","token"),t.searchParams.set("client_id",s.clientId),t.searchParams.set("redirect_uri","urn:ietf:wg:oauth:2.0:oob"),s.scope&&t.searchParams.set("scope",s.scope),t.searchParams.set("state",crypto.randomUUID()),window.open(t.toString()),new c.Notice("OAuth sign-in opened in your browser. Paste the returned token into this provider when available."),!0}async function O(s){if(s.authType==="cli"){if(!s.signInCommand)return new c.Notice("This local provider does not have a sign-in command configured."),!1;try{return await Pe(s.signInCommand,`${s.name} Sign In`),new c.Notice(`Opened Terminal for ${s.name} sign-in.`),!0}catch(t){let e=t instanceof Error?t.message:String(t);return new c.Notice(`Could not open sign-in: ${e}`),!1}}return s.authType==="api-key"?(s.setupUrl&&window.open(s.setupUrl),new c.Notice(s.apiKey?`${s.name} is connected.`:`Add your ${s.name} API key below to connect.`),!!s.apiKey):Te(s)}function Y(s){return s.replace(/'/g,"'\\''")}function X(s){return`'${Y(s)}'`}function U(s){return s.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}function V(s){return s.authType==="cli"?$(s)?"Reconnect":"Sign in":s.authType==="api-key"?s.apiKey?"Connected":"Get key":s.accessToken?"Reconnect":"Sign in"}function N(s){return $(s)?"Connected":s.authType==="api-key"?"Needs API key":s.authType==="oauth"?"Needs sign-in":s.signInCommand?"Ready to sign in":"Needs command"}function $(s){return s.authType==="api-key"?!!s.apiKey:s.authType==="oauth"?!!s.accessToken:!!s.connectedAt}function Ee(s){return s.authType==="api-key"?!!(s.apiKey&&s.apiBaseUrl):s.authType==="oauth"?!!(s.accessToken&&s.apiBaseUrl):!!s.command.trim()}function Me(s){return s.map(t=>{let e=t,n=e.id==="codex"&&e.command==="codex"?"codex exec":e.command??"";return{...e,authType:e.authType??(e.command?"cli":"api-key"),command:n,connectedAt:e.connectedAt}})}function M(s){return{...s}}function Fe(s){let t=[],e=/```(?:json)?\s*([\s\S]*?)```/gi,n=[],i;for(;(i=e.exec(s))!==null;)n.push(i[1]);n.push(s);for(let o of n)try{let r=JSON.parse(o.trim());Array.isArray(r.actions)&&t.push(...r.actions.filter(Ie))}catch{continue}return t}function Ie(s){if(!s||typeof s!="object")return!1;let t=s;return typeof t.type!="string"||typeof t.path!="string"?!1:["create","edit","append"].includes(t.type)?typeof t.content=="string":t.type==="delete"?!0:t.type==="rename"?typeof t.newPath=="string":!1}function _e(s,t){return new Promise(e=>{new T(s,t,e).open()})}async function De(s,t){let e=F(t.path),n=s.vault.getAbstractFileByPath(e);if(t.type==="create"){if(n)throw new Error("File already exists.");await z(s,e),await s.vault.create(e,t.content);return}if(!(n instanceof c.TFile))throw new Error("File does not exist.");if(t.type==="edit")await s.vault.modify(n,t.content);else if(t.type==="append"){let i=await s.vault.read(n);await s.vault.modify(n,`${i}${i.endsWith(` `)?"":` -`}${t.content}`)}else t.type==="delete"?await s.vault.delete(n):t.type==="rename"&&(await B(s,t.newPath),await s.fileManager.renameFile(n,T(t.newPath)))}async function B(s,t){let e=T(t).split("/");e.pop();let n="";for(let i of e)n=n?`${n}/${i}`:i,s.vault.getAbstractFileByPath(n)||await s.vault.createFolder(n)}function T(s){return s.replace(/^\/+/,"").replace(/\\/g,"/")}function j(s){let t=new Set;return s.filter(e=>t.has(e.path)?!1:(t.add(e.path),!0))}function be(s){return s==="low"||s==="medium"||s==="high"}function Ce(s){return s==="read-only"||s==="confirm"||s==="full-access"}function ke(s){return s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")} +`}${t.content}`)}else t.type==="delete"?await s.vault.delete(n):t.type==="rename"&&(await z(s,t.newPath),await s.fileManager.renameFile(n,F(t.newPath)))}async function z(s,t){let e=F(t).split("/");e.pop();let n="";for(let i of e)n=n?`${n}/${i}`:i,s.vault.getAbstractFileByPath(n)||await s.vault.createFolder(n)}function F(s){return s.replace(/^\/+/,"").replace(/\\/g,"/")}function ee(s){let t=new Set;return s.filter(e=>t.has(e.path)?!1:(t.add(e.path),!0))}function $e(s){return s==="low"||s==="medium"||s==="high"}function Re(s){return s==="read-only"||s==="confirm"||s==="full-access"}function Be(s){return s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")} diff --git a/main.ts b/main.ts index eaae15e..38eb5e3 100644 --- a/main.ts +++ b/main.ts @@ -14,6 +14,9 @@ import { WorkspaceLeaf, } from "obsidian"; import { spawn } from "child_process"; +import { promises as fs } from "fs"; +import { homedir } from "os"; +import * as path from "path"; const VIEW_TYPE_AI_SIDEBAR = "ai-sidebar-view"; @@ -67,6 +70,12 @@ interface AgentSkill { content: string; } +interface SkillReference { + name: string; + path: string; + source: "vault" | "global"; +} + interface VaultContext { activeFile?: VaultContextFile; selection?: string; @@ -386,6 +395,7 @@ class AISidebarView extends ItemView { private textareaEl: HTMLTextAreaElement; private contextEl: HTMLElement; private slashEl: HTMLElement; + private modelBadgeEl: HTMLElement; private activeContextPath = ""; private refreshTimer: number | undefined; private sendButton: ButtonComponent; @@ -453,6 +463,7 @@ class AISidebarView extends ItemView { .setValue(this.providerId) .onChange((value) => { this.providerId = value; + this.updateModelBadge(); }); new DropdownComponent(controls) @@ -494,6 +505,7 @@ class AISidebarView extends ItemView { }); this.textareaEl.addEventListener("input", () => { void this.updateSlashCommands(); + this.updateModelBadge(); }); this.slashEl = composer.createDiv("ai-sidebar__slash is-hidden"); @@ -510,6 +522,9 @@ class AISidebarView extends ItemView { void this.clearConversation(); }); + this.modelBadgeEl = actions.createDiv("ai-sidebar__model-badge"); + this.updateModelBadge(); + this.sendButton = new ButtonComponent(actions) .setCta() .setButtonText("Send") @@ -541,6 +556,17 @@ class AISidebarView extends ItemView { messageEl.createEl("pre", { text: message.content }); } + if (this.isRunning) { + const thinkingEl = this.threadEl.createDiv("ai-sidebar__message ai-sidebar__message--thinking"); + thinkingEl.createDiv({ cls: "ai-sidebar__message-role", text: "assistant" }); + const row = thinkingEl.createDiv("ai-sidebar__typing"); + row.createSpan({ text: "Thinking" }); + const dots = row.createSpan("ai-sidebar__typing-dots"); + dots.createSpan(); + dots.createSpan(); + dots.createSpan(); + } + this.threadEl.scrollTo({ top: this.threadEl.scrollHeight }); } @@ -602,10 +628,19 @@ class AISidebarView extends ItemView { return; } + const parsed = this.parsePromptControls(rawPrompt, provider); + if (this.isSlashOnlyPrompt(rawPrompt, parsed)) { + await this.applySlashControls(provider, parsed); + this.textareaEl.value = ""; + this.updateModelBadge(); + await this.refreshContextPreview(); + return; + } + this.isRunning = true; this.sendButton.setButtonText("Running..."); + this.sendButton.setDisabled(true); this.textareaEl.value = ""; - const parsed = this.parsePromptControls(rawPrompt, provider); const effectiveAccessMode = parsed.accessMode ?? this.accessMode; this.messages.push({ role: "user", content: rawPrompt }); this.renderMessages(); @@ -640,6 +675,8 @@ class AISidebarView extends ItemView { } finally { this.isRunning = false; this.sendButton.setButtonText("Send"); + this.sendButton.setDisabled(false); + this.updateModelBadge(); this.renderMessages(); } } @@ -695,7 +732,7 @@ class AISidebarView extends ItemView { } this.slashEl.removeClass("is-hidden"); - for (const command of commands.slice(0, 10)) { + for (const command of commands.slice(0, 6)) { const item = this.slashEl.createDiv("ai-sidebar__slash-item"); item.createDiv({ cls: "ai-sidebar__slash-name", text: command.label }); item.createDiv({ cls: "ai-sidebar__slash-path", text: command.description }); @@ -704,7 +741,7 @@ class AISidebarView extends ItemView { this.insertSlashToken(command.insert); }); } - for (const skill of skills.slice(0, 8)) { + for (const skill of skills.slice(0, 10)) { const item = this.slashEl.createDiv("ai-sidebar__slash-item"); item.createDiv({ cls: "ai-sidebar__slash-name", text: `/${skill.name}` }); item.createDiv({ cls: "ai-sidebar__slash-path", text: skill.path }); @@ -723,7 +760,7 @@ class AISidebarView extends ItemView { private currentSlashQuery(): string | null { const cursor = this.textareaEl.selectionStart; const beforeCursor = this.textareaEl.value.slice(0, cursor); - const match = beforeCursor.match(/(?:^|\s)\/([A-Za-z0-9_.-]*)$/); + const match = beforeCursor.match(/(?:^|\s)\/([A-Za-z0-9_.:-]*)$/); return match ? match[1].toLowerCase() : null; } @@ -732,7 +769,7 @@ class AISidebarView extends ItemView { const value = this.textareaEl.value; const beforeCursor = value.slice(0, cursor); const afterCursor = value.slice(cursor); - const replaced = beforeCursor.replace(/(?:^|\s)\/([A-Za-z0-9_.-]*)$/, (match) => { + const replaced = beforeCursor.replace(/(?:^|\s)\/([A-Za-z0-9_.:-]*)$/, (match) => { const prefix = match.startsWith(" ") ? " " : ""; return `${prefix}${token} `; }); @@ -774,12 +811,49 @@ class AISidebarView extends ItemView { }).trim(); return { - cleanPrompt: cleanPrompt || prompt, + cleanPrompt, selectedSkillNames: this.extractSelectedSkillNames(prompt), options, accessMode, }; } + + private isSlashOnlyPrompt(prompt: string, parsed: ParsedPrompt): boolean { + return parsed.cleanPrompt.length === 0 && this.extractSelectedSkillNames(prompt).length === 0; + } + + private async applySlashControls(provider: AgentProvider, parsed: ParsedPrompt) { + const changes: string[] = []; + if (parsed.options.model && parsed.options.model !== provider.model) { + provider.model = parsed.options.model; + changes.push(`model ${parsed.options.model}`); + } + if (parsed.options.reasoningEffort && parsed.options.reasoningEffort !== provider.reasoningEffort) { + provider.reasoningEffort = parsed.options.reasoningEffort; + changes.push(`reasoning ${parsed.options.reasoningEffort}`); + } + if (parsed.accessMode && parsed.accessMode !== this.accessMode) { + this.accessMode = parsed.accessMode; + this.plugin.settings.defaultAccessMode = parsed.accessMode; + changes.push(`access ${this.accessModeLabel()}`); + } + if (parsed.options.memoryEnabled !== this.plugin.settings.enableConversationMemory) { + this.plugin.settings.enableConversationMemory = parsed.options.memoryEnabled; + changes.push(`memory ${parsed.options.memoryEnabled ? "on" : "off"}`); + } + await this.plugin.saveSettings(); + new Notice(changes.length > 0 ? `Updated ${changes.join(", ")}.` : "Slash command applied."); + } + + private updateModelBadge() { + if (!this.modelBadgeEl) return; + const provider = this.plugin.getProvider(this.providerId); + const override = this.textareaEl?.value.match(/(?:^|\s)\/model:([A-Za-z0-9_.:/-]+)/i)?.[1]; + const model = override || provider?.model || parseProviderModels(provider ?? DEFAULT_SETTINGS.providers[0]).first() || "default"; + this.modelBadgeEl.setText(model); + this.modelBadgeEl.toggleClass("is-override", Boolean(override)); + this.modelBadgeEl.setAttr("aria-label", override ? `Model override: ${model}` : `Model: ${model}`); + } } class AISidebarSettingTab extends PluginSettingTab { @@ -1334,27 +1408,90 @@ function parseProviderModels(provider: AgentProvider): string[] { return Array.from(new Set(models)).slice(0, 12); } -async function listAgentSkills(app: App, query = ""): Promise> { +async function listAgentSkills(app: App, query = ""): Promise { const normalizedQuery = query.toLowerCase(); - const skills = app.vault + const vaultSkills = app.vault .getFiles() .filter((file) => isSkillFile(file)) .map((file) => ({ name: skillNameFromPath(file.path), path: file.path, - })) + source: "vault" as const, + })); + const adapterSkills = await listAgentSkillsFromAdapter(app); + const globalSkills = await listGlobalAgentSkills(); + const skills = [...vaultSkills, ...adapterSkills, ...globalSkills] .filter((skill) => !normalizedQuery || skill.name.toLowerCase().includes(normalizedQuery)) .sort((a, b) => a.name.localeCompare(b.name)); const seen = new Set(); return skills.filter((skill) => { - const key = skill.name.toLowerCase(); + const key = `${skill.source}:${skill.name.toLowerCase()}`; if (seen.has(key)) return false; seen.add(key); return true; }); } +async function listAgentSkillsFromAdapter(app: App): Promise { + const roots = [".agent/skills", ".agents/skills", "agent/skills", "agents/skills"]; + const files: string[] = []; + for (const root of roots) { + files.push(...await listAdapterFiles(app, root)); + } + return files + .filter((path) => isSkillPath(path)) + .map((path) => ({ + name: skillNameFromPath(path), + path, + source: "vault" as const, + })); +} + +async function listGlobalAgentSkills(): Promise { + const roots = [ + path.join(homedir(), ".agent", "skills"), + path.join(homedir(), ".agents", "skills"), + ]; + const files: string[] = []; + for (const root of roots) { + files.push(...await listFilesystemFiles(root)); + } + return files + .filter((filePath) => isSkillPath(filePath)) + .map((filePath) => ({ + name: skillNameFromPath(filePath), + path: filePath, + source: "global" as const, + })); +} + +async function listFilesystemFiles(folder: string): Promise { + try { + const entries = await fs.readdir(folder, { withFileTypes: true }); + const results = await Promise.all(entries.map(async (entry) => { + const childPath = path.join(folder, entry.name); + if (entry.isDirectory()) return listFilesystemFiles(childPath); + if (entry.isFile()) return [childPath]; + return []; + })); + return results.flat(); + } catch { + return []; + } +} + +async function listAdapterFiles(app: App, folder: string): Promise { + try { + const listed = await app.vault.adapter.list(folder); + const childFiles = listed.files; + const nested = await Promise.all(listed.folders.map((child) => listAdapterFiles(app, child))); + return [...childFiles, ...nested.flat()]; + } catch { + return []; + } +} + async function readSelectedSkills( app: App, selectedSkillNames: string[], @@ -1364,22 +1501,43 @@ async function readSelectedSkills( if (names.size === 0) return []; const skills: AgentSkill[] = []; - for (const file of app.vault.getFiles().filter((candidate) => isSkillFile(candidate))) { - const name = skillNameFromPath(file.path); + const skillFiles = [...await listAgentSkillsFromAdapter(app), ...await listGlobalAgentSkills()]; + const vaultFiles = app.vault + .getFiles() + .filter((candidate) => isSkillFile(candidate)) + .map((file) => ({ name: skillNameFromPath(file.path), path: file.path, source: "vault" as const })); + for (const skillFile of [...vaultFiles, ...skillFiles]) { + const name = skillFile.name; if (!names.has(name.toLowerCase()) || !budget.hasRoom()) continue; - const content = await app.vault.cachedRead(file); + const content = await readSkillContent(app, skillFile); skills.push({ name, - path: file.path, + path: skillFile.path, content: budget.take(content), }); } return skills; } +async function readSkillContent(app: App, skill: SkillReference): Promise { + if (skill.source === "global") return fs.readFile(skill.path, "utf8"); + const file = app.vault.getAbstractFileByPath(skill.path); + if (file instanceof TFile) return app.vault.cachedRead(file); + return app.vault.adapter.read(skill.path); +} + function isSkillFile(file: TFile): boolean { - if (!["md", "txt", "json", "yaml", "yml"].includes(file.extension)) return false; - return file.path.startsWith(".agent/skills/") || file.path.startsWith(".agents/skills/"); + return isSkillPath(file.path); +} + +function isSkillPath(path: string): boolean { + const extension = path.split(".").last()?.toLowerCase() ?? ""; + if (!["md", "txt", "json", "yaml", "yml", "prompt"].includes(extension)) return false; + const normalized = path.replace(/^\/+/, ""); + return normalized.startsWith(".agent/skills/") + || normalized.startsWith(".agents/skills/") + || normalized.startsWith("agent/skills/") + || normalized.startsWith("agents/skills/"); } function skillNameFromPath(path: string): string { diff --git a/styles.css b/styles.css index 1a31b8f..241ecfc 100644 --- a/styles.css +++ b/styles.css @@ -29,6 +29,11 @@ gap: 8px; } +.ai-sidebar__composer-actions { + align-items: center; + flex-wrap: wrap; +} + .ai-sidebar__controls select { min-width: 0; width: 100%; @@ -93,6 +98,11 @@ color: var(--text-on-accent); } +.ai-sidebar__message--thinking { + background: var(--background-secondary); + opacity: 0.95; +} + .ai-sidebar__message-role { color: var(--text-muted); font-size: 11px; @@ -108,6 +118,49 @@ white-space: pre-wrap; } +.ai-sidebar__typing { + align-items: center; + color: var(--text-muted); + display: flex; + font-size: 13px; + gap: 8px; +} + +.ai-sidebar__typing-dots { + display: inline-flex; + gap: 4px; +} + +.ai-sidebar__typing-dots span { + animation: ai-sidebar-dot-pulse 1.2s infinite ease-in-out; + background: var(--text-muted); + border-radius: 999px; + height: 5px; + opacity: 0.35; + width: 5px; +} + +.ai-sidebar__typing-dots span:nth-child(2) { + animation-delay: 0.16s; +} + +.ai-sidebar__typing-dots span:nth-child(3) { + animation-delay: 0.32s; +} + +@keyframes ai-sidebar-dot-pulse { + 0%, + 80%, + 100% { + opacity: 0.25; + transform: translateY(0); + } + 40% { + opacity: 1; + transform: translateY(-3px); + } +} + .ai-sidebar__composer { border-top: 1px solid var(--background-modifier-border); display: flex; @@ -123,6 +176,26 @@ width: 100%; } +.ai-sidebar__model-badge { + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: 999px; + color: var(--text-muted); + font-size: 11px; + font-weight: 600; + line-height: 1; + max-width: 180px; + overflow: hidden; + padding: 7px 10px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ai-sidebar__model-badge.is-override { + border-color: var(--interactive-accent); + color: var(--text-normal); +} + .ai-sidebar__slash { background: var(--background-primary); border: 1px solid var(--background-modifier-border);