Improve command center keyboard navigation and new tab shortcut
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
Platform,
|
Platform,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
import { memo, useEffect, useMemo, useRef, type ReactNode } from "react";
|
||||||
import { Plus, Settings } from "lucide-react-native";
|
import { Plus, Settings } from "lucide-react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { useCommandCenter } from "@/hooks/use-command-center";
|
import { useCommandCenter } from "@/hooks/use-command-center";
|
||||||
@@ -20,6 +21,37 @@ function agentKey(agent: Pick<AggregatedAgent, "serverId" | "id">): string {
|
|||||||
return `${agent.serverId}:${agent.id}`;
|
return `${agent.serverId}:${agent.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CommandCenterRowProps = {
|
||||||
|
active: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
onPress: () => void;
|
||||||
|
registerRow: (el: View | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommandCenterRow = memo(function CommandCenterRow({
|
||||||
|
active,
|
||||||
|
children,
|
||||||
|
onPress,
|
||||||
|
registerRow,
|
||||||
|
}: CommandCenterRowProps) {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
ref={registerRow}
|
||||||
|
style={({ hovered, pressed }) => [
|
||||||
|
styles.row,
|
||||||
|
(hovered || pressed || active) && {
|
||||||
|
backgroundColor: theme.colors.surface1,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export function CommandCenter() {
|
export function CommandCenter() {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const {
|
const {
|
||||||
@@ -33,10 +65,52 @@ export function CommandCenter() {
|
|||||||
handleSelectItem,
|
handleSelectItem,
|
||||||
} = useCommandCenter();
|
} = useCommandCenter();
|
||||||
|
|
||||||
|
const rowRefs = useRef<Map<number, View>>(new Map());
|
||||||
|
const resultsRef = useRef<ScrollView>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const row = rowRefs.current.get(activeIndex);
|
||||||
|
if (!row || typeof document === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scrollNode =
|
||||||
|
(resultsRef.current as
|
||||||
|
| (ScrollView & {
|
||||||
|
getScrollableNode?: () => HTMLElement | null;
|
||||||
|
})
|
||||||
|
| null)?.getScrollableNode?.() ?? null;
|
||||||
|
const rowEl = row as unknown as HTMLElement;
|
||||||
|
|
||||||
|
if (!scrollNode) {
|
||||||
|
rowEl.scrollIntoView?.({ block: "nearest" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowTop = rowEl.offsetTop;
|
||||||
|
const rowBottom = rowTop + rowEl.offsetHeight;
|
||||||
|
const visibleTop = scrollNode.scrollTop;
|
||||||
|
const visibleBottom = visibleTop + scrollNode.clientHeight;
|
||||||
|
|
||||||
|
if (rowTop < visibleTop) {
|
||||||
|
scrollNode.scrollTop = rowTop;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowBottom > visibleBottom) {
|
||||||
|
scrollNode.scrollTop = rowBottom - scrollNode.clientHeight;
|
||||||
|
}
|
||||||
|
}, [activeIndex]);
|
||||||
|
|
||||||
if (Platform.OS !== "web") return null;
|
if (Platform.OS !== "web") return null;
|
||||||
|
|
||||||
const actionItems = items.filter((item) => item.kind === "action");
|
const actionItems = useMemo(
|
||||||
const agentItems = items.filter((item) => item.kind === "agent");
|
() => items.filter((item) => item.kind === "action"),
|
||||||
|
[items]
|
||||||
|
);
|
||||||
|
const agentItems = useMemo(
|
||||||
|
() => items.filter((item) => item.kind === "agent"),
|
||||||
|
[items]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -71,6 +145,7 @@ export function CommandCenter() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
ref={resultsRef}
|
||||||
style={styles.results}
|
style={styles.results}
|
||||||
contentContainerStyle={styles.resultsContent}
|
contentContainerStyle={styles.resultsContent}
|
||||||
keyboardShouldPersistTaps="always"
|
keyboardShouldPersistTaps="always"
|
||||||
@@ -105,14 +180,13 @@ export function CommandCenter() {
|
|||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<CommandCenterRow
|
||||||
key={`action:${action.id}`}
|
key={`action:${action.id}`}
|
||||||
style={({ hovered, pressed }) => [
|
registerRow={(el: View | null) => {
|
||||||
styles.row,
|
if (el) rowRefs.current.set(index, el);
|
||||||
(hovered || pressed || active) && {
|
else rowRefs.current.delete(index);
|
||||||
backgroundColor: theme.colors.surface1,
|
}}
|
||||||
},
|
active={active}
|
||||||
]}
|
|
||||||
onPress={() => handleSelectItem(item)}
|
onPress={() => handleSelectItem(item)}
|
||||||
>
|
>
|
||||||
<View style={styles.rowContent}>
|
<View style={styles.rowContent}>
|
||||||
@@ -133,7 +207,7 @@ export function CommandCenter() {
|
|||||||
<Shortcut keys={action.shortcutKeys} style={styles.rowShortcut} />
|
<Shortcut keys={action.shortcutKeys} style={styles.rowShortcut} />
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</CommandCenterRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
@@ -154,14 +228,13 @@ export function CommandCenter() {
|
|||||||
const active = rowIndex === activeIndex;
|
const active = rowIndex === activeIndex;
|
||||||
const agent = item.agent;
|
const agent = item.agent;
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<CommandCenterRow
|
||||||
key={agentKey(agent)}
|
key={agentKey(agent)}
|
||||||
style={({ hovered, pressed }) => [
|
registerRow={(el: View | null) => {
|
||||||
styles.row,
|
if (el) rowRefs.current.set(rowIndex, el);
|
||||||
(hovered || pressed || active) && {
|
else rowRefs.current.delete(rowIndex);
|
||||||
backgroundColor: theme.colors.surface1,
|
}}
|
||||||
},
|
active={active}
|
||||||
]}
|
|
||||||
onPress={() => handleSelectItem(item)}
|
onPress={() => handleSelectItem(item)}
|
||||||
>
|
>
|
||||||
<View style={styles.rowContent}>
|
<View style={styles.rowContent}>
|
||||||
@@ -184,12 +257,12 @@ export function CommandCenter() {
|
|||||||
style={[styles.subtitle, { color: theme.colors.foregroundMuted }]}
|
style={[styles.subtitle, { color: theme.colors.foregroundMuted }]}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
{agent.serverLabel} · {shortenPath(agent.cwd)} · {formatTimeAgo(agent.lastActivityAt)}
|
{shortenPath(agent.cwd)} · {formatTimeAgo(agent.lastActivityAt)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</CommandCenterRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
@@ -277,6 +350,8 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
textContent: {
|
textContent: {
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
gap: 2,
|
gap: 2,
|
||||||
},
|
},
|
||||||
rowShortcut: {
|
rowShortcut: {
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import type { TextInput } from "react-native";
|
|||||||
import { router, usePathname, type Href } from "expo-router";
|
import { router, usePathname, type Href } from "expo-router";
|
||||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||||
import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher";
|
import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher";
|
||||||
import { useAggregatedAgents, type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
|
||||||
|
import { useAllAgentsList } from "@/hooks/use-all-agents-list";
|
||||||
|
import type { AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||||
import {
|
import {
|
||||||
clearCommandCenterFocusRestoreElement,
|
clearCommandCenterFocusRestoreElement,
|
||||||
takeCommandCenterFocusRestoreElement,
|
takeCommandCenterFocusRestoreElement,
|
||||||
@@ -23,8 +25,7 @@ function isMatch(agent: AggregatedAgent, query: string): boolean {
|
|||||||
const q = query.toLowerCase();
|
const q = query.toLowerCase();
|
||||||
const title = (agent.title ?? "New agent").toLowerCase();
|
const title = (agent.title ?? "New agent").toLowerCase();
|
||||||
const cwd = agent.cwd.toLowerCase();
|
const cwd = agent.cwd.toLowerCase();
|
||||||
const host = agent.serverLabel.toLowerCase();
|
return title.includes(q) || cwd.includes(q);
|
||||||
return title.includes(q) || cwd.includes(q) || host.includes(q);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortAgents(left: AggregatedAgent, right: AggregatedAgent): number {
|
function sortAgents(left: AggregatedAgent, right: AggregatedAgent): number {
|
||||||
@@ -103,34 +104,49 @@ export type CommandCenterItem =
|
|||||||
|
|
||||||
export function useCommandCenter() {
|
export function useCommandCenter() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { agents } = useAggregatedAgents();
|
const { daemons } = useDaemonRegistry();
|
||||||
const open = useKeyboardShortcutsStore((s) => s.commandCenterOpen);
|
const open = useKeyboardShortcutsStore((s) => s.commandCenterOpen);
|
||||||
const setOpen = useKeyboardShortcutsStore((s) => s.setCommandCenterOpen);
|
const setOpen = useKeyboardShortcutsStore((s) => s.setCommandCenterOpen);
|
||||||
const inputRef = useRef<TextInput>(null);
|
const inputRef = useRef<TextInput>(null);
|
||||||
const didNavigateRef = useRef(false);
|
const didNavigateRef = useRef(false);
|
||||||
const prevOpenRef = useRef(open);
|
const prevOpenRef = useRef(open);
|
||||||
|
const activeIndexRef = useRef(0);
|
||||||
|
const itemsRef = useRef<CommandCenterItem[]>([]);
|
||||||
|
const handleCloseRef = useRef<() => void>(() => undefined);
|
||||||
|
const handleSelectItemRef = useRef<(item: CommandCenterItem) => void>(() => undefined);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
|
|
||||||
|
const activeServerId = useMemo(() => {
|
||||||
|
const serverIdFromPath = parseServerIdFromPathname(pathname);
|
||||||
|
if (serverIdFromPath) {
|
||||||
|
const routeMatch = daemons.find((entry) => entry.serverId === serverIdFromPath);
|
||||||
|
if (routeMatch) {
|
||||||
|
return routeMatch.serverId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return daemons[0]?.serverId ?? null;
|
||||||
|
}, [daemons, pathname]);
|
||||||
|
|
||||||
|
const { agents } = useAllAgentsList({
|
||||||
|
serverId: activeServerId,
|
||||||
|
});
|
||||||
|
|
||||||
const agentResults = useMemo(() => {
|
const agentResults = useMemo(() => {
|
||||||
const filtered = agents.filter((agent) => isMatch(agent, query));
|
const filtered = agents.filter((agent) => isMatch(agent, query));
|
||||||
filtered.sort(sortAgents);
|
filtered.sort(sortAgents);
|
||||||
return filtered;
|
return filtered;
|
||||||
}, [agents, query]);
|
}, [agents, query]);
|
||||||
|
|
||||||
const fallbackServerId = agents[0]?.serverId ?? null;
|
|
||||||
|
|
||||||
const newAgentRoute = useMemo<Href>(() => {
|
const newAgentRoute = useMemo<Href>(() => {
|
||||||
const serverIdFromPath =
|
const serverIdFromPath = activeServerId;
|
||||||
parseServerIdFromPathname(pathname) ?? fallbackServerId;
|
|
||||||
return serverIdFromPath ? (buildHostOpenProjectRoute(serverIdFromPath) as Href) : "/";
|
return serverIdFromPath ? (buildHostOpenProjectRoute(serverIdFromPath) as Href) : "/";
|
||||||
}, [fallbackServerId, pathname]);
|
}, [activeServerId]);
|
||||||
|
|
||||||
const settingsRoute = useMemo<Href>(() => {
|
const settingsRoute = useMemo<Href>(() => {
|
||||||
const serverIdFromPath =
|
const serverIdFromPath = activeServerId;
|
||||||
parseServerIdFromPathname(pathname) ?? fallbackServerId;
|
|
||||||
return serverIdFromPath ? (buildHostSettingsRoute(serverIdFromPath) as Href) : "/";
|
return serverIdFromPath ? (buildHostSettingsRoute(serverIdFromPath) as Href) : "/";
|
||||||
}, [fallbackServerId, pathname]);
|
}, [activeServerId]);
|
||||||
|
|
||||||
const actionItems = useMemo(() => {
|
const actionItems = useMemo(() => {
|
||||||
return COMMAND_CENTER_ACTIONS.filter((action) =>
|
return COMMAND_CENTER_ACTIONS.filter((action) =>
|
||||||
@@ -209,6 +225,22 @@ export function useCommandCenter() {
|
|||||||
[handleSelectAction, handleSelectAgent]
|
[handleSelectAction, handleSelectAgent]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
activeIndexRef.current = activeIndex;
|
||||||
|
}, [activeIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
itemsRef.current = items;
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleCloseRef.current = handleClose;
|
||||||
|
}, [handleClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleSelectItemRef.current = handleSelectItem;
|
||||||
|
}, [handleSelectItem]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const prevOpen = prevOpenRef.current;
|
const prevOpen = prevOpenRef.current;
|
||||||
prevOpenRef.current = open;
|
prevOpenRef.current = open;
|
||||||
@@ -259,6 +291,7 @@ export function useCommandCenter() {
|
|||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
||||||
const handler = (event: KeyboardEvent) => {
|
const handler = (event: KeyboardEvent) => {
|
||||||
|
const currentItems = itemsRef.current;
|
||||||
const key = event.key;
|
const key = event.key;
|
||||||
if (
|
if (
|
||||||
key !== "ArrowDown" &&
|
key !== "ArrowDown" &&
|
||||||
@@ -271,26 +304,29 @@ export function useCommandCenter() {
|
|||||||
|
|
||||||
if (key === "Escape") {
|
if (key === "Escape") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleClose();
|
handleCloseRef.current();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "Enter") {
|
if (key === "Enter") {
|
||||||
if (items.length === 0) return;
|
if (currentItems.length === 0) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const index = Math.max(0, Math.min(activeIndex, items.length - 1));
|
const index = Math.max(
|
||||||
handleSelectItem(items[index]!);
|
0,
|
||||||
|
Math.min(activeIndexRef.current, currentItems.length - 1)
|
||||||
|
);
|
||||||
|
handleSelectItemRef.current(currentItems[index]!);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "ArrowDown" || key === "ArrowUp") {
|
if (key === "ArrowDown" || key === "ArrowUp") {
|
||||||
if (items.length === 0) return;
|
if (currentItems.length === 0) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setActiveIndex((current) => {
|
setActiveIndex((current) => {
|
||||||
const delta = key === "ArrowDown" ? 1 : -1;
|
const delta = key === "ArrowDown" ? 1 : -1;
|
||||||
const next = current + delta;
|
const next = current + delta;
|
||||||
if (next < 0) return items.length - 1;
|
if (next < 0) return currentItems.length - 1;
|
||||||
if (next >= items.length) return 0;
|
if (next >= currentItems.length) return 0;
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -299,7 +335,7 @@ export function useCommandCenter() {
|
|||||||
// react-native-web can stop propagation on key events, so listen in capture phase.
|
// react-native-web can stop propagation on key events, so listen in capture phase.
|
||||||
window.addEventListener("keydown", handler, true);
|
window.addEventListener("keydown", handler, true);
|
||||||
return () => window.removeEventListener("keydown", handler, true);
|
return () => window.removeEventListener("keydown", handler, true);
|
||||||
}, [activeIndex, handleClose, handleSelectItem, items, open]);
|
}, [open]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
open,
|
open,
|
||||||
|
|||||||
@@ -152,8 +152,9 @@ describe("keyboard-shortcuts", () => {
|
|||||||
payload: { delta: 1 },
|
payload: { delta: 1 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "matches Alt+Shift+T to open new tab",
|
name: "matches Mod+T to open new tab",
|
||||||
event: { key: "T", code: "KeyT", altKey: true, shiftKey: true },
|
event: { key: "t", code: "KeyT", metaKey: true },
|
||||||
|
context: { isMac: true },
|
||||||
action: "workspace.tab.new",
|
action: "workspace.tab.new",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -222,6 +223,10 @@ describe("keyboard-shortcuts", () => {
|
|||||||
event: { key: "n", code: "KeyN", metaKey: true, altKey: true },
|
event: { key: "n", code: "KeyN", metaKey: true, altKey: true },
|
||||||
context: { isMac: true },
|
context: { isMac: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "does not keep old Alt+Shift+T binding",
|
||||||
|
event: { key: "T", code: "KeyT", altKey: true, shiftKey: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "does not match question-mark shortcut inside editable scopes",
|
name: "does not match question-mark shortcut inside editable scopes",
|
||||||
event: { key: "?", code: "Slash", shiftKey: true },
|
event: { key: "?", code: "Slash", shiftKey: true },
|
||||||
@@ -269,6 +274,7 @@ describe("keyboard-shortcut help sections", () => {
|
|||||||
context: { isMac: true, isTauri: false },
|
context: { isMac: true, isTauri: false },
|
||||||
expectedKeys: {
|
expectedKeys: {
|
||||||
"new-agent": ["mod", "shift", "O"],
|
"new-agent": ["mod", "shift", "O"],
|
||||||
|
"workspace-tab-new": ["mod", "T"],
|
||||||
"workspace-jump-index": ["alt", "1-9"],
|
"workspace-jump-index": ["alt", "1-9"],
|
||||||
"workspace-tab-jump-index": ["alt", "shift", "1-9"],
|
"workspace-tab-jump-index": ["alt", "shift", "1-9"],
|
||||||
"workspace-tab-close-current": ["alt", "shift", "W"],
|
"workspace-tab-close-current": ["alt", "shift", "W"],
|
||||||
@@ -279,6 +285,7 @@ describe("keyboard-shortcut help sections", () => {
|
|||||||
context: { isMac: true, isTauri: true },
|
context: { isMac: true, isTauri: true },
|
||||||
expectedKeys: {
|
expectedKeys: {
|
||||||
"new-agent": ["mod", "shift", "O"],
|
"new-agent": ["mod", "shift", "O"],
|
||||||
|
"workspace-tab-new": ["mod", "T"],
|
||||||
"workspace-jump-index": ["mod", "1-9"],
|
"workspace-jump-index": ["mod", "1-9"],
|
||||||
"workspace-tab-jump-index": ["alt", "1-9"],
|
"workspace-tab-jump-index": ["alt", "1-9"],
|
||||||
"workspace-tab-close-current": ["mod", "W"],
|
"workspace-tab-close-current": ["mod", "W"],
|
||||||
|
|||||||
@@ -138,20 +138,19 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "workspace-tab-new-alt-shift-t",
|
id: "workspace-tab-new-mod-t",
|
||||||
action: "workspace.tab.new",
|
action: "workspace.tab.new",
|
||||||
matches: (event) =>
|
matches: (event) =>
|
||||||
!event.metaKey &&
|
isMod(event) &&
|
||||||
!event.ctrlKey &&
|
!event.altKey &&
|
||||||
event.altKey &&
|
!event.shiftKey &&
|
||||||
event.shiftKey &&
|
|
||||||
(event.code === "KeyT" || event.key.toLowerCase() === "t"),
|
(event.code === "KeyT" || event.key.toLowerCase() === "t"),
|
||||||
when: (context) => !context.commandCenterOpen,
|
when: (context) => !context.commandCenterOpen,
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-tab-new",
|
id: "workspace-tab-new",
|
||||||
section: "global",
|
section: "global",
|
||||||
label: "New agent tab",
|
label: "New agent tab",
|
||||||
keys: ["alt", "shift", "T"],
|
keys: ["mod", "T"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu";
|
} from "@/components/ui/context-menu";
|
||||||
|
import { Shortcut } from "@/components/ui/shortcut";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { useWorkspaceTabLayout } from "@/screens/workspace/use-workspace-tab-layout";
|
import { useWorkspaceTabLayout } from "@/screens/workspace/use-workspace-tab-layout";
|
||||||
import {
|
import {
|
||||||
@@ -367,7 +368,10 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
<Plus size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
<Plus size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom" align="end" offset={8}>
|
<TooltipContent side="bottom" align="end" offset={8}>
|
||||||
<Text style={styles.newTabTooltipText}>New agent tab</Text>
|
<View style={styles.newTabTooltipRow}>
|
||||||
|
<Text style={styles.newTabTooltipText}>New agent tab</Text>
|
||||||
|
<Shortcut keys={["mod", "T"]} style={styles.newTabTooltipShortcut} />
|
||||||
|
</View>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</View>
|
</View>
|
||||||
@@ -488,4 +492,13 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
},
|
},
|
||||||
|
newTabTooltipRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
},
|
||||||
|
newTabTooltipShortcut: {
|
||||||
|
backgroundColor: theme.colors.surface3,
|
||||||
|
borderColor: theme.colors.borderAccent,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
|||||||
import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
|
import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
|
||||||
import { ScreenHeader } from "@/components/headers/screen-header";
|
import { ScreenHeader } from "@/components/headers/screen-header";
|
||||||
import { Combobox } from "@/components/ui/combobox";
|
import { Combobox } from "@/components/ui/combobox";
|
||||||
|
import { Shortcut } from "@/components/ui/shortcut";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -1157,38 +1158,36 @@ function WorkspaceScreenContent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workspaceTabActionRequest.kind === "new") {
|
const request = workspaceTabActionRequest;
|
||||||
|
clearWorkspaceTabActionRequest(request.id);
|
||||||
|
|
||||||
|
if (request.kind === "new") {
|
||||||
handleCreateDraftTab();
|
handleCreateDraftTab();
|
||||||
clearWorkspaceTabActionRequest(workspaceTabActionRequest.id);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (workspaceTabActionRequest.kind === "close-current") {
|
if (request.kind === "close-current") {
|
||||||
if (activeTabId) {
|
if (activeTabId) {
|
||||||
void handleCloseTabById(activeTabId);
|
void handleCloseTabById(activeTabId);
|
||||||
}
|
}
|
||||||
clearWorkspaceTabActionRequest(workspaceTabActionRequest.id);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (workspaceTabActionRequest.kind === "navigate-index") {
|
if (request.kind === "navigate-index") {
|
||||||
const next = tabs[workspaceTabActionRequest.index - 1] ?? null;
|
const next = tabs[request.index - 1] ?? null;
|
||||||
if (next?.tabId) {
|
if (next?.tabId) {
|
||||||
navigateToTabId(next.tabId);
|
navigateToTabId(next.tabId);
|
||||||
}
|
}
|
||||||
clearWorkspaceTabActionRequest(workspaceTabActionRequest.id);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (workspaceTabActionRequest.kind === "navigate-relative") {
|
if (request.kind === "navigate-relative") {
|
||||||
if (tabs.length > 0) {
|
if (tabs.length > 0) {
|
||||||
const currentIndex = tabs.findIndex((tab) => tab.tabId === activeTabId);
|
const currentIndex = tabs.findIndex((tab) => tab.tabId === activeTabId);
|
||||||
const fromIndex = currentIndex >= 0 ? currentIndex : 0;
|
const fromIndex = currentIndex >= 0 ? currentIndex : 0;
|
||||||
const nextIndex =
|
const nextIndex = (fromIndex + request.delta + tabs.length) % tabs.length;
|
||||||
(fromIndex + workspaceTabActionRequest.delta + tabs.length) % tabs.length;
|
|
||||||
const next = tabs[nextIndex] ?? null;
|
const next = tabs[nextIndex] ?? null;
|
||||||
if (next?.tabId) {
|
if (next?.tabId) {
|
||||||
navigateToTabId(next.tabId);
|
navigateToTabId(next.tabId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clearWorkspaceTabActionRequest(workspaceTabActionRequest.id);
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
activeTabId,
|
activeTabId,
|
||||||
@@ -1525,7 +1524,10 @@ function WorkspaceScreenContent({
|
|||||||
<Plus size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
<Plus size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom" align="end" offset={8}>
|
<TooltipContent side="bottom" align="end" offset={8}>
|
||||||
<Text style={styles.newTabTooltipText}>New agent tab</Text>
|
<View style={styles.newTabTooltipRow}>
|
||||||
|
<Text style={styles.newTabTooltipText}>New agent tab</Text>
|
||||||
|
<Shortcut keys={["mod", "T"]} style={styles.newTabTooltipShortcut} />
|
||||||
|
</View>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
@@ -1728,6 +1730,15 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
color: theme.colors.popoverForeground,
|
color: theme.colors.popoverForeground,
|
||||||
},
|
},
|
||||||
|
newTabTooltipRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
},
|
||||||
|
newTabTooltipShortcut: {
|
||||||
|
backgroundColor: theme.colors.surface3,
|
||||||
|
borderColor: theme.colors.borderAccent,
|
||||||
|
},
|
||||||
mobileTabsRow: {
|
mobileTabsRow: {
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: theme.colors.border,
|
borderBottomColor: theme.colors.border,
|
||||||
|
|||||||
Reference in New Issue
Block a user