Skip to content

fix: always swap conversation data on tab switch#166

Open
psypeal wants to merge 9 commits intomatt1398:mainfrom
psypeal:fix/tab-switch-conversation
Open

fix: always swap conversation data on tab switch#166
psypeal wants to merge 9 commits intomatt1398:mainfrom
psypeal:fix/tab-switch-conversation

Conversation

@psypeal
Copy link
Copy Markdown
Contributor

@psypeal psypeal commented Apr 5, 2026

Summary

  • Remove sessionChanged guard from setActiveTab — always swap conversation data when switching tabs
  • The global conversation may belong to a different tab even when selectedSessionId matches (multi-tab scenario)
  • Ensures correct conversation is displayed when clicking between tabs

Test plan

  • pnpm typecheck passes (pre-existing errors in LinkedToolItem.tsx are unrelated)
  • pnpm test — all 652 tests pass
  • Manual: open session A in tab 1, session B in tab 2, switch between them — both load correctly
  • Manual: open same session in two tabs — switching works without blank content

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Ctrl+R/Cmd+R now performs in-place session refresh with automatic scroll to bottom
    • Added Bash tool viewer with collapsible output sections
    • Added markdown preview mode for .md/.mdx files in tool viewers
    • Enhanced syntax highlighting for Bash, C, Java, Kotlin, Swift, Lua, HTML, and YAML

psypeal and others added 9 commits February 21, 2026 04:51
# Conflicts:
#	src/renderer/components/chat/ContextBadge.tsx
#	src/renderer/utils/contextTracker.ts
# Conflicts:
#	src/renderer/components/chat/ContextBadge.tsx
sessionAnalyzer now reads pre-computed costUsd from calculateMetrics()
instead of re-computing costs independently. Parent cost uses
detail.metrics.costUsd, subagent cost uses proc.metrics.costUsd.
This ensures the cost analysis panel and chat header always agree.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Electron's before-input-event preventDefault blocks both Chromium's built-in reload AND keydown propagation to the renderer. Route Ctrl+R through IPC instead: main process intercepts and sends session:refresh, preload bridges to renderer, store listener refreshes in place.

Switch all refresh paths (IPC, refresh button, keyboard shortcut) to refreshSessionInPlace to avoid unmounting the scroll container, then dispatch a custom event to smoothly scroll to the bottom.

Fixes matt1398#85
# Conflicts:
#	src/renderer/utils/sessionAnalyzer.ts
#	test/renderer/utils/sessionAnalyzer.test.ts
…hlighting

