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

apowers313 / aiforge / 21570337701

01 Feb 2026 09:11PM UTC coverage: 81.026% (-2.9%) from 83.954%
21570337701

push

github

apowers313
test: increase coverage to 80%+

2049 of 2382 branches covered (86.02%)

Branch coverage included in aggregate %.

1849 of 2529 new or added lines in 25 files covered. (73.11%)

681 existing lines in 21 files now uncovered.

9861 of 12317 relevant lines covered (80.06%)

26.33 hits per line

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

77.46
/src/client/hooks/useShells.ts
1
import { useQuery, useMutation, useQueryClient, type UseQueryResult, type UseMutationResult } from '@tanstack/react-query';
1✔
2
import { api } from '@client/services/api';
1✔
3
import { useUIStore } from '@client/stores/uiStore';
1✔
4
import { queryKeys } from './queryKeys';
1✔
5
import type { Shell, ShellType } from '@shared/types';
6
import { log } from '@client/services/logger';
1✔
7

8
const shellLog = log.shell;
1✔
9

10
/**
11
 * Hook to fetch shells for a specific project
12
 */
13
export function useShells(projectId: string | null): UseQueryResult<Shell[]> {
1✔
14
  return useQuery({
47✔
15
    queryKey: queryKeys.shells.byProject(projectId ?? ''),
47✔
16
    queryFn: async () => {
47✔
17
      if (!projectId) return [];
26!
18
      const result = await api.getShells(projectId);
26✔
19
      return result.shells;
21✔
20
    },
26✔
21
    enabled: !!projectId,
47✔
22
    placeholderData: [],
47✔
23
  });
47✔
24
}
47✔
25

26
/**
27
 * Hook to fetch shells for all projects at once
28
 */
29
export function useAllShells(projectIds: string[]): UseQueryResult<Shell[]> {
1✔
30
  const queryClient = useQueryClient();
21✔
31

32
  return useQuery({
21✔
33
    queryKey: ['shells', 'all', projectIds.join(',')],
21✔
34
    queryFn: async () => {
21✔
35
      const results = await Promise.all(
3✔
36
        projectIds.map(async (projectId) => {
3✔
37
          const result = await api.getShells(projectId);
4✔
38
          // Also populate individual project shell caches
39
          queryClient.setQueryData(queryKeys.shells.byProject(projectId), result.shells);
2✔
40
          return result.shells;
2✔
41
        }),
3✔
42
      );
3✔
43
      return results.flat();
1✔
44
    },
3✔
45
    enabled: projectIds.length > 0,
21✔
46
  });
21✔
47
}
21✔
48

49
/**
50
 * Hook to create a new shell
51
 * Phase 6: Now supports optional worktreePath for shell-worktree association
52
 */
53
export function useCreateShell(): UseMutationResult<{ shell: Shell }, Error, { projectId: string; name?: string; type?: ShellType; worktreePath?: string }> {
1✔
54
  const queryClient = useQueryClient();
213✔
55

56
  return useMutation({
213✔
57
    mutationFn: ({ projectId, name, type, worktreePath }: { projectId: string; name?: string; type?: ShellType; worktreePath?: string }) => {
213✔
58
      shellLog.info({ projectId, name, type, worktreePath }, 'Creating shell');
9✔
59
      return api.createShell(projectId, name, type, worktreePath);
9✔
60
    },
9✔
61
    onSuccess: (data) => {
213✔
62
      shellLog.info({ shellId: data.shell.id, shellName: data.shell.name, type: data.shell.type, worktreePath: data.shell.worktreePath }, 'Shell created');
9✔
63
      // Invalidate project shells
64
      void queryClient.invalidateQueries({
9✔
65
        queryKey: queryKeys.shells.byProject(data.shell.projectId),
9✔
66
      });
9✔
67
      // Phase 6: Also invalidate worktree shells if this shell is associated with a worktree
68
      if (data.shell.worktreePath) {
9!
UNCOV
69
        void queryClient.invalidateQueries({
×
UNCOV
70
          queryKey: queryKeys.shells.byWorktree(data.shell.worktreePath),
×
UNCOV
71
        });
×
UNCOV
72
      }
×
73
    },
9✔
74
    onError: (error, { projectId, name, type, worktreePath }) => {
213✔
75
      shellLog.error({ projectId, name, type, worktreePath, error: error.message }, 'Shell creation failed');
2✔
76
    },
2✔
77
  });
213✔
78
}
213✔
79

80
interface DeleteShellContext {
81
  previousShells: Shell[] | undefined;
82
  projectId: string;
83
}
84

85
/**
86
 * Hook to delete a shell
87
 */
88
export function useDeleteShell(): UseMutationResult<{ shellId: string; projectId: string }, Error, { shellId: string; projectId: string }, DeleteShellContext> {
1✔
89
  const queryClient = useQueryClient();
109✔
90
  const setActiveShell = useUIStore((state) => state.setActiveShell);
109✔
91
  const activeShellId = useUIStore((state) => state.activeShellId);
109✔
92

93
  return useMutation({
109✔
94
    mutationFn: ({ shellId, projectId }: { shellId: string; projectId: string }) => {
109✔
95
      shellLog.info({ shellId, projectId }, 'Deleting shell');
5✔
96
      return api.deleteShell(shellId).then(() => ({ shellId, projectId }));
5✔
97
    },
5✔
98
    // Optimistic update
99
    onMutate: async ({ shellId, projectId }) => {
109✔
100
      shellLog.debug({ shellId, projectId }, 'Optimistically removing shell from UI');
5✔
101
      await queryClient.cancelQueries({ queryKey: queryKeys.shells.byProject(projectId) });
5✔
102

103
      const previousShells = queryClient.getQueryData<Shell[]>(
5✔
104
        queryKeys.shells.byProject(projectId),
5✔
105
      );
5✔
106

107
      queryClient.setQueryData<Shell[]>(
5✔
108
        queryKeys.shells.byProject(projectId),
5✔
109
        (old) => old?.filter((s) => s.id !== shellId) ?? [],
5✔
110
      );
5✔
111

112
      // Clear active shell if it's the one being deleted
113
      if (activeShellId === shellId) {
5✔
114
        shellLog.debug({ shellId }, 'Clearing active shell (deleted)');
1✔
115
        setActiveShell(null);
1✔
116
      }
1✔
117

118
      return { previousShells, projectId };
5✔
119
    },
5✔
120
    onError: (err, { shellId }, context) => {
109✔
121
      shellLog.error({ shellId, error: err.message }, 'Shell deletion failed, rolling back');
1✔
122
      if (context?.previousShells) {
1!
UNCOV
123
        queryClient.setQueryData(
×
UNCOV
124
          queryKeys.shells.byProject(context.projectId),
×
UNCOV
125
          context.previousShells,
×
UNCOV
126
        );
×
UNCOV
127
      }
×
128
    },
1✔
129
    onSettled: (_data, _error, variables) => {
109✔
130
      shellLog.debug({ shellId: variables.shellId }, 'Shell deletion settled');
5✔
131
      void queryClient.invalidateQueries({
5✔
132
        queryKey: queryKeys.shells.byProject(variables.projectId),
5✔
133
      });
5✔
134
    },
5✔
135
  });
109✔
136
}
109✔
137

138
/**
139
 * Hook to update a shell (e.g., rename, mark as done)
140
 */
141
export function useUpdateShell(): UseMutationResult<{ shell: Shell }, Error, { shellId: string; updates: { name?: string; done?: boolean } }> {
1✔
142
  const queryClient = useQueryClient();
102✔
143

144
  return useMutation({
102✔
145
    mutationFn: ({ shellId, updates }: { shellId: string; updates: { name?: string; done?: boolean } }) => {
102✔
146
      shellLog.info({ shellId, updates }, 'Updating shell');
4✔
147
      return api.updateShell(shellId, updates);
4✔
148
    },
4✔
149
    onSuccess: (data) => {
102✔
150
      shellLog.info({ shellId: data.shell.id, shellName: data.shell.name }, 'Shell updated');
4✔
151
      void queryClient.invalidateQueries({
4✔
152
        queryKey: queryKeys.shells.byProject(data.shell.projectId),
4✔
153
      });
4✔
154
    },
4✔
155
    onError: (error, { shellId }) => {
102✔
UNCOV
156
      shellLog.error({ shellId, error: error.message }, 'Shell update failed');
×
UNCOV
157
    },
×
158
  });
102✔
159
}
102✔
160

161
/**
162
 * Hook to restart a shell
163
 */
164
export function useRestartShell(): UseMutationResult<{ shell: Shell }, Error, string> {
1✔
165
  const queryClient = useQueryClient();
103✔
166

167
  return useMutation({
103✔
168
    mutationFn: (shellId: string) => {
103✔
169
      shellLog.info({ shellId }, 'Restarting shell');
2✔
170
      return api.restartShell(shellId);
2✔
171
    },
2✔
172
    onSuccess: (data) => {
103✔
173
      shellLog.info({ shellId: data.shell.id, shellName: data.shell.name, newPid: data.shell.pid }, 'Shell restarted');
2✔
174
      void queryClient.invalidateQueries({
2✔
175
        queryKey: queryKeys.shells.byProject(data.shell.projectId),
2✔
176
      });
2✔
177
    },
2✔
178
    onError: (error, shellId) => {
103✔
UNCOV
179
      shellLog.error({ shellId, error: error.message }, 'Shell restart failed');
×
UNCOV
180
    },
×
181
  });
103✔
182
}
103✔
183

184
interface ActiveShellId {
185
  activeShellId: string | null;
186
  setActiveShell: (id: string | null) => void;
187
}
188

189
/**
190
 * Hook to get active shell ID from UI store
191
 */
192
export function useActiveShellId(): ActiveShellId {
1✔
193
  const activeShellId = useUIStore((state) => state.activeShellId);
333✔
194
  const setActiveShell = useUIStore((state) => state.setActiveShell);
333✔
195
  return { activeShellId, setActiveShell };
333✔
196
}
333✔
197

198
/**
199
 * Hook to get a specific shell by ID from the cache
200
 * Searches all project shell caches for the shell
201
 */
202
export function useShell(shellId: string | null): Shell | undefined {
1✔
203
  const queryClient = useQueryClient();
×
204

205
  if (!shellId) return undefined;
×
206

207
  // Get all cached shell queries and search for the shell
208
  const cache = queryClient.getQueryCache();
×
209
  const shellQueries = cache.findAll({ queryKey: ['shells'] });
×
210

211
  for (const query of shellQueries) {
×
212
    const shells = query.state.data as Shell[] | undefined;
×
UNCOV
213
    if (shells) {
×
UNCOV
214
      const shell = shells.find((s) => s.id === shellId);
×
UNCOV
215
      if (shell) return shell;
×
UNCOV
216
    }
×
UNCOV
217
  }
×
218

UNCOV
219
  return undefined;
×
220
}
×
221

222
/**
223
 * Hook to start a shell (activate PTY)
224
 */
225
export function useStartShell(): UseMutationResult<{ shell: Shell }, Error, string> {
1✔
226
  const queryClient = useQueryClient();
×
227

228
  return useMutation({
×
229
    mutationFn: (shellId: string) => {
×
230
      shellLog.info({ shellId }, 'Starting shell PTY');
×
231
      return api.startShell(shellId);
×
232
    },
×
233
    onSuccess: (data) => {
×
234
      shellLog.info({ shellId: data.shell.id, shellName: data.shell.name, pid: data.shell.pid }, 'Shell PTY started');
×
235
      // Update the shell in the cache
236
      const projectId = data.shell.projectId;
×
237
      queryClient.setQueryData<Shell[]>(
×
238
        queryKeys.shells.byProject(projectId),
×
UNCOV
239
        (old) => old?.map((s) => (s.id === data.shell.id ? data.shell : s)) ?? [],
×
UNCOV
240
      );
×
UNCOV
241
    },
×
UNCOV
242
    onError: (error, shellId) => {
×
UNCOV
243
      shellLog.error({ shellId, error: error.message }, 'Shell start failed');
×
UNCOV
244
    },
×
UNCOV
245
  });
×
UNCOV
246
}
×
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