Commit graph

1119 commits

Author SHA1 Message Date
Logan Yang
56c19b3aae
fix(chat): keep status-bar clearance in sync so bottom-right banners can't cover the input
The desktop chat input reserves space for Obsidian's bottom-right status bar
via the `--copilot-status-bar-clearance` variable, but `ChatViewLayout` only
re-measured the overlap on `css-change`. When a core or plugin status-bar item
appears or grows at runtime (update-available notice, sync status, ...) the bar
can get taller without a `css-change` event, so the stale clearance let the
banner overlap the chat input and its send button.

Attach a ResizeObserver to the status bar so any size change re-runs the
measurement. This stays generalizable: it measures the bar's geometry rather
than hardcoding any specific banner or plugin, and rebinds across theme reloads
that swap the element. Mobile is unaffected (the whole mechanism early-returns
on mobile).

Closes #99

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:56:09 -07:00
Logan Yang
7261171638
fix(agent-mode): start a fresh opencode session for every new chat (#2583)
opencode persists its preload probe session id and resumes it on the
next preload, and createSession adopted that warm probe session as the
user's chat. Once a real conversation happened in the probe, every later
"new chat" reattached to it, replaying the entire prior transcript to the
model and showing the previous session's auto-title (e.g. a fresh chat
arriving pre-seeded with an unrelated "how many ml is 1 cup" exchange).

Reuse the warm subprocess (the expensive spawn + initialize handshake)
but never its session: always start a fresh newSession for the chat. The
probe's state still seeds the picker so it doesn't blink. The persisted
probe session now stays a pristine catalog-only session across launches
and is never fed user messages.

