Skip to content

Commit d7f5279

Browse files
feat: add multi-repo UI for task creation and sidebar badge
- TaskInput: "Add repository" button appears when a primary repo is selected. Each additional repo gets its own folder picker, mode selector, and branch selector. Repos can be removed individually. - AdditionalRepoRow: new component for per-repo configuration rows - TaskItem: renders "+N" badge with tooltip listing additional repos when a task has multiple repositories - useTaskCreation: passes additionalRepos through to TaskCreationSaga - TaskCreationSaga: resolves folders for additional repos and passes them as additionalRepos to workspace.create mutation Generated-By: PostHog Code Task-Id: 230ec8e6-6781-43b4-ae30-ea24faa410dc
1 parent bc1cd27 commit d7f5279

6 files changed

Lines changed: 204 additions & 1 deletion

File tree

apps/code/src/renderer/features/sidebar/components/TaskListView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ function TaskRow({
107107
isPinned={task.isPinned}
108108
needsPermission={task.needsPermission}
109109
taskRunStatus={task.taskRunStatus}
110+
additionalRepositories={task.additionalRepositories}
110111
timestamp={timestamp}
111112
onClick={onClick}
112113
onDoubleClick={onDoubleClick}

apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ import { selectIsFocusedOnWorktree, useFocusStore } from "@stores/focusStore";
1515
import { useCallback, useEffect, useRef, useState } from "react";
1616
import { SidebarItem } from "../SidebarItem";
1717

18+
interface AdditionalRepo {
19+
fullPath: string;
20+
name: string;
21+
}
22+
1823
interface TaskItemProps {
1924
depth?: number;
2025
taskId: string;
@@ -35,6 +40,8 @@ interface TaskItemProps {
3540
| "cancelled";
3641
timestamp?: number;
3742
isEditing?: boolean;
43+
/** Additional repos for multi-repo tasks (renders +N badge). */
44+
additionalRepositories?: AdditionalRepo[];
3845
onClick: () => void;
3946
onDoubleClick?: () => void;
4047
onContextMenu: (e: React.MouseEvent) => void;
@@ -186,6 +193,7 @@ export function TaskItem({
186193
taskRunStatus,
187194
timestamp,
188195
isEditing = false,
196+
additionalRepositories,
189197
onClick,
190198
onDoubleClick,
191199
onContextMenu,
@@ -254,6 +262,18 @@ export function TaskItem({
254262
</span>
255263
) : null;
256264

265+
const multiRepoBadge =
266+
additionalRepositories && additionalRepositories.length > 0 ? (
267+
<Tooltip
268+
content={`Also includes: ${additionalRepositories.map((r) => r.name).join(", ")}`}
269+
side="right"
270+
>
271+
<span className="shrink-0 rounded-sm bg-gray-4 px-1 text-[10px] text-gray-11 group-hover:hidden">
272+
+{additionalRepositories.length}
273+
</span>
274+
</Tooltip>
275+
) : null;
276+
257277
const toolbar =
258278
onArchive || onTogglePin ? (
259279
<TaskHoverToolbar
@@ -264,8 +284,9 @@ export function TaskItem({
264284
) : null;
265285

266286
const endContent =
267-
timestampNode || toolbar ? (
287+
timestampNode || multiRepoBadge || toolbar ? (
268288
<>
289+
{multiRepoBadge}
269290
{timestampNode}
270291
{toolbar}
271292
</>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { FolderPicker } from "@features/folder-picker/components/FolderPicker";
2+
import { BranchSelector } from "@features/git-interaction/components/BranchSelector";
3+
import { useGitQueries } from "@features/git-interaction/hooks/useGitQueries";
4+
import { X } from "@phosphor-icons/react";
5+
import { Flex } from "@radix-ui/themes";
6+
import { type WorkspaceMode, WorkspaceModeSelect } from "./WorkspaceModeSelect";
7+
8+
export interface AdditionalRepoConfig {
9+
id: string;
10+
directory: string;
11+
mode: WorkspaceMode;
12+
branch: string | null;
13+
}
14+
15+
interface AdditionalRepoRowProps {
16+
config: AdditionalRepoConfig;
17+
onChange: (config: AdditionalRepoConfig) => void;
18+
onRemove: () => void;
19+
disabled?: boolean;
20+
}
21+
22+
export function AdditionalRepoRow({
23+
config,
24+
onChange,
25+
onRemove,
26+
disabled,
27+
}: AdditionalRepoRowProps) {
28+
const { currentBranch, branchLoading, defaultBranch } = useGitQueries(
29+
config.directory,
30+
);
31+
32+
return (
33+
<Flex gap="2" align="center" style={{ minWidth: 0, overflow: "hidden" }}>
34+
<FolderPicker
35+
value={config.directory}
36+
onChange={(dir) =>
37+
onChange({ ...config, directory: dir, branch: null })
38+
}
39+
placeholder="Add repository…"
40+
size="1"
41+
/>
42+
<WorkspaceModeSelect
43+
value={config.mode}
44+
onChange={(mode) => onChange({ ...config, mode })}
45+
size="1"
46+
overrideModes={["worktree", "local"]}
47+
/>
48+
{config.directory && (
49+
<BranchSelector
50+
repoPath={config.directory}
51+
currentBranch={currentBranch}
52+
defaultBranch={defaultBranch}
53+
disabled={disabled || !config.directory}
54+
loading={branchLoading}
55+
workspaceMode={config.mode}
56+
selectedBranch={config.branch}
57+
onBranchSelect={(branch) => onChange({ ...config, branch })}
58+
/>
59+
)}
60+
<button
61+
type="button"
62+
onClick={onRemove}
63+
className="flex h-6 w-6 shrink-0 items-center justify-center rounded text-gray-10 transition-colors hover:bg-gray-4 hover:text-gray-12"
64+
title="Remove repository"
65+
>
66+
<X size={12} />
67+
</button>
68+
</Flex>
69+
);
70+
}

apps/code/src/renderer/features/task-detail/components/TaskInput.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3333
import { useHotkeys } from "react-hotkeys-hook";
3434
import { usePreviewConfig } from "../hooks/usePreviewConfig";
3535
import { useTaskCreation } from "../hooks/useTaskCreation";
36+
import {
37+
type AdditionalRepoConfig,
38+
AdditionalRepoRow,
39+
} from "./AdditionalRepoRow";
3640
import { TaskInputEditor } from "./TaskInputEditor";
3741
import { type WorkspaceMode, WorkspaceModeSelect } from "./WorkspaceModeSelect";
3842

@@ -84,6 +88,9 @@ export function TaskInput({
8488
);
8589

8690
const [selectedDirectory, setSelectedDirectory] = useState("");
91+
const [additionalRepos, setAdditionalRepos] = useState<
92+
AdditionalRepoConfig[]
93+
>([]);
8794
const workspaceMode = lastUsedWorkspaceMode || "local";
8895
const adapter = lastUsedAdapter;
8996

@@ -275,6 +282,7 @@ export function TaskInput({
275282
effectiveWorkspaceMode === "cloud" && selectedCloudEnvId
276283
? selectedCloudEnvId
277284
: undefined,
285+
additionalRepos: additionalRepos.length > 0 ? additionalRepos : undefined,
278286
});
279287

280288
const handleCycleMode = useCallback(() => {
@@ -485,6 +493,44 @@ export function TaskInput({
485493
)}
486494
</Flex>
487495

496+
{additionalRepos.map((repo) => (
497+
<AdditionalRepoRow
498+
key={repo.id}
499+
config={repo}
500+
onChange={(updated) =>
501+
setAdditionalRepos((prev) =>
502+
prev.map((r) => (r.id === repo.id ? updated : r)),
503+
)
504+
}
505+
onRemove={() =>
506+
setAdditionalRepos((prev) =>
507+
prev.filter((r) => r.id !== repo.id),
508+
)
509+
}
510+
disabled={isCreatingTask}
511+
/>
512+
))}
513+
514+
{workspaceMode !== "cloud" && selectedDirectory && (
515+
<button
516+
type="button"
517+
onClick={() =>
518+
setAdditionalRepos((prev) => [
519+
...prev,
520+
{
521+
id: crypto.randomUUID(),
522+
directory: "",
523+
mode: workspaceMode,
524+
branch: null,
525+
},
526+
])
527+
}
528+
className="self-start rounded px-2 py-0.5 text-[12px] text-gray-10 transition-colors hover:bg-gray-3 hover:text-gray-12"
529+
>
530+
+ Add repository
531+
</button>
532+
)}
533+
488534
<TaskInputEditor
489535
ref={editorRef}
490536
sessionId={sessionId}

apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ import type { TaskCreationInput, TaskService } from "../service/service";
2020

2121
const log = logger.scope("task-creation");
2222

23+
interface AdditionalRepoOption {
24+
directory: string;
25+
mode: WorkspaceMode;
26+
branch: string | null;
27+
}
28+
2329
interface UseTaskCreationOptions {
2430
editorRef: React.RefObject<MessageEditorHandle | null>;
2531
selectedDirectory: string;
@@ -34,6 +40,7 @@ interface UseTaskCreationOptions {
3440
reasoningLevel?: string;
3541
environmentId?: string | null;
3642
sandboxEnvironmentId?: string;
43+
additionalRepos?: AdditionalRepoOption[];
3744
onTaskCreated?: (task: Task) => void;
3845
}
3946

@@ -57,8 +64,18 @@ function prepareTaskInput(
5764
reasoningLevel?: string;
5865
environmentId?: string | null;
5966
sandboxEnvironmentId?: string;
67+
additionalRepos?: AdditionalRepoOption[];
6068
},
6169
): TaskCreationInput {
70+
const validAdditionalRepos = options.additionalRepos
71+
?.filter((r) => r.directory)
72+
.map((r) => ({
73+
repoPath: r.directory,
74+
mode: r.mode,
75+
branch: r.branch,
76+
label: r.directory.split("/").pop(),
77+
}));
78+
6279
return {
6380
content: contentToXml(content).trim(),
6481
filePaths: extractFilePaths(content),
@@ -73,6 +90,10 @@ function prepareTaskInput(
7390
reasoningLevel: options.reasoningLevel,
7491
environmentId: options.environmentId ?? undefined,
7592
sandboxEnvironmentId: options.sandboxEnvironmentId,
93+
additionalRepos:
94+
validAdditionalRepos && validAdditionalRepos.length > 0
95+
? validAdditionalRepos
96+
: undefined,
7697
};
7798
}
7899

@@ -101,6 +122,7 @@ export function useTaskCreation({
101122
reasoningLevel,
102123
environmentId,
103124
sandboxEnvironmentId,
125+
additionalRepos,
104126
onTaskCreated,
105127
}: UseTaskCreationOptions): UseTaskCreationReturn {
106128
const [isCreatingTask, setIsCreatingTask] = useState(false);
@@ -149,6 +171,7 @@ export function useTaskCreation({
149171
reasoningLevel,
150172
environmentId,
151173
sandboxEnvironmentId,
174+
additionalRepos,
152175
});
153176

154177
if (executionMode) {
@@ -197,6 +220,7 @@ export function useTaskCreation({
197220
reasoningLevel,
198221
environmentId,
199222
sandboxEnvironmentId,
223+
additionalRepos,
200224
invalidateTasks,
201225
navigateToTask,
202226
onTaskCreated,

apps/code/src/renderer/sagas/task/task-creation.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ const sagaLogger: SagaLogger = {
5555
warn: (message, data) => log.warn(message, data),
5656
};
5757

58+
export interface AdditionalRepoInput {
59+
repoPath: string;
60+
mode: WorkspaceMode;
61+
branch?: string | null;
62+
label?: string;
63+
}
64+
5865
export interface TaskCreationInput {
5966
// For opening existing task
6067
taskId?: string;
@@ -73,6 +80,8 @@ export interface TaskCreationInput {
7380
environmentId?: string;
7481
sandboxEnvironmentId?: string;
7582
signalReportId?: string;
83+
/** Additional repos for multi-repo tasks. */
84+
additionalRepos?: AdditionalRepoInput[];
7685
}
7786

7887
export interface TaskCreationOutput {
@@ -160,6 +169,34 @@ export class TaskCreationSaga extends Saga<
160169
this.resolveFolder(repoPath),
161170
);
162171

172+
// Resolve additional repo folders in parallel
173+
let additionalRepoConfigs:
174+
| {
175+
mainRepoPath: string;
176+
folderId: string;
177+
folderPath: string;
178+
mode: WorkspaceMode;
179+
branch?: string;
180+
label?: string;
181+
}[]
182+
| undefined;
183+
if (input.additionalRepos && input.additionalRepos.length > 0) {
184+
const resolvedFolders = await Promise.all(
185+
input.additionalRepos.map(async (repo) => ({
186+
folder: await this.resolveFolder(repo.repoPath),
187+
repo,
188+
})),
189+
);
190+
additionalRepoConfigs = resolvedFolders.map(({ folder: f, repo }) => ({
191+
mainRepoPath: repo.repoPath,
192+
folderId: f.id,
193+
folderPath: repo.repoPath,
194+
mode: (repo.mode ?? workspaceMode) as WorkspaceMode,
195+
branch: repo.branch ?? undefined,
196+
label: repo.label ?? repo.repoPath.split("/").pop(),
197+
}));
198+
}
199+
163200
const workspaceInfos = await this.step({
164201
name: "workspace_creation",
165202
execute: async () => {
@@ -170,6 +207,10 @@ export class TaskCreationSaga extends Saga<
170207
folderPath: repoPath,
171208
mode: workspaceMode,
172209
branch: branch ?? undefined,
210+
label: additionalRepoConfigs
211+
? repoPath.split("/").pop()
212+
: undefined,
213+
additionalRepos: additionalRepoConfigs,
173214
});
174215
},
175216
rollback: async () => {

0 commit comments

Comments
 (0)