feat(core): migrate workspace + runtimes to TanStack Query (Phase 3+4)
- Create core/workspace/ with queries (members, agents, skills, list) and mutations - Create core/runtimes/ with queries - Migrate 11 consumer files from useWorkspaceStore.members/agents/skills to useQuery - Replace all WS refreshMap entries with qc.invalidateQueries - Simplify workspace store: delete members/agents/skills fields + refresh methods, hydrateWorkspace becomes synchronous (TQ auto-fetches on component mount) - Delete useRuntimeStore (no consumers left), runtimes-page uses local useState + TQ - Remove workspace→runtime cross-store dependency - Clean up dead test helper mocks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1d812bd446
commit
e40341ab73
24 changed files with 251 additions and 289 deletions
|
|
@ -75,10 +75,11 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||
import { api } from "@/shared/api";
|
||||
import { useAuthStore } from "@/features/auth";
|
||||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
import { useRuntimeStore } from "@/features/runtimes";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { runtimeListOptions } from "@core/runtimes/queries";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { issueListOptions } from "@core/issues/queries";
|
||||
import { skillListOptions, agentListOptions, workspaceKeys } from "@core/workspace/queries";
|
||||
import { ActorAvatar } from "@/components/common/actor-avatar";
|
||||
import { useFileUpload } from "@/shared/hooks/use-file-upload";
|
||||
|
||||
|
|
@ -445,8 +446,9 @@ function SkillsTab({
|
|||
}: {
|
||||
agent: Agent;
|
||||
}) {
|
||||
const workspaceSkills = useWorkspaceStore((s) => s.skills);
|
||||
const refreshAgents = useWorkspaceStore((s) => s.refreshAgents);
|
||||
const qc = useQueryClient();
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: workspaceSkills = [] } = useQuery(skillListOptions(wsId));
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
|
||||
|
|
@ -458,7 +460,7 @@ function SkillsTab({
|
|||
try {
|
||||
const newIds = [...agent.skills.map((s) => s.id), skillId];
|
||||
await api.setAgentSkills(agent.id, { skill_ids: newIds });
|
||||
await refreshAgents();
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.agents(wsId) });
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to add skill");
|
||||
} finally {
|
||||
|
|
@ -472,7 +474,7 @@ function SkillsTab({
|
|||
try {
|
||||
const newIds = agent.skills.filter((s) => s.id !== skillId).map((s) => s.id);
|
||||
await api.setAgentSkills(agent.id, { skill_ids: newIds });
|
||||
await refreshAgents();
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.agents(wsId) });
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to remove skill");
|
||||
} finally {
|
||||
|
|
@ -1547,21 +1549,17 @@ function AgentDetail({
|
|||
export default function AgentsPage() {
|
||||
const isLoading = useAuthStore((s) => s.isLoading);
|
||||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
const agents = useWorkspaceStore((s) => s.agents);
|
||||
const refreshAgents = useWorkspaceStore((s) => s.refreshAgents);
|
||||
const qc = useQueryClient();
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: agents = [] } = useQuery(agentListOptions(wsId));
|
||||
const [selectedId, setSelectedId] = useState<string>("");
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const runtimes = useRuntimeStore((s) => s.runtimes);
|
||||
const fetchRuntimes = useRuntimeStore((s) => s.fetchRuntimes);
|
||||
const { data: runtimes = [] } = useQuery(runtimeListOptions(wsId));
|
||||
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
|
||||
id: "multica_agents_layout",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) fetchRuntimes();
|
||||
}, [workspace, fetchRuntimes]);
|
||||
|
||||
const filteredAgents = useMemo(
|
||||
() => showArchived ? agents.filter((a) => !!a.archived_at) : agents.filter((a) => !a.archived_at),
|
||||
[agents, showArchived],
|
||||
|
|
@ -1578,14 +1576,14 @@ export default function AgentsPage() {
|
|||
|
||||
const handleCreate = async (data: CreateAgentRequest) => {
|
||||
const agent = await api.createAgent(data);
|
||||
await refreshAgents();
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.agents(wsId) });
|
||||
setSelectedId(agent.id);
|
||||
};
|
||||
|
||||
const handleUpdate = async (id: string, data: Record<string, unknown>) => {
|
||||
try {
|
||||
await api.updateAgent(id, data as UpdateAgentRequest);
|
||||
await refreshAgents();
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.agents(wsId) });
|
||||
toast.success("Agent updated");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to update agent");
|
||||
|
|
@ -1596,7 +1594,7 @@ export default function AgentsPage() {
|
|||
const handleArchive = async (id: string) => {
|
||||
try {
|
||||
await api.archiveAgent(id);
|
||||
await refreshAgents();
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.agents(wsId) });
|
||||
toast.success("Agent archived");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to archive agent");
|
||||
|
|
@ -1606,7 +1604,7 @@ export default function AgentsPage() {
|
|||
const handleRestore = async (id: string) => {
|
||||
try {
|
||||
await api.restoreAgent(id);
|
||||
await refreshAgents();
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.agents(wsId) });
|
||||
toast.success("Agent restored");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to restore agent");
|
||||
|
|
|
|||
|
|
@ -36,8 +36,11 @@ import {
|
|||
DropdownMenuSubContent,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { toast } from "sonner";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuthStore } from "@/features/auth";
|
||||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { memberListOptions, workspaceKeys } from "@core/workspace/queries";
|
||||
import { api } from "@/shared/api";
|
||||
|
||||
const roleConfig: Record<MemberRole, { label: string; icon: typeof Crown; description: string }> = {
|
||||
|
|
@ -140,8 +143,9 @@ function MemberRow({
|
|||
export function MembersTab() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
const refreshMembers = useWorkspaceStore((s) => s.refreshMembers);
|
||||
const qc = useQueryClient();
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||||
|
||||
const [inviteEmail, setInviteEmail] = useState("");
|
||||
const [inviteRole, setInviteRole] = useState<MemberRole>("member");
|
||||
|
|
@ -168,7 +172,7 @@ export function MembersTab() {
|
|||
});
|
||||
setInviteEmail("");
|
||||
setInviteRole("member");
|
||||
await refreshMembers();
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.members(wsId) });
|
||||
toast.success("Member added");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to add member");
|
||||
|
|
@ -182,7 +186,7 @@ export function MembersTab() {
|
|||
setMemberActionId(memberId);
|
||||
try {
|
||||
await api.updateMember(workspace.id, memberId, { role });
|
||||
await refreshMembers();
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.members(wsId) });
|
||||
toast.success("Role updated");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to update member");
|
||||
|
|
@ -201,7 +205,7 @@ export function MembersTab() {
|
|||
setMemberActionId(member.id);
|
||||
try {
|
||||
await api.deleteMember(workspace.id, member.id);
|
||||
await refreshMembers();
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.members(wsId) });
|
||||
toast.success("Member removed");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to remove member");
|
||||
|
|
|
|||
|
|
@ -6,15 +6,19 @@ import { Input } from "@/components/ui/input";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { toast } from "sonner";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuthStore } from "@/features/auth";
|
||||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { memberListOptions } from "@core/workspace/queries";
|
||||
import { api } from "@/shared/api";
|
||||
import type { WorkspaceRepo } from "@/shared/types";
|
||||
|
||||
export function RepositoriesTab() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||||
const updateWorkspace = useWorkspaceStore((s) => s.updateWorkspace);
|
||||
|
||||
const [repos, setRepos] = useState<WorkspaceRepo[]>(workspace?.repos ?? []);
|
||||
|
|
|
|||
|
|
@ -18,14 +18,18 @@ import {
|
|||
AlertDialogAction,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { toast } from "sonner";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuthStore } from "@/features/auth";
|
||||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { memberListOptions } from "@core/workspace/queries";
|
||||
import { api } from "@/shared/api";
|
||||
|
||||
export function WorkspaceTab() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||||
const updateWorkspace = useWorkspaceStore((s) => s.updateWorkspace);
|
||||
const leaveWorkspace = useWorkspaceStore((s) => s.leaveWorkspace);
|
||||
const deleteWorkspace = useWorkspaceStore((s) => s.deleteWorkspace);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
import type { ReactNode } from "react";
|
||||
import { Users } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { HoverCard, HoverCardTrigger, HoverCardContent } from "@/components/ui/hover-card";
|
||||
import { ActorAvatar } from "@/components/common/actor-avatar";
|
||||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { memberListOptions, agentListOptions } from "@core/workspace/queries";
|
||||
|
||||
interface MentionHoverCardProps {
|
||||
type: string;
|
||||
|
|
@ -13,8 +15,9 @@ interface MentionHoverCardProps {
|
|||
}
|
||||
|
||||
function MentionHoverCard({ type, id, children }: MentionHoverCardProps) {
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
const agents = useWorkspaceStore((s) => s.agents);
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||||
const { data: agents = [] } = useQuery(agentListOptions(wsId));
|
||||
|
||||
if (type === "all") {
|
||||
return (
|
||||
|
|
|
|||
1
apps/web/core/runtimes/index.ts
Normal file
1
apps/web/core/runtimes/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { runtimeKeys, runtimeListOptions } from "./queries";
|
||||
14
apps/web/core/runtimes/queries.ts
Normal file
14
apps/web/core/runtimes/queries.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { api } from "@/shared/api";
|
||||
|
||||
export const runtimeKeys = {
|
||||
all: (wsId: string) => ["runtimes", wsId] as const,
|
||||
list: (wsId: string) => [...runtimeKeys.all(wsId), "list"] as const,
|
||||
};
|
||||
|
||||
export function runtimeListOptions(wsId: string) {
|
||||
return queryOptions({
|
||||
queryKey: runtimeKeys.list(wsId),
|
||||
queryFn: () => api.listRuntimes({ workspace_id: wsId }),
|
||||
});
|
||||
}
|
||||
13
apps/web/core/workspace/index.ts
Normal file
13
apps/web/core/workspace/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export {
|
||||
workspaceKeys,
|
||||
workspaceListOptions,
|
||||
memberListOptions,
|
||||
agentListOptions,
|
||||
skillListOptions,
|
||||
} from "./queries";
|
||||
|
||||
export {
|
||||
useCreateWorkspace,
|
||||
useLeaveWorkspace,
|
||||
useDeleteWorkspace,
|
||||
} from "./mutations";
|
||||
34
apps/web/core/workspace/mutations.ts
Normal file
34
apps/web/core/workspace/mutations.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { api } from "@/shared/api";
|
||||
import { workspaceKeys } from "./queries";
|
||||
|
||||
export function useCreateWorkspace() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { name: string; slug: string; description?: string }) =>
|
||||
api.createWorkspace(data),
|
||||
onSettled: () => {
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.list() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useLeaveWorkspace() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (workspaceId: string) => api.leaveWorkspace(workspaceId),
|
||||
onSettled: () => {
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.list() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteWorkspace() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (workspaceId: string) => api.deleteWorkspace(workspaceId),
|
||||
onSettled: () => {
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.list() });
|
||||
},
|
||||
});
|
||||
}
|
||||
39
apps/web/core/workspace/queries.ts
Normal file
39
apps/web/core/workspace/queries.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { api } from "@/shared/api";
|
||||
|
||||
export const workspaceKeys = {
|
||||
all: (wsId: string) => ["workspaces", wsId] as const,
|
||||
list: () => ["workspaces", "list"] as const,
|
||||
members: (wsId: string) => ["workspaces", wsId, "members"] as const,
|
||||
agents: (wsId: string) => ["workspaces", wsId, "agents"] as const,
|
||||
skills: (wsId: string) => ["workspaces", wsId, "skills"] as const,
|
||||
};
|
||||
|
||||
export function workspaceListOptions() {
|
||||
return queryOptions({
|
||||
queryKey: workspaceKeys.list(),
|
||||
queryFn: () => api.listWorkspaces(),
|
||||
});
|
||||
}
|
||||
|
||||
export function memberListOptions(wsId: string) {
|
||||
return queryOptions({
|
||||
queryKey: workspaceKeys.members(wsId),
|
||||
queryFn: () => api.listMembers(wsId),
|
||||
});
|
||||
}
|
||||
|
||||
export function agentListOptions(wsId: string) {
|
||||
return queryOptions({
|
||||
queryKey: workspaceKeys.agents(wsId),
|
||||
queryFn: () =>
|
||||
api.listAgents({ workspace_id: wsId, include_archived: true }),
|
||||
});
|
||||
}
|
||||
|
||||
export function skillListOptions(wsId: string) {
|
||||
return queryOptions({
|
||||
queryKey: workspaceKeys.skills(wsId),
|
||||
queryFn: () => api.listSkills(),
|
||||
});
|
||||
}
|
||||
|
|
@ -13,7 +13,8 @@ import { computePosition, offset, flip, shift } from "@floating-ui/dom";
|
|||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
import { getQueryClient } from "@core/query-client";
|
||||
import { issueKeys } from "@core/issues/queries";
|
||||
import type { Issue, ListIssuesResponse } from "@/shared/types";
|
||||
import { workspaceKeys } from "@core/workspace/queries";
|
||||
import type { Issue, ListIssuesResponse, MemberWithUser, Agent } from "@/shared/types";
|
||||
import { ActorAvatar } from "@/components/common/actor-avatar";
|
||||
import { StatusIcon } from "@/features/issues/components/status-icon";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
|
@ -218,8 +219,9 @@ export function createMentionSuggestion(): Omit<
|
|||
> {
|
||||
return {
|
||||
items: ({ query }) => {
|
||||
const { members, agents } = useWorkspaceStore.getState();
|
||||
const wsId = useWorkspaceStore.getState().workspace?.id;
|
||||
const members: MemberWithUser[] = wsId ? getQueryClient().getQueryData(workspaceKeys.members(wsId)) ?? [] : [];
|
||||
const agents: Agent[] = wsId ? getQueryClient().getQueryData(workspaceKeys.agents(wsId)) ?? [] : [];
|
||||
const issues: Issue[] = wsId
|
||||
? getQueryClient().getQueryData<ListIssuesResponse>(issueKeys.list(wsId))?.issues ?? []
|
||||
: [];
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ import { useAuthStore } from "@/features/auth";
|
|||
import { useWorkspaceStore, useActorName } from "@/features/workspace";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { issueListOptions, issueDetailOptions } from "@core/issues/queries";
|
||||
import { memberListOptions, agentListOptions } from "@core/workspace/queries";
|
||||
import { useUpdateIssue, useDeleteIssue } from "@core/issues/mutations";
|
||||
import { useIssueTimeline } from "@/features/issues/hooks/use-issue-timeline";
|
||||
import { useIssueReactions } from "@/features/issues/hooks/use-issue-reactions";
|
||||
|
|
@ -177,12 +178,12 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
|||
const router = useRouter();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
const agents = useWorkspaceStore((s) => s.agents);
|
||||
const currentMemberRole = members.find((m) => m.user_id === user?.id)?.role;
|
||||
|
||||
// Issue navigation — read from TQ list cache
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||||
const { data: agents = [] } = useQuery(agentListOptions(wsId));
|
||||
const currentMemberRole = members.find((m) => m.user_id === user?.id)?.role;
|
||||
const { data: allIssues = [] } = useQuery(issueListOptions(wsId));
|
||||
const currentIndex = allIssues.findIndex((i) => i.id === id);
|
||||
const prevIssue = currentIndex > 0 ? allIssues[currentIndex - 1] : null;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@ import {
|
|||
PRIORITY_CONFIG,
|
||||
} from "@/features/issues/config";
|
||||
import { StatusIcon, PriorityIcon } from "@/features/issues/components";
|
||||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { memberListOptions, agentListOptions } from "@core/workspace/queries";
|
||||
import { ActorAvatar } from "@/components/common/actor-avatar";
|
||||
import {
|
||||
useIssueViewStore,
|
||||
|
|
@ -155,8 +157,9 @@ function ActorSubContent({
|
|||
noAssigneeCount?: number;
|
||||
}) {
|
||||
const [search, setSearch] = useState("");
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
const agents = useWorkspaceStore((s) => s.agents);
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||||
const { data: agents = [] } = useQuery(agentListOptions(wsId));
|
||||
const query = search.toLowerCase();
|
||||
const filteredMembers = members.filter((m) =>
|
||||
m.name.toLowerCase().includes(query),
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@
|
|||
import { useState } from "react";
|
||||
import { Lock, UserMinus } from "lucide-react";
|
||||
import type { Agent, IssueAssigneeType, UpdateIssueRequest } from "@/shared/types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuthStore } from "@/features/auth";
|
||||
import { useWorkspaceStore, useActorName } from "@/features/workspace";
|
||||
import { useActorName } from "@/features/workspace";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { memberListOptions, agentListOptions } from "@core/workspace/queries";
|
||||
import { ActorAvatar } from "@/components/common/actor-avatar";
|
||||
import {
|
||||
PropertyPicker,
|
||||
|
|
@ -44,8 +47,9 @@ export function AssigneePicker({
|
|||
const setOpen = controlledOnOpenChange ?? setInternalOpen;
|
||||
const [filter, setFilter] = useState("");
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
const agents = useWorkspaceStore((s) => s.agents);
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||||
const { data: agents = [] } = useQuery(agentListOptions(wsId));
|
||||
const { getActorName } = useActorName();
|
||||
|
||||
const currentMember = members.find((m) => m.user_id === user?.id);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ import { TitleEditor } from "@/features/editor";
|
|||
import { StatusIcon, PriorityIcon } from "@/features/issues/components";
|
||||
import { ALL_STATUSES, STATUS_CONFIG, PRIORITY_ORDER, PRIORITY_CONFIG } from "@/features/issues/config";
|
||||
import { useWorkspaceStore, useActorName } from "@/features/workspace";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { memberListOptions, agentListOptions } from "@core/workspace/queries";
|
||||
import { useIssueDraftStore } from "@/features/issues/stores/draft-store";
|
||||
import { useCreateIssue } from "@core/issues/mutations";
|
||||
import { useFileUpload } from "@/shared/hooks/use-file-upload";
|
||||
|
|
@ -67,8 +70,9 @@ function PillButton({
|
|||
export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?: Record<string, unknown> | null }) {
|
||||
const router = useRouter();
|
||||
const workspaceName = useWorkspaceStore((s) => s.workspace?.name);
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
const agents = useWorkspaceStore((s) => s.agents);
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||||
const { data: agents = [] } = useQuery(agentListOptions(wsId));
|
||||
const { getActorName } = useActorName();
|
||||
|
||||
const draft = useIssueDraftStore((s) => s.draft);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||
import { useAuthStore } from "@/features/auth";
|
||||
import { useWorkspaceStore, WorkspaceAvatar } from "@/features/workspace";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { agentListOptions } from "@core/workspace/queries";
|
||||
import { filterIssues } from "@/features/issues/utils/filter";
|
||||
import { BOARD_STATUSES } from "@/features/issues/config";
|
||||
import { ViewStoreProvider } from "@/features/issues/stores/view-store-context";
|
||||
|
|
@ -26,8 +27,8 @@ import { MyIssuesHeader } from "./my-issues-header";
|
|||
export function MyIssuesPage() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
const agents = useWorkspaceStore((s) => s.agents);
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: agents = [] } = useQuery(agentListOptions(wsId));
|
||||
const { data: allIssues = [], isLoading: loading } = useQuery(issueListOptions(wsId));
|
||||
|
||||
const viewMode = useStore(myIssuesViewStore, (s) => s.viewMode);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { toast } from "sonner";
|
|||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
import { useAuthStore } from "@/features/auth";
|
||||
import { createLogger } from "@/shared/logger";
|
||||
import { api } from "@/shared/api";
|
||||
import { issueKeys } from "@core/issues/queries";
|
||||
import {
|
||||
onIssueCreated,
|
||||
|
|
@ -16,6 +15,7 @@ import {
|
|||
} from "@core/issues/ws-updaters";
|
||||
import { onInboxNew, onInboxInvalidate, onInboxIssueStatusChanged } from "@core/inbox/ws-updaters";
|
||||
import { inboxKeys } from "@core/inbox/queries";
|
||||
import { workspaceKeys } from "@core/workspace/queries";
|
||||
import type {
|
||||
MemberAddedPayload,
|
||||
WorkspaceDeletedPayload,
|
||||
|
|
@ -55,22 +55,21 @@ export function useRealtimeSync(ws: WSClient | null) {
|
|||
const wsId = useWorkspaceStore.getState().workspace?.id;
|
||||
if (wsId) onInboxInvalidate(qc, wsId);
|
||||
},
|
||||
agent: () => void useWorkspaceStore.getState().refreshAgents(),
|
||||
member: () => void useWorkspaceStore.getState().refreshMembers(),
|
||||
workspace: () => {
|
||||
// Lightweight: only re-fetch workspace list, don't hydrate everything.
|
||||
// workspace:deleted is handled by a precise side-effect handler below.
|
||||
api.listWorkspaces().then((wsList) => {
|
||||
const current = useWorkspaceStore.getState().workspace;
|
||||
const updated = current
|
||||
? wsList.find((w) => w.id === current.id)
|
||||
: null;
|
||||
if (updated) useWorkspaceStore.getState().updateWorkspace(updated);
|
||||
}).catch((err) => {
|
||||
logger.error("workspace refresh failed", err);
|
||||
});
|
||||
agent: () => {
|
||||
const wsId = useWorkspaceStore.getState().workspace?.id;
|
||||
if (wsId) qc.invalidateQueries({ queryKey: workspaceKeys.agents(wsId) });
|
||||
},
|
||||
member: () => {
|
||||
const wsId = useWorkspaceStore.getState().workspace?.id;
|
||||
if (wsId) qc.invalidateQueries({ queryKey: workspaceKeys.members(wsId) });
|
||||
},
|
||||
workspace: () => {
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.list() });
|
||||
},
|
||||
skill: () => {
|
||||
const wsId = useWorkspaceStore.getState().workspace?.id;
|
||||
if (wsId) qc.invalidateQueries({ queryKey: workspaceKeys.skills(wsId) });
|
||||
},
|
||||
skill: () => void useWorkspaceStore.getState().refreshSkills(),
|
||||
};
|
||||
|
||||
const timers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
|
@ -191,12 +190,11 @@ export function useRealtimeSync(ws: WSClient | null) {
|
|||
if (wsId) {
|
||||
qc.invalidateQueries({ queryKey: issueKeys.all(wsId) });
|
||||
qc.invalidateQueries({ queryKey: inboxKeys.all(wsId) });
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.agents(wsId) });
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.members(wsId) });
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.skills(wsId) });
|
||||
}
|
||||
await Promise.all([
|
||||
useWorkspaceStore.getState().refreshAgents(),
|
||||
useWorkspaceStore.getState().refreshMembers(),
|
||||
useWorkspaceStore.getState().refreshSkills(),
|
||||
]);
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.list() });
|
||||
} catch (e) {
|
||||
logger.error("reconnect refetch failed", e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { Server } from "lucide-react";
|
||||
import { useDefaultLayout } from "react-resizable-panels";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
ResizablePanelGroup,
|
||||
ResizablePanel,
|
||||
|
|
@ -10,38 +11,35 @@ import {
|
|||
} from "@/components/ui/resizable";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useAuthStore } from "@/features/auth";
|
||||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { runtimeListOptions, runtimeKeys } from "@core/runtimes/queries";
|
||||
import { useWSEvent } from "@/features/realtime";
|
||||
import { useRuntimeStore } from "../store";
|
||||
import { RuntimeList } from "./runtime-list";
|
||||
import { RuntimeDetail } from "./runtime-detail";
|
||||
|
||||
export default function RuntimesPage() {
|
||||
const isLoading = useAuthStore((s) => s.isLoading);
|
||||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
const runtimes = useRuntimeStore((s) => s.runtimes);
|
||||
const selectedId = useRuntimeStore((s) => s.selectedId);
|
||||
const fetching = useRuntimeStore((s) => s.fetching);
|
||||
const fetchRuntimes = useRuntimeStore((s) => s.fetchRuntimes);
|
||||
const setSelectedId = useRuntimeStore((s) => s.setSelectedId);
|
||||
const wsId = useWorkspaceId();
|
||||
const qc = useQueryClient();
|
||||
const { data: runtimes = [], isLoading: fetching } = useQuery(runtimeListOptions(wsId));
|
||||
const [selectedId, setSelectedId] = useState("");
|
||||
|
||||
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
|
||||
id: "multica_runtimes_layout",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) fetchRuntimes();
|
||||
}, [workspace, fetchRuntimes]);
|
||||
|
||||
// Re-fetch on daemon register/deregister events.
|
||||
// Heartbeat events are not broadcast over WS, so no handler needed.
|
||||
const handleDaemonEvent = useCallback(() => {
|
||||
fetchRuntimes();
|
||||
}, [fetchRuntimes]);
|
||||
qc.invalidateQueries({ queryKey: runtimeKeys.list(wsId) });
|
||||
}, [qc, wsId]);
|
||||
|
||||
useWSEvent("daemon:register", handleDaemonEvent);
|
||||
|
||||
const selected = runtimes.find((r) => r.id === selectedId) ?? null;
|
||||
// Auto-select first runtime if nothing selected
|
||||
const effectiveSelectedId = selectedId && runtimes.some((r) => r.id === selectedId)
|
||||
? selectedId
|
||||
: runtimes[0]?.id ?? "";
|
||||
const selected = runtimes.find((r) => r.id === effectiveSelectedId) ?? null;
|
||||
|
||||
if (isLoading || fetching) {
|
||||
return (
|
||||
|
|
@ -95,7 +93,7 @@ export default function RuntimesPage() {
|
|||
>
|
||||
<RuntimeList
|
||||
runtimes={runtimes}
|
||||
selectedId={selectedId}
|
||||
selectedId={effectiveSelectedId}
|
||||
onSelect={setSelectedId}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
export { RuntimesPage } from "./components";
|
||||
export { useRuntimeStore } from "./store";
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
import type { AgentRuntime } from "@/shared/types";
|
||||
import { api } from "@/shared/api";
|
||||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
|
||||
interface RuntimeState {
|
||||
runtimes: AgentRuntime[];
|
||||
selectedId: string;
|
||||
fetching: boolean;
|
||||
}
|
||||
|
||||
interface RuntimeActions {
|
||||
fetchRuntimes: () => Promise<void>;
|
||||
setSelectedId: (id: string) => void;
|
||||
/** Patch a single runtime in-place (e.g. status/last_seen_at from WS event). */
|
||||
patchRuntime: (id: string, updates: Partial<AgentRuntime>) => void;
|
||||
/** Replace the full runtimes list (used on daemon:register events). */
|
||||
setRuntimes: (runtimes: AgentRuntime[]) => void;
|
||||
}
|
||||
|
||||
type RuntimeStore = RuntimeState & RuntimeActions;
|
||||
|
||||
export const useRuntimeStore = create<RuntimeStore>((set, get) => ({
|
||||
// State
|
||||
runtimes: [],
|
||||
selectedId: "",
|
||||
fetching: true,
|
||||
|
||||
// Actions
|
||||
fetchRuntimes: async () => {
|
||||
const workspace = useWorkspaceStore.getState().workspace;
|
||||
if (!workspace) return;
|
||||
try {
|
||||
const data = await api.listRuntimes({ workspace_id: workspace.id });
|
||||
const { selectedId } = get();
|
||||
set({
|
||||
runtimes: data,
|
||||
fetching: false,
|
||||
// Auto-select first if nothing selected
|
||||
selectedId: selectedId && data.some((r) => r.id === selectedId)
|
||||
? selectedId
|
||||
: data[0]?.id ?? "",
|
||||
});
|
||||
} catch {
|
||||
set({ fetching: false });
|
||||
}
|
||||
},
|
||||
|
||||
setSelectedId: (id) => set({ selectedId: id }),
|
||||
|
||||
patchRuntime: (id, updates) => {
|
||||
set((state) => ({
|
||||
runtimes: state.runtimes.map((r) =>
|
||||
r.id === id ? { ...r, ...updates } : r,
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
setRuntimes: (runtimes) => {
|
||||
const { selectedId } = get();
|
||||
set({
|
||||
runtimes,
|
||||
selectedId: selectedId && runtimes.some((r) => r.id === selectedId)
|
||||
? selectedId
|
||||
: runtimes[0]?.id ?? "",
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
|
@ -33,8 +33,10 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||
import { toast } from "sonner";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { api } from "@/shared/api";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuthStore } from "@/features/auth";
|
||||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { skillListOptions, workspaceKeys } from "@core/workspace/queries";
|
||||
|
||||
import { FileTree } from "./file-tree";
|
||||
import { FileViewer } from "./file-viewer";
|
||||
|
|
@ -346,6 +348,8 @@ function SkillDetail({
|
|||
onUpdate: (id: string, data: UpdateSkillRequest) => Promise<void>;
|
||||
onDelete: (id: string) => Promise<void>;
|
||||
}) {
|
||||
const qc = useQueryClient();
|
||||
const wsId = useWorkspaceId();
|
||||
const [name, setName] = useState(skill.name);
|
||||
const [description, setDescription] = useState(skill.description);
|
||||
const [content, setContent] = useState(skill.content);
|
||||
|
|
@ -370,12 +374,12 @@ function SkillDetail({
|
|||
setSelectedPath(SKILL_MD);
|
||||
setLoadingFiles(true);
|
||||
api.getSkill(skill.id).then((full) => {
|
||||
useWorkspaceStore.getState().upsertSkill(full);
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.skills(wsId) });
|
||||
setFiles((full.files ?? []).map((f) => ({ path: f.path, content: f.content })));
|
||||
}).catch((e) => {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to load skill files");
|
||||
}).finally(() => setLoadingFiles(false));
|
||||
}, [skill.id]);
|
||||
}, [skill.id, qc, wsId]);
|
||||
|
||||
// Build the virtual file map
|
||||
const fileMap = useMemo(() => buildFileMap(content, files), [content, files]);
|
||||
|
|
@ -610,10 +614,9 @@ function SkillDetail({
|
|||
|
||||
export default function SkillsPage() {
|
||||
const isLoading = useAuthStore((s) => s.isLoading);
|
||||
const skills = useWorkspaceStore((s) => s.skills);
|
||||
const refreshSkills = useWorkspaceStore((s) => s.refreshSkills);
|
||||
const upsertSkill = useWorkspaceStore((s) => s.upsertSkill);
|
||||
const removeSkill = useWorkspaceStore((s) => s.removeSkill);
|
||||
const qc = useQueryClient();
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: skills = [] } = useQuery(skillListOptions(wsId));
|
||||
const [selectedId, setSelectedId] = useState<string>("");
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
|
||||
|
|
@ -628,22 +631,22 @@ export default function SkillsPage() {
|
|||
|
||||
const handleCreate = async (data: CreateSkillRequest) => {
|
||||
const skill = await api.createSkill(data);
|
||||
upsertSkill(skill);
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.skills(wsId) });
|
||||
setSelectedId(skill.id);
|
||||
toast.success("Skill created");
|
||||
};
|
||||
|
||||
const handleImport = async (url: string) => {
|
||||
const skill = await api.importSkill({ url });
|
||||
upsertSkill(skill);
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.skills(wsId) });
|
||||
setSelectedId(skill.id);
|
||||
toast.success("Skill imported");
|
||||
};
|
||||
|
||||
const handleUpdate = async (id: string, data: UpdateSkillRequest) => {
|
||||
try {
|
||||
const updated = await api.updateSkill(id, data);
|
||||
upsertSkill(updated);
|
||||
await api.updateSkill(id, data);
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.skills(wsId) });
|
||||
toast.success("Skill saved");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to save skill");
|
||||
|
|
@ -658,7 +661,7 @@ export default function SkillsPage() {
|
|||
const remaining = skills.filter((s) => s.id !== id);
|
||||
setSelectedId(remaining[0]?.id ?? "");
|
||||
}
|
||||
removeSkill(id);
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.skills(wsId) });
|
||||
toast.success("Skill deleted");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to delete skill");
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { useWorkspaceStore } from "./store";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { memberListOptions, agentListOptions } from "@core/workspace/queries";
|
||||
|
||||
export function useActorName() {
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
const agents = useWorkspaceStore((s) => s.agents);
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||||
const { data: agents = [] } = useQuery(agentListOptions(wsId));
|
||||
|
||||
const getMemberName = (userId: string) => {
|
||||
const m = members.find((m) => m.user_id === userId);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
import type { Workspace, MemberWithUser, Agent, Skill } from "@/shared/types";
|
||||
import { useRuntimeStore } from "@/features/runtimes";
|
||||
import type { Workspace } from "@/shared/types";
|
||||
import { toast } from "sonner";
|
||||
import { api } from "@/shared/api";
|
||||
import { createLogger } from "@/shared/logger";
|
||||
|
|
@ -12,30 +11,21 @@ const logger = createLogger("workspace-store");
|
|||
interface WorkspaceState {
|
||||
workspace: Workspace | null;
|
||||
workspaces: Workspace[];
|
||||
members: MemberWithUser[];
|
||||
agents: Agent[];
|
||||
skills: Skill[];
|
||||
}
|
||||
|
||||
interface WorkspaceActions {
|
||||
hydrateWorkspace: (
|
||||
wsList: Workspace[],
|
||||
preferredWorkspaceId?: string | null,
|
||||
) => Promise<Workspace | null>;
|
||||
switchWorkspace: (workspaceId: string) => Promise<void>;
|
||||
) => Workspace | null;
|
||||
switchWorkspace: (workspaceId: string) => void;
|
||||
refreshWorkspaces: () => Promise<Workspace[]>;
|
||||
refreshMembers: () => Promise<void>;
|
||||
updateAgent: (id: string, updates: Partial<Agent>) => void;
|
||||
refreshAgents: () => Promise<void>;
|
||||
refreshSkills: () => Promise<void>;
|
||||
upsertSkill: (skill: Skill) => void;
|
||||
removeSkill: (id: string) => void;
|
||||
updateWorkspace: (ws: Workspace) => void;
|
||||
createWorkspace: (data: {
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
}) => Promise<Workspace>;
|
||||
updateWorkspace: (ws: Workspace) => void;
|
||||
leaveWorkspace: (workspaceId: string) => Promise<void>;
|
||||
deleteWorkspace: (workspaceId: string) => Promise<void>;
|
||||
clearWorkspace: () => void;
|
||||
|
|
@ -47,12 +37,9 @@ export const useWorkspaceStore = create<WorkspaceStore>((set, get) => ({
|
|||
// State
|
||||
workspace: null,
|
||||
workspaces: [],
|
||||
members: [],
|
||||
agents: [],
|
||||
skills: [],
|
||||
|
||||
// Actions
|
||||
hydrateWorkspace: async (wsList, preferredWorkspaceId) => {
|
||||
hydrateWorkspace: (wsList, preferredWorkspaceId) => {
|
||||
set({ workspaces: wsList });
|
||||
|
||||
const nextWorkspace =
|
||||
|
|
@ -65,53 +52,35 @@ export const useWorkspaceStore = create<WorkspaceStore>((set, get) => ({
|
|||
if (!nextWorkspace) {
|
||||
api.setWorkspaceId(null);
|
||||
localStorage.removeItem("multica_workspace_id");
|
||||
set({ workspace: null, members: [], agents: [], skills: [] });
|
||||
set({ workspace: null });
|
||||
return null;
|
||||
}
|
||||
|
||||
api.setWorkspaceId(nextWorkspace.id);
|
||||
localStorage.setItem("multica_workspace_id", nextWorkspace.id);
|
||||
set({ workspace: nextWorkspace });
|
||||
|
||||
logger.debug("hydrate workspace", nextWorkspace.name, nextWorkspace.id);
|
||||
const [nextMembers, nextAgents, nextSkills] = await Promise.all([
|
||||
api.listMembers(nextWorkspace.id).catch((e) => {
|
||||
logger.error("failed to load members", e);
|
||||
toast.error("Failed to load members");
|
||||
return [] as MemberWithUser[];
|
||||
}),
|
||||
api.listAgents({ workspace_id: nextWorkspace.id, include_archived: true }).catch((e) => {
|
||||
logger.error("failed to load agents", e);
|
||||
toast.error("Failed to load agents");
|
||||
return [] as Agent[];
|
||||
}),
|
||||
api.listSkills().catch(() => [] as Skill[]),
|
||||
]);
|
||||
logger.info("hydrate complete", "members:", nextMembers.length, "agents:", nextAgents.length);
|
||||
set({ members: nextMembers, agents: nextAgents, skills: nextSkills });
|
||||
|
||||
// Members, agents, skills, issues, inbox are all managed by TanStack Query.
|
||||
// They auto-fetch when components mount with the workspace ID in their query key.
|
||||
|
||||
return nextWorkspace;
|
||||
},
|
||||
|
||||
switchWorkspace: async (workspaceId) => {
|
||||
switchWorkspace: (workspaceId) => {
|
||||
logger.info("switching to", workspaceId);
|
||||
const { workspaces, hydrateWorkspace } = get();
|
||||
const ws = workspaces.find((item) => item.id === workspaceId);
|
||||
if (!ws) return;
|
||||
|
||||
// Switch identity FIRST — api client, localStorage, and the
|
||||
// workspace object in this store — so that any in-flight refetch
|
||||
// (e.g. triggered by a WS event during the async gap) already
|
||||
// targets the new workspace.
|
||||
api.setWorkspaceId(ws.id);
|
||||
localStorage.setItem("multica_workspace_id", ws.id);
|
||||
|
||||
// Clear stale data across stores before hydrating.
|
||||
// Issue + inbox caches are managed by TanStack Query (keyed by wsId, auto-refetches).
|
||||
useRuntimeStore.getState().setRuntimes([]);
|
||||
set({ workspace: ws, members: [], agents: [], skills: [] });
|
||||
// All data caches (issues, inbox, members, agents, skills, runtimes)
|
||||
// are managed by TanStack Query, keyed by wsId — auto-refetch on switch.
|
||||
set({ workspace: ws });
|
||||
|
||||
await hydrateWorkspace(workspaces, ws.id);
|
||||
hydrateWorkspace(workspaces, ws.id);
|
||||
},
|
||||
|
||||
refreshWorkspaces: async () => {
|
||||
|
|
@ -119,7 +88,7 @@ export const useWorkspaceStore = create<WorkspaceStore>((set, get) => ({
|
|||
const storedWorkspaceId = localStorage.getItem("multica_workspace_id");
|
||||
try {
|
||||
const wsList = await api.listWorkspaces();
|
||||
await hydrateWorkspace(wsList, workspace?.id ?? storedWorkspaceId);
|
||||
hydrateWorkspace(wsList, workspace?.id ?? storedWorkspaceId);
|
||||
return wsList;
|
||||
} catch (e) {
|
||||
logger.error("failed to refresh workspaces", e);
|
||||
|
|
@ -128,77 +97,6 @@ export const useWorkspaceStore = create<WorkspaceStore>((set, get) => ({
|
|||
}
|
||||
},
|
||||
|
||||
refreshMembers: async () => {
|
||||
const { workspace } = get();
|
||||
if (!workspace) return;
|
||||
try {
|
||||
const members = await api.listMembers(workspace.id);
|
||||
set({ members });
|
||||
} catch (e) {
|
||||
logger.error("failed to refresh members", e);
|
||||
toast.error("Failed to refresh members");
|
||||
}
|
||||
},
|
||||
|
||||
updateAgent: (id, updates) =>
|
||||
set((s) => ({
|
||||
agents: s.agents.map((a) => (a.id === id ? { ...a, ...updates } : a)),
|
||||
})),
|
||||
|
||||
refreshAgents: async () => {
|
||||
const { workspace } = get();
|
||||
if (!workspace) return;
|
||||
try {
|
||||
const agents = await api.listAgents({ workspace_id: workspace.id, include_archived: true });
|
||||
set({ agents });
|
||||
} catch (e) {
|
||||
logger.error("failed to refresh agents", e);
|
||||
toast.error("Failed to refresh agents");
|
||||
}
|
||||
},
|
||||
|
||||
refreshSkills: async () => {
|
||||
const { workspace, skills: existing } = get();
|
||||
if (!workspace) return;
|
||||
try {
|
||||
const fetched = await api.listSkills();
|
||||
// listSkills doesn't include files — preserve files from existing entries
|
||||
const filesById = new Map(
|
||||
existing.filter((s) => s.files?.length).map((s) => [s.id, s.files]),
|
||||
);
|
||||
const merged = fetched.map((s) => ({
|
||||
...s,
|
||||
files: s.files ?? filesById.get(s.id) ?? [],
|
||||
}));
|
||||
set({ skills: merged });
|
||||
} catch (e) {
|
||||
logger.error("failed to refresh skills", e);
|
||||
toast.error("Failed to refresh skills");
|
||||
}
|
||||
},
|
||||
|
||||
upsertSkill: (skill) => {
|
||||
set((state) => {
|
||||
const idx = state.skills.findIndex((s) => s.id === skill.id);
|
||||
if (idx >= 0) {
|
||||
const next = [...state.skills];
|
||||
next[idx] = skill;
|
||||
return { skills: next };
|
||||
}
|
||||
return { skills: [...state.skills, skill] };
|
||||
});
|
||||
},
|
||||
|
||||
removeSkill: (id) => {
|
||||
set((state) => ({ skills: state.skills.filter((s) => s.id !== id) }));
|
||||
},
|
||||
|
||||
createWorkspace: async (data) => {
|
||||
const ws = await api.createWorkspace(data);
|
||||
set((state) => ({ workspaces: [...state.workspaces, ws] }));
|
||||
return ws;
|
||||
},
|
||||
|
||||
updateWorkspace: (ws) => {
|
||||
set((state) => ({
|
||||
workspace: state.workspace?.id === ws.id ? ws : state.workspace,
|
||||
|
|
@ -208,13 +106,19 @@ export const useWorkspaceStore = create<WorkspaceStore>((set, get) => ({
|
|||
}));
|
||||
},
|
||||
|
||||
createWorkspace: async (data) => {
|
||||
const ws = await api.createWorkspace(data);
|
||||
set((state) => ({ workspaces: [...state.workspaces, ws] }));
|
||||
return ws;
|
||||
},
|
||||
|
||||
leaveWorkspace: async (workspaceId) => {
|
||||
await api.leaveWorkspace(workspaceId);
|
||||
const { workspace, hydrateWorkspace } = get();
|
||||
const wsList = await api.listWorkspaces();
|
||||
const preferredWorkspaceId =
|
||||
workspace?.id === workspaceId ? null : (workspace?.id ?? null);
|
||||
await hydrateWorkspace(wsList, preferredWorkspaceId);
|
||||
hydrateWorkspace(wsList, preferredWorkspaceId);
|
||||
},
|
||||
|
||||
deleteWorkspace: async (workspaceId) => {
|
||||
|
|
@ -223,11 +127,11 @@ export const useWorkspaceStore = create<WorkspaceStore>((set, get) => ({
|
|||
const wsList = await api.listWorkspaces();
|
||||
const preferredWorkspaceId =
|
||||
workspace?.id === workspaceId ? null : (workspace?.id ?? null);
|
||||
await hydrateWorkspace(wsList, preferredWorkspaceId);
|
||||
hydrateWorkspace(wsList, preferredWorkspaceId);
|
||||
},
|
||||
|
||||
clearWorkspace: () => {
|
||||
api.setWorkspaceId(null);
|
||||
set({ workspace: null, workspaces: [], members: [], agents: [], skills: [] });
|
||||
set({ workspace: null, workspaces: [] });
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -85,8 +85,6 @@ export const mockAuthValue: Record<string, any> = {
|
|||
leaveWorkspace: vi.fn(),
|
||||
deleteWorkspace: vi.fn(),
|
||||
refreshWorkspaces: vi.fn(),
|
||||
refreshMembers: vi.fn(),
|
||||
refreshAgents: vi.fn(),
|
||||
getMemberName: (userId: string) => {
|
||||
const m = mockMembers.find((m) => m.user_id === userId);
|
||||
return m?.name ?? "Unknown";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue