feat(app,server): rank new-workspace picker by recency with debounced search
Plumb updatedAt through GitHub issue/PR search and committerDate through branch suggestions so the new-workspace picker interleaves branches and PRs sorted by last activity. Debounce the search query so remote GitHub searches are driven by what the user typed rather than a static list.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Pressable, Text, View } from "react-native";
|
import { Pressable, Text, View } from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { createNameId } from "mnemonic-id";
|
import { createNameId } from "mnemonic-id";
|
||||||
@@ -41,8 +41,6 @@ interface NewWorkspaceScreenProps {
|
|||||||
interface PickerOptionData {
|
interface PickerOptionData {
|
||||||
options: ComboboxOptionType[];
|
options: ComboboxOptionType[];
|
||||||
itemById: Map<string, PickerItem>;
|
itemById: Map<string, PickerItem>;
|
||||||
firstBranchId: string | null;
|
|
||||||
firstPrId: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PickerSelection {
|
interface PickerSelection {
|
||||||
@@ -119,8 +117,16 @@ export function NewWorkspaceScreen({
|
|||||||
const [pendingAction, setPendingAction] = useState<"chat" | null>(null);
|
const [pendingAction, setPendingAction] = useState<"chat" | null>(null);
|
||||||
const [pickerSelection, setPickerSelection] = useState<PickerSelection | null>(null);
|
const [pickerSelection, setPickerSelection] = useState<PickerSelection | null>(null);
|
||||||
const [pickerOpen, setPickerOpen] = useState(false);
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
|
const [pickerSearchQuery, setPickerSearchQuery] = useState("");
|
||||||
|
const [debouncedPickerSearchQuery, setDebouncedPickerSearchQuery] = useState("");
|
||||||
const pickerAnchorRef = useRef<View>(null);
|
const pickerAnchorRef = useRef<View>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const trimmed = pickerSearchQuery.trim();
|
||||||
|
const timer = setTimeout(() => setDebouncedPickerSearchQuery(trimmed), 180);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [pickerSearchQuery]);
|
||||||
|
|
||||||
const displayName = displayNameProp?.trim() ?? "";
|
const displayName = displayNameProp?.trim() ?? "";
|
||||||
const workspace = createdWorkspace;
|
const workspace = createdWorkspace;
|
||||||
const selectedItem = pickerSelection?.item ?? null;
|
const selectedItem = pickerSelection?.item ?? null;
|
||||||
@@ -161,31 +167,40 @@ export function NewWorkspaceScreen({
|
|||||||
const currentBranch = checkoutStatusQuery.data?.currentBranch ?? null;
|
const currentBranch = checkoutStatusQuery.data?.currentBranch ?? null;
|
||||||
|
|
||||||
const branchSuggestionsQuery = useQuery({
|
const branchSuggestionsQuery = useQuery({
|
||||||
queryKey: ["branch-suggestions", serverId, sourceDirectory],
|
queryKey: ["branch-suggestions", serverId, sourceDirectory, debouncedPickerSearchQuery],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const connectedClient = withConnectedClient();
|
const connectedClient = withConnectedClient();
|
||||||
return connectedClient.getBranchSuggestions({ cwd: sourceDirectory, limit: 20 });
|
return connectedClient.getBranchSuggestions({
|
||||||
|
cwd: sourceDirectory,
|
||||||
|
query: debouncedPickerSearchQuery || undefined,
|
||||||
|
limit: 20,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
enabled: isConnected && !!client,
|
enabled: isConnected && !!client,
|
||||||
staleTime: 15_000,
|
staleTime: 15_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const githubPrSearchQuery = useQuery({
|
const githubPrSearchQuery = useQuery({
|
||||||
queryKey: ["new-workspace-github-prs", serverId, sourceDirectory],
|
queryKey: ["new-workspace-github-prs", serverId, sourceDirectory, debouncedPickerSearchQuery],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const connectedClient = withConnectedClient();
|
const connectedClient = withConnectedClient();
|
||||||
return connectedClient.searchGitHub({
|
return connectedClient.searchGitHub({
|
||||||
cwd: sourceDirectory,
|
cwd: sourceDirectory,
|
||||||
query: "",
|
query: debouncedPickerSearchQuery,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
kinds: ["github-pr"],
|
kinds: ["github-pr"],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
enabled: isConnected && !!client,
|
enabled: isConnected && !!client,
|
||||||
staleTime: 60_000,
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const branchNames: string[] = branchSuggestionsQuery.data?.branches ?? [];
|
const branchDetails = useMemo(() => {
|
||||||
|
const details = branchSuggestionsQuery.data?.branchDetails;
|
||||||
|
if (details && details.length > 0) return details;
|
||||||
|
const names = branchSuggestionsQuery.data?.branches ?? [];
|
||||||
|
return names.map((name) => ({ name, committerDate: 0 }));
|
||||||
|
}, [branchSuggestionsQuery.data?.branchDetails, branchSuggestionsQuery.data?.branches]);
|
||||||
const githubFeaturesEnabled = githubPrSearchQuery.data?.githubFeaturesEnabled !== false;
|
const githubFeaturesEnabled = githubPrSearchQuery.data?.githubFeaturesEnabled !== false;
|
||||||
const prItems: GitHubSearchItem[] = useMemo(() => {
|
const prItems: GitHubSearchItem[] = useMemo(() => {
|
||||||
if (!githubFeaturesEnabled) return [];
|
if (!githubFeaturesEnabled) return [];
|
||||||
@@ -193,37 +208,35 @@ export function NewWorkspaceScreen({
|
|||||||
return items.filter((item): item is GitHubSearchItem => item.kind === "pr");
|
return items.filter((item): item is GitHubSearchItem => item.kind === "pr");
|
||||||
}, [githubFeaturesEnabled, githubPrSearchQuery.data?.items]);
|
}, [githubFeaturesEnabled, githubPrSearchQuery.data?.items]);
|
||||||
|
|
||||||
const { options, itemById, firstBranchId, firstPrId }: PickerOptionData = useMemo(() => {
|
const { options, itemById }: PickerOptionData = useMemo(() => {
|
||||||
const collectedOptions: ComboboxOptionType[] = [];
|
|
||||||
const idMap = new Map<string, PickerItem>();
|
const idMap = new Map<string, PickerItem>();
|
||||||
let firstBranch: string | null = null;
|
|
||||||
let firstPr: string | null = null;
|
|
||||||
|
|
||||||
for (const name of branchNames) {
|
interface TimedOption {
|
||||||
const id = branchOptionId(name);
|
option: ComboboxOptionType;
|
||||||
collectedOptions.push({ id, label: name });
|
timestamp: number;
|
||||||
idMap.set(id, { kind: "branch", name });
|
}
|
||||||
if (firstBranch === null) firstBranch = id;
|
const timedOptions: TimedOption[] = [];
|
||||||
|
|
||||||
|
for (const branch of branchDetails) {
|
||||||
|
const id = branchOptionId(branch.name);
|
||||||
|
const option = { id, label: branch.name };
|
||||||
|
idMap.set(id, { kind: "branch", name: branch.name });
|
||||||
|
timedOptions.push({ option, timestamp: branch.committerDate });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pr of prItems) {
|
for (const pr of prItems) {
|
||||||
if (!pr.headRefName) continue;
|
if (!pr.headRefName) continue;
|
||||||
const id = prOptionId(pr.number);
|
const id = prOptionId(pr.number);
|
||||||
collectedOptions.push({ id, label: formatPrLabel(pr) });
|
const option = { id, label: formatPrLabel(pr) };
|
||||||
idMap.set(id, {
|
idMap.set(id, { kind: "github-pr", item: pr });
|
||||||
kind: "github-pr",
|
const updatedAtMs = pr.updatedAt ? Date.parse(pr.updatedAt) : 0;
|
||||||
item: pr,
|
const timestamp = Number.isNaN(updatedAtMs) ? 0 : Math.floor(updatedAtMs / 1000);
|
||||||
});
|
timedOptions.push({ option, timestamp });
|
||||||
if (firstPr === null) firstPr = id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
timedOptions.sort((a, b) => b.timestamp - a.timestamp);
|
||||||
options: collectedOptions,
|
return { options: timedOptions.map((t) => t.option), itemById: idMap };
|
||||||
itemById: idMap,
|
}, [branchDetails, prItems]);
|
||||||
firstBranchId: firstBranch,
|
|
||||||
firstPrId: firstPr,
|
|
||||||
};
|
|
||||||
}, [branchNames, prItems]);
|
|
||||||
|
|
||||||
const triggerLabel = useMemo(() => {
|
const triggerLabel = useMemo(() => {
|
||||||
if (selectedItem) return pickerItemTriggerLabel(selectedItem);
|
if (selectedItem) return pickerItemTriggerLabel(selectedItem);
|
||||||
@@ -383,12 +396,6 @@ export function NewWorkspaceScreen({
|
|||||||
if (!item) return <View key={option.id} />;
|
if (!item) return <View key={option.id} />;
|
||||||
|
|
||||||
const isBranch = item.kind === "branch";
|
const isBranch = item.kind === "branch";
|
||||||
const isFirstInSection =
|
|
||||||
(isBranch && option.id === firstBranchId) || (!isBranch && option.id === firstPrId);
|
|
||||||
|
|
||||||
const header = isFirstInSection ? (
|
|
||||||
<Text style={styles.sectionHeader}>{isBranch ? "Branches" : "Pull requests"}</Text>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const leadingSlot = (
|
const leadingSlot = (
|
||||||
<View style={styles.rowIconBox}>
|
<View style={styles.rowIconBox}>
|
||||||
@@ -408,29 +415,19 @@ export function NewWorkspaceScreen({
|
|||||||
!isBranch && item.item.baseRefName ? `into ${item.item.baseRefName}` : undefined;
|
!isBranch && item.item.baseRefName ? `into ${item.item.baseRefName}` : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ComboboxItem
|
||||||
{header}
|
testID={testID}
|
||||||
<ComboboxItem
|
label={pickerItemLabel(item)}
|
||||||
testID={testID}
|
description={description}
|
||||||
label={pickerItemLabel(item)}
|
selected={selected}
|
||||||
description={description}
|
active={active}
|
||||||
selected={selected}
|
disabled={isPending}
|
||||||
active={active}
|
onPress={onPress}
|
||||||
disabled={isPending}
|
leadingSlot={leadingSlot}
|
||||||
onPress={onPress}
|
/>
|
||||||
leadingSlot={leadingSlot}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[
|
[isPending, itemById, theme.colors.foregroundMuted, theme.iconSize.sm],
|
||||||
firstBranchId,
|
|
||||||
firstPrId,
|
|
||||||
isPending,
|
|
||||||
itemById,
|
|
||||||
theme.colors.foregroundMuted,
|
|
||||||
theme.iconSize.sm,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -532,10 +529,20 @@ export function NewWorkspaceScreen({
|
|||||||
searchPlaceholder="Search branches and PRs"
|
searchPlaceholder="Search branches and PRs"
|
||||||
title="Start from"
|
title="Start from"
|
||||||
open={pickerOpen}
|
open={pickerOpen}
|
||||||
onOpenChange={setPickerOpen}
|
onOpenChange={(nextOpen) => {
|
||||||
|
setPickerOpen(nextOpen);
|
||||||
|
if (!nextOpen) {
|
||||||
|
setPickerSearchQuery("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSearchQueryChange={setPickerSearchQuery}
|
||||||
desktopPlacement="bottom-start"
|
desktopPlacement="bottom-start"
|
||||||
anchorRef={pickerAnchorRef}
|
anchorRef={pickerAnchorRef}
|
||||||
emptyText="No matching refs."
|
emptyText={
|
||||||
|
branchSuggestionsQuery.isFetching || githubPrSearchQuery.isFetching
|
||||||
|
? "Searching..."
|
||||||
|
: "No matching refs."
|
||||||
|
}
|
||||||
renderOption={renderPickerOption}
|
renderOption={renderPickerOption}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -644,13 +651,4 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
sectionHeader: {
|
|
||||||
paddingHorizontal: theme.spacing[3],
|
|
||||||
paddingTop: theme.spacing[2],
|
|
||||||
paddingBottom: theme.spacing[1],
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: 0,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -4088,11 +4088,12 @@ export class Session {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const resolvedCwd = expandTilde(cwd);
|
const resolvedCwd = expandTilde(cwd);
|
||||||
const branches = await listBranchSuggestions(resolvedCwd, { query, limit });
|
const branchDetails = await listBranchSuggestions(resolvedCwd, { query, limit });
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "branch_suggestions_response",
|
type: "branch_suggestions_response",
|
||||||
payload: {
|
payload: {
|
||||||
branches,
|
branches: branchDetails.map((branch) => branch.name),
|
||||||
|
branchDetails,
|
||||||
error: null,
|
error: null,
|
||||||
requestId,
|
requestId,
|
||||||
},
|
},
|
||||||
@@ -4102,6 +4103,7 @@ export class Session {
|
|||||||
type: "branch_suggestions_response",
|
type: "branch_suggestions_response",
|
||||||
payload: {
|
payload: {
|
||||||
branches: [],
|
branches: [],
|
||||||
|
branchDetails: [],
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
requestId,
|
requestId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const GitHubIssueSummarySchema = z.object({
|
|||||||
state: z.string().catch(""),
|
state: z.string().catch(""),
|
||||||
body: z.string().nullable().catch(null),
|
body: z.string().nullable().catch(null),
|
||||||
labels: z.array(LabelSchema).catch([]),
|
labels: z.array(LabelSchema).catch([]),
|
||||||
|
updatedAt: z.string().catch(""),
|
||||||
});
|
});
|
||||||
|
|
||||||
const GitHubPullRequestSummarySchema = z.object({
|
const GitHubPullRequestSummarySchema = z.object({
|
||||||
@@ -31,6 +32,7 @@ const GitHubPullRequestSummarySchema = z.object({
|
|||||||
baseRefName: z.string().catch(""),
|
baseRefName: z.string().catch(""),
|
||||||
headRefName: z.string().catch(""),
|
headRefName: z.string().catch(""),
|
||||||
labels: z.array(LabelSchema).catch([]),
|
labels: z.array(LabelSchema).catch([]),
|
||||||
|
updatedAt: z.string().catch(""),
|
||||||
});
|
});
|
||||||
|
|
||||||
const PullRequestCheckRunNodeSchema = z.object({
|
const PullRequestCheckRunNodeSchema = z.object({
|
||||||
@@ -123,6 +125,7 @@ export interface GitHubPullRequestSummary {
|
|||||||
baseRefName: string;
|
baseRefName: string;
|
||||||
headRefName: string;
|
headRefName: string;
|
||||||
labels: string[];
|
labels: string[];
|
||||||
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GitHubIssueSummary {
|
export interface GitHubIssueSummary {
|
||||||
@@ -132,6 +135,7 @@ export interface GitHubIssueSummary {
|
|||||||
state: string;
|
state: string;
|
||||||
body: string | null;
|
body: string | null;
|
||||||
labels: string[];
|
labels: string[];
|
||||||
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PullRequestCheckStatus = "pending" | "success" | "failure" | "cancelled" | "skipped";
|
export type PullRequestCheckStatus = "pending" | "success" | "failure" | "cancelled" | "skipped";
|
||||||
@@ -337,7 +341,7 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G
|
|||||||
"--search",
|
"--search",
|
||||||
options.query ?? "",
|
options.query ?? "",
|
||||||
"--json",
|
"--json",
|
||||||
"number,title,url,state,body,labels,baseRefName,headRefName",
|
"number,title,url,state,body,labels,baseRefName,headRefName,updatedAt",
|
||||||
"--limit",
|
"--limit",
|
||||||
String(options.limit ?? 20),
|
String(options.limit ?? 20),
|
||||||
],
|
],
|
||||||
@@ -361,7 +365,7 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G
|
|||||||
"--search",
|
"--search",
|
||||||
options.query ?? "",
|
options.query ?? "",
|
||||||
"--json",
|
"--json",
|
||||||
"number,title,url,state,body,labels",
|
"number,title,url,state,body,labels,updatedAt",
|
||||||
"--limit",
|
"--limit",
|
||||||
String(options.limit ?? 20),
|
String(options.limit ?? 20),
|
||||||
],
|
],
|
||||||
@@ -384,7 +388,7 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G
|
|||||||
"view",
|
"view",
|
||||||
String(options.number),
|
String(options.number),
|
||||||
"--json",
|
"--json",
|
||||||
"number,title,url,state,body,labels,baseRefName,headRefName",
|
"number,title,url,state,body,labels,baseRefName,headRefName,updatedAt",
|
||||||
],
|
],
|
||||||
{ cwd: options.cwd },
|
{ cwd: options.cwd },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1189,6 +1189,7 @@ export const GitHubSearchItemSchema = z.object({
|
|||||||
labels: z.array(z.string()),
|
labels: z.array(z.string()),
|
||||||
baseRefName: z.string().nullable().optional(),
|
baseRefName: z.string().nullable().optional(),
|
||||||
headRefName: z.string().nullable().optional(),
|
headRefName: z.string().nullable().optional(),
|
||||||
|
updatedAt: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GitHubSearchKindSchema = z.enum(["github-issue", "github-pr"]);
|
export const GitHubSearchKindSchema = z.enum(["github-issue", "github-pr"]);
|
||||||
@@ -2598,6 +2599,14 @@ export const BranchSuggestionsResponseSchema = z.object({
|
|||||||
type: z.literal("branch_suggestions_response"),
|
type: z.literal("branch_suggestions_response"),
|
||||||
payload: z.object({
|
payload: z.object({
|
||||||
branches: z.array(z.string()),
|
branches: z.array(z.string()),
|
||||||
|
branchDetails: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
committerDate: z.number(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
error: z.string().nullable(),
|
error: z.string().nullable(),
|
||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -765,12 +765,21 @@ const x = 1;
|
|||||||
execSync("git fetch origin", { cwd: repoDir });
|
execSync("git fetch origin", { cwd: repoDir });
|
||||||
|
|
||||||
const branches = await listBranchSuggestions(repoDir, { limit: 50 });
|
const branches = await listBranchSuggestions(repoDir, { limit: 50 });
|
||||||
expect(branches).toContain("main");
|
const branchNames = branches.map((branch) => branch.name);
|
||||||
expect(branches).toContain("local-only");
|
expect(branchNames).toContain("main");
|
||||||
expect(branches).toContain("remote-only");
|
expect(branchNames).toContain("local-only");
|
||||||
expect(branches.filter((name) => name === "main")).toHaveLength(1);
|
expect(branchNames).toContain("remote-only");
|
||||||
expect(branches).not.toContain("HEAD");
|
expect(branchNames.filter((name) => name === "main")).toHaveLength(1);
|
||||||
expect(branches.some((name) => name.startsWith("origin/"))).toBe(false);
|
expect(branchNames).not.toContain("HEAD");
|
||||||
|
expect(branchNames.some((name) => name.startsWith("origin/"))).toBe(false);
|
||||||
|
expect(branches).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "main",
|
||||||
|
committerDate: expect.any(Number),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("filters branch suggestions by query and enforces result limit", async () => {
|
it("filters branch suggestions by query and enforces result limit", async () => {
|
||||||
@@ -786,7 +795,8 @@ const x = 1;
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
});
|
});
|
||||||
expect(branches).toHaveLength(1);
|
expect(branches).toHaveLength(1);
|
||||||
expect(branches[0]?.toLowerCase()).toContain("feature/");
|
expect(branches[0]?.name.toLowerCase()).toContain("feature/");
|
||||||
|
expect(branches[0]?.committerDate).toEqual(expect.any(Number));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables GitHub features when gh is unavailable", async () => {
|
it("disables GitHub features when gh is unavailable", async () => {
|
||||||
@@ -816,6 +826,7 @@ const x = 1;
|
|||||||
state: "OPEN",
|
state: "OPEN",
|
||||||
body: "issue body",
|
body: "issue body",
|
||||||
labels: ["bug"],
|
labels: ["bug"],
|
||||||
|
updatedAt: "2026-04-18T12:00:00Z",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -832,6 +843,7 @@ const x = 1;
|
|||||||
baseRefName: "main",
|
baseRefName: "main",
|
||||||
headRefName: "feature",
|
headRefName: "feature",
|
||||||
labels: ["enhancement"],
|
labels: ["enhancement"],
|
||||||
|
updatedAt: "2026-04-18T13:00:00Z",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -853,6 +865,7 @@ const x = 1;
|
|||||||
labels: ["bug"],
|
labels: ["bug"],
|
||||||
baseRefName: null,
|
baseRefName: null,
|
||||||
headRefName: null,
|
headRefName: null,
|
||||||
|
updatedAt: "2026-04-18T12:00:00Z",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "pr",
|
kind: "pr",
|
||||||
@@ -864,6 +877,7 @@ const x = 1;
|
|||||||
labels: ["enhancement"],
|
labels: ["enhancement"],
|
||||||
baseRefName: "main",
|
baseRefName: "main",
|
||||||
headRefName: "feature",
|
headRefName: "feature",
|
||||||
|
updatedAt: "2026-04-18T13:00:00Z",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -890,6 +904,7 @@ const x = 1;
|
|||||||
baseRefName: "main",
|
baseRefName: "main",
|
||||||
headRefName: "feature",
|
headRefName: "feature",
|
||||||
labels: ["enhancement"],
|
labels: ["enhancement"],
|
||||||
|
updatedAt: "2026-04-18T13:00:00Z",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -913,6 +928,7 @@ const x = 1;
|
|||||||
labels: ["enhancement"],
|
labels: ["enhancement"],
|
||||||
baseRefName: "main",
|
baseRefName: "main",
|
||||||
headRefName: "feature",
|
headRefName: "feature",
|
||||||
|
updatedAt: "2026-04-18T13:00:00Z",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -128,6 +128,11 @@ interface GitRef {
|
|||||||
committerDate: number;
|
committerDate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BranchSuggestion {
|
||||||
|
name: string;
|
||||||
|
committerDate: number;
|
||||||
|
}
|
||||||
|
|
||||||
async function listGitRefs(cwd: string, refPrefix: string): Promise<GitRef[]> {
|
async function listGitRefs(cwd: string, refPrefix: string): Promise<GitRef[]> {
|
||||||
const { stdout } = await runGitCommand(
|
const { stdout } = await runGitCommand(
|
||||||
[
|
[
|
||||||
@@ -181,7 +186,7 @@ function sortBranchSuggestions(
|
|||||||
export async function listBranchSuggestions(
|
export async function listBranchSuggestions(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
options?: { query?: string; limit?: number },
|
options?: { query?: string; limit?: number },
|
||||||
): Promise<string[]> {
|
): Promise<BranchSuggestion[]> {
|
||||||
await requireGitRepo(cwd);
|
await requireGitRepo(cwd);
|
||||||
|
|
||||||
const requestedLimit = options?.limit ?? 50;
|
const requestedLimit = options?.limit ?? 50;
|
||||||
@@ -227,7 +232,10 @@ export async function listBranchSuggestions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ordered = sortBranchSuggestions(filteredNames, branchMeta, query);
|
const ordered = sortBranchSuggestions(filteredNames, branchMeta, query);
|
||||||
return ordered.slice(0, limit);
|
return ordered.slice(0, limit).map((name) => ({
|
||||||
|
name,
|
||||||
|
committerDate: branchMeta.get(name)?.committerDate ?? 0,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listCheckoutFileChanges(
|
async function listCheckoutFileChanges(
|
||||||
@@ -1936,6 +1944,7 @@ export interface GitHubSearchResult {
|
|||||||
labels: string[];
|
labels: string[];
|
||||||
baseRefName?: string | null;
|
baseRefName?: string | null;
|
||||||
headRefName?: string | null;
|
headRefName?: string | null;
|
||||||
|
updatedAt?: string;
|
||||||
}>;
|
}>;
|
||||||
githubFeaturesEnabled: boolean;
|
githubFeaturesEnabled: boolean;
|
||||||
}
|
}
|
||||||
@@ -1997,6 +2006,7 @@ export async function searchGitHubIssuesAndPrs(
|
|||||||
labels: item.labels,
|
labels: item.labels,
|
||||||
baseRefName: null,
|
baseRefName: null,
|
||||||
headRefName: null,
|
headRefName: null,
|
||||||
|
updatedAt: item.updatedAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2013,6 +2023,7 @@ export async function searchGitHubIssuesAndPrs(
|
|||||||
labels: item.labels,
|
labels: item.labels,
|
||||||
baseRefName: item.baseRefName,
|
baseRefName: item.baseRefName,
|
||||||
headRefName: item.headRefName,
|
headRefName: item.headRefName,
|
||||||
|
updatedAt: item.updatedAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user