mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 21:03:46 +00:00
feat(foundry): add manual task owner change via UI dropdown
Add an owner dropdown to the Overview tab that lets users reassign task ownership to any organization member. The owner's GitHub credentials are used for git operations in the sandbox. Full-stack implementation: - Backend: changeTaskOwnerManually action on task actor, routed through org actor's changeWorkspaceTaskOwner action, with primaryUser schema columns on both task and org index tables - Client: changeOwner method on workspace client (mock + remote) - Frontend: owner dropdown in right sidebar Overview tab showing org members, with avatar and role display - Shared: TaskWorkspaceChangeOwnerInput type and primaryUser fields on workspace snapshot types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b1b785ae79
commit
3684e2e5f5
18 changed files with 647 additions and 11 deletions
|
|
@ -12,6 +12,7 @@ import type {
|
|||
TaskRecord,
|
||||
TaskSummary,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceChangeOwnerInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceCreateTaskResponse,
|
||||
TaskWorkspaceDiffInput,
|
||||
|
|
@ -110,6 +111,7 @@ interface OrganizationHandle {
|
|||
stopWorkspaceSession(input: TaskWorkspaceSessionInput & AuthSessionScopedInput): Promise<void>;
|
||||
closeWorkspaceSession(input: TaskWorkspaceSessionInput & AuthSessionScopedInput): Promise<void>;
|
||||
publishWorkspacePr(input: TaskWorkspaceSelectInput & AuthSessionScopedInput): Promise<void>;
|
||||
changeWorkspaceTaskOwner(input: TaskWorkspaceChangeOwnerInput & AuthSessionScopedInput): Promise<void>;
|
||||
revertWorkspaceFile(input: TaskWorkspaceDiffInput & AuthSessionScopedInput): Promise<void>;
|
||||
adminReloadGithubOrganization(): Promise<void>;
|
||||
adminReloadGithubRepository(input: { repoId: string }): Promise<void>;
|
||||
|
|
@ -304,6 +306,7 @@ export interface BackendClient {
|
|||
stopWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
closeWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
publishWorkspacePr(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
changeWorkspaceTaskOwner(organizationId: string, input: TaskWorkspaceChangeOwnerInput): Promise<void>;
|
||||
revertWorkspaceFile(organizationId: string, input: TaskWorkspaceDiffInput): Promise<void>;
|
||||
adminReloadGithubOrganization(organizationId: string): Promise<void>;
|
||||
adminReloadGithubRepository(organizationId: string, repoId: string): Promise<void>;
|
||||
|
|
@ -1282,6 +1285,10 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
await (await organization(organizationId)).publishWorkspacePr(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async changeWorkspaceTaskOwner(organizationId: string, input: TaskWorkspaceChangeOwnerInput): Promise<void> {
|
||||
await (await organization(organizationId)).changeWorkspaceTaskOwner(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async revertWorkspaceFile(organizationId: string, input: TaskWorkspaceDiffInput): Promise<void> {
|
||||
await (await organization(organizationId)).revertWorkspaceFile(await withAuthSessionInput(input));
|
||||
},
|
||||
|
|
|
|||
|
|
@ -188,6 +188,8 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
unread: tab.unread,
|
||||
created: tab.created,
|
||||
})),
|
||||
primaryUserLogin: null,
|
||||
primaryUserAvatarUrl: null,
|
||||
});
|
||||
|
||||
const buildTaskDetail = (task: TaskWorkspaceSnapshot["tasks"][number]): WorkspaceTaskDetail => ({
|
||||
|
|
@ -750,6 +752,15 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async changeWorkspaceTaskOwner(
|
||||
_organizationId: string,
|
||||
input: { repoId: string; taskId: string; targetUserId: string; targetUserName: string; targetUserEmail: string },
|
||||
): Promise<void> {
|
||||
await workspace.changeOwner(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
},
|
||||
|
||||
async revertWorkspaceFile(_organizationId: string, input: TaskWorkspaceDiffInput): Promise<void> {
|
||||
await workspace.revertFile(input);
|
||||
emitOrganizationSnapshot();
|
||||
|
|
|
|||
|
|
@ -349,7 +349,10 @@ class MockWorkspaceStore implements TaskWorkspaceClient {
|
|||
|
||||
return {
|
||||
...currentTask,
|
||||
activeSessionId: currentTask.activeSessionId === input.sessionId ? (currentTask.sessions.find((candidate) => candidate.id !== input.sessionId)?.id ?? null) : currentTask.activeSessionId,
|
||||
activeSessionId:
|
||||
currentTask.activeSessionId === input.sessionId
|
||||
? (currentTask.sessions.find((candidate) => candidate.id !== input.sessionId)?.id ?? null)
|
||||
: currentTask.activeSessionId,
|
||||
sessions: currentTask.sessions.filter((candidate) => candidate.id !== input.sessionId),
|
||||
};
|
||||
});
|
||||
|
|
@ -396,6 +399,14 @@ class MockWorkspaceStore implements TaskWorkspaceClient {
|
|||
}));
|
||||
}
|
||||
|
||||
async changeOwner(input: { repoId: string; taskId: string; targetUserId: string; targetUserName: string; targetUserEmail: string }): Promise<void> {
|
||||
this.updateTask(input.taskId, (currentTask) => ({
|
||||
...currentTask,
|
||||
primaryUserLogin: input.targetUserName,
|
||||
primaryUserAvatarUrl: null,
|
||||
}));
|
||||
}
|
||||
|
||||
private updateState(updater: (current: TaskWorkspaceSnapshot) => TaskWorkspaceSnapshot): void {
|
||||
const nextSnapshot = updater(this.snapshot);
|
||||
this.snapshot = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type {
|
||||
TaskWorkspaceAddSessionResponse,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceChangeOwnerInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceCreateTaskResponse,
|
||||
TaskWorkspaceDiffInput,
|
||||
|
|
@ -140,6 +141,11 @@ class RemoteWorkspaceStore implements TaskWorkspaceClient {
|
|||
await this.refresh();
|
||||
}
|
||||
|
||||
async changeOwner(input: TaskWorkspaceChangeOwnerInput): Promise<void> {
|
||||
await this.backend.changeWorkspaceTaskOwner(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
private ensureStarted(): void {
|
||||
if (!this.unsubscribeWorkspace) {
|
||||
this.unsubscribeWorkspace = this.backend.subscribeWorkspace(this.organizationId, () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type {
|
||||
TaskWorkspaceAddSessionResponse,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
TaskWorkspaceChangeOwnerInput,
|
||||
TaskWorkspaceCreateTaskInput,
|
||||
TaskWorkspaceCreateTaskResponse,
|
||||
TaskWorkspaceDiffInput,
|
||||
|
|
@ -43,6 +44,7 @@ export interface TaskWorkspaceClient {
|
|||
closeSession(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
addSession(input: TaskWorkspaceSelectInput): Promise<TaskWorkspaceAddSessionResponse>;
|
||||
changeModel(input: TaskWorkspaceChangeModelInput): Promise<void>;
|
||||
changeOwner(input: TaskWorkspaceChangeOwnerInput): Promise<void>;
|
||||
}
|
||||
|
||||
export function createTaskWorkspaceClient(options: CreateTaskWorkspaceClientOptions): TaskWorkspaceClient {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ function organizationSnapshot(): OrganizationSummarySnapshot {
|
|||
pullRequest: null,
|
||||
activeSessionId: null,
|
||||
sessionsSummary: [],
|
||||
primaryUserLogin: null,
|
||||
primaryUserAvatarUrl: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -159,6 +161,8 @@ describe("RemoteSubscriptionManager", () => {
|
|||
pullRequest: null,
|
||||
activeSessionId: null,
|
||||
sessionsSummary: [],
|
||||
primaryUserLogin: null,
|
||||
primaryUserAvatarUrl: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue