feat: Obsidian plugin — native .nodepad rendering with full provider support#2
Conversation
Implements the full Obsidian plugin integration (backlog mskayyali#22-mskayyali#26). The plugin bundles all nodepad React components via esbuild and renders them inside an Obsidian TextFileView leaf. .nodepad files are stored in the vault and auto-saved on every state change — no Next.js server required. Plugin structure: - plugin/src/main.ts — registers .nodepad extension, ribbon icon, command palette - plugin/src/view.tsx — React mount via createRoot(), full NodepadApp component - plugin/src/settings.ts — Obsidian settings tab (provider/model/key/local-toggle) - plugin/src/ai-adapter.ts — direct AI calls via requestUrl() + child_process bridge - plugin/src/styles.css — Tailwind tokens mapped to Obsidian CSS variables - plugin/esbuild.config.mjs — bundles from local lib/ and components/ AI providers supported in plugin: - OpenRouter, OpenAI, Z.ai — via requestUrl() (CORS-free in Electron) - Ollama — direct localhost:11434 or Cloud, different /api/chat request shape - Gemini CLI — two-stage child_process pipeline with optional web grounding Shared component patches (no web app regression): - components/ui/sheet.tsx — SheetPortal/SheetContent accept container prop - components/about-panel.tsx — forwards container to SheetContent - components/status-bar.tsx — accepts and forwards portalContainer - components/vim-input.tsx — isPlugin mode: hides Projects nav, filters actions, uses dynamic grid columns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers installation (manual sideload and BRAT), usage, all five AI providers (OpenRouter, OpenAI, Z.ai, Ollama, Gemini CLI), build-from- source steps, troubleshooting, and roadmap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The esbuild config was copying src/styles.css verbatim to dist/styles.css. Obsidian received the unprocessed Tailwind v4 source (@import "tailwindcss", @source directives) which it cannot interpret, resulting in zero utility classes being applied and the entire nodepad layout collapsing. Fix: add @tailwindcss/cli to devDependencies and run the Tailwind compiler as the first step of npm run build. The compiled dist/styles.css is now 74 KB of generated utilities instead of the 3 KB raw source. Dev workflow: run npm run dev:css in a separate terminal alongside npm run dev to get Tailwind watch mode during development. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Issue 3 — API key show/hide: Adds an eye icon ExtraButton alongside the API key input. Clicking it toggles inputEl.type between "password" and "text" and swaps the icon to eye-off. The input element reference is captured directly from the addText callback so there is no fragile DOM query. Issue 4 — Per-provider key persistence: Adds providerKeys: Partial<Record<AIProvider, string>> to NodepadSettings. When the provider dropdown changes, the current apiKey is saved into providerKeys[previousProvider] before switching, then the stored key for the new provider is restored from providerKeys[newProvider] ?? "". The apiKey field is also kept in sync with providerKeys[currentProvider] on every keystroke so the latest value is always preserved when switching. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Issue 2 — Ollama model auto-discovery: - Adds ollamaModels: string[] to NodepadSettings (persisted) - Switching to Ollama auto-fetches localhost:11434/api/tags and stores model names; first discovered model is set as active if current modelId is not in the list - Discover models button saves results, updates the dropdown, and re-renders rather than just showing a Notice - Model section shows a dropdown from discovered models when available, falls back to free-text input with a hint when Ollama is unreachable - Discovery count shown in the setting description after first run Issue 5 — Web grounding toggle: - Adds webGrounding: boolean to NodepadSettings (default false) - Settings tab shows a grounding toggle for OpenRouter, OpenAI, and Gemini CLI with a provider-specific description of what it does - Ollama and Z.ai are excluded (no web-search mechanism in the adapter) - getPluginAIConfig() now derives supportsGrounding from the toggle and restricts it to the three providers that actually implement grounding - enrichBlock() shouldGround check now gates on config.supportsGrounding so grounding is fully opt-in instead of always-on for truth-dependent note types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ollama grounding was incorrectly excluded. The fork implements a full hybrid RAG pipeline in /api/ai/route.ts: Ollama Cloud web_search for retrieval, embeddinggemma at localhost:11434/api/embed for local vectorization, cosine similarity ranking, and top-5 chunk injection. Ports that pipeline into ai-adapter.ts so it runs via requestUrl() without needing the Next.js server: - ollamaRAGContext() calls ollama.com/api/web_search with the note text - getLocalEmbeddings() vectors both query and chunks via embeddinggemma - cosineSimilarity() ranks chunks and top 5 are injected into userMessage - Terminal logs mirror the web app's RAG logging pattern Also adds "ollama" to GROUNDING_PROVIDERS in both adapter and settings, and adds an Ollama-specific description to the web grounding toggle explaining the embeddinggemma dependency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ion header
Two bugs both showed the same "no API key" error tile:
Bug A (local Ollama, no key entered):
getPluginAIConfig() returned null for any non-geminicli provider when
apiKey was empty. Local Ollama at localhost:11434 requires no key, so
the null check now also exempts provider=ollama + useLocalOllama=true.
The hasKey guard in view.tsx is updated the same way so the amber
warning banner does not appear for local Ollama users without a key.
Bug B (Ollama Cloud, key entered but ignored):
Both enrichBlock and generateGhost hardcoded headers as
{ "Content-Type": "application/json" } for Ollama, omitting the
Authorization header entirely. Ollama Cloud returned 401 which the
error parser maps to "Invalid or missing API key". Fixed by replacing
the hardcoded header with getProviderHeaders(config), which already
adds Authorization: Bearer <key> when config.apiKey is non-empty and
omits it when empty (correct for local Ollama).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every provider now logs to the Obsidian DevTools console (Ctrl+Shift+I) for both enrichment and ghost synthesis, making it easy to confirm which provider/model is being used and whether requests are succeeding. Enrichment logs: [Nodepad/openrouter] Enriching with openai/gpt-4o + grounding [Nodepad/openrouter] Enrich done → claim (confidence 87) [Nodepad/openai] Enriching with gpt-4o [Nodepad/openai] Enrich done → question (confidence 92) [Nodepad/zai] Enriching with glm-4.7 [Nodepad/zai] Enrich done → idea (confidence 78) [Nodepad/Ollama] Enriching with llama3.2 + RAG [Nodepad/Ollama] Enrich done → claim (confidence 85) [Nodepad/GeminiCLI] Enrich done (question) Ghost synthesis logs: [Nodepad/openrouter] Generating ghost synthesis with openai/gpt-4o [Nodepad/openrouter] Ghost done → "Tension between X and Y implies..." [Nodepad/Ollama] Generating ghost synthesis with llama3.2 [Nodepad/GeminiCLI] Generating ghost synthesis... [Nodepad/GeminiCLI] Ghost done → "Tension between..." Ollama RAG and Gemini CLI stage logs were already present and unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ificity Obsidian's .view-content svg rule has higher specificity than our .nodepad-view svg:is(...) selectors, causing all Lucide icon SVGs to inherit Obsidian's dimensions rather than their Tailwind size classes (h-[18px] w-[18px], h-4, h-5, etc.). Fix: add !important to all explicit width/height declarations on SVGs inside .nodepad-view so they win regardless of Obsidian's cascade. Also adds a max-width/max-height cap on unsized SVGs as a fallback guard, and moves display/flex-shrink to the base .nodepad-view svg rule so they always apply. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cascade Obsidian's .view-content button rule resets padding to 0 with higher specificity than our .nodepad-view button:is(...) overrides, causing palette buttons (py-4 px-2) to have zero padding. With no internal space the flex-col layout has nowhere to centre the icon + label, so the icon clips at the top of the button. Fix: add !important to all button padding and height overrides so they win the cascade regardless of Obsidian's base stylesheet, matching the same approach already applied to SVG size overrides. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Obsidian applies a min-height to buttons that makes the standard 1rem top/bottom padding insufficient to visually centre the flex-col icon + label stack. Values found via DevTools experimentation: padding-top: 1.8rem padding-bottom: 1.6rem The slight asymmetry accounts for the label text sitting below the icon. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Obsidian forces display:inline-block !important on all SVGs in the plugin view (needed for icon sizing). For small icon SVGs this is correct, but for the full-screen graph SVG it restricts pointer-event hit-testing to painted regions only — i.e. the node circles. Blank canvas areas receive no pointer events, so scroll-to-zoom and mousedown-to-pan only work when the cursor is over a node. Fix: insert a transparent <rect> covering the full SVG dimensions as the first child of the SVG element. This gives the SVG a painted surface across its entire area, so wheel and mousedown events fire everywhere regardless of the CSS display mode. Standard D3 practice for force-directed graphs rendered inside constrained containers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The minimap page button used p-1.5 which is caught by the global button:is(.p-1.5) CSS override, causing the dot grid to clip. The correct padding (1.5rem top/bottom, 0.375rem left/right found via DevTools) also affected status bar buttons which use the same class. Fix: remove p-1.5 from the minimap button className and apply the correct values as an inline style prop instead. Inline styles have higher priority than any CSS rule so they are unaffected by either Obsidian's base styles or our override, while the global p-1.5 override continues to work correctly for status bar buttons. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Testing complete — all issues resolvedWent through the full plugin after the initial build and resolved every rendering and functional issue found during testing. Summary of what was found and fixed: CSS compilation missing (root cause of broken styling)Problem: Fix: Added Dev workflow: Obsidian CSS specificity overrides (multiple elements)Problem: Obsidian's Fixes applied:
Command palette icon overflowProblem: Despite the SVG Fix: Applied Minimap dot grid clipping outside buttonProblem: The tiling minimap page buttons use Fix: Removed Per-provider API key not restored when switching providersProblem: Fix: Added API key permanently masked with no show/hide toggleProblem: The API key input had Fix: Added Ollama provider auth bugs (two separate bugs, same symptom)Bug A — Local Ollama, no key: Fix: Added Bug B — Ollama Cloud, key present but ignored: The Ollama enrichment and ghost requests hardcoded Fix: Replaced the hardcoded header with Ollama model list not auto-populatingProblem: Selecting Ollama showed a free-text model input. The "Discover models" button showed a Notice with model names but didn't persist or render them as a selectable dropdown. Fix: Added No web grounding toggleProblem: Fix: Added Ollama web grounding (RAG pipeline) missing from pluginProblem: Ollama's hybrid RAG pipeline (Ollama Cloud Fix: Ported the full pipeline: Graph view — pan and zoom only worked over nodesProblem: Scroll-to-zoom and drag-to-pan only worked when the cursor was directly over a node. The blank canvas area received no pointer events. Root cause: Our Fix: Added a transparent Runtime logging added for all providersAll providers now log to Obsidian DevTools console ( All 9 original test checklist items are now passing. Ready for merge. |
Summary
plugin/) that renders.nodepadfiles natively inside the Obsidian workspace as aTextFileViewleafchild_processplugin/README.mdcovering install, usage, provider setup, build steps, and troubleshooting@tailwindcss/cli)Closes backlog mskayyali#22, mskayyali#23, mskayyali#24, mskayyali#25, mskayyali#26.
What changed
New:
plugin/directorysrc/main.ts.nodepadextension, ribbon icon, command palette, folder right-clicksrc/view.tsxTextFileViewsubclass - mounts fullNodepadAppReact tree, auto-saves on every state changesrc/settings.tssrc/ai-adapter.tsrequestUrl()(CORS-free in Electron);child_process.spawn("gemini")for Gemini CLI; correct/api/chatshape for Ollamasrc/styles.cssesbuild.config.mjslib/andcomponents/- uses fork codeREADME.mdShared component patches
components/ui/sheet.tsx- SheetPortal/SheetContent acceptcontainer?propcomponents/about-panel.tsx- forwardscontainerto SheetContentcomponents/status-bar.tsx- accepts and forwardsportalContainercomponents/vim-input.tsx-isPluginmode: hides Projects nav, filters to Export MD / Copy MD / Clear, dynamic grid columnsConfirmed working (tested with Gemini CLI)
.nodepadfile opens as a styled canvas inside Obsidian - full nodepad UI renders correctly after CSS compilation fixKnown issues found during testing
Issue 1 - Command palette icons overflow button bounds
Observed: In the Ctrl+K palette (Views / Navigate / Actions sections), icon SVGs render larger than their button containers.
Root cause: Obsidian's
.view-content svgrule has higher specificity than Tailwindh-[18px] w-[18px]utilities. Thestyles.cssoverrides cover some size classes but Obsidian wins on the palette buttons specifically.Solution: Increase specificity of
.nodepad-view svgoverrides using a tighter selector (e.g..nodepad-view button svg) and ensure all icon size combinations used invim-input.tsxare covered.Issue 2 - Ollama model list does not auto-populate from local device
Observed: Selecting Ollama as provider shows a free-text model input. The "Discover models" button fires a Notice showing found model names but the input is never populated with them.
Root cause:
getModelsForProvider("ollama")returns[](dynamic-only), so settings.ts falls through to the free-text path. The discovery result is displayed but not persisted or rendered as a selectable list.Solution: Add
ollamaModels: string[]toNodepadSettings. On Ollama provider selection and on settings tab open, auto-fetchhttp://localhost:11434/api/tags, persist the model names, and render a<select>dropdown. Fall back to free-text input if the fetch fails or returns empty.Issue 3 - API key field has no show/hide toggle
Observed: The API key input is permanently masked (
type="password") with no way to verify what is currently stored.Solution: Add an eye icon toggle button via
Setting.addExtraButtonthat switchesinputEl.typebetween"password"and"text". This is a standard pattern in Obsidian community plugins.Issue 4 - Switching providers does not restore the previously saved API key for that provider
Observed: Switching between providers (e.g. OpenRouter to OpenAI and back) does not restore the key previously entered for each provider. The field stays blank or retains the wrong value, requiring the user to manually re-enter each key every time.
Root cause:
NodepadSettingshas a singleapiKeystring. When the provider dropdown changes, the previous key is overwritten with no per-provider store, so it cannot be recovered when switching back.Solution: Add
providerKeys: Partial<Record<AIProvider, string>>toNodepadSettings- this field already exists inlib/ai-settings.tsfor the web app, so it matches the established pattern. In the provideronChangehandler: save currentapiKeyintoproviderKeys[previousProvider], then restoreproviderKeys[newProvider] ?? ""intoapiKeybefore callingsaveSettings().Issue 5 - No web grounding toggle in plugin settings
Observed: Web grounding is never activated from the plugin.
getPluginAIConfig()hardcodessupportsGrounding: falseregardless of provider or model.Solution:
webGrounding: booleantoNodepadSettings(defaultfalse):onlinemodels, OpenAI with search-preview models, Ollama, Gemini CLI)getPluginAIConfig()to setsupportsGrounding: settings.webGrounding && modelSupportsGroundingai-adapter.ts- enrichment and ghost logic already branches correctly onsupportsGroundingInstall
Obsidian - Settings - Community plugins - disable Restricted mode - enable Nodepad
Dev workflow
Test checklist
Generated with Claude Code (https://claude.ai/claude-code)