Replace blocking alert() calls in MCP delete handlers with non-blocking deleteError state updates and setSaveStatus('error') on failure.
Thread deleteError through Settings -> Agents settings views and render inline error feedback in Claude/Codex MCP sections near delete actions.
Prevent orphaned save-close timers by clearing and nulling closeTimerRef before scheduling a new timeout, and by nulling the ref during unmount cleanup.
Resolves merge conflict in VersionUpgradeModal.tsx — combines PR #402's
file relocation and copyTextToClipboard utility with the installMode
feature from main that detects git vs npm installs and shows the
correct upgrade command.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Issue
- Markdown preview used rehype-raw, which interprets raw HTML from document content.
- For untrusted markdown (user files, copied LLM output), this could allow script-capable HTML payloads to execute in preview.
Change
- Removed rehypeRaw from MarkdownPreview.
- Kept rehype-katex enabled so math rendering still works.
- Result: raw HTML is no longer interpreted as DOM; it is treated as markdown text.
Reproduction (before fix)
1. Open/create any .md file in the code editor.
2. Add: <iframe srcdoc="<script>parent.alert('xss')</script>"></iframe>
3. Toggle Markdown Preview.
4. Observe script execution (alert) in vulnerable behavior.
Expected after fix
- The same payload does not execute; raw HTML is not rendered as active DOM.
Validation
- npm run typecheck (passes).
The shell connection hook relied on React state (isConnecting/isConnected) as the only guard for connect attempts. Because state updates are asynchronous, rapid connect triggers could race before isConnecting became true and create duplicate WebSocket instances.
This change adds a synchronous ref lock (connectingRef) that is checked immediately in connectToShell and connectWebSocket. connectToShell now sets connectingRef.current = true before invoking connectWebSocket so concurrent calls cannot pass between state updates.
connectWebSocket now:
- returns early when a connection is already locked
- sets connectingRef.current = true when creating a socket
- clears connectingRef.current alongside setIsConnecting(false) in onopen, onclose, onerror, and catch
- clears connectingRef.current when no WebSocket URL is available
disconnectFromShell also resets connectingRef to keep lock/state behavior consistent across manual disconnect flows.
The previous edit flow deleted the existing MCP server before attempting to add the replacement entry. If add failed (CLI error, validation, transport, duplicate handling), the original entry was already gone, causing destructive data loss from an edit action.
This change makes both save flows non-destructive:
- saveMcpServer now posts /api/mcp/cli/add first.
- saveCodexMcpServer now posts /api/codex/mcp/cli/add first.
- Old entry deletion only happens after successful create.
- Old-entry deletion runs only when identity actually changed (name and for Claude scope).
- Cleanup delete failures are caught and logged (console.warn) instead of failing the save.
Result: editing is atomic from caller perspective with respect to create failures, and resilient to cleanup failures.
There were 2 issues.
The copy state was not being reset after copying, which caused the
"Copied!" message to persist indefinitely.
The return value from the copy function was not being used to determine
if the copy was successful. Even on false(i.e. copy unsuccessful), the user would see a success message.
Problem
Git panel status/diff requests could resolve after the user switched projects and still write into state (`gitStatus`, `gitDiff`, `currentBranch`), causing cross-project UI corruption. Also, `createInitialCommit` used `window.alert` on failure, which blocks the UI and prevents centralized error handling.
Root cause
`fetchGitStatus` and `fetchFileDiff` captured `selectedProject` in async callbacks without request cancellation or project-identity guards. The project-change reset effect did not abort in-flight status/diff fetches. `createInitialCommit` handled failures with alerts instead of thrown errors.
Changes
- Added a per-session `AbortController` in the project-reset `useEffect`, and abort on cleanup.
- Passed `AbortSignal` through status/diff flow:
- `fetchGitStatus(signal)` -> `fetchWithAuth(..., { signal })` + `readJson(..., signal)`
- `fetchFileDiff(filePath, signal)` -> `fetchWithAuth(..., { signal })` + `readJson(..., signal)`
- Captured `projectName` at the start of each async function and skipped state setters when:
- `signal.aborted`, or
- current selected project name differs from captured project name.
- Added abort-aware JSON parsing helper and ignored `AbortError` noise in catch paths.
- Removed `alert(...)` from `createInitialCommit`.
- On API failure now throws:
- `new Error(data.error || 'Failed to create initial commit')`
- In catch now rethrows normalized error:
- `new Error(error?.message || 'Failed to create initial commit')`
- Kept existing success refresh behavior (`fetchGitStatus`, `fetchRemoteStatus`) and `setIsCreatingInitialCommit(false)` in `finally`.
Result
- Stale status/diff responses from prior projects are ignored and do not corrupt current project UI.
- Initial commit failures no longer block the UI; callers can surface errors via toast/notification.
Problem
The file tree hook returned early when selectedProject?.name became falsy, but only cleared files. If a fetch had already set loading=true and was then aborted by cleanup, that early return path could leave loading stuck true.
Root cause
The !projectName branch in useFileTreeData exited before setting loading=false, while the in-flight request's finally block was guarded by isActive and therefore skipped after cleanup marked isActive=false.
Change
- Updated the early-return path to call setLoading(false) immediately before returning.
- Kept existing abortController cleanup behavior unchanged.
- Kept existing isActive guards in try/catch/finally unchanged.
Result
Clearing the selected project now deterministically resets loading state, preventing stale loading UI after project deletion/clear during an in-flight file-tree fetch.
`editorToolbarPanel` keeps `currentIndex` as local state while diff chunks are
recomputed from `getChunks(view.state)` on each `updatePanel()` call.
When the diff shrank (for example after resolving edits), `currentIndex` could
remain greater than `chunkCount - 1`. That caused two user-facing issues:
- The counter could render impossible values like `4/2 changes`.
- Prev/next handlers could read `chunks[currentIndex]` as `undefined`, so
navigation would fail to scroll to a valid chunk.
Repro in UI:
1. Open a file with multiple diff chunks.
2. Navigate to a high chunk index using Next.
3. Edit content so the number of chunks decreases.
4. Observe stale index in the counter and broken nav behavior.
Fix:
- After recomputing chunks, clamp the index immediately:
`currentIndex = Math.max(0, Math.min(currentIndex, Math.max(0, chunkCount - 1)))`
- Keep all downstream uses (counter rendering and prev/next logic) based on the
clamped index.
Result:
- Counter always stays within valid bounds.
- Navigation never references an out-of-range chunk.
- Zero-chunk behavior remains intact (counter and disabled buttons).
File:
- src/components/code-editor/utils/editorToolbarPanel.ts
The code editor document hook was reloading more often than necessary because
`useEffect` depended on the full `file` object (and `projectPath`, which was not
used in the effect body). Save flow also emitted verbose production debug logs
and used a blocking `alert()` for errors.
Changes:
- Refactored `useCodeEditorDocument` to derive stable file primitives:
`fileProjectName`, `filePath`, `fileName`, `fileDiffNewString`,
`fileDiffOldString`.
- Updated `loadFileContent` to use those stable fields while preserving existing
diff-first loading logic (`file.diffInfo`, `new_string`, `old_string`).
- Replaced the effect dependency array with only stable primitive values used by
the effect and removed `projectPath` from dependencies.
- Removed debug `console.log` calls in `handleSave`:
- "Saving file:"
- "Save response:"
- "Save successful:"
- Kept error logging via `console.error` for failure diagnostics.
- Replaced blocking `alert()` on save failure with non-blocking UI feedback:
- Added local `saveError` state in the hook.
- Set `saveError` from `getErrorMessage(error)` in the save catch path.
- Exposed `saveError` to `CodeEditor` and rendered an inline error banner.
- Preserved save lifecycle behavior:
- `setSaving(false)` still runs in `finally`.
- `setSaveSuccess(true)` and 2s timeout reset behavior remain unchanged.
There were 2 issues.
1. The copy state was not being reset after copying, which caused the
"Copied!" message to persist indefinitely.
2. The return value from the copy function was not being used to determine
if the copy was successful. Even on `false`(i.e. copy unsuccessful), the user would see
a success message.
- @media (prefers-color-scheme: dark) only responds to the user's system
preference for dark mode, so it will apply the dark mode styles when the
user has enabled dark mode on their device and Not when the user toggles
the dark mode in the app. So, it's better to use tailwind's dark mode
classes instead.
Root cause:
- Processing ownership was derived from UI view state in ChatInterface.
- While switching sessions, an in-flight isLoading=true could stamp the newly selected session as processing, even if a different session was actually running.
- session-status handling only promoted isProcessing=true and did not clear stale processing state on isProcessing=false, which could leave sessions blocked.
Changes:
- ChatInterface: removed implicit processing propagation effect that called onSessionProcessing(selectedSession?.id || currentSessionId) when isLoading was true.
This decouples processing ownership from transient view/session transitions.
- useChatComposerState: added onSessionProcessing callback usage and explicitly marks processing at submit time for the concrete effectiveSessionId (non-temporary IDs only).
This ties processing to the session that actually started work.
- useChatRealtimeHandlers: expanded session-status handling to support both states.
- isProcessing=true: mark session as processing; set loading/abort only when it is the currently viewed session.
- isProcessing=false: clear active+processing markers and clear loading indicators for the current session view.
Behavioral outcome:
- Running session A no longer blocks session B after navigation.
- Users can work in multiple sessions concurrently, and processing badges/loading state stay session-scoped.
Verification:
- npm run typecheck
- npm run build