Commit graph

936 commits

Author SHA1 Message Date
Zero Liu
f2f90126bb Add test plan 2026-05-12 12:40:35 -07:00
Zero Liu
247885d0d1 fix: address Obsidian community scorecard warnings
Resolves 906 scorecard warnings from the Obsidian community plugin scan:
replace direct `setTimeout`/`document`/`atob`/`btoa`/`globalThis` with
`window.*` and `activeDocument`/`Buffer` equivalents for popout window
compatibility; swap `as TFile`/`as TFolder` casts for `instanceof` checks;
route file deletion through `FileManager.trashFile`; replace direct
`element.style` writes with Tailwind classes and CSS variables; tighten
many `any` types. Adds eslint rules to prevent regressions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 12:36:05 -07:00
Zero Liu
358b373953
chore: remove orphaned RemoveFromIndexModal (#2395)
The modal and its REMOVE_FILES_FROM_COPILOT_INDEX command were orphaned in
the vault search v3 migration (#1703): the addCommand registration was
removed but the modal file and its constant entries were left behind.
Nothing in the codebase has instantiated it since. Deleting it also clears
the risk-report finding about contentEl.createEl("style", ...), which
Obsidian disallows at runtime.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 10:45:15 -07:00
Wenzheng Jiang
38a23358cd
fix(miyo): send portable folder-prefixed path to related-notes endpoint (#2393)
findRelevantNotes and MiyoIndexBackend.getDocumentsByPath were sending the
local absolute filesystem path as Miyo's file_path. On remote-vault setups
the indexed path is the server's absolute path, so the scroll filter
matched zero points and the related-notes endpoint always returned 404.

Replace getMiyoAbsolutePath with getMiyoFilePath, which returns
"<FolderName>/<vaultRelativePath>" — the inverse of
getVaultRelativeMiyoPath. Miyo's resolveFileInput remaps this format to
the registered folder's canonical absolute path, so it works identically
for local and remote setups.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 09:52:04 +09:00
齐柯宇 (Qi Kery)
e826889c11
fix: use /responses for GitHub Copilot codex models (#2320)
* fix: use /responses for GitHub Copilot codex models
Route Copilot codex models away from /chat/completions to avoid 400
errors, and keep ping/model verification aligned with runtime routing.

* refactor: deduplicate shouldUseGitHubCopilotResponsesApi calls and restore rationale comments

  Cache the result in useCopilotResponses in both createModelInstance() and
  tryPing() instead of calling the helper multiple times per flow.

  Restore Reason comments removed in c96a9d8 that document non-obvious
  constraints: why ChatOpenAICompletions is used over ChatOpenAI, the
  delta.role fallback to avoid broken tool_call_chunks, defaultRole?: any
  type choice, streamUsage/baseURL SDK behavior, and the typeof Request
  guard for Obsidian mobile runtimes.
2026-05-10 17:06:26 -07:00
HT
baa2b954dc
fix: prevent </div> from being consumed by indented code blocks in think sections (#2343)
When thinking content ends with a 4-space-indented line (e.g. Gemma's
bullet-point reasoning), markdown's indented code block rule would consume
the immediately-following </div> closing tag and render it as literal
"&lt;/div&gt;" text. Fix by always trimming section content and appending
\n before the closing tag so it lands on its own unindented line.

Also adds integration tests covering non-streaming, streaming-complete,
and streaming-unclosed think block paths.

Co-authored-by: H <trulyshelton@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 17:02:55 -07:00
Arjun Pramanik
bfc7a4e017
docs(ai): unify AGENTS.md and CLAUDE.md (#2358) 2026-05-09 14:27:22 -07:00
Octopus
c7a1d36b3b
fix: remove 100ms IME timeout causing Enter key delay (fixes #2354) (#2367)
The previous implementation used compositionstart/compositionend events
with a 100ms timeout to track IME state. This caused Korean (Hangul)
IME users to experience a noticeable delay when pressing Enter to send:
the composition-ending Enter was blocked, and subsequent Enters within
100ms were also blocked, requiring users to hold the key or wait.

In Chromium/Electron, the keydown event for IME-confirming Enter fires
with isComposing=true (before compositionend), so the explicit ref-based
tracking is redundant. Relying directly on event.isComposing and the
legacy keyCode 229 is sufficient and matches the simpler approach already
used in follow-up-input.tsx.

Co-authored-by: octo-patch <octo-patch@github.com>
2026-05-09 14:15:25 -07:00
Emt-lin
506de5477b
feat: migrate project storage from data.json to vault files (#2324)
* feat: migrate project storage from data.json to vault files

Migrate project definitions and metadata from data.json to individual
markdown files in the vault's projects directory.

- Project file manager with YAML frontmatter parsing and persistence
- Migration logic from legacy data.json format with unsupported/ backup
- Project UI components (AddProjectModal, ProjectList, ProgressCard)
- Chat history integration for the new storage model
- Status sorting, cache-first ordering, and URL removal in processing
- Atomic cache operations to prevent transient disappearance on rename
- Awaited context cache cleanup on all delete/rename/folder-switch paths
- Raw-content-first frontmatter parsing to avoid stale metadataCache
- BOM/CRLF support in frontmatter read and write helpers
- Accessibility: aria-labels on all icon-only buttons

* fix: address codex review feedback

- Defer vault listener registration from constructor to initialize()
  to avoid premature cache mutations and frontmatter writes during
  initial vault load (before onLayoutReady).

- Coerce numeric YAML project IDs to strings so bare numeric values
  like `copilot-project-id: 123` are not silently dropped.
2026-05-09 12:21:04 -07:00
Wenzheng Jiang
94737297b7
fix(miyo): strip vault folder-name prefix from indexed paths (#2390)
Miyo returns indexed file paths prefixed with the owning folder name
(the vault name), but getVaultRelativeMiyoPath was stripping the
absolute filesystem path prefix instead. As a result, the "List
Indexed Files" command rendered every entry as a broken Obsidian link
(e.g. [[MyVault/notes/foo.md]] instead of [[notes/foo.md]]).

Switch the strip logic to match what Miyo actually returns. Files from
other vaults pass through unchanged so cross-vault search results stay
intact. Always return a forward-slash-normalized path so callers see a
consistent separator. Drop the now-unused getVaultBasePath helper.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 21:21:07 +09:00
Zero Liu
37bea9a65c
fix(ollama): route through safeFetch when enableCors is set (#2382)
Other OpenAI-compatible providers (LM Studio, OpenAI-format, SiliconFlow,
etc.) already pass a fetch override so requests can go through Obsidian's
requestUrl API. The Ollama branch was missing this wiring, so toggling
"Enable CORS" on an Ollama model had no effect and mobile (WKWebView)
requests to http:// Ollama hosts failed with "Load failed" due to CORS /
mixed-content blocking.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 15:48:45 -07:00
Wenzheng Jiang
95d23b2387
release: v3.2.8 (#2369)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 20:43:24 +09:00
Wenzheng Jiang
2e3d6ba0d2
feat(miyo): add toggle to search all indexed content (#2353)
* feat(miyo): add toggle to search all indexed content across folders

Allow users to opt into searching everything in Miyo by omitting the
folder_name from search requests. This is enabled by default so Copilot
searches all indexed content. When disabled, searches are scoped to the
current vault folder only. Only affects the search endpoint — other Miyo
APIs (parseDoc, listFolderFiles, etc.) still send folder_name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(miyo): default miyoSearchAll to false

Scope searches to the current vault folder by default, which is the
safer default. Users can opt into cross-folder search via the toggle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:15:57 +09:00
Wenzheng Jiang
1960d86a3a
release: v3.2.7 (#2352)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:31:09 +09:00
Wenzheng Jiang
d8890ada6d
refactor(miyo): rename folder_path request param to folder_name (#2349)
Miyo server now supports folder_name, which better reflects the
semantic that we send a vault name rather than a filesystem path.
Also removes folder_path from response interfaces as the server
no longer returns it.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:49:20 +09:00
Wenzheng Jiang
c350f37d98
refactor(miyo): rename folderPath to folderName and send relative path in parse-doc (#2348)
The Miyo folder identifier is actually the vault name, not a filesystem
path. Rename getMiyoFolderPath -> getMiyoFolderName and all folderPath
params -> folderName across MiyoClient and callers to reflect this.

For parse-doc, send vault-relative file.path instead of absolute path,
and include folder_path in the request body for consistency with all
other Miyo endpoints. Wire-format keys (folder_path) are unchanged.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:05:24 +09:00
Logan Yang
8577705cba
fix(debug): remove idx column from search results debug table (#2344)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 05:15:55 -07:00
Wenzheng Jiang
ccf687b5cb
Use vault name for Miyo requests (#2342) 2026-04-05 22:21:20 +09:00
Wenzheng Jiang
7084705067
release: v3.2.6 (#2335)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 09:25:46 +09:00
Wenzheng Jiang
81fd4e5157
feat(miyo): add remote vault folder path override for remote servers (#2334)
When using a Miyo remote server, the vault may be mounted at a
different path on the remote machine. This adds an optional
"Remote Vault Folder" setting that overrides the folder path sent
to Miyo when a custom server URL is configured.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 09:17:17 +09:00
Wenzheng Jiang
47e61259d2
Refactor Miyo integration for folder API (#2331)
* refactor Miyo integration for folder API

* chore: remove Miyo related-search log

* docs: add Miyo Node Service API reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 09:05:34 +09:00
Wenzheng Jiang
c0d79efb7c
feat(miyo): disable miyo on mobile without a remote server URL (#2328)
* feat(miyo): disable miyo on mobile without a remote server URL

Adds `shouldUseMiyo()` to `miyoUtils.ts` as the single source of truth
for whether Miyo should be active. On mobile, Miyo is silently ignored
unless an explicit remote server URL is configured, since local service
discovery is unavailable on mobile. The three previously duplicated
`shouldUseMiyo` checks in RetrieverFactory, findRelevantNotes, and
VectorStoreManager now delegate to this shared function.

Also drops the redundant `enableSemanticSearchV3` guard — the UI already
enforces that enabling Miyo enables semantic search and disabling semantic
search disables Miyo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(miyo): align RelevantNotes UI check with shouldUseMiyo

Replaces the local `shouldUseMiyoIndex` predicate (which duplicated the
self-host grace-period logic and missed the mobile platform guard) with a
direct call to `shouldUseMiyo` from miyoUtils. This ensures the UI's
index-state check matches what the backend actually uses, so the
"Build Index"/"No relevant notes" state is always correct on mobile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(miyo): update findRelevantNotes mock to use shouldUseMiyo

Replace the isSelfHostAccessValid mock with shouldUseMiyo, which is now
the single gate checked by findRelevantNotes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 09:12:23 +09:00
Wenzheng Jiang
d6283e3151
feat(miyo): rename vault name to remote vault path and align with server API (#2326)
* feat(miyo): rename vault name to remote vault path and align MiyoClient with server API

- Rename `miyoVaultName` setting to `miyoRemoteVaultPath` with migration from old key
- Rename UI field "Vault Name" → "Remote Vault Path (Optional)" and "Custom Miyo Server URL" → "Remote Miyo Server URL (Optional)"
- Always show Remote Vault Path field (not gated by server URL); both fields default to blank with no placeholder
- Show bot name indicator (remote vault path + local/remote label) under Enable Miyo when enabled
- Rename `sourceId` parameters in MiyoClient public methods to `vault` to match the actual server API field name
- Update `searchRelated` options property from `sourceId` to `vault`
- Update call sites in findRelevantNotes.ts accordingly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(miyo): remote vault path saves silently without triggering re-index

It is a remote-only identifier — changing it has no effect on the
local index, so no confirmation modal or re-indexing is needed.
Also drops the now-unused logWarn import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(miyo): remote vault path only overrides when remote server URL is also set

getMiyoSourceId now requires both miyoRemoteVaultPath and miyoServerUrl to be
set before using the remote path — if only the path is set (no server URL),
it falls back to the auto-detected local vault path. The settings indicator
applies the same logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(miyo): rename getMiyoSourceId → getMiyoVault, resolveMiyoSourceId → resolveMiyoVault

Removes the last "sourceId" terminology — all Miyo identifiers now consistently
use the "vault" name that the server API expects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(miyo): only show remote vault path indicator when remote server URL is set

Avoids showing the auto-detected local vault path under a "Remote vault path"
label, which was misleading when Miyo was running locally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(miyo): replace custom remote vault path input with plain text field

Removes the Apply button, pending state, useEffect, and handler — the field
now saves with the standard debounced onChange, matching the URL field above it.
Also drops the unused Input and useEffect imports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(miyo): show effective vault path under Enable Miyo when enabled

Displays the auto-detected local vault path when no remote server is
configured, or the remote vault path override when both remote URL and
remote vault path are set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(test): update findRelevantNotes test to use vault instead of sourceId

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 16:24:29 +09:00
Logan Yang
23fb3b42ef
chore: improve release agent to read PR descriptions for better notes (#2319)
The release agent now fetches and reads every PR's description before
writing release notes, instead of relying on titles alone. This
produces more accurate, user-facing release notes.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 16:26:52 -07:00
Logan Yang
79877e3ef5
feat(openrouter): add per-model toggle for prompt caching (#2318)
* feat(openrouter): add per-model toggle for prompt caching

OpenRouter's Zero Data Retention (ZDR) endpoints do not support
Anthropic's automatic prompt caching (cache_control), causing 404
errors. This adds an `enablePromptCaching` option (default: true)
so users can disable it per model in settings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(openrouter): show question mark tooltip for prompt caching toggle

Display a visible HelpCircle icon next to the label instead of making
the label itself the hover target. Tooltip text: "Disable if your
OpenRouter endpoint uses Zero Data Retention (ZDR)..."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move prompt caching toggle to top-level model settings

The toggle was nested inside the advanced parameters section. Move it
to the top level alongside Base URL and API Key so it's immediately
visible to OpenRouter users.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ui): simplify prompt caching to single checkbox with tooltip

Remove the redundant FormField heading — now just a checkbox labeled
"Prompt Caching" with a ? tooltip explaining ZDR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prompt caching applies to multiple providers, not just Anthropic

OpenRouter's cache_control supports OpenAI, Anthropic, DeepSeek,
Gemini, Grok, and others. Remove "Anthropic" from tooltip and docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 16:20:54 -07:00
Logan Yang
01c2c7c9fc
release: v3.2.5 (#2314)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 15:00:02 -07:00
Logan Yang
8b2b694e21
fix(ui): show enabled models without API keys as disabled in dropdown (#2313)
* fix(ui): show enabled models without API keys as disabled in dropdown

Previously, enabled models without API keys were hidden from the chat
input model dropdown, making it confusing since "Enable" in settings
didn't mean the model would appear. Now they show as greyed-out disabled
items with a "Needs API key" label.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: reorder BUILTIN_CHAT_MODELS so enabled models are listed first

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 06:47:50 -07:00
Logan Yang
8e9970afd3
feat: add PR pricing agent for sizing and pricing PRs (#2312)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 05:43:42 -07:00
Wenzheng Jiang
ddb16af59c
feat(miyo): add customizable vault name setting (#2311)
* feat(miyo): add customizable vault name setting for Miyo indexing

Allows users to set a custom vault name that Miyo uses as the source ID
for indexing, instead of the auto-detected vault path. When changed while
Miyo is enabled, clears the old index and triggers a full re-index under
the new name. Includes guidance for remote/multi-device setups to use a
consistent vault name across devices.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(miyo): compare resolved source IDs before clearing index on vault name change

Avoids unnecessary index clear and full re-index when the user types the
auto-detected vault path (the value that the blank field resolves to),
since the effective Miyo source ID is unchanged in that case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(miyo): prevent applying an empty vault name

The Apply button is hidden when the input is blank, and the handler
guards against an empty string, so users cannot accidentally clear
the vault name and submit it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(miyo): allow clearing vault name to restore auto-detected vault path

An empty vault name is valid — it falls back to the auto-detected vault
path as the Miyo source ID. Removes the empty-string guard so users can
clear a previously set name and revert to the default behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 10:57:06 +09:00
Wenzheng Jiang
d308423315
Composer V2: Replace replaceInFile with editFile as the primary targeted-edit tool (#2305)
* feat(editFile): write-first with inline diff in chat

Replace the blocking accept/reject modal in editFile with a write-first
approach: changes are written to disk immediately, and a unified diff is
returned inline in the chat as a fenced \`\`\`diff block. Multiple
editFile calls in a single agent turn each produce their own diff
without requiring any user interaction.

Also cleans up associated rename/refactor of writeToFileTool →
writeFileTool across chain runner, streamer, and related tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(editFile): add Show Diff button and side-by-side diff view in ApplyView

- Pipe editFileDiffs through message types (ChatMessage, StoredMessage) and
  MessageRepository so writeFile/editFile diffs are stored as a typed field
  (same pattern as sources)
- Collect diffs in AutonomousAgentChainRunner.runReActLoop() by parsing the
  JSON result of successful writeFile/editFile tool calls, then pass through
  ReActLoopResult → handleResponse() → ChatMessage.editFileDiffs
- ChatSingleMessage reads message.editFileDiffs directly (instead of trying
  to parse TOOL_CALL markers which don't exist in agent mode)
- ChatButtons shows a "Show diff" button when editFileDiffs is present;
  clicking it opens ApplyView for each diff
- ApplyView redesigned: renders change blocks side-by-side (original left
  with red highlights, modified right with green highlights) using the
  SideBySideBlock component, unchanged blocks shown as plain text between
  change blocks — restores the classic diff block presentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Revert "feat(editFile): add Show Diff button and side-by-side diff view in ApplyView"

This reverts commit 80945493b48d50c26b9a01bc40de5eced2fcfd18.

* feat(editFile): show ApplyView with simple mode (no per-block buttons)

- editFile now shows the Apply View preview and blocks until user accepts/rejects
- Respects autoAcceptEdits setting (bypasses preview when enabled)
- Adds `simple` flag to ApplyViewState to hide per-block accept/reject/revert buttons
- Removes unused createPatch import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(tools): update writeFile description to "Create or rewrite files in your vault"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address Codex review — GPT prompt, fuzzy match safety, tool ID migration

- modelAdapter: update GPT prompt to use editFile's oldText/newText params
  instead of the old SEARCH/REPLACE block syntax
- ComposerTools: fix fuzzy match to replace only the matched span in the
  original content; previously the entire file was rebuilt from the
  fuzzy-normalized string, inadvertently applying trimEnd and quote/dash
  normalization to text outside the matched span
- settings/model: migrate persisted autonomousAgentEnabledToolIds from
  writeToFile→writeFile and replaceInFile→editFile on upgrade

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(editFile): add unit tests for fuzzy match via applyEditToContent

Extract pure replacement logic into applyEditToContent() for testability,
then wire editFileTool.func to call it (removes duplicated logic).

Tests cover:
- Exact match: basic replacement, deletion, NOT_FOUND, AMBIGUOUS
- Fuzzy via smart quotes (single, double), en-dash, NBSP
- Fuzzy via trailing whitespace (spaces, tabs)
- Key invariant: fuzzy match only replaces the matched span — smart quotes
  and trailing spaces outside the span are left untouched
- Multiline fuzzy match with mixed artifacts
- CRLF and LF preservation
- BOM preservation

Also fixes mapFuzzyIndexToNormal to extend matchEnd to the full original
line length when the fuzzy match ends at end-of-line, preventing orphaned
trailing whitespace after replacement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address Codex review — NFKC position mapping and legacy tag compat

- ComposerTools: fix mapFuzzyIndexToNormal to handle NFKC expansion
  (e.g. Ⅳ → IV) by walking char-by-char within each line instead of
  assuming a 1:1 column mapping; adds two unit tests covering this case
- ChatSingleMessage: normalise legacy <writeToFile> tags to <writeFile>
  before rendering so saved chat history from before the tool rename
  continues to display correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(modelAdapter): update assertions for editFile oldText/newText prompt

Replace stale SEARCH/REPLACE references with the new parameter names.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(editFile): replace sentinel strings with ApplyEditResult discriminated union

- `applyEditToContent` now returns `{ ok: true; content: string } | { ok: false; reason: 'NOT_FOUND' } | { ok: false; reason: 'AMBIGUOUS'; occurrences: number }` instead of raw sentinel strings, eliminating false positives when file content matches 'NOT_FOUND' or starts with 'AMBIGUOUS:'
- Update `editFileTool` to pattern-match on the discriminated union
- Update `cleanMessageForCopy` regexes to also strip legacy `<writeToFile>` tags from saved chat history
- Update all `applyEditToContent` test assertions to use the new structured return type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(editFile): sanitize file path before vault lookup

Apply `sanitizeFilePath` in `editFileTool` to match the same treatment
`writeFileTool` already does, preventing "File not found" failures when
the path has a basename that exceeds 255 bytes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(editFile): guard against degenerate fuzzy match inside NFKC expansion

When `oldText` fuzzy-matches a span whose boundaries both map to the same
position in the original (e.g. searching "I" inside fuzzy "IV" derived from
"Ⅳ"), `matchStart >= matchEnd` and the replacement would be zero-width or
corrupt. Return NOT_FOUND instead of silently producing a wrong edit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(builtinTools): remove invalid daily:append/daily:prepend from prompt

The obsidianDailyNote tool schema only accepts 'daily', 'daily:read', and
'daily:path'. The prompt was incorrectly advertising non-existent
daily:append and daily:prepend commands, which would cause LLM-generated
tool calls to fail schema validation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(editFile): add Stage 3 to tolerate trailing newline mismatch at EOF

LLMs frequently append \n to the last line of oldText even when the file
has no final newline. Neither exact nor fuzzy match would find this, causing
avoidable NOT_FOUND failures. Stage 3 retries with one trailing \n stripped
from oldText (and mirrors the trim on newText to preserve the file's
no-trailing-newline format).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): update editFile integration test to oldText/newText shape

The argumentValidator was still asserting the legacy args.diff SEARCH/REPLACE
payload. Updated to check args.oldText and args.newText to match the new
editFile tool contract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(editFile): count overlapping matches to prevent wrong-location edits

countOccurrences was advancing by searchText.length, so overlapping patterns
like "aba" in "ababa" were counted as 1 (non-overlapping) instead of 2. This
caused an ambiguous match to be silently applied at the first position rather
than returning AMBIGUOUS. Advance by 1 to catch all overlapping occurrences.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(editFile): round-trip check guards partial NFKC-expansion fuzzy matches

The existing matchStart >= matchEnd guard only caught degenerate zero-width
spans (e.g. "I" in "Ⅳ"). A partial match like "V" against "Ⅳ" produces a
valid non-zero span [0,1) but would replace the entire source character even
though "V" doesn't exist standalone in the file.

Fix: after mapping indices back to the original, verify that
normalizeForFuzzyMatch(original[matchStart:matchEnd]) === workingSearch.
If not, the match straddled an NFKC expansion boundary → return NOT_FOUND.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(editFile): return structured failed result on edit errors

Replace plain string returns with `{ result: "failed", message }` objects
for all failure paths in editFileTool (file not found, text not found,
ambiguous match, and exceptions), matching writeFileTool's pattern so
the LLM and result formatters can reliably detect failures via the
`result` field. No-op case returns `result: "accepted"` since the edit
technically succeeded.

Also fix a stale tool name reference in builtinTools prompt instructions
(writeToFile → writeFile).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 18:51:39 +09:00
Logan Yang
e3d47adfa2
feat(lm-studio): use Responses API for LM Studio models (#2306)
Switch LM Studio from /v1/chat/completions to /v1/responses via a thin
ChatLMStudio wrapper that patches LangChain compatibility issues
(text.format requirement, strict:null in tool definitions).

- New ChatLMStudio class with fetch wrapper for tool sanitization
- Opt-out toggle in model settings (useResponsesApi)
- Ping uses ChatLMStudio to test the correct endpoint
- ThinkBlockStreamer: strip special tokens from text content

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 19:37:38 -07:00
Wenzheng Jiang
8b43355c77
fix(search): allow remote backends to re-index on mobile when disableIndexOnMobile is enabled (#2308)
The disableIndexOnMobile guard was blocking all re-indexing on mobile,
including the Miyo backend which is a remote HTTP service with no local
index. Add isRemoteBackend() to SemanticIndexBackend interface so guards
in IndexEventHandler and VectorStoreManager can skip the check for
remote backends.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:21:38 +09:00
Logan Yang
ce7c51f336
refactor(tools): remove daily note append/prepend CLI commands (#2307)
Remove daily:append and daily:prepend from the obsidianDailyNote tool.
These commands overlapped with writeToFile/replaceInFile, creating
ambiguity for the agent when the user asks to add content to a daily
note. Users should use writeToFile or replaceInFile instead.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 05:49:49 -07:00
Michael Ryan
87ee9d0d56
fix(rename): Add automatic file renaming to match generated topic titles (#2240)
* fix(rename): Add automatic file renaming to match generated topic titles

Adds automatic file renaming so that notes reflect the finalized AI-generated
topic in their filename. I believe this restores previously expected behavior.

Refs #2005

* Fix chat file renaming to respect original project and hidden directories

- Pass captured project context to generateFileName for async rename,
  ensuring the filename uses the project active at save time rather than global current-project state.
- Add fallback for hidden-directory files when reading epoch for rename,
  using the vault adapter if metadataCache is missing, so AI-generated topic renaming works for hidden folders.

Fix #2240

* fix: honor explicit null project context in async chat renaming

Refactor ChatPersistenceManager to distinguish between 'undefined' project (fallback to global) and 'null' project (explicitly no project). This prevents a race condition where non-project chats were incorrectly moved into project directories upon async file renaming.

Key changes:
- Updated 'generateFileName' to use strict undefined checking for project fallback.
- Reordered method signatures for 'generateFileName', 'generateTopicAsyncIfNeeded', and 'renameFileToMatchTopic' to prioritize the project context.
- Verified all internal call sites have been updated to match the new signatures.
- Confirmed fix via unit tests and local compilation.
2026-03-16 04:34:14 -07:00
Logan Yang
1bf571d63f
fix(tools): daily note template workflow for past/future dates (#2304)
* fix(tools): improve daily note template workflow for past/future dates

Update prompt instructions for creating past/future daily notes to
use template:read for template content, with a fallback to ask the
user for the template path if the Templates plugin isn't configured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tools): list templates before reading in daily note workflow

The workflow told the agent to call template:read directly without
the required name argument. Now it explicitly lists templates first
to discover the name, then passes it to template:read.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tools): route task queries to obsidianTasks instead of localSearch

Add explicit prompt instruction to always use obsidianTasks for task
queries, not localSearch. For time-based task queries, use verbose
mode and filter by file dates. localSearch searches note content but
doesn't understand markdown checkbox semantics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tools): reduce reasoning noise and fix tasks schema hint

Skip redundant success reasoning steps (e.g. "Listed vault tasks"
after "Listing vault tasks"). Only show result steps on failure or
when they add info (search source counts).

Also fix tasks tool schema: the LLM kept guessing command="list"
instead of "tasks" because z.literal lacked a clear hint. Added
'Must be exactly "tasks"' to the description.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 23:30:05 -07:00
Logan Yang
00fa339506
feat(tools): add base:create command and .base active note support (#2303)
* feat(tools): add base:create command and .base active note support

Add `base:create` to the obsidianBases CLI tool, enabling creation of
new items in Obsidian Bases through natural language. Also add .base
files as recognized active notes so they appear in chat context.

Changes:
- Add base:create command with name, content, view params to CLI tool
- Support .base files as active notes in chat context
- Add TEXT_READABLE_EXTENSIONS and ALLOWED_NOTE_CONTEXT_EXTENSIONS to
  constants.ts as single source of truth for file extension checks
- Use isTextReadableFile() in contextProcessor and utils instead of
  hardcoded extension checks
- Register .base in MarkdownParser for content reading
- Fix inaccurate filter syntax in Base YAML prompt instructions
- Fix broken lint-staged eslint flag (--no-warn-ignored not in eslint 8)
- Update design doc with base:create in v1 tier and write policy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tools): improve LLM routing for .base file operations

Add explicit disambiguation in both writeToFile and obsidianBases
prompts to prevent the LLM from using Composer for .base operations.
writeToFile now excludes .base files by default, and obsidianBases
declares itself as the primary tool for all .base operations with
clear examples of when each tool should be used.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tools): add execution-time guard for .base file routing

Add a code-level guard in executeSequentialToolCall that intercepts
writeToFile/replaceInFile calls targeting .base files and redirects
the LLM to use obsidianBases instead. This is more reliable than
prompt-only disambiguation since it works regardless of what the LLM
decides.

The guard only fires when obsidianBases is available (desktop). On
mobile or when the tool is disabled, writeToFile proceeds normally.
The error message tells the LLM exactly which commands to use, so it
self-corrects on the next iteration.

Also simplifies the prompt instructions by removing the verbose
disambiguation text that wasn't working reliably.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(composer): respect user auto-accept setting over LLM confirmation param

The writeToFile tool allowed the LLM to bypass the diff preview by
passing confirmation=false, even when the user had not enabled
auto-accept edits in settings. Now the preview is always shown unless
the user has explicitly enabled auto-accept. The LLM's confirmation
parameter is ignored.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(tools): add daily command to create today's daily note

The agent had no way to create a daily note - daily:append/prepend
require non-empty content and don't apply templates. Add the CLI
`daily` command which creates today's daily note with the user's
configured template and folder from the core Daily Notes plugin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tools): allow composer to create new .base files

The execution-time guard was blocking writeToFile for ALL .base paths,
including creating brand new .base files. Now the guard only redirects
when the .base file already exists (editing an existing base should
use CLI). Creating a new .base file via composer proceeds normally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tools): improve Base YAML filter syntax and add validation step

Fix LLM generating invalid Base YAML by adding explicit filter format
rules: single string or and/or/not objects only, bare YAML lists are
invalid. Also add a validation step that calls base:views after
creating a .base file to verify it parses correctly, enabling the
agent to self-correct on errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tools): remove .base file execution guard

The guard was too aggressive - it blocked legitimate .base YAML edits
(adding columns, changing filters) because the CLI has no commands to
modify .base file structure. Editing .base YAML requires writeToFile.
Routing between CLI (base:create, base:query) and composer (YAML
edits) is now handled entirely by prompt instructions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restrict embedded note segment parsing to markdown files only

Revert the embed gate from isTextReadableFile() back to .md-only.
The extractMarkdownSegment logic treats # as a heading reference,
but .base files use # for view names (![[Library.base#To Read]]).
Routing .base embeds through heading parsing would fail and return
an error block instead of usable context.

The chain restriction check (Plus-mode gating) correctly uses
isTextReadableFile since it's about access control, not parsing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:08:14 -07:00
Logan Yang
31492eb159
feat: enhance CLI tool instructions from obsidian-skills reference (#2301)
Improve agent prompt instructions for all Obsidian CLI tools based on
kepano/obsidian-skills reference:

- dailyNote: add inline flag, escape sequences, template workflow
- properties: add sort=count, file= vs path= targeting, modification tip
- tasks: add status character filter examples
- links: add counts, all flag, file targeting docs
- bases: add full YAML structure, filter syntax, formula functions,
  view types, property types, quoting rules, and embed syntax

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:19:44 -07:00
Logan Yang
31ad9b825c
fix(agent): improve inline citations, query dedup, and answer source priority (#2300)
* fix(agent): improve inline citations, query dedup, and answer source priority

- Make extractSourcesSection handle --- separator and bare trailing
  footnotes so agent mode citations become clickable
- Re-add citation format reminder before [User query]: via
  getCitationFormatReminder + injectGuidanceBeforeUserQuery
- Move linkInlineCitations to run after all segments are rendered
- Improve query dedup metric to max(jaccard, containment) with 0.6
  threshold for better subset-style duplicate detection
- Add 4-tier answer source priority: explicit context > vault > web >
  baked-in knowledge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: defer linkInlineCitations until streaming completes

Skip DOM-walking citation linkage during streaming since the sources
section is incomplete and the DOM is rebuilt every chunk anyway.
Citations become clickable all at once when the stream finishes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Codex review comments - HR sources validation and lastIndexOf

- Strategy 2 of extractSourcesSection now requires ALL non-empty lines
  after the --- separator to be footnote definitions, preventing content
  loss when --- appears as a divider before mixed content (P1)
- injectGuidanceBeforeUserQuery uses lastIndexOf instead of indexOf to
  target the final [User query]: label, avoiding misinjection when tool
  output contains that literal string (P2)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: citation placeholder badges during streaming

- Wrap raw [^N] footnote marks in placeholder spans during streaming
  to prevent markdown renderer from showing bare superscript numbers
- Style placeholders as small muted superscript pills (copilot-citation-ref)
- Style final clickable citations as matching superscript pills with
  accent color (copilot-citation-group)
- Seamless visual transition: same pill shape, muted -> accent colored

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 23:41:31 -07:00
Logan Yang
728ccb25d4
fix: restore expanded search limits for time-range and tag queries (#2295)
* fix: restore expanded search limits for time-range and tag queries

PR #2273 removed the agent-facing returnAll parameter but also
removed the implicit expanded limits that time-range and tag-focused
queries relied on. This restores the behavior: when a search has a
timeRange or tag terms, automatically use RETURN_ALL_LIMIT (100)
instead of the default maxSourceChunks, and pass returnAll to the
underlying retrievers. The agent still cannot explicitly request
returnAll via the tool schema.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: two-tier search result formatting to reduce context bloat for expanded queries

When search results exceed maxSourceChunks, split into two tiers:
- Tier 1 (first maxSourceChunks docs): full content XML as before
- Tier 2 (overflow docs): metadata-only with title, path, mtime, 150-char snippet

Achieves ~85% context reduction for expanded-limit queries while preserving
discoverability. Adds formatMetadataOnlyDocuments() as a pure, testable utility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: two-tier search formatting for time-range and tag queries

- Add isFilterOnlyResults() and isTimeDominantResults() helpers in searchResultUtils
- Update formatMetadataOnlyDocuments() with configurable snippetLength (default 300)
- Update prepareLocalSearchResult() with three-case logic:
  - Time-dominant: sort by mtime desc, top maxSourceChunks get full content, rest metadata-only
  - Tag-only: all docs get metadata-only (no ranking available)
  - Ranked: existing behavior unchanged
- Extract FILTER_SOURCES to module-level constant to avoid per-call Set allocation
- Add tests for isFilterOnlyResults, isTimeDominantResults, and custom snippetLength

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: update readNote instruction in metadata-only search results

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Codex P1/P2 review findings for two-tier search

P1: Remove title-match from FILTER_SOURCES so explicit note references
([[Note Name]]) get full content in tier 1, not metadata-only. This
restores prior behavior where title-match queries provided full content
without requiring an extra readNote tool call.

P2: Fall back to parsing tags directly from the query string when
salientTerms has no tag terms. Mirrors the regex logic in FilterRetriever
so needsExpandedLimits is true even when salientTerms is incomplete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* revert: remove unrelated Prettier formatting changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: preserve no-results marker and sanitize metadata snippets

- Guard the metadata-only path with tier2Docs.length > 0 so empty-result
  searches fall through to formatSearchResultsForLLM and return the
  expected "No relevant documents found." marker instead of an empty string.
- Apply sanitizeContentForCitations to doc.content in
  formatMetadataOnlyDocuments before slicing the snippet, preventing
  leaked citation markers (e.g. [^12], [3]) from note content into the
  tier-2 metadata prompt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: decouple timeDominant detection from filterOnly

Time-range queries via FilterRetriever often produce a mix of
"time-filtered" and "title-match" (daily notes from getTitleMatches)
docs. Since "title-match" is not in FILTER_SOURCES, isFilterOnlyResults
returns false for these result sets, making the old `timeDominant =
filterOnly && isTimeDominantResults(...)` always false — skipping the
mtime-based recency sort for time-range queries.

Fix by evaluating isTimeDominantResults independently: if any doc has
source "time-filtered", use mtime sort regardless of whether all docs
are filter-only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: populate tagTerms from query when salientTerms misses hashtags

When salientTerms is incomplete (e.g. query="#python" with empty
salientTerms), tagTerms was always an empty array, so SelfHostRetriever
received no server-side tag filters despite expanding to RETURN_ALL_LIMIT.

Extract hashtags from the raw query using the same regex as FilterRetriever
as a fallback, and assign the result directly to tagTerms so all retriever
paths (including SelfHostRetriever) get proper tag constraints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 20:45:22 -07:00
Logan Yang
89711d5136
feat: add Gemini Embedding 2 preview model support (#2299)
Adds `gemini-embedding-2-preview` to the builtin embedding model list.
This is Google's latest multimodal embedding model with 8K token input
limit and up to 3072 output dimensions, offering improved quality over
the existing gemini-embedding-001.

Closes #2298

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 20:33:40 -07:00
Logan Yang
095a5f2011
chore: fix pre-commit hook to enforce formatting and linting (#2297)
The pre-commit hook existed but was not executable and used raw
prettier instead of lint-staged. This fixes:

- Make .husky/pre-commit executable (was -rw-r--r--)
- Use `npx lint-staged` instead of raw prettier/eslint commands
- Remove deprecated husky v4 config from package.json (hooks key)
- Remove unnecessary `git add` from lint-staged (auto-staged in v15)
- Add eslint --fix to lint-staged for JS/TS files

All contributors now get automatic formatting + linting on commit.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 20:10:05 -07:00
Logan Yang
1f2539712d
feat(openrouter): enable prompt caching via cache_control (#2279)
* feat(openrouter): enable prompt caching via cache_control field

Add `cache_control: { type: "ephemeral" }` to all OpenRouter invocation
params so Anthropic models automatically detect cache breakpoints and
reduce token costs. Other providers (OpenAI, DeepSeek, Gemini, Grok,
Groq) either auto-cache without this field or safely ignore it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(openrouter): gate cache_control to OpenRouter endpoints only

ChatOpenRouter is shared by LM_STUDIO and COPILOT_PLUS providers which
use their own OpenAI-compatible base URLs. Sending cache_control to
strict OpenAI-format backends causes 4xx request failures. Detect
OpenRouter by checking the baseURL and only inject cache_control when
talking to openrouter.ai.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update Claude Sonnet builtin model to claude-sonnet-4-6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update builtin models - add gemini-3.1-flash-lite-preview, reduce enabled defaults

- Update Claude Sonnet to claude-sonnet-4-6
- Add gemini-3.1-flash-lite-preview (enabled, projectEnabled)
- Disable most builtin models by default to reduce clutter
- Keep enabled: Copilot Plus Flash, OpenRouter Gemini 2.5 Flash,
  GPT-5.2, GPT-5 mini, Claude Sonnet 4.6, Gemini 2.5 Flash,
  Gemini 3.1 Flash Lite Preview
- Update Bedrock example placeholder to claude-sonnet-4-6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 17:18:01 -07:00
Logan Yang
25e3faa088
feat: unify Azure OpenAI and Azure Foundry into single Azure provider (#2291)
* feat: unify Azure OpenAI and Azure Foundry into single Azure provider

Combines the two Azure providers into one. Users can now provide the
full endpoint URL as the base URL and skip the legacy instance name,
deployment name, and API version fields. The URL is automatically
normalized (strips /chat/completions, extracts api-version from query
params). Legacy field-based configuration still works for backward
compatibility.

Changes:
- Rename Azure OpenAI label to "Azure" with updated example host
- Remove azure-openai builtin model (users add models manually)
- Add normalizeAzureUrl() to handle full URLs pasted by users
- Make legacy Azure fields optional in ModelAddDialog when base URL set
- Update CURL generation to use base URL directly when provided
- Disable Responses API for Azure provider (not supported by Azure)
- Default api-version to 2024-05-01-preview when not specified

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Codex review - Azure embedding validation and curl model field

- Require legacy Azure fields (instanceName, deploymentName, apiVersion) for
  embedding models even when baseUrl is provided, since EmbeddingManager reads
  those fields directly and never consumes baseUrl
- Include model name in Azure base-URL curl payloads for both chat and embedding
  requests so the copied curl command works with Azure Foundry /models endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: show Azure legacy fields in UI for embedding models even when baseUrl is set

The renderProviderSpecificFields early-return was hiding instanceName,
embeddingDeploymentName, and apiVersion for all Azure models when baseUrl
was present. But validateFields() requires those fields for embedding models
regardless of baseUrl. Now only chat models skip the legacy UI fields when
baseUrl is provided; embedding models always render them.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: trim baseUrl before validation and skip Responses API verbosity for Azure

- Use model.baseUrl?.trim() in validateFields() and renderProviderSpecificFields()
  so whitespace-only input cannot bypass required-field validation
- Skip text.verbosity in getOpenAISpecialConfig() for Azure models, since this
  PR intentionally excludes Azure from useResponsesApi and the Responses-API
  verbosity param would produce a broken payload for Azure GPT-5 with verbosity set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:41:14 -07:00
Logan Yang
d3ee3f999b
fix: agent loop improvements, clickable citations, and UI fixes (#2287)
* fix: prevent agent ReAct loop from getting stuck with small local models

Small local models (e.g. qwen3.5-4b-mlx) get stuck calling localSearch
repeatedly with near-identical queries and never synthesizing an answer.

- Remove QueryExpander from agent loop to avoid model contention on
  single-connection local models (retriever still expands internally)
- Add query deduplication that pre-filters tool call batches using
  Jaccard word-overlap similarity before execution
- Force synthesis via raw model (without tools) after 2 consecutive
  iterations where all tool calls are duplicates
- Add agent-specific system prompt guidance for search limits
- Strip leaked chat template role tokens from intermediate content
- Add intermediate model output logging for debugging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: make inline citation numbers clickable and restore Max Sources setting

Inline citation numbers like [1], [2] in chat responses are now clickable
links that open the corresponding source note. Also restores the Max Sources
slider in QA settings, reverting the hardcoded DEFAULT_MAX_SOURCE_CHUNKS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove excessive 32px bottom padding from chat view on desktop

The padding-bottom on .view-content used max(safe-area + 4px, 32px),
forcing a 32px minimum even on desktop where safe-area-inset-bottom is 0.
Simplified to safe-area + 4px so desktop gets minimal padding while
devices with safe areas still get proper spacing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(agent): track dedup queries post-execution to keep failed searches retryable

Move previousSearchQueries tracking from before to after each localSearch tool
call executes. Previously, queries were marked as seen before any tool ran, so
if a call was aborted or the loop threw early, those queries were still blocked
from retrying on the next iteration. Now each query is added to
previousSearchQueries immediately after its tool call returns, keeping the dedup
effective for completed calls while leaving transient failures retryable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(citations): preserve Obsidian internal-link attributes on inline citation anchors

When building clickable inline citation links (e.g. [1], [2]), the previous code
only copied the href from the source section anchor. Obsidian vault-note links have
additional attributes such as data-href and class="internal-link" that are required
for in-app note navigation. Without them, clicking an inline citation resolves the
link as a plain relative URL instead of opening the corresponding note.

Fix: store the source anchor element rather than just the href, then copy all
attributes onto the generated citation anchor before setting the citation-specific
class and aria-label overrides.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(agent): only mark localSearch queries as seen after successful execution

Change the dedup tracking condition from always tracking queries post-execution
to only tracking when the tool call succeeds (result.success is true). This
ensures transient failures (e.g. temporary index unavailability) leave the
query retryable: if a localSearch fails, the model can issue the same query
again on the next iteration. Previously, even a failed execution would mark
the query as seen, blocking recovery.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: deduplicate inline citations after source consolidation

When multiple chunks come from the same note, the LLM cites them
separately. After consolidation maps them to the same source number,
adjacent duplicates like [1][1] or [1] and [1] now collapse to [1].

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update GPT-5.2 to GPT-5.4 in builtin models

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add dynamic status bar clearance for desktop chat view

The chat input was hidden behind Obsidian's status bar on desktop.
Adds ChatViewLayout which measures the geometric overlap between
the view-content and the status bar, setting a CSS variable to
provide the exact padding needed. Adapts to any theme: no clearance
for auto-hide themes (opacity: 0) or themes that already account
for the status bar in their layout; correct clearance for themes
where the bar is visible and overlapping. Re-checks on theme
switches via debounced css-change listener.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: suppress thinking tokens during streaming when reasoning is disabled

When a local model (e.g. Qwen 3.5 via LM Studio) emits text-level
<think> tags but the user has not enabled the reasoning capability,
the thinking content was visibly rendered during streaming and only
stripped once the </think> tag arrived. Now tracks the position of
excluded think blocks across chunks and truncates fullResponse to
the safe prefix on each chunk, so thinking content never appears.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 16:17:37 -07:00
Logan Yang
1703b283d0
fix: add "None" option to system prompt dropdown in chat settings (#2293)
The chat settings popover (gear menu) was missing the "None (use
built-in prompt)" option that exists in the Advanced settings tab.
Users could not clear a custom system prompt from the chat settings.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:05:11 -07:00
Emt-lin
9fdc91f79a
fix: Quick Ask/Command panel positioning overhaul (#2278)
- Dual-anchor model: use selection.to for below, selection.from for above,
  with focusPos for reverse selections, preventing panel from obscuring text
- Visual multi-line detection via coordinate comparison instead of logical
  line numbers, fixing soft-wrap misclassification
- Extract computeVerticalPlacement() pure helper with regression tests
- Shared height constants (MODAL_MIN_HEIGHT_COMPACT/EXPANDED) eliminating
  fragile three-way coupling between positioning, modal, and DraggableModal
- anchorBottom support in DraggableModal for upward growth on above placement
- Consistent "center in editor" fallback across all insufficient-space cases
  for both Quick Ask and Quick Command
- Space checks use scrollRect (editor visible area) instead of window
- Multi-line selections center horizontally on the editor
- Horizontal clamp to scrollRect first, then viewport as safety net
- Reset DraggableModal transient state on reopen transitions
- Side lock to prevent flipping during streaming output
2026-03-13 21:50:01 -07:00
Logan Yang
09ce24115b
fix: defense-in-depth overrides to prevent tiktoken CDN timeout in Plus mode (#2283)
* fix: defense-in-depth overrides to prevent tiktoken CDN timeout in Plus mode (#2282)

Plus mode's planToolCalls uses invoke() on a streaming model, which
causes LangChain's _generate() to call _getEstimatedTokenCountFromPrompt
after streaming. This triggers getNumTokens -> encodingForModel -> CDN
fetch with 6 retries, hanging for minutes when tiktoken.pages.dev is
unreachable.

The existing getNumTokens instance override (PR #2160) is bypassed in
some production bundle configurations. Add overrides at two additional
levels: getNumTokensFromMessages and _getEstimatedTokenCountFromPrompt.
The latter returns 0 since actual token usage comes from API response
metadata.

Agent mode is unaffected because it uses stream() which bypasses
_generate() entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use stream() instead of invoke() in planToolCalls to avoid tiktoken CDN fetch

Instance property overrides on getNumTokens are bypassed in esbuild
production bundles (prototype methods called directly). Instead of
trying to override methods, eliminate the problematic code path: replace
invoke() with stream() + chunk collection in planToolCalls.

invoke() goes through _generate() which, with streaming: true, calls
_getEstimatedTokenCountFromPrompt -> getNumTokensFromMessages ->
getNumTokens -> encodingForModel -> tiktoken CDN fetch (6 retries).

stream() goes through _streamResponseChunks() directly, bypassing
_generate() and the entire token estimation path. This is the same
approach Agent mode uses, which is why it was never affected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: patch getNumTokens on prototype instead of instance for esbuild compat

Instance property overrides are bypassed in esbuild production bundles
because minified code resolves this.method() calls directly to the
prototype. Patch the prototype with a guard flag to ensure the tiktoken
CDN fetch is prevented in all code paths including production builds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: patch tiktoken on BaseLanguageModel prototype and convert invoke to stream

Two fixes for the tiktoken CDN timeout that blocks web search:

1. Walk up the prototype chain to patch getNumTokens on
   BaseLanguageModel.prototype (where it's defined) instead of the
   direct prototype. One patch now covers ALL model classes
   (ChatOpenAI, ChatOpenRouter, etc.) instead of only the first
   class instantiated.

2. Convert getStandaloneQuestion() from invoke() to stream() to
   avoid _generate() -> _getEstimatedTokenCountFromPrompt() ->
   getNumTokens() -> tiktoken CDN fetch path entirely. This is
   the invoke() call triggered during @websearch tool execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: simplify tiktoken patch to one-time module-level override

Replace the per-instance prototype walking with a single module-level
patch on BaseLanguageModel.prototype.getNumTokens. Runs at import time
before any model is created, covering all model classes and all code
paths (createModelInstance, ping, etc.) with zero per-call overhead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: handle array content in getStandaloneQuestion stream loop

Use extractTextFromChunk() instead of a bare string check so that
providers sending content-part arrays (e.g. Anthropic) are handled
correctly during streaming. Previously array content was silently
dropped, which could produce an empty condensed question.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 21:48:13 -07:00
Logan Yang
cd726a0e83
feat: drag relevant notes and sources into editor to insert wikilinks (#2288)
* feat: drag relevant notes and sources into editor to insert wikilinks

Adds Obsidian-native drag-and-drop support to the Relevant Notes panel
and Sources modal, using app.dragManager so dropping onto an editor
automatically inserts [[wikilink]] without any custom drop handler.

- New useNoteDrag hook wraps app.dragManager.dragLink/onDragStart
- RelevantNotes: title link (expanded) and badge (collapsed) are draggable
- SourcesModal: source links are draggable (vanilla JS class context)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: prevent drop zone overlay when dragging from relevant notes

The chat container's "drop files here" overlay incorrectly appeared when
dragging notes from the relevant notes pane. Mark internal drags with a
custom data transfer type so the drop zone hook can skip them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 11:56:55 -07:00
Emt-lin
ec5cdcfe25
fix: support both classic and modern YouTube transcript panel DOM structures (#2286)
YouTube now serves two different transcript panel types server-side:
- Classic ("In this video"): ytd-transcript-segment-renderer with .segment-timestamp
- Modern ("Transcript"): transcript-segment-view-model with .ytwTranscriptSegmentViewModelTimestamp

The old code only handled the classic panel, causing empty transcripts when
Web Viewer referenced a video with the modern panel.

Key changes:
- Panel-scoped queries via findTranscriptPanel() to avoid false matches
- Visibility-aware panel detection to skip hidden/stale panels
- extractFromPanel() auto-detects classic vs modern layout
- Close button uses captured panel element instead of hardcoded target-id
2026-03-12 20:59:56 -07:00
Logan Yang
6d14b3ee89
fix: strip leaked special tokens from local model responses (#2284) (#2285)
Local model servers (LM Studio, Ollama) sometimes leak chat template
control tokens like <|im_end|> into visible responses when stop tokens
are not configured on the server side.

Add stripSpecialTokens utility that removes known tokens (ChatML,
Llama 3, Gemma, Phi, Mistral, Qwen, DeepSeek, Command R) via a single
pre-compiled regex. Applied in ThinkBlockStreamer.handleDeepseekChunk
before content is appended to the response.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 17:36:31 -07:00