Skip to content

Commit faa7eed

Browse files
committed
also count threads with a running terminal as non-idle
1 parent 75e2d35 commit faa7eed

File tree

3 files changed

+104
-39
lines changed

3 files changed

+104
-39
lines changed

apps/web/src/components/Sidebar.logic.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from "vitest";
22

33
import {
4+
collectSidebarNonIdleProjectIds,
45
hasUnseenCompletion,
56
resolveSidebarNewThreadEnvMode,
67
resolveThreadRowClassName,
@@ -83,6 +84,55 @@ describe("resolveSidebarNewThreadEnvMode", () => {
8384
});
8485
});
8586

87+
describe("collectSidebarNonIdleProjectIds", () => {
88+
const projectA = "project-a" as never;
89+
const projectB = "project-b" as never;
90+
const threadA = { id: "thread-a" as never, projectId: projectA };
91+
const threadB = { id: "thread-b" as never, projectId: projectB };
92+
const workingStatus = {
93+
label: "Working" as const,
94+
colorClass: "text-sky-600",
95+
dotClass: "bg-sky-500",
96+
pulse: true,
97+
};
98+
99+
it("preserves a project when one of its threads has a running terminal", () => {
100+
const ids = collectSidebarNonIdleProjectIds({
101+
activeProjectId: null,
102+
threads: [threadA],
103+
threadStatusById: new Map([[threadA.id, null]]),
104+
runningTerminalThreadIds: new Set([threadA.id]),
105+
});
106+
107+
expect(ids).toEqual(new Set([projectA]));
108+
});
109+
110+
it("excludes projects that have neither thread status nor running terminals", () => {
111+
const ids = collectSidebarNonIdleProjectIds({
112+
activeProjectId: null,
113+
threads: [threadA, threadB],
114+
threadStatusById: new Map([
115+
[threadA.id, null],
116+
[threadB.id, workingStatus],
117+
]),
118+
runningTerminalThreadIds: new Set(),
119+
});
120+
121+
expect(ids).toEqual(new Set([projectB]));
122+
});
123+
124+
it("preserves the active project even without thread status or terminal activity", () => {
125+
const ids = collectSidebarNonIdleProjectIds({
126+
activeProjectId: projectA,
127+
threads: [threadA],
128+
threadStatusById: new Map([[threadA.id, null]]),
129+
runningTerminalThreadIds: new Set(),
130+
});
131+
132+
expect(ids).toEqual(new Set([projectA]));
133+
});
134+
});
135+
86136
describe("resolveThreadStatusPill", () => {
87137
const baseThread = {
88138
interactionMode: "plan" as const,

apps/web/src/components/Sidebar.logic.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ProjectId, ThreadId } from "@t3tools/contracts";
12
import type { Thread } from "../types";
23
import { cn } from "../lib/utils";
34
import {
@@ -50,6 +51,27 @@ export function resolveSidebarNewThreadEnvMode(input: {
5051
return input.requestedEnvMode ?? input.defaultEnvMode;
5152
}
5253

54+
export function collectSidebarNonIdleProjectIds(input: {
55+
activeProjectId: ProjectId | null;
56+
threads: readonly Pick<Thread, "id" | "projectId">[];
57+
threadStatusById: ReadonlyMap<ThreadId, ThreadStatusPill | null>;
58+
runningTerminalThreadIds: ReadonlySet<ThreadId>;
59+
}): Set<ProjectId> {
60+
const ids = new Set<ProjectId>();
61+
if (input.activeProjectId) {
62+
ids.add(input.activeProjectId);
63+
}
64+
65+
for (const thread of input.threads) {
66+
const threadStatus = input.threadStatusById.get(thread.id) ?? null;
67+
if (threadStatus !== null || input.runningTerminalThreadIds.has(thread.id)) {
68+
ids.add(thread.projectId);
69+
}
70+
}
71+
72+
return ids;
73+
}
74+
5375
export function resolveThreadRowClassName(input: {
5476
isActive: boolean;
5577
isSelected: boolean;

apps/web/src/components/Sidebar.tsx

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import { useThreadSelectionStore } from "../threadSelectionStore";
8585
import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup";
8686
import { isNonEmpty as isNonEmptyString } from "effect/String";
8787
import {
88+
collectSidebarNonIdleProjectIds,
8889
resolveSidebarNewThreadEnvMode,
8990
resolveThreadRowClassName,
9091
resolveThreadStatusPill,
@@ -105,12 +106,6 @@ function formatRelativeTime(iso: string): string {
105106
return `${Math.floor(hours / 24)}d ago`;
106107
}
107108

108-
interface TerminalStatusIndicator {
109-
label: "Terminal process running";
110-
colorClass: string;
111-
pulse: boolean;
112-
}
113-
114109
interface PrStatusIndicator {
115110
label: "PR open" | "PR closed" | "PR merged";
116111
colorClass: string;
@@ -120,19 +115,6 @@ interface PrStatusIndicator {
120115

121116
type ThreadPr = GitStatusResult["pr"];
122117

123-
function terminalStatusFromRunningIds(
124-
runningTerminalIds: string[],
125-
): TerminalStatusIndicator | null {
126-
if (runningTerminalIds.length === 0) {
127-
return null;
128-
}
129-
return {
130-
label: "Terminal process running",
131-
colorClass: "text-teal-600 dark:text-teal-300/90",
132-
pulse: true,
133-
};
134-
}
135-
136118
function prStatusIndicator(pr: ThreadPr): PrStatusIndicator | null {
137119
if (!pr) return null;
138120

@@ -377,27 +359,35 @@ export default function Sidebar() {
377359
return map;
378360
}, [threads]);
379361

362+
const runningTerminalThreadIds = useMemo(() => {
363+
const ids = new Set<ThreadId>();
364+
for (const thread of threads) {
365+
if (selectThreadTerminalState(terminalStateByThreadId, thread.id).runningTerminalIds.length) {
366+
ids.add(thread.id);
367+
}
368+
}
369+
return ids;
370+
}, [terminalStateByThreadId, threads]);
371+
380372
const activeThread = routeThreadId
381373
? threads.find((thread) => thread.id === routeThreadId)
382374
: undefined;
383375
const activeDraftThread = useComposerDraftStore((store) =>
384376
routeThreadId ? store.draftThreadsByThreadId[routeThreadId] : undefined,
385377
);
386-
const activeProjectId = activeThread?.projectId ?? activeDraftThread?.projectId;
378+
const activeProjectId = activeThread?.projectId ?? activeDraftThread?.projectId ?? null;
387379

388-
// Currently active project and projects with a thread status
389-
const activeProjectIds = useMemo(() => {
390-
const ids = new Set<ProjectId>();
391-
if (activeProjectId) {
392-
ids.add(activeProjectId);
393-
}
394-
for (const thread of threads) {
395-
if (threadStatusById.get(thread.id) !== null) {
396-
ids.add(thread.projectId);
397-
}
398-
}
399-
return ids;
400-
}, [activeProjectId, threadStatusById, threads]);
380+
// Currently active project and projects with a thread status or running terminal
381+
const nonIdleProjectIds = useMemo(
382+
() =>
383+
collectSidebarNonIdleProjectIds({
384+
activeProjectId,
385+
threads,
386+
threadStatusById,
387+
runningTerminalThreadIds,
388+
}),
389+
[activeProjectId, runningTerminalThreadIds, threadStatusById, threads],
390+
);
401391

402392
const openPrLink = useCallback((event: React.MouseEvent<HTMLElement>, prUrl: string) => {
403393
event.preventDefault();
@@ -1029,12 +1019,12 @@ export default function Sidebar() {
10291019

10301020
const handleCollapseIdleProjects = useCallback(() => {
10311021
for (const project of projects) {
1032-
if (!project.expanded || activeProjectIds.has(project.id)) {
1022+
if (!project.expanded || nonIdleProjectIds.has(project.id)) {
10331023
continue;
10341024
}
10351025
setProjectExpanded(project.id, false);
10361026
}
1037-
}, [projects, activeProjectIds, setProjectExpanded]);
1027+
}, [projects, nonIdleProjectIds, setProjectExpanded]);
10381028

10391029
useEffect(() => {
10401030
const onMouseDown = (event: globalThis.MouseEvent) => {
@@ -1489,10 +1479,13 @@ export default function Sidebar() {
14891479
const prStatus = prStatusIndicator(
14901480
prByThreadId.get(thread.id) ?? null,
14911481
);
1492-
const terminalStatus = terminalStatusFromRunningIds(
1493-
selectThreadTerminalState(terminalStateByThreadId, thread.id)
1494-
.runningTerminalIds,
1495-
);
1482+
const terminalStatus = runningTerminalThreadIds.has(thread.id)
1483+
? {
1484+
label: "Terminal process running",
1485+
colorClass: "text-teal-600 dark:text-teal-300/90",
1486+
pulse: true,
1487+
}
1488+
: null;
14961489

14971490
return (
14981491
<SidebarMenuSubItem

0 commit comments

Comments
 (0)