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

apowers313 / aiforge / 21152598554

19 Jan 2026 10:00PM UTC coverage: 83.846% (+0.9%) from 82.966%
21152598554

push

github

apowers313
Merge branch 'master' of https://github.com/apowers313/aiforge

1604 of 1876 branches covered (85.5%)

Branch coverage included in aggregate %.

8242 of 9867 relevant lines covered (83.53%)

20.24 hits per line

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

96.6
/src/client/components/projects/AddProjectModal.tsx
1
import { useState, useEffect, useCallback } from 'react';
1✔
2
import { Modal, Box, Group, Button, Text, Stack, UnstyledButton, Breadcrumbs, Anchor, Loader, Alert, Center } from '@mantine/core';
1✔
3
import { IconFolder, IconAlertCircle } from '@tabler/icons-react';
1✔
4
import { api, type DirectoryEntry } from '@client/services/api';
1✔
5
import { log } from '@client/services/logger';
1✔
6

7
const uiLog = log.ui;
1✔
8

9
interface AddProjectModalProps {
10
  opened: boolean;
11
  onClose: () => void;
12
  onSelect: (path: string) => void;
13
}
14

15
interface BreadcrumbItem {
16
  name: string;
17
  path: string;
18
}
19

20
function pathToBreadcrumbs(path: string, homePath: string): BreadcrumbItem[] {
45✔
21
  if (!homePath || !path) {
45✔
22
    return [];
28✔
23
  }
28✔
24

25
  // Get the username from home path (last segment)
26
  const homeSegments = homePath.split('/').filter(Boolean);
17✔
27
  const username = homeSegments[homeSegments.length - 1] ?? 'home';
45!
28

29
  const breadcrumbs: BreadcrumbItem[] = [{ name: username, path: homePath }];
45✔
30

31
  // Only add breadcrumbs for paths beyond the home directory
32
  if (path.startsWith(homePath) && path !== homePath) {
45✔
33
    const relativePath = path.slice(homePath.length);
3✔
34
    const parts = relativePath.split('/').filter(Boolean);
3✔
35
    let currentPath = homePath;
3✔
36
    for (const part of parts) {
3✔
37
      currentPath += '/' + part;
3✔
38
      breadcrumbs.push({ name: part, path: currentPath });
3✔
39
    }
3✔
40
  }
3✔
41

42
  return breadcrumbs;
17✔
43
}
17✔
44

