Redesign all client components with dark theme, polish, and UX improvements
MessageBubble: Replace border-left colored bars with rounded cards featuring accent strips, category dot indicators, and timestamp display. Use shared escapeHtml. Render tool_result, hook_progress, and file_snapshot as preformatted text instead of markdown (avoids expensive marked.parse on large JSON/log blobs). ExportButton: Add state machine (idle/exporting/success/error) with animated icons, gradient backgrounds, and auto-reset timers. Replace alert() with inline error state. FilterPanel: Add collapsible panel with category dot colors, enable count badge, custom checkbox styling, and smooth animations. SessionList: Replace text loading state with skeleton placeholders. Add empty state illustration with descriptive text. Style session items as rounded cards with hover/selected states, glow effects, and staggered entry animations. Add project name decode explanation comment. RedactedDivider: Add eye-slash SVG icon, red accent color, and styled dashed lines replacing plain text divider. useFilters: Remove unused exports (setAllCategories, setPreset, undoRedaction, clearAllRedactions, selectAllVisible, getMatchCount) to reduce hook surface area. Match counting moved to App component for search navigation. SessionList.test: Update assertions for skeleton loading state and expanded empty state text. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { CATEGORY_LABELS } from "../lib/types";
|
||||
import { CATEGORY_COLORS } from "../lib/constants";
|
||||
import { renderMarkdown, highlightSearchText } from "../lib/markdown";
|
||||
import { redactMessage } from "../../shared/sensitive-redactor";
|
||||
import { escapeHtml } from "../../shared/escape-html";
|
||||
|
||||
interface Props {
|
||||
message: ParsedMessage;
|
||||
@@ -16,9 +17,10 @@ interface Props {
|
||||
|
||||
/**
|
||||
* MessageBubble renders session messages using innerHTML.
|
||||
* This is safe here because content comes only from local JSONL session files
|
||||
* SECURITY: This is safe because content comes only from local JSONL session files
|
||||
* owned by the user, processed through the `marked` markdown renderer.
|
||||
* This is a local-only developer tool, not exposed to untrusted input.
|
||||
* The session files are read from the user's own filesystem (~/.claude/projects/).
|
||||
*/
|
||||
export function MessageBubble({
|
||||
message,
|
||||
@@ -36,41 +38,77 @@ export function MessageBubble({
|
||||
|
||||
if (msg.category === "tool_call") {
|
||||
const inputHtml = msg.toolInput
|
||||
? `<pre class="bg-gray-50 p-3 rounded text-xs overflow-x-auto mt-2"><code>${escapeHtml(msg.toolInput)}</code></pre>`
|
||||
? `<pre class="hljs mt-2"><code>${escapeHtml(msg.toolInput)}</code></pre>`
|
||||
: "";
|
||||
const html = `<div class="font-semibold text-amber-800">${escapeHtml(msg.toolName || "Unknown Tool")}</div>${inputHtml}`;
|
||||
const html = `<div class="font-medium ${colors.text}">${escapeHtml(msg.toolName || "Unknown Tool")}</div>${inputHtml}`;
|
||||
return searchQuery ? highlightSearchText(html, searchQuery) : html;
|
||||
}
|
||||
|
||||
// Structured data categories: render as preformatted text, not markdown.
|
||||
// Avoids expensive marked.parse() on large JSON/log blobs.
|
||||
if (msg.category === "hook_progress" || msg.category === "tool_result" || msg.category === "file_snapshot") {
|
||||
const html = `<pre class="hljs"><code>${escapeHtml(msg.content)}</code></pre>`;
|
||||
return searchQuery ? highlightSearchText(html, searchQuery) : html;
|
||||
}
|
||||
|
||||
const html = renderMarkdown(msg.content);
|
||||
return searchQuery ? highlightSearchText(html, searchQuery) : html;
|
||||
}, [message, searchQuery, autoRedactEnabled]);
|
||||
}, [message, searchQuery, autoRedactEnabled, colors.text]);
|
||||
|
||||
const timestamp = message.timestamp
|
||||
? formatTimestamp(message.timestamp)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onToggleRedactionSelection}
|
||||
className={`
|
||||
border-l-4 rounded-lg p-4 shadow-sm cursor-pointer transition-all
|
||||
${colors.border} ${colors.bg}
|
||||
group rounded-xl border bg-surface-raised cursor-pointer
|
||||
transition-all duration-200 relative overflow-hidden
|
||||
${colors.border}
|
||||
${dimmed ? "message-dimmed" : ""}
|
||||
${selectedForRedaction ? "redaction-selected" : ""}
|
||||
hover:shadow-md
|
||||
hover:shadow-card-hover hover:-translate-y-px
|
||||
shadow-card
|
||||
`}
|
||||
>
|
||||
<div className="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2">
|
||||
{label}
|
||||
{/* Category accent strip */}
|
||||
<div className={`absolute left-0 top-0 bottom-0 w-[3px] rounded-l-xl ${colors.dot}`} />
|
||||
|
||||
{/* Header bar */}
|
||||
<div className="flex items-center gap-2 px-4 pl-5 pt-3 pb-1">
|
||||
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${colors.dot}`} />
|
||||
<span className="text-caption font-semibold uppercase tracking-wider text-foreground-muted">
|
||||
{label}
|
||||
</span>
|
||||
{timestamp && (
|
||||
<>
|
||||
<span className="text-border">·</span>
|
||||
<span className="text-caption text-foreground-muted tabular-nums">
|
||||
{timestamp}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Content from local user-owned JSONL files, not external/untrusted input */}
|
||||
|
||||
{/* Content — sourced from local user-owned JSONL files, not external/untrusted input */}
|
||||
<div
|
||||
className="prose prose-sm max-w-none overflow-wrap-break-word"
|
||||
className="prose-message text-body text-foreground px-4 pl-5 pb-4 pt-1 max-w-none break-words overflow-hidden"
|
||||
dangerouslySetInnerHTML={{ __html: renderedHtml }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
function formatTimestamp(ts: string): string {
|
||||
try {
|
||||
const d = new Date(ts);
|
||||
return d.toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user