• 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

84.91
/src/client/components/shells/ShellItem.tsx
1
import { useState, useEffect } from 'react';
1✔
2
import { Box, Group, Text, UnstyledButton, ActionIcon, Menu, Badge, Modal, TextInput, Button, Stack } from '@mantine/core';
1✔
3
import { IconTerminal2, IconDots, IconTrash, IconPencil, IconRefresh, IconSparkles, IconCheck, IconPlayerPlay } from '@tabler/icons-react';
1✔
4
import type { Shell } from '@shared/types';
5
import { useDeleteShell, useUpdateShell, useRestartShell, useActiveShellId } from '@client/hooks/useShells';
1✔
6
import { useUIStore } from '@client/stores/uiStore';
1✔
7

8
interface ShellItemProps {
9
  shell: Shell;
10
  projectId: string;
11
  /** Timeout in milliseconds before AI shell is considered idle (default: 5000) */
12
  aiIdleTimeoutMs?: number;
13
  /** Offset in pixels for positioning the AI activity indicator in the left gutter */
14
  indicatorOffset?: number;
15
}
16

17
/**
18
 * Check if a shell has recent activity (utility function)
19
 */
20
function isShellRecentlyActive(
13✔
21
  shell: Shell,
13✔
22
  clientActivityTimestamp: number | undefined,
13✔
23
  idleTimeoutMs: number,
13✔
24
): boolean {
13✔
25
  let lastActivityTime: number | null = null;
13✔
26

27
  if (clientActivityTimestamp) {
13✔
28
    lastActivityTime = clientActivityTimestamp;
4✔
29
  } else if (shell.lastActivityAt) {
13✔
30
    lastActivityTime = new Date(shell.lastActivityAt).getTime();
1✔
31
  }
1✔
32

33
  if (!lastActivityTime) {
13✔
34
    return false;
8✔
35
  }
8✔
36

37
  const now = Date.now();
5✔
38
  return now - lastActivityTime < idleTimeoutMs;
5✔
39
}
5✔
40

41
/**
42
 * Hook to determine if an AI shell has had recent activity (input or output)
43
 * Uses client-side tracking from UI store for immediate responsiveness
44
 */
45
export function useAiShellActivity(shell: Shell, idleTimeoutMs: number): boolean {
1✔
46
  const [isRecentlyActive, setIsRecentlyActive] = useState(false);
75✔
47
  const clientActivityTimestamp = useUIStore(
75✔
48
    (state) => state.shellActivityTimestamps[shell.id],
75✔
49
  );
75✔
50

51
  useEffect(() => {
75✔
52
    if (shell.type !== 'ai') {
33✔
53
      return;
21✔
54
    }
21✔
55

56
    const checkActivity = (): void => {
12✔
57
      const isActive = isShellRecentlyActive(shell, clientActivityTimestamp, idleTimeoutMs);
13✔
58
      const now = Date.now();
13✔
59
      const timeSinceClient = clientActivityTimestamp ? String(now - clientActivityTimestamp) : 'N/A';
13✔
60
      const timeSinceServer = shell.lastActivityAt ? String(now - new Date(shell.lastActivityAt).getTime()) : 'N/A';
13✔
61
      console.log(`[AI_INDICATOR] shell=${shell.name} isActive=${String(isActive)} clientTs=${timeSinceClient}ms serverTs=${timeSinceServer}ms`);
13✔
62
      setIsRecentlyActive(isActive);
13✔
63
    };
13✔
64

65
    // Check immediately
66
    checkActivity();
12✔
67

68
    // Set up interval to re-check periodically
69
    const interval = setInterval(checkActivity, 1000);
12✔
70

71
    return (): void => {
12✔
72
      clearInterval(interval);
12✔
73
    };
12✔
74
  }, [shell.type, shell.id, shell.lastActivityAt, clientActivityTimestamp, idleTimeoutMs]);
75✔
75

76
  return isRecentlyActive;
75✔
77
}
75✔
78

79
export type ProjectAiStatus = 'red' | 'green' | 'blue' | null;
80

81
/**
82
 * Hook to determine the aggregated AI status for a project
83
 * Returns:
84
 * - 'red' if any AI shell is idle (not done, not recently active)
85
 * - 'green' if any AI shell is active (not done, recently active)
86
 * - 'blue' if all AI shells are done
87
 * - null if there are no AI shells
88
 */
89
export function useProjectAiStatus(shells: Shell[], idleTimeoutMs = 5000): ProjectAiStatus {
1✔
90
  const [status, setStatus] = useState<ProjectAiStatus>(null);
56✔
91
  const shellActivityTimestamps = useUIStore((state) => state.shellActivityTimestamps);
56✔
92

93
  useEffect(() => {
56✔
94
    const aiShells = shells.filter((s) => s.type === 'ai');
32✔
95

96
    if (aiShells.length === 0) {
32✔
97
      setStatus(null);
32✔
98
      return;
32✔
99
    }
32!
100

101
    const checkStatus = (): void => {
×
102
      let hasRed = false;
×
103
      let hasGreen = false;
×
104
      let hasBlue = false;
×
105

106
      for (const shell of aiShells) {
×
107
        if (shell.done) {
×
108
          hasBlue = true;
×
109
        } else {
×
110
          const isActive = isShellRecentlyActive(
×
111
            shell,
×
112
            shellActivityTimestamps[shell.id],
×
113
            idleTimeoutMs,
×
114
          );
×
115
          if (isActive) {
×
116
            hasGreen = true;
×
117
          } else {
×
118
            hasRed = true;
×
119
          }
×
120
        }
×
121
      }
×
122

123
      // Priority: red > green > blue
124
      if (hasRed) {
×
125
        setStatus('red');
×
126
      } else if (hasGreen) {
×
127
        setStatus('green');
×
128
      } else if (hasBlue) {
×
129
        setStatus('blue');
×
130
      } else {
×
131
        setStatus(null);
×
132
      }
×
133
    };
×
134

135
    // Check immediately
136
    checkStatus();
×
137

138
    // Set up interval to re-check periodically
139
    const interval = setInterval(checkStatus, 1000);
×
140

141
    return (): void => {
×
142
      clearInterval(interval);
×
143
    };
×
144
  }, [shells, shellActivityTimestamps, idleTimeoutMs]);
56✔
145

146
  return status;
56✔
147
}
56✔
148

