Skip to content

Commit f0b61c6

Browse files
Merge pull request #21 from sidharthmirch/sidharthmirch/fix-cmd-number-keybindings
Fix cmd+number keybindings for tools and compare blocks - added cmd+shift+space to focus selected block
2 parents 8be197b + ccaa390 commit f0b61c6

2 files changed

Lines changed: 131 additions & 52 deletions

File tree

src/core/infra/ModelOrderStore.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ interface ModelOrderStore {
55
setModelOrder: (chatId: string, modelIds: string[]) => void;
66
getModelOrder: (chatId: string) => string[] | undefined;
77
clearChat: (chatId: string) => void;
8+
// The resolved visual order (after applying finish-time sorting, custom order, etc.)
9+
// Written by ToolsBlockView so keybinding handlers can use the correct visual index.
10+
currentVisualOrderByChatId: Map<string, string[]>;
11+
setCurrentVisualOrder: (chatId: string, modelIds: string[]) => void;
812
}
913

1014
const useModelOrderStore = create<ModelOrderStore>((set, get) => ({
@@ -21,10 +25,23 @@ const useModelOrderStore = create<ModelOrderStore>((set, get) => ({
2125

2226
clearChat: (chatId) =>
2327
set((state) => {
24-
if (!state.modelOrderByChatId.has(chatId)) return state;
25-
const next = new Map(state.modelOrderByChatId);
26-
next.delete(chatId);
27-
return { modelOrderByChatId: next };
28+
const nextModelOrder = new Map(state.modelOrderByChatId);
29+
nextModelOrder.delete(chatId);
30+
const nextVisualOrder = new Map(state.currentVisualOrderByChatId);
31+
nextVisualOrder.delete(chatId);
32+
return {
33+
modelOrderByChatId: nextModelOrder,
34+
currentVisualOrderByChatId: nextVisualOrder,
35+
};
36+
}),
37+
38+
currentVisualOrderByChatId: new Map(),
39+
40+
setCurrentVisualOrder: (chatId, modelIds) =>
41+
set((state) => {
42+
const next = new Map(state.currentVisualOrderByChatId);
43+
next.set(chatId, modelIds);
44+
return { currentVisualOrderByChatId: next };
2845
}),
2946
}));
3047

src/ui/components/MultiChat.tsx

Lines changed: 110 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,6 +1647,9 @@ function ToolsBlockView({
16471647
chatId ? state.modelOrderByChatId.get(chatId) : undefined,
16481648
);
16491649
const setModelOrder = useModelOrderStore((state) => state.setModelOrder);
1650+
const setCurrentVisualOrder = useModelOrderStore(
1651+
(state) => state.setCurrentVisualOrder,
1652+
);
16501653

16511654
// Track which models finished streaming and in what order, so we can sort
16521655
// finished models to the left (first finished = leftmost slot) when no
@@ -1770,37 +1773,55 @@ function ToolsBlockView({
17701773
}
17711774
}, [toolsBlock.chatMessages, minimizedModels, onMinimize]);
17721775

1773-
const activeMessages = [...toolsBlock.chatMessages]
1774-
.filter((m) => !minimizedModels.has(m.model))
1775-
.sort((a, b) => {
1776-
// Respect explicit drag-and-drop ordering if the user has set one.
1777-
if (customOrder) {
1778-
const aIdx = customOrder.indexOf(a.model);
1779-
const bIdx = customOrder.indexOf(b.model);
1780-
if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
1781-
if (aIdx !== -1) return -1;
1782-
if (bIdx !== -1) return 1;
1783-
}
1784-
// Default: finished (idle) models go left of still-streaming/loading
1785-
// models, sorted by completion order (first finished = leftmost slot).
1786-
const aIdle = a.state === "idle";
1787-
const bIdle = b.state === "idle";
1788-
if (aIdle !== bIdle) return aIdle ? -1 : 1;
1789-
if (aIdle && bIdle) {
1790-
const aFinishIdx = finishedModelsOrder.indexOf(a.model);
1791-
const bFinishIdx = finishedModelsOrder.indexOf(b.model);
1792-
if (aFinishIdx !== -1 && bFinishIdx !== -1)
1793-
return aFinishIdx - bFinishIdx;
1794-
}
1795-
return getDisplayName(a.model).localeCompare(
1796-
getDisplayName(b.model),
1797-
);
1798-
});
1776+
const activeMessages = useMemo(
1777+
() =>
1778+
[...toolsBlock.chatMessages]
1779+
.filter((m) => !minimizedModels.has(m.model))
1780+
.sort((a, b) => {
1781+
// Respect explicit drag-and-drop ordering if the user has set one.
1782+
if (customOrder) {
1783+
const aIdx = customOrder.indexOf(a.model);
1784+
const bIdx = customOrder.indexOf(b.model);
1785+
if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
1786+
if (aIdx !== -1) return -1;
1787+
if (bIdx !== -1) return 1;
1788+
}
1789+
// Default: finished (idle) models go left of still-streaming/loading
1790+
// models, sorted by completion order (first finished = leftmost slot).
1791+
const aIdle = a.state === "idle";
1792+
const bIdle = b.state === "idle";
1793+
if (aIdle !== bIdle) return aIdle ? -1 : 1;
1794+
if (aIdle && bIdle) {
1795+
const aFinishIdx = finishedModelsOrder.indexOf(a.model);
1796+
const bFinishIdx = finishedModelsOrder.indexOf(b.model);
1797+
if (aFinishIdx !== -1 && bFinishIdx !== -1)
1798+
return aFinishIdx - bFinishIdx;
1799+
}
1800+
return getDisplayName(a.model).localeCompare(
1801+
getDisplayName(b.model),
1802+
);
1803+
}),
1804+
[
1805+
toolsBlock.chatMessages,
1806+
minimizedModels,
1807+
customOrder,
1808+
finishedModelsOrder,
1809+
getDisplayName,
1810+
],
1811+
);
17991812
const toolsItemOrder = useMemo(
18001813
() => activeMessages.map((m) => m.model),
18011814
[activeMessages],
18021815
);
18031816

1817+
// Keep the store in sync with the current visual order so cmd+number
1818+
// keybindings can index into the same order the user sees on screen.
1819+
useEffect(() => {
1820+
if (chatId) {
1821+
setCurrentVisualOrder(chatId, toolsItemOrder);
1822+
}
1823+
}, [chatId, toolsItemOrder, setCurrentVisualOrder]);
1824+
18041825
const handleAddModel = (modelId: string) => {
18051826
void (async () => {
18061827
try {
@@ -1889,6 +1910,7 @@ function ToolsBlockView({
18891910
key={message.model}
18901911
layout
18911912
layoutId={`tools-col-${message.model}-${messageSetId}`}
1913+
data-tools-message-id={message.id}
18921914
>
18931915
<SortableColumnItem
18941916
id={message.model}
@@ -2414,6 +2436,9 @@ export default function MultiChat() {
24142436
const customCompareOrder = useModelOrderStore((state) =>
24152437
chatId ? state.modelOrderByChatId.get(chatId) : undefined,
24162438
);
2439+
const currentVisualOrder = useModelOrderStore((state) =>
2440+
chatId ? state.currentVisualOrderByChatId.get(chatId) : undefined,
2441+
);
24172442
const sortedCompareMessages = useMemo(() => {
24182443
if (!currentCompareBlock) return [];
24192444
return [...currentCompareBlock.messages].sort((a, b) => {
@@ -2662,31 +2687,67 @@ export default function MultiChat() {
26622687
if (e.metaKey && /^[1-8]$/.test(e.key)) {
26632688
// cmd + 1-8: select message at index
26642689
e.preventDefault();
2665-
if (currentMessageSet?.selectedBlockType !== "compare") {
2666-
console.warn(
2667-
"skipping cmd+1-8 because we're not in compare mode",
2668-
);
2669-
return;
2670-
}
2671-
// Get message at index (1-based)
26722690
const index = parseInt(e.key) - 1;
2673-
if (
2674-
!currentCompareBlock ||
2675-
sortedCompareMessages.length <= index
2676-
) {
2677-
console.warn(
2678-
`couldn't select message at ${index} from cmd+${index + 1}`,
2691+
if (currentMessageSet?.selectedBlockType === "compare") {
2692+
if (
2693+
!currentCompareBlock ||
2694+
sortedCompareMessages.length <= index
2695+
) {
2696+
console.warn(
2697+
`couldn't select message at ${index} from cmd+${index + 1}`,
2698+
);
2699+
return;
2700+
}
2701+
selectMessage.mutate({
2702+
chatId: chatId!,
2703+
messageSetId: currentMessageSet.id,
2704+
messageId: sortedCompareMessages[index].id,
2705+
blockType: "compare",
2706+
});
2707+
} else if (currentMessageSet?.selectedBlockType === "tools") {
2708+
// Use the resolved visual order so cmd+1 selects the
2709+
// leftmost column regardless of streaming completion order.
2710+
const allMsgs = currentMessageSet.toolsBlock.chatMessages;
2711+
const orderedMsgs = currentVisualOrder
2712+
? currentVisualOrder
2713+
.map((id) => allMsgs.find((m) => m.model === id))
2714+
.filter((m) => m !== undefined)
2715+
: allMsgs;
2716+
if (orderedMsgs.length <= index) {
2717+
console.warn(
2718+
`couldn't select message at ${index} from cmd+${index + 1}`,
2719+
);
2720+
return;
2721+
}
2722+
selectMessage.mutate({
2723+
chatId: chatId!,
2724+
messageSetId: currentMessageSet.id,
2725+
messageId: orderedMsgs[index].id,
2726+
blockType: "tools",
2727+
});
2728+
}
2729+
} else if (
2730+
(e.metaKey || e.ctrlKey) &&
2731+
e.shiftKey &&
2732+
e.key === " "
2733+
) {
2734+
// cmd/ctrl + shift + space: scroll selected chat column into view
2735+
e.preventDefault();
2736+
const selectedMsg =
2737+
currentMessageSet?.toolsBlock?.chatMessages.find(
2738+
(m) => m.selected,
26792739
);
2680-
return;
2740+
if (selectedMsg) {
2741+
document
2742+
.querySelector(
2743+
`[data-tools-message-id="${selectedMsg.id}"]`,
2744+
)
2745+
?.scrollIntoView({
2746+
behavior: "smooth",
2747+
inline: "nearest",
2748+
block: "nearest",
2749+
});
26812750
}
2682-
const message = sortedCompareMessages[index];
2683-
2684-
selectMessage.mutate({
2685-
chatId: chatId!,
2686-
messageSetId: currentMessageSet.id,
2687-
messageId: message.id,
2688-
blockType: "compare",
2689-
});
26902751
} else if (e.metaKey && e.key === "s" && !e.shiftKey) {
26912752
e.preventDefault();
26922753
if (!currentMessageSet) return;
@@ -2746,6 +2807,7 @@ export default function MultiChat() {
27462807
setVisionModeEnabled,
27472808
// nextTools,
27482809
handleToggleVisionMode,
2810+
currentVisualOrder,
27492811
]);
27502812

27512813
const scrollToLatestMessageSet = useCallback(() => {

0 commit comments

Comments
 (0)