From f4111bdafc6dcffa98e91cb8a80c41ef2d3d8021 Mon Sep 17 00:00:00 2001 From: Denis Berekchiyan Date: Mon, 11 May 2026 14:00:54 -0700 Subject: [PATCH] ui: conversation delete cleanup + new-chat snap-back fix + deletable Main chat (v0.10.3 refresh) Three small cherry-picks on top of v0.10.3 (no version bump): - fix: + New chat no longer snaps back to Main chat 500ms after creation. populateAgentDropdown's safety net now respects the currently-selected conversation id instead of always defaulting to 'default'. - fix: deleting a conversation now also removes its threads sidecar and prunes empty conversations/ folders; ChatSession.dispose() aborts thread sub-sessions in memory. - feat: Main chat (legacy default conversation) is now deletable. Panel re-synthesizes an empty Main chat after the last conversation is deleted so the view never strands. 110/110 tests green (+2 dispose tests). Co-Authored-By: Claude Opus 4.7 (1M context) --- main.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.js b/main.js index a28cb5f..d5e487a 100644 --- a/main.js +++ b/main.js @@ -11664,7 +11664,7 @@ ${e} --- ${s} -`}function fe(r){return r.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/(^-|-$)/g,"")}function Ot(r,t){return r.length<=t?r:`${r.slice(0,t-1)}\u2026`}var ut=require("child_process"),sn=require("fs"),Qt=require("os"),Tt=require("path");function ua(){return(0,Qt.homedir)()}function Zt(){return(0,Tt.join)((0,Qt.homedir)(),".claude")}function es(){return(0,Tt.join)((0,Qt.homedir)(),".claude.json")}function an(){if(process.platform==="darwin")return"/bin/zsh";for(let r of["/bin/bash","/bin/zsh","/bin/sh"])if((0,sn.existsSync)(r))return r;return"/bin/sh"}function rr(r){return`'${r.replace(/'/g,"'\\''")}'`}function pt(r,t,e){let s={cwd:e?.cwd,env:e?.env};if(process.platform==="win32")return(0,ut.spawn)(r,t,s);let a=an(),n=[r,...t].map(rr).join(" ");return(0,ut.spawn)(a,["-l","-c",n],s)}function pa(r,t){let e={cwd:t?.cwd,env:t?.env,stdio:["pipe","pipe","pipe"]};if(process.platform==="win32")return(0,ut.spawn)(r,[],{...e,shell:!0});let s=an();return(0,ut.spawn)(s,["-l","-c",r],e)}function nn(r){try{require("electron").shell.openExternal(r)}catch{switch(process.platform){case"darwin":(0,ut.spawn)("open",[r],{stdio:"ignore"});break;case"win32":(0,ut.spawn)("cmd.exe",["/c","start","",r.replace(/&/g,"^&")],{stdio:"ignore"});break;default:(0,ut.spawn)("xdg-open",[r],{stdio:"ignore"});break}}}function me(r){return r.split(/\r?\n/)}function rn(r){let t=(0,Qt.homedir)();if(process.platform==="win32")return[r,(0,Tt.join)(process.env.APPDATA??"","Claude","claude.exe"),(0,Tt.join)(process.env.LOCALAPPDATA??"","Claude","claude.exe"),(0,Tt.join)(t,".local","bin","claude.exe"),"claude.exe","claude"].filter(s=>!!s&&tn(s));let e=[r,(0,Tt.join)(t,".local","bin","claude")];return process.platform==="darwin"&&e.push("/opt/homebrew/bin/claude"),e.push("/usr/local/bin/claude","/usr/bin/claude","claude"),e.filter(s=>!!s&&tn(s))}function tn(r){return!r||/[\n\r\0]/.test(r)?!1:r.startsWith("/")?/^[\w/.@+-]+$/.test(r):r.startsWith("~")?/^~[\w/.@+-]*$/.test(r):/^[a-zA-Z]:[\\/]/.test(r)?/^[a-zA-Z]:[\\/][\w\\/. @+-]+$/.test(r):r.startsWith("\\\\")?/^\\\\[\w\\/. @+-]+$/.test(r):!r.includes("/")&&!r.includes("\\")?/^[\w.@+-]+$/.test(r):!1}function fa(r){return!!(r.includes("/")||r.includes("\\"))}function xs(r){return typeof r=="object"&&r!==null}function M(r){return typeof r=="string"?r:void 0}function Je(r,t){return typeof r=="boolean"?r:t}function $e(r,t){return typeof r=="number"&&Number.isFinite(r)?r:t}function ve(r){return Array.isArray(r)?r.filter(t=>typeof t=="string"):[]}function on(r){return(0,C.normalizePath)(r.replace(/\.md$/,".permissions.json"))}function ln(r){let t=0;for(let e=0;ee.path.startsWith(`${this.getFleetRoot()}/`));for(let e of t)await this.loadFile(e);return this.validateReferences(),this.getSnapshot()}async loadFile(t){let e=typeof t=="string"?this.vault.getAbstractFileByPath(t):t;if(!(e instanceof C.TFile)||e.extension!=="md")return;if(this.isInsideAgentFolder(e.path)){await this.reloadFolderAgentContaining(e.path);return}if(this.isInsideSkillFolder(e.path)){await this.reloadFolderSkillContaining(e.path);return}this.clearStoredFile(e.path);let s=`${this.getSubfolder("channels")}/`;if(e.path.startsWith(s)){if(!e.path.slice(s.length).includes("/")){let o=await this.vault.cachedRead(e),c=this.parseChannelFile(e.path,o);c&&this.channels.set(e.path,c)}return}let a=await this.vault.cachedRead(e),n=this.parseFile(e.path,a);if(n)if("taskId"in n)this.tasks.set(e.path,n);else if("model"in n){if(!n.isFolder){let i=on(e.path),o=this.vault.getAbstractFileByPath(i);if(o instanceof C.TFile)try{let c=await this.vault.cachedRead(o),l=JSON.parse(c);n.permissionRules={allow:ve(l.allow),deny:ve(l.deny)}}catch{}}this.agents.set(e.path,n)}else this.skills.set(e.path,n)}async reloadFolderAgentContaining(t){let e=`${this.getSubfolder("agents")}/`,a=t.slice(e.length).split("/")[0];if(!a)return;let n=(0,C.normalizePath)(`${e}${a}`),i=(0,C.normalizePath)(`${n}/agent.md`);if(this.agents.delete(i),!(this.vault.getAbstractFileByPath(n)instanceof C.TFolder))return;let c=this.vault.getAbstractFileByPath(i);if(!(c instanceof C.TFile))return;let l=await this.loadFolderAgent(n,c);l&&this.agents.set(i,l)}isInsideAgentFolder(t){let e=`${this.getSubfolder("agents")}/`;return t.startsWith(e)?t.slice(e.length).includes("/"):!1}isInsideSkillFolder(t){let e=`${this.getSubfolder("skills")}/`;return t.startsWith(e)?t.slice(e.length).includes("/"):!1}async reloadFolderSkillContaining(t){let e=`${this.getSubfolder("skills")}/`,a=t.slice(e.length).split("/")[0];if(!a)return;let n=(0,C.normalizePath)(`${e}${a}`),i=(0,C.normalizePath)(`${n}/skill.md`);if(this.skills.delete(i),!(this.vault.getAbstractFileByPath(n)instanceof C.TFolder))return;let c=this.vault.getAbstractFileByPath(i);if(!(c instanceof C.TFile))return;let l=await this.loadFolderSkill(n,c);l&&this.skills.set(i,l)}async loadFolderSkills(){let t=this.vault.getAbstractFileByPath(this.getSubfolder("skills"));if(t instanceof C.TFolder)for(let e of t.children){if(!(e instanceof C.TFolder))continue;let s=(0,C.normalizePath)(`${e.path}/skill.md`),a=this.vault.getAbstractFileByPath(s);if(!(a instanceof C.TFile))continue;let n=await this.loadFolderSkill(e.path,a);n&&this.skills.set(s,n)}}async loadFolderSkill(t,e){let s=await this.vault.cachedRead(e),{frontmatter:a,body:n}=ne(s),i=M(a.name);if(!i)return this.setIssue(e.path,"Folder skill skill.md requires string field `name`."),null;let o=async c=>{let l=(0,C.normalizePath)(`${t}/${c}`),d=this.vault.getAbstractFileByPath(l);if(!(d instanceof C.TFile))return"";let h=await this.vault.cachedRead(d);return ne(h).body};return{filePath:e.path,name:i,description:M(a.description),tags:ve(a.tags),body:n,toolsBody:await o("tools.md"),referencesBody:await o("references.md"),examplesBody:await o("examples.md"),isFolder:!0}}async loadFolderAgents(){let t=this.vault.getAbstractFileByPath(this.getSubfolder("agents"));if(t instanceof C.TFolder)for(let e of t.children){if(!(e instanceof C.TFolder))continue;let s=(0,C.normalizePath)(`${e.path}/agent.md`),a=this.vault.getAbstractFileByPath(s);if(!(a instanceof C.TFile))continue;let n=await this.loadFolderAgent(e.path,a);n&&this.agents.set(s,n)}}async loadFolderAgent(t,e){let s=await this.vault.cachedRead(e),{frontmatter:a,body:n}=ne(s),i=M(a.name);if(!i)return this.setIssue(e.path,"Folder agent agent.md requires string field `name`."),null;let o={},c=(0,C.normalizePath)(`${t}/config.md`),l=this.vault.getAbstractFileByPath(c);if(l instanceof C.TFile){let B=await this.vault.cachedRead(l);o=ne(B).frontmatter}let d={allow:[],deny:[]},h=(0,C.normalizePath)(`${t}/permissions.json`),u=this.vault.getAbstractFileByPath(h);if(u instanceof C.TFile)try{let B=await this.vault.cachedRead(u),I=JSON.parse(B);d={allow:ve(I.allow),deny:ve(I.deny)}}catch{}if(d.allow.length===0&&d.deny.length===0){let B=ve(o.allowed_tools),I=ve(o.blocked_tools);(B.length>0||I.length>0)&&(d={allow:B,deny:I},this.warnedLegacyPerms.has(i)||(this.warnedLegacyPerms.add(i),console.warn(`Agent Fleet: "${i}" still uses legacy allowed_tools/blocked_tools in config.md. Permission rules now live in permissions.json. Open this agent in Edit and Save to migrate.`)))}let p="",m=(0,C.normalizePath)(`${t}/SKILLS.md`),f=this.vault.getAbstractFileByPath(m);if(f instanceof C.TFile){let B=await this.vault.cachedRead(f);p=ne(B).body}let y="",k=(0,C.normalizePath)(`${t}/CONTEXT.md`),b=this.vault.getAbstractFileByPath(k);if(b instanceof C.TFile){let B=await this.vault.cachedRead(b);y=ne(B).body}let v=!1,g="",x="",T=!0,_="",R=(0,C.normalizePath)(`${t}/HEARTBEAT.md`),A=this.vault.getAbstractFileByPath(R);if(A instanceof C.TFile){let B=await this.vault.cachedRead(A),I=ne(B);v=Je(I.frontmatter.enabled,!1),g=M(I.frontmatter.schedule)??"",T=Je(I.frontmatter.notify,!0),_=M(I.frontmatter.channel)??"",x=I.body}let S=M(a.model),L=M(o.model);S&&L&&S!==L&&(this.warnedFolderAgentModelConflict.has(i)||(this.warnedFolderAgentModelConflict.add(i),console.warn(`Agent Fleet: "${i}" has conflicting model fields \u2014 agent.md says "${S}", config.md says "${L}". config.md wins. Remove agent.md's model field or sync the values to silence this warning.`)));let E=L??S??this.settings.defaultModel;return{filePath:e.path,name:i,description:M(a.description),model:E,adapter:M(o.adapter)??"claude-code",permissionMode:M(o.permission_mode)??"bypassPermissions",effort:M(o.effort),maxRetries:$e(o.max_retries,1),skills:ve(a.skills),mcpServers:ve(a.mcp_servers),cwd:M(o.cwd)||M(a.cwd),enabled:Je(a.enabled,!0),timeout:$e(o.timeout,$e(a.timeout,300)),approvalRequired:ve(o.approval_required),memory:Je(o.memory,Je(a.memory,!1)),memoryMaxEntries:$e(o.memory_max_entries,100),autoCompactThreshold:$e(o.auto_compact_threshold??a.auto_compact_threshold,85),tags:ve(a.tags),avatar:M(a.avatar)??"",body:n,contextBody:y,skillsBody:p,env:this.parseEnvMap(o.env),permissionRules:d,isFolder:!0,heartbeatEnabled:v,heartbeatSchedule:g,heartbeatBody:x,heartbeatNotify:T,heartbeatChannel:_,wikiKeeper:this.parseWikiKeeperConfig(o.wiki_keeper??a.wiki_keeper),wikiReferences:this.parseWikiReferences(o.wiki_references??a.wiki_references)}}parseWikiReferences(t){if(!Array.isArray(t))return;let e=[];for(let s of t)if(typeof s=="string"&&s.trim())e.push({agent:s.trim()});else if(s&&typeof s=="object"){let a=s.agent;typeof a=="string"&&a.trim()&&e.push({agent:a.trim()})}return e.length>0?e:void 0}parseWikiKeeperConfig(t){if(!t||typeof t!="object")return;let e=t;return{scopeRoot:M(e.scope_root)??"",inboxPath:M(e.inbox_path)??"_sources/inbox",archivePath:M(e.archive_path)??"_sources/archive",failedPath:M(e.failed_path)??"_sources/failed",topicsRoot:M(e.topics_root)??"_topics",indexPath:M(e.index_path)??"index.md",logPath:M(e.log_path)??"log.md",watchedFolders:ve(e.watched_folders),excludePatterns:ve(e.exclude_patterns),watchedSince:M(e.watched_since)??"",fileSubstantiveAnswers:Je(e.file_substantive_answers,!0),obsidianUrlScheme:Je(e.obsidian_url_scheme,!0),maxTokensPerIngest:$e(e.max_tokens_per_ingest,6e4),maxTokensPerRefresh:$e(e.max_tokens_per_refresh,3e4),indexSplitThreshold:$e(e.index_split_threshold,100),dedupSimilarityThreshold:$e(e.dedup_similarity_threshold,.82),summaryStaleDays:$e(e.summary_stale_days,30),stateFile:M(e.state_file)??".wiki-keeper-state.json"}}removeFile(t){this.clearStoredFile(t)}getSnapshot(){return{agents:Array.from(this.agents.values()).sort((t,e)=>t.name.localeCompare(e.name)),skills:Array.from(this.skills.values()).sort((t,e)=>t.name.localeCompare(e.name)),tasks:Array.from(this.tasks.values()).sort((t,e)=>t.taskId.localeCompare(e.taskId)),channels:Array.from(this.channels.values()).sort((t,e)=>t.name.localeCompare(e.name)),validationIssues:Array.from(this.validationIssues.values()).flat()}}getAgentByName(t){return Array.from(this.agents.values()).find(e=>e.name===t)}getSkillByName(t){return Array.from(this.skills.values()).find(e=>e.name===t)}getTaskById(t){return Array.from(this.tasks.values()).find(e=>e.taskId===t)}getTasksForAgent(t){return Array.from(this.tasks.values()).filter(e=>e.agent===t)}getChannelByName(t){return Array.from(this.channels.values()).find(e=>e.name===t)}getChannelsForAgent(t){return Array.from(this.channels.values()).filter(e=>e.defaultAgent===t)}getRunsRoot(){return this.getSubfolder("runs")}getMemoryPath(t){return(0,C.normalizePath)(`${this.getSubfolder("memory")}/${fe(t)}.md`)}getConversationsDir(t){if(t.isFolder){let s=t.filePath.replace(/\/agent\.md$/,"");return(0,C.normalizePath)(`${s}/conversations`)}let e=this.getMemoryPath(t.name).replace(/\/[^/]+$/,"");return(0,C.normalizePath)(`${e}/${t.name}-conversations`)}getLegacyChatPath(t){if(t.isFolder){let s=t.filePath.replace(/\/agent\.md$/,"");return(0,C.normalizePath)(`${s}/chat.json`)}let e=this.getMemoryPath(t.name).replace(/\/[^/]+$/,"");return(0,C.normalizePath)(`${e}/${t.name}-chat.json`)}getConversationPath(t,e){if(e==="default")return this.getLegacyChatPath(t);let s=this.getConversationsDir(t),a=fe(e)||"conversation";return(0,C.normalizePath)(`${s}/${a}.json`)}async listConversations(t){let e=[],s=this.getLegacyChatPath(t),a=this.vault.getAbstractFileByPath(s);if(a instanceof C.TFile){let o=await this.readConversationMeta(a,"default",!0);o&&e.push(o)}let n=this.getConversationsDir(t),i=this.vault.getAbstractFileByPath(n);if(i instanceof C.TFolder)for(let o of i.children){if(!(o instanceof C.TFile)||o.extension!=="json")continue;let c=o.basename,l=await this.readConversationMeta(o,c,!1);l&&e.push(l)}return e.sort((o,c)=>c.lastActive.localeCompare(o.lastActive)),e}async readConversationMeta(t,e,s){try{let a=await this.vault.cachedRead(t),n=JSON.parse(a);return{id:e,name:n.name?.trim()||(s?"Main chat":"Untitled"),lastActive:n.lastActive??new Date(t.stat.mtime).toISOString(),messageCount:Array.isArray(n.messages)?n.messages.length:0,isLegacy:s}}catch{return null}}async createConversation(t,e,s){if(e==="default")throw new Error('Cannot create a conversation with the reserved id "default"');let a=this.getConversationPath(t,e);if(this.vault.getAbstractFileByPath(a)instanceof C.TFile)return;let i=a.replace(/\/[^/]+$/,"");await this.ensureFolder(i);let o=new Date().toISOString(),c={sessionId:null,messages:[],lastActive:o,createdAt:o,name:s.trim()};await this.vault.create(a,JSON.stringify(c,null,2))}async renameConversation(t,e,s){let a=this.getConversationPath(t,e),n=this.vault.getAbstractFileByPath(a);if(!(n instanceof C.TFile))return;let i=await this.vault.cachedRead(n),o;try{o=JSON.parse(i)}catch{return}o.name=s.trim(),await this.vault.modify(n,JSON.stringify(o,null,2))}async deleteConversation(t,e){if(e==="default")throw new Error("The default (legacy) conversation can't be deleted from the registry; use the chat view's New Chat instead.");let s=this.getConversationPath(t,e),a=this.vault.getAbstractFileByPath(s);a instanceof C.TFile&&await this.vault.delete(a)}async getMemory(t){let e=this.getMemoryPath(t),s=this.vault.getAbstractFileByPath(e);if(!(s instanceof C.TFile))return null;let a=await this.vault.cachedRead(s),{frontmatter:n,body:i}=ne(a);return{filePath:e,agent:M(n.agent)??t,lastUpdated:M(n.last_updated),body:i}}async appendMemory(t,e){if(e.length===0)return;let s=this.getMemoryPath(t),a=this.vault.getAbstractFileByPath(s),n=new Date().toISOString(),i=e.map(o=>`- ${o.trim()}`).join(` +`}function fe(r){return r.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/(^-|-$)/g,"")}function Ot(r,t){return r.length<=t?r:`${r.slice(0,t-1)}\u2026`}var ut=require("child_process"),sn=require("fs"),Qt=require("os"),Tt=require("path");function ua(){return(0,Qt.homedir)()}function Zt(){return(0,Tt.join)((0,Qt.homedir)(),".claude")}function es(){return(0,Tt.join)((0,Qt.homedir)(),".claude.json")}function an(){if(process.platform==="darwin")return"/bin/zsh";for(let r of["/bin/bash","/bin/zsh","/bin/sh"])if((0,sn.existsSync)(r))return r;return"/bin/sh"}function rr(r){return`'${r.replace(/'/g,"'\\''")}'`}function pt(r,t,e){let s={cwd:e?.cwd,env:e?.env};if(process.platform==="win32")return(0,ut.spawn)(r,t,s);let a=an(),n=[r,...t].map(rr).join(" ");return(0,ut.spawn)(a,["-l","-c",n],s)}function pa(r,t){let e={cwd:t?.cwd,env:t?.env,stdio:["pipe","pipe","pipe"]};if(process.platform==="win32")return(0,ut.spawn)(r,[],{...e,shell:!0});let s=an();return(0,ut.spawn)(s,["-l","-c",r],e)}function nn(r){try{require("electron").shell.openExternal(r)}catch{switch(process.platform){case"darwin":(0,ut.spawn)("open",[r],{stdio:"ignore"});break;case"win32":(0,ut.spawn)("cmd.exe",["/c","start","",r.replace(/&/g,"^&")],{stdio:"ignore"});break;default:(0,ut.spawn)("xdg-open",[r],{stdio:"ignore"});break}}}function me(r){return r.split(/\r?\n/)}function rn(r){let t=(0,Qt.homedir)();if(process.platform==="win32")return[r,(0,Tt.join)(process.env.APPDATA??"","Claude","claude.exe"),(0,Tt.join)(process.env.LOCALAPPDATA??"","Claude","claude.exe"),(0,Tt.join)(t,".local","bin","claude.exe"),"claude.exe","claude"].filter(s=>!!s&&tn(s));let e=[r,(0,Tt.join)(t,".local","bin","claude")];return process.platform==="darwin"&&e.push("/opt/homebrew/bin/claude"),e.push("/usr/local/bin/claude","/usr/bin/claude","claude"),e.filter(s=>!!s&&tn(s))}function tn(r){return!r||/[\n\r\0]/.test(r)?!1:r.startsWith("/")?/^[\w/.@+-]+$/.test(r):r.startsWith("~")?/^~[\w/.@+-]*$/.test(r):/^[a-zA-Z]:[\\/]/.test(r)?/^[a-zA-Z]:[\\/][\w\\/. @+-]+$/.test(r):r.startsWith("\\\\")?/^\\\\[\w\\/. @+-]+$/.test(r):!r.includes("/")&&!r.includes("\\")?/^[\w.@+-]+$/.test(r):!1}function fa(r){return!!(r.includes("/")||r.includes("\\"))}function xs(r){return typeof r=="object"&&r!==null}function M(r){return typeof r=="string"?r:void 0}function Je(r,t){return typeof r=="boolean"?r:t}function $e(r,t){return typeof r=="number"&&Number.isFinite(r)?r:t}function ve(r){return Array.isArray(r)?r.filter(t=>typeof t=="string"):[]}function on(r){return(0,C.normalizePath)(r.replace(/\.md$/,".permissions.json"))}function ln(r){let t=0;for(let e=0;ee.path.startsWith(`${this.getFleetRoot()}/`));for(let e of t)await this.loadFile(e);return this.validateReferences(),this.getSnapshot()}async loadFile(t){let e=typeof t=="string"?this.vault.getAbstractFileByPath(t):t;if(!(e instanceof C.TFile)||e.extension!=="md")return;if(this.isInsideAgentFolder(e.path)){await this.reloadFolderAgentContaining(e.path);return}if(this.isInsideSkillFolder(e.path)){await this.reloadFolderSkillContaining(e.path);return}this.clearStoredFile(e.path);let s=`${this.getSubfolder("channels")}/`;if(e.path.startsWith(s)){if(!e.path.slice(s.length).includes("/")){let o=await this.vault.cachedRead(e),c=this.parseChannelFile(e.path,o);c&&this.channels.set(e.path,c)}return}let a=await this.vault.cachedRead(e),n=this.parseFile(e.path,a);if(n)if("taskId"in n)this.tasks.set(e.path,n);else if("model"in n){if(!n.isFolder){let i=on(e.path),o=this.vault.getAbstractFileByPath(i);if(o instanceof C.TFile)try{let c=await this.vault.cachedRead(o),l=JSON.parse(c);n.permissionRules={allow:ve(l.allow),deny:ve(l.deny)}}catch{}}this.agents.set(e.path,n)}else this.skills.set(e.path,n)}async reloadFolderAgentContaining(t){let e=`${this.getSubfolder("agents")}/`,a=t.slice(e.length).split("/")[0];if(!a)return;let n=(0,C.normalizePath)(`${e}${a}`),i=(0,C.normalizePath)(`${n}/agent.md`);if(this.agents.delete(i),!(this.vault.getAbstractFileByPath(n)instanceof C.TFolder))return;let c=this.vault.getAbstractFileByPath(i);if(!(c instanceof C.TFile))return;let l=await this.loadFolderAgent(n,c);l&&this.agents.set(i,l)}isInsideAgentFolder(t){let e=`${this.getSubfolder("agents")}/`;return t.startsWith(e)?t.slice(e.length).includes("/"):!1}isInsideSkillFolder(t){let e=`${this.getSubfolder("skills")}/`;return t.startsWith(e)?t.slice(e.length).includes("/"):!1}async reloadFolderSkillContaining(t){let e=`${this.getSubfolder("skills")}/`,a=t.slice(e.length).split("/")[0];if(!a)return;let n=(0,C.normalizePath)(`${e}${a}`),i=(0,C.normalizePath)(`${n}/skill.md`);if(this.skills.delete(i),!(this.vault.getAbstractFileByPath(n)instanceof C.TFolder))return;let c=this.vault.getAbstractFileByPath(i);if(!(c instanceof C.TFile))return;let l=await this.loadFolderSkill(n,c);l&&this.skills.set(i,l)}async loadFolderSkills(){let t=this.vault.getAbstractFileByPath(this.getSubfolder("skills"));if(t instanceof C.TFolder)for(let e of t.children){if(!(e instanceof C.TFolder))continue;let s=(0,C.normalizePath)(`${e.path}/skill.md`),a=this.vault.getAbstractFileByPath(s);if(!(a instanceof C.TFile))continue;let n=await this.loadFolderSkill(e.path,a);n&&this.skills.set(s,n)}}async loadFolderSkill(t,e){let s=await this.vault.cachedRead(e),{frontmatter:a,body:n}=ne(s),i=M(a.name);if(!i)return this.setIssue(e.path,"Folder skill skill.md requires string field `name`."),null;let o=async c=>{let l=(0,C.normalizePath)(`${t}/${c}`),d=this.vault.getAbstractFileByPath(l);if(!(d instanceof C.TFile))return"";let h=await this.vault.cachedRead(d);return ne(h).body};return{filePath:e.path,name:i,description:M(a.description),tags:ve(a.tags),body:n,toolsBody:await o("tools.md"),referencesBody:await o("references.md"),examplesBody:await o("examples.md"),isFolder:!0}}async loadFolderAgents(){let t=this.vault.getAbstractFileByPath(this.getSubfolder("agents"));if(t instanceof C.TFolder)for(let e of t.children){if(!(e instanceof C.TFolder))continue;let s=(0,C.normalizePath)(`${e.path}/agent.md`),a=this.vault.getAbstractFileByPath(s);if(!(a instanceof C.TFile))continue;let n=await this.loadFolderAgent(e.path,a);n&&this.agents.set(s,n)}}async loadFolderAgent(t,e){let s=await this.vault.cachedRead(e),{frontmatter:a,body:n}=ne(s),i=M(a.name);if(!i)return this.setIssue(e.path,"Folder agent agent.md requires string field `name`."),null;let o={},c=(0,C.normalizePath)(`${t}/config.md`),l=this.vault.getAbstractFileByPath(c);if(l instanceof C.TFile){let B=await this.vault.cachedRead(l);o=ne(B).frontmatter}let d={allow:[],deny:[]},h=(0,C.normalizePath)(`${t}/permissions.json`),u=this.vault.getAbstractFileByPath(h);if(u instanceof C.TFile)try{let B=await this.vault.cachedRead(u),I=JSON.parse(B);d={allow:ve(I.allow),deny:ve(I.deny)}}catch{}if(d.allow.length===0&&d.deny.length===0){let B=ve(o.allowed_tools),I=ve(o.blocked_tools);(B.length>0||I.length>0)&&(d={allow:B,deny:I},this.warnedLegacyPerms.has(i)||(this.warnedLegacyPerms.add(i),console.warn(`Agent Fleet: "${i}" still uses legacy allowed_tools/blocked_tools in config.md. Permission rules now live in permissions.json. Open this agent in Edit and Save to migrate.`)))}let p="",m=(0,C.normalizePath)(`${t}/SKILLS.md`),f=this.vault.getAbstractFileByPath(m);if(f instanceof C.TFile){let B=await this.vault.cachedRead(f);p=ne(B).body}let y="",k=(0,C.normalizePath)(`${t}/CONTEXT.md`),b=this.vault.getAbstractFileByPath(k);if(b instanceof C.TFile){let B=await this.vault.cachedRead(b);y=ne(B).body}let v=!1,g="",x="",T=!0,_="",R=(0,C.normalizePath)(`${t}/HEARTBEAT.md`),A=this.vault.getAbstractFileByPath(R);if(A instanceof C.TFile){let B=await this.vault.cachedRead(A),I=ne(B);v=Je(I.frontmatter.enabled,!1),g=M(I.frontmatter.schedule)??"",T=Je(I.frontmatter.notify,!0),_=M(I.frontmatter.channel)??"",x=I.body}let S=M(a.model),L=M(o.model);S&&L&&S!==L&&(this.warnedFolderAgentModelConflict.has(i)||(this.warnedFolderAgentModelConflict.add(i),console.warn(`Agent Fleet: "${i}" has conflicting model fields \u2014 agent.md says "${S}", config.md says "${L}". config.md wins. Remove agent.md's model field or sync the values to silence this warning.`)));let E=L??S??this.settings.defaultModel;return{filePath:e.path,name:i,description:M(a.description),model:E,adapter:M(o.adapter)??"claude-code",permissionMode:M(o.permission_mode)??"bypassPermissions",effort:M(o.effort),maxRetries:$e(o.max_retries,1),skills:ve(a.skills),mcpServers:ve(a.mcp_servers),cwd:M(o.cwd)||M(a.cwd),enabled:Je(a.enabled,!0),timeout:$e(o.timeout,$e(a.timeout,300)),approvalRequired:ve(o.approval_required),memory:Je(o.memory,Je(a.memory,!1)),memoryMaxEntries:$e(o.memory_max_entries,100),autoCompactThreshold:$e(o.auto_compact_threshold??a.auto_compact_threshold,85),tags:ve(a.tags),avatar:M(a.avatar)??"",body:n,contextBody:y,skillsBody:p,env:this.parseEnvMap(o.env),permissionRules:d,isFolder:!0,heartbeatEnabled:v,heartbeatSchedule:g,heartbeatBody:x,heartbeatNotify:T,heartbeatChannel:_,wikiKeeper:this.parseWikiKeeperConfig(o.wiki_keeper??a.wiki_keeper),wikiReferences:this.parseWikiReferences(o.wiki_references??a.wiki_references)}}parseWikiReferences(t){if(!Array.isArray(t))return;let e=[];for(let s of t)if(typeof s=="string"&&s.trim())e.push({agent:s.trim()});else if(s&&typeof s=="object"){let a=s.agent;typeof a=="string"&&a.trim()&&e.push({agent:a.trim()})}return e.length>0?e:void 0}parseWikiKeeperConfig(t){if(!t||typeof t!="object")return;let e=t;return{scopeRoot:M(e.scope_root)??"",inboxPath:M(e.inbox_path)??"_sources/inbox",archivePath:M(e.archive_path)??"_sources/archive",failedPath:M(e.failed_path)??"_sources/failed",topicsRoot:M(e.topics_root)??"_topics",indexPath:M(e.index_path)??"index.md",logPath:M(e.log_path)??"log.md",watchedFolders:ve(e.watched_folders),excludePatterns:ve(e.exclude_patterns),watchedSince:M(e.watched_since)??"",fileSubstantiveAnswers:Je(e.file_substantive_answers,!0),obsidianUrlScheme:Je(e.obsidian_url_scheme,!0),maxTokensPerIngest:$e(e.max_tokens_per_ingest,6e4),maxTokensPerRefresh:$e(e.max_tokens_per_refresh,3e4),indexSplitThreshold:$e(e.index_split_threshold,100),dedupSimilarityThreshold:$e(e.dedup_similarity_threshold,.82),summaryStaleDays:$e(e.summary_stale_days,30),stateFile:M(e.state_file)??".wiki-keeper-state.json"}}removeFile(t){this.clearStoredFile(t)}getSnapshot(){return{agents:Array.from(this.agents.values()).sort((t,e)=>t.name.localeCompare(e.name)),skills:Array.from(this.skills.values()).sort((t,e)=>t.name.localeCompare(e.name)),tasks:Array.from(this.tasks.values()).sort((t,e)=>t.taskId.localeCompare(e.taskId)),channels:Array.from(this.channels.values()).sort((t,e)=>t.name.localeCompare(e.name)),validationIssues:Array.from(this.validationIssues.values()).flat()}}getAgentByName(t){return Array.from(this.agents.values()).find(e=>e.name===t)}getSkillByName(t){return Array.from(this.skills.values()).find(e=>e.name===t)}getTaskById(t){return Array.from(this.tasks.values()).find(e=>e.taskId===t)}getTasksForAgent(t){return Array.from(this.tasks.values()).filter(e=>e.agent===t)}getChannelByName(t){return Array.from(this.channels.values()).find(e=>e.name===t)}getChannelsForAgent(t){return Array.from(this.channels.values()).filter(e=>e.defaultAgent===t)}getRunsRoot(){return this.getSubfolder("runs")}getMemoryPath(t){return(0,C.normalizePath)(`${this.getSubfolder("memory")}/${fe(t)}.md`)}getConversationsDir(t){if(t.isFolder){let s=t.filePath.replace(/\/agent\.md$/,"");return(0,C.normalizePath)(`${s}/conversations`)}let e=this.getMemoryPath(t.name).replace(/\/[^/]+$/,"");return(0,C.normalizePath)(`${e}/${t.name}-conversations`)}getLegacyChatPath(t){if(t.isFolder){let s=t.filePath.replace(/\/agent\.md$/,"");return(0,C.normalizePath)(`${s}/chat.json`)}let e=this.getMemoryPath(t.name).replace(/\/[^/]+$/,"");return(0,C.normalizePath)(`${e}/${t.name}-chat.json`)}getConversationPath(t,e){if(e==="default")return this.getLegacyChatPath(t);let s=this.getConversationsDir(t),a=fe(e)||"conversation";return(0,C.normalizePath)(`${s}/${a}.json`)}async listConversations(t){let e=[],s=this.getLegacyChatPath(t),a=this.vault.getAbstractFileByPath(s);if(a instanceof C.TFile){let o=await this.readConversationMeta(a,"default",!0);o&&e.push(o)}let n=this.getConversationsDir(t),i=this.vault.getAbstractFileByPath(n);if(i instanceof C.TFolder)for(let o of i.children){if(!(o instanceof C.TFile)||o.extension!=="json")continue;let c=o.basename,l=await this.readConversationMeta(o,c,!1);l&&e.push(l)}return e.sort((o,c)=>c.lastActive.localeCompare(o.lastActive)),e}async readConversationMeta(t,e,s){try{let a=await this.vault.cachedRead(t),n=JSON.parse(a);return{id:e,name:n.name?.trim()||(s?"Main chat":"Untitled"),lastActive:n.lastActive??new Date(t.stat.mtime).toISOString(),messageCount:Array.isArray(n.messages)?n.messages.length:0,isLegacy:s}}catch{return null}}async createConversation(t,e,s){if(e==="default")throw new Error('Cannot create a conversation with the reserved id "default"');let a=this.getConversationPath(t,e);if(this.vault.getAbstractFileByPath(a)instanceof C.TFile)return;let i=a.replace(/\/[^/]+$/,"");await this.ensureFolder(i);let o=new Date().toISOString(),c={sessionId:null,messages:[],lastActive:o,createdAt:o,name:s.trim()};await this.vault.create(a,JSON.stringify(c,null,2))}async renameConversation(t,e,s){let a=this.getConversationPath(t,e),n=this.vault.getAbstractFileByPath(a);if(!(n instanceof C.TFile))return;let i=await this.vault.cachedRead(n),o;try{o=JSON.parse(i)}catch{return}o.name=s.trim(),await this.vault.modify(n,JSON.stringify(o,null,2))}async deleteConversation(t,e){let s=this.getConversationPath(t,e),a=this.vault.getAbstractFileByPath(s);a instanceof C.TFile&&await this.vault.delete(a);let n=s.replace(/\.json$/,".threads"),i=this.vault.getAbstractFileByPath(n);if(i instanceof C.TFolder&&await this.vault.delete(i,!0),e!=="default"){let o=this.getConversationsDir(t),c=this.vault.getAbstractFileByPath(o);c instanceof C.TFolder&&c.children.length===0&&await this.vault.delete(c,!0)}}async getMemory(t){let e=this.getMemoryPath(t),s=this.vault.getAbstractFileByPath(e);if(!(s instanceof C.TFile))return null;let a=await this.vault.cachedRead(s),{frontmatter:n,body:i}=ne(a);return{filePath:e,agent:M(n.agent)??t,lastUpdated:M(n.last_updated),body:i}}async appendMemory(t,e){if(e.length===0)return;let s=this.getMemoryPath(t),a=this.vault.getAbstractFileByPath(s),n=new Date().toISOString(),i=e.map(o=>`- ${o.trim()}`).join(` `);if(a instanceof C.TFile){let c=`${(await this.getMemory(t))?.body.trim()||"## Learned Context"} ${i}`.trim();await this.vault.modify(a,J({agent:t,last_updated:n},c));return}await this.createFileIfMissing(s,J({agent:t,last_updated:n},`## Learned Context @@ -11796,7 +11796,7 @@ ${(s??e.body).trim()}`),a.filter(Boolean).join(` ## Task ${n}`,this.basePromptSent=!0),await this.ensureProcess();let i=this.needsCompactBeforeNextTurn;i&&(this.needsCompactBeforeNextTurn=!1,this.lastCompactTriggerAt=Date.now()),this.stats.lastCompact&&(this.stats.lastCompact=void 0,this.emitStats()),this.activeOnEvent=e,this.turnResponseText="",this.turnToolCalls=[],this.pendingTurns=i?2:1,this.setStreaming(!0),this.armWatchdog();let o=c=>{let l=JSON.stringify({type:"user",message:{role:"user",content:c}});this.process.stdin.write(l+` `)};try{i&&o("/compact"),o(n)}catch(c){throw this.pendingTurns=0,this.setStreaming(!1),new Error(`Failed to write to Claude process stdin: ${c instanceof Error?c.message:String(c)}`)}return new Promise((c,l)=>{this.turnResolve=c,this.turnReject=l})}scheduleCompact(){this.needsCompactBeforeNextTurn=!0}injectMessage(t,e,s){if(!this.process||!this.isProcessAlive)return;this.messages.push({id:Fs(),role:"user",content:t,timestamp:new Date().toISOString(),attachments:s&&s.length>0?s:void 0});let n=JSON.stringify({type:"user",message:{role:"user",content:e??t}});try{this.process.stdin.write(n+` -`)}catch(i){console.warn("Agent Fleet: injectMessage stdin write failed",i);return}this.pendingTurns++}abort(){this.detachProcessListeners(),this.process&&(this.process.kill(),this.process=null),this.isProcessAlive=!1,this.stdoutBuffer="",this.turnResponseText="",this.turnToolCalls=[],this.pendingTurns=0,this.clearWatchdog(),this.setStreaming(!1),Et(this.settingsState),this.settingsState=null;let t=this.turnReject;this.turnResolve=null,this.turnReject=null,t?.(new Error("Aborted"))}hibernate(){this.isStreaming||this.pendingTurns>0||(this.detachProcessListeners(),this.process&&(this.process.kill(),this.process=null),this.isProcessAlive=!1,this.stdoutBuffer="",Et(this.settingsState),this.settingsState=null)}clearSessionId(){this.claudeSessionId=null,this.basePromptSent=!1}async buildBasePrompt(){let t=[this.agent.body.trim()];for(let s of this.agent.skills){let a=this.repository.getSkillByName(s);if(a){let n=[a.body.trim()];a.toolsBody.trim()&&n.push(`### Tools +`)}catch(i){console.warn("Agent Fleet: injectMessage stdin write failed",i);return}this.pendingTurns++}abort(){this.detachProcessListeners(),this.process&&(this.process.kill(),this.process=null),this.isProcessAlive=!1,this.stdoutBuffer="",this.turnResponseText="",this.turnToolCalls=[],this.pendingTurns=0,this.clearWatchdog(),this.setStreaming(!1),Et(this.settingsState),this.settingsState=null;let t=this.turnReject;this.turnResolve=null,this.turnReject=null,t?.(new Error("Aborted"))}dispose(){for(let t of this.threads.values())t.abort();this.threads.clear(),this.threadIndex={},this.abort()}hibernate(){this.isStreaming||this.pendingTurns>0||(this.detachProcessListeners(),this.process&&(this.process.kill(),this.process=null),this.isProcessAlive=!1,this.stdoutBuffer="",Et(this.settingsState),this.settingsState=null)}clearSessionId(){this.claudeSessionId=null,this.basePromptSent=!1}async buildBasePrompt(){let t=[this.agent.body.trim()];for(let s of this.agent.skills){let a=this.repository.getSkillByName(s);if(a){let n=[a.body.trim()];a.toolsBody.trim()&&n.push(`### Tools ${a.toolsBody.trim()}`),a.referencesBody.trim()&&n.push(`### References ${a.referencesBody.trim()}`),a.examplesBody.trim()&&n.push(`### Examples ${a.examplesBody.trim()}`),t.push(`## Skill: ${a.name} @@ -11924,9 +11924,9 @@ DEBUG=true`,rows:"3"}});_.addEventListener("input",()=>{c.envVars=_.value});let Includes the agent's system prompt, attached skills, tool schemas, memory, and prior turns. A fresh chat starts non-zero because all of that is loaded on turn 1. -To get 1M context on Opus, set the agent's model to claude-opus-4-7[1m] (or us.anthropic.claude-opus-4-7[1m] on Bedrock).`}if(e.lastCompact){let{preTokens:a,postTokens:n}=e.lastCompact,i=s.createSpan({cls:"af-chat-stats-compact"});i.setText(`compacted ${Jt(a)} \u2192 ${Jt(n)}`),i.title=`Conversation was summarized to free up context. ${Jt(a)} tokens reduced to ${Jt(n)}.`}}populateAgentDropdown(){let e=this.plugin.runtime.getSnapshot().agents,s=this.agentSelect.value;if(this.agentSelect.empty(),e.length===0){let n=this.agentSelect.createEl("option",{text:"No agents available",attr:{value:"",disabled:"true"}});n.selected=!0,this.textarea.disabled=!0,this.showEmptyState();return}if(!this.selectedAgentName){let n=this.agentSelect.createEl("option",{text:"Select agent\u2026",attr:{value:"",disabled:"true"}});n.selected=!0}for(let n of e){let i=n.avatar?.trim(),o=i&&!/^[a-z][a-z0-9-]*$/.test(i)?`${i} `:"";this.agentSelect.createEl("option",{text:`${o}${n.name}`,attr:{value:n.name}})}if(this.selectedAgentName&&e.some(n=>n.name===this.selectedAgentName))this.agentSelect.value=this.selectedAgentName,this.textarea.disabled=!1;else if(s&&e.some(n=>n.name===s))this.agentSelect.value=s,this.selectedAgentName=s,this.textarea.disabled=!1;else{this.selectedAgentName=null,this.leaf.updateHeader(),this.textarea.disabled=!0,this.textarea.placeholder="Select an agent to start chatting\u2026",this.showEmptyState();return}this.leaf.updateHeader(),this.textarea.placeholder="Message the agent\u2026 (Ctrl+Enter to send)";let a=this.messagesInner.querySelector(".af-chat-bubble")!==null;this.selectedAgentName&&!a&&this.switchToAgent(this.selectedAgentName)}showEmptyState(){this.messagesInner.empty();let e=this.messagesInner.createDiv({cls:"af-chat-view-empty"}),s=e.createDiv({cls:"af-chat-view-empty-icon"});this.plugin.runtime.getSnapshot().agents.length===0?((0,V.setIcon)(s,"bot"),e.createDiv({cls:"af-chat-view-empty-text",text:"No agents available"}),e.createDiv({cls:"af-chat-view-empty-hint",text:"Create an agent to start chatting"})):((0,V.setIcon)(s,"message-circle"),e.createDiv({cls:"af-chat-view-empty-text",text:"Select an agent to start"}),e.createDiv({cls:"af-chat-view-empty-hint",text:"Choose an agent from the dropdown above"}))}async switchToAgent(e,s=ht){let n=this.plugin.runtime.getSnapshot().agents.find(l=>l.name===e);if(!n)return;this.selectedAgentName=e,this.selectedConversationId=s,this.leaf.updateHeader(),this.activityEl=null,this.streamingDot=null,this.messagesInner.empty(),this.threadExpanded.clear(),await this.refreshConversationsList(n);let i=oa(e,s),o=this.sessions.get(i);if(!o){let l=new Ut(n,this.plugin.settings,this.plugin.repository,this.app.vault,{inAppConversationId:s});o={session:l},this.sessions.set(i,o),await l.loadPersistedState()}this.setStatsSource(o.session);for(let l of o.session.messages)if(l.role==="user")this.addBubble("user",l.content,l.attachments);else{let d=this.addBubble("assistant");if(this.renderMarkdownBubble(d,l.content),d._setRawText?.(l.content),this.attachThreadAffordance(d,l.id,o.session),l.toolCalls&&l.toolCalls.length>0){let h=this.getOrCreateAffordancesRow(d);this.buildToolSummary(l.toolCalls,h)}}this.activityUnsub?.();let c=o.session;this.activityUnsub=c.onActivityChange(()=>{this.getCurrentSession()?.session===c&&this.renderIndicators(c)}),this.textarea.disabled=!1,this.textarea.focus()}getCurrentSession(){if(this.selectedAgentName)return this.sessions.get(oa(this.selectedAgentName,this.selectedConversationId))}async refreshConversationsList(e){this.conversationsCache=await this.plugin.repository.listConversations(e),this.conversationsCache.length===0&&this.conversationsCache.push({id:ht,name:"Main chat",lastActive:new Date().toISOString(),messageCount:0,isLegacy:!0}),this.renderConvoPanel(),this.leaf.updateHeader()}renderConvoPanel(){if(!this.convoPanelEl||(this.convoPanelEl.empty(),!this.selectedAgentName))return;this.convoPanelEl.createDiv({cls:"af-chat-convo-header",text:"CONVERSATIONS"});let e=this.convoPanelEl.createDiv({cls:"af-chat-convo-new"}),s=e.createSpan({cls:"af-chat-convo-new-icon"});(0,V.setIcon)(s,"plus"),e.createSpan({cls:"af-chat-convo-new-label",text:"New chat"}),e.onclick=()=>void this.handleNewChat();let a=this.convoPanelEl.createDiv({cls:"af-chat-convo-list"});for(let n of this.conversationsCache)this.renderConvoRow(a,n)}renderConvoRow(e,s){let a=s.id===this.selectedConversationId,n=e.createDiv({cls:`af-chat-convo-item${a?" active":""}`}),i=n.createDiv({cls:"af-chat-convo-name",text:s.name});i.title=s.name,i.ondblclick=l=>{l.stopPropagation(),this.beginInlineRename(i,s)};let o=n.createDiv({cls:"af-chat-convo-meta"}),c=s.messageCount===1?"1 msg":`${s.messageCount} msgs`;if(o.appendText(`${c} \xB7 ${ml(s.lastActive)}`),!s.isLegacy){let l=n.createEl("button",{cls:"af-chat-convo-trash",attr:{title:"Delete conversation","aria-label":"Delete conversation"}});(0,V.setIcon)(l,"trash-2"),l.onclick=d=>{d.stopPropagation(),this.handleDeleteConversation(s)}}n.onclick=()=>{s.id!==this.selectedConversationId&&this.handleConvoSelected(s.id)}}beginInlineRename(e,s){let a=s.name,n=document.createElement("input");n.type="text",n.value=a,n.className="af-chat-convo-name-input",e.replaceWith(n),n.focus(),n.select();let i=!1,o=l=>{let d=document.createElement("div");return d.className="af-chat-convo-name",d.textContent=l??a,d.title=l??a,d.ondblclick=h=>{h.stopPropagation();let u=this.conversationsCache.find(p=>p.id===s.id)??s;this.beginInlineRename(d,u)},n.replaceWith(d),d},c=async()=>{if(i)return;i=!0;let l=n.value.trim();if(!l||l===a){o(null);return}try{await this.saveConversationName(s.id,l)}catch(d){new V.Notice(`Couldn't rename: ${d instanceof Error?d.message:String(d)}`),o(null);return}this.renderConvoPanel(),this.leaf.updateHeader()};n.addEventListener("keydown",l=>{l.key==="Enter"?(l.preventDefault(),c()):l.key==="Escape"&&(l.preventDefault(),i=!0,o(null))}),n.addEventListener("blur",()=>{c()})}async saveConversationName(e,s){if(!this.selectedAgentName)return;let n=this.plugin.runtime.getSnapshot().agents.find(l=>l.name===this.selectedAgentName);if(!n)return;try{await this.plugin.repository.renameConversation(n,e,s)}catch{}let i=oa(this.selectedAgentName,e),o=this.sessions.get(i);o&&await o.session.setConversationName(s);let c=this.conversationsCache.findIndex(l=>l.id===e);c>=0&&(this.conversationsCache[c]={...this.conversationsCache[c],name:s})}async handleConvoSelected(e){if(!this.selectedAgentName)return;let s=this.plugin.app.workspace.getLeavesOfType(st);for(let a of s)if(a.view!==this&&a.view instanceof r&&a.view.selectedAgentName===this.selectedAgentName&&a.view.selectedConversationId===e){this.plugin.app.workspace.revealLeaf(a);return}await this.switchToAgent(this.selectedAgentName,e)}async handleDeleteConversation(e){if(!this.selectedAgentName||e.isLegacy)return;let a=this.plugin.runtime.getSnapshot().agents.find(o=>o.name===this.selectedAgentName);if(!a||!window.confirm(`Delete conversation "${e.name}"? +To get 1M context on Opus, set the agent's model to claude-opus-4-7[1m] (or us.anthropic.claude-opus-4-7[1m] on Bedrock).`}if(e.lastCompact){let{preTokens:a,postTokens:n}=e.lastCompact,i=s.createSpan({cls:"af-chat-stats-compact"});i.setText(`compacted ${Jt(a)} \u2192 ${Jt(n)}`),i.title=`Conversation was summarized to free up context. ${Jt(a)} tokens reduced to ${Jt(n)}.`}}populateAgentDropdown(){let e=this.plugin.runtime.getSnapshot().agents,s=this.agentSelect.value;if(this.agentSelect.empty(),e.length===0){let n=this.agentSelect.createEl("option",{text:"No agents available",attr:{value:"",disabled:"true"}});n.selected=!0,this.textarea.disabled=!0,this.showEmptyState();return}if(!this.selectedAgentName){let n=this.agentSelect.createEl("option",{text:"Select agent\u2026",attr:{value:"",disabled:"true"}});n.selected=!0}for(let n of e){let i=n.avatar?.trim(),o=i&&!/^[a-z][a-z0-9-]*$/.test(i)?`${i} `:"";this.agentSelect.createEl("option",{text:`${o}${n.name}`,attr:{value:n.name}})}if(this.selectedAgentName&&e.some(n=>n.name===this.selectedAgentName))this.agentSelect.value=this.selectedAgentName,this.textarea.disabled=!1;else if(s&&e.some(n=>n.name===s))this.agentSelect.value=s,this.selectedAgentName=s,this.textarea.disabled=!1;else{this.selectedAgentName=null,this.leaf.updateHeader(),this.textarea.disabled=!0,this.textarea.placeholder="Select an agent to start chatting\u2026",this.showEmptyState();return}this.leaf.updateHeader(),this.textarea.placeholder="Message the agent\u2026 (Ctrl+Enter to send)";let a=this.messagesInner.querySelector(".af-chat-bubble")!==null;this.selectedAgentName&&!a&&this.switchToAgent(this.selectedAgentName,this.selectedConversationId)}showEmptyState(){this.messagesInner.empty();let e=this.messagesInner.createDiv({cls:"af-chat-view-empty"}),s=e.createDiv({cls:"af-chat-view-empty-icon"});this.plugin.runtime.getSnapshot().agents.length===0?((0,V.setIcon)(s,"bot"),e.createDiv({cls:"af-chat-view-empty-text",text:"No agents available"}),e.createDiv({cls:"af-chat-view-empty-hint",text:"Create an agent to start chatting"})):((0,V.setIcon)(s,"message-circle"),e.createDiv({cls:"af-chat-view-empty-text",text:"Select an agent to start"}),e.createDiv({cls:"af-chat-view-empty-hint",text:"Choose an agent from the dropdown above"}))}async switchToAgent(e,s=ht){let n=this.plugin.runtime.getSnapshot().agents.find(l=>l.name===e);if(!n)return;this.selectedAgentName=e,this.selectedConversationId=s,this.leaf.updateHeader(),this.activityEl=null,this.streamingDot=null,this.messagesInner.empty(),this.threadExpanded.clear(),await this.refreshConversationsList(n);let i=oa(e,s),o=this.sessions.get(i);if(!o){let l=new Ut(n,this.plugin.settings,this.plugin.repository,this.app.vault,{inAppConversationId:s});o={session:l},this.sessions.set(i,o),await l.loadPersistedState()}this.setStatsSource(o.session);for(let l of o.session.messages)if(l.role==="user")this.addBubble("user",l.content,l.attachments);else{let d=this.addBubble("assistant");if(this.renderMarkdownBubble(d,l.content),d._setRawText?.(l.content),this.attachThreadAffordance(d,l.id,o.session),l.toolCalls&&l.toolCalls.length>0){let h=this.getOrCreateAffordancesRow(d);this.buildToolSummary(l.toolCalls,h)}}this.activityUnsub?.();let c=o.session;this.activityUnsub=c.onActivityChange(()=>{this.getCurrentSession()?.session===c&&this.renderIndicators(c)}),this.textarea.disabled=!1,this.textarea.focus()}getCurrentSession(){if(this.selectedAgentName)return this.sessions.get(oa(this.selectedAgentName,this.selectedConversationId))}async refreshConversationsList(e){this.conversationsCache=await this.plugin.repository.listConversations(e),this.conversationsCache.length===0&&this.conversationsCache.push({id:ht,name:"Main chat",lastActive:new Date().toISOString(),messageCount:0,isLegacy:!0}),this.renderConvoPanel(),this.leaf.updateHeader()}renderConvoPanel(){if(!this.convoPanelEl||(this.convoPanelEl.empty(),!this.selectedAgentName))return;this.convoPanelEl.createDiv({cls:"af-chat-convo-header",text:"CONVERSATIONS"});let e=this.convoPanelEl.createDiv({cls:"af-chat-convo-new"}),s=e.createSpan({cls:"af-chat-convo-new-icon"});(0,V.setIcon)(s,"plus"),e.createSpan({cls:"af-chat-convo-new-label",text:"New chat"}),e.onclick=()=>void this.handleNewChat();let a=this.convoPanelEl.createDiv({cls:"af-chat-convo-list"});for(let n of this.conversationsCache)this.renderConvoRow(a,n)}renderConvoRow(e,s){let a=s.id===this.selectedConversationId,n=e.createDiv({cls:`af-chat-convo-item${a?" active":""}`}),i=n.createDiv({cls:"af-chat-convo-name",text:s.name});i.title=s.name,i.ondblclick=d=>{d.stopPropagation(),this.beginInlineRename(i,s)};let o=n.createDiv({cls:"af-chat-convo-meta"}),c=s.messageCount===1?"1 msg":`${s.messageCount} msgs`;o.appendText(`${c} \xB7 ${ml(s.lastActive)}`);let l=n.createEl("button",{cls:"af-chat-convo-trash",attr:{title:"Delete conversation","aria-label":"Delete conversation"}});(0,V.setIcon)(l,"trash-2"),l.onclick=d=>{d.stopPropagation(),this.handleDeleteConversation(s)},n.onclick=()=>{s.id!==this.selectedConversationId&&this.handleConvoSelected(s.id)}}beginInlineRename(e,s){let a=s.name,n=document.createElement("input");n.type="text",n.value=a,n.className="af-chat-convo-name-input",e.replaceWith(n),n.focus(),n.select();let i=!1,o=l=>{let d=document.createElement("div");return d.className="af-chat-convo-name",d.textContent=l??a,d.title=l??a,d.ondblclick=h=>{h.stopPropagation();let u=this.conversationsCache.find(p=>p.id===s.id)??s;this.beginInlineRename(d,u)},n.replaceWith(d),d},c=async()=>{if(i)return;i=!0;let l=n.value.trim();if(!l||l===a){o(null);return}try{await this.saveConversationName(s.id,l)}catch(d){new V.Notice(`Couldn't rename: ${d instanceof Error?d.message:String(d)}`),o(null);return}this.renderConvoPanel(),this.leaf.updateHeader()};n.addEventListener("keydown",l=>{l.key==="Enter"?(l.preventDefault(),c()):l.key==="Escape"&&(l.preventDefault(),i=!0,o(null))}),n.addEventListener("blur",()=>{c()})}async saveConversationName(e,s){if(!this.selectedAgentName)return;let n=this.plugin.runtime.getSnapshot().agents.find(l=>l.name===this.selectedAgentName);if(!n)return;try{await this.plugin.repository.renameConversation(n,e,s)}catch{}let i=oa(this.selectedAgentName,e),o=this.sessions.get(i);o&&await o.session.setConversationName(s);let c=this.conversationsCache.findIndex(l=>l.id===e);c>=0&&(this.conversationsCache[c]={...this.conversationsCache[c],name:s})}async handleConvoSelected(e){if(!this.selectedAgentName)return;let s=this.plugin.app.workspace.getLeavesOfType(st);for(let a of s)if(a.view!==this&&a.view instanceof r&&a.view.selectedAgentName===this.selectedAgentName&&a.view.selectedConversationId===e){this.plugin.app.workspace.revealLeaf(a);return}await this.switchToAgent(this.selectedAgentName,e)}async handleDeleteConversation(e){if(!this.selectedAgentName)return;let a=this.plugin.runtime.getSnapshot().agents.find(o=>o.name===this.selectedAgentName);if(!a||!window.confirm(`Delete conversation "${e.name}"? -This removes its message history. The agent and its other conversations are untouched.`))return;let i=oa(this.selectedAgentName,e.id);this.sessions.get(i)?.session.abort(),this.sessions.delete(i);try{await this.plugin.repository.deleteConversation(a,e.id)}catch(o){new V.Notice(`Couldn't delete: ${o instanceof Error?o.message:String(o)}`);return}if(await this.refreshConversationsList(a),e.id===this.selectedConversationId){let o=this.conversationsCache[0]?.id??ht;await this.switchToAgent(this.selectedAgentName,o)}}toggleConvoPanel(){this.convoPanelCollapsed=!this.convoPanelCollapsed,this.userToggledCollapse=!0,this.applyCollapsedClass(),this.leaf.updateHeader()}applyCollapsedClass(){this.convoPanelEl&&(this.convoPanelCollapsed?this.convoPanelEl.addClass("collapsed"):this.convoPanelEl.removeClass("collapsed")),this.collapseBtn&&(this.collapseBtn.empty(),(0,V.setIcon)(this.collapseBtn,this.convoPanelCollapsed?"panel-left-open":"panel-left-close"))}renderMarkdownBubble(e,s){let a=e.querySelector(".af-chat-copy-btn"),n=a?.parentNode?.removeChild(a)??null;e.empty(),e.addClass("af-compact-md"),V.MarkdownRenderer.render(this.app,s,e,"",this.plugin).then(()=>{n&&e.appendChild(n),this.wireBubbleLinks(e),e.querySelectorAll("pre").forEach(i=>{i.querySelector(".copy-code-button")?.remove();let o=i.querySelector("code");if(!o)return;let c=document.createElement("button");c.className="af-code-copy-btn",c.setAttribute("aria-label","Copy code"),(0,V.setIcon)(c,"copy"),c.onclick=l=>{l.stopPropagation(),navigator.clipboard.writeText(o.textContent??"").then(()=>{c.addClass("copied"),(0,V.setIcon)(c,"check"),setTimeout(()=>{c.removeClass("copied"),(0,V.setIcon)(c,"copy")},1500)})},i.style.position="relative",i.appendChild(c)})})}wireBubbleLinks(e){e.addEventListener("click",s=>{let a=s.target.closest("a");if(!a)return;if(a.classList.contains("internal-link")){s.preventDefault(),s.stopPropagation();let i=a.getAttribute("data-href")||a.getAttribute("href")||a.textContent||"";if(!i)return;let o=s.shiftKey?"split":"tab";this.app.workspace.openLinkText(i,"",o);return}let n=a.getAttribute("href")||"";if(n.startsWith("obsidian://")){s.preventDefault(),s.stopPropagation(),window.location.href=n;return}if(a.classList.contains("external-link")||/^https?:\/\//.test(n)){s.preventDefault(),s.stopPropagation(),window.open(n,"_blank");return}})}addCopyBtn(e,s){let a=e.createEl("button",{cls:"af-chat-copy-btn",attr:{"aria-label":"Copy message"}});(0,V.setIcon)(a,"copy"),a.onclick=n=>{n.stopPropagation(),navigator.clipboard.writeText(s()).then(()=>{a.addClass("copied"),(0,V.setIcon)(a,"check"),setTimeout(()=>{a.removeClass("copied"),(0,V.setIcon)(a,"copy")},1500)})}}addBubble(e,s,a){if(e==="user"&&a&&a.length>0){let i=this.messagesInner.createDiv({cls:"af-chat-bubble-attachments"});for(let o of a){let c=i.createSpan({cls:"af-chat-pill af-chat-pill-inline"}),l=c.createSpan({cls:"af-chat-pill-icon"}),d=/\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(o);(0,V.setIcon)(l,d?"image":"file-text"),c.createSpan({cls:"af-chat-pill-name",text:o})}}let n=this.messagesInner.createDiv({cls:`af-chat-bubble af-chat-bubble-${e}`});if(s&&(e==="assistant"?this.renderMarkdownBubble(n,s):n.setText(s)),e==="assistant"){let i=s??"";this.addCopyBtn(n,()=>i),n._setRawText=o=>{i=o}}return this.messagesEl.scrollTop=this.messagesEl.scrollHeight,n}getOrCreateAffordancesRow(e){let s=e.parentElement;if(!s)throw new Error("bubble has no parent");let a=e.nextElementSibling;if(a&&a.classList.contains("af-chat-affordances"))return a;let n=document.createElement("div");return n.className="af-chat-affordances",s.insertBefore(n,e.nextSibling),n}attachThreadAffordance(e,s,a){let n=e.parentElement;if(!n)return;let i=this.getOrCreateAffordancesRow(e);if(i.querySelector(".af-thread-badge"))return;let o=document.createElement("div");o.className="af-thread-badge",o.setAttribute("role","button"),o.setAttribute("tabindex","0"),i.appendChild(o),(0,V.setIcon)(o,"message-circle");let c=o.createSpan({cls:"af-thread-badge-label"}),l=document.createElement("div");l.className="af-thread-container",l.style.display="none",n.insertBefore(l,i.nextSibling);let d=()=>{let f=a.getThreadIndex()[s]?.messageCount??0;f<=0?c.setText("Thread"):c.setText(`${f} ${f===1?"reply":"replies"}`),f>0&&o.addClass("has-replies")};d();let h=!1,u=async()=>{if(this.threadExpanded.get(s)===!0){l.style.display="none",this.threadExpanded.set(s,!1),o.removeClass("expanded"),this.setStatsSource(a);return}if(this.threadExpanded.set(s,!0),l.style.display="",o.addClass("expanded"),!h)try{let m=await a.openOrCreateThread(s);this.renderThreadContainer(l,m,a,d),h=!0}catch(m){l.setText(`Failed to open thread: ${m instanceof Error?m.message:String(m)}`)}};o.onclick=()=>void u(),o.onkeydown=p=>{(p.key==="Enter"||p.key===" ")&&(p.preventDefault(),u())}}renderThreadContainer(e,s,a,n){e.empty();let i=e.createDiv({cls:"af-thread-wrap"}),o=i.createDiv({cls:"af-thread-messages"}),c=null,l=null,d=S=>{S?(c||(c=o.createDiv({cls:"af-chat-activity"})),c.setText(`Working\u2026 (${S})`)):c&&(c.remove(),c=null)},h=S=>{if(S&&!l){l=o.createDiv({cls:"af-chat-streaming-dot"});for(let L=0;L<3;L++)l.createSpan()}else!S&&l&&(l.remove(),l=null)},u=(S,L,E)=>{if(S==="user"&&E&&E.length>0){let I=o.createDiv({cls:"af-chat-bubble-attachments"});for(let q of E){let F=I.createSpan({cls:"af-chat-pill af-chat-pill-inline"}),$=F.createSpan({cls:"af-chat-pill-icon"});(0,V.setIcon)($,q.match(/\.(png|jpe?g|gif|webp|svg)$/i)?"image":"file-text"),F.createSpan({cls:"af-chat-pill-name",text:q})}}let B=o.createDiv({cls:`af-thread-bubble af-thread-bubble-${S}`});return S==="assistant"?this.renderMarkdownBubble(B,L):B.setText(L),B};for(let S of s.messages)u(S.role,S.content,S.attachments);let p=[],m=[],f=i.createDiv({cls:"af-thread-composer-wrap"}),y=f.createDiv({cls:"af-chat-pills-row af-thread-pills-row"});y.style.display="none";let k=()=>{if(y.empty(),p.length===0&&m.length===0){y.style.display="none";return}y.style.display="flex";for(let S of p){let L=y.createDiv({cls:"af-chat-pill"}),E=L.createSpan({cls:"af-chat-pill-icon"});(0,V.setIcon)(E,"file-text"),L.createSpan({cls:"af-chat-pill-name",text:S.name});let B=L.createSpan({cls:"af-chat-pill-remove"});(0,V.setIcon)(B,"x"),B.onclick=I=>{I.stopPropagation();let q=p.findIndex(F=>F.path===S.path);q>=0&&p.splice(q,1),k()}}for(let S of m){let L=y.createDiv({cls:"af-chat-pill"}),E=L.createSpan({cls:"af-chat-pill-icon"});(0,V.setIcon)(E,"image"),L.createSpan({cls:"af-chat-pill-name",text:S.name});let B=L.createSpan({cls:"af-chat-pill-remove"});(0,V.setIcon)(B,"x"),B.onclick=I=>{I.stopPropagation();let q=m.findIndex(F=>F.path===S.path);q>=0&&m.splice(q,1),k()}}},b=()=>{let S=this.app.workspace.getActiveFile();if(!S){new V.Notice("No active document to attach");return}if(p.some(E=>E.path===S.path)){new V.Notice(`"${S.name}" is already attached`);return}if(!new Set(["md","txt","json","yaml","yml","toml","csv","xml","html","css","js","ts","py","sh","sql","env","cfg","ini","log"]).has(S.extension.toLowerCase())){new V.Notice(`Can't attach "${S.name}" \u2014 only text files are supported`);return}p.push(S),k()},v=async S=>{let L=S.type.split("/")[1]?.replace("jpeg","jpg")??"png",E=S.name&&S.name!=="image"?S.name:`pasted-${Date.now()}.${L}`;if(m.some(I=>I.name===E)){new V.Notice(`"${E}" is already attached`);return}let B=await this.saveImageBlobToVault(S);B&&(m.push(B),k())},g=f.createDiv({cls:"af-chat-input-row af-thread-composer"}),x=g.createEl("button",{cls:"af-chat-attach-btn"});P(x,"plus","af-btn-icon"),x.title="Attach active document",x.onclick=S=>{S.preventDefault(),b()};let T=g.createEl("textarea",{cls:"af-chat-input af-thread-input",attr:{placeholder:"Message in thread\u2026 (Ctrl+Enter to send)",rows:"1"}});T.addEventListener("paste",S=>{let L=S.clipboardData?.items;if(L)for(let E=0;E{S.preventDefault(),S.stopPropagation(),f.addClass("af-chat-input-dragover")}),f.addEventListener("dragleave",()=>{f.removeClass("af-chat-input-dragover")}),f.addEventListener("drop",S=>{S.preventDefault(),S.stopPropagation(),f.removeClass("af-chat-input-dragover");let L=S.dataTransfer?.files;if(L)for(let E=0;E{T.style.height="auto",T.style.height=`${Math.min(T.scrollHeight,120)}px`,_.style.display=T.value.trim()?"flex":"none"};T.addEventListener("input",R),T.addEventListener("focus",()=>this.setStatsSource(s));let A=async()=>{let S=T.value.trim();if(!S||s.isStreaming)return;let L=await this.buildAttachmentContextFor(p,m),E=[...p.map(F=>F.name),...m.map(F=>F.name)],B=L?`${L}${S}`:void 0;T.value="",R(),p.length=0,m.length=0,k(),u("user",S,E.length>0?E:void 0),h(!0);let I=null,q="";try{await s.sendMessage(S,F=>{if(F.type==="text"){I||(h(!1),d(),I=u("assistant",""),I.empty()),q+=F.content;let $=I.querySelector(".af-chat-stream-text");$||($=I.createDiv({cls:"af-chat-stream-text"})),$.setText(q)}else F.type==="tool_use"?d(F.toolName):F.type==="result"&&(d(),h(!1),I&&this.renderMarkdownBubble(I,q))},B,E.length>0?E:void 0),n()}catch(F){h(!1),d();let $=F instanceof Error?F.message:String(F);o.createDiv({cls:"af-thread-error",text:`Error: ${$}`})}};_.onclick=()=>void A(),T.onkeydown=S=>{S.key==="Enter"&&(S.ctrlKey||S.metaKey)&&(S.preventDefault(),A())}}buildToolSummary(e,s){let n=(s??this.messagesInner).createDiv({cls:"af-chat-tool-summary"}),i=n.createEl("details"),o=i.createEl("summary"),c=new Map;for(let h of e){let u=c.get(h.name)??[];h.command&&u.push(h.command),c.set(h.name,u)}let l=o.createSpan({cls:"af-chat-tool-icon"});(0,V.setIcon)(l,"wrench"),o.appendText(` ${e.length} tool call${e.length!==1?"s":""}`);let d=i.createDiv({cls:"af-chat-tool-list"});for(let[h,u]of c){let p=u.length||(c.get(h)?.length??1),m=d.createDiv({cls:"af-chat-tool-item"}),f=p>1?`${h} (\xD7${p})`:h;m.createSpan({cls:"af-chat-tool-name",text:f}),u.length===1&&u[0]&&m.createSpan({cls:"af-chat-tool-cmd",text:u[0]})}return n}renderIndicators(e){let s=e.isStreaming,a=e.currentToolName,n=e.hasCurrentTurnText,i=!!this.messagesInner.querySelector(".af-chat-stream-text"),o=null;if(s&&a?o=`Working\u2026 (${a})`:s&&n&&!i&&(o="Replying\u2026"),o?(this.activityEl?(this.activityEl.parentElement!==this.messagesInner||this.activityEl.nextElementSibling!==null)&&this.messagesInner.appendChild(this.activityEl):this.activityEl=this.messagesInner.createDiv({cls:"af-chat-activity"}),this.activityEl.textContent!==o&&this.activityEl.setText(o)):this.activityEl&&(this.activityEl.remove(),this.activityEl=null),s&&!a&&!n)if(this.streamingDot)(this.streamingDot.parentElement!==this.messagesInner||this.streamingDot.nextElementSibling!==null)&&this.messagesInner.appendChild(this.streamingDot);else{this.streamingDot=this.messagesInner.createDiv({cls:"af-chat-streaming-dot"});for(let l=0;l<3;l++)this.streamingDot.createSpan()}else this.streamingDot&&(this.streamingDot.remove(),this.streamingDot=null);this.setAttachStopMode(s)}setAttachStopMode(e){e!==this.isInStopMode&&(this.isInStopMode=e,this.attachStopBtn.empty(),e?(P(this.attachStopBtn,"square","af-btn-icon"),this.attachStopBtn.title="Stop generation",this.attachStopBtn.addClass("af-chat-stop-mode")):(P(this.attachStopBtn,"plus","af-btn-icon"),this.attachStopBtn.title="Attach active document",this.attachStopBtn.removeClass("af-chat-stop-mode")))}handleStop(){let e=this.getCurrentSession();e&&(e.session.abort(),this.addBubble("error","Generation stopped"))}attachActiveDocument(){let e=this.app.workspace.getActiveFile();if(!e){new V.Notice("No active document to attach");return}if(this.attachedFiles.some(n=>n.path===e.path)){new V.Notice(`"${e.name}" is already attached`);return}let s=e.extension.toLowerCase();if(!new Set(["md","txt","json","yaml","yml","toml","csv","xml","html","css","js","ts","py","sh","sql","env","cfg","ini","log"]).has(s)){new V.Notice(`Can't attach "${e.name}" \u2014 only text files are supported`);return}this.attachedFiles.push(e),this.renderPills()}async saveImageBlobToVault(e){let s=e.type.split("/")[1]?.replace("jpeg","jpg")??"png",a=Date.now(),n=e.name&&e.name!=="image"?e.name:`pasted-${a}.${s}`,i=`${this.plugin.settings.fleetFolder}/chat-images`,o=`${i}/${a}-${n}`;try{this.app.vault.getAbstractFileByPath(i)||await this.app.vault.createFolder(i);let c=await e.arrayBuffer();await this.app.vault.createBinary(o,c);let d=this.app.vault.adapter.getBasePath?.()??"";return{name:n,path:`${d}/${o}`}}catch(c){let l=c instanceof Error?c.message:String(c);return new V.Notice(`Failed to save image: ${l}`),null}}async buildAttachmentContextFor(e,s){if(e.length===0&&s.length===0)return"";let a=[];for(let n of e)try{let i=await this.app.vault.cachedRead(n);a.push(`### ${n.name} +This removes its message history. The agent and its other conversations are untouched.`))return;let i=oa(this.selectedAgentName,e.id);this.sessions.get(i)?.session.dispose(),this.sessions.delete(i);try{await this.plugin.repository.deleteConversation(a,e.id)}catch(o){new V.Notice(`Couldn't delete: ${o instanceof Error?o.message:String(o)}`);return}if(await this.refreshConversationsList(a),e.id===this.selectedConversationId){let o=this.conversationsCache[0]?.id??ht;await this.switchToAgent(this.selectedAgentName,o)}}toggleConvoPanel(){this.convoPanelCollapsed=!this.convoPanelCollapsed,this.userToggledCollapse=!0,this.applyCollapsedClass(),this.leaf.updateHeader()}applyCollapsedClass(){this.convoPanelEl&&(this.convoPanelCollapsed?this.convoPanelEl.addClass("collapsed"):this.convoPanelEl.removeClass("collapsed")),this.collapseBtn&&(this.collapseBtn.empty(),(0,V.setIcon)(this.collapseBtn,this.convoPanelCollapsed?"panel-left-open":"panel-left-close"))}renderMarkdownBubble(e,s){let a=e.querySelector(".af-chat-copy-btn"),n=a?.parentNode?.removeChild(a)??null;e.empty(),e.addClass("af-compact-md"),V.MarkdownRenderer.render(this.app,s,e,"",this.plugin).then(()=>{n&&e.appendChild(n),this.wireBubbleLinks(e),e.querySelectorAll("pre").forEach(i=>{i.querySelector(".copy-code-button")?.remove();let o=i.querySelector("code");if(!o)return;let c=document.createElement("button");c.className="af-code-copy-btn",c.setAttribute("aria-label","Copy code"),(0,V.setIcon)(c,"copy"),c.onclick=l=>{l.stopPropagation(),navigator.clipboard.writeText(o.textContent??"").then(()=>{c.addClass("copied"),(0,V.setIcon)(c,"check"),setTimeout(()=>{c.removeClass("copied"),(0,V.setIcon)(c,"copy")},1500)})},i.style.position="relative",i.appendChild(c)})})}wireBubbleLinks(e){e.addEventListener("click",s=>{let a=s.target.closest("a");if(!a)return;if(a.classList.contains("internal-link")){s.preventDefault(),s.stopPropagation();let i=a.getAttribute("data-href")||a.getAttribute("href")||a.textContent||"";if(!i)return;let o=s.shiftKey?"split":"tab";this.app.workspace.openLinkText(i,"",o);return}let n=a.getAttribute("href")||"";if(n.startsWith("obsidian://")){s.preventDefault(),s.stopPropagation(),window.location.href=n;return}if(a.classList.contains("external-link")||/^https?:\/\//.test(n)){s.preventDefault(),s.stopPropagation(),window.open(n,"_blank");return}})}addCopyBtn(e,s){let a=e.createEl("button",{cls:"af-chat-copy-btn",attr:{"aria-label":"Copy message"}});(0,V.setIcon)(a,"copy"),a.onclick=n=>{n.stopPropagation(),navigator.clipboard.writeText(s()).then(()=>{a.addClass("copied"),(0,V.setIcon)(a,"check"),setTimeout(()=>{a.removeClass("copied"),(0,V.setIcon)(a,"copy")},1500)})}}addBubble(e,s,a){if(e==="user"&&a&&a.length>0){let i=this.messagesInner.createDiv({cls:"af-chat-bubble-attachments"});for(let o of a){let c=i.createSpan({cls:"af-chat-pill af-chat-pill-inline"}),l=c.createSpan({cls:"af-chat-pill-icon"}),d=/\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(o);(0,V.setIcon)(l,d?"image":"file-text"),c.createSpan({cls:"af-chat-pill-name",text:o})}}let n=this.messagesInner.createDiv({cls:`af-chat-bubble af-chat-bubble-${e}`});if(s&&(e==="assistant"?this.renderMarkdownBubble(n,s):n.setText(s)),e==="assistant"){let i=s??"";this.addCopyBtn(n,()=>i),n._setRawText=o=>{i=o}}return this.messagesEl.scrollTop=this.messagesEl.scrollHeight,n}getOrCreateAffordancesRow(e){let s=e.parentElement;if(!s)throw new Error("bubble has no parent");let a=e.nextElementSibling;if(a&&a.classList.contains("af-chat-affordances"))return a;let n=document.createElement("div");return n.className="af-chat-affordances",s.insertBefore(n,e.nextSibling),n}attachThreadAffordance(e,s,a){let n=e.parentElement;if(!n)return;let i=this.getOrCreateAffordancesRow(e);if(i.querySelector(".af-thread-badge"))return;let o=document.createElement("div");o.className="af-thread-badge",o.setAttribute("role","button"),o.setAttribute("tabindex","0"),i.appendChild(o),(0,V.setIcon)(o,"message-circle");let c=o.createSpan({cls:"af-thread-badge-label"}),l=document.createElement("div");l.className="af-thread-container",l.style.display="none",n.insertBefore(l,i.nextSibling);let d=()=>{let f=a.getThreadIndex()[s]?.messageCount??0;f<=0?c.setText("Thread"):c.setText(`${f} ${f===1?"reply":"replies"}`),f>0&&o.addClass("has-replies")};d();let h=!1,u=async()=>{if(this.threadExpanded.get(s)===!0){l.style.display="none",this.threadExpanded.set(s,!1),o.removeClass("expanded"),this.setStatsSource(a);return}if(this.threadExpanded.set(s,!0),l.style.display="",o.addClass("expanded"),!h)try{let m=await a.openOrCreateThread(s);this.renderThreadContainer(l,m,a,d),h=!0}catch(m){l.setText(`Failed to open thread: ${m instanceof Error?m.message:String(m)}`)}};o.onclick=()=>void u(),o.onkeydown=p=>{(p.key==="Enter"||p.key===" ")&&(p.preventDefault(),u())}}renderThreadContainer(e,s,a,n){e.empty();let i=e.createDiv({cls:"af-thread-wrap"}),o=i.createDiv({cls:"af-thread-messages"}),c=null,l=null,d=S=>{S?(c||(c=o.createDiv({cls:"af-chat-activity"})),c.setText(`Working\u2026 (${S})`)):c&&(c.remove(),c=null)},h=S=>{if(S&&!l){l=o.createDiv({cls:"af-chat-streaming-dot"});for(let L=0;L<3;L++)l.createSpan()}else!S&&l&&(l.remove(),l=null)},u=(S,L,E)=>{if(S==="user"&&E&&E.length>0){let I=o.createDiv({cls:"af-chat-bubble-attachments"});for(let q of E){let F=I.createSpan({cls:"af-chat-pill af-chat-pill-inline"}),$=F.createSpan({cls:"af-chat-pill-icon"});(0,V.setIcon)($,q.match(/\.(png|jpe?g|gif|webp|svg)$/i)?"image":"file-text"),F.createSpan({cls:"af-chat-pill-name",text:q})}}let B=o.createDiv({cls:`af-thread-bubble af-thread-bubble-${S}`});return S==="assistant"?this.renderMarkdownBubble(B,L):B.setText(L),B};for(let S of s.messages)u(S.role,S.content,S.attachments);let p=[],m=[],f=i.createDiv({cls:"af-thread-composer-wrap"}),y=f.createDiv({cls:"af-chat-pills-row af-thread-pills-row"});y.style.display="none";let k=()=>{if(y.empty(),p.length===0&&m.length===0){y.style.display="none";return}y.style.display="flex";for(let S of p){let L=y.createDiv({cls:"af-chat-pill"}),E=L.createSpan({cls:"af-chat-pill-icon"});(0,V.setIcon)(E,"file-text"),L.createSpan({cls:"af-chat-pill-name",text:S.name});let B=L.createSpan({cls:"af-chat-pill-remove"});(0,V.setIcon)(B,"x"),B.onclick=I=>{I.stopPropagation();let q=p.findIndex(F=>F.path===S.path);q>=0&&p.splice(q,1),k()}}for(let S of m){let L=y.createDiv({cls:"af-chat-pill"}),E=L.createSpan({cls:"af-chat-pill-icon"});(0,V.setIcon)(E,"image"),L.createSpan({cls:"af-chat-pill-name",text:S.name});let B=L.createSpan({cls:"af-chat-pill-remove"});(0,V.setIcon)(B,"x"),B.onclick=I=>{I.stopPropagation();let q=m.findIndex(F=>F.path===S.path);q>=0&&m.splice(q,1),k()}}},b=()=>{let S=this.app.workspace.getActiveFile();if(!S){new V.Notice("No active document to attach");return}if(p.some(E=>E.path===S.path)){new V.Notice(`"${S.name}" is already attached`);return}if(!new Set(["md","txt","json","yaml","yml","toml","csv","xml","html","css","js","ts","py","sh","sql","env","cfg","ini","log"]).has(S.extension.toLowerCase())){new V.Notice(`Can't attach "${S.name}" \u2014 only text files are supported`);return}p.push(S),k()},v=async S=>{let L=S.type.split("/")[1]?.replace("jpeg","jpg")??"png",E=S.name&&S.name!=="image"?S.name:`pasted-${Date.now()}.${L}`;if(m.some(I=>I.name===E)){new V.Notice(`"${E}" is already attached`);return}let B=await this.saveImageBlobToVault(S);B&&(m.push(B),k())},g=f.createDiv({cls:"af-chat-input-row af-thread-composer"}),x=g.createEl("button",{cls:"af-chat-attach-btn"});P(x,"plus","af-btn-icon"),x.title="Attach active document",x.onclick=S=>{S.preventDefault(),b()};let T=g.createEl("textarea",{cls:"af-chat-input af-thread-input",attr:{placeholder:"Message in thread\u2026 (Ctrl+Enter to send)",rows:"1"}});T.addEventListener("paste",S=>{let L=S.clipboardData?.items;if(L)for(let E=0;E{S.preventDefault(),S.stopPropagation(),f.addClass("af-chat-input-dragover")}),f.addEventListener("dragleave",()=>{f.removeClass("af-chat-input-dragover")}),f.addEventListener("drop",S=>{S.preventDefault(),S.stopPropagation(),f.removeClass("af-chat-input-dragover");let L=S.dataTransfer?.files;if(L)for(let E=0;E{T.style.height="auto",T.style.height=`${Math.min(T.scrollHeight,120)}px`,_.style.display=T.value.trim()?"flex":"none"};T.addEventListener("input",R),T.addEventListener("focus",()=>this.setStatsSource(s));let A=async()=>{let S=T.value.trim();if(!S||s.isStreaming)return;let L=await this.buildAttachmentContextFor(p,m),E=[...p.map(F=>F.name),...m.map(F=>F.name)],B=L?`${L}${S}`:void 0;T.value="",R(),p.length=0,m.length=0,k(),u("user",S,E.length>0?E:void 0),h(!0);let I=null,q="";try{await s.sendMessage(S,F=>{if(F.type==="text"){I||(h(!1),d(),I=u("assistant",""),I.empty()),q+=F.content;let $=I.querySelector(".af-chat-stream-text");$||($=I.createDiv({cls:"af-chat-stream-text"})),$.setText(q)}else F.type==="tool_use"?d(F.toolName):F.type==="result"&&(d(),h(!1),I&&this.renderMarkdownBubble(I,q))},B,E.length>0?E:void 0),n()}catch(F){h(!1),d();let $=F instanceof Error?F.message:String(F);o.createDiv({cls:"af-thread-error",text:`Error: ${$}`})}};_.onclick=()=>void A(),T.onkeydown=S=>{S.key==="Enter"&&(S.ctrlKey||S.metaKey)&&(S.preventDefault(),A())}}buildToolSummary(e,s){let n=(s??this.messagesInner).createDiv({cls:"af-chat-tool-summary"}),i=n.createEl("details"),o=i.createEl("summary"),c=new Map;for(let h of e){let u=c.get(h.name)??[];h.command&&u.push(h.command),c.set(h.name,u)}let l=o.createSpan({cls:"af-chat-tool-icon"});(0,V.setIcon)(l,"wrench"),o.appendText(` ${e.length} tool call${e.length!==1?"s":""}`);let d=i.createDiv({cls:"af-chat-tool-list"});for(let[h,u]of c){let p=u.length||(c.get(h)?.length??1),m=d.createDiv({cls:"af-chat-tool-item"}),f=p>1?`${h} (\xD7${p})`:h;m.createSpan({cls:"af-chat-tool-name",text:f}),u.length===1&&u[0]&&m.createSpan({cls:"af-chat-tool-cmd",text:u[0]})}return n}renderIndicators(e){let s=e.isStreaming,a=e.currentToolName,n=e.hasCurrentTurnText,i=!!this.messagesInner.querySelector(".af-chat-stream-text"),o=null;if(s&&a?o=`Working\u2026 (${a})`:s&&n&&!i&&(o="Replying\u2026"),o?(this.activityEl?(this.activityEl.parentElement!==this.messagesInner||this.activityEl.nextElementSibling!==null)&&this.messagesInner.appendChild(this.activityEl):this.activityEl=this.messagesInner.createDiv({cls:"af-chat-activity"}),this.activityEl.textContent!==o&&this.activityEl.setText(o)):this.activityEl&&(this.activityEl.remove(),this.activityEl=null),s&&!a&&!n)if(this.streamingDot)(this.streamingDot.parentElement!==this.messagesInner||this.streamingDot.nextElementSibling!==null)&&this.messagesInner.appendChild(this.streamingDot);else{this.streamingDot=this.messagesInner.createDiv({cls:"af-chat-streaming-dot"});for(let l=0;l<3;l++)this.streamingDot.createSpan()}else this.streamingDot&&(this.streamingDot.remove(),this.streamingDot=null);this.setAttachStopMode(s)}setAttachStopMode(e){e!==this.isInStopMode&&(this.isInStopMode=e,this.attachStopBtn.empty(),e?(P(this.attachStopBtn,"square","af-btn-icon"),this.attachStopBtn.title="Stop generation",this.attachStopBtn.addClass("af-chat-stop-mode")):(P(this.attachStopBtn,"plus","af-btn-icon"),this.attachStopBtn.title="Attach active document",this.attachStopBtn.removeClass("af-chat-stop-mode")))}handleStop(){let e=this.getCurrentSession();e&&(e.session.abort(),this.addBubble("error","Generation stopped"))}attachActiveDocument(){let e=this.app.workspace.getActiveFile();if(!e){new V.Notice("No active document to attach");return}if(this.attachedFiles.some(n=>n.path===e.path)){new V.Notice(`"${e.name}" is already attached`);return}let s=e.extension.toLowerCase();if(!new Set(["md","txt","json","yaml","yml","toml","csv","xml","html","css","js","ts","py","sh","sql","env","cfg","ini","log"]).has(s)){new V.Notice(`Can't attach "${e.name}" \u2014 only text files are supported`);return}this.attachedFiles.push(e),this.renderPills()}async saveImageBlobToVault(e){let s=e.type.split("/")[1]?.replace("jpeg","jpg")??"png",a=Date.now(),n=e.name&&e.name!=="image"?e.name:`pasted-${a}.${s}`,i=`${this.plugin.settings.fleetFolder}/chat-images`,o=`${i}/${a}-${n}`;try{this.app.vault.getAbstractFileByPath(i)||await this.app.vault.createFolder(i);let c=await e.arrayBuffer();await this.app.vault.createBinary(o,c);let d=this.app.vault.adapter.getBasePath?.()??"";return{name:n,path:`${d}/${o}`}}catch(c){let l=c instanceof Error?c.message:String(c);return new V.Notice(`Failed to save image: ${l}`),null}}async buildAttachmentContextFor(e,s){if(e.length===0&&s.length===0)return"";let a=[];for(let n of e)try{let i=await this.app.vault.cachedRead(n);a.push(`### ${n.name} \`\`\` ${i} \`\`\``)}catch{a.push(`### ${n.name}