- 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
204 lines
6.4 KiB
TypeScript
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,
|
|
};
|
|
};
|