-
Notifications
You must be signed in to change notification settings - Fork 30
[Feat] Team data model with SQLite persistence and Zustand store #262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| import { createStore } from 'zustand/vanilla'; | ||
| import type { Team, TeamAgent, IpcResult } from '@clawwork/shared'; | ||
|
|
||
| export interface TeamStoreDeps { | ||
| listTeams: () => Promise<IpcResult<Team[]>>; | ||
| getTeam: (id: string) => Promise<IpcResult<Team | null>>; | ||
| 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<IpcResult>; | ||
| deleteTeam: (id: string) => Promise<IpcResult>; | ||
| } | ||
|
|
||
| export interface TeamState { | ||
| teams: Record<string, Team>; | ||
| loading: boolean; | ||
| loadTeams(): Promise<void>; | ||
| createTeam(params: Omit<Team, 'id' | 'createdAt' | 'updatedAt'>): Promise<string>; | ||
| updateTeam(id: string, updates: Partial<Team>): Promise<void>; | ||
| deleteTeam(id: string): Promise<void>; | ||
| addAgentToTeam(teamId: string, agent: TeamAgent): Promise<void>; | ||
| removeAgentFromTeam(teamId: string, agentId: string): Promise<void>; | ||
| setManager(teamId: string, agentId: string, isManager: boolean): Promise<void>; | ||
| } | ||
|
|
||
| export function createTeamStore(deps: TeamStoreDeps) { | ||
| const store = createStore<TeamState>((set, get) => ({ | ||
| teams: {}, | ||
| loading: false, | ||
|
|
||
| loadTeams: async () => { | ||
| set({ loading: true }); | ||
| try { | ||
| const res = await deps.listTeams(); | ||
| if (res.ok && res.result) { | ||
| const map: Record<string, Team> = {}; | ||
| 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); | ||
| } | ||
| }, | ||
|
Comment on lines
+85
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| 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); | ||
| } | ||
| }, | ||
|
Comment on lines
+108
to
+118
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| 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; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The foreign key constraint for References
|
||
| 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 }); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
createTeamaction performs an optimistic update by adding the new team to the store before the persistence call completes. However, it lacks a rollback mechanism ifdeps.persistTeamfails. This can lead to the UI showing data that was never actually saved to the database.References