Skip to content
Open
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
96 changes: 87 additions & 9 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ function buildThreadJumpLabelMap(input: {
string,
NonNullable<ReturnType<typeof threadJumpCommandForIndex>>
>;
visibleThreadKeys: readonly string[];
}): ReadonlyMap<string, string> {
if (input.threadJumpCommandByKey.size === 0) {
return EMPTY_THREAD_JUMP_LABELS;
Expand All @@ -273,6 +274,20 @@ function buildThreadJumpLabelMap(input: {
mapping.set(threadKey, label);
}
}
for (const [index, threadKey] of input.visibleThreadKeys.slice(9, 99).entries()) {
const threadNumber = index + 10;
const firstDigit = Math.floor(threadNumber / 10);
const secondDigit = threadNumber % 10;
const command = threadJumpCommandForIndex(firstDigit - 1);
if (!command) continue;

const shortcutLabel = shortcutLabelForCommand(input.keybindings, command, shortcutLabelOptions);
const separator = shortcutLabel?.endsWith(String(firstDigit)) ? "" : ",";
const label = shortcutLabel ? `${shortcutLabel}${separator}${secondDigit}` : null;
if (label) {
mapping.set(threadKey, label);
}
}
return mapping.size > 0 ? mapping : EMPTY_THREAD_JUMP_LABELS;
}

Expand Down Expand Up @@ -2815,6 +2830,8 @@ export default function Sidebar() {
ReadonlySet<string>
>(() => new Set());
const { showThreadJumpHints, updateThreadJumpHintsVisibility } = useThreadJumpHintVisibility();
const pendingThreadJumpDigitRef = useRef<number | null>(null);
const pendingThreadJumpTimeoutRef = useRef<number | null>(null);
const dragInProgressRef = useRef(false);
const suppressProjectClickAfterDragRef = useRef(false);
const suppressProjectClickForContextMenuRef = useRef(false);
Expand Down Expand Up @@ -2949,8 +2966,17 @@ export default function Sidebar() {
shortcutLabelForCommand(keybindings, "chat.newLocal", newThreadShortcutLabelOptions) ??
shortcutLabelForCommand(keybindings, "chat.new", newThreadShortcutLabelOptions);

const clearPendingThreadJump = useCallback(() => {
pendingThreadJumpDigitRef.current = null;
if (pendingThreadJumpTimeoutRef.current !== null) {
window.clearTimeout(pendingThreadJumpTimeoutRef.current);
pendingThreadJumpTimeoutRef.current = null;
}
}, []);

const navigateToThread = useCallback(
(threadRef: ScopedThreadRef) => {
clearPendingThreadJump();
if (useThreadSelectionStore.getState().selectedThreadKeys.size > 0) {
clearSelection();
}
Expand All @@ -2963,7 +2989,7 @@ export default function Sidebar() {
params: buildThreadRouteParams(threadRef),
});
},
[clearSelection, isMobile, navigate, setOpenMobile, setSelectionAnchor],
[clearPendingThreadJump, clearSelection, isMobile, navigate, setOpenMobile, setSelectionAnchor],
);

const projectDnDSensors = useSensors(
Expand Down Expand Up @@ -3150,8 +3176,15 @@ export default function Sidebar() {
platform,
terminalOpen: sidebarShortcutContext.terminalOpen,
threadJumpCommandByKey,
visibleThreadKeys: visibleSidebarThreadKeys,
}),
[keybindings, platform, sidebarShortcutContext.terminalOpen, threadJumpCommandByKey],
[
keybindings,
platform,
sidebarShortcutContext.terminalOpen,
threadJumpCommandByKey,
visibleSidebarThreadKeys,
],
);
const shouldShowThreadJumpHintsNow = shouldShowThreadJumpHintsForModifiers(
shortcutModifiers,
Expand Down Expand Up @@ -3194,6 +3227,23 @@ export default function Sidebar() {
updateThreadJumpHintsVisibility(shouldShowThreadJumpHintsNow);
}, [shouldShowThreadJumpHintsNow, updateThreadJumpHintsVisibility]);

const navigateToThreadKey = useCallback(
(threadKey: string | undefined) => {
if (!threadKey) return;
const targetThread = sidebarThreadByKey.get(threadKey);
if (!targetThread) return;

navigateToThread(scopeThreadRef(targetThread.environmentId, targetThread.id));
},
[navigateToThread, sidebarThreadByKey],
);

useEffect(() => {
clearPendingThreadJump();
}, [clearPendingThreadJump, routeThreadKey]);

useEffect(() => () => clearPendingThreadJump(), [clearPendingThreadJump]);

useEffect(() => {
const onWindowKeyDown = (event: globalThis.KeyboardEvent) => {
const shortcutContext = getCurrentSidebarShortcutContext();
Expand All @@ -3202,6 +3252,29 @@ export default function Sidebar() {
return;
}

const pendingThreadJumpDigit = pendingThreadJumpDigitRef.current;
if (pendingThreadJumpDigit !== null) {
const digitCodeMatch = /^Digit([0-9])$/.exec(event.code);
const nextDigit = /^[0-9]$/.test(event.key)
? Number(event.key)
: digitCodeMatch?.[1]
? Number(digitCodeMatch[1])
: null;
if (nextDigit !== null) {
Comment thread
svenbuild marked this conversation as resolved.
const targetIndex = pendingThreadJumpDigit * 10 + nextDigit - 1;
clearPendingThreadJump();
event.preventDefault();
event.stopPropagation();
if (targetIndex >= orderedSidebarThreadKeys.length) return;
navigateToThreadKey(orderedSidebarThreadKeys[targetIndex]);
Comment on lines +3265 to +3269
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve single-digit jump when second digit is invalid

When a second digit is entered during the 250ms window, the handler clears the pending timeout and consumes the event before checking bounds; if the computed two-digit index is out of range, it returns without navigating. In practice (e.g., 12 visible threads, then thread.jump.1 followed by 9), the original single-digit fallback to thread 1 is canceled and the shortcut becomes a no-op, which makes jump behavior unpredictable for mistyped second digits.

Useful? React with 👍 / 👎.

return;
Comment thread
svenbuild marked this conversation as resolved.
}
Comment thread
cursor[bot] marked this conversation as resolved.

if (!["Alt", "Control", "Meta", "Shift"].includes(event.key)) {
clearPendingThreadJump();
}
}

const command = resolveShortcutCommand(event, keybindings, {
platform,
context: shortcutContext,
Expand All @@ -3223,6 +3296,7 @@ export default function Sidebar() {

event.preventDefault();
event.stopPropagation();
clearPendingThreadJump();
navigateToThread(scopeThreadRef(targetThread.environmentId, targetThread.id));
return;
}
Expand All @@ -3232,18 +3306,20 @@ export default function Sidebar() {
return;
}

const targetThreadKey = threadJumpThreadKeys[jumpIndex];
if (!targetThreadKey) {
return;
}
const targetThread = sidebarThreadByKey.get(targetThreadKey);
if (!targetThread) {
const firstDigit = jumpIndex + 1;
if (firstDigit > orderedSidebarThreadKeys.length) {
return;
}

event.preventDefault();
event.stopPropagation();
navigateToThread(scopeThreadRef(targetThread.environmentId, targetThread.id));
clearPendingThreadJump();
pendingThreadJumpDigitRef.current = firstDigit;
pendingThreadJumpTimeoutRef.current = window.setTimeout(() => {
pendingThreadJumpTimeoutRef.current = null;
pendingThreadJumpDigitRef.current = null;
navigateToThreadKey(threadJumpThreadKeys[jumpIndex]);
}, 250);
Comment thread
svenbuild marked this conversation as resolved.
};

window.addEventListener("keydown", onWindowKeyDown);
Expand All @@ -3252,9 +3328,11 @@ export default function Sidebar() {
window.removeEventListener("keydown", onWindowKeyDown);
};
}, [
clearPendingThreadJump,
Comment thread
svenbuild marked this conversation as resolved.
getCurrentSidebarShortcutContext,
keybindings,
navigateToThread,
navigateToThreadKey,
orderedSidebarThreadKeys,
platform,
routeThreadKey,
Expand Down
Loading