Skip to content

Redesign Defaults and Chat settings; fix semantic search, sync churn and workspace duplication - #416

Merged
ProfSynapse merged 76 commits into
mainfrom
feat/defaults-settings-redesign
Oct 14, 2026
Merged

ProfSynapse merged 76 commits into
mainfrom
feat/defaults-settings-redesign

Conversation

@ProfSynapse

Copy link
Copy Markdown
Owner

Summary

Redesigns Settings → Nexus → Defaults and the in-chat Chat settings modal, rebuilds Get started as in-tab pages, makes semantic search controllable at runtime, and fixes a set of search, storage and sync bugs found along the way (several also affect 5.20.x).

Visual contract: docs/mockups/defaults-settings-essentials.html (accepted 2026-10-14). Plan: docs/plans/defaults-settings-redesign-plan.md (see "Owner decisions").

Settings redesign

  • Defaults and Chat settings share one "essentials + folds" layout: Provider / Model / Thinking always visible; folds for Chat options, Context, Subagents, Images, Video, Voice, File conversion, Semantic search. Built from Obsidian SettingGroup/Setting components; folds use the shared, restyled Accordion. Fold state is device-local.
  • Controls follow the model: Thinking hidden or filtered to supported levels, Creativity disabled for models that reject temperature (supportsSamplingParams: false), context window is a slider that snaps to 32K–1M presets up to the model's max.
  • Subagents get Provider/Model/Thinking/Creativity/Context window, and those settings now actually reach subagent and executePrompt requests (previously mostly ignored). Thinking is gated at the request boundary for non-reasoning models.
  • Chat settings context window applies to the conversation only; Defaults no longer change open chats.
  • No made-up values when no provider is configured; display names instead of raw model ids.
  • minAppVersion → 1.11.0 (SettingGroup).

Get started

  • One navigable row above the tabs opening in-tab pages (BackButton): Get started → Chat inside Obsidian / Connect an external agent.
  • External agents: Node.js check first (no more bare node fallback), OS-correct absolute paths, one-click connect for Claude Desktop, Claude Code (claude mcp add-json --scope user), Codex and Antigravity (~/.gemini/config/mcp_config.json, replaces "Gemini CLI"), and a four-step "Other agents" flow with a copyable config and setup prompt. Reconnect preserves other config keys; unreadable configs are backed up.

Semantic search

  • On/off applies without restart, with confirmation; Delete index (confirmed) and VACUUM; pruning of vectors for notes removed while off; status, progress and a "couldn't load" error state with Try again.
  • When off, the search tool says so and falls back to keyword search with a note instead of claiming it is "still initializing".

Bug fixes (several affect released versions)

  • Embedding model never loaded when Settings had focus (Obsidian 1.14 Settings is its own window): iframe was attached to the focused window while its listener stayed on the main window → indexing reported "complete" with 0 notes. Also turning off took ~30 s. (5.20.x)
  • Conversation search never reached the index: embeddingService was never registered after e1eada3a; all five lookups replaced with resolveEmbeddingService().
  • Conversation backfill stuck "completed" with 0 conversations if it ever ran on an empty table → past chats never indexed. Now self-heals on next start. (5.20.x)
  • "Indexing 0/N" for minutes on every start: thousands of too-short "Used tool" traces re-walked each start. Now skipped; progress phases are honest.
  • Default workspace re-created on every load (SQLITE_CONSTRAINT_UNIQUE) appending duplicate workspace_created events that reset edits on cache rebuild. Creation is now idempotent; replay ignores duplicates. (5.17.0–5.20.x)
  • data.json uploaded to Sync every minute per device: the workflow scheduler watermark moved to device-local storage. (5.20.x)
  • Multi-window: voice output, base-query render host, composer suggesters and Node require loaders no longer bind to whichever window has focus.
  • Cache IndexedDB bound to the main window; image-generation timeout leak; chat welcome "Open settings" opens Providers; workspace prompt sync; PDF import row order; dead stateManager lookup; semantic results one per file.

Test plan

  • npm run build (schemas check, lint incl. mobile import check, tsc, esbuild, connector)
  • Full Jest: 550 suites / 6,538+ tests passing (each new test confirmed failing on the old code)
  • Live in a throwaway vault: settings pages, Get started, semantic on/off/fallback/re-enable, delete index, conversation backfill self-heal
  • Live in a real 7.5k-note vault: indexing completes, semantic note queries rank correctly, conversation search finds past chats after backfill self-heals, data.json no longer changes every minute
  • Antigravity config path against a real Antigravity install
  • Chat settings modal flows and subagent settings end-to-end with a live provider

🤖 Generated with Claude Code

ProfSynapse and others added 30 commits October 14, 2026 08:46
Blessed mockup: docs/mockups/defaults-settings-essentials.html (2026-10-14, 5.20.4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StatusBadge had no importer in src/ or tests/, so the component and its
.nexus-status-badge rules go. The settings header also loses a bare ';'
and the dead "Title and description" comment above it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Chat settings modal no longer merges its context window into the
global contextWindowOverrides defaults; the value applies to the open
conversation only, through the existing requestContextChange handoff.
This reverses the save-to-defaults half of PR #410.

Defaults no longer push context window changes into the active chat
(ChatView.applyContextWindowDefault is removed). Defaults apply to new
chats only, per the owner decision of 2026-10-14.

Tests: the modal handoff suite now asserts global defaults are never
written; the Defaults suite asserts the open chat is not touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving Chat settings while no conversation is open kept the values in
ModelAgentManager memory, but selecting the first conversation created
afterwards re-read the plugin defaults and dropped them. The modal now
marks those selections to carry; the next conversation that has no saved
selections yet adopts them (including the committed context window) and
stores them in its chatSettings. Reloading the welcome defaults clears
the carry.

ChatSettingsWithoutConversation.test.ts drives the real modal save,
ModelAgentManager and chat ConversationManager.createConversation; it
failed on the baseline with temperature reset to the default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add resolveEmbeddingService(plugin), which reads plugin.embeddingManager
and returns its service only while the manager is enabled and
initialized. ServiceAccessors used to ask the registry for
'embeddingService', a name nothing registers, so semantic search of past
conversations never ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SearchContentTool cached the first service it saw and SearchManagerAgent
pre-seeded that cache from a dead registry lookup. Drop both and the
setEmbeddingService seam; the tool now asks resolveEmbeddingService on
each semantic search, so a stopped or rebuilt service is never used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
initializeSuggesters captured getServiceIfReady('embeddingService') once,
which was always null, so composer suggestions were always fuzzy.
searchNotes now accepts a resolver as well as an instance, and the note
suggester resolves the service per query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The task board and the Workspaces tab task detail looked up
'embeddingService' in the registry and always got null. Both now go
through resolveEmbeddingService.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Statically collects string names passed to getServiceIfReady/getService
under src/ and checks them against CORE_SERVICE_DEFINITIONS,
ADDITIONAL_SERVICE_FACTORIES and the src/core/index.ts registrations.
The dead 'stateManager' shutdown lookup is listed as a known exception.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no provider configured, the chat welcome's "Open settings" button
opened the per-chat Chat settings modal, which has nothing to offer
until a provider exists. It now opens Settings -> Nexus on the
Providers tab.

- UIStateController: the welcome callback is now
  setOpenProviderSettingsCallback.
- SettingsTabManager.openNexusSettings(route) hands the route to the
  settings view, then opens Obsidian settings on the Nexus tab; exposed
  through PluginLifecycleManager and NexusPlugin.openNexusSettings.
- SettingsView.openTo(route) navigates at once when settings are on
  screen, otherwise on the next display(), before the tab strip is built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WorkspaceFormRenderer saves the workspace prompt binding as the
top-level dedicatedAgentId, but ChatSettingsRenderer.syncWorkspacePrompt
read only the deprecated context.dedicatedAgent.agentId, and
DefaultsTab.loadWorkspaces passed only { id, name }. Choosing a workspace
on Defaults therefore never selected its prompt.

The renderer now uses WorkspacePromptResolver's precedence (top-level
first, then the deprecated path), its workspace option type carries
dedicatedAgentId, and Defaults passes dedicatedAgentId and context.

The context-note Add and remove buttons now register clicks through the
renderer's Component (registerDomEvent) instead of onclick assignments;
the Component is unloaded on every redraw and on destroy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ingestion and Embeddings were appended after ChatSettingsRenderer.render(),
which empties its container on every redraw (provider or model change, LLM
settings change), so both sections vanished after the first redraw. They
now render inside the renderer's renderAfterImageSection hook, next to
Video and Voice, as an interim fix until the redesigned layout replaces
the hook.

Within Ingestion the toggle is created first and every dependent row,
including auto-convert, lives in the container that is hidden while the
toggle is off. Rows render synchronously; only the vision model dropdowns
wait for the capability lookup, and a lookup failure no longer escapes
as an unhandled rejection.

Copy: the toggle is "File import" ("Drop PDFs or audio into chat to turn
them into notes.") per the owner decision; auto-convert is "Convert new
files automatically" and no longer refers to "the defaults below".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add resolveThinkingForModel, which keeps the stored thinking preference but
returns enabled:false for registry models without supportsThinking, and use
it in ModelAgentPromptContextAssembler so a hidden preference is no longer
sent to non-reasoning models such as Mistral Large.

Add getSupportedThinkingEfforts / normalizeThinkingEffortForModel, a pure
helper for the Thinking dropdown (owner decision 8): it derives the levels a
provider/model distinguishes from the adapters' own effort mappings and
returns all five when there is no capability data. The Anthropic adaptive
model predicate moves into ThinkingEffortMapper so the adapter and the
helper share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w settings

Add SubagentModelResolver, the single precedence for agent runs (subagents
and executePrompt): explicit tool parameter, the chat's subagent model, the
default subagent model, a cloud chat model, then the first cloud provider.
resolveAgentRunSettings combines thinking (gated by model capability),
Creativity (dropped for models with supportsSamplingParams false) and the
context budget (clamped to the model window).

LLMService.getAgentModel now honours a configured agentModel whatever the
chat model is, exposes getAgentDefaults(), and executePrompt forwards
enableThinking/thinkingEffort to the adapter.

New optional settings, no migration: llmProviders.agentTemperature and
llmProviders.agentContextWindowOverrides, and per-chat
chatSettings.agentTemperature / agentContextWindow carried through the
defaults resolver, ModelAgentManager restore/save and the settings store.
The conversation_updated event stores the whole chatSettings object and
the applier replays it verbatim, so the new fields survive a cache rebuild.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mpt runs

Subagents no longer inherit the chat model and thinking unconditionally.
SubagentController.buildSubagentContext resolves provider, model, thinking,
Creativity and context window with the shared agent precedence, using this
chat's subagent settings (new SubagentContextProvider.getAgentSettings) and
the defaults. SubagentExecutor forwards thinking and temperature to the
streaming generator, which passes them to generateResponseStream, and uses
the context window as a budget for the inherited workspace data and context
files: whole items beyond it are dropped and named in one prompt line.

executePrompt (PromptExecutor) now sends the agent temperature (omitted for
models that reject sampling) and gated agent thinking, and picks its model
with the same precedence. A chat's own subagent settings apply when that
chat calls the tool: the tool batch executor passes its runtime identity to
tools that opt in via acceptsToolRuntimeContext, and ExecutePromptsTool
matches it against the chat view's conversation, as the subagent tool does.
MCP calls and other conversations use the defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drops and recreates the three vec0 tables with the schema DDL and empties
the embedding metadata tables and the conversation backfill cursor, in one
transaction, then saves. Conversations, messages, traces, the notes index
and the learned retrieval adapter file are untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EmbeddingManager.setEnabled() starts and stops the engine, watchers, queue,
status bar and dream cycle without a restart (the dream interval and the
dream-now command are now undone on stop). A shared teardown backs both
setEnabled(false) and shutdown(), and a run generation stops a background
chain between phases once it is torn down.

New public surface for the settings fold: getStatus(), getProgress(),
onStateChange() (start, stop, delete and every queue progress event),
pauseIndexing()/resumeIndexing(), and getStats() that reads stored counts
from the database even while off, plus a distinct conversation count.
deleteIndex() clears the vectors only while off and keeps the AdapterStore.

IndexingQueue gains pruneMissingNotes() and pruneMissingConversations(),
run before the first phase on every pass, so vectors of notes deleted or
renamed and chats deleted while search was off are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A synced enableEmbeddings change no longer counts as needs-reload; it is
adopted and applied through PluginLifecycleManager.setEmbeddingsEnabled().
ChatTraceService follows the manager's state, so traces stop going to a
stopped embedding service and pick up the new one after a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
search content keeps its semantic property (ToolCliNormalizer rejects
unknown flags, so removing it would turn --semantic into an error) but
re-describes it, drops the semantic pitch from the tool description, and
answers semantic=true with keyword results and a note saying semantic
search is turned off in Nexus settings. The description follows the
setting live. With search on, the committed description is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Their deferred 3s indexing timer stayed open after the test ended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The obsidian mock's Setting appended nothing and dropped its name, so no
test could observe row order, descriptions, disabled state or whether a
redraw replaced a node. FakeElement keeps an ordered tree, tracks
isConnected, dispatches registered events and answers simple selectors.

Given a FakeElement container, Setting renders Obsidian's markup and the
Dropdown, Toggle, Slider, Button, ExtraButton and ProgressBar mocks
render real-looking controls that react to change/click events. The
Slider mock reproduces the native span.slider-value that Obsidian 1.13+
draws, with setDisplayFormat. SettingGroup and Modal.setTitle are added.
Every other container keeps the old call-recording mock unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The redesigned Defaults and Chat settings use SettingGroup and the core
.setting-group styles, both added in Obsidian 1.11.0; the
obsidianmd/no-unsupported-api lint rejects them under 1.10.0. Raise
minAppVersion in manifest.json; version-bump.mjs records the new
mapping in versions.json at the next release, so users on 1.10 stay on
5.20.x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Accordion takes an options object (id, title, icon, summary, open,
onToggle). Its toggle is a native button with aria-expanded and
aria-controls; the content is a setting-group with one stable
setting-items list (itemsEl), replacing getContentEl(), which created a
new div on every call. The summary shows only while closed.

FoldStateStore remembers open folds per device and per surface with
app.saveLocalStorage, so a fold toggle never becomes a synced settings
change that other devices must reload for.

createSettingGroup wraps SettingGroup and returns its row list.
bindSliderReadout writes the formatted value into the span.slider-value
Obsidian 1.13+ draws itself (setDisplayFormat) and adds one span only
when none exists. The extra plugin span was the cause of the duplicate
"High 2" and "1,000,000 tokens 1000000" readouts, confirmed by reading
the 1.14.4 app bundle; recorded in the testing skill's refinement log.

Get started moves to the new Accordion API (rocket icon).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both surfaces keep one shared renderer and now draw the blessed layout:
no heading or intro, an always-open SettingGroup with Provider, Model
and Thinking, then icon folds that start collapsed and summarise
themselves in one line: Chat options, Context, Subagents, Images,
Video, Voice, File conversion and Semantic search. Video, File
conversion and Semantic search (desktop only) are Defaults-only folds;
Web search and Workflow appear only in Chat settings.

The shell is built once. Each group empties and refills only its own
rows (chat model -> top block, Chat options, Subagents; subagent model
-> Subagents; image provider -> Images; workspace -> Context; synced
settings -> every group), so open folds and the scroll position
survive redraws. Fold state is remembered per device and per surface.

- Thinking is one dropdown (Off, Low ... Max) over the existing
  { enabled, effort } storage; Off keeps the effort. getThinkingLevels
  is the hook for capability filtering.
- Creativity and Context window use the single slider readout. The
  context window keeps its per-model 1-token slider; a discovered window
  redraws Chat options instead of appending rows out of order.
- Context notes are Setting rows with an Add note button and a remove
  extra button per note. A text-only provider is explained in the Model
  row description instead of a warning box.
- VoiceSettings is the only voice implementation. It edits the renderer
  settings; Defaults persists read-aloud and live-voice picks only once
  the user makes them (source: 'user'), and opening the tab saves
  nothing. Warnings become the Provider row description.
- VideoSettings, FileConversionSettings and SemanticSearchSettings are
  the Defaults folds. File conversion renders File import first and its
  dependent rows only while it is on, Vision rows only for Vision.
  SemanticSearchSettings hosts the enableEmbeddings switch behind a
  render(container) entry point for the full semantic UI.
- DefaultsTab loses its header, intro and duplicate voice, video,
  ingestion and embeddings code; every write goes through its save queue.
- Chat settings uses the modal title and a bottom modal-button-container.

CSS: the shared accordion section replaces the old mcp-accordion rules;
dead csr-* (except csr-section, still used by DataTab), nexus-voice-*,
thinking-settings and chat-settings header/button rules are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the Semantic search fold copy (index status, indexing, delete index)
and the Turn off semantic search confirmation from the blessed mockup, plus
the Delete the search index confirmation copy (owner decision 6). The fold
summary reads "Off · no index" once the index has been deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e index

The Semantic search fold on Defaults now reads the embedding runtime:
- On: "Index" with note and chat counts from getStats().
- Indexing: a ProgressBarComponent, done / total, an estimate and a
  pause / resume ExtraButton, updated in place on each progress event.
- Turning off asks first (SemanticSearchOffModal); Cancel or Escape keeps
  it on. The switch is applied at runtime through the lifecycle manager,
  with no restart notice.
- Off: "Index" with what is kept ("Paused · N notes") and a Delete index
  button (mod-warning) behind a "Delete the search index?" confirmation,
  which calls EmbeddingManager.deleteIndex().

The fold subscribes to onStateChange while mounted and unsubscribes on
destroy. The runtime is resolved on every use, since it is created only
once storage is ready.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…king rows

The selected model now decides its rows in Defaults and Chat settings:

- Creativity is disabled with "<Model> sets its own temperature." and a "—"
  readout for models whose registry entry rejects sampling parameters
  (StaticModelsService now carries supportsSamplingParams). Unknown models
  keep it enabled.
- Context window is a SliderComponent over preset stops (32K … 1M) strictly
  below the model maximum, plus the maximum. A single stop disables it, a
  saved size between stops shows as saved until moved, and picking a new
  provider or model starts that model at its maximum.
- Thinking offers only the levels the model distinguishes (owner decision 8),
  using the shared thinkingLevels helper, and shows a folded level as the
  level really sent without rewriting the stored preference.
- The Subagents fold gains Creativity and Context window, backed by
  agentTemperature and agentContextWindowOverrides. Without a subagent model
  its rows and summary describe the cloud chat model subagents fall back to.
- Fold summaries follow: "Fixed temperature", and the Subagents summary adds
  the subagent tuning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ProfSynapse and others added 26 commits October 14, 2026 10:58
…omplete

EmbeddingService.initialize() swallows a load failure and turns the
service off, after which embedNote() is a silent no-op. The note pass
walked the queue anyway and ended with phase 'complete', N of N, over an
empty index, next to a "Failed to load embedding model" notice. It now
ends with phase 'error', which the status bar reports once, so the
service no longer raises its own notice as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The store is built at plugin load and lives as long as the plugin, but
took its factory from window.activeWindow. When Nexus loads while a
popout or the Settings window (its own window since Obsidian 1.14) has
focus, as when the plugin is enabled from Settings, the factory belongs
to that window. Verified in Obsidian 1.14.4: once that window closes,
its indexedDB.open() returns null, so every later cache save would
throw. Same origin, same database, so the main window's factory is the
correct one in every case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Get started

Replace the stale Gemini CLI section with Antigravity's real config path
(~/.gemini/config/mcp_config.json, legacy location noted) and send readers to
Get started -> Connect an external agent instead of the old MCP integration
page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing has registered 'stateManager' since ProcessedFilesStateManager was
removed, so the shutdown save was a no-op. Remove it and empty the contract
test's KNOWN_UNREGISTERED list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…esters

The task board edit modal and the task detail view built their note-link
suggesters with the embedding service captured when they opened, so turning
semantic search on (or a rebuild) while they were open was never picked up.
NoteInputSuggester now takes an EmbeddingServiceSource resolver, like the chat
[[ suggester, and both callers pass one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Try again

Since the note pass ends with phase 'error' when the embedding model does
not load, the Semantic search fold fell through to the "on" view and
showed the index counts as if nothing were wrong. It now shows
"Couldn't load the search model" with the queue's detail and a
Try again button, and the fold summary reads "On · couldn't load".

Try again calls a new EmbeddingManager.retry(): after a load failure the
manager still counts as running, so setEnabled(true) is a no-op and the
service has turned itself off. retry() tears the components down and
starts them over, serialized with setEnabled and deleteIndex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…main window

Both are hidden elements that outlive the click that started them, and
both were appended to window.activeDocument, which since Obsidian 1.14
includes the Settings window as well as any popout.

- The live-voice <audio> element is attached after an awaited
  client-secret request and stays for the whole call. If focus moved to
  a popout or Settings meanwhile, closing that window removed the
  element and the assistant went silent.
- base analyze renders a scratch base into an off-screen host for the
  length of the query. It is started by a tool call, so the focused
  window is arbitrary; if that window closed mid-query the host was
  detached, the controller's isShown() gate never opened, and the call
  timed out with no rows.

Neither is visible, so the main window is always the right host, as for
the embedding iframe in 2b15f95. The Jest window mock now exposes its
own document so tests can tell it apart from activeDocument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The click-outside listener was added to window.activeDocument when the
suggester was built, with the chat view. A chat view restored into a
popout is built while the main window has focus, so the listener heard
the wrong window for the life of the view: clicking away inside the
popout never closed the dropdown. destroy() removed the listener from
whichever window had focus at teardown, so the original one leaked.

The listener, the dropdown and its positioning now use the input
element's ownerDocument and that document's window (the bottom offset
used the main window's innerHeight, misplacing the dropdown in a popout
of a different height). destroy() removes the listener from the
document it was added to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
13633b3 replaced globalThis with window.activeWindow in desktopRequire
and the eight private loadDesktopModule copies. activeWindow is
whichever window has focus: a popout, or the Settings window (its own
window since Obsidian 1.14), which is where OAuth and CLI-provider setup
is clicked. The plugin runs in the main window, the only one guaranteed
to have Node's require; looked up elsewhere it may be missing, or hand
back modules whose objects (the OAuth callback server, child processes)
belong to a window that can close under them.

All nine now use window, which is what globalThis meant before. A
source guard covers the private copies, which cannot be reached without
standing each service up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two open handles kept a Jest worker alive after the full run:

- BaseImageAdapter raced generation against a 2-minute timeout and never
  cleared it, so the timer outlived every finished generation (in the plugin
  too, not just tests). It is now cleared once the race settles.
- MessageBubble tests never loaded their bubbles, so cleanup()'s unload was a
  no-op and the reasoning ThinkingLoader interval ran forever. The tests now
  load each bubble the way MessageDisplay does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers the Obsidian 1.11.0 requirement, the Defaults and Chat settings
redesign, subagent model and settings, Get started and agent connect,
runtime semantic search controls, and the embedding fixes. No version bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DefaultWorkspaceManager initializes in the background while the hybrid
adapter is still starting. Its existence check (getWorkspace ->
withReadableBackend) fell back to the legacy .workspaces index because the
adapter was not ready, and that index never holds hybrid workspaces, so it
answered "missing". The create then waited for the adapter
(resolveWritableAdapter) and wrote through it, where the workspace existed:
one more workspace_created event per load, then SQLITE_CONSTRAINT_UNIQUE on
workspaces.name logged by SQLiteCacheManager and workspaceRepository.

Add WorkspaceService.ensureWorkspace, which checks the store it will write
to after that store has finished replaying, and adopts a legacy default by
name. DefaultWorkspaceManager and ChatTraceService use it instead of the
getWorkspace + createWorkspace pair. The default-workspace unique-conflict
recovery in createWorkspace now also recognizes the repository's typed
WorkspaceConflictError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WorkspaceRepository.create appended workspace_created to JSONL and only
then let the SQLite INSERT discover that the id or name was taken. The
append is outside what the transaction rolls back, so every rejected create
left an event for a workspace that was never created. Replay applies it with
INSERT OR REPLACE: a same-id duplicate reset the workspace to its creation
values, and a same-name phantom deleted the real workspace on the next
cache rebuild. Measured in a test vault: an edit to the default workspace
was gone after "Nexus: Rebuild cache".

- create checks for an id/name conflict inside the transaction, before the
  append, and throws WorkspaceConflictError (not logged as a failure).
- WorkspaceEventApplier treats a workspace_created for an id that already
  has a row as a no-op, so streams that 5.17.0-5.20.4 already filled with
  duplicate default-workspace creates replay with their edits intact.

Tests run on real in-memory SQLite (node:sqlite) with the production DDL
and transaction coordinator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fresh install stores the shipped default chat model (an OpenAI model)
and default image model before any provider is set up. The Defaults tab
described them anyway: Chat options read "Fixed temperature · 1.05M
context", Subagents named the OpenAI default with "Thinking off", the top
block showed a Thinking "Off" dropdown, and Images showed the raw id
"gemini-3.1-flash-image".

- The chat model counts only when its provider is usable (the runtime
  skips such a model too). Without one, Thinking is hidden, Creativity and
  Context window are disabled rows reading "—", and the Chat options
  summary says "Pick a model first". The saved model is left alone.
- Subagents with no model of their own and no usable cloud chat model say
  "No model" (no cloud provider) or "Not set", never a model name. Their
  Provider dropdown gains an explicit "Same as chat" option, selected
  while no subagent model is set, and the Model row reads
  "Same as chat (<model>)".
- Images and Voice summaries use catalog display names before the model
  lists load: the image adapters' model tables are now module exports
  read by imageModelNames.ts, and Voice falls back to the transcription
  and speech catalogs. The raw id is shown only for an unknown model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A worktree's node_modules symlink was committed in e4951b2 because the
ignore pattern node_modules/ only matches directories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…letes

A backfill that ran while the conversations table was still empty (a cache
that had not replayed its conversation events yet) recorded status
'completed' with 0 conversations, and every later start returned at that row
before looking at a single conversation. Measured in a real vault: 53
conversations, 1 indexed, backfill row completed/0/0.

- Never record completion on an empty conversations table.
- When the row says completed, index the non-branch conversations that have
  messages and no conversation_embedding_metadata rows (one indexed
  anti-join; a start with nothing missing costs one query and writes
  nothing). Vaults already stuck heal on their next start.

Resume checkpoint and per-pair idempotency are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The status bar read "Indexing: 0/6644" for about six minutes on every
start in a real vault. That was the trace backfill, not notes: 6,660 of the
6,681 un-embedded traces were tool-call traces whose content is "Used tool",
which embedTrace() skips as too short, so they never got a row and were
re-counted and re-walked (50 ms yield each) on every start, with progress
reported only at the start and the end.

- TraceIndexer counts only traces that can be embedded and reports progress
  after each one.
- The note pass's content-hash comparison reports a 'checking' phase with
  notes-compared counts, every 100 notes, and is a running pass that pause
  and cancel reach. 'indexing' counts only notes being embedded.
- Progress events carry a scope (notes, conversations, traces). The status
  bar says "Checking notes", "Indexing chats", "Indexing memory", and
  the completion notice no longer calls traces or chats notes.
- The semantic search fold shows a "Checking for changes" row and names
  what chat and memory passes count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reported as semantic search returning the same file several times
('Application_Tracking.md' x3). Read live, those were different files
sharing a basename (one per job-application folder); embedding_metadata
holds one row per notePath, so the index did not produce duplicates.

Guard the result shaping anyway: keep the best-ranked hit per path and keep
filling up to limit with distinct files. Tests cover both the collapse and
that same-basename files stay separate results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n data.json

The scheduler ticks every minute on every device and saved
workflowScheduler.lastCheckAt into data.json each time, so Obsidian Sync
uploaded data.json about once a minute per device and every other device
reloaded and reconciled it. The watermark is per-device bookkeeping (runKey
prevents duplicate runs across devices), so it now lives in vault-scoped
localStorage. A value older versions left in data.json is honoured as a floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ProfSynapse
ProfSynapse merged commit ee091e0 into main Oct 14, 2026
1 check passed
@ProfSynapse
ProfSynapse deleted the feat/defaults-settings-redesign branch October 14, 2026 16:49
ProfSynapse added a commit that referenced this pull request Oct 14, 2026
Bump to 5.21.0 (minAppVersion 1.11.0), finalize the changelog entry for
#416-#419, and bring README, native chat, CLI and semantic search guides in
line with the redesigned Get started pages and Defaults/Chat settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant