Skip to content
Open
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
60 changes: 59 additions & 1 deletion src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ import {
hasLeaf,
leafIds,
navigateFocusedBlocks,
submitToLeaf,
type TerminalPaneHandle,
useTerminalFileDrop,
writeToSession,
} from "@/modules/terminal";
import { useTerminalComposerStore } from "@/modules/terminal/composer/terminalComposerStore";
import {
SpaceSwitcher,
useSpaces,
Expand Down Expand Up @@ -273,6 +275,7 @@ export default function App() {

const [newEditorOpen, setNewEditorOpen] = useState(false);
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
const [terminalComposerOpen, setTerminalComposerOpen] = useState(false);
const [paletteInitialMode, setPaletteInitialMode] = useState<
"commands" | "content"
>("commands");
Expand Down Expand Up @@ -300,6 +303,42 @@ export default function App() {
const isEditorTab = activeTab?.kind === "editor";
const isGitHistoryTab = activeTab?.kind === "git-history";

const closeTerminalComposer = useCallback(
() => setTerminalComposerOpen(false),
[],
);
const toggleTerminalComposer = useCallback(() => {
if (!activeTerminalTab || activeLeafId === null) return;
setTerminalComposerOpen((open) => !open);
}, [activeTerminalTab, activeLeafId]);
const sendTerminalComposerText = useCallback(
(text: string) => {
if (activeLeafId === null) return;
submitToLeaf(activeLeafId, text);
setTerminalComposerOpen(false);
terminalRefs.current.get(activeLeafId)?.focus();
},
[activeLeafId],
);
const sendTerminalQueuedPromptText = useCallback(
(text: string): boolean => {
if (activeLeafId === null) return false;
if (!submitToLeaf(activeLeafId, text)) return false;
terminalRefs.current.get(activeLeafId)?.focus();
return true;
},
[activeLeafId],
);
const sendNextQueuedTerminalPrompt = useCallback(() => {
if (activeLeafId === null) return;
const store = useTerminalComposerStore.getState();
const item = store.queuedFor(activeLeafId)[0];
if (!item) return;
if (sendTerminalQueuedPromptText(item.text)) {
store.dequeueById(activeLeafId, item.id);
}
}, [activeLeafId, sendTerminalQueuedPromptText]);

useEditorFileSync({ tabs, tabsRef, editorRefs });
useThemeFileEditing({ tabsRef, openFileTab });

Expand Down Expand Up @@ -673,6 +712,8 @@ export default function App() {
},
"terminal.toggleInput": () =>
window.dispatchEvent(new CustomEvent(TOGGLE_BLOCK_INPUT_EVENT)),
"terminalComposer.toggle": toggleTerminalComposer,
"terminalComposer.sendQueued": sendNextQueuedTerminalPrompt,
"blocks.prev": () => navigateFocusedBlocks(-1),
"blocks.next": () => navigateFocusedBlocks(1),
"search.focus": () => searchInlineRef.current?.focus(),
Expand Down Expand Up @@ -707,6 +748,8 @@ export default function App() {
splitActivePaneInActiveTab,
focusNextPaneInTab,
toggleSourceControl,
toggleTerminalComposer,
sendNextQueuedTerminalPrompt,
togglePanelAndFocus,
askFromSelection,
toggleSidebar,
Expand Down Expand Up @@ -742,10 +785,19 @@ export default function App() {
}
if (
id === "terminal.toggleInput" ||
id === "terminalComposer.toggle" ||
id === "terminalComposer.sendQueued" ||
id === "blocks.prev" ||
id === "blocks.next"
) {
return !(activeTab?.kind === "terminal" && activeTab.blocks === true);
if (
id === "terminal.toggleInput" ||
id === "blocks.prev" ||
id === "blocks.next"
) {
return !(activeTab?.kind === "terminal" && activeTab.blocks === true);
}
return activeTab?.kind !== "terminal";
}
if (id === "sidebar.toggle") {
// Ctrl+B is also Claude Code's "run in background" key. While a terminal
Expand Down Expand Up @@ -989,6 +1041,7 @@ export default function App() {
closeActiveTabOrPane: handleCloseTabOrPane,
splitPaneRight: () => splitActivePaneInActiveTab("row"),
splitPaneDown: () => splitActivePaneInActiveTab("col"),
toggleTerminalComposer,
focusSearch: () => searchInlineRef.current?.focus(),
focusExplorerSearch: () => explorerRef.current?.focusSearch(),
toggleSidebar,
Expand Down Expand Up @@ -1018,6 +1071,7 @@ export default function App() {
toggleSourceControl,
handleCloseTabOrPane,
splitActivePaneInActiveTab,
toggleTerminalComposer,
toggleSidebar,
togglePanelAndFocus,
askFromSelection,
Expand Down Expand Up @@ -1181,6 +1235,10 @@ export default function App() {
hasComposer={hasComposer}
panelOpen={panelOpen}
keysLoaded={keysLoaded}
terminalComposerOpen={terminalComposerOpen}
onTerminalComposerClose={closeTerminalComposer}
onTerminalComposerSend={sendTerminalComposerText}
onTerminalQueuedPromptSend={sendTerminalQueuedPromptText}
onConnect={() => void openSettingsWindow("models")}
/>
</div>
Expand Down
52 changes: 49 additions & 3 deletions src/app/components/WorkspaceInputBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { AiInputBarConnect } from "@/modules/ai";
import { Chip } from "@/modules/ai/components/Chip";
import { ChipsRow } from "@/modules/ai/components/ChipsRow";
import { useComposer } from "@/modules/ai/lib/composer";
import { TerminalPromptQueue } from "@/modules/terminal/composer/TerminalPromptQueue";
import { useTerminalComposerStore } from "@/modules/terminal/composer/terminalComposerStore";
import { useBlockController } from "@/modules/terminal/lib/blockController";
import { focusLeafInput } from "@/modules/terminal/lib/useTerminalSession";
import { useTheme } from "@/modules/theme";
Expand All @@ -25,6 +27,11 @@ const AiComposerInput = lazy(() =>
default: m.AiComposerInput,
})),
);
const TerminalComposer = lazy(() =>
import("@/modules/terminal/composer/TerminalComposer").then((m) => ({
default: m.TerminalComposer,
})),
);

export const TOGGLE_BLOCK_INPUT_EVENT = "terax:toggle-block-input";

Expand All @@ -37,6 +44,10 @@ type Props = {
hasComposer: boolean;
panelOpen: boolean;
keysLoaded: boolean;
terminalComposerOpen: boolean;
onTerminalComposerClose: () => void;
onTerminalComposerSend: (text: string) => void;
onTerminalQueuedPromptSend: (text: string) => boolean;
onConnect: () => void;
};

Expand All @@ -49,6 +60,10 @@ export function WorkspaceInputBar({
hasComposer,
panelOpen,
keysLoaded,
terminalComposerOpen,
onTerminalComposerClose,
onTerminalComposerSend,
onTerminalQueuedPromptSend,
onConnect,
}: Props) {
const c = useComposer();
Expand All @@ -73,9 +88,21 @@ export function WorkspaceInputBar({
const showToggle = isBlockTab && hasComposer;
const [mode, setMode] = useState<"shell" | "ai">("shell");
const effectiveMode = !isBlockTab ? "ai" : hasComposer ? mode : "shell";
const terminalComposerVisible =
isTerminalTab && activeLeafId !== null && terminalComposerOpen;
const terminalQueueCount = useTerminalComposerStore((state) =>
activeLeafId === null ? 0 : (state.queues[activeLeafId]?.length ?? 0),
);
const terminalQueueVisible =
isTerminalTab && activeLeafId !== null && terminalQueueCount > 0;

const mounted = keysLoaded || isBlockTab;
const open = isBlockTab || (keysLoaded && panelOpen);
const mounted =
terminalComposerVisible || terminalQueueVisible || keysLoaded || isBlockTab;
const open =
terminalComposerVisible ||
terminalQueueVisible ||
isBlockTab ||
(keysLoaded && panelOpen);

const [aiLoaded, setAiLoaded] = useState(false);
useEffect(() => {
Expand Down Expand Up @@ -190,7 +217,26 @@ export function WorkspaceInputBar({
className="terax-reveal"
aria-hidden={!open}
>
<div>{content}</div>
{terminalQueueVisible && activeLeafId !== null && (
<TerminalPromptQueue
leafId={activeLeafId}
onSend={onTerminalQueuedPromptSend}
/>
)}
<div>
{terminalComposerVisible && activeLeafId !== null ? (
<Suspense fallback={null}>
<TerminalComposer
key={activeLeafId}
leafId={activeLeafId}
onSend={onTerminalComposerSend}
onClose={onTerminalComposerClose}
/>
</Suspense>
) : (
content
)}
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
);
}
Expand Down
99 changes: 99 additions & 0 deletions src/modules/command-palette/commands.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, expect, it, vi } from "vitest";

import { DEFAULT_SPACE_ID, type Tab } from "@/modules/tabs";

import {
createCommandItems,
type CommandPaletteActionContext,
} from "./commands";

function terminalTab(id: number, leafId: number): Tab {
return {
id,
kind: "terminal",
title: "Terminal",
spaceId: DEFAULT_SPACE_ID,
paneTree: { kind: "leaf", id: leafId },
activeLeafId: leafId,
};
}

function editorTab(id: number): Tab {
return {
id,
kind: "editor",
title: "Editor",
spaceId: DEFAULT_SPACE_ID,
path: "/tmp/file.ts",
dirty: false,
preview: false,
};
}

function context(
tab: Tab,
overrides: Partial<CommandPaletteActionContext> & {
toggleTerminalComposer?: () => void;
} = {},
): CommandPaletteActionContext {
return {
tabs: [tab],
activeId: tab.id,
searchTarget: null,
explorerRoot: "/tmp",
home: "/tmp",
openNewTab: vi.fn(),
openNewBlock: vi.fn(),
openNewPrivate: vi.fn(),
openNewEditor: vi.fn(),
openNewPreview: vi.fn(),
openGitGraph: vi.fn(),
toggleSourceControl: vi.fn(),
closeActiveTabOrPane: vi.fn(),
splitPaneRight: vi.fn(),
splitPaneDown: vi.fn(),
focusSearch: vi.fn(),
focusExplorerSearch: vi.fn(),
toggleSidebar: vi.fn(),
toggleAi: vi.fn(),
askAiSelection: vi.fn(),
openSettings: vi.fn(),
openKeyboardShortcuts: vi.fn(),
spaces: [],
activeSpaceId: DEFAULT_SPACE_ID,
openSpacesOverview: vi.fn(),
newSpace: vi.fn(),
switchSpace: vi.fn(),
...overrides,
} as CommandPaletteActionContext;
}

describe("createCommandItems", () => {
it("includes a terminal composer action for terminal tabs", () => {
const toggleTerminalComposer = vi.fn();
const items = createCommandItems(
context(terminalTab(1, 101), { toggleTerminalComposer }),
);

const item = items.find((entry) => entry.id === "terminalComposer.toggle");

expect(item).toMatchObject({
title: "Open terminal composer",
group: "Terminal",
shortcutId: "terminalComposer.toggle",
disabledReason: undefined,
});
item?.run();
expect(toggleTerminalComposer).toHaveBeenCalledOnce();
});

it("disables the terminal composer action outside terminal tabs", () => {
const items = createCommandItems(context(editorTab(1)));

expect(
items.find((entry) => entry.id === "terminalComposer.toggle"),
).toMatchObject({
disabledReason: "No terminal tab",
});
});
});
18 changes: 15 additions & 3 deletions src/modules/command-palette/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const COMMAND_GROUPS = [
"Spaces",
"Tabs",
"Panes",
"Terminal",
"Git",
"Search",
"View",
Expand All @@ -48,6 +49,7 @@ export type CommandPaletteActionContext = {
closeActiveTabOrPane: () => void;
splitPaneRight: () => void;
splitPaneDown: () => void;
toggleTerminalComposer: () => void;
focusSearch: () => void;
focusExplorerSearch: () => void;
toggleSidebar: () => void;
Expand Down Expand Up @@ -148,9 +150,19 @@ export function createCommandItems(
title: "New block terminal",
group: "Tabs",
keywords: ["blocks", "warp", "command blocks", "terminal"],
icon: DashboardSquare01Icon,
run: ctx.openNewBlock,
},
icon: DashboardSquare01Icon,
run: ctx.openNewBlock,
},
{
id: "terminalComposer.toggle",
title: "Open terminal composer",
group: "Terminal",
keywords: ["composer", "draft", "prompt", "queue", "terminal"],
icon: TerminalIcon,
shortcutId: "terminalComposer.toggle",
disabledReason: activeTerminalTab ? undefined : "No terminal tab",
run: ctx.toggleTerminalComposer,
},
{
id: "tab.newPrivate",
title: "New private terminal",
Expand Down
Loading