389 lines
12 KiB
Markdown
389 lines
12 KiB
Markdown
---
|
|
name: ccc:chatgroup-architecture
|
|
description: ChatGroup architecture — how conversation data flows from raw JSONL to rendered chat groups. Use when working on UserGroup, AIGroup, SystemGroup, display items, tool linking, chunks, or the rendering hierarchy.
|
|
---
|
|
|
|
# ChatGroup Architecture
|
|
|
|
How conversation data flows from raw JSONL messages to rendered chat groups.
|
|
|
|
## Core Design Principle
|
|
|
|
Chat groups are **independent items in a flat chronological list**, not paired turns.
|
|
There is no UserTurn/AITurn pairing — each group stands alone.
|
|
|
|
```typescript
|
|
// src/renderer/types/groups.ts
|
|
export type ChatItem =
|
|
| { type: 'user'; group: UserGroup }
|
|
| { type: 'system'; group: SystemGroup }
|
|
| { type: 'ai'; group: AIGroup }
|
|
| { type: 'compact'; group: CompactGroup };
|
|
|
|
export interface SessionConversation {
|
|
sessionId: string;
|
|
items: ChatItem[]; // Flat chronological list
|
|
totalUserGroups: number;
|
|
totalSystemGroups: number;
|
|
totalAIGroups: number;
|
|
totalCompactGroups: number;
|
|
}
|
|
```
|
|
|
|
## Pipeline Overview
|
|
|
|
```
|
|
Raw JSONL messages
|
|
→ MessageClassifier (classify into user/system/ai/hardNoise)
|
|
→ ChunkBuilder (buffer AI messages, flush on user/system boundary)
|
|
→ ChunkFactory (build EnhancedAIChunk with SemanticSteps)
|
|
→ groupTransformer (chunks → flat ChatItem[] conversation)
|
|
→ aiGroupEnhancer (AIGroup → EnhancedAIGroup with displayItems, linkedTools, lastOutput)
|
|
→ React components render
|
|
```
|
|
|
|
Primary source files:
|
|
|
|
- `src/main/services/parsing/MessageClassifier.ts`
|
|
- `src/main/services/analysis/ChunkBuilder.ts`
|
|
- `src/main/services/analysis/ChunkFactory.ts`
|
|
- `src/renderer/utils/groupTransformer.ts`
|
|
- `src/renderer/utils/aiGroupEnhancer.ts`
|
|
- `src/renderer/utils/displayItemBuilder.ts`
|
|
- `src/renderer/types/groups.ts`
|
|
- `src/main/types/chunks.ts`
|
|
|
|
## Data Models
|
|
|
|
### UserGroup
|
|
|
|
```typescript
|
|
// src/renderer/types/groups.ts
|
|
interface UserGroup {
|
|
id: string;
|
|
message: ParsedMessage;
|
|
timestamp: Date;
|
|
content: UserGroupContent;
|
|
index: number; // Ordering index within session
|
|
}
|
|
|
|
interface UserGroupContent {
|
|
text?: string; // Plain text (commands removed)
|
|
rawText?: string; // Original text
|
|
commands: CommandInfo[]; // Extracted /commands
|
|
images: ImageData[]; // Attached images
|
|
fileReferences: FileReference[]; // @file.ts mentions
|
|
}
|
|
```
|
|
|
|
Renders right-aligned blue bubble. Contains markdown text, slash commands, images, and file references.
|
|
|
|
### AIGroup
|
|
|
|
```typescript
|
|
interface AIGroup {
|
|
id: string;
|
|
turnIndex: number; // 0-based (for turn navigation)
|
|
startTime: Date;
|
|
endTime: Date;
|
|
durationMs: number;
|
|
steps: SemanticStep[]; // Core semantic steps
|
|
tokens: AIGroupTokens;
|
|
summary: AIGroupSummary; // For collapsed view
|
|
status: AIGroupStatus; // 'complete' | 'interrupted' | 'error' | 'in_progress'
|
|
processes: Process[]; // Subagent processes
|
|
chunkId: string;
|
|
metrics: SessionMetrics;
|
|
responses: ParsedMessage[]; // All assistant + internal messages
|
|
isOngoing?: boolean; // True for last group in ongoing session
|
|
}
|
|
```
|
|
|
|
### EnhancedAIGroup
|
|
|
|
The renderer enhances `AIGroup` before rendering:
|
|
|
|
```typescript
|
|
interface EnhancedAIGroup extends AIGroup {
|
|
lastOutput: AIGroupLastOutput | null; // Always-visible output
|
|
displayItems: AIGroupDisplayItem[]; // Flattened chronological items
|
|
linkedTools: Map<string, LinkedToolItem>; // Tool call/result pairs
|
|
itemsSummary: string; // "2 thinking, 4 tool calls, 3 subagents"
|
|
mainModel: ModelInfo | null;
|
|
subagentModels: ModelInfo[];
|
|
claudeMdStats: ClaudeMdStats | null;
|
|
}
|
|
```
|
|
|
|
Enhancement happens in `src/renderer/utils/aiGroupEnhancer.ts`:
|
|
|
|
1. `findLastOutput` — extracts the final visible output (text, tool result, interruption, plan exit, ongoing)
|
|
2. `linkToolCallsToResults` — pairs tool calls with their results into `LinkedToolItem`
|
|
3. `buildDisplayItems` — flattens steps into chronological `AIGroupDisplayItem[]`
|
|
4. `buildSummary` — generates human-readable summary string
|
|
5. `extractMainModel` / `extractSubagentModels` — extracts model info
|
|
|
|
### AIGroupDisplayItem
|
|
|
|
```typescript
|
|
type AIGroupDisplayItem =
|
|
| { type: 'thinking'; content: string; timestamp: Date; tokenCount?: number }
|
|
| { type: 'tool'; tool: LinkedToolItem }
|
|
| { type: 'subagent'; subagent: Process }
|
|
| { type: 'output'; content: string; timestamp: Date; tokenCount?: number }
|
|
| { type: 'slash'; slash: SlashItem }
|
|
| { type: 'teammate_message'; teammateMessage: TeammateMessage };
|
|
```
|
|
|
|
Display items are sorted chronologically in `src/renderer/utils/displayItemBuilder.ts`.
|
|
|
|
### LinkedToolItem
|
|
|
|
```typescript
|
|
interface LinkedToolItem {
|
|
id: string;
|
|
name: string;
|
|
input: Record<string, unknown>;
|
|
callTokens?: number;
|
|
result?: {
|
|
content: string | unknown[];
|
|
isError: boolean;
|
|
toolUseResult?: ToolUseResultData;
|
|
tokenCount?: number;
|
|
};
|
|
inputPreview: string; // First 100 chars
|
|
outputPreview?: string; // First 200 chars
|
|
startTime: Date;
|
|
endTime?: Date;
|
|
durationMs?: number;
|
|
isOrphaned: boolean; // No result received
|
|
sourceModel?: string;
|
|
skillInstructions?: string; // For Skill tool calls
|
|
skillInstructionsTokenCount?: number;
|
|
}
|
|
```
|
|
|
|
### AIGroupLastOutput
|
|
|
|
Always-visible output below the AI group header:
|
|
|
|
```typescript
|
|
interface AIGroupLastOutput {
|
|
type: 'text' | 'tool_result' | 'interruption' | 'ongoing' | 'plan_exit';
|
|
text?: string;
|
|
toolName?: string;
|
|
toolResult?: string;
|
|
isError?: boolean;
|
|
interruptionMessage?: string;
|
|
planContent?: string;
|
|
planPreamble?: string;
|
|
timestamp: Date;
|
|
}
|
|
```
|
|
|
|
### SystemGroup
|
|
|
|
```typescript
|
|
interface SystemGroup {
|
|
id: string;
|
|
message: ParsedMessage;
|
|
timestamp: Date;
|
|
commandOutput: string;
|
|
commandName?: string;
|
|
}
|
|
```
|
|
|
|
Renders left-aligned with neutral gray styling. Monospace pre with ANSI escape codes cleaned.
|
|
|
|
### CompactGroup
|
|
|
|
```typescript
|
|
interface CompactGroup {
|
|
id: string;
|
|
timestamp: Date;
|
|
message: ParsedMessage;
|
|
tokenDelta?: CompactionTokenDelta;
|
|
startingPhaseNumber?: number;
|
|
}
|
|
```
|
|
|
|
Visual boundary for context compaction events.
|
|
|
|
## Backend: Chunk Building
|
|
|
|
### Message Classification
|
|
|
|
`src/main/services/parsing/MessageClassifier.ts` classifies raw messages into 4 categories:
|
|
|
|
| Category | Description | Result |
|
|
|----------|-------------|--------|
|
|
| `user` | Genuine user input | Creates UserChunk, renders right |
|
|
| `system` | Command output | Creates SystemChunk, renders left |
|
|
| `ai` | Assistant responses | Buffered into AIChunk, renders left |
|
|
| `hardNoise` | System metadata, caveats, reminders | Filtered out entirely |
|
|
|
|
### Chunk Building
|
|
|
|
`src/main/services/analysis/ChunkBuilder.ts` buffers AI messages and flushes on boundaries:
|
|
|
|
```
|
|
for each classified message:
|
|
hardNoise → skip
|
|
compact → flush AI buffer, emit CompactChunk
|
|
user → flush AI buffer, emit UserChunk
|
|
system → flush AI buffer, emit SystemChunk
|
|
ai → add to AI buffer
|
|
```
|
|
|
|
AI buffer is flushed when a non-AI message arrives, producing one `AIChunk` per contiguous run of assistant messages.
|
|
|
|
### Semantic Step Extraction
|
|
|
|
`src/main/services/analysis/ChunkFactory.ts` enriches AI chunks:
|
|
|
|
1. Build tool executions from message content blocks
|
|
2. Collect sidechain messages within the time range
|
|
3. Link subagent processes to the chunk
|
|
4. Extract semantic steps (`SemanticStep[]`)
|
|
5. Fill timeline gaps
|
|
6. Calculate step context accumulation
|
|
7. Build step groups
|
|
|
|
```typescript
|
|
type SemanticStepType = 'thinking' | 'tool_call' | 'tool_result' | 'subagent' | 'output' | 'interruption';
|
|
|
|
interface SemanticStep {
|
|
id: string;
|
|
type: SemanticStepType;
|
|
startTime: Date;
|
|
endTime?: Date;
|
|
durationMs: number;
|
|
content: { /* type-specific fields */ };
|
|
tokens?: { input: number; output: number; cached?: number };
|
|
context: 'main' | 'subagent';
|
|
}
|
|
```
|
|
|
|
## Rendering
|
|
|
|
### Component Hierarchy
|
|
|
|
```
|
|
ChatHistory
|
|
└─ ChatHistoryItem (router)
|
|
├─ UserChatGroup → right-aligned blue bubble
|
|
├─ SystemChatGroup → left-aligned gray block
|
|
├─ AIChatGroup → left-aligned, collapsible
|
|
│ ├─ Header (model, summary, tokens, duration, timestamp)
|
|
│ ├─ DisplayItemList (when expanded)
|
|
│ │ ├─ ThinkingItem
|
|
│ │ ├─ LinkedToolItem (Read, Edit, Write, Skill, etc.)
|
|
│ │ ├─ SubagentItem (with nested trace)
|
|
│ │ ├─ TextItem
|
|
│ │ ├─ SlashItem
|
|
│ │ └─ TeammateMessageItem
|
|
│ └─ LastOutputDisplay (always visible)
|
|
└─ CompactBoundary
|
|
```
|
|
|
|
Primary render files:
|
|
|
|
- `src/renderer/components/chat/ChatHistory.tsx`
|
|
- `src/renderer/components/chat/ChatHistoryItem.tsx`
|
|
- `src/renderer/components/chat/UserChatGroup.tsx`
|
|
- `src/renderer/components/chat/SystemChatGroup.tsx`
|
|
- `src/renderer/components/chat/AIChatGroup.tsx`
|
|
- `src/renderer/components/chat/DisplayItemList.tsx`
|
|
- `src/renderer/components/chat/LastOutputDisplay.tsx`
|
|
|
|
### AI Group: Collapsed vs Expanded
|
|
|
|
**Collapsed** (default):
|
|
- Header: Bot icon, "Claude", model badge, items summary, chevron
|
|
- Right side: context badge, token usage, duration, timestamp
|
|
- Last output (always visible below header)
|
|
|
|
**Expanded**:
|
|
- All collapsed content, plus:
|
|
- `DisplayItemList` with chronologically ordered items
|
|
- Each display item can be individually expanded (nested expansion)
|
|
|
|
### Last Output Rendering
|
|
|
|
Always visible regardless of expansion state. Renders based on `type`:
|
|
|
|
| Type | Rendering |
|
|
|------|-----------|
|
|
| `text` | Markdown in code-bg rounded block |
|
|
| `tool_result` | Tool name + pre-formatted result |
|
|
| `interruption` | Warning banner with AlertTriangle |
|
|
| `plan_exit` | Plan preamble + plan content in special block |
|
|
| `ongoing` | Ongoing session banner (last AI group only) |
|
|
|
|
## Per-Tab UI State Isolation
|
|
|
|
Each tab maintains **completely independent** expansion state via `tabUISlice.ts`:
|
|
|
|
```typescript
|
|
interface TabUIState {
|
|
expandedAIGroupIds: Set<string>;
|
|
expandedDisplayItemIds: Map<string, Set<string>>;
|
|
expandedSubagentTraceIds: Set<string>;
|
|
showContextPanel: boolean;
|
|
selectedContextPhase: number | null;
|
|
savedScrollTop?: number;
|
|
}
|
|
```
|
|
|
|
Accessed via `useTabUI()` hook which reads `tabId` from `TabUIContext`:
|
|
|
|
```typescript
|
|
// src/renderer/hooks/useTabUI.tsx
|
|
const { isAIGroupExpanded, toggleAIGroupExpansion, expandAIGroup,
|
|
getExpandedDisplayItemIds, toggleDisplayItemExpansion,
|
|
isSubagentTraceExpanded, toggleSubagentTraceExpansion } = useTabUI();
|
|
```
|
|
|
|
Auto-expansion triggers:
|
|
- Error deep linking (contains highlighted error tool)
|
|
- Search results (contains search match)
|
|
|
|
## Store Integration
|
|
|
|
### Session Detail Slice
|
|
|
|
`src/renderer/store/slices/sessionDetailSlice.ts` manages the fetch pipeline:
|
|
|
|
```
|
|
fetchSessionDetail(projectId, sessionId, tabId?)
|
|
→ IPC: getSessionDetail (returns chunks + processes)
|
|
→ transformChunksToConversation (chunks → ChatItem[])
|
|
→ processSessionClaudeMd (compute CLAUDE.md stats)
|
|
→ processSessionContextWithPhases (compute context stats)
|
|
→ store in global state + per-tab tabSessionData
|
|
```
|
|
|
|
Key state:
|
|
|
|
| Field | Purpose |
|
|
|-------|---------|
|
|
| `sessionDetail` | Raw session data from main process |
|
|
| `conversation` | Transformed `SessionConversation` |
|
|
| `conversationLoading` | True during fetch (causes ChatHistory unmount) |
|
|
| `tabSessionData` | Per-tab copies of session data |
|
|
| `sessionClaudeMdStats` | CLAUDE.md injection stats per AI group |
|
|
| `sessionContextStats` | Context stats per AI group |
|
|
| `sessionPhaseInfo` | Phase boundary info |
|
|
|
|
### Real-Time Updates
|
|
|
|
`refreshSessionInPlace` re-fetches and transforms without setting `conversationLoading: true`, avoiding ChatHistory unmount/remount flicker.
|
|
|
|
## Invariants
|
|
|
|
1. Chat items are always flat and chronological — no nesting at the conversation level.
|
|
2. AI groups are self-contained — all semantic steps, tool links, and display items are computed per group.
|
|
3. Display items within an AI group are chronologically sorted.
|
|
4. Per-tab UI state is fully isolated — expanding a group in one tab doesn't affect another.
|
|
5. Last output is always visible regardless of AI group expansion state.
|
|
6. `conversationLoading: true` unmounts ChatHistory — avoid setting it unnecessarily for existing tabs.
|