Skip to content
Draft
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
9 changes: 9 additions & 0 deletions apps/server/src/keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [
{ key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" },
{ key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" },
{ key: "mod+o", command: "editor.openFavorite" },
{ key: "mod+1", command: "thread.jump.1" },
{ key: "mod+2", command: "thread.jump.2" },
{ key: "mod+3", command: "thread.jump.3" },
{ key: "mod+4", command: "thread.jump.4" },
{ key: "mod+5", command: "thread.jump.5" },
{ key: "mod+6", command: "thread.jump.6" },
{ key: "mod+7", command: "thread.jump.7" },
{ key: "mod+8", command: "thread.jump.8" },
{ key: "mod+9", command: "thread.jump.9" },
];

function normalizeKeyToken(token: string): string {
Expand Down
69 changes: 69 additions & 0 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";

import {
getFallbackThreadIdAfterDelete,
getVisibleThreadJumpTargets,
getVisibleThreadsForProject,
getProjectSortTimestamp,
hasUnseenCompletion,
Expand Down Expand Up @@ -96,6 +97,74 @@ describe("resolveSidebarNewThreadEnvMode", () => {
});
});

function makeJumpProject(
expanded: boolean,
threadIds: ThreadId[],
shouldShowThreadPanel = expanded,
) {
return {
project: { expanded },
renderedThreads: threadIds.map((id) => ({ id })),
shouldShowThreadPanel,
};
}

describe("getVisibleThreadJumpTargets", () => {
function tid(n: number): ThreadId {
return ThreadId.makeUnsafe(`thread-${n}`);
}

it("returns thread IDs from expanded projects only", () => {
const targets = getVisibleThreadJumpTargets([
makeJumpProject(true, [tid(1), tid(2)]),
makeJumpProject(false, [tid(3)]),
makeJumpProject(true, [tid(4)]),
]);

expect(targets).toEqual([tid(1), tid(2), tid(4)]);
});

it("skips projects where shouldShowThreadPanel is false", () => {
const targets = getVisibleThreadJumpTargets([
makeJumpProject(true, [tid(1)], false),
makeJumpProject(true, [tid(2)], true),
]);

expect(targets).toEqual([tid(2)]);
});

it("caps at 9 targets", () => {
const allThreads = Array.from({ length: 12 }, (_, i) => tid(i));
const targets = getVisibleThreadJumpTargets([makeJumpProject(true, allThreads)]);

expect(targets).toHaveLength(9);
expect(targets).toEqual(allThreads.slice(0, 9));
});

it("returns empty array when all projects are collapsed", () => {
const targets = getVisibleThreadJumpTargets([
makeJumpProject(false, [tid(1), tid(2)]),
makeJumpProject(false, [tid(3)]),
]);

expect(targets).toEqual([]);
});

it("returns empty array for no projects", () => {
expect(getVisibleThreadJumpTargets([])).toEqual([]);
});

it("preserves order across multiple expanded projects", () => {
const targets = getVisibleThreadJumpTargets([
makeJumpProject(true, [tid(1), tid(2)]),
makeJumpProject(true, [tid(3), tid(4)]),
makeJumpProject(true, [tid(5)]),
]);

expect(targets).toEqual([tid(1), tid(2), tid(3), tid(4), tid(5)]);
});
});

describe("resolveThreadStatusPill", () => {
const baseThread = {
interactionMode: "plan" as const,
Expand Down
32 changes: 32 additions & 0 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings";
import type { ThreadId } from "@t3tools/contracts";
import type { Thread } from "../types";
import { cn } from "../lib/utils";
import {
Expand All @@ -8,6 +9,7 @@ import {
} from "../session-logic";

export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]";
const MAX_THREAD_JUMP_TARGETS = 9;
export type SidebarNewThreadEnvMode = "local" | "worktree";
type SidebarProject = {
id: string;
Expand Down Expand Up @@ -67,6 +69,36 @@ export function resolveSidebarNewThreadEnvMode(input: {
return input.requestedEnvMode ?? input.defaultEnvMode;
}

/**
* Returns an ordered array of thread IDs eligible for Cmd/Ctrl+N jump shortcuts.
*
* Only threads that are actually visible in the sidebar are counted:
* - Collapsed projects are skipped entirely.
* - Threads hidden behind "show more" are excluded (already filtered by `renderedThreads`).
* - At most 9 targets are returned (matching Cmd+1 through Cmd+9).
*/
export function getVisibleThreadJumpTargets(
renderedProjects: ReadonlyArray<{
project: { expanded: boolean };
renderedThreads: ReadonlyArray<{ id: ThreadId }>;
shouldShowThreadPanel: boolean;
}>,
): ThreadId[] {
const targets: ThreadId[] = [];

for (const entry of renderedProjects) {
if (!entry.project.expanded) continue;
if (!entry.shouldShowThreadPanel) continue;

for (const thread of entry.renderedThreads) {
targets.push(thread.id);
if (targets.length >= MAX_THREAD_JUMP_TARGETS) return targets;
}
}

return targets;
}

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