update
This commit is contained in:
1 parent
54bcc77ef7
commit
7a3db27ffa
33 files changed
+5906
-3089
No files matched your search
+124
-211
@@ -3,7 +3,6 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import {
|
||||
ArrowClockwise,
|
||||
Bell,
|
||||
BookOpen,
|
||||
Brain,
|
||||
CalendarBlank,
|
||||
CaretUpDown,
|
||||
@@ -39,8 +38,6 @@ import {
|
||||
X,
|
||||
} from "@phosphor-icons/react";
|
||||
import type {
|
||||
AgentMessageRecord,
|
||||
AutomationRuleRecord,
|
||||
CompanyRecord,
|
||||
CompanyMetrics,
|
||||
DesktopEvent,
|
||||
@@ -51,7 +48,6 @@ import type {
|
||||
SocialAccountRecord,
|
||||
BrowserActionRecord,
|
||||
StandupReport,
|
||||
WorkflowPipelineRecord,
|
||||
} from "@shared/types";
|
||||
import type { SidebarBadges } from "@shared/contracts";
|
||||
import { CompanyOnboarding } from "./components/CompanyOnboarding";
|
||||
@@ -72,6 +68,7 @@ import {
|
||||
TaskInspector,
|
||||
TaskQueue,
|
||||
WorkspaceCreator,
|
||||
WorkspaceInspector,
|
||||
WorkspaceList,
|
||||
} from "./components/execution";
|
||||
import {
|
||||
@@ -90,6 +87,7 @@ import { SocialAccountsPanel } from "./components/social";
|
||||
import { AutomationPanel } from "./components/automation";
|
||||
import { CommunicationHub } from "./components/communication";
|
||||
import { HiringPipeline } from "./components/hiring";
|
||||
import { DocumentCenter, KnowledgeBaseManager, MeetingsManager, SprintManager } from "./components/company-operations";
|
||||
import { ActionButton, ErrorBoundary, ListFrame, ListRow, Modal, Panel, SectionButton, Select, StatusPill, useFocusTrap, useReducedMotion } from "./components/ui";
|
||||
import { unwrap } from "./lib/desktop";
|
||||
import { formatMoney, formatTime } from "./lib/formatters";
|
||||
@@ -175,12 +173,15 @@ export function App() {
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
||||
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
||||
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string | null>(null);
|
||||
const [selectedCommunicationEntityId, setSelectedCommunicationEntityId] = useState<string | null>(null);
|
||||
const [taskViewMode, setTaskViewMode] = useState<"list" | "kanban">("list");
|
||||
const logBufferRef = useRef<Record<string, string>>({});
|
||||
const [, setLogRevision] = useState(0);
|
||||
const [badges, setBadges] = useState<SidebarBadges | null>(null);
|
||||
const [commandPaletteSearch, setCommandPaletteSearch] = useState("");
|
||||
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||
const [initialOnboardingLocked, setInitialOnboardingLocked] = useState(false);
|
||||
const [inboxItems, setInboxItems] = useState<InboxItem[]>([]);
|
||||
const [companyMetrics, setCompanyMetrics] = useState<CompanyMetrics | null>(null);
|
||||
const [standupReport, setStandupReport] = useState<StandupReport | null>(null);
|
||||
@@ -191,23 +192,46 @@ export function App() {
|
||||
try {
|
||||
const next = unwrap<ProfileSnapshot>(await window.agentCompany.loadSnapshot());
|
||||
setSnapshot(next);
|
||||
// Tasks are opened explicitly by clicking; don't auto-select.
|
||||
if (next.companies.length === 0) {
|
||||
setInitialOnboardingLocked(true);
|
||||
}
|
||||
if (!selectedConnectorId && next.connectors[0]) setSelectedConnectorId(next.connectors[0].id);
|
||||
if (!selectedApprovalId && next.approvals[0]) setSelectedApprovalId(next.approvals[0].id);
|
||||
if (!selectedRunId && next.runs[0]) setSelectedRunId(next.runs[0].id);
|
||||
if (next.currentCompanyId) {
|
||||
const badgeResult = await window.agentCompany.getSidebarBadges(next.currentCompanyId);
|
||||
if (badgeResult.ok) setBadges(badgeResult.data);
|
||||
setSocialAccounts(next.socialAccounts?.filter(a => a.companyId === next.currentCompanyId) ?? []);
|
||||
const browserActionResult = await window.agentCompany.listBrowserActions({ companyId: next.currentCompanyId, limit: 50 });
|
||||
if (browserActionResult.ok) {
|
||||
setBrowserActions(browserActionResult.data);
|
||||
}
|
||||
}
|
||||
setError(null);
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : String(nextError));
|
||||
}
|
||||
}, [selectedApprovalId, selectedConnectorId, selectedRunId]);
|
||||
}, [selectedConnectorId, selectedRunId]);
|
||||
|
||||
// Stable ref so the subscribe callback always calls the latest refresh
|
||||
// without tearing down the subscription when refresh identity changes.
|
||||
const refreshRef = useRef(refresh);
|
||||
useEffect(() => {
|
||||
refreshRef.current = refresh;
|
||||
}, [refresh]);
|
||||
|
||||
// Debounce timer ref for coalescing rapid real-time events
|
||||
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const debouncedRefresh = useCallback(() => {
|
||||
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
debounceTimerRef.current = null;
|
||||
void refreshRef.current();
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
void refreshRef.current();
|
||||
const unsubscribe = window.agentCompany.subscribe((event: DesktopEvent) => {
|
||||
if (event.type === "run-log") {
|
||||
logBufferRef.current = {
|
||||
@@ -220,13 +244,18 @@ export function App() {
|
||||
if (event.type === "shortcut") {
|
||||
if (event.action === "toggle-command-palette") setCommandPaletteOpen((current) => !current);
|
||||
if (event.action === "toggle-console") setConsoleOpen((current) => !current);
|
||||
if (event.action === "refresh") void refresh();
|
||||
if (event.action === "refresh") void refreshRef.current();
|
||||
return;
|
||||
}
|
||||
void refresh();
|
||||
// All data-mutating events trigger a debounced snapshot reload:
|
||||
// domain-changed, run-status, new-message, automation-fired, heartbeat-status
|
||||
debouncedRefresh();
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [refresh]);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
||||
};
|
||||
}, [debouncedRefresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedRunId) return;
|
||||
@@ -272,13 +301,9 @@ export function App() {
|
||||
|
||||
if (section === "social") {
|
||||
setSocialAccounts(snapshot.socialAccounts?.filter(a => a.companyId === companyId) ?? []);
|
||||
void window.agentCompany.listBrowserActions({ companyId, limit: 50 }).then(result => {
|
||||
if (result.ok) setBrowserActions(result.data);
|
||||
});
|
||||
}
|
||||
}, [section, snapshot?.currentCompanyId]);
|
||||
}, [section, snapshot]);
|
||||
|
||||
// Global Escape key handler for command palette
|
||||
useEffect(() => {
|
||||
if (!commandPaletteOpen) return;
|
||||
function onKey(event: KeyboardEvent) {
|
||||
@@ -302,11 +327,10 @@ export function App() {
|
||||
const selectedTask = selectedTaskId ? (tasks.find((task) => task.id === selectedTaskId) ?? null) : null;
|
||||
const selectedConnector = snapshot?.connectors.find((connector) => connector.id === selectedConnectorId) ?? snapshot?.connectors[0] ?? null;
|
||||
const approvals = useMemo(() => snapshot?.approvals.filter((approval) => !currentCompany || approval.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
|
||||
const selectedApproval = approvals.find((approval) => approval.id === selectedApprovalId) ?? approvals[0] ?? null;
|
||||
const selectedApproval = selectedApprovalId ? (approvals.find((approval) => approval.id === selectedApprovalId) ?? null) : null;
|
||||
const runs = useMemo(() => snapshot?.runs.filter((run) => !currentCompany || run.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
|
||||
const selectedRun = runs.find((run) => run.id === selectedRunId) ?? runs[0] ?? null;
|
||||
|
||||
// Derived selections for organization entities
|
||||
const companyAgents = useMemo(() => snapshot?.agents.filter((a) => !currentCompany || a.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
|
||||
const selectedAgent = companyAgents.find((a) => a.id === selectedAgentId) ?? null;
|
||||
const companyGoals = useMemo(() => snapshot?.goals.filter((g) => !currentCompany || g.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
|
||||
@@ -314,32 +338,24 @@ export function App() {
|
||||
const companyProjects = useMemo(() => snapshot?.projects.filter((p) => !currentCompany || p.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
|
||||
const selectedProject = companyProjects.find((p) => p.id === selectedProjectId) ?? null;
|
||||
|
||||
// Sidebar badge mapping
|
||||
const activeInboxCount = useMemo(() => inboxItems.filter((item) => !item.dismissed).length, [inboxItems]);
|
||||
const activeInboxCount = inboxItems.length;
|
||||
const navBadges = useMemo<Partial<Record<SectionId, { count: number; tone: "default" | "warn" | "danger" }>>>(() => badges ? {
|
||||
inbox: activeInboxCount > 0 ? { count: activeInboxCount, tone: "warn" } : undefined,
|
||||
tasks: badges.todoTasks > 0 ? { count: badges.todoTasks, tone: "default" } : undefined,
|
||||
approvals: badges.pendingApprovals > 0 ? { count: badges.pendingApprovals, tone: "warn" } : undefined,
|
||||
costs: badges.budgetWarnings > 0 ? { count: badges.budgetWarnings, tone: "danger" } : undefined,
|
||||
communication: badges.unreadMessages > 0 ? { count: badges.unreadMessages, tone: "warn" } : undefined,
|
||||
} : {}, [badges, activeInboxCount]);
|
||||
|
||||
const liveRuns = useMemo(() => runs.filter((run) => run.status === "running" || run.status === "queued"), [runs]);
|
||||
const failedRuns = useMemo(() => runs.filter((run) => ["failed", "timed_out", "interrupted"].includes(run.status)), [runs]);
|
||||
const pendingApprovals = useMemo(() => approvals.filter((approval) => approval.state === "pending"), [approvals]);
|
||||
const tasksInProgress = useMemo(() => tasks.filter((task) => task.status === "in_progress" || task.status === "in_review"), [tasks]);
|
||||
const companyWorkspaces = useMemo(() => snapshot?.workspaces.filter((workspace) => !currentCompany || workspace.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
|
||||
const selectedWorkspace = companyWorkspaces.find((workspace) => workspace.id === selectedWorkspaceId) ?? companyWorkspaces[0] ?? null;
|
||||
const companyCosts = useMemo(() => snapshot?.costs.filter((entry) => !currentCompany || entry.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
|
||||
const filteredActivity = useMemo(() => snapshot?.activity.filter((entry) => !currentCompany || entry.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
|
||||
const companySecrets = useMemo(() => snapshot?.secrets.filter((secret) => !currentCompany || secret.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
|
||||
const totalSpend = useMemo(() => companyCosts.reduce((sum, entry) => sum + (entry.amountUsd ?? 0), 0), [companyCosts]);
|
||||
const overviewRows = useMemo(() => [
|
||||
{ label: "Live runs", value: String(liveRuns.length), tone: liveRuns.length > 0 ? "text-[color:var(--success)]" : "" },
|
||||
{ label: "Pending approvals", value: String(pendingApprovals.length), tone: pendingApprovals.length > 0 ? "text-[color:var(--warn)]" : "" },
|
||||
{ label: "Active tasks", value: String(tasksInProgress.length), tone: "" },
|
||||
{ label: "Known spend", value: formatMoney(totalSpend), tone: "" },
|
||||
], [liveRuns, pendingApprovals, tasksInProgress, totalSpend]);
|
||||
|
||||
// Command palette entity search
|
||||
const companyDocuments = useMemo(() => snapshot?.documents?.filter((d) => !currentCompany || d.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
|
||||
const companyKnowledge = useMemo(() => snapshot?.knowledgeBase?.filter((k) => !currentCompany || k.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
|
||||
const companyMeetings = useMemo(() => snapshot?.meetings?.filter((m) => !currentCompany || m.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
|
||||
const companySprints = useMemo(() => snapshot?.sprints?.filter((s) => !currentCompany || s.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
|
||||
const paletteQuery = commandPaletteSearch.trim().toLowerCase();
|
||||
const paletteResults = useMemo(() => {
|
||||
const filteredNavItems = paletteQuery
|
||||
@@ -354,6 +370,18 @@ export function App() {
|
||||
}, [paletteQuery, companyAgents, tasks, companyGoals, companyProjects]);
|
||||
const { filteredNavItems, matchedAgents, matchedTasks, matchedGoals, matchedProjects, hasEntityResults } = paletteResults;
|
||||
|
||||
const navigateTo = useCallback((targetSection: SectionId, entityId?: string) => {
|
||||
setSection(targetSection);
|
||||
if (entityId) {
|
||||
if (targetSection === "agents") setSelectedAgentId(entityId);
|
||||
else if (targetSection === "tasks") setSelectedTaskId(entityId);
|
||||
else if (targetSection === "approvals") setSelectedApprovalId(entityId);
|
||||
else if (targetSection === "goals") setSelectedGoalId(entityId);
|
||||
else if (targetSection === "projects") setSelectedProjectId(entityId);
|
||||
else if (targetSection === "communication") setSelectedCommunicationEntityId(entityId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setCompany = useCallback(async (companyId: string) => {
|
||||
if (companyId === "__new__") {
|
||||
setShowOnboarding(true);
|
||||
@@ -419,8 +447,12 @@ export function App() {
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.companies.length === 0 || showOnboarding) {
|
||||
return <CompanyOnboarding connectors={snapshot.connectors} onDone={async () => { setShowOnboarding(false); await refresh(); }} />;
|
||||
if (snapshot.companies.length === 0 || showOnboarding || initialOnboardingLocked) {
|
||||
return <CompanyOnboarding connectors={snapshot.connectors} onDone={async () => {
|
||||
setShowOnboarding(false);
|
||||
setInitialOnboardingLocked(false);
|
||||
await refresh();
|
||||
}} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -560,7 +592,7 @@ export function App() {
|
||||
className={`focus-ring mt-1 flex w-full items-center rounded-[6px] py-[7px] text-[color:var(--muted)] transition duration-150 hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)] ${sidebarCollapsed ? "justify-center p-2" : "gap-2.5 px-3"}`}
|
||||
onClick={() => setSidebarCollapsed((c) => !c)}
|
||||
>
|
||||
<SidebarSimple size={sidebarCollapsed ? 18 : 17} className={sidebarCollapsed ? "" : ""} />
|
||||
<SidebarSimple size={sidebarCollapsed ? 18 : 17} />
|
||||
{!sidebarCollapsed ? <span className="text-[13px] font-medium tracking-[0.005em]">Collapse</span> : null}
|
||||
</button>
|
||||
</div>
|
||||
@@ -626,67 +658,33 @@ export function App() {
|
||||
activity={filteredActivity}
|
||||
costs={companyCosts}
|
||||
connectors={snapshot.connectors}
|
||||
meetings={snapshot.meetings?.filter((m) => !currentCompany || m.companyId === currentCompany.id)}
|
||||
sprints={snapshot.sprints?.filter((s) => !currentCompany || s.companyId === currentCompany.id)}
|
||||
onNavigate={(targetSection, entityId) => {
|
||||
setSection(targetSection);
|
||||
if (entityId) {
|
||||
if (targetSection === "agents") setSelectedAgentId(entityId);
|
||||
else if (targetSection === "tasks") setSelectedTaskId(entityId);
|
||||
else if (targetSection === "approvals") setSelectedApprovalId(entityId);
|
||||
else if (targetSection === "goals") setSelectedGoalId(entityId);
|
||||
else if (targetSection === "projects") setSelectedProjectId(entityId);
|
||||
}
|
||||
}}
|
||||
meetings={companyMeetings}
|
||||
sprints={companySprints}
|
||||
messages={snapshot.agentMessages?.filter((message) => !currentCompany || message.companyId === currentCompany.id) ?? []}
|
||||
browserActions={browserActions}
|
||||
workflows={snapshot.workflows?.filter((workflow) => !currentCompany || workflow.companyId === currentCompany.id) ?? []}
|
||||
onNavigate={navigateTo}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{section === "inbox" ? (
|
||||
<Inbox
|
||||
items={inboxItems}
|
||||
onNavigate={(targetSection, entityId) => {
|
||||
setSection(targetSection);
|
||||
if (entityId) {
|
||||
if (targetSection === "agents") setSelectedAgentId(entityId);
|
||||
else if (targetSection === "tasks") setSelectedTaskId(entityId);
|
||||
else if (targetSection === "approvals") setSelectedApprovalId(entityId);
|
||||
else if (targetSection === "goals") setSelectedGoalId(entityId);
|
||||
else if (targetSection === "projects") setSelectedProjectId(entityId);
|
||||
}
|
||||
}}
|
||||
onDismiss={(_itemId) => { /* no-op for now */ }}
|
||||
onNavigate={navigateTo}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{section === "standup" ? (
|
||||
<StandupView
|
||||
report={standupReport}
|
||||
onNavigate={(targetSection, entityId) => {
|
||||
setSection(targetSection);
|
||||
if (entityId) {
|
||||
if (targetSection === "agents") setSelectedAgentId(entityId);
|
||||
else if (targetSection === "tasks") setSelectedTaskId(entityId);
|
||||
else if (targetSection === "approvals") setSelectedApprovalId(entityId);
|
||||
else if (targetSection === "goals") setSelectedGoalId(entityId);
|
||||
else if (targetSection === "projects") setSelectedProjectId(entityId);
|
||||
}
|
||||
}}
|
||||
onNavigate={navigateTo}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{section === "performance" ? (
|
||||
<PerformanceView
|
||||
metrics={companyMetrics}
|
||||
onNavigate={(targetSection, entityId) => {
|
||||
setSection(targetSection);
|
||||
if (entityId) {
|
||||
if (targetSection === "agents") setSelectedAgentId(entityId);
|
||||
else if (targetSection === "tasks") setSelectedTaskId(entityId);
|
||||
else if (targetSection === "approvals") setSelectedApprovalId(entityId);
|
||||
else if (targetSection === "goals") setSelectedGoalId(entityId);
|
||||
else if (targetSection === "projects") setSelectedProjectId(entityId);
|
||||
}
|
||||
}}
|
||||
onNavigate={navigateTo}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -717,7 +715,7 @@ export function App() {
|
||||
</Panel>
|
||||
<Panel
|
||||
title={selectedConnector.label}
|
||||
eyebrow={undefined}
|
||||
|
||||
action={<ActionButton label="Run health check" onClick={() => void quickConnectorCheck()} icon={Wrench} tone="accent" />}
|
||||
>
|
||||
<ConnectorEditor connector={selectedConnector} onSaved={refresh} />
|
||||
@@ -762,7 +760,6 @@ export function App() {
|
||||
|
||||
{section === "hiring" && currentCompany ? (
|
||||
<HiringPipeline
|
||||
companyId={currentCompany.id}
|
||||
agents={companyAgents}
|
||||
approvals={(snapshot?.approvals ?? []).filter(a => a.companyId === currentCompany!.id)}
|
||||
onRefresh={refresh}
|
||||
@@ -906,7 +903,7 @@ export function App() {
|
||||
tasks={tasks}
|
||||
companyId={currentCompany!.id}
|
||||
onDecide={async (state, note) => {
|
||||
unwrap(await window.agentCompany.decideApproval({ approvalId: selectedApproval.id, state, decisionNote: note ?? `${state} from desktop control surface.` }));
|
||||
unwrap(await window.agentCompany.decideApproval({ companyId: currentCompany!.id, approvalId: selectedApproval.id, state, decisionNote: note ?? `${state} from desktop control surface.` }));
|
||||
await refresh();
|
||||
}}
|
||||
onDeleted={async () => { setSelectedApprovalId(null); await refresh(); }}
|
||||
@@ -965,16 +962,19 @@ export function App() {
|
||||
}}
|
||||
onLogin={async (accountId) => {
|
||||
await window.agentCompany.triggerBrowserLogin(accountId);
|
||||
await refresh();
|
||||
}}
|
||||
onNavigate={(s, id) => {
|
||||
setSection(s);
|
||||
if (id) {
|
||||
if (s === "agents") setSelectedAgentId(id);
|
||||
else if (s === "tasks") setSelectedTaskId(id);
|
||||
else if (s === "approvals") setSelectedApprovalId(id);
|
||||
else if (s === "goals") setSelectedGoalId(id);
|
||||
else if (s === "projects") setSelectedProjectId(id);
|
||||
}
|
||||
onOpenPath={async (path) => {
|
||||
await window.agentCompany.openPath(path);
|
||||
}}
|
||||
onCancelAction={async (actionId) => {
|
||||
await window.agentCompany.cancelBrowserAction({ runId: actionId });
|
||||
await refresh();
|
||||
}}
|
||||
onNavigate={(sectionName, entityId) => {
|
||||
setSection(sectionName);
|
||||
if (sectionName === "tasks") setSelectedTaskId(entityId);
|
||||
if (sectionName === "approvals") setSelectedApprovalId(entityId);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -983,7 +983,6 @@ export function App() {
|
||||
<AutomationPanel
|
||||
rules={snapshot.automationRules?.filter(r => r.companyId === currentCompany.id) ?? []}
|
||||
workflows={snapshot.workflows?.filter(w => w.companyId === currentCompany.id) ?? []}
|
||||
agents={companyAgents}
|
||||
companyId={currentCompany.id}
|
||||
onRefresh={refresh}
|
||||
/>
|
||||
@@ -992,137 +991,36 @@ export function App() {
|
||||
{section === "communication" && currentCompany ? (
|
||||
<CommunicationHub
|
||||
messages={snapshot.agentMessages?.filter(m => m.companyId === currentCompany.id) ?? []}
|
||||
comments={snapshot.comments?.filter((comment) => comment.companyId === currentCompany.id) ?? []}
|
||||
tasks={tasks}
|
||||
agents={companyAgents}
|
||||
companyId={currentCompany.id}
|
||||
selectedEntityId={selectedCommunicationEntityId}
|
||||
onRefresh={refresh}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{section === "documents" && currentCompany ? (
|
||||
<Panel title="Company Documents" action={<span className="text-[11px] text-[color:var(--muted)]">{(snapshot.documents?.filter(d => d.companyId === currentCompany.id) ?? []).length} documents</span>}>
|
||||
{(snapshot.documents?.filter(d => d.companyId === currentCompany.id) ?? []).length === 0 ? (
|
||||
<div className="py-10 text-center text-[13px] text-[color:var(--muted)]">
|
||||
<FileText size={32} className="mx-auto mb-3 opacity-40" />
|
||||
<p>No documents yet. Agents will create PRDs, specs, and reports as they work.</p>
|
||||
</div>
|
||||
) : (
|
||||
<ListFrame>
|
||||
{(snapshot.documents?.filter(d => d.companyId === currentCompany.id) ?? []).map(doc => (
|
||||
<ListRow key={doc.id}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText size={14} className="text-[color:var(--accent)]" />
|
||||
<span className="text-[13px] font-medium text-[color:var(--text)]">{doc.title}</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-3 text-[11px] text-[color:var(--muted)]">
|
||||
<span className="uppercase tracking-wider">{doc.type.replace(/_/g, " ")}</span>
|
||||
<span>v{doc.version}</span>
|
||||
{doc.authorAgentId && <span>by {companyAgents.find(a => a.id === doc.authorAgentId)?.name ?? "Unknown"}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<StatusPill status={doc.status} />
|
||||
</div>
|
||||
</ListRow>
|
||||
))}
|
||||
</ListFrame>
|
||||
)}
|
||||
</Panel>
|
||||
<DocumentCenter
|
||||
companyId={currentCompany.id}
|
||||
documents={companyDocuments}
|
||||
agents={companyAgents}
|
||||
projects={companyProjects}
|
||||
goals={companyGoals}
|
||||
onRefresh={refresh}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{section === "knowledge" && currentCompany ? (
|
||||
<Panel title="Knowledge Base" action={<span className="text-[11px] text-[color:var(--muted)]">{(snapshot.knowledgeBase?.filter(k => k.companyId === currentCompany.id) ?? []).length} entries</span>}>
|
||||
{(snapshot.knowledgeBase?.filter(k => k.companyId === currentCompany.id) ?? []).length === 0 ? (
|
||||
<div className="py-10 text-center text-[13px] text-[color:var(--muted)]">
|
||||
<Brain size={32} className="mx-auto mb-3 opacity-40" />
|
||||
<p>No knowledge entries yet. Agents learn and document lessons as they work.</p>
|
||||
</div>
|
||||
) : (
|
||||
<ListFrame>
|
||||
{(snapshot.knowledgeBase?.filter(k => k.companyId === currentCompany.id) ?? []).map(entry => (
|
||||
<ListRow key={entry.id}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Brain size={14} className="text-[color:var(--accent)]" />
|
||||
<span className="text-[13px] font-medium text-[color:var(--text)]">{entry.topic}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-[color:var(--muted)]">
|
||||
<span className="uppercase tracking-wider">{entry.category.replace(/_/g, " ")}</span>
|
||||
<span className="mx-2">-</span>
|
||||
<span>{entry.content.slice(0, 100)}{entry.content.length > 100 ? "..." : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
<StatusPill status={entry.importance} />
|
||||
</div>
|
||||
</ListRow>
|
||||
))}
|
||||
</ListFrame>
|
||||
)}
|
||||
</Panel>
|
||||
<KnowledgeBaseManager companyId={currentCompany.id} entries={companyKnowledge} onRefresh={refresh} />
|
||||
) : null}
|
||||
|
||||
{section === "meetings" && currentCompany ? (
|
||||
<Panel title="Meetings" action={<span className="text-[11px] text-[color:var(--muted)]">{(snapshot.meetings?.filter(m => m.companyId === currentCompany.id) ?? []).length} meetings</span>}>
|
||||
{(snapshot.meetings?.filter(m => m.companyId === currentCompany.id) ?? []).length === 0 ? (
|
||||
<div className="py-10 text-center text-[13px] text-[color:var(--muted)]">
|
||||
<CalendarBlank size={32} className="mx-auto mb-3 opacity-40" />
|
||||
<p>No meetings scheduled. Agents will schedule standups, reviews, and planning sessions.</p>
|
||||
</div>
|
||||
) : (
|
||||
<ListFrame>
|
||||
{(snapshot.meetings?.filter(m => m.companyId === currentCompany.id) ?? []).map(meeting => (
|
||||
<ListRow key={meeting.id}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarBlank size={14} className="text-[color:var(--accent)]" />
|
||||
<span className="text-[13px] font-medium text-[color:var(--text)]">{meeting.title}</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-3 text-[11px] text-[color:var(--muted)]">
|
||||
<span className="uppercase tracking-wider">{meeting.type.replace(/_/g, " ")}</span>
|
||||
<span>{new Date(meeting.scheduledAt).toLocaleString()}</span>
|
||||
<span>{meeting.durationMinutes}min</span>
|
||||
</div>
|
||||
</div>
|
||||
<StatusPill status={meeting.status} />
|
||||
</div>
|
||||
</ListRow>
|
||||
))}
|
||||
</ListFrame>
|
||||
)}
|
||||
</Panel>
|
||||
<MeetingsManager companyId={currentCompany.id} meetings={companyMeetings} agents={companyAgents} onRefresh={refresh} />
|
||||
) : null}
|
||||
|
||||
{section === "sprints" && currentCompany ? (
|
||||
<Panel title="Sprint Board" action={<span className="text-[11px] text-[color:var(--muted)]">{(snapshot.sprints?.filter(s => s.companyId === currentCompany.id) ?? []).length} sprints</span>}>
|
||||
{(snapshot.sprints?.filter(s => s.companyId === currentCompany.id) ?? []).length === 0 ? (
|
||||
<div className="py-10 text-center text-[13px] text-[color:var(--muted)]">
|
||||
<Rows size={32} className="mx-auto mb-3 opacity-40" />
|
||||
<p>No sprints yet. Start a sprint to organize work into focused cycles.</p>
|
||||
</div>
|
||||
) : (
|
||||
<ListFrame>
|
||||
{(snapshot.sprints?.filter(s => s.companyId === currentCompany.id) ?? []).map(sprint => (
|
||||
<ListRow key={sprint.id}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Rows size={14} className="text-[color:var(--accent)]" />
|
||||
<span className="text-[13px] font-medium text-[color:var(--text)]">{sprint.name}</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-3 text-[11px] text-[color:var(--muted)]">
|
||||
<span>{sprint.startDate} - {sprint.endDate}</span>
|
||||
<span>{sprint.completedPoints}/{sprint.velocityPoints} pts</span>
|
||||
</div>
|
||||
</div>
|
||||
<StatusPill status={sprint.status} />
|
||||
</div>
|
||||
</ListRow>
|
||||
))}
|
||||
</ListFrame>
|
||||
)}
|
||||
</Panel>
|
||||
<SprintManager companyId={currentCompany.id} sprints={companySprints} onRefresh={refresh} />
|
||||
) : null}
|
||||
|
||||
{section === "workspaces" ? (
|
||||
@@ -1135,15 +1033,30 @@ export function App() {
|
||||
workspaces={companyWorkspaces}
|
||||
projects={companyProjects}
|
||||
companyId={currentCompany!.id}
|
||||
selectedWorkspaceId={selectedWorkspace?.id ?? null}
|
||||
onSelect={setSelectedWorkspaceId}
|
||||
onOpen={(path) => window.agentCompany.openPath(path).then(() => undefined)}
|
||||
onDeleted={refresh}
|
||||
/>
|
||||
</Panel>
|
||||
}
|
||||
secondary={
|
||||
<Panel title="Add workspace">
|
||||
<WorkspaceCreator companyId={currentCompany!.id} projects={companyProjects} onSaved={refresh} />
|
||||
</Panel>
|
||||
<div className="space-y-6">
|
||||
<Panel title="Workspace control center">
|
||||
<WorkspaceInspector
|
||||
workspace={selectedWorkspace}
|
||||
agents={companyAgents}
|
||||
tasks={tasks}
|
||||
runs={runs}
|
||||
documents={companyDocuments}
|
||||
project={selectedWorkspace ? (companyProjects.find((project) => project.id === selectedWorkspace.projectId) ?? null) : null}
|
||||
onOpen={(path) => window.agentCompany.openPath(path).then(() => undefined)}
|
||||
/>
|
||||
</Panel>
|
||||
<Panel title="Add workspace">
|
||||
<WorkspaceCreator companyId={currentCompany!.id} projects={companyProjects} onSaved={refresh} />
|
||||
</Panel>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
Reference in new issue
Block a user