42861ea7fa
Multi-adapter mobile UI for AI coding assistants. Supports Claude Code, Codex CLI, and Gemini CLI through one interface. Features: - Real-time bidirectional sync via tmux + WebSocket - Cross-AI review (send one AI's output to another for review) - Multi-review tabs with minimize/expand - Push notifications (PWA) with smart session-aware filtering - Three-channel event system (hooks, file watcher, pane monitor) - Voice input, image paste, draft persistence - Terminal-native design (JetBrains Mono, dark theme, pixel art claw) - CLI with --adapter flag on every command - Zero-overhead fire-and-forget hooks
4.9 KiB
4.9 KiB
InsightBlock — Adapter-Specific Content Rendering
Problem
Claude Code produces "Insight" blocks in its markdown responses:
`★ Insight ─────────────────────────────────────`
[educational content]
`─────────────────────────────────────────────────`
These currently render as ugly inline <code> elements in ReactMarkdown. Need a dedicated, collapsible UI component — while keeping the architecture extensible for other adapters (Gemini, Codex) that may have their own text patterns.
Design
Approach: Frontend Text Transform with Adapter-Scoped Patterns
- No server changes. The Insight text flows through the existing pipeline as
{ type: 'text' }content blocks. - Regex patterns defined per-adapter in adapter-scoped files.
- Generic splitter in
src/lib/accepts patterns as parameters — no coupling to any specific adapter. - Collapsible card UI matching ToolCallCard's expand/collapse pattern.
File Structure
src/
├── lib/
│ └── text-transforms.ts # Generic: splitTextSegments(text, patterns)
├── components/
│ ├── adapters/
│ │ └── claude/
│ │ ├── InsightBlock.tsx # Collapsible insight card
│ │ └── patterns.ts # INSIGHT_RE regex + segment type
│ ├── MessageBubble.tsx # Modified: split → map → render
│ └── ...
Dependency direction:
MessageBubble → text-transforms (generic)
MessageBubble → adapters/claude/patterns (adapter-specific)
MessageBubble → adapters/claude/InsightBlock (adapter-specific)
Generic code never imports adapter-specific code. MessageBubble is the composition root.
src/components/adapters/claude/patterns.ts
Exports Claude-specific text patterns:
import type { TextPattern } from '@/lib/text-transforms';
export const CLAUDE_PATTERNS: TextPattern[] = [
{
type: 'insight',
regex: /`[★✦]?\s*Insight[─\-\s]*`\n([\s\S]*?)\n`[─\-]+[.。]?`/g,
},
];
src/lib/text-transforms.ts
Generic segment splitter — adapter-agnostic:
export interface TextPattern {
type: string;
regex: RegExp;
}
export type TextSegment = {
type: string; // 'markdown' | 'insight' | future types
text: string;
};
export function splitTextSegments(text: string, patterns: TextPattern[]): TextSegment[] {
// Fast path: no patterns or no text → return as-is
// For each pattern, find all matches and record their positions
// Split text into alternating markdown/matched segments
// Streaming safety: unmatched opening fence → treat as plain markdown
}
src/components/adapters/claude/InsightBlock.tsx
Collapsible card — Style C from brainstorming:
- Collapsed (default):
★icon + "Insight" label + first-line summary (truncated) +▼chevron - Expanded: Full content rendered via ReactMarkdown with prose styling
- Visual:
bg-surface/30 border border-border/50 rounded-lg— subtle card with accent star
src/components/MessageBubble.tsx Changes
+ import { splitTextSegments } from '@/lib/text-transforms';
+ import { CLAUDE_PATTERNS } from './adapters/claude/patterns';
+ import { InsightBlock } from './adapters/claude/InsightBlock';
// In assistant message render:
const textContent = content.filter(...).map(...).join('');
+ const segments = splitTextSegments(textContent, CLAUDE_PATTERNS);
- <ReactMarkdown ...>{textContent}</ReactMarkdown>
+ {segments.map((seg, i) =>
+ seg.type === 'insight'
+ ? <InsightBlock key={i} text={seg.text} />
+ : <ReactMarkdown key={i} components={markdownComponents}>{seg.text}</ReactMarkdown>
+ )}
Extensibility
When Gemini adds "Analysis" blocks:
src/components/adapters/gemini/patterns.ts— exportGEMINI_PATTERNSsrc/components/adapters/gemini/AnalysisBlock.tsx— new componentMessageBubble.tsx— merge patterns:[...CLAUDE_PATTERNS, ...GEMINI_PATTERNS]- Add one more segment type case in the render map
No server changes. No refactoring. Explicit additions only.
Edge Cases
- Streaming: Partial insight (opening fence without closing) →
splitTextSegmentstreats as plain markdown. InsightBlock only renders when both fences are present. - No insights: Fast path —
splitTextSegmentsreturns[{ type: 'markdown', text }], single ReactMarkdown render. No performance regression. - Multiple insights: Each becomes its own InsightBlock in the segments array, with markdown segments between them.
- Nested markdown in insight body: ReactMarkdown inside InsightBlock handles bullet points, code, links.
Not Changing
- Server pipeline (TranscriptParser, session-manager, IAdapter)
- ChatView.tsx (Insight is text-layer, not content-block-layer)
- useChat.ts
- WS protocol