That is, instead of a single pane on the right side bar, have chats be opened each on its own separate tab, similar to how notes are opened.
When researching, I often have multiple tabs of Claude, Chat GPT, and Google AI Studio, all opened in my Chrome browser, with each separate topic in its own browser tab. It would be really great if something like that was integrated into Obsidian via your plugin.
Now, most AI chat plugins out there doesn't have this "multiple AI chat tabs" feature. I got fed up so I wrote my own script for the "Text Generator" plugin that parses a specialized note as a special chat note, where I could attach "context notes" and bounce off ideas with the AI using plain markdown, each in its own separate markdown tab in Obsidian. I'll share some of them below in case it would help others like me:
AI Chat.md
~~~md
---
promptId: AI Chat
name: ๐ AI Chat
description: Chat with the AI using a specialized note saved to your Obsidian vault! If the active note is not the specialized chat note, a new one will be created and opened.
author: jasonsparc
tags:
version: 0.0.1
disableProvider: true
commands:
- generate
mode: insert
x.debug.log: true
x.debug.noGen: true
x.dir: "!ObsidianSetup/01 TextGen Chat/"
---
{{#script}}
```js
const workspace = app.workspace
const vault = app.vault
const activeView = workspace.getMostRecentLeaf()?.view
const activeFile = activeView?.file
if (activeFile?.extension != "md") return error("No active note.")
const editor = activeView.editor
if (!editor) return error("No active editor.")
const MSG_HR_START = "โโโโ "
const MSG_HR_END = " โโโโ"
const MSG_HR_SPLITTER = /^โโโโ[ \t]*(โง*)[ \t]*โโโโ[ \t]*(?:\r?\n|[\r\u2028\u2029]|$)/mu
const MSG_HR_TEST = /^โโโโ[ \t]*โง*[ \t]*โโโโ[ \t]*$/mu
const USER_MARK = "โงโง"
const AI_MARK = "โง"
const USER_HR = `${MSG_HR_START}${USER_MARK}${MSG_HR_END}`
const AI_HR = `${MSG_HR_START}${AI_MARK}${MSG_HR_END}`
const CHAT_SIGNATURE = "#ai-chat "
const metadataCache = app.metadataCache
const metadata = metadataCache.getFileCache(activeFile)
if (this.newTab || !editor.getLine(
(metadata?.frontmatterPosition?.end?.line ?? -1) + 1
).startsWith(CHAT_SIGNATURE)) {
const template =
`${CHAT_SIGNATURE}#no-graph
${USER_MARK} : User
${AI_MARK} : AI
# Context Notes
${await run("Suggest Context Notes", { noCursorMove: true, includeSelf: true })}
# AI Chat History
${USER_HR}
`
const chatsDir = PromptInfo["x.dir"]
if (!chatsDir) return error('Template property not configured: "x.dir"')
if (!vault.getAbstractFileByPath(chatsDir)) vault.createFolder(chatsDir)
const timestamp = moment().format("YYYY-MM-DD HH-mm-ss")
let filePath = `${chatsDir}/${timestamp}.md`
for (let i = 1; vault.getAbstractFileByPath(filePath); i++)
filePath = `${chatsDir}/${timestamp}-${i}.md`
const newFile = await vault.create(filePath, template)
const leaf = workspace.getLeaf(true)
await leaf.openFile(newFile)
workspace.setActiveLeaf(leaf)
goToLastLine(leaf.view.editor)
return "" // Done! Skip code below.
}
function goToLastLine(editor) {
const lastLine_i = editor.lastLine()
editor.setCursor(lastLine_i)
editor.focus()
return lastLine_i
}
const lastLine = editor.getLine(goToLastLine(editor))
let system = `You are an AI assistant provided by the "Text Generator" plugin for Obsidian. You engage in natural-sounding conversations and respond to a wide range of prompts and questions. Your responses should be informative, comprehensive, and tailored to the user's input. If you don't know an answer, admit it and suggest specific resources that might help, such as websites, books, research papers, or relevant Obsidian notes.
Guidelines:
1. Follow the user's lead in conversation; don't change the subject unless asked.
2. Stay focused on the user's stated task or topic unless directed otherwise.
3. If a request is unclear, politely ask for clarification.
4. When providing multiple suggestions:
a. Indicate your top recommendation(s), explaining your reasoning briefly.
b. If there are multiple top choices or close seconds, acknowledge this and explain the trade-offs.
c. Consider ranking or categorizing options if it adds value (e.g., "best for beginners," "most cost-effective").
5. Always strive to follow given instructions.
6. Use provided Obsidian notes (if any) for context, acknowledging any discrepancies.
7. If no notes are provided or they're irrelevant, rely on your general knowledge.
8. Generally respond using Obsidian-flavored markdown.
Your primary goal is to be honest, helpful, and assist the user in any way you can.`
const children = metadata?.links
?.map(lc => metadataCache.getFirstLinkpathDest(lc.link, activeFile.path)) || []
const messages = []
const seenPaths = new Set([activeFile.path])
const CONTEXT_PATH = "path"
const CONTEXT_TITLE = "title"
const CONTEXT_NOTE = "CONTEXT NOTE"
const CONTEXT_REPLY = 'โฆ'
let contextNotes_n = 0
for (const child of children) {
if (!child) continue
const path = child.path
if (seenPaths.has(path)) continue
seenPaths.add(path)
const contextNote =
`${CONTEXT_NOTE} ${contextNotes_n}
${CONTEXT_PATH}: ${path}
${CONTEXT_TITLE}: ${child.basename}
${await vault.read(child)}`
messages.push(contextNote, CONTEXT_REPLY)
contextNotes_n++
}
if (contextNotes_n > 0) {
const END_OF_CONTEXT_NOTES = "END OF CONTEXT NOTES"
messages.push(END_OF_CONTEXT_NOTES, CONTEXT_REPLY)
system += `
Obsidian Notes Context:
- ${contextNotes_n} Obsidian notes will be provided as context.
- Each note starts with the line "${CONTEXT_NOTE} N" (where N is an integer).
- Respond only with "${CONTEXT_REPLY}" when you see a "${CONTEXT_NOTE} N" prompt.
- Context notes include '${CONTEXT_PATH}' and '${CONTEXT_TITLE}' fields, followed by an empty line, then followed by the verbatim content of the note.
- After all notes are provided, you'll receive an "${END_OF_CONTEXT_NOTES}" prompt. Respond only with "${CONTEXT_REPLY}" when you see this prompt.
- Only then should you begin responding to user queries.
These "${CONTEXT_NOTE} N" and "${END_OF_CONTEXT_NOTES}" prompts are provided by the system. The user may not be aware of these prompts and will simply assume that their first message to you is the user prompt after the "${END_OF_CONTEXT_NOTES}" prompt.`
}
const content = await vault.read(activeFile)
const msgSlabs = content.split(MSG_HR_SPLITTER)
for (let i = 1, N = msgSlabs.length, lastType = AI_MARK; i < N; i += 2) {
const type = msgSlabs[i]
if (type != USER_MARK && type != AI_MARK)
return error(`Invalid chat mark: ${type}`)
if (type == lastType) messages.push(' ')
lastType = type
messages.push(unindent1(msgSlabs[i + 1].trimEnd()) || ' ')
}
// Ensure odd count messages
if (messages.length % 2 == 0) {
if (messages.at(-1) == ' ') messages.pop()
else messages.push(' ')
}
const genParams = { system, messages }
if (PromptInfo["x.debug.log"]) console.log(genParams)
const genOutput = PromptInfo["x.debug.noGen"] ? "DEMO RESPONSE"
: (await this.gen(void 0, genParams)).trimEnd()
this.vars.output = `${lastLine ? '\n' : ''}${AI_HR}
${MSG_HR_TEST.test(genOutput) ? indent1(genOutput) : genOutput }
${USER_HR}
`
return "" // Done!
function indent1(s) {
return s.replaceAll(/^(?!$)/gm, ' ') // Just 1 space indentation
}
function unindent1(s) {
const lines = s.split(/^/m) // Preserves line breaks
for (let i = 0, N = lines.length; i < N; i++) {
const line = lines[i]
const m = line.match(/^(?:[ \t]|$)/m) // Just 1 whitespace indentation
if (!m) return s // Some were not indented
lines[i] = line.slice(m[0].length)
}
return lines.join('')
}
```
{{/script}}
***
{{#script}}
```js
return ""
```
{{/script}}
***
{{#script}}
```js
return this.vars.output || ""
```
{{/script}}
~~~
AI Chat in New Tab.md
~~~md
---
promptId: AI Chat in New Tab
name: ๐ New AI Chat Tab
description: Chat with the AI using a specialized note saved to your Obsidian vault! The specialized note will be created and opened in a new tab.
author: jasonsparc
tags:
version: 0.0.1
disableProvider: true
commands:
- generate
mode: insert
---
{{#script}}
```js
await run("AI Chat", { newTab: true })
return ""
```
{{/script}}
***
{{#script}}
```js
return ""
```
{{/script}}
***
{{#script}}
```js
return ""
```
{{/script}}
~~~
Suggest Context Notes.md
~~~md
---
promptId: Suggest Context Notes
name: ๐บ Suggest context notes
description: Lists outgoing links in the active note and their respective outgoing links.
author: jasonsparc
tags:
version: 0.0.1
disableProvider: true
commands:
- generate
---
{{#script}}
```js
const activeView = app.workspace.getMostRecentLeaf()?.view
const activeFile = activeView?.file
if (activeFile?.extension != "md") return error("No active note.")
const metadataCache = app.metadataCache
function getChildren(file) {
if (file) {
const children = metadataCache.getFileCache(file)?.links
?.map(lc => metadataCache.getFirstLinkpathDest(lc.link, file.path))
if (children) return children
}
return []
}
const includeSelf = this.includeSelf
const children = getChildren(activeFile)
let output = Array.from(
[...(includeSelf ? [activeFile] : []), ...children, ...children.flatMap(getChildren)]
.reduce((set, file) => file?.extension == "md"
&& (includeSelf || file.path != activeFile.path)
? set.add(`[[${file.path.slice(0, -3)}]]\n`) : set, new Set())
).join('')
if (!this.noCursorMove) {
const editor = activeView.editor
if (editor) {
const cursorLine_i = editor.getCursor().line
if (editor.getLine(cursorLine_i)) {
editor.setCursor(cursorLine_i)
output = "\n" + output.slice(0, -1)
}
}
}
this.vars.output = output
return ""
```
{{/script}}
***
{{#script}}
```js
return ""
```
{{/script}}
***
{{#script}}
```js
return this.vars.output || ""
```
{{/script}}
~~~
So far, I'm very happy with the results so far (even though the above may have bugs). I'm even thinking of uninstalling the SystemSculpt AI plugin because of that, but I feel like it would be helpful for others if I instead requested you to implement a similar feature for SystemSculpt AI.
Fun fact: I'm currently using Obsidian to write a GDD (Game Design Document) with the entire Obsidian vault itself serving as the GDD, and hence the need for me to open up multiple AI chat threads for diverse aspects of the game and GDD I'm working on.
Killer enhancement idea here, thanks for the insight - I love it and will definitely implement it. Will keep this open until it's implemented in the next release. Cheers!
Describe the enhancement
That is, instead of a single pane on the right side bar, have chats be opened each on its own separate tab, similar to how notes are opened.
When researching, I often have multiple tabs of Claude, Chat GPT, and Google AI Studio, all opened in my Chrome browser, with each separate topic in its own browser tab. It would be really great if something like that was integrated into Obsidian via your plugin.
Now, most AI chat plugins out there doesn't have this "multiple AI chat tabs" feature. I got fed up so I wrote my own script for the "Text Generator" plugin that parses a specialized note as a special chat note, where I could attach "context notes" and bounce off ideas with the AI using plain markdown, each in its own separate markdown tab in Obsidian. I'll share some of them below in case it would help others like me:
AI Chat.md
~~~md --- promptId: AI Chat name: ๐ AI Chat description: Chat with the AI using a specialized note saved to your Obsidian vault! If the active note is not the specialized chat note, a new one will be created and opened. author: jasonsparc tags: version: 0.0.1 disableProvider: true commands: - generate mode: insert x.debug.log: true x.debug.noGen: true x.dir: "!ObsidianSetup/01 TextGen Chat/" --- {{#script}} ```js const workspace = app.workspace const vault = app.vault const activeView = workspace.getMostRecentLeaf()?.view const activeFile = activeView?.file if (activeFile?.extension != "md") return error("No active note.") const editor = activeView.editor if (!editor) return error("No active editor.") const MSG_HR_START = "โโโโ " const MSG_HR_END = " โโโโ" const MSG_HR_SPLITTER = /^โโโโ[ \t]*(โง*)[ \t]*โโโโ[ \t]*(?:\r?\n|[\r\u2028\u2029]|$)/mu const MSG_HR_TEST = /^โโโโ[ \t]*โง*[ \t]*โโโโ[ \t]*$/mu const USER_MARK = "โงโง" const AI_MARK = "โง" const USER_HR = `${MSG_HR_START}${USER_MARK}${MSG_HR_END}` const AI_HR = `${MSG_HR_START}${AI_MARK}${MSG_HR_END}` const CHAT_SIGNATURE = "#ai-chat " const metadataCache = app.metadataCache const metadata = metadataCache.getFileCache(activeFile) if (this.newTab || !editor.getLine( (metadata?.frontmatterPosition?.end?.line ?? -1) + 1 ).startsWith(CHAT_SIGNATURE)) { const template = `${CHAT_SIGNATURE}#no-graph ${USER_MARK} : User ${AI_MARK} : AI # Context Notes ${await run("Suggest Context Notes", { noCursorMove: true, includeSelf: true })} # AI Chat History ${USER_HR} ` const chatsDir = PromptInfo["x.dir"] if (!chatsDir) return error('Template property not configured: "x.dir"') if (!vault.getAbstractFileByPath(chatsDir)) vault.createFolder(chatsDir) const timestamp = moment().format("YYYY-MM-DD HH-mm-ss") let filePath = `${chatsDir}/${timestamp}.md` for (let i = 1; vault.getAbstractFileByPath(filePath); i++) filePath = `${chatsDir}/${timestamp}-${i}.md` const newFile = await vault.create(filePath, template) const leaf = workspace.getLeaf(true) await leaf.openFile(newFile) workspace.setActiveLeaf(leaf) goToLastLine(leaf.view.editor) return "" // Done! Skip code below. } function goToLastLine(editor) { const lastLine_i = editor.lastLine() editor.setCursor(lastLine_i) editor.focus() return lastLine_i } const lastLine = editor.getLine(goToLastLine(editor)) let system = `You are an AI assistant provided by the "Text Generator" plugin for Obsidian. You engage in natural-sounding conversations and respond to a wide range of prompts and questions. Your responses should be informative, comprehensive, and tailored to the user's input. If you don't know an answer, admit it and suggest specific resources that might help, such as websites, books, research papers, or relevant Obsidian notes. Guidelines: 1. Follow the user's lead in conversation; don't change the subject unless asked. 2. Stay focused on the user's stated task or topic unless directed otherwise. 3. If a request is unclear, politely ask for clarification. 4. When providing multiple suggestions: a. Indicate your top recommendation(s), explaining your reasoning briefly. b. If there are multiple top choices or close seconds, acknowledge this and explain the trade-offs. c. Consider ranking or categorizing options if it adds value (e.g., "best for beginners," "most cost-effective"). 5. Always strive to follow given instructions. 6. Use provided Obsidian notes (if any) for context, acknowledging any discrepancies. 7. If no notes are provided or they're irrelevant, rely on your general knowledge. 8. Generally respond using Obsidian-flavored markdown. Your primary goal is to be honest, helpful, and assist the user in any way you can.` const children = metadata?.links ?.map(lc => metadataCache.getFirstLinkpathDest(lc.link, activeFile.path)) || [] const messages = [] const seenPaths = new Set([activeFile.path]) const CONTEXT_PATH = "path" const CONTEXT_TITLE = "title" const CONTEXT_NOTE = "CONTEXT NOTE" const CONTEXT_REPLY = 'โฆ' let contextNotes_n = 0 for (const child of children) { if (!child) continue const path = child.path if (seenPaths.has(path)) continue seenPaths.add(path) const contextNote = `${CONTEXT_NOTE} ${contextNotes_n} ${CONTEXT_PATH}: ${path} ${CONTEXT_TITLE}: ${child.basename} ${await vault.read(child)}` messages.push(contextNote, CONTEXT_REPLY) contextNotes_n++ } if (contextNotes_n > 0) { const END_OF_CONTEXT_NOTES = "END OF CONTEXT NOTES" messages.push(END_OF_CONTEXT_NOTES, CONTEXT_REPLY) system += ` Obsidian Notes Context: - ${contextNotes_n} Obsidian notes will be provided as context. - Each note starts with the line "${CONTEXT_NOTE} N" (where N is an integer). - Respond only with "${CONTEXT_REPLY}" when you see a "${CONTEXT_NOTE} N" prompt. - Context notes include '${CONTEXT_PATH}' and '${CONTEXT_TITLE}' fields, followed by an empty line, then followed by the verbatim content of the note. - After all notes are provided, you'll receive an "${END_OF_CONTEXT_NOTES}" prompt. Respond only with "${CONTEXT_REPLY}" when you see this prompt. - Only then should you begin responding to user queries. These "${CONTEXT_NOTE} N" and "${END_OF_CONTEXT_NOTES}" prompts are provided by the system. The user may not be aware of these prompts and will simply assume that their first message to you is the user prompt after the "${END_OF_CONTEXT_NOTES}" prompt.` } const content = await vault.read(activeFile) const msgSlabs = content.split(MSG_HR_SPLITTER) for (let i = 1, N = msgSlabs.length, lastType = AI_MARK; i < N; i += 2) { const type = msgSlabs[i] if (type != USER_MARK && type != AI_MARK) return error(`Invalid chat mark: ${type}`) if (type == lastType) messages.push(' ') lastType = type messages.push(unindent1(msgSlabs[i + 1].trimEnd()) || ' ') } // Ensure odd count messages if (messages.length % 2 == 0) { if (messages.at(-1) == ' ') messages.pop() else messages.push(' ') } const genParams = { system, messages } if (PromptInfo["x.debug.log"]) console.log(genParams) const genOutput = PromptInfo["x.debug.noGen"] ? "DEMO RESPONSE" : (await this.gen(void 0, genParams)).trimEnd() this.vars.output = `${lastLine ? '\n' : ''}${AI_HR} ${MSG_HR_TEST.test(genOutput) ? indent1(genOutput) : genOutput } ${USER_HR} ` return "" // Done! function indent1(s) { return s.replaceAll(/^(?!$)/gm, ' ') // Just 1 space indentation } function unindent1(s) { const lines = s.split(/^/m) // Preserves line breaks for (let i = 0, N = lines.length; i < N; i++) { const line = lines[i] const m = line.match(/^(?:[ \t]|$)/m) // Just 1 whitespace indentation if (!m) return s // Some were not indented lines[i] = line.slice(m[0].length) } return lines.join('') } ``` {{/script}} *** {{#script}} ```js return "" ``` {{/script}} *** {{#script}} ```js return this.vars.output || "" ``` {{/script}} ~~~AI Chat in New Tab.md
~~~md --- promptId: AI Chat in New Tab name: ๐ New AI Chat Tab description: Chat with the AI using a specialized note saved to your Obsidian vault! The specialized note will be created and opened in a new tab. author: jasonsparc tags: version: 0.0.1 disableProvider: true commands: - generate mode: insert --- {{#script}} ```js await run("AI Chat", { newTab: true }) return "" ``` {{/script}} *** {{#script}} ```js return "" ``` {{/script}} *** {{#script}} ```js return "" ``` {{/script}} ~~~Suggest Context Notes.md
~~~md --- promptId: Suggest Context Notes name: ๐บ Suggest context notes description: Lists outgoing links in the active note and their respective outgoing links. author: jasonsparc tags: version: 0.0.1 disableProvider: true commands: - generate --- {{#script}} ```js const activeView = app.workspace.getMostRecentLeaf()?.view const activeFile = activeView?.file if (activeFile?.extension != "md") return error("No active note.") const metadataCache = app.metadataCache function getChildren(file) { if (file) { const children = metadataCache.getFileCache(file)?.links ?.map(lc => metadataCache.getFirstLinkpathDest(lc.link, file.path)) if (children) return children } return [] } const includeSelf = this.includeSelf const children = getChildren(activeFile) let output = Array.from( [...(includeSelf ? [activeFile] : []), ...children, ...children.flatMap(getChildren)] .reduce((set, file) => file?.extension == "md" && (includeSelf || file.path != activeFile.path) ? set.add(`[[${file.path.slice(0, -3)}]]\n`) : set, new Set()) ).join('') if (!this.noCursorMove) { const editor = activeView.editor if (editor) { const cursorLine_i = editor.getCursor().line if (editor.getLine(cursorLine_i)) { editor.setCursor(cursorLine_i) output = "\n" + output.slice(0, -1) } } } this.vars.output = output return "" ``` {{/script}} *** {{#script}} ```js return "" ``` {{/script}} *** {{#script}} ```js return this.vars.output || "" ``` {{/script}} ~~~So far, I'm very happy with the results so far (even though the above may have bugs). I'm even thinking of uninstalling the SystemSculpt AI plugin because of that, but I feel like it would be helpful for others if I instead requested you to implement a similar feature for SystemSculpt AI.
Fun fact: I'm currently using Obsidian to write a GDD (Game Design Document) with the entire Obsidian vault itself serving as the GDD, and hence the need for me to open up multiple AI chat threads for diverse aspects of the game and GDD I'm working on.