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
95 changes: 95 additions & 0 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { describe, expect, it } from "vitest";

import {
formatThreadJumpHintLabel,
getFallbackThreadIdAfterDelete,
getThreadJumpKey,
getVisibleThreadsForProject,
getProjectSortTimestamp,
hasUnseenCompletion,
isThreadJumpModifierPressed,
resolveThreadJumpIndex,
resolveProjectStatusIndicator,
resolveSidebarNewThreadEnvMode,
resolveThreadRowClassName,
Expand Down Expand Up @@ -96,6 +100,97 @@ describe("resolveSidebarNewThreadEnvMode", () => {
});
});

describe("thread jump helpers", () => {
it("assigns jump keys for the first nine visible threads", () => {
expect(getThreadJumpKey(0)).toBe("1");
expect(getThreadJumpKey(8)).toBe("9");
expect(getThreadJumpKey(9)).toBeNull();
});

it("detects the active jump modifier by platform", () => {
expect(
isThreadJumpModifierPressed(
{
key: "Meta",
metaKey: true,
ctrlKey: false,
shiftKey: false,
altKey: false,
},
"MacIntel",
),
).toBe(true);
expect(
isThreadJumpModifierPressed(
{
key: "Control",
metaKey: false,
ctrlKey: true,
shiftKey: false,
altKey: false,
},
"Win32",
),
).toBe(true);
expect(
isThreadJumpModifierPressed(
{
key: "Control",
metaKey: false,
ctrlKey: true,
shiftKey: true,
altKey: false,
},
"Win32",
),
).toBe(false);
});

it("resolves mod+digit events to zero-based visible thread indices", () => {
expect(
resolveThreadJumpIndex(
{
key: "1",
metaKey: true,
ctrlKey: false,
shiftKey: false,
altKey: false,
},
"MacIntel",
),
).toBe(0);
expect(
resolveThreadJumpIndex(
{
key: "9",
metaKey: false,
ctrlKey: true,
shiftKey: false,
altKey: false,
},
"Linux",
),
).toBe(8);
expect(
resolveThreadJumpIndex(
{
key: "0",
metaKey: false,
ctrlKey: true,
shiftKey: false,
altKey: false,
},
"Linux",
),
).toBeNull();
});

it("formats thread jump hint labels for macOS and non-macOS", () => {
expect(formatThreadJumpHintLabel("3", "MacIntel")).toBe("⌘3");
expect(formatThreadJumpHintLabel("3", "Linux")).toBe("Ctrl+3");
});
});

describe("resolveThreadStatusPill", () => {
const baseThread = {
interactionMode: "plan" as const,
Expand Down
45 changes: 44 additions & 1 deletion apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings";
import type { Thread } from "../types";
import { cn } from "../lib/utils";
import { cn, isMacPlatform } from "../lib/utils";
import {
findLatestProposedPlan,
hasActionableProposedPlan,
isLatestTurnSettled,
} from "../session-logic";

export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]";
const THREAD_JUMP_KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] as const;
export type SidebarNewThreadEnvMode = "local" | "worktree";
type SidebarProject = {
id: string;
Expand All @@ -16,6 +17,16 @@ type SidebarProject = {
updatedAt?: string | undefined;
};
type SidebarThreadSortInput = Pick<Thread, "createdAt" | "updatedAt" | "messages">;
type ThreadJumpKey = (typeof THREAD_JUMP_KEYS)[number];
export type { ThreadJumpKey };

export interface ThreadJumpEvent {
key: string;
metaKey: boolean;
ctrlKey: boolean;
shiftKey: boolean;
altKey: boolean;
}

export interface ThreadStatusPill {
label:
Expand Down Expand Up @@ -67,6 +78,38 @@ export function resolveSidebarNewThreadEnvMode(input: {
return input.requestedEnvMode ?? input.defaultEnvMode;
}

export function getThreadJumpKey(index: number): ThreadJumpKey | null {
return THREAD_JUMP_KEYS[index] ?? null;
}

export function isThreadJumpModifierPressed(
event: ThreadJumpEvent,
platform = navigator.platform,
): boolean {
return (
(isMacPlatform(platform) ? event.metaKey : event.ctrlKey) && !event.altKey && !event.shiftKey
);
}

export function resolveThreadJumpIndex(
event: ThreadJumpEvent,
platform = navigator.platform,
): number | null {
if (!isThreadJumpModifierPressed(event, platform)) {
return null;
}

const index = THREAD_JUMP_KEYS.indexOf(event.key as ThreadJumpKey);
return index === -1 ? null : index;
}

export function formatThreadJumpHintLabel(
key: ThreadJumpKey,
platform = navigator.platform,
): string {
return isMacPlatform(platform) ? `⌘${key}` : `Ctrl+${key}`;
}

export function resolveThreadRowClassName(input: {
isActive: boolean;
isSelected: boolean;
Expand Down
Loading