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

agentic-dev-library / thumbcode / 21933693261

12 Feb 2026 04:34AM UTC coverage: 27.702% (-0.7%) from 28.372%
21933693261

push

github

web-flow
feat(ai): real Anthropic/OpenAI streaming + agent routing (#117)

* feat(ai): add AI client abstraction with Anthropic and OpenAI implementations

Create provider-agnostic AIClient interface with types for messages and
streaming chunks. Implement AnthropicClient using @anthropic-ai/sdk with
message streaming via the stream() API, and OpenAIClient using the openai
SDK with async iterator streaming. Add AIClientFactory for provider-based
client creation. Includes comprehensive unit tests for all components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(ai): replace ChatService mock with real AI streaming

Remove simulateAgentResponse() and getAgentResponsePlaceholder() mock
methods. Integrate AIClientFactory with CredentialService to resolve the
user's AI provider and API key from secure storage. Stream real AI
responses through the existing message delta event system. Add error
handling that displays helpful messages when no API key is configured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(ai): add agent-specific system prompts for response routing

Create AgentPrompts module with specialized system prompts for each agent
type: Architect (system design), Implementer (code generation), Reviewer
(code review), and Tester (test writing). Wire ChatService to use
agent-specific prompts when calling the AI client, giving each agent a
distinct personality and area of expertise.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

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

399 of 2256 branches covered (17.69%)

Branch coverage included in aggregate %.

36 of 73 new or added lines in 5 files covered. (49.32%)

204 existing lines in 13 files now uncovered.

1075 of 3065 relevant lines covered (35.07%)

7.74 hits per line

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

0.0
/src/components/code/FileTree.tsx
1
/**
2
 * FileTree Component
3
 *
4
 * Displays a hierarchical file/folder structure.
5
 * Supports expandable folders and file type icons.
6
 * Uses paint daube icons for brand consistency.
7
 */
8

9
import type React from 'react';
10
import { useMemo, useState } from 'react';
11
import { Pressable, View } from 'react-native';
12
import {
13
  ChevronDownIcon,
14
  FileCodeIcon,
15
  FileConfigIcon,
16
  FileDataIcon,
17
  FileDocIcon,
18
  FileIcon,
19
  FileMediaIcon,
20
  FileStyleIcon,
21
  FileWebIcon,
22
  FolderIcon,
23
  FolderOpenIcon,
24
  type IconColor,
25
} from '@/components/icons';
26
import { Text } from '@/components/ui';
27
import { organicBorderRadius } from '@/lib/organic-styles';
28

29
export interface FileNode {
30
  name: string;
31
  type: 'file' | 'folder';
32
  path: string;
33
  children?: FileNode[];
34
  modified?: boolean;
35
  added?: boolean;
36
  deleted?: boolean;
37
}
38

39
interface FileTreeProps {
40
  /** Root nodes of the tree */
41
  data: FileNode[];
42
  /** Callback when a file is selected */
43
  onSelectFile?: (path: string) => void;
44
  /** Currently selected file path */
45
  selectedPath?: string;
46
  /** Initially expanded folders */
47
  defaultExpanded?: string[];
48
  /** Show file status indicators */
49
  showStatus?: boolean;
50
}
51

52
interface FileTreeNodeProps {
53
  node: FileNode;
54
  depth: number;
55
  onSelectFile?: (path: string) => void;
56
  selectedPath?: string;
57
  expandedPaths: Set<string>;
58
  toggleExpanded: (path: string) => void;
59
  showStatus?: boolean;
60
}
61

62
/** File icon component type */
63
type FileIconComponent = React.FC<{ size?: number; color?: IconColor; turbulence?: number }>;
64

65
interface FileIconInfo {
66
  Icon: FileIconComponent;
67
  color: IconColor;
68
}
69

70
function getFileIconInfo(name: string): FileIconInfo {
71
  const ext = name.split('.').pop()?.toLowerCase();
×
72
  const iconMap: Record<string, FileIconInfo> = {
×
73
    ts: { Icon: FileCodeIcon, color: 'teal' },
74
    tsx: { Icon: FileCodeIcon, color: 'teal' },
75
    js: { Icon: FileCodeIcon, color: 'gold' },
76
    jsx: { Icon: FileCodeIcon, color: 'gold' },
77
    json: { Icon: FileDataIcon, color: 'gold' },
78
    md: { Icon: FileDocIcon, color: 'warmGray' },
79
    css: { Icon: FileStyleIcon, color: 'coral' },
80
    scss: { Icon: FileStyleIcon, color: 'coral' },
81
    html: { Icon: FileWebIcon, color: 'teal' },
82
    png: { Icon: FileMediaIcon, color: 'coral' },
83
    jpg: { Icon: FileMediaIcon, color: 'coral' },
84
    jpeg: { Icon: FileMediaIcon, color: 'coral' },
85
    svg: { Icon: FileMediaIcon, color: 'coral' },
86
    gif: { Icon: FileMediaIcon, color: 'coral' },
87
    git: { Icon: FileConfigIcon, color: 'warmGray' },
88
    env: { Icon: FileConfigIcon, color: 'warmGray' },
89
    lock: { Icon: FileConfigIcon, color: 'warmGray' },
90
    yaml: { Icon: FileConfigIcon, color: 'warmGray' },
91
    yml: { Icon: FileConfigIcon, color: 'warmGray' },
92
  };
93
  return iconMap[ext || ''] || { Icon: FileIcon, color: 'warmGray' };
×
94
}
95

96
function getStatusColor(node: FileNode): string {
97
  if (node.added) return 'text-teal-400';
×
98
  if (node.modified) return 'text-gold-400';
×
99
  if (node.deleted) return 'text-coral-400';
×
100
  return '';
×
101
}
102

103
function getStatusText(node: FileNode): string {
104
  if (node.added) return 'added';
×
105
  if (node.modified) return 'modified';
×
106
  if (node.deleted) return 'deleted';
×
107
  return '';
×
108
}
109

110
function getStatusLabel(node: FileNode): string {
111
  if (node.added) return 'A';
×
112
  if (node.modified) return 'M';
×
113
  if (node.deleted) return 'D';
×
114
  return '';
×
115
}
116

117
function getAccessibilityHint(
118
  isFolder: boolean,
119
  hasChildren: boolean,
120
  isExpanded: boolean
121
): string {
122
  if (!isFolder) return 'Open file';
×
123
  if (!hasChildren) return 'Empty folder';
×
124
  return isExpanded ? 'Collapse folder' : 'Expand folder';
×
125
}
126

127
function FileTreeNodeRow({
128
  node,
129
  depth,
130
  isSelected,
131
  isExpanded,
132
  isFolder,
133
  hasChildren,
134
  statusColor,
135
  onPress,
136
  accessibilityLabel,
137
  accessibilityHint,
138
}: {
139
  node: FileNode;
140
  depth: number;
141
  isSelected: boolean;
142
  isExpanded: boolean;
143
  isFolder: boolean;
144
  hasChildren: boolean;
145
  statusColor: string;
146
  onPress: () => void;
147
  accessibilityLabel: string;
148
  accessibilityHint: string;
149
}) {
150
  // Get the appropriate icon based on file type or folder state
151
  let iconInfo: FileIconInfo;
UNCOV
152
  if (isFolder) {
×
UNCOV
153
    const Icon = isExpanded ? FolderOpenIcon : FolderIcon;
×
UNCOV
154
    iconInfo = { Icon, color: 'gold' };
×
155
  } else {
156
    iconInfo = getFileIconInfo(node.name);
×
157
  }
158

UNCOV
159
  const rowClass = isSelected ? 'bg-teal-600/20' : 'active:bg-neutral-700';
×
UNCOV
160
  const textClass = isSelected ? 'text-teal-300' : 'text-neutral-200';
×
161

UNCOV
162
  return (
×
163
    <Pressable
164
      onPress={onPress}
165
      accessibilityRole="button"
166
      accessibilityLabel={accessibilityLabel}
167
      accessibilityHint={accessibilityHint}
168
      className={`flex-row items-center py-1.5 px-2 ${rowClass}`}
169
      style={{ paddingLeft: 8 + depth * 16 }}
170
    >
171
      <View className="w-4 mr-1 items-center justify-center">
172
        {isFolder && hasChildren ? (
×
173
          <View style={{ transform: [{ rotate: isExpanded ? '0deg' : '-90deg' }] }}>
×
174
            <ChevronDownIcon size={12} color="warmGray" turbulence={0.12} />
175
          </View>
176
        ) : null}
177
      </View>
178
      <View className="mr-2">
179
        <iconInfo.Icon size={16} color={iconInfo.color} turbulence={0.15} />
180
      </View>
181
      <Text className={`font-mono text-sm flex-1 ${textClass} ${statusColor}`} numberOfLines={1}>
182
        {node.name}
183
      </Text>
184
    </Pressable>
185
  );
186
}
187

188
function FileTreeNode({
189
  node,
190
  depth,
191
  onSelectFile,
192
  selectedPath,
193
  expandedPaths,
194
  toggleExpanded,
195
  showStatus,
196
}: Readonly<FileTreeNodeProps>) {
197
  const isExpanded = expandedPaths.has(node.path);
×
UNCOV
198
  const isSelected = selectedPath === node.path;
×
199
  const isFolder = node.type === 'folder';
×
200
  const hasChildren = Boolean(node.children?.length);
×
201
  const statusColor = getStatusColor(node);
×
202

203
  const handlePress = () => {
×
UNCOV
204
    if (isFolder && hasChildren) {
×
UNCOV
205
      toggleExpanded(node.path);
×
UNCOV
206
    } else if (!isFolder) {
×
207
      onSelectFile?.(node.path);
×
208
    }
209
  };
210

UNCOV
211
  const shouldShowStatus = showStatus && Boolean(node.added || node.modified || node.deleted);
×
212

213
  // Accessibility labels for screen readers
214
  const accessibilityLabel = [node.name, isFolder ? 'folder' : 'file', getStatusText(node)]
×
215
    .filter(Boolean)
216
    .join(', ');
217

UNCOV
218
  return (
×
219
    <View>
220
      <View className="flex-row items-center">
221
        <View className="flex-1">
222
          <FileTreeNodeRow
223
            node={node}
224
            depth={depth}
225
            isSelected={isSelected}
226
            isExpanded={isExpanded}
227
            isFolder={isFolder}
228
            hasChildren={hasChildren}
229
            statusColor={statusColor}
230
            onPress={handlePress}
231
            accessibilityLabel={accessibilityLabel}
232
            accessibilityHint={getAccessibilityHint(isFolder, hasChildren, isExpanded)}
233
          />
234
        </View>
235
        {shouldShowStatus && (
×
236
          <Text className={`text-xs ${statusColor} mr-2`}>{getStatusLabel(node)}</Text>
237
        )}
238
      </View>
239

240
      {isFolder && isExpanded && hasChildren && (
×
241
        <View>
242
          {node.children?.map((child) => (
UNCOV
243
            <FileTreeNode
×
244
              key={child.path}
245
              node={child}
246
              depth={depth + 1}
247
              onSelectFile={onSelectFile}
248
              selectedPath={selectedPath}
249
              expandedPaths={expandedPaths}
250
              toggleExpanded={toggleExpanded}
251
              showStatus={showStatus}
252
            />
253
          ))}
254
        </View>
255
      )}
256
    </View>
257
  );
258
}
259

260
export function FileTree({
261
  data,
262
  onSelectFile,
263
  selectedPath,
264
  defaultExpanded = [],
×
265
  showStatus = true,
×
266
}: Readonly<FileTreeProps>) {
267
  const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set(defaultExpanded));
×
268

269
  const toggleExpanded = (path: string) => {
×
UNCOV
270
    setExpandedPaths((prev) => {
×
271
      const next = new Set(prev);
×
UNCOV
272
      if (next.has(path)) {
×
273
        next.delete(path);
×
274
      } else {
UNCOV
275
        next.add(path);
×
276
      }
UNCOV
277
      return next;
×
278
    });
279
  };
280

281
  // Sort nodes: folders first, then alphabetically
282
  const sortedData = useMemo(() => {
×
283
    const sortNodes = (nodes: FileNode[]): FileNode[] => {
×
UNCOV
284
      return [...nodes]
×
285
        .sort((a, b) => {
UNCOV
286
          if (a.type !== b.type) {
×
287
            return a.type === 'folder' ? -1 : 1;
×
288
          }
UNCOV
289
          return a.name.localeCompare(b.name);
×
290
        })
UNCOV
291
        .map((node) => ({
×
292
          ...node,
293
          children: node.children ? sortNodes(node.children) : undefined,
×
294
        }));
295
    };
UNCOV
296
    return sortNodes(data);
×
297
  }, [data]);
298

UNCOV
299
  return (
×
300
    <View
301
      accessibilityRole="list"
302
      accessibilityLabel="File tree"
303
      className="bg-surface overflow-hidden"
304
      style={organicBorderRadius.card}
305
    >
306
      {sortedData.map((node) => (
UNCOV
307
        <FileTreeNode
×
308
          key={node.path}
309
          node={node}
310
          depth={0}
311
          onSelectFile={onSelectFile}
312
          selectedPath={selectedPath}
313
          expandedPaths={expandedPaths}
314
          toggleExpanded={toggleExpanded}
315
          showStatus={showStatus}
316
        />
317
      ))}
318
    </View>
319
  );
320
}
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