import { api } from "@code/backend/convex/_generated/api"; import type { Id } from "@code/backend/convex/_generated/dataModel"; import { useFlueAgent } from "@flue/react"; import { useQuery } from "convex/react"; import { useMemo, useState } from "react"; import { useChatAgent } from "@/hooks/chat/use-chat-agent"; import { usePersonalOrganization } from "@/hooks/use-personal-organization"; import { useProjectWorkspace } from "@/hooks/use-project-workspace"; import type { ChatAgentState } from "@/lib/chat/types"; import { buildWorkUnitCard, buildWorkUnitDetail, eventsForIssue, } from "@/lib/work-os/work-unit-projection"; import type { LinkedSignal, WorkUnitCard, WorkUnitDetail, } from "@/lib/work-os/work-unit-projection"; export interface SignalItem { readonly title: string; readonly summary: string; readonly sourceCount: number; readonly createdAt: number; } export type ComposerMode = "project" | "work-unit"; export interface WorkOsState { readonly projects: ReturnType["projects"]; readonly selectedProject: ReturnType< typeof useProjectWorkspace >["selectedProject"]; readonly selectedProjectId: ReturnType< typeof useProjectWorkspace >["selectedProjectId"]; readonly repository: string; readonly setRepository: (value: string) => void; readonly connectRepository: () => Promise; readonly workUnitCards: readonly WorkUnitCard[]; readonly selectedIssue: ReturnType< typeof useProjectWorkspace >["selectedIssue"]; readonly selectedIssueId: Id<"projectIssues"> | null; readonly selectedDetail: WorkUnitDetail | null; readonly selectIssue: (issueId: Id<"projectIssues"> | null) => void; readonly startIssue: ReturnType["startIssue"]; readonly raiseIssue: ReturnType["raiseIssue"]; readonly issueTitle: string; readonly setIssueTitle: (value: string) => void; readonly issueBody: string; readonly setIssueBody: (value: string) => void; readonly signals: readonly SignalItem[]; readonly composerMode: ComposerMode; readonly setComposerMode: (mode: ComposerMode) => void; readonly projectAgent: ChatAgentState; readonly workUnitAgent: ChatAgentState; readonly pendingAction: string | null; readonly error: string | null; } const toLinkedSignals = ( entries: | readonly { readonly signalId: Id<"signals">; readonly title: string; readonly summary: string; readonly sourceCount: number; readonly createdAt: number; }[] | undefined ): readonly LinkedSignal[] => (entries ?? []).map((entry) => ({ createdAt: entry.createdAt, signalId: entry.signalId, sourceCount: entry.sourceCount, summary: entry.summary, title: entry.title, })); export const useWorkOs = (): WorkOsState => { const workspace = useProjectWorkspace(); const orgState = usePersonalOrganization(); const projectAgent = useChatAgent(); const activeProjectId = workspace.selectedProjectId; const workUnitFlue = useFlueAgent({ id: workspace.selectedIssueId ? String(workspace.selectedIssueId) : undefined, live: "sse", name: "project-manager", }); const workUnitAgent: ChatAgentState = { error: workUnitFlue.error, historyReady: workUnitFlue.historyReady, messages: workUnitFlue.messages, sendMessage: workUnitFlue.sendMessage, status: workspace.selectedIssueId ? workUnitFlue.status : "idle", }; // Project-level signals for the global sidebar panel. const allSignals = useQuery( api.signals.list, orgState.organizationId ? { organizationId: orgState.organizationId } : "skip" ); const signals = useMemo(() => { if (!allSignals || !activeProjectId) { return []; } return allSignals .filter((entry) => entry.signal.projectId === activeProjectId) .map((entry) => ({ createdAt: entry.signal.createdAt, sourceCount: entry.sources.length, summary: entry.signal.problemStatement.summary, title: entry.signal.problemStatement.title, })); }, [allSignals, activeProjectId]); // All linked signals for the active project, grouped by issue id. // Used for card counts and detail display. const linkedByIssue = useQuery( api.signals.listLinkedSignalsForProject, activeProjectId ? { projectId: activeProjectId } : "skip" ); const [composerMode, setComposerMode] = useState("project"); const selectIssue = (issueId: Id<"projectIssues"> | null) => { workspace.setSelectedIssueId(issueId); setComposerMode(issueId ? "work-unit" : "project"); }; const workUnitCards = useMemo(() => { if (!workspace.issues) { return []; } return workspace.issues.map((issue) => { const issueEvents = eventsForIssue(workspace.events, issue._id); const linked = toLinkedSignals(linkedByIssue?.[String(issue._id)]); return buildWorkUnitCard({ issue, issueEvents, linkedSignals: linked, }); }); }, [workspace.issues, workspace.events, linkedByIssue]); const selectedDetail = useMemo(() => { if (!workspace.selectedIssue) { return null; } const issueEvents = eventsForIssue( workspace.events, workspace.selectedIssue._id ); const linked = toLinkedSignals( linkedByIssue?.[String(workspace.selectedIssue._id)] ); return buildWorkUnitDetail({ issue: workspace.selectedIssue, issueEvents, linkedSignals: linked, }); }, [workspace.selectedIssue, workspace.events, linkedByIssue]); return { composerMode, connectRepository: workspace.connectRepository, error: workspace.error, issueBody: workspace.issueBody, issueTitle: workspace.issueTitle, pendingAction: workspace.pendingAction, projectAgent, projects: workspace.projects, raiseIssue: workspace.raiseIssue, repository: workspace.repository, selectIssue, selectedDetail, selectedIssue: workspace.selectedIssue, selectedIssueId: workspace.selectedIssueId, selectedProject: workspace.selectedProject, selectedProjectId: activeProjectId, setComposerMode, setIssueBody: workspace.setIssueBody, setIssueTitle: workspace.setIssueTitle, setRepository: workspace.setRepository, signals, startIssue: workspace.startIssue, workUnitAgent, workUnitCards, }; };