Replace the 3-attempt retry loop with a single nudge when the model
explicitly mentioned taking action. Remove misleading nudges for models
that never expressed intent, which wasted round-trips and confused
responses. Also streamline the userWantsVaultOps phrase list to remove
weak triggers like "ok", "yes", and "please", and add fallback content
for empty responses.
This commit adds support for multi-turn tool calling by making processToolCalls
recursive. Previously, follow-up LLM responses that included additional tool
calls would drop those calls and present the text response instead.
Key changes:
- processToolCalls now accepts a depth parameter (default 0)
- Recurses when follow-up responses contain more tool_calls, up to MAX_TOOL_CALL_DEPTH
- Adds MAX_TOOL_CALL_DEPTH constant set to 5 to prevent infinite loops
- Clears pending actions only after processing is complete or before recursing
- Fixes new-chat container flex layout in styles.css to wrap and start from left
I need to write a clean git commit message for this diff. Let me analyze what changed:
1. In `main.js` and `src/chat-view.ts`:
- `processToolCalls` now has a `depth` parameter with default `0`
- Added `MAX_TOOL_CALL_DEPTH = 5` constant
- Added depth check at start of `processToolCalls`
- When follow-up LLM calls return more tool calls, it now recursively calls `processToolCalls` instead of ignoring
them
- `clearPendingActions()` moved around to be called at appropriate times
- Some variable renames from `finalResponse` to `followUpContent` for clarity
2. In `styles.css`:
- `.ollama-new-chat-container` changed from `justify-content: flex-end` to `flex-wrap: wrap; justify-content:
flex-start`
The main feature is recursive/multi-turn tool calling with a depth limit. The CSS change seems like a minor UI fix
included in the same commit.
Subject line ideas:
- "Add recursive tool call handling with depth limit" (47 chars) - good
- "Support multi-turn tool calls with depth limit" (46 chars) - good
- "Add recursive tool calls and depth limit" (41 chars) - good
- "Implement recursive tool calling with depth limit" (47 chars) - good
Wait, I also need to mention the CSS change? It's minor but part of the commit. However, usually the subject should
capture the main change. If there are multiple unrelated changes, sometimes people mention them, but here the CSS change
seems minor enough that it could be in the body or omitted if not important. Actually looking at the diff, the CSS
change is just adjusting flex properties. It might be related to UI changes needed for the tool call flow, or it could
be an unrelated fix piggybacking.
Actually, the prompt says "write a short clear commit message that summarizes the changes." If I can express it in the
subject line, don't include body. But this seems like a significant enough change that a body might help explain the
"why" and key details.
Let me think about the body. The key points:
- Makes `processToolCalls` recursive to handle follow-up tool calls
- Adds `MAX_TOOL_CALL_DEPTH` (5) to prevent infinite recursion
- Previously, follow-up responses containing tool_calls would drop them
- Also fixes new-chat container layout
Subject: Add recursive tool call handling with depth limit (47 chars)
Body:
Support multi-turn tool calling by recursing in processToolCalls when
follow-up LLM responses contain additional tool_calls. Previously these
were ignored and the text response was shown instead.
Add MAX_TOOL_CALL_DEPTH constant set to 5 to prevent infinite loops.
Clear pending actions at the appropriate recursion boundaries.
Fix new-chat container flex layout to wrap and align left.
Wait, I should check if "Add recursive tool call handling with depth limit" is clear enough. Yes.
Let me count chars:
"Add recursive tool call handling with depth limit"
A-d-d- -r-e-c-u-r-s-i-v-e- -t-o-o-l- -c-a-l-l- -h-a-n-d-l-i-n-g- -w-i-t-h- -d-e-p-t-h- -l-i-m-i-t
3+1+9+1+4+1+4+1+9+1+4+1+5+1+5 = let's just count: "Add" (3) + " " (1) + "recursive" (9) + " " (1) + "tool" (4) + " " (1)
+ "call" (4) + " " (1) + "handling" (8) + " " (1) + "with" (4) + " " (1) + "depth" (5) + " " (1) + "limit" (5) =
3+1+9+1+4+1+4+1+8+1+4+1+5+1+5 = 48 chars. Good.
Actually, looking more carefully at the code:
- The depth check is added
- Recursion is added
- clearPendingActions is moved
- The CSS change is there
Is the body useful? Yes, it explains the behavioral change and why. But maybe I can keep it shorter.
Actually, looking at the instructions again: "If you can accurately express the change in just the subject line, don't
include anything in the message body."
Hmm. This is a fairly complex change. But the subject line "Add recursive tool call handling with depth limit" does
accurately express the main change. The CSS change is minor. However, there are enough details (the depth limit of 5,
the recursion aspect, the fact that it handles follow-up tool calls) that a brief body might be useful to someone
looking at the commit log.
But the instructions also say "Only use the body when it is providing *useful* information" and "Don't repeat
information from the subject line in the message body."
If I use the subject line I proposed, what useful info could the body add?
- The depth limit value (5)
- That it affects follow-up LLM responses
- The CSS fix
Actually, I think a body is appropriate here because the change is not entirely captured by just knowing it's
"recursive" - the fact that it previously dropped tool calls on follow-up is useful context (the "why").
Let me draft:
Add recursive tool call handling with depth limit
When follow-up LLM responses contain additional tool_calls, recurse in
processToolCalls instead of ignoring them. Add MAX_TOOL_CALL_DEPTH
constant set to 5 to prevent infinite loops. Move clearPendingActions
to appropriate recursion boundaries. Fix new-chat container flex layout
to wrap and align left.
Wait, I should check line wrapping. Max 72 chars.
Line 1: "Add recursive tool call handling with depth limit"
Line 2: (blank)
Line 3: "When follow-up LLM responses contain additional tool_calls, recurse in"
Line 4: "processToolCalls instead of ignoring them. Add MAX_TOOL_CALL_DEPTH"
Line 5: "constant set to 5 to prevent infinite loops. Move clearPendingActions"
Line 6: "to appropriate recursion boundaries. Fix new-chat container flex layout"
Line 7: "to wrap and align left."
Let me check lengths:
Line 3: "When follow-up LLM responses contain additional tool_calls, recurse in" = 70 chars. Good.
Line 4: "processToolCalls instead of ignoring them. Add MAX_TOOL_CALL_DEPTH" = 67 chars. Good.
Line 5: "constant set to 5 to prevent infinite loops. Move clearPendingActions" = 69 chars. Good.
Line 6: "to appropriate recursion boundaries. Fix new-chat container flex layout" = 69 chars. Good.
Line 7: "to wrap and align left." = 23 chars. Good.
Actually, should I mention processToolCalls specifically? It's a detail but useful.
Alternative body:
Support multi-turn tool calling by recursing when follow-up responses
contain additional tool_calls. Previously these were discarded and the
text response was shown instead. Add MAX_TOOL_CALL_DEPTH constant set
to 5. Also fix new-chat container flex layout.
Length check:
"Support multi-turn tool calling by recursing when follow-up responses" = 67
"contain additional tool_calls. Previously these were discarded and the" = 70
"text response was shown instead. Add MAX_TOOL_CALL_DEPTH constant set" = 69
"to 5. Also fix new-chat container flex layout." = 46
I think this is clearer. But does it repeat the subject? The subject says "Add recursive tool call handling" and the
body says "Support multi-turn tool calling by recursing..." - that's similar but explains the mechanism, not repeating
per se.
Actually, I could also make the subject slightly different to be more specific:
"Add multi-turn tool call recursion with depth limit"
Then body:
When follow-up LLM responses contain additional tool_calls, recurse in
processToolCalls rather than discarding them. Add MAX_TOOL_CALL_DEPTH
constant set to 5. Move clearPendingActions to correct boundaries.
Also fix new-chat container flex layout.
Hmm. "Add multi-turn tool call recursion with depth limit" is 49 chars. Good.
Actually, looking at the diff again, the most important thing is that `processToolCalls` now recurses. Let me make sure
I capture that.
Another option: The commit could be split conceptually, but since it's one diff, we summarize all changes.
Final decision:
Subject: Add recursive tool call handling with depth limit
Body:
When follow-up LLM responses contain additional tool_calls, recurse in
processToolCalls instead of ignoring them. Add MAX_TOOL_CALL_DEPTH
constant set to 5 to prevent infinite loops. Move clearPendingActions
to appropriate recursion boundaries. Fix new-chat container flex layout
to wrap and align left.
Wait, I want to make sure I don't exceed 72 chars per line in the body.
Line 1: When follow-up LLM responses contain additional tool_calls, recurse in
Count: W(1)h(2)e(3)n(4) (5)f(6)o(7)l(8)l(9)o(10)w(11)-(12)u(13)p(14) (15)L(16)L(17)M(18)
(19)r(20)e(21)s(22)p(23)o(24)n(25)s(26)e(27)s(28) (29)c(30)o(31)n(32)t(33)a(34)i(35)n(36)
(37)a(38)d(39)d(40)i(41)t(42)i(43)o(44)n(45)a(46)l(47) (48)t(49)o(50)o(51)l(52)_(53)c(54)a(55)l(56)l(57)s(58),(59)
(60)r(61)e(62)c(63)u(64)r(65)s(66)e(67) (68)i(69)n(70) = 70 chars. OK.
Line 2: processToolCalls instead of ignoring them. Add MAX_TOOL_CALL_DEPTH
p(1)r(2)o(3)c(4)e(5)s(6)s(7)T(8)o(9)o(10)l(11)C(12)a(13)l(14)l(15)s(16) (17)i(18)n(19)s(20)t(21)e(22)a(23)d(24)
(25)o(26)f(27) (28)i(29)g(30)n(31)o(32)r(33)i(34)n(35)g(36) (37)t(38)h(39)e(40)m(41).(42) (43)A(44)d(45)d(46)
(47)M(48)A(49)X(50)_(51)T(52)O(53)O(54)L(55)_(56)C(57)A(58)L(59)L(60)_(61)D(62)E(63)P(64)T(65)H(66) = 66 chars. OK.
Line 3: constant set to 5 to prevent infinite loops. Move clearPendingActions
c(1)o(2)n(3)s(4)t(5)a(6)n(7)t(8) (9)s(10)e(11)t(12) (13)t(14)o(15) (16)5(17) (18)t(19)o(20)
(21)p(22)r(23)e(24)v(25)e(26)n(27)t(28) (29)i(30)n(31)f(32)i(33)n(34)i(35)t(36)e(37) (38)l(39)o(40)o(41)p(42)s(43).(44)
(45)M(46)o(47)v(48)e(49)
(50)c(51)l(52)e(53)a(54)r(55)P(56)e(57)n(58)d(59)i(60)n(61)g(62)A(63)c(64)t(65)i(66)o(67)n(68)s(69) = 69 chars. OK.
Line 4: to appropriate recursion boundaries. Fix new-chat container flex layout
t(1)o(2) (3)a(4)p(5)p(6)r(7)o(8)p(9)r(10)i(11)a(12)t(13)e(14) (15)r(16)e(17)c(18)u(19)r(20)s(21)i(22)o(23)n(24)
(25)b(26)o(27)u(28)n(29)d(30)a(31)r(32)i(33)e(34)s(35).(36) (37)F(38)i(39)x(40)
(41)n(42)e(43)w(44)-(45)c(46)h(47)a(48)t(49) (50)c(51)o(52)n(53)t(54)a(55)i(56)n(57)e(58)r(59) (60)f(61)l(62)e(63)x(64)
(65)l(66)a(67)y(68)o(69)u(70)t(71) = 71 chars. OK.
Line 5: to wrap and align left.
t(1)o(2) (3)w(4)r(5)a(6)p(7) (8)a(9)n(10)d(11) (12)a(13)l(14)i(15)g(16)n(17) (18)l(19)e(20)f(21)t(22).(23) = 23 chars.
OK.
This looks good. But I should double check: does the body provide useful info not in the subject? Yes, it explains the
specific behavior change (recursing instead of ignoring), the constant name and value, and the CSS fix.
Actually, should I include the CSS fix in the body? It's part of the diff. If I omit it, the
- Persist selected model to settings when changed via dropdown
- Reorder chat controls: model selector before mode selector, new chat before delete
- Add null guards for modelSelectorEl during async populate
- Fix streaming state and empty response handling in tool nudge loop
- Expand vault operation keyword detection
- Enable text selection in chat messages
- Include original error message in chat error display
- Add `listModels()`, `setModel()`, and `getModel()` methods to `OllamaClient`
- Populate model dropdown in chat view from `/api/tags` endpoint
- Synchronize model selection across main and agent clients
- Detect user intent for vault operations from message content
- Expand action phrase matching for `shouldAutoRunReadTools()`
- Streamline retry logic and suppress "Let me..." text with actual tool calls
- Improve fallback search query generation by removing more filler words
Move send button after activity indicator in DOM order and reposition
elements via CSS. Set textarea rows to 3, increase min/max heights,
add flex-shrink to activity dot to prevent squishing, and fix
activity indicator padding and white-space handling.
Expand CRITICAL RULE instructions across edit, organize, and research agent
modes to explicitly prohibit planning language, reasoning, apologies, and
permission-seeking. Replace generic follow-up text with empty content when
tool calls are emitted.
Add a retry loop (up to 3 attempts) that suppresses text-only responses
and re-prompts the model to emit tool_calls immediately. If the model
still fails to call tools after retries, fall back to automatically
generated read tool calls based on the user's message intent.
Shows a pulsing dot with status text during workflow execution, LLM
thinking, and tool use. Disables the input and send button while active
to prevent duplicate submissions. Also restructures the chat layout to
use flexbox with the input container locked at the bottom.
Replace manual intent phrase matching with dedicated helpers that detect
when the assistant wants to use tools but didn't emit tool calls. When
detected, automatically build and execute read-only tool calls based on
the user's original message instead of nudging the model again.
This reduces back-and-forth latency for organize and research modes by
directly querying vault stats, tags, and files when the assistant
expresses intent like "let me read" or "I will check" but fails to
actually call tools.
Includes tests for the new auto-run behavior in organize mode.
Implement persistent chat history using a new ChatHistoryManager class.
Sessions are saved on view close, restored on open, and selectable via a
dropdown in the chat header. Includes delete and clear-all commands,
with automatic title generation from the first user message.
Tighten system prompts across edit, organize, and research modes to
prohibit "Let me..." / "I will..." prefatory text and require immediate
tool_call emission. Remove stale instructions referencing deleted tools.
Improve search_vault_files description for LLM clarity. Add runtime
auto-nudge: if a tool-capable mode emits no tool_calls but language
suggests intent to act, re-prompt the model to emit the required calls.
Extend the tool registry to support vault-wide tag listing and statistical
overview tools, adding them to READ, EDIT, ORGANIZE, and RESEARCH agent
modes. Includes implementations for tag aggregation, folder structure
reporting, and metadataCache integration in VaultIndexer.
- Add listener/history API to Logger for live log streaming
- Add toggle button and styled log panel to chat view
- Enhance ToolExecutor to use VaultIndexer for rich search results
- Instruct all agent modes to emit tool calls immediately instead of describing intent
Split the single model setting into `chatModel` and `agentModel` to allow
using different LLMs for conversational modes (Ask, Research) versus
agentic modes (Edit, Organize, Workflow, auto-organizer). Defaults are
`deepseek-v4-flash` for chat and `glm-5.1` for agents.
Includes backward compatibility migration from legacy `model` field,
updated settings UI, per-mode tool filtering via new `agent-modes.ts`
configs, and vault search scoring improvements (exact phrase, recency,
filename bonuses).
- chat-view.ts: include 'Tags: ...' prefix in vault context when tags exist
- chat-view.ts: update system prompt to instruct AI to pay attention to tags
when organizing or categorizing notes
- Add AutoTagger: scans untagged notes, generates AI tags via Ollama,
and applies them to frontmatter
- Add AutoLinker: finds semantically related notes via vault search
and appends a 'Related Notes' section with wiki-links
- Add settings UI for both features with configurable thresholds,
prompt templates, and limits
- Add commands: 'Auto-Tag Untagged Notes' and 'Auto-Link Related Notes'
- Add auto-organizer.test.ts with 17 tests covering tagging,
linking, frontmatter manipulation, and filtering
- main.ts: process files sequentially (not Promise.all) to avoid concurrent
embedding requests hammering Ollama; batch size reduced to 1
- vectorization.ts: shorten embedding prompts from 1000 to 500 chars,
limit headings to 5, remove frontmatter from prompt to stay well within
embedding model context window
- Update vectorization and indexing-pipeline tests for new prompt format
- Add AbortController to cancel ongoing indexing before clear/rebuild
- Track currentIndexingPromise to await cancellation before clearing collection
- Batch background indexing (5 files at a time) with 100ms delays between batches
- Skip incremental event-based indexing while a full rebuild is in progress
- Cancel indexing on plugin unload
- Fix nginx CORS headers on OPTIONS preflight responses
- src/semantic-cache.ts: Add robust URL sanitization in initialize().
Trim whitespace and reject malformed URLs (e.g. empty host like
'http://:8666') before passing to ChromaClient.
- src/main.ts: Validate ChromaDB URL in the settings tab onChange.
Fall back to 'http://localhost:8000' if the value is empty or lacks
'://'.
- docker-compose.yml: Add CHROMA_SERVER_CORS_ALLOW_ORIGINS=["*"] to
allow cross-origin requests from Obsidian's app://obsidian.md origin.
The plugin hard-capped streaming responses at 1000 chunks. For large
models like qwen2.5:32b generating detailed answers, this limit was
easily exceeded, causing the response to stop mid-sentence.
The Ollama stream already terminates naturally when the model sends the
final done signal, so the artificial chunk limit served no purpose.
- src/chat-view.ts: Remove chunkCount tracking and MAX_STREAM_CHUNKS
constant. Let the stream run until Ollama signals completion.
- src/types.ts: Add isThinking flag to ChatMessage to track transient
'model is working' state.
- src/chat-view.ts: Set isThinking: true on the assistant placeholder
message when user sends input. Clear it when the first stream chunk
arrives or on error. Update render() to show a spinner + 'Thinking…'
text while isThinking is active.
- styles.css: Add ollama-thinking-indicator class with a CSS spinner
animation and muted italic text styling.
- src/chat-view.ts: Add getIcon() returning 'bot' for the view tab icon.
Improve render() with role-specific CSS classes (user vs assistant) and
message header structure for better styling hooks.
- src/main.ts: Add ribbon icon ('bot') in the left sidebar that opens the
chat view with a single click.
- styles.css (new): Modern chat UI with message bubbles, distinct user and
assistant themes using Obsidian CSS variables, sticky input bar, styled
send button with accent color, and emoji role indicators.
- install.sh: Copy styles.css into the plugin directory and verify its
presence during installation.
- README.md: Include styles.css in manual install instructions.
- __mocks__/obsidian.ts: Add addRibbonIcon() mock for test compatibility.
- tests/chat-view.test.ts: Add getIcon() assertion.
- src/ollama-client.ts: Detect HTTP 404 on /api/chat and throw a descriptive
ApiError with the model name and the exact ollama pull command needed.
- src/error-handler.ts: For API_ERROR type, return the error message directly
instead of prefixing with 'API error: ', so the user-friendly 404 message
is shown cleanly in the Obsidian notice.
- tests/ollama-client.test.ts: Update 404 assertions to match the new
descriptive error message.
- src/semantic-cache.ts: Replace require('chromadb') with static import and
use proper ChromaClient/Collection types instead of any. Fix camelCase
API parameters (queryEmbeddings, nResults) and wrap single embedding into
Embedding[] for upsert. Fix clearCache to call client.reset() instead of
collection.reset() (matches actual chromadb API).
- src/workflow-engine/workflow-engine.ts: Fix unnecessary escapes in regex,
remove redundant 'as unknown' assertion, handle never type in template
literal, and add type annotations to replace callback to satisfy
no-unsafe-argument and no-base-to-string rules.
- tests/semantic-cache.test.ts: Update mocks to include client.reset() and
adjust clearCache assertions to match new implementation.
- Bundle chromadb into main.js via esbuild instead of externalizing it.
Obsidian's renderer cannot resolve bare require('chromadb') against a
plugin-local node_modules. By bundling, the client library is inlined and
works out of the box with just manifest.json + main.js.
- Remove eager cache initialization from OllamaClient constructor to avoid
unhandled promise rejections when chromadb/ChromaDB is unavailable.
- Fix 'Failed to execute fetch on Window: Illegal invocation' by wrapping
the default fetch fallback in an arrow function:
this.fetchFn = fetchFn ?? ((url, init) => fetch(url, init));
This preserves the window binding when fetch is called later.
- Update install.sh and README to remove the obsolete node_modules/chromadb
copy step.
- Update ollama-client-cache tests to reflect that cache initialization is
no longer eager.
Replace dynamic ESM import with require() for chromadb to ensure
Electron can resolve the package against the plugin's node_modules.
Also wrap default fetch fallback in an arrow function to avoid
potential strict mode issues with global fetch.
The root cause: Obsidian's plugin loader expects main.js at the plugin root
alongside manifest.json. The previous 'dist/' output + shim approach caused
'Cannot find module ./dist/main.js' because dist/ was either missing or not
resolved correctly in Obsidian's module loader.
Changes:
- build: use esbuild to bundle all source into a single main.js (61KB)
tsc --noEmit for type checking; esbuild for the actual bundle
- main.js: no longer a shim — it's the fully bundled plugin
- package.json: added esbuild as devDependency; obsidian moved to devDeps
- install.sh: remove dist/ copy step, add cleanup of old dist/ from vault
- README.md: updated manual install steps to reflect bundling
- package.json: move obsidian from dependencies to devDependencies
(it is a type stub — having it in dependencies risks shadowing
Obsidian's built-in API if node_modules is present in the plugin folder)
- main.js: explicitly extract default export from dist/main.js so the
shim works with both plugin.default and direct-export loader patterns
Obsidian's plugin loader expects main.js at the plugin root alongside
manifest.json — it does not resolve subdirectory paths in the 'main'
field. Added a thin CommonJS shim that re-exports from dist/main.js.
- main.js: new entry shim (module.exports = require('./dist/main.js'))
- manifest.json: 'main' changed from 'dist/main.js' to 'main.js'
- install.sh: copy main.js shim to plugin folder, verify its presence
- README.md: add main.js to manual install instructions