Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 124 additions & 2 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
groupSidebarThreadsByProject,
resolveSidebarFolderDot,
sidebarFolderExpansionKeys,
filterSidebarV2VisibleThreads,
} from "./Sidebar.logic";
import { scopeThreadRef, scopedThreadKey } from "@t3tools/client-runtime/environment";
import {
Expand All @@ -62,6 +63,65 @@ import {

const localEnvironmentId = EnvironmentId.make("environment-local");

describe("sidebar environment filtering", () => {
const remoteEnvironmentId = EnvironmentId.make("environment-remote");
const local = {
...makeThreadFixture({ id: ThreadId.make("local") }),
environmentId: localEnvironmentId,
};
const remote = {
...makeThreadFixture({ id: ThreadId.make("remote") }),
environmentId: remoteEnvironmentId,
};

it("excludes an environment independently of project scope", () => {
expect(
filterSidebarV2VisibleThreads(
[local, remote],
null,
new Set(),
new Set([remoteEnvironmentId]),
),
).toEqual([local]);
expect(
filterSidebarV2VisibleThreads(
[local, remote],
new Set([`${remoteEnvironmentId}:${remote.projectId}`]),
new Set(),
new Set([remoteEnvironmentId]),
),
).toEqual([]);
});

it("keeps newly connected environments visible and clears back to all environments", () => {
const added = { ...remote, environmentId: EnvironmentId.make("environment-new") };
expect(
filterSidebarV2VisibleThreads(
[local, remote, added],
null,
new Set(),
new Set([remoteEnvironmentId]),
),
).toEqual([local, added]);
expect(filterSidebarV2VisibleThreads([local, remote], null, new Set())).toEqual([
local,
remote,
]);
});

it("can hide every environment, including threads without a listed project", () => {
const scratch = { ...local, projectId: ProjectId.make("scratch") };
expect(
filterSidebarV2VisibleThreads(
[scratch, remote],
null,
new Set(),
new Set([localEnvironmentId, remoteEnvironmentId]),
),
).toEqual([]);
});
});

describe("deleteSelectedThreadEntries", () => {
const entries = [{ threadKey: "one" }, { threadKey: "two" }, { threadKey: "three" }] as const;
const success = AsyncResult.success(undefined);
Expand Down Expand Up @@ -819,6 +879,67 @@ describe("sortScopedProjectsForSidebar", () => {
});

describe("sortSidebarV2ProjectGroups", () => {
it.each(["created_at", "updated_at"] as const)(
"ignores excluded environments when sorting project activity by %s",
(sortOrder) => {
const remoteEnvironmentId = EnvironmentId.make("environment-remote");
const olderProjectId = ProjectId.make("project-older");
const newerProjectId = ProjectId.make("project-newer");
const remoteProjectId = ProjectId.make("project-remote");
const olderProject = {
...makeProject({ id: olderProjectId }),
projectKey: "logical-older",
memberProjectRefs: [
{ environmentId: localEnvironmentId, projectId: olderProjectId },
{ environmentId: remoteEnvironmentId, projectId: remoteProjectId },
],
};
const newerProject = {
...makeProject({ id: newerProjectId }),
projectKey: "logical-newer",
memberProjectRefs: [{ environmentId: localEnvironmentId, projectId: newerProjectId }],
};
const projects = [olderProject, newerProject];
const threads = [
makeThread({
projectId: olderProjectId,
createdAt: "2026-03-09T10:01:00.000Z",
updatedAt: "2026-03-09T10:01:00.000Z",
}),
makeThread({
projectId: newerProjectId,
createdAt: "2026-03-09T10:05:00.000Z",
updatedAt: "2026-03-09T10:05:00.000Z",
}),
{
...makeThread({
projectId: remoteProjectId,
createdAt: "2026-03-09T10:10:00.000Z",
updatedAt: "2026-03-09T10:10:00.000Z",
}),
environmentId: remoteEnvironmentId,
},
];

expect(
sortSidebarV2ProjectGroups(
projects,
threads,
sortOrder,
new Set(),
new Set([remoteEnvironmentId]),
),
).toEqual([newerProject, olderProject]);
expect(sortSidebarV2ProjectGroups(projects, threads, sortOrder, new Set())).toEqual([
olderProject,
newerProject,
]);
expect(
sortSidebarV2ProjectGroups(projects, threads, sortOrder, new Set(), new Set()),
).toEqual([olderProject, newerProject]);
},
);

it("does not let a hidden subagent thread reorder projects", () => {
const olderProjectId = ProjectId.make("project-older");
const newerProjectId = ProjectId.make("project-newer");
Expand Down Expand Up @@ -1534,17 +1655,18 @@ describe("resolveSidebarRouteReveal", () => {
const hiddenKey = "environment-test:thread-test";
const findThread = (key: string) => (key === hiddenKey ? hidden : undefined);

it("widens the scope for a newly opened thread the scope hides", () => {
it.each([false, true])("only widens for a visible environment (excluded: %s)", (excluded) => {
expect(
resolveSidebarRouteReveal({
reveal: { observedKey: "environment-test:other", pendingKey: null },
routeKey: hiddenKey,
isSearching: false,
scopedProjectKeys,
delegatedUnderParent: new Set(),
excludedEnvironmentIds: excluded ? new Set([hidden.environmentId]) : new Set(),
findThread,
}),
).toEqual({ pendingKey: hiddenKey, widensScope: true });
).toEqual({ pendingKey: hiddenKey, widensScope: !excluded });
});

it("never widens for a thread whose reveal already completed", () => {
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ export function filterSidebarV2VisibleThreads<
threads: readonly T[],
scopedProjectKeys: ReadonlySet<string> | null,
delegatedUnderParent: ReadonlySet<string>,
excludedEnvironmentIds?: ReadonlySet<string>,
): T[] {
return threads.filter(
(thread) =>
Expand All @@ -627,6 +628,7 @@ export function filterSidebarV2VisibleThreads<
thread.lineage,
delegatedUnderParent,
) &&
!excludedEnvironmentIds?.has(thread.environmentId) &&
(scopedProjectKeys === null ||
scopedProjectKeys.has(`${thread.environmentId}:${thread.projectId}`)),
);
Expand Down Expand Up @@ -1444,10 +1446,11 @@ export function sortSidebarV2ProjectGroups<
threads: readonly TThread[],
sortOrder: SidebarProjectSortOrder,
delegatedUnderParent: ReadonlySet<string>,
excludedEnvironmentIds?: ReadonlySet<string>,
): TProject[] {
return sortLogicalProjectsForSidebar(
projects,
filterSidebarV2VisibleThreads(threads, null, delegatedUnderParent),
filterSidebarV2VisibleThreads(threads, null, delegatedUnderParent, excludedEnvironmentIds),
sortOrder,
);
}
Expand Down
Loading
Loading