Edit agent role, seniority, and model from the roster
This commit is contained in:
1 parent
452fe07f4e
commit
c3c00d5d2e
7 files changed
+103
-16
No files matched your search
@@ -86,6 +86,7 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
|||||||
ipcMain.handle(IPC_CHANNELS.setActiveOrganization, (_event, input) => mutate(() => organization.setActiveOrganization(input.orgId)));
|
ipcMain.handle(IPC_CHANNELS.setActiveOrganization, (_event, input) => mutate(() => organization.setActiveOrganization(input.orgId)));
|
||||||
ipcMain.handle(IPC_CHANNELS.deleteOrganization, (_event, input) => mutate(() => organization.deleteOrganization(input.orgId)));
|
ipcMain.handle(IPC_CHANNELS.deleteOrganization, (_event, input) => mutate(() => organization.deleteOrganization(input.orgId)));
|
||||||
ipcMain.handle(IPC_CHANNELS.createAgent, (_event, input) => mutate(() => organization.createAgent(input)));
|
ipcMain.handle(IPC_CHANNELS.createAgent, (_event, input) => mutate(() => organization.createAgent(input)));
|
||||||
|
ipcMain.handle(IPC_CHANNELS.updateAgent, (_event, input) => mutate(() => organization.updateAgent(input)));
|
||||||
ipcMain.handle(IPC_CHANNELS.createTask, (_event, input) => mutate(() => organization.createTask(input)));
|
ipcMain.handle(IPC_CHANNELS.createTask, (_event, input) => mutate(() => organization.createTask(input)));
|
||||||
ipcMain.handle(IPC_CHANNELS.updateTaskStatus, (_event, input) => mutate(() => organization.updateTaskStatus(input.orgId, input.taskId, input.status)));
|
ipcMain.handle(IPC_CHANNELS.updateTaskStatus, (_event, input) => mutate(() => organization.updateTaskStatus(input.orgId, input.taskId, input.status)));
|
||||||
ipcMain.handle(IPC_CHANNELS.runTask, async (_event, input) => {
|
ipcMain.handle(IPC_CHANNELS.runTask, async (_event, input) => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { DatabaseSync } from "node:sqlite";
|
|||||||
import { APPROVAL_STATE, RISK_LEVEL, TASK_STATUS } from "../../shared/domain";
|
import { APPROVAL_STATE, RISK_LEVEL, TASK_STATUS } from "../../shared/domain";
|
||||||
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
|
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
|
||||||
import { createId } from "../../shared/ids";
|
import { createId } from "../../shared/ids";
|
||||||
import type { CreateAgentInput, CreateOrganizationInput, CreateTaskInput } from "../../shared/commands";
|
import type { CreateAgentInput, CreateOrganizationInput, CreateTaskInput, UpdateAgentInput } from "../../shared/commands";
|
||||||
import { AuditService } from "./audit-service";
|
import { AuditService } from "./audit-service";
|
||||||
import { PolicyService } from "./policy-service";
|
import { PolicyService } from "./policy-service";
|
||||||
import { IdentityAccessService } from "./identity-access-service";
|
import { IdentityAccessService } from "./identity-access-service";
|
||||||
@@ -213,6 +213,34 @@ export class OrganizationService {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateAgent(input: UpdateAgentInput): void {
|
||||||
|
const agent = this.db.prepare("select id from agents where id = ? and org_id = ?")
|
||||||
|
.get(input.agentId, input.orgId) as { id: string } | undefined;
|
||||||
|
if (!agent) throw new Error("Agent was not found.");
|
||||||
|
const role = input.role.trim();
|
||||||
|
if (!role) throw new Error("Agent role is required.");
|
||||||
|
if (input.modelProviderId) {
|
||||||
|
const provider = this.db.prepare("select id from providers where id = ? and org_id = ?")
|
||||||
|
.get(input.modelProviderId, input.orgId) as { id: string } | undefined;
|
||||||
|
if (!provider) throw new Error("Agent provider was not found.");
|
||||||
|
}
|
||||||
|
const result = this.db.prepare(`
|
||||||
|
update agents
|
||||||
|
set role = ?, title = ?, role_level = ?, model_provider_id = ?, model_id = ?
|
||||||
|
where id = ? and org_id = ?
|
||||||
|
`).run(role, input.title.trim() || role, input.roleLevel.trim() || "staff", input.modelProviderId, input.modelId, input.agentId, input.orgId);
|
||||||
|
if (result.changes === 0) throw new Error("Agent update failed.");
|
||||||
|
this.audit.record({
|
||||||
|
orgId: input.orgId,
|
||||||
|
entityType: "agent",
|
||||||
|
entityId: input.agentId,
|
||||||
|
actorType: "employee",
|
||||||
|
actorId: null,
|
||||||
|
action: "updated",
|
||||||
|
payload: { role, roleLevel: input.roleLevel.trim() || "staff" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
createTask(input: CreateTaskInput): string {
|
createTask(input: CreateTaskInput): string {
|
||||||
const id = createId("tsk");
|
const id = createId("tsk");
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const api: DesktopApi = {
|
|||||||
setActiveOrganization: (input) => invoke(IPC_CHANNELS.setActiveOrganization, input),
|
setActiveOrganization: (input) => invoke(IPC_CHANNELS.setActiveOrganization, input),
|
||||||
deleteOrganization: (input) => invoke(IPC_CHANNELS.deleteOrganization, input),
|
deleteOrganization: (input) => invoke(IPC_CHANNELS.deleteOrganization, input),
|
||||||
createAgent: (input) => invoke(IPC_CHANNELS.createAgent, input),
|
createAgent: (input) => invoke(IPC_CHANNELS.createAgent, input),
|
||||||
|
updateAgent: (input) => invoke(IPC_CHANNELS.updateAgent, input),
|
||||||
createTask: (input) => invoke(IPC_CHANNELS.createTask, input),
|
createTask: (input) => invoke(IPC_CHANNELS.createTask, input),
|
||||||
updateTaskStatus: (input) => invoke(IPC_CHANNELS.updateTaskStatus, input),
|
updateTaskStatus: (input) => invoke(IPC_CHANNELS.updateTaskStatus, input),
|
||||||
runTask: (input) => invoke(IPC_CHANNELS.runTask, input),
|
runTask: (input) => invoke(IPC_CHANNELS.runTask, input),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ function optional(form: FormData, key: string): string | null {
|
|||||||
export function TeamPage({ snapshot, actions }: PageProps) {
|
export function TeamPage({ snapshot, actions }: PageProps) {
|
||||||
const orgId = activeOrgId(snapshot);
|
const orgId = activeOrgId(snapshot);
|
||||||
const [hireOpen, setHireOpen] = useState(false);
|
const [hireOpen, setHireOpen] = useState(false);
|
||||||
|
const [editAgentId, setEditAgentId] = useState<string | null>(null);
|
||||||
const [filter, setFilter] = useState<"all" | "human" | "agent">("all");
|
const [filter, setFilter] = useState<"all" | "human" | "agent">("all");
|
||||||
|
|
||||||
const employees = useMemo(() => snapshot.employees.filter((item) => item.orgId === orgId), [snapshot.employees, orgId]);
|
const employees = useMemo(() => snapshot.employees.filter((item) => item.orgId === orgId), [snapshot.employees, orgId]);
|
||||||
@@ -46,6 +47,23 @@ export function TeamPage({ snapshot, actions }: PageProps) {
|
|||||||
})).then((result) => { if (result) setHireOpen(false); });
|
})).then((result) => { if (result) setHireOpen(false); });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const editingAgent = editAgentId ? agents.find((agent) => agent.id === editAgentId) ?? null : null;
|
||||||
|
|
||||||
|
const submitEdit = (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!editingAgent) return;
|
||||||
|
const form = new FormData(event.currentTarget);
|
||||||
|
void actions.run(() => window.agentCompany.updateAgent({
|
||||||
|
orgId,
|
||||||
|
agentId: editingAgent.id,
|
||||||
|
role: text(form, "role"),
|
||||||
|
title: text(form, "title") || text(form, "role"),
|
||||||
|
roleLevel: text(form, "roleLevel") || "staff",
|
||||||
|
modelProviderId: optional(form, "modelProviderId"),
|
||||||
|
modelId: optional(form, "modelId"),
|
||||||
|
})).then((result) => { if (result) setEditAgentId(null); });
|
||||||
|
};
|
||||||
|
|
||||||
const rosterRows = buildRoster(employees, agents, providers, filter);
|
const rosterRows = buildRoster(employees, agents, providers, filter);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -95,7 +113,12 @@ export function TeamPage({ snapshot, actions }: PageProps) {
|
|||||||
<div>Member</div><div>Department</div><div>Model</div><div>Budget</div><div />
|
<div>Member</div><div>Department</div><div>Model</div><div>Budget</div><div />
|
||||||
</div>
|
</div>
|
||||||
{rosterRows.map((row, index) => (
|
{rosterRows.map((row, index) => (
|
||||||
<RosterRow key={row.id} row={row} last={index === rosterRows.length - 1} />
|
<RosterRow
|
||||||
|
key={row.id}
|
||||||
|
row={row}
|
||||||
|
last={index === rosterRows.length - 1}
|
||||||
|
onEdit={row.human ? null : () => setEditAgentId(row.id)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -111,19 +134,8 @@ export function TeamPage({ snapshot, actions }: PageProps) {
|
|||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Model">
|
<Field label="Model">
|
||||||
<input
|
<input className="input" name="modelId" list="agent-model-suggestions" />
|
||||||
className="input"
|
|
||||||
name="modelId"
|
|
||||||
list="agent-model-suggestions"
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
<datalist id="agent-model-suggestions">
|
|
||||||
{snapshot.providerCatalog.flatMap((entry) =>
|
|
||||||
entry.models.map((model) => (
|
|
||||||
<option key={`${entry.key}-${model.id}`} value={model.id}>{entry.label} · {model.name}</option>
|
|
||||||
)),
|
|
||||||
)}
|
|
||||||
</datalist>
|
|
||||||
<Field label="Monthly budget"><input className="input" name="budget" type="number" min="0" step="1" defaultValue="250" /></Field>
|
<Field label="Monthly budget"><input className="input" name="budget" type="number" min="0" step="1" defaultValue="250" /></Field>
|
||||||
<Field label="Seniority">
|
<Field label="Seniority">
|
||||||
<select className="input" name="roleLevel" defaultValue="staff">
|
<select className="input" name="roleLevel" defaultValue="staff">
|
||||||
@@ -134,6 +146,38 @@ export function TeamPage({ snapshot, actions }: PageProps) {
|
|||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{editingAgent && (
|
||||||
|
<Dialog key={editingAgent.id} title={`Edit ${editingAgent.name}`} open submitLabel="Save" onClose={() => setEditAgentId(null)} onSubmit={submitEdit}>
|
||||||
|
<Field label="Role"><input className="input" name="role" defaultValue={editingAgent.role} required /></Field>
|
||||||
|
<Field label="Title"><input className="input" name="title" defaultValue={editingAgent.title} /></Field>
|
||||||
|
<Field label="Provider">
|
||||||
|
<select className="input" name="modelProviderId" defaultValue={editingAgent.modelProviderId ?? ""}>
|
||||||
|
<option value="">Unassigned</option>
|
||||||
|
{providers.map((provider) => <option key={provider.id} value={provider.id}>{provider.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
<Field label="Model">
|
||||||
|
<input className="input" name="modelId" defaultValue={editingAgent.modelId ?? ""} list="agent-model-suggestions" />
|
||||||
|
</Field>
|
||||||
|
<Field label="Seniority">
|
||||||
|
<select className="input" name="roleLevel" defaultValue={editingAgent.roleLevel}>
|
||||||
|
<option value="staff">Staff</option>
|
||||||
|
<option value="lead">Lead</option>
|
||||||
|
<option value="director">Director</option>
|
||||||
|
<option value="c_suite">Executive</option>
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<datalist id="agent-model-suggestions">
|
||||||
|
{snapshot.providerCatalog.flatMap((entry) =>
|
||||||
|
entry.models.map((model) => (
|
||||||
|
<option key={`${entry.key}-${model.id}`} value={model.id}>{entry.label} · {model.name}</option>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -244,7 +288,7 @@ function buildRoster(
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RosterRow({ row, last }: { row: RosterEntry; last: boolean }) {
|
function RosterRow({ row, last, onEdit }: { row: RosterEntry; last: boolean; onEdit: (() => void) | null }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="table-row"
|
className="table-row"
|
||||||
@@ -289,7 +333,7 @@ function RosterRow({ row, last }: { row: RosterEntry; last: boolean }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: "right" }}>
|
<div style={{ textAlign: "right" }}>
|
||||||
<IconButton icon="more" label="More" />
|
{onEdit && <IconButton icon="edit" label={`Edit ${row.name}`} onClick={onEdit} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,6 +36,16 @@ export interface CreateOrganizationInput {
|
|||||||
reviewDeliverables?: boolean;
|
reviewDeliverables?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateAgentInput {
|
||||||
|
orgId: string;
|
||||||
|
agentId: string;
|
||||||
|
role: string;
|
||||||
|
title: string;
|
||||||
|
roleLevel: string;
|
||||||
|
modelProviderId: string | null;
|
||||||
|
modelId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateAgentInput {
|
export interface CreateAgentInput {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
BootstrapOnboardingSummary,
|
BootstrapOnboardingSummary,
|
||||||
CancelProviderResponseInput,
|
CancelProviderResponseInput,
|
||||||
CreateAgentInput,
|
CreateAgentInput,
|
||||||
|
UpdateAgentInput,
|
||||||
CreateApprovalInput,
|
CreateApprovalInput,
|
||||||
CreateAuditReplayInput,
|
CreateAuditReplayInput,
|
||||||
CreateChatSpaceInput,
|
CreateChatSpaceInput,
|
||||||
@@ -68,6 +69,7 @@ export interface DesktopApi {
|
|||||||
deleteOrganization(input: DeleteOrganizationInput): Promise<DesktopResult<AppSnapshot>>;
|
deleteOrganization(input: DeleteOrganizationInput): Promise<DesktopResult<AppSnapshot>>;
|
||||||
createOrganization(input: CreateOrganizationInput): Promise<DesktopResult<AppSnapshot>>;
|
createOrganization(input: CreateOrganizationInput): Promise<DesktopResult<AppSnapshot>>;
|
||||||
createAgent(input: CreateAgentInput): Promise<DesktopResult<AppSnapshot>>;
|
createAgent(input: CreateAgentInput): Promise<DesktopResult<AppSnapshot>>;
|
||||||
|
updateAgent(input: UpdateAgentInput): Promise<DesktopResult<AppSnapshot>>;
|
||||||
createTask(input: CreateTaskInput): Promise<DesktopResult<AppSnapshot>>;
|
createTask(input: CreateTaskInput): Promise<DesktopResult<AppSnapshot>>;
|
||||||
updateTaskStatus(input: UpdateTaskStatusInput): Promise<DesktopResult<AppSnapshot>>;
|
updateTaskStatus(input: UpdateTaskStatusInput): Promise<DesktopResult<AppSnapshot>>;
|
||||||
runTask(input: RunTaskInput): Promise<DesktopResult<AppSnapshot>>;
|
runTask(input: RunTaskInput): Promise<DesktopResult<AppSnapshot>>;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const IPC_CHANNELS = {
|
|||||||
deleteOrganization: "agentcompany:delete-organization",
|
deleteOrganization: "agentcompany:delete-organization",
|
||||||
createOrganization: "agentcompany:create-organization",
|
createOrganization: "agentcompany:create-organization",
|
||||||
createAgent: "agentcompany:create-agent",
|
createAgent: "agentcompany:create-agent",
|
||||||
|
updateAgent: "agentcompany:update-agent",
|
||||||
createTask: "agentcompany:create-task",
|
createTask: "agentcompany:create-task",
|
||||||
updateTaskStatus: "agentcompany:update-task-status",
|
updateTaskStatus: "agentcompany:update-task-status",
|
||||||
runTask: "agentcompany:run-task",
|
runTask: "agentcompany:run-task",
|
||||||
|
|||||||
Reference in new issue
Block a user