Skip to content
Merged
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
50 changes: 50 additions & 0 deletions apps/web/src/components/CommandPalette.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
enumerateCommandPaletteItems,
filterPinnedBrowseEntries,
filterCommandPaletteGroups,
getExistingThreadForProjectAdd,
reduceCommandPaletteUiState,
type CommandPaletteGroup,
} from "./CommandPalette.logic";
Expand Down Expand Up @@ -343,6 +344,55 @@ function makeThread(overrides: Partial<Thread> = {}): Thread {
};
}

describe("adding an existing project", () => {
it("starts a new thread when the latest unarchived thread is settled", () => {
const olderActive = makeThread({
id: ThreadId.make("older-active"),
updatedAt: "2026-03-01T00:00:00.000Z",
});
const newestSettled = makeThread({
id: ThreadId.make("newest-settled"),
updatedAt: "2026-03-02T00:00:00.000Z",
settledOverride: "settled",
settledAt: "2026-03-02T00:00:00.000Z",
});
expect(
getExistingThreadForProjectAdd([olderActive, newestSettled], PROJECT_ID, "updated_at"),
).toBeNull();

expect(
getExistingThreadForProjectAdd(
[olderActive, { ...newestSettled, settledOverride: "active", settledAt: null }],
PROJECT_ID,
"updated_at",
)?.id,
).toBe(newestSettled.id);
});

it("ignores archived threads while leaving ordinary active threads available", () => {
const active = makeThread({ id: ThreadId.make("active") });
const archived = makeThread({
id: ThreadId.make("archived"),
updatedAt: "2026-03-03T00:00:00.000Z",
archivedAt: "2026-03-03T00:00:00.000Z",
});
expect(getExistingThreadForProjectAdd([active, archived], PROJECT_ID, "updated_at")?.id).toBe(
active.id,
);
});

it("keeps snoozed threads eligible because snoozing does not settle them", () => {
const snoozed = makeThread({
id: ThreadId.make("snoozed"),
snoozedAt: "2026-03-01T00:00:00.000Z",
snoozedUntil: "2026-03-04T00:00:00.000Z",
});
expect(getExistingThreadForProjectAdd([snoozed], PROJECT_ID, "updated_at")?.id).toBe(
snoozed.id,
);
});
});

describe("buildProjectActionItems", () => {
it("shows the grouped display name but keeps the real title for icons", () => {
const project = makeProject({ title: "fleet", workspaceRoot: "/Users/theo/Code/p/fleet" });
Expand Down
16 changes: 15 additions & 1 deletion apps/web/src/components/CommandPalette.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests
import type { CommandPaletteLinkedThreads } from "../commandPaletteBus";
import {
type EnvironmentId,
type ProjectId,
type FilesystemBrowseEntry,
type KeybindingCommand,
THREAD_JUMP_KEYBINDING_COMMANDS,
Expand All @@ -11,7 +12,7 @@ import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings";
import * as Arr from "effect/Array";
import * as Result from "effect/Result";
import { type ReactNode } from "react";
import { sortThreads } from "../lib/threadSort";
import { getLatestThreadForProject, sortThreads, type ThreadSortInput } from "../lib/threadSort";
import { normalizeSearchText } from "../lib/utils";
import { formatRelativeTimeLabel } from "../timestampFormat";
import { type Project, type SidebarThreadSummary, type Thread } from "../types";
Expand All @@ -20,6 +21,19 @@ export const RECENT_THREAD_LIMIT = 12;
export const ITEM_ICON_CLASS = "size-4 text-icon-muted";
export const ADDON_ICON_CLASS = "size-4";

/** Adding an existing project should open its latest thread only while it remains active. */
export function getExistingThreadForProjectAdd<
T extends {
readonly id: string;
readonly projectId: ProjectId;
readonly archivedAt: string | null;
readonly settledOverride: string | null;
} & ThreadSortInput,
>(threads: readonly T[], projectId: ProjectId, sortOrder: SidebarThreadSortOrder): T | null {
const latestThread = getLatestThreadForProject(threads, projectId, sortOrder);
return latestThread?.settledOverride === "settled" ? null : latestThread;
}

/** A PR's relations include archived threads that normal palette search omits. */
export function buildLinkedThreadActionItems(
input: CommandPaletteLinkedThreads & {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ import {
filterPinnedBrowseEntries,
getCommandPaletteInputPlaceholder,
getCommandPaletteMode,
getExistingThreadForProjectAdd,
ITEM_ICON_CLASS,
RECENT_THREAD_LIMIT,
reduceCommandPaletteUiState,
Expand Down Expand Up @@ -2086,7 +2087,7 @@ function OpenCommandPaletteDialog(props: {
cwd,
);
if (existing) {
const latestThread = getLatestThreadForProject(
const latestThread = getExistingThreadForProjectAdd(
threads.filter((thread) => thread.environmentId === existing.environmentId),
existing.id,
clientSettings.sidebarThreadSortOrder,
Expand Down
Loading