From 8b09846f98c9cc804539f1351e8101731ebf130c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 9 May 2026 17:33:14 +0800 Subject: [PATCH] feat(app): import-agent pill above draft composer (#833) Surfaces the existing Import-agent sheet directly from the new-agent draft composer, so users don't have to dig into the workspace header menu. The pill renders only on draft tabs (gated by the prop being provided) and reuses the existing WorkspaceImportSheet. --- packages/app/src/panels/draft-panel.tsx | 12 +++- packages/app/src/panels/pane-context.tsx | 1 + .../workspace/composer-import-pill.tsx | 63 +++++++++++++++++++ .../workspace/workspace-draft-agent-tab.tsx | 24 ++++++- .../workspace/workspace-pane-content.test.tsx | 1 + .../workspace/workspace-pane-content.tsx | 3 + .../screens/workspace/workspace-screen.tsx | 2 + 7 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 packages/app/src/screens/workspace/composer-import-pill.tsx diff --git a/packages/app/src/panels/draft-panel.tsx b/packages/app/src/panels/draft-panel.tsx index be9623c60..5db7b5372 100644 --- a/packages/app/src/panels/draft-panel.tsx +++ b/packages/app/src/panels/draft-panel.tsx @@ -18,8 +18,15 @@ function useDraftPanelDescriptor() { } function DraftPanel() { - const { serverId, workspaceId, tabId, target, openFileInWorkspace, retargetCurrentTab } = - usePaneContext(); + const { + serverId, + workspaceId, + tabId, + target, + openFileInWorkspace, + openImportSheet, + retargetCurrentTab, + } = usePaneContext(); const { isInteractive } = usePaneFocus(); invariant(target.kind === "draft", "DraftPanel requires draft target"); @@ -52,6 +59,7 @@ function DraftPanel() { isPaneFocused={isInteractive} onOpenWorkspaceFile={handleOpenWorkspaceFile} onCreated={handleCreated} + onOpenImportSheet={openImportSheet} /> ); } diff --git a/packages/app/src/panels/pane-context.tsx b/packages/app/src/panels/pane-context.tsx index 4ee0c6c0a..34c5869f5 100644 --- a/packages/app/src/panels/pane-context.tsx +++ b/packages/app/src/panels/pane-context.tsx @@ -11,6 +11,7 @@ export interface PaneContextValue { closeCurrentTab: () => void; retargetCurrentTab: (target: WorkspaceTabTarget) => void; openFileInWorkspace: (filePath: string) => void; + openImportSheet: () => void; } export interface PaneFocusContextValue { diff --git a/packages/app/src/screens/workspace/composer-import-pill.tsx b/packages/app/src/screens/workspace/composer-import-pill.tsx new file mode 100644 index 000000000..25267a834 --- /dev/null +++ b/packages/app/src/screens/workspace/composer-import-pill.tsx @@ -0,0 +1,63 @@ +import { useCallback, useMemo, useState } from "react"; +import { Pressable, Text, View } from "react-native"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { Import as ImportIcon } from "lucide-react-native"; +import type { Theme } from "@/styles/theme"; + +const ThemedImportIcon = withUnistyles(ImportIcon); +const iconColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); + +interface ComposerImportPillProps { + onPress: () => void; + disabled?: boolean; +} + +export function ComposerImportPill({ onPress, disabled = false }: ComposerImportPillProps) { + const [isHovered, setIsHovered] = useState(false); + const handleHoverIn = useCallback(() => setIsHovered(true), []); + const handleHoverOut = useCallback(() => setIsHovered(false), []); + const bodyStyle = useMemo(() => [styles.body, isHovered && styles.bodyHovered], [isHovered]); + return ( + + + + + Import agent + + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + row: { + flexDirection: "row", + }, + body: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + paddingHorizontal: theme.spacing[3], + paddingVertical: theme.spacing[2], + borderRadius: theme.borderRadius.md, + borderWidth: theme.borderWidth[1], + borderColor: theme.colors.borderAccent, + backgroundColor: theme.colors.surface1, + }, + bodyHovered: { + backgroundColor: theme.colors.surface2, + }, + label: { + color: theme.colors.foreground, + fontSize: theme.fontSize.sm, + }, +})); diff --git a/packages/app/src/screens/workspace/workspace-draft-agent-tab.tsx b/packages/app/src/screens/workspace/workspace-draft-agent-tab.tsx index c8aca2261..6c2837052 100644 --- a/packages/app/src/screens/workspace/workspace-draft-agent-tab.tsx +++ b/packages/app/src/screens/workspace/workspace-draft-agent-tab.tsx @@ -4,6 +4,7 @@ import { StyleSheet } from "react-native-unistyles"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import invariant from "tiny-invariant"; import { Composer } from "@/components/composer"; +import { ComposerImportPill } from "@/screens/workspace/composer-import-pill"; import { FileDropZone } from "@/components/file-drop-zone"; import { AgentStreamView } from "@/components/agent-stream-view"; import { composerWorkspaceAttachment } from "@/attachments/composer-workspace-attachments"; @@ -28,7 +29,7 @@ import { useWorkspaceAttachmentScopeKey, } from "@/attachments/workspace-attachments-store"; import type { UserMessageImageAttachment } from "@/types/stream"; -import { useIsCompactFormFactor } from "@/constants/layout"; +import { MAX_CONTENT_WIDTH, useIsCompactFormFactor } from "@/constants/layout"; import { isWeb } from "@/constants/platform"; const EMPTY_PENDING_PERMISSIONS = new Map(); @@ -278,6 +279,7 @@ interface WorkspaceDraftAgentTabProps { isPaneFocused: boolean; onCreated: (snapshot: AgentSnapshotPayload) => void; onOpenWorkspaceFile: (input: { filePath: string }) => void; + onOpenImportSheet?: () => void; } export function WorkspaceDraftAgentTab({ @@ -288,6 +290,7 @@ export function WorkspaceDraftAgentTab({ isPaneFocused, onCreated, onOpenWorkspaceFile, + onOpenImportSheet, }: WorkspaceDraftAgentTabProps) { const insets = useSafeAreaInsets(); const client = useHostRuntimeClient(serverId); @@ -590,6 +593,13 @@ export function WorkspaceDraftAgentTab({ + {onOpenImportSheet ? ( + + + + + + ) : null} ({ width: "100%", backgroundColor: theme.colors.surface0, }, + importPillRow: { + width: "100%", + paddingHorizontal: theme.spacing[4], + paddingTop: theme.spacing[3], + paddingBottom: theme.spacing[3], + alignItems: "center", + }, + importPillContent: { + width: "100%", + maxWidth: MAX_CONTENT_WIDTH, + flexDirection: "row", + }, errorContainer: { marginTop: theme.spacing[2], paddingHorizontal: theme.spacing[3], diff --git a/packages/app/src/screens/workspace/workspace-pane-content.test.tsx b/packages/app/src/screens/workspace/workspace-pane-content.test.tsx index c4c422809..d700da58c 100644 --- a/packages/app/src/screens/workspace/workspace-pane-content.test.tsx +++ b/packages/app/src/screens/workspace/workspace-pane-content.test.tsx @@ -64,6 +64,7 @@ function buildContent() { onCloseCurrentTab: vi.fn(), onRetargetCurrentTab: vi.fn(), onOpenWorkspaceFile: vi.fn(), + onOpenImportSheet: vi.fn(), }); } diff --git a/packages/app/src/screens/workspace/workspace-pane-content.tsx b/packages/app/src/screens/workspace/workspace-pane-content.tsx index f0d9b3ab8..cef49ad59 100644 --- a/packages/app/src/screens/workspace/workspace-pane-content.tsx +++ b/packages/app/src/screens/workspace/workspace-pane-content.tsx @@ -24,6 +24,7 @@ export interface BuildWorkspacePaneContentModelInput { onCloseCurrentTab: () => void; onRetargetCurrentTab: (target: WorkspaceTabDescriptor["target"]) => void; onOpenWorkspaceFile: (filePath: string) => void; + onOpenImportSheet: () => void; } export function buildWorkspacePaneContentModel({ @@ -34,6 +35,7 @@ export function buildWorkspacePaneContentModel({ onCloseCurrentTab, onRetargetCurrentTab, onOpenWorkspaceFile, + onOpenImportSheet, }: BuildWorkspacePaneContentModelInput): WorkspacePaneContentModel { ensurePanelsRegistered(); const registration = getPanelRegistration(tab.kind); @@ -50,6 +52,7 @@ export function buildWorkspacePaneContentModel({ closeCurrentTab: onCloseCurrentTab, retargetCurrentTab: onRetargetCurrentTab, openFileInWorkspace: onOpenWorkspaceFile, + openImportSheet: onOpenImportSheet, }, }; } diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 5bb8cf134..2725c430c 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -2833,6 +2833,7 @@ function WorkspaceScreenContent({ } handleOpenFileFromChat({ filePath }); }, + onOpenImportSheet: openImportSheet, }), [ handleCloseTabById, @@ -2841,6 +2842,7 @@ function WorkspaceScreenContent({ navigateToTabId, normalizedServerId, normalizedWorkspaceId, + openImportSheet, openWorkspaceTabFocused, persistenceKey, convertWorkspaceDraftToAgent,