Skip to content
17 changes: 13 additions & 4 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,12 +481,21 @@ function createWindow(): void {
const ZOOM_OUT_KEYS = new Set(['-', '_']);
mainWindow.webContents.on('before-input-event', (event, input) => {
if (!mainWindow || mainWindow.isDestroyed()) return;

if (input.type !== 'keyDown') return;

// Prevent Electron's default Ctrl+R / Cmd+R page reload so the renderer
// keyboard handler can use it as "Refresh Session" (fixes #58).
// Also prevent Ctrl+Shift+R / Cmd+Shift+R (hard reload).
if ((input.control || input.meta) && input.key.toLowerCase() === 'r') {
// Intercept Ctrl+R / Cmd+R to prevent Chromium's built-in page reload,
// then notify the renderer via IPC so it can refresh the session (fixes #58, #85).
// We must preventDefault here because Chromium handles Ctrl+R at the browser
// engine level, which also blocks the keydown from reaching the renderer —
// hence the IPC bridge.
if ((input.control || input.meta) && !input.shift && input.key.toLowerCase() === 'r') {
event.preventDefault();
mainWindow.webContents.send('session:refresh');
return;
}
// Also block Ctrl+Shift+R (hard reload)
if ((input.control || input.meta) && input.shift && input.key.toLowerCase() === 'r') {
event.preventDefault();
return;
}
Expand Down
3 changes: 3 additions & 0 deletions src/preload/constants/ipcChannels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,6 @@ export const WINDOW_IS_MAXIMIZED = 'window:isMaximized';

/** Relaunch the application */
export const APP_RELAUNCH = 'app:relaunch';

/** Refresh session shortcut (main → renderer, triggered by Ctrl+R / Cmd+R) */
export const SESSION_REFRESH = 'session:refresh';
10 changes: 10 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
HTTP_SERVER_GET_STATUS,
HTTP_SERVER_START,
HTTP_SERVER_STOP,
SESSION_REFRESH,
SSH_CONNECT,
SSH_DISCONNECT,
SSH_GET_CONFIG_HOSTS,
Expand Down Expand Up @@ -347,6 +348,15 @@ const electronAPI: ElectronAPI = {
};
},

// Session refresh event (Ctrl+R / Cmd+R intercepted by main process)
onSessionRefresh: (callback: () => void): (() => void) => {
const listener = (): void => callback();
ipcRenderer.on(SESSION_REFRESH, listener);
return (): void => {
ipcRenderer.removeListener(SESSION_REFRESH, listener);
};
},

// Shell operations
openPath: (targetPath: string, projectRoot?: string) =>
ipcRenderer.invoke('shell:openPath', targetPath, projectRoot),
Expand Down
5 changes: 5 additions & 0 deletions src/renderer/api/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,11 @@ export class HttpAPIClient implements ElectronAPI {
onTodoChange = (callback: (event: FileChangeEvent) => void): (() => void) =>
this.addEventListener('todo-change', callback);

// No-op in browser mode — Ctrl+R refresh is Electron-only
onSessionRefresh = (_callback: () => void): (() => void) => {
return () => {};
};

// ---------------------------------------------------------------------------
// Shell operations (browser fallbacks)
// ---------------------------------------------------------------------------
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/components/chat/ChatHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,15 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
checkScrollButton();
}, [conversation, checkScrollButton]);

// Listen for session-refresh-scroll-bottom events (from Ctrl+R / refresh button)
useEffect(() => {
const handler = (): void => {
scrollToBottom('smooth');
};
window.addEventListener('session-refresh-scroll-bottom', handler);
return () => window.removeEventListener('session-refresh-scroll-bottom', handler);
}, [scrollToBottom]);

// Callback to register AI group refs (combines with visibility hook)
const registerAIGroupRefCombined = useCallback(
(groupId: string) => {
Expand Down
14 changes: 13 additions & 1 deletion src/renderer/components/chat/items/LinkedToolItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getToolContextTokens,
getToolStatus,
getToolSummary,
hasBashContent,
hasEditContent,
hasReadContent,
hasSkillInstructions,
Expand All @@ -31,6 +32,7 @@ import { Wrench } from 'lucide-react';
import { BaseItem, StatusDot } from './BaseItem';
import { formatDuration } from './baseItemHelpers';
import {
BashToolViewer,
DefaultToolViewer,
EditToolViewer,
ReadToolViewer,
Expand All @@ -39,6 +41,7 @@ import {
WriteToolViewer,
} from './linkedTool';

import type { StepVariant } from '@renderer/constants/stepVariants';
import type { LinkedToolItem as LinkedToolItemType } from '@renderer/types/groups';

interface LinkedToolItemProps {
Expand Down Expand Up @@ -139,7 +142,12 @@ export const LinkedToolItem: React.FC<LinkedToolItemProps> = ({
const useWriteViewer =
linkedTool.name === 'Write' && hasWriteContent(linkedTool) && !linkedTool.result?.isError;
const useSkillViewer = linkedTool.name === 'Skill' && hasSkillInstructions(linkedTool);
const useDefaultViewer = !useReadViewer && !useEditViewer && !useWriteViewer && !useSkillViewer;
const useBashViewer = linkedTool.name === 'Bash' && hasBashContent(linkedTool);
const useDefaultViewer =
!useReadViewer && !useEditViewer && !useWriteViewer && !useSkillViewer && !useBashViewer;

// Determine step variant for colored borders/icons
const toolVariant: StepVariant = status === 'error' ? 'tool-error' : 'tool';

// Check if we should show error display for Read/Write tools
const showReadError = linkedTool.name === 'Read' && linkedTool.result?.isError;
Expand All @@ -164,6 +172,7 @@ export const LinkedToolItem: React.FC<LinkedToolItemProps> = ({
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.

>
{/* Read tool with CodeBlockViewer */}
{useReadViewer && <ReadToolViewer linkedTool={linkedTool} />}
Expand All @@ -177,6 +186,9 @@ export const LinkedToolItem: React.FC<LinkedToolItemProps> = ({
{/* Skill tool with instructions */}
{useSkillViewer && <SkillToolViewer linkedTool={linkedTool} />}

{/* Bash tool with syntax-highlighted command */}
{useBashViewer && <BashToolViewer linkedTool={linkedTool} status={status} />}

{/* Default rendering for other tools */}
{useDefaultViewer && <DefaultToolViewer linkedTool={linkedTool} status={status} />}

Expand Down
52 changes: 52 additions & 0 deletions src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* BashToolViewer
*
* Renders Bash tool calls with syntax-highlighted command input
* via CodeBlockViewer and collapsible output section.
*/

import React from 'react';

import { CodeBlockViewer } from '@renderer/components/chat/viewers';

import { type ItemStatus } from '../BaseItem';

import { CollapsibleOutputSection } from './CollapsibleOutputSection';
import { renderOutput } from './renderHelpers';

import type { LinkedToolItem } from '@renderer/types/groups';

interface BashToolViewerProps {
linkedTool: LinkedToolItem;
status: ItemStatus;
}

export const BashToolViewer: React.FC<BashToolViewerProps> = ({ linkedTool, status }) => {
const command = linkedTool.input.command as string;
const description = linkedTool.input.description as string | undefined;

// Use the description (truncated) as the file name label, or fallback to "bash"
const fileName = description
? description.length > 60
? description.slice(0, 57) + '...'
: description
: 'bash';

return (
<>
{/* Input Section — Syntax-highlighted command */}
<CodeBlockViewer
fileName={fileName}
content={command}
language="bash"
/>

{/* Output Section — Collapsible */}
{!linkedTool.isOrphaned && linkedTool.result && (
<CollapsibleOutputSection status={status}>
{renderOutput(linkedTool.result.content)}
</CollapsibleOutputSection>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* CollapsibleOutputSection
*
* Reusable component that wraps tool output in a collapsed-by-default section.
* Shows a clickable header with label, StatusDot, and chevron toggle.
*/

import React, { useState } from 'react';

import { ChevronDown, ChevronRight } from 'lucide-react';

import { type ItemStatus, StatusDot } from '../BaseItem';

interface CollapsibleOutputSectionProps {
status: ItemStatus;
children: React.ReactNode;
/** Label shown in the header (default: "Output") */
label?: string;
}

export const CollapsibleOutputSection: React.FC<CollapsibleOutputSectionProps> = ({
status,
children,
label = 'Output',
}) => {
const [isExpanded, setIsExpanded] = useState(false);

return (
<div>
<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)}
>
{isExpanded ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
{label}
<StatusDot status={status} />
</button>
{isExpanded && (
<div
className="max-h-96 overflow-auto rounded p-3 font-mono text-xs"
style={{
backgroundColor: 'var(--code-bg)',
border: '1px solid var(--code-border)',
color:
status === 'error'
? 'var(--tool-result-error-text)'
: 'var(--color-text-secondary)',
}}
>
{children}
</div>
)}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

import React from 'react';

import { type ItemStatus, StatusDot } from '../BaseItem';
import { type ItemStatus } from '../BaseItem';

import { CollapsibleOutputSection } from './CollapsibleOutputSection';
import { renderInput, renderOutput } from './renderHelpers';

import type { LinkedToolItem } from '@renderer/types/groups';
Expand Down Expand Up @@ -37,30 +38,11 @@ export const DefaultToolViewer: React.FC<DefaultToolViewerProps> = ({ linkedTool
</div>
</div>

{/* Output Section */}
{/* Output Section — Collapsed by default */}
{!linkedTool.isOrphaned && linkedTool.result && (
<div>
<div
className="mb-1 flex items-center gap-2 text-xs"
style={{ color: 'var(--tool-item-muted)' }}
>
Output
<StatusDot status={status} />
</div>
<div
className="max-h-96 overflow-auto rounded p-3 font-mono text-xs"
style={{
backgroundColor: 'var(--code-bg)',
border: '1px solid var(--code-border)',
color:
status === 'error'
? 'var(--tool-result-error-text)'
: 'var(--color-text-secondary)',
}}
>
{renderOutput(linkedTool.result.content)}
</div>
</div>
<CollapsibleOutputSection status={status}>
{renderOutput(linkedTool.result.content)}
</CollapsibleOutputSection>
)}
</>
);
Expand Down
51 changes: 44 additions & 7 deletions src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import React from 'react';

import { CodeBlockViewer } from '@renderer/components/chat/viewers';
import { CodeBlockViewer, MarkdownViewer } from '@renderer/components/chat/viewers';

import type { LinkedToolItem } from '@renderer/types/groups';

Expand Down Expand Up @@ -54,12 +54,49 @@ export const ReadToolViewer: React.FC<ReadToolViewerProps> = ({ linkedTool }) =>
? startLine + limit - 1
: 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.


return (
<CodeBlockViewer
fileName={filePath}
content={content}
startLine={startLine}
endLine={endLine}
/>
<div className="space-y-2">
{isMarkdownFile && (
<div className="flex items-center justify-end gap-1">
<button
type="button"
onClick={() => setViewMode('code')}
className="rounded px-2 py-1 text-xs transition-colors"
style={{
backgroundColor: viewMode === 'code' ? 'var(--tag-bg)' : 'transparent',
color: viewMode === 'code' ? 'var(--tag-text)' : 'var(--color-text-muted)',
border: '1px solid var(--tag-border)',
}}
>
Code
</button>
<button
type="button"
onClick={() => setViewMode('preview')}
className="rounded px-2 py-1 text-xs transition-colors"
style={{
backgroundColor: viewMode === 'preview' ? 'var(--tag-bg)' : 'transparent',
color: viewMode === 'preview' ? 'var(--tag-text)' : 'var(--color-text-muted)',
border: '1px solid var(--tag-border)',
}}
>
Preview
</button>
</div>
)}
{isMarkdownFile && viewMode === 'preview' ? (
<MarkdownViewer content={content} label="Markdown Preview" copyable />
) : (
<CodeBlockViewer
fileName={filePath}
content={content}
startLine={startLine}
endLine={endLine}
/>
)}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const WriteToolViewer: React.FC<WriteToolViewerProps> = ({ linkedTool })
const content = (toolUseResult?.content as string) || (linkedTool.input.content as string) || '';
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.


return (
<div className="space-y-2">
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/components/chat/items/linkedTool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* Exports all specialized tool viewer components.
*/

export { BashToolViewer } from './BashToolViewer';
export { CollapsibleOutputSection } from './CollapsibleOutputSection';
export { DefaultToolViewer } from './DefaultToolViewer';
export { EditToolViewer } from './EditToolViewer';
export { ReadToolViewer } from './ReadToolViewer';
Expand Down
Loading