diff --git a/apps/web/app/(dashboard)/_components/app-sidebar.tsx b/apps/web/app/(dashboard)/_components/app-sidebar.tsx index 5229b44f..bbcea35f 100644 --- a/apps/web/app/(dashboard)/_components/app-sidebar.tsx +++ b/apps/web/app/(dashboard)/_components/app-sidebar.tsx @@ -44,9 +44,8 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip import { useAuthStore } from "@/features/auth"; import { useWorkspaceStore } from "@/features/workspace"; import { useQuery } from "@tanstack/react-query"; -import { inboxKeys } from "@core/inbox/queries"; -import { deduplicateInboxItems } from "@core/inbox/queries"; -import { api } from "@/shared/api"; +import { useWorkspaceId } from "@core/hooks"; +import { inboxListOptions, deduplicateInboxItems } from "@core/inbox/queries"; import { useModalStore } from "@/features/modals"; const primaryNav = [ @@ -77,12 +76,8 @@ export function AppSidebar() { const workspaces = useWorkspaceStore((s) => s.workspaces); const switchWorkspace = useWorkspaceStore((s) => s.switchWorkspace); - const wsId = workspace?.id; - const { data: inboxItems = [] } = useQuery({ - queryKey: wsId ? inboxKeys.list(wsId) : ["inbox", "disabled"], - queryFn: () => api.listInbox(), - enabled: !!wsId, - }); + const wsId = useWorkspaceId(); + const { data: inboxItems = [] } = useQuery(inboxListOptions(wsId)); const unreadCount = React.useMemo( () => deduplicateInboxItems(inboxItems).filter((i) => !i.read).length, [inboxItems], diff --git a/apps/web/app/(dashboard)/agents/page.tsx b/apps/web/app/(dashboard)/agents/page.tsx index bd3c6ddd..d28099be 100644 --- a/apps/web/app/(dashboard)/agents/page.tsx +++ b/apps/web/app/(dashboard)/agents/page.tsx @@ -995,7 +995,7 @@ function TriggersTab({ value={scheduledConfig.cron ?? ""} onChange={(e) => updateTriggerConfig(trigger.id, { - ...scheduledConfig, + ...(trigger.config ?? {}), cron: e.target.value, }) } @@ -1012,7 +1012,7 @@ function TriggersTab({ value={scheduledConfig.timezone ?? ""} onChange={(e) => updateTriggerConfig(trigger.id, { - ...scheduledConfig, + ...(trigger.config ?? {}), timezone: e.target.value, }) } diff --git a/apps/web/features/inbox/store.ts b/apps/web/features/inbox/store.ts deleted file mode 100644 index 0489a30b..00000000 --- a/apps/web/features/inbox/store.ts +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; - -import { create } from "zustand"; -import type { InboxItem, IssueStatus } from "@/shared/types"; -import { toast } from "sonner"; -import { api } from "@/shared/api"; -import { createLogger } from "@/shared/logger"; - -const logger = createLogger("inbox-store"); - -/** - * Deduplicate inbox items by issue_id (one entry per issue, Linear-style), - * keep latest, sort by time DESC. - * Memoized by reference — returns the same array if `items` hasn't changed. - */ -let _prevItems: InboxItem[] = []; -let _prevDeduped: InboxItem[] = []; - -function deduplicateInboxItems(items: InboxItem[]): InboxItem[] { - if (items === _prevItems) return _prevDeduped; - _prevItems = items; - - const active = items.filter((i) => !i.archived); - const groups = new Map(); - active.forEach((item) => { - const key = item.issue_id ?? item.id; - const group = groups.get(key) ?? []; - group.push(item); - groups.set(key, group); - }); - const merged: InboxItem[] = []; - groups.forEach((group) => { - const sorted = group.sort( - (a, b) => - new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), - ); - if (sorted[0]) merged.push(sorted[0]); - }); - _prevDeduped = merged.sort( - (a, b) => - new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), - ); - return _prevDeduped; -} - -interface InboxState { - items: InboxItem[]; - loading: boolean; - fetch: () => Promise; - setItems: (items: InboxItem[]) => void; - addItem: (item: InboxItem) => void; - markRead: (id: string) => void; - archive: (id: string) => void; - markAllRead: () => void; - archiveAll: () => void; - archiveAllRead: () => void; - updateIssueStatus: (issueId: string, status: IssueStatus) => void; - dedupedItems: () => InboxItem[]; - unreadCount: () => number; -} - -export const useInboxStore = create((set, get) => ({ - items: [], - loading: true, - - fetch: async () => { - logger.debug("fetch start"); - const isInitialLoad = get().items.length === 0; - if (isInitialLoad) set({ loading: true }); - try { - const data = await api.listInbox(); - logger.info("fetched", data.length, "items"); - set({ items: data, loading: false }); - } catch (err) { - logger.error("fetch failed", err); - toast.error("Failed to load inbox"); - if (isInitialLoad) set({ loading: false }); - } - }, - - setItems: (items) => set({ items }), - addItem: (item) => - set((s) => ({ - items: s.items.some((i) => i.id === item.id) - ? s.items - : [item, ...s.items], - })), - markRead: (id) => - set((s) => ({ - items: s.items.map((i) => (i.id === id ? { ...i, read: true } : i)), - })), - archive: (id) => - set((s) => { - const target = s.items.find((i) => i.id === id); - const issueId = target?.issue_id; - return { - items: s.items.map((i) => - i.id === id || (issueId && i.issue_id === issueId) - ? { ...i, archived: true } - : i, - ), - }; - }), - markAllRead: () => - set((s) => ({ - items: s.items.map((i) => (!i.archived ? { ...i, read: true } : i)), - })), - archiveAll: () => - set((s) => ({ - items: s.items.map((i) => (!i.archived ? { ...i, archived: true } : i)), - })), - archiveAllRead: () => - set((s) => ({ - items: s.items.map((i) => - i.read && !i.archived ? { ...i, archived: true } : i - ), - })), - updateIssueStatus: (issueId, status) => - set((s) => ({ - items: s.items.map((i) => - i.issue_id === issueId ? { ...i, issue_status: status } : i - ), - })), - dedupedItems: () => deduplicateInboxItems(get().items), - unreadCount: () => - get().dedupedItems().filter((i) => !i.read).length, -})); diff --git a/apps/web/features/runtimes/store.ts b/apps/web/features/runtimes/store.ts deleted file mode 100644 index 03e9f716..00000000 --- a/apps/web/features/runtimes/store.ts +++ /dev/null @@ -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; - setSelectedId: (id: string) => void; - /** Patch a single runtime in-place (e.g. status/last_seen_at from WS event). */ - patchRuntime: (id: string, updates: Partial) => void; - /** Replace the full runtimes list (used on daemon:register events). */ - setRuntimes: (runtimes: AgentRuntime[]) => void; -} - -type RuntimeStore = RuntimeState & RuntimeActions; - -export const useRuntimeStore = create((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 ?? "", - }); - }, -}));