149
export function ShellItem({ shell, projectId, aiIdleTimeoutMs = 5000, indicatorOffset = 7 }: ShellItemProps): React.ReactElement {
1✔
150
  const { activeShellId, setActiveShell } = useActiveShellId();
75✔
151
  const toggleContextSidebar = useUIStore((state) => state.toggleContextSidebar);
75✔
152
  const deleteShellMutation = useDeleteShell();
75✔
153
  const updateShellMutation = useUpdateShell();
75✔
154
  const restartShellMutation = useRestartShell();
75✔
155
  const [renameModalOpen, setRenameModalOpen] = useState(false);
75✔
156
  const [newName, setNewName] = useState(shell.name);
75✔
157
  const [contextMenuOpened, setContextMenuOpened] = useState(false);
75✔
158
  const isAiRecentlyActive = useAiShellActivity(shell, aiIdleTimeoutMs);
75✔
159

160
  const isActive = activeShellId === shell.id;
75✔
161
  const isAiShell = shell.type === 'ai';
75✔
162

163
  const handleClick = (): void => {
75✔
164
    console.log(`[TERMINAL_SWITCH] ${performance.now().toFixed(2)}ms - Click on shell: ${shell.id} (${shell.name})`);
4✔
165
    (window as unknown as { __terminalSwitchStart?: number }).__terminalSwitchStart = performance.now();
4✔
166
    setActiveShell(shell.id);
4✔
167
    toggleContextSidebar('shell', shell.id);
4✔
168
  };
4✔
169

170
  const handleDelete = (): void => {
75✔
171
    deleteShellMutation.mutate({ shellId: shell.id, projectId });
1✔
172
  };
1✔
173

174
  const handleRenameClick = (): void => {
75✔
175
    setNewName(shell.name);
1✔
176
    setRenameModalOpen(true);
1✔
177
  };
1✔
178

179
  const handleRenameSubmit = (): void => {
75✔
180
    if (!newName.trim() || newName === shell.name) {
1!
181
      setRenameModalOpen(false);
×
182
      return;
×
183
    }
×
184
    updateShellMutation.mutate(
1✔
185
      { shellId: shell.id, updates: { name: newName.trim() } },
1✔
186
      {
1✔
187
        onSuccess: () => {
1✔
188
          setRenameModalOpen(false);
1✔
189
        },
1✔
190
      },
1✔
191
    );
1✔
192
  };
1✔
193

194
  const handleRestart = (): void => {
75✔
195
    restartShellMutation.mutate(shell.id);
1✔
196
  };
1✔
197

198
  const handleToggleDone = (): void => {
75✔
199
    updateShellMutation.mutate({
2✔
200
      shellId: shell.id,
2✔
201
      updates: { done: !shell.done },
2✔
202
    });
2✔
203
  };
2✔
204

205
  const statusColor = shell.status === 'active' ? 'green' : shell.status === 'error' ? 'red' : 'gray';
75!
206

207
  // Activity indicator color for AI shells
208
  // Blue when done, green when active, red when idle
209
  const activityIndicatorColor = shell.done
75✔
210
    ? 'var(--mantine-color-blue-6)'
11✔
211
    : isAiRecentlyActive
64✔
212
      ? 'var(--mantine-color-green-6)'
3✔
213
      : 'var(--mantine-color-red-6)';
61✔
214

215
  const handleContextMenu = (e: React.MouseEvent): void => {
75✔
216
    e.preventDefault();
×
217
    e.stopPropagation();
×
218
    setContextMenuOpened(true);
×
219
  };
×
220

221
  return (
75✔
222
    <>
75✔
223
      <Box
75✔
224
        style={{
75✔
225
          display: 'flex',
75✔
226
          alignItems: 'stretch',
75✔
227
          position: 'relative',
75✔
228
        }}
75✔
229
        onContextMenu={handleContextMenu}
75✔
230
      >
231
        {/* Activity indicator for AI shells - positioned in the left gutter */}
232
        {isAiShell && (
75✔
233
          <Box
29✔
234
            style={{
29✔
235
              position: 'absolute',
29✔
236
              left: -indicatorOffset,
29✔
237
              top: 0,
29✔
238
              bottom: 0,
29✔
239
              width: 3,
29✔
240
              backgroundColor: activityIndicatorColor,
29✔
241
              borderRadius: 'var(--mantine-radius-xs)',
29✔
242
            }}
29✔
243
            data-testid="ai-activity-indicator"
29✔
244
          />
29✔
245
        )}
246
        <Group gap={0} wrap="nowrap" style={{ flex: 1 }}>
75✔
247
          <UnstyledButton
75✔
248
            onClick={handleClick}
75✔
249
            style={{
75✔
250
              flex: 1,
75✔
251
              padding: '6px 10px',
75✔
252
              borderRadius: 'var(--mantine-radius-sm)',
75✔
253
              backgroundColor: isActive ? 'var(--mantine-color-dark-5)' : 'transparent',
75✔
254
              display: 'flex',
75✔
255
              alignItems: 'center',
75✔
256
              gap: 8,
75✔
257
            }}
75✔
258
            className="shell-item"
75✔
259
            data-testid="shell-item"
75✔
260
          >
261
            {isAiShell ? (
75✔
262
              <IconSparkles size={14} style={{ flexShrink: 0, color: 'var(--mantine-color-violet-4)' }} />
29✔
263
            ) : (
264
              <IconTerminal2 size={14} style={{ flexShrink: 0, color: 'var(--mantine-color-green-4)' }} />
46✔
265
            )}
266
            <Text size="xs" truncate style={{ flex: 1 }}>
75✔
267
              {shell.name}
75✔
268
            </Text>
75✔
269
            <Badge size="xs" variant="dot" color={statusColor}>
75✔
270
              {shell.status}
75✔
271
            </Badge>
75✔
272
          </UnstyledButton>
75✔
273

274
          <Menu position="bottom-end" withinPortal opened={contextMenuOpened} onClose={() => { setContextMenuOpened(false); }}>
75✔
275
            <Menu.Target>
75✔
276
              <ActionIcon
75✔
277
                variant="subtle"
75✔
278
                size="xs"
75✔
279
                onClick={(e) => { e.stopPropagation(); setContextMenuOpened(true); }}
75✔
280
              >
281
                <IconDots size={12} />
75✔
282
              </ActionIcon>
75✔
283
            </Menu.Target>
75✔
284
            <Menu.Dropdown>
75✔
285
              <Menu.Item
75✔
286
                leftSection={<IconPencil size={14} />}
75✔
287
                onClick={handleRenameClick}
75✔
288
              >
75✔
289
                Rename
290
              </Menu.Item>
75✔
291
              <Menu.Item
75✔
292
                leftSection={<IconRefresh size={14} />}
75✔
293
                onClick={handleRestart}
75✔
294
              >
75✔
295
                Restart
296
              </Menu.Item>
75✔
297
              {isAiShell && (
75✔
298
                <Menu.Item
29✔
299
                  leftSection={shell.done ? <IconPlayerPlay size={14} /> : <IconCheck size={14} />}
29✔
300
                  onClick={handleToggleDone}
29✔
301
                >
302
                  {shell.done ? 'Mark as Active' : 'Mark as Done'}
29✔
303
                </Menu.Item>
29✔
304
              )}
305
              <Menu.Divider />
75✔
306
              <Menu.Item
75✔
307
                color="red"
75✔
308
                leftSection={<IconTrash size={14} />}
75✔
309
                onClick={handleDelete}
75✔
310
              >
75✔
311
                Close Shell
312
              </Menu.Item>
75✔
313
            </Menu.Dropdown>
75✔
314
          </Menu>
75✔
315
        </Group>
75✔
316
      </Box>
75✔
317

318
      <Modal
75✔
319
        opened={renameModalOpen}
75✔
320
        onClose={() => { setRenameModalOpen(false); }}
75✔
321
        title="Rename Shell"
75✔
322
        size="sm"
75✔
323
      >
324
        <Stack>
75✔
325
          <TextInput
75✔
326
            label="Shell name"
75✔
327
            value={newName}
75✔
328
            onChange={(e) => { setNewName(e.currentTarget.value); }}
75✔
329
            placeholder="Enter shell name"
75✔
330
            autoFocus
75✔
331
            onKeyDown={(e) => {
75✔
332
              if (e.key === 'Enter') {
8!
333
                handleRenameSubmit();
×
334
              }
×
335
            }}
8✔
336
          />
75✔
337
          <Group justify="flex-end">
75✔
338
            <Button variant="subtle" onClick={() => { setRenameModalOpen(false); }}>
75✔
339
              Cancel
340
            </Button>
75✔
341
            <Button onClick={handleRenameSubmit} loading={updateShellMutation.isPending}>
75✔
342
              Rename
343
            </Button>
75✔
344
          </Group>
75✔
345
        </Stack>
75✔
346
      </Modal>
75✔
347
    </>
75✔
348
  );
349
}
75✔
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