feat(web): workspace components — ComparePane, Memory page, McpDialog, error boundaries, message-parts

- Add ComparePane.tsx: side-by-side AI response comparison
- Add Memory.tsx: memory management page with CRUD UI
- Add McpPermissionDialog.tsx: MCP tool permission approval dialog
- Add McpResponseDisplay.tsx: MCP response visualization
- Add MessageBoundary.tsx + MessageListErrorBoundary.tsx: error resilience
- Add EmptyState.tsx: contextual empty state component
- Add KeyboardShortcutsDialog.tsx: keyboard shortcut reference
- Add message-parts/: ActionRow, CompactCard, MistakeRecoverySentinel, ReasoningBlock, SendToTerminalMenu, StatsLine, SummaryCard
- Add useDraftPersistence.ts: draft message persistence hook
- Add useTerminals.ts: terminal session management hook
- Add keyboard-shortcuts.ts + tool-utils.ts: shared utilities
- Extend components: ChatInput, MessageBubble, MessageList, Workspace, panes
- Extend hooks: useTerminalSocket, useSessionStream test suite
- Update pages: Home, Project — workspace layout and session flow
This commit is contained in:
2026-06-08 03:49:22 +00:00
parent 51733c1338
commit 50de80ee75
51 changed files with 3352 additions and 96 deletions

View File

@@ -8,6 +8,7 @@ import Markdown from 'react-markdown';
import type { Components } from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { CodeBlock } from './CodeBlock';
import { MessageBoundary } from './MessageBoundary';
import { linkifyPaths } from '@/lib/linkify-paths';
function linkifyChildren(children: ReactNode, keyPrefix = 'l'): ReactNode {
@@ -40,7 +41,11 @@ const codeRenderer = (props: { children?: unknown; className?: string }) => {
const langMatch = /language-([\w-]+)/.exec(className ?? '');
const isBlock = !!langMatch || text.includes('\n');
if (isBlock) {
return <CodeBlock code={text} lang={langMatch?.[1]} />;
return (
<MessageBoundary fallback={<pre className="overflow-x-auto px-3 py-2 font-mono text-xs leading-relaxed">{text}</pre>}>
<CodeBlock code={text} lang={langMatch?.[1]} />
</MessageBoundary>
);
}
return (
<code
@@ -102,8 +107,10 @@ const MARKDOWN_COMPONENTS: Components = {
export const MarkdownRenderer = memo(function MarkdownRenderer({ content }: { content: string }) {
return (
<Markdown remarkPlugins={[remarkGfm]} components={MARKDOWN_COMPONENTS}>
{content}
</Markdown>
<MessageBoundary>
<Markdown remarkPlugins={[remarkGfm]} components={MARKDOWN_COMPONENTS}>
{content}
</Markdown>
</MessageBoundary>
);
});