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.
This commit is contained in:
Mohamed Boudra
2026-05-09 17:33:14 +08:00
committed by GitHub
parent e3ef88c297
commit 8b09846f98
7 changed files with 103 additions and 3 deletions

View File

@@ -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}
/>
);
}

View File

@@ -11,6 +11,7 @@ export interface PaneContextValue {
closeCurrentTab: () => void;
retargetCurrentTab: (target: WorkspaceTabTarget) => void;
openFileInWorkspace: (filePath: string) => void;
openImportSheet: () => void;
}
export interface PaneFocusContextValue {

View File

@@ -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 (
<View style={styles.row}>
<Pressable
testID="composer-import-agent-pill"
accessibilityRole="button"
accessibilityLabel="Import agent"
onPress={onPress}
disabled={disabled}
onHoverIn={handleHoverIn}
onHoverOut={handleHoverOut}
style={bodyStyle}
>
<ThemedImportIcon size={14} uniProps={iconColorMapping} />
<Text style={styles.label} numberOfLines={1}>
Import agent
</Text>
</Pressable>
</View>
);
}
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,
},
}));

View File

@@ -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({
</View>
<View style={inputAreaWrapperStyle}>
{onOpenImportSheet ? (
<View style={styles.importPillRow}>
<View style={styles.importPillContent}>
<ComposerImportPill onPress={onOpenImportSheet} disabled={isSubmitting} />
</View>
</View>
) : null}
<Composer
agentId={tabId}
serverId={serverId}
@@ -644,6 +654,18 @@ const styles = StyleSheet.create((theme) => ({
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],

View File

@@ -64,6 +64,7 @@ function buildContent() {
onCloseCurrentTab: vi.fn(),
onRetargetCurrentTab: vi.fn(),
onOpenWorkspaceFile: vi.fn(),
onOpenImportSheet: vi.fn(),
});
}

View File

@@ -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,
},
};
}

View File

@@ -2833,6 +2833,7 @@ function WorkspaceScreenContent({
}
handleOpenFileFromChat({ filePath });
},
onOpenImportSheet: openImportSheet,
}),
[
handleCloseTabById,
@@ -2841,6 +2842,7 @@ function WorkspaceScreenContent({
navigateToTabId,
normalizedServerId,
normalizedWorkspaceId,
openImportSheet,
openWorkspaceTabFocused,
persistenceKey,
convertWorkspaceDraftToAgent,