mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
1167 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ca9b573941
|
test(agent-mode): cover effort landing on a cross-backend pick + guard-less backend
Adds regression coverage for the audited transitions: a cross-backend Claude seed lands both the picked model and its effort, and a guard-less setModel backend (codex-style) still issues the switch after the seed-drop change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2870d072e3
|
fix(agent-mode): stop a stale state_changed from reverting the picked model
opencode (an ACP subprocess) broadcasts config_option_update notifications that the ACP layer synthesizes into state_changed events. One can race a model switch and carry the pre-switch default; because handleSessionEvent applied state_changed unconditionally, the late push clobbered the user's just-applied model back to the default — intermittently. These backends never self-switch the model, so a state_changed that regresses the model away from the last user-applied selection is a stale echo. Track the last applied model and, when an incoming state reverts it (and the backend still offers it), keep it; every other dimension (effort, mode, availableModels) stays authoritative. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6b8840b197
|
fix(agent-mode): fire the real model switch on a cross-backend Claude pick
confirmSeededSelection optimistically seeds the display to the picked model, then delegates the real switch to descriptor.applySelection. Claude's applySelection guards its setSessionModel on the session's current model — which the optimistic seed already set to the pick — so the guard saw "already there" and skipped the switch, leaving the session running Claude's default model. The seed was dropped before applySelection only for config-option backends, on a false assumption that setModel-style backends always round-trip. Claude is setModel-style but guards on current, so it broke. Drop the optimistic seed unconditionally so applySelection sees the backend's true reported model and issues the switch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7366499229
|
feat(agent-home): surface Relevant Notes as a home-shelf tab (#2639)
* feat(agent-home): Relevant Notes tab body + pop-out hint + device-local prefs
Phase 1 of surfacing Relevant Notes on the agent home shelf. Adds
RelevantNotesShelfPanel (reuses the existing RelevantNotes component
with a dismissible 'open in its own pane' hint) and homeShelfPrefs
localStorage helpers (selected tab + hint-dismissed, device-local).
Not wired into the shelf yet — that is Phase 2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agent-home): wire Relevant Notes shelf tab with persistence + pane mutual-exclusion
Phase 2. Adds the Relevant Notes tab to the agent home shelf (order:
Recent Chats, Relevant Notes, Projects), rendered unconditionally so
embeddings-off users hit the existing enable-semantic-search CTA.
- useRelevantNotesPaneOpen: hide the tab while the dedicated pane is
open (no duplicate surface); restores when the pane closes.
- AgentHomeShelf: optional storageKey persists the selected tab in
device-local localStorage; keeps the fallback-to-first-selectable
resolution so an absent persisted id never breaks.
- AgentHomeTab/Section: count is optional; badge renders only when > 0
(Relevant Notes omits it, no eager semantic compute).
Adds focused AgentHomeShelf tests (count suppression, persistence,
fallback). No ribbon; the dedicated pane + command stay as the pop-out
target.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(agent-home): refresh Relevant Notes shelf tab on note-switch
The Relevant Notes shelf tab (inside CopilotAgentView) never refreshed on
note-switch and stayed empty for indexed notes: its useActiveFile listens
for ACTIVE_LEAF_CHANGE on the view's own eventTarget, but nothing fed that
target. The legacy CopilotView was fed by a special-case dispatch in main.ts
and RelevantNotesView self-bridged, but CopilotAgentView did neither, so its
useActiveFile froze at the mount seed.
Extract one shared registerActiveLeafChangeBridge helper and call it in
CopilotView, CopilotAgentView, and RelevantNotesView onOpen; remove the
legacy-only dispatch from main.ts so every chat view self-owns its bridge.
Verified via Obsidian CLI: the agent view's eventTarget now fires on
note-switch (0x before, 3x after).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(jest): map @orama/orama to its CJS build
configuredModelGrouping.test.ts began failing to load: it imports the
@/agentMode barrel (for ModelEnableRow/isOpencodeZenWireId), and this PR
made AgentHome — re-exported through that barrel via CopilotAgentView —
depend on RelevantNotes → findRelevantNotes → dbOperations, which imports
@orama/orama. Under jsdom, @orama/orama resolves to its ESM "browser"
entry, which Jest can't parse ("Unexpected token 'export'").
Map it to the CJS build it ships under dist/commonjs/, mirroring the
existing yaml entry. Only modules that already reach @orama/orama are
affected, so no passing suite changes behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
f5f38d36e1
|
fix(agent-mode): measure tab strip synchronously so tabs don't collapse into overflow (#206) (#2660)
AgentTabStrip drove computeVisibleCount() from stripWidth state that only a ResizeObserver callback updated, with no synchronous fallback measurement. On a real remount, a missed or delayed first RO delivery left stripWidth stuck at 0, collapsing project tabs into the "..." overflow menu despite available width. Confirmed via an A/B repro (stub ResizeObserver + force remount -> 3 tabs collapse to 1 at identical width). Switch the effect to useLayoutEffect with a synchronous primer measurement, guard for ResizeObserver-less test envs, and depend on sessionCount so the 0 -> N mount path (the strip returns null at 0, so the ref is absent) measures once mounted. Mirrors the sync-primer pattern already used in PatternListEditor, ProjectContextBadgeList, and ProjectContextSourceEditor. |
||
|
|
358abe6b8d
|
feat(agent-mode): collapse empty context row, dock status icon in a composer accessory column, label context-held queue rows (#2662)
* feat(agent-mode): collapse empty context row, dock status icon in a composer accessory column, label context-held queue rows (#205) * fix(agent-mode): omit showIndexingCard instead of passing NOOP so the indexing chip's render guard holds * docs(agent-mode): state the overlay rejection rationale instead of build history in the accessory DESIGN NOTE |
||
|
|
9fc6153ae6
|
feat(agent-mode): custom "Other" response option in the AskUserQuestion card (#2653)
* feat(agent-mode): add custom "Other" response to AskUserQuestion card
Each question in the inline AskUserQuestion card now offers an "Other" row (radio for single-select, checkbox for multi-select) that reveals a free-form textarea, mirroring Claude Code CLI. The typed text becomes the answer (single-select) or is appended to the checked labels (multi-select). Confined to the UI component — the answer stays a plain string, so no type/bridge/backend changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(agent-mode): cover custom "Other" response in AskUserQuestion card
Five RTL cases: single-select Other→trimmed text, multi-select presets+Other→joined, empty Other disables Submit, Cancel→{}, and a single-select preset regression.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
4cfea5761d
|
fix(agent-mode): stop folding the completed trail into "Worked for X" (#2652)
* fix(agent-mode): always render the completed trail inline Remove the "Worked for X" collapse that folded everything before the final text block once a turn cleanly ended. It hid content the user was mid-read on and broke reading flow; a finished turn now renders exactly like the live streaming view — nothing is hidden. Also drops the now-unused splitTrailingText helper and adds a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(agent-mode): drop dead turnDurationMs plumbing With the "Worked for X" collapse removed, the turn's wall-clock duration is never read. Remove turnDurationMs end-to-end (session, store, and the persisted message type) and the turnStartedAt bookkeeping that existed solely to compute it. turnStopReason is fully preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
703529ac67
|
fix(ui): rebuild SettingSwitch on Obsidian native toggle vars (#2651)
The switch mixed CSS units: the track width used Tailwind's default `w-10` (2.5rem, font-relative) while height, thumb, and travel were fixed px via Obsidian's --size-4-* tokens. Obsidian's interface font-size setting drives the rem root, so changing it stretched the width alone and the thumb stopped landing at the edge (the reported break). Source all geometry from Obsidian's native small-toggle variables (--toggle-s-width/-thumb-width/-thumb-height/-border-width, --toggle-radius, --toggle-thumb-radius) so the switch is 100% px and pixel-matches Obsidian's own toggles; the thumb travel is derived from those sizes rather than a hardcoded distance. Public API, ARIA, and keyboard behavior unchanged. Adds a React Testing Library behavior test. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
55c9a2aa0c
|
Stop the Copilot Plus license hint click from toggling the group (#2649)
Tapping the key-icon hint (which opens the tooltip on mobile) bubbled to the CollapsibleTrigger and collapsed/expanded the whole group. Wrap it in a span that stops pointer/click propagation. Claude-Session: https://claude.ai/code/session_018ou7V5v4UYHkzP3csodLLm Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a78b38c8c2
|
feat(entitlement): gate multi-agent to Plus tier via signed entitlement token (#2644)
* feat(entitlement): gate multi-agent to Plus tier via signed entitlement token Multi-agent fan-out was gated on the binary isPlusUser flag, which any valid license flips true, so a future Lite tier would wrongly pass. Introduce a server-signed entitlement token (ES256, verified offline via WebCrypto) as the single entitlement primitive. - Rename the broad flag isPlusUser to isPaidUser (any paid license, incl. Lite); all existing Plus-feature gates move to it, preserving current behavior. - Add a new strict isPlusUser (tier >= Plus) derived from the token's multi_agent feature. The multi-agent UI gate and the authoritative send-boundary check (ensureMultiAgentEntitlement) now key on it. - No-token fallback (isPlusUser = isPaidUser) preserves today's behavior until the server ships tokens alongside Lite/Pro, so there is no plan-name list in the public client to patch out. - Settings sanitize migration backfills isPaidUser from the legacy isPlusUser. - src/entitlement/ verifies the JWS signature with an embedded public key (no key can mint tokens client-side); forged data.json / faked responses fail. Tests cover tier gating across Free/Lite/Plus/Pro plus the crypto layer (valid, expired, wrong-user, tampered, unknown-kid, malformed). Self-host migration to the token is deferred to logancyang/obsidian-copilot-preview#201. Closes logancyang/obsidian-copilot-preview#200. Design: "Copilot Entitlement Token Design". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(test): swap in Node WebCrypto when jsdom's subtle lacks generateKey CI (Node 22) ships a jsdom whose window.crypto.subtle exists but is partial (no generateKey), so the previous `!subtle` guard skipped the polyfill and the entitlement crypto tests failed. Probe for the actual method and install Node's complete WebCrypto when it's missing. Runtime is unaffected (test-only setup). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(entitlement): never downgrade on an unverifiable token (Codex P1/P2) Addresses two Codex findings: - P1: when /license starts returning a token but the client's public keys are not shipped yet (or during kid rotation), every token verified as null and applyEntitlement cleared the flags, dropping paying users to free. Now applyEntitlement never clears: it returns false when it cannot verify, and validateLicenseKey falls back to turnOnPaid() for a license the server already confirmed valid. Only an authoritative negative (invalid license / no key) downgrades, via turnOffPaid. - P2: removed refreshEntitlementFromCache() and its concurrent startup call, which could clear flags after checkIsPaidUser() restored them. The persisted flags already hold the last verdict and the online check is authoritative; offline expiry enforcement belongs to the deferred self-host migration (#201). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(test): run entitlement crypto test in node env, not flaky jsdom subtle jsdom ships only a partial SubtleCrypto (no generateKey), and patching it in jest.setup was nondeterministic across CI Node builds (passed on one commit, failed on the next with no setup change). Run verify.test under the node environment, where crypto.subtle is Node's complete WebCrypto — the verification logic is WebCrypto-spec behavior, identical between Node and the webview. Also drop verify.ts's @/logger import so the entitlement module is a pure leaf (no settings/obsidian graph), which keeps the node-env test import-safe and matches the module's intended dependency-light design. jest.setup now guards its window usage so it is a no-op under the node environment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(test): force WebCrypto onto globalThis, not just window The prior fix patched window.crypto, but a bare `crypto` reference in a module resolves to globalThis.crypto, and jest's jsdom environment installs a partial SubtleCrypto (no generateKey) on globalThis that the window-only patch missed — so CI kept failing in jsdom while local (clean jsdom) passed. Patch globalThis (and window) with Node's complete WebCrypto, with a fallback to replacing `.subtle` if `crypto` is a locked accessor. Reverts the node-env docblock on the crypto test; the global patch covers the standard jsdom env. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(test): inject Node WebCrypto into entitlement verify instead of patching globals Root cause of the repeated CI failures: jest's jsdom environment exposes a partial, locked SubtleCrypto (no generateKey/ECDSA) on the global the module resolves `crypto` from, and neither replacing window/globalThis.crypto nor the @jest-environment node docblock reliably overrode it on CI (local jsdom resolves clean, so it couldn't be reproduced locally). Fix: give verifyEntitlement an injectable `subtle` option (default `crypto.subtle`, as used at runtime on desktop + mobile), and have the test pass Node's webcrypto.subtle explicitly. The test no longer depends on the environment's global WebCrypto at all, so it's deterministic in any jest env. jest.setup reverted to its original form (no crypto hacks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(entitlement): close 3 Codex findings (Lite bypass, log leak, offline expiry) - P1 (Lite bypass): an unverifiable entitlement token no longer grants the strict gate. validateLicenseKey now calls markPaidPendingEntitlement() — paid so general Plus features work, but isPlusUser stays false so an unverifiable Lite token can't pass the multi-agent gate before the verifying key ships (fails closed). Tokenless (old server) still grants paid + Plus. - P2 (log leak): parseBrevilabsResponse redacts the `entitlement` JWS before logInfo, so it never lands in the shared copilot-log.md. - P2 (offline expiry): persist the token's exp as entitlementExpiresAt and have the strict gate (isPlusEnabled / useIsPlusUser) honor it, so multi-agent locks at expiry even while /license is unavailable. The broad paid gate keeps legacy behavior. ensureMultiAgentEntitlement's slow path re-reads it, so an expired token offline is blocked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * feat(entitlement): embed prod ES256 public key (kid ent-2026-06) Populate ENTITLEMENT_PUBLIC_KEYS with the production verify-only public JWK so verifyEntitlement can validate server-signed tokens offline. The matching private key signs tokens in brevilabs-api. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(entitlement): require in-session signature proof for strict Plus Address Codex P1: isPlusEnabled() trusted the persisted isPlusUser boolean + entitlementExpiresAt without re-verifying the JWS, so an edited data.json (isPlusUser: true + future expiry) bypassed the multi-agent gate offline and in the startup window. The signed token was not actually the gate on cached state. Gate token-derived strict Plus on an in-memory, non-persisted proof that a signed entitlement granting multi_agent was cryptographically verified in THIS process — set by applyEntitlement (server response) or the new verifyCachedEntitlement (cached token re-checked at startup, offline via WebCrypto). A persisted boolean alone never sets it, so it fails closed. The tokenless fallback (pre-token server, entitlementExpiresAt === 0) is unchanged: no signed artifact exists to verify, so it honors the license-confirmed flag as today and preserves new-plugin/old-server compatibility. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bd60f10f56
|
fix(agent-mode): copy/insert the full agent response, not just trailing prose (#166) (#2616)
* docs(plan): fix copying all text parts of an agent response (#166) Otacon-approved plan for issue logancyang/obsidian-copilot-preview#166: agentResponseText collects every text part, not just the trailing run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): copy/insert the full agent response, not just trailing prose (#166) Copy and Insert/Replace acted on finalAnswerText, which reused splitTrailingText and kept only the last contiguous run of text parts — so a 'text -> thought -> text' turn dropped the earlier prose. Rename the helper to agentResponseText and have it collect every non-empty text part in stream order (joined with blank lines, then cleanMessageForCopy). splitTrailingText is untouched, so the 'Worked for X' folding is unchanged. Fixes logancyang/obsidian-copilot-preview#166 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(plan): archive completed plan for #166 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * update docs * test(agent-mode): clarify interleaved-prose fixture wording (#166) The flipped test reused fixture strings that said the early prose 'should NOT be copied', which now contradicts the assertion (it is copied). Reword the strings to describe the new behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
60dc554c1f
|
feat(model-management): add DeepInfra Copilot Plus models, default off (#2645)
* feat(model-management): add DeepInfra Copilot Plus models, default off Expand the curated Copilot Plus lineup (`COPILOT_PLUS_MODELS`) from just `copilot-plus-flash` to the full public set served by models.brevilabs.com/v1/models: kimi-k2.6, glm-5.2, kimi-k2.7-code, deepseek-v4-pro, mimo-v2.5, minimax-m2.7. These surface in the chat + opencode pickers (the "Copilot Plus" section under OpenCode) for the user to toggle on. Only copilot-plus-flash is enabled by default. Adds an optional `autoEnrollModelIds` to `registerPlusProvider`/`#reconcileModels` so Plus can curate a default-on subset: newly-added models outside the set are created (and shown in the pickers) but left unenrolled. `undefined` preserves the prior enroll-everything behavior, so BYOK is unaffected. This also keeps existing users' curation intact - the 6 new models arrive off on the next sign-in sync rather than auto-enabling. Adds the new wire ids to the `ChatModels` enum and threads canonical one-line descriptions through from the models service. Also fixes capability resolution for bridged Plus models so image input routes correctly: CopilotPlusChainRunner.isMultimodalModel() -> hasCapability() -> ChatModelManager.findModelByName() only searched legacy `settings.activeModels`, so bridged ConfiguredModel-only models (e.g. kimi-k2.7-code) were treated as non-multimodal and silently dropped attached images. findModelByName now falls back to the active bridged model when its name matches; that CustomModel carries capabilities derived from its modalities (configuredModelToCustomModel maps image input -> VISION). Legacy lookups are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ou7V5v4UYHkzP3csodLLm * feat(copilot-plus): reasoning-effort picker for Plus models + usage-cap credits prompt Builds on the Plus model lineup to surface reasoning effort and handle cap hits. - Mark the reasoning-capable Plus models (all but Azure kimi-k2.6) with reasoning: true so the chat + agent pickers offer an effort selector instead of "na". Matches the models service's supports_reasoning. These models don't reason unless an effort is picked, so flash stays fast by default. - Forward the picked effort in the Simple Chat COPILOT_PLUS provider branch (the relay reads reasoning_effort), gated on the REASONING capability. - Inject reasoning: true into the OpenCode provider model config so opencode surfaces a thought-level option for these models (it has no catalog entry for Copilot Plus / self-hosted OpenAI-compatible providers otherwise). - Usage-cap (plan limit) errors now render a friendly, actionable message with a link to the website usage dashboard to purchase credits, instead of the raw relay error. New formatUsageCapError util (deep-searches the thrown error for the cap signal, cycle-safe) wired into the central getApiErrorMessage, so both Simple Chat and Agent Mode show it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ou7V5v4UYHkzP3csodLLm * fix(copilot-plus): re-sync capability fields so reasoning effort surfaces registerPlusProvider's reconcile only refreshed displayName/description on existing ConfiguredModels, never the capability fields. So an existing Plus user kept a stale snapshot with reasoning unset — the chat bridge derived no REASONING capability and the OpenCode config injection (gated on info.reasoning) never fired, so the effort picker stayed empty ("na") even after the models were flagged reasoning: true. Reconcile now also re-syncs modalities, reasoning, and toolCall in place when they drift (Plus models are server-curated, so there are no user overrides to clobber). Verified end to end: persisted info.reasoning flips to true on re-sign-in, so the reasoning models advertise effort. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ou7V5v4UYHkzP3csodLLm * fix(copilot-plus): keep flash fast by default; prefer bridged model capabilities Addresses two Codex review comments on the reasoning-effort wiring: - Gate enableReasoning on an explicit effort for the Copilot Plus provider. ChatOpenRouter.invocationParams falls back to `reasoning: { max_tokens: 1024 }` whenever reasoning is enabled without an effort, so flagging copilot-plus-flash REASONING-capable made the default-on model start spending reasoning budget. Now enableReasoning requires a user-picked effort, so flash stays fast until the user chooses one. - findModelByName prefers the active bridged model on an exact name match before the legacy activeModels lookup. copilot-plus-flash exists in both (the built-in legacy entry advertises only VISION); the early legacy return masked the bridged REASONING/VISION capabilities, so CopilotPlusChainRunner.hasCapability treated flash as non-reasoning and hid reasoning content. The bridged model is the one actually running, so it wins. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ou7V5v4UYHkzP3csodLLm * fix(copilot-plus): show usage-cap credits message on the streaming error path getApiErrorMessage (with formatUsageCapError) was only reached from the planning catch. The common cap path — the relay returns token_limit_error mid-invocation after planning succeeds — goes through BaseChainRunner.handleError, which rendered the raw 429 payload. Route usage-cap errors through formatUsageCapError there too, so the purchase-credits message + dashboard link shows on the main invocation path across all chain runners. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ou7V5v4UYHkzP3csodLLm * fix(copilot-plus): render usage-cap link as plain text for ErrorBlock The streaming error path renders the message via ErrorBlock as plain text (whitespace-pre-wrap), so the Markdown link syntax showed literally as `[purchase credits ...](url)`. Switch formatUsageCapError to a plain-text message with a bare URL, which is readable on the streaming path and still auto-links in any Markdown-rendered context. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ou7V5v4UYHkzP3csodLLm * Refine GLM-5.2 description in Copilot Plus model list Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ou7V5v4UYHkzP3csodLLm * Badge Copilot Plus group with privacy + license hint, taller model list Add a "privacy" badge and a "Copilot license required" hover hint (key icon) to the Copilot Plus group header in the model enable lists, and raise the list's max height so more models are visible without scrolling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ou7V5v4UYHkzP3csodLLm --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
83f1f7070e
|
feat(models): flag non-vision models, guard image sends (#2627)
* feat(models): guard image sends to non-vision chat models Add a shared modelSupportsVision() helper and block sends in Chat.handleSendMessage when images are attached but the active chat model is known to lack vision — Notice names the model and preserves the typed text + images instead of failing silently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-mode): guard image sends to non-vision agent models Enrich agent picker entries with capabilities derived from the models.dev catalog (by provider + baseModelId), and apply the same known-non-vision guard in AgentChatInput. Models we can't resolve in the catalog keep capabilities undefined and are never falsely blocked. Catalog access is threaded as a structural type to respect the agent-mode ui import boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(byok): show model capabilities + add vision/reasoning overrides Render read-only capability icons per model row in the Configure provider dialog, and add a collapsible Advanced panel with per-model vision/reasoning toggles. Overrides overlay onto ConfiguredModel.info modalities/reasoning at save, surviving the catalog-first resolve precedence (R1) and flowing through the existing bridge to become CustomModel.capabilities. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(models): show modality icons in BYOK + agent pickers; hide reasoning icon BYOK checklist and the agent-mode model picker derived capabilities from the async models.dev catalog but never loaded/reacted to it, so no icons rendered (legacy works only via hardcoded BUILTIN_CHAT_MODELS.capabilities). Warm the catalog and re-render on change in both surfaces. Hide the reasoning/"thinking" capability icon everywhere (ubiquitous on modern frontier models) while keeping ModelCapability.REASONING in the data model so runtime extended-thinking gating (chatModelManager, chain runners, Bedrock) stays intact. - CatalogLookup: optional ensureLoaded()/onChange(); useAgentModelPicker warms + re-renders on catalog populate - ConfigureProviderBody: ensureLoaded + onChange bumps catalogMetadata memo; drop the Reasoning override toggle (keep Vision) - model-display: remove reasoning icon + "Reasoning" text label Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-models): show vision icon in agent pickers from persisted info The settings agent model picker (ModelEnableList) rendered no capability icons at all, and the chat agent picker derived them only from a live, often-cold models.dev catalog lookup keyed by reported provider — so the vision icon never showed reliably for agent models. Source vision from each model's persisted ConfiguredModel.info.modalities (the snapshot taken at setup, like Copilot Plus's explicit image input) — the same single source of truth BYOK and legacy already use, no live catalog required: - EnabledModelEntry gains optional `capabilities`; the claude/codex and opencode enabled-entry builders populate it via capabilitiesFromConfiguredInfo (undefined when info has no modalities → picker still falls back to catalog) - chat agent picker prefers enabled.capabilities over the catalog lookup - ModelEnableList renders ModelCapabilityIcons; toRow derives them from info, so the settings agent + Quick Chat lists finally show the vision icon Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * update modality indicator * refactor(byok): remove manual vision capability override The "Advanced" section in the Configure Provider dialog let users manually toggle a model's Vision capability — unintuitive, since vision is already detected automatically from the models.dev catalog (or the persisted snapshot). Remove the override entirely. - Drop the Advanced collapsible + its override state/plumbing (capOverrides, applyCapsToModelInfo, CapFlags) and retire the Risk R1 override-preservation logic; capabilities now ride on each ModelInfo via resolveModelInfo. - Fix the chat image-block notice that pointed at the now-removed "enable Vision in provider settings" path. Automatic detection (modality icons, image-send guards) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(agent-picker): drop live-catalog fallback; use persisted capabilities only The "snapshot at save time" architecture already exists — every ConfiguredModel.info carries modalities + reasoning, and all three agent backends feed the picker capabilities from that persisted snapshot. The live models.dev catalog in the picker was a redundant fallback, so this removes it entirely along with the complex useCatalogVersion hook. - agentModelPickerHelpers: delete CatalogLookup/CatalogModelInfo, capabilitiesFromModelInfo/lookupCatalogModelInfo/capabilitiesFromCatalog; drop the catalog param throughout; capabilities = enabled.capabilities. - useAgentModelPicker: delete useCatalogVersion; drop catalog arg + memo dep. - AgentHome: useAgentModelPicker(manager) — no longer threads catalogService. - tests: replace catalog-enrichment specs with persisted-capability propagation. ConfigureProviderDialog/ByokPanel keep the live catalog — they show capabilities for not-yet-saved models, which have no persisted snapshot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(byok): show modality icon in BYOK settings model table Render the shared capability icon inline after each model name in the read-only BYOK table, reusing capabilitiesFromConfiguredInfo + the same ModelCapabilityIcons treatment the Configure dialog already uses. Brings the last model surface to parity: a muted eye-off marks models that can't take image input (incl. text-only embeddings), vision-capable and unknown-modality models stay silent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(model-display): drop web-search globe badge; eye-off is the only modality signal The no-vision eye-off is now the sole capability icon: web-search is no longer badged with a globe, and getModelDisplayWithIcons stops appending a "Websearch" suffix. hasCapabilityIcons/ModelCapabilityIcons reduce to a single 'known to lack vision' check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat-picker): treat unknown model capabilities as unknown, not text-only useChatModelPicker hand-rolled its capabilities array, always emitting an empty (but defined) array even when ConfiguredModel.info.modalities was absent. The Chat.tsx image guard reads a defined array as "known", so legacy/manual/unknown snapshots got the eye-off icon and had images blocked. Use capabilitiesFromConfiguredInfo() so capabilities stay undefined when modalities are missing — matching the agent pickers and the intended "unknown stays unblocked" semantics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9b4ae5721a
|
fix(agent-mode): ship Windows cmd/ps1 runtime for builtin relay skills (#2643)
* fix(agent-mode): ship Windows cmd/ps1 runtime for builtin relay skills The builtin Copilot Plus relay skills (read-pdf, web-search, web-fetch, youtube-transcript, fetch-x) shipped a POSIX `.sh` plus a Node `.mjs` fallback. On Windows a managed-opencode session launches in PowerShell with neither `sh` (no Git Bash) nor `node` on PATH (Obsidian's reduced PATH excludes nvm/Volta/Homebrew node), so the skill dead-ended — the PDF report in #2634 hit exactly this ("node is not recognized"). Replace the Node fallback with a native Windows path, mirroring the `miyo-search` model: each skill now ships one runnable script per OS — a `.sh` for macOS/Linux and a `.cmd` wrapper that drives an adjacent PowerShell `.ps1`. Windows PowerShell 5.1 ships with the OS, so the relay (TLS1.2, base64 + HTTP POST, same env-driven config, same no-license / 401-403 fallback mapping) runs with zero install. SKILL.md "How to run" and the system-prompt steering now route per OS with no "install Node.js" dead-end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(agent-mode): send PowerShell relay body as UTF-8 bytes Windows PowerShell 5.1 ASCII-encodes a string -Body by default (UTF-8 only became the default in 7.4), so non-ASCII queries/URLs would arrive corrupted at the relay. Encode the JSON as UTF-8 bytes and pass the byte[] verbatim (matching curl), with charset=utf-8 on the content type. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(agent-mode): emit Windows relay output as UTF-8 Windows PowerShell 5.1 defaults Console.OutputEncoding to the system code page, so non-ASCII relay output (Japanese results, fetched pages, transcripts) could be mojibaked before the agent read it. Set [Console]::OutputEncoding and $OutputEncoding to UTF-8 at the top of the script, the output-side counterpart to the UTF-8 request body fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6d878e175c
|
feat(agent-projects): project-scoped Agent Mode — Agent Home PR2 (#2604)
* feat(agent-mode): project-scoped Agent Mode workspaces (PR2)
Turn Agent Mode "projects" into Claude Projects-style workspaces: entering a
project gives a scoped workspace with its own sessions, chat history, and
materialized context, while the global workspace becomes one special scope
(GLOBAL_SCOPE sentinel) sharing the same code path.
Session scope
- AgentSessionManager stores sessions by internal id but binds each to an
immutable projectId; per-scope MRU, resolveSessionCwd, getSessionsForScope, and
enterProject/exitProject all key off that projectId, and the active-session
invariant (active.projectId === activeProjectId) keeps existing UI helpers
untouched.
- Backend restarts / in-place replacement inherit the replaced session's
projectId; project scope reuses the warm process instead of adopting the
vault-root probe session.
- Re-entering a project opens as a fresh visit: conversational chats detach from
the tab strip (kept live in the pool, listed in chat history) instead of
dragging every prior tab back, while an unused empty landing is reused rather
than stacked. The global workspace keeps its restore-the-last-tab behavior.
Context materialization (off-vault shared cache)
- projectContextMaterializer + contextCacheStore + manifestBuilder capture a
project's URL/YouTube/PDF context once per session (single-flight per project)
into a per-vault, off-vault store shared across projects
(~/.obsidian-copilot/vaults/<vaultId>/context-cache/): a source referenced by N
projects is converted once per vault, not N times. Paths derive from one source
of truth (conversionsLocation.ts); getVaultId lives in utils/appPaths.
- Layout: shared remotes/ + files/ snapshots keyed by source identity (md5),
per-project markers/<md5(projectId)>/ failure markers. A root-confined node:fs
backend (createNodeContextCacheFs) writes atomically (temp+rename), throws on
write/mkdir/read while list/remove/clear tolerate a missing target (the store
treats a failed read as a cache miss), and never ascends past the cache root; it
loads node:fs lazily behind the desktop boundary so Obsidian mobile never pulls
it into the bundle.
- A per-artifact async-mutex lock makes two projects cold-converting the same
source converge to a single fetch (the waiter re-reads meta inside the lock and
cheap-skips). Shared snapshots are never reconciled per-project; a project's
stale marker is cleared on a cheap-skip hit or when a usable stale snapshot
survives a failed refresh.
- Cache semantics match the legacy CAG cache: a successful snapshot is kept
indefinitely (fingerprint cheap-skip, no TTL); a failure with no usable
snapshot writes a marker and is cheap-skipped on later automatic runs
(re-surfacing the error) until the file changes or the user retries. A
schemaVersion stamped into every snapshot/marker makes a future format change
re-materialize instead of misreading stale content; MATERIALIZED_SOURCE_TYPES
is the single source of truth for source kinds.
- The first prompt inlines a <project_context> block listing absolute snapshot
paths (keyed by type:source so a same-URL web+youtube pair resolves correctly)
so all three backends reach the shared store; opencode gets an
external_directory allow for the cache root. Per-source status mirrors CAG's
ProjectLoadTracker into the projectId-keyed agentProjectContextLoadAtom
(remotes parallel, files sequential; each flips Ready/Failed as it settles) and
the composer shows a context status icon and queues sends while materializing.
The never-reject contract and contextSignature/warm reactivity are preserved;
released CAG caches (.copilot/*) are untouched.
Instructions mirror
- project.md stays the single source of truth, with a marker-gated AGENTS.md
mirror generated by ensureAgentsMirror: codex/opencode auto-discover it from
the session cwd, claude gets the same composed instructions via
getProjectProfile. A built-in project policy is layered into each project's
instructions (a user-authored, unmarked AGENTS.md is left untouched).
Landing & project UI
- AgentLandingStack spine with project landing state, header, Welcome card,
Project Chats tab variant, and project CRUD (anchored create popover, edit
modal, overflow menu); shelf lists cap at 5 behind a View-all popover.
- One context editor (ProjectContextSourceEditor) is shared by the home shelf and
the Edit Project modal; the Manage modal leads with a Links section whose single
add affordance is the sidebar "+URL" popover (the old right-pane inline URL
input is removed for consistency with Tags/Folders/Files). A three-state
(processing / ready / failed) status popover offers inline per-source retry, its
landing refresh deferred to popover-close and scope-guarded. The web/youtube
classification is consolidated into one UrlKind type reused across every URL
surface. Truncated previews mark the cut with a labelled divider and a "Copy
full content" pill, and cap render so large snapshots don't freeze the modal.
History scope
- Agent chat history is scope-keyed: a project view lists only its own chats,
while GLOBAL_SCOPE is the flat all-chats view. Native session history is scoped
to projects and resumed transcripts hydrate from the session's scope cwd; legacy
chats with no projectId resolve to GLOBAL_SCOPE. A failed resume/create during a
cross-scope history load rolls back the active scope (rollbackHistoryLoadScope)
so the active-session invariant can't be left split.
Todo / plan
- Unified backend todo lists across claude/codex/opencode feed the project info
popover, with per-session todo-id tracking and symmetric plan clear.
Hardening & fixes
- contextCacheFs rejects `..`, absolute, and root-escaping paths; symlink escape
is documented as out of the cache's threat model.
- A hard-disabled composer (orphaned project) blocks keyboard sends and queued
flushes, not just pointer events, so a turn can't drain into a dead project.
- The per-message attachment envelope is renamed <copilot-context> ->
<attached_context> to disambiguate from the project-wide <project_context>.
Tests
- 30+ suites covering session scope, the off-vault cache store/fs (including a
real-filesystem dedup integration test), the materializer, AGENTS.md mirror,
todo-plan pipeline, landing UI, the context-signature refresh state machine, and
the opencode external_directory allow; plus a mobile load-boundary smoke guard
over the cache consumers.
* fix(agent-mode): keep project edit modal open on save failure
ProjectInfoPopover's edit onSave swallowed updateProject errors into a
Notice, so onSave resolved and AddProjectModal closed — discarding the
user's edits on duplicate-name / folder-collision failures. Log and
rethrow instead so the modal keeps the form open and surfaces the error,
matching AgentProjectRowActions.handleEdit.
* fix(agent-mode): preserve project context after first-turn fan-out; clear off-vault markers on delete
A first-turn multi-agent fan-out set firstPromptSent while only the
ephemeral sub-sessions received the <project_context> block — the visible
backend is never prompted on that path. The next normal turn then injected
null, permanently stripping the project manifest from the main chat. Stop
the fan-out branch from consuming the first-turn flag so the next visible
turn delivers it. Adds a regression test.
Also clear the off-vault failure-marker bucket (markers/<md5(projectId)>)
in deleteProject so a project recreated under the same id can't inherit
stale negative-cache state. Desktop-gated + dynamically imported; the
helper roots a confined fs at the bucket and reuses recursive clear().
* fix(agent-mode): refresh empty project landing on System Prompt edit
An empty landing session bakes in its project instructions at creation
(Claude via systemPromptAppend, codex/opencode via the AGENTS.md mirror),
but the landing-refresh trigger keyed on the materialization signature,
which deliberately omits systemPrompt. A System-Prompt-only edit therefore
left the open landing untouched, so the first message ran with stale
instructions until a manual new chat.
Add getProjectLandingCaptureSignature (materialization signature + the
instruction body) and key the landing-refresh observer on it, leaving
getProjectContextSignature untouched so prompt edits never trigger
re-materialization. Adds tests.
* fix(agent-mode): preserve compose draft typed during landing refresh
The empty-landing context refresh replaces the session in place; its
draftEmpty guard ran before the async backend-startup window, so text the
user typed during that window landed in the old session's draft and was
pruned when the old session closed — breaking the guard's stated 'never
discard text the user has started typing' contract.
Add useAgentInputDrafts.migrateDraft and call it right after the swap to
carry any draft typed during startup onto the new session id. Writes
setDrafts directly to bypass the live-id guard; ordering is safe because
closeSession awaits cancel() before deleting the old session, so the
source draft is still present when migration runs. Adds tests.
* fix(agent-mode): don't reuse a project landing that captured stale instructions
The previous fix made the active landing refresh on a System-Prompt edit,
but enterProject's session-reuse path gated only on the materialization
dirty flag, which deliberately ignores systemPrompt. So editing a
non-active project's System Prompt then re-entering reused the old landing
and ran the next turn with stale captured instructions.
Track a per-session landing-capture signature (materialization signature +
instruction body) at creation and require a reuse candidate to match the
live project config. When spawning fresh, detach existing empty landings so
a stale blank one can't linger. Leaves the materialization dirty machinery
untouched (it must stay systemPrompt-insensitive). Adds a regression test.
* docs(agent-mode): note PR2 tag/extension routing and Tier-1 exclusion scope
Two DESIGN NOTEs at the project context materializer, recording decisions
already settled in PR2_DESIGN.md so future reviews don't re-flag them:
- tag/extension inclusions are forwarded as source labels and reached via the
agent's native search; precise resolution (MCP tag_search) is a PR3 item.
- exclusions are Tier 1 (best-effort: don't materialize / don't feed); they do
not hard-block the agent's native grep — that needs a Tier 2/3 sandbox,
deferred past PR2.
* fix(agent-mode): let project sessions read opted-in off-vault context
The built-in project prompt's default-only-cwd rule buried its carve-out in
a negative clause, so agents read an off-vault materialized snapshot path
(~/.obsidian-copilot/.../context-cache/...) as a forbidden external file and
declined to read it — even though the project itself configured that source.
Restructure the built-in prompt by axis (write / read+exception / boundary)
and name the <project_context> block explicitly: sources listed there are
opt-in to read and search even outside cwd, and a -> <abs path> snapshot
pointer is read directly. Pin the cross-file <project_context> tag coupling
with a regression test that diffs the prompt against the manifest's real
output.
* refactor(agent-mode): unify agent context status into one shared view
Per-item conversion status (icon + label + lookup key) was duplicated across
three agent surfaces: the composer status popover, the Manage modal's Links
panel, and — newly needed — its File Context file list. Each had its own
status->icon mapping, and the file list had none at all (it was wired to the
CAG load atom, which the agent pipeline never populates, so agent files showed
no status while URLs did).
Introduce processingItemStatusView as the single source of truth
(processingSourceKey / buildProcessingItemLookup / getProcessingStatusLabel /
ProcessingStatusIcon) and route all three surfaces through it. The Manage modal
now builds one agent status lookup (gated to the agent variant) shared by both
the Links panel and the file list, so file rows finally show conversion status
keyed by file:<path>. The legacy CAG status stays separate (different cache,
different semantics). Adds useAgentProcessingItems({ enabled }) so CAG/mobile
callers skip the off-vault read entirely.
* feat(agent-mode): show conversion status + snapshot preview for project files
The context editor's File Context list showed no conversion status or preview
for agent project files, while the Links panel showed both for URLs — an
asymmetry, since file snapshots also live in the off-vault cache. Wire file
rows to the same agent status pipeline: a ready file now shows its status icon
and a 'view parsed content' arrow that opens the off-vault snapshot via
openAgentCachedItemPreview (the same opener URLs use). Markdown has no snapshot
so it shows neither; the CAG path is unchanged.
Collapse the per-prop enableLinks branching at the row into one
getFileRowStatusProps resolver (agent vs CAG dispatched once). Also corrects
stale comments flagged in review (STATUS_COLOR is CAG-badge-only; the agent
hook still subscribes to its atom but the value is unused when disabled).
* refactor(agent-mode): neutralize reviewer-dialogue comments; share source builder
Drop the "if a future review flags this, point them here" reviewer-meta
sentences from this branch's own comments (keeping all why/invariant
rationale), leaving the pre-existing base-convention instances untouched.
Extract buildAgentProcessingSources() so useAgentProcessingItems and
useAgentPersistentFailureCount construct the AgentProcessingSource[] shape
through one shared pure helper instead of two copies that could drift; each
hook still passes its own candidate set (draft vs saved), so behavior is
unchanged.
* fix(agent-mode): restore the <copilot-context> attachment envelope
The per-message attachment envelope was renamed <copilot-context> →
<attached_context> in an earlier feature commit (8b880361), bundled as an
incidental "disambiguate from <project_context>" tweak. That changed AI prompt
content emitted to EVERY agent chat (not just project-scoped ones) without an
explicit request, against the repo rule "Never modify AI prompt content unless
the user explicitly asks"; base v4-preview has used <copilot-context> since
Agent Mode was introduced. Revert the tag (the inner instruction text was
unchanged) so the established prompt contract is preserved. The two
claudeSessionTranscript tests already expected <copilot-context>, so this also
restores prod/test consistency.
* fix(agent-mode): supersede a stale in-flight context materialization on edit
The per-project single-flight let any non-force caller join the in-flight run
unconditionally. If a project's context was edited while an earlier
materialization was still running (slow URL/PDF conversion + a queued first
prompt), the next session joined the stale run and received the pre-edit
<project_context> / additionalDirectories — the queued send could flush with
the old source set, and the new links/files were only captured on a later chat.
Carry the run's context signature in the in-flight entry and join only when it
matches the caller's current record signature; a differing signature supersedes
via the existing take-over-slot path (which defers disk work until the prior run
settles, so no cache-file race). Regression test added to the single-flight
describe.
* fix(agent-mode): roll back project scope when auto-spawn fails
enterProject optimistically parks the prior scope, points activeProjectId
at the new project, detaches its tabs, and nulls activeSessionId before
awaiting the auto-spawn of the scope's first session. When that spawn
rejects (e.g. a missing backend binary), the callers only surface a
Notice, leaving the manager scoped to a project with no active chat until
the user manually recovers.
Wrap the project-scope auto-spawn in spawnEnteredScopeOrRollback, which
restores the previous scope's activeProjectId + active-session pointer and
re-notifies on failure, then rethrows so the caller still reports it. The
rollback is guarded on still being parked in the attempted scope so a
concurrent enterProject isn't clobbered — mirroring the existing
rollbackHistoryLoadScope precedent for the history-load path.
* fix(agent-mode): address review findings (URL trim perf, snapshot meta parse, docs)
Three PR-introduced fixes from the PR2 review plus a deferral note:
- urlTagUtils: rewrite trimUrlTrailingPunctuation from O(n²) (a full split
scan per stripped char) to O(n) — pre-count brackets once, then walk the end
pointer left. A pathological trailing-bracket paste no longer freezes the main
thread. Semantics are unchanged (balanced brackets kept, unbalanced stripped,
hidden punctuation re-trimmed). Adds a 100k perf tripwire test.
- contextCacheStore: anchor parseSnapshotMeta's close marker to a line boundary
(\n--> ) instead of the first --> anywhere, which a source path containing -->
would match inside the embedded JSON, truncating it and forcing a permanent
cache miss. Adds a regression test.
- projects/state: correct doc comments — project.md is the SSOT, AGENTS.md is
its generated mirror (was inverted).
- AgentSessionManager: DESIGN NOTE only — detached sessions on project re-entry
are kept alive intentionally and not yet evicted; bounding the idle class is
deferred (needs a per-session backend close primitive).
|
||
|
|
f10430aea2
|
fix(agent-mode): allow exec/skill tools in read-only fan-out QA turns (#2636)
Multi-agent QA sub-sessions hard-denied the `execute` tool kind, which
silently killed web search for any backend whose only web path is a
shell-run skill script. opencode loads the `copilot-web-search` skill and
runs it via bash, so the denial left it with no research path and it
returned an empty answer ("This agent did not answer."), while Claude and
Codex answered via their native web search.
Narrow the read-only gate to deny only genuine vault mutation
(edit/delete/move); allow `execute` so Copilot's skill-script relay tools
(web search/fetch, PDF, YouTube, X) run. The read-only prompt preamble and
each backend's native read-only sandbox keep shell from writing the vault.
`other` (unverifiable third-party MCP) stays denied as before.
Rename `isWriteOrExecToolKind` -> `isVaultWriteToolKind` to match the new
semantics and update both enforcement sites (UI prompter + Claude SDK
bridge) and their tests.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
93727cde34
|
fix(agent-mode): restore inline code background when reloading a chat (#2635)
Reloaded Agent Mode messages render through ChatSingleMessage, which builds `.message-segment` text divs but never adds `markdown-rendered`. Without that class Obsidian's native reading-view stylesheet does not apply, so inline `<code>` spans lose the gray background pill the live render path (AgentMarkdownText) gives them. Add `markdown-rendered` to each rendered text segment so reloaded messages match the live styling, scoped to the segment divs to avoid regressing the existing `.message-content` rules. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f4da806b1c
|
feat(agent-settings): tab the Agents settings into per-backend panels (#2632)
* feat(agent-settings): tab the Agents settings into per-backend panels Convert the Agents settings tab from stacked vertical sections into a sub-tab strip (OpenCode / Claude / Codex / Quick Chat) built on the existing setting-tabs primitive. The global default-backend picker and MCP servers panel stay above the strip. Within each backend panel the default-model picker now sits above the model enable list, followed by the backend's binary/auth config. Quick Chat keeps its existing model curation. Placement-only revamp; no default-model resolution, seeding, or persistence changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-settings): anchor tab strip on switch; add Quick Chat icon + default-model picker Switching sub-tabs no longer jumps the settings scroll: the tab strip is pinned to its pre-switch viewport position when panel heights differ. The Quick Chat tab gains a chat-bubble icon (was iconless) and a Default model picker mirroring the per-agent panels, writing the shared defaultModelKey. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * chore(scripts): reload deploy-target vault by cwd; timestamp-only build label The Obsidian CLI resolves its target vault from the working directory, so the reload step now runs from $VAULT_PATH (was hitting the repo's own vault). The manifest label is tagged with the build timestamp only, not the branch name. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
769b8724bf
|
feat(agent-mode): explicit per-agent Default model setting (#2631)
* feat(agent-mode): explicit per-agent Default model setting Each toggled-on agent (opencode / claude / codex) now has an explicit "Default model" (+ effort) picker in its settings section, sourced from that agent's enabled model list. This default seeds every new session and every fan-out answerer on that backend, and an already-open chat picks it up on its next turn. The chat model picker is now transient: a chat-side model switch mutates only the active session and no longer writes the durable default. The settings picker is the sole writer of `agentMode.backends.<id>.defaultModel`. - AgentSessionManager.applySelection no longer persists the selection. - runCrossBackendPick seeds the freshly-spawned session with the picked model directly (new createSession seedSelection arg) instead of persisting-then-reading the default. - AgentSessionManager subscribes to default-model settings changes and re-applies them to live sessions via descriptor.applySelection (next turn; in-flight turns unaffected). - Extracted resolveEffortOptions (shared by the chat picker and the new settings picker); changing the selected model resets effort to the new model's first valid option, since opencode effort is model-specific. Closes logancyang/obsidian-copilot-preview#186 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): address Codex review on per-agent default model Two correctness fixes from Codex review of the per-agent Default model setting: - Defer the live default re-apply for sessions still in their startup window. setModel/setConfigOption throw before backendSessionId is assigned, so an immediate apply was lost with only a logged warning; the new chat kept its old seed. Queue the apply off session.ready and re-read the latest default then, so the final value wins when several changes land before startup completes. - Make the settings default-model snapshot reflect model-cache changes. The useSyncExternalStore snapshot was only the preload status, so the post-"ready" effort-catalog prefetch never triggered a rerender and the Default effort dropdown would not appear until an unrelated rerender. The snapshot is now a cache signature covering status, cached state, and the effort catalog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): address second Codex review round Two more correctness fixes from Codex review of 61c9948d, both regressions introduced by this PR: - Preserve a transient effort across a cross-backend pick. runCrossBackendPick now seeds the new session without persisting, but Claude's applyInitialSessionConfig replayed the persisted default effort, overwriting the user's drafted effort on startup. Pass the resolved seed selection through to applyInitialSessionConfig so the seed's effort wins over the persisted default; a plain new session (no transient seed) still replays the persisted default unchanged. - Represent an unset default explicitly in the settings picker. With no stored defaultModel the picker showed the first enabled model as selected even though createSession uses the agent's native default, and an effort-only change would silently persist that model. Add an explicit "Agent default" option that maps to the cleared state; selecting it clears the default, and the effort row only appears for a concrete default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): keep a stale default visible so it can be cleared Codex review of 2c234d45 (P2): when a user saved a default and later disabled that model (or all enabled models), getEnabledModelEntries returned empty and the settings control was hidden, even though new chats and fan-out still read getDefaultSelection and would keep starting on the now-disabled model with no way to clear it. Hide the control only when there are no enabled models AND no stored default. A stored default whose model is no longer enabled is rendered as a "(disabled)" option so the user can see it and switch to "Agent default" to clear it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): revert live sessions to native default when cleared Codex review of 32c13fea (P2): when the user set Default model back to "Agent default", persistDefaultSelection(..., null) made the new value null, so onDefaultSelectionsChanged's `if (!after) continue` skipped live sessions. An open chat stayed pinned to the old explicit model even though the settings copy says open chats switch on the next turn. Treat a cleared default as a change to the agent's native default: resolve the catalog-declared native model (availableModels[0]) and apply it to live sessions on that backend, so the next turn uses native instead of the stale explicit model. With no probed catalog there's no native id to target, so the session is left as-is. The change-detection guard now also covers null->null (no-op) correctly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): seed cleared agent-default sessions to catalog native When an explicit default is cleared, a warm/running subprocess (e.g. opencode) keeps serving the model baked into its spawn-time config, so a brand-new "Agent default" chat would silently inherit the stale model. createSession now falls back to the catalog native default selection when there's no transient seed and no stored default, confirming the new session onto native. With no probed catalog there's no native id to target, so the seed stays undefined and behavior is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): serialize default re-applies and seed fan-out clears Two follow-up edge cases on the agent-default flow: - onDefaultSelectionsChanged fired the live-session re-apply fire-and- forget, so two rapid default changes could race their setModel/ setConfigOption round-trips and leave the session on a stale model. Re-applies now serialize per session on a chain that re-reads the latest default at run time, so the final settings value always wins. This also subsumes the prior "starting session" deferral. - Fan-out sub-sessions read the default via FanoutHost.getDefaultSelection and no-oped on a cleared default, so multi-agent answers kept inheriting the model baked into a warm/running subprocess. The host now mirrors createSession's catalog-native fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): show unset default effort instead of first option A stored default with effort: null means "let the agent choose", but some catalogs (e.g. Claude's low/medium/high) enumerate only concrete values, so the effort select fell through to effortOptions[0] and showed e.g. "low" as selected while the runtime treated null as unset. The picker now prepends an explicit "Agent default" (null-valued) effort option when the catalog lacks one, and binds the select to the unset value when no effort is persisted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): reset effort to agent default on a model-only change Changing only the Default model used to auto-persist the new model's first concrete effort (e.g. "low"), silently running new chats and fan-out at an effort the user never picked. Now that the unset state is representable, a model-only change persists effort: null (agent default); the user can pick a concrete effort explicitly. This also drops any stale effort from the previous model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): apply seeded effort through the backend's own channel A cross-backend pick seeds the new session with a drafted (model, effort) and confirmSeededSelection used to apply it via a raw applyModelWireId. For config-option opencode (>=1.15.13) effort is a separate thought_level option, so packing base/effort into the model wire id started the session at the model's default effort or failed, and opencode has no applyInitialSessionConfig to split it post-startup. confirmSeededSelection now dispatches through descriptor.applySelection, which splits model and effort per backend. Because that path guards its model switch on the current state, the optimistic baseModelId seed is first dropped for config-option backends so the real switch still fires; setModel-style backends keep the seed (no blink) since they always issue the round-trip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): reset seeded effort to native when cleared over a stale value Follow-up to routing the seed through applySelection: a config-option opencode process can bake a concrete effort (e.g. model/high), so after the user clears the default effort to agent default a fresh session reports the same base but the stale concrete effort. applySelection skips the model write (base matches) and returns for null effort, leaving the chat on the stale value. confirmSeededSelection now re-writes the bare model option in that case to reset effort to the model's native default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6ecea570f1
|
fix(agent-mode): hide Recent Chats entries not resumable on this device (#2633)
* fix(agent-mode): hide Recent Chats entries not resumable on this device A chat started on another machine syncs its markdown note (carrying the backend session id) via the vault, but the backend's transcript store (~/.claude/projects, opencode/codex session dirs) is machine-local and does not sync. The row therefore appears in Recent Chats on a second device but dead-ends on resume (Claude loops on "No conversation found"). Add an optional `sessionExistsLocally` capability to the BackendProcess contract and implement it for the Claude SDK (a cheap fs check on the session jsonl). `AgentSessionManager.getChatHistoryItems` now drops markdown chats a running backend definitively reports absent; an unknown answer (no capability, backend not running, or probe error) keeps the row, so a local chat is never hidden. Memoize the resolved Claude config dir so the per-entry locality probe doesn't repeat a Plus license-key decryption on every history render. Refs logancyang/obsidian-copilot-preview#173 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): tombstone native twin when dropping non-local chat A normal autosaved chat carries both a markdown note and a flushIndexTouch session-index entry on its origin machine. When the note syncs to a second device but the backend's local transcript store does not, dropping only the markdown row left index.getEntries() still returning the same (backendId, sessionId), so mergeChatHistoryItems resurfaced it as a native-only row that dead-ends in loadNativeSessionFromHistory with no markdown fallback. dropNonLocalMarkdownEntries now tombstones the matching index entry (cancelling any pending index touch, mirroring deleteChatHistory) whenever the locality probe definitively reports the session absent, so the non-resumable chat is fully removed rather than reappearing. Refs logancyang/obsidian-copilot-preview#173 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0aa3a92766
|
fix(agent-mode): highlight active multi-agent tab + readable summary (#2630)
* fix(agent-mode): highlight the active multi-agent response tab The selected fan-out tab only swapped its background fill, which didn't read as active. Match AgentTabStrip's proven active-tab treatment: accent border + faint accent tint + normal-weight medium text, so the active tab is clearly distinct from the others without hovering. No !important — resolved via cn()/twMerge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * feat(agent-mode): make the multi-agent summary readable (headings, not dense bullets) The summary used bold inline labels and one dense bullet per agent. Switch to a FORMAT rule that leads with the bottom line, then proper ### subheadings (Each agent / Agreements / Disagreements, or Options / Recommendation) with short paragraphs and whitespace, and only sparse one-line bullets for genuine lists. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): a finished-but-empty fan-out slot no longer shows check + "Thinking…" A fan-out agent that finished with no answer text (status done, empty text) rendered a green success check on its tab AND "Thinking…" in the body — reading as both done and still working (repro: opencode). Resolve a done+empty slot to a new `empty` presentational state: the tab dot is a muted slash (not a check) and the body reads "This agent did not answer." instead of the running-spinner fallback. Re-applies handling that was dropped during the earlier slim-down. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f071eb896c
|
feat(agent-mode): shimmer the multi-agent QA "thinking" status text (#2629)
Apply the shared running-gradient `copilot-shimmer-text` effect (the same one used by BottomLoadingIndicator) to the fan-out turn's active status lines — "Waiting for answers…", "Writing summary…", "Streaming…", "Thinking…" — via a `shimmer` prop on FanoutStatusLine. Terminal/error lines (cancelled, unavailable, failed) stay static. Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2cf80bc411
|
feat(agent-mode): multi-agent per-turn QA (#57) (#2628)
* feat(agent-mode): @-mention agent selection in composer (phase 1/5, #57) Add an "Agents" group at the top of the existing @-typeahead so users can @-mention installed coding agents (e.g. @claude / @codex / @opencode) to ask the same turn of several agents. The group is registry-driven and installed- only: it lists every backend whose install state is `ready`, so newly registered backends are mentionable automatically with no per-agent branches. Selections render as removable agent pills (reusing the shared Lexical pill / typeahead rendering) and resolve at send time into a structured `mentionedAgents: BackendId[]` (main agent first, deduped if re-mentioned), never left as literal text in the prompt. The host chat-components stay agent-agnostic: a local `AgentMentionBrand` shape is passed down as props, so the generic composer never imports Agent Mode internals. `mentionedAgents` threads through sendMessage -> sendPrompt to a typed seam, `AgentSession.getLastMentionedAgents()`, that later phases consume; the single-agent path (no mentions) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): address review on @-mention agent selection (phase 1/5, #57) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-mode): fan-out orchestration + read-only enforcement (phase 2/5, #57) When a turn has more than one agent (main + ≥1 @-mentioned), runTurn now dispatches the identical prompt + context to every backend in parallel via ephemeral read-only sub-sessions instead of the single backend.prompt(). The single-agent path (0 mentions) is unchanged. Each sub-session is created on its own backend (ensureBackend + newSession), never registered as a visible AgentSession / tab, runs the agent's configured default model, and is closed at turn end. Read-only is enforced three ways: a universal "answer only, no writes" prompt preamble; a shared permission prompter that hard-denies write/exec tool kinds (edit/delete/move/execute) and allows reads/search/fetch for fan-out sub-sessions on ALL backends (claude + codex + opencode); plus a per-backend read-only sandbox mode for setMode-style backends (codex plan→read-only) as belt-and-suspenders. Answers stream live into per-agent slots of a single in-memory FanoutTurn; a failed agent sets its slot to error and the others continue (allSettled-style), and sub-session prompts are cancellable via the turn's abort signal. The summary slot is left pending for Phase 3. Persistence is no-migration: a fan-out turn collapses to summary-only text written as an ordinary assistant message, so per-agent answers are never serialized and existing single-agent transcripts load unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): address review on fan-out orchestration + read-only enforcement (phase 2/5, #57) Classify NotebookEdit as an edit tool kind so the read-only fan-out permission policy denies it. Previously NotebookEdit (a native Claude SDK .ipynb write tool) fell through deriveToolKind to "other", which isWriteOrExecToolKind allows, letting a fan-out read-only sub-session on the Claude SDK backend perform a notebook write. Adds a guard test over every native write/exec tool so this class of gap cannot regress. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-mode): main-agent narrative summary for multi-agent QA (phase 3/5, #57) Once every fan-out answer settles, the session's main agent generates a narrative summary that reconciles and contrasts the per-agent answers, streamed token-by-token into the live FanoutTurn's summary slot (status pending -> streaming -> done) for Phase 4 to render. The summary runs read-only via the Phase 2 mechanism: per-agent answers and the summary now share a single runReadOnlySubSession helper that opens an ephemeral sub-session, registers it as read-only (hard write/exec denial), applies the sandbox mode + default model, streams assistant text, and tears down. The summary feeds the main agent a NEW composed user-turn prompt (read-only instruction + the user's original question + each succeeded answer labeled by display name); no existing system prompt is touched. Failure-aware (D7): the summary runs over the agents that succeeded and names any that failed or returned empty. With zero successes it does not fabricate a summary -- it lands summary.status="done" with a brief all-failed note (the chosen zero-success terminal state), and dispatches no sub-session. Cancellation skips the summary. Persistence is unchanged: the collapse-to-summary-only path from Phase 2 now writes the generated summary text to disk. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): address review on multi-agent QA summary (phase 3/5, #57) Correct doc comments that this commit makes stale: the summary slot is now filled by the main agent over the survivors (D6) once every answer settles, not left pending for a future phase. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-mode): summary-first dropdown UI for multi-agent QA (phase 4/5, #57) Render a live multi-agent FanoutTurn as one assistant turn: a summary-first Select switcher (D8) that drills into the main agent's narrative summary or each participating agent's full answer. Each agent entry reflects its live state (D7) — spinner while streaming, the answer when done, an error state on failure — and updates per token as the orchestrator streams into the live turn. The fan-out view renders only for the active/live turn (the last assistant message while AgentSession.getLiveFanoutTurn() is non-null); normal single-agent assistant messages and reloaded summary-only transcripts render exactly as before through the existing path. Branded icons + display names are registry-driven (backendRegistry), and answers/summary reuse the existing AgentMarkdownText renderer. The live turn is surfaced through AgentChatBackend.getLiveFanoutTurn() and the useAgentChatRuntimeState subscription, so only the memoized FanoutTurnView re-renders on the streaming hot path. Pure option-list derivation and status-to-state mapping live in fanoutDropdown.ts with unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): address review on multi-agent QA dropdown UI (phase 4/5, #57) The orchestrator mutates one FanoutTurn in place and re-emits the SAME reference per streamed token, so the Phase 4 UI subscription handed that identity straight into React state — setState bailed (Object.is-equal) and the dropdown froze on its first frame, never reflecting live per-agent streaming/status updates. getLiveFanoutTurn() now returns a structural snapshot so each coalesced notify yields a fresh reference and re-renders. Also clear a prior turn's live fan-out state BEFORE the next turn's placeholder is announced (was cleared after notifyMessages fired), so a following single-agent turn's placeholder can no longer briefly render the stale dropdown. Adds snapshotFanoutTurn unit tests and a cross-turn no-leak session test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-mode): partial-failure & cancellation hardening for multi-agent QA (phase 5/5, #57) Harden the two failure modes of the multi-agent read-only QA fan-out so the feature is robust end-to-end: - Partial failure (D7): one agent's rejection/throw was already isolated to its own error slot; add a per-agent answer TIMEOUT so a hung/long-running sub-session fails its OWN slot (error + reason) without stalling the siblings or the summary. Duration is a single documented constant (FANOUT_AGENT_TIMEOUT_MS, 5 min); no user-facing setting. The summary still runs over the survivors. - Cancellation: cancelling the turn aborts every in-flight sub-session promptly (proc.cancel via the run signal) and an abort mid-prompt now transitions the slot to a terminal "cancelled" state instead of being mislabelled "done" or left "running". The sub-session is closed in finally on every exit path (done / aborted / timeout / throw) so no ACP sub-session leaks, and no summary is generated after cancel. - UI: add a distinct "cancelled" agent state (muted slash chip) alongside the refined error chip (short reason, incl. timeouts); both preserve any partial text streamed before the stop. Registry-driven, reuses existing tokens. Tests: one agent rejects -> error slot, others complete, summary over survivors; per-agent timeout fires -> that slot errors, turn + summary still finish; cancel mid-flight -> every in-flight sub-session gets cancel, slots reach terminal cancelled, no summary after cancel; UI maps error + cancelled states. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): address review on partial-failure & cancellation hardening (phase 5/5, #57) Clear the per-agent deadline timer (and remove the abort listener) on the abort path of runPromptWithTimeout. Previously onAbort resolved "aborted" without calling cleanup(), leaving a live 5-minute setTimeout (holding proc / sessionId / prompt) running after the user cancelled the turn. Move cleanup() into both the onAbort and timeout callbacks so every settle path tears down both the timer and the listener exactly once. Add a regression test asserting the deadline timer count is 0 after an abort and that advancing past the deadline never relabels a settled cancelled slot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): clear stale agent mentions on session switch + terminal summary states (#57) Address Codex review on PR #2628: - P2: reset `mentionedAgentIdsRef` in the session-switch effect so an `@agent` pill composed in one session cannot ride along into an unrelated prompt in another (the ref lives in AgentChatInput, not the keyed ChatInput, so the editor remount does not clear it). - P3: render terminal summary states instead of a perpetual spinner. A cancelled turn leaves the summary `pending` (runSummary skipped) and a failed summary lands `done` with empty text; both now show a "Summary cancelled" / "Summary unavailable" line via the new pure `summaryDisplayState` helper, unit-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): carry fan-out Q+summary into the next single-agent turn (#57) A fan-out turn runs every agent through ephemeral read-only sub-sessions and only writes the summary to the store; the visible session's backend never sees the turn, so a normal single-agent follow-up loses that context. Buffer each completed fan-out turn's {question, summary} on the session and inject the buffered entries (in order) as one labeled <prior_turns> context block ahead of the user message on the next single-agent prompt, then clear. Consecutive fan-out turns accumulate; only single-agent turns flush. Backend-agnostic; live-only, no persistence change. Empty buffer leaves the prompt byte-for-byte unchanged. Addresses Codex P1 on PR #2628. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): address review on fan-out follow-up continuity (#57) Clear the pending fan-out buffer only AFTER backend.prompt() resolves, not before. The block is captured into promptBlocks pre-await, so the prompt sent is unchanged, but a thrown prompt now preserves the buffer so the dropped multi-agent QA context replays on the next single-agent turn instead of being lost with no record. Adds a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): pill text leak, read-only plan-file bypass, blank summary persistence (#57) E (pill text leak): AgentPillNode extended BasePillNode without overriding getTextContent(), so the editor serialized the backend id into the prompt (`@Claude …` became `claude …`). Override getTextContent() to return "" — the mention already feeds mentionedAgents structurally and the visual pill comes from decorate(). Other pill types are unchanged. C (read-only fan-out could still write Claude plan files): - (a) Stop abusing canonical.plan as the read-only sandbox. Add an optional ModeMapping.readOnlyModeId for a backend's GENUINE read-only sandbox; set it only on Codex ("read-only"). Claude/OpenCode leave it unset. FanoutOrchestrator .applyReadOnlyMode now applies readOnlyModeId (skipped when absent) instead of canonical.plan, so a read-only QA turn never enters Claude's real plan mode (which drafts and writes plan files). Registry-driven, no name branch. - (b) Make the read-only-session check precede the plan-file auto-allow in the Claude SDK permissionBridge. The bridge now consults a read-only-session predicate (threaded generically via the optional BackendProcess .setReadOnlySessionPredicate, wired from AgentSessionManager.isReadOnlyFanout Session) at the TOP of canUseTool and hard-denies write/exec tools (reusing isWriteOrExecToolKind) BEFORE the plan-file auto-allow — closing the hole even if a mode switch is wrong for some backend. A (summary failure persisted a blank assistant message): when summary generation threw/ended empty but agents answered, the turn collapsed to "" and reloaded as a blank bubble. collapseFanoutTurnToSummaryText now returns a new FANOUT_SUMMARY_UNAVAILABLE note whenever at least one agent succeeded but no summary text exists; zero-success still uses FANOUT_ALL_FAILED_SUMMARY (written by runSummary); a turn with no successes and no summary collapses to "" so nothing misleading is persisted. Per-agent answers remain live-only. Tests: agent pill contributes empty text + prompt omits backend id; read-only session denies a ~/.claude/plans/*.md Write before the auto-allow; applyReadOnly Mode applies readOnlyModeId (never plan) only when advertised; summary-throws- with-answers persists the fallback, zero-success uses the all-failed note, a normal summary is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-mode): give fan-out agents the conversation history (#57) Fan-out QA dispatched every agent to a FRESH ephemeral session that received only the current turn's prompt, so `@agent` follow-ups that reference earlier conversation ("that plan", "the answer above") and fan-out→fan-out continuity failed (Codex finding D on PR #2628). This realizes D9 of the fan-out design: render the prior visible transcript into a read-only `<conversation_history>` block and prepend it to every fan-out agent's prompt, ahead of the current question + context. - New pure `buildConversationHistoryBlock(messages, maxChars)` in fanoutTypes.ts renders prior display messages labeled by role, escaped, framed as read-only context (not a task to redo). Returns null on empty history so the prompt stays byte-for-byte unchanged. - Oldest-first safety cap (`FANOUT_HISTORY_MAX_CHARS` = 48000) with a truncation marker: fan-out sub-sessions are single-turn, so the whole transcript rides in one prompt with nothing to compact; an oversized block would hard-error the model API rather than auto-truncate. - Injection reuses `buildPromptBlocks`'s `leadingContextBlock` seam (same as the single-agent prior-fan-out path); the current turn is excluded by the exact user/placeholder ids `sendPrompt` captured. - The block is backend-prompt only — never displayed or persisted — and every agent (main + mentioned) gets the identical block (D10). Separate from the existing single-agent pending-fan-out buffer, which stays. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): apply user model+effort to fan-out sub-sessions (#57) FanoutOrchestrator.applyDefaultModel applied only the encoded model id to each ephemeral sub-session via setSessionModel. For backends where effort travels via a config option rather than the wire id (Claude SDK: claudeWire drops effort, effortConfigFor exposes the select spec), the sub-session never received the user's configured effort and ran at the backend default. Mirror descriptor.applySelection generically off the wire codec: after setSessionModel, when selection.effort is set and descriptor.wire.effortConfigFor(baseModelId) returns a spec, apply effort with setSessionConfigOption. Wire-encoded-effort backends (codex/opencode suffix style) return nothing from effortConfigFor and need no extra call, so there is no per-agent-name branching. Kept best-effort (try/catch with logWarn) so a missing/unsupported option leaves the backend default in place. The opencode config-option-model catalog case (where set_model itself is gone) remains a documented residual. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): address review on fan-out model+effort apply (#57) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): bound fan-out history cap and include tool/plan parts (#57) Two fixes to the pure buildConversationHistoryBlock used to inject the prior visible transcript into every fan-out QA sub-session. Finding 1 (real cap): the oldest-first drop loop stopped at a single turn, so one giant recent message (a long answer or pasted dump) passed through uncapped and risked a prompt-too-large error in the fresh sub-session. Add a final hard cap: after dropping older turns, if the joined body still exceeds FANOUT_HISTORY_MAX_CHARS, truncate its head and append a "[turn truncated]" marker. The rendered block body is now bounded by ~maxChars plus the small constant framing/markers for ANY input, never unbounded, never empty when there is at least one non-empty turn. Finding 2 (fidelity): the builder rendered only prose and dropped turns whose prose was empty, so a follow-up like "explain the command output above" or "review the plan above" got no context that single-agent backend memory would have carried. Serialize each turn's renderable parts off part kind (no per-agent branching): prose from message (which already aggregates the text parts, so no duplication), tool_call parts as "[tool: <identity>]" plus their text output (trimmed per part so one card can't dominate), and plan parts as their entries. thought parts are omitted. A turn with any renderable content (prose OR tool/plan parts) is no longer dropped; only truly empty turns are skipped. All content stays XML-escaped so it can't break the framing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): settle timed-out fan-out prompts before reusing the backend (#57) On a per-agent prompt timeout/abort the orchestrator called proc.cancel and resolved the slot immediately, but cancel only INTERRUPTS — the underlying proc.prompt() keeps unwinding the backend query. Since run() reuses the main agent's backend for the summary, the summary's prompt could start on the same backend while the main agent's timed-out prompt was still unwinding. The Claude SDK backend's permission-bridge/session context is process-global for the active query, so two overlapping prompts on one backend can misroute permission decisions/events or corrupt the summary. runPromptWithTimeout now holds the real prompt promise and, on the abort AND timeout paths, awaits its actual settlement (swallowed) before resolving the helper — so "settled" means the backend query truly stopped, not just that cancel was requested. That wait is bounded by a new FANOUT_CANCEL_GRACE_MS (3s) constant so a backend that ignores cancel cannot hang the turn forever (it logs and proceeds, preserving the timeout's bounded wall-clock). The happy path (prompt resolves on its own) never enters the grace. Applied uniformly to every sub-session, not special-cased to the main agent. Preserves the single-shot settled guard, clears the deadline + grace timers on all paths (no timer leak), keeps the swallowed prompt rejection from surfacing as unhandled, and keeps abort terminal-cancelled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): address review on timed-out prompt settle (#57) The settle-wait awaited the swallowed prompt via a bare `promptPromise.finally` whose derived promise was only `void`-ed. `.finally` re-raises the original rejection, and a cancelled/timed-out backend prompt usually REJECTS as it unwinds, so that branch leaked an unhandled rejection (the up-front `promptPromise.catch` only guards its own branch, not the `.finally` chain). Replace it with a `promptSettled = promptPromise.then(noop, noop)` that maps both outcomes to a resolved value; the cancel grace awaits `promptSettled`, so the swallowed rejection can never surface as unhandled while settlement semantics (fulfilled OR rejected both count as "the backend query stopped") are unchanged. Add a regression test that rejects the cancelled main prompt and asserts no unhandled rejection plus the summary still reuses the backend. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): preserve replay buffer on cancel + route fan-out model via config-option channel (#57) Finding 1 (UX continuity): the post-fanout single-agent replay cleared the pendingFanoutContext buffer whenever backend.prompt() resolved, including a CANCELLED resolution. A user stopping that first replay mid-stream could lose the multi-agent <prior_turns> context because the backend may not have durably ingested the injected block. The clear is now gated on resp.stopReason !== "cancelled", so a cancelled replay keeps the buffer and re-injects on the next turn (a duplicate re-injection is harmless). The empty-buffer byte-for-byte path and the thrown-prompt-preserves-buffer behavior are unchanged. Finding 2 (UX correct model): FanoutOrchestrator.applyDefaultModel always applied the QA sub-session's model via setSessionModel, which hits the now-gone session/set_model RPC on opencode >= 1.15.13 (where the catalog is a category:"model" config option) and silently left the sub-session on the backend default model. The orchestrator now threads the opened sub-session's BackendState.model.apply spec from newSession and, for a setConfigOption spec, applies the model via setSessionConfigOption (and effort via the model-specific effortConfigId reported by the refreshed state), mirroring AgentSession.applyModelWireId + the opencode descriptor.applySelection. The setSessionModel backends (claude/codex) are unchanged. This resolves the opencode >=1.15.13 model residual. Channel selection is driven off the apply spec, with no per-agent-name branching, and remains best-effort (failures are logged and leave the backend default in place). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): capture trailing ACP chunks before closing fan-out sub-sessions (#57) Some ACP backends (opencode, fast models) flush a turn's final `agent_message_chunk` events just AFTER the `session/prompt` result resolves. Fan-out sub-sessions previously unregistered their per-session update handler immediately once `prompt()` resolved, so those trailing chunks arrived after the handler was gone and were dropped, truncating a fan-out agent's answer or the main-agent summary right at the end. On the normal resolve path only, hold the still-registered handler open for a short bounded grace (FANOUT_TRAILING_CHUNK_GRACE_MS = 500ms) before unregistering and closing the ephemeral sub-session, so late chunks still land in the same slot. Cancel/timeout/throw skip the grace entirely, mirroring the single-agent "except on explicit cancel" carve-out where suppressed output stays suppressed. The grace timer is one-shot and cannot leak; the existing cancel-settle grace, single-shot guard, deadline-timer cleanup, no-unhandled-rejection handling, and terminal slot states are all preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): mark prior image attachments in fan-out history (#57) The fan-out conversation-history renderer built each prior turn's block from prose (message.message) and tool/plan parts only, ignoring message.content — the display array that carries image attachment blocks. As a result an image-only turn (empty prose, no parts) rendered as null and was DROPPED from history entirely, and a text+image turn lost any signal the image existed, so an @agent follow-up like "what's in the screenshot above?" reached the fresh fan-out sessions with no hint of the image. renderTurnContent now counts image attachment blocks in content (defensively narrowed: a non-null object whose type is "image" or "image_url") and appends a concise marker, e.g. "[1 image attachment omitted from history; ...]" (singular/plural correct). An image-only turn now renders the marker and is no longer dropped; a turn with no image content is byte-for-byte unchanged. This only signals that omitted image context existed — threading the actual image bytes into the prompt (full multimodal history) is a tracked follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): include prior-turn context (selections/notes/tabs) in fan-out history (#57) The fan-out conversation-history renderer (buildConversationHistoryBlock / renderTurnContent in fanoutTypes.ts) serialized each prior turn's prose, structured parts, and image markers but never message.context. Fan-out agents run fresh sessions with no memory, so any visible context a prior turn attached (selected-text excerpts, notes, folders, web tabs, urls, tags) was lost — a follow-up like "@codex explain the selected excerpt above" reached them with no excerpt, even though the single-agent backend had seen it inline. renderTurnContent now appends a [context] section rendering the stored context: selection excerpts (label + actual content, per-item trimmed so one can't dominate) plus concise identifier lines for notes/folders/urls/tags/web tabs. Empty/undefined context renders nothing (byte-for-byte unchanged); a context-only turn is no longer dropped. Dynamic values are escaped by the existing outer escapeXml pass, matching the tool/plan/image renderers. Full note/web-tab bodies are read from the vault at prompt time and are not stored on message.context, so only note NAMES + the already-captured selection excerpts are included; threading full note bodies is a tracked follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): preserve note paths and web-tab URLs in fan-out history Fan-out history rendered notes by basename and web tabs by title only, so a fresh fan-out sub-session asked to read the note above or check the page above had no resolvable path/URL. Render n.path and keep the URL alongside each tab title so the actionable identifier survives. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * feat(agent-mode): main agent summarizes only; answerers are the @-mentioned set Pre-ship redesign of multi-agent per-turn QA. The session's main agent no longer auto-joins the answerer list: answerers are the deduped, installed @-mentioned agents (it answers only if explicitly @-ed), and the main agent always writes the summary. Fan-out fires for >=1 answerer except a lone [main], which collapses to the normal single-agent path. The summary always runs, even for a single answerer. Pure routing helpers (resolveAnswerers/isFanout/EMPTY_ANSWERERS) live in a UI-free fanout/answerers.ts so the session and composer share one source of truth without a session->ui dependency. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * feat(agent-mode): persist multi-agent response on the message object with a tabs UI The AI response message now owns its fan-out sub-responses. A live-only message.fanout drives the UI; the full composite is persisted as the message body itself - composite markdown with invisible HTML-comment section markers - so reload reconstructs the dropdown and any unaware loader/export still renders clean markdown. No new on-disk field, no migration; marker-less bodies parse to null and render exactly as before. serializeFanoutComposite/parseFanoutComposite round-trip losslessly (a self-disambiguating two-symbol escape prevents marker forgery AND preserves answer text that contains the escape sentinel); each persisted answer is capped. The store setFanout bumps the message version so the streaming dropdown re-renders without freezing. FanoutTurnView is now a segmented tab row (Summary first/default, brand icon + live status dot per tab) with a per-slot inline copy. The new FanoutMessageCard reuses the existing Agent-Mode AI action bar (Insert/Replace + Copy, no Retry) acting on the whole composite. The liveFanoutTurn side-channel is retired. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * feat(agent-mode): gate the @agent typeahead to paid users (paywall layer 1) Multi-agent per-turn QA is paid-only. Free users get the frozen EMPTY_AGENT_MENTION_BRANDS, so the @-mention 'Agents' group never renders and no agent pill can be inserted (typeahead, paste/text-parse, and draft-restore paths all verified closed). The gate is reactive via useCanUseMultiAgent(), so it flips live on upgrade/downgrade; paid users hit the unchanged path. A subtle conditional upsell hint above the composer points free users to upgrade (navigateToPlusPage with a new MULTI_AGENT UTM medium). canUseMultiAgent()/useCanUseMultiAgent() mirror isPlusEnabled()/useIsPlusUser() respectively; their intentional sync-vs-reactive delta on the unvalidated self-host edge is documented. A determined free user pasting agent-pill HTML is caught by the send-time backend check in the next phase. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * feat(agent-mode): authoritative send-boundary paywall for multi-agent QA (layer 2) The session boundary is the unbypassable gate: in runTurn, before any fan-out work, ensureMultiAgentEntitlement() must pass. Paying users hit a sync isPlusEnabled() fast path with zero network cost; otherwise it re-verifies against the backend /license endpoint (covering a stale-false cache for a real paid user) and blocks on a negative or unverifiable result. A blocked turn is a hard stop - no silent single-agent fallback - that shows an upgrade prompt (Notice + the MULTI_AGENT upgrade page), marks the placeholder as an error, settles as 'refusal', and returns the session to idle so the user can resend. This catches any agent pill that slips past the UI gate (e.g. pasted pill HTML). app is threaded via getApp DI, never the global. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): resolve send-time mentions against the real installed set, not the paywalled list The @-mention typeahead list is entitlement-gated (empty for free users), but it was also reused as the send-time installed-agent allowlist. So a pasted/imported agent pill (or a stale-false Plus cache) resolved to answerers=[], omitted mentionedAgents, and silently ran single-agent - never reaching AgentSession's authoritative entitlement gate. Resolve installed backends independently of the gated UI list so such turns fan out and hit the session gate, which blocks or re-verifies. Fixes Codex P2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): make the fan-out summary concise and answer-focused The summary instruction asked for a reconcile-and-contrast narrative with no length bound, so it ballooned and analyzed the environment scaffolding (tool lists, skill/agent catalogs) that agents dump into their answers. Rewrite it to give one direct, concise answer scaled to the question, ignore that scaffolding, and stop narrating each agent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): one context-aware copy/insert for fan-out, drop the per-slot row The per-slot copy sat in its own full-width hover row above each answer, and the action bar separately copied the whole composite - redundant and visually heavy on a single-agent tab. Collapse to ONE affordance: the card's action bar Copy/Insert now act on the tab in view (summary tab copies the summary, an agent tab copies that agent - copy what you see). FanoutTurnView becomes controlled so the card owns the selected tab. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): persist a terminal fan-out agent's partial text on reload A cancelled/errored agent that streamed partial text shows it in the live tab (FanoutTerminalState), but serialization wrote every non-succeeded slot as a body-less 'did not answer' marker, so autosave/reload lost output the user saw (and an all-terminal turn could reload with no agent bodies). Persist non-empty terminal text (with its status, and the error reason in an error attr) so reload matches the live tab; truly empty slots still get the body-less note. The summary still excludes terminal slots. Reverses the earlier deliberate drop-partial choice in favor of live/reload consistency. Fixes Codex P2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): summary must report the agents' answers, not answer as itself The prior instruction said 'give a direct answer to the question', so for a first-person question (e.g. 'what model are you') the summarizer answered about ITSELF instead of synthesizing the agents' answers. Reframe it as a third-party reconciler: it summarizes what THE AGENTS said, never substitutes its own identity/opinion, attributes claims when it matters, and stays concise. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): restore whole-composite copy/insert via the Summary tab The minimal copy redesign dropped any way to copy the whole turn. Bring it back without the old redundant top row: the single context-aware action bar now copies/inserts the WHOLE composite on the Summary tab (the default, so copy-all is one click) and just the selected agent on an agent tab. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): escape > in fan-out marker attributes Backend-controlled error text (now persisted in an error= marker attribute) can contain >, which terminated the agent marker early because parseFanoutComposite matches with agent[^>]*, corrupting the reloaded turn. Neutralize > (like the existing -- and " escapes) so such markers always parse. Fixes Codex P2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): replay the answers when a fan-out summary is unavailable When the summary failed/cancelled but agents answered, only the generic 'summary unavailable' note was buffered for single-agent continuity, so a follow-up like 'what did they say?' lost the per-agent answers the user saw and that are persisted in the composite. Replay the readable answers (renderFanoutComposite) in that case; a real summary is still preferred when present. Fixes Codex P2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): persist partial answers on cancel; tighten summary prompt boundaries Two fixes to fan-out content fidelity and the summary: - Persist-on-cancel (Codex P2): the content gate only counted summary/succeeded text, so a turn cancelled mid-stream (partials, no done slot) saved nothing even though serializeFanoutComposite can now persist partials. Count any non-empty slot text, and include terminal partial text in renderFanoutComposite so the composite, copy-all, and single-agent replay all match the live tabs. - Summary boundaries: the summarizer kept adding meta ('the agents converge…', 'only environment scaffolding', 'Note: …'). The instruction now forbids mentioning agent counts, who did/didn't answer, scaffolding, or any caveats, and buildSummaryUserPrompt no longer tells it to 'note this gap' — failed agents are omitted from the prompt entirely so it can't mention them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): use the sigma thinking spinner in the fan-out response area The body's thinking/streaming/waiting/writing states used a generic Loader2 ring; swap them to the app's animated sigma (Σ) CopilotSpinner so multi-agent matches the thinking/reasoning indicator everywhere else. The tab status dots keep their compact icons. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): bound fan-out per-agent setup against cancel and timeout Only the per-agent prompt was raced against cancel + the timeout; the SETUP phase (ensureBackendForFanout, newSession, read-only-mode/default-model round-trips) was not, so a cold or wedged backend whose session/new never returned could hang the whole fan-out turn with Stop unable to settle it. Generalize runPromptWithTimeout into runAttemptWithTimeout, bounding the ENTIRE attempt (setup + prompt) by one FANOUT_AGENT_TIMEOUT_MS deadline + the abort signal. A wedged setup now settles promptly: cancelled on abort, errored on timeout, without awaiting the stuck promise. Teardown stays in the attempt's single finally so a late-resolving newSession is still cancelled (no orphan sub-session); timer + abort listener are cleared on every settle path; the prompt-phase cancel grace (FANOUT_CANCEL_GRACE_MS) and trailing-chunk handling are preserved (and now correctly skipped on the timeout path too). Fixes Codex P2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): high-quality, strictly-attributed fan-out summary prompt The summarizer kept answering in the first person ('I am powered by…') because the agents' answers are first-person and it mirrored them. Rewrite the summary instruction as a structured spec: strict THIRD-PERSON attribution by agent name (explicitly converting the agents' 'I/my' into '<agent> says it…', no sentence may begin with 'I'); per-agent / agreements / disagreements sections for two or more answers, degrading to a short attributed summary for a single agent; and the existing scope guards (ignore scaffolding, never mention counts or non-answerers). Uses generic placeholder examples, no hardcoded model/agent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): settle a pre-aborted fan-out turn before launching agents If Stop is pressed during the async work before fan-out dispatch (license re-verify, web-tab serialization), the signal is already aborted when runAttemptWithTimeout runs; the abort listener it arms never fires for an already-fired signal, so agents still opened sub-sessions and dispatched read-only prompts, only marked cancelled after they finished/timed out. Check signal.aborted right after arming the listener and settle via the same cancel path without starting the attempt, so no sub-session opens and no prompt runs after Stop. Fixes Codex P2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): replay answers after an interrupted/partial fan-out summary The continuity buffer preferred summary.text whenever it was non-empty, but a summary that streamed partial text then was cancelled or errored is incomplete — runSummary's finally forces status to 'done' regardless, so status couldn't tell them apart, and a follow-up like 'what did they say?' got only the partial summary while the full answers sat unused in the composite. Add FanoutSummary.complete (live-only), set by runSummary ONLY on a clean finish (or the all-failed terminal). The replay trusts summary.text only when complete; otherwise it falls back to the composite (answers), like the no-summary path. Fixes Codex P2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): don't dispatch a fan-out prompt after the race already settled If abort/timeout won while the attempt was still in setup, the detached attempt later reached prompt() anyway — onPrompt only cancelled AFTER the backend query started, so a timed-out/cancelled agent could still run a read-only prompt in the background and, for a main-agent timeout, overlap the later summary on the same backend. Check raceSettled() before dispatch and just tear down the late sub-session instead. Fixes Codex P2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): strip fan-out serialization markers from conversation history A fan-out turn's persisted message body is the composite — HTML-comment section markers plus marker-escaped content. buildConversationHistoryBlock fed that raw into <conversation_history>, so a later @agent turn saw hidden markers, status/error attributes, and the escape sentinel that were never visible to the user. Render the clean composite from the live/parsed fanout turn instead (falling back to parsing the body); non-fan-out messages are unchanged. Fixes Codex P2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * feat(agent-mode): adaptive two-mode fan-out summary (answer vs deliverable) The agreements/disagreements form only suits convergent questions. When the agents each produce an ALTERNATIVE artifact (a rewrite, draft, message, code, plan), those are options to choose between, not claims to reconcile. The summary prompt now first picks a mode: ANSWER mode keeps the each-agent / agreements / disagreements reconciliation; DELIVERABLE mode instead lists what is DISTINCTIVE about each option (angle, tone, structure, who it suits) and gives a Recommendation (pick one, or which parts to merge), without reproducing the artifacts. The summarizer detects the mode itself, so the user need not flag it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): require the full composite wrapper before parsing a fan-out body parseFanoutComposite keyed on a mere marker substring, so a normal assistant answer that just MENTIONS the format (e.g. <!--copilot:...--> in a code block) parsed to a non-null empty turn; loadMessages then set message.fanout and reload rendered the fan-out card instead of the real answer, hiding it. Require the COMPLETE wrapper (a version-agnostic open marker AND the close marker) and reject an empty parse (no summary text and no agent sections). Fixes Codex P2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): cap agent answers fed into the summary prompt buildSummaryUserPrompt concatenated each succeeded answer in FULL, with no cap (unlike the persisted and history paths), so one huge answer (a long file excerpt or tool dump) could push the summary sub-session past its context window or time it out, leaving an otherwise-useful turn with no summary. Truncate each answer to FANOUT_SUMMARY_ANSWER_MAX_CHARS (tighter than the persisted cap, since several answers stack into one model input) with a marker. Fixes Codex P2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): serialize live fan-out state on autosave (no blank bubble) A fan-out turn streams into message.fanout; its composite body is written to message.message only at completion. If autosave fired mid-turn (a long turn outliving the debounce) and the user then reloaded/closed/crashed, formatChatContent serialized only the empty message.message, saving a blank assistant bubble even though per-agent text was visible. Serialize the live fanout (backend-id labels; the completed turn later overwrites the body with display-name labels) so the streamed answers survive an interrupted autosave. Fixes Codex P2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * docs(agent-mode): justify the multi-agent fan-out PR scope and design Add designdocs/MULTI_AGENT_FANOUT_ARCHITECTURE.md: a full accounting of the ~7.2k-line PR (54% tests, fan-out core isolated in a new folder, additive cross-cutting edits) plus the design rationale for routing, the composite message object, backward-compatible persistence, isolation/timeouts/caps, the adaptive summary prompt, and the two-layer paywall. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): stop fan-out sub-sessions leaking into Recent Chats Fan-out sub-sessions open a real newSession on the backend; opencode/codex persist it to disk and the native-discovery sweep then lists each as a phantom Recent Chats entry with an auto-generated title. proc.cancel ends the query but does not delete the session, so the prior "never persisted" comment was wrong for those backends. Tombstone each sub-session id on creation (via the existing index tombstone honored by mergeDiscoveredSessions) so the sweep skips it, even across restarts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * test(agent-mode): slim fan-out test suite to load-bearing cases Cut the multi-agent fan-out test suite from ~3,872 to ~1,800 added lines: keep one happy-path plus the load-bearing edges per module (serialize/parse +escape round-trip, routing, both paywall layers, one orchestrator success/error/cancel/ summary-unavailable, persistence non-blank, one continuity replay, read-only safety gating) and drop exhaustive timing/grace permutations and most pure-render UI assertions. Full suite green (3,844). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * refactor(agent-mode): trim verbose comments on fan-out code to STYLE_GUIDE minimum Comment-only pass over the new fan-out/paywall production code: condense or drop multi-line JSDoc that restated the code and decision-ref narration, keeping the non-obvious WHY notes (escape correctness, cancel/settle ordering, read-only invariants, referential stability). Pure comment changes; no code/behavior change. Prod added lines 3,326 -> 2,886. Suite green (3,844). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * refactor(agent-mode): drop tool/plan from fan-out history, trim history comments, slim doc Phase 4 (behavior-preserving golf): the fan-out conversation-history block no longer renders agent-internal tool-call/plan cards (all USER context kept: prose, image markers, selections/notes/web-tabs). Trim the history-machinery comments to the STYLE_GUIDE minimum and inline a single-use helper. Slim the architecture doc to a concise reference. The orchestrator trailing-chunk grace is intentionally left intact (it captures answer text some backends flush after prompt resolve). Suite green (3,844). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * refactor(agent-mode): trim plusUtils multi-agent entitlement comments Comment-only condensation of the multi-agent paywall helpers' JSDoc (left over from the Phase 3 comment pass). No code or behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): close fan-out sub-session sweep race; restore sentinel test; drop dead helper - Recent Chats leak: the index tombstone is async/fire-and-forget, so a native listSessions sweep racing the in-flight turn could still surface an ephemeral fan-out sub-session. Also skip currently-registered read-only sub-sessions in the sweep (closes the window before the tombstone lands). - Restore the serializer collision test's PUA escape sentinel (was reduced to an empty string during the test slim, dropping coverage of the raw-sentinel path). - Remove the now-dead collapseFanoutTurnToSummaryText helper and FANOUT_SUMMARY_UNAVAILABLE (composite persistence superseded them; their comments wrongly said "summary only"). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): deny unknown MCP tools in read-only fan-out turns An MCP tool whose name isn't a known built-in derives to kind `other`, which the read-only gate otherwise allows — so a third-party mutating tool like mcp__filesystem__write_file could run inside a read-only QA sub-session, breaking the no-writes guarantee. Fail safe: deny unknown MCP tools (mcpServer present + kind `other`) in read-only sessions; known-classified MCP reads still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * docs(agent-mode): document fan-out history contract + deferred design debt Record the intentional history tradeoff (prose + user context, no tool/plan cards) and the read-only MCP fail-safe, and capture the deferred design follow-ups (host sub-session primitive, fanoutTypes split, explicit summary status enum) so they aren't lost. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): deny unknown ACP MCP tools in read-only; fingerprint live fan-out in autosave - Read-only consistency: the ACP permission prompter (opencode/codex) allowed kind `other`, so an unknown/MCP tool could run in a read-only fan-out turn — the same hole already closed on the Claude SDK bridge. Deny `other` there too (read/search/fetch/think/switch_mode still pass). - Autosave de-dupe keyed only on the last message's text, which stays empty for an in-flight fan-out turn, so mid-stream saves were skipped and a crash kept only the first partial snapshot. Fold a fingerprint of the live fan-out slots (per-answer status+length, summary status+length) into the signature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG * fix(agent-mode): anchor @-mentions to the active session, not a cold-starting backend The fan-out mention anchor (mainAgentId) preferred getStartingBackendId() over the active session's backend. While another backend cold-starts in a different tab, startingBackendId is set before the new session becomes active, so a queued prompt from the visible chat (e.g. Claude) that mentions the starting backend (e.g. @opencode) saw mainAgentId === opencode — the degenerate single-answerer case — and silently flushed back as a single-agent turn instead of fanning out. Anchor to the active session's backend when one exists; fall back to the starting backend only for the initial no-session startup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Swdw8vKE3zczYJFzAboWG --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
16fedfbae1
|
Revert "feat(agent-mode): add miyo-chat-search skill for searching saved AI chats (#2621)" (#2625)
This reverts commit
|
||
|
|
8db705f147
|
feat(agent-mode): add miyo-chat-search skill for searching saved AI chats (#2621)
Splits AI-chat-history search into its own builtin skill so each Miyo skill has a focused "when to use" signal: miyo-search stays vault-wide, while the new miyo-chat-search targets the user's synced ChatGPT/Claude conversations. The chat skill ships a two-step workflow: miyo-folders.* lists indexed folders so the agent can identify chat folders by "origin": "chat_sync" (matched on origin, not hardcoded names), then miyo-chat-search.* runs a search scoped to them via repeatable --path filters forwarded to `miyo search`. Binary resolution is shared across all wrappers. miyo-search is restored byte-identical to its prior vault-only form (version unchanged); the host now seeds and prunes both Miyo skills together, gated on Miyo being in use. Co-authored-by: Wenzheng Jiang <wenzhengjiang@Wenzhengs-Mac-mini.local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
87317ed263
|
fix(agent-mode): bundle the frame log as .txt so GitHub accepts it (#2624)
GitHub's issue attachment allowlist rejects `.ndjson`, so the diagnostic frame log could not be dragged into a report. Save the bundled copy as `acp-frames.ndjson.txt` (an allowed type, keeping `.ndjson` in the name as a format hint); content is unchanged. The loose files stay exactly as the user reviews them, so editing/redacting them before attaching works as the modal warning says. Fixes https://github.com/logancyang/obsidian-copilot-preview/issues/155. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f75c2ebbee
|
feat(agent-mode): one-click "Report an Issue" flow with screenshot + frame log (#2614)
* feat(agent-mode): one-click "Report an Issue" flow with screenshot + frame log
Reporting an Agent Mode bug was high-friction: the diagnostic frame log
defaulted off (so it usually wasn't capturing when a problem hit), and there
was no in-product path to assemble a report.
- Default `agentMode.debugFullFrames` to ON for new installs so the frame log
is already capturing when a bug occurs. The existing sanitize migration
preserves an explicit prior choice (a user who turned it off stays off).
- Add a "Report an Issue" button to the Agent Mode control bar. It opens a
modal for a note, captures a screenshot of the chat surface (Electron
capturePage, popout-aware, degrades gracefully), bundles it with the current
frame log into a timestamped folder, reveals the folder, and opens a
prefilled GitHub issue.
- Optionally include the OpenCode log when that backend is active, and show an
in-flow privacy disclosure shown only when the user chooses to share.
Desktop-only; mobile and capture failures degrade to a report without a
screenshot. Adds unit tests for the new default, the preserve-explicit-choice
migration, the bundle assembler, and the opencode-log locator.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agent-mode): make opencode log attachment opt-in by default
The Report-issue flow bundles opencode's newest *global* log, which may
belong to an unrelated CLI/Desktop session for another project. Defaulting
the checkbox to checked could silently attach that log, exposing unrelated
prompts and tool output. Default it to unchecked so the user opts in.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agent-mode): cap report issue URL length and document the flow
- buildReportIssueUrl: truncate the prefilled body so the assembled URL
stays under Electron's ~2081-char openExternal limit on Windows. Without
this, a long note made openExternal reject silently while the success
notice still claimed the issue page opened. The full report is preserved
in report.md on disk, and the truncated body points the user there.
- Document the Report an Issue flow (bundle contents, drag-drop attach,
privacy note, opt-in OpenCode log) in docs/agent-mode-and-tools.md per
DOCS_GUIDE.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agent-mode): skip frame log in report when logging is disabled
If a user turns off "Log Full Agent Mode Frames" but a stale
acp-frames.ndjson still exists in the temp dir, the report assembler would
copy that old file into the bundle, leaking plaintext prompts/tool output
despite logging being opted out. Only bundle the frame log when
debugFullFrames is currently enabled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agent-mode): file reports to public repo; clarify report UX and settings
Addresses review feedback on the Report an Issue flow:
- File user reports to the PUBLIC logancyang/obsidian-copilot repo (label
"bug"), never the private preview repo, which users can't see.
- Report modal: state plainly that the screenshot is of the Agent Mode chat
pane (not the whole screen), spell out what "Prepare report" does (save
files to a folder, open it, open a prefilled GitHub issue to attach them),
and replace the obscure muted disclaimer with a prominent warning callout.
- Advanced settings: split Agent Mode logging into its own "Agent Mode
debugging" section with plain-language copy (renamed to "Keep an Agent Mode
activity log" / "Agent Mode activity log file"), and clarify that the legacy
Debug Mode / Create Log File tools are for the regular chat, not Agent Mode.
- Update docs to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(agent-mode): move Report an Issue to the Advanced settings section
Per UX feedback, the entry point moves out of the agent chat control bar into
Settings → Advanced → Agent Mode debugging, alongside the activity-log
controls it relates to.
- Remove the bug-icon button and its wiring from AgentChatControls/AgentHome.
- Add a "Report an Issue" button to the Agent Mode debugging settings section.
- ReportIssueModal now resolves its screenshot target lazily via
resolveCaptureTarget(): the settings caller closes the Settings window and
reveals the agent pane first, so the screenshot is still the chat surface
(not the dialog). No agent pane open -> screenshot is skipped gracefully.
- Export ReportIssueModal from the agentMode barrel so host code can reach it
without crossing the ui boundary.
- Update docs to point at the new location.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agent-mode): honor opencode env overrides when finding its log
OpencodeBackend spawns opencode with `{ ...process.env, ...envOverrides }`, so a
user who relocates opencode's data dir via XDG_DATA_HOME/HOME overrides has its
logs written there. The report's opencode-log lookup only consulted ambient
process.env/homedir, so it could attach an unrelated global log or miss the
active session's. Resolve the log path from the same merged env + HOME override.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agent-mode): report uses active session backend; gate import on desktop
Two review fixes for the settings-based Report an Issue button:
- Read the backend from the active Agent Mode session, not the persisted
default. Switching tabs across backends changes the active session without
touching settings.agentMode.activeBackend, so the modal could hide/show the
OpenCode-log option for the wrong tab and name the wrong backend in the env
block.
- Check isDesktopRuntime() before importing the @/agentMode barrel. On mobile
the barrel evaluates Node-only modules; importing before the modal's own
desktop guard could reject during module load instead of showing the
desktop-only notice. Mirrors the existing frame-log buttons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
decca79498
|
fix(skills): make the built-in miyo-search skill reliably invokable (#2613)
* fix(skills): make the built-in miyo-search skill reliably invokable Smaller/local models were giving up on miyo-search: the prose-only skill told the agent to try `miyo` on PATH first, then fall back to the absolute path across OS variants. In Obsidian-launched shells PATH is reduced, so the first attempt fails and the model concludes the tool is unavailable instead of recovering. (A user's hand-written custom skill that calls `~/.miyo/bin/miyo search` directly works every time — isolating the problem to the built-in skill's construction.) Ship a runnable wrapper (`miyo-search.sh` + Node `miyo-search.mjs`, mirroring the Plus relay skills) that resolves the binary itself — absolute install path first (`~/.miyo/bin/miyo`, `%LOCALAPPDATA%\Miyo\bin\miyo\miyo.exe`), PATH as a fallback — and runs one `miyo search … --json`. The SKILL.md "How to run" is now a single command; binary/OS branching lives in the script. Bumped the builtin version (1 → 2) so existing installs re-seed. Verified both scripts against the live miyo binary under a reduced PATH (the exact failure scenario): both resolve the absolute path and return valid JSON. Closes logancyang/obsidian-copilot-preview#161 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): honor custom Miyo URL and resolve Windows path in the sh wrapper Addresses two Codex review points on the miyo-search wrapper: - Custom/remote Miyo: shouldUseMiyo seeds the skill for users with a custom server URL (incl. mobile), but the wrapper ran a bare local `miyo search`. Inject MIYO_URL (which the CLI reads) from the configured custom URL in buildCopilotPlusEnv, so the agent transparently targets the right service — no script change needed. Independent of Plus (self-host can use Miyo). - Windows + Git Bash prefers the .sh, but it only checked the POSIX install path; add cygpath-based resolution of %LOCALAPPDATA%\Miyo\bin\miyo\miyo.exe so it no longer falsely reports "not installed" there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(skills): steer Windows to the Node miyo-search wrapper On Windows the `.mjs` invokes the Miyo executable directly (no POSIX shell, native %LOCALAPPDATA% resolution), which is more reliable than `.sh` (only runs under Git Bash). SKILL.md now tells the agent to prefer sh on macOS/Linux and node on Windows; either still works on any platform as a fallback. The .sh cygpath branch stays as defense-in-depth for Git Bash users. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): restart backends when the Miyo server URL changes MIYO_URL is injected into the managed spawn env, so it's only read on a fresh spawn. A mid-session change to the Remote Miyo Server URL previously only hit restartSystemPromptAffected(), a no-op while Miyo stays enabled (the prompt key doesn't include the URL), so a running codex/opencode subprocess kept querying the old server. Treat a miyoServerUrl change like a Plus-license change and restart every backend so the new URL takes effect without a reload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): ship a Windows .cmd wrapper so Miyo works without sh or node A managed-opencode Windows session can be ready from the downloaded native binary while neither Git Bash (`sh`) nor `node` is on PATH. The v2 reseed left only `.sh` + `.mjs`, so those users were told to install Node even though Miyo installs a runnable `%LOCALAPPDATA%\Miyo\bin\miyo\miyo.exe` (the v1 prose could call it directly). Add `miyo-search.cmd` — runnable from cmd or PowerShell with no extra runtime — which resolves the exe under %LOCALAPPDATA% then PATH and runs `miyo search … --json`. SKILL.md now documents the sh → node → cmd chain. The version stays 2; the seeder's missing-support-file check reseeds existing v2 installs to pick up the new script. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(skills): drop the Node miyo wrapper — sh (POSIX) + cmd (Windows) suffice The Windows .cmd (always present, calls the local miyo.exe directly) is a strictly better Windows fallback than the Node .mjs, which made Node redundant for this skill. Ship exactly two OS-matched wrappers and let the SKILL.md map OS → script with no runtime choice: - macOS/Linux → miyo-search.sh (POSIX-only; the cygpath/Windows-path branch is gone since Windows no longer uses .sh) - Windows → miyo-search.cmd Removes the Node dependency, the cygpath path-translation, and the 3-way runtime selection — fewer moving parts and failure modes. MIYO_URL env injection and the restart-on-URL-change still apply to both wrappers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): document the PowerShell call operator for the cmd wrapper A bare quoted path is a string in PowerShell — it won't execute, and vault paths often contain spaces, so an agent running the example from PowerShell would stop before reaching miyo-search.cmd. Show the call-operator form (& "...\miyo-search.cmd" "...") and note cmd omits the &. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(skills): don't claim miyo search is always local With MIYO_URL injection a configured Remote Miyo Server URL sends the search to that server, so "All calls are local — no network" was inaccurate and could make the agent misrepresent where a query goes. Reword: searches go only to the user's own Miyo service — the local app by default, or their configured remote server — never a third-party API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f55514bdb3
|
Add Copilot Plus web-fetch skill and stop hard-blocking free users on web tools (#2612)
* feat(agent-mode): add Copilot Plus web-fetch skill and stop hard-blocking free users on web tools Adds a `copilot-web-fetch` builtin agent skill wrapping the firecrawl-backed `/url4llm` relay endpoint, so Copilot Plus users' agent-mode page fetches use the higher-quality Plus fetch instead of the agent's own WebFetch. Reworks the shared skill-script preamble so a missing or invalid license no longer forces an "upgrade to Copilot Plus" reply. When no license is configured (free user) the script now tells the agent to silently fall back to its own built-in web tools and never refuse, appending a gentle upsell only ~1 in 4 runs (keyed off the process id) so the nudge stays occasional. A 401/403 from the relay (lapsed Plus) gets a distinct, still-non-blocking renewal note. Updates the system-prompt steering with a web-fetch bullet and softened, never-block fallback wording. Bumps all relay skill versions so seeded copies refresh. Closes logancyang/obsidian-copilot-preview#160 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): genericize Plus-inactive fallback message and add license-key CTA The shared no-license/invalid-license message is embedded in copilot-read-pdf as well as the web skills, so telling the agent to "use your own built-in web tools" misdirected the recovery path for a local PDF (per Codex review). Make the fallback generic: complete the request with your own equivalent built-in tools if you have them, otherwise tell the user it's unavailable. Mirror the same wording in the system-prompt steering. Also add a clear, concise "get a license key ... to access them" call to action to the occasional upsell. * fix(agent-mode): route non-license web-fetch relay failures to the native tool Per Codex review: an active Plus user fetching a page only got the fallback for missing/disabled/inactive-license cases. A non-401/403 relay failure (the page can't be fetched, a transient 5xx, or the relay is unreachable) emitted a bare "Request failed (HTTP ...)" and dead-ended — even though the steering now routes page reads away from the agent's native WebFetch. Both the sh and node relay error paths now append a fallback hint telling the agent to use its own equivalent tool, and the system-prompt steering's fallback clause covers "fails for this particular request" in addition to the unavailable cases. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
367aedddaf
|
feat(agent-mode): warn about privacy terms on free opencode models (#2611)
* feat(agent-mode): warn about privacy terms on free opencode models Free opencode models (big pickle, grok-code, the *-free variants) are routed through third-party providers whose terms commonly allow logging or training on prompts, but the model picker gave no signal. Each free model now shows a warning icon + privacy tooltip in the Agent Mode picker. "Free" is derived generically from the models.dev catalog cost (input === 0 && output === 0), so any future free model is flagged with no hardcoded id list. To make cost reach agent-discovered opencode rows, AgentSetupApi's catalog lookup now also keys models by the provider-qualified `<provider>/<model>` form: opencode reports prefixed wire ids (`opencode/big-pickle`) while the catalog keys models bare (`big-pickle`), so the prior bare-only lookup missed and never snapshotted cost. The resolved `ModelInfo.id` is pinned to the requested wire id so enriched rows keep their prefixed identity (model selection / reconcile unaffected). Reconcile also refreshes `cost` in place, so already-enrolled rows backfill the free signal on the next discovery sync (runs on app load) with no migration code. Closes logancyang/obsidian-copilot-preview#159 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-mode): show free-model warning in the Agents settings model list Extend the free-model privacy warning to the per-model toggle rows in Settings -> Agents (the opencode model enable list), not just the chat model picker. Same generic cost-based signal. Factor the shared pieces out: `isFreeModelCost` (catalog-cost predicate) into modelManagement, and the warning copy into components/ui/freeModelWarning so both the picker and the settings list use one string. The settings row carries an `isFree` flag computed in `toRow`, and ModelEnableList renders the same AlertTriangle + HelpTooltip beside the label. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): exclude self-hosted/local models from the free-model warning Local endpoints (Ollama, LM Studio) often match a zero-cost catalog entry, so they were incorrectly flagged with the free-model privacy warning. The warning is about prompts leaving the machine to a third party, which doesn't apply to self-hosted models. Gate the free flag on `!isSelfHostedProvider(provider)` at both computation sites (opencode picker entries and the settings model rows). opencode's agent-origin native provider has no baseUrl, so opencode Zen free models (big pickle, grok-code) stay flagged; a localhost baseUrl is excluded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(agent-mode): mark free models by opencode Zen membership, not models.dev cost Switch the free-model warning to opencode's own signal: a model is flagged when its wire-id prefix is `opencode/` (opencode Zen, opencode's self-hosted free tier), straight from what opencode reports. This drops the entire models.dev cost pipeline (catalog isFree, AgentSetupApi qualified-key lookup + cost reconcile, self-hosted gating) added earlier. Why: opencode's ACP report carries no cost/free flag, and models.dev cost==0 caught local open models too (LM Studio gpt-oss etc. showed the warning). opencode Zen membership is the precise, opencode-native signal — it marks Big Pickle and the *-Free Zen models and excludes the lmstudio/ sub-group and every other backend. Also fix the hover error (`isShown is not a function`): replace the Radix HelpTooltip + aria-label on the icon with a native `title` attribute via a shared FreeModelWarningIcon, used by both the chat picker and settings list. Caveat: pure membership would also flag a paid Zen model if opencode ever reports one; the free tier is all that surfaces today. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): show free-model tooltip instantly on hover The native `title` attribute has a fixed ~1s delay. Switch FreeModelWarningIcon to the Radix HelpTooltip with delayDuration={0} so it appears immediately. No aria-label on the icon (that path triggers Obsidian's native tooltip and the `isShown` error); the tooltip content carries the message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(agent-mode): trim the free-model warning implementation - Inline the warning string into FreeModelWarningIcon and drop the separate freeModelWarning.ts module (it had a single consumer). - Drop the unused OPENCODE_ZEN_PROVIDER_ID barrel export. The opencode-Zen flag still originates in the opencode backend (opencodeEnabledModelEntries / toRow) and is carried to the UI as a plain `isFree` boolean: the eslint-boundaries `ui -> backend` rule forbids the UI from importing the opencode detection helper directly, so the flag-on-entry plumbing is the boundary-correct shape, not incidental. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
09cb6bb136
|
fix(agent-mode): restore user message text when a recent chat has image attachments (#2610)
A native Claude chat reopened from recent chats rebuilds its transcript from the on-disk CLI .jsonl. parseClaudeTranscript only accepted user records with string content, treating all array content as a tool_result and skipping it. A user message with an attached image is logged with array content (text + image blocks), so the entire user turn — including its text — was dropped, leaving an orphaned assistant reply. Now array user content is kept unless it carries a tool_result block: its text blocks are restored and images are omitted from the display. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4c3044f4b5
|
fix(agent-mode): show attached images in the posted user message (#2609)
* fix(agent-mode): show attached images in the posted user message
Attached images were delivered to the model but never appeared in the posted
user bubble: sendPrompt stored the user message without a `content` field and
forwarded the image promptContent blocks only to the backend. The renderer
(ChatSingleMessage) shows images only from `content` entries shaped
`{ type: "image_url", image_url: { url } }`.
Project the outgoing image promptContent blocks into that display shape
(base64 -> data URL) and store them as the user message's `content`, leaving
the backend promptContent path untouched. Markdown autosave still serializes
only message text, so saved notes are not bloated with base64.
Closes logancyang/obsidian-copilot-preview#157
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agent-mode): keep prompt text visible alongside attached images
Address Codex review: when a user message has a content array, ChatSingleMessage
renders text only from a `type: "text"` content item. buildUserDisplayContent
emitted image_url entries only, so a mixed text+image prompt hid the user's
text. Include the prompt text as the first content entry (text-first, then
images, mirroring the legacy chat composer); image-only messages still omit the
text entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
894c21d056
|
fix: unblock eligible self-host users' own web search and YouTube tools (#2606)
* fix: unblock eligible self-host users' own web search and YouTube tools Self-host tools (web search, YouTube transcription) require isSelfHostModeValid() = enableSelfHostMode toggle AND a valid validation receipt (selfHostModeValidatedAt + selfHostValidationCount). The toggle and receipt are stored separately and could disagree: the legacy enableSelfHostedSearch -> enableSelfHostMode migration in sanitizeSettings seeded the toggle but never the receipt, so isSelfHostAccessValid() returned false on the selfHostModeValidatedAt == null check. As a result YouTube hard-returned "requires a Copilot Plus subscription" and web search fell through to the Brevilabs backend (MissingPlusLicenseError), even for eligible Believer/Supporter users with their own keys. Defensively seed the validation receipt in sanitizeSettings: whenever the sanitized result has enableSelfHostMode === true but selfHostModeValidatedAt == null, set selfHostModeValidatedAt = Date.now() and selfHostValidationCount = max(existing || 0, 1). This is placed after the legacy migration so the migrated toggle value is already applied, and it covers both the migration gap and any "toggle on but receipt null" state. We never seed when the toggle is off, so users who never enabled self-host mode are not granted access, and periodic refreshSelfHostModeValidation() still disables/clears for genuinely ineligible plans. Scope is the tools path only; agent-mode self-host (src/agentMode/skills) is tracked separately in #146. Adds unit tests for the sanitize seeding (migration + toggle-on-null- receipt + the off-toggle guard + no-overwrite) and for the isSelfHostModeValid()/isSelfHostAccessValid() paths in plusUtils. Closes logancyang/obsidian-copilot-preview#145 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(self-host): gate tools on the toggle, drop validation-receipt seeding Per preview-phase policy (no migration/temporary logic), replace the defensive receipt seeding in sanitizeSettings with a toggle-only gate. isSelfHostModeValid() now returns enableSelfHostMode === true. The startup refreshSelfHostModeValidation() still disables the toggle for genuinely ineligible plans, so the toggle is a sufficient gate. This removes the buggy seeding that re-stamped Date.now() on every restart (never persisted, so the 15-day grace renewed indefinitely) instead of persisting it. isSelfHostAccessValid() and the grace/permanent machinery are left intact since miyoUtils still depends on them; ripping those out is a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e1abb9259b
|
fix(agent-mode): correct session tab titles for codex and Claude Code (#2607)
* fix(agent-mode): correct session tab titles for codex and Claude Code
codex named sessions after the raw first prompt, leaking the injected
<copilot-context> envelope into the tab title, and Claude Code fell back to the
backend display name ("Claude") because its SDK exposes no title API. Only
opencode summarizes its own titles.
Add a required `summarizesSessionTitle` flag to BackendDescriptor (opencode:
true, codex/claude: false). Backends that summarize keep trusting their
session/list and pushed session_info_update titles; the rest now derive the tab
label client-side from the user's first visible message (reusing
deriveChatTitleFromMessages) and ignore backend-provided titles. The derived
title is agent-sourced, so a user Rename still wins and the first message's
title is stable across later turns.
Closes logancyang/obsidian-copilot-preview#113
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agent-mode): skip native title sweep for non-summarizing backends
Address Codex review: sweepNativeSessions() merged listSessions titles into the
recent-chats index for every backend, so a codex session whose first prompt
carried attached context could still be discovered (and reopened) with the raw
<copilot-context> title, bypassing the AgentSession-level gating.
Gate the sweep on summarizesSessionTitle too: non-summarizing backends (codex,
Claude Code) are skipped entirely since the sweep has no transcript to derive a
clean title from. Their sessions are still indexed via flushIndexTouch, which
uses the client-derived title.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
641bfadb7a
|
fix(agent-mode): coalesce streamed-token re-renders (#2602)
Agent Mode rebuilt the entire chat transcript on every streamed-token notification: AgentMessageStore.getDisplayMessages() mapped over and re-adapted every visible message into fresh objects each call, so the memoized AgentChatMessages component (and every AgentTrail under it) re-rendered once per token. On a fast token stream that drove one full main-thread re-render per token, causing lag between Enter and the user message appearing and stuttering of the CopilotSpinner sigma dots. Streamed-token notifications were already rAF-coalesced at the AgentSession boundary (scheduleNotifyMessages), with discrete events (send, turn-complete, error, permission) flushing immediately via notifyMessages(). The remaining cost was the per-notification rebuild in the store. Memoize getDisplayMessages() so identity is stable: - Each stored message carries a `version` bumped on every in-place mutation; the adapted AgentChatMessage is cached by version, so an unchanged message keeps the SAME object reference across ticks and its memoized React component skips re-rendering. - The returned array is itself cached and only rebuilt after a mutation, so an idle notification returns the exact same reference and the top-level messages memo bails out without diffing. Streaming correctness is preserved: the store still mutates synchronously so no tokens are dropped, and the trailing turn-complete notification still flushes immediately (notifyMessages cancels any pending streaming rAF), so the message always settles to its complete final state. Tests: store-level memoization (stable identity for unchanged messages, re-adaptation after each mutation kind, no-op upsert preserves identity, cache eviction on delete/truncate) and session-level coalescing (burst-of-chunks collapses to one notification per frame; the trailing turn-complete notification is always delivered and cancels the stale pending frame). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
893406c9bd
|
fix(agent-mode): stop tool-path clicks from creating phantom vault folders (#2599)
Clicking the path on a Read/Edit tool-call card for a file outside the vault (e.g. /tmp/chap1.txt) called openLinkText directly, so Obsidian dropped the leading slash and materialized a phantom note + folder chain inside the vault. The markdown-link handler already guarded against this; extract that logic into a shared openVaultPath helper and route both the tool-call card and the rendered markdown links through it, so every agent-response click target opens outside-vault absolutes in the OS app. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
95d4fe380f
|
fix(agent-mode): show the invoked skill name in the tool-call render (#2598)
The Claude harness logs a skill invocation as a tool_use named "Skill" with the real skill in input.skill, but toolSummaries had no Skill entry so it fell through to the generic "Skill" label and dropped the name. Add a Skill summary (e.g. "Ran skill copilot-read-pdf", args in the expanded card) plus a Sparkles icon, following the vendor-registry pattern. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
714eb611c2
|
fix(agent-mode): translate slash-command syntax to agent chat syntax (#2596)
* fix(agent-mode): translate slash-command syntax to agent chat syntax
Custom slash-commands in Agent Mode ran through `processPrompt`, which
inlined the full active-note body as `<variable name="activeNote">…</variable>`
— so a one-line command shipped the whole note (and fought Agent Mode's
reference-by-path model). Replace that call with a purpose-built
`translateCommandToAgentText` that converts command template tokens into the
syntax a user would type in chat: `{activeNote}`/`{[[Note]]}` become
`[[wikilink]]` references, `{}`/`{copilot-selection}` inline the selection, and
`{#tag}`/`{folder}` are handed to the agent bare (it resolves them via its own
search/read tools over the vault). Notes are never inlined; the agent reads them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix active note token casing in agent commands
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
10441fa056
|
fix(agent-mode): recover from stalled Claude SDK streams instead of hanging (#2595)
* fix(agent-mode): recover from a stalled Claude SDK stream instead of hanging the turn A half-open/dropped streaming response from the Claude Agent SDK could stop yielding mid-message without ever emitting a terminal `result`. The turn loop in `ClaudeSdkBackendProcess.prompt()` only ends on `result`, so it parked forever: the turn stayed "running" indefinitely and the user had to interrupt and re-prompt to recover (observed after approving a plan, mid tool-call streaming). Wrap the SDK stream in `guardStreamStall`, a small async-generator that arms an idle timer only while an assistant message is actively streaming (after the first content block, until `message_stop`) so tool runs, first-token latency, and permission waits aren't timed. On a mid-stream stall it aborts the query (via a wired `AbortController`) and throws a clear error the session surfaces as the turn's in-chat error, plus a transient `Notice` toast (wired at the descriptor layer so `sdk/` stays UI-free). Adds focused unit tests for the guard and integration coverage in the backend. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): surface stream-stall error in chat only, drop the Notice toast The stall error already renders inline as the turn's error — the thrown `STREAM_STALL_MESSAGE` flows through `AgentSession`'s catch into `markMessageError`, which appends `**Error:** …` to the assistant turn — so the extra `Notice` toast was redundant. Remove the `notifyUser` hook and its descriptor wiring; `onStall` now just records the stall in the frame trace. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(agent-mode): scope the stream-stall guard to the Claude SDK by name Rename `streamStallGuard` → `sdkStreamStallGuard` (`guardSdkStreamStall`, `SDK_STREAM_STALL_*`) and document that it is Claude Agent SDK only. The guard keys off Anthropic streaming events (`content_block_*` / `message_*`); the ACP backends (opencode, codex) speak a different protocol — `session/update` notifications over JSON-RPC request/response — and are not covered by it. The `SDKMessage` type signature already prevents misuse on an ACP stream. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(agent-mode): trim the stream-stall guard header comment Drop the ACP-exclusion paragraph; the "Claude Agent SDK only" title line and the `SDKMessage` type signature already convey the scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
aa1a378652
|
fix(agent-mode): store recent-chats index outside the vault (device-local) (#2594)
* fix(agent-mode): store the recent-chats index outside the vault, device-local The agent session index mirrors device-local backend stores (~/.claude/projects, opencode's own session dirs) and records session ids that only resolve on the machine that created them. It was written into the vault at .obsidian/plugins/copilot/agent-chat-index.json, so whole-vault sync (iCloud/Syncthing/Dropbox) and Obsidian Sync would carry it to other devices — producing ghost entries that can't resume, plus write conflicts/churn on a file rewritten on a 500ms debounce. Move it out of the vault into the OS app-data dir the plugin already uses for device-local runtimes: ~/.obsidian-copilot/vaults/<vaultId>/agent-chat-index.json (vaultId = hash of the vault path, matching the keychain scheme), written via Node fs. This matches the codebase's existing precedent for device-local data (deviceProfiles, the opencode binary under ~/.obsidian-copilot). Agent Mode is desktop-only, so Node fs is fine and the barrel stays off the mobile load path (smoke test passes). Includes a one-time migration: on first load, if the old in-vault index exists and the new one does not, ingest it, write the new location, then remove the old (synced) file. Covered by new AgentSessionIndex migration tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: drop index migration, clean cutover to the home-dir location The previous in-vault index location (#2591) hasn't shipped to production, so there's no existing data to migrate. Remove the one-time migration entirely: the AgentSessionIndexMigration interface, the constructor param, the load-time legacy-read branch, the barrel's legacy-path wiring, and the migration tests. The index simply starts fresh at ~/.obsidian-copilot/vaults/<vaultId>/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d9fbbd180c
|
feat(settings): rename autosave label to 'Autosave Chat as Markdown' and always generate AI titles on save (#2593) | ||
|
|
23b5ca0054
|
fix: normalize provider base URL per consumer (legacy chat vs opencode) (#2589)
One stored Provider.baseUrl feeds two SDK stacks with opposite conventions: the legacy-chat LangChain clients for Google and Groq append their own version path (/v1beta, /openai/v1) and need the host-only form, while opencode's AI SDK treats baseURL as the complete prefix and needs the versioned models.dev form. Whichever form was stored, exactly one chat mode 404'd — a versioned URL doubled the path in legacy chat, a host-only URL dropped the version segment in agent chat (surfacing as "opencode finished the turn without returning any assistant text"). Normalize at each consumer boundary instead of fighting over the stored value: - chatModelManager strips a trailing version suffix before handing the URL to ChatGoogleGenerativeAI / ChatGroq (mirrors what listGoogleModels already did for verification). - buildOpencodeConfig drops the baseURL when it is just the provider's canonical endpoint in any spelling, letting opencode's registry default (always the correct versioned form) apply; genuine proxy/gateway overrides are still forwarded verbatim. Fixes logancyang/obsidian-copilot-preview#152 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f29768c69b
|
feat(agent-mode): show recent chats from native harness history regardless of markdown autosave (#2591)
* feat(agent-mode): show recent chats from native harness history regardless of markdown autosave Recent chats was built exclusively from autosaved agent__ markdown notes, so turning Autosave Chat off emptied history even though every harness still held a resumable session. Decouple the two concerns: - New plugin-local AgentSessionIndex (.obsidian/plugins/<id>/ agent-chat-index.json): write-through record of every session with user-visible messages (backendId, sessionId, title, createdAt, lastAccessedAt), independent of the autosaveChat setting. Works for all backends including Claude Code, whose SDK has no listSessions. - getChatHistoryItems() merges markdown items with index entries, de-duplicated on backendId + sessionId (frontmatter already stores the session id); merged rows take the fresher lastAccessedAt. - Opportunistic listSessions sweep of already-running backends (never spawns one), filtered to the vault cwd client-side, excluding the preloader probe session and untitled/placeholder sessions, bounded by a 1.5s timeout. - Native-only rows get a copilot-agent-session:// id; selecting one resumes through the existing loadSession/resumeSession path. Rename updates the index (and the live session label); delete tombstones the key so the entry can't resurrect from a native sweep, without deleting the harness's own session store. Deleting a markdown chat tombstones its native twin too. - Open-source-file on a native row explains there is no note instead of erroring. Closes #151 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: keep native chat id helpers off the mobile load path; adapter frontmatter fallback for hidden-folder dedup Two fixes from CI + review: - main.ts statically value-imported @/agentMode for the native chat id helpers, which the mobile-load smoke test correctly rejects (any Agent Mode value import drags Node-only modules into the mobile bundle). Move the dependency-free id helpers to @/utils/nativeChatId, which both main.ts and agentMode/session may import. - getChatHistoryItems read backendId/sessionId from the metadata cache only, so chats saved in hidden folders (never indexed by the cache) could not merge with their native twins and showed up twice. Reuse readSessionRefFromFile's adapter frontmatter fallback per file, with a regression test covering the hidden-folder case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: sweep warm preloader probes so native history shows on first Agent Home open The listSessions sweep only queried manager-owned backends, which exist only after a chat starts — so a fresh Agent Home open listed no native history at all. The preloader's warm probe subprocesses are already running for every installed backend at plugin load; sweep those too (read-only RPC, entries not consumed), so codex/opencode session stores surface immediately without paying any extra spawn. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: user renames survive native sweeps; match live sessions by backend+session pair Addresses two Codex review findings: - mergeDiscoveredSessions preferred the discovered (agent-store) title, so a plugin-side rename of a native entry was clobbered by the next listSessions sweep. Track titleSource on index entries (mirroring AgentSession.labelSource): setTitle marks user, the write-through path rides the live label's source, and discovered merges never overwrite a user title. - loadNativeSessionFromHistory matched live sessions by sessionId alone; a cross-backend id collision would focus the wrong tab. Require the (backendId, sessionId) pair. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: guard native rename's live-session lookup by backend+session pair Same cross-backend id-collision class as the loadNativeSessionFromHistory fix, at the updateChatTitle native branch: getSessionByBackendId matches by session id alone, so renaming a native entry could setLabel the wrong backend's live tab (and its index entry via the label autosave). Require live.backendId === native.backendId before applying the label. Regression test added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: derive native chat title from first user message when no agent title exists Claude Code's SDK exposes no session-title API, so CC native history rows all read 'Untitled chat'. Fall back to a title derived from the first user message (mirroring the markdown autosave filename/topic), recorded as an overridable agent-sourced title so an opencode/codex summarizer title still wins later and a user rename always wins. Makes autosave-off history consistent with autosave-on, where Claude chats already get a readable title from the first message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-mode): make Recent Chats a searchable management list; drop redundant New chat row The landing Recent Chats section was open-on-click preview rows with all management hidden behind a 'View all' popover, plus a 'New chat' row that duplicated what the landing already is. - Recent Chats is now a searchable, scrollable list whose rows manage chats in place: open, rename (inline), delete (two-step confirm), and open the source note. Same affordances as the chat history popover, no separate view-all step. - Go-to-file is conditional: shown only for markdown-saved chats; native (autosave-off) sessions have no note, so the action is hidden for them (gated on isNativeChatId). - Removed the 'New chat' row — the home surface is already a new chat, and the tab strip '+' / in-tab New Chat cover spawning a tab. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-mode): rebuild Claude native chat transcript from on-disk session jsonl Clicking a native (autosave-off) Claude chat in recent chats resumed the session but showed nothing: the Claude SDK has no transcript API and resumeSession returns no prior messages, so the chat opened empty and the UI stayed on the landing — looking like the click did nothing. Read the CLI's session record at <config>/projects/<encoded-cwd>/<sessionId>.jsonl (cwd encoded with every non-alphanumeric char -> '-', honoring CLAUDE_CONFIG_DIR) and parse it into display messages: user prompts (stripping the <user-message> context envelope) and assistant prose, skipping tool calls, tool results, meta, sidechain, and summary records. Wired via an optional BackendProcess.readPersistedTranscript that the manager calls on native resume when the session came back empty; ACP backends replay via loadSession and omit it. Best-effort — a missing/GC'd file degrades to the resumed-but-blank session rather than failing the open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): render loaded transcript immediately and reuse the empty landing tab Two history-open bugs: 1. Opening a chat showed a blank tab until you switched tabs and back. The manager mutated the store via store.loadMessages, which fires no change notification, so the runtime hook (subscribed to the session) only re-read on a backend-identity change — i.e. a tab switch. The native Claude path made it always reproduce: the await on the on-disk transcript read falls between activating the tab and loading messages. Add AgentSession.loadDisplayMessages (loadMessages + notifyMessages) and use it on both the markdown and native resume paths. 2. Opening a chat always spawned a new tab, even from an empty landing tab. absorbIntoEmptyActiveTab now gives the loaded session the empty active tab's strip position and closes the empty one, so opening a chat reuses the current tab when it has no conversation. A tab with real messages is never clobbered (opens a new tab in that case). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): three native-history review findings (title source, delete race, claude config dir) All three are legit P2s from Codex review: - Native resume reapplied the index title via setLabel(), marking it user-sourced and freezing future agent title refreshes for resumed opencode/codex sessions. Add AgentSession.restoreLabel(label, source) and apply the index title with its recorded titleSource — user renames stay sticky, agent/derived titles stay refreshable. Tested both directions. - Deleting a chat within the ~500ms index-touch debounce window let the already-queued flushIndexTouch re-add it after the tombstone was written (recordSession clears the tombstone), resurrecting it in Recent Chats. cancelPendingIndexTouch clears the pre-delete timer for the matching live session before tombstoning; post-delete activity still re-indexes. - readPersistedTranscript resolved CLAUDE_CONFIG_DIR from process.env only, so users who set a custom config dir via Agent Mode env overrides got a blank restored chat. Resolve it with the same precedence the SDK is spawned with (env overrides > managed env > process.env > ~/.claude). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(agent-mode): match live sessions by backend+session id in one lookup Codex follow-up: the native open/rename paths found the first live session by sessionId and checked backendId after, so an (effectively impossible, UUID) cross-backend id collision where the wrong-backend tab was created first would hide the correct already-open tab. Add findLiveSession(backendId, sessionId) that matches both at once and excludes closed sessions; use it in both loadNativeSessionFromHistory and the updateChatTitle native branch. Existing collision tests stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(agent-mode): inset the Recent Chats search box from the panel edges Wrap the search bar in a small (p-1) padded container so it isn't flush against the section's top/side edges. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): adapter frontmatter fallback when routing a clicked chat loadChatById decided agent-vs-legacy from metadataCache frontmatter only. A hidden-folder agent note (dot-folder save location) isn't indexed by the cache, so its mode: agent was invisible and the click routed to the legacy Copilot chat loader instead of agent resume — even though Recent Chats now surfaces such notes via the merge's adapter fallback. Read frontmatter via the adapter when the cache misses before routing. Only runs on a cache miss, so the common (visible folder) path is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): keep Recent Chats row actions reachable by keyboard The row's action cluster (open-source/rename/delete) was tw-hidden until group-hover, so keyboard users could focus the row and open the chat but never reach the management buttons — they sit out of the tab order while display:none. Reveal the cluster on group-focus-within too (and hide the relative time then), so focusing the row exposes the actions and Tab can move into them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): row Enter/Space opens only when the row itself is focused After the focus-within a11y fix made the action buttons keyboard-focusable, pressing Enter/Space on a focused rename/delete/open-source button bubbled its keydown to the row handler, which also called onOpen — so managing a chat by keyboard would additionally open/resume it (the buttons stop click propagation, not keydown). Guard the row handler to act only when the row is the keydown target. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4844e60e4f
|
Manage window migrated event in agent view (#2590) | ||
|
|
8ba39517d8
|
fix(agent-mode): surface provider errors stringified in data.message (#2586)
codex-acp wraps the upstream provider error as a JSON string inside `error.data.message`, but findProviderErrorPayload only walked nested objects, so the user saw a bare "Internal error" with no actionable text. Parse a JSON-object `message` and recurse so the real reason surfaces — e.g. "The 'gpt-5.5' model requires a newer version of Codex. Please upgrade…" when an outdated codex-acp can't run a newer model. Generic across providers; no codex- or model-specific branching. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ee870b4dce
|
fix(mobile): keep Agent Mode off the emulated-mobile load path (#2587)
* fix(mobile): keep Agent Mode off the emulated-mobile load path `app.emulateMobile(true)` flips Platform.isMobile to true (stubbing Node's built-ins to null) but leaves Platform.isDesktopApp true — so gating Agent Mode on isDesktopApp still imported the @/agentMode barrel, whose module-scope `promisify(execFile)` then crashed the whole plugin with "Cannot read properties of null (reading 'promisify')". Add isDesktopRuntime() (Platform.isDesktopApp && !Platform.isMobile) and gate every Agent Mode load/registration on it so the barrel is never imported on a Node-less runtime. Update the mobile-load smoke gate rule to enforce the new gate and add a unit test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(lint): ban Platform.isDesktopApp; route desktop gating through isDesktopRuntime() Platform.isDesktopApp stays true under app.emulateMobile(true) (which stubs Node), so it doesn't keep desktop-only/Node code off the emulated-mobile path. Add a no-restricted-properties ESLint rule banning it (the canonical check in src/utils/desktopRuntime.ts is exempt via an inline disable), and migrate all 14 existing usages — web viewer, encryption/keychain, Miyo discovery, web-tab and @-mention chat hooks, and main.ts's web-viewer gates — to isDesktopRuntime(). Behavior is unchanged on real desktop and mobile; only emulateMobile now faithfully hides these desktop-only features. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e962c79f60
|
Surface a custom model set via ANTHROPIC_MODEL in the Claude model picker (#2588)
* feat(agent-mode): surface a custom ANTHROPIC_MODEL as a selectable Claude model Setting ANTHROPIC_MODEL=<id> in the Claude backend's env overrides had no effect on the model picker: the picker is built only from the SDK's advertised catalog, so a free-form id had nowhere to surface. Read ANTHROPIC_MODEL from the backend's env overrides and merge it into the catalog as a synthetic entry, so it flows through model discovery into the curation list and chat picker. Selecting it sends the id as options.model; it is also honored as the persisted default seed. A custom id that shadows a built-in is deduped (catalog returned by the same reference). Closes #149 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-mode): restart the affected backend when its env overrides change An env-overrides edit only reached the live or warm backend process on the next spawn/probe: nothing watched agentMode.backends.<id>.envOverrides, so the picker kept serving the pre-edit cached catalog until an unrelated restart or a new session recomputed it. With the Claude SDK now folding a custom ANTHROPIC_MODEL into its catalog, that staleness became user-visible. Subscribe to settings changes and restart the backend whose env-overrides record actually changed (order-independent key, so unrelated settings writes and key reordering are no-ops). The editor debounces commits and the manager/preloader coalesce rapid restarts into one re-probe, matching the existing provider-config subscription. Addresses the Codex P2 review finding on #2588. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(agent-mode): surface ANTHROPIC_MODEL via the SDK's native catalog Per review (zeroliu): avoid maintaining a second source of truth for Claude models. Verified empirically that the `claude` CLI already reflects a user-set `ANTHROPIC_MODEL` into its init handshake `init.models` (with full effort metadata) when the env var is present in the probe's environment. The custom model never appeared only because `probeClaudeSdkCatalog` spawned the init probe with `pathToClaudeCodeExecutable` alone and no `env`, so the override never reached it. Fix: thread the backend's `envOverrides` into the probe (merged onto `process.env`, mirroring the prompt path). The model now surfaces natively as a first-class catalog entry, including effort levels. Drops the synthetic catalog injection (`customModelFromEnv` / `mergeCustomModel` / `effectiveCatalog`). Scopes the plugin-lifetime catalog cache by an order-independent env-overrides key so editing `ANTHROPIC_MODEL` invalidates a stale entry; the next `ensureModelCatalog()` re-probes with the new env. The env-change restart wiring from the prior commit still triggers that re-probe and the picker refresh. Tests: env-scoped cache + probe env-passing in effortOption.test.ts; backend threading of env overrides into the probe in ClaudeSdkBackendProcess.test.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
05f5d7f7c4
|
Remove unnecessary chat bottom padding calculation (#2585) | ||
|
|
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> |