mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-23 13:02:23 +02:00
[eric] reset history button (replaces dead View Changes) + thumbnail capture path fix
This commit is contained in:
@@ -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]}"` : '';
|
||||
|
||||
Reference in New Issue
Block a user