diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8683ea3..7bff666 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -61,6 +61,9 @@ export type { TaskState, TaskStoreDeps, PendingNewTask } from './stores/task-sto export { createRoomStore } from './stores/room-store.js'; export type { RoomState, RoomStoreDeps, PerformerAgent } from './stores/room-store.js'; +export { createTeamStore } from './stores/team-store.js'; +export type { TeamState, TeamStoreDeps } from './stores/team-store.js'; + export { createSystemSessionStore } from './stores/system-session-store.js'; export type { SystemSessionState, SystemSessionMessage, SystemSessionStatus } from './stores/system-session-store.js'; diff --git a/packages/core/src/stores/team-store.ts b/packages/core/src/stores/team-store.ts new file mode 100644 index 0000000..0c00c1d --- /dev/null +++ b/packages/core/src/stores/team-store.ts @@ -0,0 +1,144 @@ +import { createStore } from 'zustand/vanilla'; +import type { Team, TeamAgent, IpcResult } from '@clawwork/shared'; + +export interface TeamStoreDeps { + listTeams: () => Promise>; + getTeam: (id: string) => Promise>; + persistTeam: (team: { + id: string; + name: string; + emoji?: string; + description?: string; + gatewayId: string; + source?: string; + version?: string; + agents: Array<{ agentId: string; role?: string; isManager?: boolean }>; + createdAt: string; + updatedAt: string; + }) => Promise; + deleteTeam: (id: string) => Promise; +} + +export interface TeamState { + teams: Record; + loading: boolean; + loadTeams(): Promise; + createTeam(params: Omit): Promise; + updateTeam(id: string, updates: Partial): Promise; + deleteTeam(id: string): Promise; + addAgentToTeam(teamId: string, agent: TeamAgent): Promise; + removeAgentFromTeam(teamId: string, agentId: string): Promise; + setManager(teamId: string, agentId: string, isManager: boolean): Promise; +} + +export function createTeamStore(deps: TeamStoreDeps) { + const store = createStore((set, get) => ({ + teams: {}, + loading: false, + + loadTeams: async () => { + set({ loading: true }); + try { + const res = await deps.listTeams(); + if (res.ok && res.result) { + const map: Record = {}; + for (const t of res.result) { + map[t.id] = t; + } + set({ teams: map }); + } + } catch (err) { + console.error('[team-store] loadTeams failed:', err); + } finally { + set({ loading: false }); + } + }, + + createTeam: async (params) => { + const now = new Date().toISOString(); + const id = crypto.randomUUID(); + const team: Team = { + id, + ...params, + createdAt: now, + updatedAt: now, + }; + set((s) => ({ teams: { ...s.teams, [id]: team } })); + const res = await deps.persistTeam({ + id: team.id, + name: team.name, + emoji: team.emoji, + description: team.description, + gatewayId: team.gatewayId, + source: team.source, + version: team.version, + agents: team.agents, + createdAt: team.createdAt, + updatedAt: team.updatedAt, + }); + if (!res.ok) { + console.error('[team-store] persistTeam failed:', res.error); + } + return id; + }, + + updateTeam: async (id, updates) => { + const existing = get().teams[id]; + if (!existing) return; + const now = new Date().toISOString(); + const updated: Team = { ...existing, ...updates, updatedAt: now }; + set((s) => ({ teams: { ...s.teams, [id]: updated } })); + const res = await deps.persistTeam({ + id: updated.id, + name: updated.name, + emoji: updated.emoji, + description: updated.description, + gatewayId: updated.gatewayId, + source: updated.source, + version: updated.version, + agents: updated.agents, + createdAt: updated.createdAt, + updatedAt: updated.updatedAt, + }); + if (!res.ok) { + console.error('[team-store] updateTeam persist failed:', res.error); + } + }, + + deleteTeam: async (id) => { + set((s) => { + const next = { ...s.teams }; + delete next[id]; + return { teams: next }; + }); + const res = await deps.deleteTeam(id); + if (!res.ok) { + console.error('[team-store] deleteTeam failed:', res.error); + } + }, + + addAgentToTeam: async (teamId, agent) => { + const existing = get().teams[teamId]; + if (!existing) return; + if (existing.agents.some((a) => a.agentId === agent.agentId)) return; + const updated: Team = { ...existing, agents: [...existing.agents, agent] }; + await get().updateTeam(teamId, { agents: updated.agents }); + }, + + removeAgentFromTeam: async (teamId, agentId) => { + const existing = get().teams[teamId]; + if (!existing) return; + const updated: Team = { ...existing, agents: existing.agents.filter((a) => a.agentId !== agentId) }; + await get().updateTeam(teamId, { agents: updated.agents }); + }, + + setManager: async (teamId, agentId, isManager) => { + const existing = get().teams[teamId]; + if (!existing) return; + const agents = existing.agents.map((a) => (a.agentId === agentId ? { ...a, isManager } : a)); + await get().updateTeam(teamId, { agents }); + }, + })); + + return store; +} diff --git a/packages/desktop/src/main/db/index.ts b/packages/desktop/src/main/db/index.ts index 06768b0..0e5f2c6 100644 --- a/packages/desktop/src/main/db/index.ts +++ b/packages/desktop/src/main/db/index.ts @@ -136,6 +136,30 @@ function openDatabaseAt(workspacePath: string): void { sqlite.exec("ALTER TABLE artifacts ADD COLUMN content_text TEXT NOT NULL DEFAULT ''"); } catch {} + sqlite.exec(` + CREATE TABLE IF NOT EXISTS teams ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + emoji TEXT DEFAULT '', + description TEXT DEFAULT '', + gateway_id TEXT NOT NULL, + source TEXT DEFAULT 'local', + version TEXT DEFAULT '', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + `); + + sqlite.exec(` + CREATE TABLE IF NOT EXISTS team_agents ( + team_id TEXT NOT NULL REFERENCES teams(id), + agent_id TEXT NOT NULL, + role TEXT DEFAULT '', + is_manager INTEGER DEFAULT 0, + PRIMARY KEY (team_id, agent_id) + ) + `); + initFTS(sqlite); db = drizzle(sqlite, { schema }); diff --git a/packages/desktop/src/main/db/schema.ts b/packages/desktop/src/main/db/schema.ts index dc5f82b..158ebbb 100644 --- a/packages/desktop/src/main/db/schema.ts +++ b/packages/desktop/src/main/db/schema.ts @@ -1,4 +1,4 @@ -import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; +import { sqliteTable, text, integer, primaryKey } from 'drizzle-orm/sqlite-core'; export const tasks = sqliteTable('tasks', { id: text('id').primaryKey(), @@ -54,6 +54,31 @@ export const taskRoomSessions = sqliteTable('task_room_sessions', { verifiedAt: text('verified_at').notNull(), }); +export const teams = sqliteTable('teams', { + id: text('id').primaryKey(), + name: text('name').notNull(), + emoji: text('emoji').default(''), + description: text('description').default(''), + gatewayId: text('gateway_id').notNull(), + source: text('source').default('local'), + version: text('version').default(''), + createdAt: text('created_at').notNull(), + updatedAt: text('updated_at').notNull(), +}); + +export const teamAgents = sqliteTable( + 'team_agents', + { + teamId: text('team_id') + .notNull() + .references(() => teams.id), + agentId: text('agent_id').notNull(), + role: text('role').default(''), + isManager: integer('is_manager', { mode: 'boolean' }).default(false), + }, + (table) => [primaryKey({ columns: [table.teamId, table.agentId] })], +); + export const artifacts = sqliteTable('artifacts', { id: text('id').primaryKey(), taskId: text('task_id') diff --git a/packages/desktop/src/main/ipc/data-handlers.ts b/packages/desktop/src/main/ipc/data-handlers.ts index 6aadb1f..692e44b 100644 --- a/packages/desktop/src/main/ipc/data-handlers.ts +++ b/packages/desktop/src/main/ipc/data-handlers.ts @@ -1,7 +1,7 @@ import { ipcMain } from 'electron'; import { eq, desc } from 'drizzle-orm'; import { getDb, isDbReady } from '../db/index.js'; -import { tasks, messages, artifacts, taskRooms, taskRoomSessions } from '../db/schema.js'; +import { tasks, messages, artifacts, taskRooms, taskRoomSessions, teams, teamAgents } from '../db/schema.js'; import { autoExtractArtifacts } from '../artifact/auto-extract.js'; import { getWorkspacePath } from '../workspace/config.js'; @@ -343,4 +343,139 @@ export function registerDataHandlers(): void { return ipcError(err); } }); + + ipcMain.handle('data:teams-list', () => { + if (!isDbReady()) return { ok: true, result: [] }; + try { + const db = getDb(); + const rows = db.select().from(teams).orderBy(desc(teams.createdAt)).all(); + const agentRows = db.select().from(teamAgents).all(); + const agentsByTeam = new Map< + string, + Array<{ agentId: string; role: string | null; isManager: boolean | null }> + >(); + for (const a of agentRows) { + let list = agentsByTeam.get(a.teamId); + if (!list) { + list = []; + agentsByTeam.set(a.teamId, list); + } + list.push({ agentId: a.agentId, role: a.role, isManager: a.isManager }); + } + return { + ok: true, + result: rows.map((r) => ({ + ...r, + agents: (agentsByTeam.get(r.id) ?? []).map((a) => ({ + agentId: a.agentId, + role: a.role ?? '', + isManager: a.isManager ?? false, + })), + })), + }; + } catch (err) { + console.error('[data] teams-list failed:', err); + return ipcError(err); + } + }); + + ipcMain.handle('data:team-get', (_event, params: { id: string }) => { + if (!isDbReady()) return ipcError(new Error('database not ready')); + try { + const db = getDb(); + const row = db.select().from(teams).where(eq(teams.id, params.id)).get(); + if (!row) return { ok: true, result: null }; + const agents = db + .select() + .from(teamAgents) + .where(eq(teamAgents.teamId, params.id)) + .all() + .map((a) => ({ agentId: a.agentId, role: a.role ?? '', isManager: a.isManager ?? false })); + return { ok: true, result: { ...row, agents } }; + } catch (err) { + console.error('[data] team-get failed:', err); + return ipcError(err); + } + }); + + ipcMain.handle( + 'data:team-persist', + ( + _event, + params: { + id: string; + name: string; + emoji?: string; + description?: string; + gatewayId: string; + source?: string; + version?: string; + agents: Array<{ agentId: string; role?: string; isManager?: boolean }>; + createdAt: string; + updatedAt: string; + }, + ) => { + if (!isDbReady()) return ipcError(new Error('database not ready')); + try { + const db = getDb(); + db.transaction((tx) => { + tx.insert(teams) + .values({ + id: params.id, + name: params.name, + emoji: params.emoji ?? '', + description: params.description ?? '', + gatewayId: params.gatewayId, + source: params.source ?? 'local', + version: params.version ?? '', + createdAt: params.createdAt, + updatedAt: params.updatedAt, + }) + .onConflictDoUpdate({ + target: [teams.id], + set: { + name: params.name, + emoji: params.emoji ?? '', + description: params.description ?? '', + gatewayId: params.gatewayId, + source: params.source ?? 'local', + version: params.version ?? '', + updatedAt: params.updatedAt, + }, + }) + .run(); + tx.delete(teamAgents).where(eq(teamAgents.teamId, params.id)).run(); + for (const agent of params.agents) { + tx.insert(teamAgents) + .values({ + teamId: params.id, + agentId: agent.agentId, + role: agent.role ?? '', + isManager: agent.isManager ?? false, + }) + .run(); + } + }); + return { ok: true }; + } catch (err) { + console.error('[data] team-persist failed:', err); + return ipcError(err); + } + }, + ); + + ipcMain.handle('data:team-delete', (_event, params: { id: string }) => { + if (!isDbReady()) return ipcError(new Error('database not ready')); + try { + const db = getDb(); + db.transaction((tx) => { + tx.delete(teamAgents).where(eq(teamAgents.teamId, params.id)).run(); + tx.delete(teams).where(eq(teams.id, params.id)).run(); + }); + return { ok: true }; + } catch (err) { + console.error('[data] team-delete failed:', err); + return ipcError(err); + } + }); } diff --git a/packages/desktop/src/preload/clawwork.d.ts b/packages/desktop/src/preload/clawwork.d.ts index 273882d..6cfea7e 100644 --- a/packages/desktop/src/preload/clawwork.d.ts +++ b/packages/desktop/src/preload/clawwork.d.ts @@ -1,6 +1,7 @@ import type { IpcResult as SharedIpcResult, ChatAttachment as SharedChatAttachment, + Team, ApprovalDecision, CronJob, CronJobCreate, @@ -478,6 +479,22 @@ export interface ClawWorkAPI { runCronJob: (gatewayId: string, jobId: string, mode?: 'due' | 'force') => Promise>; listCronRuns: (gatewayId: string, params?: CronRunsParams) => Promise; + listTeams: () => Promise>; + getTeam: (id: string) => Promise>; + persistTeam: (team: { + id: string; + name: string; + emoji?: string; + description?: string; + gatewayId: string; + source?: string; + version?: string; + agents: Array<{ agentId: string; role?: string; isManager?: boolean }>; + createdAt: string; + updatedAt: string; + }) => Promise; + deleteTeam: (id: string) => Promise; + saveAgentAvatar: (gatewayId: string, agentId: string, dataUrl: string) => Promise; deleteAgentAvatar: (gatewayId: string, agentId: string) => Promise; listLocalAvatars: (gatewayId: string) => Promise>; diff --git a/packages/desktop/src/preload/index.ts b/packages/desktop/src/preload/index.ts index 13e103c..9d3d992 100644 --- a/packages/desktop/src/preload/index.ts +++ b/packages/desktop/src/preload/index.ts @@ -367,6 +367,22 @@ function buildApi(): ClawWorkAPI { listCronRuns: (gatewayId: string, params?: CronRunsParams) => ipcRenderer.invoke('ws:cron-runs', { gatewayId, ...params }), + listTeams: () => ipcRenderer.invoke('data:teams-list'), + getTeam: (id: string) => ipcRenderer.invoke('data:team-get', { id }), + persistTeam: (team: { + id: string; + name: string; + emoji?: string; + description?: string; + gatewayId: string; + source?: string; + version?: string; + agents: Array<{ agentId: string; role?: string; isManager?: boolean }>; + createdAt: string; + updatedAt: string; + }) => ipcRenderer.invoke('data:team-persist', team), + deleteTeam: (id: string) => ipcRenderer.invoke('data:team-delete', { id }), + saveAgentAvatar: (gatewayId: string, agentId: string, dataUrl: string) => ipcRenderer.invoke('avatar:save', { gatewayId, agentId, dataUrl }), deleteAgentAvatar: (gatewayId: string, agentId: string) => diff --git a/packages/desktop/src/renderer/i18n/locales/de.json b/packages/desktop/src/renderer/i18n/locales/de.json index 053608e..fdf0b5a 100644 --- a/packages/desktop/src/renderer/i18n/locales/de.json +++ b/packages/desktop/src/renderer/i18n/locales/de.json @@ -11,6 +11,7 @@ "cancel": "Abbrechen", "save": "Speichern", "close": "Schliessen", + "loading": "Laden...", "gateway": "Gateway", "dark": "Dunkel", "light": "Hell", @@ -599,7 +600,10 @@ "descPlaceholder": "Was macht dieses Team?", "startChat": "Chat starten", "editTeam": "Bearbeiten", - "deleteTeam": "Löschen" + "deleteTeam": "Löschen", + "gateway": "Gateway", + "selectAgents": "Agenten", + "noAgents": "Keine Agenten verfügbar" }, "commandPalette": { "placeholder": "Befehl eingeben oder suchen…", diff --git a/packages/desktop/src/renderer/i18n/locales/en.json b/packages/desktop/src/renderer/i18n/locales/en.json index b2060d4..019c922 100644 --- a/packages/desktop/src/renderer/i18n/locales/en.json +++ b/packages/desktop/src/renderer/i18n/locales/en.json @@ -11,6 +11,7 @@ "cancel": "Cancel", "save": "Save", "close": "Close", + "loading": "Loading...", "gateway": "Gateway", "dark": "Dark", "light": "Light", @@ -599,7 +600,10 @@ "descPlaceholder": "What does this team do?", "startChat": "Start Chat", "editTeam": "Edit", - "deleteTeam": "Delete" + "deleteTeam": "Delete", + "gateway": "Gateway", + "selectAgents": "Agents", + "noAgents": "No agents available" }, "commandPalette": { "placeholder": "Type a command or search…", diff --git a/packages/desktop/src/renderer/i18n/locales/es.json b/packages/desktop/src/renderer/i18n/locales/es.json index 3b3e829..b1b7b2a 100644 --- a/packages/desktop/src/renderer/i18n/locales/es.json +++ b/packages/desktop/src/renderer/i18n/locales/es.json @@ -11,6 +11,7 @@ "cancel": "Cancelar", "save": "Guardar", "close": "Cerrar", + "loading": "Cargando...", "gateway": "Gateway", "dark": "Oscuro", "light": "Claro", @@ -599,7 +600,10 @@ "descPlaceholder": "¿Qué hace este equipo?", "startChat": "Iniciar chat", "editTeam": "Editar", - "deleteTeam": "Eliminar" + "deleteTeam": "Eliminar", + "gateway": "Gateway", + "selectAgents": "Agentes", + "noAgents": "No hay agentes disponibles" }, "commandPalette": { "placeholder": "Escribe un comando o busca…", diff --git a/packages/desktop/src/renderer/i18n/locales/ja.json b/packages/desktop/src/renderer/i18n/locales/ja.json index f874128..f37424d 100644 --- a/packages/desktop/src/renderer/i18n/locales/ja.json +++ b/packages/desktop/src/renderer/i18n/locales/ja.json @@ -11,6 +11,7 @@ "cancel": "キャンセル", "save": "保存", "close": "閉じる", + "loading": "読み込み中...", "gateway": "ゲートウェイ", "dark": "ダーク", "light": "ライト", @@ -599,7 +600,10 @@ "descPlaceholder": "このチームは何をしますか?", "startChat": "チャットを開始", "editTeam": "編集", - "deleteTeam": "削除" + "deleteTeam": "削除", + "gateway": "ゲートウェイ", + "selectAgents": "エージェント", + "noAgents": "利用可能なエージェントがありません" }, "commandPalette": { "placeholder": "コマンドを入力または検索…", diff --git a/packages/desktop/src/renderer/i18n/locales/ko.json b/packages/desktop/src/renderer/i18n/locales/ko.json index 15640b1..4b1fcf2 100644 --- a/packages/desktop/src/renderer/i18n/locales/ko.json +++ b/packages/desktop/src/renderer/i18n/locales/ko.json @@ -11,6 +11,7 @@ "cancel": "취소", "save": "저장", "close": "닫기", + "loading": "로딩 중...", "gateway": "게이트웨이", "dark": "다크", "light": "라이트", @@ -599,7 +600,10 @@ "descPlaceholder": "이 팀은 무엇을 하나요?", "startChat": "대화 시작", "editTeam": "편집", - "deleteTeam": "삭제" + "deleteTeam": "삭제", + "gateway": "게이트웨이", + "selectAgents": "에이전트", + "noAgents": "사용 가능한 에이전트가 없습니다" }, "commandPalette": { "placeholder": "명령어 입력 또는 검색…", diff --git a/packages/desktop/src/renderer/i18n/locales/pt.json b/packages/desktop/src/renderer/i18n/locales/pt.json index 401d8ef..4aedcce 100644 --- a/packages/desktop/src/renderer/i18n/locales/pt.json +++ b/packages/desktop/src/renderer/i18n/locales/pt.json @@ -11,6 +11,7 @@ "cancel": "Cancelar", "save": "Salvar", "close": "Fechar", + "loading": "Carregando...", "gateway": "Gateway", "dark": "Escuro", "light": "Claro", @@ -599,7 +600,10 @@ "descPlaceholder": "O que esta equipe faz?", "startChat": "Iniciar conversa", "editTeam": "Editar", - "deleteTeam": "Excluir" + "deleteTeam": "Excluir", + "gateway": "Gateway", + "selectAgents": "Agentes", + "noAgents": "Nenhum agente disponível" }, "commandPalette": { "placeholder": "Digite um comando ou pesquise…", diff --git a/packages/desktop/src/renderer/i18n/locales/zh-TW.json b/packages/desktop/src/renderer/i18n/locales/zh-TW.json index 05cd4e4..ffa21c2 100644 --- a/packages/desktop/src/renderer/i18n/locales/zh-TW.json +++ b/packages/desktop/src/renderer/i18n/locales/zh-TW.json @@ -11,6 +11,7 @@ "cancel": "取消", "save": "儲存", "close": "關閉", + "loading": "載入中...", "gateway": "Gateway", "dark": "深色", "light": "淺色", @@ -599,7 +600,10 @@ "descPlaceholder": "這個團隊做什麼?", "startChat": "發起對話", "editTeam": "編輯", - "deleteTeam": "刪除" + "deleteTeam": "刪除", + "gateway": "閘道器", + "selectAgents": "選擇 Agent", + "noAgents": "暫無可用 Agent" }, "commandPalette": { "placeholder": "輸入命令或搜尋…", diff --git a/packages/desktop/src/renderer/i18n/locales/zh.json b/packages/desktop/src/renderer/i18n/locales/zh.json index f1c9622..bbf9bc1 100644 --- a/packages/desktop/src/renderer/i18n/locales/zh.json +++ b/packages/desktop/src/renderer/i18n/locales/zh.json @@ -11,6 +11,7 @@ "cancel": "取消", "save": "保存", "close": "关闭", + "loading": "加载中...", "gateway": "网关", "dark": "深色", "light": "浅色", @@ -599,7 +600,10 @@ "descPlaceholder": "这个团队做什么?", "startChat": "发起对话", "editTeam": "编辑", - "deleteTeam": "删除" + "deleteTeam": "删除", + "gateway": "网关", + "selectAgents": "选择 Agent", + "noAgents": "暂无可用 Agent" }, "commandPalette": { "placeholder": "输入命令或搜索…", diff --git a/packages/desktop/src/renderer/layouts/TeamsPanel/CreateTeamDialog.tsx b/packages/desktop/src/renderer/layouts/TeamsPanel/CreateTeamDialog.tsx index de2e025..7c2ff70 100644 --- a/packages/desktop/src/renderer/layouts/TeamsPanel/CreateTeamDialog.tsx +++ b/packages/desktop/src/renderer/layouts/TeamsPanel/CreateTeamDialog.tsx @@ -1,5 +1,7 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { Check } from 'lucide-react'; +import type { AgentInfo } from '@clawwork/shared'; import { Dialog, DialogContent, @@ -10,6 +12,7 @@ import { } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'; +import { useUiStore } from '@/platform'; const EMOJI_OPTIONS = [ '🤖', @@ -41,28 +44,82 @@ const EMOJI_OPTIONS = [ interface CreateTeamDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - onCreate: (data: { name: string; emoji: string; description: string }) => void; + defaultGatewayId: string; + onCreate: (data: { + name: string; + emoji: string; + description: string; + gatewayId: string; + agents: Array<{ agentId: string; role: string; isManager: boolean }>; + }) => void; } -export default function CreateTeamDialog({ open, onOpenChange, onCreate }: CreateTeamDialogProps) { +export default function CreateTeamDialog({ open, onOpenChange, defaultGatewayId, onCreate }: CreateTeamDialogProps) { const { t } = useTranslation(); const [name, setName] = useState(''); const [emoji, setEmoji] = useState('🤖'); const [description, setDescription] = useState(''); const [emojiOpen, setEmojiOpen] = useState(false); + const [gatewayId, setGatewayId] = useState(defaultGatewayId); + const [selectedAgentIds, setSelectedAgentIds] = useState>(new Set()); + const [agentCatalog, setAgentCatalog] = useState([]); + const [loadingAgents, setLoadingAgents] = useState(false); + + const gatewayInfoMap = useUiStore((s) => s.gatewayInfoMap); + const gateways = useMemo(() => Object.entries(gatewayInfoMap), [gatewayInfoMap]); + + useEffect(() => { + if (!open) return; + setGatewayId(defaultGatewayId); + }, [open, defaultGatewayId]); + + useEffect(() => { + if (!open || !gatewayId) return; + setLoadingAgents(true); + setSelectedAgentIds(new Set()); + window.clawwork + .listAgents(gatewayId) + .then((res) => { + if (res.ok && res.result) { + const payload = res.result as { agents?: AgentInfo[] }; + setAgentCatalog(payload.agents ?? []); + } + }) + .catch(() => {}) + .finally(() => setLoadingAgents(false)); + }, [open, gatewayId]); const resetForm = useCallback(() => { setName(''); setEmoji('🤖'); setDescription(''); + setSelectedAgentIds(new Set()); + setAgentCatalog([]); + }, []); + + const toggleAgent = useCallback((agentId: string) => { + setSelectedAgentIds((prev) => { + const next = new Set(prev); + if (next.has(agentId)) { + next.delete(agentId); + } else { + next.add(agentId); + } + return next; + }); }, []); const handleCreate = useCallback(() => { - if (!name.trim()) return; - onCreate({ name: name.trim(), emoji, description: description.trim() }); + if (!name.trim() || selectedAgentIds.size === 0) return; + const agents = Array.from(selectedAgentIds).map((agentId) => ({ + agentId, + role: '', + isManager: false, + })); + onCreate({ name: name.trim(), emoji, description: description.trim(), gatewayId, agents }); resetForm(); onOpenChange(false); - }, [name, emoji, description, onCreate, resetForm, onOpenChange]); + }, [name, emoji, description, gatewayId, selectedAgentIds, onCreate, resetForm, onOpenChange]); const handleOpenChange = useCallback( (next: boolean) => { @@ -131,13 +188,60 @@ export default function CreateTeamDialog({ open, onOpenChange, onCreate }: Creat className="w-full px-3 py-2 rounded-md bg-[var(--bg-primary)] border border-[var(--border)] text-[var(--text-primary)] placeholder:text-[var(--text-muted)] glow-focus focus:border-transparent transition-all resize-none" /> + + {gateways.length > 1 && ( +
+ + +
+ )} + +
+ + {loadingAgents ? ( +
{t('common.loading')}
+ ) : agentCatalog.length === 0 ? ( +
{t('teams.noAgents')}
+ ) : ( +
+ {agentCatalog.map((agent) => { + const selected = selectedAgentIds.has(agent.id); + return ( + + ); + })} +
+ )} +
- diff --git a/packages/desktop/src/renderer/layouts/TeamsPanel/TeamCard.tsx b/packages/desktop/src/renderer/layouts/TeamsPanel/TeamCard.tsx index 63b9738..0fabf36 100644 --- a/packages/desktop/src/renderer/layouts/TeamsPanel/TeamCard.tsx +++ b/packages/desktop/src/renderer/layouts/TeamsPanel/TeamCard.tsx @@ -1,6 +1,7 @@ import { motion } from 'framer-motion'; import { MessageSquare, MoreHorizontal, Pencil, Trash2, Users } from 'lucide-react'; import { useTranslation } from 'react-i18next'; +import type { Team } from '@clawwork/shared'; import { cn } from '@/lib/utils'; import { motion as motionPresets } from '@/styles/design-tokens'; import { Button } from '@/components/ui/button'; @@ -11,14 +12,6 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -interface Team { - id: string; - name: string; - emoji: string; - description: string; - memberCount: number; -} - interface TeamCardProps { team: Team; onStartChat: () => void; @@ -26,8 +19,6 @@ interface TeamCardProps { onDelete: () => void; } -export type { Team }; - export default function TeamCard({ team, onStartChat, onEdit, onDelete }: TeamCardProps) { const { t } = useTranslation(); @@ -69,7 +60,7 @@ export default function TeamCard({ team, onStartChat, onEdit, onDelete }: TeamCa
- {t('teams.memberCount', { count: team.memberCount })} + {t('teams.memberCount', { count: team.agents.length })}