diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index ce42f2d76..e799cdc5d 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -84,6 +84,7 @@ interface SplitContainerProps { onCloseTab: (tabId: string) => Promise | void; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onReloadAgent: (agentId: string) => Promise | void; onCloseTabsToLeft: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise | void; onCloseTabsToRight: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise | void; onCloseOtherTabs: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise | void; @@ -265,6 +266,7 @@ export function SplitContainer({ onCloseTab, onCopyResumeCommand, onCopyAgentId, + onReloadAgent, onCloseTabsToLeft, onCloseTabsToRight, onCloseOtherTabs, @@ -534,6 +536,7 @@ export function SplitContainer({ onCloseTab={onCloseTab} onCopyResumeCommand={onCopyResumeCommand} onCopyAgentId={onCopyAgentId} + onReloadAgent={onReloadAgent} onCloseTabsToLeft={onCloseTabsToLeft} onCloseTabsToRight={onCloseTabsToRight} onCloseOtherTabs={onCloseOtherTabs} @@ -656,6 +659,7 @@ function SplitNodeView({ onCloseTab, onCopyResumeCommand, onCopyAgentId, + onReloadAgent, onCloseTabsToLeft, onCloseTabsToRight, onCloseOtherTabs, @@ -691,6 +695,7 @@ function SplitNodeView({ onCloseTab={onCloseTab} onCopyResumeCommand={onCopyResumeCommand} onCopyAgentId={onCopyAgentId} + onReloadAgent={onReloadAgent} onCloseTabsToLeft={onCloseTabsToLeft} onCloseTabsToRight={onCloseTabsToRight} onCloseOtherTabs={onCloseOtherTabs} @@ -741,6 +746,7 @@ function SplitNodeView({ onCloseTab={onCloseTab} onCopyResumeCommand={onCopyResumeCommand} onCopyAgentId={onCopyAgentId} + onReloadAgent={onReloadAgent} onCloseTabsToLeft={onCloseTabsToLeft} onCloseTabsToRight={onCloseTabsToRight} onCloseOtherTabs={onCloseOtherTabs} @@ -790,6 +796,7 @@ function SplitPaneView({ onCloseTab, onCopyResumeCommand, onCopyAgentId, + onReloadAgent, onCloseTabsToLeft, onCloseTabsToRight, onCloseOtherTabs, @@ -901,6 +908,7 @@ function SplitPaneView({ onCloseTab={onCloseTab} onCopyResumeCommand={onCopyResumeCommand} onCopyAgentId={onCopyAgentId} + onReloadAgent={onReloadAgent} onCloseTabsToLeft={(tabId) => onCloseTabsToLeft(tabId, paneTabs)} onCloseTabsToRight={(tabId) => onCloseTabsToRight(tabId, paneTabs)} onCloseOtherTabs={(tabId) => onCloseOtherTabs(tabId, paneTabs)} diff --git a/packages/app/src/components/ui/context-menu.tsx b/packages/app/src/components/ui/context-menu.tsx index c539af754..db3196e03 100644 --- a/packages/app/src/components/ui/context-menu.tsx +++ b/packages/app/src/components/ui/context-menu.tsx @@ -30,6 +30,7 @@ import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { Check, CheckCircle } from "lucide-react-native"; import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from "@gorhom/bottom-sheet"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; // Keep parity with dropdown-menu action statuses. export type ActionStatus = "idle" | "pending" | "success"; @@ -595,6 +596,7 @@ export function ContextMenuItem({ successLabel, closeOnSelect = true, testID, + tooltip, }: PropsWithChildren<{ description?: string; onSelect?: () => void; @@ -612,6 +614,7 @@ export function ContextMenuItem({ successLabel?: string; closeOnSelect?: boolean; testID?: string; + tooltip?: string; }>): ReactElement { const { theme } = useUnistyles(); const { setOpen } = useContextMenuContext("ContextMenuItem"); @@ -642,7 +645,7 @@ export function ContextMenuItem({ ) : null); - return ( + const content = ( {trailingContent} : null} ); + + if (!tooltip) { + return content; + } + + return ( + + {content} + + {tooltip} + + + ); } const styles = StyleSheet.create((theme) => ({ @@ -762,6 +778,10 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.xs, color: theme.colors.foregroundMuted, }, + tooltipText: { + fontSize: theme.fontSize.sm, + color: theme.colors.foreground, + }, item: { flexDirection: "row", alignItems: "center", diff --git a/packages/app/src/components/ui/dropdown-menu.tsx b/packages/app/src/components/ui/dropdown-menu.tsx index 32280504a..2441b14d4 100644 --- a/packages/app/src/components/ui/dropdown-menu.tsx +++ b/packages/app/src/components/ui/dropdown-menu.tsx @@ -27,6 +27,7 @@ import { import Animated, { Keyframe, runOnJS } from "react-native-reanimated"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { Check, CheckCircle } from "lucide-react-native"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; // Action status for menu items with loading/success feedback export type ActionStatus = "idle" | "pending" | "success"; @@ -471,6 +472,7 @@ export function DropdownMenuItem({ successLabel, closeOnSelect = true, testID, + tooltip, }: PropsWithChildren<{ description?: string; onSelect?: () => void; @@ -491,6 +493,7 @@ export function DropdownMenuItem({ successLabel?: string; closeOnSelect?: boolean; testID?: string; + tooltip?: string; }>): ReactElement { const { theme } = useUnistyles(); const { setOpen } = useDropdownMenuContext("DropdownMenuItem"); @@ -524,7 +527,7 @@ export function DropdownMenuItem({ ) : null); - return ( + const content = ( {trailingContent} : null} ); + + if (!tooltip) { + return content; + } + + return ( + + {content} + + {tooltip} + + + ); } const styles = StyleSheet.create((theme) => ({ @@ -630,6 +646,10 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.xs, color: theme.colors.foregroundMuted, }, + tooltipText: { + fontSize: theme.fontSize.sm, + color: theme.colors.foreground, + }, item: { flexDirection: "row", alignItems: "center", diff --git a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx index 595be3647..110f01970 100644 --- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx +++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx @@ -14,6 +14,7 @@ import { ArrowRightToLine, Columns2, Copy, + RotateCw, Rows2, SquarePen, SquareTerminal, @@ -71,6 +72,7 @@ type WorkspaceDesktopTabsRowProps = { onCloseTab: (tabId: string) => Promise | void; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onReloadAgent: (agentId: string) => Promise | void; onCloseTabsToLeft: (tabId: string) => Promise | void; onCloseTabsToRight: (tabId: string) => Promise | void; onCloseOtherTabs: (tabId: string) => Promise | void; @@ -291,11 +293,14 @@ function TabChip({ disabled={entry.disabled} destructive={entry.destructive} onSelect={entry.onSelect} + tooltip={entry.tooltip} leading={(() => { const iconColor = theme.colors.foregroundMuted; switch (entry.icon) { case "copy": return ; + case "rotate-cw": + return ; case "arrow-left-to-line": return ; case "arrow-right-to-line": @@ -335,6 +340,7 @@ export function WorkspaceDesktopTabsRow({ onCloseTab, onCopyResumeCommand, onCopyAgentId, + onReloadAgent, onCloseTabsToLeft, onCloseTabsToRight, onCloseOtherTabs, @@ -457,6 +463,7 @@ export function WorkspaceDesktopTabsRow({ normalizedWorkspaceId={normalizedWorkspaceId} onCopyResumeCommand={onCopyResumeCommand} onCopyAgentId={onCopyAgentId} + onReloadAgent={onReloadAgent} onCloseTabsToLeft={onCloseTabsToLeft} onCloseTabsToRight={onCloseTabsToRight} onCloseOtherTabs={onCloseOtherTabs} @@ -585,6 +592,7 @@ function ResolvedDesktopTabChip({ normalizedWorkspaceId, onCopyResumeCommand, onCopyAgentId, + onReloadAgent, onCloseTabsToLeft, onCloseTabsToRight, onCloseOtherTabs, @@ -608,6 +616,7 @@ function ResolvedDesktopTabChip({ normalizedWorkspaceId: string; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onReloadAgent: (agentId: string) => Promise | void; onCloseTabsToLeft: (tabId: string) => Promise | void; onCloseTabsToRight: (tabId: string) => Promise | void; onCloseOtherTabs: (tabId: string) => Promise | void; @@ -630,6 +639,7 @@ function ResolvedDesktopTabChip({ tabCount, onCopyResumeCommand, onCopyAgentId, + onReloadAgent, onCloseTab, onCloseTabsToLeft, onCloseTabsToRight, @@ -644,6 +654,7 @@ function ResolvedDesktopTabChip({ onCloseTabsToRight, onCopyAgentId, onCopyResumeCommand, + onReloadAgent, tabCount, ], ); diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index e9b402145..ff241f11a 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -21,6 +21,7 @@ import { Ellipsis, EllipsisVertical, PanelRight, + RotateCw, SquarePen, SquareTerminal, X, @@ -182,6 +183,7 @@ type MobileWorkspaceTabSwitcherProps = { onSelectSwitcherTab: (key: string) => void; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onReloadAgent: (agentId: string) => Promise | void; onCloseTab: (tabId: string) => Promise | void; onCloseTabsAbove: (tabId: string) => Promise | void; onCloseTabsBelow: (tabId: string) => Promise | void; @@ -269,6 +271,7 @@ function MobileWorkspaceTabOption({ onPress, onCopyResumeCommand, onCopyAgentId, + onReloadAgent, onCloseTab, onCloseTabsAbove, onCloseTabsBelow, @@ -284,6 +287,7 @@ function MobileWorkspaceTabOption({ onPress: () => void; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onReloadAgent: (agentId: string) => Promise | void; onCloseTab: (tabId: string) => Promise | void; onCloseTabsAbove: (tabId: string) => Promise | void; onCloseTabsBelow: (tabId: string) => Promise | void; @@ -299,6 +303,7 @@ function MobileWorkspaceTabOption({ menuTestIDBase, onCopyResumeCommand, onCopyAgentId, + onReloadAgent, onCloseTab, onCloseTabsBefore: onCloseTabsAbove, onCloseTabsAfter: onCloseTabsBelow, @@ -342,11 +347,14 @@ function MobileWorkspaceTabOption({ disabled={entry.disabled} destructive={entry.destructive} onSelect={entry.onSelect} + tooltip={entry.tooltip} leading={(() => { const iconColor = theme.colors.foregroundMuted; switch (entry.icon) { case "copy": return ; + case "rotate-cw": + return ; case "arrow-left-to-line": return ; case "arrow-right-to-line": @@ -389,6 +397,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({ onSelectSwitcherTab, onCopyResumeCommand, onCopyAgentId, + onReloadAgent, onCloseTab, onCloseTabsAbove, onCloseTabsBelow, @@ -460,6 +469,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({ onPress={onPress} onCopyResumeCommand={onCopyResumeCommand} onCopyAgentId={onCopyAgentId} + onReloadAgent={onReloadAgent} onCloseTab={onCloseTab} onCloseTabsAbove={onCloseTabsAbove} onCloseTabsBelow={onCloseTabsBelow} @@ -1366,6 +1376,23 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) [normalizedServerId, toast], ); + const handleReloadAgent = useCallback( + async (agentId: string) => { + if (!client || !isConnected) { + toast.error("Host is not connected"); + return; + } + + try { + await client.refreshAgent(agentId); + toast.show("Reloaded agent", { variant: "success" }); + } catch (error) { + toast.error(error instanceof Error ? error.message : "Failed to reload agent"); + } + }, + [client, isConnected, toast], + ); + const handleCopyWorkspacePath = useCallback(async () => { if (!isAbsolutePath(normalizedWorkspaceId)) { toast.error("Workspace path not available"); @@ -2211,6 +2238,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) onSelectSwitcherTab={handleSelectSwitcherTab} onCopyResumeCommand={handleCopyResumeCommand} onCopyAgentId={handleCopyAgentId} + onReloadAgent={handleReloadAgent} onCloseTab={handleCloseTabById} onCloseTabsAbove={handleCloseTabsToLeft} onCloseTabsBelow={handleCloseTabsToRight} @@ -2231,6 +2259,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) onCloseTab={handleCloseTabById} onCopyResumeCommand={handleCopyResumeCommand} onCopyAgentId={handleCopyAgentId} + onReloadAgent={handleReloadAgent} onCloseTabsToLeft={handleCloseTabsToLeft} onCloseTabsToRight={handleCloseTabsToRight} onCloseOtherTabs={handleCloseOtherTabs} @@ -2267,6 +2296,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) onCloseTab={handleCloseTabById} onCopyResumeCommand={handleCopyResumeCommand} onCopyAgentId={handleCopyAgentId} + onReloadAgent={handleReloadAgent} onCloseTabsToLeft={handleCloseTabsToLeftInPane} onCloseTabsToRight={handleCloseTabsToRightInPane} onCloseOtherTabs={handleCloseOtherTabsInPane} diff --git a/packages/app/src/screens/workspace/workspace-tab-menu.test.ts b/packages/app/src/screens/workspace/workspace-tab-menu.test.ts index 67c58efb8..ba8395234 100644 --- a/packages/app/src/screens/workspace/workspace-tab-menu.test.ts +++ b/packages/app/src/screens/workspace/workspace-tab-menu.test.ts @@ -15,6 +15,7 @@ describe("buildWorkspaceTabMenuEntries", () => { it("uses desktop tab ordering labels for desktop menus", () => { const onCopyResumeCommand = vi.fn(); const onCopyAgentId = vi.fn(); + const onReloadAgent = vi.fn(); const onCloseTab = vi.fn(); const onCloseTabsBefore = vi.fn(); const onCloseTabsAfter = vi.fn(); @@ -28,6 +29,7 @@ describe("buildWorkspaceTabMenuEntries", () => { menuTestIDBase: "workspace-tab-context-agent_123", onCopyResumeCommand, onCopyAgentId, + onReloadAgent, onCloseTab, onCloseTabsBefore, onCloseTabsAfter, @@ -37,6 +39,7 @@ describe("buildWorkspaceTabMenuEntries", () => { expect(entries.filter((entry) => entry.kind === "item").map((entry) => entry.label)).toEqual([ "Copy resume command", "Copy agent id", + "Reload agent", "Close to the left", "Close to the right", "Close other tabs", @@ -53,6 +56,7 @@ describe("buildWorkspaceTabMenuEntries", () => { menuTestIDBase: "workspace-tab-menu-agent_123", onCopyResumeCommand: vi.fn(), onCopyAgentId: vi.fn(), + onReloadAgent: vi.fn(), onCloseTab: vi.fn(), onCloseTabsBefore: vi.fn(), onCloseTabsAfter: vi.fn(), @@ -62,6 +66,7 @@ describe("buildWorkspaceTabMenuEntries", () => { expect(entries.filter((entry) => entry.kind === "item").map((entry) => entry.label)).toEqual([ "Copy resume command", "Copy agent id", + "Reload agent", "Close tabs above", "Close tabs below", "Close other tabs", @@ -83,6 +88,7 @@ describe("buildWorkspaceTabMenuEntries", () => { menuTestIDBase: "workspace-tab-menu-draft_123", onCopyResumeCommand: vi.fn(), onCopyAgentId: vi.fn(), + onReloadAgent: vi.fn(), onCloseTab: vi.fn(), onCloseTabsBefore: vi.fn(), onCloseTabsAfter: vi.fn(), @@ -92,6 +98,34 @@ describe("buildWorkspaceTabMenuEntries", () => { expect(entries.some((entry) => entry.kind === "item" && entry.label === "Copy agent id")).toBe( false, ); + expect(entries.some((entry) => entry.kind === "item" && entry.label === "Reload agent")).toBe( + false, + ); expect(entries.some((entry) => entry.kind === "separator")).toBe(false); }); + + it("adds reload tooltip copy for agent tabs", () => { + const entries = buildWorkspaceTabMenuEntries({ + surface: "desktop", + tab: createAgentTab(), + index: 0, + tabCount: 1, + menuTestIDBase: "workspace-tab-context-agent_123", + onCopyResumeCommand: vi.fn(), + onCopyAgentId: vi.fn(), + onReloadAgent: vi.fn(), + onCloseTab: vi.fn(), + onCloseTabsBefore: vi.fn(), + onCloseTabsAfter: vi.fn(), + onCloseOtherTabs: vi.fn(), + }); + + expect(entries).toContainEqual( + expect.objectContaining({ + kind: "item", + key: "reload-agent", + tooltip: "Reload agent to update skills, MCPs or login status.", + }), + ); + }); }); diff --git a/packages/app/src/screens/workspace/workspace-tab-menu.ts b/packages/app/src/screens/workspace/workspace-tab-menu.ts index fea7255f4..00741d215 100644 --- a/packages/app/src/screens/workspace/workspace-tab-menu.ts +++ b/packages/app/src/screens/workspace/workspace-tab-menu.ts @@ -8,8 +8,15 @@ export type WorkspaceTabMenuEntry = kind: "item"; key: string; label: string; - icon?: "copy" | "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" + | "x"; hint?: string; + tooltip?: string; disabled?: boolean; destructive?: boolean; testID: string; @@ -28,6 +35,7 @@ interface BuildWorkspaceTabMenuEntriesInput { menuTestIDBase: string; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onReloadAgent: (agentId: string) => Promise | void; onCloseTab: (tabId: string) => Promise | void; onCloseTabsBefore: (tabId: string) => Promise | void; onCloseTabsAfter: (tabId: string) => Promise | void; @@ -40,6 +48,7 @@ interface BuildWorkspaceDesktopTabActionsInput { tabCount: number; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onReloadAgent: (agentId: string) => Promise | void; onCloseTab: (tabId: string) => Promise | void; onCloseTabsToLeft: (tabId: string) => Promise | void; onCloseTabsToRight: (tabId: string) => Promise | void; @@ -92,6 +101,7 @@ export function buildWorkspaceTabMenuEntries( menuTestIDBase, onCopyResumeCommand, onCopyAgentId, + onReloadAgent, onCloseTab, onCloseTabsBefore, onCloseTabsAfter, @@ -125,6 +135,17 @@ export function buildWorkspaceTabMenuEntries( void onCopyAgentId(agentId); }, }); + entries.push({ + kind: "item", + key: "reload-agent", + label: "Reload agent", + icon: "rotate-cw", + tooltip: "Reload agent to update skills, MCPs or login status.", + testID: `${menuTestIDBase}-reload-agent`, + onSelect: () => { + void onReloadAgent(agentId); + }, + }); entries.push({ kind: "separator", key: "copy-separator", @@ -192,6 +213,7 @@ export function buildWorkspaceDesktopTabActions( menuTestIDBase: contextMenuTestId, onCopyResumeCommand: input.onCopyResumeCommand, onCopyAgentId: input.onCopyAgentId, + onReloadAgent: input.onReloadAgent, onCloseTab: input.onCloseTab, onCloseTabsBefore: input.onCloseTabsToLeft, onCloseTabsAfter: input.onCloseTabsToRight,