• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

agentic-dev-library / thumbcode / 21118375421

18 Jan 2026 08:48PM UTC coverage: 43.195% (-2.1%) from 45.31%
21118375421

push

github

web-flow
feat(chat): implement real-time chat system for human-agent collaboration (#49)

Implements Issue #12 - Real-time chat system with:

**ChatService** (src/services/chat/):
- Thread management (create, delete, pin, mark as read)
- Message handling with streaming support
- Approval workflow (request/respond)
- Search across threads
- Event system for real-time updates
- Typing indicators
- Abort/cancel support for long-running requests

**UI Components** (src/components/chat/):
- ChatMessage: Renders messages with support for text, code, and approval types
- ChatThread: Scrollable message list with auto-scroll and typing indicators
- ChatInput: Input field with send functionality
- ThreadList: Thread list with pinned/recent sections and unread badges
- ApprovalCard: Action approval UI with approve/reject buttons
- CodeBlock: Code display with language header and copy functionality

**Tests**:
- 129 total tests passing (8 test suites)
- Comprehensive ChatService tests covering all functionality

All components follow ThumbCode brand guidelines with organic daube styling.

Closes #12

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

181 of 616 branches covered (29.38%)

Branch coverage included in aggregate %.

44 of 174 new or added lines in 7 files covered. (25.29%)

495 of 949 relevant lines covered (52.16%)

1.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/components/chat/ThreadList.tsx
1
/**
2
 * Thread List Component
3
 *
4
 * Displays a list of chat threads with pinned threads at the top.
5
 * Uses organic styling with visual indicators for unread messages.
6
 */
7

8
import {
9
  type ChatThread,
10
  selectPinnedThreads,
11
  selectRecentThreads,
12
  useChatStore,
13
} from '@thumbcode/state';
14
import { FlatList, Pressable, Text, View } from 'react-native';
15

16
interface ThreadListProps {
17
  onSelectThread: (threadId: string) => void;
18
  onCreateThread?: () => void;
19
}
20

21
interface ThreadItemProps {
22
  thread: ChatThread;
23
  onPress: () => void;
24
}
25

26
/**
27
 * Format relative time for thread
28
 */
29
function formatRelativeTime(timestamp: string): string {
NEW
30
  const date = new Date(timestamp);
×
NEW
31
  const now = new Date();
×
NEW
32
  const diff = now.getTime() - date.getTime();
×
33

NEW
34
  const minutes = Math.floor(diff / 60000);
×
NEW
35
  const hours = Math.floor(minutes / 60);
×
NEW
36
  const days = Math.floor(hours / 24);
×
37

NEW
38
  if (minutes < 1) return 'Just now';
×
NEW
39
  if (minutes < 60) return `${minutes}m ago`;
×
NEW
40
  if (hours < 24) return `${hours}h ago`;
×
NEW
41
  if (days < 7) return `${days}d ago`;
×
NEW
42
  return date.toLocaleDateString();
×
43
}
44

45
/**
46
 * Get participant badge colors
47
 */
48
function getParticipantBadge(participant: ChatThread['participants'][number]) {
NEW
49
  const colorMap: Record<string, string> = {
×
50
    architect: 'bg-coral-500',
51
    implementer: 'bg-gold-500',
52
    reviewer: 'bg-teal-500',
53
    tester: 'bg-neutral-500',
54
  };
NEW
55
  return colorMap[participant] || 'bg-neutral-600';
×
56
}
57

58
function ThreadItem({ thread, onPress }: ThreadItemProps) {
NEW
59
  const hasUnread = thread.unreadCount > 0;
×
60

NEW
61
  return (
×
62
    <Pressable
63
      onPress={onPress}
64
      className="bg-surface-elevated p-4 mb-2 active:bg-neutral-700"
65
      style={{
66
        borderRadius: '14px 12px 16px 10px',
67
        transform: [{ rotate: '-0.2deg' }],
68
      }}
69
    >
70
      <View className="flex-row items-start justify-between">
71
        <View className="flex-1 mr-3">
72
          {/* Title with unread indicator */}
73
          <View className="flex-row items-center mb-1">
74
            {thread.isPinned && <Text className="mr-1">📌</Text>}
×
75
            <Text
76
              className={`font-display text-base ${hasUnread ? 'text-white' : 'text-neutral-200'}`}
×
77
              numberOfLines={1}
78
            >
79
              {thread.title}
80
            </Text>
81
          </View>
82

83
          {/* Participants */}
84
          <View className="flex-row items-center mb-1">
85
            {thread.participants
NEW
86
              .filter((p) => p !== 'user')
×
87
              .slice(0, 3)
88
              .map((participant, index) => (
NEW
89
                <View
×
90
                  key={participant}
91
                  className={`w-2 h-2 rounded-full ${getParticipantBadge(participant)} ${
92
                    index > 0 ? 'ml-1' : ''
×
93
                  }`}
94
                />
95
              ))}
96
            {thread.participants.length > 4 && (
×
97
              <Text className="text-xs text-neutral-500 ml-1">
98
                +{thread.participants.length - 4}
99
              </Text>
100
            )}
101
          </View>
102

103
          {/* Timestamp */}
104
          <Text className="text-xs text-neutral-500 font-body">
105
            {formatRelativeTime(thread.lastMessageAt)}
106
          </Text>
107
        </View>
108

109
        {/* Unread badge */}
110
        {hasUnread && (
×
111
          <View
112
            className="bg-coral-500 px-2 py-0.5 min-w-[20px] items-center"
113
            style={{ borderRadius: '8px 10px 8px 12px' }}
114
          >
115
            <Text className="text-xs font-body text-white font-semibold">
116
              {thread.unreadCount > 99 ? '99+' : thread.unreadCount}
×
117
            </Text>
118
          </View>
119
        )}
120
      </View>
121
    </Pressable>
122
  );
123
}
124

125
export function ThreadList({ onSelectThread, onCreateThread }: ThreadListProps) {
NEW
126
  const pinnedThreads = useChatStore(selectPinnedThreads);
×
NEW
127
  const recentThreads = useChatStore(selectRecentThreads);
×
128

NEW
129
  const allThreads = [...pinnedThreads, ...recentThreads];
×
130

NEW
131
  if (allThreads.length === 0) {
×
NEW
132
    return (
×
133
      <View className="flex-1 items-center justify-center p-6">
134
        <Text className="font-display text-lg text-neutral-400 text-center mb-2">
135
          No conversations yet
136
        </Text>
137
        <Text className="font-body text-sm text-neutral-500 text-center mb-4">
138
          Start a new thread to collaborate with AI agents
139
        </Text>
140
        {onCreateThread && (
×
141
          <Pressable
142
            onPress={onCreateThread}
143
            className="bg-coral-500 px-6 py-3 active:bg-coral-600"
144
            style={{ borderRadius: '12px 14px 10px 16px' }}
145
          >
146
            <Text className="font-body text-white font-semibold">New Thread</Text>
147
          </Pressable>
148
        )}
149
      </View>
150
    );
151
  }
152

NEW
153
  return (
×
154
    <View className="flex-1">
155
      {/* Header with new thread button */}
156
      <View className="flex-row justify-between items-center px-4 py-3 border-b border-neutral-700">
157
        <Text className="font-display text-lg text-white">Conversations</Text>
158
        {onCreateThread && (
×
159
          <Pressable
160
            onPress={onCreateThread}
161
            className="bg-teal-600 px-3 py-1.5 active:bg-teal-700"
162
            style={{ borderRadius: '8px 10px 6px 12px' }}
163
          >
164
            <Text className="font-body text-sm text-white font-semibold">+ New</Text>
165
          </Pressable>
166
        )}
167
      </View>
168

169
      {/* Pinned section */}
170
      {pinnedThreads.length > 0 && (
×
171
        <View className="mb-2">
172
          <Text className="px-4 py-2 text-xs text-neutral-500 font-body uppercase tracking-wider">
173
            Pinned
174
          </Text>
175
          {pinnedThreads.map((thread) => (
NEW
176
            <View key={thread.id} className="px-3">
×
NEW
177
              <ThreadItem thread={thread} onPress={() => onSelectThread(thread.id)} />
×
178
            </View>
179
          ))}
180
        </View>
181
      )}
182

183
      {/* Recent threads */}
184
      <FlatList
185
        data={recentThreads}
NEW
186
        keyExtractor={(item) => item.id}
×
187
        renderItem={({ item }) => (
NEW
188
          <View className="px-3">
×
NEW
189
            <ThreadItem thread={item} onPress={() => onSelectThread(item.id)} />
×
190
          </View>
191
        )}
192
        ListHeaderComponent={
193
          recentThreads.length > 0 ? (
×
194
            <Text className="px-4 py-2 text-xs text-neutral-500 font-body uppercase tracking-wider">
195
              Recent
196
            </Text>
197
          ) : null
198
        }
199
        showsVerticalScrollIndicator={false}
200
        contentContainerStyle={{ paddingBottom: 16 }}
201
      />
202
    </View>
203
  );
204
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc