• 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

78.16
/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({
62✔
15
    queryKey: queryKeys.shells.byProject(projectId ?? ''),
62✔
16
    queryFn: async () => {
62✔
17
      if (!projectId) return [];
19!
18
      const result = await api.getShells(projectId);
19✔
19
      return result.shells;
15✔
20
    },
19✔
21
    enabled: !!projectId,
62✔
22
    placeholderData: [],
62✔
23
  });
62✔
24
}
62✔
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
 */
52
export function useCreateShell(): UseMutationResult<{ shell: Shell }, Error, { projectId: string; name?: string; type?: ShellType }> {
1✔
53
  const queryClient = useQueryClient();
60✔
54

55
  return useMutation({
60✔
56
    mutationFn: ({ projectId, name, type }: { projectId: string; name?: string; type?: ShellType }) => {
60✔
57
      shellLog.info({ projectId, name, type }, 'Creating shell');
5✔
58
      return api.createShell(projectId, name, type);
5✔
59
    },
5✔
60
    onSuccess: (data) => {
60✔
61
      shellLog.info({ shellId: data.shell.id, shellName: data.shell.name, type: data.shell.type }, 'Shell created');
5✔
62
      void queryClient.invalidateQueries({
5✔
63
        queryKey: queryKeys.shells.byProject(data.shell.projectId),
5✔
64
      });
5✔
65
    },
5✔
66
    onError: (error, { projectId, name, type }) => {
60✔
67
      shellLog.error({ projectId, name, type, error: error.message }, 'Shell creation failed');
×
68
    },
×
69
  });
60✔
70
}
60✔
71

72
interface DeleteShellContext {
73
  previousShells: Shell[] | undefined;
74
  projectId: string;
75
}
76

77
/**
78
 * Hook to delete a shell
79
 */
80
export function useDeleteShell(): UseMutationResult<{ shellId: string; projectId: string }, Error, { shellId: string; projectId: string }, DeleteShellContext> {
1✔
81
  const queryClient = useQueryClient();
83✔
82
  const setActiveShell = useUIStore((state) => state.setActiveShell);
83✔
83
  const activeShellId = useUIStore((state) => state.activeShellId);
83✔
84

85
  return useMutation({
83✔
86
    mutationFn: ({ shellId, projectId }: { shellId: string; projectId: string }) => {
83✔
87
      shellLog.info({ shellId, projectId }, 'Deleting shell');
5✔
88
      return api.deleteShell(shellId).then(() => ({ shellId, projectId }));
5✔
89
    },
5✔
90
    // Optimistic update
91
    onMutate: async ({ shellId, projectId }) => {
83✔
92
      shellLog.debug({ shellId, projectId }, 'Optimistically removing shell from UI');
5✔
93
      await queryClient.cancelQueries({ queryKey: queryKeys.shells.byProject(projectId) });
5✔
94

95
      const previousShells = queryClient.getQueryData<Shell[]>(
5✔
96
        queryKeys.shells.byProject(projectId),
5✔
97
      );
5✔
98

99
      queryClient.setQueryData<Shell[]>(
5✔
100
        queryKeys.shells.byProject(projectId),
5✔
101
        (old) => old?.filter((s) => s.id !== shellId) ?? [],
5✔
102
      );
5✔
103

104
      // Clear active shell if it's the one being deleted
105
      if (activeShellId === shellId) {
5✔
106
        shellLog.debug({ shellId }, 'Clearing active shell (deleted)');
1✔
107
        setActiveShell(null);
1✔
108
      }
1✔
109

110
      return { previousShells, projectId };
5✔
111
    },
5✔
112
    onError: (err, { shellId }, context) => {
83✔
113
      shellLog.error({ shellId, error: err.message }, 'Shell deletion failed, rolling back');
1✔
114
      if (context?.previousShells) {
1!
115
        queryClient.setQueryData(
×
116
          queryKeys.shells.byProject(context.projectId),
×
117
          context.previousShells,
×
118
        );
×
119
      }
×
120
    },
1✔
121
    onSettled: (_data, _error, variables) => {
83✔
122
      shellLog.debug({ shellId: variables.shellId }, 'Shell deletion settled');
5✔
123
      void queryClient.invalidateQueries({
5✔
124
        queryKey: queryKeys.shells.byProject(variables.projectId),
5✔
125
      });
5✔
126
    },
5✔
127
  });
83✔
128
}
83✔
129

130
/**
131
 * Hook to update a shell (e.g., rename, mark as done)
132
 */
133
export function useUpdateShell(): UseMutationResult<{ shell: Shell }, Error, { shellId: string; updates: { name?: string; done?: boolean } }> {
1✔
134
  const queryClient = useQueryClient();
76✔
135

136
  return useMutation({
76✔
137
    mutationFn: ({ shellId, updates }: { shellId: string; updates: { name?: string; done?: boolean } }) => {
76✔
138
      shellLog.info({ shellId, updates }, 'Updating shell');
4✔
139
      return api.updateShell(shellId, updates);
4✔
140
    },
4✔
141
    onSuccess: (data) => {
76✔
142
      shellLog.info({ shellId: data.shell.id, shellName: data.shell.name }, 'Shell updated');
4✔
143
      void queryClient.invalidateQueries({
4✔
144
        queryKey: queryKeys.shells.byProject(data.shell.projectId),
4✔
145
      });
4✔
146
    },
4✔
147
    onError: (error, { shellId }) => {
76✔
148
      shellLog.error({ shellId, error: error.message }, 'Shell update failed');
×
149
    },
×
150
  });
76✔
151
}
76✔
152

153
/**
154
 * Hook to restart a shell
155
 */
156
export function useRestartShell(): UseMutationResult<{ shell: Shell }, Error, string> {
1✔
157
  const queryClient = useQueryClient();
76✔
158

159
  return useMutation({
76✔
160
    mutationFn: (shellId: string) => {
76✔
161
      shellLog.info({ shellId }, 'Restarting shell');
2✔
162
      return api.restartShell(shellId);
2✔
163
    },
2✔
164
    onSuccess: (data) => {
76✔
165
      shellLog.info({ shellId: data.shell.id, shellName: data.shell.name, newPid: data.shell.pid }, 'Shell restarted');
2✔
166
      void queryClient.invalidateQueries({
2✔
167
        queryKey: queryKeys.shells.byProject(data.shell.projectId),
2✔
168
      });
2✔
169
    },
2✔
170
    onError: (error, shellId) => {
76✔
171
      shellLog.error({ shellId, error: error.message }, 'Shell restart failed');
×
172
    },
×
173
  });
76✔
174
}
76✔
175

176
interface ActiveShellId {
177
  activeShellId: string | null;
178
  setActiveShell: (id: string | null) => void;
179
}
180

181
/**
182
 * Hook to get active shell ID from UI store
183
 */
184
export function useActiveShellId(): ActiveShellId {
1✔
185
  const activeShellId = useUIStore((state) => state.activeShellId);
154✔
186
  const setActiveShell = useUIStore((state) => state.setActiveShell);
154✔
187
  return { activeShellId, setActiveShell };
154✔
188
}
154✔
189

190
/**
191
 * Hook to get a specific shell by ID from the cache
192
 * Searches all project shell caches for the shell
193
 */
194
export function useShell(shellId: string | null): Shell | undefined {
1✔
195
  const queryClient = useQueryClient();
×
196

197
  if (!shellId) return undefined;
×
198

199
  // Get all cached shell queries and search for the shell
200
  const cache = queryClient.getQueryCache();
×
201
  const shellQueries = cache.findAll({ queryKey: ['shells'] });
×
202

203
  for (const query of shellQueries) {
×
204
    const shells = query.state.data as Shell[] | undefined;
×
205
    if (shells) {
×
206
      const shell = shells.find((s) => s.id === shellId);
×
207
      if (shell) return shell;
×
208
    }
×
209
  }
×
210

211
  return undefined;
×
212
}
×
213

214
/**
215
 * Hook to start a shell (activate PTY)
216
 */
217
export function useStartShell(): UseMutationResult<{ shell: Shell }, Error, string> {
1✔
218
  const queryClient = useQueryClient();
×
219

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