Files
zopu-code/repos/web/hooks/work-os/use-work-os.ts
-Puter cc47007fa9 Slice 1 polish: archive dormant code, merge work-os into primitives, add futures
- Archive dormant apps (daemon, desktop, native, tui) and superseded
  packages/server into repos/ for reference
- Archive 21 dead web components (chat page shell, work-os cluster, strays)
  into repos/web/; keep reused message-rendering primitives in components/chat
- Merge @code/work-os into @code/primitives; work.ts absorbed via ./work
  subpath and barrel export; update all four importers
- Add docs/futures.md tracking audio/video (blocked at Flue + provider),
  web layout cleanup, and message rendering quality
- Repoint dev:zopu script to @code/agents (the live Flue server)
- Note repos/ reference-code convention in AGENTS.md
2026-07-27 16:34:17 +05:30

204 lines
6.4 KiB
TypeScript

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<typeof useProjectWorkspace>["projects"];
readonly selectedProject: ReturnType<
typeof useProjectWorkspace
>["selectedProject"];
readonly selectedProjectId: ReturnType<
typeof useProjectWorkspace
>["selectedProjectId"];
readonly repository: string;
readonly setRepository: (value: string) => void;
readonly connectRepository: () => Promise<void>;
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<typeof useProjectWorkspace>["startIssue"];
readonly raiseIssue: ReturnType<typeof useProjectWorkspace>["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<readonly SignalItem[]>(() => {
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<ComposerMode>("project");
const selectIssue = (issueId: Id<"projectIssues"> | null) => {
workspace.setSelectedIssueId(issueId);
setComposerMode(issueId ? "work-unit" : "project");
};
const workUnitCards = useMemo<readonly WorkUnitCard[]>(() => {
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<WorkUnitDetail | null>(() => {
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,
};
};