- Add BashToolViewer with syntax-highlighted command input via CodeBlockViewer
- Add CollapsibleOutputSection (collapsed by default) for tool output
- Apply collapsible output to DefaultToolViewer and BashToolViewer
- Add bash/zsh/fish keyword sets for syntax highlighting
- Add keyword sets for c, cpp, java, kotlin, swift, lua, html, yaml
- Fix markdown/html false type-coloring on capitalized words
- Add markdown preview toggle to ReadToolViewer (defaults to preview)
- Default WriteToolViewer to preview mode for markdown files
- Extend comment detection for zsh, fish, yaml (#) and lua (--)
Remove the sessionChanged guard from setActiveTab — the global
conversation may belong to a different tab even when selectedSessionId
matches (multi-tab scenario). Always swap from per-tab cache or
trigger a fetch to ensure the correct conversation is displayed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai coderabbitai bot added the bug Something isn't working label Apr 5, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a custom session refresh mechanism by intercepting Ctrl+R/Cmd+R in the main process and notifying the renderer via IPC, resolving issues #58 and #85. It adds a specialized BashToolViewer with syntax highlighting and collapsible output, and provides Markdown preview support for Read and Write tool viewers. The syntax highlighter is expanded to support multiple new languages, and tab state management is refactored for better consistency. Feedback is provided to remove a redundant language check in the syntax highlighter.


// If no highlighting support, return plain text as single-element array
if (keywords.size === 0 && !['json', 'css', 'html', 'bash', 'markdown'].includes(language)) {
if (keywords.size === 0 && !['json', 'css', 'bash'].includes(language)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The inclusion of 'bash' in the exclusion list is now redundant because bash has been added to the KEYWORDS map (line 405). Since keywords.size will be greater than 0 for bash, the condition keywords.size === 0 will be false, and the code will correctly proceed to the highlighting loop regardless of whether 'bash' is in this array. This matches the logic applied to 'html' which was correctly removed from this list.

Suggested change
if (keywords.size === 0 && !['json', 'css', 'bash'].includes(language)) {
if (keywords.size === 0 && !['json', 'css'].includes(language)) {

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

📝 Walkthrough

Walkthrough

This PR implements session refresh triggered by Ctrl+R/Cmd+R, distinguishing it from hard reload (Ctrl+Shift+R). It adds an IPC channel for main-to-renderer communication, updates preload APIs, refactors tool viewers with a new Bash viewer and collapsible output component, improves syntax highlighting coverage, and adds Markdown preview toggles in file viewers.

Changes

Cohort / File(s) Summary
IPC & Main Process
src/main/index.ts, src/preload/constants/ipcChannels.ts
Modified Ctrl+R/Cmd+R handling to send session:refresh IPC event and gate it separately from Ctrl+Shift+R hard reload; added SESSION_REFRESH constant.
Preload & Electron API
src/preload/index.ts, src/shared/types/api.ts, src/renderer/api/httpClient.ts
Added onSessionRefresh listener hook to expose via preload bridge and extend ElectronAPI interface; browser mode provides no-op implementation.
Bash Tool Viewer
src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx, src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx, src/renderer/components/chat/items/linkedTool/index.ts, src/renderer/utils/toolRendering/toolContentChecks.ts, src/renderer/utils/toolRendering/index.ts
Added new BashToolViewer component with collapsible output section; added hasBashContent detector; wired into LinkedToolItem with variant styling.
Tool Viewer Refactoring
src/renderer/components/chat/items/LinkedToolItem.tsx, src/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsx, src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx, src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx
Integrated CollapsibleOutputSection into DefaultToolViewer; added Markdown preview toggle logic to ReadToolViewer and WriteToolViewer; updated LinkedToolItem to detect and render Bash tools with error variant styling.
Session Refresh Handlers
src/renderer/store/index.ts, src/renderer/components/layout/TabBar.tsx, src/renderer/hooks/useKeyboardShortcuts.ts
Added IPC listener in store initialization to call refreshSessionInPlace and dispatch scroll-bottom event; updated tab bar and keyboard shortcut handlers to use refresh action instead of fetch detail.
Tab State Management
src/renderer/store/slices/tabSlice.ts
Removed sessionChanged guard in setActiveTab, now always syncing session state when cache exists; unconditional fetch on cache miss.
Chat History
src/renderer/components/chat/ChatHistory.tsx
Added event listener for session-refresh-scroll-bottom to trigger smooth scroll-to-bottom on session refresh.
Syntax Highlighting
src/renderer/components/chat/viewers/syntaxHighlighter.ts
Extended keyword support for bash, c, java, kotlin, swift, lua, html, yaml; added cpp/hpp keyword sets; expanded comment detection for yaml and lua; removed html/markdown from unsupported languages.

Possibly related PRs

Suggested labels

bug

✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/tab-switch-conversation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/main/index.ts (1)

492-496: Consider adding SESSION_REFRESH to local constants for consistency.

The channel name 'session:refresh' is hardcoded here, while other IPC channels like SSH_STATUS and CONTEXT_CHANGED are defined as local constants at lines 47-51 to avoid preload boundary violations. Adding SESSION_REFRESH to those local constants would improve maintainability and consistency.

♻️ Suggested addition to local constants
 // IPC channel constants (duplicated from `@preload` to avoid boundary violation)
 const SSH_STATUS = 'ssh:status';
 const CONTEXT_CHANGED = 'context:changed';
 const HTTP_SERVER_START = 'httpServer:start';
 const HTTP_SERVER_STOP = 'httpServer:stop';
 const HTTP_SERVER_GET_STATUS = 'httpServer:getStatus';
+const SESSION_REFRESH = 'session:refresh';

Then use SESSION_REFRESH instead of the string literal at line 494.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/index.ts` around lines 492 - 496, The hardcoded IPC channel
'session:refresh' should be extracted to a local constant to match existing
pattern; add a new constant name SESSION_REFRESH alongside SSH_STATUS and
CONTEXT_CHANGED and replace the string literal in the
mainWindow.webContents.send call inside the keyboard handler (the block checking
input.control/meta/shift and input.key.toLowerCase() === 'r') with
SESSION_REFRESH to avoid preload boundary issues and keep channel names
consistent.
src/renderer/components/chat/ChatHistory.tsx (1)

380-387: Global event may scroll inactive tabs in split-pane layouts.

The session-refresh-scroll-bottom event is dispatched globally (from src/renderer/store/index.ts:282, src/renderer/hooks/useKeyboardShortcuts.ts:267, and src/renderer/components/layout/TabBar.tsx:221), but this listener doesn't check if isThisTabActive before scrolling. In split-pane scenarios, all mounted ChatHistory instances will call scrollToBottom, potentially disrupting scroll positions in inactive panes.

Consider guarding the scroll action:

♻️ Proposed fix to scope scroll to active tab
   // Listen for session-refresh-scroll-bottom events (from Ctrl+R / refresh button)
   useEffect(() => {
     const handler = (): void => {
+      if (!isThisTabActive) return;
       scrollToBottom('smooth');
     };
     window.addEventListener('session-refresh-scroll-bottom', handler);
     return () => window.removeEventListener('session-refresh-scroll-bottom', handler);
-  }, [scrollToBottom]);
+  }, [scrollToBottom, isThisTabActive]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/ChatHistory.tsx` around lines 380 - 387, The
global "session-refresh-scroll-bottom" handler in ChatHistory currently calls
scrollToBottom for every mounted instance, which scrolls inactive split-pane
tabs; update the useEffect that registers the handler to check the component's
isThisTabActive (or only attach the listener when isThisTabActive is true)
before calling scrollToBottom, and include isThisTabActive in the effect
dependencies so the listener is added/removed correctly; reference the useEffect
in ChatHistory and the scrollToBottom and isThisTabActive symbols when making
the change.
src/renderer/hooks/useKeyboardShortcuts.ts (1)

263-268: Consider adding error handling for the refresh operation.

The Promise.all chain lacks a .catch() handler. If either promise rejects, the failure is silent and session-refresh-scroll-bottom won't be dispatched. For a user-initiated refresh (Cmd+R), consider providing feedback on failure.

♻️ Proposed fix to add error handling
         void Promise.all([
           refreshSessionInPlace(selectedProjectId, selectedSessionId),
           fetchSessions(selectedProjectId),
         ]).then(() => {
           window.dispatchEvent(new CustomEvent('session-refresh-scroll-bottom'));
+        }).catch((error) => {
+          logger.error('Failed to refresh session', error);
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/useKeyboardShortcuts.ts` around lines 263 - 268, The
Promise.all call using refreshSessionInPlace(selectedProjectId,
selectedSessionId) and fetchSessions(selectedProjectId) has no error
handling—add a .catch handler to log the error (console.error) and surface user
feedback (e.g., dispatch a distinct CustomEvent like 'session-refresh-failed' or
call the app's toast/notification API) so failures aren't silent; keep the
existing .then to dispatch 'session-refresh-scroll-bottom' only on success and
ensure the .catch receives the same error context (project/session IDs) for
clearer diagnostics.
src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx (1)

30-39: Consider adding aria-expanded for accessibility.

The button toggles visibility but lacks the aria-expanded attribute, which helps screen readers announce the current state.

♿ Proposed accessibility improvement
       <button
         type="button"
         className="mb-1 flex items-center gap-2 text-xs"
         style={{ color: 'var(--tool-item-muted)', background: 'none', border: 'none', padding: 0, cursor: 'pointer' }}
         onClick={() => setIsExpanded((prev) => !prev)}
+        aria-expanded={isExpanded}
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx`
around lines 30 - 39, The toggle button in CollapsibleOutputSection is missing
accessibility state; update the button element (the one using onClick={() =>
setIsExpanded(prev => !prev)} and rendering isExpanded, Chevron icons and
StatusDot) to include aria-expanded set to the boolean isExpanded
(aria-expanded={isExpanded}) so screen readers announce the current state;
additionally ensure the collapsible content element uses an id referenced by
aria-controls on the same button (aria-controls="...") and toggles its
visibility/aria-hidden in sync with isExpanded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx`:
- Line 58: The viewMode state in ReadToolViewer is only initialized once and
therefore can leak between reused component instances; add an effect that
watches the active file identity (e.g., the isMarkdownFile prop or the linked
tool's file id) and calls setViewMode(isMarkdownFile ? 'preview' : 'code')
whenever that identity changes so the view resets correctly when switching
files. Ensure the useEffect references the unique symbols used in this component
(viewMode, setViewMode, isMarkdownFile or the linked tool/file id) and updates
viewMode accordingly.

In `@src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx`:
- Line 24: The component initializes viewMode based on isMarkdownFile but never
updates it when linkedTool (or its result/file type) changes; add a
React.useEffect that depends on isMarkdownFile (and/or linkedTool/result) to
call setViewMode(isMarkdownFile ? 'preview' : 'code') so the UI switches when
the file type changes; apply the same pattern to ReadToolViewer; target the
viewMode and setViewMode state in WriteToolViewer and ReadToolViewer and ensure
the effect runs when isMarkdownFile (or linkedTool/result) changes.

In `@src/renderer/components/chat/items/LinkedToolItem.tsx`:
- Line 175: The code currently passes variant={toolVariant} into BaseItem but
BaseItemProps doesn't define variant, causing a TS error; remove variant from
the BaseItem usage (the instance that renders BaseItem) and instead pass
variant={toolVariant} into the BashToolViewer component (the BashToolViewer
invocation around lines ~189-190). Update the JSX so BaseItem no longer receives
variant and BashToolViewer receives the toolVariant prop, ensuring the
BashToolViewer prop type accepts variant if needed.

In `@src/renderer/store/slices/tabSlice.ts`:
- Around line 299-316: Change the cache-hit logic to treat an existing
tabSessionData entry as a cache regardless of whether conversation is null: if
cachedTabData exists (not just cachedTabData.conversation), call set(...) to
restore the cached state's
conversationLoading/sessionDetailLoading/sessionDetailError/sessionClaudeMdStats/sessionContextStats/sessionPhaseInfo/visibleAIGroupId/selectedAIGroup
(preserving their in-flight/loading values) instead of calling
get().fetchSessionDetail(...) which triggers duplicate requests; apply this same
change to both places where the code currently checks cachedTabData.conversation
(the branches that call get().fetchSessionDetail), and only call
fetchSessionDetail when no cachedTabData exists at all.

---

Nitpick comments:
In `@src/main/index.ts`:
- Around line 492-496: The hardcoded IPC channel 'session:refresh' should be
extracted to a local constant to match existing pattern; add a new constant name
SESSION_REFRESH alongside SSH_STATUS and CONTEXT_CHANGED and replace the string
literal in the mainWindow.webContents.send call inside the keyboard handler (the
block checking input.control/meta/shift and input.key.toLowerCase() === 'r')
with SESSION_REFRESH to avoid preload boundary issues and keep channel names
consistent.

In `@src/renderer/components/chat/ChatHistory.tsx`:
- Around line 380-387: The global "session-refresh-scroll-bottom" handler in
ChatHistory currently calls scrollToBottom for every mounted instance, which
scrolls inactive split-pane tabs; update the useEffect that registers the
handler to check the component's isThisTabActive (or only attach the listener
when isThisTabActive is true) before calling scrollToBottom, and include
isThisTabActive in the effect dependencies so the listener is added/removed
correctly; reference the useEffect in ChatHistory and the scrollToBottom and
isThisTabActive symbols when making the change.

In `@src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx`:
- Around line 30-39: The toggle button in CollapsibleOutputSection is missing
accessibility state; update the button element (the one using onClick={() =>
setIsExpanded(prev => !prev)} and rendering isExpanded, Chevron icons and
StatusDot) to include aria-expanded set to the boolean isExpanded
(aria-expanded={isExpanded}) so screen readers announce the current state;
additionally ensure the collapsible content element uses an id referenced by
aria-controls on the same button (aria-controls="...") and toggles its
visibility/aria-hidden in sync with isExpanded.

In `@src/renderer/hooks/useKeyboardShortcuts.ts`:
- Around line 263-268: The Promise.all call using
refreshSessionInPlace(selectedProjectId, selectedSessionId) and
fetchSessions(selectedProjectId) has no error handling—add a .catch handler to
log the error (console.error) and surface user feedback (e.g., dispatch a
distinct CustomEvent like 'session-refresh-failed' or call the app's
toast/notification API) so failures aren't silent; keep the existing .then to
dispatch 'session-refresh-scroll-bottom' only on success and ensure the .catch
receives the same error context (project/session IDs) for clearer diagnostics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 206ebec3-d523-40f7-9a0b-71f86eea5560

📥 Commits

Reviewing files that changed from the base of the PR and between c0708bb and fd8ea3b.

📒 Files selected for processing (20)
  • src/main/index.ts
  • src/preload/constants/ipcChannels.ts
  • src/preload/index.ts
  • src/renderer/api/httpClient.ts
  • src/renderer/components/chat/ChatHistory.tsx
  • src/renderer/components/chat/items/LinkedToolItem.tsx
  • src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx
  • src/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/index.ts
  • src/renderer/components/chat/viewers/syntaxHighlighter.ts
  • src/renderer/components/layout/TabBar.tsx
  • src/renderer/hooks/useKeyboardShortcuts.ts
  • src/renderer/store/index.ts
  • src/renderer/store/slices/tabSlice.ts
  • src/renderer/utils/toolRendering/index.ts
  • src/renderer/utils/toolRendering/toolContentChecks.ts
  • src/shared/types/api.ts

: undefined;

const isMarkdownFile = /\.mdx?$/i.test(filePath);
const [viewMode, setViewMode] = React.useState<'code' | 'preview'>(isMarkdownFile ? 'preview' : 'code');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether ReadToolViewer remounts on tool-item switch (key-based mount)
# If not remounted, the state-sync effect above is required.
rg -n --type=tsx -C4 'ReadToolViewer|key=' src/renderer/components/chat

Repository: matt1398/claude-devtools

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Correct the file type and search for ReadToolViewer usage
rg -n --type=typescript -C4 'ReadToolViewer' src/renderer/components/chat

Repository: matt1398/claude-devtools

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Also search for the component definition and its parent usages
fd --type f -e tsx -e ts 'linkedTool|ReadToolViewer' src/renderer/components/chat

Repository: matt1398/claude-devtools

Length of output: 132


🏁 Script executed:

#!/bin/bash
# Find the parent component that uses ReadToolViewer
rg -n '<ReadToolViewer' src/renderer/components --type=typescript -C6

Repository: matt1398/claude-devtools

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Search for ReadToolViewer usage without type flag
rg 'ReadToolViewer' src/renderer/components --context=5

Repository: matt1398/claude-devtools

Length of output: 4901


🏁 Script executed:

#!/bin/bash
# Read the ReadToolViewer component to check dependencies and useState
cat -n src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx | head -100

Repository: matt1398/claude-devtools

Length of output: 4047


🏁 Script executed:

#!/bin/bash
# Find parent components that render ReadToolViewer
rg '<ReadToolViewer' src/renderer/components --context=8

Repository: matt1398/claude-devtools

Length of output: 3313


🏁 Script executed:

#!/bin/bash
# Check LinkedToolItem.tsx to verify the parent rendering context
cat -n src/renderer/components/chat/items/LinkedToolItem.tsx | grep -A 15 -B 5 'ReadToolViewer'

Repository: matt1398/claude-devtools

Length of output: 1874


Sync viewMode when the active file changes.

Line 58 initializes viewMode only on mount. When users switch between different linked tools (markdown and non-markdown files), the component instance is reused without remounting, causing the view mode to leak from the previous file. For example, if a markdown file is viewed in preview mode, switching to a code file will incorrectly remain in preview mode.

Proposed fix
   const isMarkdownFile = /\.mdx?$/i.test(filePath);
   const [viewMode, setViewMode] = React.useState<'code' | 'preview'>(isMarkdownFile ? 'preview' : 'code');
+  React.useEffect(() => {
+    setViewMode(isMarkdownFile ? 'preview' : 'code');
+  }, [isMarkdownFile]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [viewMode, setViewMode] = React.useState<'code' | 'preview'>(isMarkdownFile ? 'preview' : 'code');
const isMarkdownFile = /\.mdx?$/i.test(filePath);
const [viewMode, setViewMode] = React.useState<'code' | 'preview'>(isMarkdownFile ? 'preview' : 'code');
React.useEffect(() => {
setViewMode(isMarkdownFile ? 'preview' : 'code');
}, [isMarkdownFile]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx` at line 58,
The viewMode state in ReadToolViewer is only initialized once and therefore can
leak between reused component instances; add an effect that watches the active
file identity (e.g., the isMarkdownFile prop or the linked tool's file id) and
calls setViewMode(isMarkdownFile ? 'preview' : 'code') whenever that identity
changes so the view resets correctly when switching files. Ensure the useEffect
references the unique symbols used in this component (viewMode, setViewMode,
isMarkdownFile or the linked tool/file id) and updates viewMode accordingly.

const isCreate = toolUseResult?.type === 'create';
const isMarkdownFile = /\.mdx?$/i.test(filePath);
const [viewMode, setViewMode] = React.useState<'code' | 'preview'>('code');
const [viewMode, setViewMode] = React.useState<'code' | 'preview'>(isMarkdownFile ? 'preview' : 'code');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether WriteToolViewer remounts with a distinct key when tool/file changes.
rg -n --type=tsx -C4 'WriteToolViewer|key=' src/renderer/components/chat

Repository: matt1398/claude-devtools

Length of output: 95


🏁 Script executed:

# Search for WriteToolViewer usage to understand remounting behavior
rg -n 'WriteToolViewer' src/renderer/components --type ts --type tsx

Repository: matt1398/claude-devtools

Length of output: 95


🏁 Script executed:

# Also check for key prop patterns in chat component files
rg -n 'key=' src/renderer/components/chat -A 2 -B 2

Repository: matt1398/claude-devtools

Length of output: 25345


🏁 Script executed:

# Look at the parent component to understand when/how filePath changes
fd -e ts -e tsx src/renderer/components/chat | head -20

Repository: matt1398/claude-devtools

Length of output: 485


🏁 Script executed:

# Search directly for WriteToolViewer component
rg -n 'WriteToolViewer' src/renderer

Repository: matt1398/claude-devtools

Length of output: 727


🏁 Script executed:

# Also search in all locations (not just renderer)
rg -n 'WriteToolViewer'

Repository: matt1398/claude-devtools

Length of output: 727


🏁 Script executed:

# Look at the WriteToolViewer file itself to understand its props
cat -n src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx | head -50

Repository: matt1398/claude-devtools

Length of output: 2316


🏁 Script executed:

# Check LinkedToolItem and how it's rendered - look for parent usage and key patterns
rg -n 'LinkedToolItem' src/renderer/components/chat -B 2 -A 2

Repository: matt1398/claude-devtools

Length of output: 9529


🏁 Script executed:

# Check the full LinkedToolItem component around line 184
sed -n '175,195p' src/renderer/components/chat/items/LinkedToolItem.tsx

Repository: matt1398/claude-devtools

Length of output: 901


🏁 Script executed:

# Check if itemId is based on tool.id and if it's stable across updates
sed -n '130,170p' src/renderer/components/chat/items/ExecutionTrace.tsx

Repository: matt1398/claude-devtools

Length of output: 1629


🏁 Script executed:

# Also check DisplayItemList to see how itemKey is constructed for tools
sed -n '145,165p' src/renderer/components/chat/DisplayItemList.tsx

Repository: matt1398/claude-devtools

Length of output: 838


🏁 Script executed:

# Check ReadToolViewer to see if it has similar initialization and if it's been fixed
cat -n src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx | head -40

Repository: matt1398/claude-devtools

Length of output: 1479


🏁 Script executed:

# Check if LinkedToolItem receives key in DisplayItemList - look for the full context
sed -n '148,162p' src/renderer/components/chat/DisplayItemList.tsx

Repository: matt1398/claude-devtools

Length of output: 698


🏁 Script executed:

# Check the structure of linkedTool/item.tool to understand if it's immutable or can change
rg -n 'linkedTool.*filePath|item\.tool' src/renderer/components/chat/items/linkedTool -A 1 | head -30

Repository: matt1398/claude-devtools

Length of output: 50


🏁 Script executed:

# Get full context around LinkedToolItem rendering in DisplayItemList - see if key is assigned
sed -n '145,170p' src/renderer/components/chat/DisplayItemList.tsx

Repository: matt1398/claude-devtools

Length of output: 962


🏁 Script executed:

# Check how items array is constructed and if tool objects are immutable or mutable
rg -n 'case.*tool|linkedTool' src/renderer/components/chat/DisplayItemList.tsx -B 3 -A 8 | head -50

Repository: matt1398/claude-devtools

Length of output: 780


🏁 Script executed:

# Check if itemKey is being used as key prop for LinkedToolItem - search around line 153-165
sed -n '148,175p' src/renderer/components/chat/DisplayItemList.tsx

Repository: matt1398/claude-devtools

Length of output: 1071


🏁 Script executed:

# Check if the items array content is mutable or immutable when tool result updates
rg -n 'items.*=' src/renderer/components/chat/DisplayItemList.tsx | head -10

Repository: matt1398/claude-devtools

Length of output: 148


🏁 Script executed:

# Look at ReadToolViewer to see if it also has the same pattern (useState without useEffect)
sed -n '17,60p' src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx

Repository: matt1398/claude-devtools

Length of output: 1598


🏁 Script executed:

# Critical check: does LinkedToolItem get a key prop in DisplayItemList?
sed -n '150,180p' src/renderer/components/chat/DisplayItemList.tsx | grep -E 'key=|linkedKey|itemKey.*='

Repository: matt1398/claude-devtools

Length of output: 185


🏁 Script executed:

# Let me see the full map function in DisplayItemList to confirm key prop usage
sed -n '100,120p' src/renderer/components/chat/DisplayItemList.tsx

Repository: matt1398/claude-devtools

Length of output: 839


🏁 Script executed:

# Check if there's a key prop being passed in the rendered element
rg -n 'return.*itemKey' src/renderer/components/chat/DisplayItemList.tsx -A 15 | head -40

Repository: matt1398/claude-devtools

Length of output: 50


🏁 Script executed:

# Check the return statement in the map function - what wraps the element?
sed -n '100,330p' src/renderer/components/chat/DisplayItemList.tsx | tail -50

Repository: matt1398/claude-devtools

Length of output: 1865


🏁 Script executed:

# More focused: look at the end of the switch and what happens to element and itemKey
sed -n '285,310p' src/renderer/components/chat/DisplayItemList.tsx

Repository: matt1398/claude-devtools

Length of output: 1123


🏁 Script executed:

# Perfect! Now let's check if ReadToolViewer also has the same issue - does it have a useEffect?
grep -n "useEffect\|viewMode" src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx

Repository: matt1398/claude-devtools

Length of output: 602


🏁 Script executed:

# Check WriteToolViewer for any useEffect
grep -n "useEffect\|viewMode" src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx

Repository: matt1398/claude-devtools

Length of output: 602


🏁 Script executed:

# Look at the full wrapper - see the key being passed
sed -n '325,345p' src/renderer/components/chat/DisplayItemList.tsx

Repository: matt1398/claude-devtools

Length of output: 558


Add a useEffect to sync viewMode state with isMarkdownFile.

Line 24 initializes viewMode correctly, but it won't update if the linkedTool prop changes after mount (e.g., if the write result is updated with a different file). Since LinkedToolItem is keyed by item.tool.id, the component won't remount on result updates—only the prop changes. Add a useEffect to keep the mode in sync with the file type:

  const [viewMode, setViewMode] = React.useState<'code' | 'preview'>(isMarkdownFile ? 'preview' : 'code');
+  React.useEffect(() => {
+    setViewMode(isMarkdownFile ? 'preview' : 'code');
+  }, [isMarkdownFile]);

Note: ReadToolViewer has the same pattern and should also receive this sync effect.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx` at line
24, The component initializes viewMode based on isMarkdownFile but never updates
it when linkedTool (or its result/file type) changes; add a React.useEffect that
depends on isMarkdownFile (and/or linkedTool/result) to call
setViewMode(isMarkdownFile ? 'preview' : 'code') so the UI switches when the
file type changes; apply the same pattern to ReadToolViewer; target the viewMode
and setViewMode state in WriteToolViewer and ReadToolViewer and ensure the
effect runs when isMarkdownFile (or linkedTool/result) changes.

highlightClasses={highlightClasses}
highlightStyle={highlightStyle}
notificationDotColor={notificationDotColor}
variant={toolVariant}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check BaseItemProps interface definition
rg -nA 40 'interface BaseItemProps' --type=ts src/renderer/components/chat/items/BaseItem.tsx

# Also check if variant is used anywhere in BaseItem implementation
rg -n 'variant' src/renderer/components/chat/items/BaseItem.tsx

Repository: matt1398/claude-devtools

Length of output: 1569


Pass variant to BashToolViewer directly, not to BaseItem.

The BaseItemProps interface does not include a variant property. The code passes variant={toolVariant} to BaseItem on line 175, which will cause a TypeScript error. Move the variant prop to the BashToolViewer component on line 189-190 where it belongs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/items/LinkedToolItem.tsx` at line 175, The code
currently passes variant={toolVariant} into BaseItem but BaseItemProps doesn't
define variant, causing a TS error; remove variant from the BaseItem usage (the
instance that renders BaseItem) and instead pass variant={toolVariant} into the
BashToolViewer component (the BashToolViewer invocation around lines ~189-190).
Update the JSX so BaseItem no longer receives variant and BashToolViewer
receives the toolVariant prop, ensuring the BashToolViewer prop type accepts
variant if needed.

Comment on lines +299 to +316
// Always swap conversation to match this tab — the global conversation
// may belong to a different tab even when selectedSessionId matches.
if (hasCachedData) {
// Swap global state from per-tab cache (no re-fetch)
set({
sessionDetail: cachedTabData.sessionDetail,
conversation: cachedTabData.conversation,
conversationLoading: false,
sessionDetailLoading: false,
sessionDetailError: null,
sessionClaudeMdStats: cachedTabData.sessionClaudeMdStats,
sessionContextStats: cachedTabData.sessionContextStats,
sessionPhaseInfo: cachedTabData.sessionPhaseInfo,
visibleAIGroupId: cachedTabData.visibleAIGroupId,
selectedAIGroup: cachedTabData.selectedAIGroup,
});
} else {
void get().fetchSessionDetail(foundWorktree, sessionId, tabId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Treat an in-flight tabSessionData entry as cache, not a miss.

Lines 301 and 337 treat conversation != null as the only cache hit. But fetchSessionDetail() creates tabSessionData[tabId] with loading flags before the request resolves in src/renderer/store/slices/sessionDetailSlice.ts Lines 150-172, and only fills conversation after success in src/renderer/store/slices/sessionDetailSlice.ts Lines 428-448. If the user re-activates that tab while the first request is still in flight, this branch fires a second fetch and flips the global loading state again. That duplicates requests and can reintroduce the flicker this PR is trying to remove.

Please branch on the presence/loading state of cachedTabData, or restore the tab’s cached loading state instead of re-fetching whenever conversation is still null.

🛠️ Suggested direction
-      const hasCachedData = cachedTabData?.conversation != null;
+      const isTabDataCached = cachedTabData != null;
...
-        if (hasCachedData) {
+        if (isTabDataCached) {
           set({
             sessionDetail: cachedTabData.sessionDetail,
             conversation: cachedTabData.conversation,
-            conversationLoading: false,
-            sessionDetailLoading: false,
-            sessionDetailError: null,
+            conversationLoading: cachedTabData.conversationLoading,
+            sessionDetailLoading: cachedTabData.sessionDetailLoading,
+            sessionDetailError: cachedTabData.sessionDetailError,
             sessionClaudeMdStats: cachedTabData.sessionClaudeMdStats,
             sessionContextStats: cachedTabData.sessionContextStats,
             sessionPhaseInfo: cachedTabData.sessionPhaseInfo,
             visibleAIGroupId: cachedTabData.visibleAIGroupId,
             selectedAIGroup: cachedTabData.selectedAIGroup,
           });
         } else {
           void get().fetchSessionDetail(/* same args as today */);
         }

Apply the same adjustment in both branches.

Also applies to: 335-352

🧰 Tools
🪛 ESLint

[error] 316-316: Remove this use of the "void" operator.

(sonarjs/void-use)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/store/slices/tabSlice.ts` around lines 299 - 316, Change the
cache-hit logic to treat an existing tabSessionData entry as a cache regardless
of whether conversation is null: if cachedTabData exists (not just
cachedTabData.conversation), call set(...) to restore the cached state's
conversationLoading/sessionDetailLoading/sessionDetailError/sessionClaudeMdStats/sessionContextStats/sessionPhaseInfo/visibleAIGroupId/selectedAIGroup
(preserving their in-flight/loading values) instead of calling
get().fetchSessionDetail(...) which triggers duplicate requests; apply this same
change to both places where the code currently checks cachedTabData.conversation
(the branches that call get().fetchSessionDetail), and only call
fetchSessionDetail when no cachedTabData exists at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant