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

agentic-dev-library / thumbcode / 22325513623

23 Feb 2026 09:26PM UTC coverage: 61.293% (+2.6%) from 58.65%
22325513623

push

github

jbdevprimary
feat: v1.0 consolidation wave 2 — US-001, US-003, US-008, US-011, US-013, US-015 completed (15/24 stories done)

1645 of 2983 branches covered (55.15%)

Branch coverage included in aggregate %.

70 of 92 new or added lines in 6 files covered. (76.09%)

4 existing lines in 3 files now uncovered.

2659 of 4039 relevant lines covered (65.83%)

41.98 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 { Badge } from '@/components/display';
15
import { Text } from '@/components/ui';
16
import { formatRelativeTime, getParticipantColor } from '@/lib/chat-utils';
17

18
/** Props for the ThreadList component */
19
interface ThreadListProps {
20
  /** Called when a thread is selected */
21
  onSelectThread: (threadId: string) => void;
22
  /** Called when the user wants to create a new thread */
23
  onCreateThread?: () => void;
24
}
25

26
/** Props for the ThreadItem component */
27
interface ThreadItemProps {
28
  /** The chat thread to display */
29
  thread: ChatThread;
30
  /** Called when the thread item is pressed */
31
  onPress: () => void;
32
}
33

34
function ThreadItem({ thread, onPress }: Readonly<ThreadItemProps>) {
UNCOV
35
  const hasUnread = thread.unreadCount > 0;
×
36
  const accessibilityLabel = [thread.title, hasUnread ? `${thread.unreadCount} unread` : '']
×
37
    .filter(Boolean)
38
    .join(', ');
39

40
  return (
×
41
    <button
42
      type="button"
43
      onClick={onPress}
44
      className="bg-surface-elevated p-4 mb-2 active:bg-neutral-700 rounded-organic-card"
45
      aria-label={accessibilityLabel}
46
      aria-description="Open this thread"
47
      style={{ transform: 'rotate(-0.2deg)' }}
48
    >
49
      <div className="flex-row items-start justify-between">
50
        <div className="flex-1 mr-3">
51
          {/* Title with unread indicator */}
52
          <div className="flex-row items-center mb-1">
53
            {thread.isPinned && (
×
54
              <div className="mr-2">
55
                <Badge variant="warning" size="sm">
56
                  Pinned
57
                </Badge>
58
              </div>
59
            )}
60
            <Text
61
              variant="display"
62
              size="base"
63
              className={hasUnread ? 'text-white' : 'text-neutral-200'}
×
64
              numberOfLines={1}
65
            >
66
              {thread.title}
67
            </Text>
68
          </div>
69

70
          {/* Participants */}
71
          <div className="flex-row items-center mb-1">
72
            {thread.participants
73
              .filter((p) => p !== 'user')
×
74
              .slice(0, 3)
75
              .map((participant, index) => (
76
                <div
×
77
                  key={participant}
78
                  className={`w-2 h-2 rounded-full ${getParticipantColor(participant)} ${
79
                    index > 0 ? 'ml-1' : ''
×
80
                  }`}
81
                />
82
              ))}
83
            {thread.participants.length > 4 && (
×
84
              <Text size="xs" className="text-neutral-500 ml-1">
85
                +{thread.participants.length - 4}
86
              </Text>
87
            )}
88
          </div>
89

90
          {/* Timestamp */}
91
          <Text size="xs" className="text-neutral-500">
92
            {formatRelativeTime(thread.lastMessageAt)}
93
          </Text>
94
        </div>
95

96
        {/* Unread badge */}
97
        {hasUnread && (
×
98
          <div
99
            className="bg-coral-500 px-2 py-0.5 min-w-[20px] items-center rounded-organic-input"
100
          >
101
            <Text size="xs" weight="semibold" className="text-white">
102
              {thread.unreadCount > 99 ? '99+' : thread.unreadCount}
×
103
            </Text>
104
          </div>
105
        )}
106
      </div>
107
    </button>
108
  );
109
}
110

111
export function ThreadList({ onSelectThread, onCreateThread }: Readonly<ThreadListProps>) {
112
  const pinnedThreads = useChatStore(selectPinnedThreads);
×
113
  const recentThreads = useChatStore(selectRecentThreads);
×
114

115
  const allThreads = [...pinnedThreads, ...recentThreads];
×
116

117
  if (allThreads.length === 0) {
×
118
    return (
×
119
      <div className="flex-1 items-center justify-center p-6">
120
        <Text variant="display" size="lg" className="text-neutral-400 text-center mb-2">
121
          No conversations yet
122
        </Text>
123
        <Text size="sm" className="text-neutral-500 text-center mb-4">
124
          Start a new thread to collaborate with AI agents
125
        </Text>
126
        {onCreateThread && (
×
127
          <button
128
            type="button"
129
            onClick={onCreateThread}
130
            className="bg-coral-500 px-6 py-3 active:bg-coral-600 rounded-organic-button"
131
            aria-label="New Thread"
132
            aria-description="Create a new chat thread"
133
          >
134
            <Text weight="semibold" className="text-white">
135
              New Thread
136
            </Text>
137
          </button>
138
        )}
139
      </div>
140
    );
141
  }
142

143
  return (
×
144
    <div className="flex-1">
145
      {/* Header with new thread button */}
146
      <div className="flex-row justify-between items-center px-4 py-3 border-b border-neutral-700">
147
        <Text variant="display" size="lg" className="text-white">
148
          Conversations
149
        </Text>
150
        {onCreateThread && (
×
151
          <button
152
            type="button"
153
            onClick={onCreateThread}
154
            className="bg-teal-600 px-3 py-1.5 active:bg-teal-700 rounded-organic-button"
155
            aria-label="New Thread"
156
            aria-description="Create a new chat thread"
157
          >
158
            <Text size="sm" weight="semibold" className="text-white">
159
              + New
160
            </Text>
161
          </button>
162
        )}
163
      </div>
164

165
      {/* Pinned section */}
166
      {pinnedThreads.length > 0 && (
×
167
        <div className="mb-2">
168
          <Text size="xs" className="px-4 py-2 text-neutral-500 uppercase tracking-wider">
169
            Pinned
170
          </Text>
171
          {pinnedThreads.map((thread) => (
172
            <div key={thread.id} className="px-3">
×
173
              <ThreadItem thread={thread} onPress={() => onSelectThread(thread.id)} />
×
174
            </div>
175
          ))}
176
        </div>
177
      )}
178

179
      {/* Recent threads */}
180
      {recentThreads.length > 0 && (
×
181
        <Text size="xs" className="px-4 py-2 text-neutral-500 uppercase tracking-wider">
182
          Recent
183
        </Text>
184
      )}
185
      {recentThreads.map((item) => (
186
        <div key={item.id} className="px-3">
×
187
          <ThreadItem thread={item} onPress={() => onSelectThread(item.id)} />
×
188
        </div>
189
      ))}
190
    </div>
191
  );
192
}
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