Merge v4-preview into master - #2781
Conversation
Squashed from 8 commits on zero/acp-test: - feat(agent-mode): introduce Agent Mode feature (squashed) - Fix eslint - hide add context button - chore: ban parent-relative imports and rewrite existing ones to @/ aliases - wip(agent-mode): skills management + slash command revamp - fix(agent-mode): initialize SkillManager before preload probes - fix(build): align svgr jsxRuntime with tsconfig classic jsx - fix(test): stub ItemView/WorkspaceLeaf in ChatSingleMessage obsidian mock
Bash tool calls collapse the command to 60 chars in the trail, which is correct for noise but left the full command unreachable. Add an optional `expandedDetails` hook on `ToolSummary` and implement it for Bash so clicking the card surfaces the full command (with `# description` prefix when present). Drop the `!inline` expand restriction so commands inside an `AggregateCard` are individually expandable too. Also adds an AGENTS.md rule requiring `cn()` around any Tailwind class string that lives outside a literal JSX `className=` attribute so `eslint-plugin-tailwindcss` actually lints them, and refactors ActionCard's class composition to follow it. Includes two unrelated micro-edits already in the working tree: trailing-whitespace fix in chatModelManager.ts and a stale TODO removal in AGENT_MODE_TODOS.md. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… GUI PATH (#2480) macOS GUI apps launched from Finder/Dock inherit a minimal launchd PATH (`/usr/bin:/bin:/usr/sbin:/sbin`) that omits Homebrew and user install prefixes, so `which codex-acp` returned nothing and the Auto-detect button falsely reported "not found." Augment PATH with the well-known prefixes (`/opt/homebrew/bin`, `/usr/local/bin`) before invoking `which`, sharing one source of truth with the existing spawn-time augmentation in `nodeShebangPath`. For Claude, the Auto-detect button now routes through the existing `resolveClaudeBinary` resolver (Volta/asdf/NVM/Homebrew/ npm-global/`~/.local/bin`) via a new optional `detect` prop on `BinaryPathSetting`, matching the candidate list already used by the top status row.
…2481) New installs now start with Agent Mode on. Mobile is locked off at the sanitizer level so a desktop-synced settings file can't activate the unsupported subprocess flow. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Support agent image and PDF prompt context * Use agent read for PDF context
* fix(agent-mode): preserve persisted enabled flag on mobile Sanitization no longer force-overrides agentMode.enabled on mobile, so a desktop-synced preference is not silently overwritten the next time mobile persists settings. Mobile gating now lives at usage sites via a shared isAgentModeEnabled / useIsAgentModeEnabled helper. * fix(lint): drop unused Platform import in commands/index.ts The remaining `Platform.isDesktopApp` site uses a dynamic import that shadows the top-level binding, so the static import is unused after the agent-mode gating refactor.
* feat(agent-mode): tint chat input border and mode picker by mode Plan mode shows a blue border and soft glow; auto mode shows red. The mode picker label picks up a matching faded color that brightens on hover/focus, making the active agent mode obvious at a glance. * fix(agent-mode): preserve mode tint on keyboard focus The ghost2 button variant applies focus-visible:tw-text-normal, which overrode the focus: tint on keyboard focus. Switch to focus-visible: so tailwind-merge resolves the conflict in favor of the mode color.
…2485) * fix(agent-mode): queue mode/model switch while session is still starting * fix(agent-mode): disable picker while session is starting instead of queueing Replace the await-session.ready queueing approach with a UI-level gate: canSwitchModel/Effort/Mode now return false while status === "starting", so the picker renders as disabled until the backend session is ready. The await-during-onChange path made the picker feel buggy because clicks appeared to do nothing for a beat before applying. * fix(agent-mode): re-render mode/model picker on session status change The picker hooks compute a memoization signal that React diffs to decide when to rebuild the override. Status was not part of the signal, so the starting → idle transition fired listeners but the snapshot string was unchanged and the picker stayed disabled forever. Include `getStatus()` in both signals so the picker re-enables once the session is ready.
Consolidates the three duplicated model configuration surfaces (basic settings keys, Models settings table, agent mode curation) into a single design for product designer handoff. Covers unified Models panel, agent-first onboarding with three paths, per-backend defaults, and OpenCode/Copilot Plus as system-managed sections. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hoist the agent-mode picker out of the not-generating branch so users can flip between Auto/Plan/Safe mid-turn instead of having to wait for the agent to stop. Non-agent chats are unaffected since they don't pass modePickerOverride.
* Stop persisting agent mode selection * Force canonical default mode on every new agent session Restore `applyPersistedMode` as a small coerce helper and wire it back into every backend's `applyInitialSessionConfig` with a literal `"default"` — never reading from settings. Codex-acp's natural starting native mode is `read-only` (canonical `plan`), so without this the picker landed on Plan on every fresh chat. For codex specifically, also pin the spawn-time approval/sandbox config via `-c approval_policy="on-request"` and `-c sandbox_mode="workspace-write"` so codex-acp's first `currentModeId` already matches the canonical `auto` preset. Without these, the picker briefly flashed "Plan" before the post-spawn coerce switched it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Drop redundant post-spawn mode coerce The codex picker briefly flashing "Plan" was fixed at the spawn level (`-c approval_policy/sandbox_mode` in CodexBackend). Claude's SDK and opencode's spawn-time `default_agent` already start in canonical `default`, so the runtime `applyPersistedMode(session, "default")` calls on all three backends were dead defense-in-depth. Remove them along with the now-unused helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Drop stale type-only import comment in settings/model.ts `import type` is erased at runtime, so there's no actual cycle to guard against — the keyword itself already signals "type-only" to any reader. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w-up (#2490) - MODEL_MANAGEMENT_REDESIGN.md: BYOK is now the central, user-bring-only registry; drop per-model and per-provider Availability/Capability toggles; one global table with provider section rows; Quick Chat added as a 4th agent sub-tab; OpenCode/Plus models stay in the Agent panel only. - QUICK_CHAT_AGENT_INTEGRATION.md (new): follow-up plan for elevating LangChain chat into a first-class agent backend (routing service, session adapter, v2->v3 settings migration, milestones Q1-Q6). - AGENT_MODE_TODOS.md: mark shipped items; add notes for opencode/local models and subscription-based opencode flows. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the brief "Read Read" / "Edited Edit" flash that appeared while a tool call's input JSON was still streaming. `targetFromTitle` now returns an ellipsis when the title is just the vendor tool name (the SDK's documented placeholder state), and a new `verb()` helper picks the present-participle form for pending/in_progress calls and the past-tense form for completed/failed ones — so cards read "Reading notes/foo.md" → "Read notes/foo.md", "Fetching url" → "Fetched url", etc. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the modal-based permission prompter with inline ToolPermissionCard rendered at the tail of the chat scroll, matching how plan proposals already work. Refactor AgentSession status to be derived from underlying primitives (resolver maps, abortController, etc.) so it cannot drift from reality, and add tests covering the awaiting_permission lifecycle and turn-error reset. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an "Environment variables" editor to each Agent Mode backend (Claude, Codex, opencode) so users can inject vars like CLAUDE_CONFIG_DIR, CODEX_HOME, XDG_CONFIG_HOME, or HTTPS_PROXY without polluting the parent shell. Overrides are merged onto process.env at spawn (Codex, opencode) or per prompt() (Claude SDK), and persisted through a shared sanitizer that enforces POSIX-style names, drops control chars, and caps the record at 64 entries. The editor debounces commits to avoid rewriting settings on every keystroke. Also relocates BinaryPathSetting into src/agentMode/backends/shared/ to honor the agent-mode layer boundaries. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex (and other non-Claude agents) silently ignore the Claude-only frontmatter flags (`disable-model-invocation`, `user-invocable`, `model`), so hard-disabling their toggles on those skills was overly restrictive. The inline chips on the row already communicate which flags are Claude-specific.
`rowsToRecord` was persisting every non-empty trimmed key, so names failing `ENV_VAR_NAME_RE` (e.g. `BAD KEY`, `FOO=BAR`) flowed through `onChange` into settings and were applied verbatim by backend spawn paths, which read `envOverrides` directly without re-sanitizing. Filter invalid rows at the commit boundary so the persisted record and subprocess env only contain valid POSIX identifiers. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agent-mode): teach backends about @-mention pill syntax
The chat editor serializes @-mention pills to `[[note]]`, `{folder}`, and
`{activeNote}` tokens, but only `[[note]]` was understood — the others
were literal strings to the agent. Add a shared pill-syntax directive
appended to opencode, codex, and claude SDK spawn-time instructions
(mirroring the existing skill-creation directive plumbing), and resolve
`{activeNote}` to the real `[[Note Title]]` at send time since the active
file is a client-side concept the agent can't query.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(agent-mode): prune narration comments from pill-syntax change
Cut multi-line JSDoc and inline rationale that explained WHAT or referenced
callers, per the project's "one short line max, only WHY" rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(agent-mode): preserve extension when resolving {activeNote}
Non-markdown active files (.pdf, .canvas) lost their extension when
{activeNote} was rewritten via activeFile.basename, leaving the agent
with an ambiguous [[name]] that didn't match the path the pill envelope
carries. Mirror NotePillNode.getTextContent's extension-aware
serialization so the inline token matches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2503) * feat(agent-mode): make note links clickable in chat and tool cards Switches markdown rendering to the modern `MarkdownRenderer.render(app, …)` API via a shared `renderMarkdown` wrapper, and adds a delegated click handler so `a.internal-link` inside chat/agent/plan-preview views opens via `app.workspace.openLinkText` (left-click in current pane, cmd/middle in a new tab). Tool ActionCards now also expose the collapsed line as a clickable target once the call has completed. Threads `app` explicitly through the components and utilities that previously relied on the global `app` (lexicalTextUtils, notePreviewUtils, vaultPath, AtMention/Note/Paste/TextInsertion plugins) using the `useApp()` hook at the leaf, and updates AGENTS.md to discourage new uses of the global. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(chat): fix ChatSingleMessage mocks for wireInternalLinks The `renderMarkdown` wrapper now calls `component.register(...)` after `MarkdownRenderer.render(...)`. The test's `obsidian.Component` mock was missing `register`, causing the promise to reject and `logError` (which calls the unmocked `getSettings()`) to crash. Add `register` to the Component mock and mock `getSettings` as a safety net. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agent-mode): resolve internal links against active note Pass the active file path as sourcePath to renderMarkdown so wikilinks with duplicate basenames or heading-only references resolve to the right file instead of falling back to vault root. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds OBSIDIAN_CLI_TEST.md with a field guide for driving the plugin through the Obsidian desktop CLI: multi-vault targeting (the biggest gotcha — default routes to whichever window was touched last), popout window limitations, settings round-trips via eval/saveData, real-input UI driving via dev:cdp, screenshots, console/error capture, performance metrics, and a smoke-test scaffold. AGENTS.md now points to it and keeps only the multi-vault warning inline so there is one source of truth. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace non-existent `copilot:open-copilot-chat` with the registered `copilot:agent-chat-open-window` ID in both the "Run a command" example and the smoke-test scaffold so the documented commands actually work. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2507) * perf(agent-mode): warm-adopt preload probe to skip first-chat spawn The preloader's probe subprocess is now retained as a "warm" entry and adopted by the first chat-open on that backend, so the first session skips both the subprocess spawn and the `newSession` round-trip. Preload status is also tracked per-backend: the chat gates on the active backend's status only, and the picker shows per-backend loading rows for the others. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agent-mode): defer model+effort seeding to backend confirmation - Seed only baseModelId (not effort) so descriptor-style backends (Claude) don't see a matching seeded effort and skip the real setConfigOption inside applyInitialSessionConfig. - Gate warm-session `ready` on confirmSeededSelection so sendPrompt can't race onto the probe's model before setModel resolves. - Add regression tests covering both paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…reshes (#2508) * fix(skills): eliminate Skills Settings UI lag from watcher-driven refreshes Replaces the time-based watcher suppression with path-scoped expectations: each SkillManager FS write registers predicates (exists/missing/modified/ subtree-*) and the matching vault events are dropped without triggering a debounced reconcile. A 10s safety timer backstops never-arriving events. Toggle/delete/rename/saveProperties now publish incremental in-memory updates instead of re-running full discovery, so the Properties modal save returns synchronously and the grid reflects the change on the next render. * fix(skills): heal drift when expectations expire; preserve external reconciles Two fixes for the watcher-suppression machinery on PR #2508: - `armSafetyTimer`: when the 10s backstop fires with expectations still pending, schedule a debounced reconcile before clearing. Catches the case where an external process undid our change (e.g. deleted a link we expected to exist) — the matching event was suppressed, predicate never satisfied, and previously the state drifted silently until an unrelated watched event fired. - `runInternalMutation`: stop cancelling pre-existing reconcile timers in the finally block. Events fired during the mutation are dropped by the depth gate, so any debounce timer left armed was scheduled by an external event before the mutation started and still needs servicing. Adds two regression tests and updates the pre-existing "safety timer clears stale expectations" test which is superseded by the new healing behavior.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2513) Replaces the rescan/import-consent flow with unified discovery across canonical and agent project folders, lazy migration with a global suppress flag, and an overflow-menu lockdown for mirrored duplicates.
…sh TODOs (#2514) - Add `.claude/skills/create-agent-issue/SKILL.md`: project-level skill that codifies the standard for one well-formed GitHub issue (title shape, body sections, label palette, draft artifact format, reviewer rubric, three-stage workflow: draft / review-and-push / all). - Add `designdocs/todo/AGENT_TODOS_TO_ISSUES_PLAYBOOK.md`: orchestrator playbook for migrating `AGENT_MODE_TODOS.md` into GitHub issues with a two-subagent (author + reviewer) flow, autonomous triage, parallelism, idempotency, and verbatim subagent prompt templates. - Revamp `designdocs/todo/AGENT_MODE_TODOS.md`: richer per-item context, "design needed" markers, reorganized MCP and Codex auth items, current pending count is 33 top-level items (P0=7, P1=17, P2=7, P3=2). - Drop `designdocs/todo/MCP_EXTERNALLY_MANAGED_SERVERS.md` (stale). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces every pending top-level `- [ ]` in `AGENT_MODE_TODOS.md` with a link to its corresponding issue in logancyang/obsidian-copilot-preview (#59-#98). Follows the playbook in `AGENT_TODOS_TO_ISSUES_PLAYBOOK.md`: sub-bullets folded into issue bodies, and "Integrate copilot plus tool calls" split into 5 child issues (#80-#84) under a now-plain parent bullet. 39 issues opened end-to-end via paired author/reviewer subagents. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the per-backend install/settings panels with a shared Configure dialog layout. Introduces ConfigDialogShell + ConfigSection, InstallCommandRow, and an installStatus badge/status-line module (with tests), and reworks the Claude, Codex, and opencode Configure dialogs onto it. The settings page now shows each backend's icon, an install-status badge, and a Configure button, dropping the old BinaryInstallContent and SimpleBackendSettingsPanel helpers. Copy is simplified for non-technical users and dialog spacing/dividers are made consistent. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts (#2738) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ering (#2739) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ctions (#2740) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#2742) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…line (#2743) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…a time (#2744) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… control bar (#2758) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ith the binary path (#2752) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ault (#2774) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…2773) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… cap (#2777) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae26ffe7a0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ).open(); | ||
| } | ||
|
|
||
| updateSetting("upgradedToV8FromLegacy", false); |
There was a problem hiding this comment.
Migrate legacy data before switching folder readers
For upgrades with customized conversation, prompt, skill, or memory folders, the files remain at their legacy paths while the new accessors immediately read only the derived <copilotFolder>/... paths. This method merely opens an informational modal and then clears the upgrade flag regardless of whether the user moved anything, so dismissing or acknowledging it makes existing chats and other persisted data disappear from the plugin with no retry. Migrate the files automatically or retain a compatibility fallback until relocation succeeds.
AGENTS.md reference: AGENTS.md:L74-L79
Useful? React with 👍 / 👎.
| } catch (err) { | ||
| failed++; | ||
| logError(`[byok-migration] failed to migrate "${descriptor.displayName}"; continuing`, err); |
There was a problem hiding this comment.
Retry provider migrations before stamping the schema
If setupProvider fails transiently—for example because Keychain is unavailable during this startup—the exception is swallowed here, after which runSettingsMigrations still stamps settingsVersion to v8. Every later startup therefore skips the failed provider permanently, leaving its legacy models absent from the new backend/model registry even after the underlying failure is resolved. Propagate or return the failure so the version is not advanced until every descriptor has migrated; successful descriptors are already deduplicated on retry.
AGENTS.md reference: AGENTS.md:L74-L79
Useful? React with 👍 / 👎.
| @@ -184,30 +354,72 @@ export default class CopilotPlugin extends Plugin { | |||
| // Single source of truth for Active Web Tab ({activeWebTab}) state | |||
| // Preserves activeWebTab when switching to Chat view | |||
| // Only run on desktop - Web Viewer is not available on mobile | |||
| if (Platform.isDesktopApp) { | |||
| if (isDesktopRuntime()) { | |||
| const { activeLeafRef, layoutRef } = startActiveWebTabTracking(this.app, { | |||
| preserveOnViewTypes: [CHAT_VIEWTYPE], | |||
There was a problem hiding this comment.
Preserve the active web tab for Agent Chat
When a user switches directly from Obsidian's Web Viewer to the new Agent Chat view, that view type is absent from this preserve list. WebViewerStateManager consequently clears activeWebTabForMentions on the leaf change, while AgentChatInput uses that exact state for its Active Web Tab affordance and send snapshot, so {activeWebTab} and the include-active-tab option stop working specifically in Agent Chat. Include CHAT_AGENT_VIEWTYPE alongside the legacy chat view.
Useful? React with 👍 / 👎.
Why
masterhas been parked at the v3.3.3 release sincebd8829f5while all v4 workaccumulated on
v4-preview. This brings the two back together somastercarries the v4 codebase.
What
Merges the v4 work from
v4-previewintomaster. The merge is clean — noconflicts.
masterhad one commitv4-previewlacked (632c1e81, the README v4promo); because
masterenforces strict status checks, that commit was firstmerged into
v4-preview, so this PR is now a straight fast-forward ofmaster.This publishes nothing.
release.ymlfires on any PR merged intomasterbut exits early unless the PR title is a semver string. This title is not, so no
tag is created and no GitHub Release is cut.
package.jsonstays at3.3.3.Non goal
target semver, which is what triggers the release workflow.
RELEASES.mdentry.v4-previewPR by PR.Screenshot
Not applicable — a branch integration with no diff of its own.
Risk
Low
v4-previewnode.js.ymlruns the full suite on this PRv4-previewv4-previewReview: confirm the title is not a semver string, confirm the base is
masterand the head is
v4-preview, and confirm CI is green. There is no per-file diffto inspect that was not already reviewed on its own PR.
Verification
release.ymlwill exit early.masterandv4-previewnow point at the same tree:git diff origin/master origin/v4-previewis empty.