Refs logancyang/obsidian-copilot-preview#148

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 08:07:44 -07:00
Logan Yang
5471abf5a8
fix(agent-mode): steer the agent to miyo-search for vault search, gated on Miyo enabled (#2582)
* fix(agent-mode): steer the agent to miyo-search for vault search, gated on Miyo enabled

The bundled `miyo-search` skill (#2566) was invoked unreliably: a prose skill
only runs if the model thinks to use it, and the SKILL.md description alone
wasn't a strong enough signal.

- Name `miyo-search` explicitly in the agent system prompt, the same way
  COPILOT_PLUS_TOOLS_STEERING names the relay skills, with concrete triggers:
  use it for any vault-search intent when builtin `grep` is too slow or doesn't
  surface enough relevant notes, or when the user explicitly asks for Miyo search.
- Mirror the same concise triggers in the SKILL.md description + body.

Respect the user's "Miyo enabled" setting: the new steering is appended only
when `shouldUseMiyo(getSettings())` is true — the same gate that seeds/prunes
the skill in agentMode/index.ts — so the prompt never points at a skill that
isn't seeded, and both halves stay in lockstep with the setting.

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

* fix(agent-mode): restart spawn-time backends when Miyo steering flips

The miyo-search system-prompt steering is gated on shouldUseMiyo, so toggling
Miyo mid-session changes buildAgentSystemPrompt(). codex/opencode bake the
prompt at spawn, so without a restart the skill would be (un)seeded while the
running agent kept the stale prompt until a manual reload.

Call restartSystemPromptAffected() on Miyo availability change. It dedupes on
the real prompt key (backendSystemPromptKey === buildAgentSystemPrompt()), so
it restarts only when the rebuilt prompt actually differs and is a no-op
otherwise.

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

* fix(agent-mode): chain the Miyo prompt restart after seeding completes

restartSystemPromptAffected() ran synchronously while seedManagedBuiltins was
fire-and-forget, so on a Miyo-on flip codex/opencode could respawn with
steering that points at miyo-search before the skill is written/reconciled into
the agent dirs (and codex has restartOnManagedSkillsChange: false, so no later
repair restart). Chain the restart in the seed pass's .then() so the prompt and
the skill filesystem stay in lockstep.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 00:01:12 -07:00
Logan Yang
af89839a8d
fix(skills): keep overflow menu non-modal so Reveal in vault can't strand a scroll lock (#2581)
The Skills overflow menu used a modal Radix DropdownMenu, which engages
react-remove-scroll's document-level wheel/touch listener. "Reveal in vault"
synchronously moves focus into the file-explorer leaf, which can interrupt
the menu's close/unmount and leave that listener attached, killing mouse-wheel
scrolling across the whole app until restart (issue #118).

Set modal={false}, matching the sibling overflow menu in AgentTabStrip, so the
scroll lock is never engaged. Adds a regression test asserting the open menu
leaves no data-scroll-locked marker on the body.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 22:41:56 -07:00
Logan Yang
775d832ee5
fix(mobile): guard Agent Mode off the mobile load path (#2577)
* fix(mobile): shim `process` so the bundle survives module-eval on mobile

Follow-up to #2576 (preview issue #125). After the EventEmitter fix, mobile now
crashes at load with "ReferenceError: Can't find variable: process".

Cause: Agent Mode is desktop-only but its modules are statically imported, so
they're evaluated on every platform. Several read the Node global `process` at
module scope (e.g. `InstallCommandRow.tsx`'s `const DEFAULT_LABEL =
process.platform === "win32" ? ...`, the codex/claude/opencode descriptors),
and bundled Node deps read `process.env` at init. Mobile's WebView has no
`process`, so evaluating any of them throws during import and kills the plugin.

Fix: prepend a minimal `process` shim in the esbuild banner —
`var process = globalThis.process || { env:{}, platform:"", ... }`. Desktop
(Electron) has a real `globalThis.process` and uses it unchanged; mobile gets
the stub, so module evaluation no longer throws. The desktop-only code paths
that actually use `process` never run on mobile.

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

* chore(test-vault): copy-deploy into iCloud-synced vaults for mobile testing

`npm run test:vault` symlinks main.js/styles.css by default, but iCloud syncs
file contents, not link targets, so a symlinked build never reaches the phone.
Detect iCloud vaults (path under "/Mobile Documents/") and copy real files
instead, so deploying to an iCloud vault makes the build testable on Obsidian
mobile. Mirrors the existing WSL copy-mode rule.

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

* fix(mobile): keep agent mode off load path

* test(mobile): add load smoke check

* fix(mobile): share agent chat mode constant

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:56:58 -07:00
Wenzheng Jiang
c62211628c
Add miyo:// deeplink to register vault folder when enabling Miyo Search (#2578)
* Add miyo:// deeplink to register vault folder when enabling Miyo Search

When enabling Miyo Search, distinguish two failure modes and guide the
user with a miyo:// deeplink: (1) the Miyo app is not running -> offer
"Open Miyo" to start it; (2) the app is reachable but the vault folder
is not registered yet -> prompt the user to add it in Miyo first.

ConfirmModal now accepts React.ReactNode content so the scan-request
dialog can render a deeplink and folder name inline.

Ported from preview repo (#144).

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

* Drop the Request Miyo Scan confirmation; Miyo owns scanning

Scanning is handled entirely inside Miyo, so confirming a scan request on
enable was unnecessary. When the vault folder is already registered, enable
Miyo Search directly. The only remaining prompt is the deeplink that guides
the user to register their folder in Miyo when it isn't registered yet.

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

* Drop unnecessary batch-size reset and index call when enabling Miyo

Both were no-ops on the Miyo path: embeddingBatchSize is only consumed by
local Orama embedding (Miyo's backend has requiresEmbeddings() === false and
indexVaultToVectorStore short-circuits before the batch logic), and the index
call just issued a Miyo scan request via requestIndexRefresh -> scanFolder,
which Miyo's own folder watcher/scanner already handles. The active backend is
switched by VectorStoreManager's settings-change subscriber, so enabling only
needs to flip enableMiyo (and enableSemanticSearchV3).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:38:35 +09:00
Wenzheng Jiang
5929710d0d
Filter Miyo search results by inclusion/exclusion rules and cap output (#2580)
* Filter Miyo search results by inclusion/exclusion rules and cap output

Miyo semantic results now honor the same QA inclusion/exclusion scope as
locally-indexed search. A reusable createCopilotPatternFilter() predicate
resolves the active patterns once and keeps only chunks whose source path
passes; paths that don't resolve to a vault TFile are kept since the rules
can't be evaluated for them.

The retriever over-fetches candidates (RETURN_ALL_LIMIT) so filtering still
leaves enough chunks to fill finalK, then caps the result set to finalK
(the requested maxK, or DEFAULT_FINAL_K when unspecified). The sizing math
moves out of RetrieverFactory into the retriever.

Ported from preview repo (#131).

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

* fix(miyo): only over-fetch when an inclusion/exclusion filter is active

Default Miyo searches (no QA patterns, returnAll off) were always
requesting RETURN_ALL_LIMIT (100) candidates and slicing back down,
transferring/processing up to ~10x more chunks than the caller asked
for with no filtering benefit.

Add `hasActiveCopilotPatterns()` and bound the request to `finalK` when
no pattern filter is configured and the caller didn't request everything;
only over-fetch when filtering can actually drop results. Restores the
old bounded behavior for ordinary searches while keeping the over-fetch
needed to fill the cap after inclusion/exclusion filtering.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:28:56 +09:00
Logan Yang
ce8469d596
fix(mobile): don't evaluate node:events at load — fixes plugin failing to load (#2576)
Closes preview issue #125. On mobile the plugin failed to load entirely with
"TypeError: undefined is not an object (evaluating 'DKe.EventEmitter')".

Cause: `src/utils/rendererEventsShim.ts` imported `EventEmitter` from
`node:events` (marked external → `undefined` on mobile) and ran `installShim()`
as a module-load side effect via a side-effect import in main.ts. Reading
`.EventEmitter` off the undefined module threw during import — before onload or
any `Platform.isMobile` guard — taking the whole plugin down. The shim is a
desktop-only Electron-renderer/Claude-SDK workaround, irrelevant on mobile.

Fix: export `installRendererEventsShim()` instead of running at module scope,
guard it with `Platform.isMobile` (returns before any EventEmitter access), and
call it from `onload`. Mobile never evaluates node:events; desktop behavior is
unchanged.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:22:29 -07:00
Logan Yang
784ed61506
feat(agent-mode): expose Miyo vault search via a bundled miyo CLI skill (#2566)
* feat(agent-mode): add Miyo vault-search skill via the local `miyo` CLI

Closes preview issue #106. Ships a bundled `miyo-search` builtin skill that
instructs the agent to call the Miyo desktop app's `miyo` CLI (`miyo search`
/ `miyo files --json`) directly in its own shell. No MCP server, no bash
dependency (works under POSIX shells and Windows PowerShell), and no key
handed to the agent — the CLI talks to a loopback service it discovers itself.

- Cross-platform binary resolution: PATH-first with a per-OS absolute fallback
  (`~/.miyo/bin/miyo` on macOS/Linux, `%LOCALAPPDATA%\Miyo\bin\miyo\miyo.exe`
  on Windows), because Obsidian-launched shells often inherit a reduced PATH
  that misses Miyo's install dir.
- Graceful degradation guidance for "not installed" vs "service not running".
- Gated on Miyo being in use: the host seeds the skill when `shouldUseMiyo()`
  is true and prunes the seeded copy when Miyo is turned off (reconcile's
  reverse-sweep then removes the agent-dir symlinks). Re-runs on folder change
  and on Miyo-availability settings flips.
- `managedBuiltinSkills(includeMiyo)` keeps the gate decision in the host layer
  (skills layer must not import `@/miyo`) while staying unit-testable.
- `removeSeededBuiltin()` only deletes folders carrying the builtin version
  marker, never user-authored skills.

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

* fix(agent-mode): re-seed Miyo skill when self-host validation flips

Addresses Codex review on #2566. The Miyo-availability change detector only
watched enableMiyo/miyoServerUrl/isPlusUser, but `shouldUseMiyo` also depends
on `isSelfHostAccessValid()`, whose `selfHostModeValidatedAt` /
`selfHostValidationCount` fields are refreshed asynchronously at startup —
after the initial seed pass. So a vault where validation flips invalid→valid
post-load kept the miyo-search skill pruned until the user toggled Miyo,
changed the skills folder, or reloaded. Watch those two fields too.

(This guard becomes dead once the Miyo paywall is removed — tracked in
logancyang/obsidian-copilot-preview#140 — at which point shouldUseMiyo no
longer depends on self-host validation and these clauses are deleted.)

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:09:51 -07:00
Logan Yang
d761adda73
fix(miyo): make the Miyo backend failure notice user-facing (#2575)
The notice on Miyo init failure read "Failed to initialize Miyo backend. Check
Miyo service discovery or folder setup." — dev jargon that doesn't tell the
user the actual likely cause: Miyo isn't running, or this vault hasn't been
added to Miyo as a folder.

Replace it with a concise, actionable message that names the vault:
"Miyo isn't available for this vault ("<name>"). Make sure Miyo is running and
this vault is added to it." The raw error is still logged via logWarn for devs.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:51:53 -07:00
Logan Yang
a323d83831
feat(agent-home): disable Projects until supported; lead with Recent chats (#2574)
Closes preview issue #137. The Agent Home shelf listed Projects first (left)
and rendered a full, clickable project list, even though projects aren't wired
yet (selecting one only fired a "coming soon" Notice). That reads as an
available feature and leaks the project list into a UI that can't act on it.

- Recent chats now leads (left, default-active); Projects sits on the right,
  greyed and non-selectable, with a "Coming soon" tooltip on hover. Its list
  never mounts.
- AgentHomeShelf: a section can be `disabled` (+ `disabledTooltip`); the shelf
  defaults to and only ever resolves the active tab to the first selectable
  section, so a disabled tab's `renderBody` never runs.
- AgentHomeTab: greyed + `aria-disabled` + no-op click (kept non-native so the
  hover tooltip still fires), count hidden so an unshipped feature shows no
  tally; wrapped in the shared Tooltip when a hint is set.
- Dropped the "Projects are coming soon" Notice (the disabled tab + tooltip is
  the affordance); ProjectPickerList stays wired for when selection lands.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:16:00 -07:00
Logan Yang
137ab985e4
fix(agent-mode): mask the home dir in the opencode config Destination path (#2573)
The opencode configure dialog printed the managed binary's install path raw
(e.g. `/Users/alice/.obsidian-copilot/opencode`), leaking the OS username in
the UI and in screenshots users share. Every other agent-settings path is
collapsed to `~` for exactly this reason (#2546); this Destination row was
missed.

Wrap it in the existing `formatBinaryPathForDisplay()` so it shows
`~/.obsidian-copilot/opencode`. Display-only — the spawned/stored path keeps
its real absolute form.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:13:43 -07:00
Logan Yang
ef5a4035f4
feat(agent-mode): make Uninstall fully reclaim opencode (all copies, incl. in-vault) (#2572)
Closes preview issue #136. Now that the managed opencode binary lives outside
the vault (#2569), uninstalling the Copilot plugin no longer removes it, and
preview testers still have the old copy inside their vault that a plugin update
won't clear.

Rather than add a second, overlapping "remove" button, this folds the full
reclaim into the existing Uninstall action:
- `OpencodeBinaryManager.uninstall()` now removes EVERY downloaded copy — the
  whole OS-local `~/.obsidian-copilot/opencode` tree (all versions) AND the
  pre-#2569 in-vault copy (`<vault>/.obsidian/plugins/<id>/data/opencode`) —
  then clears managed settings. A custom binary path is left untouched.
- `downloadsSize()` sums both locations so the confirm shows what's freed and
  a freshly-updated tester (binary still only in the vault) sees a real size.

The in-vault sweep runs only on user click (not an auto-running migration), so
a preview tester migrates in one click: Uninstall, then Install.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:54:14 -07:00
Zero Liu
89823e1d03
feat(chat): power legacy chat from the model-management chat backend (#2556)
* feat(chat): power legacy chat from the model-management "chat" backend

Legacy chat, project chat, vault QA, quick command, and quick ask now
select models from the model-management "chat" backend
(backends.chat.enabledModels) instead of the legacy activeModels /
defaultModelKey. A new ConfiguredModel→CustomModel bridge feeds the
existing ChatModelManager (the stubbed v4 ChatModelFactory is untouched),
and a new "Quick Chat" curation section under the Agents settings tab
lets users choose which BYOK/Plus models appear in the chat picker.

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

* style(skills): apply prettier formatting to builtinSkills.test.ts

Collapse a multi-line expect() onto one line to satisfy the project's
prettier config (surfaced when running npm run format).

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

* fix(chat): stabilize chat backend model selections

* fix(chat): address chat backend review regressions

* fix(chat): show legacy chat models in a flat list

Drop the per-provider _group on chat picker entries so the legacy chat
model picker renders a single flat list (provider icons inline, no
section headers), matching pre-migration behavior. Agent Mode keeps its
per-backend headers via its own helper.

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

* refactor(chat): remove dead per-model parameter editing

After the model-management chat migration, per-model parameter editing
is unreachable: the chat selection is now a configuredModelId UUID, so
the ChatSettingsPopover editor (gated on a legacy activeModels match)
never renders, and TokenLimitWarning's activeModels lookup always misses
and falls back to global settings. v4 agent mode no longer supports
per-model parameter editing.

- ChatSettingsPopover: drop the model-param plumbing (originalModel /
  bridgedModel / canEditModelParameters, local-model save/debounce,
  ModelParametersEditor); keep System Prompt + Disable Builtin controls.
- TokenLimitWarning: always open the global Copilot settings tab.
- Delete the now-orphaned ModelEditDialog and ModelParametersEditor.

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

* refactor(chat): drop dead legacy model-key fallback in streaming session

The streaming chat session's chain cache key fell back to name|provider
when configuredModelId was absent. Both callers (Quick Ask, custom-command
chat) now source their model from useResolvedChatBackendModel ->
configuredModelToCustomModel, which always stamps configuredModelId, so the
fallback was unreachable. Derive the key directly from configuredModelId;
a model without one is treated as no usable model via the existing guards.

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

* refactor(chat): remove test-only getLegacyChatModelKey helper

The singular getLegacyChatModelKey was exported but had no production
callers — only the barrel re-export and one test assertion referenced it.
Remove it (and its export + assertion); the load-bearing plural
getLegacyChatModelKeys, which powers legacy-key resolution in
isChatModelSelectionForEntry, is kept and now carries the rationale comment.

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

* refactor(model-management): dedupe catalog-id to provider map

Drop the duplicate CATALOG_ID_TO_LEGACY_PROVIDER table in chatModelSelection
and reuse the now-exported CATALOG_ID_TO_CHAT_PROVIDER from
configuredModelToCustomModel (extended with anthropic/google for the
legacy-key path). Single source of truth for catalog-id to ChatModelProviders.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:52:50 -07:00
Logan Yang
5c8298fa66
refactor(agent-mode): centralize the ~/.obsidian-copilot app-data root + guard home dir (#2571)
Follow-up hardening to #2569 (preview issue #135). Moving the managed opencode
binary out of the vault made `~/.obsidian-copilot` our first OS-level install
namespace; this locks down its shape before GA.

1. Single source of truth: new `copilotAppDataDir(homeDir)` + `COPILOT_APP_DIR_NAME`
   in `src/utils/appPaths.ts` define the `~/.obsidian-copilot` root exactly once,
   with the rationale (not `~/.copilot` — collides with the GitHub Copilot CLI;
   matches `~/.opencode`/`~/.miyo` convention) and the "shared across all vaults,
   never destructively prune" invariant. `opencodeManagedDataDir` now composes
   `<root>/opencode`. Future managed runtimes compose under the same root.

2. Guard `getDataDir()` against an unusable home dir (empty / filesystem root),
   and wrap the install-time `mkdir` so an unwritable root (sandboxed/confined
   HOME on Linux Flatpak/Snap, locked-down accounts) fails with an actionable
   message naming the path, instead of a confusing downstream spawn error.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:58:56 -07:00
Logan Yang
cdc08d429d
fix(agent-mode): install the managed opencode binary outside the synced vault (#2569)
* fix(agent-mode): install the managed opencode binary outside the synced vault

Closes preview issue #135. The managed opencode binary was downloaded into
`<vault>/.obsidian/plugins/copilot/data/opencode/<version>/bin/opencode`, i.e.
inside the vault, so every sync service (Obsidian Sync, iCloud, Dropbox,
Syncthing) replicated the ~100MB per-OS/per-arch binary across devices (203MB
observed on one install, since old versions were never pruned).

- `getDataDir()` now resolves to a per-user, OS-local dir OUTSIDE the vault:
  `~/.obsidian-copilot/opencode/<version>/bin/opencode` (mirrors how companion
  CLIs like Miyo install under the home dir). Pure `opencodeManagedDataDir`
  makes the path construction unit-testable.
- Version pruning (`pruneOtherVersions`) after a successful install drops
  superseded version dirs so installs no longer accumulate.

No permanent migration code: the in-vault install only ever existed in v4
prereleases, so GA users never have it. Preview testers remove their existing
in-vault copy once (delete `.obsidian/plugins/copilot/data/opencode`, or
reinstall the plugin); the existing `refreshInstallState` missing-file check
then clears the now-dangling managed path and opencode reinstalls outside the
vault on next use. Custom-path users are unaffected.

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

* fix(agent-mode): drop version pruning from the shared opencode install root

Addresses Codex review on #2569: now that `getDataDir()` points every vault
under the same OS account at the shared `~/.obsidian-copilot/opencode` root,
pruning sibling version dirs on install could delete a binary another vault's
device profile still points at. The accumulation pruning guarded against was a
*sync* problem, which moving out of the vault already solves; leftover versions
now just sit in a non-synced local dir. Removing the prune is the safe fix.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:29:41 -07:00
Emt-lin
29293c14b1
feat(agent-home): segmented-tab shelf, aligned rows, top-anchored composer (#2550)
* feat(agent-home): brand icon, rotating greeting, and collapsible landing sections

- Add CopilotBrandIcon (inline brand SVG) beside the landing title; new
  ui-title font token sizes the heading off the Obsidian UI font, and register
  text-ui-title in the tailwind-merge font-size group so cn() dedupes it.
- Rotate the landing title through a curated greeting pool (landingGreetings),
  re-rolled per session so each new chat opens with a fresh line.
- Make AgentHomeSection collapsible (local state, chevron toggle, keyboard
  support, aria-controls). The disclosure and the header action sit as siblings
  to avoid nesting interactive controls, and the body's children unmount on
  collapse so an open "View all" popover tears down with it. Enable on Projects
  and Recent Chats.
- Resolve the landing subtitle's mode label through getModeLabel so it matches
  the ModePicker trigger (default -> Safe) from one source.

* fix(agent-home): keep landing scrollable on very short panes

The landing column was overflow-hidden, so a pane too short to fit the
status/title/composer stack clipped them with no way to scroll back
(small Obsidian splits / popouts). Restore overflow-y-auto on the
landing branch: the flex spacers still fill the column when there's
room (no scrollbar), and the column scrolls only when the fixed
elements genuinely overflow.

* fix(agent-home): resolve UI font token across themes

Obsidian's stock 20px UI token is --font-ui-larger, but some themes
(e.g. Primary) define --font-ui-large instead and omit the stock one.
Chain both for ui-larger/ui-title with a literal 20px floor, so the
landing title renders at the intended size under either convention
instead of dropping to inherited sizing when one var is missing.

* feat(agent-home): polish landing — colored project tiles, uppercase section headers, tighter spacing

- Section headers (Projects / Recent Chats): uppercase + tracking, drop the
  parens around the count, remove the leading section icon (keep the collapse
  chevron) since rows now carry their own leading marks.
- Project rows: stable per-id colored folder tile (tinted square + solid-hue
  folder) via new theme-aware `text/bg.project.*` tailwind tokens; hashed from
  the project id so the color is fixed per project.
- Landing: drop the redundant backend/mode subtitle (the composer pills already
  show it); retune spacing (heading→input, input→projects, between sections).
- Add-context "+" button: drop the border, bare glyph.
- "View all" project/chat links now use the theme accent color.

* docs(agent-home): refresh AgentHome section comments after icon removal

Drop stale references to the removed section icon in AgentHomeSection's
doc comments (section no longer supplies a type icon; rows carry their own
leading mark via `leading`/`icon`), and clarify the Recent Chats "View all"
trigger's pl-6 vs the Projects list's pl-8 alignment.

* refactor(agent-home): render brand icon from shared glyph data

Replace the dangerouslySetInnerHTML injection in CopilotBrandIcon with
plain JSX. Extract the glyph transform and path into COPILOT_AGENT_ICON_TRANSFORM
and COPILOT_AGENT_ICON_PATH constants; the addIcon string is now built from
the same primitives, so the React surface and the native Obsidian icon share
one source of truth and cannot drift.

* fix(agent-home): drop redundant count from "View all" links

The section header already shows the total (PROJECTS 10 / RECENT CHATS 582),
so repeating it in the "View all projects (N)" / "View all chats (N)" trigger
was duplicated. Trim both to "View all projects" / "View all chats".

* feat(agent-home): chip shelf landing — collapse sections into a covering panel

Replace the two always-open landing sections with a Claude-style chip shelf:
- Collapsed, Projects / Recent Chats are a centered row of pill chips (icon +
  title + count), built on the shared Button.
- Opening a chip *covers* the row with that section's panel — a titled card
  (icon + title + close ✕) over the section body; only one is ever open.
- Click anywhere outside the open panel collapses it (bound to the panel's
  ownerDocument for popout windows; Radix "View all" popovers are exempt).
- Projects' "New project" action moves to the panel's top-right, under the ✕.

AgentHomeSection loses its header component (title/count/collapse now live in the
chip); ProjectPickerList / GlobalRecentChatsSection render plain panel bodies and
keep the colored tiles, accent "View all", and count-free trigger.

* feat(agent-home): segmented-tab shelf, aligned rows, top-anchored composer

Turn the landing's Projects / Recent Chats into a persistent segmented-tab
card and polish the surrounding composer behaviour:

- Rename AgentHomeChip -> AgentHomeTab. Tabs use the ghost2 Button variant
  so Obsidian's native button chrome doesn't bleed through; selection is
  shown by background fill only (no border/shadow).
- Shelf card: one outer border + rounded-md. The panel is the scroll
  container via a min-h-0 chain, so on a short pane the card caps to the
  available height and its list scrolls internally while the tab bar stays
  pinned, instead of being clipped out of reach. An inner min-h reserves a
  consistent height across tabs.
- Recent-chat rows get a size-6 neutral icon tile matching the project
  tiles, so the leading slot is the same width and labels line up when
  switching tabs.
- Landing composer is top-anchored: a fixed-fraction (h-1/4) top spacer
  that doesn't absorb the composer's height changes, plus a flex-1 shelf
  region that does. Removing the active-note context chip now collapses the
  composer's lower half and the shelf upward, keeping the greeting fixed,
  instead of re-centering and dropping the top. The conversation state is
  unchanged (composer stays docked at the bottom).

* docs(agent-home): drop PR1 milestone label from project-sort DESIGN NOTE

Codex flagged the (PR1) reference as a STYLE_GUIDE violation (no
milestone/plan-step refs in code). State the read-only invariant
directly instead. Pre-existing PR1 refs from the merged PR1 are
baseline and left untouched.
2026-06-06 13:01:24 -07:00
Zero Liu
30fb227153
feat(agent-mode): support web tab and web selection as context (#2563)
* feat(agent-mode): support web tab and web selection as context

Agent Mode already rendered the shared web-tab UI but dropped the data at
two seams: AgentChatInput discarded the webTabs send payload and hard-coded
activeWebTab=null, and the session never serialized context.webTabs. Wire
both through, reusing ContextProcessor.processContextWebTabs (reader-mode
markdown, YouTube transcripts, timeouts, dedup) for extraction and a shared
buildWebTabsWithActiveSnapshot helper (extracted from ChatManager) for the
active-tab snapshot. Web selections now serialize as <web_selected_text>.

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

* fix(agent-mode): keep backend.prompt synchronous when no web tabs

The unconditional `await serializeWebTabContext(context)` in runTurn added
an async hop before `backend.prompt`, breaking the timing assumption that
the prompt is dispatched synchronously within sendPrompt — flaking the
existing sendPrompt tests (resolvePrompt is not a function) in CI. Only take
the async hop when there are web tabs to extract.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:36:24 +08:00
Zero Liu
62ff6217b3
fix(agent-mode): disable Claude thinking when the toggle is off (#2568)
Turning off "Show extended thinking" left options.thinking unset, so the
Claude Agent SDK fell back to the model default — Sonnet 4.6 / Opus 4.6
default to adaptive "summarized", so reasoning kept streaming into the UI.
Set thinking to { type: "disabled" } when off so the toggle is authoritative.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 20:37:43 +08:00
Zero Liu
2f49513425
fix(agent-mode): stop chat file links from recreating the vault path on click (#2567)
* fix(agent-mode): stop chat file links from recreating the vault path on click

Coding agents emit chat links to notes using the note's absolute on-disk
path (e.g. /Users/me/Vault/Inbox/Foo.md). The shared markdown link handler
passed that href straight to openLinkText, which resolves it relative to the
vault root, so clicking it materialized the whole Users/me/Vault/... chain as
a phantom note + parent folders instead of opening the existing note.

Normalize the clicked href in wireInternalLinks: decode percent-encoding,
convert in-vault absolute paths to vault-relative, and route out-of-vault
absolute paths to the OS opener so no phantom note is created. Relocate the
vault-path helpers to src/utils/vaultPath.ts and extract a shared
openWithSystemDefault, de-duping the copies in SkillsSettings.

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

* fix(agent-mode): open root-relative vault links instead of treating them as absolute

A chat link like `/Folder/Foo.md` that resolves to a real vault file is a
root-relative vault link, not an OS-absolute path. Resolve it (preserving any
`#heading` anchor) before the absolute-path branch so it opens the existing
note rather than falling through to the OS opener.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 20:06:45 +08:00
Logan Yang
867af3179d
Show an install prompt when a synced vault's agent binary is missing on this device (#2554)
* fix(agent-mode): treat a synced-but-missing binary as not-installed

When a vault syncs to a second device, a backend's binary path can be present
in settings while the binary was never installed locally. Install state was
derived purely from settings fields, so it reported "ready", the auto-spawn
fired, and the agent surfaced cryptic load errors instead of an install prompt
(logancyang/obsidian-copilot-preview#123).

Make install state reflect the disk:
- opencode `computeInstallState` now requires the configured `binaryPath` to
  exist on disk to count as installed; otherwise `absent`. `fileExists` is
  injected for testability.
- shared `binaryPathInstallState` (used by codex) gets the same existence
  check, so custom-binary backends behave consistently.
- `OpencodeBinaryManager.refreshInstallState` now reads raw settings (not the
  existence-checked state) so it can still detect and clear a *managed* binary
  whose file vanished; custom paths are left untouched.

Net effect: on a device without the binary, both Agent settings and the chat
show the existing "Install <backend>" prompt (the `absent` path) and the
preload probe is skipped — no error spew.

Tests: new missing-on-device branches for `computeInstallState` and
`binaryPathInstallState`; existing installed-state tests inject a present file.
Full suite green (3474).

Closes logancyang/obsidian-copilot-preview#123

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

* fix(agent-mode): existence-check Codex install state in the configure dialog

CodexInstallModal derived its status from binaryPath truthiness alone, so in
the synced-vault case this PR fixes, clicking "Install Codex" opened a dialog
that reported "Ready" with the stale (missing) path — not guiding the user to
re-detect or clear it. Route the dialog's status through the same
existence-checked `binaryPathInstallState` the descriptor uses, so a
synced-but-missing path reads "absent" consistently.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 00:09:14 -07:00
Logan Yang
240041895d
fix(agent-mode): raise tool-output cap to a 256k runaway backstop (#2553)
A local-model user saw "[Tool output truncated in Copilot UI: 39,166
characters omitted.]" and assumed the agent lost the output. It didn't — the
old 12k cap only trimmed what Copilot *displayed*; the backend always fed the
model the full output, and the chat already renders output collapsed in a
scrollable box (ActionCard `max-h-40 overflow-auto`), so the cap only cost
visibility. A setting for it would just push our bad default onto the user.

The original cap (added in the first Agent Mode commit) was a heap-memory
guard — "keep large results out of long-lived React state" — not a render-perf
fix (output is a plain collapsed `<pre>`, and dedup already bounds comparison
via `textFingerprint`). So instead of removing it outright, raise it far above
any realistic output: a 256k backstop that only trims genuinely runaway blobs,
with a marker that's explicit the agent still received everything. The dead
`truncated`/`originalLength`/`omittedLength` fields (never read by the UI) are
dropped from `AgentToolCallOutput` and its `AgentMessageStore` equality check.

Tests: a 100k output is stored verbatim; a 300k output is trimmed with the
"agent received the full output" marker. Full suite green (3471).

Closes logancyang/obsidian-copilot-preview#121

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 23:56:41 -07:00
Logan Yang
43bfd9c4cb
Recover the agent pane after cancelling the image attachment picker (#2552)
* fix(agent-mode): recover the agent pane after cancelling the image picker

Clicking + → image → Cancel left the agent pane unresponsive (Windows error
ding, clicks/keys ignored). The picker created a detached `<input type=file>`
that was never added to the DOM and only listened for `change`; on Cancel no
`change` fires, so focus was stranded on an off-tree node — which Electron on
Windows treats as a dead focus target that swallows subsequent input.

Extract `openImagePicker`: appends a hidden input to the view's own document
(popout-safe), handles BOTH `change` (files chosen) and `cancel` (dialog
dismissed), tears the input down on either path, and restores composer focus
via an `onSettle` callback so the pane stays interactive.

Tests: new openImagePicker unit suite (cancel path runs onSettle and never
onFiles, change path forwards files, input removed in both, no-onSettle).
Full suite green (3474).

Closes logancyang/obsidian-copilot-preview#119

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

* test: use activeDocument in openImagePicker test for popout-window lint

Replaces bare `document` with Obsidian's `activeDocument` global (assigned
from the test doc in beforeEach, matching the existing test convention) to
clear the obsidianmd/prefer-active-doc warnings.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 23:55:15 -07:00
Zero Liu
edf71a6834
feat(agent-mode): auto-focus chat input when the agent view opens or activates (#2564)
Focus the Agent Mode composer when the view is opened via command, revealed, or
switched to — including the already-open-and-active-but-unfocused case. Mirrors
the main chat's CHAT_IS_VISIBLE wiring and reuses the existing event-bus/context
latch patterns (queueVisible + pendingFocusRef) so focus is driven by the React
lifecycle rather than a setTimeout. Event-driven only, so passively restoring a
background agent pane on startup doesn't steal focus from the editor.

Fixes logancyang/obsidian-copilot-preview#129

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:11:00 +08:00
Zero Liu
c4a64ebf14
fix(agent-mode): keep trailing stream chunks after early prompt result (#2561)
Some backends (observed with opencode + fast DeepSeek models) flush the
`session/prompt` result before sending the turn's final
`agent_message_chunk` notifications. AgentSession nulled `placeholderId`
the moment the prompt resolved, so `handleSessionEvent` dropped those
late chunks and the assistant message was visibly cut off.

Carry the ACP `messageId` through to the domain event and route content
chunks by it: a chunk naming the just-settled turn lands on that message
via a one-turn grace window, even after the placeholder was cleared, and
without leaking onto a newer turn's placeholder.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:10:35 +08:00
Zero Liu
634f0a0ce1
fix(agent-mode): pass model modalities to opencode so vision models keep images (#2562)
opencode resolves a model's image capability as `injected ?? models.dev
catalog ?? false`, and its `unsupportedParts` filter strips image parts when
`input.image` is false. We injected every model as an empty `{}`, so providers
opencode has no catalog entry for (Copilot Plus, self-hosted OpenAI-compatible)
defaulted to text-only and had their images silently replaced with an error
note — even for genuinely multimodal models.

Carry the model's known `modalities` (and an `attachment` flag for image input)
into the opencode config. Generalizable and regression-safe: catalog models
just agree with opencode's own catalog, and modality-less models still inject
`{}`.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 08:57:18 +08:00
Zero Liu
f3e5840af5
feat(relevant-notes): dedicated pane with redesigned populated view (#2559)
* feat(relevant-notes): dedicated pane with redesigned populated view

Move Relevant Notes out of the chat into its own command-opened pane
("Open Relevant Notes") and redesign the populated view: a toolbar with
the active-note context line + Build index, color-graded relevance
meters/percentages, a "Best" tag, hover quick actions, a live indexing
overlay, and a restyled hover preview card. Add-to-Chat routes a wikilink
into the open chat/agent view via a new INSERT_TEXT_TO_CHAT event.

Rank relevant notes by semantic similarity only so the displayed
percentages stay monotonic — links no longer boost the score (link-only
notes still appear, without a meter). Remove the old in-chat Relevant
Notes block and its showRelevantNotes setting; update docs.

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

* refine(relevant-notes): linear meter, link badges, excluded state, preview fixes

- Meter width now maps 1:1 to the similarity score (70% → 70%).
- Add an excluded-file empty state when the active note is outside the
  index inclusion/exclusion settings (takes priority over build/list).
- Replace the "Best" badge with Outgoing-link / Backlink badges; the
  badge+percentage cluster swaps for the action buttons on hover.
- Move row action buttons into the flex flow so the title never overlaps
  them and truncates with an ellipsis in both states.
- Render preview snippet line breaks (whitespace-pre-line).

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

* fix(relevant-notes): route Add to Chat to the last-focused chat view

The Relevant Notes "Add to Chat" action hardcoded agent-first when picking
the target chat (getLeavesOfType(CHAT_AGENT_VIEWTYPE)[0] ?? legacy), so with
both chats open the wikilink landed in the wrong one. Reuse the existing
lastActiveChatViewType tracking (added for the add-to-context commands in
#2555) via a shared pickContextChatViewType() helper, so the chat we reveal
and the chat we type into always agree.

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

* refine(relevant-notes): show full note content in a scrollable preview

The hover-card preview truncated content two ways: the loaded text was
sliced to 1000 chars and the paragraph was clamped to 4 lines. Drop the
slice and replace line-clamp with a bounded, scrollable area (max-h-64 +
overflow-y-auto) so the full note is reachable in a slightly larger box.

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

* refine(relevant-notes): replace mount-timing setTimeouts with reliable signals

Address PR review on the Relevant Notes work:

- insertTextIntoActiveChat: drop the 50ms guess. The chat views' event bus
  (ChatViewEventTarget) now latches queued "insert text"; the view drains it
  on mount and ChatInputContext buffers/flushes it when the Lexical editor
  registers, so delivery no longer depends on mount timing.
- RelevantNotesView: drop the 50ms initial dispatch. useActiveFile now seeds
  from the current active file on mount, so the pane populates immediately and
  still updates via the active-leaf-change listener.
- RelevanceMeter: route the score-driven width/color through CSS variables
  (.copilot-relevance-meter-fill) instead of an inline style.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 06:54:12 +08:00
Zero Liu
410257424c
fix(agent-mode): support opencode 1.15.13 model API + force-upgrade older (#2557)
* fix(agent-mode): support opencode 1.15.13 config-options model API; force-upgrade older opencode

opencode 1.15.13 (sst/opencode#29929 "promote next ACP implementation")
dropped the dedicated ACP `models` state and moved the model catalog into a
`category:"model"` config option. The plugin only read `models`, so against
1.15.13 the picker showed BYOK models as "Not offered by agent" and the
current model as a raw provider UUID that failed to run.

Derive the catalog from the `category:"model"` config option when `models`
is absent and route switching through `session/set_config_option` (via a new
ModelState.apply spec). Pin the managed binary to 1.15.13 and prompt users on
older opencode to upgrade — managed reinstall or `opencode upgrade` for custom
binaries — from both the chat view and the configure dialog.

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

* fix(agent-mode): handle opencode config-option effort

* feat(agent-mode): prefetch opencode per-model effort options for the picker

opencode ≥ 1.15.13 advertises its catalog via a `category:"model"` config
option whose entries carry no per-model effort; effort is a sibling
`category:"thought_level"` option opencode only surfaces for the *active*
model. So `translateBackendState` could only attach effortOptions to the
current model, leaving every other picker row with no effort stepper until
it was selected.

Eagerly, right after a session's catalog loads, probe each enabled model
once to learn its effort and cache it for the picker. The probe loop reuses
the existing warm probe session (a model switch is ~ms; creating a session
is ~1s) and runs before the warm entry is published, restoring the original
model in a finally — so the session the manager adopts is never left on a
probed model (race-free). opencode-only via a new optional descriptor hook
`prefetchEffortCatalog`; other backends are untouched.

- descriptor.ts: add optional `prefetchEffortCatalog` to BackendDescriptor
- opencode/descriptor.ts: implement it (skip missing-key models, swallow
  per-model errors, restore original; frozen EMPTY_EFFORT_CATALOG)
- AgentModelPreloader: effortCatalog cache + getEffortCatalog + run prefetch
  in runProbe before publishing warm; clear on clearCached/shutdown
- AgentSessionManager: getEffortCatalog passthrough
- agentModelPickerHelpers: buildEffortOptionsByModelKey falls back to the
  prefetch cache for non-active rows (live effort wins for the active model)
- useAgentModelPicker: include the effort-catalog in the picker re-render signal

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

* fix(agent-mode): re-probe effort catalog on restart with an active session

Enabling a new model restarts the backend, but when the active chat tab was
on that backend, restartBackendNow recreated the session without re-probing.
clearCached had already wiped the per-model effort catalog, and a live
session's attachModelCacheSync mirrors catalog state but not the effort
catalog — so effort steppers vanished for every model until a plugin reload.

Couple restart with probing: for an installed backend, always run the
preloader probe (which re-runs the effort prefetch). When a tab on the
backend is active, await the probe and let the replacement session adopt the
warm proc — still a single spawn, with the per-model switch flicker kept on
the throwaway probe session. The only restart-without-probe case left is an
uninstalled backend, which runProbe already self-guards.

Also reformats one unrelated builtinSkills test line via `npm run format`.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 06:54:04 +08:00
Zero Liu
6d400f67d5
fix(agent-mode): route add-to-context commands to focused chat view (#2555)
* fix(agent-mode): route "add to chat context" commands to focused chat view

The "Add selection to chat context" and "Add web selection to chat
context" commands always called activateView(), forcing focus into the
legacy chat even when the user was working in the agent chat. Since the
selected-text atom is shared and both views render it, this only routes
which chat to reveal: track the most-recently-focused chat view, then
prefer the focused one when both are open, the open one when exactly one
is, and the agent chat (when usable) when none is.

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

* style(context-pill): match selected-text pill remove button to other pills

The selected-text (line selection) pill used a hand-built Badge with an
always-visible X button on the right, unlike every other context pill,
which renders the remove control on the left via ContextBadgeWrapper
(the leading icon morphs into an X on hover/focus). Drop the duplicate
local ContextSelection in favor of the existing ContextSelectedTextBadge
so the pill matches its siblings and gains their truncation + tooltip.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:25:32 +08:00
Logan Yang
49cddd2b26
feat(agent-mode): persist last-selected permission mode across new conversations (#2549)
New Agent Mode conversations always started in the backend's natural default
mode, so a user who works in `auto` had to re-select it for every new chat.
The mode picker only reflected live session state and nothing seeded a fresh
session with a remembered choice.

Make mode sticky, symmetric with the existing model/effort persistence:
- Add `defaultMode` to each backend's settings slice (+ sanitizer wiring so it
  survives reload; the sanitizer allowlists fields).
- `applyMode` now takes the canonical mode and persists it after a successful
  apply (no persistence if the apply throws).
- New sessions replay the persisted mode once ready via a generic,
  backend-agnostic `replayPersistedMode` helper that dispatches through the
  session's own `mode.apply` spec — no per-backend or per-mode branching. A
  backend without modes, or that doesn't offer the stored mode, is a no-op
  (falls back to its default).

Persistence is per-backend so each agent reopens in its own last-used mode and
an unsupported mode never leaks across backends.

Tests: new replayPersistedMode unit suite (seed via setMode/setConfigOption,
unsupported-mode fallback, error swallowing) + updated picker dispatch test.
Full suite green (3464).

Closes logancyang/obsidian-copilot-preview#117

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 16:23:15 -07:00
Zero Liu
7aa1733857
fix(tools): remove zod transforms that break autonomous agent tool binding (#2551)
* fix(tools): remove zod transforms from tool schemas that break agent bindTools

readNote.chunkIndex was defined as z.preprocess(...).optional(), which is a
zod transform/pipe. When the autonomous agent binds tools, LangChain's
toJsonSchema() rewrites the schema to its input form (interopZodTransformInputSchema),
exposing the transform node, then zod v4's toJSONSchema() throws
"Transforms cannot be represented in JSON Schema". This surfaces at
langchainStream.ts as "Model request failed: ..." and breaks the entire
brain-icon ReAct agent loop (the non-agent Plus path is unaffected because it
binds only clean utility tools).

Replace the preprocess fields with JSON-Schema-representable unions and move the
string coercion into each tool's func:
- readNote.chunkIndex -> z.union([z.number().int().min(0), z.string()]).optional()
- writeFile.confirmation -> z.union([z.boolean(), z.string()]).optional()
  (writeFile only dodged the throw via its .default() wrapper; fixed defensively)

Add src/tools/toolSchemas.jsonschema.test.ts, which runs LangChain's
toJsonSchema() over the read/composer tools so a future transform in a tool
schema fails in CI instead of at runtime.

Refs logancyang/obsidian-copilot-preview#69

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

* fix(tools): validate readNote chunkIndex to prevent out-of-bounds crash

The union schema's string branch bypassed the old z.number().int().min(0)
validation, so a string like "-1" or "1.5" was coerced straight through; the
bounds check only rejects index >= totalChunks, so chunks[chunkIndex] became
undefined and the tool crashed while logging chunk.chunkIndex.

coerceChunkIndex now returns null for any value that is not a non-negative
integer (undefined still defaults to 0), and readNote returns a structured
invalid_chunk_index response instead of indexing out of bounds. Add coverage
for "-1", "1.5", and "abc".

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 05:56:57 +08:00
Emt-lin
c17602c936
feat(agent-home): Agent Home PR1 — landing homepage + read-only Projects/Recent Chats (frontend-only) (#2545)
Pure-frontend Agent Mode refactor: replace the AgentTabStrip+AgentChat
surface with a persistent AgentHome shell that derives a per-session
view state — a session with no user-visible messages shows the global
landing (centered composer + read-only Projects / Recent Chats); once it
has messages it becomes the conversation. The composer is a
position-stable node moved between centered and bottom via CSS only (no
remount), so drafts survive the transition.

- Split the old AgentChat monolith into AgentHome (shell, three-state
  derivation, owns draft + file-drop), AgentChatInput (controlled
  composer taking a referentially-stable `draft` prop), and renders
  AgentChatMessages directly (no transcript wrapper).
- Extract hooks: useAgentChatRuntimeState (single subscribe→sync),
  useAgentHistoryControls, useAgentInputDrafts (per-session draft store
  replacing the key={internalId} remount; useMemo-stabilized return).
- Read-only landing: ProjectPickerList + GlobalRecentChatsSection over a
  shared AgentHomeSection primitive (lazy paging); project click is a
  coming-soon Notice — zero backend touch.
- formatRelativeTime util (guards non-finite ages → "now").
- Landing spacers collapse to 0 under overflow (no min-height) so the
  column packs to the top and scrolls only on genuine overflow.

Docs: designdocs/AGENT_HOME_ARCHITECTURE.md.
2026-06-01 15:45:48 -07:00
Logan Yang
3ba94de0cb
feat(skills): ship Node fallback script for builtin Copilot Plus tools (#2548)
Windows users without Git Bash can't run the `.sh` scripts the builtin
tools (web search, PDF, YouTube, X) ship, so installation/execution
dead-ends there. Ship an equivalent Node `.mjs` script alongside each
`.sh` and teach the agent the fallback chain.

- builtinSkills.ts: add `nodeScriptPreamble()` (global fetch, node: core
  modules only, no npm deps) mirroring the shell script's env reads,
  relay call, 401/403 → upgrade mapping, and exit codes. Each skill now
  ships both a `.sh` and a `.mjs`.
- SKILL.md "How to run" (shared `howToRunSection` helper): try `sh`
  first, fall back to `node <script>.mjs` when the platform can't run sh,
  and prompt the user to install Node.js if neither runtime exists.
- agentSystemPrompt.ts: extend COPILOT_PLUS_TOOLS_STEERING with the same
  sh → node → install-Node guidance.
- Bump skill versions (relay 2→3, read-pdf 3→4) so seeded copies refresh.

Closes #115

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:50:46 -07:00
Logan Yang
cc5f2782d7
feat(agent-mode): mask home dir with ~ in Agent settings binary paths (#2546)
* feat(agent-mode): mask home dir with ~ in Agent settings binary paths

The OpenCode/Claude/Codex entries in Agent settings render the resolved
binary path verbatim, exposing the user's OS username (e.g.
/Users/<username>/.local/bin/claude). Users frequently share settings
screenshots when reporting issues, leaking that path info.

Collapse a leading home-directory prefix to ~ for display only:
- collapseHomeDir() — pure, testable helper in pathUtils.ts (injected
  homeDir, optional case-insensitive match for Windows, preserves the
  original separator style, requires a real path boundary).
- formatBinaryPathForDisplay() — thin os.homedir()/platform wrapper in
  binaryPath.ts, used in AgentSettings.tsx.

The stored/spawned path keeps its real absolute form; this is purely a
presentation change. The truncation tooltip also shows the masked path.

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

* fix(pathUtils): normalize separators before home-prefix comparison

Windows paths can be stored with forward slashes (C:/Users/Alice/bin) while
os.homedir() returns backslashes (C:\Users\Alice). collapseHomeDir now compares
separator-normalized (backslash → forward slash) views of both head and homeDir,
while slicing the suffix from the original absolutePath so its real separators
are preserved for display. Adds three test cases covering forward-slash path vs
backslash home, backslash path vs forward-slash home, and mixed-separator suffix
preservation.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 14:18:01 -07:00
Zero Liu
fd9873b98b
Add cross device path support (#2541)
* Add cross device path support

* add fallback

* update

* test: mock unavailable localStorage for device id

---------

Co-authored-by: Logan Yang <logancyang@gmail.com>
2026-05-31 21:39:42 -07:00
Logan Yang
ab96aebcd8
docs: Windows setup guide for Claude and Codex Agent Mode (#2542)
* docs: add Windows setup guide for Claude Code Agent Mode

Windows users hit a confusing dead end: the Claude Code installer does not
add the binary to PATH, so `claude` and `where.exe claude` report
not-found even on a healthy install, leading users to think it broke. Copilot
resolves the binary by file path, not PATH, so the install is fine.

Add docs/agent-mode-windows-setup.md walking through install, sign-in,
Auto-detect / manual path, and troubleshooting, with the PATH gotcha called
out up front. Link it from docs/index.md and docs/agent-mode-and-tools.md.

Docs-only slice of logancyang/obsidian-copilot-preview#108; the in-plugin
not-found copy linking to this page is a follow-up.

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

* docs: don't link v4 Windows guide from the v3 agent doc

agent-mode-and-tools.md documents the v3 in-process autonomous agent (vault
tools, @vault, max-iterations), a different feature from v4 Agent Mode (the
Claude Code / Codex / OpenCode coding-agent backends). The Windows guide is
about v4, so the callout there was misplaced. Keep only the standalone
docs/index.md entry.

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

* docs: trim Windows guide to the exact command sequence

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

* docs: add Codex Windows installer for Agent Mode

* docs: script Claude Windows Agent Mode setup

* docs: harden Codex CLI path resolution

* docs: verify Codex login before finishing setup

* docs: avoid redundant Codex login prompts

* fix: auto-detect Codex ACP on Windows

* fix: address Windows agent setup review

* fix: align Claude Windows configure modal

* fix: clarify Windows PowerShell install label

* fix: use gist Windows install helpers

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:53:56 -07:00
Logan Yang
26bed2e5a4
scripts/test-vault.sh: WSL copy mode and self-vault guard (#2543)
The script previously symlinked `main.js` / `styles.css` into the test
vault and assumed the destination filesystem could resolve POSIX
symlinks. On WSL with a vault under `/mnt/c/...`, Obsidian on Windows
cannot follow WSL-flavored symlinks stored on DrvFs/9p, so the symlinks
appear broken and the plugin fails to load.

Detect WSL (`uname -r` contains "microsoft") with a `/mnt/`-mounted
vault and switch to file copy. macOS / native Linux keep the existing
symlink behavior — no change for the canonical developer path.

Also refuse to deploy when the worktree itself lives inside the target
vault. Otherwise the build artifacts and source tree become vault
content, Obsidian indexes the whole repo on the next reload, and the
plugin directory may coincide with the worktree (deploying onto itself).
2026-05-31 17:07:13 -07:00
Logan Yang
ec26c8ef90
Expand Windows auto-detect for Claude and opencode binaries (#2540)
Claude (#110): extend `windowsCandidates` in claudeBinaryResolver to
probe the native installer destinations Claudian's resolver covers —
`~/.local/bin/claude.exe` (the `irm claude.ai/install.ps1 | iex` default
recommended in the docs), `~/.claude/local/claude.exe`,
`%LOCALAPPDATA%\Claude\claude.exe`, and the two `Program Files\Claude`
variants. Also probe `cli-wrapper.cjs` (newer Claude Code packaging)
alongside `cli.js` under each node-tool bin dir. Deliberately omit
`WindowsApps\Claude.exe` (desktop GUI launcher) and any PATH walk to
keep auto-detect from ever returning a non-headless binary; that
validation work stays scoped to #107.

opencode: add a parallel `opencodeBinaryResolver` (pure-leaf, mirrors
Claude's shape) and wire it as the `detect` callback for the "Use your
own binary" picker. Probes `~/.opencode/bin/opencode[.exe]` (native
installer), `~/.bun/bin`, `~/.local/bin`, `%LOCALAPPDATA%\opencode\bin`,
ProgramFiles variants, and each node-tool bin dir for `opencode.exe`.
Falls back to `detectBinary("opencode")` (PATH walk) when the resolver
misses, so users with a non-standard install dir on PATH still match.
Fixes the cross-OS sync case where a stale POSIX override path from
another machine left auto-detect with nowhere useful to look.
2026-05-31 14:56:51 -07:00
Zero Liu
921ce90296
use new logo 2026-05-31 13:25:48 -07:00
Logan Yang
16bae60e4c
Ship Copilot Plus tools as builtin Agent Mode skills (#2537)
* feat(agent-mode): ship Copilot Plus tools as builtin skills

Expose Copilot Plus web search, PDF extraction, YouTube transcription, and X
fetch to all three agents (Claude, Codex, OpenCode) as plugin-shipped builtin
skills, so paying users keep these capabilities in Agent Mode.

- builtin skills: four SKILL.md + Node `.mjs` script folders, seeded into the
  canonical skills store and fanned out to every agent via the existing
  symlink mechanism (metadata.copilot-enabled-agents). A `relaySkill` factory
  generates the three near-identical relay tools; PDF is bespoke (multipart).
- seedBuiltinSkills: idempotent, version-stamped writer (writes when missing or
  the bundled version is newer; never touches user skills). Wired into plugin
  load before the first SkillManager.refresh().
- copilotPlusEnv: injects the decrypted Plus license + relay base URL + user id
  into each backend's spawn env (a plugin-managed layer merged before user
  envOverrides). Claude receives it via a getManagedEnv callback so sdk/ keeps
  its lint boundary (no backends/ import). Backends restart on Plus
  sign-in/out so the license env refreshes without a reload.
- no-license UX: scripts exit non-zero with an upgrade/renew prompt the agent
  relays to the user.

Vault semantic search is intentionally deferred (it runs against the in-plugin
vector store, not the relay).

Refs https://github.com/logancyang/obsidian-copilot-preview/issues/104

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

* fix(skills): match pdf4llm JSON contract and recursive mkdir for nested skill folders

Mirror brevilabsClient.ts: send base64 JSON body to /pdf4llm instead of
multipart FormData, which is what the relay's production contract expects.
Bump READ_PDF version to 2 so existing seeded copies refresh.

ensureDir now creates parent directory segments one at a time (matching
ensureFolderExists in utils.ts) so seeding into nested paths like
`copilot/skills` works correctly on a fresh vault.

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

* fix(skills): don't overwrite user-authored skills with builtin-name collision

seededVersion now returns null (not 0) when copilot-builtin-version is absent,
so a user-authored SKILL.md whose folder name happens to match a builtin name
is treated as user content and left untouched. Adds a regression test for this
case.

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

* fix(skills): reseed builtin skills when the canonical skills folder changes

When the user changes Settings → Agent Mode → Skills folder, the discovery
refresh ran against the new folder but seedBuiltinSkills was only called at
plugin load. The Plus tools would disappear from the new folder until the
next plugin reload.

Added a subscribeToSettingsChange handler that calls seedBuiltinSkills for
the new folder path whenever agentMode.skills.folder changes.

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

* fix(skills): clarify 'How to run' instruction to resolve the script path explicitly

Replace the ambiguous \`\$SKILL_DIR\` shell-variable placeholder with explicit
prose directing the agent to find this SKILL.md on disk and use its parent
directory — making it clear the path resolution is a task for the agent, not
an unset shell variable.

Also fix a stale comment that said PDF used multipart (it now uses JSON + base64).

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

* fix(skills): re-seed when SKILL.md is current but support files are missing

If Obsidian exits after writing SKILL.md but before writing the .mjs script,
the seeder would see the current version and skip the skill, leaving the agent
with a skill that advertises a script that doesn't exist.

Now when the version is current, also check all support files exist before
skipping. If any are missing, fall through and re-seed. Adds a regression test
for the partial-write recovery case.

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

* fix(skills): chain skillManager.refresh() after seeding on folder change

The fire-and-forget seeding after a skills folder change wasn't followed by a
discovery pass, so the newly seeded Plus skills would not get symlinked into
agent dirs until a later manual refresh or plugin reload.

Chain .then(() => skillManager.refresh()) so discovery runs synchronously
after seeding completes.

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

* feat(agent-mode): steer agents toward Copilot Plus builtin skills

Add a system-prompt block telling the agent to prefer the builtin
copilot-web-search / copilot-read-pdf / copilot-youtube-transcript /
copilot-fetch-x skills over its own web/fetch tools, with an explicit
fallback: if the matching skill is missing, disabled, or reports it needs an
active Copilot Plus license, use whatever equivalent capability the agent has
(or say it's unavailable) — never refuse the request.

Sent to every user regardless of Plus status. Gating on isPlusUser would be
wrong (valid self-host mode is Plus-enabled but reports isPlusUser=false), and
the fallback clause means non-Plus / unlicensed agents transparently use their
own tools — so steering everyone is safe. Suppressed with the rest of the
builtin framing when "Disable builtin system prompt" is on.

The fallback clause also makes this safe to merge before the builtin skills
land: agents fall back to their own tools until the skills exist.

Refs https://github.com/logancyang/obsidian-copilot-preview/issues/104

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

* refactor(skills): run Copilot Plus builtin skills via sh + curl, not node

The skill scripts run in the agent's shell, where node is not reliably on
PATH (Obsidian launches with a minimal PATH that excludes nvm/Volta/Homebrew
node). Rewrite the four builtin skills (web search, PDF, YouTube, X) as POSIX
sh scripts that call the Brevilabs relay with curl — curl/sed/base64 live in
/usr/bin and are reachable regardless of the user's node setup. Bodies are fed
to curl over stdin so a large base64 PDF never hits the arg-length limit, and
a dependency-free json_escape avoids requiring jq. Versions bumped so seeded
copies refresh. Tests assert on the generated script content.

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

* fix(skills): write support files before SKILL.md to prevent stale-script partial writes

Reorder the write sequence so support files (.sh scripts) are written
first and SKILL.md (which carries the version stamp) is written last.
If Obsidian crashes mid-write the version in SKILL.md stays at the old
value, so the next startup re-seeds the whole skill instead of skipping
it as current while leaving a stale script on disk.

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

* fix(skills): preserve user-toggled copilot-enabled-agents when upgrading a builtin

When a user disables a builtin skill for a specific agent via the UI,
runToggleAgent rewrites copilot-enabled-agents on disk. Previously the
seeder would overwrite SKILL.md with the full bundled agent list on a
version bump, silently undoing that choice.

Now preserveEnabledAgents() reads the existing copilot-enabled-agents
line before overwriting and splices it into the bundled replacement, so
agent-disable preferences survive upgrades. Regression test added.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 09:59:59 -07:00
Logan Yang
c4f9c99343
Apply agent binary-path and install changes without a plugin reload (#2536)
* fix(agent-mode): apply binary-path/install changes without a plugin reload

Applying a backend binary path (or installing/updating a binary) only
re-rendered the settings status line: the running/warm process kept the old
binary, and a backend installed after plugin load was never spawned, so users
had to toggle the plugin off/on for changes to take effect.

The preload loop runs once at onload and skips non-ready backends, and nothing
in the session layer listened for install-state changes — `subscribeInstallState`
had no non-test callers (the settings UI just re-derives its status line from
the settings atom).

Wire each descriptor's install-state change into a new
`AgentSessionManager.onInstallStateChanged`, mirroring the existing provider /
skills / system-prompt restart subscriptions. It dispatches on three
transitions, reusing the existing restart/refresh coalescing:
  - now uninstalled → tear down the live proc + drop the warm probe;
  - installed with a live/warm process → restart/refresh against the new binary;
  - installed but never probed (freshly installed) → kick a first preload.

Also guard `restartBackendNow`'s replacement-session spawn on `isBackendInstalled`
so clearing a path mid-use doesn't try to respawn against a missing binary.

Refs https://github.com/logancyang/obsidian-copilot-preview/issues/102

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

* fix(agent-mode): narrow opencode install-state subscription to binary fields only

Model selection (defaultModel) and probe-session (probeSessionId) writes
update the same agentMode.backends.opencode object, so comparing by object
identity caused onInstallStateChanged → restartBackendNow to fire on every
model pick, closing/replacing an active chat unnecessarily.

Narrows the predicate to only binaryPath / binaryVersion / binarySource,
matching the codex descriptor's existing pattern.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 17:30:26 -07:00
Zero Liu
f93cbbfd60
feat(agent-mode): detect Claude CLI sign-in and add in-app OAuth (#2534)
When the `claude` CLI is signed out, sending an Agent Mode chat used to
resolve silently with an empty assistant bubble. The adapter now gates
`prompt()` on a deterministic `claude auth status --json` check, throwing a
typed `AuthRequiredError` so the turn surfaces a visible error, and the chat
status pill shows a recoverable "Sign in to Claude" CTA that runs
`claude auth login` (the CLI opens the system browser; we surface the printed
URL as a fallback link). Adds an optional `auth` capability to
`BackendDescriptor`, a `claudeAuth.ts` helper, a `useBackendAuthState` hook,
and unit tests for the parser, the adapter auth gate, and the session error
rendering.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 16:25:09 -07:00
Logan Yang
3f96a90a77
fix(agent-mode): isolate backend init failures from plugin onload/onunload (#2535) 2026-05-30 16:22:54 -07:00
Zero Liu
d10aab9861
Enforce no-global-app via ESLint and fix all violations (#2533)
* chore(lint): enforce no-global-app via no-restricted-globals; fix all violations

Flip on `no-restricted-globals` with an `app` entry (error) for production
`src/**/*.{ts,tsx}`, exempting test files (they consume the `window.app` mock).
Sweep the whole codebase to 0 violations by threading `app` explicitly:
useApp() in React, this.app / this.chainManager.app in classes, params in plain
modules, a seed pattern for no-arg getInstance() singletons, setters for eager
module-level singletons, and factory functions for app-using built-in tools.

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

* test(agent): pass full mock app to initializeBuiltinTools

The initializer's app branch dereferences app.vault.getRoot() via
registerFileTreeTool, so passing the bare vault crashed setup before any
tools registered. Pass the full mock app instead.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:52:46 -07:00
Zero Liu
25ad6bfc6e
fix(agent-mode): clarify GFM table rule to prevent broken caption pipes (#2532)
The shared agent system prompt told the model to "immediately add ` |`
after the table heading", which it read as appending a trailing pipe to
a table's caption/title line. That produced an orphan pipe-terminated
line above the real header with no delimiter row, corrupting GFM table
rendering. Replace it with an unambiguous GFM table spec that forbids
trailing pipes on any non-table-row line, and update the test.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 11:28:32 -07:00
Zero Liu
c63bacc211
feat(agent-mode): copy/insert message actions + shared action buttons (#2531)
* feat(agent-mode): copy/insert message actions + shared action buttons

Add a Copy / Insert action row beneath completed assistant turns in Agent
Mode (gated on streaming completion, non-cancelled turns, and non-empty
trailing prose) and extract shared MessageActionButton / CopyButton /
useCopyToClipboard primitives out of ChatButtons. Lifecycle buttons
(Edit / Regenerate / Delete) now render only when a handler is wired, so
Agent Mode — which can't regenerate or delete yet — shows only Copy /
Insert for both user and assistant messages. Adds an insertAtCursor(app)
helper that threads app rather than the global, plus tests.

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

* fix(insert): thread app through insertIntoEditor so popouts target the right editor

insertAtCursor validated the editor/selection via the passed app but then
delegated to insertIntoEditor, which re-resolved the leaf through the global
app — so in a popout the selection check and the actual write could target
different windows. Make insertIntoEditor take an App and resolve from it;
update both other callers (CustomCommandChatModal, YoutubeTranscriptModal).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 11:25:12 -07:00
Zero Liu
d3f8f99e1b
Inline Claude ask-user questions (#2530) 2026-05-30 10:41:38 -07:00
Zero Liu
38f6e42bcc
fix(agent-mode): surface missing provider credentials (#2529) 2026-05-29 23:39:18 -07:00
Zero Liu
9ff5ba05de
fix(agent-mode): MCP server prefix + humanized tool-call labels (#2528)
* fix(agent-mode): show MCP server prefix and humanize tool-call labels

Parse the `mcp__<server>__<tool>` qualified name into a bare tool name
plus an `mcpServer` field, threaded through the ACP and SDK translators,
session types, and merge logic. The trail now renders MCP calls as
`server · Tool name` (so an MCP tool whose bare name collides with a
native tool no longer masquerades as it), humanizes generic/unknown tool
names instead of collapsing to "…", and adds a dedicated AskUserQuestion
summary.

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

* fix(agent-mode): keep MCP tools from masquerading as native plan/aggregate tools

Both display and control-flow special-casing keyed off the bare tool name,
losing the MCP-vs-native distinction:

- Aggregate cards now keep the `server ·` prefix; toolKeyFor namespaces MCP
  calls per-server so they neither fold into a same-bare-named native tool's
  aggregate nor merge across servers.
- vendorMetaFields/deriveToolKind take mcpServer and gate isPlanProposal +
  switch_mode on its absence; the EnterPlanMode mode-switch is likewise gated.
  An mcp__srv__ExitPlanMode no longer routes through the plan-approval flow.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 23:18:22 -07:00
Zero Liu
02f667a905
feat(agent-mode): port v3 markdown formatting rules into shared agent prompt (#2527)
Port the missing v3 chat-prompt formatting conventions (DEFAULT_SYSTEM_PROMPT
rules 7-10) into COPILOT_PROMPT_BASE so all three agent backends (Claude SDK,
Codex, OpenCode) follow vault-native formatting:

- Note titles render as bare [[title]] wikilinks, never backticked (rule 7) —
  the core fix; agents were emitting `[[Note]]`, which Obsidian won't link.
- Internal/web image links must not be wrapped in backticks (rules 8-9).
- Tables port the GitHub-table heading convention (rule 10).

The new rules live inside COPILOT_PROMPT_BASE (still suppressed by the
"Disable builtin system prompt" toggle) and stay model/provider-agnostic.
Add pin tests for each ported rule.

Closes logancyang/obsidian-copilot-preview#100

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 22:37:21 -07:00