[eric] reset history button (replaces dead View Changes) + thumbnail capture path fix

This commit is contained in:
ciregenz
2026-05-11 10:19:36 -07:00
parent 3342c71ed1
commit 741fea5c89
3 changed files with 39 additions and 150 deletions
+22 -2
View File
@@ -15,6 +15,7 @@ import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import CheckIcon from '@mui/icons-material/Check';
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { openSettingsModal } from '@/shared/state/settingsSlice';
import { API_BASE, getAuthToken } from '@/shared/config';
@@ -35,6 +36,7 @@ import {
updateThinkingLevel,
fetchSession,
AgentMessage,
clearSessionMessages,
} from '@/shared/state/agentsSlice';
import { fetchModes } from '@/shared/state/modesSlice';
import { createSessionWs } from '@/shared/ws/WebSocketManager';
@@ -48,7 +50,6 @@ import ChatInput, { ChatInputHandle } from './ChatInput';
import ContextDrawer from './ContextDrawer';
import { ErrorSlime } from '@/app/components/ErrorSlime';
import { ContextPath } from '@/app/components/DirectoryBrowser';
import DiffViewer from './DiffViewer';
import { setGlowingBrowserCards, fadeGlowingBrowserCards, clearGlowingBrowserCards } from '@/shared/state/dashboardLayoutSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
@@ -911,7 +912,26 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
</Box>
)}
</Box>
{!isDraft && id && <DiffViewer sessionId={id} />}
{!isDraft && id && (
<Tooltip title="Reset history">
<IconButton
size="small"
onClick={async () => {
const sid = id;
try {
const tok = (() => { try { return getAuthToken(); } catch { return ''; } })();
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (tok) headers['Authorization'] = `Bearer ${tok}`;
await fetch(`${API_BASE}/agents/sessions/${sid}/clear`, { method: 'POST', headers });
} catch { /* surfaced via context_status */ }
dispatch(clearSessionMessages(sid));
}}
sx={{ color: c.text.tertiary, '&:hover': { color: c.text.primary } }}
>
<RestartAltIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
{onClose && (
<IconButton onClick={onClose} size="small" sx={{ color: c.text.tertiary, '&:hover': { color: c.text.primary } }}>
<CloseIcon fontSize="small" />
@@ -1,145 +0,0 @@
import React, { useState, useEffect } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import RefreshIcon from '@mui/icons-material/Refresh';
import DifferenceIcon from '@mui/icons-material/Difference';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { API_BASE } from '@/shared/config';
import { Skeleton } from '@/app/components/Loading';
const AGENTS_API = `${API_BASE}/agents`;
interface Props {
sessionId: string;
}
const DiffViewer: React.FC<Props> = ({ sessionId }) => {
const c = useClaudeTokens();
const [diff, setDiff] = useState<string>('');
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const fetchDiff = async () => {
setLoading(true);
try {
const res = await fetch(`${AGENTS_API}/sessions/${sessionId}/diff`);
const data = await res.json();
setDiff(data.diff || '');
} catch {
setDiff('Failed to fetch diff');
}
setLoading(false);
};
useEffect(() => {
if (open) fetchDiff();
}, [open, sessionId]);
if (!open) {
return (
<Tooltip title="View changes">
<IconButton onClick={() => setOpen(true)} sx={{ color: c.text.tertiary }}>
<DifferenceIcon />
</IconButton>
</Tooltip>
);
}
return (
<Box
sx={{
width: 400,
flexShrink: 0,
boxShadow: '-1px 0 4px rgba(0,0,0,0.04)',
bgcolor: c.bg.surface,
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 2,
py: 1,
borderBottom: `0.5px solid ${c.border.medium}`,
bgcolor: c.bg.secondary,
}}
>
<Typography sx={{ color: c.text.primary, fontWeight: 600, fontSize: '0.85rem' }}>
Worktree Changes
</Typography>
<Box sx={{ display: 'flex', gap: 0.5 }}>
<Tooltip title="Refresh">
<IconButton size="small" onClick={fetchDiff} sx={{ color: c.text.tertiary }}>
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
<IconButton size="small" onClick={() => setOpen(false)} sx={{ color: c.text.tertiary }}>
<Typography sx={{ fontSize: '0.85rem' }}>×</Typography>
</IconButton>
</Box>
</Box>
<Box
sx={{
flex: 1,
overflow: 'auto',
p: 1.5,
'&::-webkit-scrollbar': { width: 5, height: 5 },
'&::-webkit-scrollbar-track': { background: 'transparent' },
'&::-webkit-scrollbar-thumb': {
background: c.border.medium,
borderRadius: 3,
'&:hover': { background: c.border.strong },
},
scrollbarWidth: 'thin',
scrollbarColor: `${c.border.medium} transparent`,
}}
>
{loading ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{[0, 1, 2, 3, 4, 5, 6].map((i) => (
<Skeleton key={i} variant="line" width={`${60 + (i * 7) % 30}%`} height={10} />
))}
</Box>
) : diff ? (
<pre
style={{
margin: 0,
fontSize: '0.72rem',
fontFamily: c.font.mono,
lineHeight: 1.6,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{diff.split('\n').map((line, i) => {
let color = c.text.muted;
if (line.startsWith('+') && !line.startsWith('+++')) color = c.status.success;
else if (line.startsWith('-') && !line.startsWith('---')) color = c.status.error;
else if (line.startsWith('@@')) color = c.accent.primary;
else if (line.startsWith('diff ') || line.startsWith('index ')) color = c.text.tertiary;
return (
<span key={i} style={{ color }}>
{line}
{'\n'}
</span>
);
})}
</pre>
) : (
<Typography sx={{ color: c.text.ghost, fontSize: '0.8rem' }}>
No changes detected in the worktree.
</Typography>
)}
</Box>
</Box>
);
};
export default DiffViewer;
@@ -3,7 +3,21 @@ import { toJpeg } from 'html-to-image';
const CAPTURE_WIDTH = 1280;
const CAPTURE_HEIGHT = 800;
const JPEG_QUALITY = 0.7;
const LOAD_TIMEOUT_MS = 3000;
const LOAD_TIMEOUT_MS = 4000;
// Workspace file keys are stored relative to the workspace root with no
// leading `./` or `/` — but agent-written HTML routinely references its
// siblings as `./style.css` or `/style.css`. Without normalizing here,
// `files[href]` lookup misses and the iframe renders unstyled, producing
// the broken thumbnails (text-only Markdown Editor, layoutless Calculator,
// etc.) you'd otherwise see on the Apps page.
function lookupFile(href: string, files: Record<string, string>): string | null {
const candidates = [href, href.replace(/^\.\//, ''), href.replace(/^\//, '')];
for (const k of candidates) {
if (k in files) return files[k];
}
return null;
}
/**
* Inline local CSS/JS references so multi-file views render in a single srcdoc.
@@ -19,7 +33,7 @@ function inlineResources(html: string, files: Record<string, string>): string {
if (!hrefMatch) return match;
const href = hrefMatch[1];
if (/^(https?:)?\/\//.test(href)) return match;
const content = files[href];
const content = lookupFile(href, files);
if (content == null) return match;
return `<style>\n${content}\n</style>`;
},
@@ -32,7 +46,7 @@ function inlineResources(html: string, files: Record<string, string>): string {
if (!srcMatch) return match;
const src = srcMatch[1];
if (/^(https?:)?\/\//.test(src)) return match;
const content = files[src];
const content = lookupFile(src, files);
if (content == null) return match;
const typeMatch = attrs.match(/type=["']([^"']+)["']/);
const typeAttr = typeMatch ? ` type="${typeMatch[1]}"` : '';