diff --git a/package.json b/package.json
index 1906318dc..9a17fab36 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"version": "0.8.2",
"license": "Apache-2.0",
"type": "module",
- "packageManager": "pnpm@11.5.0+sha512.dbfcc4f81cf48597afd4bc391ffdf12c11f1a9fb83a395bfa6b0a2d9cc2fd8ffebafdb1ccbd529632153f793904c2615b7f09fe1a345473fd1c35845172a8eb1",
+ "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
"engines": {
"node": ">=22"
},
diff --git a/src/app/App.tsx b/src/app/App.tsx
index ad74bf712..d6bcf9b3a 100644
--- a/src/app/App.tsx
+++ b/src/app/App.tsx
@@ -284,6 +284,7 @@ export default function App() {
const miniOpen = useChatStore((s) => s.mini.open);
const miniPresence = usePresence(miniOpen, 200);
const openMini = useChatStore((s) => s.openMini);
+ const toggleMini = useChatStore((s) => s.toggleMini);
const focusInput = useChatStore((s) => s.focusInput);
const openPanel = useChatStore((s) => s.openPanel);
const panelOpen = useChatStore((s) => s.panelOpen);
@@ -653,7 +654,11 @@ export default function App() {
"tab.close": handleCloseTabOrPane,
"tab.next": () => stepSwitcher(1),
"tab.prev": () => stepSwitcher(-1),
- "tab.selectByIndex": (e) => selectByIndex(parseInt(e.key, 10) - 1),
+ "tab.selectByIndex": (e) =>
+ selectByIndex(
+ parseInt(e.key, 10) - 1,
+ activeSpaceId ?? DEFAULT_SPACE_ID,
+ ),
"space.next": () => cycleSpace(1),
"space.prev": () => cycleSpace(-1),
"space.overview": () => setSwitcherOpen(true),
@@ -671,6 +676,13 @@ export default function App() {
"blocks.next": () => navigateFocusedBlocks(1),
"search.focus": () => searchInlineRef.current?.focus(),
"ai.toggle": togglePanelAndFocus,
+ "ai.toggleMini": () => {
+ if (!hasComposer) {
+ void openSettingsWindow("models");
+ return;
+ }
+ toggleMini();
+ },
"ai.askSelection": askFromSelection,
"agent.focusAttention": () => {
const t = nextAttentionTarget();
@@ -700,7 +712,9 @@ export default function App() {
splitActivePaneInActiveTab,
focusNextPaneInTab,
toggleSourceControl,
+ hasComposer,
togglePanelAndFocus,
+ toggleMini,
askFromSelection,
toggleSidebar,
toggleExplorerFocus,
@@ -994,6 +1008,12 @@ export default function App() {
openSpacesOverview: () => setSwitcherOpen(true),
newSpace: () => void handleNewSpace(),
switchSpace: (id) => useSpaces.getState().setActive(id),
+ terminalTabs: tabs.filter(
+ (t) =>
+ t.kind === "terminal" &&
+ t.spaceId === (activeSpaceId ?? DEFAULT_SPACE_ID),
+ ),
+ switchTab: (id) => setActiveId(id),
})
: [],
[
diff --git a/src/modules/ai/components/AiStatusBarControls.tsx b/src/modules/ai/components/AiStatusBarControls.tsx
index 72578087b..e9d1698f7 100644
--- a/src/modules/ai/components/AiStatusBarControls.tsx
+++ b/src/modules/ai/components/AiStatusBarControls.tsx
@@ -170,9 +170,8 @@ export function AiStatusBarControls() {
diff --git a/src/modules/command-palette/commands.ts b/src/modules/command-palette/commands.ts
index 9f5e1c952..4680a1b03 100644
--- a/src/modules/command-palette/commands.ts
+++ b/src/modules/command-palette/commands.ts
@@ -24,6 +24,7 @@ import type { PaletteItem } from "./types";
export const COMMAND_GROUPS = [
"General",
"Spaces",
+ "Terminals",
"Tabs",
"Panes",
"Git",
@@ -60,6 +61,8 @@ export type CommandPaletteActionContext = {
openSpacesOverview: () => void;
newSpace: () => void;
switchSpace: (id: string) => void;
+ terminalTabs: { id: number; title: string; customTitle?: string }[];
+ switchTab: (id: number) => void;
};
const noop = () => {};
@@ -134,6 +137,15 @@ export function createCommandItems(
sp.id === ctx.activeSpaceId ? "Current space" : undefined,
run: () => ctx.switchSpace(sp.id),
})),
+ ...ctx.terminalTabs.map((tab) => ({
+ id: `terminals.switch.${tab.id}`,
+ title: tab.customTitle || tab.title,
+ group: "Terminals" as const,
+ keywords: ["terminal", "switch", "tab", tab.customTitle ?? "", tab.title],
+ icon: TerminalIcon,
+ disabledReason: tab.id === ctx.activeId ? "Current tab" : undefined,
+ run: () => ctx.switchTab(tab.id),
+ })),
{
id: "tab.new",
title: "New terminal",
diff --git a/src/modules/shortcuts/shortcuts.ts b/src/modules/shortcuts/shortcuts.ts
index 7285a84ca..0b7ee8f31 100644
--- a/src/modules/shortcuts/shortcuts.ts
+++ b/src/modules/shortcuts/shortcuts.ts
@@ -36,6 +36,7 @@ export type ShortcutId =
| "view.zoomReset"
| "view.zenMode"
| "ai.toggle"
+ | "ai.toggleMini"
| "ai.askSelection"
| "agent.focusAttention"
| "settings.open"
@@ -241,6 +242,12 @@ export const SHORTCUTS: Shortcut[] = [
group: "AI",
defaultBindings: [{ [MOD_PROP]: true, key: "i" }],
},
+ {
+ id: "ai.toggleMini",
+ label: "Toggle AI chat window",
+ group: "AI",
+ defaultBindings: [{ [MOD_PROP]: true, shift: true, key: "i" }],
+ },
{
id: "ai.askSelection",
label: "Ask AI about selection",
diff --git a/src/modules/tabs/lib/pickTabBySpaceIndex.test.ts b/src/modules/tabs/lib/pickTabBySpaceIndex.test.ts
new file mode 100644
index 000000000..def7861f9
--- /dev/null
+++ b/src/modules/tabs/lib/pickTabBySpaceIndex.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from "vitest";
+import { pickTabBySpaceIndex, type Tab } from "./useTabs";
+
+function term(id: number, spaceId: string): Tab {
+ return {
+ id,
+ kind: "terminal",
+ spaceId,
+ title: "shell",
+ paneTree: { kind: "leaf", id: id * 10 },
+ activeLeafId: id * 10,
+ } as Tab;
+}
+
+describe("pickTabBySpaceIndex", () => {
+ const tabs = [term(1, "a"), term(2, "b"), term(3, "b")];
+
+ it("Cmd+1 in space B returns B's first tab, not A's", () => {
+ expect(pickTabBySpaceIndex(tabs, 0, "b")?.id).toBe(2);
+ });
+
+ it("Cmd+2 in space B returns B's second tab", () => {
+ expect(pickTabBySpaceIndex(tabs, 1, "b")?.id).toBe(3);
+ });
+
+ it("Cmd+3 in space B returns undefined (does nothing)", () => {
+ expect(pickTabBySpaceIndex(tabs, 2, "b")).toBeUndefined();
+ });
+
+ it("Cmd+1 in space A returns A's only tab", () => {
+ expect(pickTabBySpaceIndex(tabs, 0, "a")?.id).toBe(1);
+ });
+
+ it("returns undefined for an empty space", () => {
+ expect(pickTabBySpaceIndex(tabs, 0, "c")).toBeUndefined();
+ });
+});
diff --git a/src/modules/tabs/lib/useTabs.ts b/src/modules/tabs/lib/useTabs.ts
index 3abf5bba4..044578c35 100644
--- a/src/modules/tabs/lib/useTabs.ts
+++ b/src/modules/tabs/lib/useTabs.ts
@@ -148,6 +148,17 @@ function titleFromUrl(url: string): string {
export const DEFAULT_SPACE_ID = "default";
+// Returns the tab at position `idx` within the given space, or undefined when
+// idx is out of range or no matching space tab exists.
+export function pickTabBySpaceIndex(
+ tabs: Tab[],
+ idx: number,
+ spaceId: string,
+): Tab | undefined {
+ const pool = tabs.filter((t) => t.spaceId === spaceId);
+ return pool[idx];
+}
+
// Next active after close, scoped to the closing tab's space. null = last tab of
// its space, which callers treat as "refuse to close".
export function nextActiveInSpace(
@@ -951,8 +962,10 @@ export function useTabs(initial?: Partial) {
}, []);
const selectByIndex = useCallback(
- (idx: number) => {
- const t = tabs[idx];
+ (idx: number, spaceId?: string) => {
+ const t = spaceId
+ ? pickTabBySpaceIndex(tabs, idx, spaceId)
+ : tabs[idx];
if (t) setActiveId(t.id);
},
[tabs],