45
export function AddProjectModal({ opened, onClose, onSelect }: AddProjectModalProps): React.ReactElement | null {
1✔
46
  const [currentPath, setCurrentPath] = useState('');
59✔
47
  const [homePath, setHomePath] = useState('');
59✔
48
  const [entries, setEntries] = useState<DirectoryEntry[]>([]);
59✔
49
  const [isLoading, setIsLoading] = useState(false);
59✔
50
  const [error, setError] = useState<string | null>(null);
59✔
51

52
  const fetchDirectory = useCallback(async (path: string) => {
59✔
53
    uiLog.debug({ path }, 'AddProjectModal: fetching directory');
12✔
54
    setIsLoading(true);
12✔
55
    setError(null);
12✔
56
    try {
12✔
57
      const result = await api.browseDirectory(path);
12✔
58
      setCurrentPath(result.path);
12✔
59
      setEntries(result.entries.filter((e) => e.isDirectory));
12✔
60
      uiLog.debug({ path: result.path, entryCount: result.entries.length }, 'AddProjectModal: directory loaded');
12✔
61
    } catch (err) {
12!
62
      const message = err instanceof Error ? err.message : 'Failed to load directory';
×
63
      uiLog.error({ path, error: message }, 'AddProjectModal: failed to load directory');
×
64
      setError(message);
×
65
    } finally {
12✔
66
      setIsLoading(false);
12✔
67
    }
12✔
68
  }, []);
59✔
69

70
  useEffect(() => {
59✔
71
    if (opened) {
20✔
72
      uiLog.info('AddProjectModal: opened');
11✔
73
      // Fetch user's home directory and browse it
74
      setIsLoading(true);
11✔
75
      void (async (): Promise<void> => {
11✔
76
        try {
11✔
77
          const homeResult = await api.getHomeDirectory();
11✔
78
          uiLog.debug({ homePath: homeResult.path }, 'AddProjectModal: home directory fetched');
9✔
79
          setHomePath(homeResult.path);
9✔
80
          await fetchDirectory(homeResult.path);
9✔
81
        } catch (err) {
11✔
82
          const message = err instanceof Error ? err.message : 'Failed to get home directory';
2!
83
          uiLog.error({ error: message }, 'AddProjectModal: failed to get home directory');
2✔
84
          setError(message);
2✔
85
          setIsLoading(false);
2✔
86
        }
2✔
87
      })();
11✔
88
    }
11✔
89
  }, [opened, fetchDirectory]);
59✔
90

91
  const handleDirectoryClick = (entry: DirectoryEntry): void => {
59✔
92
    const newPath = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`;
2!
93
    uiLog.debug({ directory: entry.name, newPath }, 'AddProjectModal: navigating to directory');
2✔
94
    void fetchDirectory(newPath);
2✔
95
  };
2✔
96

97
  const handleBreadcrumbClick = (path: string): void => {
59✔
98
    uiLog.debug({ path }, 'AddProjectModal: breadcrumb navigation');
1✔
99
    void fetchDirectory(path);
1✔
100
  };
1✔
101

102
  const handleSelect = (): void => {
59✔
103
    uiLog.info({ path: currentPath }, 'AddProjectModal: directory selected');
3✔
104
    onSelect(currentPath);
3✔
105
    setCurrentPath('');
3✔
106
    setHomePath('');
3✔
107
  };
3✔
108

109
  const handleClose = (): void => {
59✔
110
    uiLog.debug('AddProjectModal: closed');
1✔
111
    onClose();
1✔
112
    setCurrentPath('');
1✔
113
    setHomePath('');
1✔
114
  };
1✔
115

116
  if (!opened) {
59✔
117
    return null;
14✔
118
  }
14✔
119

120
  const breadcrumbs = pathToBreadcrumbs(currentPath, homePath);
45✔
121

122
  return (
45✔
123
    <Modal
45✔
124
      opened={opened}
45✔
125
      onClose={handleClose}
45✔
126
      title="Add Project"
45✔
127
      size="lg"
45✔
128
      centered
45✔
129
    >
130
      <Box data-testid="directory-browser">
45✔
131
        <Box mb="md">
45✔
132
          <Text size="xs" c="dimmed" mb={4}>
45✔
133
            Current path:
134
          </Text>
45✔
135
          <Text data-testid="current-path" fw={500}>
45✔
136
            {currentPath}
45✔
137
          </Text>
45✔
138
        </Box>
45✔
139

140
        <Breadcrumbs mb="md" separator="/">
45✔
141
          {breadcrumbs.map((crumb, index) => (
45✔
142
            <Anchor
20✔
143
              key={crumb.path}
20✔
144
              component="button"
20✔
145
              size="sm"
20✔
146
              onClick={() => { handleBreadcrumbClick(crumb.path); }}
20✔
147
              data-testid={index === 0 ? 'breadcrumb-home' : undefined}
20✔
148
            >
149
              {crumb.name}
20✔
150
            </Anchor>
20✔
151
          ))}
45✔
152
        </Breadcrumbs>
45✔
153

154
        <Box
45✔
155
          style={{
45✔
156
            border: '1px solid var(--mantine-color-dark-4)',
45✔
157
            borderRadius: 'var(--mantine-radius-sm)',
45✔
158
            maxHeight: 300,
45✔
159
            overflow: 'auto',
45✔
160
          }}
45✔
161
          p="xs"
45✔
162
        >
163
          {isLoading ? (
45✔
164
            <Center py="xl" data-testid="loading-indicator">
14✔
165
              <Loader size="sm" />
14✔
166
            </Center>
14✔
167
          ) : error ? (
31✔
168
            <Alert
2✔
169
              icon={<IconAlertCircle size={16} />}
2✔
170
              color="red"
2✔
171
              data-testid="error-message"
2✔
172
            >
173
              {error}
2✔
174
            </Alert>
2✔
175
          ) : entries.length === 0 ? (
29✔
176
            <Text size="sm" c="dimmed" ta="center" py="xl">
24✔
177
              No subdirectories
178
            </Text>
24✔
179
          ) : (
180
            <Stack gap={4}>
5✔
181
              {entries.map((entry) => (
5✔
182
                <UnstyledButton
5✔
183
                  key={entry.name}
5✔
184
                  onClick={() => { handleDirectoryClick(entry); }}
5✔
185
                  style={{
5✔
186
                    padding: '8px 12px',
5✔
187
                    borderRadius: 'var(--mantine-radius-sm)',
5✔
188
                    display: 'flex',
5✔
189
                    alignItems: 'center',
5✔
190
                    gap: 8,
5✔
191
                  }}
5✔
192
                  className="directory-item"
5✔
193
                >
194
                  <IconFolder size={18} style={{ color: 'var(--mantine-color-blue-4)' }} />
5✔
195
                  <Text size="sm">{entry.name}</Text>
5✔
196
                </UnstyledButton>
5✔
197
              ))}
5✔
198
            </Stack>
5✔
199
          )}
200
        </Box>
59✔
201

202
        <Group justify="flex-end" mt="lg">
59✔
203
          <Button variant="subtle" onClick={handleClose} data-testid="cancel-button">
59✔
204
            Cancel
205
          </Button>
59✔
206
          <Button onClick={handleSelect} data-testid="select-directory-button" disabled={isLoading}>
59✔
207
            Select This Directory
208
          </Button>
59✔
209
        </Group>
59✔
210
      </Box>
59✔
211
    </Modal>
59✔
212
  );
213
}
59✔
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