Merge branch 'main' of github.com:getpaseo/paseo
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { type QueryClient } from "@tanstack/react-query";
|
||||
import type { DaemonClient } from "@server/client/daemon-client";
|
||||
import type { ListTerminalsResponse } from "@server/shared/messages";
|
||||
import { AdaptiveRenameModal } from "@/components/rename-modal";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
||||
|
||||
interface RenamingTabState {
|
||||
kind: "terminal" | "agent";
|
||||
id: string;
|
||||
currentTitle: string;
|
||||
}
|
||||
|
||||
interface UseWorkspaceTabRenameInput {
|
||||
client: DaemonClient | null;
|
||||
normalizedServerId: string;
|
||||
queryClient: QueryClient;
|
||||
terminalsData: ListTerminalsResponse["payload"] | undefined;
|
||||
terminalsQueryKey: readonly unknown[];
|
||||
}
|
||||
|
||||
interface UseWorkspaceTabRenameResult {
|
||||
renamingTab: RenamingTabState | null;
|
||||
handleRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||
handleRenameModalSubmit: (nextTitle: string) => Promise<void>;
|
||||
handleRenameModalClose: () => void;
|
||||
}
|
||||
|
||||
export function useWorkspaceTabRename(
|
||||
input: UseWorkspaceTabRenameInput,
|
||||
): UseWorkspaceTabRenameResult {
|
||||
const { client, normalizedServerId, queryClient, terminalsData, terminalsQueryKey } = input;
|
||||
const [renamingTab, setRenamingTab] = useState<RenamingTabState | null>(null);
|
||||
|
||||
const handleRenameTab = useCallback(
|
||||
(tab: WorkspaceTabDescriptor) => {
|
||||
if (tab.target.kind === "terminal") {
|
||||
const { terminalId } = tab.target;
|
||||
const terminal = terminalsData?.terminals.find((entry) => entry.id === terminalId) ?? null;
|
||||
const currentTitle = terminal?.title ?? terminal?.name ?? "";
|
||||
setRenamingTab({ kind: "terminal", id: terminalId, currentTitle });
|
||||
return;
|
||||
}
|
||||
if (tab.target.kind === "agent") {
|
||||
const { agentId } = tab.target;
|
||||
const agent =
|
||||
useSessionStore.getState().sessions[normalizedServerId]?.agents?.get(agentId) ?? null;
|
||||
const currentTitle = agent?.title ?? "";
|
||||
setRenamingTab({ kind: "agent", id: agentId, currentTitle });
|
||||
}
|
||||
},
|
||||
[normalizedServerId, terminalsData],
|
||||
);
|
||||
|
||||
const handleRenameModalSubmit = useCallback(
|
||||
async (nextTitle: string) => {
|
||||
if (!renamingTab) return;
|
||||
if (!client) {
|
||||
throw new Error("Host is not connected");
|
||||
}
|
||||
const trimmed = nextTitle.trim();
|
||||
if (renamingTab.kind === "terminal") {
|
||||
const result = await client.renameTerminal({
|
||||
terminalId: renamingTab.id,
|
||||
title: trimmed,
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new Error(result.error ?? "Failed to rename terminal");
|
||||
}
|
||||
void queryClient.invalidateQueries({ queryKey: terminalsQueryKey });
|
||||
return;
|
||||
}
|
||||
await client.updateAgent(renamingTab.id, { name: trimmed });
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["sidebarAgentsList", normalizedServerId],
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["allAgents", normalizedServerId],
|
||||
});
|
||||
},
|
||||
[client, normalizedServerId, queryClient, renamingTab, terminalsQueryKey],
|
||||
);
|
||||
|
||||
const handleRenameModalClose = useCallback(() => {
|
||||
setRenamingTab(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
renamingTab,
|
||||
handleRenameTab,
|
||||
handleRenameModalSubmit,
|
||||
handleRenameModalClose,
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkspaceTabRenameModalProps {
|
||||
renamingTab: RenamingTabState | null;
|
||||
onClose: () => void;
|
||||
onSubmit: (nextTitle: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function WorkspaceTabRenameModal({
|
||||
renamingTab,
|
||||
onClose,
|
||||
onSubmit,
|
||||
}: WorkspaceTabRenameModalProps) {
|
||||
const title = renamingTab?.kind === "terminal" ? "Rename terminal" : "Rename agent";
|
||||
const initialValue = renamingTab?.currentTitle ?? "";
|
||||
const testID = renamingTab
|
||||
? `workspace-tab-rename-modal-${renamingTab.kind}-${renamingTab.id}`
|
||||
: undefined;
|
||||
return (
|
||||
<AdaptiveRenameModal
|
||||
visible={renamingTab !== null}
|
||||
title={title}
|
||||
initialValue={initialValue}
|
||||
submitLabel="Rename"
|
||||
maxLength={200}
|
||||
onClose={onClose}
|
||||
onSubmit={onSubmit}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
ArrowRightToLine,
|
||||
Columns2,
|
||||
Copy,
|
||||
Pencil,
|
||||
RotateCw,
|
||||
Rows2,
|
||||
Globe,
|
||||
@@ -71,6 +72,7 @@ const ThemedRotateCw = withUnistyles(RotateCw);
|
||||
const ThemedArrowLeftToLine = withUnistyles(ArrowLeftToLine);
|
||||
const ThemedArrowRightToLine = withUnistyles(ArrowRightToLine);
|
||||
const ThemedCopyX = withUnistyles(CopyX);
|
||||
const ThemedPencil = withUnistyles(Pencil);
|
||||
const ThemedSquarePen = withUnistyles(SquarePen);
|
||||
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
||||
const ThemedGlobe = withUnistyles(Globe);
|
||||
@@ -101,6 +103,8 @@ function TabContextMenuItem({
|
||||
return <ThemedArrowRightToLine size={16} uniProps={mutedColorMapping} />;
|
||||
case "copy-x":
|
||||
return <ThemedCopyX size={16} uniProps={mutedColorMapping} />;
|
||||
case "pencil":
|
||||
return <ThemedPencil size={16} uniProps={mutedColorMapping} />;
|
||||
case "x":
|
||||
return <ThemedX size={16} uniProps={mutedColorMapping} />;
|
||||
default:
|
||||
@@ -150,6 +154,7 @@ interface WorkspaceDesktopTabsRowProps {
|
||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
|
||||
onCloseTabsToRight: (tabId: string) => Promise<void> | void;
|
||||
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
|
||||
@@ -467,6 +472,7 @@ export function WorkspaceDesktopTabsRow({
|
||||
onCopyResumeCommand,
|
||||
onCopyAgentId,
|
||||
onReloadAgent,
|
||||
onRenameTab,
|
||||
onCloseTabsToLeft,
|
||||
onCloseTabsToRight,
|
||||
onCloseOtherTabs,
|
||||
@@ -599,6 +605,7 @@ export function WorkspaceDesktopTabsRow({
|
||||
onCopyResumeCommand={onCopyResumeCommand}
|
||||
onCopyAgentId={onCopyAgentId}
|
||||
onReloadAgent={onReloadAgent}
|
||||
onRenameTab={onRenameTab}
|
||||
onCloseTabsToLeft={onCloseTabsToLeft}
|
||||
onCloseTabsToRight={onCloseTabsToRight}
|
||||
onCloseOtherTabs={onCloseOtherTabs}
|
||||
@@ -630,6 +637,7 @@ export function WorkspaceDesktopTabsRow({
|
||||
onCopyResumeCommand,
|
||||
onNavigateTab,
|
||||
onReloadAgent,
|
||||
onRenameTab,
|
||||
setHoveredCloseTabKey,
|
||||
setHoveredTabKey,
|
||||
tabDropPreviewIndex,
|
||||
@@ -791,6 +799,7 @@ function ResolvedDesktopTabChip({
|
||||
onCopyResumeCommand,
|
||||
onCopyAgentId,
|
||||
onReloadAgent,
|
||||
onRenameTab,
|
||||
onCloseTabsToLeft,
|
||||
onCloseTabsToRight,
|
||||
onCloseOtherTabs,
|
||||
@@ -815,6 +824,7 @@ function ResolvedDesktopTabChip({
|
||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
|
||||
onCloseTabsToRight: (tabId: string) => Promise<void> | void;
|
||||
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
|
||||
@@ -838,6 +848,7 @@ function ResolvedDesktopTabChip({
|
||||
onCopyResumeCommand,
|
||||
onCopyAgentId,
|
||||
onReloadAgent,
|
||||
onRenameTab,
|
||||
onCloseTab,
|
||||
onCloseTabsToLeft,
|
||||
onCloseTabsToRight,
|
||||
@@ -853,6 +864,7 @@ function ResolvedDesktopTabChip({
|
||||
onCopyAgentId,
|
||||
onCopyResumeCommand,
|
||||
onReloadAgent,
|
||||
onRenameTab,
|
||||
tabCount,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { useStoreWithEqualityFn } from "zustand/traditional";
|
||||
import { useIsFocused } from "@react-navigation/native";
|
||||
import { ActivityIndicator, BackHandler, Keyboard, Pressable, Text, View } from "react-native";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useRouter, type Href } from "expo-router";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { DiffStat } from "@/components/diff-stat";
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
Globe,
|
||||
Import as ImportIcon,
|
||||
PanelRight,
|
||||
Pencil,
|
||||
RotateCw,
|
||||
Settings,
|
||||
SquarePen,
|
||||
@@ -109,6 +110,10 @@ import {
|
||||
WorkspaceTabOptionRow,
|
||||
type WorkspaceTabPresentation,
|
||||
} from "@/screens/workspace/workspace-tab-presentation";
|
||||
import {
|
||||
useWorkspaceTabRename,
|
||||
WorkspaceTabRenameModal,
|
||||
} from "@/screens/workspace/use-workspace-tab-rename";
|
||||
import {
|
||||
WorkspaceDesktopTabsRow,
|
||||
type WorkspaceDesktopTabRowItem,
|
||||
@@ -181,6 +186,7 @@ const ThemedRotateCw = withUnistyles(RotateCw);
|
||||
const ThemedArrowLeftToLine = withUnistyles(ArrowLeftToLine);
|
||||
const ThemedArrowRightToLine = withUnistyles(ArrowRightToLine);
|
||||
const ThemedCopyX = withUnistyles(CopyX);
|
||||
const ThemedPencil = withUnistyles(Pencil);
|
||||
const ThemedX = withUnistyles(X);
|
||||
const ThemedSquarePen = withUnistyles(SquarePen);
|
||||
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
||||
@@ -297,6 +303,7 @@ interface MobileWorkspaceTabSwitcherProps {
|
||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||
onCloseTabsAbove: (tabId: string) => Promise<void> | void;
|
||||
onCloseTabsBelow: (tabId: string) => Promise<void> | void;
|
||||
@@ -436,6 +443,8 @@ function MobileTabDropdownMenuItem({
|
||||
return <ThemedArrowRightToLine size={16} uniProps={mutedColorMapping} />;
|
||||
case "copy-x":
|
||||
return <ThemedCopyX size={16} uniProps={mutedColorMapping} />;
|
||||
case "pencil":
|
||||
return <ThemedPencil size={16} uniProps={mutedColorMapping} />;
|
||||
case "x":
|
||||
return <ThemedX size={16} uniProps={mutedColorMapping} />;
|
||||
default:
|
||||
@@ -473,6 +482,7 @@ function MobileWorkspaceTabOption({
|
||||
onCopyResumeCommand,
|
||||
onCopyAgentId,
|
||||
onReloadAgent,
|
||||
onRenameTab,
|
||||
onCloseTab,
|
||||
onCloseTabsAbove,
|
||||
onCloseTabsBelow,
|
||||
@@ -489,6 +499,7 @@ function MobileWorkspaceTabOption({
|
||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||
onCloseTabsAbove: (tabId: string) => Promise<void> | void;
|
||||
onCloseTabsBelow: (tabId: string) => Promise<void> | void;
|
||||
@@ -504,6 +515,7 @@ function MobileWorkspaceTabOption({
|
||||
onCopyResumeCommand,
|
||||
onCopyAgentId,
|
||||
onReloadAgent,
|
||||
onRenameTab,
|
||||
onCloseTab,
|
||||
onCloseTabsBefore: onCloseTabsAbove,
|
||||
onCloseTabsAfter: onCloseTabsBelow,
|
||||
@@ -558,6 +570,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
|
||||
onCopyResumeCommand,
|
||||
onCopyAgentId,
|
||||
onReloadAgent,
|
||||
onRenameTab,
|
||||
onCloseTab,
|
||||
onCloseTabsAbove,
|
||||
onCloseTabsBelow,
|
||||
@@ -611,6 +624,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
|
||||
onCopyResumeCommand={onCopyResumeCommand}
|
||||
onCopyAgentId={onCopyAgentId}
|
||||
onReloadAgent={onReloadAgent}
|
||||
onRenameTab={onRenameTab}
|
||||
onCloseTab={onCloseTab}
|
||||
onCloseTabsAbove={onCloseTabsAbove}
|
||||
onCloseTabsBelow={onCloseTabsBelow}
|
||||
@@ -627,6 +641,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
|
||||
onCopyResumeCommand,
|
||||
onCopyAgentId,
|
||||
onReloadAgent,
|
||||
onRenameTab,
|
||||
onCloseTab,
|
||||
onCloseTabsAbove,
|
||||
onCloseTabsBelow,
|
||||
@@ -1532,6 +1547,7 @@ function WorkspaceScreenContent({
|
||||
openWorkspaceTabFocused,
|
||||
toast,
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
createMutation: createTerminalMutation,
|
||||
createTerminal,
|
||||
@@ -1543,6 +1559,7 @@ function WorkspaceScreenContent({
|
||||
liveTerminalIds,
|
||||
pendingCreateInput: pendingTerminalCreateInput,
|
||||
query: terminalsQuery,
|
||||
queryKey: terminalsQueryKey,
|
||||
removeTerminalFromCache,
|
||||
standaloneTerminalIds,
|
||||
terminals,
|
||||
@@ -2125,6 +2142,14 @@ function WorkspaceScreenContent({
|
||||
|
||||
const [_hoveredTabKey, setHoveredTabKey] = useState<string | null>(null);
|
||||
const [hoveredCloseTabKey, setHoveredCloseTabKey] = useState<string | null>(null);
|
||||
const { handleRenameTab, renamingTab, handleRenameModalSubmit, handleRenameModalClose } =
|
||||
useWorkspaceTabRename({
|
||||
client,
|
||||
normalizedServerId,
|
||||
queryClient,
|
||||
terminalsData: terminalsQuery.data,
|
||||
terminalsQueryKey,
|
||||
});
|
||||
|
||||
const tabByKey = useMemo(() => {
|
||||
const map = new Map<string, WorkspaceTabDescriptor>();
|
||||
@@ -3152,6 +3177,7 @@ function WorkspaceScreenContent({
|
||||
onCopyResumeCommand={handleCopyResumeCommand}
|
||||
onCopyAgentId={handleCopyAgentId}
|
||||
onReloadAgent={handleReloadAgent}
|
||||
onRenameTab={handleRenameTab}
|
||||
onCloseTabsToLeft={handleCloseTabsToLeftInPane}
|
||||
onCloseTabsToRight={handleCloseTabsToRightInPane}
|
||||
onCloseOtherTabs={handleCloseOtherTabsInPane}
|
||||
@@ -3186,6 +3212,7 @@ function WorkspaceScreenContent({
|
||||
handleCopyResumeCommand,
|
||||
handleCopyAgentId,
|
||||
handleReloadAgent,
|
||||
handleRenameTab,
|
||||
handleCloseTabsToLeftInPane,
|
||||
handleCloseTabsToRightInPane,
|
||||
handleCloseOtherTabsInPane,
|
||||
@@ -3268,6 +3295,7 @@ function WorkspaceScreenContent({
|
||||
onCopyResumeCommand={handleCopyResumeCommand}
|
||||
onCopyAgentId={handleCopyAgentId}
|
||||
onReloadAgent={handleReloadAgent}
|
||||
onRenameTab={handleRenameTab}
|
||||
onCloseTab={handleCloseTabById}
|
||||
onCloseTabsAbove={handleCloseTabsToLeft}
|
||||
onCloseTabsBelow={handleCloseTabsToRight}
|
||||
@@ -3289,6 +3317,7 @@ function WorkspaceScreenContent({
|
||||
onCopyResumeCommand={handleCopyResumeCommand}
|
||||
onCopyAgentId={handleCopyAgentId}
|
||||
onReloadAgent={handleReloadAgent}
|
||||
onRenameTab={handleRenameTab}
|
||||
onCloseTabsToLeft={handleCloseTabsToLeft}
|
||||
onCloseTabsToRight={handleCloseTabsToRight}
|
||||
onCloseOtherTabs={handleCloseOtherTabs}
|
||||
@@ -3337,6 +3366,11 @@ function WorkspaceScreenContent({
|
||||
onClose={closeImportSheet}
|
||||
onImportedAgent={handleImportedAgent}
|
||||
/>
|
||||
<WorkspaceTabRenameModal
|
||||
renamingTab={renamingTab}
|
||||
onSubmit={handleRenameModalSubmit}
|
||||
onClose={handleRenameModalClose}
|
||||
/>
|
||||
</View>
|
||||
</WorkspaceFocusProvider>
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
||||
const onCopyResumeCommand = vi.fn();
|
||||
const onCopyAgentId = vi.fn();
|
||||
const onReloadAgent = vi.fn();
|
||||
const onRenameTab = vi.fn();
|
||||
const onCloseTab = vi.fn();
|
||||
const onCloseTabsBefore = vi.fn();
|
||||
const onCloseTabsAfter = vi.fn();
|
||||
@@ -30,6 +31,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
||||
onCopyResumeCommand,
|
||||
onCopyAgentId,
|
||||
onReloadAgent,
|
||||
onRenameTab,
|
||||
onCloseTab,
|
||||
onCloseTabsBefore,
|
||||
onCloseTabsAfter,
|
||||
@@ -39,6 +41,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
||||
expect(entries.filter((entry) => entry.kind === "item").map((entry) => entry.label)).toEqual([
|
||||
"Copy resume command",
|
||||
"Copy agent id",
|
||||
"Rename",
|
||||
"Close to the left",
|
||||
"Close to the right",
|
||||
"Close other tabs",
|
||||
@@ -57,6 +60,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
||||
onCopyResumeCommand: vi.fn(),
|
||||
onCopyAgentId: vi.fn(),
|
||||
onReloadAgent: vi.fn(),
|
||||
onRenameTab: vi.fn(),
|
||||
onCloseTab: vi.fn(),
|
||||
onCloseTabsBefore: vi.fn(),
|
||||
onCloseTabsAfter: vi.fn(),
|
||||
@@ -66,6 +70,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
||||
expect(entries.filter((entry) => entry.kind === "item").map((entry) => entry.label)).toEqual([
|
||||
"Copy resume command",
|
||||
"Copy agent id",
|
||||
"Rename",
|
||||
"Close tabs above",
|
||||
"Close tabs below",
|
||||
"Close other tabs",
|
||||
@@ -74,7 +79,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("omits agent copy actions for non-agent tabs", () => {
|
||||
it("omits agent copy actions and rename for draft tabs", () => {
|
||||
const entries = buildWorkspaceTabMenuEntries({
|
||||
surface: "mobile",
|
||||
tab: {
|
||||
@@ -89,6 +94,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
||||
onCopyResumeCommand: vi.fn(),
|
||||
onCopyAgentId: vi.fn(),
|
||||
onReloadAgent: vi.fn(),
|
||||
onRenameTab: vi.fn(),
|
||||
onCloseTab: vi.fn(),
|
||||
onCloseTabsBefore: vi.fn(),
|
||||
onCloseTabsAfter: vi.fn(),
|
||||
@@ -101,6 +107,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
||||
expect(entries.some((entry) => entry.kind === "item" && entry.label === "Reload agent")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(entries.some((entry) => entry.kind === "item" && entry.label === "Rename")).toBe(false);
|
||||
expect(entries.some((entry) => entry.kind === "separator")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -114,6 +121,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
||||
onCopyResumeCommand: vi.fn(),
|
||||
onCopyAgentId: vi.fn(),
|
||||
onReloadAgent: vi.fn(),
|
||||
onRenameTab: vi.fn(),
|
||||
onCloseTab: vi.fn(),
|
||||
onCloseTabsBefore: vi.fn(),
|
||||
onCloseTabsAfter: vi.fn(),
|
||||
@@ -128,4 +136,128 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("invokes onRenameTab when the rename entry is selected for agent tabs", () => {
|
||||
const onRenameTab = vi.fn();
|
||||
const tab = createAgentTab();
|
||||
const entries = buildWorkspaceTabMenuEntries({
|
||||
surface: "desktop",
|
||||
tab,
|
||||
index: 0,
|
||||
tabCount: 1,
|
||||
menuTestIDBase: "workspace-tab-context-agent_123",
|
||||
onCopyResumeCommand: vi.fn(),
|
||||
onCopyAgentId: vi.fn(),
|
||||
onReloadAgent: vi.fn(),
|
||||
onRenameTab,
|
||||
onCloseTab: vi.fn(),
|
||||
onCloseTabsBefore: vi.fn(),
|
||||
onCloseTabsAfter: vi.fn(),
|
||||
onCloseOtherTabs: vi.fn(),
|
||||
});
|
||||
|
||||
const renameEntry = entries.find((entry) => entry.kind === "item" && entry.label === "Rename");
|
||||
if (!renameEntry || renameEntry.kind !== "item") {
|
||||
throw new Error("Rename entry missing");
|
||||
}
|
||||
renameEntry.onSelect();
|
||||
|
||||
expect(onRenameTab).toHaveBeenCalledWith(tab);
|
||||
});
|
||||
|
||||
it("includes rename as the first entry for terminal tabs", () => {
|
||||
const onRenameTab = vi.fn();
|
||||
const terminalTab: WorkspaceTabDescriptor = {
|
||||
key: "terminal_abc",
|
||||
tabId: "terminal_abc",
|
||||
kind: "terminal",
|
||||
target: { kind: "terminal", terminalId: "terminal-abc" },
|
||||
};
|
||||
const entries = buildWorkspaceTabMenuEntries({
|
||||
surface: "desktop",
|
||||
tab: terminalTab,
|
||||
index: 0,
|
||||
tabCount: 1,
|
||||
menuTestIDBase: "workspace-tab-context-terminal_abc",
|
||||
onCopyResumeCommand: vi.fn(),
|
||||
onCopyAgentId: vi.fn(),
|
||||
onReloadAgent: vi.fn(),
|
||||
onRenameTab,
|
||||
onCloseTab: vi.fn(),
|
||||
onCloseTabsBefore: vi.fn(),
|
||||
onCloseTabsAfter: vi.fn(),
|
||||
onCloseOtherTabs: vi.fn(),
|
||||
});
|
||||
|
||||
const labels = entries.filter((entry) => entry.kind === "item").map((entry) => entry.label);
|
||||
expect(labels[0]).toBe("Rename");
|
||||
expect(labels).not.toContain("Copy resume command");
|
||||
expect(labels).not.toContain("Copy agent id");
|
||||
expect(labels).not.toContain("Reload agent");
|
||||
|
||||
const renameEntry = entries.find((entry) => entry.kind === "item" && entry.label === "Rename");
|
||||
if (!renameEntry || renameEntry.kind !== "item") {
|
||||
throw new Error("Rename entry missing");
|
||||
}
|
||||
renameEntry.onSelect();
|
||||
expect(onRenameTab).toHaveBeenCalledWith(terminalTab);
|
||||
});
|
||||
|
||||
it("uses the same rename entry shape for agent and terminal tabs", () => {
|
||||
const terminalTab: WorkspaceTabDescriptor = {
|
||||
key: "terminal_abc",
|
||||
tabId: "terminal_abc",
|
||||
kind: "terminal",
|
||||
target: { kind: "terminal", terminalId: "terminal-abc" },
|
||||
};
|
||||
const menuTestIDBase = "workspace-tab-context";
|
||||
const sharedInput = {
|
||||
surface: "desktop" as const,
|
||||
index: 0,
|
||||
tabCount: 1,
|
||||
menuTestIDBase,
|
||||
onCopyResumeCommand: vi.fn(),
|
||||
onCopyAgentId: vi.fn(),
|
||||
onReloadAgent: vi.fn(),
|
||||
onRenameTab: vi.fn(),
|
||||
onCloseTab: vi.fn(),
|
||||
onCloseTabsBefore: vi.fn(),
|
||||
onCloseTabsAfter: vi.fn(),
|
||||
onCloseOtherTabs: vi.fn(),
|
||||
};
|
||||
|
||||
const agentEntries = buildWorkspaceTabMenuEntries({ ...sharedInput, tab: createAgentTab() });
|
||||
const terminalEntries = buildWorkspaceTabMenuEntries({ ...sharedInput, tab: terminalTab });
|
||||
|
||||
const agentRename = agentEntries.find(
|
||||
(entry) => entry.kind === "item" && entry.key === "rename",
|
||||
);
|
||||
const terminalRename = terminalEntries.find(
|
||||
(entry) => entry.kind === "item" && entry.key === "rename",
|
||||
);
|
||||
if (!agentRename || agentRename.kind !== "item") throw new Error("Agent rename missing");
|
||||
if (!terminalRename || terminalRename.kind !== "item")
|
||||
throw new Error("Terminal rename missing");
|
||||
|
||||
expect({
|
||||
key: agentRename.key,
|
||||
label: agentRename.label,
|
||||
icon: agentRename.icon,
|
||||
testID: agentRename.testID,
|
||||
}).toEqual({
|
||||
key: terminalRename.key,
|
||||
label: terminalRename.label,
|
||||
icon: terminalRename.icon,
|
||||
testID: terminalRename.testID,
|
||||
});
|
||||
|
||||
const agentSeparator = agentEntries
|
||||
.slice(agentEntries.indexOf(agentRename) + 1)
|
||||
.find((entry) => entry.kind === "separator");
|
||||
const terminalSeparator = terminalEntries
|
||||
.slice(terminalEntries.indexOf(terminalRename) + 1)
|
||||
.find((entry) => entry.kind === "separator");
|
||||
expect(agentSeparator?.key).toBe("rename-separator");
|
||||
expect(terminalSeparator?.key).toBe("rename-separator");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,14 @@ export type WorkspaceTabMenuEntry =
|
||||
kind: "item";
|
||||
key: string;
|
||||
label: string;
|
||||
icon?: "copy" | "rotate-cw" | "arrow-left-to-line" | "arrow-right-to-line" | "copy-x" | "x";
|
||||
icon?:
|
||||
| "copy"
|
||||
| "rotate-cw"
|
||||
| "arrow-left-to-line"
|
||||
| "arrow-right-to-line"
|
||||
| "copy-x"
|
||||
| "pencil"
|
||||
| "x";
|
||||
hint?: string;
|
||||
tooltip?: string;
|
||||
disabled?: boolean;
|
||||
@@ -30,6 +37,7 @@ interface BuildWorkspaceTabMenuEntriesInput {
|
||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||
onCloseTabsBefore: (tabId: string) => Promise<void> | void;
|
||||
onCloseTabsAfter: (tabId: string) => Promise<void> | void;
|
||||
@@ -43,6 +51,7 @@ interface BuildWorkspaceDesktopTabActionsInput {
|
||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
|
||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
|
||||
onCloseTabsToRight: (tabId: string) => Promise<void> | void;
|
||||
@@ -102,6 +111,7 @@ export function buildWorkspaceTabMenuEntries(
|
||||
onCopyResumeCommand,
|
||||
onCopyAgentId,
|
||||
onReloadAgent,
|
||||
onRenameTab,
|
||||
onCloseTab,
|
||||
onCloseTabsBefore,
|
||||
onCloseTabsAfter,
|
||||
@@ -135,9 +145,22 @@ export function buildWorkspaceTabMenuEntries(
|
||||
void onCopyAgentId(agentId);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (tab.target.kind === "agent" || tab.target.kind === "terminal") {
|
||||
entries.push({
|
||||
kind: "item",
|
||||
key: "rename",
|
||||
label: "Rename",
|
||||
icon: "pencil",
|
||||
testID: `${menuTestIDBase}-rename`,
|
||||
onSelect: () => {
|
||||
onRenameTab(tab);
|
||||
},
|
||||
});
|
||||
entries.push({
|
||||
kind: "separator",
|
||||
key: "copy-separator",
|
||||
key: "rename-separator",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -217,6 +240,7 @@ export function buildWorkspaceDesktopTabActions(
|
||||
onCopyResumeCommand: input.onCopyResumeCommand,
|
||||
onCopyAgentId: input.onCopyAgentId,
|
||||
onReloadAgent: input.onReloadAgent,
|
||||
onRenameTab: input.onRenameTab,
|
||||
onCloseTab: input.onCloseTab,
|
||||
onCloseTabsBefore: input.onCloseTabsToLeft,
|
||||
onCloseTabsAfter: input.onCloseTabsToRight,
|
||||
|
||||
Reference in New Issue
Block a user