Auto-attach pasted GitHub PR/issue URLs in the composer (#1006)
* Auto-attach pasted GitHub PR/issue URLs in the composer Paste a GitHub PR or issue link into any composer and it shows up as an attachment pill within a debounce tick when the URL matches the workspace's remote. On the new-workspace screen, an attached PR also surfaces an inline "Check out PR #N?" hint next to the picker; one click promotes it to the checkout target through the same syncPickerPrAttachment flow the dropdown uses. Unifies the new-workspace picker and composer attachment picker on one shared GitHub search hook and cache key, so both consumers (and the auto-attach lookup) share the React Query cache. * Fix oxfmt formatting in new-workspace-screen import
This commit is contained in:
@@ -21,7 +21,6 @@ import {
|
||||
Paperclip,
|
||||
} from "lucide-react-native";
|
||||
import Animated from "react-native-reanimated";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { FOOTER_HEIGHT, MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||
import {
|
||||
AgentStatusBar,
|
||||
@@ -53,7 +52,7 @@ import {
|
||||
queueComposerMessage,
|
||||
removeComposerAttachmentAtIndex,
|
||||
sendQueuedComposerMessageNow,
|
||||
toggleGithubAttachment,
|
||||
toggleGithubAttachmentFromPicker,
|
||||
type AgentStreamWriter,
|
||||
type QueueWriter,
|
||||
type QueuedComposerMessage,
|
||||
@@ -97,6 +96,9 @@ import { AttachmentPill } from "@/components/attachment-pill";
|
||||
import { AttachmentLightbox } from "@/components/attachment-lightbox";
|
||||
import { openExternalUrl } from "@/utils/open-external-url";
|
||||
import { useIsDictationReady } from "@/hooks/use-is-dictation-ready";
|
||||
import { useGithubSearchQuery } from "@/git/use-github-search-query";
|
||||
import { useCheckoutStatusQuery } from "@/git/use-status-query";
|
||||
import { useComposerGithubAutoAttach } from "./use-composer-github-auto-attach";
|
||||
|
||||
type QueuedMessage = QueuedComposerMessage;
|
||||
|
||||
@@ -137,6 +139,20 @@ function resolveMessagePlaceholder(isDesktopWebBreakpoint: boolean): string {
|
||||
return isDesktopWebBreakpoint ? DESKTOP_MESSAGE_PLACEHOLDER : MOBILE_MESSAGE_PLACEHOLDER;
|
||||
}
|
||||
|
||||
function resolveGithubSearchEnabled(
|
||||
isGithubPickerOpen: boolean,
|
||||
isConnected: boolean,
|
||||
cwd: string,
|
||||
): boolean {
|
||||
return isGithubPickerOpen && isConnected && cwd.trim().length > 0;
|
||||
}
|
||||
|
||||
function resolveCheckoutRemoteUrl(
|
||||
checkoutStatus: ReturnType<typeof useCheckoutStatusQuery>["status"],
|
||||
): string | null {
|
||||
return checkoutStatus?.remoteUrl ?? null;
|
||||
}
|
||||
|
||||
function buildCancelButtonStyle(isConnected: boolean, isCancellingAgent: boolean): object[] {
|
||||
const disabled = !isConnected || isCancellingAgent ? styles.buttonDisabled : undefined;
|
||||
return [styles.cancelButton, disabled].filter((value): value is object => Boolean(value));
|
||||
@@ -164,31 +180,6 @@ function buildAgentStateSelector(serverId: string, agentId: string) {
|
||||
};
|
||||
}
|
||||
|
||||
interface BuildGithubSearchQueryOptionsArgs {
|
||||
serverId: string;
|
||||
cwd: string;
|
||||
githubSearchQueryTrimmed: string;
|
||||
isGithubPickerOpen: boolean;
|
||||
isConnected: boolean;
|
||||
client: ReturnType<typeof useHostRuntimeClient>;
|
||||
}
|
||||
|
||||
function buildGithubSearchQueryOptions(args: BuildGithubSearchQueryOptionsArgs) {
|
||||
const { serverId, cwd, githubSearchQueryTrimmed, isGithubPickerOpen, isConnected, client } = args;
|
||||
const hasClient = Boolean(client);
|
||||
const cwdIsSet = cwd.trim().length > 0;
|
||||
const enabled = isGithubPickerOpen && isConnected && hasClient && cwdIsSet;
|
||||
return {
|
||||
queryKey: ["composer-github-search", serverId, cwd, githubSearchQueryTrimmed],
|
||||
queryFn: async () => {
|
||||
if (!client) throw new Error("Host is not connected");
|
||||
return client.searchGitHub({ cwd, query: githubSearchQueryTrimmed, limit: 20 });
|
||||
},
|
||||
enabled,
|
||||
staleTime: 30_000,
|
||||
};
|
||||
}
|
||||
|
||||
function renderContextWindowMeterSlot(
|
||||
contextWindowMaxTokens: number | null,
|
||||
contextWindowUsedTokens: number | null,
|
||||
@@ -890,6 +881,17 @@ export function Composer({
|
||||
onOpenWorkspaceAttachment,
|
||||
});
|
||||
const setSelectedAttachments = onChangeAttachments;
|
||||
const checkoutStatusQuery = useCheckoutStatusQuery({ serverId, cwd });
|
||||
const githubAutoAttach = useComposerGithubAutoAttach({
|
||||
text: userInput,
|
||||
remoteUrl: resolveCheckoutRemoteUrl(checkoutStatusQuery.status),
|
||||
attachments,
|
||||
client,
|
||||
isConnected,
|
||||
serverId,
|
||||
cwd,
|
||||
setAttachments: setSelectedAttachments,
|
||||
});
|
||||
const [cursorIndex, setCursorIndex] = useState(0);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [isCancellingAgent, setIsCancellingAgent] = useState(false);
|
||||
@@ -1135,6 +1137,7 @@ export function Composer({
|
||||
|
||||
const handleRemoveAttachment = useCallback(
|
||||
(index: number) => {
|
||||
githubAutoAttach.markGithubAttachmentRemoved(selectedAttachments[index]);
|
||||
const didRemoveWorkspaceAttachment = removeAttachment({
|
||||
selectedAttachments,
|
||||
index,
|
||||
@@ -1146,7 +1149,7 @@ export function Composer({
|
||||
removeComposerAttachmentAtIndex({ attachments: prev, index, deleteAttachments }),
|
||||
);
|
||||
},
|
||||
[removeAttachment, selectedAttachments, setSelectedAttachments],
|
||||
[githubAutoAttach, removeAttachment, selectedAttachments, setSelectedAttachments],
|
||||
);
|
||||
|
||||
const handleOpenAttachment = useCallback(
|
||||
@@ -1376,16 +1379,13 @@ export function Composer({
|
||||
);
|
||||
|
||||
const githubSearchQueryTrimmed = githubSearchQuery.trim();
|
||||
const githubSearchResultsQuery = useQuery(
|
||||
buildGithubSearchQueryOptions({
|
||||
serverId,
|
||||
cwd,
|
||||
githubSearchQueryTrimmed,
|
||||
isGithubPickerOpen,
|
||||
isConnected,
|
||||
client,
|
||||
}),
|
||||
);
|
||||
const githubSearchResultsQuery = useGithubSearchQuery({
|
||||
client,
|
||||
serverId,
|
||||
cwd,
|
||||
query: githubSearchQueryTrimmed,
|
||||
enabled: resolveGithubSearchEnabled(isGithubPickerOpen, isConnected, cwd),
|
||||
});
|
||||
|
||||
const githubSearchItemsRaw = githubSearchResultsQuery.data?.items;
|
||||
const githubSearchItems = useMemo(() => githubSearchItemsRaw ?? [], [githubSearchItemsRaw]);
|
||||
@@ -1423,11 +1423,22 @@ export function Composer({
|
||||
|
||||
const handleToggleGithubItem = useCallback(
|
||||
(item: GitHubSearchItem) => {
|
||||
setSelectedAttachments((current) => toggleGithubAttachment(current, item));
|
||||
const nextAttachments = toggleGithubAttachmentFromPicker({
|
||||
current: attachments,
|
||||
item,
|
||||
markGithubAttachmentRemoved: githubAutoAttach.markGithubAttachmentRemoved,
|
||||
});
|
||||
setSelectedAttachments(nextAttachments);
|
||||
setIsGithubPickerOpen(false);
|
||||
setGithubSearchQuery("");
|
||||
},
|
||||
[setSelectedAttachments, setGithubSearchQuery, setIsGithubPickerOpen],
|
||||
[
|
||||
attachments,
|
||||
githubAutoAttach,
|
||||
setSelectedAttachments,
|
||||
setGithubSearchQuery,
|
||||
setIsGithubPickerOpen,
|
||||
],
|
||||
);
|
||||
|
||||
const leftContent = useMemo(
|
||||
|
||||
Reference in New Issue
Block a user